Skip to content

Commit e03caee

Browse files
authored
feat: support dynamic imports with comments. (#32)
1 parent a13e3f6 commit e03caee

File tree

13 files changed

+228
-37
lines changed

13 files changed

+228
-37
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Test
3131
run: npm test
3232
- name: Report Coverage
33-
uses: codecov/[email protected].4
33+
uses: codecov/[email protected].5
3434
with:
3535
token: ${{ secrets.CODECOV_TOKEN }}
3636
- name: Lint

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ jobs:
3333
uses: JS-DevTools/[email protected]
3434
with:
3535
token: ${{ secrets.NPM_AUTH_TOKEN }}
36+
tag: ${{ contains(github.ref, '-') && 'next' || 'latest' }}

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The `webpackChunkName` comment is added by default when registering the loader.
4646
* [`verbose`](#verbose)
4747
* [`mode`](#mode)
4848
* [`match`](#match)
49+
* [`comments`](#comments)
4950
* `[magicCommentName: string]: MagicCommentValue` see `magic-comments` [options](https://github.com/morganney/magic-comments#options) for details
5051

5152
### `verbose`
@@ -75,6 +76,18 @@ Sets how the loader finds dynamic import expressions in your source code, either
7576

7677
Sets how globs are matched, either the module file path, or the `import()` specifier.
7778

79+
### `comments`
80+
**type**
81+
```ts
82+
'ignore' | 'prepend' | 'append' | 'replace'
83+
| (cmts: Array<{ start: number; end: number; text: string }>, magicComment: string) => string
84+
```
85+
**default** `'ignore'`
86+
87+
_Note, this option only considers block comments that precede the dynamic imports specifier, and any comments coming after are ignored and left intact._
88+
89+
Sets how dynamic imports with block comments are handled. If `ignore` is used, then it will be skipped and no magic comments from your configuration will be applied. If `replace` is used, then all found comments will be replaced with the magic comments. `append` and `prepend` add the magic comments before, or after the found comments, respectively. If a function is used it will be passed the found comments, and the magic comment string that is to be applied. The return value has the same effect as `replace`.
90+
7891
## Examples
7992

8093
Below are examples for some of the supported magic comments. Consult the [loader specification](https://github.com/morganney/magic-comments-loader/blob/main/__tests__/loader.spec.js) for a comprehensive usage example.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import(
2+
/* webpackChunkNames: "test-chunk" */
3+
/* something else */
4+
/* webpackFetchPriority: "high" */
5+
'./folder/module.js'
6+
/* after the specifier */
7+
)

__tests__/formatter.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ describe('format', () => {
1010
match: 'module',
1111
source: src,
1212
filepath: 'src/module.js',
13-
comments: [{ start: openLen, end: openLen + commentLen, commentText: ' comment ' }],
13+
comments: 'ignore',
14+
astComments: [{ start: openLen, end: openLen + commentLen, text: ' comment ' }],
1415
magicCommentOptions: { webpackChunkName: true },
1516
importExpressionNodes: [
1617
{
@@ -38,7 +39,8 @@ describe('format', () => {
3839
match: 'module',
3940
source: src,
4041
filepath: 'src/module.js',
41-
comments: [],
42+
comments: 'ignore',
43+
astComments: [],
4244
magicCommentOptions: { webpackMode: () => 'invalid' },
4345
importExpressionNodes: [
4446
{

__tests__/loader.spec.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,4 +1080,124 @@ describe('loader', () => {
10801080

10811081
expect(output).toEqual(expect.stringContaining("import('./folder/module.js')"))
10821082
})
1083+
1084+
it('updates imports with comments based on configuration', async () => {
1085+
const entry = '__fixtures__/commented.js'
1086+
let stats = await build(entry, {
1087+
use: {
1088+
loader: loaderPath,
1089+
options: {
1090+
comments: 'replace',
1091+
webpackChunkName: true
1092+
}
1093+
}
1094+
})
1095+
let output = stats.toJson({ source: true }).modules[0].source
1096+
1097+
expect(output).toMatch(/webpackChunkName: "folder-module"/)
1098+
expect(output).not.toMatch(/something else/)
1099+
1100+
stats = await build(entry, {
1101+
use: {
1102+
loader: loaderPath,
1103+
options: {
1104+
comments: 'prepend',
1105+
webpackExports: () => ['a']
1106+
}
1107+
}
1108+
})
1109+
output = stats.toJson({ source: true }).modules[0].source
1110+
1111+
expect(output).toMatch(/webpackExports: \["a"\]/)
1112+
expect(output).toMatch('webpackChunkNames: "test-chunk"')
1113+
expect(output.indexOf('webpackExports') < output.indexOf('webpackChunkName')).toBe(
1114+
true
1115+
)
1116+
1117+
stats = await build(entry, {
1118+
use: {
1119+
loader: loaderPath,
1120+
options: {
1121+
comments: 'append',
1122+
webpackExports: () => ['b']
1123+
}
1124+
}
1125+
})
1126+
output = stats.toJson({ source: true }).modules[0].source
1127+
1128+
expect(output).toMatch(/webpackExports: \["b"\]/)
1129+
expect(output).toMatch('webpackFetchPriority: "high"')
1130+
expect(
1131+
output.indexOf('webpackExports') > output.indexOf('webpackFetchPriority')
1132+
).toBe(true)
1133+
1134+
let firstCmt = ''
1135+
let magicCmt = ''
1136+
1137+
stats = await build(entry, {
1138+
use: {
1139+
loader: loaderPath,
1140+
options: {
1141+
comments: (cmts, magicComment) => {
1142+
firstCmt = cmts[0]
1143+
magicCmt = magicComment
1144+
return `${magicComment} /* ${cmts[0].text} */`
1145+
},
1146+
webpackExports: () => ['c']
1147+
}
1148+
}
1149+
})
1150+
output = stats.toJson({ source: true }).modules[0].source
1151+
1152+
expect(output).toMatch(/webpackExports: \["c"\]/)
1153+
expect(output).toMatch(firstCmt.text)
1154+
expect(output.indexOf(firstCmt.text) > output.indexOf(magicCmt)).toBe(true)
1155+
1156+
stats = await build(entry, {
1157+
use: {
1158+
loader: loaderPath,
1159+
options: {
1160+
comments: () => {
1161+
return 123
1162+
},
1163+
webpackExports: () => ['c']
1164+
}
1165+
}
1166+
})
1167+
output = stats.toJson({ source: true }).modules[0].source
1168+
1169+
// Return values other than strings result in no changes
1170+
expect(output).not.toMatch(/webpackExports: \["c"\]/)
1171+
expect(output).toMatch(firstCmt.text)
1172+
1173+
stats = await build(entry, {
1174+
use: {
1175+
loader: loaderPath,
1176+
options: {
1177+
comments: 'prepend',
1178+
webpackExports: {
1179+
options: {
1180+
exports: () => ['c']
1181+
},
1182+
overrides: [
1183+
{
1184+
files: '**/*.js',
1185+
options: {
1186+
exports: () => ['d']
1187+
}
1188+
}
1189+
]
1190+
}
1191+
}
1192+
}
1193+
})
1194+
output = stats.toJson({ source: true }).modules[0].source
1195+
1196+
// The `comments` option Should work with overrides too.
1197+
expect(output).toMatch(/webpackExports: \["d"\]/)
1198+
expect(output).toMatch('webpackChunkNames: "test-chunk"')
1199+
expect(output.indexOf('webpackExports') < output.indexOf('webpackChunkName')).toBe(
1200+
true
1201+
)
1202+
})
10831203
})

__tests__/parser.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ describe('parse', () => {
2323
)
2424
}
2525
`
26-
const { comments } = parse(src)
26+
const { astComments } = parse(src)
2727

28-
expect(comments).toEqual([{ start: 175, end: 188, commentText: ' comment ' }])
28+
expect(astComments).toEqual([{ start: 175, end: 188, text: ' comment ' }])
2929
})
3030
})

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "magic-comments-loader",
3-
"version": "2.0.5",
3+
"version": "2.1.0",
44
"description": "Add webpack magic comments to your dynamic imports at build time.",
55
"main": "dist",
66
"type": "module",
@@ -68,7 +68,7 @@
6868
"magic-comments": "^2.1.12",
6969
"magic-string": "^0.30.0",
7070
"micromatch": "^4.0.4",
71-
"schema-utils": "^4.1.0"
71+
"schema-utils": "^4.2.0"
7272
},
7373
"prettier": {
7474
"printWidth": 90,

src/formatter.js

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,25 @@ import MagicString from 'magic-string'
44
const format = ({
55
match,
66
source,
7-
filepath,
87
comments,
8+
filepath,
9+
astComments,
910
magicCommentOptions,
1011
importExpressionNodes
1112
}) => {
1213
const magicImports = []
13-
const cmts = [...comments]
1414
const src = new MagicString(source)
15-
const hasComment = node => {
16-
const idx = cmts.findIndex(cmt => cmt.start > node.start && cmt.end < node.end)
17-
const wasFound = idx > -1
18-
19-
if (wasFound) {
20-
cmts.splice(idx, 1)
21-
}
22-
23-
return wasFound
15+
const getComments = node => {
16+
// This ignores comments that come after the imports specifier.
17+
return astComments.filter(
18+
cmt => cmt.start > node.start && cmt.end < node.end && cmt.start < node.source.end
19+
)
2420
}
2521

2622
for (const node of importExpressionNodes) {
27-
if (!hasComment(node)) {
23+
const cmts = getComments(node)
24+
25+
if (!cmts.length || comments !== 'ignore') {
2826
const specifier = source.substring(node.source.start, node.source.end)
2927
const magicComment = getMagicComment({
3028
match,
@@ -34,13 +32,41 @@ const format = ({
3432
})
3533

3634
if (magicComment) {
37-
magicImports.push(
38-
src
39-
.snip(node.start, node.end)
40-
.toString()
41-
.replace(specifier, `${magicComment} ${specifier}`)
42-
)
43-
src.appendLeft(node.source.start, `${magicComment} `)
35+
const clone = src.snip(node.start, node.end)
36+
37+
if (!cmts.length) {
38+
magicImports.push(
39+
clone.toString().replace(specifier, `${magicComment} ${specifier}`)
40+
)
41+
src.appendLeft(node.source.start, `${magicComment} `)
42+
} else {
43+
/**
44+
* Get the minimum start and maximum end.
45+
* Assumption is that comment nodes are sorted
46+
* in ascending order of `node.start`.
47+
*/
48+
const minStart = cmts[0].start
49+
const maxEnd = cmts[cmts.length - 1].end
50+
51+
if (comments === 'replace') {
52+
magicImports.push(clone.overwrite(minStart, maxEnd, magicComment).toString())
53+
src.overwrite(minStart, maxEnd, magicComment)
54+
} else if (comments === 'append') {
55+
magicImports.push(clone.appendRight(maxEnd, ` ${magicComment}`).toString())
56+
src.appendRight(maxEnd, ` ${magicComment}`)
57+
} else if (comments === 'prepend') {
58+
magicImports.push(clone.prependLeft(minStart, `${magicComment} `).toString())
59+
src.prependLeft(minStart, `${magicComment} `)
60+
} else {
61+
// Has to be a function or the schema validator is broken
62+
const replacement = comments(cmts, magicComment)
63+
64+
if (typeof replacement === 'string') {
65+
magicImports.push(clone.overwrite(minStart, maxEnd, replacement).toString())
66+
src.overwrite(minStart, maxEnd, replacement)
67+
}
68+
}
69+
}
4470
}
4571
}
4672
}

0 commit comments

Comments
 (0)