diff --git a/.changeset/early-taxis-carry.md b/.changeset/early-taxis-carry.md new file mode 100644 index 0000000000..7baae3b747 --- /dev/null +++ b/.changeset/early-taxis-carry.md @@ -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). diff --git a/.editorconfig b/.editorconfig index 270dd84bb6..a74160f255 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/hooks/pnpmfile/src/Hooks.ts b/hooks/pnpmfile/src/Hooks.ts index 408eb0b3ea..b1b0de4564 100644 --- a/hooks/pnpmfile/src/Hooks.ts +++ b/hooks/pnpmfile/src/Hooks.ts @@ -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 filterLog?: (log: Log) => boolean diff --git a/hooks/pnpmfile/src/requireHooks.ts b/hooks/pnpmfile/src/requireHooks.ts index 356bed05cd..890dd54d89 100644 --- a/hooks/pnpmfile/src/requireHooks.ts +++ b/hooks/pnpmfile/src/requireHooks.ts @@ -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> afterAllResolved?: Array<(lockfile: LockfileObject) => LockfileObject | Promise> filterLog?: Array['filterLog']>> @@ -104,8 +105,9 @@ export async function requireHooks ( })) const mergedFinders: Finders = {} - const cookedHooks: CookedHooks & Required> = { + const cookedHooks: CookedHooks & Required> = { readPackage: [], + beforePacking: [], preResolution: [], afterAllResolved: [], filterLog: [], @@ -154,6 +156,13 @@ export async function requireHooks ( cookedHooks.readPackage.push((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: Pkg, dir: string) => fn(pkg, dir, context)) + } + // afterAllResolved if (fileHooks.afterAllResolved) { const fn = fileHooks.afterAllResolved diff --git a/hooks/pnpmfile/src/requirePnpmfile.ts b/hooks/pnpmfile/src/requirePnpmfile.ts index decd8da56e..0e8752f5bf 100644 --- a/hooks/pnpmfile/src/requirePnpmfile.ts +++ b/hooks/pnpmfile/src/requirePnpmfile.ts @@ -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) { diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 443bb4b231..2571b8ac26 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -15,6 +15,8 @@ export type IncludedDependencies = { export type ReadPackageHook = (pkg: Pkg, dir?: string) => Pkg | Promise +export type BeforePackingHook = (pkg: Pkg, dir: string) => Pkg | Promise + export interface FinderContext { alias: string name: string diff --git a/pkg-manifest/exportable-manifest/package.json b/pkg-manifest/exportable-manifest/package.json index 51f65bcc0a..3969b82797 100644 --- a/pkg-manifest/exportable-manifest/package.json +++ b/pkg-manifest/exportable-manifest/package.json @@ -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:", diff --git a/pkg-manifest/exportable-manifest/src/index.ts b/pkg-manifest/exportable-manifest/src/index.ts index 528e33a9c7..ebd8681a26 100644 --- a/pkg-manifest/exportable-manifest/src/index.ts +++ b/pkg-manifest/exportable-manifest/src/index.ts @@ -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 { - 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 } diff --git a/pkg-manifest/exportable-manifest/test/beforePackingHook.test.ts b/pkg-manifest/exportable-manifest/test/beforePackingHook.test.ts new file mode 100644 index 0000000000..c0c8048012 --- /dev/null +++ b/pkg-manifest/exportable-manifest/test/beforePackingHook.test.ts @@ -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', + }) +}) diff --git a/pkg-manifest/exportable-manifest/tsconfig.json b/pkg-manifest/exportable-manifest/tsconfig.json index fef35b586d..7cc738bdf2 100644 --- a/pkg-manifest/exportable-manifest/tsconfig.json +++ b/pkg-manifest/exportable-manifest/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../../catalogs/types" }, + { + "path": "../../hooks/pnpmfile" + }, { "path": "../../packages/error" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85343ff35a..9855e5a356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/releasing/plugin-commands-publishing/package.json b/releasing/plugin-commands-publishing/package.json index 8d85eccdb0..6d6c318b2b 100644 --- a/releasing/plugin-commands-publishing/package.json +++ b/releasing/plugin-commands-publishing/package.json @@ -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:*", diff --git a/releasing/plugin-commands-publishing/src/pack.ts b/releasing/plugin-commands-publishing/src/pack.ts index 82c6d55cd3..0429a7c01c 100644 --- a/releasing/plugin-commands-publishing/src/pack.ts +++ b/releasing/plugin-commands-publishing/src/pack.ts @@ -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 & Pick & Partial { 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 { - 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, }) diff --git a/releasing/plugin-commands-publishing/tsconfig.json b/releasing/plugin-commands-publishing/tsconfig.json index 30d9869fc4..dbe17b2350 100644 --- a/releasing/plugin-commands-publishing/tsconfig.json +++ b/releasing/plugin-commands-publishing/tsconfig.json @@ -45,6 +45,9 @@ { "path": "../../fs/packlist" }, + { + "path": "../../hooks/pnpmfile" + }, { "path": "../../network/auth-header" },