@davidwells/extract-deps 0.1.13 → 0.1.15
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 +152 -37
- package/src/get-cjs-exports.js +7 -0
- package/src/get-exports.js +2 -3
- package/src/get-imports.js +5 -4
- package/src/get-requires.js +2 -3
- package/src/index.js +3 -1
- package/src/utils.js +21 -0
- package/tests/dep-graph-cycles.test.js +111 -0
- package/tests/dep-graph-json.test.js +79 -0
- package/tests/fixtures/cycle-fixtures/cycle-entry/index.js +2 -0
- package/tests/fixtures/cycle-fixtures/cycle-entry/leaf.js +1 -0
- package/tests/fixtures/cycle-fixtures/cycle-entry/peer.js +3 -0
- package/tests/fixtures/cycle-fixtures/diamond/a.js +2 -0
- package/tests/fixtures/cycle-fixtures/diamond/b.js +2 -0
- package/tests/fixtures/cycle-fixtures/diamond/c.js +2 -0
- package/tests/fixtures/cycle-fixtures/diamond/d.js +1 -0
- package/tests/fixtures/cycle-fixtures/diamond/index.js +3 -0
- package/tests/fixtures/cycle-fixtures/duplicate-edge/index.js +5 -0
- package/tests/fixtures/cycle-fixtures/duplicate-edge/leaf.js +1 -0
- package/tests/fixtures/cycle-fixtures/duplicate-edge/other.js +1 -0
- package/tests/fixtures/cycle-fixtures/duplicate-edge/util.js +2 -0
- package/tests/fixtures/cycle-fixtures/mixed-duplicate/base.mjs +1 -0
- package/tests/fixtures/cycle-fixtures/mixed-duplicate/helper.mjs +2 -0
- package/tests/fixtures/cycle-fixtures/mixed-duplicate/index.mjs +3 -0
- package/tests/fixtures/cycle-fixtures/self-import/index.js +2 -0
- package/tests/fixtures/cycle-fixtures/self-import/leaf.js +1 -0
- package/tests/fixtures/cycle-fixtures/self-import/self.js +3 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/index.js +3 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/leaf.js +1 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/w.js +2 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/x.js +2 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/y.js +2 -0
- package/tests/fixtures/cycle-fixtures/three-cycle/z.js +3 -0
- package/tests/fixtures/cycle-fixtures/two-cycle/a.js +2 -0
- package/tests/fixtures/cycle-fixtures/two-cycle/b.js +2 -0
- package/tests/fixtures/cycle-fixtures/two-cycle/index.js +2 -0
- package/types/dep-graph.d.ts +4 -1
- package/types/dep-graph.d.ts.map +1 -1
- package/types/get-cjs-exports.d.ts.map +1 -1
- package/types/get-exports.d.ts.map +1 -1
- package/types/get-imports.d.ts.map +1 -1
- package/types/get-requires.d.ts.map +1 -1
- package/types/utils.d.ts +8 -0
- package/types/utils.d.ts.map +1 -1
package/package.json
CHANGED
package/src/dep-graph.js
CHANGED
|
@@ -90,7 +90,10 @@ function fsExistsSync(path) {
|
|
|
90
90
|
|
|
91
91
|
function isFileSync(filePath) {
|
|
92
92
|
try {
|
|
93
|
-
|
|
93
|
+
// Most probes miss (each import tries several extensions): a missing path returns
|
|
94
|
+
// undefined instead of building and throwing an ENOENT error.
|
|
95
|
+
const stat = fs.statSync(filePath, { throwIfNoEntry: false })
|
|
96
|
+
return stat ? stat.isFile() : false
|
|
94
97
|
} catch (error) {
|
|
95
98
|
return false
|
|
96
99
|
}
|
|
@@ -161,6 +164,10 @@ function resolveImportPathUncached(baseDir, importPath) {
|
|
|
161
164
|
* @param {boolean} throwOnFirstError - Whether to throw immediately on first error
|
|
162
165
|
*/
|
|
163
166
|
function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, errors, throwOnFirstError) {
|
|
167
|
+
// JSON modules have no export statements to check: the parsed value IS the default export
|
|
168
|
+
// (ESM, `with { type: 'json' }`) or module.exports (CJS require). Scanning the JSON text
|
|
169
|
+
// for exports found none and reported a missing default export for `require('./x.json')`.
|
|
170
|
+
if (/\.json$/i.test(targetPath)) return
|
|
164
171
|
// Check if this is a destructured import
|
|
165
172
|
if (dep.definitions && dep.definitions.expectsDestructuring && dep.definitions.destructuredParameters) {
|
|
166
173
|
const importedNames = dep.definitions.destructuredParameters.map(p => p.name.replace(/,$/, ''))
|
|
@@ -448,7 +455,149 @@ function depGraph(filePath, options = {}) {
|
|
|
448
455
|
|
|
449
456
|
|
|
450
457
|
/**
|
|
451
|
-
*
|
|
458
|
+
* Order every file in the graph so dependencies come before dependents, each file once.
|
|
459
|
+
*
|
|
460
|
+
* Kahn's algorithm over the strongly connected components (Tarjan) of the file graph:
|
|
461
|
+
* an import cycle (or self-import) is one component, emitted as a unit with its members
|
|
462
|
+
* in discovery order. Duplicate edges (one file importing a module twice) count once.
|
|
463
|
+
* For an acyclic graph every component is a single file, and the queue seeding and
|
|
464
|
+
* dependent visiting orders match a plain per-file Kahn's sort over the graph's
|
|
465
|
+
* insertion order. O(V + E).
|
|
466
|
+
*
|
|
467
|
+
* @param {Map<string, string[]>} graph - file -> deps (file paths and package names)
|
|
468
|
+
* @returns {string[]}
|
|
469
|
+
*/
|
|
470
|
+
function buildOrder(graph) {
|
|
471
|
+
const nodes = Array.from(graph.keys())
|
|
472
|
+
const n = nodes.length
|
|
473
|
+
/** @type {Map<string, number>} */
|
|
474
|
+
const indexOf = new Map()
|
|
475
|
+
for (let i = 0; i < n; i++) indexOf.set(nodes[i], i)
|
|
476
|
+
|
|
477
|
+
// Unique file deps per node (first-occurrence order) and reverse edges. Dependents
|
|
478
|
+
// are appended while walking nodes in graph order, so each reverse list is in graph order.
|
|
479
|
+
/** @type {number[][]} */
|
|
480
|
+
const deps = new Array(n)
|
|
481
|
+
/** @type {number[][]} */
|
|
482
|
+
const dependents = new Array(n)
|
|
483
|
+
for (let i = 0; i < n; i++) dependents[i] = []
|
|
484
|
+
const seenStamp = new Int32Array(n).fill(-1)
|
|
485
|
+
for (let i = 0; i < n; i++) {
|
|
486
|
+
const out = []
|
|
487
|
+
for (const dep of graph.get(nodes[i])) {
|
|
488
|
+
const j = indexOf.get(dep) // package names (and unvisited paths) are not nodes
|
|
489
|
+
if (j === undefined || seenStamp[j] === i) continue
|
|
490
|
+
seenStamp[j] = i
|
|
491
|
+
out.push(j)
|
|
492
|
+
dependents[j].push(i)
|
|
493
|
+
}
|
|
494
|
+
deps[i] = out
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Tarjan's SCC, iterative (deep import chains must not overflow the call stack)
|
|
498
|
+
const UNVISITED = -1
|
|
499
|
+
const order = new Int32Array(n).fill(UNVISITED)
|
|
500
|
+
const low = new Int32Array(n)
|
|
501
|
+
const onStack = new Uint8Array(n)
|
|
502
|
+
const comp = new Int32Array(n)
|
|
503
|
+
const sccStack = []
|
|
504
|
+
const callStack = [] // [node, next dep position]
|
|
505
|
+
let counter = 0
|
|
506
|
+
let compCount = 0
|
|
507
|
+
for (let root = 0; root < n; root++) {
|
|
508
|
+
if (order[root] !== UNVISITED) continue
|
|
509
|
+
callStack.push(root, 0)
|
|
510
|
+
order[root] = low[root] = counter++
|
|
511
|
+
sccStack.push(root)
|
|
512
|
+
onStack[root] = 1
|
|
513
|
+
while (callStack.length > 0) {
|
|
514
|
+
const top = callStack.length - 2
|
|
515
|
+
const v = callStack[top]
|
|
516
|
+
const edges = deps[v]
|
|
517
|
+
const pos = callStack[top + 1]
|
|
518
|
+
if (pos < edges.length) {
|
|
519
|
+
callStack[top + 1] = pos + 1
|
|
520
|
+
const w = edges[pos]
|
|
521
|
+
if (order[w] === UNVISITED) {
|
|
522
|
+
order[w] = low[w] = counter++
|
|
523
|
+
sccStack.push(w)
|
|
524
|
+
onStack[w] = 1
|
|
525
|
+
callStack.push(w, 0)
|
|
526
|
+
} else if (onStack[w] && order[w] < low[v]) {
|
|
527
|
+
low[v] = order[w]
|
|
528
|
+
}
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
callStack.length = top
|
|
532
|
+
if (top > 0) {
|
|
533
|
+
const parent = callStack[top - 2]
|
|
534
|
+
if (low[v] < low[parent]) low[parent] = low[v]
|
|
535
|
+
}
|
|
536
|
+
if (low[v] === order[v]) {
|
|
537
|
+
let w
|
|
538
|
+
do {
|
|
539
|
+
w = sccStack.pop()
|
|
540
|
+
onStack[w] = 0
|
|
541
|
+
comp[w] = compCount
|
|
542
|
+
} while (w !== v)
|
|
543
|
+
compCount++
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Members per component in graph (discovery) order
|
|
549
|
+
/** @type {number[][]} */
|
|
550
|
+
const members = new Array(compCount)
|
|
551
|
+
for (let c = 0; c < compCount; c++) members[c] = []
|
|
552
|
+
for (let i = 0; i < n; i++) members[comp[i]].push(i)
|
|
553
|
+
|
|
554
|
+
// Component in-degree: distinct other components it depends on
|
|
555
|
+
const compInDegree = new Int32Array(compCount)
|
|
556
|
+
const compStamp = new Int32Array(compCount).fill(-1)
|
|
557
|
+
for (let c = 0; c < compCount; c++) {
|
|
558
|
+
for (const v of members[c]) {
|
|
559
|
+
for (const w of deps[v]) {
|
|
560
|
+
const cw = comp[w]
|
|
561
|
+
if (cw === c || compStamp[cw] === c) continue
|
|
562
|
+
compStamp[cw] = c
|
|
563
|
+
compInDegree[c]++
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Kahn's over components, seeded in graph order
|
|
569
|
+
const queue = []
|
|
570
|
+
const queued = new Uint8Array(compCount)
|
|
571
|
+
for (let i = 0; i < n; i++) {
|
|
572
|
+
const c = comp[i]
|
|
573
|
+
if (compInDegree[c] === 0 && !queued[c]) {
|
|
574
|
+
queued[c] = 1
|
|
575
|
+
queue.push(c)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const result = []
|
|
579
|
+
compStamp.fill(-1)
|
|
580
|
+
for (let head = 0; head < queue.length; head++) {
|
|
581
|
+
const c = queue[head]
|
|
582
|
+
for (const v of members[c]) result.push(nodes[v])
|
|
583
|
+
for (const v of members[c]) {
|
|
584
|
+
for (const d of dependents[v]) {
|
|
585
|
+
const cd = comp[d]
|
|
586
|
+
if (cd === c || compStamp[cd] === c) continue
|
|
587
|
+
compStamp[cd] = c
|
|
588
|
+
if (--compInDegree[cd] === 0) {
|
|
589
|
+
queued[cd] = 1
|
|
590
|
+
queue.push(cd)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return result
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get all dependency file paths in build order (dependencies first), deduplicated.
|
|
600
|
+
* Files in an import cycle are kept together, in discovery order.
|
|
452
601
|
* @param {string} filePath - Entry file path
|
|
453
602
|
* @param {object} options - Options to pass to depGraph
|
|
454
603
|
* @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
|
|
@@ -492,42 +641,8 @@ function resolveDepPaths(filePath, options = {}) {
|
|
|
492
641
|
}
|
|
493
642
|
}
|
|
494
643
|
|
|
495
|
-
// Calculate in-degree (number of file dependencies) for each file
|
|
496
|
-
const inDegree = new Map()
|
|
497
|
-
for (const [file, deps] of graph) {
|
|
498
|
-
// Only count file deps, not node_modules
|
|
499
|
-
const fileDeps = deps.filter(d => d.startsWith('/'))
|
|
500
|
-
inDegree.set(file, fileDeps.length)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Queue of files with no dependencies (can be processed first)
|
|
504
|
-
const queue = []
|
|
505
|
-
for (const [file, degree] of inDegree) {
|
|
506
|
-
if (degree === 0) {
|
|
507
|
-
queue.push(file)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const buildOrder = []
|
|
512
|
-
|
|
513
|
-
while (queue.length > 0) {
|
|
514
|
-
const file = queue.shift()
|
|
515
|
-
buildOrder.push(file)
|
|
516
|
-
|
|
517
|
-
// Find all files that depend on this file and decrement their in-degree
|
|
518
|
-
for (const [dependent, deps] of graph) {
|
|
519
|
-
if (deps.includes(file)) {
|
|
520
|
-
const newDegree = inDegree.get(dependent) - 1
|
|
521
|
-
inDegree.set(dependent, newDegree)
|
|
522
|
-
if (newDegree === 0) {
|
|
523
|
-
queue.push(dependent)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
644
|
const result = {
|
|
530
|
-
files: buildOrder,
|
|
645
|
+
files: buildOrder(graph),
|
|
531
646
|
nodeModules: Array.from(nodeModulesSet),
|
|
532
647
|
builtIns: Array.from(builtInsSet)
|
|
533
648
|
}
|
package/src/get-cjs-exports.js
CHANGED
|
@@ -34,7 +34,14 @@ function stripComments(code) {
|
|
|
34
34
|
return result
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Every scanner below is gated on one of these in the sanitized text. Sanitizing only
|
|
38
|
+
// blanks characters to spaces (length-preserving), so a needle without whitespace that
|
|
39
|
+
// isn't in the source can't be in the sanitized text either.
|
|
40
|
+
const CJS_NEEDLES = ['exports', 'Object.assign', 'Object.defineProperty', 'Object.keys(', '__exportStar', '__reExport']
|
|
41
|
+
|
|
37
42
|
function getCjsExports(code) {
|
|
43
|
+
// Most ESM/TS files mention none of them: skip the sanitize pass entirely.
|
|
44
|
+
if (!CJS_NEEDLES.some(needle => code.includes(needle))) return []
|
|
38
45
|
// Single tokenizer-based pass that blanks comments and code-bearing
|
|
39
46
|
// strings/templates together. Length is preserved so the position-based
|
|
40
47
|
// logic below (which slices into the original `code` for full export
|
package/src/get-exports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { getNames, extractExportName,
|
|
1
|
+
const { getNames, extractExportName, stripCommentsThenStrings } = 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
|
|
@@ -22,8 +22,7 @@ function getExports(code = '', options = {}) {
|
|
|
22
22
|
// alone would mis-pair an apostrophe inside a JSDoc comment (`Netlify's …`)
|
|
23
23
|
// with a later quote and shift subsequent backtick pairing, which could
|
|
24
24
|
// swallow real `export` declarations a few lines later.
|
|
25
|
-
code =
|
|
26
|
-
code = stripStringContents(code)
|
|
25
|
+
code = stripCommentsThenStrings(code)
|
|
27
26
|
const defaultExports = getDefaultExports(code)
|
|
28
27
|
const namedExports = getNamedExports(code)
|
|
29
28
|
const namedDeclarationExports = getNamedDeclarationExports(code)
|
package/src/get-imports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames,
|
|
1
|
+
const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripCommentsThenStrings } = require('./utils')
|
|
2
2
|
|
|
3
3
|
// https://regex101.com/r/cfEmlm/1
|
|
4
4
|
// Path excludes quotes, newlines, and `;` so multi-statement lines like
|
|
@@ -43,10 +43,11 @@ function getImports(code = '', options = {}) {
|
|
|
43
43
|
// removed before `stripStringContents` looks for string literals.
|
|
44
44
|
// (Running the two in the opposite order let comment chars masquerade as
|
|
45
45
|
// string openers and swallow real `import`/`export` lines further down.)
|
|
46
|
-
code =
|
|
47
|
-
code = stripStringContents(code)
|
|
46
|
+
code = stripCommentsThenStrings(code)
|
|
48
47
|
const staticImports = getStaticImports(code)
|
|
49
|
-
|
|
48
|
+
// Every dynamic-import match contains `import(` (most files have none; the regex's
|
|
49
|
+
// optional declaration prefix makes a miss scan the whole text).
|
|
50
|
+
const dynamicImports = code.includes('import(') ? getDynamicImports(code) : []
|
|
50
51
|
if (options.includeAssertionImports) {
|
|
51
52
|
const assertionImports = getAssertionImports(code)
|
|
52
53
|
return staticImports.concat(dynamicImports).concat(assertionImports).sort((a, b) => a.index - b.index)
|
package/src/get-requires.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { isBuiltIn, isRelativeImport, getNames,
|
|
1
|
+
const { isBuiltIn, isRelativeImport, getNames, stripCommentsThenStrings } = 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
|
|
@@ -111,8 +111,7 @@ function getRequires(code) {
|
|
|
111
111
|
// alone would mis-eat `//` inside strings; running it first instead means
|
|
112
112
|
// comment chars (incl. apostrophes/backticks inside JSDoc) disappear
|
|
113
113
|
// before `stripStringContents` looks for real string literals.
|
|
114
|
-
code =
|
|
115
|
-
code = stripStringContents(code)
|
|
114
|
+
code = stripCommentsThenStrings(code)
|
|
116
115
|
const staticRequires = getStaticRequires(code)
|
|
117
116
|
const dynamicRequires = getDynamicRequires(code)
|
|
118
117
|
return staticRequires.concat(dynamicRequires).sort((a, b) => a.index - b.index)
|
package/src/index.js
CHANGED
|
@@ -45,7 +45,9 @@ function extractDeps(code, filePath = '') {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function parseDeps(code) {
|
|
48
|
-
|
|
48
|
+
// Import-attribute imports (`import data from './x.json' with { type: 'json' }`) are
|
|
49
|
+
// dependencies too: without them a changed JSON file never reaches its importers
|
|
50
|
+
const imports = getImports(code, { includeAssertionImports: true })
|
|
49
51
|
//console.log('imports', imports)
|
|
50
52
|
const requires = getRequires(code)
|
|
51
53
|
// console.log('requires', requires)
|
package/src/utils.js
CHANGED
|
@@ -291,6 +291,26 @@ function stripComments(code) {
|
|
|
291
291
|
return stripCommentsImpl(code)
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Last input/output of stripCommentsThenStrings: one parse runs the import,
|
|
295
|
+
// require and export scanners over the same source text in a row.
|
|
296
|
+
let lastStripInput = null
|
|
297
|
+
let lastStripOutput = ''
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* `stripStringContents(stripComments(code))`, the prelude of the import, require
|
|
301
|
+
* and export scanners (comments first, then strings: see getImports). Pure in
|
|
302
|
+
* `code`; the latest result is kept for the next scanner of the same text.
|
|
303
|
+
* @param {string} code
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
function stripCommentsThenStrings(code) {
|
|
307
|
+
if (code === lastStripInput) return lastStripOutput
|
|
308
|
+
const output = stripStringContents(stripCommentsImpl(code))
|
|
309
|
+
lastStripInput = code
|
|
310
|
+
lastStripOutput = output
|
|
311
|
+
return output
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
/**
|
|
295
315
|
* Check if a module specifier is a Node.js built-in
|
|
296
316
|
* @param {string} moduleName - The module specifier (e.g., 'fs', 'node:fs/promises')
|
|
@@ -504,6 +524,7 @@ module.exports = {
|
|
|
504
524
|
stripComments,
|
|
505
525
|
stripStringContents,
|
|
506
526
|
stripCommentsAndStrings,
|
|
527
|
+
stripCommentsThenStrings,
|
|
507
528
|
isBuiltIn,
|
|
508
529
|
isRelativeImport,
|
|
509
530
|
isRelativeImportWithQuotes,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// resolveDepPaths must return every reachable file exactly once, including
|
|
2
|
+
// import cycles, files that depend on a cycle, self-imports and files that
|
|
3
|
+
// import the same module more than once (duplicate edges).
|
|
4
|
+
const { test } = require('uvu')
|
|
5
|
+
const assert = require('uvu/assert')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const { depGraph, resolveDepPaths } = require('../src/dep-graph.js')
|
|
8
|
+
|
|
9
|
+
const FIXTURES = path.resolve(__dirname, './fixtures/cycle-fixtures')
|
|
10
|
+
|
|
11
|
+
/** @param {string} rel */
|
|
12
|
+
const fixture = (rel) => path.join(FIXTURES, rel)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve deps and return basenames (fixtures use unique names per directory)
|
|
16
|
+
* @param {string} entry
|
|
17
|
+
*/
|
|
18
|
+
function names(entry) {
|
|
19
|
+
const result = resolveDepPaths(entry, { returnErrors: true })
|
|
20
|
+
assert.equal(result.errors, [], 'fixture should resolve without errors')
|
|
21
|
+
return result.files.map(f => path.basename(f))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every file in the graph (= everything reachable from the entry) appears exactly once
|
|
26
|
+
* @param {string} entry
|
|
27
|
+
*/
|
|
28
|
+
function assertComplete(entry) {
|
|
29
|
+
const graph = depGraph(entry)
|
|
30
|
+
const { files } = resolveDepPaths(entry)
|
|
31
|
+
assert.is(new Set(files).size, files.length, 'no duplicates')
|
|
32
|
+
assert.equal([...files].sort(), [...graph.keys()].sort(), 'files equal the reachable set')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('2-file cycle: both cycle members and their dependent are returned', () => {
|
|
36
|
+
const entry = fixture('two-cycle/index.js')
|
|
37
|
+
assertComplete(entry)
|
|
38
|
+
assert.equal(names(entry), ['a.js', 'b.js', 'index.js'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('cycle through the entry file: entry and its cycle peer are returned', () => {
|
|
42
|
+
const entry = fixture('cycle-entry/index.js')
|
|
43
|
+
assertComplete(entry)
|
|
44
|
+
assert.equal(names(entry), ['leaf.js', 'index.js', 'peer.js'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('3-file cycle with a dependent outside it and a leaf below it', () => {
|
|
48
|
+
const entry = fixture('three-cycle/index.js')
|
|
49
|
+
assertComplete(entry)
|
|
50
|
+
// leaf first (dependency of the cycle), cycle members in discovery order,
|
|
51
|
+
// then w (depends on the cycle), then the entry
|
|
52
|
+
assert.equal(names(entry), ['leaf.js', 'x.js', 'y.js', 'z.js', 'w.js', 'index.js'])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('self-import does not drop the file or its dependents', () => {
|
|
56
|
+
const entry = fixture('self-import/index.js')
|
|
57
|
+
assertComplete(entry)
|
|
58
|
+
assert.equal(names(entry), ['leaf.js', 'self.js', 'index.js'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('same module required twice (same and different specifiers)', () => {
|
|
62
|
+
const entry = fixture('duplicate-edge/index.js')
|
|
63
|
+
assertComplete(entry)
|
|
64
|
+
assert.equal(names(entry), ['leaf.js', 'other.js', 'util.js', 'index.js'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('same module imported (ESM) and required (CJS) in one file', () => {
|
|
68
|
+
const entry = fixture('mixed-duplicate/index.mjs')
|
|
69
|
+
assertComplete(entry)
|
|
70
|
+
assert.equal(names(entry), ['base.mjs', 'helper.mjs', 'index.mjs'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('diamond (acyclic): shared dependency once, dependencies before dependents', () => {
|
|
74
|
+
const entry = fixture('diamond/index.js')
|
|
75
|
+
assertComplete(entry)
|
|
76
|
+
assert.equal(names(entry), ['d.js', 'c.js', 'a.js', 'b.js', 'index.js'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('acyclic graphs keep dependencies-first order', () => {
|
|
80
|
+
for (const rel of ['diamond/index.js', 'duplicate-edge/index.js', 'mixed-duplicate/index.mjs']) {
|
|
81
|
+
const entry = fixture(rel)
|
|
82
|
+
const graph = depGraph(entry)
|
|
83
|
+
const { files } = resolveDepPaths(entry)
|
|
84
|
+
const pos = new Map(files.map((f, i) => [f, i]))
|
|
85
|
+
for (const [file, deps] of graph) {
|
|
86
|
+
for (const dep of deps) {
|
|
87
|
+
assert.ok(pos.get(dep) < pos.get(file), `${path.basename(dep)} before ${path.basename(file)} in ${rel}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('files outside a cycle still come after everything they depend on', () => {
|
|
94
|
+
const entry = fixture('three-cycle/index.js')
|
|
95
|
+
const { files } = resolveDepPaths(entry)
|
|
96
|
+
const pos = new Map(files.map(f => [path.basename(f), files.indexOf(f)]))
|
|
97
|
+
for (const member of ['x.js', 'y.js', 'z.js']) {
|
|
98
|
+
assert.ok(pos.get(member) < pos.get('w.js'), `${member} before w.js`)
|
|
99
|
+
assert.ok(pos.get(member) > pos.get('leaf.js'), `leaf.js before ${member}`)
|
|
100
|
+
}
|
|
101
|
+
assert.is(files[files.length - 1], entry, 'entry last when it is not in a cycle')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('output is deterministic across calls', () => {
|
|
105
|
+
for (const rel of ['two-cycle/index.js', 'three-cycle/index.js', 'cycle-entry/index.js', 'self-import/index.js']) {
|
|
106
|
+
const entry = fixture(rel)
|
|
107
|
+
assert.equal(resolveDepPaths(entry).files, resolveDepPaths(entry).files)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test.run()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// JSON modules in the dependency graph (smart-ci sci-nrnc.4 / sci-nrnc.11)
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const { resolveDepPaths, clearDepGraphCache } = require('../src/dep-graph')
|
|
8
|
+
|
|
9
|
+
function makeFiles(files) {
|
|
10
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'extract-deps-json-')))
|
|
11
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
12
|
+
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true })
|
|
13
|
+
fs.writeFileSync(path.join(dir, rel), content)
|
|
14
|
+
}
|
|
15
|
+
return dir
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const VERIFY = { verifyGraph: true, verifyNodeModules: true, includeNodeModules: true, returnErrors: true }
|
|
19
|
+
|
|
20
|
+
test('CommonJS require of JSON: listed, and no "default export" error', () => {
|
|
21
|
+
clearDepGraphCache()
|
|
22
|
+
const dir = makeFiles({
|
|
23
|
+
'handler.js': "const messages = require('./messages.json')\nmodule.exports.handler = async () => messages.hello\n",
|
|
24
|
+
'messages.json': '{ "hello": "Foobar!!!" }\n',
|
|
25
|
+
})
|
|
26
|
+
try {
|
|
27
|
+
const { files, errors } = resolveDepPaths(path.join(dir, 'handler.js'), VERIFY)
|
|
28
|
+
assert.ok(files.includes(path.join(dir, 'messages.json')), 'messages.json is a dependency')
|
|
29
|
+
assert.equal((errors || []).map(e => e.message), [])
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('CommonJS destructuring require of JSON: no export errors', () => {
|
|
36
|
+
clearDepGraphCache()
|
|
37
|
+
const dir = makeFiles({
|
|
38
|
+
'handler.js': "const { hello } = require('./messages.json')\nmodule.exports.handler = async () => hello\n",
|
|
39
|
+
'messages.json': '{ "hello": "hi" }\n',
|
|
40
|
+
})
|
|
41
|
+
try {
|
|
42
|
+
const { errors } = resolveDepPaths(path.join(dir, 'handler.js'), VERIFY)
|
|
43
|
+
assert.equal((errors || []).map(e => e.message), [])
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('ESM import of JSON with import attributes (with / assert): listed as a dependency', () => {
|
|
50
|
+
clearDepGraphCache()
|
|
51
|
+
const dir = makeFiles({
|
|
52
|
+
'src/app.mjs': "import contract from '../docs/api.json' with { type: 'json' }\nimport legacy from './legacy.json' assert { type: 'json' }\nexport const h = () => [contract, legacy]\n",
|
|
53
|
+
'src/legacy.json': '{}\n',
|
|
54
|
+
'docs/api.json': '{ "version": 1 }\n',
|
|
55
|
+
})
|
|
56
|
+
try {
|
|
57
|
+
const { files, errors } = resolveDepPaths(path.join(dir, 'src/app.mjs'), VERIFY)
|
|
58
|
+
assert.ok(files.includes(path.join(dir, 'docs/api.json')), 'with { type: json } import is a dependency')
|
|
59
|
+
assert.ok(files.includes(path.join(dir, 'src/legacy.json')), 'assert { type: json } import is a dependency')
|
|
60
|
+
assert.equal((errors || []).map(e => e.message), [])
|
|
61
|
+
} finally {
|
|
62
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('a missing JSON import is still reported', () => {
|
|
67
|
+
clearDepGraphCache()
|
|
68
|
+
const dir = makeFiles({
|
|
69
|
+
'app.mjs': "import contract from './missing.json' with { type: 'json' }\nexport const h = () => contract\n",
|
|
70
|
+
})
|
|
71
|
+
try {
|
|
72
|
+
const { errors } = resolveDepPaths(path.join(dir, 'app.mjs'), VERIFY)
|
|
73
|
+
assert.ok((errors || []).length > 0, 'missing.json is an error')
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test.run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { leaf: true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { d: true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { leaf: true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { other: true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const base = 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { leaf: true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { leaf: true }
|
package/types/dep-graph.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export function depGraph(filePath: string, options?: {
|
|
|
22
22
|
errors: Error[];
|
|
23
23
|
} | undefined;
|
|
24
24
|
/**
|
|
25
|
-
* Get all dependency file paths in build order (dependencies first), deduplicated
|
|
25
|
+
* Get all dependency file paths in build order (dependencies first), deduplicated.
|
|
26
|
+
* Files in an import cycle are kept together, in discovery order.
|
|
26
27
|
* @param {string} filePath - Entry file path
|
|
27
28
|
* @param {object} options - Options to pass to depGraph
|
|
28
29
|
* @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
|
|
@@ -42,4 +43,6 @@ export function resolveDepPaths(filePath: string, options?: {
|
|
|
42
43
|
builtIns: string[];
|
|
43
44
|
errors?: Error[];
|
|
44
45
|
};
|
|
46
|
+
/** Drop memoized parses and resolutions (e.g. between runs in a long-lived process). */
|
|
47
|
+
export function clearDepGraphCache(): void;
|
|
45
48
|
//# sourceMappingURL=dep-graph.d.ts.map
|
package/types/dep-graph.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dep-graph.d.ts","sourceRoot":"","sources":["../src/dep-graph.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dep-graph.d.ts","sourceRoot":"","sources":["../src/dep-graph.js"],"names":[],"mappings":"AA6ZA;;;;;;;;;;;GAWG;AACH,mCAVW,MAAM,YAEd;IAA0B,UAAU,GAA5B,OAAO;IACW,WAAW,GAA7B,OAAO;IACW,iBAAiB,GAAnC,OAAO;IACW,kBAAkB,GAApC,OAAO;IACW,iBAAiB,GAAnC,OAAO;IACW,YAAY,GAA9B,OAAO;CACf,GAAU,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAC;IAAC,KAAK,gBAAM;IAAC,MAAM,EAAE,KAAK,EAAE,CAAA;CAAC,GAAC,SAAS,CA8BzE;AAgJD;;;;;;;;;;GAUG;AACH,0CARW,MAAM,YAEd;IAA0B,WAAW,GAA7B,OAAO;IACW,iBAAiB,GAAnC,OAAO;IACW,iBAAiB,GAAnC,OAAO;IACW,YAAY,GAA9B,OAAO;CACf,GAAU;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;CAAE,CAgD5F;AArkBD,wFAAwF;AACxF,2CAKC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-cjs-exports.d.ts","sourceRoot":"","sources":["../src/get-cjs-exports.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-cjs-exports.d.ts","sourceRoot":"","sources":["../src/get-cjs-exports.js"],"names":[],"mappings":"AAyCA,gDAoDC;AA+BD;;;;;;;;IAqGC;AAED;;;;;;;IAqBC;AAED;;;;;;;IAqBC;AAED;;;;;;;IAiDC;AAED;;;;;;;;;IAwEC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-exports.d.ts","sourceRoot":"","sources":["../src/get-exports.js"],"names":[],"mappings":"AAiBA,+
|
|
1
|
+
{"version":3,"file":"get-exports.d.ts","sourceRoot":"","sources":["../src/get-exports.js"],"names":[],"mappings":"AAiBA,+DAsCC;AAED;;;;;;;;IAqDC;AAED;;;;;;IAeC;AAED,uEAuBC;AAED;;;;;;IAeC;AAED;;;;;;;IAgBC;AAED;;;;;;IAeC;AAED;;;;;;;;;;;;IA0BC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-imports.d.ts","sourceRoot":"","sources":["../src/get-imports.js"],"names":[],"mappings":"AAsCA;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"get-imports.d.ts","sourceRoot":"","sources":["../src/get-imports.js"],"names":[],"mappings":"AAsCA;;;;;;;;;;IAiBC;AAGD;;;;;;;;;;IAsCC;AAED;;;;;;;;;IAkBC;AAED;;;;;;;;;;;IAuBC;AAED;;;;;;;;;;;;;IA0BC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-requires.d.ts","sourceRoot":"","sources":["../src/get-requires.js"],"names":[],"mappings":"AA0GA;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"get-requires.d.ts","sourceRoot":"","sources":["../src/get-requires.js"],"names":[],"mappings":"AA0GA;;;;;;;;;;;IAWC;AAtCD;;;;;;;;;;;IAyBC;AA/FD;;;;;;;;;;;IA6BC"}
|
package/types/utils.d.ts
CHANGED
|
@@ -63,6 +63,14 @@ export function stripStringContents(code: string): string;
|
|
|
63
63
|
* @returns {string}
|
|
64
64
|
*/
|
|
65
65
|
export function stripCommentsAndStrings(code: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* `stripStringContents(stripComments(code))`, the prelude of the import, require
|
|
68
|
+
* and export scanners (comments first, then strings: see getImports). Pure in
|
|
69
|
+
* `code`; the latest result is kept for the next scanner of the same text.
|
|
70
|
+
* @param {string} code
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function stripCommentsThenStrings(code: string): string;
|
|
66
74
|
/**
|
|
67
75
|
* Check if a module specifier is a Node.js built-in
|
|
68
76
|
* @param {string} moduleName - The module specifier (e.g., 'fs', 'node:fs/promises')
|
package/types/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AA0RA;;;;;;GAMG;AACH,oCAHW,MAAM,GACJ,MAAM,CAIlB;AA1RD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,0CAHW,MAAM,GACJ,MAAM,CAuDlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,8CAHW,MAAM,GACJ,MAAM,CAIlB;AAiLD;;;;;;GAMG;AACH,+CAHW,MAAM,GACJ,MAAM,CAQlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,OAAO,CAUnB;AAED,+DAEC;AAED,yEAKC;AAgFD,6DA+DC;AAGD,yDAqCC"}
|