diff --git a/README.md b/README.md index 6fab76861..238c5347a 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,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 @@ -101,6 +102,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 diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md new file mode 100644 index 000000000..98b98871e --- /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 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, if we don't want the number 6 to show up in our chunk names: + ```javascript +{ + "dynamic-import-chunkname": [2, { + importFunctions: ["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..5b55527b2 100644 --- a/src/index.js +++ b/src/index.js @@ -35,6 +35,7 @@ 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..867808f0b --- /dev/null +++ b/src/rules/dynamic-import-chunkname.js @@ -0,0 +1,70 @@ +import docsUrl from '../docsUrl' + +module.exports = { + meta: { + docs: { + url: docsUrl('dynamic-import-chunkname'), + }, + schema: [{ + type: 'object', + properties: { + importFunctions: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + webpackChunknameFormat: { + type: 'string', + }, + }, + }], + }, + + create: function (context) { + const config = context.options[0] + const { importFunctions = [] } = config || {} + const { webpackChunknameFormat = '[0-9a-zA-Z-_/.]+' } = config || {} + + const commentFormat = ` webpackChunkName: "${webpackChunknameFormat}" ` + const commentRegex = new RegExp(commentFormat) + + return { + CallExpression(node) { + if (node.callee.type !== 'Import' && importFunctions.indexOf(node.callee.name) < 0) { + 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..329401106 --- /dev/null +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -0,0 +1,276 @@ +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 = [{ importFunctions: ['dynamicImport'] }] +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' +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: multipleImportFunctionOptions, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `definitelyNotStaticImport( + /* webpackChunkName "someModule" */ + 'someModule' + )`, + options: multipleImportFunctionOptions, + errors: [{ + message: commentFormatError, + 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', + }], + }, + ], +})