@williamthorsen/nmr 0.17.2 → 0.18.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 +27 -48
- package/dist/esm/default-scripts.js +2 -4
- package/dist/esm/helpers/package-json.d.ts +0 -3
- package/dist/esm/helpers/package-json.js +1 -9
- package/package.json +14 -6
- package/bin/nmr-attw.js +0 -11
- package/dist/esm/cli-attw.d.ts +0 -1
- package/dist/esm/cli-attw.js +0 -15
- package/dist/esm/commands/attw.d.ts +0 -34
- package/dist/esm/commands/attw.js +0 -87
package/README.md
CHANGED
|
@@ -3,18 +3,13 @@
|
|
|
3
3
|
Context-aware script runner for PNPM monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.18.0 (2026-07-13)
|
|
7
7
|
|
|
8
8
|
### 🐛 Bug fixes
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Remove the attw check and the nmr-attw binary (#469)
|
|
11
11
|
|
|
12
|
-
Fixes
|
|
13
|
-
|
|
14
|
-
- packages with no published API failed with a false `NoResolution` error;
|
|
15
|
-
- the check left stray `.tgz` files in the working tree ;
|
|
16
|
-
- output was a verbose per-package table; now output is terse on success, full diagnostics only on failure (`--verbose` to force full output);
|
|
17
|
-
- a missing `@arethetypeswrong/cli` gave a raw command-not-found; now a hint is given.
|
|
12
|
+
Fixes an issue where a repo using nmr's default scripts could not pass `check:strict` without installing `@arethetypeswrong/cli` and conforming to inflexible requirements of questionable value. The `attw` check is removed.
|
|
18
13
|
<!-- /section:release-notes -->
|
|
19
14
|
|
|
20
15
|
## Installation
|
|
@@ -144,26 +139,25 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
144
139
|
|
|
145
140
|
### Workspace scripts
|
|
146
141
|
|
|
147
|
-
| Command | Runs
|
|
148
|
-
| --------------- |
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `check`
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `fix`
|
|
156
|
-
| `
|
|
157
|
-
| `fmt`
|
|
158
|
-
| `
|
|
159
|
-
| `lint`
|
|
160
|
-
| `lint:
|
|
161
|
-
| `
|
|
162
|
-
| `test`
|
|
163
|
-
| `test:
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `view-coverage` | `open coverage/index.html` |
|
|
142
|
+
| Command | Runs |
|
|
143
|
+
| --------------- | -------------------------------------------------------- |
|
|
144
|
+
| `build` | `compile` |
|
|
145
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
146
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
147
|
+
| `clean` | `pnpm exec rimraf dist/*` |
|
|
148
|
+
| `compile` | `nmr-compile` |
|
|
149
|
+
| `fix` | `lint`, `fmt` |
|
|
150
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
151
|
+
| `fmt` | `prettier --list-different --write .` |
|
|
152
|
+
| `fmt:check` | `prettier --check .` |
|
|
153
|
+
| `lint` | `eslint --fix .` |
|
|
154
|
+
| `lint:check` | `eslint .` |
|
|
155
|
+
| `lint:strict` | `strict-lint` |
|
|
156
|
+
| `test` | `pnpm exec vitest` |
|
|
157
|
+
| `test:coverage` | `pnpm exec vitest --coverage` |
|
|
158
|
+
| `test:watch` | `pnpm exec vitest --watch` |
|
|
159
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
160
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
167
161
|
|
|
168
162
|
#### Integration test variant
|
|
169
163
|
|
|
@@ -189,12 +183,11 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
189
183
|
|
|
190
184
|
#### Check and quality
|
|
191
185
|
|
|
192
|
-
| Command | Runs
|
|
193
|
-
| ------------------- |
|
|
194
|
-
| `
|
|
195
|
-
| `check`
|
|
196
|
-
| `check:
|
|
197
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `attw`, `check:agent-files` |
|
|
186
|
+
| Command | Runs |
|
|
187
|
+
| ------------------- | ----------------------------------------------------------------------------- |
|
|
188
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
189
|
+
| `check:agent-files` | `nmr-sync-agent-files --check` |
|
|
190
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
|
|
198
191
|
|
|
199
192
|
#### Fix
|
|
200
193
|
|
|
@@ -358,20 +351,6 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
358
351
|
nmr-compile
|
|
359
352
|
```
|
|
360
353
|
|
|
361
|
-
### `nmr-attw`
|
|
362
|
-
|
|
363
|
-
Validate a package's published type-resolution surface with [`@arethetypeswrong/cli`](https://github.com/arethetypeswrong/arethetypeswrong.github.io). This is the default `attw` script — run it from a package directory. It wraps attw so that it behaves well across a monorepo:
|
|
364
|
-
|
|
365
|
-
- **Skips packages with no publishable entry point** (neither `main` nor `exports`) — a message and exit 0, before attw runs. A package with no importable surface has nothing for attw to resolve, so running it would false-positive with `NoResolution`. A private package that _does_ declare `exports` is still checked.
|
|
366
|
-
- **Leaves no `.tgz` in the working tree.** attw analyzes a packed tarball; nmr-attw packs into a temp directory instead of the package directory, so nothing litters the tree.
|
|
367
|
-
- **Condenses output** to a terse per-package result on success and full attw diagnostics only on failure. Pass `--verbose` to print the full attw output on success too.
|
|
368
|
-
|
|
369
|
-
`@arethetypeswrong/cli` is not bundled — a package that declares an entry point must have it installed; nmr-attw reports an actionable message if it is missing. Any other flags are forwarded to attw (the profile defaults to `esm-only`).
|
|
370
|
-
|
|
371
|
-
```bash
|
|
372
|
-
nmr-attw
|
|
373
|
-
```
|
|
374
|
-
|
|
375
354
|
### `ensure-prepublish-hooks`
|
|
376
355
|
|
|
377
356
|
Verify that all publishable workspace packages have a `prepublishOnly` script. Exits non-zero if any are missing.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export const commonWorkspaceScripts = {
|
|
2
|
-
attw: 'nmr-attw',
|
|
3
2
|
build: ['compile'],
|
|
4
3
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
5
|
-
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'
|
|
4
|
+
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
6
5
|
clean: 'pnpm exec rimraf dist/*',
|
|
7
6
|
compile: 'nmr-compile',
|
|
8
7
|
fix: ['lint', 'fmt'],
|
|
@@ -28,14 +27,13 @@ export const standardTestScripts = {
|
|
|
28
27
|
'test:watch': 'pnpm exec vitest --watch',
|
|
29
28
|
};
|
|
30
29
|
export const rootScripts = {
|
|
31
|
-
attw: 'pnpm --recursive exec nmr attw',
|
|
32
30
|
audit: ['audit:prod', 'audit:dev'],
|
|
33
31
|
'audit:dev': 'pnpm exec v11y --dev',
|
|
34
32
|
'audit:prod': 'pnpm exec v11y --prod',
|
|
35
33
|
build: 'pnpm --recursive exec nmr build',
|
|
36
34
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
37
35
|
'check:agent-files': 'nmr-sync-agent-files --check',
|
|
38
|
-
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage', '
|
|
36
|
+
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage', 'check:agent-files'],
|
|
39
37
|
ci: ['build', 'check:strict', 'audit'],
|
|
40
38
|
clean: 'pnpm --recursive exec nmr clean',
|
|
41
39
|
fix: ['lint', 'fmt'],
|
|
@@ -3,13 +3,10 @@ export interface PackageJson {
|
|
|
3
3
|
private?: boolean;
|
|
4
4
|
version?: string;
|
|
5
5
|
packageManager?: string;
|
|
6
|
-
main?: string;
|
|
7
|
-
exports?: unknown;
|
|
8
6
|
scripts?: Record<string, string>;
|
|
9
7
|
pnpm?: {
|
|
10
8
|
overrides?: Record<string, string>;
|
|
11
9
|
};
|
|
12
10
|
}
|
|
13
11
|
export declare function readPackageJson(dir: string): PackageJson;
|
|
14
|
-
export declare function hasPublishableEntryPoint(pkg: PackageJson): boolean;
|
|
15
12
|
export declare function getPnpmOverrides(pkg: PackageJson): Record<string, string> | undefined;
|
|
@@ -2,8 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isObject } from "./type-guards.js";
|
|
4
4
|
export function readPackageJson(dir) {
|
|
5
|
-
const
|
|
6
|
-
const parsed = JSON.parse(content);
|
|
5
|
+
const parsed = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
7
6
|
if (!isObject(parsed)) {
|
|
8
7
|
throw new TypeError(`Invalid package.json in ${dir}: expected an object`);
|
|
9
8
|
}
|
|
@@ -16,10 +15,6 @@ export function readPackageJson(dir) {
|
|
|
16
15
|
pkg.version = parsed.version;
|
|
17
16
|
if (typeof parsed.packageManager === 'string')
|
|
18
17
|
pkg.packageManager = parsed.packageManager;
|
|
19
|
-
if (typeof parsed.main === 'string')
|
|
20
|
-
pkg.main = parsed.main;
|
|
21
|
-
if (parsed.exports !== undefined)
|
|
22
|
-
pkg.exports = parsed.exports;
|
|
23
18
|
if (isObject(parsed.scripts)) {
|
|
24
19
|
const scripts = {};
|
|
25
20
|
for (const [key, val] of Object.entries(parsed.scripts)) {
|
|
@@ -41,9 +36,6 @@ export function readPackageJson(dir) {
|
|
|
41
36
|
}
|
|
42
37
|
return pkg;
|
|
43
38
|
}
|
|
44
|
-
export function hasPublishableEntryPoint(pkg) {
|
|
45
|
-
return pkg.main !== undefined || pkg.exports !== undefined;
|
|
46
|
-
}
|
|
47
39
|
export function getPnpmOverrides(pkg) {
|
|
48
40
|
if (!isObject(pkg.pnpm))
|
|
49
41
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for PNPM monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -22,14 +22,22 @@
|
|
|
22
22
|
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
23
23
|
"type": "module",
|
|
24
24
|
"exports": {
|
|
25
|
-
".":
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/esm/index.d.ts",
|
|
27
|
+
"default": "./dist/esm/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./scripts": {
|
|
30
|
+
"types": "./dist/esm/scripts.d.ts",
|
|
31
|
+
"default": "./dist/esm/scripts.js"
|
|
32
|
+
},
|
|
33
|
+
"./tests": {
|
|
34
|
+
"types": "./dist/esm/tests/consistency.d.ts",
|
|
35
|
+
"default": "./dist/esm/tests/consistency.js"
|
|
36
|
+
}
|
|
28
37
|
},
|
|
29
38
|
"bin": {
|
|
30
39
|
"ensure-prepublish-hooks": "bin/ensure-prepublish-hooks.js",
|
|
31
40
|
"nmr": "bin/nmr.js",
|
|
32
|
-
"nmr-attw": "bin/nmr-attw.js",
|
|
33
41
|
"nmr-compile": "bin/nmr-compile.js",
|
|
34
42
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
35
43
|
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
@@ -45,7 +53,7 @@
|
|
|
45
53
|
"jiti": "2.7.0",
|
|
46
54
|
"yaml": "2.9.0",
|
|
47
55
|
"zod": "4.4.3",
|
|
48
|
-
"@williamthorsen/nmr-core": "0.
|
|
56
|
+
"@williamthorsen/nmr-core": "0.7.0"
|
|
49
57
|
},
|
|
50
58
|
"devDependencies": {
|
|
51
59
|
"@williamthorsen/toolbelt.strings": "3.1.4"
|
package/bin/nmr-attw.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
try {
|
|
3
|
-
await import('../dist/esm/cli-attw.js');
|
|
4
|
-
} catch (error) {
|
|
5
|
-
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-attw: build output not found — run `pnpm run build` first\n');
|
|
7
|
-
} else {
|
|
8
|
-
process.stderr.write(`nmr-attw: failed to load: ${error.message}\n`);
|
|
9
|
-
}
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
package/dist/esm/cli-attw.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/esm/cli-attw.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
-
import { runAttw } from "./commands/attw.js";
|
|
3
|
-
try {
|
|
4
|
-
process.exitCode = runAttw({
|
|
5
|
-
packageDir: process.cwd(),
|
|
6
|
-
argv: process.argv.slice(2),
|
|
7
|
-
stdout: process.stdout,
|
|
8
|
-
stderr: process.stderr,
|
|
9
|
-
env: process.env,
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
reportError(error instanceof Error ? error.message : String(error));
|
|
14
|
-
process.exitCode = 1;
|
|
15
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { type SpawnSyncReturns } from 'node:child_process';
|
|
2
|
-
import type { Writable } from 'node:stream';
|
|
3
|
-
export type SpawnSyncFn = (command: string, args: string[], options: {
|
|
4
|
-
cwd: string;
|
|
5
|
-
encoding: 'utf8';
|
|
6
|
-
env: NodeJS.ProcessEnv;
|
|
7
|
-
}) => Pick<SpawnSyncReturns<string>, 'error' | 'status' | 'stdout' | 'stderr'>;
|
|
8
|
-
export interface RunAttwOptions {
|
|
9
|
-
packageDir: string;
|
|
10
|
-
argv: string[];
|
|
11
|
-
stdout: Writable;
|
|
12
|
-
stderr: Writable;
|
|
13
|
-
env: NodeJS.ProcessEnv;
|
|
14
|
-
spawn?: SpawnSyncFn;
|
|
15
|
-
}
|
|
16
|
-
export declare function runAttw(options: RunAttwOptions): number;
|
|
17
|
-
export declare function buildAttwArgs(argv: string[]): {
|
|
18
|
-
verbose: boolean;
|
|
19
|
-
attwArgs: string[];
|
|
20
|
-
};
|
|
21
|
-
interface AttwOutcome {
|
|
22
|
-
status: number;
|
|
23
|
-
stdout: string;
|
|
24
|
-
stderr: string;
|
|
25
|
-
}
|
|
26
|
-
export declare function formatAttwResult(params: {
|
|
27
|
-
label: string;
|
|
28
|
-
verbose: boolean;
|
|
29
|
-
attwStatus: number | null;
|
|
30
|
-
attwStdout: string;
|
|
31
|
-
attwStderr: string;
|
|
32
|
-
}): AttwOutcome;
|
|
33
|
-
export declare function attwSpawnErrorMessage(label: string, error: Error): string;
|
|
34
|
-
export {};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { hasPublishableEntryPoint, readPackageJson } from "../helpers/package-json.js";
|
|
6
|
-
const DEFAULT_PROFILE = 'esm-only';
|
|
7
|
-
export function runAttw(options) {
|
|
8
|
-
const { packageDir, argv, stdout, stderr, env } = options;
|
|
9
|
-
const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
|
|
10
|
-
const pkg = readPackageJson(packageDir);
|
|
11
|
-
const label = pkg.name ?? path.basename(packageDir);
|
|
12
|
-
if (!hasPublishableEntryPoint(pkg)) {
|
|
13
|
-
stdout.write(`⛔ ${label}: No publishable entry point (no "main"/"exports"). Skipping attw.\n`);
|
|
14
|
-
return 0;
|
|
15
|
-
}
|
|
16
|
-
const { verbose, attwArgs } = buildAttwArgs(argv);
|
|
17
|
-
const tempDir = mkdtempSync(path.join(tmpdir(), 'nmr-attw-'));
|
|
18
|
-
try {
|
|
19
|
-
const pack = spawn('npm', ['pack', '--pack-destination', tempDir], { cwd: packageDir, encoding: 'utf8', env });
|
|
20
|
-
if (pack.error !== undefined) {
|
|
21
|
-
stderr.write(`nmr-attw: npm pack failed for ${label}: ${pack.error.message}\n`);
|
|
22
|
-
return 1;
|
|
23
|
-
}
|
|
24
|
-
if (pack.status !== 0) {
|
|
25
|
-
stderr.write(pack.stderr || `nmr-attw: npm pack failed for ${label}\n`);
|
|
26
|
-
return pack.status ?? 1;
|
|
27
|
-
}
|
|
28
|
-
const tarball = readdirSync(tempDir).find((file) => file.endsWith('.tgz'));
|
|
29
|
-
if (tarball === undefined) {
|
|
30
|
-
stderr.write(`nmr-attw: npm pack produced no tarball for ${label}\n`);
|
|
31
|
-
return 1;
|
|
32
|
-
}
|
|
33
|
-
const attw = spawn('attw', [path.join(tempDir, tarball), ...attwArgs], {
|
|
34
|
-
cwd: packageDir,
|
|
35
|
-
encoding: 'utf8',
|
|
36
|
-
env,
|
|
37
|
-
});
|
|
38
|
-
if (attw.error !== undefined) {
|
|
39
|
-
stderr.write(attwSpawnErrorMessage(label, attw.error));
|
|
40
|
-
return 1;
|
|
41
|
-
}
|
|
42
|
-
const outcome = formatAttwResult({
|
|
43
|
-
label,
|
|
44
|
-
verbose,
|
|
45
|
-
attwStatus: attw.status,
|
|
46
|
-
attwStdout: attw.stdout,
|
|
47
|
-
attwStderr: attw.stderr,
|
|
48
|
-
});
|
|
49
|
-
if (outcome.stdout)
|
|
50
|
-
stdout.write(outcome.stdout);
|
|
51
|
-
if (outcome.stderr)
|
|
52
|
-
stderr.write(outcome.stderr);
|
|
53
|
-
return outcome.status;
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export function buildAttwArgs(argv) {
|
|
60
|
-
let verbose = false;
|
|
61
|
-
const attwArgs = [];
|
|
62
|
-
for (const arg of argv) {
|
|
63
|
-
if (arg === '--verbose' || arg === '-v') {
|
|
64
|
-
verbose = true;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
attwArgs.push(arg);
|
|
68
|
-
}
|
|
69
|
-
if (!attwArgs.some((arg) => arg === '--profile' || arg.startsWith('--profile='))) {
|
|
70
|
-
attwArgs.push('--profile', DEFAULT_PROFILE);
|
|
71
|
-
}
|
|
72
|
-
return { verbose, attwArgs };
|
|
73
|
-
}
|
|
74
|
-
export function formatAttwResult(params) {
|
|
75
|
-
const { label, verbose, attwStatus, attwStdout, attwStderr } = params;
|
|
76
|
-
const status = attwStatus ?? 1;
|
|
77
|
-
if (status === 0 && !verbose) {
|
|
78
|
-
return { status: 0, stdout: `✓ ${label}: types OK\n`, stderr: '' };
|
|
79
|
-
}
|
|
80
|
-
return { status, stdout: attwStdout, stderr: attwStderr };
|
|
81
|
-
}
|
|
82
|
-
export function attwSpawnErrorMessage(label, error) {
|
|
83
|
-
if ('code' in error && error.code === 'ENOENT') {
|
|
84
|
-
return `⚠ ${label}: attw not found — install @arethetypeswrong/cli to validate published types.\n`;
|
|
85
|
-
}
|
|
86
|
-
return `nmr-attw: failed to run attw for ${label}: ${error.message}\n`;
|
|
87
|
-
}
|