@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 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:
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/esm/cli-ensure-prepublish-hooks.js');
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- 231a8d6ece4406ea0590ca7f31cff212ed0b31b0afa24e5b4cc00f048c5ce2d2
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 resolved = resolveScript(command, registry, context.packageDir);
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
+ };
@@ -1,5 +1,6 @@
1
1
  export interface PackageJson {
2
2
  name?: string;
3
+ private?: boolean;
3
4
  version?: string;
4
5
  packageManager?: string;
5
6
  scripts?: Record<string, string>;
@@ -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.1.1",
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"