razor-vue-lint
Make Eslint work with .NET Razor views that contain inlined Vue templates. Wraps Razor expressions with marker blocks that are ignored by ES Lint, so that the ES linter only lints the Vue templates in the file.
Razor expressions supported
Currently it supports:
- expressions terminated by newline
\n
- expressions terminated by closing tag
</
- expressions inside enclosing
'
(JS strings or HTML attributes)
How it works
The "raw" .cshtml
file:
@using EPiServer.Core@using EPiServer.Web.Mvc.Html@using Olympus.Core.Models.Blocks.Subscription@model Olympus.Core.ViewModels.BlockViewModelBase<SubscriptionOfferingsBlock> <subscription-offerings inline-template> <div class="ss-grid ss-grid--no-gutter ss-c-offering ss-c-subscription-offerings-possible-subscriptions"> <div class="ss-c-page-title ss-c-subscription-page-title ss-grid__col--md-10 ss-grid__col--md-offset-1 ss-grid__col--xs-10 ss-grid__col--xs-offset-1"> <p>@Html.PropertyFor(m => m.CurrentBlock.Heading)</h2> <p>@Html.PropertyFor(m => m.CurrentBlock.Subtext)</p> </div> </div></subscription-offerings>
Should be transformed into an equally valid .cshtml
file with the Razor expressions escaped
/* eslint-disable */@using EPiServer.Core@using EPiServer.Web.Mvc.Html@using Olympus.Core.Models.Blocks.Subscription@model Olympus.Core.ViewModels.BlockViewModelBase<SubscriptionOfferingsBlock>/* eslint-enable */<subscription-offerings inline-template> <div class="ss-grid ss-grid--no-gutter ss-c-offering ss-c-subscription-offerings-possible-subscriptions"> <div class="ss-c-page-title ss-c-subscription-page-title ss-grid__col--md-10 ss-grid__col--md-offset-1 ss-grid__col--xs-10 ss-grid__col--xs-offset-1"> <p> /* eslint-disable */ @Html.PropertyFor(m => m.CurrentBlock.Heading)</h2> <p>@Html.PropertyFor(m => m.CurrentBlock.Subtext) /* eslint-enable */ </p> </div> </div></subscription-offerings>
We aim to support some the most common expression types. The rest must for now be added "by hand" or you can make PRs to include them.
Note that this lib is not battle-tested so there a likely many scenarios not catered for. Keep your Razor expressions and views simple!
Usage
The library exports the function esLintEscapeRazorExpressions
, which takes a single string argument containing the code to be transformed. The function returns the transformed string, with razor expressions contained inside blocks ignored by ES lint.
const esLintEscapeRazorExpressions = ; // TODO: load cshtml file into a string called code const lintEscapedCode = ; // TODO: write escaped cshtml file to a file
You will need to write a script to recursively process your code files (see below).
Then you can setup eslint to lint the cshtml files using your Vue configuration of preference and it should skip most of the sections Razor expressions, now inside "ignore blocks", between:
/* eslint-disable */
/* eslint-enable */
Extending and customizing replacers
The default expression rules are as follows:
const exprs = inLine: /@[^:]+/gm inTag: /@[^:]+/gm inAttribute: /@[^:]+/gm;
With the default matchers configuration:
const matchers = line: expr: exprsinLine replace: replaceInLine tag: expr: exprsinTag replace: replaceInTag attribute: expr: exprsinAttribute replace: replaceInAttribute ;
And precedence order, executed for each line
const matcherKeys = "attribute" "tag" "line";
You can pass your own customized or extended matchers
and matcherKeys
in the second optional options
argument.
;
Traverse
You can now use traverse functionality to:
- recursively traverse files in a folder tree
- process each file matching a criteria such as file extension
- execute the function to insert es-lint escape block on Razor expressions
- save transformed content to either a new file or overwriting original
const path = ;const traverse = ;const processFiles = traverse; const folder = path;const onSuccess = { console;}; ;
Advanced usage
In this example we add a custom filter function fileFilter
to process any file with .cs
as part of the file extension at the end of the file name. We also pass in a custom function destFilePathOf
to calculate to destination file path to write each transformed file to.
In addition we pass in the usual suspects: folder
and onSuccess
with errorFn
a custom error handler.
const traverse = ;const processFiles = traverse; const path = ;const folder = path;const onSuccess = { console;}; const opts = folder onSuccess filePath + ".lint" filePath throw err;;
See traverse.js
source for more configuration options. You can also use the internal functions to easily compose custom traverse/transform functionality.
Linting
Resources
Install es-lint Vue plugin
# Yarn $ yarn eslint eslint-plugin-vue --save-dev# NPM $ npm install eslint eslint-plugin-vue --save-dev
Update (or create) your .eslintrc.json
file.
The full eslint configuration might look something like this:
Many of the issues detected by those rules can be automatically fixed with eslint’s --fix
option.
The CLI targets only .js
files by default. You have to specify additional extensions by --ext
option or glob patterns. E.g. eslint "src/**/*.{js,vue}"
or eslint src --ext .vue
To lint .NET server generated .cshtml
files, use something like:
eslint **/*.cshtml.lintable
Integrated tooling
We can have process all the .cshtml
files and save each processed file with an additional .lintable
extension to be linted.
const opts = folder filePath + ".lintable" filePath throw err;;
Create a file scripts/make-vue-razor-views-lintable.js
const traverse = ;const processFiles = traverse; const path = ;const minimist = ;const processArgs = processargv;const opts = alias: h: 'help' s: 'src' e: 'ext' ; // args is an object, with key for each named argumentconst args = ;const defaults = srcFolder: "./" ext: 'lintable'if argshelp console process;const srcFolder = argssrc || defaultssrcFolders;const fileExt = argsext || defaultsext;const rootPath = path;const srcPath = path const opts = folder: srcFolder rootPath srcPath filePath + `.` filePath throw err;;
Cleanup after linting
Cleanup (remove) the .lintable
files after linting.
Windows > del /s /q *.lintable
Unix $ find . -type f -name '*.lintable' -delete
As an alternative, you can pass in your own custom cleanup
function (see Advanced section below)
Pre-commit hooks
You can use this recipe to integrate this linting process with git hooks, such as pre-commit hooks:
- Setup git hooks via husky
- Create node script to trigger on hook
- Setup hooks
- Install and setup husky
Add the following to your package.json
file (or create a new one using npm init
)
Lint views
Create a scripts/lint-views.js
(or some other script) file which should:
- run
node make-vue-razor-views-lintable.js
to create lintable version of your view files - run
eslint **/*.cshtml.lintable
to lint the lintable files and print linting errors - cleanup
.lintable
files so they are not committed
The runscript takes either a single string for a spawn command or an options object (fine control).
For object, pass a command
and either an arg
(string) or args
(array) of command arguments
;
The runScript
function uses the special convention that if given only a string argument and the first character is a :
it will use shell:true
to run the command (ie. execute it as a shell command)
Full example:
const runScript = ; const exitFailure = Process; const cleanup = { ;}; // Now we can run a script and invoke a callback when complete, e.g.;
For your convenience, a function runLint
is made available:
const path = ;const runLint = ; const scriptPath = path;;
Advanced usage
Create a custom cleanup
function in cleanup.js
that cleans up all processed files (ie. paths to files that were escaped for linting and saved to disk)
const fs = ; const removeFile = { try fs; return filePath; catch err return false; }; const cleanup = { const matched processed = opts; const destinationPaths = processed; // TODO: remove files in matched array return destinationPaths;}; moduleexports = cleanup;
const path = ;const runLint = ; const defaults = ;const runInternalEscapeScript = defaults; const rootPath = path; const scriptPath = path; // import custom cleanup functionconst cleanup = ; const opts = // use processFiles function directly, instead of shell command runEscapeScript: runInternalEscapeScript cleanup // use custom cleanup function, using recursive delete on processed files debug: true // add debugging rootPath // location of project to lint (and cleanup) lintExt: ".cshtml.lintable"; ;
For more customization and composition options, see the code in run-lint.js
You can f.ex test the runLintScript
and runEscapeScript
individually before composing them into
a scripting pipeline.
const runLintScript runEscapeScript = ; const opts = debug: true; ;
Running child processes
See Node.js Child Processes: Everything you need to know
Childprocess command alternatives:
fork
spawn
exec
execFile
The exec
function is a good choice if you need to use the shell syntax and if the size of the data expected from the command is small. (Remember, exec
will buffer the whole data in memory before returning it.)
The spawn
function is a much better choice when the size of the data expected from the command is large, because that data will be streamed with the standard IO objects.
With the shell: true
option, we are able to use the shell syntax in the passed spawn
command, just like we can do by default with exec
.
We can execute it in the background using the detached: true
option
If you need to execute a file without using a shell, the execFile
function is what you need. It behaves exactly like the exec
function, but does not use a shell, which makes it a bit more efficient.
The fork
function is a variation of the spawn function for spawning node processes. The biggest difference between spawn and fork is that a communication channel is established to the child process when using fork, so we can use the send function on the forked process along with the global process object itself to exchange messages between the parent and forked processes.
VS Code
To configure linting vue files in VS Code:
Go to File > Preferences > Settings and add this to your user settings JSON file:
"eslint.validate": ,
It is highly recommended to install the VS Code extension: eslint-disable-snippets
With this extension, you can be at or above a line you want to disable, and start typing eslint-disable
and usually VS Code’s auto-complete suggestions will kick up after you type even just esl
.
Testing
We are using jest for unit testing.
Testing traverse
To mock the file system for testing traverse, we are using memFs
See traverse.test.js
for traverse tests. Currently using a variant of recursive-readdir
which allows passing in a custom fs
(file system object) to be used. This approach works well to make memFs
(in-memory file system) work with Jest.
See the test/data
for testing infrastructure, such as fake file system setup and test files.
You can add debug: true
as an option to enable debug tracing.
memfs
vol
is an instance of Volume
constructor, it is the default volume created for your convenience.
fs
is an fs-like object created from vol using createFsFromVolume(vol)
.
Alternative file mocking
License
MIT