diff --git a/package-lock.json b/package-lock.json index 734fcd8f..6e305754 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "c8": "12.0.0", "eslint": "10.12.0", "jscpd": "5.4.1", - "pm-changelog": "2026.10.5", - "pm-ops": "2026.10.6", + "pm-changelog": "2026.10.12", + "pm-ops": "2026.10.12", "typescript": "7.0.2" }, "engines": { @@ -2762,9 +2762,9 @@ "peer": true }, "node_modules/pm-changelog": { - "version": "2026.10.5", - "resolved": "https://registry.npmjs.org/pm-changelog/-/pm-changelog-2026.10.5.tgz", - "integrity": "sha512-OA6EbfwVTPMzD0IsFhho3t3sBZyzqOEm5SEIt0z1GPZCTTKv2PVFkj5zkdHOahojrJ0WtH7D/cQMev5DPOJVzw==", + "version": "2026.10.12", + "resolved": "https://registry.npmjs.org/pm-changelog/-/pm-changelog-2026.10.12.tgz", + "integrity": "sha512-Ugcpr69ODMm3Rrcvmqrm3FJOH97Y6j9EQeEc+F320SW8ntexCLiwZMX2RNnL+A/1vtcAqzhmyVXdcHAoTzIhMA==", "dev": true, "license": "MIT", "bin": { @@ -2774,13 +2774,13 @@ "node": ">=22.18.0" }, "peerDependencies": { - "@unbrained/pm-cli": ">=2026.8.20" + "@unbrained/pm-cli": ">=2026.10.4" } }, "node_modules/pm-ops": { - "version": "2026.10.6", - "resolved": "https://registry.npmjs.org/pm-ops/-/pm-ops-2026.10.6.tgz", - "integrity": "sha512-G+5fC7mFz0JEZAB3gzzMJ6f3ZAhDTbQwAmOaOZK4uuHtHMOOkpdnZLEP7NoRabMlan/NArCnPh6IVXu+9pEAhA==", + "version": "2026.10.12", + "resolved": "https://registry.npmjs.org/pm-ops/-/pm-ops-2026.10.12.tgz", + "integrity": "sha512-WtOLVZby2NgM8sDcpazhIlMzjIMFaeOJ4uG7590PNBSiDutSzOTQ7sP9HMuVBg7xvMnoE3iM/ikMEXu/GV7k1Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2cbfeb07..42da101c 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "c8": "12.0.0", "eslint": "10.12.0", "jscpd": "5.4.1", - "pm-changelog": "2026.10.5", - "pm-ops": "2026.10.6", + "pm-changelog": "2026.10.12", + "pm-ops": "2026.10.12", "typescript": "7.0.2" }, "keywords": [ diff --git a/scripts/prepare-merge-driver.ts b/scripts/prepare-merge-driver.ts index f6f49f07..d9730dea 100644 --- a/scripts/prepare-merge-driver.ts +++ b/scripts/prepare-merge-driver.ts @@ -9,6 +9,8 @@ * notice; any other resolution failure (for example a pm-ops too old to export * the entry, or a `pm-ops` directory whose package.json is gone) and any * installer failure fail the install. + * Artifact-time prepare (`npm pack` or `npm publish`) instead exits with one + * notice before resolving pm-ops, preserving the packing clone's Git config. * * Canonical copy: `pm-ops/templates/prepare-merge-driver.ts`. Copy it * unchanged to `scripts/prepare-merge-driver.ts`. @@ -19,47 +21,54 @@ import { lstatSync } from "node:fs"; import { createRequire } from "node:module"; import { join } from "node:path"; -// npm runs `prepare` from the package root, so pm-ops is resolved from there. -const resolver = createRequire(join(process.cwd(), "package.json")); -let installer: string | undefined; -try { - installer = resolver.resolve("pm-ops/merge-driver/prepare"); -} catch (error) { - // Only an absent pm-ops package may skip. Probing its package.json tells that - // apart from an installed pm-ops that cannot serve the entry (exports without - // it, no exports map, a missing file): those resolve or fail differently, and - // the original error is rethrown. A probe that finds no package.json is not - // yet proof of absence: a broken install can leave `node_modules/pm-ops` (a - // directory or a dangling link) with no package.json in any ancestor Node - // searches. Such an entry also counts as present. Keep Node's global paths - // too: the installer resolution above can use them, so an absent local - // package alone cannot prove absence. An unreadable or malformed lookup path - // leaves presence uncertain and must fail closed with the original error. - // Node returns null paths only for built-in modules; pm-ops/package.json is - // a package specifier. - let packagePresent = true; +// npm also runs prepare while creating artifacts. Git configuration belongs +// to clone installation, so artifact creation must stop before resolution or +// spawning the installer. Keep the install/ci broken-package checks intact. +if (process.env.npm_command === "pack" || process.env.npm_command === "publish") { + console.error(`npm ${process.env.npm_command}: skipping merge-driver install during artifact creation`); +} else { + // npm runs `prepare` from the package root, so pm-ops is resolved from there. + const resolver = createRequire(join(process.cwd(), "package.json")); + let installer: string | undefined; try { - resolver.resolve("pm-ops/package.json"); - } catch (probe) { - packagePresent = - !(probe instanceof Error && "code" in probe && probe.code === "MODULE_NOT_FOUND") || - resolver.resolve.paths("pm-ops/package.json")!.some( - (directory) => { - try { - return lstatSync(join(directory, "pm-ops"), { throwIfNoEntry: false }) !== undefined; - } catch { - // throwIfNoEntry:false only suppresses ENOENT, not EACCES, EPERM, - // ENOTDIR, or ELOOP. Do not replace the installer diagnostic or - // treat an inconclusive presence check as an omit-dev install. - return true; - } - }, - ); + installer = resolver.resolve("pm-ops/merge-driver/prepare"); + } catch (error) { + // Only an absent pm-ops package may skip. Probing its package.json tells that + // apart from an installed pm-ops that cannot serve the entry (exports without + // it, no exports map, a missing file): those resolve or fail differently, and + // the original error is rethrown. A probe that finds no package.json is not + // yet proof of absence: a broken install can leave `node_modules/pm-ops` (a + // directory or a dangling link) with no package.json in any ancestor Node + // searches. Such an entry also counts as present. Keep Node's global paths + // too: the installer resolution above can use them, so an absent local + // package alone cannot prove absence. An unreadable or malformed lookup path + // leaves presence uncertain and must fail closed with the original error. + // Node returns null paths only for built-in modules; pm-ops/package.json is + // a package specifier. + let packagePresent = true; + try { + resolver.resolve("pm-ops/package.json"); + } catch (probe) { + packagePresent = + !(probe instanceof Error && "code" in probe && probe.code === "MODULE_NOT_FOUND") || + resolver.resolve.paths("pm-ops/package.json")!.some( + (directory) => { + try { + return lstatSync(join(directory, "pm-ops"), { throwIfNoEntry: false }) !== undefined; + } catch { + // throwIfNoEntry:false only suppresses ENOENT, not EACCES, EPERM, + // ENOTDIR, or ELOOP. Do not replace the installer diagnostic or + // treat an inconclusive presence check as an omit-dev install. + return true; + } + }, + ); + } + if (packagePresent) throw error; + } + if (installer === undefined) { + console.error("pm-ops is not installed (omit-dev install); skipping merge-driver install"); + } else { + process.exitCode = spawnSync(process.execPath, [installer], { stdio: "inherit" }).status ?? 1; } - if (packagePresent) throw error; -} -if (installer === undefined) { - console.error("pm-ops is not installed (omit-dev install); skipping merge-driver install"); -} else { - process.exitCode = spawnSync(process.execPath, [installer], { stdio: "inherit" }).status ?? 1; } diff --git a/test/prepare-merge-driver.test.ts b/test/prepare-merge-driver.test.ts index 632e0285..75a13ea8 100644 --- a/test/prepare-merge-driver.test.ts +++ b/test/prepare-merge-driver.test.ts @@ -29,9 +29,10 @@ after(() => rmSync(scratch, { recursive: true, force: true })); const posixOnly = { skip: process.platform === "win32" }; /** Every merge driver `.gitattributes` asks Git to use, e.g. `pm-history` from `merge=pm-history`. */ +/** Custom attribute drivers; Git supplies text, binary and union without local registration. */ const declaredDrivers = [ ...new Set([...readFileSync(join(root, ".gitattributes"), "utf8").matchAll(/\bmerge=([\w-]+)/g)].map((match) => match[1])), -].sort(); +].filter((driver) => !["text", "binary", "union"].includes(driver)).sort(); /** * Create a consumer checkout: a fresh Git repository carrying this