Skip to content

Commit 7a9b6dd

Browse files
committed
Add local dependency graph resolving
1 parent aa263f0 commit 7a9b6dd

File tree

4 files changed

+336
-1
lines changed

4 files changed

+336
-1
lines changed

packages/app/src/sandbox/npm/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { pickBy } from 'lodash-es';
22

33
import fetchDependencies from './fetch-dependencies';
4+
import { getDependencyVersions } from '../version-resolving';
45
import dependenciesToQuery from './dependencies-to-query';
56

67
import setScreen from '../status-screen';
@@ -12,6 +13,9 @@ type NPMDependencies = {
1213
[dependency: string]: string,
1314
};
1415

16+
const DISABLE_EXTERNAL_CONNECTION =
17+
document.location.search.indexOf('disex') > -1;
18+
1519
/**
1620
* This fetches the manifest and dependencies from the
1721
* @param {*} dependencies
@@ -30,7 +34,9 @@ export default async function loadDependencies(dependencies: NPMDependencies) {
3034
if (loadedDependencyCombination !== depQuery) {
3135
isNewCombination = true;
3236

33-
const data = await fetchDependencies(dependenciesWithoutTypings);
37+
const data = await (DISABLE_EXTERNAL_CONNECTION
38+
? getDependencyVersions
39+
: fetchDependencies)(dependenciesWithoutTypings);
3440

3541
// Mark that the last requested url is this
3642
loadedDependencyCombination = depQuery;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { resolveDependencyInfo } from './resolve-dependency';
2+
import { mergeDependencies } from './merge-dependency';
3+
4+
export async function getDependencyVersions(dependencies) {
5+
const depInfos = await Promise.all(
6+
Object.keys(dependencies).map(depName =>
7+
resolveDependencyInfo(depName, dependencies[depName])
8+
)
9+
);
10+
11+
return mergeDependencies(depInfos);
12+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { uniq } from 'lodash';
2+
import * as semver from 'semver';
3+
4+
interface ILambdaResponse {
5+
contents: {
6+
[path: string]: string,
7+
};
8+
dependency: {
9+
name: string,
10+
version: string,
11+
};
12+
peerDependencies: {
13+
[dep: string]: string,
14+
};
15+
dependencyDependencies: {
16+
[dep: string]: {
17+
semver: string,
18+
resolved: string,
19+
parents: string[],
20+
entries: string[],
21+
},
22+
};
23+
dependencyAliases: {
24+
[dep: string]: {
25+
[dep: string]: string,
26+
},
27+
};
28+
}
29+
30+
interface IDepDepInfo {
31+
semver: string;
32+
resolved: string;
33+
parents: string[];
34+
entries: string[];
35+
}
36+
37+
interface IResponse {
38+
contents: { [path: string]: string };
39+
dependencies: Array<{ name: string, version: string }>;
40+
dependencyAliases: { [dep: string]: { [dep: string]: string } };
41+
dependencyDependencies: {
42+
[dep: string]: IDepDepInfo,
43+
};
44+
}
45+
46+
/**
47+
* Compare two sorted string arrays
48+
*
49+
* @param {string[]} s1
50+
* @param {string[]} s2
51+
* @returns
52+
*/
53+
function isEqual(s1: string[], s2: string[]) {
54+
if (s1.length !== s2.length) {
55+
return false;
56+
}
57+
58+
for (let i = 0; i < s1.length; i++) {
59+
if (s1[i] !== s2[i]) {
60+
return false;
61+
}
62+
}
63+
64+
return true;
65+
}
66+
67+
/**
68+
* Replaces the start of a key with a new string
69+
*
70+
* @param {{ [key: string]: string }} paths
71+
* @param {string} oldName
72+
* @param {string} newName
73+
*/
74+
function replacePaths(
75+
paths: { [key: string]: any },
76+
oldName: string,
77+
newName: string
78+
) {
79+
Object.keys(paths).forEach(al => {
80+
if (al.startsWith(`${oldName}/`) || al === oldName) {
81+
paths[al.replace(oldName, newName)] =
82+
typeof paths[al] === 'string'
83+
? paths[al].replace(oldName, newName)
84+
: paths[al];
85+
86+
delete paths[al];
87+
}
88+
});
89+
}
90+
91+
function replaceDependencyInfo(
92+
r: ILambdaResponse,
93+
depDepName: string,
94+
newDepDep: IDepDepInfo
95+
) {
96+
console.log(
97+
'Resolving conflict for ' +
98+
depDepName +
99+
' new version: ' +
100+
newDepDep.resolved
101+
);
102+
103+
const newPath = `${depDepName}/${newDepDep.resolved}`;
104+
105+
replacePaths(
106+
r.contents,
107+
`/node_modules/${depDepName}`,
108+
`/node_modules/${newPath}`
109+
);
110+
111+
r.dependencyDependencies[newPath] = r.dependencyDependencies[depDepName];
112+
delete r.dependencyDependencies[depDepName];
113+
114+
for (const n of Object.keys(r.dependencyDependencies)) {
115+
r.dependencyDependencies[n].parents = r.dependencyDependencies[
116+
n
117+
].parents.map(p => (p === depDepName ? newPath : p));
118+
}
119+
120+
r.dependencyAliases = r.dependencyAliases || {};
121+
newDepDep.parents.forEach(p => {
122+
r.dependencyAliases[p] = r.dependencyAliases[p] || {};
123+
r.dependencyAliases[p][depDepName] = newPath;
124+
});
125+
replacePaths(r.dependencyAliases, depDepName, newPath);
126+
}
127+
128+
const intersects = (v1: string, v2: string) => {
129+
try {
130+
return semver.intersects(v1, v2);
131+
} catch (e) {
132+
return false;
133+
}
134+
};
135+
136+
export function mergeDependencies(responses: ILambdaResponse[]) {
137+
// For consistency between requests
138+
const sortedResponses = responses.sort((a, b) =>
139+
a.dependency.name.localeCompare(b.dependency.name)
140+
);
141+
142+
const response: IResponse = {
143+
contents: {},
144+
dependencies: sortedResponses.map(r => r.dependency),
145+
dependencyAliases: {},
146+
dependencyDependencies: {},
147+
};
148+
149+
// eslint-disable-next-line
150+
for (const r of sortedResponses) {
151+
for (let i = 0; i < Object.keys(r.dependencyDependencies).length; i++) {
152+
const depDepName = Object.keys(r.dependencyDependencies)[i];
153+
154+
const newDepDep = r.dependencyDependencies[depDepName];
155+
const rootDependency = response.dependencies.find(
156+
d => d.name === depDepName
157+
);
158+
159+
if (
160+
rootDependency &&
161+
!intersects(rootDependency.version, newDepDep.semver) &&
162+
rootDependency.name !== r.dependency.name // and this dependency doesn't require an older version of itself
163+
) {
164+
// If a root dependency is in conflict with a child dependency, we always
165+
// go for the root dependency
166+
replaceDependencyInfo(r, depDepName, newDepDep);
167+
168+
// Start from the beginning, to make sure everything is correct
169+
i = -1;
170+
} else if (response.dependencyDependencies[depDepName]) {
171+
const exDepDep = response.dependencyDependencies[depDepName];
172+
173+
if (exDepDep.resolved === newDepDep.resolved) {
174+
exDepDep.parents = uniq([...exDepDep.parents, ...newDepDep.parents]);
175+
exDepDep.entries = uniq([...exDepDep.entries, ...newDepDep.entries]);
176+
} else if (
177+
intersects(exDepDep.semver, newDepDep.semver) &&
178+
isEqual(exDepDep.entries, newDepDep.entries)
179+
) {
180+
const replacingDepDep = semver.gt(
181+
newDepDep.resolved,
182+
exDepDep.resolved
183+
)
184+
? newDepDep
185+
: exDepDep;
186+
187+
response.dependencyDependencies[depDepName] = replacingDepDep;
188+
response.dependencyDependencies[depDepName].parents = uniq([
189+
...exDepDep.parents,
190+
...newDepDep.parents,
191+
]);
192+
} else {
193+
replaceDependencyInfo(r, depDepName, newDepDep);
194+
// Start from the beginning, to make sure everything is correct
195+
i = -1;
196+
}
197+
} else {
198+
response.dependencyDependencies[depDepName] =
199+
r.dependencyDependencies[depDepName];
200+
}
201+
}
202+
203+
response.dependencyAliases = {
204+
...response.dependencyAliases,
205+
...r.dependencyAliases,
206+
};
207+
response.contents = { ...response.contents, ...r.contents };
208+
}
209+
210+
return response;
211+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Cache from 'lru-cache';
2+
3+
const packageJSONCache: Cache.Cache<string, Promise<any>> = new Cache({
4+
max: 100,
5+
});
6+
7+
const ROOT_DOMAIN = 'https://unpkg.com';
8+
9+
function getPackageJSON(dep: string, version: string) {
10+
const cachedPromise = packageJSONCache.get(dep + version);
11+
if (cachedPromise) {
12+
return cachedPromise;
13+
}
14+
15+
const promise = fetch(`${ROOT_DOMAIN}/${dep}@${version}/package.json`).then(
16+
res => res.json()
17+
);
18+
packageJSONCache.set(dep + version, promise);
19+
20+
return promise;
21+
}
22+
23+
function getLatestVersionForSemver(dep: string, version: string) {
24+
return getPackageJSON(dep, version).then(p => p.version);
25+
}
26+
27+
interface IPeerDependencyResult {
28+
[dep: string]: {
29+
semver: string,
30+
resolved: string,
31+
parents: string[],
32+
entries: string[],
33+
};
34+
}
35+
36+
async function getDependencyDependencies(
37+
dep: string,
38+
version: string,
39+
peerDependencyResult: IPeerDependencyResult = {}
40+
): Promise<IPeerDependencyResult> {
41+
const packageJSON = await getPackageJSON(dep, version);
42+
43+
await Promise.all(
44+
Object.keys(packageJSON.dependencies || {}).map(async (depName: string) => {
45+
const depVersion = packageJSON.dependencies[depName];
46+
47+
if (peerDependencyResult[depName]) {
48+
peerDependencyResult[depName].parents.push(dep);
49+
return;
50+
}
51+
52+
const absoluteVersion = await getLatestVersionForSemver(
53+
depName,
54+
depVersion
55+
);
56+
57+
// eslint-disable-next-line
58+
peerDependencyResult[depName] = {
59+
semver: depVersion,
60+
resolved: absoluteVersion,
61+
parents: [dep],
62+
entries: [],
63+
};
64+
65+
await getDependencyDependencies(
66+
depName,
67+
depVersion,
68+
peerDependencyResult
69+
);
70+
})
71+
);
72+
73+
return peerDependencyResult;
74+
}
75+
76+
interface IResponse {
77+
contents: {};
78+
dependency: {
79+
name: string,
80+
version: string,
81+
};
82+
peerDependencies: { [name: string]: string };
83+
dependencyDependencies: IPeerDependencyResult;
84+
}
85+
86+
export async function resolveDependencyInfo(dep: string, version: string) {
87+
const response: IResponse = {
88+
contents: {},
89+
dependency: {
90+
name: dep,
91+
version,
92+
},
93+
peerDependencies: {},
94+
dependencyDependencies: {},
95+
};
96+
97+
const packageJSON = await getPackageJSON(dep, version);
98+
response.peerDependencies = packageJSON.peerDependencies;
99+
100+
response.dependencyDependencies = await getDependencyDependencies(
101+
dep,
102+
version
103+
);
104+
105+
return response;
106+
}

0 commit comments

Comments
 (0)