feat(hooks): add beforePacking hook (#10303)

* feat(hooks): add `readPackageForPublishing` hook

* feat: pass project `dir` parameter to `readPackageForPublishing` hook

* chore: cleanup

* fix: add support for multiple pnpmfiles

* test: readPackageForPublishing hook

* test: add more tests

* test: small update

* refactor: pass in `hooks` as an option

* test: pass in `hooks` as an option

* test: small update

* chore: rename `readPackageForPublishing` to `beforePacking`
This commit is contained in:
Dasa Paddock
2025-12-21 06:49:47 -08:00
committed by GitHub
parent 90bd3c31f8
commit 29764fb140
14 changed files with 154 additions and 5 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/exportable-manifest": minor
"pnpm": minor
---
Add support for a hook called `beforePacking` that can be used to customize the `package.json` contents at publish time [#3816](https://github.com/pnpm/pnpm/issues/3816).

View File

@@ -6,6 +6,6 @@ charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
[*.{ts,js,json}]
[*.{ts,js,cjs,json}]
indent_style = space
indent_size = 2

View File

@@ -11,6 +11,8 @@ export interface HookContext {
export interface Hooks {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Flexible hook signature for any package manifest
readPackage?: (pkg: any, context: HookContext) => any
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Flexible hook signature for any package manifest
beforePacking?: (pkg: any, dir: string, context: HookContext) => any
preResolution?: PreResolutionHook
afterAllResolved?: (lockfile: LockfileObject, context: HookContext) => LockfileObject | Promise<LockfileObject>
filterLog?: (log: Log) => boolean

View File

@@ -5,7 +5,7 @@ import { createHashFromMultipleFiles } from '@pnpm/crypto.hash'
import pathAbsolute from 'path-absolute'
import type { CustomFetchers } from '@pnpm/fetcher-base'
import { type ImportIndexedPackageAsync } from '@pnpm/store-controller-types'
import { type ReadPackageHook, type BaseManifest } from '@pnpm/types'
import { type ReadPackageHook, type BeforePackingHook, type BaseManifest } from '@pnpm/types'
import { type LockfileObject } from '@pnpm/lockfile.types'
import { requirePnpmfile, type Pnpmfile, type Finders } from './requirePnpmfile.js'
import { type Hooks, type HookContext } from './Hooks.js'
@@ -34,6 +34,7 @@ interface PnpmfileEntryLoaded {
export interface CookedHooks {
readPackage?: ReadPackageHook[]
beforePacking?: BeforePackingHook[]
preResolution?: Array<(ctx: PreResolutionHookContext) => Promise<void>>
afterAllResolved?: Array<(lockfile: LockfileObject) => LockfileObject | Promise<LockfileObject>>
filterLog?: Array<Cook<Required<Hooks>['filterLog']>>
@@ -104,8 +105,9 @@ export async function requireHooks (
}))
const mergedFinders: Finders = {}
const cookedHooks: CookedHooks & Required<Pick<CookedHooks, 'readPackage' | 'preResolution' | 'afterAllResolved' | 'filterLog' | 'updateConfig'>> = {
const cookedHooks: CookedHooks & Required<Pick<CookedHooks, 'readPackage' | 'beforePacking' | 'preResolution' | 'afterAllResolved' | 'filterLog' | 'updateConfig'>> = {
readPackage: [],
beforePacking: [],
preResolution: [],
afterAllResolved: [],
filterLog: [],
@@ -154,6 +156,13 @@ export async function requireHooks (
cookedHooks.readPackage.push(<Pkg extends BaseManifest>(pkg: Pkg, _dir?: string) => fn(pkg, context))
}
// beforePacking
if (fileHooks.beforePacking) {
const fn = fileHooks.beforePacking
const context = createReadPackageHookContext(file, prefix, 'beforePacking')
cookedHooks.beforePacking.push(<Pkg extends BaseManifest>(pkg: Pkg, dir: string) => fn(pkg, dir, context))
}
// afterAllResolved
if (fileHooks.afterAllResolved) {
const fn = fileHooks.afterAllResolved

View File

@@ -82,6 +82,9 @@ export async function requirePnpmfile (pnpmFilePath: string, prefix: string): Pr
}
return newPkg
}
if (pnpmfile?.hooks?.beforePacking && typeof pnpmfile.hooks.beforePacking !== 'function') {
throw new TypeError('hooks.beforePacking should be a function')
}
}
return { pnpmfileModule: pnpmfile }
} catch (err: unknown) {

View File

@@ -15,6 +15,8 @@ export type IncludedDependencies = {
export type ReadPackageHook = <Pkg extends BaseManifest> (pkg: Pkg, dir?: string) => Pkg | Promise<Pkg>
export type BeforePackingHook = <Pkg extends BaseManifest> (pkg: Pkg, dir: string) => Pkg | Promise<Pkg>
export interface FinderContext {
alias: string
name: string

View File

@@ -43,6 +43,7 @@
"@pnpm/catalogs.config": "workspace:*",
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/exportable-manifest": "workspace:*",
"@pnpm/pnpmfile": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@types/cross-spawn": "catalog:",
"@types/ramda": "catalog:",

View File

@@ -4,6 +4,7 @@ import { type Catalogs } from '@pnpm/catalogs.types'
import { PnpmError } from '@pnpm/error'
import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser'
import { tryReadProjectManifest } from '@pnpm/read-project-manifest'
import { type Hooks } from '@pnpm/pnpmfile'
import { type Dependencies, type ProjectManifest } from '@pnpm/types'
import { omit } from 'ramda'
import pMapValues from 'p-map-values'
@@ -20,6 +21,7 @@ const PREPUBLISH_SCRIPTS = [
export interface MakePublishManifestOptions {
catalogs: Catalogs
hooks?: Hooks
modulesDir?: string
readmeFile?: string
}
@@ -29,7 +31,7 @@ export async function createExportableManifest (
originalManifest: ProjectManifest,
opts: MakePublishManifestOptions
): Promise<ProjectManifest> {
const publishManifest: ProjectManifest = omit(['pnpm', 'scripts', 'packageManager'], originalManifest)
let publishManifest: ProjectManifest = omit(['pnpm', 'scripts', 'packageManager'], originalManifest)
if (originalManifest.scripts != null) {
publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts)
}
@@ -63,6 +65,11 @@ export async function createExportableManifest (
publishManifest.readme ??= opts.readmeFile
}
for (const hook of opts?.hooks?.beforePacking ?? []) {
// eslint-disable-next-line no-await-in-loop
publishManifest = await hook(publishManifest, dir) ?? publishManifest
}
return publishManifest
}

View File

@@ -0,0 +1,101 @@
import fs from 'fs'
import { createExportableManifest, type MakePublishManifestOptions } from '@pnpm/exportable-manifest'
import { requireHooks } from '@pnpm/pnpmfile'
import { prepare } from '@pnpm/prepare'
import { sync as writeYamlFile } from 'write-yaml-file'
const defaultOpts: MakePublishManifestOptions = {
catalogs: {},
}
test('basic test', async () => {
prepare()
fs.writeFileSync('.pnpmfile.cjs', `
module.exports = {
hooks: {
beforePacking: (pkg, dir, context) => {
context.log(dir)
pkg.foo = 'bar'
return pkg // return optional
},
},
}`, 'utf8')
const { hooks } = await requireHooks(process.cwd(), { tryLoadDefaultPnpmfile: true })
expect(await createExportableManifest(process.cwd(), {
name: 'foo',
version: '1.0.0',
dependencies: {
qar: '2',
},
}, { ...defaultOpts, hooks })).toStrictEqual({
name: 'foo',
version: '1.0.0',
dependencies: {
qar: '2',
},
foo: 'bar',
})
})
test('hook returns new manifest', async () => {
prepare()
fs.writeFileSync('.pnpmfile.cjs', `
module.exports = {
hooks: {
beforePacking: (pkg) => {
return { type: 'module' }
},
},
}`, 'utf8')
const { hooks } = await requireHooks(process.cwd(), { tryLoadDefaultPnpmfile: true })
expect(await createExportableManifest(process.cwd(), {
name: 'foo',
version: '1.0.0',
}, { ...defaultOpts, hooks })).toStrictEqual({
type: 'module',
})
})
test('hook in multiple pnpmfiles', async () => {
prepare()
const pnpmfiles = ['pnpmfile1.cjs', 'pnpmfile2.cjs']
fs.writeFileSync(pnpmfiles[0], `
module.exports = {
hooks: {
beforePacking: (pkg) => {
pkg.foo = 'foo'
},
},
}`, 'utf8')
fs.writeFileSync(pnpmfiles[1], `
module.exports = {
hooks: {
beforePacking: (pkg) => {
pkg.bar = 'bar'
},
},
}`, 'utf8')
writeYamlFile('pnpm-workspace.yaml', { pnpmfile: pnpmfiles })
const { hooks } = await requireHooks(process.cwd(), { pnpmfiles })
expect(await createExportableManifest(process.cwd(), {
name: 'foo',
version: '1.0.0',
dependencies: {
qar: '2',
},
}, { ...defaultOpts, hooks })).toStrictEqual({
name: 'foo',
version: '1.0.0',
dependencies: {
qar: '2',
},
foo: 'foo',
bar: 'bar',
})
})

View File

@@ -21,6 +21,9 @@
{
"path": "../../catalogs/types"
},
{
"path": "../../hooks/pnpmfile"
},
{
"path": "../../packages/error"
},

6
pnpm-lock.yaml generated
View File

@@ -6381,6 +6381,9 @@ importers:
'@pnpm/exportable-manifest':
specifier: workspace:*
version: 'link:'
'@pnpm/pnpmfile':
specifier: workspace:*
version: link:../../hooks/pnpmfile
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare
@@ -7155,6 +7158,9 @@ importers:
'@pnpm/plugin-commands-publishing':
specifier: workspace:*
version: 'link:'
'@pnpm/pnpmfile':
specifier: workspace:*
version: link:../../hooks/pnpmfile
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare

View File

@@ -75,6 +75,7 @@
"@pnpm/catalogs.config": "workspace:*",
"@pnpm/logger": "workspace:*",
"@pnpm/plugin-commands-publishing": "workspace:*",
"@pnpm/pnpmfile": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "catalog:",
"@pnpm/test-ipc-server": "workspace:*",

View File

@@ -8,6 +8,7 @@ import { readProjectManifest } from '@pnpm/cli-utils'
import { createExportableManifest } from '@pnpm/exportable-manifest'
import { packlist } from '@pnpm/fs.packlist'
import { getBinsFromPackageManifest } from '@pnpm/package-bins'
import { type Hooks } from '@pnpm/pnpmfile'
import { type ProjectManifest, type Project, type ProjectRootDir, type ProjectsGraph, type DependencyManifest } from '@pnpm/types'
import { glob } from 'tinyglobby'
import { pick } from 'ramda'
@@ -98,6 +99,7 @@ export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs
| 'nodeLinker'
> & Partial<Pick<Config, 'extraBinPaths'
| 'extraEnv'
| 'hooks'
| 'recursive'
| 'selectedProjectsGraph'
| 'workspaceConcurrency'
@@ -239,6 +241,7 @@ export async function api (opts: PackOptions): Promise<PackResult> {
manifest,
embedReadme: opts.embedReadme,
catalogs: opts.catalogs ?? {},
hooks: opts.hooks,
})
const files = await packlist(dir, {
packageJsonCache: {
@@ -355,11 +358,13 @@ async function createPublishManifest (opts: {
modulesDir: string
manifest: ProjectManifest
catalogs: Catalogs
hooks?: Hooks
}): Promise<ProjectManifest> {
const { projectDir, embedReadme, modulesDir, manifest, catalogs } = opts
const { projectDir, embedReadme, modulesDir, manifest, catalogs, hooks } = opts
const readmeFile = embedReadme ? await readReadmeFile(projectDir) : undefined
return createExportableManifest(projectDir, manifest, {
catalogs,
hooks,
readmeFile,
modulesDir,
})

View File

@@ -45,6 +45,9 @@
{
"path": "../../fs/packlist"
},
{
"path": "../../hooks/pnpmfile"
},
{
"path": "../../network/auth-header"
},