@williamthorsen/nmr 0.17.2 → 0.18.1
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 +40 -48
- package/bin/nmr-clean.js +11 -0
- package/dist/esm/cli-clean.js +9 -0
- package/dist/esm/commands/build.js +17 -5
- package/dist/esm/commands/clean.d.ts +2 -0
- package/dist/esm/commands/clean.js +52 -0
- package/dist/esm/default-scripts.js +4 -6
- package/dist/esm/helpers/package-json.d.ts +0 -3
- package/dist/esm/helpers/package-json.js +1 -9
- package/package.json +15 -6
- package/bin/nmr-attw.js +0 -11
- 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/dist/esm/{cli-attw.d.ts → cli-clean.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
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.1 (2026-07-14)
|
|
7
7
|
|
|
8
8
|
### 🐛 Bug fixes
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Fix the stale build cache, the unshipped rimraf dependency, and the aborting root clean (#471)
|
|
11
11
|
|
|
12
|
-
Fixes the issues
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
Fixes the following issues:
|
|
13
|
+
- `nmr clean` failed with a missing-command error in any project that had not separately installed `rimraf`, a tool nmr neither declared nor shipped.
|
|
14
|
+
- If run from a monorepo root, `nmr clean` cleaned one package and then aborted, leaving every other package untouched. Only repos that build nmr from source were affected.
|
|
15
|
+
- If run from a monorepo root, `nmr clean` ignored a package's own `clean` script.
|
|
16
|
+
- `nmr compile` skipped the build for a package whose output had been deleted, because the sources were unchanged. The package compiled to an empty output directory with no error and no warning, and could be published as an empty tarball.
|
|
17
|
+
- `nmr compile` reported missing output and recompiled on every run for a package whose sources emit nothing, such as a source tree of only declaration files.
|
|
18
18
|
<!-- /section:release-notes -->
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
@@ -144,26 +144,25 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
144
144
|
|
|
145
145
|
### Workspace scripts
|
|
146
146
|
|
|
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` |
|
|
147
|
+
| Command | Runs |
|
|
148
|
+
| --------------- | -------------------------------------------------------- |
|
|
149
|
+
| `build` | `compile` |
|
|
150
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
151
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
152
|
+
| `clean` | `nmr-clean` |
|
|
153
|
+
| `compile` | `nmr-compile` |
|
|
154
|
+
| `fix` | `lint`, `fmt` |
|
|
155
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
156
|
+
| `fmt` | `prettier --list-different --write .` |
|
|
157
|
+
| `fmt:check` | `prettier --check .` |
|
|
158
|
+
| `lint` | `eslint --fix .` |
|
|
159
|
+
| `lint:check` | `eslint .` |
|
|
160
|
+
| `lint:strict` | `strict-lint` |
|
|
161
|
+
| `test` | `pnpm exec vitest` |
|
|
162
|
+
| `test:coverage` | `pnpm exec vitest --coverage` |
|
|
163
|
+
| `test:watch` | `pnpm exec vitest --watch` |
|
|
164
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
165
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
167
166
|
|
|
168
167
|
#### Integration test variant
|
|
169
168
|
|
|
@@ -185,16 +184,15 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
185
184
|
| ------- | --------------------------------- |
|
|
186
185
|
| `build` | `pnpm --recursive exec nmr build` |
|
|
187
186
|
| `ci` | `build`, `check:strict` |
|
|
188
|
-
| `clean` | `
|
|
187
|
+
| `clean` | `nmr-clean` |
|
|
189
188
|
|
|
190
189
|
#### Check and quality
|
|
191
190
|
|
|
192
|
-
| Command | Runs
|
|
193
|
-
| ------------------- |
|
|
194
|
-
| `
|
|
195
|
-
| `check`
|
|
196
|
-
| `check:
|
|
197
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `attw`, `check:agent-files` |
|
|
191
|
+
| Command | Runs |
|
|
192
|
+
| ------------------- | ----------------------------------------------------------------------------- |
|
|
193
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
194
|
+
| `check:agent-files` | `nmr-sync-agent-files --check` |
|
|
195
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
|
|
198
196
|
|
|
199
197
|
#### Fix
|
|
200
198
|
|
|
@@ -348,28 +346,22 @@ nmr sync-pnpm-version
|
|
|
348
346
|
|
|
349
347
|
## Standalone utilities
|
|
350
348
|
|
|
351
|
-
### `nmr-
|
|
349
|
+
### `nmr-clean`
|
|
352
350
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
`typescript` is a peer dependency (`>=5.7.0`, required for `rewriteRelativeImportExtensions`); the consuming repo provides it. Relative imports in source must carry explicit `.ts` extensions for them to be rewritten.
|
|
351
|
+
Remove a package's build output (`dist`) and its `nmr-compile` cache entry, leaving no state behind for the next build to skip on. Run from a package directory it cleans that package; run from the monorepo root it sweeps every workspace package in a single pass, running each package's resolved `clean` — so a package that overrides `clean`, in `.config/nmr.config.ts` or in its own `package.json`, gets its own command rather than the sweep. Removal is idempotent — cleaning an unbuilt package is a silent no-op. This is the default `clean` script at both levels.
|
|
356
352
|
|
|
357
353
|
```bash
|
|
358
|
-
nmr-
|
|
354
|
+
nmr-clean
|
|
359
355
|
```
|
|
360
356
|
|
|
361
|
-
### `nmr-
|
|
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:
|
|
357
|
+
### `nmr-compile`
|
|
364
358
|
|
|
365
|
-
|
|
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.
|
|
359
|
+
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and `.d.ts` in one pass. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. The build is skipped when no input has changed and the previous output is still on disk (a content-and-path hash is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
|
|
368
360
|
|
|
369
|
-
|
|
361
|
+
`typescript` is a peer dependency (`>=5.7.0`, required for `rewriteRelativeImportExtensions`); the consuming repo provides it. Relative imports in source must carry explicit `.ts` extensions for them to be rewritten.
|
|
370
362
|
|
|
371
363
|
```bash
|
|
372
|
-
nmr-
|
|
364
|
+
nmr-compile
|
|
373
365
|
```
|
|
374
366
|
|
|
375
367
|
### `ensure-prepublish-hooks`
|
package/bin/nmr-clean.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
await import('../dist/esm/cli-clean.js');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
+
process.stderr.write('nmr-clean: build output not found — run `pnpm run build` first\n');
|
|
7
|
+
} else {
|
|
8
|
+
process.stderr.write(`nmr-clean: failed to load: ${error.message}\n`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { runClean } from "./commands/clean.js";
|
|
3
|
+
try {
|
|
4
|
+
await runClean(process.cwd());
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
reportError(error instanceof Error ? error.message : String(error));
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { glob } from 'glob';
|
|
@@ -24,7 +24,7 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
24
24
|
ignore: options.ignore ?? DEFAULT_IGNORE,
|
|
25
25
|
});
|
|
26
26
|
const dependencies = ['package.json', ...resolveTsconfigChain(packageDir)];
|
|
27
|
-
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, cachePath);
|
|
27
|
+
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
28
28
|
if (!changed) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
@@ -199,17 +199,29 @@ function findNearestNodeModulesHost(startDir) {
|
|
|
199
199
|
}
|
|
200
200
|
return current;
|
|
201
201
|
}
|
|
202
|
-
async function detectBuildChanges(packageDir, files, emitConfig, cachePath) {
|
|
202
|
+
async function detectBuildChanges(packageDir, files, emitConfig, cachePath, outputPresent) {
|
|
203
203
|
const packageName = path.basename(packageDir);
|
|
204
204
|
const previousHash = existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : undefined;
|
|
205
205
|
const currentHash = await computeBuildHash(packageDir, files, emitConfig);
|
|
206
206
|
if (previousHash === currentHash) {
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
if (outputPresent) {
|
|
208
|
+
console.info(`${SKIPPED_ICON} ${packageName}: No changes detected. Skipping build.`);
|
|
209
|
+
return { changed: false, currentHash };
|
|
210
|
+
}
|
|
211
|
+
console.info(`${PACKAGE_ICON} ${packageName}: Build output is missing. Rebuilding.`);
|
|
212
|
+
return { changed: true, currentHash };
|
|
209
213
|
}
|
|
210
214
|
console.info(`${PACKAGE_ICON} ${packageName}: Changes detected.`);
|
|
211
215
|
return { changed: true, currentHash };
|
|
212
216
|
}
|
|
217
|
+
function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
|
|
218
|
+
const emitsOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
|
|
219
|
+
if (!emitsOutput) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const outputDir = path.resolve(packageDir, outdir);
|
|
223
|
+
return existsSync(outputDir) && readdirSync(outputDir).length > 0;
|
|
224
|
+
}
|
|
213
225
|
async function writeBuildCache(cachePath, hash) {
|
|
214
226
|
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
215
227
|
await writeFile(cachePath, hash);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { findContainingPackageDir, findMonorepoRoot, getWorkspacePackageDirs } from "../context.js";
|
|
5
|
+
import { applyDevBin, buildWorkspaceRegistry, hasIntegrationTestConfig, resolveScript } from "../resolver.js";
|
|
6
|
+
import { runCommand } from "../runner.js";
|
|
7
|
+
import { resolveBuildCachePath } from "./build.js";
|
|
8
|
+
const CLEAN_ICON = '🧹';
|
|
9
|
+
const CLEAN_COMMAND = 'clean';
|
|
10
|
+
const BUILT_IN_CLEAN = 'nmr-clean';
|
|
11
|
+
const OUTPUT_ROOT = 'dist';
|
|
12
|
+
export async function cleanPackage(packageDir) {
|
|
13
|
+
await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
|
|
14
|
+
await rm(resolveBuildCachePath(packageDir), { force: true });
|
|
15
|
+
console.info(`${CLEAN_ICON} ${path.basename(packageDir)}: Removed build output and cache.`);
|
|
16
|
+
}
|
|
17
|
+
export async function runClean(cwd = process.cwd()) {
|
|
18
|
+
let monorepoRoot;
|
|
19
|
+
try {
|
|
20
|
+
monorepoRoot = findMonorepoRoot(cwd);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
await cleanPackage(cwd);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
27
|
+
const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
|
|
28
|
+
if (packageDir !== undefined) {
|
|
29
|
+
await cleanPackage(packageDir);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await sweepWorkspace(monorepoRoot, workspacePackageDirs);
|
|
33
|
+
}
|
|
34
|
+
async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
35
|
+
const config = await loadConfig(monorepoRoot);
|
|
36
|
+
for (const packageDir of workspacePackageDirs) {
|
|
37
|
+
const registry = buildWorkspaceRegistry(config, hasIntegrationTestConfig(packageDir));
|
|
38
|
+
const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
39
|
+
if (resolved === undefined || resolved.command === '') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (resolved.command === BUILT_IN_CLEAN) {
|
|
43
|
+
await cleanPackage(packageDir);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const command = applyDevBin(resolved.command, config.devBin, monorepoRoot);
|
|
47
|
+
const exitCode = runCommand(command, packageDir);
|
|
48
|
+
if (exitCode !== 0) {
|
|
49
|
+
throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
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'
|
|
6
|
-
clean: '
|
|
4
|
+
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
5
|
+
clean: 'nmr-clean',
|
|
7
6
|
compile: 'nmr-compile',
|
|
8
7
|
fix: ['lint', 'fmt'],
|
|
9
8
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
@@ -28,16 +27,15 @@ 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
|
-
clean: '
|
|
38
|
+
clean: 'nmr-clean',
|
|
41
39
|
fix: ['lint', 'fmt'],
|
|
42
40
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
43
41
|
fmt: 'sh -c \'prettier --list-different --write "${@:-.}"\' --',
|
|
@@ -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.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for PNPM monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -22,14 +22,23 @@
|
|
|
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-
|
|
41
|
+
"nmr-clean": "bin/nmr-clean.js",
|
|
33
42
|
"nmr-compile": "bin/nmr-compile.js",
|
|
34
43
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
35
44
|
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
@@ -45,7 +54,7 @@
|
|
|
45
54
|
"jiti": "2.7.0",
|
|
46
55
|
"yaml": "2.9.0",
|
|
47
56
|
"zod": "4.4.3",
|
|
48
|
-
"@williamthorsen/nmr-core": "0.
|
|
57
|
+
"@williamthorsen/nmr-core": "0.7.0"
|
|
49
58
|
},
|
|
50
59
|
"devDependencies": {
|
|
51
60
|
"@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.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
|
-
}
|
|
File without changes
|