Skip to content

Commit aa67e12

Browse files
authored
Add support for version resolution (codesandbox#2924)
1 parent 7155426 commit aa67e12

File tree

10 files changed

+152
-18
lines changed

10 files changed

+152
-18
lines changed

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"lru-cache": "^4.1.3",
144144
"match-sorter": "^1.8.1",
145145
"memoize-one": "^4.0.0",
146+
"minimatch": "^3.0.4",
146147
"mobx": "^5.11.0",
147148
"mobx-react": "^6.1.1",
148149
"mobx-react-lite": "^1.4.1",

packages/app/src/sandbox/compile.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,10 @@ async function compile({
485485

486486
const { manifest, isNewCombination } = await loadDependencies(
487487
dependencies,
488-
disableDependencyPreprocessing
488+
{
489+
disableExternalConnection: disableDependencyPreprocessing,
490+
resolutions: parsedPackageJSON.resolutions,
491+
}
489492
);
490493

491494
if (isNewCombination && !firstLoad) {

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CSB_PKG_PROTOCOL } from '@codesandbox/common/lib/utils/ci';
33
import resolve from 'browser-resolve';
44
import DependencyNotFoundError from 'sandbox-hooks/errors/dependency-not-found-error';
55

6+
import delay from 'sandbox/utils/delay';
67
import { Module } from '../entities/module';
78
import Manager from '../manager';
89

@@ -114,7 +115,7 @@ const urlProtocols = {
114115
},
115116
};
116117

117-
async function fetchWithRetries(url: string, retries = 3): Promise<Response> {
118+
async function fetchWithRetries(url: string, retries = 6): Promise<Response> {
118119
const doFetch = () =>
119120
window.fetch(url).then(x => {
120121
if (x.ok) {
@@ -124,8 +125,15 @@ async function fetchWithRetries(url: string, retries = 3): Promise<Response> {
124125
throw new Error(`Could not fetch ${url}`);
125126
});
126127

128+
let lastTryTime = 0;
127129
for (let i = 0; i < retries; i++) {
130+
if (Date.now() - lastTryTime < 3000) {
131+
// Ensure that we at least wait 3s before retrying a request to prevent rate limits
132+
// eslint-disable-next-line
133+
await delay(3000 - (Date.now() - lastTryTime));
134+
}
128135
try {
136+
lastTryTime = Date.now();
129137
// eslint-disable-next-line
130138
return await doFetch();
131139
} catch (e) {
@@ -174,9 +182,11 @@ async function getMeta(
174182
}
175183

176184
const protocol = getFetchProtocol(version);
177-
const metaUrl = await protocol.meta(nameWithoutAlias, version);
178185

179-
metas[id] = fetchWithRetries(metaUrl).then(x => x.json());
186+
metas[id] = protocol
187+
.meta(nameWithoutAlias, version)
188+
.then(fetchWithRetries)
189+
.then(x => x.json());
180190

181191
return metas[id];
182192
}
@@ -202,9 +212,10 @@ export async function downloadDependency(
202212

203213
const nameWithoutAlias = depName.replace(ALIAS_REGEX, '');
204214
const protocol = getFetchProtocol(depVersion);
205-
const url = await protocol.file(nameWithoutAlias, depVersion, relativePath);
206215

207-
packages[id] = fetchWithRetries(url)
216+
packages[id] = protocol
217+
.file(nameWithoutAlias, depVersion, relativePath)
218+
.then(fetchWithRetries)
208219
.then(x => x.text())
209220
.catch(async () => {
210221
const fallbackProtocol = getFetchProtocol(depVersion, true);

packages/app/src/sandbox/npm/fetch-dependencies.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ async function getDependencies(dependencies: Object) {
145145
}
146146
}
147147

148-
export async function fetchDependencies(npmDependencies: Dependencies) {
148+
export async function fetchDependencies(
149+
npmDependencies: Dependencies,
150+
resolutions?: { [key: string]: string }
151+
) {
149152
if (Object.keys(npmDependencies).length !== 0) {
150153
// New Packager flow
151154

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type NPMDependencies = {
1414
};
1515

1616
/**
17-
* If there is a URL to a file we need to fetch the dependenices dynamically, at least
17+
* If there is a URL to a file we need to fetch the dependencies dynamically, at least
1818
* for the first version. In the future we might want to consider a hybrid version where
1919
* we only fetch the dynamic files for dependencies with a url as version. But this is a good
2020
* start.
@@ -31,7 +31,7 @@ function shouldFetchDynamically(dependencies: NPMDependencies) {
3131
*/
3232
export async function loadDependencies(
3333
dependencies: NPMDependencies,
34-
disableExternalConnection = false
34+
{ disableExternalConnection = false, resolutions = undefined } = {}
3535
) {
3636
let isNewCombination = false;
3737
if (Object.keys(dependencies).length !== 0) {
@@ -54,7 +54,7 @@ export async function loadDependencies(
5454
? getDependencyVersions
5555
: fetchDependencies;
5656

57-
const data = await fetchFunction(dependenciesWithoutTypings);
57+
const data = await fetchFunction(dependenciesWithoutTypings, resolutions);
5858

5959
// Mark that the last requested url is this
6060
loadedDependencyCombination = depQuery;

packages/app/src/sandbox/version-resolving/index.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { resolveDependencyInfo } from './resolve-dependency';
22
import { mergeDependencies } from './merge-dependency';
33

4-
export async function getDependencyVersions(dependencies: {
5-
[depName: string]: string;
6-
}) {
4+
import { parseResolutions } from './resolutions';
5+
6+
export async function getDependencyVersions(
7+
dependencies: {
8+
[depName: string]: string;
9+
},
10+
resolutions?: { [startGlob: string]: string }
11+
) {
12+
const parsedResolutions = parseResolutions(resolutions);
13+
714
const depInfos = await Promise.all(
815
Object.keys(dependencies).map(depName =>
9-
resolveDependencyInfo(depName, dependencies[depName])
16+
resolveDependencyInfo(depName, dependencies[depName], parsedResolutions)
1017
)
1118
);
1219

packages/app/src/sandbox/version-resolving/merge-dependency.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export function mergeDependencies(responses: ILambdaResponse[]) {
163163
if (
164164
rootDependency &&
165165
!intersects(rootDependency.version, newDepDep.semver) &&
166+
rootDependency.version !== newDepDep.resolved &&
166167
rootDependency.name !== r.dependency.name // and this dependency doesn't require an older version of itself
167168
) {
168169
// If a root dependency is in conflict with a child dependency, we always
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Parse input strings like `package-1/package-2` to an array of packages
3+
*/
4+
function parsePackagePath(input: string) {
5+
return input.match(/(@[^/]+\/)?([^/]+)/g) || [];
6+
}
7+
8+
const WRONG_PATTERNS = /\/$|\/{2,}|\*+$/;
9+
const GLOBAL_NESTED_DEP_PATTERN = '**/';
10+
11+
function isValidPackagePath(input: string) {
12+
return !WRONG_PATTERNS.test(input);
13+
}
14+
15+
export interface IParsedResolution {
16+
name: string;
17+
range: string;
18+
globPattern: string;
19+
pattern: string;
20+
}
21+
22+
export function parsePatternInfo(
23+
globPattern: string,
24+
range: string
25+
): IParsedResolution {
26+
if (!isValidPackagePath(globPattern)) {
27+
console.warn('invalidResolutionName');
28+
return null;
29+
}
30+
31+
const directories = parsePackagePath(globPattern);
32+
const name = directories.pop();
33+
34+
// For legacy support of resolutions, replace `name` with `**/name`
35+
if (name === globPattern) {
36+
// eslint-disable-next-line
37+
globPattern = `${GLOBAL_NESTED_DEP_PATTERN}${name}`;
38+
}
39+
40+
return {
41+
name,
42+
range,
43+
globPattern,
44+
pattern: `${name}@${range}`,
45+
};
46+
}
47+
48+
export function parseResolutions(resolutions?: {
49+
[name: string]: string;
50+
}): IParsedResolution[] {
51+
if (!resolutions) {
52+
return [];
53+
}
54+
55+
const keys = Object.keys(resolutions);
56+
return keys.map(key => parsePatternInfo(key, resolutions[key]));
57+
}

packages/app/src/sandbox/version-resolving/resolve-dependency.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import minimatch from 'minimatch';
2+
import * as semver from 'semver';
3+
14
import { ILambdaResponse } from './merge-dependency';
25
import { downloadDependency } from '../eval/npm/fetch-npm-module';
6+
import { IParsedResolution } from './resolutions';
37

48
function getPackageJSON(dep: string, version: string) {
59
return downloadDependency(dep, version, '/package.json').then(m => m.code);
@@ -18,9 +22,39 @@ interface IPeerDependencyResult {
1822
};
1923
}
2024

25+
function getAbsoluteVersion(
26+
originalDep: string,
27+
depName: string,
28+
depVersion: string,
29+
parsedResolutions: { [name: string]: IParsedResolution[] }
30+
) {
31+
// Try getting it from the resolutions field first, if that doesn't work
32+
// we try to get the latest version from the semver.
33+
const applicableResolutions = parsedResolutions[depName];
34+
if (applicableResolutions) {
35+
const modulePath = [originalDep, depName].join('/');
36+
37+
const { range } =
38+
applicableResolutions.find(({ globPattern }) =>
39+
minimatch(modulePath, globPattern)
40+
) || {};
41+
42+
if (range) {
43+
if (semver.valid(range)) {
44+
return getLatestVersionForSemver(depName, range);
45+
}
46+
47+
return range;
48+
}
49+
}
50+
51+
return getLatestVersionForSemver(depName, depVersion);
52+
}
53+
2154
async function getDependencyDependencies(
2255
dep: string,
2356
version: string,
57+
parsedResolutions: { [name: string]: IParsedResolution[] },
2458
peerDependencyResult: IPeerDependencyResult = {}
2559
): Promise<IPeerDependencyResult> {
2660
const packageJSONCode = await getPackageJSON(dep, version);
@@ -35,9 +69,11 @@ async function getDependencyDependencies(
3569
return;
3670
}
3771

38-
const absoluteVersion = await getLatestVersionForSemver(
72+
const absoluteVersion = await getAbsoluteVersion(
73+
dep,
3974
depName,
40-
depVersion
75+
depVersion,
76+
parsedResolutions
4177
);
4278

4379
// eslint-disable-next-line
@@ -51,6 +87,7 @@ async function getDependencyDependencies(
5187
await getDependencyDependencies(
5288
depName,
5389
depVersion,
90+
parsedResolutions,
5491
peerDependencyResult
5592
);
5693
})
@@ -59,7 +96,11 @@ async function getDependencyDependencies(
5996
return peerDependencyResult;
6097
}
6198

62-
export async function resolveDependencyInfo(dep: string, version: string) {
99+
export async function resolveDependencyInfo(
100+
dep: string,
101+
version: string,
102+
parsedResolutions: IParsedResolution[]
103+
) {
63104
const packageJSONCode = await getPackageJSON(dep, version);
64105
const packageJSON = JSON.parse(packageJSONCode);
65106
const response: ILambdaResponse = {
@@ -73,10 +114,17 @@ export async function resolveDependencyInfo(dep: string, version: string) {
73114
dependencyAliases: {},
74115
};
75116

117+
const resolutionsByPackage = {};
118+
parsedResolutions.forEach(res => {
119+
resolutionsByPackage[res.name] = resolutionsByPackage[res.name] || [];
120+
resolutionsByPackage[res.name].push(res);
121+
});
122+
76123
response.peerDependencies = packageJSON.peerDependencies || {};
77124
response.dependencyDependencies = await getDependencyDependencies(
78125
dep,
79-
version
126+
version,
127+
resolutionsByPackage
80128
);
81129

82130
response.contents = {

packages/common/src/templates/template.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export type ParsedConfigurationFiles = {
3333
main: string;
3434
dependencies?: Dependencies;
3535
devDependencies: Dependencies;
36+
resolutions?: {
37+
[source: string]: string;
38+
};
3639
[otherProperties: string]: any | undefined;
3740
}>;
3841
[path: string]: ParsedConfigurationFile<any> | undefined;

0 commit comments

Comments
 (0)