@davidwells/extract-deps 0.1.5 → 0.1.8
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 +19 -0
- package/src/get-cjs-exports.js +91 -2
- package/src/get-imports.js +44 -11
- package/src/get-requires.js +6 -4
- package/tests/dep-graph.test.js +90 -0
- package/tests/fixtures/esm-namespace-fixture/index.js +5 -0
- package/tests/fixtures/esm-namespace-fixture/package.json +4 -0
- package/tests/fixtures/esm-namespace-fixture/utils.js +7 -0
- package/tests/fixtures/esm-reexport-fixture/barrel.js +1 -0
- package/tests/fixtures/esm-reexport-fixture/impl.js +7 -0
- package/tests/fixtures/esm-reexport-fixture/index.js +5 -0
- package/tests/fixtures/esm-reexport-fixture/package.json +4 -0
- package/tests/fixtures/ts-patterns-fixture/constants.ts +2 -0
- package/tests/fixtures/ts-patterns-fixture/index.ts +17 -0
- package/tests/fixtures/ts-patterns-fixture/package.json +4 -0
- package/tests/fixtures/ts-patterns-fixture/transforms.ts +7 -0
- package/tests/fixtures/ts-patterns-fixture/utils.ts +7 -0
- package/tests/get-cjs-exports.test.js +92 -0
- package/tests/get-imports.test.js +240 -4
- package/tests/get-requires.test.js +60 -0
package/package.json
CHANGED
package/src/dep-graph.js
CHANGED
|
@@ -79,12 +79,17 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
|
|
|
79
79
|
|
|
80
80
|
// Check CommonJS exports
|
|
81
81
|
const exportedNames = new Set()
|
|
82
|
+
let hasReExportAll = false
|
|
82
83
|
if (targetDeps.moduleExports && targetDeps.moduleExports.length > 0) {
|
|
83
84
|
for (const exp of targetDeps.moduleExports) {
|
|
84
85
|
if (exp.exportType === 'named') {
|
|
85
86
|
exportedNames.add(exp.exportName)
|
|
86
87
|
} else if (exp.exportType === 'default' && exp.keys) {
|
|
87
88
|
exp.keys.forEach(key => exportedNames.add(key))
|
|
89
|
+
} else if (exp.exportType === 'all') {
|
|
90
|
+
// Bundler-emitted dynamic re-export-all (Object.keys+forEach,
|
|
91
|
+
// __exportStar, __reExport). Same semantics as ESM `export * from`.
|
|
92
|
+
hasReExportAll = true
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -92,6 +97,11 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
|
|
|
92
97
|
// Check ESM exports
|
|
93
98
|
if (targetDeps.exports && targetDeps.exports.length > 0) {
|
|
94
99
|
for (const exp of targetDeps.exports) {
|
|
100
|
+
if (exp.exportType === 'all') {
|
|
101
|
+
// export * from './module' — can't know what names are re-exported without
|
|
102
|
+
// following the chain, so skip verification for this module
|
|
103
|
+
hasReExportAll = true
|
|
104
|
+
}
|
|
95
105
|
const exportedName = exp.exportAlias || exp.exportName
|
|
96
106
|
if (exportedName) {
|
|
97
107
|
exportedNames.add(exportedName)
|
|
@@ -99,6 +109,11 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
|
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
// If module uses export * from, we can't verify names — skip
|
|
113
|
+
if (hasReExportAll) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
// Verify each imported name is exported
|
|
103
118
|
for (const importedName of importedNames) {
|
|
104
119
|
if (!exportedNames.has(importedName)) {
|
|
@@ -121,6 +136,10 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
|
|
|
121
136
|
// Only applies to ESM imports - CJS require() gets whatever module.exports is
|
|
122
137
|
else {
|
|
123
138
|
if (dep.isESM) {
|
|
139
|
+
// Namespace imports (import * as X) don't need a default export
|
|
140
|
+
if (dep.varName && dep.varName.startsWith('*')) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
124
143
|
// ESM import X from 'pkg' requires a default export
|
|
125
144
|
const hasModuleExports = targetDeps.moduleExports && targetDeps.moduleExports.length > 0
|
|
126
145
|
const hasESMExports = targetDeps.exports && targetDeps.exports.length > 0
|
package/src/get-cjs-exports.js
CHANGED
|
@@ -41,22 +41,33 @@ function getCjsExports(code) {
|
|
|
41
41
|
const assignmentExports = getModuleExportsAssignment(code, sanitizedCode)
|
|
42
42
|
const modulePropertyExports = getModuleExportsProperty(code, sanitizedCode)
|
|
43
43
|
const propertyExports = getExportsProperty(code, sanitizedCode)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
// Quick pre-checks to avoid expensive regex operations when not needed
|
|
46
46
|
const objectAssignExports = sanitizedCode.includes('Object.assign')
|
|
47
47
|
? getObjectAssignExports(code, sanitizedCode)
|
|
48
48
|
: []
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
const definePropertyExports = sanitizedCode.includes('Object.defineProperty')
|
|
51
51
|
? getDefinePropertyExports(code, sanitizedCode)
|
|
52
52
|
: []
|
|
53
53
|
|
|
54
|
+
// Detect dynamic CJS re-export-all patterns emitted by Rollup, tsc, esbuild,
|
|
55
|
+
// and friends. Cheap substring guard first to avoid the regex work on files
|
|
56
|
+
// that obviously can't contain these patterns.
|
|
57
|
+
const reExportAllExports =
|
|
58
|
+
sanitizedCode.includes('Object.keys(') ||
|
|
59
|
+
sanitizedCode.includes('__exportStar') ||
|
|
60
|
+
sanitizedCode.includes('__reExport')
|
|
61
|
+
? getReExportAllPatterns(code, sanitizedCode)
|
|
62
|
+
: []
|
|
63
|
+
|
|
54
64
|
/*
|
|
55
65
|
console.log('assignmentExports', assignmentExports)
|
|
56
66
|
console.log('modulePropertyExports', modulePropertyExports)
|
|
57
67
|
console.log('propertyExports', propertyExports)
|
|
58
68
|
console.log('objectAssignExports', objectAssignExports)
|
|
59
69
|
console.log('definePropertyExports', definePropertyExports)
|
|
70
|
+
console.log('reExportAllExports', reExportAllExports)
|
|
60
71
|
/** */
|
|
61
72
|
|
|
62
73
|
const allExports = []
|
|
@@ -65,6 +76,7 @@ function getCjsExports(code) {
|
|
|
65
76
|
.concat(propertyExports)
|
|
66
77
|
.concat(objectAssignExports)
|
|
67
78
|
.concat(definePropertyExports)
|
|
79
|
+
.concat(reExportAllExports)
|
|
68
80
|
|
|
69
81
|
return allExports.sort((a, b) => a.index - b.index)
|
|
70
82
|
}
|
|
@@ -372,6 +384,83 @@ function getDefinePropertyExports(originalText = '', sanitizedText = originalTex
|
|
|
372
384
|
return exportStatements
|
|
373
385
|
}
|
|
374
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Detect dynamic CJS re-export-all patterns where a module forwards every
|
|
389
|
+
* named export of another module at runtime. Bundlers emit these for ESM
|
|
390
|
+
* `export * from './x'` syntax when targeting CJS:
|
|
391
|
+
*
|
|
392
|
+
* - Rollup: `Object.keys(X).forEach(function(k){ if (k!=='default' && ...) exports[k]=X[k] })`
|
|
393
|
+
* - tsc/tslib: `__exportStar(require('./x'), exports)` / `tslib.__exportStar(...)`
|
|
394
|
+
* - esbuild: `__reExport(stdin_exports, require('./x'))`
|
|
395
|
+
*
|
|
396
|
+
* We don't try to resolve the require and merge names — that's recursive and
|
|
397
|
+
* filesystem-dependent. Instead each match emits `exportType: 'all'`, the same
|
|
398
|
+
* marker `get-exports.js` uses for ESM `export * from`. Downstream validators
|
|
399
|
+
* already skip name-verification when they see this marker, so importing a
|
|
400
|
+
* name that's only available via dynamic re-export no longer false-positives.
|
|
401
|
+
*
|
|
402
|
+
* Captures the require path or source-variable name into `from` when visible,
|
|
403
|
+
* for debugging and future resolve-and-merge work.
|
|
404
|
+
*/
|
|
405
|
+
function getReExportAllPatterns(originalText = '', sanitizedText = originalText) {
|
|
406
|
+
const exportStatements = []
|
|
407
|
+
|
|
408
|
+
// Rollup `Object.keys(X).forEach(function(k){ ... exports[k] = X[k] ... })`.
|
|
409
|
+
// Allow either `function (k)` or `(k) =>` and any condition on `k` inside.
|
|
410
|
+
const rollupPattern = /Object\.keys\s*\(\s*(\w+)\s*\)\s*\.forEach\s*\(\s*(?:function\s*\(\s*\w+\s*\)|\(?\s*\w+\s*\)?\s*=>)\s*\{[^}]*exports\s*\[\s*\w+\s*\]\s*=\s*\1\s*\[/g
|
|
411
|
+
let result
|
|
412
|
+
while ((result = rollupPattern.exec(sanitizedText)) !== null) {
|
|
413
|
+
const sourceVar = result[1]
|
|
414
|
+
exportStatements.push({
|
|
415
|
+
line: result[0].split('\n')[0].trim(),
|
|
416
|
+
exportType: 'all',
|
|
417
|
+
exportName: '',
|
|
418
|
+
exportValue: result[0],
|
|
419
|
+
from: sourceVar,
|
|
420
|
+
index: result.index,
|
|
421
|
+
isExport: true,
|
|
422
|
+
isReExportAll: true
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// tslib / tsc `__exportStar(<source>, exports)` where source is either
|
|
427
|
+
// a `require('./x')` call or a bare identifier holding a prior require.
|
|
428
|
+
const exportStarPattern = /(?:\w+\.)?__exportStar\s*\(\s*(?:require\s*\(\s*['"]([^'"]+)['"]\s*\)|(\w+))\s*,\s*(?:module\.)?exports\s*\)/g
|
|
429
|
+
while ((result = exportStarPattern.exec(sanitizedText)) !== null) {
|
|
430
|
+
const from = result[1] || result[2]
|
|
431
|
+
exportStatements.push({
|
|
432
|
+
line: result[0].trim(),
|
|
433
|
+
exportType: 'all',
|
|
434
|
+
exportName: '',
|
|
435
|
+
exportValue: result[0],
|
|
436
|
+
from,
|
|
437
|
+
index: result.index,
|
|
438
|
+
isExport: true,
|
|
439
|
+
isReExportAll: true
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// esbuild `__reExport(<exports-target>, <source>)`. The exports-target is
|
|
444
|
+
// typically an identifier ending in `_exports` or `exports`, and the source
|
|
445
|
+
// is again a `require()` call or an identifier.
|
|
446
|
+
const reExportPattern = /__reExport\s*\(\s*\w+\s*,\s*(?:require\s*\(\s*['"]([^'"]+)['"]\s*\)|(\w+))\s*(?:,\s*[^)]*)?\)/g
|
|
447
|
+
while ((result = reExportPattern.exec(sanitizedText)) !== null) {
|
|
448
|
+
const from = result[1] || result[2]
|
|
449
|
+
exportStatements.push({
|
|
450
|
+
line: result[0].trim(),
|
|
451
|
+
exportType: 'all',
|
|
452
|
+
exportName: '',
|
|
453
|
+
exportValue: result[0],
|
|
454
|
+
from,
|
|
455
|
+
index: result.index,
|
|
456
|
+
isExport: true,
|
|
457
|
+
isReExportAll: true
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return exportStatements
|
|
462
|
+
}
|
|
463
|
+
|
|
375
464
|
if (require.main === module) {
|
|
376
465
|
const code = `
|
|
377
466
|
// module.exports assignment
|
package/src/get-imports.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripComments } = require('./utils')
|
|
2
2
|
|
|
3
3
|
// https://regex101.com/r/cfEmlm/1
|
|
4
|
-
|
|
4
|
+
// Path excludes quotes, newlines, and `;` so multi-statement lines like
|
|
5
|
+
// `import a from 'a'; import b from 'b'` parse as two distinct matches
|
|
6
|
+
// instead of one match that spans the semicolon. The leading separator is
|
|
7
|
+
// `["'\s]+` (not `["'\s]`) so it can consume both the gap whitespace and the
|
|
8
|
+
// opening quote in one go — without that, the path group is left starting on
|
|
9
|
+
// the quote and fails for side-effect imports like `import './foo.js'`.
|
|
10
|
+
// Closing is `\s*["']` (not `["'\s]+;?`) so the captured `line` stops at the
|
|
11
|
+
// closing quote instead of bleeding into the next line's whitespace.
|
|
12
|
+
// Negative lookahead `(?!\s*(?:assert|with)\s*\{)` keeps this from matching
|
|
13
|
+
// import-attributes/assertion imports — those go through ASSERTION_IMPORT_REGEX
|
|
14
|
+
// and would otherwise be double-counted in `getImports`.
|
|
15
|
+
const IMPORT_REGEX = /import(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s]+([^"'`\n;\s]+)\s*["'](?!\s*(?:assert|with)\s*\{)/gm
|
|
5
16
|
// https://regex101.com/r/UDmXlb/1
|
|
6
17
|
const IMPORT_TYPE_REGEX = /import\(['"](.*)['"]\)\.?([A-Za-z_0-9.]*)?/gm
|
|
7
18
|
// https://regex101.com/r/UDjMoV/1
|
|
8
|
-
|
|
19
|
+
// `await` is optional so `import('./foo').then(...)`, `const p = import('./foo')`,
|
|
20
|
+
// `return import('./foo')`, and `lazy(() => import('./foo'))` all match.
|
|
21
|
+
// `(?<!\.)\b` rejects member access (`obj.import(...)`) and identifiers like
|
|
22
|
+
// `myimport(...)`.
|
|
23
|
+
const DYNAMIC_IMPORT_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)?(?:await\s+)?(?<!\.)\bimport\(([^()]+(?:\([^()]*\)[^()]*)*)\)/gm
|
|
9
24
|
// https://regex101.com/r/example/1
|
|
10
|
-
const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, ]+)from\s*)?["'\s](
|
|
25
|
+
const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s]+([^"'`\n;\s]+)\s*["']\s+(assert|with)\s*\{([^}]+)\}/gm
|
|
11
26
|
|
|
12
27
|
function getImports(code = '', options = {}) {
|
|
13
28
|
// Strip comments to avoid false positives
|
|
@@ -26,8 +41,23 @@ function getStaticImports(targetText = '') {
|
|
|
26
41
|
let importStatements = []
|
|
27
42
|
var result;
|
|
28
43
|
while((result = IMPORT_REGEX.exec(targetText)) !== null) {
|
|
29
|
-
const [ line, names,
|
|
30
|
-
|
|
44
|
+
const [ line, names, rawFrom ] = result
|
|
45
|
+
// Side-effect imports (e.g. `import './foo.js'`) and unnamed-default imports skip the
|
|
46
|
+
// optional `from` capture, so the opening quote ends up inside the path group. Strip it.
|
|
47
|
+
const from = rawFrom.replace(/^["'`]|["'`]$/g, '')
|
|
48
|
+
// Skip TypeScript type-only imports — erased at compile time, no runtime resolution needed
|
|
49
|
+
if (/^\s*import\s+type\s/.test(line)) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
// Skip false matches where `import` appears inside a string literal (e.g. `= 'import' | 'internal'`)
|
|
53
|
+
// A valid module path won't contain operators like | = ( )
|
|
54
|
+
if (/[|=()]/.test(from)) {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
// Remove inline `type` imports from destructured names — they're erased at compile time
|
|
58
|
+
// e.g. `{ foo, type Bar, type Baz }` → `{ foo }` (Bar and Baz are type-only)
|
|
59
|
+
const rawNames = (names || '').trim()
|
|
60
|
+
const namesValues = rawNames.replace(/,?\s*\btype\s+[\w$]+/g, '').replace(/^,\s*/, '').trim()
|
|
31
61
|
importStatements.push({
|
|
32
62
|
line,
|
|
33
63
|
varType: 'const',
|
|
@@ -67,19 +97,21 @@ function getDynamicImports(targetText = '') {
|
|
|
67
97
|
let importStatements = []
|
|
68
98
|
var result;
|
|
69
99
|
while((result = DYNAMIC_IMPORT_REGEX.exec(targetText)) !== null) {
|
|
70
|
-
const [ line, varType, names,
|
|
100
|
+
const [ line, varType, names, rawFrom ] = result
|
|
71
101
|
let namesValues = (names || '').trim()
|
|
72
|
-
|
|
102
|
+
// Strip surrounding quotes from dynamic import paths (e.g. './foo.js' → ./foo.js)
|
|
103
|
+
const from = rawFrom.trim().replace(/^["'`]|["'`]$/g, '')
|
|
104
|
+
|
|
73
105
|
importStatements.push({
|
|
74
106
|
line,
|
|
75
107
|
varType: varType || '',
|
|
76
108
|
varName: namesValues,
|
|
77
109
|
definitions: names ? getNames(namesValues) : null,
|
|
78
|
-
from
|
|
110
|
+
from,
|
|
79
111
|
index: result.index,
|
|
80
112
|
isImport: true,
|
|
81
|
-
isRelative: isRelativeImport(from)
|
|
82
|
-
isBuiltIn: isBuiltIn(from
|
|
113
|
+
isRelative: isRelativeImport(from),
|
|
114
|
+
isBuiltIn: isBuiltIn(from),
|
|
83
115
|
isDynamic: true
|
|
84
116
|
})
|
|
85
117
|
}
|
|
@@ -90,7 +122,8 @@ function getAssertionImports(targetText = '') {
|
|
|
90
122
|
let importStatements = []
|
|
91
123
|
var result;
|
|
92
124
|
while((result = ASSERTION_IMPORT_REGEX.exec(targetText)) !== null) {
|
|
93
|
-
let [ line, names,
|
|
125
|
+
let [ line, names, rawFrom, keyword, assertion ] = result
|
|
126
|
+
const from = rawFrom.replace(/^["'`]|["'`]$/g, '')
|
|
94
127
|
const namesValues = (names || '').trim()
|
|
95
128
|
// Remove leading newlines and whitespace from the line
|
|
96
129
|
line = line.replace(/^\s+|\s+$/g, '')
|
package/src/get-requires.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const { isBuiltIn, isRelativeImport, getNames } = 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
|
|
5
|
+
// on any const/let/var line containing string literals (e.g. `const x = arr.filter(e => e.type === 'foo')`)
|
|
6
|
+
const REQUIRE_REGEX = /(?!.*await\s+import)(?:(const|var|let)\s+([\w*{}:=\n\r\t,:= $]+)\s*=\s*)?require\(\s*["'](.*[@\w_-]+)["']\s*\);?$/gm
|
|
5
7
|
// https://regex101.com/r/kZ5xCk/3
|
|
6
|
-
const DYNAMIC_REQUIRE_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= ]+)\s*=\s*)?require\(([^()'"]+(?:\([^()]*\)[^()'"]*)*)\)/gm
|
|
8
|
+
const DYNAMIC_REQUIRE_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)?require\(([^()'"]+(?:\([^()]*\)[^()'"]*)*)\)/gm
|
|
7
9
|
|
|
8
10
|
function getDynamicRequires(code) {
|
|
9
11
|
const results = []
|
|
@@ -41,9 +43,9 @@ function getStaticRequires(targetText = '') {
|
|
|
41
43
|
let requireStatements = []
|
|
42
44
|
var result;
|
|
43
45
|
while((result = REQUIRE_REGEX.exec(targetText)) !== null) {
|
|
44
|
-
const [ line, varType, names, from
|
|
46
|
+
const [ line, varType, names, from ] = result
|
|
45
47
|
// console.log('getStaticRequires result', result)
|
|
46
|
-
const importLocation = from
|
|
48
|
+
const importLocation = from
|
|
47
49
|
const importItems = (names || '').trim()
|
|
48
50
|
requireStatements.push({
|
|
49
51
|
line,
|
package/tests/dep-graph.test.js
CHANGED
|
@@ -550,4 +550,94 @@ test('verifyNodeModules with returnErrors should collect export mismatch errors'
|
|
|
550
550
|
assert.ok(result.errors[0].message.includes('does not have a default export'), 'Should be default export error')
|
|
551
551
|
})
|
|
552
552
|
|
|
553
|
+
// Re-export (export * from) tests
|
|
554
|
+
const ESM_REEXPORT_FIXTURE = path.resolve(__dirname, './fixtures/esm-reexport-fixture/index.js')
|
|
555
|
+
|
|
556
|
+
test('import from barrel with export * should not fail verification', () => {
|
|
557
|
+
assert.not.throws(() => {
|
|
558
|
+
depGraph(ESM_REEXPORT_FIXTURE, { verifyGraph: true })
|
|
559
|
+
}, 'Importing named exports through export * barrel should not fail')
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
test('import from barrel with export * should build valid graph', () => {
|
|
563
|
+
const graph = depGraph(ESM_REEXPORT_FIXTURE, { verifyGraph: true })
|
|
564
|
+
|
|
565
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
566
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-reexport-fixture/index.js'))
|
|
567
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
568
|
+
|
|
569
|
+
const deps = graph.get(indexPath)
|
|
570
|
+
assert.ok(deps.some(d => d.includes('barrel.js')), 'Should have barrel.js as dependency')
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// Namespace import (import * as X) tests
|
|
574
|
+
const ESM_NAMESPACE_FIXTURE = path.resolve(__dirname, './fixtures/esm-namespace-fixture/index.js')
|
|
575
|
+
|
|
576
|
+
test('namespace import (import * as X) should not require default export', () => {
|
|
577
|
+
assert.not.throws(() => {
|
|
578
|
+
depGraph(ESM_NAMESPACE_FIXTURE, { verifyGraph: true })
|
|
579
|
+
}, 'import * as X should not fail when target has no default export')
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('namespace import should build valid graph', () => {
|
|
583
|
+
const graph = depGraph(ESM_NAMESPACE_FIXTURE, { verifyGraph: true })
|
|
584
|
+
|
|
585
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
586
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
587
|
+
|
|
588
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-namespace-fixture/index.js'))
|
|
589
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
590
|
+
|
|
591
|
+
const deps = graph.get(indexPath)
|
|
592
|
+
assert.ok(deps.some(d => d.includes('utils.js')), 'Should have utils.js as dependency')
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// TypeScript patterns fixture — exercises all fixed false positive patterns
|
|
596
|
+
const TS_PATTERNS_FIXTURE = path.resolve(__dirname, './fixtures/ts-patterns-fixture/index.ts')
|
|
597
|
+
|
|
598
|
+
test('TS patterns: import type, inline type, import * as, $-prefixed names should all pass', () => {
|
|
599
|
+
// This fixture contains:
|
|
600
|
+
// - import type { Context } from 'hono' (should be skipped)
|
|
601
|
+
// - import { helper, type HelperOptions } from './utils.js' (inline type stripped)
|
|
602
|
+
// - import * as Constants from './constants.js' (namespace import, no default needed)
|
|
603
|
+
// - import { $transform } from './transforms.js' ($ in name)
|
|
604
|
+
// - export type = 'import' | 'internal' (type literal, not an import)
|
|
605
|
+
// - c.req.valid('param'), .filter(), .some(), c.req.header() (not requires)
|
|
606
|
+
assert.not.throws(() => {
|
|
607
|
+
depGraph(TS_PATTERNS_FIXTURE, { verifyGraph: true })
|
|
608
|
+
}, 'All TS patterns should pass verification without errors')
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('TS patterns: should build correct dependency graph', () => {
|
|
612
|
+
const graph = depGraph(TS_PATTERNS_FIXTURE, { verifyGraph: true })
|
|
613
|
+
|
|
614
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
615
|
+
|
|
616
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('ts-patterns-fixture/index.ts'))
|
|
617
|
+
assert.ok(indexPath, 'Should have index.ts in graph')
|
|
618
|
+
|
|
619
|
+
const deps = graph.get(indexPath)
|
|
620
|
+
// Should have utils.ts, constants.ts, transforms.ts as deps
|
|
621
|
+
assert.ok(deps.some(d => d.includes('utils.ts')), 'Should depend on utils.ts')
|
|
622
|
+
assert.ok(deps.some(d => d.includes('constants.ts')), 'Should depend on constants.ts')
|
|
623
|
+
assert.ok(deps.some(d => d.includes('transforms.ts')), 'Should depend on transforms.ts')
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
test('TS patterns: resolveDepPaths should not include import type packages', () => {
|
|
627
|
+
const { nodeModules } = resolveDepPaths(TS_PATTERNS_FIXTURE)
|
|
628
|
+
|
|
629
|
+
// 'hono' was import type — should NOT appear in nodeModules
|
|
630
|
+
assert.ok(!nodeModules.includes('hono'), 'import type packages should not appear in nodeModules')
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
test('TS patterns: resolveDepPaths with returnErrors should have zero errors', () => {
|
|
634
|
+
const result = resolveDepPaths(TS_PATTERNS_FIXTURE, { returnErrors: true })
|
|
635
|
+
|
|
636
|
+
assert.ok(Array.isArray(result.errors), 'Should have errors array')
|
|
637
|
+
if (result.errors.length > 0) {
|
|
638
|
+
console.log('Unexpected errors:', result.errors.map(e => e.message))
|
|
639
|
+
}
|
|
640
|
+
assert.is(result.errors.length, 0, 'Should have zero verification errors')
|
|
641
|
+
})
|
|
642
|
+
|
|
553
643
|
test.run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './impl.js'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// This fixture exercises TypeScript patterns that previously caused false positives
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
import { helper, type HelperOptions } from './utils.js'
|
|
4
|
+
import * as Constants from './constants.js'
|
|
5
|
+
import { $transform } from './transforms.js'
|
|
6
|
+
|
|
7
|
+
export type EnrollmentSource = 'import' | 'internal' | 'manual'
|
|
8
|
+
|
|
9
|
+
export async function handler(c: Context) {
|
|
10
|
+
const { email } = c.req.valid('param')
|
|
11
|
+
const active = items.filter((e) => e.status === 'active')
|
|
12
|
+
const replied = timeline.some((e) => e.type === 'reply' || e.type === 'stop')
|
|
13
|
+
const key = c.req.header('Idempotency-Key')
|
|
14
|
+
|
|
15
|
+
const result = helper({ name: Constants.DEFAULT_NAME })
|
|
16
|
+
return $transform(result)
|
|
17
|
+
}
|
|
@@ -759,4 +759,96 @@ Object.defineProperty(exports, 'version', {
|
|
|
759
759
|
}
|
|
760
760
|
})
|
|
761
761
|
|
|
762
|
+
test('detects Rollup-style Object.keys(X).forEach re-export-all', () => {
|
|
763
|
+
// Matches the pattern emitted by Rollup (and used in AWS SDK v3 dist-cjs)
|
|
764
|
+
// for ESM `export * from './x'` when targeting CJS.
|
|
765
|
+
const code = `
|
|
766
|
+
var errors = require('./models/errors');
|
|
767
|
+
Object.keys(errors).forEach(function (k) {
|
|
768
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) exports[k] = errors[k];
|
|
769
|
+
});
|
|
770
|
+
exports.realNamed = realNamed;
|
|
771
|
+
`
|
|
772
|
+
|
|
773
|
+
const found = getCjsExports(code)
|
|
774
|
+
const reExports = found.filter(e => e.exportType === 'all')
|
|
775
|
+
assert.equal(reExports.length, 1, 'Should detect 1 dynamic re-export-all')
|
|
776
|
+
assert.equal(reExports[0].from, 'errors', 'Should capture the source variable name')
|
|
777
|
+
assert.is(reExports[0].isReExportAll, true, 'Should set isReExportAll flag')
|
|
778
|
+
|
|
779
|
+
// The explicit named export must still be picked up alongside
|
|
780
|
+
assert.ok(found.some(e => e.exportType === 'named' && e.exportName === 'realNamed'))
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
test('detects tsc/tslib __exportStar re-export-all', () => {
|
|
784
|
+
const code = `
|
|
785
|
+
var tslib_1 = require("tslib");
|
|
786
|
+
__exportStar(require("./module-a"), exports);
|
|
787
|
+
tslib_1.__exportStar(require("./module-b"), exports);
|
|
788
|
+
var moduleC = require("./module-c");
|
|
789
|
+
__exportStar(moduleC, exports);
|
|
790
|
+
`
|
|
791
|
+
|
|
792
|
+
const found = getCjsExports(code)
|
|
793
|
+
const reExports = found.filter(e => e.exportType === 'all')
|
|
794
|
+
assert.equal(reExports.length, 3, 'Should detect 3 __exportStar calls')
|
|
795
|
+
|
|
796
|
+
const fromValues = reExports.map(e => e.from).sort()
|
|
797
|
+
assert.equal(fromValues, ['./module-a', './module-b', 'moduleC'].sort())
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
test('detects esbuild __reExport re-export-all', () => {
|
|
801
|
+
const code = `
|
|
802
|
+
var stdin_exports = {};
|
|
803
|
+
__export(stdin_exports, { foo: () => foo });
|
|
804
|
+
__reExport(stdin_exports, require("./module-a"));
|
|
805
|
+
var moduleB = require("./module-b");
|
|
806
|
+
__reExport(stdin_exports, moduleB);
|
|
807
|
+
`
|
|
808
|
+
|
|
809
|
+
const found = getCjsExports(code)
|
|
810
|
+
const reExports = found.filter(e => e.exportType === 'all')
|
|
811
|
+
assert.equal(reExports.length, 2, 'Should detect 2 __reExport calls')
|
|
812
|
+
|
|
813
|
+
const fromValues = reExports.map(e => e.from).sort()
|
|
814
|
+
assert.equal(fromValues, ['./module-a', 'moduleB'].sort())
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
test('re-export-all patterns coexist with explicit named exports', () => {
|
|
818
|
+
// A real-world file mixes both — make sure both end up in moduleExports.
|
|
819
|
+
const code = `
|
|
820
|
+
var errors = require('./errors');
|
|
821
|
+
exports.Foo = Foo;
|
|
822
|
+
exports.Bar = Bar;
|
|
823
|
+
Object.keys(errors).forEach(function (k) {
|
|
824
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) exports[k] = errors[k];
|
|
825
|
+
});
|
|
826
|
+
exports.Baz = Baz;
|
|
827
|
+
`
|
|
828
|
+
|
|
829
|
+
const found = getCjsExports(code)
|
|
830
|
+
const named = found.filter(e => e.exportType === 'named').map(e => e.exportName).sort()
|
|
831
|
+
assert.equal(named, ['Bar', 'Baz', 'Foo'])
|
|
832
|
+
|
|
833
|
+
const reExports = found.filter(e => e.exportType === 'all')
|
|
834
|
+
assert.equal(reExports.length, 1)
|
|
835
|
+
assert.equal(reExports[0].from, 'errors')
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
test('does NOT flag unrelated Object.keys / forEach calls as re-export-all', () => {
|
|
839
|
+
// Only the specific `exports[k] = X[k]` shape should trigger detection.
|
|
840
|
+
const code = `
|
|
841
|
+
Object.keys(someObj).forEach(function (k) {
|
|
842
|
+
console.log(k, someObj[k]);
|
|
843
|
+
});
|
|
844
|
+
Object.keys(config).forEach((k) => {
|
|
845
|
+
process.env[k] = config[k];
|
|
846
|
+
});
|
|
847
|
+
`
|
|
848
|
+
|
|
849
|
+
const found = getCjsExports(code)
|
|
850
|
+
const reExports = found.filter(e => e.exportType === 'all')
|
|
851
|
+
assert.equal(reExports.length, 0, 'Should not match Object.keys patterns that do not assign to exports')
|
|
852
|
+
})
|
|
853
|
+
|
|
762
854
|
test.run()
|
|
@@ -170,7 +170,7 @@ test('getDynamicImports', async () => {
|
|
|
170
170
|
varType: 'const',
|
|
171
171
|
varName: 'module',
|
|
172
172
|
definitions: { declaration: 'module', name: 'module' },
|
|
173
|
-
from:
|
|
173
|
+
from: './module.js',
|
|
174
174
|
isImport: true,
|
|
175
175
|
isRelative: true,
|
|
176
176
|
isBuiltIn: false,
|
|
@@ -192,9 +192,9 @@ test('getDynamicImports', async () => {
|
|
|
192
192
|
varType: 'const',
|
|
193
193
|
varName: 'component',
|
|
194
194
|
definitions: { declaration: 'component', name: 'component' },
|
|
195
|
-
from: '
|
|
195
|
+
from: './src/${folder}/Button.js',
|
|
196
196
|
isImport: true,
|
|
197
|
-
isRelative:
|
|
197
|
+
isRelative: true,
|
|
198
198
|
isBuiltIn: false,
|
|
199
199
|
isDynamic: true
|
|
200
200
|
},
|
|
@@ -226,7 +226,7 @@ test('getDynamicImports', async () => {
|
|
|
226
226
|
],
|
|
227
227
|
name: '{ default: Component, helper }'
|
|
228
228
|
},
|
|
229
|
-
from:
|
|
229
|
+
from: './Component.js',
|
|
230
230
|
isImport: true,
|
|
231
231
|
isRelative: true,
|
|
232
232
|
isBuiltIn: false,
|
|
@@ -428,6 +428,103 @@ import realImport from 'real'
|
|
|
428
428
|
assert.equal(importsFound[0].from, 'real', 'Should find real import')
|
|
429
429
|
})
|
|
430
430
|
|
|
431
|
+
test('should handle $ in import names', () => {
|
|
432
|
+
const code = `
|
|
433
|
+
import { $add } from 'dynamodb-toolbox/entity/actions/update'
|
|
434
|
+
import { UpdateItemCommand, $add } from 'dynamodb-toolbox/entity/actions/update'
|
|
435
|
+
import { $remove } from 'dynamodb-toolbox/entity/actions/update'
|
|
436
|
+
`
|
|
437
|
+
|
|
438
|
+
const importsFound = getImports(code)
|
|
439
|
+
|
|
440
|
+
assert.equal(importsFound.length, 3, 'Should find 3 imports')
|
|
441
|
+
assert.equal(importsFound[0].from, 'dynamodb-toolbox/entity/actions/update')
|
|
442
|
+
assert.equal(importsFound[0].varName, '{ $add }')
|
|
443
|
+
assert.equal(importsFound[1].varName, '{ UpdateItemCommand, $add }')
|
|
444
|
+
assert.equal(importsFound[2].varName, '{ $remove }')
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('dynamic import should strip quotes from path', () => {
|
|
448
|
+
const code = `
|
|
449
|
+
const { findContactEmail } = await import('./sfdc-reverse-lookup.js')
|
|
450
|
+
const { invokeAsync } = await import('../../lib/invoke-async.js')
|
|
451
|
+
const { SNSClient } = await import('@aws-sdk/client-sns')
|
|
452
|
+
`
|
|
453
|
+
|
|
454
|
+
const importsFound = getDynamicImports(code)
|
|
455
|
+
|
|
456
|
+
assert.equal(importsFound.length, 3, 'Should find 3 dynamic imports')
|
|
457
|
+
assert.equal(importsFound[0].from, './sfdc-reverse-lookup.js', 'Should strip quotes from relative path')
|
|
458
|
+
assert.is(importsFound[0].isRelative, true, 'Should detect as relative')
|
|
459
|
+
assert.equal(importsFound[1].from, '../../lib/invoke-async.js', 'Should strip quotes from relative path')
|
|
460
|
+
assert.is(importsFound[1].isRelative, true, 'Should detect as relative')
|
|
461
|
+
assert.equal(importsFound[2].from, '@aws-sdk/client-sns', 'Should strip quotes from package path')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
test('should skip type literal unions containing import keyword', () => {
|
|
465
|
+
const code = `
|
|
466
|
+
export type EnrollmentSource = 'import' | 'internal' | 'manual'
|
|
467
|
+
export type ActionType = 'import' | 'export'
|
|
468
|
+
const x: 'import' | 'require' = getType()
|
|
469
|
+
`
|
|
470
|
+
|
|
471
|
+
const importsFound = getImports(code)
|
|
472
|
+
assert.equal(importsFound.length, 0, 'Should not match type literal unions')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('should remove inline type-only names from destructured imports', () => {
|
|
476
|
+
const code = `
|
|
477
|
+
import { listReviewActions, type ReviewActionChannel, type ReviewActionStatus } from '../lib/action-review.js'
|
|
478
|
+
import { normalizeWorkflowNodes, type WorkflowNode } from '../../lib/workflow-node.js'
|
|
479
|
+
import { type Foo, bar, type Baz } from 'some-pkg'
|
|
480
|
+
`
|
|
481
|
+
|
|
482
|
+
const importsFound = getImports(code)
|
|
483
|
+
assert.equal(importsFound.length, 3, 'Should find 3 imports')
|
|
484
|
+
|
|
485
|
+
// Type-only names should be removed entirely (not just renamed)
|
|
486
|
+
const firstDefs = importsFound[0].definitions.destructuredParameters.map(p => p.name)
|
|
487
|
+
assert.equal(firstDefs.length, 1, 'Should only have 1 runtime import')
|
|
488
|
+
assert.ok(firstDefs.includes('listReviewActions'), 'Should keep listReviewActions')
|
|
489
|
+
assert.ok(!firstDefs.includes('ReviewActionChannel'), 'type ReviewActionChannel should be removed')
|
|
490
|
+
assert.ok(!firstDefs.includes('ReviewActionStatus'), 'type ReviewActionStatus should be removed')
|
|
491
|
+
|
|
492
|
+
const secondDefs = importsFound[1].definitions.destructuredParameters.map(p => p.name)
|
|
493
|
+
assert.equal(secondDefs.length, 1, 'Should only have 1 runtime import')
|
|
494
|
+
assert.ok(secondDefs.includes('normalizeWorkflowNodes'), 'Should keep normalizeWorkflowNodes')
|
|
495
|
+
assert.ok(!secondDefs.includes('WorkflowNode'), 'type WorkflowNode should be removed')
|
|
496
|
+
|
|
497
|
+
// Third: type Foo and type Baz removed, only bar remains
|
|
498
|
+
const thirdDefs = importsFound[2].definitions.destructuredParameters.map(p => p.name)
|
|
499
|
+
assert.equal(thirdDefs.length, 1, 'Should only have 1 runtime import')
|
|
500
|
+
assert.ok(thirdDefs.includes('bar'), 'Should keep bar')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
test('should handle all-type destructured imports', () => {
|
|
504
|
+
const code = `import { type Foo, type Bar } from 'pkg'`
|
|
505
|
+
|
|
506
|
+
const importsFound = getImports(code)
|
|
507
|
+
assert.equal(importsFound.length, 1, 'Should still find the import statement')
|
|
508
|
+
// No runtime names to verify — definitions may be empty
|
|
509
|
+
const defs = importsFound[0].definitions?.destructuredParameters || []
|
|
510
|
+
const runtimeNames = defs.filter(p => !p.name.startsWith('type '))
|
|
511
|
+
assert.equal(runtimeNames.length, 0, 'Should have no runtime import names')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
test('should skip TypeScript import type statements', () => {
|
|
515
|
+
const code = `
|
|
516
|
+
import type { Context } from 'hono'
|
|
517
|
+
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
|
|
518
|
+
import type { WsEventType } from './ws-events.js'
|
|
519
|
+
import { realImport } from 'real-package'
|
|
520
|
+
`
|
|
521
|
+
|
|
522
|
+
const importsFound = getImports(code)
|
|
523
|
+
|
|
524
|
+
assert.equal(importsFound.length, 1, 'Should only find 1 real import')
|
|
525
|
+
assert.equal(importsFound[0].from, 'real-package', 'Should find real-package import')
|
|
526
|
+
})
|
|
527
|
+
|
|
431
528
|
test('should preserve URLs with :// in strings', () => {
|
|
432
529
|
const code = `
|
|
433
530
|
import foo from 'foo' // http://example.com
|
|
@@ -439,4 +536,143 @@ import foo from 'foo' // http://example.com
|
|
|
439
536
|
assert.equal(importsFound[0].from, 'foo', 'Should find foo import')
|
|
440
537
|
})
|
|
441
538
|
|
|
539
|
+
test('side-effect imports strip surrounding quotes from `from`', () => {
|
|
540
|
+
// Regression: before the fix, the static-import regex captured the opening quote
|
|
541
|
+
// inside the path group for side-effect imports (no `from` keyword), producing
|
|
542
|
+
// values like `'./foo.js` which broke isRelative / isBuiltIn classification.
|
|
543
|
+
const code = `
|
|
544
|
+
import './message/email/gmail/index.js'
|
|
545
|
+
import "./message/in-app/re-notifications/index.js"
|
|
546
|
+
import 'side-effect-package'
|
|
547
|
+
import "node:fs"
|
|
548
|
+
`
|
|
549
|
+
|
|
550
|
+
const importsFound = normalizeTestResult(getStaticImports(code))
|
|
551
|
+
|
|
552
|
+
assert.equal(importsFound.length, 4, 'Should find 4 side-effect imports')
|
|
553
|
+
|
|
554
|
+
assert.equal(importsFound[0].from, './message/email/gmail/index.js')
|
|
555
|
+
assert.is(importsFound[0].isRelative, true, 'single-quoted relative path is relative')
|
|
556
|
+
assert.is(importsFound[0].isBuiltIn, false)
|
|
557
|
+
|
|
558
|
+
assert.equal(importsFound[1].from, './message/in-app/re-notifications/index.js')
|
|
559
|
+
assert.is(importsFound[1].isRelative, true, 'double-quoted relative path is relative')
|
|
560
|
+
|
|
561
|
+
assert.equal(importsFound[2].from, 'side-effect-package')
|
|
562
|
+
assert.is(importsFound[2].isRelative, false)
|
|
563
|
+
assert.is(importsFound[2].isBuiltIn, false)
|
|
564
|
+
|
|
565
|
+
assert.equal(importsFound[3].from, 'node:fs')
|
|
566
|
+
assert.is(importsFound[3].isBuiltIn, true, 'node: builtins detected after quote strip')
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
test('side-effect imports preserve definitions/varName semantics', () => {
|
|
570
|
+
const code = `import './foo.js'`
|
|
571
|
+
const [imp] = getStaticImports(code)
|
|
572
|
+
|
|
573
|
+
assert.equal(imp.from, './foo.js')
|
|
574
|
+
assert.is(imp.isRelative, true)
|
|
575
|
+
assert.is(imp.isImport, true)
|
|
576
|
+
assert.equal(imp.varName, '', 'side-effect imports have no bound names')
|
|
577
|
+
assert.is(imp.definitions, null, 'side-effect imports have null definitions')
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('multiple static imports on a single line', () => {
|
|
581
|
+
// Regression: the `(.*[@\w_-]+)["'\s]+;?$` pattern was greedy plus EOL-anchored,
|
|
582
|
+
// so a line like `import a from 'a'; import b from 'b';` matched as ONE import
|
|
583
|
+
// whose path was `a'; import b from 'b`. Tightening the path char-class and
|
|
584
|
+
// dropping the `$` anchor turns this into separate matches.
|
|
585
|
+
const code = `import a from 'a'; import b from 'b'; import c from 'c';`
|
|
586
|
+
const importsFound = normalizeTestResult(getStaticImports(code))
|
|
587
|
+
|
|
588
|
+
assert.equal(importsFound.length, 3, 'Should find 3 imports on one line')
|
|
589
|
+
assert.equal(importsFound[0].from, 'a')
|
|
590
|
+
assert.equal(importsFound[1].from, 'b')
|
|
591
|
+
assert.equal(importsFound[2].from, 'c')
|
|
592
|
+
|
|
593
|
+
// Verify names parsed independently too
|
|
594
|
+
assert.equal(importsFound[0].varName, 'a')
|
|
595
|
+
assert.equal(importsFound[1].varName, 'b')
|
|
596
|
+
assert.equal(importsFound[2].varName, 'c')
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
test('static + assertion imports on the same line do not double-count', () => {
|
|
600
|
+
// The static IMPORT_REGEX uses a negative lookahead for `assert {` / `with {`
|
|
601
|
+
// so assertion imports flow only through ASSERTION_IMPORT_REGEX instead of
|
|
602
|
+
// being matched by both.
|
|
603
|
+
const code = `
|
|
604
|
+
import foo from 'fs'
|
|
605
|
+
import data from './data.json' assert { type: 'json' }
|
|
606
|
+
import styles from './styles.css' with { type: 'css' }
|
|
607
|
+
`
|
|
608
|
+
|
|
609
|
+
const all = normalizeTestResult(getImports(code, { includeAssertionImports: true }))
|
|
610
|
+
assert.equal(all.length, 3, 'Should not double-count assertion imports')
|
|
611
|
+
|
|
612
|
+
// Static-only call must also skip the assertion lines (no partial path matches)
|
|
613
|
+
const staticOnly = normalizeTestResult(getStaticImports(code))
|
|
614
|
+
assert.equal(staticOnly.length, 1, 'Static regex should skip assertion-import lines')
|
|
615
|
+
assert.equal(staticOnly[0].from, 'fs')
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
test('dynamic imports without await are detected', () => {
|
|
619
|
+
// Regression: DYNAMIC_IMPORT_REGEX required `await\s+import(`, missing
|
|
620
|
+
// every lazy-load pattern. `await` is now optional.
|
|
621
|
+
const code = `
|
|
622
|
+
import('./then-chain.js').then(m => m)
|
|
623
|
+
const eager = import('./eager.js')
|
|
624
|
+
return import('./return.js')
|
|
625
|
+
const Comp = lazy(() => import('./Component.js'))
|
|
626
|
+
const [a, b] = await Promise.all([import('./a.js'), import('./b.js')])
|
|
627
|
+
`
|
|
628
|
+
|
|
629
|
+
const importsFound = normalizeTestResult(getDynamicImports(code))
|
|
630
|
+
const froms = importsFound.map(i => i.from).sort()
|
|
631
|
+
|
|
632
|
+
assert.equal(importsFound.length, 6, 'Should find 6 dynamic imports')
|
|
633
|
+
assert.equal(froms, [
|
|
634
|
+
'./Component.js',
|
|
635
|
+
'./a.js',
|
|
636
|
+
'./b.js',
|
|
637
|
+
'./eager.js',
|
|
638
|
+
'./return.js',
|
|
639
|
+
'./then-chain.js',
|
|
640
|
+
].sort())
|
|
641
|
+
|
|
642
|
+
// All flagged as dynamic
|
|
643
|
+
assert.ok(importsFound.every(i => i.isDynamic), 'All should have isDynamic: true')
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
test('dynamic-import regex rejects member access and identifier prefixes', () => {
|
|
647
|
+
// `obj.import('x')` and `myimport('x')` are not dynamic imports — the
|
|
648
|
+
// `(?<!\.)\b` guard prevents these from matching now that `await` is optional.
|
|
649
|
+
const code = `
|
|
650
|
+
obj.import('./not-an-import.js')
|
|
651
|
+
myimport('./also-not.js')
|
|
652
|
+
const u = import.meta.url
|
|
653
|
+
const real = import('./real.js')
|
|
654
|
+
`
|
|
655
|
+
|
|
656
|
+
const importsFound = getDynamicImports(code)
|
|
657
|
+
assert.equal(importsFound.length, 1, 'Should only match the real dynamic import')
|
|
658
|
+
assert.equal(importsFound[0].from, './real.js')
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('assertion imports strip surrounding quotes from `from`', () => {
|
|
662
|
+
// Same class of bug as static imports — the assertion regex captured the
|
|
663
|
+
// opening quote into the path group when the assertion was wrapped tightly.
|
|
664
|
+
const code = `
|
|
665
|
+
import data from './data.json' assert { type: 'json' }
|
|
666
|
+
import sheet from "./sheet.css" with { type: 'css' }
|
|
667
|
+
`
|
|
668
|
+
|
|
669
|
+
const importsFound = normalizeTestResult(getAssertionImports(code))
|
|
670
|
+
|
|
671
|
+
assert.equal(importsFound.length, 2)
|
|
672
|
+
assert.equal(importsFound[0].from, './data.json')
|
|
673
|
+
assert.is(importsFound[0].isRelative, true)
|
|
674
|
+
assert.equal(importsFound[1].from, './sheet.css')
|
|
675
|
+
assert.is(importsFound[1].isRelative, true)
|
|
676
|
+
})
|
|
677
|
+
|
|
442
678
|
test.run()
|
|
@@ -208,4 +208,64 @@ test('getRequires with dynamic requires', async () => {
|
|
|
208
208
|
])
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
+
test('should not match non-require patterns with string literals', async () => {
|
|
212
|
+
const code = `
|
|
213
|
+
const { email: bdrEmail } = c.req.valid('param')
|
|
214
|
+
const activeBdrs = allActive.filter((u) => u.email !== 'CONFIG')
|
|
215
|
+
const connections = await Connection.getByMember(bdr.email, 'gmail')
|
|
216
|
+
const replied = timeline.some((e) => e.type === 'reply' || e.type === 'stop')
|
|
217
|
+
const lastSent = timeline.find((e) => e.type === 'email_sent')
|
|
218
|
+
const key = c.req.header('Idempotency-Key')
|
|
219
|
+
const { enabled, endDate, handoff } = c.req.valid('json')
|
|
220
|
+
const active = enrollmentItems.filter((e) => e.status === 'active')
|
|
221
|
+
const { limit } = c.req.valid('query')
|
|
222
|
+
const vacationPaused = enrollmentItems.filter((e) => e.status === 'paused' && e.pauseReason === 'vacation')
|
|
223
|
+
const activeEnrollments = enrollmentItems.filter((e) => e.status === 'active' || e.status === 'pending_review')
|
|
224
|
+
`
|
|
225
|
+
|
|
226
|
+
const requiresFound = getRequires(code)
|
|
227
|
+
assert.equal(requiresFound.length, 0, 'Should not match any non-require patterns')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('should handle $ and _ in variable names', async () => {
|
|
231
|
+
const code = `
|
|
232
|
+
const { $add } = require('dynamodb-toolbox')
|
|
233
|
+
const $ref = require('reactive')
|
|
234
|
+
const _lodash = require('lodash')
|
|
235
|
+
const { _private, __double } = require('internal')
|
|
236
|
+
const { $$ref, _0x1234 } = require('edge-case')
|
|
237
|
+
`
|
|
238
|
+
|
|
239
|
+
const requiresFound = getRequires(code).filter(r => !r.isDynamic)
|
|
240
|
+
assert.equal(requiresFound.length, 5, 'Should find 5 requires with special chars')
|
|
241
|
+
assert.equal(requiresFound[0].from, 'dynamodb-toolbox')
|
|
242
|
+
assert.equal(requiresFound[0].varName, '{ $add }')
|
|
243
|
+
assert.equal(requiresFound[1].from, 'reactive')
|
|
244
|
+
assert.equal(requiresFound[1].varName, '$ref')
|
|
245
|
+
assert.equal(requiresFound[2].from, 'lodash')
|
|
246
|
+
assert.equal(requiresFound[2].varName, '_lodash')
|
|
247
|
+
assert.equal(requiresFound[3].from, 'internal')
|
|
248
|
+
assert.equal(requiresFound[3].varName, '{ _private, __double }')
|
|
249
|
+
assert.equal(requiresFound[4].from, 'edge-case')
|
|
250
|
+
assert.equal(requiresFound[4].varName, '{ $$ref, _0x1234 }')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('should still match real requires mixed with non-require code', async () => {
|
|
254
|
+
const code = `
|
|
255
|
+
const path = require('path')
|
|
256
|
+
const active = items.filter((e) => e.status === 'active')
|
|
257
|
+
const { foo, bar } = require('some-pkg')
|
|
258
|
+
const key = c.req.header('Idempotency-Key')
|
|
259
|
+
require('side-effect-only')
|
|
260
|
+
`
|
|
261
|
+
|
|
262
|
+
const requiresFound = getRequires(code)
|
|
263
|
+
// Should only match the 3 real requires, not the filter or req.header
|
|
264
|
+
const staticRequires = requiresFound.filter(r => !r.isDynamic)
|
|
265
|
+
assert.equal(staticRequires.length, 3, 'Should find exactly 3 real requires')
|
|
266
|
+
assert.equal(staticRequires[0].from, 'path')
|
|
267
|
+
assert.equal(staticRequires[1].from, 'some-pkg')
|
|
268
|
+
assert.equal(staticRequires[2].from, 'side-effect-only')
|
|
269
|
+
})
|
|
270
|
+
|
|
211
271
|
test.run()
|