-
Notifications
You must be signed in to change notification settings - Fork 84
/
index.js
194 lines (163 loc) · 5.95 KB
/
index.js
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
'use strict';
const fs = require('node:fs');
const { debuglog } = require('node:util');
const cabinet = require('filing-cabinet');
const precinct = require('precinct');
const Config = require('./lib/config.js');
const debug = debuglog('tree');
/**
* Recursively find all dependencies (avoiding circular) traversing the entire dependency tree
* and returns a flat list of all unique, visited nodes
*
* @param {Object} options
* @param {String} options.filename - The path of the module whose tree to traverse
* @param {String} options.directory - The directory containing all JS files
* @param {String} [options.requireConfig] - The path to a requirejs config
* @param {String} [options.webpackConfig] - The path to a webpack config
* @param {String} [options.nodeModulesConfig] - config for resolving entry file for node_modules
* @param {Object} [options.visited] - Cache of visited, absolutely pathed files that should not be reprocessed.
* Format is a filename -> tree as list lookup table
* @param {Array} [options.nonExistent] - List of partials that do not exist
* @param {Boolean} [options.isListForm=false]
* @param {String|Object} [options.tsConfig] Path to a typescript config (or a preloaded one).
* @param {Boolean} [options.noTypeDefinitions] For TypeScript imports, whether to resolve to `*.js` instead of `*.d.ts`.
* @return {Object}
*/
module.exports = function(options = {}) {
const config = new Config(options);
if (!fs.existsSync(config.filename)) {
debug(`file ${config.filename} does not exist`);
return config.isListForm ? [] : {};
}
const results = traverse(config);
debug('traversal complete', results);
dedupeNonExistent(config.nonExistent);
debug('deduped list of nonExistent partials: ', config.nonExistent);
let tree;
if (config.isListForm) {
debug('list form of results requested');
tree = [...results];
} else {
debug('object form of results requested');
tree = {};
tree[config.filename] = results;
}
debug('final tree', tree);
return tree;
};
/**
* Executes a post-order depth first search on the dependency tree and returns a
* list of absolute file paths. The order of files in the list will be the
* proper concatenation order for bundling.
*
* In other words, for any file in the list, all of that file's dependencies (direct or indirect) will appear at
* lower indices in the list. The root (entry point) file will therefore appear last.
*
* The list will not contain duplicates.
*
* Params are those of module.exports
*/
module.exports.toList = function(options = {}) {
options.isListForm = true;
return module.exports(options);
};
/**
* Returns the list of dependencies for the given filename
*
* Protected for testing
*
* @param {Config} config
* @return {Array}
*/
module.exports._getDependencies = function(config = {}) {
const precinctOptions = config.detectiveConfig;
precinctOptions.includeCore = false;
let dependencies;
try {
dependencies = precinct.paperwork(config.filename, precinctOptions);
debug(`extracted ${dependencies.length} dependencies: `, dependencies);
} catch (error) {
debug(`error getting dependencies: ${error.message}`);
debug(error.stack);
return [];
}
const resolvedDependencies = [];
for (const dependency of dependencies) {
const result = cabinet({
partial: dependency,
filename: config.filename,
directory: config.directory,
ast: precinct.ast,
config: config.requireConfig,
webpackConfig: config.webpackConfig,
nodeModulesConfig: config.nodeModulesConfig,
tsConfig: config.tsConfig,
noTypeDefinitions: config.noTypeDefinitions
});
if (!result) {
debug(`skipping an empty filepath resolution for partial: ${dependency}`);
config.nonExistent.push(dependency);
continue;
}
const exists = fs.existsSync(result);
if (!exists) {
config.nonExistent.push(dependency);
debug(`skipping non-empty but non-existent resolution: ${result} for partial: ${dependency}`);
continue;
}
resolvedDependencies.push(result);
}
return resolvedDependencies;
};
/**
* @param {Config} config
* @return {Object|Set}
*/
function traverse(config = {}) {
const subTree = config.isListForm ? new Set() : {};
debug(`traversing ${config.filename}`);
if (config.visited[config.filename]) {
debug(`already visited ${config.filename}`);
return config.visited[config.filename];
}
let dependencies = module.exports._getDependencies(config);
debug('cabinet-resolved all dependencies: ', dependencies);
// Prevents cycles by eagerly marking the current file as read
// so that any dependent dependencies exit
config.visited[config.filename] = config.isListForm ? [] : {};
if (config.filter) {
debug('using filter function to filter out dependencies');
debug(`unfiltered number of dependencies: ${dependencies.length}`);
// eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference
dependencies = dependencies.filter(filePath => config.filter(filePath, config.filename));
debug(`filtered number of dependencies: ${dependencies.length}`);
}
for (const dependency of dependencies) {
const localConfig = config.clone();
localConfig.filename = dependency;
if (localConfig.isListForm) {
for (const item of traverse(localConfig)) {
subTree.add(item);
}
} else {
subTree[dependency] = traverse(localConfig);
}
}
if (config.isListForm) {
subTree.add(config.filename);
config.visited[config.filename].push(...subTree);
} else {
config.visited[config.filename] = subTree;
}
return subTree;
}
// Mutate the list input to do a dereferenced modification of the user-supplied list
function dedupeNonExistent(nonExistent) {
const deduped = new Set(nonExistent);
nonExistent.length = deduped.size;
let i = 0;
for (const elem of deduped) {
nonExistent[i] = elem;
i++;
}
}