razor-vue-lint v1.1.10
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 } = require("razor-vue-lint");
// TODO: load cshtml file into a string called code
const lintEscapedCode = esLintEscapeRazorExpressions(code);
// TODO: write escaped cshtml file to a fileYou 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: /@[^:]+(\n)/gm,
inTag: /@[^:]+(<\/)/gm,
inAttribute: /('\s*)@[^:]+(')/gm
};With the default matchers configuration:
const matchers = {
line: {
expr: exprs.inLine,
replace: replaceInLine
},
tag: {
expr: exprs.inTag,
replace: replaceInTag
},
attribute: {
expr: exprs.inAttribute,
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.
addIgnoreEsLintBlocksForRazorExpressions(code, {
matchers: myMatchers, // configuration used
matcherKeys: myMatcherKeys // order
});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 = require("path");
const traverse = require("razor-vue-lint");
const { processFiles } = traverse;
const folder = path.join(__dirname, "MyProject");
const onSuccess = result => {
console.log("DONE");
};
processFiles({ folder, onSuccess });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 = require("razor-vue-lint");
const { processFiles } = traverse;
const path = require("path");
const folder = path.join(__dirname, "MyProject");
const onSuccess = result => {
console.log(result);
};
const opts = {
folder,
onSuccess,
destFilePathOf: filePath => filePath + ".lint",
fileFilter: filePath => filePath.match(/\.cs\w+$/),
errorFn: err => throw err
};
processFiles({ folder, onSuccess });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-devUpdate (or create) your .eslintrc.json file.
{
"extends": ["eslint:recommended", "plugin:vue/recommended"]
}The full eslint configuration might look something like this:
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOptions": {
"parser": "babel-eslint"
},
"extends": ["airbnb-base", "plugin:vue/recommended"],
"rules": {}
}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,
destFilePathOf: filePath => filePath + ".lintable",
fileFilter: filePath => filePath.match(/\.cshtml$/),
errorFn: err => throw err
};
processFiles({ folder, onSuccess });Create a file scripts/make-vue-razor-views-lintable.js
const traverse = require("razor-vue-lint");
const { processFiles } = traverse;
const path = require("path");
const minimist = require('minimist');
const processArgs = process.argv.slice(2);
const opts = {
alias: {
h: 'help',
s: 'src',
e: 'ext'
}
};
// args is an object, with key for each named argument
const args = minimist(processArgs, opts);
const defaults = {
srcFolder: "./",
ext: 'lintable'
}
if (args.help) {
console.log(`
razor-views-lintable
--------------------
-s src folder (default: ./ )
-e lintable file extension (default: lintable)
`)
process.exit(0);
}
const srcFolder = args.src || defaults.srcFolders;
const fileExt = args.ext || defaults.ext;
const rootPath = path.join(__dirname, "..");
const srcPath = path.join(rootPath, srcFolder),
const opts = {
folder: srcFolder,
rootPath,
srcPath,
destFilePathOf: filePath => filePath + `.${fileExt}`,
fileFilter: filePath => filePath.match(/\.cshtml$/),
errorFn: err => throw err
};
processFiles(opts);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)
{
"devDependencies": {
"razor-vue-lint": "~1.1.0",
"husky": "~1.3.1"
},
"husky": {
"hooks": {
"pre-commit": "node scripts/lint-views.js"
}
}
}Lint views
Create a scripts/lint-views.js (or some other script) file which should:
- run
node make-vue-razor-views-lintable.jsto create lintable version of your view files - run
eslint **/*.cshtml.lintableto lint the lintable files and print linting errors - cleanup
.lintablefiles 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
runScript(
{ command: "execFile", arg: "./make-vue-razor-views-lintable.js" },
handlerFn
);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 } = require("razor-vue-lint");
const exitFailure = () => Process.exit(1);
const cleanup = ({ failure, success, error } = {}) => {
runScript(":del /s /q *.lintable", ({ err }) => {
if (err || failure) {
const errorMsg = error || err;
console.error(errorMsg);
exitFailure();
}
if (success) {
console.error("SUCCESS");
}
});
};
// Now we can run a script and invoke a callback when complete, e.g.
runScript(":node ./make-vue-razor-views-lintable.js", ({ err }) => {
if (err) {
cleanup({ failure: true, error: err });
}
runScript(":eslint **/*.cshtml.lintable", ({ err }) => {
const opts = err ? { failure: true } : { success: true };
cleanup(opts);
});
});For your convenience, a function runLint is made available:
const path = require("path");
const { runLint } = require("razor-vue-lint");
const scriptPath = path.join(__dirname, "./make-vue-razor-views-lintable.js");
runLint(scriptPath);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 = require("fs");
const removeFile = filePath => {
try {
fs.unlinkSync(filePath);
return filePath;
} catch (err) {
return false;
}
};
const cleanup = (opts = {}) => {
const { matched, processed } = opts;
const destinationPaths = processed.map(item => item.destFilePath);
// TODO: remove files in matched array
return destinationPaths.map(removeFile);
};
module.exports = cleanup;const path = require("path");
const { runLint } = require("razor-vue-lint");
const { defaults } = require("razor-vue-lint/src/run/run-lint");
const { runInternalEscapeScript } = defaults;
const rootPath = path.join(__dirname, "../src");
const scriptPath = path.join(__dirname, "./make-vue-razor-views-lintable.js");
// import custom cleanup function
const { cleanup } = require("./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"
};
runLint(scriptPath);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 } = require("razor-vue-lint");
const opts = {
debug: true
};
runLintScript(":eslint **/*.cshtml", opts);Running child processes
See Node.js Child Processes: Everything you need to know
Childprocess command alternatives:
forkspawnexecexecFile
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": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": false
}
],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