-
Notifications
You must be signed in to change notification settings - Fork 340
/
file-visitor.mjs
118 lines (102 loc) · 3.26 KB
/
file-visitor.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// @ts-check
import { existsSync, readFileSync, statSync } from 'fs'
import { dirname, join, parse } from 'path'
import ts from 'typescript'
import { DependencyGraph } from './dependency-graph.mjs'
/**
* tries to resolve a relative javascript module based on its specifier
* @param {string} moduleSpecifier
* @returns {(string|null)}
*/
export const resolveRelativeModule = (moduleSpecifier) => {
if (existsSync(moduleSpecifier) && statSync(moduleSpecifier).isFile()) {
return moduleSpecifier
}
if (existsSync(`${moduleSpecifier}.js`)) {
return `${moduleSpecifier}.js`
}
if (existsSync(`${moduleSpecifier}/index.js`)) {
return `${moduleSpecifier}/index.js`
}
return null
}
/**
* Parses the dependencies out of a file
* @param {string} fileName
* @param {import('./types.d').VisitorState} state
* @param {any} parent
*/
export const fileVisitor = function (fileName, state, parent) {
if (!state) {
state = { graph: new DependencyGraph(), visitorPlugins: [] }
}
if (state.graph.hasFile(fileName)) {
// if the visitor was called with a parent we only need to add the dependency
if (parent) {
state.graph.addDependency(parent, fileName)
}
// no need to traverse the file again
return
}
const folder = dirname(fileName)
const fileContent = readFileSync(fileName, 'utf-8')
const sourceFile = ts.createSourceFile(fileName, fileContent, ts.ScriptTarget.ES2020, true, ts.ScriptKind.JS)
/**
* Resolves a javascript import location
* @param {string} importLocation
* @returns {(string|null)}
*/
const resolveLocation = function (importLocation) {
const parsed = parse(importLocation)
// absolute paths don't need to be resolved
if (parsed.root) {
return importLocation
}
if (importLocation.startsWith('.')) {
return resolveRelativeModule(join(folder, importLocation))
}
// TODO: Ignored node_modules for now maybe add them later as they might be useful
}
/**
* Visits a import or require statement location
* @param {string} moduleSpecifier
*/
const visitDependency = (moduleSpecifier) => {
if (moduleSpecifier.startsWith('.')) {
const resolvedImportLocation = resolveLocation(moduleSpecifier)
if (resolvedImportLocation) {
fileVisitor(resolvedImportLocation, state, fileName)
}
}
}
/**
* Visits a typescript node
* @type {ts.Visitor}
*/
const visitor = function (node) {
// TODO: once we need import specifiers (esm or typescript add them here)
if (ts.isCallExpression(node) && node.expression.getText() === 'require' && ts.isStringLiteral(node.arguments[0])) {
visitDependency(node.arguments[0].text)
}
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
visitDependency(node.moduleSpecifier.text)
}
// go to the plugins
state.visitorPlugins.forEach((plugin) => {
const file = plugin(node)
if (file) {
fileVisitor(file, state, fileName)
}
})
node.getChildren().forEach((childNode) => {
ts.visitNode(childNode, visitor)
})
}
// start visiting the sourceFile
ts.visitNode(sourceFile, visitor)
// add node to graph
state.graph.addFile(fileName)
if (parent) {
state.graph.addDependency(parent, fileName)
}
}