@davidwells/extract-deps 0.0.3 → 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/src/get-cjs-exports.js +3 -30
- package/src/get-exports.js +38 -40
- package/src/utils.js +41 -1
- 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/tests/get-cjs-exports.test.js +53 -1
- package/tests/get-exports.test.js +162 -10
- 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
|
+
}
|