feat: --workspace-root

`--workspace-root`, `-w`: a new option that allows to focus on
the root workspace project.

E.g., the following command runs the `lint` script of the root
`package.json` from anywhere in the monorepo:

```
pnpm -w lint
```

PR #2866
This commit is contained in:
Zoltan Kochan
2020-09-17 11:07:05 +03:00
committed by GitHub
parent 767212f4ef
commit 092f8dd83a
19 changed files with 126 additions and 17 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/parse-cli-args": minor
---
When --workspace-root is used, the working directory is changed to the root of the workspace.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-installation": minor
---
When the --workspace-root option is used, it is allowed to add a new dependency to the root workspace project. Because this way the intention is clear.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-script-runners": minor
---
When a script is not found but is present in the workspace root, suggest to use `pnpm -w run`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-script-runners": minor
---
`pnpm run` prints all scripts from the root of the workspace. They may be executed using `pnpm -w run`.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/common-cli-options-help": minor
"pnpm": minor
---
New universal option added: -w, --workspace-root.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config": minor
---
New setting added: workspace-root.

View File

@@ -40,6 +40,11 @@ export const UNIVERSAL_OPTIONS = [
name: '--dir <dir>',
shortAlias: '-C',
},
{
description: 'Run the command on the root workspace project',
name: '--workspace-root',
shortAlias: '-w',
},
{
description: 'What level of logs to report. Any logs at or higher than the given level will be shown. Levels (lowest to highest): debug, info, warn, error. Or use "--silent" to turn off all logging.',
name: '--loglevel <level>',

View File

@@ -115,6 +115,7 @@ export interface Config {
registries: Registries
ignoreWorkspaceRootCheck: boolean
workspaceRoot: boolean
}
export interface ConfigWithDeprecatedSettings extends Config {

View File

@@ -85,6 +85,7 @@ export const types = Object.assign({
'virtual-store-dir': String,
'workspace-concurrency': Number,
'workspace-packages': [String, Array],
'workspace-root': Boolean,
}, npmTypes.types)
export type CliOptions = Record<string, unknown> & { dir?: string }

View File

@@ -30,9 +30,11 @@
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/parse-cli-args#readme",
"devDependencies": {
"@pnpm/parse-cli-args": "link:",
"@types/nopt": "^3.0.29"
"@types/nopt": "^3.0.29",
"tempy": "^0.7.0"
},
"dependencies": {
"@pnpm/error": "workspace:^1.3.1",
"@pnpm/find-workspace-dir": "workspace:1.0.1",
"didyoumean2": "^4.1.0",
"nopt": "^5.0.0"

View File

@@ -1,3 +1,4 @@
import PnpmError from '@pnpm/error'
import findWorkspaceDir from '@pnpm/find-workspace-dir'
import didYouMean, { ReturnTypeEnums } from 'didyoumean2'
import nopt = require('nopt')
@@ -117,6 +118,15 @@ export default async function parseCliArgs (
const workspaceDir = options['global'] // eslint-disable-line
? undefined
: await findWorkspaceDir(dir)
if (options['workspace-root']) {
if (options['global']) {
throw new PnpmError('OPTIONS_CONFLICT', '--workspace-root may not be used with --global')
}
if (!workspaceDir) {
throw new PnpmError('NOT_IN_WORKSPACE', '--workspace-root may only be used inside a workspace')
}
options['dir'] = workspaceDir
}
if (cmd === 'install' && params.length > 0) {
cmd = 'add'

View File

@@ -1,6 +1,8 @@
import PnpmError from '@pnpm/error'
import parseCliArgs from '@pnpm/parse-cli-args'
import os = require('os')
import test = require('tape')
import tempy = require('tempy')
const DEFAULT_OPTS = {
getCommandLongName: (commandName: string) => commandName,
@@ -213,3 +215,35 @@ test("don't use the fallback command if no command is present", async (t) => {
t.deepEqual(params, [])
t.end()
})
test('--workspace-root changes the directory to the workspace root', async (t) => {
const { options, workspaceDir } = await parseCliArgs({ ...DEFAULT_OPTS }, ['--workspace-root'])
t.ok(workspaceDir)
t.equal(options.dir, workspaceDir)
t.end()
})
test('--workspace-root fails if used with --global', async (t) => {
let err!: PnpmError
try {
await parseCliArgs({ ...DEFAULT_OPTS }, ['--workspace-root', '--global'])
} catch (_err) {
err = _err
}
t.ok(err)
t.equal(err.code, 'ERR_PNPM_OPTIONS_CONFLICT')
t.end()
})
test('--workspace-root fails if used outside of a workspace', async (t) => {
process.chdir(tempy.directory())
let err!: PnpmError
try {
await parseCliArgs({ ...DEFAULT_OPTS }, ['--workspace-root'])
} catch (_err) {
err = _err
}
t.ok(err)
t.equal(err.code, 'ERR_PNPM_NOT_IN_WORKSPACE')
t.end()
})

View File

@@ -160,6 +160,7 @@ export function handler (
save?: boolean
update?: boolean
useBetaCli?: boolean
workspaceRoot?: boolean
},
params: string[]
) {
@@ -172,12 +173,14 @@ export function handler (
if (
!opts.recursive &&
opts.workspaceDir === opts.dir &&
!opts.ignoreWorkspaceRootCheck
!opts.ignoreWorkspaceRootCheck &&
!opts.workspaceRoot
) {
throw new PnpmError('ADDING_TO_ROOT',
'Running this command will add the dependency to the workspace root, ' +
'which might not be what you want - if you really meant it, ' +
'make it explicit by running this command again with the -W flag (or --ignore-workspace-root-check).'
'make it explicit by running this command again with the -w flag (or --workspace-root). ' +
'If you don\'t want to see this warning anymore, you may set the ignore-workspace-root-check setting to false.'
)
}

View File

@@ -138,7 +138,7 @@ export async function handler (
if (rootManifest?.scripts?.[scriptName]) {
throw new PnpmError('NO_SCRIPT', `Missing script: ${scriptName}`, {
hint: `But ${scriptName} is present in the root of the workspace,
so you may run this command in: ${opts.workspaceDir}`,
so you may run "pnpm -w ${scriptName}"`,
})
}
}
@@ -225,15 +225,16 @@ function printProjectCommands (
if (output !== '') output += '\n\n'
output += `Commands available via "pnpm run":\n${renderCommands(otherScripts)}`
}
if (rootManifest?.scripts) {
const rootScripts = Object.entries(rootManifest.scripts)
.filter(([scriptName]) => !manifest.scripts?.[scriptName])
if (rootScripts.length) {
if (output !== '') output += '\n\n'
output += `Commands of the root workspace project (to run them, go to the root of the workspace):
${renderCommands(rootScripts)}`
}
if (!rootManifest?.scripts) {
return output
}
const rootScripts = Object.entries(rootManifest.scripts)
if (!rootScripts.length) {
return output
}
if (output !== '') output += '\n\n'
output += `Commands of the root workspace project (to run them, use "pnpm -w run"):
${renderCommands(rootScripts)}`
return output
}

View File

@@ -298,9 +298,11 @@ Commands available via "pnpm run":
foo
echo hi
Commands of the root workspace project (to run them, go to the root of the workspace):
Commands of the root workspace project (to run them, use "pnpm -w run"):
build
echo root`)
echo root
test
test-all`)
t.end()
})

View File

@@ -35,6 +35,7 @@ export const GLOBAL_OPTIONS = R.pick([
'reporter',
'stream',
'workspace-packages',
'workspace-root',
], allTypes)
export type CommandResponse = string | { output: string, exitCode: number } | undefined

View File

@@ -34,6 +34,7 @@ export default {
'frozen-shrinkwrap': '--frozen-lockfile',
'prefer-frozen-shrinkwrap': '--prefer-frozen-lockfile',
W: '--ignore-workspace-root-check',
w: '--workspace-root',
i: '--interactive',
}
// eslint-enable

View File

@@ -364,7 +364,7 @@ test('non-recursive install ignores filter from config', async (t: tape.Test) =>
await projects['project-3'].hasNot('minimatch')
})
test('adding new dependency in the root should fail if --ignore-workspace-root-check is not used', async (t: tape.Test) => {
test('adding new dependency in the root should fail if neither --workspace-root nor --ignore-workspace-root-check are used', async (t: tape.Test) => {
const project = prepare(t)
await fs.writeFile('pnpm-workspace.yaml', '', 'utf8')
@@ -377,8 +377,7 @@ test('adding new dependency in the root should fail if --ignore-workspace-root-c
t.ok(
stdout.toString().includes( // eslint-disable-line
'Running this command will add the dependency to the workspace root, ' +
'which might not be what you want - if you really meant it, ' +
'make it explicit by running this command again with the -W flag (or --ignore-workspace-root-check).'
'which might not be what you want - if you really meant it, '
)
)
}
@@ -396,6 +395,20 @@ test('adding new dependency in the root should fail if --ignore-workspace-root-c
t.equal(status, 0)
await project.has('is-negative')
}
{
const { status } = execPnpmSync(['add', 'is-odd', '--workspace-root'])
t.equal(status, 0)
await project.has('is-odd')
}
{
const { status } = execPnpmSync(['add', 'is-even', '-w'])
t.equal(status, 0)
await project.has('is-even')
}
})
test('--workspace-packages', async (t: tape.Test) => {

4
pnpm-lock.yaml generated
View File

@@ -1447,18 +1447,22 @@ importers:
write-json-file: 4.0.0
packages/parse-cli-args:
dependencies:
'@pnpm/error': 'link:../error'
'@pnpm/find-workspace-dir': 'link:../find-workspace-dir'
didyoumean2: 4.1.0
nopt: 5.0.0
devDependencies:
'@pnpm/parse-cli-args': 'link:'
'@types/nopt': 3.0.29
tempy: 0.7.0
specifiers:
'@pnpm/error': 'workspace:^1.3.1'
'@pnpm/find-workspace-dir': 'workspace:1.0.1'
'@pnpm/parse-cli-args': 'link:'
'@types/nopt': ^3.0.29
didyoumean2: ^4.1.0
nopt: ^5.0.0
tempy: ^0.7.0
packages/parse-wanted-dependency:
dependencies:
validate-npm-package-name: 3.0.0