-
-
Notifications
You must be signed in to change notification settings - Fork 250
Expand file tree
/
Copy pathspecUtils.ts
More file actions
258 lines (214 loc) · 9.45 KB
/
specUtils.ts
File metadata and controls
258 lines (214 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import {UsageError} from 'clipanion';
import fs from 'fs';
import path from 'path';
import semverSatisfies from 'semver/functions/satisfies';
import semverValid from 'semver/functions/valid';
import semverValidRange from 'semver/ranges/valid';
import {parseEnv} from 'util';
import {PreparedPackageManagerInfo} from './Engine';
import * as debugUtils from './debugUtils';
import {NodeError} from './nodeUtils';
import * as nodeUtils from './nodeUtils';
import {Descriptor, isSupportedPackageManager} from './types';
import type {LocalEnvFile} from './types';
const nodeModulesRegExp = /[\\/]node_modules[\\/](@[^\\/]*[\\/])?([^@\\/][^\\/]*)$/;
export function parseSpec(raw: unknown, source: string, {enforceExactVersion = true} = {}): Descriptor {
if (typeof raw !== `string`)
throw new UsageError(`Invalid package manager specification in ${source}; expected a string`);
const atIndex = raw.indexOf(`@`);
if (atIndex === -1 || atIndex === raw.length - 1) {
if (enforceExactVersion)
throw new UsageError(`No version specified for ${raw} in "packageManager" of ${source}`);
const name = atIndex === -1 ? raw : raw.slice(0, -1);
if (!isSupportedPackageManager(name))
throw new UsageError(`Unsupported package manager specification (${name})`);
return {
name, range: `*`,
};
}
const name = raw.slice(0, atIndex);
const range = raw.slice(atIndex + 1);
const isURL = URL.canParse(range);
if (!isURL) {
if (enforceExactVersion && !semverValid(range))
throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`);
if (!isSupportedPackageManager(name)) {
throw new UsageError(`Unsupported package manager specification (${raw})`);
}
} else if (isSupportedPackageManager(name) && process.env.COREPACK_ENABLE_UNSAFE_CUSTOM_URLS !== `1`) {
throw new UsageError(`Illegal use of URL for known package manager. Instead, select a specific version, or set COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=1 in your environment (${raw})`);
}
return {
name,
range,
};
}
type CorepackPackageJSON = {
packageManager?: string;
devEngines?: {packageManager?: DevEngineDependency};
};
interface DevEngineDependency {
name: string;
version: string;
onFail?: `ignore` | `warn` | `error`;
}
function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`]) {
switch (onFail) {
case `ignore`:
break;
case `error`:
case undefined:
throw new UsageError(errorMessage);
default:
console.warn(`! Corepack validation warning: ${errorMessage}`);
}
}
function parsePackageJSON({devEngines, packageManager}: CorepackPackageJSON) {
const spec = {
packageManager,
enforceExactVersion: true,
};
if (devEngines?.packageManager != null) {
const {packageManager: pm} = devEngines;
if (typeof pm !== `object`) {
console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(pm)}) will be ignored.`);
return spec;
}
if (Array.isArray(pm)) {
console.warn(`! Corepack does not currently support array values for devEngines.packageManager`);
return spec;
}
const {name, version, onFail} = pm;
if (typeof name !== `string` || name.includes(`@`)) {
warnOrThrow(`The value of devEngines.packageManager.name ${JSON.stringify(name)} is not a supported string value`, onFail);
return spec;
}
if (version != null && (typeof version !== `string` || !semverValidRange(version))) {
warnOrThrow(`The value of devEngines.packageManager.version ${JSON.stringify(version)} is not a valid semver range`, onFail);
return spec;
}
debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`);
if (packageManager) {
if (!packageManager.startsWith?.(`${name}@`))
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(packageManager)} which does not match the "devEngines.packageManager" field set to ${JSON.stringify(name)}`, onFail);
else if (version != null && !semverSatisfies(packageManager.slice(pm.name.length + 1), version))
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(packageManager)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail);
return spec;
}
return {
enforceExactVersion: semverValid(version),
packageManager: `${name}@${version ?? `*`}`,
};
}
return spec;
}
export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
const lookup = await loadSpec(cwd);
const range = `range` in lookup && lookup.range;
if (range) {
if (info.locator.name !== range.name || !semverSatisfies(info.locator.reference, range.range)) {
warnOrThrow(`The requested version of ${info.locator.name}@${info.locator.reference} does not match the devEngines specification (${range.name}@${range.range})`, range.onFail);
}
}
const content = lookup.type !== `NoProject`
? await fs.promises.readFile(lookup.target, `utf8`)
: ``;
const {data, indent} = nodeUtils.readPackageJson(content);
const previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`);
await fs.promises.writeFile(lookup.target, newContent, `utf8`);
return {
previousPackageManager,
};
}
interface FoundSpecResult {
type: `Found`;
target: string;
getSpec: () => Descriptor;
range?: Descriptor & {onFail?: DevEngineDependency[`onFail`]};
envFilePath?: string;
}
export type LoadSpecResult =
| {type: `NoProject`, target: string}
| {type: `NoSpec`, target: string}
| FoundSpecResult;
export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
let nextCwd = initialCwd;
let currCwd = ``;
let selection: {
data: any;
manifestPath: string;
envFilePath?: string;
localEnv: LocalEnvFile;
} | null = null;
while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) {
currCwd = nextCwd;
nextCwd = path.dirname(currCwd);
if (nodeModulesRegExp.test(currCwd))
continue;
const manifestPath = path.join(currCwd, `package.json`);
debugUtils.log(`Checking ${manifestPath}`);
let content: string;
try {
content = await fs.promises.readFile(manifestPath, `utf8`);
} catch (err) {
if ((err as NodeError)?.code === `ENOENT`) continue;
throw err;
}
let data;
try {
data = JSON.parse(content);
} catch {}
if (typeof data !== `object` || data === null)
throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`);
let localEnv: LocalEnvFile;
const envFilePath = path.resolve(currCwd, process.env.COREPACK_ENV_FILE ?? `.corepack.env`);
if (process.env.COREPACK_ENV_FILE == `0`) {
debugUtils.log(`Skipping env file as configured with COREPACK_ENV_FILE`);
localEnv = process.env;
} else if (typeof parseEnv !== `function`) {
// TODO: remove this block when support for Node.js 18.x is dropped.
debugUtils.log(`Skipping env file as it is not supported by the current version of Node.js`);
localEnv = process.env;
} else {
debugUtils.log(`Checking ${envFilePath}`);
try {
localEnv = {
...Object.fromEntries(Object.entries(parseEnv(await fs.promises.readFile(envFilePath, `utf8`))).filter(e => e[0].startsWith(`COREPACK_`))),
...process.env,
};
debugUtils.log(`Successfully loaded env file found at ${envFilePath}`);
} catch (err) {
if ((err as NodeError)?.code !== `ENOENT`)
throw err;
debugUtils.log(`No env file found at ${envFilePath}`);
localEnv = process.env;
}
}
selection = {data, manifestPath, localEnv, envFilePath};
}
if (selection === null)
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};
let envFilePath: string | undefined;
if (selection.localEnv !== process.env) {
envFilePath = selection.envFilePath;
process.env = selection.localEnv;
}
const {enforceExactVersion, packageManager} = parsePackageJSON(selection.data);
if (typeof packageManager === `undefined`)
return {type: `NoSpec`, target: selection.manifestPath};
debugUtils.log(`${selection.manifestPath} defines ${packageManager} as local package manager`);
return {
type: `Found`,
target: selection.manifestPath,
envFilePath,
range: selection.data.devEngines?.packageManager?.version && {
name: selection.data.devEngines.packageManager.name,
range: selection.data.devEngines.packageManager.version,
onFail: selection.data.devEngines.packageManager.onFail,
},
// Lazy-loading it so we do not throw errors on commands that do not need valid spec.
getSpec: () => parseSpec(packageManager, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}),
};
}