Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 71 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@
"@babel/plugin-syntax-typescript": "8.0.3",
"@toon-format/toon": "4.4.0",
"@types/node": "26.6.5",
"@unbrained/pm-cli": "2026.10.17",
"@unbrained/pm-cli": "2026.10.20",
"c8": "12.0.0",
"eslint": "10.13.0",
"jscpd": "5.4.1",
"pm-changelog": "2026.10.16",
"pm-ops": "2026.10.16",
"pm-changelog": "2026.10.20",
"pm-ops": "2026.10.20",
"typescript": "7.0.2"
},
"keywords": [
Expand Down
63 changes: 15 additions & 48 deletions scripts/prepare-merge-driver.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,37 @@
/**
* npm `prepare` hook that registers pm's field-aware Git merge drivers.
*
* Git never clones `.git/config`, so every clone must register the drivers
* `.gitattributes` declares. The installer lives in the devDependency pm-ops,
* which an `npm install --omit=dev` checkout does not have. This launcher
* therefore imports nothing from pm-ops: it resolves the installer entry from
* the package root and runs it in a child process. Only a missing pm-ops package skips, with one
* 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`.
* Frozen v1 bootstrap: copy unchanged once; behavior lives in pm-ops.
* Keep dependency-free absence checks here so omit-dev installs can succeed.
* Changes require a new launcher version and retention of this hash.
*/

import { spawnSync } from "node:child_process";
import { lstatSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";

// 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 {
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;
if ((error as NodeJS.ErrnoException).code !== "MODULE_NOT_FOUND") throw error;
// Metadata also detects self-references; physical entries include broken links.
let present = 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;
}
},
);
present = (probe as NodeJS.ErrnoException).code !== "MODULE_NOT_FOUND" ||
resolver.resolve.paths("pm-ops/package.json")!.some((directory) => {
try {
return lstatSync(join(directory, "pm-ops"), { throwIfNoEntry: false }) !== undefined;
} catch {
return true;
}
});
}
if (packagePresent) throw error;
if (present) throw error;
}
if (installer === undefined) {
console.error("pm-ops is not installed (omit-dev install); skipping merge-driver install");
Expand Down
Loading