@davidwells/extract-deps 0.0.4 → 0.0.5
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/examples/verify-basic.js +43 -0
- package/examples/verify-build-graph.js +58 -0
- package/examples/verify-build-order.js +113 -0
- package/examples/verify-find-dependents.js +74 -0
- package/examples/verify-impact-analysis.js +97 -0
- package/package.json +5 -1
- package/src/dep-graph.js +277 -0
- package/tests/dep-graph.test.js +213 -0
- package/tests/fixtures/cjs-deep-example/d-one.js +10 -0
- package/tests/fixtures/cjs-deep-example/d-sub-folder/d-sub-sub-folder/d-three.js +7 -0
- package/tests/fixtures/cjs-deep-example/d-sub-folder/d-two-a.js +8 -0
- package/tests/fixtures/cjs-deep-example/d-sub-folder/d-two-b.js +8 -0
- package/tests/fixtures/cjs-deep-example/d-sub-folder/d-two.js +8 -0
- package/tests/fixtures/cjs-deep-example/index.js +8 -0
- package/tests/fixtures/cjs-example/index.js +8 -0
- package/tests/fixtures/cjs-example/one.js +9 -0
- package/tests/fixtures/cjs-example/sub-folder/sub-sub-folder/three.js +6 -0
- package/tests/fixtures/cjs-example/sub-folder/two.js +8 -0
- package/tests/fixtures/cjs-parent.js +5 -0
- package/tests/fixtures/esm-fixture/index.js +8 -0
- package/tests/fixtures/esm-fixture/one.js +9 -0
- package/tests/fixtures/esm-fixture/sub-folder/sub-sub-folder/three.js +6 -0
- package/tests/fixtures/esm-fixture/sub-folder/two.js +8 -0
- package/types/dep-graph.d.ts +22 -0
- package/types/dep-graph.d.ts.map +1 -0
- package/types/get-cjs-exports.d.ts +35 -0
- package/types/get-cjs-exports.d.ts.map +1 -0
- package/types/get-exports.d.ts +3 -1
- package/types/get-exports.d.ts.map +1 -1
- package/types/index.d.ts +7 -3
- package/types/index.d.ts.map +1 -1
- package/types/utils.d.ts +2 -1
- package/types/utils.d.ts.map +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Basic verification of dependency graph
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { depGraph } = require('../src/dep-graph')
|
|
4
|
+
|
|
5
|
+
const cjsFixture = path.resolve(__dirname, '../tests/fixtures/cjs-example/index.js')
|
|
6
|
+
const esmFixture = path.resolve(__dirname, '../tests/fixtures/esm-fixture/index.js')
|
|
7
|
+
|
|
8
|
+
console.log('Verifying CJS fixture...')
|
|
9
|
+
try {
|
|
10
|
+
const graph = depGraph(cjsFixture)
|
|
11
|
+
console.log('✓ CJS fixture is valid - all imports resolve correctly')
|
|
12
|
+
console.log(` Found ${graph.size} files in dependency graph\n`)
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('✗ CJS fixture has errors:')
|
|
15
|
+
console.error(error.message)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('Verifying ESM fixture...')
|
|
19
|
+
try {
|
|
20
|
+
const graph = depGraph(esmFixture)
|
|
21
|
+
console.log('✓ ESM fixture is valid - all imports resolve correctly')
|
|
22
|
+
console.log(` Found ${graph.size} files in dependency graph\n`)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('✗ ESM fixture has errors:')
|
|
25
|
+
console.error(error.message)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('Verifying with throwOnFirstError option...')
|
|
29
|
+
try {
|
|
30
|
+
depGraph(cjsFixture, { throwOnFirstError: true })
|
|
31
|
+
console.log('✓ Validation passed (would have thrown on first error)\n')
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('✗ First error encountered:')
|
|
34
|
+
console.error(error.message)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('Building graph without verification (faster)...')
|
|
38
|
+
const graph = depGraph(cjsFixture, { verifyGraph: false })
|
|
39
|
+
console.log(`✓ Graph built without verification - ${graph.size} files`)
|
|
40
|
+
|
|
41
|
+
console.log('\nVerification only (no graph building)...')
|
|
42
|
+
depGraph(cjsFixture, { buildGraph: false })
|
|
43
|
+
console.log('✓ Verification passed (no graph returned)')
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Build and explore dependency graph
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { depGraph } = require('../src/dep-graph')
|
|
4
|
+
|
|
5
|
+
const fixture = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
|
|
6
|
+
|
|
7
|
+
console.log('Building dependency graph...\n')
|
|
8
|
+
|
|
9
|
+
const graph = depGraph(fixture)
|
|
10
|
+
|
|
11
|
+
console.log('=== Dependency Graph ===\n')
|
|
12
|
+
|
|
13
|
+
// Display the graph
|
|
14
|
+
for (const [file, deps] of graph) {
|
|
15
|
+
const fileName = path.basename(file)
|
|
16
|
+
const fileDir = path.relative(__dirname, path.dirname(file))
|
|
17
|
+
|
|
18
|
+
console.log(`${fileName} (${fileDir})`)
|
|
19
|
+
|
|
20
|
+
if (deps.length === 0) {
|
|
21
|
+
console.log(' └─ (no dependencies - leaf node)\n')
|
|
22
|
+
} else {
|
|
23
|
+
deps.forEach((dep, idx) => {
|
|
24
|
+
const depName = path.basename(dep)
|
|
25
|
+
const isLast = idx === deps.length - 1
|
|
26
|
+
const prefix = isLast ? ' └─' : ' ├─'
|
|
27
|
+
console.log(`${prefix} ${depName}`)
|
|
28
|
+
})
|
|
29
|
+
console.log()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`\nTotal files: ${graph.size}`)
|
|
34
|
+
|
|
35
|
+
// Find leaf nodes (files with no dependencies)
|
|
36
|
+
const leafNodes = []
|
|
37
|
+
for (const [file, deps] of graph) {
|
|
38
|
+
if (deps.length === 0) {
|
|
39
|
+
leafNodes.push(path.basename(file))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`Leaf nodes: ${leafNodes.join(', ')}`)
|
|
44
|
+
|
|
45
|
+
// Find root nodes (files not depended on by others in the graph)
|
|
46
|
+
const allDeps = new Set()
|
|
47
|
+
for (const deps of graph.values()) {
|
|
48
|
+
deps.forEach(dep => allDeps.add(dep))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rootNodes = []
|
|
52
|
+
for (const file of graph.keys()) {
|
|
53
|
+
if (!allDeps.has(file)) {
|
|
54
|
+
rootNodes.push(path.basename(file))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Root nodes: ${rootNodes.join(', ')}`)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Calculate build order using topological sort
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { depGraph } = require('../src/dep-graph')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calculate build order using topological sort (Kahn's algorithm)
|
|
7
|
+
* @param {Map<string, string[]>} graph - Dependency graph
|
|
8
|
+
* @returns {string[]} - Files in build order (dependencies first)
|
|
9
|
+
*/
|
|
10
|
+
function calculateBuildOrder(graph) {
|
|
11
|
+
// Calculate in-degree (number of dependencies) for each file
|
|
12
|
+
const inDegree = new Map()
|
|
13
|
+
for (const [file, deps] of graph) {
|
|
14
|
+
inDegree.set(file, deps.length)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Queue of files with no dependencies (can be built first)
|
|
18
|
+
const queue = []
|
|
19
|
+
for (const [file, degree] of inDegree) {
|
|
20
|
+
if (degree === 0) {
|
|
21
|
+
queue.push(file)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const buildOrder = []
|
|
26
|
+
|
|
27
|
+
while (queue.length > 0) {
|
|
28
|
+
const file = queue.shift()
|
|
29
|
+
buildOrder.push(file)
|
|
30
|
+
|
|
31
|
+
// Find all files that depend on this file and decrement their in-degree
|
|
32
|
+
for (const [dependent, deps] of graph) {
|
|
33
|
+
if (deps.includes(file)) {
|
|
34
|
+
const newDegree = inDegree.get(dependent) - 1
|
|
35
|
+
inDegree.set(dependent, newDegree)
|
|
36
|
+
if (newDegree === 0) {
|
|
37
|
+
queue.push(dependent)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return buildOrder
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Group files by build level (parallel build opportunities)
|
|
48
|
+
* @param {Map<string, string[]>} graph - Dependency graph
|
|
49
|
+
* @returns {Array<string[]>} - Array of levels, each containing files that can be built in parallel
|
|
50
|
+
*/
|
|
51
|
+
function calculateBuildLevels(graph) {
|
|
52
|
+
const inDegree = new Map()
|
|
53
|
+
for (const [file, deps] of graph) {
|
|
54
|
+
inDegree.set(file, deps.length)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const levels = []
|
|
58
|
+
const processed = new Set()
|
|
59
|
+
|
|
60
|
+
while (processed.size < graph.size) {
|
|
61
|
+
const currentLevel = []
|
|
62
|
+
|
|
63
|
+
for (const [file, degree] of inDegree) {
|
|
64
|
+
if (degree === 0 && !processed.has(file)) {
|
|
65
|
+
currentLevel.push(file)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (currentLevel.length === 0) break
|
|
70
|
+
|
|
71
|
+
levels.push(currentLevel)
|
|
72
|
+
|
|
73
|
+
for (const file of currentLevel) {
|
|
74
|
+
processed.add(file)
|
|
75
|
+
for (const [dependent, deps] of graph) {
|
|
76
|
+
if (deps.includes(file)) {
|
|
77
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return levels
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fixture = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
|
|
87
|
+
const graph = depGraph(fixture)
|
|
88
|
+
|
|
89
|
+
console.log('=== Build Order Analysis ===\n')
|
|
90
|
+
|
|
91
|
+
// Sequential build order
|
|
92
|
+
const buildOrder = calculateBuildOrder(graph)
|
|
93
|
+
console.log('Sequential build order (dependencies first):\n')
|
|
94
|
+
buildOrder.forEach((file, index) => {
|
|
95
|
+
console.log(`${index + 1}. ${path.basename(file)}`)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
console.log('\n=== Parallel Build Opportunities ===\n')
|
|
99
|
+
|
|
100
|
+
// Parallel build levels
|
|
101
|
+
const levels = calculateBuildLevels(graph)
|
|
102
|
+
console.log('Files grouped by build level (same level can build in parallel):\n')
|
|
103
|
+
|
|
104
|
+
levels.forEach((level, index) => {
|
|
105
|
+
console.log(`Level ${index + 1}: (${level.length} file${level.length === 1 ? '' : 's'})`)
|
|
106
|
+
level.forEach(file => {
|
|
107
|
+
console.log(` - ${path.basename(file)}`)
|
|
108
|
+
})
|
|
109
|
+
console.log()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
console.log(`Total build levels: ${levels.length}`)
|
|
113
|
+
console.log(`Maximum parallelism: ${Math.max(...levels.map(l => l.length))} files`)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Find all files that depend on a specific file
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { depGraph } = require('../src/dep-graph')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find all files that directly depend on a target file
|
|
7
|
+
* @param {Map<string, string[]>} graph - Dependency graph
|
|
8
|
+
* @param {string} targetFile - File to find dependents for
|
|
9
|
+
* @returns {string[]} - Array of files that depend on targetFile
|
|
10
|
+
*/
|
|
11
|
+
function findDirectDependents(graph, targetFile) {
|
|
12
|
+
const dependents = []
|
|
13
|
+
for (const [file, deps] of graph) {
|
|
14
|
+
if (deps.includes(targetFile)) {
|
|
15
|
+
dependents.push(file)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return dependents
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find all files that transitively depend on a target file
|
|
23
|
+
* @param {Map<string, string[]>} graph - Dependency graph
|
|
24
|
+
* @param {string} targetFile - File to find dependents for
|
|
25
|
+
* @returns {Set<string>} - Set of all files that depend on targetFile (directly or indirectly)
|
|
26
|
+
*/
|
|
27
|
+
function findAllDependents(graph, targetFile) {
|
|
28
|
+
const allDependents = new Set()
|
|
29
|
+
const queue = [targetFile]
|
|
30
|
+
|
|
31
|
+
while (queue.length > 0) {
|
|
32
|
+
const current = queue.shift()
|
|
33
|
+
const directDependents = findDirectDependents(graph, current)
|
|
34
|
+
|
|
35
|
+
for (const dependent of directDependents) {
|
|
36
|
+
if (!allDependents.has(dependent)) {
|
|
37
|
+
allDependents.add(dependent)
|
|
38
|
+
queue.push(dependent)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return allDependents
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fixture = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
|
|
47
|
+
const graph = depGraph(fixture)
|
|
48
|
+
|
|
49
|
+
// Find a commonly used file
|
|
50
|
+
const targetFile = Array.from(graph.keys()).find(key => key.includes('d-three.js'))
|
|
51
|
+
|
|
52
|
+
if (targetFile) {
|
|
53
|
+
console.log(`Analyzing dependencies for: ${path.basename(targetFile)}\n`)
|
|
54
|
+
|
|
55
|
+
const directDeps = findDirectDependents(graph, targetFile)
|
|
56
|
+
console.log('Direct dependents:')
|
|
57
|
+
if (directDeps.length === 0) {
|
|
58
|
+
console.log(' (none)')
|
|
59
|
+
} else {
|
|
60
|
+
directDeps.forEach(dep => {
|
|
61
|
+
console.log(` - ${path.basename(dep)}`)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const allDeps = findAllDependents(graph, targetFile)
|
|
66
|
+
console.log(`\nAll dependents (transitive): ${allDeps.size}`)
|
|
67
|
+
if (allDeps.size > 0) {
|
|
68
|
+
Array.from(allDeps).forEach(dep => {
|
|
69
|
+
console.log(` - ${path.basename(dep)}`)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`\nImpact: Changing this file affects ${allDeps.size} other file(s)`)
|
|
74
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Analyze impact of changing files in the dependency graph
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { depGraph } = require('../src/dep-graph')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calculate impact metrics for each file in the graph
|
|
7
|
+
* @param {Map<string, string[]>} graph - Dependency graph
|
|
8
|
+
* @returns {Map<string, object>} - Map of file paths to impact metrics
|
|
9
|
+
*/
|
|
10
|
+
function analyzeImpact(graph) {
|
|
11
|
+
const impact = new Map()
|
|
12
|
+
|
|
13
|
+
for (const file of graph.keys()) {
|
|
14
|
+
// Count direct dependencies
|
|
15
|
+
const directDeps = graph.get(file).length
|
|
16
|
+
|
|
17
|
+
// Count direct dependents
|
|
18
|
+
let directDependents = 0
|
|
19
|
+
for (const [_, deps] of graph) {
|
|
20
|
+
if (deps.includes(file)) {
|
|
21
|
+
directDependents++
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Calculate transitive dependents
|
|
26
|
+
const transitiveDependents = new Set()
|
|
27
|
+
const queue = [file]
|
|
28
|
+
const visited = new Set([file])
|
|
29
|
+
|
|
30
|
+
while (queue.length > 0) {
|
|
31
|
+
const current = queue.shift()
|
|
32
|
+
for (const [f, deps] of graph) {
|
|
33
|
+
if (deps.includes(current) && !visited.has(f)) {
|
|
34
|
+
visited.add(f)
|
|
35
|
+
transitiveDependents.add(f)
|
|
36
|
+
queue.push(f)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impact.set(file, {
|
|
42
|
+
directDeps,
|
|
43
|
+
directDependents,
|
|
44
|
+
transitiveDependents: transitiveDependents.size,
|
|
45
|
+
isLeaf: directDeps === 0,
|
|
46
|
+
isRoot: directDependents === 0
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return impact
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fixture = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
|
|
54
|
+
const graph = depGraph(fixture)
|
|
55
|
+
|
|
56
|
+
console.log('=== Dependency Impact Analysis ===\n')
|
|
57
|
+
|
|
58
|
+
const impact = analyzeImpact(graph)
|
|
59
|
+
|
|
60
|
+
// Sort files by impact (most dependents first)
|
|
61
|
+
const sortedFiles = Array.from(impact.entries())
|
|
62
|
+
.sort((a, b) => b[1].transitiveDependents - a[1].transitiveDependents)
|
|
63
|
+
|
|
64
|
+
console.log('Files sorted by impact (files that affect the most others):\n')
|
|
65
|
+
|
|
66
|
+
sortedFiles.forEach(([file, metrics], index) => {
|
|
67
|
+
const fileName = path.basename(file)
|
|
68
|
+
const impactLevel = metrics.transitiveDependents > 3 ? '🔴 HIGH' :
|
|
69
|
+
metrics.transitiveDependents > 1 ? '🟡 MEDIUM' :
|
|
70
|
+
'🟢 LOW'
|
|
71
|
+
|
|
72
|
+
console.log(`${index + 1}. ${fileName}`)
|
|
73
|
+
console.log(` Impact: ${impactLevel}`)
|
|
74
|
+
console.log(` Direct dependencies: ${metrics.directDeps}`)
|
|
75
|
+
console.log(` Direct dependents: ${metrics.directDependents}`)
|
|
76
|
+
console.log(` Total files affected: ${metrics.transitiveDependents}`)
|
|
77
|
+
|
|
78
|
+
if (metrics.isLeaf) {
|
|
79
|
+
console.log(` Type: Leaf node (no dependencies)`)
|
|
80
|
+
}
|
|
81
|
+
if (metrics.isRoot) {
|
|
82
|
+
console.log(` Type: Root node (nothing depends on it)`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
console.log('=== Summary ===')
|
|
89
|
+
console.log(`Total files: ${graph.size}`)
|
|
90
|
+
|
|
91
|
+
const leafNodes = Array.from(impact.values()).filter(m => m.isLeaf).length
|
|
92
|
+
const rootNodes = Array.from(impact.values()).filter(m => m.isRoot).length
|
|
93
|
+
const highImpact = sortedFiles.filter(([_, m]) => m.transitiveDependents > 3).length
|
|
94
|
+
|
|
95
|
+
console.log(`Leaf nodes: ${leafNodes}`)
|
|
96
|
+
console.log(`Root nodes: ${rootNodes}`)
|
|
97
|
+
console.log(`High impact files: ${highImpact}`)
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidwells/extract-deps",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Get imports and requires from a codebase without AST",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./dep-graph": "./src/dep-graph.js"
|
|
9
|
+
},
|
|
6
10
|
"types": "types/index.d.ts",
|
|
7
11
|
"author": "David Wells",
|
|
8
12
|
"license": "MIT",
|
package/src/dep-graph.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Build dependency graph and optionally verify imports
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { extractDeps } = require('./index')
|
|
5
|
+
|
|
6
|
+
function fsExistsSync(path) {
|
|
7
|
+
try {
|
|
8
|
+
fs.accessSync(path, fs.constants.F_OK)
|
|
9
|
+
return true
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a relative import to an actual file path
|
|
17
|
+
* @param {string} baseDir - Directory containing the importing file
|
|
18
|
+
* @param {string} importPath - The relative import path
|
|
19
|
+
* @returns {string|null} - Resolved file path or null if not found
|
|
20
|
+
*/
|
|
21
|
+
function resolveImportPath(baseDir, importPath) {
|
|
22
|
+
const possibleExtensions = ['.js', '.mjs', '.ts', '.json', '']
|
|
23
|
+
|
|
24
|
+
// Try direct file resolution
|
|
25
|
+
for (const ext of possibleExtensions) {
|
|
26
|
+
const testPath = path.resolve(baseDir, importPath + ext)
|
|
27
|
+
if (fsExistsSync(testPath)) {
|
|
28
|
+
return testPath
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try directory with index file
|
|
33
|
+
const indexExtensions = ['/index.js', '/index.mjs', '/index.ts']
|
|
34
|
+
for (const indexExt of indexExtensions) {
|
|
35
|
+
const testPath = path.resolve(baseDir, importPath + indexExt)
|
|
36
|
+
if (fsExistsSync(testPath)) {
|
|
37
|
+
return testPath
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Verify that imported values are exported from a file
|
|
46
|
+
* @param {object} dep - Dependency object with import info
|
|
47
|
+
* @param {object} targetDeps - Extracted deps from the target file
|
|
48
|
+
* @param {string} targetPath - Path to the target file
|
|
49
|
+
* @param {string} sourcePath - Path to the file doing the importing
|
|
50
|
+
* @param {Array<Error>} errors - Array to collect errors
|
|
51
|
+
* @param {boolean} throwOnFirstError - Whether to throw immediately on first error
|
|
52
|
+
*/
|
|
53
|
+
function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, errors, throwOnFirstError) {
|
|
54
|
+
// Check if this is a destructured import
|
|
55
|
+
if (dep.definitions && dep.definitions.expectsDestructuring && dep.definitions.destructuredParameters) {
|
|
56
|
+
const importedNames = dep.definitions.destructuredParameters.map(p => p.name.replace(/,$/, ''))
|
|
57
|
+
|
|
58
|
+
// Check CommonJS exports
|
|
59
|
+
const exportedNames = new Set()
|
|
60
|
+
if (targetDeps.moduleExports && targetDeps.moduleExports.length > 0) {
|
|
61
|
+
for (const exp of targetDeps.moduleExports) {
|
|
62
|
+
if (exp.exportType === 'named') {
|
|
63
|
+
exportedNames.add(exp.exportName)
|
|
64
|
+
} else if (exp.exportType === 'default' && exp.keys) {
|
|
65
|
+
exp.keys.forEach(key => exportedNames.add(key))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check ESM exports
|
|
71
|
+
if (targetDeps.exports && targetDeps.exports.length > 0) {
|
|
72
|
+
for (const exp of targetDeps.exports) {
|
|
73
|
+
const exportedName = exp.exportAlias || exp.exportName
|
|
74
|
+
if (exportedName) {
|
|
75
|
+
exportedNames.add(exportedName)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Verify each imported name is exported
|
|
81
|
+
for (const importedName of importedNames) {
|
|
82
|
+
if (!exportedNames.has(importedName)) {
|
|
83
|
+
const error = new Error(
|
|
84
|
+
`Import not found: "${importedName}" is not exported from "${targetPath}".\n` +
|
|
85
|
+
`Import statement: ${dep.line}\n` +
|
|
86
|
+
`Importing from: ${sourcePath}\n` +
|
|
87
|
+
`Available exports: ${Array.from(exportedNames).join(', ') || 'none'}`
|
|
88
|
+
)
|
|
89
|
+
if (throwOnFirstError) {
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
errors.push(error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// For non-destructured imports, just verify there's a default export
|
|
97
|
+
else {
|
|
98
|
+
const hasDefaultExport =
|
|
99
|
+
(targetDeps.moduleExports && targetDeps.moduleExports.length > 0) ||
|
|
100
|
+
(targetDeps.exports && targetDeps.exports.some(exp => exp.exportType === 'default'))
|
|
101
|
+
|
|
102
|
+
if (!hasDefaultExport) {
|
|
103
|
+
const error = new Error(
|
|
104
|
+
`Import not found: "${targetPath}" does not have a default export.\n` +
|
|
105
|
+
`Import statement: ${dep.line}\n` +
|
|
106
|
+
`Importing from: ${sourcePath}`
|
|
107
|
+
)
|
|
108
|
+
if (throwOnFirstError) {
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
errors.push(error)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Recursively build dependency graph and optionally validate imports
|
|
118
|
+
* @param {string} filePath - File to process
|
|
119
|
+
* @param {Array<Error>} errors - Array to collect errors
|
|
120
|
+
* @param {boolean} throwOnFirstError - Whether to throw immediately on first error
|
|
121
|
+
* @param {boolean} verify - Whether to verify imports/exports
|
|
122
|
+
* @param {Map<string, string[]>|null} graph - Optional map to collect dependency graph
|
|
123
|
+
* @param {Set} visited - Set of already-visited file paths to avoid cycles
|
|
124
|
+
*/
|
|
125
|
+
function buildGraphRecursively(filePath, errors, throwOnFirstError, verify, graph = null, visited = new Set()) {
|
|
126
|
+
// Avoid infinite loops
|
|
127
|
+
const normalizedPath = fs.realpathSync(filePath)
|
|
128
|
+
if (visited.has(normalizedPath)) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
visited.add(normalizedPath)
|
|
132
|
+
|
|
133
|
+
// Initialize graph entry for this file if building graph
|
|
134
|
+
if (graph !== null && !graph.has(normalizedPath)) {
|
|
135
|
+
graph.set(normalizedPath, [])
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Read and extract deps from this file
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
140
|
+
const deps = extractDeps(content)
|
|
141
|
+
|
|
142
|
+
// Collect all relative dependencies
|
|
143
|
+
const relativeDeps = []
|
|
144
|
+
if (deps.requires && deps.requires.length > 0) {
|
|
145
|
+
relativeDeps.push(...deps.requires.filter(req => req.isRelative))
|
|
146
|
+
}
|
|
147
|
+
if (deps.imports && deps.imports.length > 0) {
|
|
148
|
+
relativeDeps.push(...deps.imports.filter(imp => imp.isRelative))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Process each relative dependency
|
|
152
|
+
const fileDir = path.dirname(filePath)
|
|
153
|
+
for (const dep of relativeDeps) {
|
|
154
|
+
const { from, line } = dep
|
|
155
|
+
|
|
156
|
+
// Resolve the import path
|
|
157
|
+
const resolvedPath = resolveImportPath(fileDir, from)
|
|
158
|
+
|
|
159
|
+
if (!resolvedPath) {
|
|
160
|
+
const error = new Error(
|
|
161
|
+
`"${line}" could not be resolved in ${filePath}\n\n` +
|
|
162
|
+
`Fix by ensuring "${path.resolve(fileDir, from)}" exists and is valid.`
|
|
163
|
+
)
|
|
164
|
+
if (throwOnFirstError) {
|
|
165
|
+
throw error
|
|
166
|
+
}
|
|
167
|
+
errors.push(error)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add edge to graph if building
|
|
172
|
+
if (graph !== null) {
|
|
173
|
+
const normalizedResolvedPath = fs.realpathSync(resolvedPath)
|
|
174
|
+
graph.get(normalizedPath).push(normalizedResolvedPath)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Verify imports if verification is enabled
|
|
178
|
+
if (verify) {
|
|
179
|
+
// Extract deps from the imported file
|
|
180
|
+
const targetContent = fs.readFileSync(resolvedPath, 'utf8')
|
|
181
|
+
const targetDeps = extractDeps(targetContent)
|
|
182
|
+
|
|
183
|
+
// Verify the imported values are exported
|
|
184
|
+
verifyImportedValuesExist(dep, targetDeps, resolvedPath, filePath, errors, throwOnFirstError)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Recursively process the imported file
|
|
188
|
+
buildGraphRecursively(resolvedPath, errors, throwOnFirstError, verify, graph, visited)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build dependency graph for a file and its dependencies
|
|
194
|
+
* @param {string} filePath - File to analyze
|
|
195
|
+
* @param {object} options - Options
|
|
196
|
+
* @param {boolean} [options.buildGraph] - If true, build and return dependency graph. Default: true
|
|
197
|
+
* @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
|
|
198
|
+
* @param {boolean} [options.throwOnFirstError] - If true, throw immediately on first error. Default: false
|
|
199
|
+
* @returns {Map<string, string[]>|undefined} - Dependency graph where keys are file paths and values are arrays of dependency paths. Returns undefined if buildGraph is false.
|
|
200
|
+
*/
|
|
201
|
+
function depGraph(filePath, options = {}) {
|
|
202
|
+
const { buildGraph = true, verifyGraph = true, throwOnFirstError = false } = options
|
|
203
|
+
const errors = []
|
|
204
|
+
const graph = buildGraph ? new Map() : null
|
|
205
|
+
|
|
206
|
+
buildGraphRecursively(filePath, errors, throwOnFirstError, verifyGraph, graph)
|
|
207
|
+
|
|
208
|
+
if (errors.length > 0) {
|
|
209
|
+
const errorMessage = errors.map((err, idx) => `Error ${idx + 1}:\n${err.message}`).join('\n\n')
|
|
210
|
+
throw new Error(`Found ${errors.length} validation error${errors.length === 1 ? '' : 's'}:\n\n${errorMessage}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (buildGraph) {
|
|
214
|
+
return graph
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all dependency file paths in build order (dependencies first), deduplicated
|
|
221
|
+
* @param {string} filePath - Entry file path
|
|
222
|
+
* @param {object} options - Options to pass to depGraph
|
|
223
|
+
* @returns {string[]} - Array of unique dependency file paths in topological order
|
|
224
|
+
*/
|
|
225
|
+
function resolveDepPaths(filePath, options = {}) {
|
|
226
|
+
const graph = depGraph(filePath, options)
|
|
227
|
+
|
|
228
|
+
// Calculate in-degree (number of dependencies) for each file
|
|
229
|
+
const inDegree = new Map()
|
|
230
|
+
for (const [file, deps] of graph) {
|
|
231
|
+
inDegree.set(file, deps.length)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Queue of files with no dependencies (can be processed first)
|
|
235
|
+
const queue = []
|
|
236
|
+
for (const [file, degree] of inDegree) {
|
|
237
|
+
if (degree === 0) {
|
|
238
|
+
queue.push(file)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const buildOrder = []
|
|
243
|
+
|
|
244
|
+
while (queue.length > 0) {
|
|
245
|
+
const file = queue.shift()
|
|
246
|
+
buildOrder.push(file)
|
|
247
|
+
|
|
248
|
+
// Find all files that depend on this file and decrement their in-degree
|
|
249
|
+
for (const [dependent, deps] of graph) {
|
|
250
|
+
if (deps.includes(file)) {
|
|
251
|
+
const newDegree = inDegree.get(dependent) - 1
|
|
252
|
+
inDegree.set(dependent, newDegree)
|
|
253
|
+
if (newDegree === 0) {
|
|
254
|
+
queue.push(dependent)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return buildOrder
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if (require.main === module) {
|
|
265
|
+
const entry = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
|
|
266
|
+
const graph = depGraph(entry)
|
|
267
|
+
console.log('graph', graph)
|
|
268
|
+
const depPaths = Array.from(graph.values()).flat()
|
|
269
|
+
console.log('depPaths', depPaths)
|
|
270
|
+
const depPathsProper = resolveDepPaths(entry)
|
|
271
|
+
console.log('depPathsProper', depPathsProper)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
depGraph,
|
|
276
|
+
resolveDepPaths
|
|
277
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Build and verify dependency graph
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { depGraph, resolveDepPaths } = require('../src/dep-graph.js')
|
|
6
|
+
|
|
7
|
+
const CJS_FIXTURE_PATH = path.resolve(__dirname, './fixtures/cjs-example/index.js')
|
|
8
|
+
const ESM_FIXTURE_PATH = path.resolve(__dirname, './fixtures/esm-fixture/index.js')
|
|
9
|
+
const CJS_DEEP_FIXTURE_PATH = path.resolve(__dirname, './fixtures/cjs-deep-example/index.js')
|
|
10
|
+
|
|
11
|
+
test('should build graph by default', () => {
|
|
12
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
13
|
+
assert.ok(graph instanceof Map, 'Should return a Map by default')
|
|
14
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('should verify by default', () => {
|
|
18
|
+
assert.not.throws(() => {
|
|
19
|
+
depGraph(CJS_FIXTURE_PATH)
|
|
20
|
+
}, 'Should verify imports by default')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('should build graph for valid CJS fixture', () => {
|
|
24
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
25
|
+
|
|
26
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
27
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
28
|
+
|
|
29
|
+
const entries = Array.from(graph.keys())
|
|
30
|
+
const hasIndexFile = entries.some(key => key.includes('cjs-example/index.js'))
|
|
31
|
+
assert.ok(hasIndexFile, 'Graph should include the index.js file')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should build graph for valid ESM fixture', () => {
|
|
35
|
+
const graph = depGraph(ESM_FIXTURE_PATH)
|
|
36
|
+
|
|
37
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
38
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
39
|
+
|
|
40
|
+
const entries = Array.from(graph.keys())
|
|
41
|
+
const hasIndexFile = entries.some(key => key.includes('esm-fixture/index.js'))
|
|
42
|
+
assert.ok(hasIndexFile, 'Graph should include the index.js file')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('should return undefined when buildGraph is false', () => {
|
|
46
|
+
const result = depGraph(CJS_FIXTURE_PATH, { buildGraph: false })
|
|
47
|
+
assert.is(result, undefined, 'Should return undefined when buildGraph is false')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('should build correct dependency graph for CJS fixture', () => {
|
|
51
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
52
|
+
|
|
53
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('cjs-example/index.js'))
|
|
54
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
55
|
+
|
|
56
|
+
const indexDeps = graph.get(indexPath)
|
|
57
|
+
assert.ok(Array.isArray(indexDeps), 'Dependencies should be an array')
|
|
58
|
+
assert.is(indexDeps.length, 1, 'index.js should have 1 dependency')
|
|
59
|
+
|
|
60
|
+
const hasOneDep = indexDeps.some(dep => dep.includes('one.js'))
|
|
61
|
+
assert.ok(hasOneDep, 'index.js should depend on one.js')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('should build correct dependency graph for ESM fixture', () => {
|
|
65
|
+
const graph = depGraph(ESM_FIXTURE_PATH)
|
|
66
|
+
|
|
67
|
+
const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-fixture/index.js'))
|
|
68
|
+
assert.ok(indexPath, 'Should have index.js in graph')
|
|
69
|
+
|
|
70
|
+
const indexDeps = graph.get(indexPath)
|
|
71
|
+
assert.ok(Array.isArray(indexDeps), 'Dependencies should be an array')
|
|
72
|
+
assert.is(indexDeps.length, 1, 'index.js should have 1 dependency')
|
|
73
|
+
|
|
74
|
+
const hasOneDep = indexDeps.some(dep => dep.includes('one.js'))
|
|
75
|
+
assert.ok(hasOneDep, 'index.js should depend on one.js')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should include all files in graph', () => {
|
|
79
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
80
|
+
|
|
81
|
+
const filePaths = Array.from(graph.keys())
|
|
82
|
+
|
|
83
|
+
// Should have index, one, two, three
|
|
84
|
+
assert.ok(filePaths.some(p => p.includes('index.js')), 'Should include index.js')
|
|
85
|
+
assert.ok(filePaths.some(p => p.includes('one.js')), 'Should include one.js')
|
|
86
|
+
assert.ok(filePaths.some(p => p.includes('two.js')), 'Should include two.js')
|
|
87
|
+
assert.ok(filePaths.some(p => p.includes('three.js')), 'Should include three.js')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('should handle files with no dependencies', () => {
|
|
91
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
92
|
+
|
|
93
|
+
const threePath = Array.from(graph.keys()).find(key => key.includes('three.js'))
|
|
94
|
+
assert.ok(threePath, 'Should have three.js in graph')
|
|
95
|
+
|
|
96
|
+
const threeDeps = graph.get(threePath)
|
|
97
|
+
assert.ok(Array.isArray(threeDeps), 'Dependencies should be an array')
|
|
98
|
+
assert.is(threeDeps.length, 0, 'three.js should have no dependencies')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should skip verification when verifyGraph is false', () => {
|
|
102
|
+
const graph = depGraph(CJS_FIXTURE_PATH, { verifyGraph: false })
|
|
103
|
+
|
|
104
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
105
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('should work with all options combined', () => {
|
|
109
|
+
const graph = depGraph(CJS_FIXTURE_PATH, {
|
|
110
|
+
buildGraph: true,
|
|
111
|
+
verifyGraph: false,
|
|
112
|
+
throwOnFirstError: false
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
assert.ok(graph instanceof Map, 'Should return a Map')
|
|
116
|
+
assert.ok(graph.size > 0, 'Graph should not be empty')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// resolveDepPaths tests
|
|
120
|
+
test('resolveDepPaths should return an array', () => {
|
|
121
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH)
|
|
122
|
+
assert.ok(Array.isArray(deps), 'Should return an array')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('resolveDepPaths should return deduplicated paths', () => {
|
|
126
|
+
const deps = resolveDepPaths(CJS_DEEP_FIXTURE_PATH)
|
|
127
|
+
const uniqueDeps = [...new Set(deps)]
|
|
128
|
+
|
|
129
|
+
assert.is(deps.length, uniqueDeps.length, 'Should not have duplicate paths')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('resolveDepPaths should include all files from graph', () => {
|
|
133
|
+
const graph = depGraph(CJS_FIXTURE_PATH)
|
|
134
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH)
|
|
135
|
+
|
|
136
|
+
assert.is(deps.length, graph.size, 'Should have same number of files as graph')
|
|
137
|
+
|
|
138
|
+
// All graph keys should be in deps
|
|
139
|
+
for (const key of graph.keys()) {
|
|
140
|
+
assert.ok(deps.includes(key), `Should include ${path.basename(key)}`)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('resolveDepPaths should return paths in topological order (dependencies first)', () => {
|
|
145
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH)
|
|
146
|
+
|
|
147
|
+
// Find indices
|
|
148
|
+
const threeIdx = deps.findIndex(p => p.includes('three.js'))
|
|
149
|
+
const twoIdx = deps.findIndex(p => p.includes('two.js'))
|
|
150
|
+
const oneIdx = deps.findIndex(p => p.includes('one.js'))
|
|
151
|
+
const indexIdx = deps.findIndex(p => p.includes('index.js'))
|
|
152
|
+
|
|
153
|
+
// three.js has no dependencies, should come first
|
|
154
|
+
assert.ok(threeIdx < twoIdx, 'three.js should come before two.js')
|
|
155
|
+
assert.ok(twoIdx < oneIdx, 'two.js should come before one.js')
|
|
156
|
+
assert.ok(oneIdx < indexIdx, 'one.js should come before index.js')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('resolveDepPaths should work with ESM fixtures', () => {
|
|
160
|
+
const deps = resolveDepPaths(ESM_FIXTURE_PATH)
|
|
161
|
+
|
|
162
|
+
assert.ok(Array.isArray(deps), 'Should return an array')
|
|
163
|
+
assert.ok(deps.length > 0, 'Should have dependencies')
|
|
164
|
+
|
|
165
|
+
// Check files are included
|
|
166
|
+
assert.ok(deps.some(p => p.includes('index.js')), 'Should include index.js')
|
|
167
|
+
assert.ok(deps.some(p => p.includes('one.js')), 'Should include one.js')
|
|
168
|
+
assert.ok(deps.some(p => p.includes('two.js')), 'Should include two.js')
|
|
169
|
+
assert.ok(deps.some(p => p.includes('three.js')), 'Should include three.js')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('resolveDepPaths should have entry point last', () => {
|
|
173
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH)
|
|
174
|
+
const lastFile = deps[deps.length - 1]
|
|
175
|
+
|
|
176
|
+
assert.ok(lastFile.includes('index.js'), 'Entry point (index.js) should be last')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('resolveDepPaths should have leaf nodes first', () => {
|
|
180
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH)
|
|
181
|
+
const firstFile = deps[0]
|
|
182
|
+
|
|
183
|
+
// three.js has no dependencies - it's a leaf node
|
|
184
|
+
assert.ok(firstFile.includes('three.js'), 'Leaf node (three.js) should be first')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('resolveDepPaths should handle complex graphs with shared dependencies', () => {
|
|
188
|
+
const deps = resolveDepPaths(CJS_DEEP_FIXTURE_PATH)
|
|
189
|
+
|
|
190
|
+
// Should be deduplicated
|
|
191
|
+
const uniqueDeps = [...new Set(deps)]
|
|
192
|
+
assert.is(deps.length, uniqueDeps.length, 'Should deduplicate shared dependencies')
|
|
193
|
+
|
|
194
|
+
// Find d-three.js which is shared by d-two.js, d-two-a.js, d-two-b.js
|
|
195
|
+
const threeIdx = deps.findIndex(p => p.includes('d-three.js'))
|
|
196
|
+
const twoIdx = deps.findIndex(p => p.includes('d-two.js'))
|
|
197
|
+
const twoAIdx = deps.findIndex(p => p.includes('d-two-a.js'))
|
|
198
|
+
const twoBIdx = deps.findIndex(p => p.includes('d-two-b.js'))
|
|
199
|
+
|
|
200
|
+
// d-three.js should come before all files that depend on it
|
|
201
|
+
assert.ok(threeIdx < twoIdx, 'd-three.js should come before d-two.js')
|
|
202
|
+
assert.ok(threeIdx < twoAIdx, 'd-three.js should come before d-two-a.js')
|
|
203
|
+
assert.ok(threeIdx < twoBIdx, 'd-three.js should come before d-two-b.js')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('resolveDepPaths should pass options to depGraph', () => {
|
|
207
|
+
const deps = resolveDepPaths(CJS_FIXTURE_PATH, { verifyGraph: false })
|
|
208
|
+
|
|
209
|
+
assert.ok(Array.isArray(deps), 'Should work with options')
|
|
210
|
+
assert.ok(deps.length > 0, 'Should return dependencies')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test.run()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build dependency graph for a file and its dependencies
|
|
3
|
+
* @param {string} filePath - File to analyze
|
|
4
|
+
* @param {object} options - Options
|
|
5
|
+
* @param {boolean} [options.buildGraph] - If true, build and return dependency graph. Default: true
|
|
6
|
+
* @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
|
|
7
|
+
* @param {boolean} [options.throwOnFirstError] - If true, throw immediately on first error. Default: false
|
|
8
|
+
* @returns {Map<string, string[]>|undefined} - Dependency graph where keys are file paths and values are arrays of dependency paths. Returns undefined if buildGraph is false.
|
|
9
|
+
*/
|
|
10
|
+
export function depGraph(filePath: string, options?: {
|
|
11
|
+
buildGraph?: boolean;
|
|
12
|
+
verifyGraph?: boolean;
|
|
13
|
+
throwOnFirstError?: boolean;
|
|
14
|
+
}): Map<string, string[]> | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Get all dependency file paths in build order (dependencies first), deduplicated
|
|
17
|
+
* @param {string} filePath - Entry file path
|
|
18
|
+
* @param {object} options - Options to pass to depGraph
|
|
19
|
+
* @returns {string[]} - Array of unique dependency file paths in topological order
|
|
20
|
+
*/
|
|
21
|
+
export function resolveDepPaths(filePath: string, options?: object): string[];
|
|
22
|
+
//# sourceMappingURL=dep-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dep-graph.d.ts","sourceRoot":"","sources":["../src/dep-graph.js"],"names":[],"mappings":"AA+LA;;;;;;;;GAQG;AACH,mCAPW,MAAM;IAEY,UAAU,GAA5B,OAAO;IACW,WAAW,GAA7B,OAAO;IACW,iBAAiB,GAAnC,OAAO;IACL,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC,GAAC,SAAS,CAiB3C;AAGD;;;;;GAKG;AACH,0CAJW,MAAM,YACN,MAAM,GACJ,MAAM,EAAE,CAsCpB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function getCjsExports(code: any): any[];
|
|
2
|
+
export function getModuleExportsAssignment(originalText?: string, sanitizedText?: string): {
|
|
3
|
+
line: string;
|
|
4
|
+
exportType: string;
|
|
5
|
+
exportName: string;
|
|
6
|
+
exportDefinitionName: any;
|
|
7
|
+
exportValue: string;
|
|
8
|
+
index: number;
|
|
9
|
+
isExport: boolean;
|
|
10
|
+
}[];
|
|
11
|
+
export function getModuleExportsProperty(originalText?: string, sanitizedText?: string): {
|
|
12
|
+
line: string;
|
|
13
|
+
exportType: string;
|
|
14
|
+
exportName: string;
|
|
15
|
+
exportValue: string;
|
|
16
|
+
index: number;
|
|
17
|
+
isExport: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
export function getExportsProperty(originalText?: string, sanitizedText?: string): {
|
|
20
|
+
line: string;
|
|
21
|
+
exportType: string;
|
|
22
|
+
exportName: string;
|
|
23
|
+
exportValue: string;
|
|
24
|
+
index: number;
|
|
25
|
+
isExport: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
export function getObjectAssignExports(originalText?: string, sanitizedText?: string): {
|
|
28
|
+
line: string;
|
|
29
|
+
exportType: string;
|
|
30
|
+
exportName: string;
|
|
31
|
+
exportValue: string;
|
|
32
|
+
index: number;
|
|
33
|
+
isExport: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
//# sourceMappingURL=get-cjs-exports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-cjs-exports.d.ts","sourceRoot":"","sources":["../src/get-cjs-exports.js"],"names":[],"mappings":"AA+BA,gDAuBC;AA+BD;;;;;;;;IAqGC;AAED;;;;;;;IAqBC;AAED;;;;;;;IAqBC;AAED;;;;;;;IAiDC"}
|
package/types/get-exports.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export function getExports(code: any, options?: {}): any[];
|
|
|
2
2
|
export function getDefaultExports(targetText?: string): {
|
|
3
3
|
line: string;
|
|
4
4
|
exportType: string;
|
|
5
|
-
exportName:
|
|
5
|
+
exportName: string;
|
|
6
|
+
exportDefinitionName: any;
|
|
6
7
|
exportValue: string;
|
|
7
8
|
index: number;
|
|
8
9
|
isExport: boolean;
|
|
@@ -42,6 +43,7 @@ export function getAssertionExports(targetText?: string): {
|
|
|
42
43
|
exportType: string;
|
|
43
44
|
exportName: string;
|
|
44
45
|
definitions: any;
|
|
46
|
+
keys: any;
|
|
45
47
|
from: string;
|
|
46
48
|
assertion: string;
|
|
47
49
|
assertionType: string;
|
|
@@ -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,2DA+BC;AAED;;;;;;;;IAqDC;AAED;;;;;;IAeC;AAED,uEAuBC;AAED;;;;;;IAeC;AAED;;;;;;;IAgBC;AAED;;;;;;IAeC;AAED;;;;;;;;;;;;IA0BC"}
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function extractDeps(code: any): {
|
|
1
|
+
export function extractDeps(code: any, filePath?: string): {
|
|
2
2
|
imports: {
|
|
3
3
|
line: string;
|
|
4
4
|
varType: string;
|
|
@@ -9,6 +9,7 @@ export function extractDeps(code: any): {
|
|
|
9
9
|
isImport: boolean;
|
|
10
10
|
isRelative: boolean;
|
|
11
11
|
}[];
|
|
12
|
+
exports: any[];
|
|
12
13
|
requires: {
|
|
13
14
|
line: string;
|
|
14
15
|
varType: string;
|
|
@@ -20,8 +21,11 @@ export function extractDeps(code: any): {
|
|
|
20
21
|
isRelative: boolean;
|
|
21
22
|
isDynamic: boolean;
|
|
22
23
|
}[];
|
|
23
|
-
|
|
24
|
+
moduleExports: any[];
|
|
24
25
|
};
|
|
26
|
+
import { hasESMSyntax } from "./detect-syntax";
|
|
27
|
+
import { hasCJSSyntax } from "./detect-syntax";
|
|
28
|
+
import { detectSyntax } from "./detect-syntax";
|
|
25
29
|
import { getRequires } from "./get-requires";
|
|
26
30
|
import { getDynamicRequires } from "./get-requires";
|
|
27
31
|
import { getStaticRequires } from "./get-requires";
|
|
@@ -35,5 +39,5 @@ import { getNamedDeclarationExports } from "./get-exports";
|
|
|
35
39
|
import { getAllExports } from "./get-exports";
|
|
36
40
|
import { getNamespaceExports } from "./get-exports";
|
|
37
41
|
import { getTypeExports } from "./get-exports";
|
|
38
|
-
export { getRequires, getDynamicRequires, getStaticRequires, getImports, getStaticImports, getDynamicImports, getExports, getDefaultExports, getNamedExports, getNamedDeclarationExports, getAllExports, getNamespaceExports, getTypeExports };
|
|
42
|
+
export { hasESMSyntax, hasCJSSyntax, detectSyntax, getRequires, getDynamicRequires, getStaticRequires, getImports, getStaticImports, getDynamicImports, getExports, getDefaultExports, getNamedExports, getNamedDeclarationExports, getAllExports, getNamespaceExports, getTypeExports };
|
|
39
43
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;;;;;;;;;;EAgBC"}
|
package/types/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export function isRelativeImport(importLocation: any): boolean;
|
|
2
2
|
export function isRelativeImportWithQuotes(importLocation: any): boolean;
|
|
3
|
-
export function getNames(names: any): any;
|
|
3
|
+
export function getNames(names: any, aliasKey?: string): any;
|
|
4
|
+
export function extractExportName(exportValue: any): any;
|
|
4
5
|
//# sourceMappingURL=utils.d.ts.map
|
package/types/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAEA,+DAEC;AAED,yEAKC;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAEA,+DAEC;AAED,yEAKC;AAgFD,6DA+DC;AAGD,yDAqCC"}
|