diff --git a/package.json b/package.json index 893ade321c..a3e3feba22 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,13 @@ "lint": "npm run lint:js && npm run lint:markdown && npm run lint:markdown-js && npm run lint:markdown-links", "lint:fix": "prettier --write . --experimental-cli && eslint --fix . --cache", "prepack": "yarn build", - "test": "xvfb-maybe vitest run --project fast --project slow", + "test": "xvfb-maybe vitest run --project fast --project slow --project slow-verdaccio", "test:fast": "xvfb-maybe vitest run --project fast", "test:slow": "xvfb-maybe vitest run --project slow", "test:verdaccio": "tsx tools/verdaccio/spawn-verdaccio.ts xvfb-maybe vitest run --project slow-verdaccio", "test:clear": "tsx tools/test-clear", - "postinstall": "husky install && node -e \"try { fs.rmSync('node_modules/.bin/*.ps1', { recursive: true, force: true }) } catch (e) {}\" && tsx ./tools/gen-tsconfigs.ts && tsx ./tools/gen-ts-glue.ts" + "postinstall": "husky install && node -e \"try { fs.rmSync('node_modules/.bin/*.ps1', { recursive: true, force: true }) } catch (e) {}\" && tsx ./tools/gen-tsconfigs.ts && tsx ./tools/gen-ts-glue.ts", + "spawn-verdaccio": "tsx tools/verdaccio/spawn-verdaccio.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.654.0", diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index f25ac25739..a3dadb6e27 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -33,6 +33,10 @@ program '--electron-version [version]', 'Set a specific Electron version for your Forge project. Can take in a version string (e.g. `38.3.0`) or `latest`, `beta`, or `nightly` tags.', ) + .option( + '--package-manager [name]', + 'Set a specific package manager to use for your Forge project. Supported package managers are `npm`, `pnpm`, and `yarn`. You can also specify an exact version to use (e.g. `yarn@1.22.22`).', + ) .action(async (dir) => { const options = program.opts(); const tasks = new Listr( @@ -46,11 +50,11 @@ program initOpts.skipGit = Boolean(options.skipGit); initOpts.dir = resolveWorkingDir(dir, false); initOpts.electronVersion = options.electronVersion ?? 'latest'; + initOpts.packageManager = options.packageManager ?? 'npm@latest'; }, }, { task: async (initOpts, task): Promise => { - // only run interactive prompts if no args passed and not in CI environment if ( Object.keys(options).length > 0 || process.env.CI || @@ -79,6 +83,18 @@ program } } + const packageManager: string = await prompt.run< + Prompt + >(select, { + message: 'Select a package manager', + choices: [ + { name: 'npm', value: 'npm@latest' }, + { name: 'pnpm', value: 'pnpm@latest' }, + { name: 'Yarn (Berry)', value: 'yarn@latest' }, + { name: 'Yarn (Classic)', value: 'yarn@1' }, + ], + }); + const bundler: string = await prompt.run>( select, { @@ -121,6 +137,7 @@ program ); } + initOpts.packageManager = packageManager; initOpts.template = `${bundler}${language ? `-${language}` : ''}`; // TODO: add prompt for passing in an exact version as well diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts index 2bd85e9d10..0c3e963283 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts @@ -214,18 +214,11 @@ describe('init', () => { describe('package managers', () => { describe('with npm', () => { beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'npm'; - process.env.COREPACK_ENABLE_STRICT = '0'; - - return () => { - process.env.NODE_INSTALLER = originalPM; - }; }); it('initializes with package-lock.json', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'npm' }); expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBe(true); expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBe(false); @@ -236,15 +229,8 @@ describe('init', () => { // NOTE: we basically run all tests via Yarn Berry anyways // due to the `packageManager` entry in this monorepo. describe('with yarn (berry)', () => { - beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'yarn'; - return () => { - process.env.NODE_INSTALLER = originalPM; - }; - }); it('initializes with correct nodeLinker value', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'yarn@4.10.3' }); expect( fs.readFileSync(path.join(dir, '.yarnrc.yml'), 'utf-8'), @@ -270,19 +256,12 @@ describe('init', () => { describe('with pnpm', () => { beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'pnpm'; - // disable corepack strict to allow pnpm to be used process.env.COREPACK_ENABLE_STRICT = '0'; - - return () => { - process.env.NODE_INSTALLER = originalPM; - }; }); it('initializes with correct node-linker value', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'pnpm' }); expect(fs.readFileSync(path.join(dir, '.npmrc'), 'utf-8')).toContain( 'node-linker = hoisted', diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index cafcab0bc8..c2c726565b 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import { PMDetails, resolvePackageManager } from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; +import { spawn } from '@malept/cross-spawn-promise'; import chalk from 'chalk'; import debug from 'debug'; import { Listr } from 'listr2'; @@ -54,6 +55,10 @@ export interface InitOptions { * @defaultValue The `latest` tag on npm. */ electronVersion?: string; + /** + * Force a package manager to use (npm|yarn|pnpm). + */ + packageManager?: string; } async function validateTemplate( @@ -84,6 +89,7 @@ export default async ({ template = 'base', skipGit = false, electronVersion = 'latest', + packageManager, }: InitOptions): Promise => { d(`Initializing in: ${dir}`); @@ -96,8 +102,8 @@ export default async ({ { title: `Resolving package manager`, task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(); - task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)} v${ctx.pm.version}`; + ctx.pm = await resolvePackageManager(packageManager); + task.title = `Resolved package manager: ${chalk.cyan(`${ctx.pm.executable}@${ctx.pm.version}`)}`; }, }, { @@ -169,6 +175,24 @@ export default async ({ } }, }, + { + title: `Setting package manager with Corepack`, + // pm.executable needs to be optional here because the code gets evaluated twice (on init and on execution) + // @see https://listr2.kilic.dev/task/enable.html + enabled: ({ pm }) => pm?.executable !== 'npm', + task: async ({ pm }, task) => { + const pmString = `${pm.executable}@${pm.version}`; + try { + await spawn('corepack', ['use', pmString], { + cwd: dir, + }); + task.title = `Set ${chalk.cyan(pmString)} via Corepack`; + } catch (e) { + d('corepack failed to run with error', e); + task.title = `Forge was unable to set ${chalk.cyan(pmString)} via Corepack and will fall back to ${chalk.cyan('npm')}. If you are using Node.js >= 25, you will need to install corepack via ${chalk.green('npm install -g corepack')}. Otherwise, you may need to enable Corepack shims via ${chalk.green('corepack enable')}.`; + } + }, + }, { title: 'Installing template dependencies', task: async ({ templateModule }, task) => { diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 5bc5ad32af..61af6e3a01 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -76,7 +76,7 @@ export class BaseTemplate implements ForgeTemplate { // Support Yarn 2+ by default by initializing with nodeLinker: node-modules pm.executable === 'yarn' && pm.version && - semver.gte(pm.version, '2.0.0') + (pm.version === 'latest' || semver.gte(pm.version, '2.0.0')) ) { rootFiles.push('_yarnrc.yml'); } @@ -145,13 +145,6 @@ export class BaseTemplate implements ForgeTemplate { packageJSON.pnpm = { onlyBuiltDependencies: ['electron', 'electron-winstaller'], }; - } else if ( - pm.executable === 'yarn' && - typeof pm.version === 'string' && - semver.gte(pm.version, '2.0.0') - ) { - d('Detected Yarn 2+, adding packageManager field to package.json'); - packageJSON.packageManager = `yarn@${pm.version}`; } packageJSON.scripts.lint = 'echo "No linting configured"'; diff --git a/packages/utils/core-utils/spec/electron-version.spec.ts b/packages/utils/core-utils/spec/electron-version.spec.ts index c164814bc2..c724cc8932 100644 --- a/packages/utils/core-utils/spec/electron-version.spec.ts +++ b/packages/utils/core-utils/spec/electron-version.spec.ts @@ -117,8 +117,11 @@ describe('getElectronVersion', () => { }); describe('with yarn workspaces', () => { + let originalUserAgent: string | undefined; beforeAll(() => { - process.env.NODE_INSTALLER = 'yarn'; + originalUserAgent = process.env.npm_config_user_agent; + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; }); it('works with a non-exact version', async () => { @@ -139,7 +142,7 @@ describe('getElectronVersion', () => { }); afterAll(() => { - delete process.env.NODE_INSTALLER; + process.env.npm_config_user_agent = originalUserAgent; }); }); }); @@ -213,12 +216,15 @@ describe('getElectronModulePath', () => { }); describe('with yarn workspaces', () => { + let originalUserAgent: string | undefined; beforeAll(() => { - process.env.NODE_INSTALLER = 'yarn'; + originalUserAgent = process.env.npm_config_user_agent; + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; }); afterAll(() => { - delete process.env.NODE_INSTALLER; + process.env.npm_config_user_agent = originalUserAgent; }); it('finds the top-level electron module', async () => { diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index 408ee7ae6d..9e21a1331b 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -24,142 +24,173 @@ describe('package-manager', () => { process.env.npm_config_user_agent = originalUa; }; }); - describe('npm_config_user_agent', () => { - it.each([ - { - ua: 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64', - pm: 'yarn', - version: '1.22.22', - }, - { - ua: 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64', - pm: 'pnpm', - version: '10.0.0', - }, - { - ua: 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false', - pm: 'npm', - version: '10.9.2', - }, - ])('with $ua', async ({ ua, pm, version }) => { - process.env.npm_config_user_agent = ua; - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - pm, - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - version, - ); + describe('resolvePackageManager', () => { + describe('npm_config_user_agent', () => { + it.each([ + { + ua: 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64', + pm: 'yarn', + version: '1.22.22', + }, + { + ua: 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64', + pm: 'pnpm', + version: '10.0.0', + }, + { + ua: 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false', + pm: 'npm', + version: '10.9.2', + }, + ])('with $ua', async ({ ua, pm, version }) => { + process.env.npm_config_user_agent = ua; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + pm, + ); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + version, + ); + }); + + it('should return yarn if npm_config_user_agent=yarn', async () => { + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'yarn', + ); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + '1.22.22', + ); + }); + + it('should return pnpm if npm_config_user_agent=pnpm', async () => { + process.env.npm_config_user_agent = + 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'pnpm', + ); + }); + + it('should return npm if npm_config_user_agent=npm', async () => { + process.env.npm_config_user_agent = + 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'npm', + ); + }); }); - it('should return yarn if npm_config_user_agent=yarn', async () => { - process.env.npm_config_user_agent = - 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; + it('should use the package manager for the nearest ancestor lockfile if detected', async () => { + delete process.env.npm_config_user_agent; + vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock'); + vi.mocked(spawn).mockResolvedValue('1.22.22'); await expect(resolvePackageManager()).resolves.toHaveProperty( 'executable', 'yarn', ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '1.22.22', - ); }); - it('should return pnpm if npm_config_user_agent=pnpm', async () => { - process.env.npm_config_user_agent = - 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64'; + it('should fall back to npm if no other strategy worked', async () => { + delete process.env.npm_config_user_agent; + vi.mocked(findUp).mockResolvedValue(undefined); + vi.mocked(spawn).mockResolvedValue('9.99.99'); await expect(resolvePackageManager()).resolves.toHaveProperty( 'executable', - 'pnpm', + 'npm', ); - }); - - it('should return npm if npm_config_user_agent=npm', async () => { - process.env.npm_config_user_agent = - 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false'; await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', + 'version', + '9.99.99', ); }); - }); - describe('NODE_INSTALLER', () => { - let initialNodeInstallerValue: string | undefined; - - beforeEach(() => { - initialNodeInstallerValue = process.env.NODE_INSTALLER; - delete process.env.NODE_INSTALLER; - // NODE_INSTALLER is deprecated for Electron Forge 8 and throws a console.warn that we want to silence in tests - vi.spyOn(console, 'warn').mockImplementation(() => undefined); - - return () => { - // For cleanup, we want to restore process.env.NODE_INSTALLER. - // If it wasn't explicitly set before, we delete the value set during the test. - // Otherwise, we restore the initial value. - if (!initialNodeInstallerValue) { - delete process.env.NODE_INSTALLER; - } else { - process.env.NODE_INSTALLER = initialNodeInstallerValue; - } - vi.restoreAllMocks(); - }; - }); + describe('with an explicit package manager passed in', () => { + beforeEach(() => { + vi.resetModules(); + delete process.env.npm_config_user_agent; + }); - it.each([{ pm: 'yarn' }, { pm: 'npm' }, { pm: 'pnpm' }])( - 'should return $pm if NODE_INSTALLER=$pm', - async ({ pm }) => { - process.env.NODE_INSTALLER = pm; - vi.mocked(spawn).mockResolvedValue('9.9.9'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - pm, + it('should accept a string with only the package manager name', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '9.9.9', + const first = await resolvePackageManager('pnpm'); + expect(first.executable).toBe('pnpm'); + expect(first.version).toBe('latest'); + }); + + it('should accept the @latest tag', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' ); - }, - ); + const first = await resolvePackageManager('yarn@latest'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('latest'); + }); - it('should return npm if package manager is unsupported', async () => { - process.env.NODE_INSTALLER = 'bun'; - console.warn = vi.fn(); - vi.mocked(spawn).mockResolvedValue('1.22.22'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', - ); - expect(console.warn).toHaveBeenCalledWith( - '⚠', - expect.stringContaining('Package manager bun is unsupported'), - ); - }); - }); + it('should accept a full version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); + const first = await resolvePackageManager('yarn@1.22.22'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1.22.22'); + }); - it('should use the package manager for the nearest ancestor lockfile if detected', async () => { - delete process.env.npm_config_user_agent; - vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock'); - vi.mocked(spawn).mockResolvedValue('1.22.22'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'yarn', - ); - }); + it('should accept a major.minor version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); + const first = await resolvePackageManager('yarn@1.22'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1.22'); + }); + + it('should accept a major version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); + const first = await resolvePackageManager('yarn@1'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1'); + }); - it('should fall back to npm if no other strategy worked', async () => { - delete process.env.npm_config_user_agent; - vi.mocked(findUp).mockResolvedValue(undefined); - vi.mocked(spawn).mockResolvedValue('9.99.99'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '9.99.99', - ); + it('should cache explicit argument and ignore later env / lockfile', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); + const first = await resolvePackageManager('pnpm@10.0.0'); + expect(first.executable).toBe('pnpm'); + expect(first.version).toBe('10.0.0'); + + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; + vi.mocked(spawn).mockResolvedValue('9.9.9'); + const second = await resolvePackageManager(); + expect(second.executable).toBe('pnpm'); + expect(second.version).toBe('10.0.0'); + }); + + it('should fallback to npm and cache when explicit argument unsupported', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); + vi.mocked(spawn).mockResolvedValue('9.99.99'); + const result = await resolvePackageManager('good coffee'); + expect(result.executable).toBe('npm'); + expect(result.version).toBe('9.99.99'); + + const again = await resolvePackageManager(); + expect(again.executable).toBe('npm'); + expect(again.version).toBe('9.99.99'); + }); + }); }); describe('spawnPackageManager', () => { diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 660ccbf916..2a0a3422c4 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -5,10 +5,8 @@ import { CrossSpawnOptions, spawn, } from '@malept/cross-spawn-promise'; -import chalk from 'chalk'; import debug from 'debug'; import findUp from 'find-up'; -import logSymbols from 'log-symbols'; const d = debug('electron-forge:package-manager'); @@ -21,7 +19,7 @@ export type PMDetails = { exact: string; }; -let hasWarned = false; +let explicitPMCache: PMDetails | undefined; /** * Supported package managers and the commands and flags they need to install dependencies. @@ -75,7 +73,7 @@ function pmFromUserAgent() { /** * Resolves the package manager to use. In order, it checks the following: * - * 1. The value of the `NODE_INSTALLER` environment variable. + * 1. An explicit arg being passed into the function. * 2. The `process.env.npm_config_user_agent` value set by the executing package manager. * 3. The presence of a lockfile in an ancestor directory. * 4. If an unknown package manager is used (or none of the above apply), then we fall back to `npm`. @@ -85,7 +83,40 @@ function pmFromUserAgent() { * Supported package managers are `yarn`, `pnpm`, and `npm`. * */ -export const resolvePackageManager: () => Promise = async () => { +export const resolvePackageManager: ( + packageManager?: string, +) => Promise = async (packageManager) => { + let installer: string | undefined; + let installerVersion: string | undefined; + + // Check explicit packageManager argument FIRST, before cache + // This ensures explicit args always take precedence + if (packageManager) { + const match = packageManager.match( + /^(npm|pnpm|yarn)(?:@(latest|\d+(?:\.\d+)?(?:\.\d+)?(?:-.+)?))?$/, + ); + + if (match) { + const [, executable, version] = match; + if (Object.keys(PACKAGE_MANAGERS).includes(executable)) { + const pm = PACKAGE_MANAGERS[executable as SupportedPackageManager]; + installerVersion = version ?? 'latest'; + explicitPMCache = { ...pm, version: installerVersion }; + d(`Resolved and cached explicit package manager: ${pm.executable}`); + return explicitPMCache; + } else { + d( + `Attempted to parse ${packageManager} to regex but failed. Falling back!`, + ); + } + } + } + + if (explicitPMCache) { + d(`Using cached explicit package manager: ${explicitPMCache.executable}`); + return explicitPMCache; + } + const executingPM = pmFromUserAgent(); let lockfilePM; const lockfile = await findUp( @@ -97,34 +128,7 @@ export const resolvePackageManager: () => Promise = async () => { lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } - let installer; - let installerVersion; - - if (typeof process.env.NODE_INSTALLER === 'string') { - if (Object.keys(PACKAGE_MANAGERS).includes(process.env.NODE_INSTALLER)) { - installer = process.env.NODE_INSTALLER; - installerVersion = await spawnPackageManager( - PACKAGE_MANAGERS[installer as SupportedPackageManager], - ['--version'], - ); - if (!hasWarned) { - console.warn( - logSymbols.warning, - chalk.yellow( - `The NODE_INSTALLER environment variable is deprecated and will be removed in Electron Forge v8`, - ), - ); - hasWarned = true; - } - } else { - console.warn( - logSymbols.warning, - chalk.yellow( - `Package manager ${chalk.red(process.env.NODE_INSTALLER)} is unsupported. Falling back to ${chalk.green('npm')} instead.`, - ), - ); - } - } else if (executingPM) { + if (executingPM) { installer = executingPM.name; installerVersion = executingPM.version; } else if (lockfilePM) { @@ -140,7 +144,7 @@ export const resolvePackageManager: () => Promise = async () => { case 'npm': case 'pnpm': d( - `Resolved package manager to ${installer}. (Derived from NODE_INSTALLER: ${process.env.NODE_INSTALLER}, npm_config_user_agent: ${process.env.npm_config_user_agent}, lockfile: ${lockfilePM})`, + `Resolved package manager to ${installer}. (Derived from npm_config_user_agent: ${process.env.npm_config_user_agent}, lockfile: ${lockfilePM})`, ); return { ...PACKAGE_MANAGERS[installer], diff --git a/tools/verdaccio/spawn-verdaccio.ts b/tools/verdaccio/spawn-verdaccio.ts index 9612b0b276..07f59d2970 100644 --- a/tools/verdaccio/spawn-verdaccio.ts +++ b/tools/verdaccio/spawn-verdaccio.ts @@ -8,10 +8,11 @@ * the latest and greatest. * * Usage: - * tsx tools/verdaccio/spawn-verdaccio.ts [args...] + * tsx tools/verdaccio/spawn-verdaccio.ts [command] [args...] * - * Example: - * tsx tools/verdaccio-spawn-verdaccio.ts yarn test:slow + * Examples: + * tsx tools/verdaccio/spawn-verdaccio.ts yarn test:slow + * tsx tools/verdaccio/spawn-verdaccio.ts # Keeps Verdaccio running for manual testing */ import { ChildProcess, spawn } from 'node:child_process'; @@ -132,6 +133,9 @@ async function publishPackages(): Promise { } async function runCommand(args: string[]) { + console.log('šŸ—‘ļø Pruning pnpm store before running command'); + await spawnPromise('pnpm', ['store', 'prune']); + console.log(`šŸƒ Running: ${args.join(' ')}`); console.log(` Using registry: ${VERDACCIO_URL}`); @@ -154,32 +158,31 @@ async function runCommand(args: string[]) { async function main(): Promise { const args = process.argv.slice(2); - if (args.length === 0) { - console.error( - 'Usage: tsx tools/verdaccio/spawn-verdaccio.ts [args...]', - ); - console.error( - 'Example: tsx tools/verdaccio/spawn-verdaccio.ts yarn test:slow', - ); - process.exit(1); - } - // Handle signals process.on('SIGINT', () => { stopVerdaccio(); - process.exit(1); + process.exit(0); }); process.on('SIGTERM', () => { stopVerdaccio(); - process.exit(1); + process.exit(0); }); try { await startVerdaccio(); await publishPackages(); - await runCommand(args); - stopVerdaccio(); - process.exit(0); + + if (args.length === 0) { + // No command provided - keep Verdaccio running for manual testing + console.log(`\nāœ… Verdaccio is running at ${VERDACCIO_URL}`); + console.log(' Press Ctrl+C to stop.\n'); + // Keep the process alive + await new Promise(() => {}); + } else { + await runCommand(args); + stopVerdaccio(); + process.exit(0); + } } catch (error) { console.error('āŒ Error:', error); stopVerdaccio();