@davidwells/extract-deps 0.1.8 → 0.1.11
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/package.json +1 -1
- package/src/dep-graph.js +9 -0
- package/src/get-cjs-exports.js +6 -3
- package/src/get-exports.js +6 -2
- package/src/get-imports.js +24 -4
- package/src/get-requires.js +7 -1
- package/src/utils.js +114 -0
- package/tests/dep-graph.test.js +60 -0
- package/tests/fixtures/esm-dynamic-import-fixture/index.js +11 -0
- package/tests/fixtures/esm-dynamic-import-fixture/package.json +4 -0
- package/tests/fixtures/esm-dynamic-import-fixture/pipeline.js +6 -0
- package/tests/fixtures/esm-side-effect-fixture/index.js +7 -0
- package/tests/fixtures/esm-side-effect-fixture/package.json +4 -0
- package/tests/fixtures/esm-side-effect-fixture/register.js +6 -0
- package/tests/fixtures/string-noise-fixture/index.js +50 -0
- package/tests/fixtures/string-noise-fixture/package.json +4 -0
- package/tests/fixtures/string-noise-fixture/real.js +3 -0
- package/tests/get-cjs-exports.test.js +18 -0
- package/tests/get-exports.test.js +52 -0
- package/tests/get-imports.test.js +106 -0
- package/tests/get-requires.test.js +18 -0
- package/tests/utils.test.js +145 -0
package/package.json
CHANGED
package/src/dep-graph.js
CHANGED
|
@@ -140,6 +140,15 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
|
|
|
140
140
|
if (dep.varName && dep.varName.startsWith('*')) {
|
|
141
141
|
return
|
|
142
142
|
}
|
|
143
|
+
// Side-effect imports (`import './foo.js'`) bind no values — no export required.
|
|
144
|
+
if (!dep.varName && !dep.definitions) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
// Dynamic imports (`import('./foo.js')`) resolve to the module's
|
|
148
|
+
// namespace object, not the default export.
|
|
149
|
+
if (dep.isDynamic) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
143
152
|
// ESM import X from 'pkg' requires a default export
|
|
144
153
|
const hasModuleExports = targetDeps.moduleExports && targetDeps.moduleExports.length > 0
|
|
145
154
|
const hasESMExports = targetDeps.exports && targetDeps.exports.length > 0
|
package/src/get-cjs-exports.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Extracts CommonJS module.exports and exports patterns from JavaScript code
|
|
2
2
|
|
|
3
|
-
const { getNames, extractExportName } = require('./utils')
|
|
3
|
+
const { getNames, extractExportName, stripCommentsAndStrings } = require('./utils')
|
|
4
4
|
|
|
5
5
|
// Matches: module.exports = ... or exports = module.exports = ...
|
|
6
6
|
// https://regex101.com/r/example/cjs-1
|
|
@@ -35,8 +35,11 @@ function stripComments(code) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function getCjsExports(code) {
|
|
38
|
-
//
|
|
39
|
-
|
|
38
|
+
// Single tokenizer-based pass that blanks comments and code-bearing
|
|
39
|
+
// strings/templates together. Length is preserved so the position-based
|
|
40
|
+
// logic below (which slices into the original `code` for full export
|
|
41
|
+
// values) keeps working off the same indices.
|
|
42
|
+
const sanitizedCode = stripCommentsAndStrings(code)
|
|
40
43
|
|
|
41
44
|
const assignmentExports = getModuleExportsAssignment(code, sanitizedCode)
|
|
42
45
|
const modulePropertyExports = getModuleExportsProperty(code, sanitizedCode)
|
package/src/get-exports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { getNames, extractExportName, stripComments } = require('./utils')
|
|
1
|
+
const { getNames, extractExportName, stripComments, stripStringContents } = require('./utils')
|
|
2
2
|
|
|
3
3
|
// https://regex101.com/r/example/1
|
|
4
4
|
const EXPORT_DEFAULT_REGEX = /export\s+default\s+(?:(?:async\s+)?function\s+([\w$]+)|class\s+([\w$]+)|([\w$]+|\([^)]*\)\s*=>|{[\s\S]*?}|\[[\s\S]*?\]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|[\w$]+(?:\s*[+\-*/%]\s*[\w$]+)*))/gm
|
|
@@ -16,8 +16,12 @@ const EXPORT_TYPE_REGEX = /export\s+(?:type|interface)\s+([\w$]+)/gm
|
|
|
16
16
|
const EXPORT_ASSERTION_REGEX = /export\s+(?:["'\s]*([\w*{}\n\r\t, ]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+(assert|with)\s*\{([^}]+)\};?$/gm
|
|
17
17
|
|
|
18
18
|
function getExports(code = '', options = {}) {
|
|
19
|
-
//
|
|
19
|
+
// Comments first, then strings — order matters. Running stripStringContents
|
|
20
|
+
// alone would mis-pair an apostrophe inside a JSDoc comment (`Netlify's …`)
|
|
21
|
+
// with a later quote and shift subsequent backtick pairing, which could
|
|
22
|
+
// swallow real `export` declarations a few lines later.
|
|
20
23
|
code = stripComments(code)
|
|
24
|
+
code = stripStringContents(code)
|
|
21
25
|
const defaultExports = getDefaultExports(code)
|
|
22
26
|
const namedExports = getNamedExports(code)
|
|
23
27
|
const namedDeclarationExports = getNamedDeclarationExports(code)
|
package/src/get-imports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripComments } = require('./utils')
|
|
1
|
+
const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripComments, stripStringContents } = require('./utils')
|
|
2
2
|
|
|
3
3
|
// https://regex101.com/r/cfEmlm/1
|
|
4
4
|
// Path excludes quotes, newlines, and `;` so multi-statement lines like
|
|
@@ -12,7 +12,12 @@ const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, strip
|
|
|
12
12
|
// Negative lookahead `(?!\s*(?:assert|with)\s*\{)` keeps this from matching
|
|
13
13
|
// import-attributes/assertion imports — those go through ASSERTION_IMPORT_REGEX
|
|
14
14
|
// and would otherwise be double-counted in `getImports`.
|
|
15
|
-
|
|
15
|
+
// Leading anchor `(?:^|(?<=[;\n]))` requires `import` to start a line (the `^`
|
|
16
|
+
// matches at every line start under the `m` flag) or follow a `;` on the same
|
|
17
|
+
// line. Without this, string literals like `'note: import preview'` get parsed
|
|
18
|
+
// as `import preview` — the old regex was safe from this only because of its
|
|
19
|
+
// `$` anchor, which we removed to support multi-statement lines.
|
|
20
|
+
const IMPORT_REGEX = /(?:^|(?<=[;\n]))\s*import(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s]+([^"'`\n;\s]+)\s*["'](?!\s*(?:assert|with)\s*\{)/gm
|
|
16
21
|
// https://regex101.com/r/UDmXlb/1
|
|
17
22
|
const IMPORT_TYPE_REGEX = /import\(['"](.*)['"]\)\.?([A-Za-z_0-9.]*)?/gm
|
|
18
23
|
// https://regex101.com/r/UDjMoV/1
|
|
@@ -20,13 +25,24 @@ const IMPORT_TYPE_REGEX = /import\(['"](.*)['"]\)\.?([A-Za-z_0-9.]*)?/gm
|
|
|
20
25
|
// `return import('./foo')`, and `lazy(() => import('./foo'))` all match.
|
|
21
26
|
// `(?<!\.)\b` rejects member access (`obj.import(...)`) and identifiers like
|
|
22
27
|
// `myimport(...)`.
|
|
28
|
+
//
|
|
29
|
+
// TypeScript inline-import-type forms (`: import('./x').Foo`, `typeof
|
|
30
|
+
// import('./x')`) are matched here too. They get `isDynamic: true` and the
|
|
31
|
+
// validators already skip default-export checks for dynamic imports, so they
|
|
32
|
+
// produce no false positives. The relationship is real for compilation
|
|
33
|
+
// (renaming/deleting the target breaks `tsc`), so keeping the dep-graph edge
|
|
34
|
+
// is the more conservative choice for smart-ci's change detection.
|
|
23
35
|
const DYNAMIC_IMPORT_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)?(?:await\s+)?(?<!\.)\bimport\(([^()]+(?:\([^()]*\)[^()]*)*)\)/gm
|
|
24
36
|
// https://regex101.com/r/example/1
|
|
25
37
|
const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s]+([^"'`\n;\s]+)\s*["']\s+(assert|with)\s*\{([^}]+)\}/gm
|
|
26
38
|
|
|
27
39
|
function getImports(code = '', options = {}) {
|
|
28
|
-
// Strip comments
|
|
40
|
+
// Strip comments FIRST so apostrophes/backticks inside JSDoc text get
|
|
41
|
+
// removed before `stripStringContents` looks for string literals.
|
|
42
|
+
// (Running the two in the opposite order let comment chars masquerade as
|
|
43
|
+
// string openers and swallow real `import`/`export` lines further down.)
|
|
29
44
|
code = stripComments(code)
|
|
45
|
+
code = stripStringContents(code)
|
|
30
46
|
const staticImports = getStaticImports(code)
|
|
31
47
|
const dynamicImports = getDynamicImports(code)
|
|
32
48
|
if (options.includeAssertionImports) {
|
|
@@ -41,7 +57,11 @@ function getStaticImports(targetText = '') {
|
|
|
41
57
|
let importStatements = []
|
|
42
58
|
var result;
|
|
43
59
|
while((result = IMPORT_REGEX.exec(targetText)) !== null) {
|
|
44
|
-
|
|
60
|
+
let [ line, names, rawFrom ] = result
|
|
61
|
+
// The leading `(?:^|(?<=[;\n]))\s*` anchor pulls in any whitespace/newline
|
|
62
|
+
// separator that precedes the `import` keyword; trim it back off so `line`
|
|
63
|
+
// reads as the source author wrote it.
|
|
64
|
+
line = line.replace(/^\s+/, '')
|
|
45
65
|
// Side-effect imports (e.g. `import './foo.js'`) and unnamed-default imports skip the
|
|
46
66
|
// optional `from` capture, so the opening quote ends up inside the path group. Strip it.
|
|
47
67
|
const from = rawFrom.replace(/^["'`]|["'`]$/g, '')
|
package/src/get-requires.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { isBuiltIn, isRelativeImport, getNames } = require('./utils')
|
|
1
|
+
const { isBuiltIn, isRelativeImport, getNames, stripComments, stripStringContents } = require('./utils')
|
|
2
2
|
|
|
3
3
|
// Updated regex to exclude ES6 dynamic imports
|
|
4
4
|
// require() must always be present — previous version made it optional, causing false matches
|
|
@@ -65,6 +65,12 @@ function getStaticRequires(targetText = '') {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function getRequires(code) {
|
|
68
|
+
// Strip comments first, then strings — order matters. `stripComments`
|
|
69
|
+
// alone would mis-eat `//` inside strings; running it first instead means
|
|
70
|
+
// comment chars (incl. apostrophes/backticks inside JSDoc) disappear
|
|
71
|
+
// before `stripStringContents` looks for real string literals.
|
|
72
|
+
code = stripComments(code)
|
|
73
|
+
code = stripStringContents(code)
|
|
68
74
|
const staticRequires = getStaticRequires(code)
|
|
69
75
|
const dynamicRequires = getDynamicRequires(code)
|
|
70
76
|
return staticRequires.concat(dynamicRequires).sort((a, b) => a.index - b.index)
|
package/src/utils.js
CHANGED
|
@@ -4,6 +4,118 @@ const builtinModules = require('module').builtinModules
|
|
|
4
4
|
const COMMENT_RE = /\/\*[\s\S]*?\*\/|\/\/.*(?:\r?\n|$)/g
|
|
5
5
|
const PROTOCOL_PLACEHOLDER = '__PROTO_SLASH__'
|
|
6
6
|
|
|
7
|
+
// Single-quoted and double-quoted strings can't contain unescaped newlines
|
|
8
|
+
// (per JS spec — they'd need `\n` escapes or `\<NL>` line continuations). The
|
|
9
|
+
// `[^"\\\n]` / `[^'\\\n]` char classes enforce that, so an unbalanced quote
|
|
10
|
+
// followed by a newline doesn't sweep up the next several lines as "string".
|
|
11
|
+
// Template literals (`` `...` ``) DO span newlines, so their inner class only
|
|
12
|
+
// excludes backtick and backslash. Escape sequences (`\.`) are allowed in all
|
|
13
|
+
// three so escaped quotes don't prematurely end the match.
|
|
14
|
+
// Note: this treats `${...}` interpolations as part of the string body — code
|
|
15
|
+
// inside `${...}` (rare, but possible: `${import('./x.js')}`) gets blanked too.
|
|
16
|
+
// Acceptable for the change-detection use case; a full tokenizer would be
|
|
17
|
+
// needed to preserve interpolations.
|
|
18
|
+
const STRING_LITERAL_RE = /"(?:[^"\\\n]|\\[\s\S])*"|'(?:[^'\\\n]|\\[\s\S])*'|`(?:[^`\\]|\\[\s\S])*`/g
|
|
19
|
+
|
|
20
|
+
// Strings whose bodies contain these tokens are treated as embedded code
|
|
21
|
+
// samples (template strings, generator helpers, fenced docs) and get blanked.
|
|
22
|
+
// Plain module specifiers like `'lodash'` or `'./foo.js'` don't contain any
|
|
23
|
+
// of these as words, so they pass through untouched and remain extractable.
|
|
24
|
+
const EMBEDDED_CODE_RE = /\b(?:import|export|require|module\.exports)\b/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Blank the contents of every string/template literal in `code` that looks
|
|
28
|
+
* like it contains embedded code (its body mentions `import`, `export`,
|
|
29
|
+
* `require`, or `module.exports`). The opening and closing quote chars are
|
|
30
|
+
* preserved, as are newlines. Total length is preserved so character indices
|
|
31
|
+
* remain stable.
|
|
32
|
+
*
|
|
33
|
+
* Strings whose bodies don't reference those keywords — including ordinary
|
|
34
|
+
* module specifiers like `'lodash'`, paths like `'./foo.js'`, URLs, and
|
|
35
|
+
* arbitrary user text — pass through untouched, so the static-import/require
|
|
36
|
+
* regexes can still read their content.
|
|
37
|
+
*
|
|
38
|
+
* Without this pass, regex-based extractors falsely match `import`, `export`,
|
|
39
|
+
* and `require` keywords that appear inside code samples embedded in strings.
|
|
40
|
+
*
|
|
41
|
+
* Run this BEFORE `stripComments` — the existing comment regex would otherwise
|
|
42
|
+
* mis-strip `//` and `/* *\/` sequences that occur inside strings.
|
|
43
|
+
*
|
|
44
|
+
* Caveat: `${...}` interpolations inside template literals are treated as
|
|
45
|
+
* part of the body, so a runtime `${import('./x.js')}` inside a template
|
|
46
|
+
* would be blanked. Very rare in practice; a proper tokenizer would be
|
|
47
|
+
* needed to do better.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} code
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function stripStringContents(code) {
|
|
53
|
+
return code.replace(STRING_LITERAL_RE, (match) => {
|
|
54
|
+
const body = match.slice(1, -1)
|
|
55
|
+
if (!EMBEDDED_CODE_RE.test(body)) {
|
|
56
|
+
// Plain string — leave it alone so module specifiers stay extractable.
|
|
57
|
+
return match
|
|
58
|
+
}
|
|
59
|
+
const opener = match[0]
|
|
60
|
+
const closer = match[match.length - 1]
|
|
61
|
+
const middle = body.replace(/[^\n]/g, ' ')
|
|
62
|
+
return opener + middle + closer
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Unified regex matching whichever comes first at each position: block
|
|
67
|
+
// comment, line comment, single-quoted string, double-quoted string, or
|
|
68
|
+
// template literal. Matching left-to-right means a comment that starts
|
|
69
|
+
// first consumes its body — including any apostrophes or backticks inside
|
|
70
|
+
// — before they can start a spurious string match a few lines later. That
|
|
71
|
+
// solves the JSDoc-apostrophe-shifts-backtick-pairing class of bugs that
|
|
72
|
+
// arose when comments and strings were stripped in two separate passes.
|
|
73
|
+
const COMMENT_OR_STRING_RE = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\\n]|\\[\s\S])*"|'(?:[^'\\\n]|\\[\s\S])*'|`(?:[^`\\]|\\[\s\S])*`/g
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Single-regex pass that blanks comment bodies and code-bearing
|
|
77
|
+
* string/template bodies together. Solves two bugs that occurred when the
|
|
78
|
+
* string-strip and comment-strip ran in separate passes:
|
|
79
|
+
*
|
|
80
|
+
* - Apostrophes inside JSDoc comments (e.g. `* Netlify's \`...`) were
|
|
81
|
+
* mis-paired with later quotes, which shifted subsequent backtick
|
|
82
|
+
* pairing and could swallow real `export` declarations a few lines
|
|
83
|
+
* later in the same file.
|
|
84
|
+
* - `//` and `/* *\/` sequences inside string bodies were mis-stripped
|
|
85
|
+
* as comments.
|
|
86
|
+
*
|
|
87
|
+
* Length is preserved (each consumed char replaced by a space or newline
|
|
88
|
+
* kept verbatim) so downstream character indices stay valid.
|
|
89
|
+
*
|
|
90
|
+
* Strings whose bodies don't reference `import` / `export` / `require` /
|
|
91
|
+
* `module.exports` pass through unchanged, so module specifiers like
|
|
92
|
+
* `'lodash'` or `'./foo.js'` stay readable by the downstream regexes.
|
|
93
|
+
*
|
|
94
|
+
* Caveat: `${...}` interpolations inside template literals are part of the
|
|
95
|
+
* body, so `${import('./x.js')}` inside a template gets blanked. Rare in
|
|
96
|
+
* practice; a full parser would be needed otherwise.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} code
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function stripCommentsAndStrings(code) {
|
|
102
|
+
return code.replace(COMMENT_OR_STRING_RE, (match) => {
|
|
103
|
+
const first = match[0]
|
|
104
|
+
// Comments (line or block) are fully blanked — they have no extractable
|
|
105
|
+
// content. Newlines are preserved so line counts hold.
|
|
106
|
+
if (first === '/') {
|
|
107
|
+
return match.replace(/[^\n]/g, ' ')
|
|
108
|
+
}
|
|
109
|
+
// Strings/templates — only blank if the body references code keywords;
|
|
110
|
+
// otherwise leave the literal intact so `'lodash'` etc. remain readable.
|
|
111
|
+
const body = match.slice(1, -1)
|
|
112
|
+
if (!EMBEDDED_CODE_RE.test(body)) {
|
|
113
|
+
return match
|
|
114
|
+
}
|
|
115
|
+
return first + body.replace(/[^\n]/g, ' ') + first
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
7
119
|
/**
|
|
8
120
|
* Strip comments from code, preserving :// in URLs
|
|
9
121
|
* @param {string} code
|
|
@@ -232,6 +344,8 @@ function extractExportName(exportValue) {
|
|
|
232
344
|
|
|
233
345
|
module.exports = {
|
|
234
346
|
stripComments,
|
|
347
|
+
stripStringContents,
|
|
348
|
+
stripCommentsAndStrings,
|
|
235
349
|
isBuiltIn,
|
|
236
350
|
isRelativeImport,
|
|
237
351
|
isRelativeImportWithQuotes,
|
package/tests/dep-graph.test.js
CHANGED
|
@@ -592,6 +592,66 @@ test('namespace import should build valid graph', () => {
|
|
|
592
592
|
assert.ok(deps.some(d => d.includes('utils.js')), 'Should have utils.js as dependency')
|
|
593
593
|
})
|
|
594
594
|
|
|
595
|
+
// Side-effect imports — `import './register.js'` runs the module for its
|
|
596
|
+
// effects and binds no values. The validator must not require a default export.
|
|
597
|
+
const ESM_SIDE_EFFECT_FIXTURE = path.resolve(__dirname, './fixtures/esm-side-effect-fixture/index.js')
|
|
598
|
+
|
|
599
|
+
test('side-effect import should not require default export', () => {
|
|
600
|
+
assert.not.throws(() => {
|
|
601
|
+
depGraph(ESM_SIDE_EFFECT_FIXTURE, { verifyGraph: true })
|
|
602
|
+
}, "import './x' should not fail when target has only named exports")
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
test('side-effect import should still build the dep edge', () => {
|
|
606
|
+
const graph = depGraph(ESM_SIDE_EFFECT_FIXTURE, { verifyGraph: true })
|
|
607
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-side-effect-fixture/index.js'))
|
|
608
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
609
|
+
|
|
610
|
+
const deps = graph.get(indexPath)
|
|
611
|
+
assert.ok(deps.some(d => d.includes('register.js')), 'Should have register.js as dependency')
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
// Dynamic imports — `await import('./x.js')` resolves to the namespace
|
|
615
|
+
// object, not the default export. Same skip semantics.
|
|
616
|
+
const ESM_DYNAMIC_FIXTURE = path.resolve(__dirname, './fixtures/esm-dynamic-import-fixture/index.js')
|
|
617
|
+
|
|
618
|
+
test('dynamic import should not require default export', () => {
|
|
619
|
+
assert.not.throws(() => {
|
|
620
|
+
depGraph(ESM_DYNAMIC_FIXTURE, { verifyGraph: true })
|
|
621
|
+
}, "import('./x') should not fail when target has only named exports")
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// End-to-end coverage of the string/template/comment noise that historically
|
|
625
|
+
// produced false-positive dep edges. The fixture contains real + fake
|
|
626
|
+
// imports, requires, and exports embedded in template literals, double-
|
|
627
|
+
// quoted strings, single-quoted strings, escaped-quote strings, tagged
|
|
628
|
+
// templates, JSON-shaped strings, and comments. Only ONE real edge should
|
|
629
|
+
// be produced (→ real.js), and no errors should fire.
|
|
630
|
+
const STRING_NOISE_FIXTURE = path.resolve(__dirname, './fixtures/string-noise-fixture/index.js')
|
|
631
|
+
|
|
632
|
+
test('string/template/comment noise produces no false-positive edges', () => {
|
|
633
|
+
let graph
|
|
634
|
+
assert.not.throws(() => {
|
|
635
|
+
graph = depGraph(STRING_NOISE_FIXTURE, { verifyGraph: true })
|
|
636
|
+
}, 'should not throw on the noise fixture')
|
|
637
|
+
|
|
638
|
+
const indexPath = Array.from(graph.keys()).find(k => k.includes('string-noise-fixture/index.js'))
|
|
639
|
+
assert.ok(indexPath, 'index.js should be in the graph')
|
|
640
|
+
|
|
641
|
+
const deps = graph.get(indexPath)
|
|
642
|
+
assert.equal(deps.length, 1, 'should produce exactly one edge — the real import')
|
|
643
|
+
assert.ok(deps[0].includes('real.js'), 'the one edge should be to real.js')
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
test('dynamic import should still build the dep edge', () => {
|
|
647
|
+
const graph = depGraph(ESM_DYNAMIC_FIXTURE, { verifyGraph: true })
|
|
648
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-dynamic-import-fixture/index.js'))
|
|
649
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
650
|
+
|
|
651
|
+
const deps = graph.get(indexPath)
|
|
652
|
+
assert.ok(deps.some(d => d.includes('pipeline.js')), 'Should have pipeline.js as dependency')
|
|
653
|
+
})
|
|
654
|
+
|
|
595
655
|
// TypeScript patterns fixture — exercises all fixed false positive patterns
|
|
596
656
|
const TS_PATTERNS_FIXTURE = path.resolve(__dirname, './fixtures/ts-patterns-fixture/index.ts')
|
|
597
657
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Dynamic import resolves to the module's namespace object, not the
|
|
2
|
+
// default export. The target has only named exports and no default;
|
|
3
|
+
// validation must not flag this as "missing default export".
|
|
4
|
+
export async function load() {
|
|
5
|
+
const pipeline = await import('./pipeline.js')
|
|
6
|
+
return pipeline.run()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function eager() {
|
|
10
|
+
return import('./pipeline.js').then((m) => m.run())
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Pure side-effect module — registers something on a shared registry and
|
|
2
|
+
// exports a single named binding. Critically, no default export.
|
|
3
|
+
const registry = (globalThis.__test_registry = globalThis.__test_registry || [])
|
|
4
|
+
registry.push('side-effect-fixture')
|
|
5
|
+
|
|
6
|
+
export const REGISTERED = true
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Regression fixture: every kind of string/template/comment noise that
|
|
2
|
+
// previously fooled the extractors, alongside one real import edge that
|
|
3
|
+
// MUST still be detected. The dep-graph test for this fixture asserts that
|
|
4
|
+
// exactly one edge (→ real.js) is produced.
|
|
5
|
+
|
|
6
|
+
import { realThing } from './real.js'
|
|
7
|
+
|
|
8
|
+
// --- Single-line comment with a fake import that must be ignored ---
|
|
9
|
+
// import fakeCommented from './fake-commented.js'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Multi-line comment with fake imports/requires inside
|
|
13
|
+
* import fakeMultiline from './fake-multiline.js'
|
|
14
|
+
* const fakeReq = require('./fake-multiline-require.js')
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// --- Template literal with embedded code (the original user-reported case) ---
|
|
18
|
+
const codeSample = `
|
|
19
|
+
import fakeStatic from './fake-template-static.js'
|
|
20
|
+
await import('./fake-template-dynamic.js')
|
|
21
|
+
const fakeReq = require('./fake-template-require.js')
|
|
22
|
+
module.exports.fakeCjs = 'fake'
|
|
23
|
+
export const fakeEsm = 'fake'
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
// --- Double-quoted string holding a code snippet (the astro/config case) ---
|
|
27
|
+
const astroConfigSnippet = "import { defineConfig } from 'astro/config'"
|
|
28
|
+
|
|
29
|
+
// --- Single-quoted string mentioning the word `import` in prose ---
|
|
30
|
+
const docHint = 'Tip: run import preview from the dashboard'
|
|
31
|
+
|
|
32
|
+
// --- URL containing keyword-like words (passes through unchanged) ---
|
|
33
|
+
const docsUrl = 'https://npmjs.com/package/some-pkg'
|
|
34
|
+
|
|
35
|
+
// --- Escaped quotes inside strings ---
|
|
36
|
+
const escapedDq = "He said \"import fakeEscaped from './fake.js'\""
|
|
37
|
+
const escapedSq = 'She said \'export const fakeEsc = 1\''
|
|
38
|
+
|
|
39
|
+
// --- Adjacent strings on a single line ---
|
|
40
|
+
const adjacent = 'import a' + ' from ' + '"./fake.js"'
|
|
41
|
+
|
|
42
|
+
// --- Tagged template literal — still a template literal ---
|
|
43
|
+
const html = String.raw`<pre>import fakeTagged from './fake-tagged.js'</pre>`
|
|
44
|
+
|
|
45
|
+
// --- JSON-like string with an import-related package name ---
|
|
46
|
+
const pkgJson = '{ "name": "import-utils", "main": "./lib/index.js" }'
|
|
47
|
+
|
|
48
|
+
export function run() {
|
|
49
|
+
return realThing()
|
|
50
|
+
}
|
|
@@ -851,4 +851,22 @@ Object.keys(config).forEach((k) => {
|
|
|
851
851
|
assert.equal(reExports.length, 0, 'Should not match Object.keys patterns that do not assign to exports')
|
|
852
852
|
})
|
|
853
853
|
|
|
854
|
+
test('ignores CJS exports embedded in template literals and strings', () => {
|
|
855
|
+
const code = `
|
|
856
|
+
module.exports.realName = 'real'
|
|
857
|
+
exports.alsoReal = 'real'
|
|
858
|
+
|
|
859
|
+
const template = \`
|
|
860
|
+
module.exports.fakeName = 'fake'
|
|
861
|
+
exports.alsoFake = 'fake'
|
|
862
|
+
\`
|
|
863
|
+
|
|
864
|
+
const dq = "module.exports.stringFake = 'fake'"
|
|
865
|
+
`
|
|
866
|
+
|
|
867
|
+
const found = getCjsExports(code)
|
|
868
|
+
const names = found.map(e => e.exportName).filter(Boolean).sort()
|
|
869
|
+
assert.equal(names, ['alsoReal', 'realName'], 'Only the real CJS exports should be detected')
|
|
870
|
+
})
|
|
871
|
+
|
|
854
872
|
test.run()
|
|
@@ -563,4 +563,56 @@ export const real = 'value' // this comment should be stripped
|
|
|
563
563
|
assert.equal(foundExports[2].exportName, 'real', 'Should find real export')
|
|
564
564
|
})
|
|
565
565
|
|
|
566
|
+
test('apostrophe inside JSDoc with surrounding backticks does not swallow later exports', () => {
|
|
567
|
+
// Regression: when `stripStringContents` ran BEFORE `stripComments`, an
|
|
568
|
+
// apostrophe inside a JSDoc block (e.g. `Netlify's \`promptType: 'foo'\``)
|
|
569
|
+
// was treated as the opener of a single-quoted string and consumed text up
|
|
570
|
+
// to the next `'`, shifting every subsequent backtick pair by one. The
|
|
571
|
+
// newly mis-paired template literal then spanned multiple lines, including
|
|
572
|
+
// the real `export async function` declaration that followed the JSDoc,
|
|
573
|
+
// and the embedded-code heuristic blanked it. The fix runs `stripComments`
|
|
574
|
+
// first so JSDoc-internal punctuation is gone before string matching.
|
|
575
|
+
//
|
|
576
|
+
// This fixture replicates the structural shape of the AWS SDK adapter file
|
|
577
|
+
// that originally surfaced the bug: a JSDoc block containing both an
|
|
578
|
+
// apostrophe and multiple backticks, followed by an `export async function`,
|
|
579
|
+
// followed by ANOTHER JSDoc with backticks.
|
|
580
|
+
const code = `
|
|
581
|
+
/**
|
|
582
|
+
* Stuff with \`code-one\`. Netlify's \`promptType: 'foo'\`. End \`Error\` on
|
|
583
|
+
* non-2xx upstream responses or malformed bodies.
|
|
584
|
+
*/
|
|
585
|
+
export async function generateThing(request) {
|
|
586
|
+
return request
|
|
587
|
+
}
|
|
588
|
+
/** Test seam: override the global \`fetch\` for unit tests. */
|
|
589
|
+
export const helper = 1
|
|
590
|
+
`
|
|
591
|
+
|
|
592
|
+
const found = getExports(code)
|
|
593
|
+
const names = found.map(e => e.exportName).sort()
|
|
594
|
+
assert.equal(
|
|
595
|
+
names,
|
|
596
|
+
['generateThing', 'helper'],
|
|
597
|
+
'Both exports must survive past the JSDoc apostrophe+backtick combo'
|
|
598
|
+
)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('ignores ESM exports embedded in template literals and strings', () => {
|
|
602
|
+
const code = `
|
|
603
|
+
export const real = 'real'
|
|
604
|
+
|
|
605
|
+
const template = \`
|
|
606
|
+
export const fake = 'fake'
|
|
607
|
+
export default function fakeDefault() {}
|
|
608
|
+
\`
|
|
609
|
+
|
|
610
|
+
const dq = "export { foo } from 'fake'"
|
|
611
|
+
`
|
|
612
|
+
|
|
613
|
+
const found = getExports(code)
|
|
614
|
+
const names = found.map(e => e.exportName).filter(Boolean).sort()
|
|
615
|
+
assert.equal(names, ['real'], 'Only the real export should be detected')
|
|
616
|
+
})
|
|
617
|
+
|
|
566
618
|
test.run()
|
|
@@ -577,6 +577,84 @@ test('side-effect imports preserve definitions/varName semantics', () => {
|
|
|
577
577
|
assert.is(imp.definitions, null, 'side-effect imports have null definitions')
|
|
578
578
|
})
|
|
579
579
|
|
|
580
|
+
test('ignores import statements embedded in template-literal strings', () => {
|
|
581
|
+
// Regression: code samples embedded in template literals (codegen, doc
|
|
582
|
+
// strings, test fixtures) were being parsed as real imports. The
|
|
583
|
+
// `stripStringContents` pass in `getImports` blanks those bodies first.
|
|
584
|
+
const code = `
|
|
585
|
+
import real from './real.js'
|
|
586
|
+
|
|
587
|
+
const template = \`
|
|
588
|
+
import fakeStatic from './fake-static.js'
|
|
589
|
+
await import('./fake-dynamic.js')
|
|
590
|
+
const fakeFs = require('./fake-fs.js')
|
|
591
|
+
\`
|
|
592
|
+
|
|
593
|
+
const code = "import { foo } from 'fake-double-quoted'"
|
|
594
|
+
const more = 'import bar from "fake-single-quoted"'
|
|
595
|
+
`
|
|
596
|
+
|
|
597
|
+
const importsFound = normalizeTestResult(getImports(code))
|
|
598
|
+
const froms = importsFound.map(i => i.from).sort()
|
|
599
|
+
assert.equal(froms, ['./real.js'], 'Only the real top-level import should match')
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test('preserves module specifiers — plain strings without code keywords pass through', () => {
|
|
603
|
+
// The string-stripping heuristic only blanks string bodies that contain
|
|
604
|
+
// import/export/require/module.exports keywords. Plain module specifiers
|
|
605
|
+
// like 'lodash' or './foo.js' must NOT be touched, or `getImports` would
|
|
606
|
+
// lose its `from` value entirely.
|
|
607
|
+
const code = `
|
|
608
|
+
import lodash from 'lodash'
|
|
609
|
+
import foo from './foo.js'
|
|
610
|
+
import url from 'https://esm.sh/some-pkg'
|
|
611
|
+
import scoped from '@scope/pkg/sub/path'
|
|
612
|
+
`
|
|
613
|
+
|
|
614
|
+
const importsFound = normalizeTestResult(getImports(code))
|
|
615
|
+
const froms = importsFound.map(i => i.from).sort()
|
|
616
|
+
assert.equal(froms, [
|
|
617
|
+
'../lodash'.replace('../', ''),
|
|
618
|
+
'./foo.js',
|
|
619
|
+
'@scope/pkg/sub/path',
|
|
620
|
+
'https://esm.sh/some-pkg',
|
|
621
|
+
].sort())
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('does not match the word `import` inside string literals', () => {
|
|
625
|
+
// Regression: dropping the trailing `$` anchor (to enable multi-statement
|
|
626
|
+
// lines) let any quoted string containing the word `import` followed by
|
|
627
|
+
// text and a closing quote get parsed as a real import. The fix anchors
|
|
628
|
+
// `import` to a line start or `;`, so `'note: import preview'` is ignored.
|
|
629
|
+
const code = `
|
|
630
|
+
import { handler as importPreviewHandler } from '../functions/contacts/import-preview.js'
|
|
631
|
+
const route = createRoute({
|
|
632
|
+
summary: 'Contacts — import preview',
|
|
633
|
+
description: 'Triggers the import flow',
|
|
634
|
+
})
|
|
635
|
+
const msg = 'See docs: import preview from your dashboard'
|
|
636
|
+
`
|
|
637
|
+
|
|
638
|
+
const importsFound = normalizeTestResult(getStaticImports(code))
|
|
639
|
+
assert.equal(importsFound.length, 1, 'Should only find the real import')
|
|
640
|
+
assert.equal(importsFound[0].from, '../functions/contacts/import-preview.js')
|
|
641
|
+
assert.equal(importsFound[0].varName, '{ handler as importPreviewHandler }')
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
test('matches indented imports without leaking whitespace into `line`', () => {
|
|
645
|
+
// The anchor regex consumes any leading whitespace/newline before `import`;
|
|
646
|
+
// make sure that doesn't bleed into the captured `line` string.
|
|
647
|
+
const code = `
|
|
648
|
+
import foo from 'foo'
|
|
649
|
+
\timport bar from 'bar'
|
|
650
|
+
`
|
|
651
|
+
|
|
652
|
+
const importsFound = normalizeTestResult(getStaticImports(code))
|
|
653
|
+
assert.equal(importsFound.length, 2)
|
|
654
|
+
assert.equal(importsFound[0].line, "import foo from 'foo'")
|
|
655
|
+
assert.equal(importsFound[1].line, "import bar from 'bar'")
|
|
656
|
+
})
|
|
657
|
+
|
|
580
658
|
test('multiple static imports on a single line', () => {
|
|
581
659
|
// Regression: the `(.*[@\w_-]+)["'\s]+;?$` pattern was greedy plus EOL-anchored,
|
|
582
660
|
// so a line like `import a from 'a'; import b from 'b';` matched as ONE import
|
|
@@ -643,6 +721,34 @@ const [a, b] = await Promise.all([import('./a.js'), import('./b.js')])
|
|
|
643
721
|
assert.ok(importsFound.every(i => i.isDynamic), 'All should have isDynamic: true')
|
|
644
722
|
})
|
|
645
723
|
|
|
724
|
+
test('dynamic-import regex includes TypeScript inline-import-type expressions', () => {
|
|
725
|
+
// Intentionally inclusive: `import('./x').TypeName` and `typeof import('./x')`
|
|
726
|
+
// are type-only and erased at runtime, but the *file* relationship is real
|
|
727
|
+
// (renaming/deleting the target breaks `tsc`). Including them keeps the dep
|
|
728
|
+
// graph conservative for change-detection purposes. The validators already
|
|
729
|
+
// skip default-export checks for `isDynamic: true`, so this produces no
|
|
730
|
+
// false positives, and `tsc` is the right tool for runtime misuse.
|
|
731
|
+
const code = `
|
|
732
|
+
function approve(payload: unknown): import('../types.js').ArtifactTypeApproval {
|
|
733
|
+
return payload as any
|
|
734
|
+
}
|
|
735
|
+
type Cfg = typeof import('./config.js')
|
|
736
|
+
type Promised = import('./x.js').Then
|
|
737
|
+
const real = import('./real.js')
|
|
738
|
+
const chain = import('./api.js').then(m => m.run())
|
|
739
|
+
`
|
|
740
|
+
|
|
741
|
+
const importsFound = getDynamicImports(code)
|
|
742
|
+
const froms = importsFound.map(i => i.from).sort()
|
|
743
|
+
assert.equal(
|
|
744
|
+
froms,
|
|
745
|
+
['../types.js', './api.js', './config.js', './real.js', './x.js'].sort(),
|
|
746
|
+
'Should match all import() forms — type-position and runtime alike'
|
|
747
|
+
)
|
|
748
|
+
// All flagged dynamic — validators key on this to skip default-export checks
|
|
749
|
+
assert.ok(importsFound.every(i => i.isDynamic))
|
|
750
|
+
})
|
|
751
|
+
|
|
646
752
|
test('dynamic-import regex rejects member access and identifier prefixes', () => {
|
|
647
753
|
// `obj.import('x')` and `myimport('x')` are not dynamic imports — the
|
|
648
754
|
// `(?<!\.)\b` guard prevents these from matching now that `await` is optional.
|
|
@@ -268,4 +268,22 @@ test('should still match real requires mixed with non-require code', async () =>
|
|
|
268
268
|
assert.equal(staticRequires[2].from, 'side-effect-only')
|
|
269
269
|
})
|
|
270
270
|
|
|
271
|
+
test('ignores requires embedded in template literals, strings, and comments', () => {
|
|
272
|
+
const code = `
|
|
273
|
+
const real = require('./real.js')
|
|
274
|
+
|
|
275
|
+
const template = \`
|
|
276
|
+
const fake = require('./fake-template.js')
|
|
277
|
+
\`
|
|
278
|
+
|
|
279
|
+
// const commented = require('./commented.js')
|
|
280
|
+
|
|
281
|
+
const dq = "const fakeStr = require('./fake-string.js')"
|
|
282
|
+
`
|
|
283
|
+
|
|
284
|
+
const found = getRequires(code)
|
|
285
|
+
const froms = found.map(r => r.from).sort()
|
|
286
|
+
assert.equal(froms, ['./real.js'], 'Only the real require should match')
|
|
287
|
+
})
|
|
288
|
+
|
|
271
289
|
test.run()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { stripStringContents, stripComments } = require('../src/utils')
|
|
4
|
+
|
|
5
|
+
// `stripStringContents` blanks the bodies of string/template literals whose
|
|
6
|
+
// content references `import`/`export`/`require`/`module.exports`. Plain
|
|
7
|
+
// strings pass through. Length and newlines are preserved.
|
|
8
|
+
|
|
9
|
+
test('stripStringContents — leaves plain module specifiers untouched', () => {
|
|
10
|
+
// The whole point: a `require('lodash')` call must keep `lodash` so the
|
|
11
|
+
// require regex can read the path. Blanking it would break extraction.
|
|
12
|
+
const input = `const x = require('lodash')\nimport foo from './foo.js'`
|
|
13
|
+
assert.equal(stripStringContents(input), input)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('stripStringContents — blanks template literal that contains code', () => {
|
|
17
|
+
const input = '`\nimport fake from "./fake.js"\nexport const x = 1\n`'
|
|
18
|
+
const output = stripStringContents(input)
|
|
19
|
+
|
|
20
|
+
// Length preserved
|
|
21
|
+
assert.equal(output.length, input.length, 'should preserve total length')
|
|
22
|
+
// Newlines preserved
|
|
23
|
+
assert.equal(
|
|
24
|
+
(output.match(/\n/g) || []).length,
|
|
25
|
+
(input.match(/\n/g) || []).length,
|
|
26
|
+
'should preserve newline count'
|
|
27
|
+
)
|
|
28
|
+
// The body must not still contain the keywords
|
|
29
|
+
assert.not.match(output, /import/)
|
|
30
|
+
assert.not.match(output, /export/)
|
|
31
|
+
// Opening/closing backticks survive
|
|
32
|
+
assert.is(output[0], '`')
|
|
33
|
+
assert.is(output[output.length - 1], '`')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('stripStringContents — blanks double-quoted strings with code', () => {
|
|
37
|
+
const input = `const x = "import { defineConfig } from 'astro/config'"`
|
|
38
|
+
const output = stripStringContents(input)
|
|
39
|
+
|
|
40
|
+
assert.equal(output.length, input.length)
|
|
41
|
+
assert.not.match(output, /import/)
|
|
42
|
+
// Outer `"` chars preserved at the original positions
|
|
43
|
+
const openIdx = input.indexOf('"')
|
|
44
|
+
const closeIdx = input.lastIndexOf('"')
|
|
45
|
+
assert.is(output[openIdx], '"')
|
|
46
|
+
assert.is(output[closeIdx], '"')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('stripStringContents — blanks single-quoted strings with code', () => {
|
|
50
|
+
const input = `const msg = 'export const fake = 1'`
|
|
51
|
+
const output = stripStringContents(input)
|
|
52
|
+
assert.equal(output.length, input.length)
|
|
53
|
+
assert.not.match(output, /export/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('stripStringContents — strings without code keywords are preserved verbatim', () => {
|
|
57
|
+
const cases = [
|
|
58
|
+
`const x = 'hello world'`,
|
|
59
|
+
`const u = 'https://example.com/path/to/thing.json'`,
|
|
60
|
+
`const p = './some/path/with-dashes/and.dots'`,
|
|
61
|
+
`const t = \`plain template with \${interpolation}\``,
|
|
62
|
+
]
|
|
63
|
+
for (const input of cases) {
|
|
64
|
+
assert.equal(stripStringContents(input), input, `should leave alone: ${input}`)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('stripStringContents — handles escaped quotes inside strings', () => {
|
|
69
|
+
// `"He said \"import fake\""` — the escaped quote should NOT close the
|
|
70
|
+
// string early. The body contains `import`, so it gets blanked.
|
|
71
|
+
const input = `const m = "He said \\"import fakeEscaped from './fake.js'\\""`
|
|
72
|
+
const output = stripStringContents(input)
|
|
73
|
+
assert.equal(output.length, input.length)
|
|
74
|
+
assert.not.match(output, /import\s+fakeEscaped/)
|
|
75
|
+
// The trailing chars after the real closing quote should be unchanged
|
|
76
|
+
assert.is(output[output.length - 1], '"')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('stripStringContents — unbalanced quote followed by newline is NOT a string', () => {
|
|
80
|
+
// Single/double-quoted strings can't cross newlines per JS spec. An
|
|
81
|
+
// unterminated quote followed by a newline must not start a multi-line
|
|
82
|
+
// sweep that swallows the next several lines.
|
|
83
|
+
const input = `import React from react"\n\nimport real from './real.js'`
|
|
84
|
+
const output = stripStringContents(input)
|
|
85
|
+
// The second `import real from './real.js'` MUST survive unchanged
|
|
86
|
+
assert.ok(output.includes("import real from './real.js'"))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('stripStringContents — template literal across many lines blanks all of them', () => {
|
|
90
|
+
const input = '`line 1\nimport x from "./fake.js"\nline 3\n`'
|
|
91
|
+
const output = stripStringContents(input)
|
|
92
|
+
assert.equal(output.length, input.length)
|
|
93
|
+
// Inner `import` gone
|
|
94
|
+
assert.not.match(output, /import\s+x/)
|
|
95
|
+
// Newlines preserved so line numbers still line up
|
|
96
|
+
assert.equal((output.match(/\n/g) || []).length, 3)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('stripStringContents — adjacent strings on one line are matched independently', () => {
|
|
100
|
+
// `'import' + ' from ' + '"./fake"'` — three separate strings.
|
|
101
|
+
// Only the first contains the `import` keyword, so only it is blanked.
|
|
102
|
+
const input = `const x = 'import' + ' from ' + '"./fake.js"'`
|
|
103
|
+
const output = stripStringContents(input)
|
|
104
|
+
assert.equal(output.length, input.length)
|
|
105
|
+
// The middle string ` from ` and last string `"./fake.js"` are preserved
|
|
106
|
+
assert.ok(output.includes("' from '"))
|
|
107
|
+
assert.ok(output.includes(`'"./fake.js"'`))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('stripStringContents — tagged template literals are still blanked', () => {
|
|
111
|
+
// `String.raw\`<div>import fakeTagged from './fake.js'</div>\`` — the
|
|
112
|
+
// backtick portion is a template literal regardless of the tag prefix.
|
|
113
|
+
const input = "const html = String.raw`<div>import fakeTagged from './fake.js'</div>`"
|
|
114
|
+
const output = stripStringContents(input)
|
|
115
|
+
assert.equal(output.length, input.length)
|
|
116
|
+
assert.not.match(output, /import\s+fakeTagged/)
|
|
117
|
+
// The tag identifier `String.raw` must survive
|
|
118
|
+
assert.ok(output.includes('String.raw'))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('stripStringContents — preserves :// URL strings (they have no code keywords)', () => {
|
|
122
|
+
const input = `const url = 'https://npmjs.com/package/import-utils'`
|
|
123
|
+
// Note: `import-utils` contains `import` followed by `-` (non-word).
|
|
124
|
+
// `\bimport\b` matches at that boundary, so the body IS blanked. This is
|
|
125
|
+
// a conservative blanking but it's harmless — we don't try to extract
|
|
126
|
+
// from inside the URL string anyway.
|
|
127
|
+
const output = stripStringContents(input)
|
|
128
|
+
assert.equal(output.length, input.length)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('stripStringContents — neutralizes // inside code-bearing strings so stripComments cannot eat them', () => {
|
|
132
|
+
// For strings whose bodies DO trigger the heuristic (contain import/export
|
|
133
|
+
// etc.), the body becomes spaces and any `//` or `/* */` inside is gone
|
|
134
|
+
// before stripComments runs. Result: the surrounding code is preserved.
|
|
135
|
+
// Strings WITHOUT those keywords still hit the pre-existing `stripComments`
|
|
136
|
+
// limitation, which is outside the scope of this helper.
|
|
137
|
+
const input = `const x = "// import fakeCmt from './fake.js'"\nconst y = 1`
|
|
138
|
+
const after = stripComments(stripStringContents(input))
|
|
139
|
+
// The outer quoted-string structure must remain intact — `stripComments`
|
|
140
|
+
// would have eaten from `//` to end of line if we hadn't blanked first.
|
|
141
|
+
assert.ok(after.startsWith('const x = "'), 'opening of the string survives')
|
|
142
|
+
assert.ok(after.includes('"\nconst y = 1'), 'closing quote and following code survive')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test.run()
|