Skip to content

Commit a2f8192

Browse files
committed
feat: add limited support for devEngines
1 parent 8163608 commit a2f8192

3 files changed

Lines changed: 142 additions & 12 deletions

File tree

sources/specUtils.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import {UsageError} from 'clipanion';
2-
import fs from 'fs';
3-
import path from 'path';
4-
import semverValid from 'semver/functions/valid';
5-
6-
import {PreparedPackageManagerInfo} from './Engine';
7-
import * as debugUtils from './debugUtils';
8-
import {NodeError} from './nodeUtils';
9-
import * as nodeUtils from './nodeUtils';
10-
import {Descriptor, isSupportedPackageManager} from './types';
1+
import {UsageError} from 'clipanion';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import semverSatisfies from 'semver/functions/satisfies';
5+
import semverValid from 'semver/functions/valid';
6+
import {parseEnv} from 'util';
7+
8+
import type {PreparedPackageManagerInfo} from './Engine';
9+
import * as debugUtils from './debugUtils';
10+
import type {NodeError} from './nodeUtils';
11+
import * as nodeUtils from './nodeUtils';
12+
import {isSupportedPackageManager} from './types';
13+
import type {LocalEnvFile, Descriptor} from './types';
1114

1215
const nodeModulesRegExp = /[\\/]node_modules[\\/](@[^\\/]*[\\/])?([^@\\/][^\\/]*)$/;
1316

@@ -52,6 +55,43 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t
5255
};
5356
}
5457

58+
type CorepackPackageJSON = {
59+
packageManager?: string;
60+
devEngines?: { packageManager?: DevEngineDependency };
61+
};
62+
63+
interface DevEngineDependency {
64+
name: string;
65+
version: string;
66+
}
67+
function parsePackageJSON(packageJSONContent: CorepackPackageJSON, localEnv?: LocalEnvFile) {
68+
if (packageJSONContent.devEngines?.packageManager) {
69+
const {packageManager} = packageJSONContent.devEngines;
70+
71+
if (Array.isArray(packageManager))
72+
throw new UsageError(`Providing several package managers is currently not supported`);
73+
74+
let {version} = packageManager;
75+
if (!version)
76+
throw new UsageError(`Providing no version nor ranger for package manager is currently not supported`);
77+
78+
const localEnvKey = `COREPACK_DEV_ENGINES_${packageManager.name.toUpperCase()}`;
79+
const localEnvVersion = localEnv?.[localEnvKey];
80+
if (localEnvVersion) {
81+
if (!semverSatisfies(localEnvVersion, version))
82+
throw new UsageError(`Local env key ${localEnvKey} defines a value of ${localEnvVersion} which does not match the version defined in package.json devEngines.packageManager of ${version}`);
83+
84+
debugUtils.log(`Using ${localEnvVersion} defined in .corepack.env`);
85+
version = localEnvVersion;
86+
}
87+
88+
89+
return `${packageManager.name}@${version}`;
90+
}
91+
92+
return packageJSONContent.packageManager;
93+
}
94+
5595
export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
5696
const lookup = await loadSpec(cwd);
5797

@@ -84,6 +124,7 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
84124
let selection: {
85125
data: any;
86126
manifestPath: string;
127+
localEnv?: LocalEnvFile;
87128
} | null = null;
88129

89130
while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) {
@@ -111,13 +152,24 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
111152
if (typeof data !== `object` || data === null)
112153
throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`);
113154

114-
selection = {data, manifestPath};
155+
let localEnv: LocalEnvFile | undefined;
156+
const envFilePath = path.join(currCwd, `.corepack.env`);
157+
debugUtils.log(`Checking ${envFilePath}`);
158+
try {
159+
localEnv = parseEnv(await fs.promises.readFile(envFilePath, `utf8`)) as LocalEnvFile;
160+
} catch (err) {
161+
if ((err as NodeError)?.code !== `ENOENT`) {
162+
throw err;
163+
}
164+
}
165+
166+
selection = {data, manifestPath, localEnv};
115167
}
116168

117169
if (selection === null)
118170
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};
119171

120-
const rawPmSpec = selection.data.packageManager;
172+
const rawPmSpec = parsePackageJSON(selection.data, selection.localEnv);
121173
if (typeof rawPmSpec === `undefined`)
122174
return {type: `NoSpec`, target: selection.manifestPath};
123175

sources/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,5 @@ export interface Locator {
145145
*/
146146
reference: string;
147147
}
148+
149+
export type LocalEnvFile = Record<"COREPACK_ENABLE_AUTO_PIN" | "COREPACK_ENABLE_STRICT" | "COREPACK_INTEGRITY_KEYS" | "COREPACK_PACKAGE_MANAGER", string>;

tests/main.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ for (const [name, version, expectedVersion = version.split(`+`, 1)[0]] of tested
150150
stderr: ``,
151151
stdout: `${expectedVersion}\n`,
152152
});
153+
154+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
155+
devEngines: {packageManager: {name, version}},
156+
});
157+
158+
await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject({
159+
exitCode: 0,
160+
stderr: ``,
161+
stdout: `${expectedVersion}\n`,
162+
});
153163
});
154164
});
155165
}
@@ -231,6 +241,72 @@ it(`should ignore the packageManager field when found within a node_modules vend
231241
});
232242
});
233243

244+
it(`should prefer devEngines to packageManager`, async () => {
245+
await xfs.mktempPromise(async cwd => {
246+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
247+
packageManager: `yarn@1.22.4`,
248+
devEngines: {
249+
packageManager: {
250+
name: `yarn`,
251+
version: `3.0.0-rc.2+sha224.f83f6d1cbfac10ba6b516a62ccd2a72ccd857aa6c514d1cd7185ec60`,
252+
},
253+
},
254+
});
255+
256+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
257+
exitCode: 0,
258+
stderr: ``,
259+
stdout: `3.0.0-rc.2\n`,
260+
});
261+
});
262+
});
263+
264+
it(`should accept range in devEngines only if a specific version is provided in .corepack.env`, async () => {
265+
await xfs.mktempPromise(async cwd => {
266+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
267+
devEngines: {
268+
packageManager: {
269+
name: `pnpm`,
270+
version: `6.x`,
271+
},
272+
},
273+
});
274+
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
275+
exitCode: 1,
276+
stderr: `Invalid package manager specification in package.json (pnpm@6.x); expected a semver version\n`,
277+
stdout: ``,
278+
});
279+
280+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as PortablePath),
281+
`COREPACK_DEV_ENGINES_PNPM=6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073\n`);
282+
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
283+
exitCode: 0,
284+
stderr: ``,
285+
stdout: `6.6.2\n`,
286+
});
287+
});
288+
});
289+
290+
it(`should reject if range in devEngines does not match version provided in .corepack.env`, async () => {
291+
await xfs.mktempPromise(async cwd => {
292+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
293+
devEngines: {
294+
packageManager: {
295+
name: `pnpm`,
296+
version: `10.x`,
297+
},
298+
},
299+
});
300+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as PortablePath),
301+
`COREPACK_DEV_ENGINES_PNPM=6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073\n`);
302+
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
303+
exitCode: 1,
304+
stderr: `Local env key COREPACK_DEV_ENGINES_PNPM defines a value of 6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073 which does not match the version defined in package.json devEngines.packageManager of 10.x\n`,
305+
stdout: ``,
306+
});
307+
});
308+
});
309+
234310
it(`should use the closest matching packageManager field`, async () => {
235311
await xfs.mktempPromise(async cwd => {
236312
await xfs.mkdirPromise(ppath.join(cwd, `foo` as PortablePath), {recursive: true});

0 commit comments

Comments
 (0)