From 72ed0b851d1c8510247c7558043e884d01e2e64d Mon Sep 17 00:00:00 2001 From: Kim Strauch Date: Sun, 8 Apr 2018 21:39:09 -0700 Subject: [PATCH 1/5] add dynamic import webpackChunkName comment rule --- docs/rules/dynamic-import-chunkname.md | 66 +++++ src/index.js | 3 +- src/rules/dynamic-import-chunkname.js | 71 ++++++ tests/src/rules/dynamic-import-chunkname.js | 251 ++++++++++++++++++++ 4 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 docs/rules/dynamic-import-chunkname.md create mode 100644 src/rules/dynamic-import-chunkname.js create mode 100644 tests/src/rules/dynamic-import-chunkname.js diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md new file mode 100644 index 000000000..0ccc5697d --- /dev/null +++ b/docs/rules/dynamic-import-chunkname.md @@ -0,0 +1,66 @@ +# dynamic imports require a leading comment with a webpackChunkName (dynamic-import-chunkname) + +This rule reports any dynamic imports without a webpackChunkName specified in a leading block comment in the proper format. + +This is a useful rule because if the webpackChunkName is not defined in a dynamic import, Webpack will autogenerate the chunk name. + +## Rule Details +This rule runs against `import` by default, but can be configured to also run against an alternative dynamic-import function, e.g. 'dynamicImport.' +You can also configure the regex format you'd like to accept for the webpackChunkName - for example, we don't want the number 6 to show up in our chunk names. + ```javascript +{ + "dynamic-import-chunkname": [2, { + importFunction: "dynamicImport", + webpackChunknameFormat: "[a-zA-Z0-57-9-/_]" + }] +} +``` + +### invalid +The following patterns are invalid: + +```javascript +// no leading comment +import('someModule'); + +// incorrectly formatted comment +import( + /*webpackChunkName:"someModule"*/ + 'someModule', +); + +// chunkname contains a 6 (forbidden by rule config) +import( + /* webpackChunkName: "someModule6" */ + 'someModule', +); + +// using single quotes instead of double quotes +import( + /* webpackChunkName: 'someModule' */ + 'someModule', +); + +// single-line comment, not a block-style comment +import( + // webpackChunkName: "someModule" + 'someModule', +); +``` +### valid +The following patterns are valid: + +```javascript + import( + /* webpackChunkName: "someModule" */ + 'someModule', + ); + import( + /* webpackChunkName: "someOtherModule12345789" */ + 'someModule', + ); +``` + +## When Not To Use It + +If you don't care that Webpack will autogenerate chunk names and may blow up browser caches and bundle size reports. diff --git a/src/index.js b/src/index.js index 2d6352b83..61c64214b 100644 --- a/src/index.js +++ b/src/index.js @@ -35,7 +35,8 @@ export const rules = { 'unambiguous': require('./rules/unambiguous'), 'no-unassigned-import': require('./rules/no-unassigned-import'), 'no-useless-path-segments': require('./rules/no-useless-path-segments'), - + 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'), + // export 'exports-last': require('./rules/exports-last'), diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js new file mode 100644 index 000000000..50b85508a --- /dev/null +++ b/src/rules/dynamic-import-chunkname.js @@ -0,0 +1,71 @@ +import docsUrl from '../docsUrl' + +module.exports = { + meta: { + docs: { + url: docsUrl('dynamic-import-chunkname'), + }, + schema: [{ + type: 'object', + properties: { + importFunction: { + type: 'string', + }, + webpackChunknameFormat: { + type: 'string', + }, + }, + }], + }, + + create: function (context) { + const config = context.options[0] + let importFunction + if (config) { + ({ importFunction } = config) + } + + let webpackChunknameFormat = '[0-9a-zA-Z-_/.]+' + if (config && config.webpackChunknameFormat) { + ({ webpackChunknameFormat } = config) + } + const commentFormat = ` webpackChunkName: "${webpackChunknameFormat}" ` + const commentRegex = new RegExp(commentFormat) + + return { + CallExpression(node) { + if (node.callee.name !== importFunction && node.callee.type !== 'Import') { + return + } + const sourceCode = context.getSourceCode() + const arg = node.arguments[0] + const leadingComments = sourceCode.getComments(arg).leading + + if (!leadingComments || leadingComments.length !== 1) { + context.report({ + node, + message: 'dynamic imports require a leading comment with the webpack chunkname', + }) + return + } + + const comment = leadingComments[0] + if (comment.type !== 'Block') { + context.report({ + node, + message: 'dynamic imports require a /* foo */ style comment, not a // foo comment', + }) + return + } + + const webpackChunkDefinition = comment.value + if (!webpackChunkDefinition.match(commentRegex)) { + context.report({ + node, + message: `dynamic imports require a leading comment in the form /*${commentFormat}*/`, + }) + } + }, + } + }, +} diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js new file mode 100644 index 000000000..c56f2fae6 --- /dev/null +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -0,0 +1,251 @@ +import { SYNTAX_CASES } from '../utils' +import { RuleTester } from 'eslint' + +const rule = require('rules/dynamic-import-chunkname') +const ruleTester = new RuleTester() + +const commentFormat = '[0-9a-zA-Z-_/.]+' +const pickyCommentFormat = '[a-zA-Z-_/.]+' +const options = [{ importFunction: 'dynamicImport' }] +const pickyCommentOptions = [{ + importFunction: 'dynamicImport', + webpackChunknameFormat: pickyCommentFormat, +}] +const parser = 'babel-eslint' + +const noLeadingCommentError = 'dynamic imports require a leading comment with the webpack chunkname' +const nonBlockCommentError = 'dynamic imports require a /* foo */ style comment, not a // foo comment' +const commentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${commentFormat}" */` +const pickyCommentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${pickyCommentFormat}" */` + +ruleTester.run('dynamic-import-chunkname', rule, { + valid: [ + { + code: `dynamicImport( + /* webpackChunkName: "someModule" */ + 'test' + )`, + options, + }, + { + code: `dynamicImport( + /* webpackChunkName: "Some_Other_Module" */ + "test" + )`, + options, + }, + { + code: `dynamicImport( + /* webpackChunkName: "SomeModule123" */ + "test" + )`, + options, + }, + { + code: `dynamicImport( + /* webpackChunkName: "someModule" */ + 'someModule' + )`, + options: pickyCommentOptions, + errors: [{ + message: pickyCommentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName: "someModule" */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackChunkName: "Some_Other_Module" */ + "test" + )`, + options, + parser, + }, + { + code: `import( + /* webpackChunkName: "SomeModule123" */ + "test" + )`, + options, + parser, + }, + { + code: `import( + /* webpackChunkName: "someModule" */ + 'someModule' + )`, + options: pickyCommentOptions, + parser, + errors: [{ + message: pickyCommentFormatError, + type: 'CallExpression', + }], + }, + ...SYNTAX_CASES, + ], + + invalid: [ + { + code: `import( + // webpackChunkName: "someModule" + 'someModule' + )`, + options, + parser, + errors: [{ + message: nonBlockCommentError, + type: 'CallExpression', + }], + }, + { + code: 'import(\'test\')', + options, + parser, + errors: [{ + message: noLeadingCommentError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName: someModule */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName: 'someModule' */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName "someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName:"someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName: "someModule123" */ + 'someModule' + )`, + options: pickyCommentOptions, + parser, + errors: [{ + message: pickyCommentFormatError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + // webpackChunkName: "someModule" + 'someModule' + )`, + options, + errors: [{ + message: nonBlockCommentError, + type: 'CallExpression', + }], + }, + { + code: 'dynamicImport(\'test\')', + options, + errors: [{ + message: noLeadingCommentError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + /* webpackChunkName: someModule */ + 'someModule' + )`, + options, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + /* webpackChunkName: 'someModule' */ + 'someModule' + )`, + options, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + /* webpackChunkName "someModule" */ + 'someModule' + )`, + options, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + /* webpackChunkName:"someModule" */ + 'someModule' + )`, + options, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `dynamicImport( + /* webpackChunkName: "someModule123" */ + 'someModule' + )`, + options: pickyCommentOptions, + errors: [{ + message: pickyCommentFormatError, + type: 'CallExpression', + }], + }, + ], +}) From c9d7d660b43f91e490a692d82fb655f983bc4e99 Mon Sep 17 00:00:00 2001 From: Kim Strauch Date: Sun, 8 Apr 2018 21:40:37 -0700 Subject: [PATCH 2/5] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 529525363..e28a407ec 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Forbid default exports ([`no-default-export`]) * Forbid anonymous values as default exports ([`no-anonymous-default-export`]) * Prefer named exports to be grouped together in a single export declaration ([`group-exports`]) +* Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`]) [`first`]: ./docs/rules/first.md [`exports-last`]: ./docs/rules/exports-last.md @@ -99,6 +100,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md [`group-exports`]: ./docs/rules/group-exports.md [`no-default-export`]: ./docs/rules/no-default-export.md +[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md ## Installation From e6e4e98d328ab54b7dbd2a5e3119cd560a6bb3bb Mon Sep 17 00:00:00 2001 From: Kim Strauch Date: Mon, 9 Apr 2018 22:05:29 -0700 Subject: [PATCH 3/5] respond to PR feedback --- docs/rules/dynamic-import-chunkname.md | 10 ++++----- src/index.js | 2 +- src/rules/dynamic-import-chunkname.js | 23 ++++++++++----------- tests/src/rules/dynamic-import-chunkname.js | 4 ++-- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index 0ccc5697d..98b98871e 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -2,15 +2,15 @@ This rule reports any dynamic imports without a webpackChunkName specified in a leading block comment in the proper format. -This is a useful rule because if the webpackChunkName is not defined in a dynamic import, Webpack will autogenerate the chunk name. +This rule enforces naming of webpack chunks in dynamic imports. When you don't explicitly name chunks, webpack will autogenerate chunk names that are not consistent across builds, which prevents long-term browser caching. ## Rule Details -This rule runs against `import` by default, but can be configured to also run against an alternative dynamic-import function, e.g. 'dynamicImport.' -You can also configure the regex format you'd like to accept for the webpackChunkName - for example, we don't want the number 6 to show up in our chunk names. +This rule runs against `import()` by default, but can be configured to also run against an alternative dynamic-import function, e.g. 'dynamicImport.' +You can also configure the regex format you'd like to accept for the webpackChunkName - for example, if we don't want the number 6 to show up in our chunk names: ```javascript { "dynamic-import-chunkname": [2, { - importFunction: "dynamicImport", + importFunctions: ["dynamicImport"], webpackChunknameFormat: "[a-zA-Z0-57-9-/_]" }] } @@ -63,4 +63,4 @@ The following patterns are valid: ## When Not To Use It -If you don't care that Webpack will autogenerate chunk names and may blow up browser caches and bundle size reports. +If you don't care that webpack will autogenerate chunk names and may blow up browser caches and bundle size reports. diff --git a/src/index.js b/src/index.js index 61c64214b..5b55527b2 100644 --- a/src/index.js +++ b/src/index.js @@ -36,7 +36,7 @@ export const rules = { 'no-unassigned-import': require('./rules/no-unassigned-import'), 'no-useless-path-segments': require('./rules/no-useless-path-segments'), 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'), - + // export 'exports-last': require('./rules/exports-last'), diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index 50b85508a..0b64b26b6 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -8,8 +8,11 @@ module.exports = { schema: [{ type: 'object', properties: { - importFunction: { - type: 'string', + importFunctions: { + type: 'array', + items: { + type: 'string', + }, }, webpackChunknameFormat: { type: 'string', @@ -20,23 +23,19 @@ module.exports = { create: function (context) { const config = context.options[0] - let importFunction - if (config) { - ({ importFunction } = config) - } + const { importFunctions = [] } = config || {} + const { webpackChunknameFormat = '[0-9a-zA-Z-_/.]+' } = config || {} - let webpackChunknameFormat = '[0-9a-zA-Z-_/.]+' - if (config && config.webpackChunknameFormat) { - ({ webpackChunknameFormat } = config) - } const commentFormat = ` webpackChunkName: "${webpackChunknameFormat}" ` const commentRegex = new RegExp(commentFormat) return { - CallExpression(node) { - if (node.callee.name !== importFunction && node.callee.type !== 'Import') { + [`CallExpression[callee.type="Import"],CallExpression[callee.name]`](node) { + const { callee: { name }} = node + if (name && !importFunctions.includes(name)) { return } + const sourceCode = context.getSourceCode() const arg = node.arguments[0] const leadingComments = sourceCode.getComments(arg).leading diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index c56f2fae6..eb76ce91a 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -6,9 +6,9 @@ const ruleTester = new RuleTester() const commentFormat = '[0-9a-zA-Z-_/.]+' const pickyCommentFormat = '[a-zA-Z-_/.]+' -const options = [{ importFunction: 'dynamicImport' }] +const options = [{ importFunctions: ['dynamicImport'] }] const pickyCommentOptions = [{ - importFunction: 'dynamicImport', + importFunctions: ['dynamicImport'], webpackChunknameFormat: pickyCommentFormat, }] const parser = 'babel-eslint' From 9be016f64c154547e8cd3b2dade4e1c3d0a875a4 Mon Sep 17 00:00:00 2001 From: Kim Strauch Date: Mon, 9 Apr 2018 22:41:32 -0700 Subject: [PATCH 4/5] replace includes() with an indexOf() call, add test cases for multiple import functions --- src/rules/dynamic-import-chunkname.js | 3 ++- tests/src/rules/dynamic-import-chunkname.js | 25 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index 0b64b26b6..dbd1bf790 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -10,6 +10,7 @@ module.exports = { properties: { importFunctions: { type: 'array', + uniqueItems: true, items: { type: 'string', }, @@ -32,7 +33,7 @@ module.exports = { return { [`CallExpression[callee.type="Import"],CallExpression[callee.name]`](node) { const { callee: { name }} = node - if (name && !importFunctions.includes(name)) { + if (name && importFunctions.indexOf(name) < 0) { return } diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index eb76ce91a..329401106 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -11,6 +11,9 @@ const pickyCommentOptions = [{ importFunctions: ['dynamicImport'], webpackChunknameFormat: pickyCommentFormat, }] +const multipleImportFunctionOptions = [{ + importFunctions: ['dynamicImport', 'definitelyNotStaticImport'], +}] const parser = 'babel-eslint' const noLeadingCommentError = 'dynamic imports require a leading comment with the webpack chunkname' @@ -173,6 +176,28 @@ ruleTester.run('dynamic-import-chunkname', rule, { type: 'CallExpression', }], }, + { + code: `dynamicImport( + /* webpackChunkName "someModule" */ + 'someModule' + )`, + options: multipleImportFunctionOptions, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `definitelyNotStaticImport( + /* webpackChunkName "someModule" */ + 'someModule' + )`, + options: multipleImportFunctionOptions, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, { code: `dynamicImport( // webpackChunkName: "someModule" From 115b6fb3c8ab4845bbe4d079320a4f7354ff82a2 Mon Sep 17 00:00:00 2001 From: Kim Strauch Date: Mon, 9 Apr 2018 23:18:20 -0700 Subject: [PATCH 5/5] remove AST selectors --- src/rules/dynamic-import-chunkname.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index dbd1bf790..867808f0b 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -31,9 +31,8 @@ module.exports = { const commentRegex = new RegExp(commentFormat) return { - [`CallExpression[callee.type="Import"],CallExpression[callee.name]`](node) { - const { callee: { name }} = node - if (name && importFunctions.indexOf(name) < 0) { + CallExpression(node) { + if (node.callee.type !== 'Import' && importFunctions.indexOf(node.callee.name) < 0) { return }