diff --git a/packages/lifecycle/src/runLifecycleHook.ts b/packages/lifecycle/src/runLifecycleHook.ts index 1432624c16..24f913f252 100644 --- a/packages/lifecycle/src/runLifecycleHook.ts +++ b/packages/lifecycle/src/runLifecycleHook.ts @@ -1,13 +1,14 @@ import { lifecycleLogger } from '@pnpm/core-loggers' -import { PackageJson } from '@pnpm/types' +import { DependencyManifest, ImporterManifest } from '@pnpm/types' import lifecycle = require('@zkochan/npm-lifecycle') function noop () {} // tslint:disable-line:no-empty export default async function runLifecycleHook ( stage: string, - pkg: PackageJson, + manifest: ImporterManifest | DependencyManifest, opts: { + args?: string[], depPath: string, optional?: boolean, pkgRoot: string, @@ -22,13 +23,22 @@ export default async function runLifecycleHook ( lifecycleLogger.debug({ depPath: opts.depPath, optional, - script: pkg.scripts![stage], + script: manifest.scripts![stage], stage, wd: opts.pkgRoot, }) } - return lifecycle(pkg, stage, opts.pkgRoot, { + const m = { _id: getId(manifest), ...manifest } + m.scripts = { ...m.scripts } + + if (stage === 'start' && !m.scripts.start) { + m.scripts.start = 'node server.js' + } + if (opts.args && opts.args.length && m.scripts && m.scripts[stage]) { + m.scripts[stage] = `${m.scripts[stage]} ${opts.args.map((arg) => `"${arg}"`).join(' ')}` + } + return lifecycle(m, stage, opts.pkgRoot, { config: opts.rawNpmConfig, dir: opts.rootNodeModulesDir, log: { @@ -76,3 +86,13 @@ export default async function runLifecycleHook ( } } } + +function getId (manifest: ImporterManifest | DependencyManifest) { + if (!manifest.name) { + return undefined + } + if (!manifest.version) { + return manifest.name + } + return `${manifest.name}@${manifest.version}` +} diff --git a/packages/pnpm/src/bin/pnpm.ts b/packages/pnpm/src/bin/pnpm.ts index b3d8cfacfb..7117938f20 100755 --- a/packages/pnpm/src/bin/pnpm.ts +++ b/packages/pnpm/src/bin/pnpm.ts @@ -43,15 +43,12 @@ if (argv.includes('--help') || argv.includes('-h') || argv.includes('--h')) { case 'profile': case 'publish': case 'repo': - case 'restart': case 's': case 'se': case 'search': case 'set': case 'star': case 'stars': - case 'start': - case 'stop': case 'team': case 'token': case 'unpublish': @@ -63,17 +60,6 @@ if (argv.includes('--help') || argv.includes('-h') || argv.includes('--h')) { case 'xmas': await passThruToNpm() break - case 't': - case 'tst': - case 'test': - case 'run': - case 'run-script': - if (argv.includes('--filter')) { - await runPnpm() - } else { - await passThruToNpm() - } - break default: await runPnpm() break diff --git a/packages/pnpm/src/cmd/help.ts b/packages/pnpm/src/cmd/help.ts index f3d7137c30..37d0a447da 100644 --- a/packages/pnpm/src/cmd/help.ts +++ b/packages/pnpm/src/cmd/help.ts @@ -37,7 +37,8 @@ function getHelpText (command: string) { -E, --save-exact install exact version -g, --global install as a global package -r run installation recursively in every package found in subdirectories - or in every workspace package, when executed inside a workspace + or in every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" --store the location where all the packages are saved on the disk. --offline trigger an error if any required dependencies are not available in local store --prefer-offline skip staleness checks for cached data, but request missing data from the server @@ -128,7 +129,8 @@ function getHelpText (command: string) { Options: -r uninstall from every package found in subdirectories - or from every workspace package, when executed inside a workspace + or from every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" Discouraged options: --shamefully-flatten attempt to flatten the dependency tree, similar to what npm and Yarn do @@ -154,7 +156,8 @@ function getHelpText (command: string) { Options: -r unlink in every package found in subdirectories - or in every workspace package, when executed inside a workspace + or in every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" ` case 'update': @@ -166,7 +169,8 @@ function getHelpText (command: string) { Options: -r update in every package found in subdirectories - or every workspace package, when executed inside a workspace + or every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" -g, --global update globally installed packages --depth how deep should levels of dependencies be inspected 0 is default, which means top-level dependencies @@ -187,7 +191,8 @@ function getHelpText (command: string) { Options: -r perform command on every package in subdirectories - or on every workspace package, when executed inside a workspace + or on every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" --long show extended information --parseable show parseable output instead of tree view -g, --global list packages in the global install prefix instead of in the current project @@ -212,8 +217,12 @@ function getHelpText (command: string) { case 'install-test': return stripIndent` - This command runs an \`npm install\` followed immediately by an \`npm test\`. - It takes exactly the same arguments as \`npm install\`. + pnpm install-test + + Aliases: it + + Runs a \`pnpm install\` followed immediately by a \`pnpm test\`. + It takes exactly the same arguments as \`pnpm install\`. ` case 'store': @@ -269,7 +278,8 @@ function getHelpText (command: string) { Options: -r check for outdated dependencies in every package found in subdirectories - or in every workspace package, when executed inside a workspace + or in every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" ` case 'rebuild': @@ -283,10 +293,62 @@ function getHelpText (command: string) { Options: -r rebuild every package found in subdirectories or every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" --pending rebuild packages that were not build during installation. Packages are not build when installing with the --ignore-scripts flag ` + case 'run': + return stripIndent` + pnpm run [-- ...] + + Aliases: run-script + + Runs a defined package script. + + Options: + -r run the defined package script in every package found in subdirectories + or every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" + ` + + case 'test': + return stripIndent` + pnpm test [-- ...] + + Aliases: t, tst + + Runs a package's "test" script, if one was provided. + + Options: + -r run the tests in every package found in subdirectories + or every workspace package, when executed inside a workspace. + For options that may be used with \`-r\`, see "pnpm help recursive" + ` + + case 'start': + return stripIndent` + pnpm start [-- ...] + + Runs an arbitrary command specified in the package's "start" property of its "scripts" object. + If no "start" property is specified on the "scripts" object, it will run node server.js. + ` + + case 'stop': + return stripIndent` + pnpm stop [-- ...] + + Runs a package's "stop" script, if one was provided. + ` + + case 'restart': + return stripIndent` + pnpm restart [-- ...] + + Restarts a package. + Runs a package's "stop", "restart", and "start" scripts, and associated pre- and post- scripts. + ` + case 'server': return stripIndent` pnpm server start @@ -405,36 +467,42 @@ function getHelpText (command: string) { Commands: + - import - install - - update - - uninstall + - install-test - link - - unlink - list - outdated - prune - - install-test - - store add - - store status - - store prune - - root - rebuild - - import + - restart + - root + - run + - start + - stop + - test + - uninstall + - unlink + - update - - recursive unlink + - recursive exec - recursive install - - recursive update - - recursive uninstall - recursive list - recursive outdated + - recursive rebuild - recursive run - recursive test - - recursive rebuild - - recursive exec + - recursive uninstall + - recursive unlink + - recursive update - server start - - server stop - server status + - server stop + + - store add + - store prune + - store status Other commands are passed through to npm ` diff --git a/packages/pnpm/src/cmd/index.ts b/packages/pnpm/src/cmd/index.ts index a404e5eb49..dca31e5742 100644 --- a/packages/pnpm/src/cmd/index.ts +++ b/packages/pnpm/src/cmd/index.ts @@ -9,6 +9,7 @@ import prune from './prune' import rebuild from './rebuild' import recursive from './recursive' import root from './root' +import run, { restart, start, stop, test } from './run' import server from './server' import store from './store' import uninstall from './uninstall' @@ -26,9 +27,14 @@ export default { prune, rebuild, recursive, + restart, root, + run, server, + start, + stop, store, + test, uninstall, unlink, update, diff --git a/packages/pnpm/src/cmd/installTest.ts b/packages/pnpm/src/cmd/installTest.ts index 14cd04c65e..ffab8b62fd 100644 --- a/packages/pnpm/src/cmd/installTest.ts +++ b/packages/pnpm/src/cmd/installTest.ts @@ -1,8 +1,8 @@ import { PnpmOptions } from '../types' import install from './install' -import runNpm from './runNpm' +import { test } from './run' export default async function (input: string[], opts: PnpmOptions) { await install(input, opts) - runNpm(['test']) + await test(input, opts) } diff --git a/packages/pnpm/src/cmd/run.ts b/packages/pnpm/src/cmd/run.ts new file mode 100644 index 0000000000..bd964214f9 --- /dev/null +++ b/packages/pnpm/src/cmd/run.ts @@ -0,0 +1,103 @@ +import runLifecycleHooks from '@pnpm/lifecycle' +import { readImporterManifestOnly } from '@pnpm/read-importer-manifest' +import { realNodeModulesDir } from '@pnpm/utils' + +export default async function run ( + args: string[], + opts: { + prefix: string, + rawNpmConfig: object, + argv: { + cooked: string[], + original: string[], + remain: string[], + }, + }, +) { + const manifest = await readImporterManifestOnly(opts.prefix) + const scriptName = args[0] + if (scriptName !== 'start' && (!manifest.scripts || !manifest.scripts[scriptName])) { + const err = new Error(`Missing script: ${scriptName}`) + err['code'] = 'ERR_PNPM_NO_SCRIPT' + throw err + } + const dashDashIndex = opts.argv.cooked.indexOf('--') + const lifecycleOpts = { + args: dashDashIndex === -1 ? [] : opts.argv.cooked.slice(dashDashIndex + 1), + depPath: opts.prefix, + pkgRoot: opts.prefix, + rawNpmConfig: opts.rawNpmConfig, + rootNodeModulesDir: await realNodeModulesDir(opts.prefix), + stdio: 'inherit', + unsafePerm: true, // when running scripts explicitly, assume that they're trusted. + } + if (manifest.scripts && manifest.scripts[`pre${scriptName}`]) { + await runLifecycleHooks(`pre${scriptName}`, manifest, lifecycleOpts) + } + await runLifecycleHooks(scriptName, manifest, lifecycleOpts) + if (manifest.scripts && manifest.scripts[`post${scriptName}`]) { + await runLifecycleHooks(`post${scriptName}`, manifest, lifecycleOpts) + } +} + +export async function start ( + args: string[], + opts: { + prefix: string, + rawNpmConfig: object, + argv: { + cooked: string[], + original: string[], + remain: string[], + }, + }, +) { + return run(['start', ...args], opts) +} + +export async function stop ( + args: string[], + opts: { + prefix: string, + rawNpmConfig: object, + argv: { + cooked: string[], + original: string[], + remain: string[], + }, + }, +) { + return run(['stop', ...args], opts) +} + +export async function test ( + args: string[], + opts: { + prefix: string, + rawNpmConfig: object, + argv: { + cooked: string[], + original: string[], + remain: string[], + }, + }, +) { + return run(['test', ...args], opts) +} + +export async function restart ( + args: string[], + opts: { + prefix: string, + rawNpmConfig: object, + argv: { + cooked: string[], + original: string[], + remain: string[], + }, + }, +) { + await stop(args, opts) + await run(['restart', ...args], opts) + await start(args, opts) +} diff --git a/packages/pnpm/src/main.ts b/packages/pnpm/src/main.ts index af2f1070c2..095e0cddaf 100644 --- a/packages/pnpm/src/main.ts +++ b/packages/pnpm/src/main.ts @@ -40,16 +40,19 @@ type CANONICAL_COMMAND_NAMES = 'help' | 'prune' | 'rebuild' | 'recursive' + | 'restart' | 'root' | 'run' | 'server' + | 'start' + | 'stop' | 'store' | 'test' | 'uninstall' | 'unlink' | 'update' -const COMMANDS_WITH_NO_DASHDASH_FILTER = new Set(['run', 'exec', 'test']) +const COMMANDS_WITH_NO_DASHDASH_FILTER = new Set(['run', 'exec', 'restart', 'start', 'stop', 'test']) const supportedCmds = new Set([ 'install', @@ -58,7 +61,10 @@ const supportedCmds = new Set([ 'link', 'prune', 'install-test', + 'restart', 'server', + 'start', + 'stop', 'store', 'list', 'unlink', @@ -181,6 +187,7 @@ export default async function run (argv: string[]) { optionalDependencies: opts.optional !== false, } opts.forceSharedLockfile = typeof opts.workspacePrefix === 'string' && opts.sharedWorkspaceLockfile === true + opts.argv = cliConf.argv if (opts.filter) { Array.prototype.push.apply(opts.filter, filterArgs) } else { diff --git a/packages/pnpm/src/types.ts b/packages/pnpm/src/types.ts index 4d1b01245c..26a7adabd0 100644 --- a/packages/pnpm/src/types.ts +++ b/packages/pnpm/src/types.ts @@ -8,6 +8,11 @@ import { export type ReadPackageHook = (pkg: PackageManifest) => PackageManifest export interface PnpmOptions { + argv: { + cooked: string[], + original: string[], + remain: string[], + }, bail: boolean, cliArgs: object, filter: string[], diff --git a/packages/pnpm/test/run.ts b/packages/pnpm/test/run.ts index 6ccb70d507..e48cfcffcd 100644 --- a/packages/pnpm/test/run.ts +++ b/packages/pnpm/test/run.ts @@ -1,12 +1,18 @@ -import prepare from '@pnpm/prepare' +import prepare, { + prepareWithJson5Manifest, + prepareWithYamlManifest, +} from '@pnpm/prepare' +import fs = require('mz/fs') +import path = require('path') import tape = require('tape') import promisifyTape from 'tape-promise' -import { execPnpmSync } from './utils' +import { execPnpm, execPnpmSync } from './utils' const test = promisifyTape(tape) +const testOnly = promisifyTape(tape.only) test('pnpm run: returns correct exit code', async (t: tape.Test) => { - const project = prepare(t, { + prepare(t, { scripts: { exit0: 'exit 0', exit1: 'exit 1', @@ -17,14 +23,128 @@ test('pnpm run: returns correct exit code', async (t: tape.Test) => { t.equal(execPnpmSync('run', 'exit1').status, 1) }) -test('pass the args to the command that is specfied in the build script', async t => { +test('run: pass the args to the command that is specfied in the build script', async (t: tape.Test) => { prepare(t, { + scripts: { + foo: 'ts-node test' + }, + }) + + const result = execPnpmSync('run', 'foo', '--', '--flag=true') + + t.ok((result.stdout as Buffer).toString('utf8').match(/ts-node test "--flag=true"/), 'command was successful') +}) + +test('run: pass the args to the command that is specfied in the build script of a package.yaml manifest', async (t: tape.Test) => { + prepareWithYamlManifest(t, { + scripts: { + foo: 'ts-node test' + }, + }) + + const result = execPnpmSync('run', 'foo', '--', '--flag=true') + + t.ok((result.stdout as Buffer).toString('utf8').match(/ts-node test "--flag=true"/), 'command was successful') +}) + +test('test: pass the args to the command that is specfied in the build script of a package.yaml manifest', async (t: tape.Test) => { + prepareWithYamlManifest(t, { scripts: { test: 'ts-node test' }, }) - const result = execPnpmSync('run', 'test', '--', '--flag=true') + const result = execPnpmSync('test', '--', '--flag=true') t.ok((result.stdout as Buffer).toString('utf8').match(/ts-node test "--flag=true"/), 'command was successful') }) + +test('start: pass the args to the command that is specfied in the build script of a package.yaml manifest', async (t: tape.Test) => { + prepareWithYamlManifest(t, { + scripts: { + start: 'ts-node test' + }, + }) + + const result = execPnpmSync('start', '--', '--flag=true') + + t.ok((result.stdout as Buffer).toString('utf8').match(/ts-node test "--flag=true"/), 'command was successful') +}) + +test('start: run "node server.js" by default', async (t: tape.Test) => { + prepareWithYamlManifest(t) + + await fs.writeFile('server.js', 'console.log("Hello world!")', 'utf8') + + const result = execPnpmSync('start') + + t.ok((result.stdout as Buffer).toString('utf8').match(/Hello world!/), 'command was successful') +}) + +test('stop: pass the args to the command that is specfied in the build script', async (t: tape.Test) => { + prepare(t, { + scripts: { + stop: 'ts-node test' + }, + }) + + const result = execPnpmSync('stop', '--', '--flag=true') + + t.ok((result.stdout as Buffer).toString('utf8').match(/ts-node test "--flag=true"/), 'command was successful') +}) + +test('restart: run stop, restart and start', async (t: tape.Test) => { + prepare(t, { + scripts: { + poststop: `node -e "process.stdout.write('poststop')" | json-append ./output.json`, + prestop: `node -e "process.stdout.write('prestop')" | json-append ./output.json`, + stop: `node -e "process.stdout.write('stop')" | json-append ./output.json`, + + postrestart: `node -e "process.stdout.write('postrestart')" | json-append ./output.json`, + prerestart: `node -e "process.stdout.write('prerestart')" | json-append ./output.json`, + restart: `node -e "process.stdout.write('restart')" | json-append ./output.json`, + + poststart: `node -e "process.stdout.write('poststart')" | json-append ./output.json`, + prestart: `node -e "process.stdout.write('prestart')" | json-append ./output.json`, + start: `node -e "process.stdout.write('start')" | json-append ./output.json`, + }, + }) + + await execPnpm('add', 'json-append@1') + await execPnpm('restart') + + const scriptsRan = await import(path.resolve('output.json')) + t.deepEqual(scriptsRan, [ + 'prestop', + 'stop', + 'poststop', + 'prerestart', + 'restart', + 'postrestart', + 'prestart', + 'start', + 'poststart', + ]) +}) + +test('install-test: install dependencies and runs tests', async (t: tape.Test) => { + prepareWithJson5Manifest(t, { + dependencies: { + 'json-append': '1', + }, + scripts: { + posttest: `node -e "process.stdout.write('posttest')" | json-append ./output.json`, + pretest: `node -e "process.stdout.write('pretest')" | json-append ./output.json`, + test: `node -e "process.stdout.write('test')" | json-append ./output.json`, + }, + }) + + await execPnpm('install-test') + + const scriptsRan = await import(path.resolve('output.json')) + t.deepEqual(scriptsRan, [ + 'pretest', + 'test', + 'posttest', + ]) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3be8bf45b4..630aa9d63d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2407,13 +2407,16 @@ importers: dependencies: '@pnpm/assert-project': 'link:../assert-project' '@pnpm/modules-yaml': 'link:../../packages/modules-yaml' + '@pnpm/types': 'link:../../packages/types' '@types/mkdirp': 0.5.2 '@types/node': 11.13.8 '@types/tape': 4.2.33 '@types/write-pkg': 3.1.0 mkdirp: 0.5.1 tape: 4.10.1 + write-json5-file: 2.0.0 write-pkg: 4.0.0 + write-yaml-file: 2.0.0 devDependencies: rimraf: 2.6.3 tslint: 5.16.0_typescript@3.4.5 @@ -2423,6 +2426,7 @@ importers: specifiers: '@pnpm/assert-project': 2.0.0 '@pnpm/modules-yaml': 3.0.2 + '@pnpm/types': 3.2.0 '@types/mkdirp': 0.5.2 '@types/node': '*' '@types/tape': 4.2.33 @@ -2434,7 +2438,9 @@ importers: tslint-config-standard: 8.0.1 tslint-eslint-rules: 5.4.0 typescript: 3.4.5 + write-json5-file: 2.0.0 write-pkg: 4.0.0 + write-yaml-file: 2.0.0 tools: devDependencies: '@commitlint/cli': 7.5.2 diff --git a/privatePackages/prepare/package.json b/privatePackages/prepare/package.json index d8466825b2..683d832528 100644 --- a/privatePackages/prepare/package.json +++ b/privatePackages/prepare/package.json @@ -7,13 +7,16 @@ "dependencies": { "@pnpm/assert-project": "2.0.0", "@pnpm/modules-yaml": "3.0.2", + "@pnpm/types": "3.2.0", "@types/mkdirp": "0.5.2", "@types/node": "*", "@types/tape": "4.2.33", "@types/write-pkg": "3.1.0", "mkdirp": "0.5.1", "tape": "4.10.1", - "write-pkg": "4.0.0" + "write-json5-file": "2.0.0", + "write-pkg": "4.0.0", + "write-yaml-file": "2.0.0" }, "devDependencies": { "rimraf": "2.6.3", diff --git a/privatePackages/prepare/src/index.ts b/privatePackages/prepare/src/index.ts index b2ed604445..4b2db26f0c 100644 --- a/privatePackages/prepare/src/index.ts +++ b/privatePackages/prepare/src/index.ts @@ -1,9 +1,12 @@ import assertProject from '@pnpm/assert-project' import { Modules } from '@pnpm/modules-yaml' +import { ImporterManifest } from '@pnpm/types' import mkdirp = require('mkdirp') import path = require('path') import { Test } from 'tape' import writePkg = require('write-pkg') +import { sync as writeYamlFile } from 'write-yaml-file' +import { sync as writeJson5File } from 'write-json5-file' // the testing folder should be outside of the project to avoid lookup in the project's node_modules const tmpPath = path.join(__dirname, '..', '..', '..', '..', '.tmp') @@ -26,7 +29,7 @@ export function tempDir (t: Test) { export function preparePackages ( t: Test, - pkgs: Array<{ location: string, package: Object } | Object>, + pkgs: Array<{ location: string, package: ImporterManifest } | ImporterManifest>, pkgTmpPath?: string, ): { [name: string]: { @@ -52,18 +55,22 @@ export function preparePackages ( if (typeof aPkg['location'] === 'string') { result[aPkg['package']['name']] = prepare(t, aPkg['package'], path.join(dirname, aPkg['location'])) } else { - result[aPkg['name']] = prepare(t, aPkg, path.join(dirname, aPkg['name'])) + result[aPkg['name']] = prepare(t, aPkg as ImporterManifest, path.join(dirname, aPkg['name'])) } } process.chdir('..') return result } -export default function prepare (t: Test, pkg?: Object, pkgTmpPath?: string) { +export default function prepare ( + t: Test, + manifest?: ImporterManifest, + pkgTmpPath?: string, +) { pkgTmpPath = pkgTmpPath || path.join(tempDir(t), 'project') mkdirp.sync(pkgTmpPath) - writePkg.sync(pkgTmpPath, Object.assign({ name: 'project', version: '0.0.0' }, pkg)) + writePkg.sync(pkgTmpPath, { name: 'project', version: '0.0.0', ...manifest } as any) // tslint:disable-line process.chdir(pkgTmpPath) return assertProject(t, pkgTmpPath) @@ -77,3 +84,29 @@ export function prepareEmpty (t: Test) { return assertProject(t, pkgTmpPath) } + +export function prepareWithYamlManifest ( + t: Test, + manifest?: ImporterManifest, +) { + const pkgTmpPath = path.join(tempDir(t), 'project') + + mkdirp.sync(pkgTmpPath) + writeYamlFile(path.join(pkgTmpPath, 'package.yaml'), { name: 'project', version: '0.0.0', ...manifest } as any) // tslint:disable-line + process.chdir(pkgTmpPath) + + return assertProject(t, pkgTmpPath) +} + +export function prepareWithJson5Manifest ( + t: Test, + manifest?: ImporterManifest, +) { + const pkgTmpPath = path.join(tempDir(t), 'project') + + mkdirp.sync(pkgTmpPath) + writeJson5File(path.join(pkgTmpPath, 'package.json5'), { name: 'project', version: '0.0.0', ...manifest } as any) // tslint:disable-line + process.chdir(pkgTmpPath) + + return assertProject(t, pkgTmpPath) +}