diff --git a/.changeset/easy-toys-design.md b/.changeset/easy-toys-design.md new file mode 100644 index 0000000000..f7bff8e079 --- /dev/null +++ b/.changeset/easy-toys-design.md @@ -0,0 +1,10 @@ +--- +"@pnpm/plugin-commands-rebuild": major +"@pnpm/modules-yaml": major +"@pnpm/headless": major +"@pnpm/build-modules": major +"@pnpm/core": major +"@pnpm/exec.build-commands": major +--- + +`ignoreBuilds` is now a set of DepPath. diff --git a/.changeset/sharp-snakes-love.md b/.changeset/sharp-snakes-love.md new file mode 100644 index 0000000000..b6430b948e --- /dev/null +++ b/.changeset/sharp-snakes-love.md @@ -0,0 +1,5 @@ +--- +"@pnpm/types": minor +--- + +Add type for IgnoredBuilds. diff --git a/.changeset/tender-socks-show.md b/.changeset/tender-socks-show.md new file mode 100644 index 0000000000..b846d0c568 --- /dev/null +++ b/.changeset/tender-socks-show.md @@ -0,0 +1,5 @@ +--- +"pnpm": patch +--- + +Improved reporting of ignored dependency scripts [#10276](https://github.com/pnpm/pnpm/pull/10276). diff --git a/exec/build-commands/package.json b/exec/build-commands/package.json index 5e1b27ba1d..141e302f06 100644 --- a/exec/build-commands/package.json +++ b/exec/build-commands/package.json @@ -34,6 +34,7 @@ "dependencies": { "@pnpm/config": "workspace:*", "@pnpm/config.config-writer": "workspace:*", + "@pnpm/dependency-path": "workspace:*", "@pnpm/modules-yaml": "workspace:*", "@pnpm/plugin-commands-rebuild": "workspace:*", "@pnpm/prepare-temp-dir": "workspace:*", diff --git a/exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts b/exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts index 8467c8e6ac..bae3095e6c 100644 --- a/exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts +++ b/exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts @@ -1,4 +1,5 @@ import path from 'path' +import { parse } from '@pnpm/dependency-path' import { type Modules, readModulesManifest } from '@pnpm/modules-yaml' import { type IgnoredBuildsCommandOpts } from './ignoredBuilds.js' @@ -11,8 +12,18 @@ export interface GetAutomaticallyIgnoredBuildsResult { export async function getAutomaticallyIgnoredBuilds (opts: IgnoredBuildsCommandOpts): Promise { const modulesDir = getModulesDir(opts) const modulesManifest = await readModulesManifest(modulesDir) + let automaticallyIgnoredBuilds: null | string[] + if (modulesManifest?.ignoredBuilds) { + const ignoredPkgNames = new Set() + for (const depPath of modulesManifest?.ignoredBuilds) { + ignoredPkgNames.add(parse(depPath).name ?? depPath) + } + automaticallyIgnoredBuilds = Array.from(ignoredPkgNames) + } else { + automaticallyIgnoredBuilds = null + } return { - automaticallyIgnoredBuilds: modulesManifest && (modulesManifest.ignoredBuilds ?? []), + automaticallyIgnoredBuilds, modulesDir, modulesManifest, } diff --git a/exec/build-commands/test/approveBuilds.test.ts b/exec/build-commands/test/approveBuilds.test.ts index d7be81c728..3616c793c2 100644 --- a/exec/build-commands/test/approveBuilds.test.ts +++ b/exec/build-commands/test/approveBuilds.test.ts @@ -6,7 +6,7 @@ import { type RebuildCommandOpts } from '@pnpm/plugin-commands-rebuild' import { prepare } from '@pnpm/prepare' import { type ProjectManifest } from '@pnpm/types' import { getConfig } from '@pnpm/config' -import { type Modules, readModulesManifest } from '@pnpm/modules-yaml' +import { readModulesManifest } from '@pnpm/modules-yaml' import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { jest } from '@jest/globals' import { loadJsonFileSync } from 'load-json-file' @@ -124,7 +124,7 @@ test('approve no builds', async () => { expect(fs.readdirSync('node_modules/@pnpm.e2e/install-script-example')).not.toContain('generated-by-install.js') // Covers https://github.com/pnpm/pnpm/issues/9296 - expect(await readModulesManifest('node_modules')).not.toHaveProperty(['ignoredBuilds' satisfies keyof Modules]) + expect((await readModulesManifest('node_modules'))!.ignoredBuilds).toBeUndefined() }) test("works when root project manifest doesn't exist in a workspace", async () => { diff --git a/exec/build-commands/test/ignoredBuilds.test.ts b/exec/build-commands/test/ignoredBuilds.test.ts index 51d23d4456..213247848f 100644 --- a/exec/build-commands/test/ignoredBuilds.test.ts +++ b/exec/build-commands/test/ignoredBuilds.test.ts @@ -3,6 +3,7 @@ import fs from 'fs' import { ignoredBuilds } from '@pnpm/exec.build-commands' import { tempDir } from '@pnpm/prepare-temp-dir' import { writeModulesManifest } from '@pnpm/modules-yaml' +import { type DepPath } from '@pnpm/types' const DEFAULT_MODULES_MANIFEST = { hoistedDependencies: {}, @@ -30,7 +31,7 @@ test('ignoredBuilds lists automatically ignored dependencies', async () => { fs.mkdirSync(modulesDir, { recursive: true }) await writeModulesManifest(modulesDir, { ...DEFAULT_MODULES_MANIFEST, - ignoredBuilds: ['foo'], + ignoredBuilds: new Set(['foo@1.0.0' as DepPath]), }) const output = await ignoredBuilds.handler({ dir, @@ -46,7 +47,7 @@ test('ignoredBuilds lists explicitly ignored dependencies', async () => { fs.mkdirSync(modulesDir, { recursive: true }) await writeModulesManifest(modulesDir, { ...DEFAULT_MODULES_MANIFEST, - ignoredBuilds: [], + ignoredBuilds: new Set(), }) const output = await ignoredBuilds.handler({ dir, @@ -66,7 +67,7 @@ test('ignoredBuilds lists both automatically and explicitly ignored dependencies fs.mkdirSync(modulesDir, { recursive: true }) await writeModulesManifest(modulesDir, { ...DEFAULT_MODULES_MANIFEST, - ignoredBuilds: ['foo', 'bar'], + ignoredBuilds: new Set(['foo@1.0.0', 'bar@1.0.0'] as DepPath[]), }) const output = await ignoredBuilds.handler({ dir, diff --git a/exec/build-commands/tsconfig.json b/exec/build-commands/tsconfig.json index b433bb4510..5731bbc60f 100644 --- a/exec/build-commands/tsconfig.json +++ b/exec/build-commands/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../../config/config-writer" }, + { + "path": "../../packages/dependency-path" + }, { "path": "../../packages/types" }, diff --git a/exec/build-modules/package.json b/exec/build-modules/package.json index b4447b692d..9b0ab4bcfb 100644 --- a/exec/build-modules/package.json +++ b/exec/build-modules/package.json @@ -36,6 +36,7 @@ "@pnpm/calc-dep-state": "workspace:*", "@pnpm/config": "workspace:*", "@pnpm/core-loggers": "workspace:*", + "@pnpm/dependency-path": "workspace:*", "@pnpm/deps.graph-sequencer": "workspace:*", "@pnpm/fs.hard-link-dir": "workspace:*", "@pnpm/lifecycle": "workspace:*", diff --git a/exec/build-modules/src/index.ts b/exec/build-modules/src/index.ts index b934ce4600..b5e2fd4d07 100644 --- a/exec/build-modules/src/index.ts +++ b/exec/build-modules/src/index.ts @@ -4,6 +4,7 @@ import util from 'util' import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state' import { getWorkspaceConcurrency } from '@pnpm/config' import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers' +import * as dp from '@pnpm/dependency-path' import { runPostinstallHooks } from '@pnpm/lifecycle' import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins' import { logger } from '@pnpm/logger' @@ -11,7 +12,12 @@ import { hardLinkDir } from '@pnpm/worker' import { readPackageJsonFromDir, safeReadPackageJsonFromDir } from '@pnpm/read-package-json' import { type StoreController } from '@pnpm/store-controller-types' import { applyPatchToDir } from '@pnpm/patching.apply-patch' -import { type AllowBuild, type DependencyManifest } from '@pnpm/types' +import { + type AllowBuild, + type DependencyManifest, + type DepPath, + type IgnoredBuilds, +} from '@pnpm/types' import pDefer, { type DeferredPromise } from 'p-defer' import { pickBy } from 'ramda' import runGroups from 'run-groups' @@ -47,7 +53,7 @@ export async function buildModules ( rootModulesDir: string hoistedLocations?: Record } -): Promise<{ ignoredBuilds?: string[] }> { +): Promise<{ ignoredBuilds?: IgnoredBuilds }> { if (!rootDepPaths.length) return {} const warn = (message: string) => { logger.warn({ message, prefix: opts.lockfileDir }) @@ -61,7 +67,7 @@ export async function buildModules ( } const chunks = buildSequence(depGraph, rootDepPaths) if (!chunks.length) return {} - const ignoredPkgs = new Set() + let ignoredBuilds = new Set() const allowBuild = opts.allowBuild ?? (() => true) const groups = chunks.map((chunk) => { chunk = chunk.filter((depPath) => { @@ -77,7 +83,7 @@ export async function buildModules ( let ignoreScripts = Boolean(buildDepOpts.ignoreScripts) if (!ignoreScripts) { if (depGraph[depPath].requiresBuild && !allowBuild(depGraph[depPath].name, depGraph[depPath].version)) { - ignoredPkgs.add(depGraph[depPath].name) + ignoredBuilds.add(depGraph[depPath].depPath) ignoreScripts = true } } @@ -90,14 +96,15 @@ export async function buildModules ( }) await runGroups.default(getWorkspaceConcurrency(opts.childConcurrency), groups) if (opts.ignoredBuiltDependencies?.length) { - for (const ignoredBuild of opts.ignoredBuiltDependencies) { - // We already ignore the build of this dependency. - // No need to report it. - ignoredPkgs.delete(ignoredBuild) - } + // We already ignore the build of these dependencies. + // No need to report them. + ignoredBuilds = new Set(Array.from(ignoredBuilds).filter((ignoredPkgDepPath) => + !opts.ignoredBuiltDependencies!.some((ignoredInSettings) => + (ignoredInSettings === ignoredPkgDepPath) || (dp.parse(ignoredPkgDepPath).name === ignoredInSettings) + ) + )) } - const packageNames = Array.from(ignoredPkgs) - return { ignoredBuilds: packageNames } + return { ignoredBuilds } } async function buildDependency ( diff --git a/exec/build-modules/tsconfig.json b/exec/build-modules/tsconfig.json index 358e8d0e39..4e8257451c 100644 --- a/exec/build-modules/tsconfig.json +++ b/exec/build-modules/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../../packages/core-loggers" }, + { + "path": "../../packages/dependency-path" + }, { "path": "../../packages/logger" }, diff --git a/exec/plugin-commands-rebuild/src/implementation/index.ts b/exec/plugin-commands-rebuild/src/implementation/index.ts index 053f3ddbd0..ffbbf16266 100644 --- a/exec/plugin-commands-rebuild/src/implementation/index.ts +++ b/exec/plugin-commands-rebuild/src/implementation/index.ts @@ -26,7 +26,13 @@ import { lockfileWalker, type LockfileWalkerStep } from '@pnpm/lockfile.walker' import { logger, streamParser } from '@pnpm/logger' import { writeModulesManifest } from '@pnpm/modules-yaml' import { createOrConnectStoreController } from '@pnpm/store-connection-manager' -import { type DepPath, type ProjectManifest, type ProjectId, type ProjectRootDir } from '@pnpm/types' +import { + type DepPath, + type IgnoredBuilds, + type ProjectManifest, + type ProjectId, + type ProjectRootDir, +} from '@pnpm/types' import { createAllowBuildFunction } from '@pnpm/builder.policy' import { pkgRequiresBuild } from '@pnpm/exec.pkg-requires-build' import * as dp from '@pnpm/dependency-path' @@ -92,7 +98,7 @@ export async function rebuildSelectedPkgs ( projects: Array<{ buildIndex: number, manifest: ProjectManifest, rootDir: ProjectRootDir }>, pkgSpecs: string[], maybeOpts: RebuildOptions -): Promise<{ ignoredBuilds?: string[] }> { +): Promise<{ ignoredBuilds?: IgnoredBuilds }> { const reporter = maybeOpts?.reporter if ((reporter != null) && typeof reporter === 'function') { streamParser.on('data', reporter) @@ -269,7 +275,7 @@ async function _rebuild ( extraNodePaths: string[] } & Pick, opts: StrictRebuildOptions -): Promise<{ pkgsThatWereRebuilt: Set, ignoredPkgs: string[] }> { +): Promise<{ pkgsThatWereRebuilt: Set, ignoredPkgs: IgnoredBuilds }> { const depGraph = lockfileToDepGraph(ctx.currentLockfile) const depsStateCache: DepsStateCache = {} const pkgsThatWereRebuilt = new Set() @@ -309,12 +315,12 @@ async function _rebuild ( logger.info({ message, prefix: opts.dir }) } - const ignoredPkgs: string[] = [] + const ignoredPkgs = new Set() const _allowBuild = createAllowBuildFunction(opts) ?? (() => true) - const allowBuild = (pkgName: string, version: string) => { + const allowBuild = (pkgName: string, version: string, depPath: DepPath) => { if (_allowBuild(pkgName, version)) return true if (!opts.ignoredBuiltDependencies?.includes(pkgName)) { - ignoredPkgs.push(pkgName) + ignoredPkgs.add(depPath) } return false } @@ -370,7 +376,7 @@ async function _rebuild ( requiresBuild = pkgRequiresBuild(pgkManifest, {}) } - const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version) && await runPostinstallHooks({ + const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version, depPath) && await runPostinstallHooks({ depPath, extraBinPaths, extraEnv: opts.extraEnv, diff --git a/packages/dependency-path/test/index.ts b/packages/dependency-path/test/index.ts index e05259ab0b..be01897deb 100644 --- a/packages/dependency-path/test/index.ts +++ b/packages/dependency-path/test/index.ts @@ -7,6 +7,7 @@ import { refToRelative, tryGetPackageId, isRuntimeDepPath, + removeSuffix, } from '@pnpm/dependency-path' import { type DepPath } from '@pnpm/types' @@ -145,3 +146,7 @@ test('isRuntimeDepPath', () => { expect(isRuntimeDepPath('node@runtime:20.1.0' as DepPath)).toBeTruthy() expect(isRuntimeDepPath('node@20.1.0' as DepPath)).toBeFalsy() }) + +test('removeSuffix', () => { + expect(removeSuffix('foo@1.0.0(patch_hash=0000)(@types/babel__core@7.1.14)')).toBe('foo@1.0.0') +}) diff --git a/packages/types/src/misc.ts b/packages/types/src/misc.ts index 6a4787453b..2dc059cd84 100644 --- a/packages/types/src/misc.ts +++ b/packages/types/src/misc.ts @@ -42,3 +42,5 @@ export type PinnedVersion = | 'patch' | 'minor' | 'major' + +export type IgnoredBuilds = Set diff --git a/pkg-manager/core/package.json b/pkg-manager/core/package.json index 30f0f37e84..8b7b2dc5a3 100644 --- a/pkg-manager/core/package.json +++ b/pkg-manager/core/package.json @@ -106,6 +106,7 @@ "@pnpm/store-controller-types": "workspace:*", "@pnpm/symlink-dependency": "workspace:*", "@pnpm/types": "workspace:*", + "@pnpm/util.lex-comparator": "catalog:", "@zkochan/rimraf": "catalog:", "enquirer": "catalog:", "is-inner-link": "catalog:", diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index d7a8e593d21..bc77ae56e0 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -17,6 +17,7 @@ import { summaryLogger, } from '@pnpm/core-loggers' import { hashObjectNullableWithPrefix } from '@pnpm/crypto.object-hasher' +import * as dp from '@pnpm/dependency-path' import { calcPatchHashes, createOverridesMapFromParsed, @@ -67,12 +68,14 @@ import { type DepPath, type DependenciesField, type DependencyManifest, + type IgnoredBuilds, type PeerDependencyIssues, type ProjectId, type ProjectManifest, type ReadPackageHook, type ProjectRootDir, } from '@pnpm/types' +import { lexCompare } from '@pnpm/util.lex-comparator' import isSubdir from 'is-subdir' import pLimit from 'p-limit' import { map as mapValues, clone, isEmpty, pipeWith, props } from 'ramda' @@ -151,7 +154,7 @@ export interface InstallResult { */ updatedCatalogs: Catalogs | undefined updatedManifest: ProjectManifest - ignoredBuilds: string[] | undefined + ignoredBuilds: IgnoredBuilds | undefined } export async function install ( @@ -204,7 +207,7 @@ export type MutateModulesOptions = InstallOptions & { export interface MutateModulesInSingleProjectResult { updatedCatalogs: Catalogs | undefined updatedProject: UpdatedProject - ignoredBuilds: string[] | undefined + ignoredBuilds: IgnoredBuilds | undefined } export async function mutateModulesInSingleProject ( @@ -246,7 +249,7 @@ export interface MutateModulesResult { updatedProjects: UpdatedProject[] stats: InstallationResultStats depsRequiringBuild?: DepPath[] - ignoredBuilds: string[] | undefined + ignoredBuilds: IgnoredBuilds | undefined } const pickCatalogSpecifier: CatalogResultMatcher = { @@ -365,10 +368,14 @@ export async function mutateModules ( } let ignoredBuilds = result.ignoredBuilds - if (!opts.ignoreScripts && ignoredBuilds?.length) { + if (!opts.ignoreScripts && ignoredBuilds?.size) { ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds) } - ignoredScriptsLogger.debug({ packageNames: ignoredBuilds }) + if (!opts.neverBuiltDependencies) { + ignoredScriptsLogger.debug({ + packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [], + }) + } if ((reporter != null) && typeof reporter === 'function') { streamParser.removeListener('data', reporter) @@ -387,7 +394,7 @@ export async function mutateModules ( readonly updatedProjects: UpdatedProject[] readonly stats?: InstallationResultStats readonly depsRequiringBuild?: DepPath[] - readonly ignoredBuilds: string[] | undefined + readonly ignoredBuilds: IgnoredBuilds | undefined } async function _install (): Promise { @@ -878,17 +885,19 @@ Note that in CI environments, this setting is enabled by default.`, } } -async function runUnignoredDependencyBuilds (opts: StrictInstallOptions, previousIgnoredBuilds: string[]): Promise { +async function runUnignoredDependencyBuilds (opts: StrictInstallOptions, previousIgnoredBuilds: IgnoredBuilds): Promise> { if (!opts.onlyBuiltDependencies?.length) { return previousIgnoredBuilds } const onlyBuiltDeps = createPackageVersionPolicy(opts.onlyBuiltDependencies) - const pkgsToBuild = previousIgnoredBuilds.flatMap((ignoredPkg) => { - const matchResult = onlyBuiltDeps(ignoredPkg) + const pkgsToBuild = Array.from(previousIgnoredBuilds).flatMap((ignoredPkg) => { + const ignoredPkgName = dp.parse(ignoredPkg).name + if (!ignoredPkgName) return [] + const matchResult = onlyBuiltDeps(ignoredPkgName) if (matchResult === true) { - return [ignoredPkg] + return [ignoredPkgName] } else if (Array.isArray(matchResult)) { - return matchResult.map(version => `${ignoredPkg}@${version}`) + return matchResult.map(version => `${ignoredPkgName}@${version}`) } return [] }) @@ -1068,7 +1077,7 @@ interface InstallFunctionResult { projects: UpdatedProject[] stats?: InstallationResultStats depsRequiringBuild: DepPath[] - ignoredBuilds?: string[] + ignoredBuilds?: IgnoredBuilds } type InstallFunction = ( @@ -1288,7 +1297,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { } let stats: InstallationResultStats | undefined const allowBuild = createAllowBuildFunction(opts) - let ignoredBuilds: string[] | undefined + let ignoredBuilds: IgnoredBuilds | undefined if (!opts.lockfileOnly && !isInstallationOnlyForLockfileCheck && opts.enableModulesDir) { const result = await linkPackages( projects, @@ -1388,8 +1397,13 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { unsafePerm: opts.unsafePerm, userAgent: opts.userAgent, })).ignoredBuilds - if (ignoredBuilds == null && ctx.modulesFile?.ignoredBuilds?.length) { - ignoredBuilds = ctx.modulesFile.ignoredBuilds + if (ctx.modulesFile?.ignoredBuilds?.size) { + ignoredBuilds ??= new Set() + for (const ignoredBuild of ctx.modulesFile.ignoredBuilds.values()) { + if (result.currentLockfile.packages?.[ignoredBuild]) { + ignoredBuilds.add(ignoredBuild) + } + } } } } @@ -1720,3 +1734,16 @@ async function linkAllBins ( depNodes.map(async depNode => limitLinking(async () => linkBinsOfDependencies(depNode, depGraph, opts))) ) } + +export class IgnoredBuildsError extends PnpmError { + constructor (ignoredBuilds: IgnoredBuilds) { + const packageNames = dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) + super('IGNORED_BUILDS', `Ignored build scripts: ${packageNames.join(', ')}`, { + hint: 'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.', + }) + } +} + +function dedupePackageNamesFromIgnoredBuilds (ignoredBuilds: IgnoredBuilds): string[] { + return Array.from(new Set(Array.from(ignoredBuilds ?? []).map(dp.removeSuffix))).sort(lexCompare) +} diff --git a/pkg-manager/core/test/install/lifecycleScripts.ts b/pkg-manager/core/test/install/lifecycleScripts.ts index 46aa338a2c..8fcd6db1af 100644 --- a/pkg-manager/core/test/install/lifecycleScripts.ts +++ b/pkg-manager/core/test/install/lifecycleScripts.ts @@ -486,7 +486,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies', { const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg - expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example']) + expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0']) } reporter.resetHistory() @@ -527,7 +527,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies us { const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg - expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example']) + expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0']) } reporter.resetHistory() diff --git a/pkg-manager/headless/src/index.ts b/pkg-manager/headless/src/index.ts index 066c67f9fa..2a59a8dd44 100644 --- a/pkg-manager/headless/src/index.ts +++ b/pkg-manager/headless/src/index.ts @@ -62,6 +62,7 @@ import { type DepPath, type DependencyManifest, type HoistedDependencies, + type IgnoredBuilds, type ProjectId, type ProjectManifest, type Registries, @@ -186,7 +187,7 @@ export interface InstallationResultStats { export interface InstallationResult { stats: InstallationResultStats - ignoredBuilds: string[] | undefined + ignoredBuilds: IgnoredBuilds | undefined } export async function headlessInstall (opts: HeadlessOptions): Promise { @@ -512,7 +513,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise depPath) ) } - let ignoredBuilds: string[] | undefined + let ignoredBuilds: IgnoredBuilds | undefined if ((!opts.ignoreScripts || Object.keys(opts.patchedDependencies ?? {}).length > 0) && opts.enableModulesDir !== false) { const directNodes = new Set() for (const id of union(importerIds, ['.'])) { @@ -557,8 +558,13 @@ export async function headlessInstall (opts: HeadlessOptions): Promise } +export type Modules = Omit & { + ignoredBuilds?: IgnoredBuilds +} + export async function readModulesManifest (modulesDir: string): Promise { const modulesYamlPath = path.join(modulesDir, MODULES_FILENAME) - let modules!: Modules + let modulesRaw!: ModulesRaw try { - modules = await readYamlFile.default(modulesYamlPath) - if (!modules) return modules + modulesRaw = await readYamlFile.default(modulesYamlPath) + if (!modulesRaw) return modulesRaw } catch (err: any) { // eslint-disable-line if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { throw err } return null } + const modules = { + ...modulesRaw, + ignoredBuilds: modulesRaw.ignoredBuilds ? new Set(modulesRaw.ignoredBuilds) : undefined, + } if (!modules.virtualStoreDir) { modules.virtualStoreDir = path.join(modulesDir, '.pnpm') } else if (!path.isAbsolute(modules.virtualStoreDir)) { @@ -107,7 +121,7 @@ export async function writeModulesManifest ( } ): Promise { const modulesYamlPath = path.join(modulesDir, MODULES_FILENAME) - const saveModules = { ...modules } + const saveModules = { ...modules, ignoredBuilds: modules.ignoredBuilds ? Array.from(modules.ignoredBuilds) : undefined } if (saveModules.skipped) saveModules.skipped.sort() if (saveModules.hoistPattern == null || (saveModules.hoistPattern as unknown) === '') { diff --git a/pkg-manager/modules-yaml/test/index.ts b/pkg-manager/modules-yaml/test/index.ts index e84462235e..b0433c0fec 100644 --- a/pkg-manager/modules-yaml/test/index.ts +++ b/pkg-manager/modules-yaml/test/index.ts @@ -1,21 +1,21 @@ /// import fs from 'fs' import path from 'path' -import { readModulesManifest, writeModulesManifest } from '@pnpm/modules-yaml' +import { readModulesManifest, writeModulesManifest, type StrictModules } from '@pnpm/modules-yaml' import { sync as readYamlFile } from 'read-yaml-file' import isWindows from 'is-windows' import { temporaryDirectory } from 'tempy' test('writeModulesManifest() and readModulesManifest()', async () => { const modulesDir = temporaryDirectory() - const modulesYaml = { + const modulesYaml: StrictModules = { hoistedDependencies: {}, included: { dependencies: true, devDependencies: true, optionalDependencies: true, }, - ignoredBuilds: [], + ignoredBuilds: new Set(), layoutVersion: 1, packageManager: 'pnpm@2', pendingBuilds: [], @@ -66,14 +66,14 @@ test('backward compatible read of .modules.yaml created with shamefully-hoist=fa test('readModulesManifest() should not create a node_modules directory if it does not exist', async () => { const modulesDir = path.join(temporaryDirectory(), 'node_modules') - const modulesYaml = { + const modulesYaml: StrictModules = { hoistedDependencies: {}, included: { dependencies: true, devDependencies: true, optionalDependencies: true, }, - ignoredBuilds: [], + ignoredBuilds: new Set(), layoutVersion: 1, packageManager: 'pnpm@2', pendingBuilds: [], @@ -94,14 +94,14 @@ test('readModulesManifest() should not create a node_modules directory if it doe test('readModulesManifest() should create a node_modules directory if makeModuleDir is set to true', async () => { const modulesDir = path.join(temporaryDirectory(), 'node_modules') - const modulesYaml = { + const modulesYaml: StrictModules = { hoistedDependencies: {}, included: { dependencies: true, devDependencies: true, optionalDependencies: true, }, - ignoredBuilds: [], + ignoredBuilds: new Set(), layoutVersion: 1, packageManager: 'pnpm@2', pendingBuilds: [], diff --git a/pkg-manager/plugin-commands-installation/src/errors.ts b/pkg-manager/plugin-commands-installation/src/errors.ts deleted file mode 100644 index 14905da32b..0000000000 --- a/pkg-manager/plugin-commands-installation/src/errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PnpmError } from '@pnpm/error' - -export class IgnoredBuildsError extends PnpmError { - constructor (ignoredBuilds: string[]) { - super('IGNORED_BUILDS', `Ignored build scripts: ${ignoredBuilds.join(', ')}`, { - hint: 'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.', - }) - } -} diff --git a/pkg-manager/plugin-commands-installation/src/installDeps.ts b/pkg-manager/plugin-commands-installation/src/installDeps.ts index 9dc7c40bd3..70a5782f9b 100644 --- a/pkg-manager/plugin-commands-installation/src/installDeps.ts +++ b/pkg-manager/plugin-commands-installation/src/installDeps.ts @@ -15,6 +15,7 @@ import { rebuildProjects } from '@pnpm/plugin-commands-rebuild' import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager' import { type IncludedDependencies, type Project, type ProjectsGraph, type ProjectRootDir, type PrepareExecutionEnv } from '@pnpm/types' import { + IgnoredBuildsError, install, mutateModulesInSingleProject, type MutateModulesOptions, @@ -26,7 +27,6 @@ import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer' import { createPkgGraph } from '@pnpm/workspace.pkgs-graph' import { updateWorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state' import isSubdir from 'is-subdir' -import { IgnoredBuildsError } from './errors.js' import { getPinnedVersion } from './getPinnedVersion.js' import { getSaveType } from './getSaveType.js' import { getNodeExecPath } from './nodeExecPath.js' @@ -343,7 +343,7 @@ when running add/update with the --workspace option') configDependencies: opts.configDependencies, }) } - if (opts.strictDepBuilds && ignoredBuilds?.length) { + if (opts.strictDepBuilds && ignoredBuilds?.size) { throw new IgnoredBuildsError(ignoredBuilds) } return @@ -364,7 +364,7 @@ when running add/update with the --workspace option') }), ]) } - if (opts.strictDepBuilds && ignoredBuilds?.length) { + if (opts.strictDepBuilds && ignoredBuilds?.size) { throw new IgnoredBuildsError(ignoredBuilds) } diff --git a/pkg-manager/plugin-commands-installation/src/recursive.ts b/pkg-manager/plugin-commands-installation/src/recursive.ts index 71738d7c1e..f16fd4d6c7 100755 --- a/pkg-manager/plugin-commands-installation/src/recursive.ts +++ b/pkg-manager/plugin-commands-installation/src/recursive.ts @@ -23,6 +23,7 @@ import { requireHooks } from '@pnpm/pnpmfile' import { sortPackages } from '@pnpm/sort-packages' import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager' import { + type IgnoredBuilds, type IncludedDependencies, type PackageManifest, type Project, @@ -34,6 +35,7 @@ import { import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer' import { addDependenciesToPackage, + IgnoredBuildsError, install, type InstallOptions, type MutatedProject, @@ -50,7 +52,6 @@ import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './u import { getSaveType } from './getSaveType.js' import { getPinnedVersion } from './getPinnedVersion.js' import { type PreferredVersions } from '@pnpm/resolver-base' -import { IgnoredBuildsError } from './errors.js' export type RecursiveOptions = CreateStoreControllerOptions & Pick Promise @@ -419,7 +420,7 @@ export async function recursive ( Object.assign(updatedCatalogs, newCatalogsAddition) } } - if (opts.strictDepBuilds && ignoredBuilds?.length) { + if (opts.strictDepBuilds && ignoredBuilds?.size) { throw new IgnoredBuildsError(ignoredBuilds) } result[rootDir].status = 'passed' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e65d93b5a..57deee9ab1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2508,6 +2508,9 @@ importers: '@pnpm/config.config-writer': specifier: workspace:* version: link:../../config/config-writer + '@pnpm/dependency-path': + specifier: workspace:* + version: link:../../packages/dependency-path '@pnpm/logger': specifier: 'catalog:' version: 1001.0.0 @@ -2581,6 +2584,9 @@ importers: '@pnpm/core-loggers': specifier: workspace:* version: link:../../packages/core-loggers + '@pnpm/dependency-path': + specifier: workspace:* + version: link:../../packages/dependency-path '@pnpm/deps.graph-sequencer': specifier: workspace:* version: link:../../deps/graph-sequencer @@ -5087,6 +5093,9 @@ importers: '@pnpm/types': specifier: workspace:* version: link:../../packages/types + '@pnpm/util.lex-comparator': + specifier: 'catalog:' + version: 3.0.2 '@pnpm/worker': specifier: workspace:^ version: link:../../worker diff --git a/pnpm/test/install/lifecycleScripts.ts b/pnpm/test/install/lifecycleScripts.ts index ead18d2d2b..5dc4a1b9b4 100644 --- a/pnpm/test/install/lifecycleScripts.ts +++ b/pnpm/test/install/lifecycleScripts.ts @@ -383,8 +383,8 @@ test('the list of ignored builds is preserved after a repeat install', async () expect(result.stdout.toString()).toContain('Ignored build scripts:') const modulesManifest = project.readModulesManifest() - expect(modulesManifest?.ignoredBuilds?.sort()).toStrictEqual([ - '@pnpm.e2e/pre-and-postinstall-scripts-example', - 'esbuild', + expect(Array.from(modulesManifest!.ignoredBuilds!).sort()).toStrictEqual([ + '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', + 'esbuild@0.25.0', ]) })