magic-comments-loader 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ![CI](https://github.com/morganney/magic-comments-loader/actions/workflows/ci.yml/badge.svg)
4
4
  [![codecov](https://codecov.io/gh/morganney/magic-comments-loader/branch/master/graph/badge.svg?token=1DWQL43B8V)](https://codecov.io/gh/morganney/magic-comments-loader)
5
5
 
6
- Keep your source code clean, add [magic coments](https://webpack.js.org/api/module-methods/#magic-comments) to your dynamic `import()` statements at build time.
6
+ Keep your source code clean, add [magic comments](https://webpack.js.org/api/module-methods/#magic-comments) to your dynamic `import()` expressions at build time.
7
7
 
8
8
  ## Getting Started
9
9
 
@@ -19,7 +19,7 @@ Next add the loader to your `webpack.config.js` file:
19
19
  module: {
20
20
  rules: [
21
21
  {
22
- test: /\.[jt]sx?$/,
22
+ test: /\.jsx?$/,
23
23
  use: ['magic-comments-loader']
24
24
  }
25
25
  ]
@@ -45,6 +45,7 @@ The `webpackChunkName` comment is added by default when registering the loader.
45
45
  Most loader options can be defined with a [`CommentConfig`](#commentconfig) object to support overrides and suboptions ([`CommentOptions`](#commentoptions)). Options that support globs use [`micromatch`](https://github.com/micromatch/micromatch) for pattern matching.
46
46
 
47
47
  * [`verbose`](#verbose)
48
+ * [`mode`](#mode)
48
49
  * [`match`](#match)
49
50
  * [`webpackChunkName`](#webpackchunkname)
50
51
  * [`webpackFetchPriority`](#webpackfetchpriority)
@@ -95,6 +96,15 @@ boolean
95
96
 
96
97
  Prints console statements of the module filepath and updated `import()` during the webpack build. Useful for debugging.
97
98
 
99
+ ### `mode`
100
+ **type**
101
+ ```ts
102
+ 'parser' | 'regexp'
103
+ ```
104
+ **default** `'parser'`
105
+
106
+ Sets how the loader finds dynamic import expressions in your source code, either using an [ECMAScript parser](https://github.com/acornjs/acorn), or a regular expression. Your mileage may vary when using `'regexp'`.
107
+
98
108
  ### `match`
99
109
  **type**
100
110
  ```ts
@@ -525,7 +535,7 @@ When using a [`CommentConfig`](#commentconfig) object, you can override the conf
525
535
  }
526
536
  ```
527
537
 
528
- The `files` and `config` keys are both required, where the former is glob string, or array thereof, and the latter is the associated magic comment's [`CommentOptions`](#commentoptions).
538
+ The `files` and `config` keys are both required, where the former is a glob string, or an array thereof, and the latter is the associated magic comment's [`CommentOptions`](#commentoptions).
529
539
 
530
540
  Here's a more complete example of how overrides can be applied:
531
541
 
@@ -581,3 +591,27 @@ import(/* webpackMode: "lazy" */ './folder/module.js')
581
591
  import(/* webpackMode: "eager" */ './eager/module.js')
582
592
  import(/* webpackChunkName: "locales-[request]", webpackMode: "lazy-once" */ `./locales/${lang}.json`)
583
593
  ```
594
+
595
+ ### TypeScript
596
+
597
+ When using TypeScript or experimental ECMAScript features <= [stage 3](https://tc39.es/process-document/), i.e. non spec compliant, you must chain the appropriate loaders with `magic-comments-loader` coming after.
598
+
599
+ For example, if your project source code is written in TypeScript, and you use `babel-loader` to transpile and remove type annotations via `@babel/preset-typescript`, while `tsc` is used for type-checking only, chain loaders like this:
600
+
601
+ **config**
602
+ ```js
603
+ module: {
604
+ rules: [
605
+ {
606
+ test: /\.[jt]sx?$/,
607
+ // Webpack loader chains are processed in reverse order, i.e. last comes first.
608
+ use: [
609
+ 'magic-comments-loader',
610
+ 'babel-loader'
611
+ ]
612
+ }
613
+ ]
614
+ }
615
+ ```
616
+
617
+ You would configure `ts-loader` similarly, or any other loader that transpiles your source code into spec compliant ECMAScript.
@@ -5,16 +5,17 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.getCommenter = void 0;
7
7
  var _strategy = require("./strategy.cjs");
8
+ var _util = require("./util.cjs");
8
9
  const getCommenter = (filepath, options, logger) => (rgxMatch, capturedImportPath) => {
9
10
  const importPath = capturedImportPath.trim();
10
- const bareImportPath = importPath.replace(/['"`]/g, '');
11
+ const bareImportPath = (0, _util.getBareImportSpecifier)(importPath);
11
12
  const {
12
13
  verbose,
13
14
  match,
14
- ...magicCommentOptions
15
+ magicCommentOptions
15
16
  } = options;
16
17
  const magicComment = Object.keys(magicCommentOptions).map(key => _strategy.commentFor[key](filepath, bareImportPath, magicCommentOptions[key], match)).filter(Boolean);
17
- const magicImport = rgxMatch.replace(capturedImportPath, magicComment.length > 0 ? `/* ${magicComment.join(', ')} */ ${importPath}` : `${importPath}`);
18
+ const magicImport = rgxMatch.replace(capturedImportPath, magicComment.length > 0 ? `/* ${magicComment.join(', ')} */ ${importPath}` : importPath);
18
19
  if (verbose) {
19
20
  logger.info(`${filepath} : ${magicImport}`);
20
21
  }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.format = void 0;
7
+ var _magicString = _interopRequireDefault(require("magic-string"));
8
+ var _strategy = require("./strategy.cjs");
9
+ var _util = require("./util.cjs");
10
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
+ const format = ({
12
+ match,
13
+ source,
14
+ filepath,
15
+ comments,
16
+ magicCommentOptions,
17
+ importExpressionNodes
18
+ }) => {
19
+ const magicImports = [];
20
+ const step = 'import('.length;
21
+ const cmts = [...comments];
22
+ const src = new _magicString.default(source);
23
+ const hasComment = node => {
24
+ const idx = cmts.findIndex(cmt => cmt.start > node.start && cmt.end < node.end);
25
+ const wasFound = idx > -1;
26
+ if (wasFound) {
27
+ cmts.splice(idx, 1);
28
+ }
29
+ return wasFound;
30
+ };
31
+ for (const node of importExpressionNodes) {
32
+ if (!hasComment(node)) {
33
+ const specifier = source.substring(node.start + step, node.end - 1);
34
+ const bareImportPath = (0, _util.getBareImportSpecifier)(specifier);
35
+ const magic = Object.keys(magicCommentOptions).map(key => _strategy.commentFor[key](filepath, bareImportPath, magicCommentOptions[key], match)).filter(Boolean);
36
+ if (magic.length) {
37
+ const magicComment = `/* ${magic.join(', ')} */ `;
38
+ magicImports.push(src.snip(node.start, node.end).toString().replace(specifier, `${magicComment}${specifier}`));
39
+ src.appendRight(node.start + step, `${magicComment}`);
40
+ }
41
+ }
42
+ }
43
+ return [src.toString(), magicImports];
44
+ };
45
+ exports.format = format;
@@ -6,20 +6,44 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.loader = void 0;
7
7
  var _schemaUtils = require("schema-utils");
8
8
  var _schema = require("./schema.cjs");
9
+ var _parser = require("./parser.cjs");
10
+ var _formatter = require("./formatter.cjs");
9
11
  var _comment = require("./comment.cjs");
10
12
  var _util = require("./util.cjs");
11
- const loader = function (source, map, meta) {
13
+ const loader = function (source) {
12
14
  const options = this.getOptions();
13
- const optionKeys = Object.keys(options);
14
15
  const logger = this.getLogger('MCL');
15
16
  (0, _schemaUtils.validate)(_schema.schema, options, {
16
17
  name: 'magic-comments-loader'
17
18
  });
18
- const filepath = this.utils.contextify(this.rootContext, this.resourcePath);
19
- const magicComments = (0, _comment.getCommenter)(filepath.replace(/^\.\/?/, ''), optionKeys.length > 0 ? options : {
20
- match: 'module',
19
+ const {
20
+ mode = 'parser',
21
+ match = 'module',
22
+ verbose = false,
23
+ ...rest
24
+ } = options;
25
+ const magicCommentOptions = Object.keys(rest).length ? rest : {
21
26
  webpackChunkName: true
22
- }, logger);
23
- this.callback(null, source.replace(_util.dynamicImportsWithoutComments, magicComments), map, meta);
27
+ };
28
+ const filepath = this.utils.contextify(this.rootContext, this.resourcePath).replace(/^\.\/?/, '');
29
+ if (mode === 'parser') {
30
+ const [magicSource, magicImports] = (0, _formatter.format)({
31
+ ...(0, _parser.parse)(source),
32
+ match,
33
+ filepath,
34
+ magicCommentOptions
35
+ });
36
+ if (verbose) {
37
+ magicImports.forEach(magicImport => {
38
+ logger.info(`${filepath} : ${magicImport}`);
39
+ });
40
+ }
41
+ return magicSource;
42
+ }
43
+ return source.replace(_util.dynamicImportsWithoutComments, (0, _comment.getCommenter)(filepath, {
44
+ verbose,
45
+ match,
46
+ magicCommentOptions
47
+ }, logger));
24
48
  };
25
49
  exports.loader = loader;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.parse = void 0;
7
+ var _acorn = require("acorn");
8
+ var _acornWalk = require("acorn-walk");
9
+ var _acornJsxWalk = require("acorn-jsx-walk");
10
+ var _acornJsx = _interopRequireDefault(require("acorn-jsx"));
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ /**
13
+ * NOTE: Side-effect of importing this module's exports.
14
+ *
15
+ * Extend acorn-walk's base with missing JSX nodes.
16
+ * @see https://github.com/acornjs/acorn/issues/829
17
+ *
18
+ * Consider another parser that supports more syntaxes out-of-the-box.
19
+ * That would enable less requirements on loader chaining.
20
+ */
21
+ (0, _acornJsxWalk.extend)(_acornWalk.base);
22
+ const jsxParser = _acorn.Parser.extend((0, _acornJsx.default)());
23
+ const parse = source => {
24
+ const comments = [];
25
+ const importExpressionNodes = [];
26
+ const ast = jsxParser.parse(source, {
27
+ locations: false,
28
+ ecmaVersion: 2023,
29
+ sourceType: 'module',
30
+ allowAwaitOutsideFunction: true,
31
+ allowReturnOutsideFunction: true,
32
+ allowImportExportEverywhere: true,
33
+ onComment: (isBlock, commentText, start, end) => {
34
+ if (isBlock) {
35
+ comments.push({
36
+ start,
37
+ end,
38
+ commentText
39
+ });
40
+ }
41
+ }
42
+ });
43
+ (0, _acornWalk.simple)(ast, {
44
+ ImportExpression(node) {
45
+ importExpressionNodes.push(node);
46
+ }
47
+ });
48
+ return {
49
+ ast,
50
+ comments,
51
+ importExpressionNodes,
52
+ source
53
+ };
54
+ };
55
+ exports.parse = parse;
@@ -19,6 +19,9 @@ const schema = {
19
19
  verbose: {
20
20
  type: 'boolean'
21
21
  },
22
+ mode: {
23
+ enum: ['parser', 'regexp']
24
+ },
22
25
  match: {
23
26
  enum: ['module', 'import']
24
27
  },
package/dist/cjs/util.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.pathIsMatch = exports.importPrefix = exports.getOverrideSchema = exports.getOverrideConfig = exports.dynamicImportsWithoutComments = void 0;
6
+ exports.pathIsMatch = exports.importPrefix = exports.getOverrideSchema = exports.getOverrideConfig = exports.getBareImportSpecifier = exports.dynamicImportsWithoutComments = void 0;
7
7
  var _micromatch = _interopRequireDefault(require("micromatch"));
8
8
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
9
9
  const pathIsMatch = (path, files) => {
@@ -56,6 +56,10 @@ const getOverrideConfig = (overrides, filepath, config) => {
56
56
  return config;
57
57
  };
58
58
  exports.getOverrideConfig = getOverrideConfig;
59
+ const getBareImportSpecifier = specifier => {
60
+ return specifier.replace(/['"`]/g, '');
61
+ };
62
+ exports.getBareImportSpecifier = getBareImportSpecifier;
59
63
  const importPrefix = /^(?:(\.{1,2}\/)+)|^\/|^.+:\/\/\/?[.-\w]+\//;
60
64
  exports.importPrefix = importPrefix;
61
65
  const dynamicImportsWithoutComments = /(?<![\w.]|#!|(?:\/{2}.+\n?)+|\/\*[\s\w]*?|\*.+?|['"`][^)$,\n]*)import\s*\((?!\s*\/\*)(?<path>\s*?['"`][^)]+['"`]\s*)\)(?!\s*?\*\/)/gm;
package/dist/comment.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { commentFor } from './strategy.js';
2
+ import { getBareImportSpecifier } from './util.js';
2
3
  const getCommenter = (filepath, options, logger) => (rgxMatch, capturedImportPath) => {
3
4
  const importPath = capturedImportPath.trim();
4
- const bareImportPath = importPath.replace(/['"`]/g, '');
5
+ const bareImportPath = getBareImportSpecifier(importPath);
5
6
  const {
6
7
  verbose,
7
8
  match,
8
- ...magicCommentOptions
9
+ magicCommentOptions
9
10
  } = options;
10
11
  const magicComment = Object.keys(magicCommentOptions).map(key => commentFor[key](filepath, bareImportPath, magicCommentOptions[key], match)).filter(Boolean);
11
- const magicImport = rgxMatch.replace(capturedImportPath, magicComment.length > 0 ? `/* ${magicComment.join(', ')} */ ${importPath}` : `${importPath}`);
12
+ const magicImport = rgxMatch.replace(capturedImportPath, magicComment.length > 0 ? `/* ${magicComment.join(', ')} */ ${importPath}` : importPath);
12
13
  if (verbose) {
13
14
  logger.info(`${filepath} : ${magicImport}`);
14
15
  }
@@ -0,0 +1,38 @@
1
+ import MagicString from 'magic-string';
2
+ import { commentFor } from './strategy.js';
3
+ import { getBareImportSpecifier } from './util.js';
4
+ const format = ({
5
+ match,
6
+ source,
7
+ filepath,
8
+ comments,
9
+ magicCommentOptions,
10
+ importExpressionNodes
11
+ }) => {
12
+ const magicImports = [];
13
+ const step = 'import('.length;
14
+ const cmts = [...comments];
15
+ const src = new MagicString(source);
16
+ const hasComment = node => {
17
+ const idx = cmts.findIndex(cmt => cmt.start > node.start && cmt.end < node.end);
18
+ const wasFound = idx > -1;
19
+ if (wasFound) {
20
+ cmts.splice(idx, 1);
21
+ }
22
+ return wasFound;
23
+ };
24
+ for (const node of importExpressionNodes) {
25
+ if (!hasComment(node)) {
26
+ const specifier = source.substring(node.start + step, node.end - 1);
27
+ const bareImportPath = getBareImportSpecifier(specifier);
28
+ const magic = Object.keys(magicCommentOptions).map(key => commentFor[key](filepath, bareImportPath, magicCommentOptions[key], match)).filter(Boolean);
29
+ if (magic.length) {
30
+ const magicComment = `/* ${magic.join(', ')} */ `;
31
+ magicImports.push(src.snip(node.start, node.end).toString().replace(specifier, `${magicComment}${specifier}`));
32
+ src.appendRight(node.start + step, `${magicComment}`);
33
+ }
34
+ }
35
+ }
36
+ return [src.toString(), magicImports];
37
+ };
38
+ export { format };
package/dist/loader.js CHANGED
@@ -1,19 +1,43 @@
1
1
  import { validate } from 'schema-utils';
2
2
  import { schema } from './schema.js';
3
+ import { parse } from './parser.js';
4
+ import { format } from './formatter.js';
3
5
  import { getCommenter } from './comment.js';
4
6
  import { dynamicImportsWithoutComments } from './util.js';
5
- const loader = function (source, map, meta) {
7
+ const loader = function (source) {
6
8
  const options = this.getOptions();
7
- const optionKeys = Object.keys(options);
8
9
  const logger = this.getLogger('MCL');
9
10
  validate(schema, options, {
10
11
  name: 'magic-comments-loader'
11
12
  });
12
- const filepath = this.utils.contextify(this.rootContext, this.resourcePath);
13
- const magicComments = getCommenter(filepath.replace(/^\.\/?/, ''), optionKeys.length > 0 ? options : {
14
- match: 'module',
13
+ const {
14
+ mode = 'parser',
15
+ match = 'module',
16
+ verbose = false,
17
+ ...rest
18
+ } = options;
19
+ const magicCommentOptions = Object.keys(rest).length ? rest : {
15
20
  webpackChunkName: true
16
- }, logger);
17
- this.callback(null, source.replace(dynamicImportsWithoutComments, magicComments), map, meta);
21
+ };
22
+ const filepath = this.utils.contextify(this.rootContext, this.resourcePath).replace(/^\.\/?/, '');
23
+ if (mode === 'parser') {
24
+ const [magicSource, magicImports] = format({
25
+ ...parse(source),
26
+ match,
27
+ filepath,
28
+ magicCommentOptions
29
+ });
30
+ if (verbose) {
31
+ magicImports.forEach(magicImport => {
32
+ logger.info(`${filepath} : ${magicImport}`);
33
+ });
34
+ }
35
+ return magicSource;
36
+ }
37
+ return source.replace(dynamicImportsWithoutComments, getCommenter(filepath, {
38
+ verbose,
39
+ match,
40
+ magicCommentOptions
41
+ }, logger));
18
42
  };
19
43
  export { loader };
package/dist/parser.js ADDED
@@ -0,0 +1,49 @@
1
+ import { Parser } from 'acorn';
2
+ import { simple, base } from 'acorn-walk';
3
+ import { extend } from 'acorn-jsx-walk';
4
+ import jsx from 'acorn-jsx';
5
+
6
+ /**
7
+ * NOTE: Side-effect of importing this module's exports.
8
+ *
9
+ * Extend acorn-walk's base with missing JSX nodes.
10
+ * @see https://github.com/acornjs/acorn/issues/829
11
+ *
12
+ * Consider another parser that supports more syntaxes out-of-the-box.
13
+ * That would enable less requirements on loader chaining.
14
+ */
15
+ extend(base);
16
+ const jsxParser = Parser.extend(jsx());
17
+ const parse = source => {
18
+ const comments = [];
19
+ const importExpressionNodes = [];
20
+ const ast = jsxParser.parse(source, {
21
+ locations: false,
22
+ ecmaVersion: 2023,
23
+ sourceType: 'module',
24
+ allowAwaitOutsideFunction: true,
25
+ allowReturnOutsideFunction: true,
26
+ allowImportExportEverywhere: true,
27
+ onComment: (isBlock, commentText, start, end) => {
28
+ if (isBlock) {
29
+ comments.push({
30
+ start,
31
+ end,
32
+ commentText
33
+ });
34
+ }
35
+ }
36
+ });
37
+ simple(ast, {
38
+ ImportExpression(node) {
39
+ importExpressionNodes.push(node);
40
+ }
41
+ });
42
+ return {
43
+ ast,
44
+ comments,
45
+ importExpressionNodes,
46
+ source
47
+ };
48
+ };
49
+ export { parse };
package/dist/schema.js CHANGED
@@ -13,6 +13,9 @@ const schema = {
13
13
  verbose: {
14
14
  type: 'boolean'
15
15
  },
16
+ mode: {
17
+ enum: ['parser', 'regexp']
18
+ },
16
19
  match: {
17
20
  enum: ['module', 'import']
18
21
  },
package/dist/util.js CHANGED
@@ -46,6 +46,9 @@ const getOverrideConfig = (overrides, filepath, config) => {
46
46
  }
47
47
  return config;
48
48
  };
49
+ const getBareImportSpecifier = specifier => {
50
+ return specifier.replace(/['"`]/g, '');
51
+ };
49
52
  const importPrefix = /^(?:(\.{1,2}\/)+)|^\/|^.+:\/\/\/?[.-\w]+\//;
50
53
  const dynamicImportsWithoutComments = /(?<![\w.]|#!|(?:\/{2}.+\n?)+|\/\*[\s\w]*?|\*.+?|['"`][^)$,\n]*)import\s*\((?!\s*\/\*)(?<path>\s*?['"`][^)]+['"`]\s*)\)(?!\s*?\*\/)/gm;
51
- export { getOverrideConfig, getOverrideSchema, pathIsMatch, importPrefix, dynamicImportsWithoutComments };
54
+ export { getBareImportSpecifier, getOverrideConfig, getOverrideSchema, pathIsMatch, importPrefix, dynamicImportsWithoutComments };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "magic-comments-loader",
3
- "version": "1.5.1",
4
- "description": "Add webpack magic comments to your dynamic imports at build time",
3
+ "version": "1.6.0",
4
+ "description": "Add webpack magic comments to your dynamic imports at build time.",
5
5
  "main": "dist",
6
6
  "type": "module",
7
7
  "exports": {
@@ -64,7 +64,12 @@
64
64
  "webpack": "^5.87.0"
65
65
  },
66
66
  "dependencies": {
67
+ "acorn": "^8.9.0",
68
+ "acorn-jsx": "^5.3.2",
69
+ "acorn-jsx-walk": "^2.0.0",
70
+ "acorn-walk": "^8.2.0",
67
71
  "loader-utils": "^3.2.1",
72
+ "magic-string": "^0.30.0",
68
73
  "micromatch": "^4.0.4",
69
74
  "schema-utils": "^4.1.0"
70
75
  }