Skip to content

Commit a92581c

Browse files
authored
Fix duplicate dependency resolving (codesandbox#468)
1 parent 5f6df9f commit a92581c

File tree

1 file changed

+140
-60
lines changed

1 file changed

+140
-60
lines changed

packages/app/src/sandbox/eval/npm/fetch-npm-module.js

Lines changed: 140 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ export function getCombinedMetas() {
3030
return combinedMetas;
3131
}
3232

33-
function normalize(depName: string, files: MetaFiles, fileObject: Meta = {}) {
33+
function normalize(
34+
depName: string,
35+
files: MetaFiles,
36+
fileObject: Meta = {},
37+
rootPath: string
38+
) {
3439
for (let i = 0; i < files.length; i += 1) {
3540
if (files[i].type === 'file') {
36-
const absolutePath = pathUtils.join(
37-
'/node_modules',
38-
depName,
39-
files[i].path
40-
);
41+
const absolutePath = pathUtils.join(rootPath, files[i].path);
4142
fileObject[absolutePath] = true; // eslint-disable-line no-param-reassign
4243
}
4344

4445
if (files[i].files) {
45-
normalize(depName, files[i].files, fileObject);
46+
normalize(depName, files[i].files, fileObject, rootPath);
4647
}
4748
}
4849

@@ -57,12 +58,7 @@ function getMeta(name: string, version: string) {
5758

5859
metas[id] = window
5960
.fetch(`https://unpkg.com/${name}@${version}/?meta`)
60-
.then(x => x.json())
61-
.then(metaInfo => normalize(name, metaInfo.files))
62-
.then(normalizedMetas => {
63-
combinedMetas = { ...combinedMetas, ...normalizedMetas };
64-
return normalizedMetas;
65-
});
61+
.then(x => x.json());
6662

6763
return metas[id];
6864
}
@@ -73,7 +69,9 @@ function downloadDependency(depName: string, depVersion: string, path: string) {
7369
}
7470

7571
const relativePath = path.replace(
76-
pathUtils.join('/node_modules', depName),
72+
new RegExp(
73+
`.*${pathUtils.join('/node_modules', depName)}`.replace('/', '\\/')
74+
),
7775
''
7876
);
7977

@@ -99,42 +97,13 @@ function downloadDependency(depName: string, depVersion: string, path: string) {
9997
return packages[path];
10098
}
10199

102-
function findDependencyVersion(manifest: Manifest, dependencyName: string) {
103-
let version = null;
104-
105-
if (manifest.dependencyDependencies[dependencyName]) {
106-
version = manifest.dependencyDependencies[dependencyName].resolved;
107-
} else {
108-
const dep = manifest.dependencies.find(m => m.name === dependencyName);
109-
110-
if (dep) {
111-
version = dep.version;
112-
}
113-
}
114-
115-
if (version) {
116-
return version;
117-
}
118-
119-
return null;
120-
}
121-
122-
export default async function fetchModule(
100+
function resolvePath(
123101
path: string,
124102
currentPath: string,
125103
manager: Manager,
126-
defaultExtensions: Array<string> = ['js', 'jsx', 'json']
127-
): Promise<Module> {
128-
const dependencyName = getDependencyName(path);
129-
130-
const version = findDependencyVersion(manager.manifest, dependencyName);
131-
132-
if (!version) {
133-
throw new DependencyNotFoundError(path);
134-
}
135-
136-
const meta = await getMeta(dependencyName, version);
137-
104+
defaultExtensions: Array<string> = ['js', 'jsx', 'json'],
105+
meta = {}
106+
): Promise<string> {
138107
return new Promise((res, reject) => {
139108
resolve(
140109
path,
@@ -145,22 +114,42 @@ export default async function fetchModule(
145114
'node_modules',
146115
manager.envVariables.NODE_PATH,
147116
].filter(x => x),
148-
isFile: (p, c) => c(null, !!manager.transpiledModules[p] || !!meta[p]),
117+
isFile: (p, c, cb) => {
118+
const callback = cb || c;
119+
120+
callback(null, !!manager.transpiledModules[p] || !!meta[p]);
121+
},
149122
readFile: async (p, c, cb) => {
150123
const callback = cb || c;
124+
151125
if (manager.transpiledModules[p]) {
152126
return callback(null, manager.transpiledModules[p].module.code);
153127
}
154128

155129
const depPath = p.replace('/node_modules/', '');
156130
const depName = getDependencyName(depPath);
157131

158-
const subDepVersion = findDependencyVersion(
159-
manager.manifest,
132+
// We don't try to download package.json, because we can assume that
133+
// all package.json files have been included in the bundle sent by
134+
// the packager.
135+
if (depPath.endsWith('package.json')) {
136+
const err = new Error('Could not find ' + p);
137+
err.code = 'ENOENT';
138+
139+
callback(err);
140+
return null;
141+
}
142+
143+
// eslint-disable-next-line
144+
const subDepVersionVersionInfo = await findDependencyVersion(
145+
currentPath,
146+
manager,
147+
defaultExtensions,
160148
depName
161149
);
162150

163-
if (subDepVersion) {
151+
if (subDepVersionVersionInfo) {
152+
const { version: subDepVersion } = subDepVersionVersionInfo;
164153
try {
165154
const module = await downloadDependency(
166155
depName,
@@ -192,16 +181,107 @@ export default async function fetchModule(
192181
return reject(err);
193182
}
194183

195-
if (resolvedPath === '//empty.js') {
196-
return res({
197-
path: '//empty.js',
198-
code: 'module.exports = {};',
199-
requires: [],
200-
});
201-
}
202-
203-
return res(downloadDependency(dependencyName, version, resolvedPath));
184+
return res(resolvedPath);
204185
}
205186
);
206187
});
207188
}
189+
190+
async function findDependencyVersion(
191+
currentPath: string,
192+
manager: Manager,
193+
defaultExtensions: Array<string> = ['js', 'jsx', 'json'],
194+
dependencyName: string
195+
) {
196+
const manifest = manager.manifest;
197+
198+
try {
199+
const foundPackageJSONPath = await resolvePath(
200+
pathUtils.join(dependencyName, 'package.json'),
201+
currentPath,
202+
manager,
203+
defaultExtensions
204+
);
205+
206+
const packageJSON =
207+
manager.transpiledModules[foundPackageJSONPath] &&
208+
manager.transpiledModules[foundPackageJSONPath].module.code;
209+
const { version } = JSON.parse(packageJSON);
210+
211+
if (packageJSON !== '//empty.js') {
212+
return { packageJSONPath: foundPackageJSONPath, version };
213+
}
214+
} catch (e) {
215+
/* do nothing */
216+
}
217+
218+
let version = null;
219+
220+
if (manifest.dependencyDependencies[dependencyName]) {
221+
version = manifest.dependencyDependencies[dependencyName].resolved;
222+
} else {
223+
const dep = manifest.dependencies.find(m => m.name === dependencyName);
224+
225+
if (dep) {
226+
version = dep.version;
227+
}
228+
}
229+
230+
if (version) {
231+
return { packageJSONPath: null, version };
232+
}
233+
234+
return null;
235+
}
236+
237+
export default async function fetchModule(
238+
path: string,
239+
currentPath: string,
240+
manager: Manager,
241+
defaultExtensions: Array<string> = ['js', 'jsx', 'json']
242+
): Promise<Module> {
243+
const dependencyName = getDependencyName(path);
244+
245+
const versionInfo = await findDependencyVersion(
246+
currentPath,
247+
manager,
248+
defaultExtensions,
249+
dependencyName
250+
);
251+
252+
if (!versionInfo) {
253+
throw new DependencyNotFoundError(path);
254+
}
255+
256+
const { packageJSONPath, version } = versionInfo;
257+
258+
const meta = await getMeta(dependencyName, version);
259+
260+
const normalizedMeta = normalize(
261+
dependencyName,
262+
meta.files,
263+
{},
264+
packageJSONPath
265+
? pathUtils.dirname(packageJSONPath)
266+
: pathUtils.join('/node_modules', dependencyName)
267+
);
268+
combinedMetas = { ...combinedMetas, ...normalizedMeta };
269+
270+
const foundPath = await resolvePath(
271+
path,
272+
currentPath,
273+
manager,
274+
defaultExtensions,
275+
normalizedMeta
276+
);
277+
278+
if (foundPath === '//empty.js') {
279+
return {
280+
path: '//empty.js',
281+
code: 'module.exports = {};',
282+
requires: [],
283+
};
284+
}
285+
286+
return downloadDependency(dependencyName, version, foundPath);
287+
}

0 commit comments

Comments
 (0)