Skip to content

Commit 2717088

Browse files
committed
[FIX] npm t8r: Add deduplication of npm dependencies
1 parent 0378b77 commit 2717088

File tree

1 file changed

+78
-18
lines changed

1 file changed

+78
-18
lines changed

lib/translators/npm.js

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {promisify} = require("util");
66
const fs = require("fs");
77
const realpath = promisify(fs.realpath);
88
const resolveModulePath = promisify(require("resolve"));
9+
const parentNameRegExp = new RegExp(/:([^:]+):$/i);
910

1011
class NpmTranslator {
1112
constructor() {
@@ -17,12 +18,18 @@ class NpmTranslator {
1718
/*
1819
Returns a promise with an array of projects
1920
*/
20-
async processPkg(data, parentName) {
21+
async processPkg(data, parentPath) {
2122
const cwd = data.path;
2223
const moduleName = data.name;
2324
const pkg = data.pkg;
25+
const parentName = parentPath && this.getParentNameFromPath(parentPath) || "nothing - root project";
2426

25-
log.verbose("Analyzing %s (%s) (dependency of %s)", moduleName, cwd, parentName || "nothing - root project");
27+
log.verbose("Analyzing %s (%s) (dependency of %s)", moduleName, cwd, parentName);
28+
29+
if (!parentPath) {
30+
parentPath = ":";
31+
}
32+
parentPath += `${moduleName}:`;
2633

2734
/*
2835
* Inject collection definitions for some known projects
@@ -76,7 +83,7 @@ class NpmTranslator {
7683
if (!pkg.collection) {
7784
return this.getDepProjects({
7885
cwd,
79-
parentName: moduleName,
86+
parentPath,
8087
dependencies,
8188
optionalDependencies: pkg.optionalDependencies
8289
}).then((depProjects) => {
@@ -98,6 +105,7 @@ class NpmTranslator {
98105
// our dependencies later on.
99106
this.registerPendingDependencies({
100107
parentProject: project,
108+
parentPath,
101109
dependencies: optDependencies
102110
});
103111
return [project];
@@ -112,7 +120,7 @@ class NpmTranslator {
112120
log.verbose("Ignoring module with same name as parent: " + parentName);
113121
return null;
114122
}
115-
return this.readProject({modulePath, moduleName: depName, parentName: moduleName});
123+
return this.readProject({modulePath, moduleName: depName, parentPath});
116124
})
117125
).then((projects) => {
118126
// Array needs to be flattened because:
@@ -123,16 +131,25 @@ class NpmTranslator {
123131
}
124132
}
125133

126-
getDepProjects({cwd, dependencies, optionalDependencies, parentName}) {
134+
getParentNameFromPath(parentPath) {
135+
const parentNameMatch = parentPath.match(parentNameRegExp);
136+
if (parentNameMatch) {
137+
return parentNameMatch[1];
138+
} else {
139+
log.error(`Failed to get parent name from path ${parentPath}`);
140+
}
141+
}
142+
143+
getDepProjects({cwd, dependencies, optionalDependencies, parentPath}) {
127144
return Promise.all(
128145
Object.keys(dependencies).map((moduleName) => {
129146
return this.findModulePath(cwd, moduleName).then((modulePath) => {
130-
return this.readProject({modulePath, moduleName, parentName});
147+
return this.readProject({modulePath, moduleName, parentPath});
131148
}, (err) => {
132149
// Due to normalization done by by the "read-pkg-up" module the values
133-
// in optionalDependencies get added to dependencies. Also as described here:
150+
// in "optionalDependencies" get added to the modules "dependencies". Also described here:
134151
// https://github.com/npm/normalize-package-data#what-normalization-currently-entails
135-
// Resolution errors of optionalDependencies are being ignored
152+
// Ignore resolution errors for optional dependencies
136153
if (optionalDependencies && optionalDependencies[moduleName]) {
137154
return null;
138155
} else {
@@ -143,17 +160,32 @@ class NpmTranslator {
143160
).then((depProjects) => {
144161
// Array needs to be flattened because:
145162
// readProject returns an array + Promise.all returns an array = array filled with arrays
146-
// Filter out null values of ignored packages
163+
// Also filter out null values of ignored packages
147164
return Array.prototype.concat.apply([], depProjects.filter((p) => p !== null));
148165
});
149166
}
150167

151-
readProject({modulePath, moduleName, parentName}) {
168+
readProject({modulePath, moduleName, parentPath}) {
152169
if (this.projectCache[modulePath]) {
153-
return this.projectCache[modulePath];
170+
const cache = this.projectCache[modulePath];
171+
// Check whether modules has already been processed in the current subtree (indicates a loop)
172+
if (parentPath.indexOf(`:${moduleName}:`) !== -1) {
173+
// This is a loop => abort further processing
174+
return cache.pPkg.then((pkg) => {
175+
return [{
176+
id: moduleName,
177+
version: pkg.version,
178+
path: modulePath,
179+
dependencies: [],
180+
deduped: true
181+
}];
182+
});
183+
} else {
184+
return cache.pProject;
185+
}
154186
}
155187

156-
return this.projectCache[modulePath] = readPkg(modulePath).catch((err) => {
188+
const pPkg = readPkg(modulePath).catch((err) => {
157189
// Failed to read package
158190
// If dependency shim is available, fake the package
159191

@@ -171,12 +203,14 @@ class NpmTranslator {
171203
};
172204
}*/
173205
throw err;
174-
}).then((pkg) => {
206+
});
207+
208+
const pProject = pPkg.then((pkg) => {
175209
return this.processPkg({
176210
name: moduleName,
177211
pkg: pkg,
178212
path: modulePath
179-
}, parentName).then((projects) => {
213+
}, parentPath).then((projects) => {
180214
// Flatten the array of project arrays (yes, because collections)
181215
return Array.prototype.concat.apply([], projects.filter((p) => p !== null));
182216
});
@@ -189,6 +223,12 @@ class NpmTranslator {
189223
dependencies: []
190224
}];
191225
});
226+
227+
this.projectCache[modulePath] = {
228+
pPkg,
229+
pProject
230+
};
231+
return pProject;
192232
}
193233

194234
/* Returns path to a module
@@ -234,13 +274,19 @@ class NpmTranslator {
234274
});
235275
}
236276

237-
registerPendingDependencies({dependencies, parentProject}) {
277+
registerPendingDependencies({dependencies, parentProject, parentPath}) {
238278
Object.keys(dependencies).forEach((moduleName) => {
239279
if (this.pendingDeps[moduleName]) {
240-
this.pendingDeps[moduleName].parents.push(parentProject);
280+
this.pendingDeps[moduleName].parents.push({
281+
project: parentProject,
282+
path: parentPath
283+
});
241284
} else {
242285
this.pendingDeps[moduleName] = {
243-
parents: [parentProject]
286+
parents: [{
287+
project: parentProject,
288+
path: parentPath,
289+
}]
244290
};
245291
}
246292
});
@@ -263,7 +309,21 @@ class NpmTranslator {
263309

264310
if (this.pendingDeps[project.id]) {
265311
for (let i = this.pendingDeps[project.id].parents.length - 1; i >= 0; i--) {
266-
this.pendingDeps[project.id].parents[i].dependencies.push(project);
312+
const parent = this.pendingDeps[project.id].parents[i];
313+
// Check whether modules has already been processed in the current subtree (indicates a loop)
314+
if (parent.path.indexOf(`:${project.id}:`) !== -1) {
315+
// This is a loop => abort further processing
316+
const dedupedProject = {
317+
id: project.id,
318+
version: project.version,
319+
path: project.path,
320+
dependencies: [],
321+
deduped: true
322+
};
323+
parent.project.dependencies.push(dedupedProject);
324+
} else {
325+
parent.project.dependencies.push(project);
326+
}
267327
}
268328
this.pendingDeps[project.id] = null;
269329
}

0 commit comments

Comments
 (0)