mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-24 08:35:19 -04:00
feat(pkg): implement native pnpm pkg command (#11512)
Implements `pnpm pkg` natively with `get`, `set`, `delete`, and `fix` subcommands. Workspace usage follows pnpm conventions: use `-r` / `--recursive` for all selected workspace projects, and `--filter` to narrow the selected project graph. This does not add npm-style `--workspace` or `--workspaces` flags. The PR also extends `@pnpm/object.property-path` with safe set/delete helpers used by the command. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
8
.changeset/native-pkg-command.md
Normal file
8
.changeset/native-pkg-command.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/object.property-path": minor
|
||||
"@pnpm/pkg-manifest.commands": minor
|
||||
"@pnpm/workspace.project-manifest-reader": patch
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Implement `pnpm pkg` command natively, following `npm pkg` standards.
|
||||
@@ -134,6 +134,7 @@
|
||||
"idempotency",
|
||||
"imagetools",
|
||||
"imurmurhash",
|
||||
"invalidformat",
|
||||
"ionicons",
|
||||
"isexe",
|
||||
"istvan",
|
||||
@@ -320,6 +321,7 @@
|
||||
"stdtype",
|
||||
"streamsearch",
|
||||
"stringifying",
|
||||
"subcmd",
|
||||
"subdep",
|
||||
"subdependencies",
|
||||
"subdependency",
|
||||
|
||||
51
object/property-path/src/delete.ts
Normal file
51
object/property-path/src/delete.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { parsePropertyPath } from './parse.js'
|
||||
import { rejectUnsafeKeys } from './unsafeKeys.js'
|
||||
|
||||
type ObjectOrArray = Record<string | number, unknown> | unknown[]
|
||||
|
||||
/**
|
||||
* Remove the value at a nested property path on {@link object}.
|
||||
*
|
||||
* No-op if the path does not resolve to an existing value. Array elements are
|
||||
* removed via `splice` so no `null` hole is left behind.
|
||||
*
|
||||
* Throws on unsafe keys (`__proto__`, `constructor`, `prototype`) to prevent
|
||||
* prototype pollution.
|
||||
*/
|
||||
export function deleteObjectValueByPropertyPath (object: ObjectOrArray, propertyPath: Iterable<string | number>): void {
|
||||
const path = Array.from(propertyPath)
|
||||
if (path.length === 0) return
|
||||
rejectUnsafeKeys(path)
|
||||
|
||||
let obj: ObjectOrArray = object
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i]
|
||||
if (
|
||||
typeof obj !== 'object' ||
|
||||
obj === null ||
|
||||
!Object.hasOwn(obj, key) ||
|
||||
(Array.isArray(obj) && typeof key !== 'number')
|
||||
) {
|
||||
return
|
||||
}
|
||||
obj = (obj as Record<string | number, unknown>)[key] as ObjectOrArray
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) return
|
||||
|
||||
const lastKey = path[path.length - 1]
|
||||
if (Array.isArray(obj) && isArrayIndex(lastKey)) {
|
||||
obj.splice(Number(lastKey), 1)
|
||||
return
|
||||
}
|
||||
delete (obj as Record<string | number, unknown>)[lastKey]
|
||||
}
|
||||
|
||||
export const deleteObjectValueByPropertyPathString = (object: ObjectOrArray, propertyPath: string): void =>
|
||||
deleteObjectValueByPropertyPath(object, parsePropertyPath(propertyPath))
|
||||
|
||||
function isArrayIndex (key: string | number): boolean {
|
||||
if (typeof key === 'number') return Number.isInteger(key) && key >= 0
|
||||
if (!/^(?:0|[1-9]\d*)$/.test(key)) return false
|
||||
return Number.isSafeInteger(Number(key))
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
export * from './delete.js'
|
||||
export * from './get.js'
|
||||
export * from './parse.js'
|
||||
export * from './set.js'
|
||||
export * from './token/index.js'
|
||||
export * from './unsafeKeys.js'
|
||||
|
||||
67
object/property-path/src/set.ts
Normal file
67
object/property-path/src/set.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
|
||||
import { parsePropertyPath } from './parse.js'
|
||||
import { rejectUnsafeKeys } from './unsafeKeys.js'
|
||||
|
||||
type ObjectOrArray = Record<string | number, unknown> | unknown[]
|
||||
|
||||
export class EmptyPropertyPathError extends PnpmError {
|
||||
constructor () {
|
||||
super('EMPTY_PROPERTY_PATH', 'Cannot set a value with an empty property path')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value at a nested property path on {@link object}.
|
||||
*
|
||||
* Creates intermediate objects or arrays as needed. If an intermediate node
|
||||
* exists but its shape disagrees with the next path segment (a scalar where a
|
||||
* container is needed, an array where an object is needed, or vice versa), it
|
||||
* is replaced with a fresh container so the write is persisted in a shape that
|
||||
* round-trips through `JSON.stringify`.
|
||||
*
|
||||
* Throws on unsafe keys (`__proto__`, `constructor`, `prototype`) to prevent
|
||||
* prototype pollution.
|
||||
*/
|
||||
export function setObjectValueByPropertyPath (object: ObjectOrArray, propertyPath: Iterable<string | number>, value: unknown): void {
|
||||
const path = Array.from(propertyPath)
|
||||
if (path.length === 0) throw new EmptyPropertyPathError()
|
||||
rejectUnsafeKeys(path)
|
||||
|
||||
let obj: ObjectOrArray = object
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i]
|
||||
const current = (obj as Record<string | number, unknown>)[key]
|
||||
const needsArray = typeof path[i + 1] === 'number'
|
||||
const isContainer = typeof current === 'object' && current !== null
|
||||
if (!isContainer || Array.isArray(current) !== needsArray) {
|
||||
const replacement: ObjectOrArray = needsArray ? [] : {}
|
||||
defineOwnProperty(obj, key, replacement)
|
||||
obj = replacement
|
||||
} else {
|
||||
obj = current as ObjectOrArray
|
||||
}
|
||||
}
|
||||
|
||||
defineOwnProperty(obj, path[path.length - 1], value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value as an own enumerable, writable, configurable property.
|
||||
*
|
||||
* Using `Object.defineProperty` rather than bracket assignment ensures that
|
||||
* even if a `__proto__`-like key slipped past {@link rejectUnsafeKeys}, the
|
||||
* write would create an own property instead of invoking the prototype
|
||||
* setter, so this assignment site cannot be a prototype-pollution sink.
|
||||
*/
|
||||
function defineOwnProperty (obj: ObjectOrArray, key: string | number, value: unknown): void {
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
export const setObjectValueByPropertyPathString = (object: ObjectOrArray, propertyPath: string, value: unknown): void =>
|
||||
setObjectValueByPropertyPath(object, parsePropertyPath(propertyPath), value)
|
||||
23
object/property-path/src/unsafeKeys.ts
Normal file
23
object/property-path/src/unsafeKeys.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
|
||||
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
||||
|
||||
export class UnsafePropertyPathKeyError extends PnpmError {
|
||||
readonly key: string
|
||||
constructor (key: string) {
|
||||
super('UNSAFE_PROPERTY_PATH_KEY', `Key "${key}" is not allowed in a property path`)
|
||||
this.key = key
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw if the property path contains a key that could trigger prototype
|
||||
* pollution when used to mutate an object (e.g. via {@link setObjectValueByPropertyPath}).
|
||||
*/
|
||||
export function rejectUnsafeKeys (propertyPath: Iterable<string | number>): void {
|
||||
for (const segment of propertyPath) {
|
||||
if (typeof segment === 'string' && UNSAFE_KEYS.has(segment)) {
|
||||
throw new UnsafePropertyPathKeyError(segment)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
object/property-path/test/delete.test.ts
Normal file
64
object/property-path/test/delete.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from '@jest/globals'
|
||||
|
||||
import {
|
||||
deleteObjectValueByPropertyPathString,
|
||||
type UnsafePropertyPathKeyError,
|
||||
} from '../src/index.js'
|
||||
|
||||
test('deletes a top-level key', () => {
|
||||
const obj: Record<string, unknown> = { name: 'foo', version: '1.0.0' }
|
||||
deleteObjectValueByPropertyPathString(obj, 'version')
|
||||
expect(obj).toEqual({ name: 'foo' })
|
||||
})
|
||||
|
||||
test('deletes a nested key', () => {
|
||||
const obj: Record<string, unknown> = { scripts: { build: 'tsc', test: 'jest' } }
|
||||
deleteObjectValueByPropertyPathString(obj, 'scripts.test')
|
||||
expect(obj).toEqual({ scripts: { build: 'tsc' } })
|
||||
})
|
||||
|
||||
test('removes an array element without leaving a hole', () => {
|
||||
const obj: Record<string, unknown> = { contributors: [{ name: 'Alice' }, { name: 'Bob' }] }
|
||||
deleteObjectValueByPropertyPathString(obj, 'contributors[0]')
|
||||
expect(obj).toEqual({ contributors: [{ name: 'Bob' }] })
|
||||
})
|
||||
|
||||
test('removes an array element by string index without leaving a hole', () => {
|
||||
const obj: Record<string, unknown> = { contributors: [{ name: 'Alice' }, { name: 'Bob' }] }
|
||||
deleteObjectValueByPropertyPathString(obj, 'contributors["0"]')
|
||||
expect(obj).toEqual({ contributors: [{ name: 'Bob' }] })
|
||||
})
|
||||
|
||||
test('no-op on a missing path', () => {
|
||||
const obj: Record<string, unknown> = { name: 'foo' }
|
||||
deleteObjectValueByPropertyPathString(obj, 'scripts.test')
|
||||
expect(obj).toEqual({ name: 'foo' })
|
||||
})
|
||||
|
||||
test('no-op when an intermediate value is null', () => {
|
||||
const obj: Record<string, unknown> = { a: null }
|
||||
deleteObjectValueByPropertyPathString(obj, 'a.b')
|
||||
expect(obj).toEqual({ a: null })
|
||||
})
|
||||
|
||||
test('no-op when an intermediate value is a scalar', () => {
|
||||
const obj: Record<string, unknown> = { a: 'scalar' }
|
||||
deleteObjectValueByPropertyPathString(obj, 'a.b')
|
||||
expect(obj).toEqual({ a: 'scalar' })
|
||||
})
|
||||
|
||||
test('no-op on an empty property path', () => {
|
||||
const obj: Record<string, unknown> = { name: 'foo' }
|
||||
deleteObjectValueByPropertyPathString(obj, '')
|
||||
expect(obj).toEqual({ name: 'foo' })
|
||||
})
|
||||
|
||||
test('rejects __proto__, constructor and prototype keys', () => {
|
||||
for (const unsafe of ['__proto__', 'constructor', 'prototype']) {
|
||||
expect(() => deleteObjectValueByPropertyPathString({}, `${unsafe}.foo`))
|
||||
.toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_UNSAFE_PROPERTY_PATH_KEY',
|
||||
key: unsafe,
|
||||
} as Partial<UnsafePropertyPathKeyError>))
|
||||
}
|
||||
})
|
||||
71
object/property-path/test/set.test.ts
Normal file
71
object/property-path/test/set.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from '@jest/globals'
|
||||
|
||||
import {
|
||||
EmptyPropertyPathError,
|
||||
setObjectValueByPropertyPathString,
|
||||
UnsafePropertyPathKeyError,
|
||||
} from '../src/index.js'
|
||||
|
||||
test('sets a top-level key', () => {
|
||||
const obj: Record<string, unknown> = { name: 'foo' }
|
||||
setObjectValueByPropertyPathString(obj, 'version', '1.0.0')
|
||||
expect(obj).toEqual({ name: 'foo', version: '1.0.0' })
|
||||
})
|
||||
|
||||
test('creates missing intermediate objects', () => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
setObjectValueByPropertyPathString(obj, 'scripts.build', 'tsc')
|
||||
expect(obj).toEqual({ scripts: { build: 'tsc' } })
|
||||
})
|
||||
|
||||
test('creates an array when the next segment is numeric', () => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
setObjectValueByPropertyPathString(obj, 'contributors[0].name', 'Alice')
|
||||
expect(obj).toEqual({ contributors: [{ name: 'Alice' }] })
|
||||
})
|
||||
|
||||
test('replaces a scalar intermediate with the right container', () => {
|
||||
const obj: Record<string, unknown> = { scripts: 'echo hi' }
|
||||
setObjectValueByPropertyPathString(obj, 'scripts.test', 'vitest')
|
||||
expect(obj).toEqual({ scripts: { test: 'vitest' } })
|
||||
})
|
||||
|
||||
test('replaces a scalar intermediate with an array when the next segment is numeric', () => {
|
||||
const obj: Record<string, unknown> = { keywords: 'oops' }
|
||||
setObjectValueByPropertyPathString(obj, 'keywords[0]', 'pnpm')
|
||||
expect(obj).toEqual({ keywords: ['pnpm'] })
|
||||
})
|
||||
|
||||
test('replaces an array intermediate with an object when the next segment is a string', () => {
|
||||
const obj: Record<string, unknown> = { contributors: [] }
|
||||
setObjectValueByPropertyPathString(obj, 'contributors.name', 'Alice')
|
||||
expect(obj).toEqual({ contributors: { name: 'Alice' } })
|
||||
})
|
||||
|
||||
test('replaces an object intermediate with an array when the next segment is numeric', () => {
|
||||
const obj: Record<string, unknown> = { contributors: { x: 1 } }
|
||||
setObjectValueByPropertyPathString(obj, 'contributors[0]', 'Alice')
|
||||
expect(obj).toEqual({ contributors: ['Alice'] })
|
||||
})
|
||||
|
||||
test('overwrites an existing value', () => {
|
||||
const obj: Record<string, unknown> = { scripts: { build: 'old' } }
|
||||
setObjectValueByPropertyPathString(obj, 'scripts.build', 'new')
|
||||
expect(obj).toEqual({ scripts: { build: 'new' } })
|
||||
})
|
||||
|
||||
test('rejects __proto__, constructor and prototype keys', () => {
|
||||
for (const unsafe of ['__proto__', 'constructor', 'prototype']) {
|
||||
expect(() => setObjectValueByPropertyPathString({}, `${unsafe}.polluted`, true))
|
||||
.toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_UNSAFE_PROPERTY_PATH_KEY',
|
||||
key: unsafe,
|
||||
} as Partial<UnsafePropertyPathKeyError>))
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(({} as any).polluted).toBeUndefined()
|
||||
})
|
||||
|
||||
test('throws on empty property path', () => {
|
||||
expect(() => setObjectValueByPropertyPathString({}, '', 'value')).toThrow(EmptyPropertyPathError)
|
||||
})
|
||||
38
pkg-manifest/commands/package.json
Normal file
38
pkg-manifest/commands/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@pnpm/pkg-manifest.commands",
|
||||
"version": "1100.0.0",
|
||||
"description": "Commands for managing package.json",
|
||||
"keywords": ["pnpm", "pnpm11", "pkg"],
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"repository": "https://github.com/pnpm/pnpm/tree/main/pkg-manifest/commands",
|
||||
"homepage": "https://github.com/pnpm/pnpm/tree/main/pkg-manifest/commands#readme",
|
||||
"bugs": { "url": "https://github.com/pnpm/pnpm/issues" },
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"exports": { ".": "./lib/index.js" },
|
||||
"files": ["lib", "!*.map"],
|
||||
"scripts": {
|
||||
"compile": "tsgo --build && pn lint --fix",
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"test": "pn compile && pn .test",
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
|
||||
"prepublishOnly": "tsgo --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/cli.utils": "workspace:*",
|
||||
"@pnpm/config.reader": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/object.property-path": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"render-help": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "catalog:",
|
||||
"@pnpm/pkg-manifest.commands": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*"
|
||||
},
|
||||
"engines": { "node": ">=22.13" },
|
||||
"jest": { "preset": "@pnpm/jest-config" }
|
||||
}
|
||||
2
pkg-manifest/commands/src/index.ts
Normal file
2
pkg-manifest/commands/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as pkg from './pkg.js'
|
||||
|
||||
253
pkg-manifest/commands/src/pkg.ts
Normal file
253
pkg-manifest/commands/src/pkg.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { docsUrl, readProjectManifest, readProjectManifestOnly } from '@pnpm/cli.utils'
|
||||
import { types as allTypes } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import {
|
||||
deleteObjectValueByPropertyPathString,
|
||||
getObjectValueByPropertyPathString,
|
||||
setObjectValueByPropertyPathString,
|
||||
} from '@pnpm/object.property-path'
|
||||
import type { ProjectManifest } from '@pnpm/types'
|
||||
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'],
|
||||
json: Boolean,
|
||||
recursive: Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
export const commandNames = ['pkg']
|
||||
|
||||
interface PkgCommandOptions {
|
||||
dir: string
|
||||
json?: boolean
|
||||
recursive?: boolean
|
||||
workspaceDir?: string
|
||||
selectedProjectsGraph?: Record<string, { package: { rootDir: string, manifest: Record<string, unknown> } }>
|
||||
}
|
||||
|
||||
export async function handler (opts: PkgCommandOptions, params: string[]): Promise<string | void> {
|
||||
if (params.length === 0) {
|
||||
throw new PnpmError('PKG_MISSING_SUBCOMMAND', 'Missing subcommand', {
|
||||
hint: help(),
|
||||
})
|
||||
}
|
||||
|
||||
if (params[0] === '--help' || params[0] === '-h') {
|
||||
return help()
|
||||
}
|
||||
|
||||
const [subcmd, ...args] = params
|
||||
|
||||
if (opts.recursive) {
|
||||
return handleRecursiveCommand(opts, subcmd, args)
|
||||
}
|
||||
|
||||
return runSubcommand(opts, subcmd, args)
|
||||
}
|
||||
|
||||
async function runSubcommand (opts: PkgCommandOptions, subcmd: string, args: string[]): Promise<string | void> {
|
||||
switch (subcmd) {
|
||||
case 'get':
|
||||
return pkgGet(opts, args)
|
||||
case 'set':
|
||||
return pkgSet(opts, args)
|
||||
case 'delete':
|
||||
return pkgDelete(opts, args)
|
||||
case 'fix':
|
||||
return pkgFix(opts)
|
||||
default:
|
||||
throw new PnpmError('PKG_UNKNOWN_SUBCOMMAND', `Unknown subcommand "${subcmd}"`, {
|
||||
hint: help(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRecursiveCommand (opts: PkgCommandOptions, subcmd: string, args: string[]): Promise<string | void> {
|
||||
const workspaceDir = opts.workspaceDir
|
||||
if (!workspaceDir) {
|
||||
throw new PnpmError('PKG_RECURSIVE_NO_ROOT', 'Cannot run recursively outside of a workspace')
|
||||
}
|
||||
|
||||
const selectedProjects = opts.selectedProjectsGraph == null
|
||||
? []
|
||||
: Object.values(opts.selectedProjectsGraph)
|
||||
|
||||
if (selectedProjects.length === 0) {
|
||||
throw new PnpmError('PKG_RECURSIVE_NO_PACKAGES', 'No workspace packages were selected')
|
||||
}
|
||||
|
||||
if (subcmd === 'get') {
|
||||
const entries = await Promise.all(selectedProjects.map(async ({ package: pkg }) => {
|
||||
const manifest = await readProjectManifestOnly(pkg.rootDir) as Record<string, unknown>
|
||||
const pkgName = String(manifest.name ?? path.relative(workspaceDir, pkg.rootDir))
|
||||
return [pkgName, selectFromManifest(manifest, args)] as const
|
||||
}))
|
||||
return JSON.stringify(Object.fromEntries(entries), undefined, 2)
|
||||
}
|
||||
|
||||
await Promise.all(selectedProjects.map(({ package: pkg }) =>
|
||||
runSubcommand({ ...opts, dir: pkg.rootDir }, subcmd, args)
|
||||
))
|
||||
}
|
||||
|
||||
async function pkgGet (opts: PkgCommandOptions, args: string[]): Promise<string> {
|
||||
const manifest = await readProjectManifestOnly(opts.dir) as Record<string, unknown>
|
||||
|
||||
if (args.length === 1) {
|
||||
const value = getObjectValueByPropertyPathString(manifest, args[0])
|
||||
if (value === undefined) return ''
|
||||
if (opts.json) return JSON.stringify(value, undefined, 2)
|
||||
return typeof value === 'string' ? value : JSON.stringify(value, undefined, 2)
|
||||
}
|
||||
|
||||
return JSON.stringify(selectFromManifest(manifest, args), undefined, 2)
|
||||
}
|
||||
|
||||
function selectFromManifest (manifest: Record<string, unknown>, args: string[]): unknown {
|
||||
if (args.length === 0) return manifest
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const key of args) {
|
||||
result[key] = getObjectValueByPropertyPathString(manifest, key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function pkgSet (opts: PkgCommandOptions, args: string[]): Promise<void> {
|
||||
if (args.length === 0) {
|
||||
throw new PnpmError('PKG_SET_MISSING_ARGS', 'Missing key=value pairs', {
|
||||
hint: help(),
|
||||
})
|
||||
}
|
||||
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.dir)
|
||||
|
||||
for (const arg of args) {
|
||||
const eqIndex = arg.indexOf('=')
|
||||
if (eqIndex === -1) {
|
||||
throw new PnpmError('PKG_SET_INVALID_ARG', `Invalid argument "${arg}". Expected key=value format`, {
|
||||
hint: 'Example: pnpm pkg set name=my-package',
|
||||
})
|
||||
}
|
||||
|
||||
const key = arg.slice(0, eqIndex)
|
||||
let value: unknown = arg.slice(eqIndex + 1)
|
||||
|
||||
if (opts.json) {
|
||||
try {
|
||||
value = JSON.parse(value as string)
|
||||
} catch {
|
||||
throw new PnpmError('PKG_SET_JSON_PARSE', `Failed to parse value as JSON: "${value as string}"`)
|
||||
}
|
||||
}
|
||||
|
||||
setObjectValueByPropertyPathString(manifest as unknown as Record<string, unknown>, key, value)
|
||||
}
|
||||
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
|
||||
async function pkgDelete (opts: PkgCommandOptions, args: string[]): Promise<void> {
|
||||
if (args.length === 0) {
|
||||
throw new PnpmError('PKG_DELETE_MISSING_ARGS', 'Missing keys to delete', {
|
||||
hint: help(),
|
||||
})
|
||||
}
|
||||
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.dir)
|
||||
|
||||
for (const key of args) {
|
||||
deleteObjectValueByPropertyPathString(manifest as unknown as Record<string, unknown>, key)
|
||||
}
|
||||
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
|
||||
async function pkgFix (opts: PkgCommandOptions): Promise<void> {
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.dir)
|
||||
const m = manifest as ProjectManifest & Record<string, unknown>
|
||||
|
||||
if ('name' in m && typeof m.name !== 'string') {
|
||||
delete m.name
|
||||
}
|
||||
|
||||
if ('version' in m && typeof m.version !== 'string') {
|
||||
delete m.version
|
||||
}
|
||||
|
||||
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', 'scripts'] as const) {
|
||||
if (field in m && !isPlainObject(m[field])) {
|
||||
delete m[field]
|
||||
}
|
||||
}
|
||||
|
||||
if ('bin' in m && typeof m.bin !== 'string' && !isPlainObject(m.bin)) {
|
||||
delete m.bin
|
||||
}
|
||||
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
|
||||
function isPlainObject (value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function help (): string {
|
||||
return renderHelp({
|
||||
description: 'Manages your package.json',
|
||||
descriptionLists: [
|
||||
{
|
||||
title: 'Commands',
|
||||
list: [
|
||||
{
|
||||
description: 'Retrieves a value from package.json',
|
||||
name: 'get [<key> [<key> ...]]',
|
||||
},
|
||||
{
|
||||
description: 'Sets a value in package.json',
|
||||
name: 'set <key>=<value> [<key>=<value> ...]',
|
||||
},
|
||||
{
|
||||
description: 'Deletes a key from package.json',
|
||||
name: 'delete <key> [<key> ...]',
|
||||
},
|
||||
{
|
||||
description: 'Auto corrects common errors in package.json',
|
||||
name: 'fix',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Options',
|
||||
list: [
|
||||
{
|
||||
description: 'When setting, parse the value as JSON. When getting a single key, return its JSON-encoded form instead of the raw value',
|
||||
name: '--json',
|
||||
},
|
||||
{
|
||||
description: 'Run on every workspace project or every project selected by a filter',
|
||||
name: '--recursive',
|
||||
shortAlias: '-r',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
url: docsUrl('pkg'),
|
||||
usages: [
|
||||
'pnpm pkg get [<key> [<key> ...]]',
|
||||
'pnpm pkg set <key>=<value> [<key>=<value> ...]',
|
||||
'pnpm pkg delete <key> [<key> ...]',
|
||||
'pnpm pkg fix',
|
||||
'pnpm pkg set <key>=<value> --json',
|
||||
'pnpm -r pkg get name',
|
||||
'pnpm --filter <selector> pkg get name',
|
||||
'pnpm -r pkg set version=1.0.0',
|
||||
],
|
||||
})
|
||||
}
|
||||
387
pkg-manifest/commands/test/pkg.test.ts
Normal file
387
pkg-manifest/commands/test/pkg.test.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { beforeEach, describe, expect, test } from '@jest/globals'
|
||||
import { pkg } from '@pnpm/pkg-manifest.commands'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
|
||||
const { cliOptionsTypes, handler } = pkg
|
||||
|
||||
describe('pkg command', () => {
|
||||
let tmpDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = tempDir()
|
||||
})
|
||||
|
||||
describe('get subcommand', () => {
|
||||
test('gets all fields when no keys provided', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
dependencies: { foo: '1.0.0' },
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
const result = await handler({ dir: tmpDir }, ['get'])
|
||||
const parsed = JSON.parse(result as string)
|
||||
expect(parsed.name).toBe('test-package')
|
||||
expect(parsed.version).toBe('1.0.0')
|
||||
})
|
||||
|
||||
test('gets a single key as a raw value', async () => {
|
||||
const manifest = { name: 'test-package', version: '1.0.0' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
expect(await handler({ dir: tmpDir }, ['get', 'name'])).toBe('test-package')
|
||||
})
|
||||
|
||||
test('returns an empty string when a single key is missing', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
expect(await handler({ dir: tmpDir }, ['get', 'description'])).toBe('')
|
||||
})
|
||||
|
||||
test('gets a single key as JSON when --json is set', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
expect(await handler({ dir: tmpDir, json: true }, ['get', 'name'])).toBe('"test-package"')
|
||||
})
|
||||
|
||||
test('returns an object when multiple keys are requested', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'A test package',
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
const parsed = JSON.parse(await handler({ dir: tmpDir }, ['get', 'name', 'version']) as string)
|
||||
expect(parsed.name).toBe('test-package')
|
||||
expect(parsed.version).toBe('1.0.0')
|
||||
expect(parsed.description).toBeUndefined()
|
||||
})
|
||||
|
||||
test('gets nested keys using dot notation', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
scripts: { build: 'tsc', test: 'jest' },
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
expect(await handler({ dir: tmpDir }, ['get', 'scripts.build'])).toBe('tsc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('set subcommand', () => {
|
||||
test('sets a simple key-value pair', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['set', 'version=1.0.0'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.version).toBe('1.0.0')
|
||||
})
|
||||
|
||||
test('sets nested keys using dot notation', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['set', 'scripts.build=tsc'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.scripts.build).toBe('tsc')
|
||||
})
|
||||
|
||||
test('sets multiple key-value pairs', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['set', 'version=1.0.0', 'description=A test'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.version).toBe('1.0.0')
|
||||
expect(updated.description).toBe('A test')
|
||||
})
|
||||
|
||||
test('sets JSON values with --json flag', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir, json: true }, ['set', 'version=2'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.version).toBe(2)
|
||||
})
|
||||
|
||||
test('throws error for invalid key=value format', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await expect(handler({ dir: tmpDir }, ['set', 'invalidformat'])).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('sets nested values through array index notation', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['set', 'contributors[0].name=Alice'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.contributors).toEqual([{ name: 'Alice' }])
|
||||
})
|
||||
|
||||
test('replaces a scalar intermediate value with an object when descending', async () => {
|
||||
const manifest = { scripts: 'echo hi' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['set', 'scripts.test=vitest'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.scripts).toEqual({ test: 'vitest' })
|
||||
})
|
||||
|
||||
test('rejects unsafe keys to prevent prototype pollution', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await expect(handler({ dir: tmpDir }, ['set', '__proto__.polluted=true'])).rejects.toThrow()
|
||||
await expect(handler({ dir: tmpDir }, ['set', 'constructor.prototype.polluted=true'])).rejects.toThrow()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(({} as any).polluted).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete subcommand', () => {
|
||||
test('deletes a key from package.json', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'To be deleted',
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['delete', 'description'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.description).toBeUndefined()
|
||||
expect(updated.name).toBe('test-package')
|
||||
})
|
||||
|
||||
test('deletes nested keys using dot notation', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
scripts: { build: 'tsc', test: 'jest' },
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['delete', 'scripts.test'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.scripts.test).toBeUndefined()
|
||||
expect(updated.scripts.build).toBe('tsc')
|
||||
})
|
||||
|
||||
test('throws error when no keys provided', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await expect(handler({ dir: tmpDir }, ['delete'])).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('removes an array element without leaving a hole', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
contributors: [{ name: 'Alice' }, { name: 'Bob' }],
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['delete', 'contributors[0]'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.contributors).toEqual([{ name: 'Bob' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fix subcommand', () => {
|
||||
test('fixes invalid name field', async () => {
|
||||
const manifest = {
|
||||
name: 123 as unknown,
|
||||
version: '1.0.0',
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['fix'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.name).toBeUndefined()
|
||||
expect(updated.version).toBe('1.0.0')
|
||||
})
|
||||
|
||||
test('fixes invalid dependencies field', async () => {
|
||||
const manifest = {
|
||||
name: 'test-package',
|
||||
dependencies: 'invalid' as unknown,
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['fix'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.dependencies).toBeUndefined()
|
||||
})
|
||||
|
||||
test('removes array-valued object fields', async () => {
|
||||
const manifest: Record<string, unknown> = {
|
||||
name: 'test-package',
|
||||
dependencies: [],
|
||||
scripts: [],
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['fix'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.dependencies).toBeUndefined()
|
||||
expect(updated.scripts).toBeUndefined()
|
||||
})
|
||||
|
||||
test('removes null-valued object fields', async () => {
|
||||
const manifest: Record<string, unknown> = {
|
||||
name: 'test-package',
|
||||
dependencies: null,
|
||||
}
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await handler({ dir: tmpDir }, ['fix'])
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'))
|
||||
expect(updated.dependencies).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
test('throws error for unknown subcommand', async () => {
|
||||
const manifest = { name: 'test-package' }
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
|
||||
await expect(handler({ dir: tmpDir }, ['unknown'])).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('throws error when no subcommand provided', async () => {
|
||||
await expect(handler({ dir: tmpDir }, [])).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cliOptionsTypes', () => {
|
||||
test('returns correct option types', () => {
|
||||
const types = cliOptionsTypes()
|
||||
expect(types).toHaveProperty('dir')
|
||||
expect(types).toHaveProperty('json')
|
||||
expect(types).toHaveProperty('recursive')
|
||||
expect(types).not.toHaveProperty('workspace')
|
||||
expect(types).not.toHaveProperty('workspaces')
|
||||
expect(types).not.toHaveProperty('ws')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recursive mode', () => {
|
||||
function setupWorkspace (manifests: Record<string, Record<string, unknown>>) {
|
||||
const allProjects = Object.entries(manifests).map(([name, manifest]) => {
|
||||
const rootDir = path.join(tmpDir, name)
|
||||
fs.mkdirSync(rootDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(rootDir, 'package.json'), JSON.stringify(manifest, null, 2))
|
||||
return { rootDir, manifest }
|
||||
})
|
||||
const selectedProjectsGraph = Object.fromEntries(
|
||||
allProjects.map(p => [p.rootDir, { package: p }])
|
||||
)
|
||||
return { allProjects, selectedProjectsGraph }
|
||||
}
|
||||
|
||||
test('aggregates `get` results from each selected workspace package', async () => {
|
||||
const { selectedProjectsGraph } = setupWorkspace({
|
||||
'pkg-a': { name: 'pkg-a', version: '1.0.0' },
|
||||
'pkg-b': { name: 'pkg-b', version: '2.0.0' },
|
||||
})
|
||||
|
||||
const result = await handler({
|
||||
dir: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
}, ['get', 'name'])
|
||||
|
||||
expect(JSON.parse(result as string)).toEqual({
|
||||
'pkg-a': { name: 'pkg-a' },
|
||||
'pkg-b': { name: 'pkg-b' },
|
||||
})
|
||||
})
|
||||
|
||||
test('runs `set` against every selected workspace package', async () => {
|
||||
const { allProjects, selectedProjectsGraph } = setupWorkspace({
|
||||
'pkg-a': { name: 'pkg-a', version: '1.0.0' },
|
||||
'pkg-b': { name: 'pkg-b', version: '2.0.0' },
|
||||
})
|
||||
|
||||
await handler({
|
||||
dir: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
}, ['set', 'license=MIT'])
|
||||
|
||||
for (const { rootDir } of allProjects) {
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
|
||||
expect(updated.license).toBe('MIT')
|
||||
}
|
||||
})
|
||||
|
||||
test('runs `delete` against every selected workspace package', async () => {
|
||||
const { allProjects, selectedProjectsGraph } = setupWorkspace({
|
||||
'pkg-a': { name: 'pkg-a', version: '1.0.0', extra: 'a' },
|
||||
'pkg-b': { name: 'pkg-b', version: '2.0.0', extra: 'b' },
|
||||
})
|
||||
|
||||
await handler({
|
||||
dir: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
}, ['delete', 'extra'])
|
||||
|
||||
for (const { rootDir } of allProjects) {
|
||||
const updated = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
|
||||
expect(updated.extra).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
test('runs against only the selected projects', async () => {
|
||||
const { allProjects, selectedProjectsGraph } = setupWorkspace({
|
||||
'pkg-a': { name: 'pkg-a', version: '1.0.0' },
|
||||
'pkg-b': { name: 'pkg-b', version: '2.0.0' },
|
||||
'pkg-c': { name: 'pkg-c', version: '3.0.0' },
|
||||
})
|
||||
const selected = Object.fromEntries(
|
||||
[allProjects[0], allProjects[2]].map(p => [p.rootDir, selectedProjectsGraph[p.rootDir]])
|
||||
)
|
||||
|
||||
await handler({
|
||||
dir: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
recursive: true,
|
||||
selectedProjectsGraph: selected,
|
||||
}, ['set', 'license=MIT'])
|
||||
|
||||
const read = (name: string) =>
|
||||
JSON.parse(fs.readFileSync(path.join(tmpDir, name, 'package.json'), 'utf8'))
|
||||
expect(read('pkg-a').license).toBe('MIT')
|
||||
expect(read('pkg-b').license).toBeUndefined()
|
||||
expect(read('pkg-c').license).toBe('MIT')
|
||||
})
|
||||
|
||||
test('throws when used outside of a workspace', async () => {
|
||||
await expect(handler({ dir: tmpDir, recursive: true }, ['get']))
|
||||
.rejects.toMatchObject({ code: 'ERR_PNPM_PKG_RECURSIVE_NO_ROOT' })
|
||||
})
|
||||
|
||||
test('throws when no workspace packages were selected', async () => {
|
||||
await expect(handler({
|
||||
dir: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
recursive: true,
|
||||
selectedProjectsGraph: {},
|
||||
}, ['get'])).rejects.toMatchObject({ code: 'ERR_PNPM_PKG_RECURSIVE_NO_PACKAGES' })
|
||||
})
|
||||
})
|
||||
})
|
||||
18
pkg-manifest/commands/test/tsconfig.json
Normal file
18
pkg-manifest/commands/test/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "../node_modules/.test.lib",
|
||||
"rootDir": "..",
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
19
pkg-manifest/commands/tsconfig.json
Normal file
19
pkg-manifest/commands/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../__utils__/prepare" },
|
||||
{ "path": "../../cli/utils" },
|
||||
{ "path": "../../config/reader" },
|
||||
{ "path": "../../core/error" },
|
||||
{ "path": "../../core/types" },
|
||||
{ "path": "../../object/property-path" }
|
||||
]
|
||||
}
|
||||
8
pkg-manifest/commands/tsconfig.lint.json
Normal file
8
pkg-manifest/commands/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -7601,6 +7601,37 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
pkg-manifest/commands:
|
||||
dependencies:
|
||||
'@pnpm/cli.utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/utils
|
||||
'@pnpm/config.reader':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/reader
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/error
|
||||
'@pnpm/object.property-path':
|
||||
specifier: workspace:*
|
||||
version: link:../../object/property-path
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/types
|
||||
render-help:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0
|
||||
devDependencies:
|
||||
'@jest/globals':
|
||||
specifier: 'catalog:'
|
||||
version: 30.3.0
|
||||
'@pnpm/pkg-manifest.commands':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/prepare':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/prepare
|
||||
|
||||
pkg-manifest/reader:
|
||||
dependencies:
|
||||
'@pnpm/error':
|
||||
@@ -7777,6 +7808,9 @@ importers:
|
||||
'@pnpm/patching.commands':
|
||||
specifier: workspace:*
|
||||
version: link:../patching/commands
|
||||
'@pnpm/pkg-manifest.commands':
|
||||
specifier: workspace:*
|
||||
version: link:../pkg-manifest/commands
|
||||
'@pnpm/pkg-manifest.reader':
|
||||
specifier: workspace:*
|
||||
version: link:../pkg-manifest/reader
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/nopt": "catalog:",
|
||||
"@pnpm/patching.commands": "workspace:*",
|
||||
"@pnpm/pkg-manifest.commands": "workspace:*",
|
||||
"@pnpm/pkg-manifest.reader": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@pnpm/registry-mock": "catalog:",
|
||||
|
||||
@@ -18,6 +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 { 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'
|
||||
@@ -150,6 +151,7 @@ const commands: CommandDefinition[] = [
|
||||
selfUpdate,
|
||||
init,
|
||||
install,
|
||||
pkg,
|
||||
installTest,
|
||||
link,
|
||||
list,
|
||||
|
||||
@@ -8,7 +8,6 @@ const NOT_IMPLEMENTED_COMMANDS = [
|
||||
'issues',
|
||||
'prefix',
|
||||
'profile',
|
||||
'pkg',
|
||||
'repo',
|
||||
'set-script',
|
||||
'team',
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
{
|
||||
"path": "../patching/commands"
|
||||
},
|
||||
{
|
||||
"path": "../pkg-manifest/commands"
|
||||
},
|
||||
{
|
||||
"path": "../pkg-manifest/reader"
|
||||
},
|
||||
|
||||
@@ -290,7 +290,7 @@ function normalize (manifest: ProjectManifest): ProjectManifest {
|
||||
for (const key in manifest) {
|
||||
if (Object.hasOwn(manifest, key)) {
|
||||
const value = manifest[key as keyof ProjectManifest]
|
||||
if (typeof value !== 'object' || !dependencyKeys.has(key)) {
|
||||
if (typeof value !== 'object' || value === null || !dependencyKeys.has(key)) {
|
||||
result[key] = structuredClone(value)
|
||||
} else {
|
||||
const keys = Object.keys(value)
|
||||
|
||||
Reference in New Issue
Block a user