@williamthorsen/nmr 0.32.0 → 0.32.2
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/.readyup/kits/default.js +32 -7
- package/.readyup/manifest.json +6 -6
- package/README.md +30 -31
- package/dist/esm/default-scripts.js +3 -3
- package/dist/esm/help.js +1 -1
- package/dist/esm/resolver.d.ts +2 -1
- package/dist/esm/resolver.js +21 -5
- package/dist/esm/runCli.js +60 -7
- package/dist/esm/steps.d.ts +6 -0
- package/dist/esm/steps.js +108 -36
- package/dist/esm/taze.js +2 -0
- package/package.json +4 -3
package/.readyup/kits/default.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.29.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
@@ -43,9 +43,8 @@ var workspaceScripts = {
|
|
|
43
43
|
"test:unit": "pnpm exec vitest --project unit",
|
|
44
44
|
"test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
|
|
45
45
|
typecheck: "tsgo --noEmit",
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
upgrade: "nmr-report-catalog && nmr-taze --include-locked",
|
|
46
|
+
// The command is a string because neither half names an nmr command: both are binaries.
|
|
47
|
+
upgrade: "nmr-report-catalog && nmr-taze",
|
|
49
48
|
"view-coverage": "open coverage/index.html"
|
|
50
49
|
};
|
|
51
50
|
var rootScripts = {
|
|
@@ -81,7 +80,7 @@ var rootScripts = {
|
|
|
81
80
|
"root:typecheck": "tsgo --noEmit",
|
|
82
81
|
// Carries the override report for the same reason `upgrade` does: both end in the tool that rewrites a
|
|
83
82
|
// `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
|
|
84
|
-
"root:upgrade": "nmr-report-overrides && nmr-taze
|
|
83
|
+
"root:upgrade": "nmr-report-overrides && nmr-taze",
|
|
85
84
|
test: ["root:test", "-R test"],
|
|
86
85
|
"test:all": ["root:test:all", "-R test:all"],
|
|
87
86
|
"test:coverage": ["root:test", "-R test:coverage"],
|
|
@@ -93,7 +92,7 @@ var rootScripts = {
|
|
|
93
92
|
typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", declinesArgs: true }],
|
|
94
93
|
// The command is a string because neither half names an nmr command: both are binaries, and a composite
|
|
95
94
|
// element can name only a command.
|
|
96
|
-
upgrade: "nmr-report-overrides && nmr-taze --
|
|
95
|
+
upgrade: "nmr-report-overrides && nmr-taze --recursive"
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
// src/resolve-scripts.ts
|
|
@@ -272,6 +271,13 @@ var default_default = defineRdyKit({
|
|
|
272
271
|
check: () => prettierConfigBuildsOnSharedConfig(),
|
|
273
272
|
fix: "Replace the Prettier config with: import { definePrettierConfig } from '@williamthorsen/nmr/prettier'; export default definePrettierConfig();"
|
|
274
273
|
},
|
|
274
|
+
// -- Shared upgrade policy -----------------------------------------------
|
|
275
|
+
{
|
|
276
|
+
name: "taze.config.ts builds on @williamthorsen/nmr/taze",
|
|
277
|
+
severity: "warn",
|
|
278
|
+
check: () => tazeConfigBuildsOnSharedConfig(),
|
|
279
|
+
fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); \u2014 nmr's upgrade policy reaches a repo only through this file, so without it `nmr upgrade` reports nothing where dependencies are pinned to exact versions"
|
|
280
|
+
},
|
|
275
281
|
// -- Audit dependency --------------------------------------------------------
|
|
276
282
|
{
|
|
277
283
|
name: "v11y-check in devDependencies",
|
|
@@ -303,6 +309,8 @@ var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
|
|
|
303
309
|
var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
|
|
304
310
|
var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
|
|
305
311
|
var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
|
|
312
|
+
var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
|
|
313
|
+
var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
|
|
306
314
|
var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
|
|
307
315
|
var MIN_ESLINT_VERSION = "10.0.0";
|
|
308
316
|
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
@@ -394,7 +402,7 @@ function formatPaths(paths) {
|
|
|
394
402
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
395
403
|
}
|
|
396
404
|
function getMinVersion() {
|
|
397
|
-
const picked = { "version": "0.32.
|
|
405
|
+
const picked = { "version": "0.32.2" };
|
|
398
406
|
if (typeof picked["version"] !== "string") {
|
|
399
407
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
400
408
|
}
|
|
@@ -495,6 +503,22 @@ function readPnpmFieldKeys(content) {
|
|
|
495
503
|
function resolvesVersionViaWorkspace(range) {
|
|
496
504
|
return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
|
|
497
505
|
}
|
|
506
|
+
function tazeConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
507
|
+
const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
|
|
508
|
+
if (configs.length === 0) {
|
|
509
|
+
return { ok: false, detail: describeMissingTazeConfig(cwd) };
|
|
510
|
+
}
|
|
511
|
+
const stale = configs.filter(
|
|
512
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "defineConfig", SHARED_TAZE_MODULE)
|
|
513
|
+
);
|
|
514
|
+
if (stale.length === 0) return true;
|
|
515
|
+
return { ok: false, detail: `does not import defineConfig from ${SHARED_TAZE_MODULE}: ${stale.join(", ")}` };
|
|
516
|
+
}
|
|
517
|
+
function describeMissingTazeConfig(cwd) {
|
|
518
|
+
const inert = findFiles(INERT_TAZE_CONFIGS, cwd);
|
|
519
|
+
if (inert.length > 0) return `holds no code to call the factory: ${inert.join(", ")}`;
|
|
520
|
+
return "taze.config.ts is missing";
|
|
521
|
+
}
|
|
498
522
|
function toolVersionsHasNoPnpm() {
|
|
499
523
|
const content = readFile(".tool-versions");
|
|
500
524
|
if (content === void 0) return true;
|
|
@@ -515,6 +539,7 @@ export {
|
|
|
515
539
|
noReExportOnlyVitestConfigs,
|
|
516
540
|
noRetiredVitestConfigs,
|
|
517
541
|
prettierConfigBuildsOnSharedConfig,
|
|
542
|
+
tazeConfigBuildsOnSharedConfig,
|
|
518
543
|
vitestConfigBuildsOnSharedConfig,
|
|
519
544
|
vitestRootConfigBuildsOnSharedConfig
|
|
520
545
|
};
|
package/.readyup/manifest.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"esbuildVersion": "0.28.1",
|
|
9
9
|
"inputs": [
|
|
10
10
|
{
|
|
11
|
-
"hash": "
|
|
11
|
+
"hash": "904ac753",
|
|
12
12
|
"kind": "module",
|
|
13
13
|
"path": "kits/default.ts"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"hash": "
|
|
16
|
+
"hash": "cca25de2",
|
|
17
17
|
"kind": "inline",
|
|
18
18
|
"path": "../package.json",
|
|
19
19
|
"paths": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"hash": "
|
|
24
|
+
"hash": "94bb3c62",
|
|
25
25
|
"kind": "module",
|
|
26
26
|
"path": "../src/default-scripts.ts"
|
|
27
27
|
},
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"name": "default",
|
|
40
40
|
"path": "kits/default.js",
|
|
41
|
-
"readyupVersion": "0.
|
|
41
|
+
"readyupVersion": "0.29.0",
|
|
42
42
|
"source": "kits/default.ts",
|
|
43
|
-
"sourceHash": "
|
|
44
|
-
"targetHash": "
|
|
43
|
+
"sourceHash": "904ac753",
|
|
44
|
+
"targetHash": "bc4d49d1"
|
|
45
45
|
}
|
|
46
46
|
]
|
|
47
47
|
}
|
package/README.md
CHANGED
|
@@ -3,31 +3,19 @@
|
|
|
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.32.
|
|
6
|
+
## Release notes — v0.32.2 (2026-08-19)
|
|
7
7
|
|
|
8
|
-
###
|
|
9
|
-
|
|
10
|
-
- Carry the test-tier convention in the ambient rulebook (#689)
|
|
11
|
-
|
|
12
|
-
Adds the test-tier convention to `nmr.md`, the ambient rulebook `codeassembly sync` deploys to each consuming repo's agent guidance. The guidance describes the `<subject>[.<aspect>].<tier>.test.ts` grammar and the four tiers, and warns that tests outside a `__tests__` directory are ignored. Tests not explicitly attributed to a tier are flagged by the ReadyUp kit bundled with `nmr`.
|
|
13
|
-
|
|
14
|
-
The previous guidance is removed from `AGENTS.md`.
|
|
15
|
-
|
|
16
|
-
- Close each bin's output with a statement of what the run came to (#691)
|
|
17
|
-
|
|
18
|
-
Five nmr bins now end their output with a statement of the run's final result, rather than on whichever line the run happened to reach last, so the last line a reader sees carries the finding.
|
|
19
|
-
|
|
20
|
-
Separately, `ensure-prepublish-hooks` prints its whole report, including failure messages, on stdout, to avoid splitting the report across two streams. Failure still results in a non-zero exit code.
|
|
8
|
+
### 🐛 Bug fixes
|
|
21
9
|
|
|
22
|
-
-
|
|
10
|
+
- Recognize nmr behind a quoted environment assignment (#709)
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
Fixes an issue where a `package.json` entry that re-invoked its own command from behind an environment assignment whose quoted value carried a space (e.g., `"build": "NODE_OPTIONS=\"--a --b\" nmr build"`) caused nmr to hang.
|
|
25
13
|
|
|
26
|
-
|
|
14
|
+
- Restore upgrade reporting for repos that pin exact versions (#720)
|
|
27
15
|
|
|
28
|
-
|
|
16
|
+
Fixes an issue where `nmr upgrade` reported nothing in a repo that pins dependencies to exact versions, however far behind the tree was. nmr's shared upgrade policy now supplies both settings the upgrade tool needs to report a bare pin -- locked dependencies are included, and the range searched is `minor` -- and the default `upgrade` scripts no longer pass `--include-locked`, which went inert on its own in taze 20.0.1. The policy reaches a repo only through a `taze.config.ts` calling `defineConfig`, and nmr's ReadyUp kit now warns when a repo carries none.
|
|
29
17
|
|
|
30
|
-
|
|
18
|
+
The `minor` range mode applies to every dependency, not only a pinned one: a `~` range is searched as `^`, so minor upgrades are proposed for it, and a `packageMode` entry other than `minor` drops its dependency from a default pass rather than narrowing it.
|
|
31
19
|
<!-- /section:release-notes -->
|
|
32
20
|
|
|
33
21
|
## Installation
|
|
@@ -115,6 +103,10 @@ If no per-package override exists, the highest-tier value that is set wins. Set
|
|
|
115
103
|
|
|
116
104
|
> **Tip:** If your repo uses `eslint-plugin-package-json/valid-scripts`, empty strings are flagged as invalid. Use `":"` (the POSIX null command) instead — nmr treats it as a regular override that exits successfully and does nothing.
|
|
117
105
|
|
|
106
|
+
A `package.json` entry that re-invokes the command it is declared under is never honored. Honoring `"build": "nmr build"` would spawn a shell running `nmr build` in the same directory, reaching the same entry again without bound, so nmr discards the entry and resolves the command from the tier below. An entry that carries only the self-reference, trailing arguments included, is discarded in silence: it declares no step to lose. An entry that chains anything else onto it — `"build": "nmr build && rdy compile"`, or `"build": "rdy compile && nmr build"` — would lose those steps, so nmr rejects the invocation instead, naming the file, the key, and where the steps belong. For an ordinary command that is a `<command>:pre` or `<command>:post` script: `"build:pre": "rdy verify"` and `"build:post": "rdy compile"` wrap nmr's own `build` rather than replacing it. For a hook entry it is the entry itself, since nmr wraps a `:pre` or `:post` script in no hooks of its own — `"build:post": "nmr build:post && rdy compile"` becomes `"build:post": "rdy compile"`.
|
|
107
|
+
|
|
108
|
+
The re-invocation is recognized wherever it stands in the entry, on the grammar [the shelled-nmr line](#script-values) uses, and a segment carrying `-F` or `-R` is not one — it hands the command to other scopes rather than back to this one. Nor is `-w` from inside a package, which reaches the root's registry and the root's `package.json`. A filter that selects the declaring package is the hole in that exemption: `nmr -F own-pkg build` in `own-pkg`'s own `build` re-enters without bound, and nmr does not recognize it.
|
|
109
|
+
|
|
118
110
|
### Script values
|
|
119
111
|
|
|
120
112
|
A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` carry an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
|
|
@@ -145,9 +137,11 @@ An invocation carrying trailing arguments also serves no part of its work from t
|
|
|
145
137
|
|
|
146
138
|
A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step reaching nmr through a shell.
|
|
147
139
|
|
|
148
|
-
The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first.
|
|
140
|
+
The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first. A hook is the exception at both tiers: nmr wraps a `:pre` or `:post` script in no hooks of its own, so a hook's other steps become a script of their own that the step list names.
|
|
149
141
|
|
|
150
|
-
nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, or
|
|
142
|
+
nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule — it covers a step nmr runs as written, and only the reporting of the nested run is degraded — and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
|
|
143
|
+
|
|
144
|
+
A [self-referential `package.json` entry](#three-tier-override-system) is the shape nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply — what goes unrecognized there re-enters nmr without bound, which hangs rather than passing quietly.
|
|
151
145
|
|
|
152
146
|
## Configuration
|
|
153
147
|
|
|
@@ -544,8 +538,8 @@ A `pnpm.overrides` block left in the root `package.json` fails the command inste
|
|
|
544
538
|
The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
|
|
545
539
|
|
|
546
540
|
```bash
|
|
547
|
-
nmr upgrade # upgrades
|
|
548
|
-
nmr upgrade major # major upgrades,
|
|
541
|
+
nmr upgrade # minor and patch upgrades, regardless of the range each dependency declares
|
|
542
|
+
nmr upgrade major # major upgrades, for dependencies carrying no per-package ceiling
|
|
549
543
|
nmr upgrade --write # apply the proposals to package.json
|
|
550
544
|
```
|
|
551
545
|
|
|
@@ -564,11 +558,16 @@ export default defineConfig({
|
|
|
564
558
|
});
|
|
565
559
|
```
|
|
566
560
|
|
|
567
|
-
`defineConfig` supplies nmr's shared upgrade policy —
|
|
561
|
+
`defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
|
|
568
562
|
|
|
569
563
|
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here.
|
|
570
564
|
|
|
571
|
-
> **Note:**
|
|
565
|
+
> **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
|
|
566
|
+
|
|
567
|
+
Two consequences of the `minor` range mode the policy declares:
|
|
568
|
+
|
|
569
|
+
- A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
|
|
570
|
+
- A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
|
|
572
571
|
|
|
573
572
|
## Default script registries
|
|
574
573
|
|
|
@@ -598,7 +597,7 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
598
597
|
| `test:unit` | `pnpm exec vitest --project unit` |
|
|
599
598
|
| `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
|
|
600
599
|
| `typecheck` | `tsgo --noEmit` |
|
|
601
|
-
| `upgrade` | `nmr-report-catalog && nmr-taze
|
|
600
|
+
| `upgrade` | `nmr-report-catalog && nmr-taze` |
|
|
602
601
|
| `view-coverage` | `open coverage/index.html` |
|
|
603
602
|
|
|
604
603
|
`fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
|
|
@@ -712,9 +711,9 @@ A file git ignores is never formatted, even when named directly. Trailing argume
|
|
|
712
711
|
|
|
713
712
|
#### Dependencies
|
|
714
713
|
|
|
715
|
-
| Command | Runs
|
|
716
|
-
| --------- |
|
|
717
|
-
| `upgrade` | `nmr-report-overrides && nmr-taze --
|
|
714
|
+
| Command | Runs |
|
|
715
|
+
| --------- | ---------------------------------------------- |
|
|
716
|
+
| `upgrade` | `nmr-report-overrides && nmr-taze --recursive` |
|
|
718
717
|
|
|
719
718
|
See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
|
|
720
719
|
|
|
@@ -733,7 +732,7 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
733
732
|
| `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
|
|
734
733
|
| `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
|
|
735
734
|
| `root:typecheck` | `tsgo --noEmit` |
|
|
736
|
-
| `root:upgrade` | `nmr-report-overrides && nmr-taze
|
|
735
|
+
| `root:upgrade` | `nmr-report-overrides && nmr-taze` |
|
|
737
736
|
|
|
738
737
|
#### Utilities
|
|
739
738
|
|
|
@@ -885,7 +884,7 @@ Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool
|
|
|
885
884
|
Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
|
|
886
885
|
|
|
887
886
|
```bash
|
|
888
|
-
nmr-taze --
|
|
887
|
+
nmr-taze --recursive
|
|
889
888
|
```
|
|
890
889
|
|
|
891
890
|
### `ensure-prepublish-hooks`
|
|
@@ -22,7 +22,7 @@ export const workspaceScripts = {
|
|
|
22
22
|
'test:unit': 'pnpm exec vitest --project unit',
|
|
23
23
|
'test:watch': `pnpm exec vitest ${GATE_PROJECTS} --watch`,
|
|
24
24
|
typecheck: 'tsgo --noEmit',
|
|
25
|
-
upgrade: 'nmr-report-catalog && nmr-taze
|
|
25
|
+
upgrade: 'nmr-report-catalog && nmr-taze',
|
|
26
26
|
'view-coverage': 'open coverage/index.html',
|
|
27
27
|
};
|
|
28
28
|
export const rootScripts = {
|
|
@@ -52,7 +52,7 @@ export const rootScripts = {
|
|
|
52
52
|
'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
|
|
53
53
|
'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
|
|
54
54
|
'root:typecheck': 'tsgo --noEmit',
|
|
55
|
-
'root:upgrade': 'nmr-report-overrides && nmr-taze
|
|
55
|
+
'root:upgrade': 'nmr-report-overrides && nmr-taze',
|
|
56
56
|
test: ['root:test', '-R test'],
|
|
57
57
|
'test:all': ['root:test:all', '-R test:all'],
|
|
58
58
|
'test:coverage': ['root:test', '-R test:coverage'],
|
|
@@ -60,5 +60,5 @@ export const rootScripts = {
|
|
|
60
60
|
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
61
61
|
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
62
62
|
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', declinesArgs: true }],
|
|
63
|
-
upgrade: 'nmr-report-overrides && nmr-taze --
|
|
63
|
+
upgrade: 'nmr-report-overrides && nmr-taze --recursive',
|
|
64
64
|
};
|
package/dist/esm/help.js
CHANGED
|
@@ -43,7 +43,7 @@ function collectOverrides(packageDir) {
|
|
|
43
43
|
for (const [name, value] of Object.entries(scripts)) {
|
|
44
44
|
if (isHookName(name))
|
|
45
45
|
continue;
|
|
46
|
-
if (isSelfReferential(value, name))
|
|
46
|
+
if (isSelfReferential(value, name, packageDir))
|
|
47
47
|
continue;
|
|
48
48
|
filtered[name] = value;
|
|
49
49
|
}
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -20,5 +20,6 @@ export declare function describeScript(script: ScriptValue): string;
|
|
|
20
20
|
export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
|
|
21
21
|
export declare function buildWorkspaceRegistry(config: NmrConfig): ScriptRegistry;
|
|
22
22
|
export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
|
|
23
|
-
export declare function
|
|
23
|
+
export declare function findChainedSelfReference(packageDir: string | undefined, commandName: string): string | undefined;
|
|
24
|
+
export declare function isSelfReferential(script: string, commandName: string, packageDir: string): boolean;
|
|
24
25
|
export declare function resolveScript(commandName: string, registry: ScriptRegistry, packageDir: string | undefined, workspaceRoot: boolean): ResolvedScript | undefined;
|
package/dist/esm/resolver.js
CHANGED
|
@@ -4,7 +4,8 @@ import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
|
4
4
|
import { parsePackageJson, readScriptRecord, resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
5
5
|
import { isObject } from "./helpers/type-guards.js";
|
|
6
6
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
7
|
-
import { composeNmrStep } from "./steps.js";
|
|
7
|
+
import { composeNmrStep, readSelfReference } from "./steps.js";
|
|
8
|
+
import { isMonorepoRoot } from "./workspace.js";
|
|
8
9
|
export function applyDevBin(command, devBin, monorepoRoot) {
|
|
9
10
|
if (!devBin) {
|
|
10
11
|
return command;
|
|
@@ -88,16 +89,31 @@ export function buildRootRegistry(config) {
|
|
|
88
89
|
...config.rootScripts,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
export function
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
export function findChainedSelfReference(packageDir, commandName) {
|
|
93
|
+
if (packageDir === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const scripts = readPackageJsonScripts(packageDir);
|
|
97
|
+
if (scripts === undefined || !Object.hasOwn(scripts, commandName)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const script = scripts[commandName];
|
|
101
|
+
if (script === undefined) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return readSelfReference({ anchoredAtRoot: isMonorepoRoot(packageDir), commandName, script }) === 'chained'
|
|
105
|
+
? script
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
export function isSelfReferential(script, commandName, packageDir) {
|
|
109
|
+
return readSelfReference({ anchoredAtRoot: isMonorepoRoot(packageDir), commandName, script }) !== undefined;
|
|
94
110
|
}
|
|
95
111
|
export function resolveScript(commandName, registry, packageDir, workspaceRoot) {
|
|
96
112
|
if (packageDir) {
|
|
97
113
|
const pkgScripts = readPackageJsonScripts(packageDir);
|
|
98
114
|
if (pkgScripts && Object.hasOwn(pkgScripts, commandName)) {
|
|
99
115
|
const override = pkgScripts[commandName];
|
|
100
|
-
if (override !== undefined && !isSelfReferential(override, commandName)) {
|
|
116
|
+
if (override !== undefined && !isSelfReferential(override, commandName, packageDir)) {
|
|
101
117
|
return {
|
|
102
118
|
origin: { tier: 'package', file: resolvePackageJsonPath(packageDir), key: commandName },
|
|
103
119
|
steps: [{ kind: 'opaque', command: override }],
|
package/dist/esm/runCli.js
CHANGED
|
@@ -7,17 +7,24 @@ import { resolveContext } from "./context.js";
|
|
|
7
7
|
import { generateHelp } from "./help.js";
|
|
8
8
|
import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
|
|
9
9
|
import { isHookName } from "./helpers/hook-name.js";
|
|
10
|
+
import { resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
10
11
|
import { composeTranscript } from "./helpers/transcript.js";
|
|
11
12
|
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
12
13
|
import { assembleReplay } from "./replay-assembly.js";
|
|
13
14
|
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
14
|
-
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, resolveScript, } from "./resolver.js";
|
|
15
|
+
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, resolveScript, } from "./resolver.js";
|
|
15
16
|
import { resolveChannel, runSteps } from "./runner.js";
|
|
16
17
|
import { composeNmrStep, findNmrCrossing, renderChain } from "./steps.js";
|
|
18
|
+
import { UserError } from "./UserError.js";
|
|
17
19
|
import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity } from "./verbosity.js";
|
|
18
20
|
import { writeVerdict } from "./verdict.js";
|
|
19
21
|
const VERSION = readPackageVersion(import.meta.url);
|
|
20
22
|
const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
|
|
23
|
+
const NAMED_ESCAPES = new Map([
|
|
24
|
+
['\n', String.raw `\n`],
|
|
25
|
+
['\r', String.raw `\r`],
|
|
26
|
+
['\t', String.raw `\t`],
|
|
27
|
+
]);
|
|
21
28
|
export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
|
|
22
29
|
export async function runCli(options) {
|
|
23
30
|
const { args, cwd, env, stdout, stderr } = options;
|
|
@@ -84,6 +91,14 @@ export async function runCli(options) {
|
|
|
84
91
|
return runSteps([delegation.step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
85
92
|
}
|
|
86
93
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
94
|
+
assertNoSelfReference({
|
|
95
|
+
anchorDir,
|
|
96
|
+
command,
|
|
97
|
+
isReading: parsed.log,
|
|
98
|
+
monorepoRoot: context.monorepoRoot,
|
|
99
|
+
registry,
|
|
100
|
+
workspaceRoot: parsed.workspaceRoot,
|
|
101
|
+
});
|
|
87
102
|
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
88
103
|
if (!resolved) {
|
|
89
104
|
if (env[RUN_IF_PRESENT_ENV_VAR] === '1') {
|
|
@@ -255,8 +270,7 @@ function describeCrossingRemedy(options) {
|
|
|
255
270
|
};
|
|
256
271
|
case 'config':
|
|
257
272
|
return {
|
|
258
|
-
remedy: `Write the nmr steps as a step list, and move any others to
|
|
259
|
-
`\`${origin.key}:post\` script.`,
|
|
273
|
+
remedy: `Write the nmr steps as a step list, and move any others to ${describeStepDestination(origin.key)}.`,
|
|
260
274
|
subject: `${configSite}: \`${origin.field}.${origin.key}\``,
|
|
261
275
|
};
|
|
262
276
|
case 'package':
|
|
@@ -270,6 +284,12 @@ function describeCrossingRemedy(options) {
|
|
|
270
284
|
}
|
|
271
285
|
}
|
|
272
286
|
}
|
|
287
|
+
function describeStepDestination(key) {
|
|
288
|
+
if (isHookName(key)) {
|
|
289
|
+
return 'a script of their own that the step list names, because a hook has no `:pre` or `:post` of its own';
|
|
290
|
+
}
|
|
291
|
+
return `a \`${key}:pre\` or \`${key}:post\` script`;
|
|
292
|
+
}
|
|
273
293
|
function describeOrigin(origin, config, useRoot) {
|
|
274
294
|
if (origin.tier === 'package') {
|
|
275
295
|
return origin;
|
|
@@ -280,9 +300,17 @@ function describeOrigin(origin, config, useRoot) {
|
|
|
280
300
|
? { tier: 'config', field, key: origin.key }
|
|
281
301
|
: { tier: 'default', key: origin.key };
|
|
282
302
|
}
|
|
303
|
+
function escapeControlCharacters(text) {
|
|
304
|
+
let escaped = text;
|
|
305
|
+
for (const [char, escape] of NAMED_ESCAPES) {
|
|
306
|
+
escaped = escaped.split(char).join(escape);
|
|
307
|
+
}
|
|
308
|
+
return escaped;
|
|
309
|
+
}
|
|
283
310
|
function formatNmrCrossingWarning(options) {
|
|
284
311
|
const { remedy, subject } = describeCrossingRemedy(options);
|
|
285
|
-
return `⚠️ ${subject} reaches nmr through a shell (\`${options.crossing}\`),
|
|
312
|
+
return (`⚠️ ${subject} reaches nmr through a shell (\`${escapeControlCharacters(options.crossing)}\`), ` +
|
|
313
|
+
`${CROSSING_CONSEQUENCE} ${remedy}`);
|
|
286
314
|
}
|
|
287
315
|
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
288
316
|
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
@@ -291,18 +319,26 @@ function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
|
291
319
|
}
|
|
292
320
|
return `📦 ${path.basename(anchorDir)}: Using override script: ${renderChain(resolved.steps)}\n`;
|
|
293
321
|
}
|
|
322
|
+
function formatSelfReferenceRemedy(options) {
|
|
323
|
+
const { command, monorepoRoot, registry, script, workspaceRoot } = options;
|
|
324
|
+
if (isHookName(command)) {
|
|
325
|
+
return `Delete the re-invocation: \`${command}\` runs the steps standing beside it.`;
|
|
326
|
+
}
|
|
327
|
+
const configSite = path.relative(monorepoRoot, resolveConfigPath(monorepoRoot));
|
|
328
|
+
return formatPackageRemedy({ configSite, crossing: script, key: command, registry, workspaceRoot });
|
|
329
|
+
}
|
|
294
330
|
function formatPackageRemedy(options) {
|
|
295
331
|
const { configSite, crossing, key, registry, workspaceRoot } = options;
|
|
296
332
|
const registryEntry = Object.hasOwn(registry, key) ? registry[key] : undefined;
|
|
297
333
|
if (registryEntry === undefined) {
|
|
298
334
|
return (`A \`package.json\` script holds no step list: define \`${key}\` in \`${configSite}\` and move the ` +
|
|
299
|
-
`package-specific steps to
|
|
335
|
+
`package-specific steps to ${describeStepDestination(key)}.`);
|
|
300
336
|
}
|
|
301
337
|
const registryChain = renderChain(expandScript(registryEntry, workspaceRoot));
|
|
302
338
|
if (registryChain === crossing) {
|
|
303
|
-
return `Delete the entry: nmr's own \`${key}\` already runs \`${registryChain}\`.`;
|
|
339
|
+
return `Delete the entry: nmr's own \`${key}\` already runs \`${escapeControlCharacters(registryChain)}\`.`;
|
|
304
340
|
}
|
|
305
|
-
return `Delete the entry and move the steps it adds to
|
|
341
|
+
return `Delete the entry and move the steps it adds to ${describeStepDestination(key)}.`;
|
|
306
342
|
}
|
|
307
343
|
function findArgError(parsed) {
|
|
308
344
|
if (parsed.log && parsed.command === undefined && !parsed.help && !parsed.version) {
|
|
@@ -319,7 +355,24 @@ function findNoOpReason(resolvedCommand) {
|
|
|
319
355
|
}
|
|
320
356
|
return undefined;
|
|
321
357
|
}
|
|
358
|
+
function assertNoSelfReference(options) {
|
|
359
|
+
const { anchorDir, command, isReading, monorepoRoot, registry, workspaceRoot } = options;
|
|
360
|
+
if (isReading) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const script = findChainedSelfReference(anchorDir, command);
|
|
364
|
+
if (script === undefined) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const remedy = formatSelfReferenceRemedy({ command, monorepoRoot, registry, script, workspaceRoot });
|
|
368
|
+
const site = path.relative(monorepoRoot, resolvePackageJsonPath(anchorDir));
|
|
369
|
+
throw new UserError(`${site}: \`scripts.${command}\` re-invokes \`nmr ${command}\` (\`${escapeControlCharacters(script)}\`), ` +
|
|
370
|
+
`so nmr cannot run the steps it chains. ${remedy}`);
|
|
371
|
+
}
|
|
322
372
|
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
373
|
+
if (findChainedSelfReference(anchorDir, hookName) !== undefined) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
323
376
|
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
324
377
|
if (!resolved)
|
|
325
378
|
return false;
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -11,8 +11,14 @@ export interface NmrStepTarget {
|
|
|
11
11
|
isDelegate: boolean;
|
|
12
12
|
isWorkspaceRoot: boolean;
|
|
13
13
|
}
|
|
14
|
+
export type SelfReference = 'chained' | 'sole';
|
|
14
15
|
export declare function composeNmrStep(element: string, workspaceRoot: boolean, declinesArgs?: boolean): Step;
|
|
15
16
|
export declare function findNmrCrossing(steps: readonly Step[]): string | undefined;
|
|
16
17
|
export declare function findUnexpressibleToken(element: string): string | undefined;
|
|
17
18
|
export declare function readNmrStep(step: Step): NmrStepTarget | undefined;
|
|
19
|
+
export declare function readSelfReference(options: {
|
|
20
|
+
anchoredAtRoot: boolean;
|
|
21
|
+
commandName: string;
|
|
22
|
+
script: string;
|
|
23
|
+
}): SelfReference | undefined;
|
|
18
24
|
export declare function renderChain(steps: readonly Step[]): string;
|
package/dist/esm/steps.js
CHANGED
|
@@ -8,15 +8,16 @@ const LAUNCHERS = new Map([
|
|
|
8
8
|
['yarn', new Set(['dlx', 'exec'])],
|
|
9
9
|
]);
|
|
10
10
|
const QUOTES = new Set(['"', "'"]);
|
|
11
|
-
const SEGMENT_SEPARATORS = new Set([';', '|', '&']);
|
|
11
|
+
const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
|
12
12
|
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
13
|
+
const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
|
|
13
14
|
export function composeNmrStep(element, workspaceRoot, declinesArgs = false) {
|
|
14
15
|
const flags = workspaceRoot ? ['-w'] : [];
|
|
15
16
|
return { kind: 'structural', argv: ['nmr', ...flags, ...tokenize(element)], ...(declinesArgs && { declinesArgs }) };
|
|
16
17
|
}
|
|
17
18
|
export function findNmrCrossing(steps) {
|
|
18
19
|
for (const step of steps) {
|
|
19
|
-
if (step.kind === 'opaque' && splitSegments(step.command).some(
|
|
20
|
+
if (step.kind === 'opaque' && splitSegments(step.command).some((segment) => readNmrTail(segment) !== undefined)) {
|
|
20
21
|
return step.command;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -30,14 +31,65 @@ export function readNmrStep(step) {
|
|
|
30
31
|
return undefined;
|
|
31
32
|
}
|
|
32
33
|
const [file, ...rest] = step.argv;
|
|
33
|
-
|
|
34
|
+
return file === 'nmr' ? readNmrTarget(rest) : undefined;
|
|
35
|
+
}
|
|
36
|
+
export function readSelfReference(options) {
|
|
37
|
+
const { anchoredAtRoot, commandName, script } = options;
|
|
38
|
+
const segments = splitSegments(script).filter((segment) => segment.trim() !== '');
|
|
39
|
+
const reenters = segments.some((segment) => {
|
|
40
|
+
const tail = readNmrTail(segment);
|
|
41
|
+
const target = tail === undefined ? undefined : readNmrTarget(tail);
|
|
42
|
+
return target?.command === commandName && !target.isDelegate && (anchoredAtRoot || !target.isWorkspaceRoot);
|
|
43
|
+
});
|
|
44
|
+
if (!reenters) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return segments.length === 1 ? 'sole' : 'chained';
|
|
48
|
+
}
|
|
49
|
+
export function renderChain(steps) {
|
|
50
|
+
return steps.map(renderStep).join(' && ');
|
|
51
|
+
}
|
|
52
|
+
function dropLeadingAssignments(tokens) {
|
|
53
|
+
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
54
|
+
return start === -1 ? [] : tokens.slice(start);
|
|
55
|
+
}
|
|
56
|
+
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
57
|
+
if (char !== '&' && char !== '|') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
|
|
61
|
+
}
|
|
62
|
+
function quoteToken(token) {
|
|
63
|
+
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
64
|
+
return token;
|
|
65
|
+
}
|
|
66
|
+
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
67
|
+
}
|
|
68
|
+
function readNmrTail(segment) {
|
|
69
|
+
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
70
|
+
if (head === undefined) {
|
|
34
71
|
return undefined;
|
|
35
72
|
}
|
|
73
|
+
if (head === 'nmr') {
|
|
74
|
+
return rest;
|
|
75
|
+
}
|
|
76
|
+
const subcommands = LAUNCHERS.get(head);
|
|
77
|
+
if (subcommands === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (subcommands.size === 0) {
|
|
81
|
+
const nameIndex = rest.findIndex((token) => !token.startsWith('-'));
|
|
82
|
+
return nameIndex !== -1 && rest[nameIndex] === 'nmr' ? rest.slice(nameIndex + 1) : undefined;
|
|
83
|
+
}
|
|
84
|
+
const subcommandIndex = rest.findIndex((token) => subcommands.has(token));
|
|
85
|
+
return subcommandIndex !== -1 && rest[subcommandIndex + 1] === 'nmr' ? rest.slice(subcommandIndex + 2) : undefined;
|
|
86
|
+
}
|
|
87
|
+
function readNmrTarget(tokens) {
|
|
36
88
|
let isDelegate = false;
|
|
37
89
|
let isWorkspaceRoot = false;
|
|
38
90
|
let index = 0;
|
|
39
|
-
while (index <
|
|
40
|
-
const token =
|
|
91
|
+
while (index < tokens.length) {
|
|
92
|
+
const token = tokens[index] ?? '';
|
|
41
93
|
switch (token) {
|
|
42
94
|
case '-F':
|
|
43
95
|
case '--filter':
|
|
@@ -63,37 +115,6 @@ export function readNmrStep(step) {
|
|
|
63
115
|
}
|
|
64
116
|
return undefined;
|
|
65
117
|
}
|
|
66
|
-
export function renderChain(steps) {
|
|
67
|
-
return steps.map(renderStep).join(' && ');
|
|
68
|
-
}
|
|
69
|
-
function dropLeadingAssignments(tokens) {
|
|
70
|
-
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
71
|
-
return start === -1 ? [] : tokens.slice(start);
|
|
72
|
-
}
|
|
73
|
-
function quoteToken(token) {
|
|
74
|
-
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
75
|
-
return token;
|
|
76
|
-
}
|
|
77
|
-
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
78
|
-
}
|
|
79
|
-
function reachesNmr(segment) {
|
|
80
|
-
const [head, ...rest] = dropLeadingAssignments(tokenize(segment));
|
|
81
|
-
if (head === undefined) {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
if (head === 'nmr') {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
const subcommands = LAUNCHERS.get(head);
|
|
88
|
-
if (subcommands === undefined) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if (subcommands.size === 0) {
|
|
92
|
-
return rest.find((token) => !token.startsWith('-')) === 'nmr';
|
|
93
|
-
}
|
|
94
|
-
const subcommandIndex = rest.findIndex((token) => subcommands.has(token));
|
|
95
|
-
return subcommandIndex !== -1 && rest[subcommandIndex + 1] === 'nmr';
|
|
96
|
-
}
|
|
97
118
|
function renderStep(step) {
|
|
98
119
|
return step.kind === 'opaque' ? step.command : step.argv.map(quoteToken).join(' ');
|
|
99
120
|
}
|
|
@@ -128,6 +149,11 @@ function splitSegments(command) {
|
|
|
128
149
|
continue;
|
|
129
150
|
}
|
|
130
151
|
if (SEGMENT_SEPARATORS.has(char)) {
|
|
152
|
+
if (isRedirectionOperator(char, current, command[index + 1])) {
|
|
153
|
+
current += char;
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
131
157
|
index += command[index + 1] === char ? 2 : 1;
|
|
132
158
|
segments.push(current);
|
|
133
159
|
current = '';
|
|
@@ -142,3 +168,49 @@ function splitSegments(command) {
|
|
|
142
168
|
function tokenize(element) {
|
|
143
169
|
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
144
170
|
}
|
|
171
|
+
function tokenizeSegment(segment) {
|
|
172
|
+
const tokens = [];
|
|
173
|
+
let current = '';
|
|
174
|
+
let quote;
|
|
175
|
+
let index = 0;
|
|
176
|
+
while (index < segment.length) {
|
|
177
|
+
const char = segment[index] ?? '';
|
|
178
|
+
if (quote !== undefined) {
|
|
179
|
+
if (char === '\\' && quote === '"') {
|
|
180
|
+
current += char + (segment[index + 1] ?? '');
|
|
181
|
+
index += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
current += char;
|
|
185
|
+
if (char === quote)
|
|
186
|
+
quote = undefined;
|
|
187
|
+
index += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (char === '\\') {
|
|
191
|
+
current += char + (segment[index + 1] ?? '');
|
|
192
|
+
index += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (QUOTES.has(char)) {
|
|
196
|
+
quote = char;
|
|
197
|
+
current += char;
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (TOKEN_SEPARATORS.has(char)) {
|
|
202
|
+
if (current !== '') {
|
|
203
|
+
tokens.push(current);
|
|
204
|
+
}
|
|
205
|
+
current = '';
|
|
206
|
+
index += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
current += char;
|
|
210
|
+
index += 1;
|
|
211
|
+
}
|
|
212
|
+
if (current !== '') {
|
|
213
|
+
tokens.push(current);
|
|
214
|
+
}
|
|
215
|
+
return tokens;
|
|
216
|
+
}
|
package/dist/esm/taze.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -73,13 +73,14 @@
|
|
|
73
73
|
"prettier-plugin-sh": "0.19.0",
|
|
74
74
|
"taze": "20.0.1",
|
|
75
75
|
"yaml": "2.9.0",
|
|
76
|
-
"@williamthorsen/nmr-core": "0.10.
|
|
76
|
+
"@williamthorsen/nmr-core": "0.10.2"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
+
"@williamthorsen/toolbelt.filesystem": "0.6.0",
|
|
79
80
|
"@williamthorsen/toolbelt.testing": "0.3.0",
|
|
80
81
|
"@williamthorsen/toolbelt.vitest": "0.4.0",
|
|
81
82
|
"prettier": "3.9.6",
|
|
82
|
-
"readyup": "0.
|
|
83
|
+
"readyup": "0.29.0",
|
|
83
84
|
"tinyglobby": "0.2.17",
|
|
84
85
|
"typescript": "6.0.3",
|
|
85
86
|
"vitest": "4.1.10"
|