mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 15:18:15 -05:00
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:
6
.changeset/early-taxis-carry.md
Normal file
6
.changeset/early-taxis-carry.md
Normal 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).
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
101
pkg-manifest/exportable-manifest/test/beforePackingHook.test.ts
Normal file
101
pkg-manifest/exportable-manifest/test/beforePackingHook.test.ts
Normal 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',
|
||||
})
|
||||
})
|
||||
@@ -21,6 +21,9 @@
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../hooks/pnpmfile"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
{
|
||||
"path": "../../fs/packlist"
|
||||
},
|
||||
{
|
||||
"path": "../../hooks/pnpmfile"
|
||||
},
|
||||
{
|
||||
"path": "../../network/auth-header"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user