-
-
Notifications
You must be signed in to change notification settings - Fork 211
feat: add limited support for devEngines
#643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b36c7f0
073440e
e9f6adf
6754521
b25cc77
32cca55
8a53d7d
21f9fec
a86f6d3
54be297
bf09e9a
b5c28a8
8994e0a
81419f3
b6e8a4f
c982089
e28e3c0
573b688
a952550
32407e7
f761471
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,6 +113,35 @@ use in the archive). | |
} | ||
``` | ||
|
||
#### `devEngines.packageManager` | ||
|
||
When a `devEngines.packageManager` field is defined, and is an object containing | ||
a `"name"` field (can also optionally contain `version` and `onFail` fields), | ||
Corepack will use it to validate you're using a compatible package manager. | ||
|
||
Depending on the value of `devEngines.packageManager.onFail`: | ||
|
||
- if set to `ignore`, Corepack won't print any warning or error. | ||
- if unset or set to `error`, Corepack will throw an error in case of a mismatch. | ||
- if set to `warn` or some other value, Corepack will print a warning in case | ||
of mismatch. | ||
|
||
If the top-level `packageManager` field is missing, Corepack will use the | ||
package manager defined in `devEngines.packageManager` – in which case you must | ||
provide a specific version in `devEngines.packageManager.version`, ideally with | ||
a hash, as explained in the previous section: | ||
|
||
```json | ||
{ | ||
"devEngines":{ | ||
"packageManager": { | ||
"name": "yarn", | ||
"version": "3.2.3+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## Known Good Releases | ||
|
||
When running Corepack within projects that don't list a supported package | ||
|
@@ -246,6 +275,7 @@ it. | |
|
||
Unlike `corepack use` this command doesn't take a package manager name nor a | ||
version range, as it will always select the latest available version from the | ||
range specified in `devEngines.packageManager.version`, or fallback to the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about top-level There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what |
||
same major line. Should you need to upgrade to a new major, use an explicit | ||
`corepack use {name}@latest` call (or simply `corepack use {name}`). | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
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 {PreparedPackageManagerInfo} from './Engine'; | ||
import * as debugUtils from './debugUtils'; | ||
|
@@ -52,16 +54,87 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t | |
}; | ||
} | ||
|
||
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(packageJSONContent: CorepackPackageJSON) { | ||
const {packageManager: pm} = packageJSONContent; | ||
if (packageJSONContent.devEngines?.packageManager != null) { | ||
aduh95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const {packageManager} = packageJSONContent.devEngines; | ||
|
||
if (typeof packageManager !== `object`) { | ||
console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(packageManager)}) will be ignored.`); | ||
return pm; | ||
} | ||
if (Array.isArray(packageManager)) { | ||
console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); | ||
return pm; | ||
} | ||
|
||
const {name, version, onFail} = packageManager; | ||
if (typeof name !== `string` || name.includes(`@`)) { | ||
warnOrThrow(`The value of devEngines.packageManager.name ${JSON.stringify(name)} is not a supported string value`, onFail); | ||
return pm; | ||
} | ||
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 pm; | ||
} | ||
|
||
debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); | ||
|
||
if (pm) { | ||
if (!pm.startsWith?.(`${name}@`)) | ||
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the "devEngines.packageManager" field set to ${JSON.stringify(name)}`, onFail); | ||
|
||
else if (version != null && !semverSatisfies(pm.slice(packageManager.name.length + 1), version)) | ||
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail); | ||
|
||
return pm; | ||
} | ||
|
||
|
||
return `${name}@${version ?? `*`}`; | ||
} | ||
|
||
return pm; | ||
} | ||
|
||
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 ?? `unknown`; | ||
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`); | ||
|
@@ -75,7 +148,7 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM | |
export type LoadSpecResult = | ||
| {type: `NoProject`, target: string} | ||
| {type: `NoSpec`, target: string} | ||
| {type: `Found`, target: string, getSpec: () => Descriptor}; | ||
| {type: `Found`, target: string, getSpec: () => Descriptor, range?: Descriptor & {onFail?: DevEngineDependency['onFail']}}; | ||
|
||
export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> { | ||
let nextCwd = initialCwd; | ||
|
@@ -117,13 +190,20 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> { | |
if (selection === null) | ||
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)}; | ||
|
||
const rawPmSpec = selection.data.packageManager; | ||
const rawPmSpec = parsePackageJSON(selection.data); | ||
if (typeof rawPmSpec === `undefined`) | ||
return {type: `NoSpec`, target: selection.manifestPath}; | ||
|
||
debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`); | ||
|
||
return { | ||
type: `Found`, | ||
target: selection.manifestPath, | ||
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(rawPmSpec, path.relative(initialCwd, selection.manifestPath)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,24 +17,119 @@ beforeEach(async () => { | |
}); | ||
|
||
describe(`UpCommand`, () => { | ||
it(`should upgrade the package manager from the current project`, async () => { | ||
await xfs.mktempPromise(async cwd => { | ||
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { | ||
packageManager: `[email protected]`, | ||
describe(`should update the "packageManager" field from the current project`, () => { | ||
it(`to the same major if no devEngines range`, async () => { | ||
await xfs.mktempPromise(async cwd => { | ||
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { | ||
packageManager: `[email protected]`, | ||
}); | ||
|
||
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stderr: ``, | ||
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), | ||
}); | ||
|
||
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ | ||
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, | ||
}); | ||
|
||
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stdout: `2.4.3\n`, | ||
stderr: ``, | ||
}); | ||
}); | ||
}); | ||
|
||
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stderr: ``, | ||
it(`to whichever range devEngines defines`, async () => { | ||
await xfs.mktempPromise(async cwd => { | ||
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { | ||
packageManager: `[email protected]`, | ||
devEngines: { | ||
packageManager: { | ||
name: `yarn`, | ||
version: `1.x || 2.x`, | ||
}, | ||
}, | ||
}); | ||
|
||
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stderr: ``, | ||
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), | ||
}); | ||
|
||
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ | ||
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, | ||
}); | ||
|
||
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stdout: `2.4.3\n`, | ||
stderr: ``, | ||
}); | ||
}); | ||
}); | ||
|
||
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ | ||
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, | ||
it(`to whichever range devEngines defines even if onFail is set to ignore`, async () => { | ||
await xfs.mktempPromise(async cwd => { | ||
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { | ||
packageManager: `[email protected]`, | ||
devEngines: { | ||
packageManager: { | ||
name: `yarn`, | ||
version: `1.x || 2.x`, | ||
onFail: `ignore`, | ||
}, | ||
}, | ||
}); | ||
|
||
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stderr: ``, | ||
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), | ||
}); | ||
|
||
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ | ||
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, | ||
}); | ||
|
||
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stdout: `2.4.3\n`, | ||
stderr: ``, | ||
}); | ||
}); | ||
}); | ||
|
||
it(`should succeed even if no 'packageManager' field`, async () => { | ||
await xfs.mktempPromise(async cwd => { | ||
process.env.NO_COLOR = `1`; | ||
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { | ||
devEngines: { | ||
packageManager: { | ||
name: `yarn`, | ||
version: `1.x || 2.x`, | ||
}, | ||
}, | ||
}); | ||
|
||
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stderr: ``, | ||
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), | ||
}); | ||
|
||
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ | ||
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, | ||
}); | ||
|
||
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stdout: `2.4.3\n`, | ||
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ | ||
exitCode: 0, | ||
stdout: `2.4.3\n`, | ||
stderr: ``, | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the first sentence of this section.
Because it sounds like this
devEngines.packageManager
is ignored when there is a top levelpackageManager
, right?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's never ignored. As said in the first paragraph of this section, if it is defined to something Corepack recognises, it will throw/warn/do nothing (depending of the value of
onFail
) if the user is trying to use an incompatible package manager (i.e. if the one defined inpackage.json#packageManager
does not match the one defined inpackage.json#devEngines.packageManager
for when the user is using a Corepack shim)