@williamthorsen/nmr 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/bin/ensure-prepublish-hooks.js +2 -0
- package/dist/esm/.cache +1 -1
- package/dist/esm/cli-ensure-prepublish-hooks.d.ts +1 -0
- package/dist/esm/cli-ensure-prepublish-hooks.js +62 -0
- package/dist/esm/cli.js +7 -2
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +17 -0
- package/dist/esm/commands/ensure-prepublish-hooks.js +76 -0
- package/dist/esm/helpers/package-json.d.ts +1 -0
- package/dist/esm/helpers/package-json.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -70,6 +70,39 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` invo
|
|
|
70
70
|
// expands to: nmr compile && nmr generate-typings
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Additional subcommands
|
|
74
|
+
|
|
75
|
+
These commands are available as `nmr` subcommands and as standalone `nmr-`-prefixed binaries (for use in lifecycle hooks).
|
|
76
|
+
|
|
77
|
+
### `report-overrides`
|
|
78
|
+
|
|
79
|
+
Report any active `pnpm.overrides` in the root `package.json`. Useful as a `postinstall` hook to remind developers of active overrides that may need cleanup.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
nmr report-overrides
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `sync-pnpm-version`
|
|
86
|
+
|
|
87
|
+
Synchronize the pnpm version from the root `package.json` `packageManager` field into the GitHub `code-quality.yaml` workflow file.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
nmr sync-pnpm-version
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Standalone utilities
|
|
94
|
+
|
|
95
|
+
### `ensure-prepublish-hooks`
|
|
96
|
+
|
|
97
|
+
Verify that all publishable workspace packages have a `prepublishOnly` script. Exits non-zero if any are missing.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ensure-prepublish-hooks # Check only
|
|
101
|
+
ensure-prepublish-hooks --fix # Add missing hooks (default: "npm run build")
|
|
102
|
+
ensure-prepublish-hooks --dry-run # Preview what --fix would do
|
|
103
|
+
ensure-prepublish-hooks --command "pnpm build" # Use a custom hook command
|
|
104
|
+
```
|
|
105
|
+
|
|
73
106
|
## Consumer migration
|
|
74
107
|
|
|
75
108
|
After installing, a consuming repo's root `package.json` scripts shrink to lifecycle hooks:
|
package/dist/esm/.cache
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
75a615bf272a9fdadb44845d2a33161432fe36543278a1abdfa819bdeb581654
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DEFAULT_HOOK, ensurePrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
|
|
2
|
+
import { findMonorepoRoot } from "./context.js";
|
|
3
|
+
let fix = false;
|
|
4
|
+
let dryRun = false;
|
|
5
|
+
let command;
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
switch (arg) {
|
|
10
|
+
case "--fix":
|
|
11
|
+
fix = true;
|
|
12
|
+
break;
|
|
13
|
+
case "--dry-run":
|
|
14
|
+
dryRun = true;
|
|
15
|
+
break;
|
|
16
|
+
case "--command":
|
|
17
|
+
i++;
|
|
18
|
+
command = args[i];
|
|
19
|
+
if (!command) {
|
|
20
|
+
console.error("--command requires a value");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
console.error(`Unknown argument: ${arg}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const monorepoRoot = findMonorepoRoot();
|
|
31
|
+
const hookCommand = command ?? DEFAULT_HOOK;
|
|
32
|
+
const options = command ? { fix, dryRun, command } : { fix, dryRun };
|
|
33
|
+
const result = ensurePrepublishHooks(monorepoRoot, options);
|
|
34
|
+
for (const pkg of result.packages) {
|
|
35
|
+
if (pkg.isPrivate) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
switch (pkg.action) {
|
|
39
|
+
case "ok":
|
|
40
|
+
console.info(`\u2713 ${pkg.packageName}: prepublishOnly = "${pkg.prepublishOnly}"`);
|
|
41
|
+
break;
|
|
42
|
+
case "missing":
|
|
43
|
+
console.warn(`\u2717 ${pkg.packageName}: missing prepublishOnly`);
|
|
44
|
+
break;
|
|
45
|
+
case "fixed":
|
|
46
|
+
console.info(`\u2713 ${pkg.packageName}: added prepublishOnly = "${hookCommand}"`);
|
|
47
|
+
break;
|
|
48
|
+
case "would-fix":
|
|
49
|
+
console.info(`~ ${pkg.packageName}: would add prepublishOnly = "${hookCommand}"`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (result.hasFailures) {
|
|
54
|
+
const missing = result.packages.filter((p) => p.action === "missing").length;
|
|
55
|
+
console.error(`
|
|
56
|
+
${missing} package(s) missing prepublishOnly. Use --fix to add it.`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -78,14 +78,19 @@ async function main() {
|
|
|
78
78
|
process.exit(code2);
|
|
79
79
|
}
|
|
80
80
|
if (parsed.recursive) {
|
|
81
|
+
process.env.NMR_RUN_IF_PRESENT = "1";
|
|
81
82
|
const delegateCmd = `pnpm --recursive exec nmr ${command}${passthrough}`;
|
|
82
83
|
const code2 = runCommand(delegateCmd, context.monorepoRoot, runOptions);
|
|
83
84
|
process.exit(code2);
|
|
84
85
|
}
|
|
85
86
|
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
86
87
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config, parsed.intTest);
|
|
87
|
-
const
|
|
88
|
+
const packageDir = context.packageDir ?? context.monorepoRoot;
|
|
89
|
+
const resolved = resolveScript(command, registry, packageDir);
|
|
88
90
|
if (!resolved) {
|
|
91
|
+
if (process.env.NMR_RUN_IF_PRESENT === "1") {
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
89
94
|
console.error(`Unknown command: ${command}`);
|
|
90
95
|
process.exit(1);
|
|
91
96
|
}
|
|
@@ -95,7 +100,7 @@ async function main() {
|
|
|
95
100
|
}
|
|
96
101
|
process.exit(0);
|
|
97
102
|
}
|
|
98
|
-
if (resolved.source === "package" && !parsed.quiet) {
|
|
103
|
+
if (resolved.source === "package" && !parsed.quiet && registry[command] !== void 0) {
|
|
99
104
|
console.info(`Using override script: ${resolved.command}`);
|
|
100
105
|
}
|
|
101
106
|
const fullCommand = resolved.command + passthrough;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PackageHookStatus {
|
|
2
|
+
packageName: string;
|
|
3
|
+
packageDir: string;
|
|
4
|
+
isPrivate: boolean;
|
|
5
|
+
prepublishOnly: string | undefined;
|
|
6
|
+
action: 'ok' | 'missing' | 'fixed' | 'would-fix';
|
|
7
|
+
}
|
|
8
|
+
export interface EnsurePrepublishHooksResult {
|
|
9
|
+
packages: PackageHookStatus[];
|
|
10
|
+
hasFailures: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_HOOK = "npm run build";
|
|
13
|
+
export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
|
|
14
|
+
fix: boolean;
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
command?: string;
|
|
17
|
+
}): EnsurePrepublishHooksResult;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getWorkspacePackageDirs } from "../context.js";
|
|
4
|
+
import { readPackageJson } from "../helpers/package-json.js";
|
|
5
|
+
import { isObject } from "../helpers/type-guards.js";
|
|
6
|
+
const DEFAULT_HOOK = "npm run build";
|
|
7
|
+
function ensurePrepublishHooks(monorepoRoot, options) {
|
|
8
|
+
const hookCommand = options.command ?? DEFAULT_HOOK;
|
|
9
|
+
const packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
10
|
+
const packages = [];
|
|
11
|
+
for (const packageDir of packageDirs) {
|
|
12
|
+
const pkg = readPackageJson(packageDir);
|
|
13
|
+
const packageName = pkg.name ?? path.basename(packageDir);
|
|
14
|
+
const isPrivate = pkg.private === true;
|
|
15
|
+
if (isPrivate) {
|
|
16
|
+
packages.push({
|
|
17
|
+
packageName,
|
|
18
|
+
packageDir,
|
|
19
|
+
isPrivate: true,
|
|
20
|
+
prepublishOnly: pkg.scripts?.prepublishOnly,
|
|
21
|
+
action: "ok"
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const existing = pkg.scripts?.prepublishOnly;
|
|
26
|
+
if (existing) {
|
|
27
|
+
packages.push({
|
|
28
|
+
packageName,
|
|
29
|
+
packageDir,
|
|
30
|
+
isPrivate: false,
|
|
31
|
+
prepublishOnly: existing,
|
|
32
|
+
action: "ok"
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (options.fix) {
|
|
37
|
+
const action = options.dryRun ? "would-fix" : "fixed";
|
|
38
|
+
if (!options.dryRun) {
|
|
39
|
+
addPrepublishOnly(packageDir, hookCommand);
|
|
40
|
+
}
|
|
41
|
+
packages.push({
|
|
42
|
+
packageName,
|
|
43
|
+
packageDir,
|
|
44
|
+
isPrivate: false,
|
|
45
|
+
prepublishOnly: void 0,
|
|
46
|
+
action
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
packages.push({
|
|
50
|
+
packageName,
|
|
51
|
+
packageDir,
|
|
52
|
+
isPrivate: false,
|
|
53
|
+
prepublishOnly: void 0,
|
|
54
|
+
action: "missing"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const hasFailures = packages.some((p) => p.action === "missing");
|
|
59
|
+
return { packages, hasFailures };
|
|
60
|
+
}
|
|
61
|
+
function addPrepublishOnly(packageDir, command) {
|
|
62
|
+
const filePath = path.join(packageDir, "package.json");
|
|
63
|
+
const raw = readFileSync(filePath, "utf8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (!isObject(parsed)) {
|
|
66
|
+
throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
|
|
67
|
+
}
|
|
68
|
+
const scripts = isObject(parsed.scripts) ? parsed.scripts : {};
|
|
69
|
+
scripts.prepublishOnly = command;
|
|
70
|
+
parsed.scripts = scripts;
|
|
71
|
+
writeFileSync(filePath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
DEFAULT_HOOK,
|
|
75
|
+
ensurePrepublishHooks
|
|
76
|
+
};
|
|
@@ -9,6 +9,7 @@ function readPackageJson(dir) {
|
|
|
9
9
|
}
|
|
10
10
|
const pkg = {};
|
|
11
11
|
if (typeof parsed.name === "string") pkg.name = parsed.name;
|
|
12
|
+
if (parsed.private === true) pkg.private = true;
|
|
12
13
|
if (typeof parsed.version === "string") pkg.version = parsed.version;
|
|
13
14
|
if (typeof parsed.packageManager === "string") pkg.packageManager = parsed.packageManager;
|
|
14
15
|
if (isObject(parsed.scripts)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for PNPM monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./tests": "./dist/esm/tests/consistency.js"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
+
"ensure-prepublish-hooks": "bin/ensure-prepublish-hooks.js",
|
|
20
21
|
"nmr": "bin/nmr.js",
|
|
21
22
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
22
23
|
"nmr-sync-pnpm-version": "bin/nmr-sync-pnpm-version.js"
|