Skip to content

Commit 8fd9696

Browse files
authored
Replace O(N^2) Set-merge traversal with O(N) post-order DFS for list form (#213)
The previous list-form implementation built each node's result by iterating over every child's full transitive Set and re-adding items into the parent Set. For K files sharing a common package with M transitive deps this cost O(K*M) Set.add calls, and the spread push(...subTree) over a large Set risked a RangeError on very deep trees. The new traverseList / traverseListHelper pair does a single post-order DFS with one seen Set for cycle/duplicate detection, visiting each file exactly once and pushing it to the result array after its children - O(N) in unique files with identical output order.
1 parent 1f95299 commit 8fd9696

1 file changed

Lines changed: 48 additions & 18 deletions

File tree

index.js

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function dependencyTree(options = {}) {
2121
return config.isListForm ? [] : {};
2222
}
2323

24-
const results = traverse(config);
24+
const results = config.isListForm ? traverseList(config) : traverse(config);
2525
debug('traversal complete', results);
2626

2727
dedupeNonExistent(config.nonExistent);
@@ -30,7 +30,7 @@ function dependencyTree(options = {}) {
3030
let tree;
3131
if (config.isListForm) {
3232
debug('list form of results requested');
33-
tree = [...results];
33+
tree = results;
3434
} else {
3535
debug('object form of results requested');
3636
tree = {
@@ -112,10 +112,10 @@ function getDependencies(config) {
112112

113113
/**
114114
* @param {Config} config
115-
* @returns {Object|Set<string>}
115+
* @returns {Object}
116116
*/
117117
function traverse(config) {
118-
const subTree = config.isListForm ? new Set() : {};
118+
const subTree = {};
119119

120120
debug(`traversing ${config.filename}`);
121121

@@ -128,7 +128,7 @@ function traverse(config) {
128128

129129
debug('cabinet-resolved all dependencies: ', dependencies);
130130
// Eagerly mark the current file before recursing so any re-entrant visit exits early
131-
config.visited[config.filename] = config.isListForm ? [] : {};
131+
config.visited[config.filename] = {};
132132

133133
if (config.filter) {
134134
debug('using filter function to filter out dependencies');
@@ -142,24 +142,54 @@ function traverse(config) {
142142
const localConfig = config.clone();
143143
localConfig.filename = dependency;
144144
localConfig.directory = getLocalConfigDirectory(localConfig);
145+
subTree[dependency] = traverse(localConfig);
146+
}
145147

146-
if (localConfig.isListForm) {
147-
for (const item of traverse(localConfig)) {
148-
subTree.add(item);
149-
}
150-
} else {
151-
subTree[dependency] = traverse(localConfig);
152-
}
148+
config.visited[config.filename] = subTree;
149+
150+
return subTree;
151+
}
152+
153+
/**
154+
* @param {Config} config
155+
* @returns {Array<string>}
156+
*/
157+
function traverseList(config) {
158+
const result = [];
159+
traverseListHelper(config, result);
160+
return result;
161+
}
162+
163+
/**
164+
* @param {Config} config
165+
* @param {Array<string>} result
166+
*/
167+
function traverseListHelper(config, result) {
168+
if (config.visited[config.filename]) return;
169+
170+
// Eagerly mark the current file before recursing so any re-entrant visit exits early
171+
config.visited[config.filename] = [];
172+
173+
let dependencies = getDependencies(config);
174+
175+
debug('cabinet-resolved all dependencies: ', dependencies);
176+
177+
if (config.filter) {
178+
debug('using filter function to filter out dependencies');
179+
debug(`unfiltered number of dependencies: ${dependencies.length}`);
180+
// eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference
181+
dependencies = dependencies.filter(filePath => config.filter(filePath, config.filename));
182+
debug(`filtered number of dependencies: ${dependencies.length}`);
153183
}
154184

155-
if (config.isListForm) {
156-
subTree.add(config.filename);
157-
config.visited[config.filename].push(...subTree);
158-
} else {
159-
config.visited[config.filename] = subTree;
185+
for (const dependency of dependencies) {
186+
const localConfig = config.clone();
187+
localConfig.filename = dependency;
188+
localConfig.directory = getLocalConfigDirectory(localConfig);
189+
traverseListHelper(localConfig, result);
160190
}
161191

162-
return subTree;
192+
result.push(config.filename);
163193
}
164194

165195
// Dedupe in-place so the caller's array reference stays valid

0 commit comments

Comments
 (0)