diff --git a/README.md b/README.md index e2adeb949..49e151715 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,11 @@ When run, this command will retrieve the latest release matching the provided descriptor, assign it to the project's package.json file, and automatically perform an install. +With `COREPACK_ENABLE_NETWORK=0`, omitting the version uses the default release +instead, such as the Last Known Good version selected by +`corepack install -g yarn@4.8.0`. The selected release must already be cached. +Explicit tags and version ranges still require registry access. + ### `corepack up` Retrieve the latest available version for the current major release line of diff --git a/sources/commands/Use.ts b/sources/commands/Use.ts index 508a2864e..895b7cf89 100644 --- a/sources/commands/Use.ts +++ b/sources/commands/Use.ts @@ -1,5 +1,7 @@ import {Command, Option, UsageError} from 'clipanion'; +import {isSupportedPackageManager} from '../types.ts'; + import {BaseCommand} from './Base.ts'; export class UseCommand extends BaseCommand { @@ -13,6 +15,9 @@ export class UseCommand extends BaseCommand { When run, this command will retrieve the latest release matching the provided descriptor, assign it to the project's package.json file, and automatically perform an install. + + When network access is disabled, omitting the version selects the default + release, which must already be cached. `, examples: [[ `Configure the project to use the latest Yarn release`, @@ -27,6 +32,10 @@ export class UseCommand extends BaseCommand { patterns: [this.pattern], }); + const versionIsOmitted = this.pattern === descriptor.name || this.pattern === `${descriptor.name}@`; + if (versionIsOmitted && process.env.COREPACK_ENABLE_NETWORK === `0` && isSupportedPackageManager(descriptor.name)) + descriptor.range = await this.context.engine.getDefaultVersion(descriptor.name); + const resolved = await this.context.engine.resolveDescriptor(descriptor, {allowTags: true, useCache: false}); if (resolved === null) throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); diff --git a/tests/Use.test.ts b/tests/Use.test.ts index 6dd37939e..766934c67 100644 --- a/tests/Use.test.ts +++ b/tests/Use.test.ts @@ -17,6 +17,135 @@ beforeEach(async () => { }); describe(`UseCommand`, () => { + describe(`default version selection`, () => { + const defaultPackageManager = `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`; + + beforeEach(async () => { + await xfs.mktempPromise(async cwd => { + await expect(runCli(cwd, [`install`, `-g`, defaultPackageManager])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + // The most recent cached version must not replace the selected default. + await expect(runCli(cwd, [`install`, `-g`, `--cache-only`, `yarn@2.4.3`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + }); + + delete process.env.COREPACK_DEFAULT_TO_LATEST; + }); + + it.each([`yarn`, `yarn@`])(`should use the default for %s when the network is disabled`, async pattern => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `yarn@2.4.3`, + license: `MIT`, + }); + + await expect(runCli(cwd, [`use`, pattern])).resolves.toMatchObject({ + exitCode: 0, + stdout: expect.stringContaining(`Installing ${defaultPackageManager} in the project...\n`), + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: defaultPackageManager, + }); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `1.22.4\n`, + stderr: ``, + }); + }); + }); + + it(`should read the network setting from .corepack.env before selecting the default`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {license: `MIT`}); + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), `COREPACK_ENABLE_NETWORK=0\n`); + + await expect(runCli(cwd, [`use`, `yarn`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: defaultPackageManager, + }); + }); + }); + + it.each([`yarn@*`, `yarn@latest`, `yarn@2`])(`should not replace an explicit %s with the default offline`, async pattern => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await xfs.mktempPromise(async cwd => { + const manifest = {packageManager: defaultPackageManager, license: `MIT`}; + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), manifest); + + await expect(runCli(cwd, [`use`, pattern])).resolves.toMatchObject({ + exitCode: 1, + stdout: expect.stringContaining(`Network access disabled by the environment`), + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toEqual(manifest); + }); + }); + + it(`should keep using an explicitly requested cached version offline`, async () => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {license: `MIT`}); + + await expect(runCli(cwd, [`use`, `yarn@2.4.3`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `2.4.3\n`, + stderr: ``, + }); + }); + }); + + it(`should not select an arbitrary cached version without a default offline`, async () => { + const home = npath.toPortablePath(process.env.COREPACK_HOME!); + await xfs.unlinkPromise(ppath.join(home, `lastKnownGood.json`)); + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await xfs.mktempPromise(async cwd => { + const manifest = {license: `MIT`}; + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), manifest); + + await expect(runCli(cwd, [`use`, `yarn`])).resolves.toMatchObject({ + exitCode: 1, + stdout: expect.stringContaining(`Network access disabled by the environment`), + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toEqual(manifest); + }); + }); + + it(`should still resolve the latest version when the network is enabled`, async () => { + process.env.YARN_ENABLE_IMMUTABLE_INSTALLS = `false`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {license: `MIT`}); + + await expect(runCli(cwd, [`use`, `yarn`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: expect.not.stringMatching(/^yarn@(?:1\.22\.4|2\.4\.3)\+/), + }); + }); + }); + }); + describe(`should set the package manager in the current project`, () => { it(`With an existing 'packageManager' field`, async () => { await xfs.mktempPromise(async cwd => {