mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
feat(publish): add --skip-manifest-obfuscation flag for pack/publish (#11393)
* feat(publish): add preserve-manifest-fields option * fix(publish): omit pnpm field when preserveManifestFields is enabled The preserve-manifest-fields option was deep-cloning the entire manifest, which leaked the pnpm-specific `pnpm` field into packed/published manifests. The PR description explicitly calls for this field to remain stripped; align the implementation, tests, help text, and changeset accordingly. * refactor(publish): rename preserve-manifest-fields to skip-manifest-obfuscation The original name implied the flag preserves *all* manifest fields, which isn't true — the pnpm-specific `pnpm` field is still stripped, and `publishConfig` / workspace-protocol / catalog rewriting still happen. The flag is really an escape hatch from pnpm's manifest mangling, so name it that way. Help text and changeset updated to match. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
8
.changeset/skip-manifest-obfuscation-opt-in.md
Normal file
8
.changeset/skip-manifest-obfuscation-opt-in.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/releasing.commands": minor
|
||||
"@pnpm/releasing.exportable-manifest": minor
|
||||
"@pnpm/config.reader": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add a `skip-manifest-obfuscation` option for `pnpm pack` and `pnpm publish`. When enabled, the original `packageManager` field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific `pnpm` field continues to be omitted.
|
||||
@@ -210,6 +210,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
modulesCacheMaxAge: number
|
||||
dlxCacheMaxAge: number
|
||||
embedReadme?: boolean
|
||||
skipManifestObfuscation?: boolean
|
||||
gitShallowHosts?: string[]
|
||||
legacyDirFiltering?: boolean
|
||||
allowBuilds?: Record<string, boolean | string>
|
||||
|
||||
@@ -152,6 +152,7 @@ export const excludedPnpmKeys = [
|
||||
'test-pattern',
|
||||
'changed-files-ignore-pattern',
|
||||
'embed-readme',
|
||||
'skip-manifest-obfuscation',
|
||||
'fail-if-no-match',
|
||||
'sync-injected-deps-after-scripts',
|
||||
'cpu',
|
||||
|
||||
@@ -208,6 +208,7 @@ export async function getConfig (opts: {
|
||||
'workspace-concurrency': getDefaultWorkspaceConcurrency(),
|
||||
'workspace-prefix': opts.workspaceDir,
|
||||
'embed-readme': false,
|
||||
'skip-manifest-obfuscation': false,
|
||||
'registry-supports-time-field': false,
|
||||
'virtual-store-dir-max-length': isWindows() ? 60 : 120,
|
||||
'virtual-store-only': false,
|
||||
@@ -886,4 +887,3 @@ function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config & ConfigCo
|
||||
}
|
||||
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ export const pnpmTypes = {
|
||||
'test-pattern': [String, Array],
|
||||
'changed-files-ignore-pattern': [String, Array],
|
||||
'embed-readme': Boolean,
|
||||
'skip-manifest-obfuscation': Boolean,
|
||||
'update-notifier': Boolean,
|
||||
'agent': [null, String],
|
||||
'registry-supports-time-field': Boolean,
|
||||
|
||||
@@ -32,6 +32,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
...cliOptionsTypes(),
|
||||
...pick([
|
||||
'npm-path',
|
||||
'skip-manifest-obfuscation',
|
||||
], allTypes),
|
||||
}
|
||||
}
|
||||
@@ -45,6 +46,7 @@ export function cliOptionsTypes (): Record<string, unknown> {
|
||||
'pack-destination',
|
||||
'pack-gzip-level',
|
||||
'json',
|
||||
'skip-manifest-obfuscation',
|
||||
'workspace-concurrency',
|
||||
], allTypes),
|
||||
}
|
||||
@@ -82,6 +84,10 @@ export function help (): string {
|
||||
name: '--recursive',
|
||||
shortAlias: '-r',
|
||||
},
|
||||
{
|
||||
description: 'Skip pnpm\'s manifest obfuscation: keep the original `packageManager` field and publish lifecycle scripts in the packed manifest instead of stripping them. The pnpm-specific `pnpm` field is still omitted.',
|
||||
name: '--skip-manifest-obfuscation',
|
||||
},
|
||||
{
|
||||
description: `Set the maximum number of concurrency. Default is ${getDefaultWorkspaceConcurrency()}. For unlimited concurrency use Infinity.`,
|
||||
name: '--workspace-concurrency <number>',
|
||||
@@ -98,6 +104,7 @@ export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs
|
||||
| 'embedReadme'
|
||||
| 'packGzipLevel'
|
||||
| 'nodeLinker'
|
||||
| 'skipManifestObfuscation'
|
||||
| 'userAgent'
|
||||
> & Partial<Pick<Config, 'extraBinPaths'
|
||||
| 'extraEnv'
|
||||
@@ -230,6 +237,7 @@ export async function api (opts: PackOptions): Promise<PackResult> {
|
||||
embedReadme: opts.embedReadme,
|
||||
catalogs: opts.catalogs ?? {},
|
||||
hooks: opts.hooks,
|
||||
skipManifestObfuscation: opts.skipManifestObfuscation,
|
||||
})
|
||||
// Strip semver build metadata (the `+<build>` segment) from the published version so that
|
||||
// the tarball, the manifest packed inside it, and the metadata sent to the registry all agree.
|
||||
@@ -394,14 +402,16 @@ async function createPublishManifest (opts: {
|
||||
manifest: ProjectManifest
|
||||
catalogs: Catalogs
|
||||
hooks?: Hooks
|
||||
skipManifestObfuscation?: boolean
|
||||
}): Promise<ExportedManifest> {
|
||||
const { projectDir, embedReadme, modulesDir, manifest, catalogs, hooks } = opts
|
||||
const { projectDir, embedReadme, modulesDir, manifest, catalogs, hooks, skipManifestObfuscation } = opts
|
||||
const readmeFile = embedReadme ? await readReadmeFile(projectDir) : undefined
|
||||
return createExportableManifest(projectDir, manifest, {
|
||||
catalogs,
|
||||
hooks,
|
||||
readmeFile,
|
||||
modulesDir,
|
||||
skipManifestObfuscation,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'access',
|
||||
'git-checks',
|
||||
'ignore-scripts',
|
||||
'skip-manifest-obfuscation',
|
||||
'provenance',
|
||||
'npm-path',
|
||||
'otp',
|
||||
@@ -87,6 +88,10 @@ export function help (): string {
|
||||
description: 'Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)',
|
||||
name: '--ignore-scripts',
|
||||
},
|
||||
{
|
||||
description: 'Skip pnpm\'s manifest obfuscation: keep the original `packageManager` field and publish lifecycle scripts in the published manifest instead of stripping them. The pnpm-specific `pnpm` field is still omitted.',
|
||||
name: '--skip-manifest-obfuscation',
|
||||
},
|
||||
{
|
||||
description: 'Packages are proceeded to be published even if their current version is already in the registry. This is useful when a "prepublishOnly" script bumps the version of the package before it is published',
|
||||
name: '--force',
|
||||
@@ -124,7 +129,7 @@ export async function handler (
|
||||
json?: boolean
|
||||
recursive?: boolean
|
||||
workspaceDir?: string
|
||||
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme'>
|
||||
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'skipManifestObfuscation'>
|
||||
& Pick<ConfigContext, 'allProjects'>,
|
||||
params: string[]
|
||||
): Promise<{ exitCode?: number, output?: string } | undefined> {
|
||||
@@ -161,7 +166,7 @@ export async function publish (
|
||||
engineStrict?: boolean
|
||||
recursive?: boolean
|
||||
workspaceDir?: string
|
||||
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel'>
|
||||
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel' | 'skipManifestObfuscation'>
|
||||
& Pick<ConfigContext, 'allProjects'>,
|
||||
params: string[]
|
||||
): Promise<PublishResult> {
|
||||
|
||||
@@ -464,6 +464,42 @@ test('pack: remove publishConfig', async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('pack: preserves packageManager and publish lifecycle scripts when skipManifestObfuscation is enabled, but still omits pnpm', async () => {
|
||||
prepare({
|
||||
name: 'skip-manifest-obfuscation',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
pnpm: {
|
||||
testField: true,
|
||||
},
|
||||
scripts: {
|
||||
prepublishOnly: 'echo prepublishOnly',
|
||||
postinstall: 'echo postinstall',
|
||||
},
|
||||
} as Parameters<typeof prepare>[0] & { pnpm?: Record<string, unknown> })
|
||||
|
||||
await pack.handler({
|
||||
...DEFAULT_OPTS,
|
||||
argv: { original: [] },
|
||||
dir: process.cwd(),
|
||||
extraBinPaths: [],
|
||||
packDestination: process.cwd(),
|
||||
skipManifestObfuscation: true,
|
||||
})
|
||||
|
||||
await tar.x({ file: 'skip-manifest-obfuscation-1.0.0.tgz' })
|
||||
|
||||
expect((await import(path.resolve('package/package.json'))).default).toEqual({
|
||||
name: 'skip-manifest-obfuscation',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
scripts: {
|
||||
prepublishOnly: 'echo prepublishOnly',
|
||||
postinstall: 'echo postinstall',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('pack should read from the correct node_modules when publishing from a custom directory', async () => {
|
||||
prepare({
|
||||
name: 'custom-publish-dir',
|
||||
|
||||
@@ -381,6 +381,50 @@ test('publish: package with publishConfig.directory', async () => {
|
||||
expect(fs.existsSync('node_modules/publish_config_directory_dist_package/prepublishOnly')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('publish: preserves packageManager and publish lifecycle scripts when skipManifestObfuscation is enabled, but still omits pnpm', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'test-publish-skip-manifest-obfuscation',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
pnpm: {
|
||||
testField: true,
|
||||
},
|
||||
scripts: {
|
||||
prepublishOnly: 'echo prepublishOnly',
|
||||
postinstall: 'echo postinstall',
|
||||
},
|
||||
} as Parameters<typeof preparePackages>[0][number] & { pnpm?: Record<string, unknown> },
|
||||
{
|
||||
name: 'test-publish-skip-manifest-obfuscation-installation',
|
||||
version: '1.0.0',
|
||||
},
|
||||
])
|
||||
|
||||
process.chdir('test-publish-skip-manifest-obfuscation')
|
||||
await publish.handler({
|
||||
...DEFAULT_OPTS,
|
||||
argv: { original: ['publish'] },
|
||||
configByUri: CONFIG_BY_URI,
|
||||
dir: process.cwd(),
|
||||
skipManifestObfuscation: true,
|
||||
}, [])
|
||||
|
||||
process.chdir('../test-publish-skip-manifest-obfuscation-installation')
|
||||
crossSpawn.sync(pnpmBin, ['add', 'test-publish-skip-manifest-obfuscation', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`], { env: SPAWN_ENV })
|
||||
|
||||
const { default: publishedManifest } = await import(path.resolve('node_modules/test-publish-skip-manifest-obfuscation/package.json'))
|
||||
expect(publishedManifest).toEqual({
|
||||
name: 'test-publish-skip-manifest-obfuscation',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
scripts: {
|
||||
prepublishOnly: 'echo prepublishOnly',
|
||||
postinstall: 'echo postinstall',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.skip('publish package that calls executable from the workspace .bin folder in prepublishOnly script', async () => {
|
||||
await using server = await createTestIpcServer()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser'
|
||||
import type { Dependencies, ProjectManifest } from '@pnpm/types'
|
||||
import { tryReadProjectManifest } from '@pnpm/workspace.project-manifest-reader'
|
||||
import { pMapValues } from 'p-map-values'
|
||||
import { omit } from 'ramda'
|
||||
import { clone, omit } from 'ramda'
|
||||
|
||||
import { overridePublishConfig } from './overridePublishConfig.js'
|
||||
import { type ExportedManifest, transform } from './transform/index.js'
|
||||
@@ -28,6 +28,7 @@ export interface MakePublishManifestOptions {
|
||||
catalogs: Catalogs
|
||||
hooks?: Hooks
|
||||
modulesDir?: string
|
||||
skipManifestObfuscation?: boolean
|
||||
readmeFile?: string
|
||||
}
|
||||
|
||||
@@ -36,9 +37,14 @@ export async function createExportableManifest (
|
||||
originalManifest: ProjectManifest,
|
||||
opts: MakePublishManifestOptions
|
||||
): Promise<ExportedManifest> {
|
||||
let publishManifest: ProjectManifest = omit(['scripts', 'packageManager', 'pnpm' as keyof ProjectManifest], originalManifest)
|
||||
if (originalManifest.scripts != null) {
|
||||
publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts)
|
||||
let publishManifest: ProjectManifest
|
||||
if (opts.skipManifestObfuscation) {
|
||||
publishManifest = omit(['pnpm' as keyof ProjectManifest], clone(originalManifest))
|
||||
} else {
|
||||
publishManifest = omit(['scripts', 'packageManager', 'pnpm' as keyof ProjectManifest], originalManifest)
|
||||
if (originalManifest.scripts != null) {
|
||||
publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts)
|
||||
}
|
||||
}
|
||||
|
||||
const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs)
|
||||
|
||||
@@ -82,6 +82,63 @@ test('publish lifecycle scripts are removed', async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('packageManager and publish lifecycle scripts are preserved when skipManifestObfuscation is enabled, but pnpm is still omitted', async () => {
|
||||
const manifest: ProjectManifest & { pnpm?: Record<string, unknown> } = {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
pnpm: {
|
||||
testField: true,
|
||||
},
|
||||
scripts: {
|
||||
prepublishOnly: 'echo',
|
||||
postinstall: 'echo hello',
|
||||
},
|
||||
}
|
||||
|
||||
expect(await createExportableManifest(process.cwd(), manifest, {
|
||||
...defaultOpts,
|
||||
skipManifestObfuscation: true,
|
||||
})).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
packageManager: 'pnpm@10.0.0',
|
||||
scripts: {
|
||||
prepublishOnly: 'echo',
|
||||
postinstall: 'echo hello',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('skipManifestObfuscation does not mutate the original manifest', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
publishConfig: {
|
||||
main: './dist/index.js',
|
||||
},
|
||||
}
|
||||
|
||||
expect(await createExportableManifest(process.cwd(), manifest, {
|
||||
...defaultOpts,
|
||||
skipManifestObfuscation: true,
|
||||
readmeFile: 'readme content',
|
||||
})).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
main: './dist/index.js',
|
||||
readme: 'readme content',
|
||||
})
|
||||
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
publishConfig: {
|
||||
main: './dist/index.js',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('readme added to published manifest', async () => {
|
||||
expect(await createExportableManifest(process.cwd(), {
|
||||
name: 'foo',
|
||||
|
||||
Reference in New Issue
Block a user