From afcca3d9d8a22521b2f5e3e435bca0a027ef18ad Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 21 Aug 2024 12:36:19 -0400 Subject: [PATCH 01/10] add `corepack project install` command --- sources/commands/Base.ts | 4 ++++ sources/commands/Project.ts | 46 +++++++++++++++++++++++++++++++++++++ sources/main.ts | 2 ++ tests/Project.test.ts | 9 ++++++++ 4 files changed, 61 insertions(+) create mode 100644 sources/commands/Project.ts create mode 100644 tests/Project.test.ts diff --git a/sources/commands/Base.ts b/sources/commands/Base.ts index ef7c0e4c8..9de7001aa 100644 --- a/sources/commands/Base.ts +++ b/sources/commands/Base.ts @@ -32,6 +32,10 @@ export abstract class BaseCommand extends Command { previousPackageManager, } = await specUtils.setLocalPackageManager(this.context.cwd, info); + await this.installLocalPackageManager(info, previousPackageManager); + } + + async installLocalPackageManager(info: PreparedPackageManagerInfo, previousPackageManager?: string) { const command = this.context.engine.getPackageManagerSpecFor(info.locator).commands?.use ?? null; if (command === null) return 0; diff --git a/sources/commands/Project.ts b/sources/commands/Project.ts new file mode 100644 index 000000000..3a746c40c --- /dev/null +++ b/sources/commands/Project.ts @@ -0,0 +1,46 @@ +import {Command, UsageError} from 'clipanion'; +import semverValid from 'semver/functions/valid'; +import semverValidRange from 'semver/ranges/valid'; + +import {BaseCommand} from './Base'; + +// modified from ./Enable.ts +// https://github.com/nodejs/corepack/issues/505 +export class ProjectInstallCommand extends BaseCommand { + static paths = [ + [`project`, `install`], + ]; + + static usage = Command.Usage({ + description: `Add the Corepack shims to the install directories, and run the install command of the specified package manager`, + details: ` + When run, this command will check whether the shims for the specified package managers can be found with the correct values inside the install directory. If not, or if they don't exist, they will be created. + + Then, it will run the install command of the specified package manager. If no package manager is specified, it will default to NPM. + + By default it will locate the install directory by running the equivalent of \`which corepack\`, but this can be tweaked by explicitly passing the install directory via the \`--install-directory\` flag. + `, + examples: [[ + `Enable all shims and install, putting shims next to the \`corepack\` binary`, + `$0 project install`, + ]], + }); + + async execute() { + const [descriptor] = await this.resolvePatternsToDescriptors({ + patterns: [], + }); + + if (!semverValid(descriptor.range) && !semverValidRange(descriptor.range)) + throw new UsageError(`The 'corepack project install' command can only be used when your project's packageManager field is set to a semver version or semver range`); + + const resolved = await this.context.engine.resolveDescriptor(descriptor, {useCache: true}); + if (!resolved) + throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); + + this.context.stdout.write(`Installing ${resolved.name}@${resolved.reference} in the project...\n`); + + const packageManagerInfo = await this.context.engine.ensurePackageManager(resolved); + await this.installLocalPackageManager(packageManagerInfo); + } +} diff --git a/sources/main.ts b/sources/main.ts index 8849e8832..c7ba88048 100644 --- a/sources/main.ts +++ b/sources/main.ts @@ -9,6 +9,7 @@ import {EnableCommand} from './commands/Enable'; import {InstallGlobalCommand} from './commands/InstallGlobal'; import {InstallLocalCommand} from './commands/InstallLocal'; import {PackCommand} from './commands/Pack'; +import {ProjectInstallCommand} from './commands/Project'; import {UpCommand} from './commands/Up'; import {UseCommand} from './commands/Use'; import {HydrateCommand} from './commands/deprecated/Hydrate'; @@ -62,6 +63,7 @@ export async function runMain(argv: Array) { cli.register(PackCommand); cli.register(UpCommand); cli.register(UseCommand); + cli.register(ProjectInstallCommand); // Deprecated commands cli.register(HydrateCommand); diff --git a/tests/Project.test.ts b/tests/Project.test.ts new file mode 100644 index 000000000..58ca3eb99 --- /dev/null +++ b/tests/Project.test.ts @@ -0,0 +1,9 @@ +import {describe, it} from 'vitest'; + +describe(`ProjectCommand`, () => { + describe(`InstallSubcommand`, () => { + it(`should add the binaries in the folder found in the PATH`, async () => { + // todo + }); + }); +}); From 4b929734806b2faeaacee14223bb284b68644b5b Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 21 Aug 2024 12:47:45 -0400 Subject: [PATCH 02/10] if --- sources/commands/Base.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sources/commands/Base.ts b/sources/commands/Base.ts index 9de7001aa..b85489e75 100644 --- a/sources/commands/Base.ts +++ b/sources/commands/Base.ts @@ -42,7 +42,9 @@ export abstract class BaseCommand extends Command { // Adding it into the environment avoids breaking package managers that // don't expect those options. - process.env.COREPACK_MIGRATE_FROM = previousPackageManager; + if (previousPackageManager) + process.env.COREPACK_MIGRATE_FROM = previousPackageManager; + this.context.stdout.write(`\n`); const [binaryName, ...args] = command; From 793bbb13437567cdd263f35280b7b8a14dd13228 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 21 Aug 2024 14:08:47 -0400 Subject: [PATCH 03/10] fix descirptions --- sources/commands/Project.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/commands/Project.ts b/sources/commands/Project.ts index 3a746c40c..01f4fadd6 100644 --- a/sources/commands/Project.ts +++ b/sources/commands/Project.ts @@ -18,7 +18,7 @@ export class ProjectInstallCommand extends BaseCommand { Then, it will run the install command of the specified package manager. If no package manager is specified, it will default to NPM. - By default it will locate the install directory by running the equivalent of \`which corepack\`, but this can be tweaked by explicitly passing the install directory via the \`--install-directory\` flag. + Tt will locate the install directory by running the equivalent of \`which corepack\`. `, examples: [[ `Enable all shims and install, putting shims next to the \`corepack\` binary`, @@ -34,7 +34,7 @@ export class ProjectInstallCommand extends BaseCommand { if (!semverValid(descriptor.range) && !semverValidRange(descriptor.range)) throw new UsageError(`The 'corepack project install' command can only be used when your project's packageManager field is set to a semver version or semver range`); - const resolved = await this.context.engine.resolveDescriptor(descriptor, {useCache: true}); + const resolved = await this.context.engine.resolveDescriptor(descriptor); if (!resolved) throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); From e2d65672199f215b1d5f3d0ab30b156c575db45b Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 21 Aug 2024 14:27:09 -0400 Subject: [PATCH 04/10] add tests --- tests/Project.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/Project.test.ts b/tests/Project.test.ts index 58ca3eb99..6309dd4c7 100644 --- a/tests/Project.test.ts +++ b/tests/Project.test.ts @@ -1,9 +1,75 @@ -import {describe, it} from 'vitest'; +import {ppath, xfs, npath} from '@yarnpkg/fslib'; +import process from 'node:process'; +import {describe, beforeEach, it, expect} from 'vitest'; + +import {runCli} from './_runCli'; + +beforeEach(async () => { + // `process.env` is reset after each tests in setupTests.js. + process.env.COREPACK_HOME = npath.fromPortablePath(await xfs.mktempPromise()); + process.env.COREPACK_DEFAULT_TO_LATEST = `0`; +}); describe(`ProjectCommand`, () => { describe(`InstallSubcommand`, () => { - it(`should add the binaries in the folder found in the PATH`, async () => { - // todo + it(`should install with npm`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `npm@10.8.2`, + dependencies: { + ms: `2.1.3`, + }, + }); + + await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + const dir = await xfs.readdirPromise(cwd); + expect(dir).toContain(`package-lock.json`); + expect(dir).toContain(`node_modules`); + }); + }); + + it(`should install with pnpm`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `pnpm@9.4.0`, + dependencies: { + ms: `2.1.3`, + }, + }); + + await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + const dir = await xfs.readdirPromise(cwd); + expect(dir).toContain(`pnpm-lock.yaml`); + expect(dir).toContain(`node_modules`); + }); + }); + + it(`should install with yarn`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `yarn@2.1.0`, + dependencies: { + ms: `2.1.3`, + }, + }); + + await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + const dir = await xfs.readdirPromise(cwd); + expect(dir).toContain(`yarn.lock`); + expect(dir).toContain(`.pnp.js`); + }); }); }); }); From 2f790c8993a63fd631d943acd1057c691ac0cbfb Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:57:57 -0400 Subject: [PATCH 05/10] Update Project.ts Co-authored-by: Steven --- sources/commands/Project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/commands/Project.ts b/sources/commands/Project.ts index 01f4fadd6..7232e2452 100644 --- a/sources/commands/Project.ts +++ b/sources/commands/Project.ts @@ -18,7 +18,7 @@ export class ProjectInstallCommand extends BaseCommand { Then, it will run the install command of the specified package manager. If no package manager is specified, it will default to NPM. - Tt will locate the install directory by running the equivalent of \`which corepack\`. + It will locate the install directory by running the equivalent of \`which corepack\`. `, examples: [[ `Enable all shims and install, putting shims next to the \`corepack\` binary`, From 1510e374dbc004b84ae3e261f9cb92273d028d11 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Sun, 25 Aug 2024 19:51:27 -0400 Subject: [PATCH 06/10] docs --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d94614aff..0d4edb597 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,18 @@ version range, as it will always select the latest available version from the 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}`). +### `corepack project install` + +Installs the package manager used in the local project, and then uses it to +install dependencies. + +This is roughly equivalent to running `corepack enable && npm install` for a +project using npm, or `corepack enable && pnpm install` for project using pnpm +and so on. + +It can be useful for writing scripts to run against arbitrary projects, when +the package manager is not known in advance. + ## Environment Variables - `COREPACK_DEFAULT_TO_LATEST` can be set to `0` in order to instruct Corepack From a5d870d6cef908b72422ecec90657309905a4e51 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 13 Sep 2024 14:45:34 -0400 Subject: [PATCH 07/10] use same binaries as other tests --- tests/Project.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Project.test.ts b/tests/Project.test.ts index 6309dd4c7..a8786877e 100644 --- a/tests/Project.test.ts +++ b/tests/Project.test.ts @@ -15,7 +15,7 @@ describe(`ProjectCommand`, () => { it(`should install with npm`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { - packageManager: `npm@10.8.2`, + packageManager: `npm@6.14.2`, dependencies: { ms: `2.1.3`, }, @@ -23,7 +23,7 @@ describe(`ProjectCommand`, () => { await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ exitCode: 0, - stderr: ``, + stderr: expect.stringContaining(`created a lockfile as package-lock.json`), }); const dir = await xfs.readdirPromise(cwd); @@ -35,7 +35,7 @@ describe(`ProjectCommand`, () => { it(`should install with pnpm`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { - packageManager: `pnpm@9.4.0`, + packageManager: `pnpm@5.8.0`, dependencies: { ms: `2.1.3`, }, @@ -55,7 +55,7 @@ describe(`ProjectCommand`, () => { it(`should install with yarn`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { - packageManager: `yarn@2.1.0`, + packageManager: `yarn@2.2.2`, dependencies: { ms: `2.1.3`, }, From ca88ec32d6af58b1ce8a349fdd59ffba11d8aa99 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 13 Sep 2024 14:56:01 -0400 Subject: [PATCH 08/10] fix tests + make em easier to debug on failure specifically: - assert that `stdout` doesn't contain the word "Error" - use the same npm/pnpm/yarn versions that are already used elsewhere, maybe some mocking is happening - add a license field - check for existence of node_modules/ms/package.json --- tests/Project.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/Project.test.ts b/tests/Project.test.ts index a8786877e..09933e788 100644 --- a/tests/Project.test.ts +++ b/tests/Project.test.ts @@ -16,6 +16,7 @@ describe(`ProjectCommand`, () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { packageManager: `npm@6.14.2`, + license: `MIT`, dependencies: { ms: `2.1.3`, }, @@ -23,12 +24,13 @@ describe(`ProjectCommand`, () => { await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ exitCode: 0, + stdout: expect.stringMatching(/^(?!.*Error).*$/s), stderr: expect.stringContaining(`created a lockfile as package-lock.json`), }); const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`package-lock.json`); - expect(dir).toContain(`node_modules`); + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) }); }); @@ -36,6 +38,7 @@ describe(`ProjectCommand`, () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { packageManager: `pnpm@5.8.0`, + license: `MIT`, dependencies: { ms: `2.1.3`, }, @@ -43,19 +46,21 @@ describe(`ProjectCommand`, () => { await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ exitCode: 0, + stdout: expect.stringMatching(/^(?!.*Error).*$/s), stderr: ``, }); const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`pnpm-lock.yaml`); - expect(dir).toContain(`node_modules`); + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) }); }); it(`should install with yarn`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { - packageManager: `yarn@2.2.2`, + packageManager: `yarn@1.22.4`, + license: `MIT`, dependencies: { ms: `2.1.3`, }, @@ -63,12 +68,13 @@ describe(`ProjectCommand`, () => { await expect(runCli(cwd, [`project`, `install`])).resolves.toMatchObject({ exitCode: 0, + stdout: expect.stringMatching(/^(?!.*Error).*$/s), stderr: ``, }); const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`yarn.lock`); - expect(dir).toContain(`.pnp.js`); + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) }); }); }); From 6a0aed2caf0bfd086ab33f929fb8663b1dd5da2a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 13 Sep 2024 14:56:15 -0400 Subject: [PATCH 09/10] fix npm casing --- sources/commands/Project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/commands/Project.ts b/sources/commands/Project.ts index 7232e2452..eada1bd27 100644 --- a/sources/commands/Project.ts +++ b/sources/commands/Project.ts @@ -16,7 +16,7 @@ export class ProjectInstallCommand extends BaseCommand { details: ` When run, this command will check whether the shims for the specified package managers can be found with the correct values inside the install directory. If not, or if they don't exist, they will be created. - Then, it will run the install command of the specified package manager. If no package manager is specified, it will default to NPM. + Then, it will run the install command of the specified package manager. If no package manager is specified, it will default to npm. It will locate the install directory by running the equivalent of \`which corepack\`. `, From 6ccc639bcb35ba51cd5641f4cfe7fc1ef00f93ff Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Oct 2024 13:53:10 -0400 Subject: [PATCH 10/10] lint --fix --- tests/Project.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Project.test.ts b/tests/Project.test.ts index 09933e788..5273866a8 100644 --- a/tests/Project.test.ts +++ b/tests/Project.test.ts @@ -30,7 +30,7 @@ describe(`ProjectCommand`, () => { const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`package-lock.json`); - expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true); }); }); @@ -52,7 +52,7 @@ describe(`ProjectCommand`, () => { const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`pnpm-lock.yaml`); - expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true); }); }); @@ -74,7 +74,7 @@ describe(`ProjectCommand`, () => { const dir = await xfs.readdirPromise(cwd); expect(dir).toContain(`yarn.lock`); - expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true) + expect(xfs.existsSync(ppath.join(cwd, `node_modules/ms/package.json`))).toBe(true); }); }); });