/// import fs from 'fs' import path from 'path' import { type PnpmError } from '@pnpm/error' import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir' import { restart, run, } from '@pnpm/plugin-commands-script-runners' import { prepare, preparePackages } from '@pnpm/prepare' import { createTestIpcServer } from '@pnpm/test-ipc-server' import execa from 'execa' import isWindows from 'is-windows' import { sync as writeYamlFile } from 'write-yaml-file' import { DEFAULT_OPTS, REGISTRY_URL } from './utils' const pnpmBin = path.join(__dirname, '../../../pnpm/bin/pnpm.cjs') const skipOnWindows = isWindows() ? test.skip : test const onlyOnWindows = !isWindows() ? test.skip : test test('pnpm run: returns correct exit code', async () => { prepare({ scripts: { exit0: 'exit 0', exit1: 'exit 1', }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['exit0']) let err!: Error & { errno: number } try { await run.handler({ ...DEFAULT_OPTS, bail: true, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['exit1']) } catch (_err: any) { // eslint-disable-line err = _err } expect(err.errno).toBe(1) }) test('pnpm run --no-bail never fails', async () => { prepare({ scripts: { exit1: 'node recordArgs && exit 1', }, }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', bail: false, dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['exit1']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([[]]) }) const RECORD_ARGS_FILE = 'require(\'fs\').writeFileSync(\'args.json\', JSON.stringify(require(\'./args.json\').concat([process.argv.slice(2)])), \'utf8\')' test('run: pass the args to the command that is specified in the build script', async () => { prepare({ scripts: { foo: 'node recordArgs', postfoo: 'node recordArgs', prefoo: 'node recordArgs', }, }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['foo', 'arg', '--flag=true', '--help', '-h']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([['arg', '--flag=true', '--help', '-h']]) }) test('run: pass the args to the command that is specified in the build script of a package.yaml manifest', async () => { prepare({ scripts: { foo: 'node recordArgs', postfoo: 'node recordArgs', prefoo: 'node recordArgs', }, }, { manifestFormat: 'YAML' }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['foo', 'arg', '--flag=true', '--help', '-h']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([['arg', '--flag=true', '--help', '-h']]) }) test('test: pass the args to the command that is specified in the build script of a package.yaml manifest', async () => { prepare({ scripts: { posttest: 'node recordArgs', pretest: 'node recordArgs', test: 'node recordArgs', }, }, { manifestFormat: 'YAML' }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['test', 'arg', '--flag=true', '--help', '-h']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([['arg', '--flag=true', '--help', '-h']]) }) test('run start: pass the args to the command that is specified in the build script of a package.yaml manifest', async () => { prepare({ scripts: { poststart: 'node recordArgs', prestart: 'node recordArgs', start: 'node recordArgs', }, }, { manifestFormat: 'YAML' }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['start', 'arg', '--flag=true', '--help', '-h']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([['arg', '--flag=true', '--help', '-h']]) }) test('run stop: pass the args to the command that is specified in the build script of a package.yaml manifest', async () => { prepare({ scripts: { poststop: 'node recordArgs', prestop: 'node recordArgs', stop: 'node recordArgs', }, }, { manifestFormat: 'YAML' }) fs.writeFileSync('args.json', '[]', 'utf8') fs.writeFileSync('recordArgs.js', RECORD_ARGS_FILE, 'utf8') await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['stop', 'arg', '--flag=true', '--help', '-h']) const { default: args } = await import(path.resolve('args.json')) expect(args).toStrictEqual([['arg', '--flag=true', '--help', '-h']]) }) test('restart: run stop, restart and start', async () => { await using server = await createTestIpcServer() prepare({ scripts: { poststop: server.sendLineScript('poststop'), prestop: server.sendLineScript('prestop'), stop: server.sendLineScript('stop'), postrestart: server.sendLineScript('postrestart'), prerestart: server.sendLineScript('prerestart'), restart: server.sendLineScript('restart'), poststart: server.sendLineScript('poststart'), prestart: server.sendLineScript('prestart'), start: server.sendLineScript('start'), }, }) await restart.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, []) expect(server.getLines()).toStrictEqual([ 'stop', 'restart', 'start', ]) }) test('restart: run stop, restart and start and all the pre/post scripts', async () => { await using server = await createTestIpcServer() prepare({ scripts: { poststop: server.sendLineScript('poststop'), prestop: server.sendLineScript('prestop'), stop: `${server.sendLineScript('stop')} && pnpm poststop`, postrestart: server.sendLineScript('postrestart'), prerestart: server.sendLineScript('prerestart'), restart: server.sendLineScript('restart'), poststart: server.sendLineScript('poststart'), prestart: server.sendLineScript('prestart'), start: server.sendLineScript('start'), }, }) await restart.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), enablePrePostScripts: true, extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, []) expect(server.getLines()).toStrictEqual([ 'prestop', 'stop', 'poststop', 'prerestart', 'restart', 'postrestart', 'prestart', 'start', 'poststart', ]) }) test('"pnpm run" prints the list of available commands', async () => { prepare({ scripts: { foo: 'echo hi', test: 'ts-node test', }, }) const output = await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, []) expect(output).toBe(`\ Lifecycle scripts: test ts-node test Commands available via "pnpm run": foo echo hi`) }) test('"pnpm run" prints the list of available commands, including commands of the root workspace project', async () => { preparePackages([ { location: '.', package: { scripts: { build: 'echo root', test: 'test-all', }, }, }, { name: 'foo', version: '1.0.0', scripts: { foo: 'echo hi', test: 'ts-node test', }, }, ]) writeYamlFile('pnpm-workspace.yaml', {}) const workspaceDir = process.cwd() const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), []) { process.chdir('foo') const output = await run.handler({ ...DEFAULT_OPTS, allProjects, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, selectedProjectsGraph, workspaceDir, }, []) expect(output).toBe(`\ Lifecycle scripts: test ts-node test Commands available via "pnpm run": foo echo hi Commands of the root workspace project (to run them, use "pnpm -w run"): build echo root test test-all`) } { process.chdir('..') const output = await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', allProjects, dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, selectedProjectsGraph, workspaceDir, }, []) expect(output).toBe(`\ Lifecycle scripts: test test-all Commands available via "pnpm run": build echo root`) } }) test('pnpm run does not fail with --if-present even if the wanted script is not present', async () => { prepare({}) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, ifPresent: true, pnpmHomeDir: '', rawConfig: {}, }, ['build']) }) test('if a script is not found but is present in the root, print an info message about it in the error message', async () => { preparePackages([ { location: '.', package: { scripts: { build: 'node -e "process.stdout.write(\'root\')"', }, }, }, { name: 'foo', version: '1.0.0', }, ]) writeYamlFile('pnpm-workspace.yaml', {}) await execa(pnpmBin, [ 'install', '-r', '--registry', REGISTRY_URL, '--store-dir', path.resolve(DEFAULT_OPTS.storeDir), ]) const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), []) let err!: PnpmError try { await run.handler({ ...DEFAULT_OPTS, allProjects, dir: path.resolve('foo'), selectedProjectsGraph, workspaceDir: process.cwd(), }, ['build']) } catch (_err: any) { // eslint-disable-line err = _err } expect(err).toBeTruthy() expect(err.hint).toMatch(/But script matched with build is present in the root/) }) test('scripts work with PnP', async () => { prepare({ scripts: { foo: 'hello-world-js-bin > ./output.txt', }, }) await execa(pnpmBin, ['add', '@pnpm.e2e/hello-world-js-bin@1.0.0'], { env: { NPM_CONFIG_REGISTRY: REGISTRY_URL, NPM_CONFIG_NODE_LINKER: 'pnp', NPM_CONFIG_SYMLINK: 'false', }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['foo']) // https://github.com/pnpm/registry-mock/blob/ac2e129eb262009d2e7cd43ed869c31097793073/packages/hello-world-js-bin%401.0.0/index.js#L2 const helloWorldJsBinOutput = 'Hello world!\n' const fooOutput = fs.readFileSync(path.resolve('output.txt')).toString() expect(fooOutput).toStrictEqual(helloWorldJsBinOutput) }) // A .exe file to configure as the scriptShell option would be necessary to test // this behavior on Windows. Skip this test for now since compiling a custom // .exe would be quite involved and hard to maintain. skipOnWindows('pnpm run with custom shell', async () => { prepare({ scripts: { build: 'foo bar', }, dependencies: { '@pnpm.e2e/shell-mock': '0.0.0', }, }) await execa(pnpmBin, [ 'install', `--registry=${REGISTRY_URL}`, '--store-dir', path.resolve(DEFAULT_OPTS.storeDir), ]) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, scriptShell: path.resolve('node_modules/.bin/shell-mock'), }, ['build']) expect((await import(path.resolve('shell-input.json'))).default).toStrictEqual(['-c', 'foo bar']) }) onlyOnWindows('pnpm shows error if script-shell is .cmd', async () => { prepare({ scripts: { build: 'foo bar', }, dependencies: { '@pnpm.e2e/shell-mock': '0.0.0', }, }) await execa(pnpmBin, [ 'install', `--registry=${REGISTRY_URL}`, '--store-dir', path.resolve(DEFAULT_OPTS.storeDir), ]) async function runScript () { await run.handler({ ...DEFAULT_OPTS, bail: true, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, scriptShell: path.resolve('node_modules/.bin/shell-mock.cmd'), }, ['build']) } await expect(runScript).rejects.toEqual(expect.objectContaining({ code: 'ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS', })) }) test('pnpm run with RegExp script selector should work', async () => { prepare({ scripts: { 'build:a': 'node -e "require(\'fs\').writeFileSync(\'./output-build-a.txt\', \'a\', \'utf8\')"', 'build:b': 'node -e "require(\'fs\').writeFileSync(\'./output-build-b.txt\', \'b\', \'utf8\')"', 'build:c': 'node -e "require(\'fs\').writeFileSync(\'./output-build-c.txt\', \'c\', \'utf8\')"', build: 'node -e "require(\'fs\').writeFileSync(\'./output-build-a.txt\', \'should not run\', \'utf8\')"', 'lint:a': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-a.txt\', \'a\', \'utf8\')"', 'lint:b': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-b.txt\', \'b\', \'utf8\')"', 'lint:c': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-c.txt\', \'c\', \'utf8\')"', lint: 'node -e "require(\'fs\').writeFileSync(\'./output-lint-a.txt\', \'should not run\', \'utf8\')"', }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['/^(lint|build):.*/']) expect(fs.readFileSync('output-build-a.txt', { encoding: 'utf-8' })).toEqual('a') expect(fs.readFileSync('output-build-b.txt', { encoding: 'utf-8' })).toEqual('b') expect(fs.readFileSync('output-build-c.txt', { encoding: 'utf-8' })).toEqual('c') expect(fs.readFileSync('output-lint-a.txt', { encoding: 'utf-8' })).toEqual('a') expect(fs.readFileSync('output-lint-b.txt', { encoding: 'utf-8' })).toEqual('b') expect(fs.readFileSync('output-lint-c.txt', { encoding: 'utf-8' })).toEqual('c') }) test('pnpm run with RegExp script selector should work also for pre/post script', async () => { prepare({ scripts: { 'build:a': 'node -e "require(\'fs\').writeFileSync(\'./output-a.txt\', \'a\', \'utf8\')"', 'prebuild:a': 'node -e "require(\'fs\').writeFileSync(\'./output-pre-a.txt\', \'pre-a\', \'utf8\')"', }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, enablePrePostScripts: true, }, ['/build:.*/']) expect(fs.readFileSync('output-a.txt', { encoding: 'utf-8' })).toEqual('a') expect(fs.readFileSync('output-pre-a.txt', { encoding: 'utf-8' })).toEqual('pre-a') }) test('pnpm run with RegExp script selector should work parallel as a default behavior (parallel execution limits number is four)', async () => { await using serverA = await createTestIpcServer() await using serverB = await createTestIpcServer() prepare({ scripts: { 'build:a': `node -e "let i = 20;setInterval(() => {if (!--i) process.exit(0); console.log(Date.now());},50)" | ${serverA.generateSendStdinScript()}`, 'build:b': `node -e "let i = 40;setInterval(() => {if (!--i) process.exit(0); console.log(Date.now());},25)" | ${serverB.generateSendStdinScript()}`, }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, }, ['/build:.*/']) const outputsA = serverA.getLines().map(x => Number.parseInt(x)) const outputsB = serverB.getLines().map(x => Number.parseInt(x)) expect(Math.max(outputsA[0], outputsB[0]) < Math.min(outputsA[outputsA.length - 1], outputsB[outputsB.length - 1])).toBeTruthy() }) test('pnpm run with RegExp script selector should work sequentially with --workspace-concurrency=1', async () => { await using serverA = await createTestIpcServer() await using serverB = await createTestIpcServer() prepare({ scripts: { 'build:a': `node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); console.log(Date.now()); },16)" | ${serverA.generateSendStdinScript()}`, 'build:b': `node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); console.log(Date.now()); },16)" | ${serverB.generateSendStdinScript()}`, }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, workspaceConcurrency: 1, }, ['/build:.*/']) const outputsA = serverA.getLines().map(x => Number.parseInt(x)) const outputsB = serverB.getLines().map(x => Number.parseInt(x)) expect(outputsA[0] < outputsB[0] && outputsA[1] < outputsB[1]).toBeTruthy() }) test.each(['d', 'g', 'i', 'm', 'u', 'v', 'y', 's'])('pnpm run with RegExp script selector with flag %s should throw error', async (flag) => { await using serverA = await createTestIpcServer() await using serverB = await createTestIpcServer() prepare({ scripts: { 'build:a': `node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); console.log(Date.now()); },16)" | ${serverA.generateSendStdinScript()}`, 'build:b': `node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); console.log(Date.now()); },16)" | ${serverB.generateSendStdinScript()}`, }, }) let err!: Error try { await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, workspaceConcurrency: 1, }, [`/build:.*/${flag}`]) } catch (_err: any) { // eslint-disable-line err = _err } expect(err.message).toBe('RegExp flags are not supported in script command selector') }) test('pnpm run with slightly incorrect command suggests correct one', async () => { prepare({ scripts: { build: 'echo 0', }, }) // cspell:ignore buil await expect(run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, workspaceConcurrency: 1, }, ['buil'])).rejects.toEqual(expect.objectContaining({ code: 'ERR_PNPM_NO_SCRIPT', hint: 'Command "buil" not found. Did you mean "pnpm run build"?', })) }) test('pnpm run with custom node-options', async () => { prepare({ scripts: { build: 'node -e "if (process.env.NODE_OPTIONS !== \'--max-old-space-size=1200\') { process.exit(1) }"', }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: '', rawConfig: {}, nodeOptions: '--max-old-space-size=1200', workspaceConcurrency: 1, }, ['build']) }) test('pnpm run without node version', async () => { prepare({ scripts: { 'assert-node-version': `node -e "assert.equal(process.version, '${process.version}')"`, }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: process.cwd(), rawConfig: {}, workspaceConcurrency: 1, }, ['assert-node-version']) }) test('pnpm run with node version', async () => { prepare({ scripts: { 'assert-node-version': 'node -e "assert.equal(process.version, \'v20.0.0\')"', }, pnpm: { executionEnv: { nodeVersion: '20.0.0', }, }, }) await run.handler({ ...DEFAULT_OPTS, bin: 'node_modules/.bin', dir: process.cwd(), extraBinPaths: [], extraEnv: {}, pnpmHomeDir: process.cwd(), rawConfig: {}, workspaceConcurrency: 1, }, ['assert-node-version']) })