mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-24 08:35:19 -04:00
feat(pkg-manifest): add native set-script command with ss alias (#11504)
* feat: add native set-script command with ss alias * refactor(pkg-manifest): host set-script and wire it into the CLI - Move set-script into @pnpm/pkg-manifest.commands (drops the orphan @pnpm/pkg.commands package; pkg/* is not in the workspace). - Use readProjectManifest from @pnpm/cli.utils so package.json5 and package.yaml are updated in place instead of growing a stray package.json. - Remove set-script from notImplemented and register the command in pnpm/src/cmd/index.ts. - Cover the ss alias and the multi-word command path in tests. * refactor(set-script): share the pkg-set primitive Replace direct manifest.scripts mutation with setObjectValueByPropertyPath - the same primitive pkg-set uses. Reuses the prototype-pollution rejection for free and keeps the two commands on the same write path. Avoids the pkg-set string-CLI's first-equals key/value split, so script names containing '=' work too. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/native-set-script-command.md
Normal file
6
.changeset/native-set-script-command.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/pkg-manifest.commands": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Implements `pnpm set-script` (alias `ss`) natively. Adds or updates an entry in the `scripts` field of the project manifest, supporting `package.json`, `package.json5`, and `package.yaml` formats.
|
||||
@@ -1,2 +1,2 @@
|
||||
export * as pkg from './pkg.js'
|
||||
|
||||
export * as setScript from './setScript.js'
|
||||
|
||||
40
pkg-manifest/commands/src/setScript.ts
Normal file
40
pkg-manifest/commands/src/setScript.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { docsUrl, readProjectManifest } from '@pnpm/cli.utils'
|
||||
import { types as allTypes } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { setObjectValueByPropertyPath } from '@pnpm/object.property-path'
|
||||
import { renderHelp } from 'render-help'
|
||||
|
||||
export const rcOptionsTypes = cliOptionsTypes
|
||||
|
||||
export function cliOptionsTypes (): Record<string, unknown> {
|
||||
const types = allTypes as Record<string, unknown>
|
||||
return { dir: types['dir'] }
|
||||
}
|
||||
|
||||
export const commandNames = ['set-script', 'ss']
|
||||
|
||||
export async function handler (
|
||||
opts: { dir: string },
|
||||
params: string[]
|
||||
): Promise<void> {
|
||||
if (params.length < 2) {
|
||||
throw new PnpmError('SET_SCRIPT_MISSING_ARGS', 'Missing script name or command', {
|
||||
hint: help(),
|
||||
})
|
||||
}
|
||||
|
||||
const [name, ...commandParts] = params
|
||||
const command = commandParts.join(' ')
|
||||
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.dir)
|
||||
setObjectValueByPropertyPath(manifest as unknown as Record<string, unknown>, ['scripts', name], command)
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
|
||||
export function help (): string {
|
||||
return renderHelp({
|
||||
description: 'Set a script in package.json',
|
||||
usages: ['pnpm set-script <name> <command>'],
|
||||
url: docsUrl('set-script'),
|
||||
})
|
||||
}
|
||||
78
pkg-manifest/commands/test/setScript.test.ts
Normal file
78
pkg-manifest/commands/test/setScript.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { beforeEach, describe, expect, test } from '@jest/globals'
|
||||
import { setScript } from '@pnpm/pkg-manifest.commands'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
|
||||
describe('set-script command', () => {
|
||||
let tmpDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = tempDir()
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'package.json'),
|
||||
JSON.stringify({ name: 'test-package', version: '1.0.0' }, null, 2)
|
||||
)
|
||||
})
|
||||
|
||||
test('exposes the ss alias', () => {
|
||||
expect(setScript.commandNames).toEqual(['set-script', 'ss'])
|
||||
})
|
||||
|
||||
test('adds a script when none exist', async () => {
|
||||
await setScript.handler({ dir: tmpDir }, ['build', 'tsc -b'])
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(written.scripts).toEqual({ build: 'tsc -b' })
|
||||
})
|
||||
|
||||
test('overwrites an existing script', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'package.json'),
|
||||
JSON.stringify({ name: 'test-package', scripts: { build: 'old' } }, null, 2)
|
||||
)
|
||||
|
||||
await setScript.handler({ dir: tmpDir }, ['build', 'tsc -b'])
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(written.scripts.build).toBe('tsc -b')
|
||||
})
|
||||
|
||||
test('joins remaining params into the command', async () => {
|
||||
await setScript.handler({ dir: tmpDir }, ['lint', 'eslint', '--fix', 'src'])
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(written.scripts.lint).toBe('eslint --fix src')
|
||||
})
|
||||
|
||||
test('accepts script names that contain dots, hyphens, and quotes', async () => {
|
||||
await setScript.handler({ dir: tmpDir }, ['my-build', 'tsc -b'])
|
||||
await setScript.handler({ dir: tmpDir }, ['pre.publish', 'echo'])
|
||||
await setScript.handler({ dir: tmpDir }, ['weird"name', 'echo weird'])
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(written.scripts).toEqual({
|
||||
'my-build': 'tsc -b',
|
||||
'pre.publish': 'echo',
|
||||
'weird"name': 'echo weird',
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts script names containing an equals sign', async () => {
|
||||
await setScript.handler({ dir: tmpDir }, ['with=eq', 'echo with=eq'])
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(written.scripts['with=eq']).toBe('echo with=eq')
|
||||
})
|
||||
|
||||
test('throws when arguments are missing', async () => {
|
||||
await expect(setScript.handler({ dir: tmpDir }, ['build']))
|
||||
.rejects.toThrow('Missing script name or command')
|
||||
})
|
||||
|
||||
test('rejects unsafe script names', async () => {
|
||||
await expect(setScript.handler({ dir: tmpDir }, ['__proto__', 'echo']))
|
||||
.rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@pnpm/exec.commands'
|
||||
import { add, dedupe, fetch, importCommand, install, link, prune, remove, unlink, update } from '@pnpm/installing.commands'
|
||||
import { patch, patchCommit, patchRemove } from '@pnpm/patching.commands'
|
||||
import { pkg } from '@pnpm/pkg-manifest.commands'
|
||||
import { pkg, setScript } from '@pnpm/pkg-manifest.commands'
|
||||
import { deprecate, distTag, owner, ping, search, star, stars, undeprecate, unpublish, unstar, whoami } from '@pnpm/registry-access.commands'
|
||||
import { deploy, pack, packApp, publish, stage, version } from '@pnpm/releasing.commands'
|
||||
import { catFile, catIndex, findHash, store } from '@pnpm/store.commands'
|
||||
@@ -178,6 +178,7 @@ const commands: CommandDefinition[] = [
|
||||
root,
|
||||
run,
|
||||
sbom,
|
||||
setScript,
|
||||
setup,
|
||||
search,
|
||||
star,
|
||||
|
||||
@@ -9,7 +9,6 @@ const NOT_IMPLEMENTED_COMMANDS = [
|
||||
'prefix',
|
||||
'profile',
|
||||
'repo',
|
||||
'set-script',
|
||||
'team',
|
||||
'token',
|
||||
'xmas',
|
||||
|
||||
Reference in New Issue
Block a user