@cplieger/deadset-ts 8.0.0-dev.12 → 8.0.0-dev.13

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.
Files changed (73) hide show
  1. package/README.md +2 -0
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/dist/src/build-scripts.js +6 -13
  4. package/dist/src/commands/read.js +156 -0
  5. package/dist/src/commands/shell.js +75 -0
  6. package/dist/src/commands/tools.js +460 -0
  7. package/dist/src/configuration-files.js +11 -25
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/dependencies.js +67 -115
  10. package/dist/src/load/synthesize.js +12 -5
  11. package/dist/src/manifest.js +27 -14
  12. package/dist/src/roots.js +18 -11
  13. package/dist/src/text/named-files.js +24 -3
  14. package/dist/src/text/yaml.js +464 -173
  15. package/dist/src/vendor/unbash/ansi-c.js +132 -0
  16. package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
  17. package/dist/src/vendor/unbash/arithmetic.js +591 -0
  18. package/dist/src/vendor/unbash/assignment.js +131 -0
  19. package/dist/src/vendor/unbash/chars.js +48 -0
  20. package/dist/src/vendor/unbash/command.js +41 -0
  21. package/dist/src/vendor/unbash/expansion.js +80 -0
  22. package/dist/src/vendor/unbash/glob.js +268 -0
  23. package/dist/src/vendor/unbash/heredoc.js +27 -0
  24. package/dist/src/vendor/unbash/internal-types.js +1 -0
  25. package/dist/src/vendor/unbash/lexer.js +4274 -0
  26. package/dist/src/vendor/unbash/limits.js +3 -0
  27. package/dist/src/vendor/unbash/parser.js +1366 -0
  28. package/dist/src/vendor/unbash/parts.js +57 -0
  29. package/dist/src/vendor/unbash/substitution-context.js +8 -0
  30. package/dist/src/vendor/unbash/traversal.js +212 -0
  31. package/dist/src/vendor/unbash/types.js +1 -0
  32. package/dist/src/vendor/unbash/word-value.js +61 -0
  33. package/dist/src/vendor/unbash/word.js +40 -0
  34. package/dist/src/workflow-steps.js +111 -133
  35. package/jsr.json +2 -1
  36. package/package.json +4 -2
  37. package/src/build-scripts.ts +6 -13
  38. package/src/commands/read.ts +230 -0
  39. package/src/commands/shell.ts +126 -0
  40. package/src/commands/tools.ts +618 -0
  41. package/src/configuration-files.ts +10 -26
  42. package/src/conformance-record.ts +1 -1
  43. package/src/dependencies.ts +99 -123
  44. package/src/load/synthesize.ts +18 -5
  45. package/src/manifest.ts +43 -25
  46. package/src/roots.ts +20 -13
  47. package/src/text/named-files.ts +26 -3
  48. package/src/text/yaml.ts +503 -179
  49. package/src/vendor/unbash/LICENSE +15 -0
  50. package/src/vendor/unbash/VENDORED.md +16 -0
  51. package/src/vendor/unbash/ansi-c.ts +132 -0
  52. package/src/vendor/unbash/arithmetic-parts.ts +53 -0
  53. package/src/vendor/unbash/arithmetic.ts +641 -0
  54. package/src/vendor/unbash/assignment.ts +163 -0
  55. package/src/vendor/unbash/chars.ts +48 -0
  56. package/src/vendor/unbash/command.ts +47 -0
  57. package/src/vendor/unbash/expansion.ts +144 -0
  58. package/src/vendor/unbash/glob.ts +284 -0
  59. package/src/vendor/unbash/heredoc.ts +34 -0
  60. package/src/vendor/unbash/internal-types.ts +94 -0
  61. package/src/vendor/unbash/lexer.ts +4352 -0
  62. package/src/vendor/unbash/limits.ts +3 -0
  63. package/src/vendor/unbash/parser.ts +1612 -0
  64. package/src/vendor/unbash/parts.ts +96 -0
  65. package/src/vendor/unbash/substitution-context.ts +20 -0
  66. package/src/vendor/unbash/traversal.ts +182 -0
  67. package/src/vendor/unbash/types.ts +660 -0
  68. package/src/vendor/unbash/word-value.ts +64 -0
  69. package/src/vendor/unbash/word.ts +50 -0
  70. package/src/workflow-steps.ts +153 -154
  71. package/dist/src/text/shell.js +0 -159
  72. package/src/text/named-files.inputs.json +0 -1522
  73. package/src/text/shell.ts +0 -161
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Layer 2 of the command reader: one row per tool, its reader of a simple command's words after
3
+ * its program word and the options it names its configuration file by, each as the tool
4
+ * documents them, and one reader per shell builtin that moves the shell's directory. A reader
5
+ * never guesses: an option it does not handle, or a dynamic word where it needs a value, leaves
6
+ * the command unread.
7
+ */
8
+
9
+ import { resolvePath } from "../paths.ts";
10
+ import type { Word } from "./shell.ts";
11
+
12
+ /** One module a runtime loads before its entry, and the resolution its option names it by. */
13
+ export interface Preload {
14
+ readonly specifier: string;
15
+ /** `require` for a CommonJS `require` path, `import` for an ECMAScript module URL. */
16
+ readonly resolution: "require" | "import";
17
+ }
18
+
19
+ /** What one simple command is read as. */
20
+ export type Reading =
21
+ /** It runs `entry`, a path, after loading `preloads`. */
22
+ | {
23
+ readonly kind: "run";
24
+ readonly entry: string | undefined;
25
+ readonly preloads: readonly Preload[];
26
+ /** `node` where the entry runs under Node itself. */
27
+ readonly runtime: "node" | undefined;
28
+ }
29
+ /** It runs the command `words`. */
30
+ | { readonly kind: "command"; readonly words: readonly Word[] }
31
+ /** It runs `text` as a command line of its own. */
32
+ | { readonly kind: "line"; readonly text: string };
33
+
34
+ type Reader = (words: readonly Word[]) => Reading | undefined;
35
+
36
+ /** The options a tool takes before its first operand. */
37
+ interface Options {
38
+ /** The options that take a value, the next word or, for a long one, the text after `=`. */
39
+ readonly valued: ReadonlySet<string>;
40
+ /** The options that take none. */
41
+ readonly flags: ReadonlySet<string>;
42
+ /** Whether a one-letter valued option may carry its value in the same word, as `-uNAME`. */
43
+ readonly attached?: boolean;
44
+ }
45
+
46
+ /** The options before the first operand, with their values, and the operand's index. */
47
+ interface Read {
48
+ readonly values: ReadonlyMap<string, readonly string[]>;
49
+ readonly at: number;
50
+ /** Whether a `--` ended the options. */
51
+ readonly ended: boolean;
52
+ }
53
+
54
+ /** The options `words` start with, or `undefined` where one is unhandled or a needed word is dynamic. */
55
+ function optionsOf(words: readonly Word[], options: Options): Read | undefined {
56
+ const values = new Map<string, string[]>();
57
+ const keep = (name: string, value: string): void => {
58
+ values.set(name, [...(values.get(name) ?? []), value]);
59
+ };
60
+ for (let at = 0; at < words.length; at += 1) {
61
+ const word = words[at]?.value;
62
+ if (word === undefined) {
63
+ return undefined;
64
+ }
65
+ if (word === "--") {
66
+ return { values, at: at + 1, ended: true };
67
+ }
68
+ if (!word.startsWith("-") || options.flags.has(word)) {
69
+ if (!word.startsWith("-")) {
70
+ return { values, at, ended: false };
71
+ }
72
+ continue;
73
+ }
74
+ const equals = word.startsWith("--") ? word.indexOf("=") : -1;
75
+ const name = equals < 0 ? word : word.slice(0, equals);
76
+ if (options.valued.has(name)) {
77
+ const value = equals < 0 ? words[(at += 1)]?.value : word.slice(equals + 1);
78
+ if (value === undefined) {
79
+ return undefined;
80
+ }
81
+ keep(name, value);
82
+ } else if (
83
+ options.attached === true &&
84
+ options.valued.has(word.slice(0, 2)) &&
85
+ !word.startsWith("--")
86
+ ) {
87
+ keep(word.slice(0, 2), word.slice(2));
88
+ } else {
89
+ return undefined;
90
+ }
91
+ }
92
+ return { values, at: words.length, ended: false };
93
+ }
94
+
95
+ /** A specifier that starts with a URL scheme, `file:` and `node:` among them. */
96
+ const SCHEME = /^[a-z][a-z\d+.-]*:/iu;
97
+
98
+ /** A specifier written as a path: `.`, `..`, or one starting `./`, `../` or `/`. */
99
+ const RELATIVE = /^(?:\.{1,2}(?:\/|$)|\/)/u;
100
+
101
+ /**
102
+ * The path a URL's path text names, percent-decoded as Node's `fileURLToPath` does, with a
103
+ * backslash read as the URL parser reads it, or `undefined` where it does not decode or
104
+ * encodes a separator: https://nodejs.org/api/url.html#urlfileurltopathurl-options
105
+ */
106
+ function urlPath(text: string): string | undefined {
107
+ const path = text.replace(/[?#].*$/su, "").replaceAll("\\", "/");
108
+ if (/%2f|%5c/iu.test(path)) {
109
+ return undefined;
110
+ }
111
+ try {
112
+ return decodeURIComponent(path);
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Where one preload is, read in `dir`: the absolute path of a file, a package's name, or
120
+ * `undefined` where it names neither. `require` reads a path as written; an ECMAScript
121
+ * specifier is a URL, so a relative one or a `file:` one is percent-decoded and any other
122
+ * scheme loads no file: https://nodejs.org/api/esm.html#urls
123
+ */
124
+ export function preloadTarget(
125
+ preload: Preload,
126
+ dir: string,
127
+ ): { readonly path: string } | { readonly package: string } | undefined {
128
+ const { specifier, resolution } = preload;
129
+ if (resolution === "require") {
130
+ return RELATIVE.test(specifier)
131
+ ? { path: resolvePath(dir, specifier) }
132
+ : SCHEME.test(specifier)
133
+ ? undefined
134
+ : { package: specifier };
135
+ }
136
+ if (!RELATIVE.test(specifier) && !SCHEME.test(specifier)) {
137
+ return { package: specifier };
138
+ }
139
+ const local = /^file:(?:\/\/(?=\/))?/iu.exec(specifier);
140
+ const rest = specifier.slice(local?.[0].length ?? 0);
141
+ // `file://host/…` names a file on another host.
142
+ if ((SCHEME.test(specifier) && local === null) || rest.startsWith("//")) {
143
+ return undefined;
144
+ }
145
+ const path = urlPath(rest);
146
+ return path === undefined ? undefined : { path: resolvePath(dir, path) };
147
+ }
148
+
149
+ /** A runtime: its preload options, then its entry, and every later word handed to the program. */
150
+ function runtime(
151
+ preloads: ReadonlyMap<string, Preload["resolution"]>,
152
+ flags: readonly string[],
153
+ runsNode: boolean,
154
+ subcommand?: string,
155
+ ): Reader {
156
+ const options = { valued: new Set(preloads.keys()), flags: new Set(flags) };
157
+ return (words) => {
158
+ const from = subcommand !== undefined && words[0]?.value === subcommand ? 1 : 0;
159
+ const read = optionsOf(words.slice(from), options);
160
+ if (read === undefined) {
161
+ return undefined;
162
+ }
163
+ const entry = words[from + read.at];
164
+ const loads = [...preloads].flatMap(([name, resolution]) =>
165
+ (read.values.get(name) ?? []).map((specifier) => ({ specifier, resolution })),
166
+ );
167
+ if (
168
+ (entry !== undefined && entry.value === undefined) ||
169
+ loads.some((one) => preloadTarget(one, "/") === undefined)
170
+ ) {
171
+ return undefined;
172
+ }
173
+ return {
174
+ kind: "run",
175
+ entry: entry?.value,
176
+ preloads: loads,
177
+ runtime: runsNode && entry !== undefined ? "node" : undefined,
178
+ };
179
+ };
180
+ }
181
+
182
+ /**
183
+ * `node [options] [entry] [arguments]`, https://nodejs.org/api/cli.html: the options that load
184
+ * a module before the entry, `-r` as `require` does and the rest as an ECMAScript module.
185
+ * `-e`, `-p` and `-` run text that is no file, so they are unhandled.
186
+ */
187
+ const node = runtime(
188
+ new Map([
189
+ ["-r", "require"],
190
+ ["--require", "require"],
191
+ ["--import", "import"],
192
+ ["--loader", "import"],
193
+ ["--experimental-loader", "import"],
194
+ ]),
195
+ [],
196
+ true,
197
+ );
198
+
199
+ /** `tsx [watch] [options] <file> [arguments]`, https://tsx.is/usage, https://tsx.is/node-enhancement */
200
+ const tsx = runtime(
201
+ new Map([
202
+ ["-r", "require"],
203
+ ["--require", "require"],
204
+ ["--import", "import"],
205
+ ]),
206
+ [],
207
+ false,
208
+ "watch",
209
+ );
210
+
211
+ /** `ts-node [options] [script] [arguments]`, https://typestrong.org/ts-node/docs/options; `--esm` picks no file. */
212
+ const tsNode = runtime(
213
+ new Map([
214
+ ["-r", "require"],
215
+ ["--require", "require"],
216
+ ]),
217
+ ["--esm"],
218
+ false,
219
+ );
220
+
221
+ /** Hands on the command `words` from `at`, after the assignments `assigns` accepts. */
222
+ function afterAssignments(
223
+ words: readonly Word[],
224
+ at: number,
225
+ assigns: (value: string) => boolean,
226
+ ): Reading | undefined {
227
+ let end = at;
228
+ while (end < words.length && words[end]?.value?.includes("=") === true) {
229
+ if (!assigns(words[end]?.value ?? "")) {
230
+ return undefined;
231
+ }
232
+ end += 1;
233
+ }
234
+ const program = words[end];
235
+ if (program !== undefined && (program.value === undefined || program.value.startsWith("-"))) {
236
+ return undefined;
237
+ }
238
+ return program === undefined
239
+ ? { kind: "run", entry: undefined, preloads: [], runtime: undefined }
240
+ : { kind: "command", words: words.slice(end) };
241
+ }
242
+
243
+ /**
244
+ * `env [option]... [name=value]... [command [argument]...]`,
245
+ * https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html: the options that
246
+ * change only the environment, `-i`, `-u` and `-v` in each spelling. GNU env takes any word
247
+ * holding an `=` after its first character as an assignment, and fails before running anything
248
+ * on a `-u` name that is empty or holds an `=`.
249
+ */
250
+ const env: Reader = (words) => {
251
+ const read = optionsOf(words, {
252
+ valued: new Set(["-u", "--unset"]),
253
+ flags: new Set(["-i", "--ignore-environment", "-", "-v", "--debug"]),
254
+ attached: true,
255
+ });
256
+ const unset = [...(read?.values.get("-u") ?? []), ...(read?.values.get("--unset") ?? [])];
257
+ return read === undefined || unset.some((name) => name === "" || name.includes("="))
258
+ ? undefined
259
+ : afterAssignments(words, read.at, (value) => !value.startsWith("="));
260
+ };
261
+
262
+ /** `cross-env [name=value]... command [argument]...`, https://github.com/kentcdodds/cross-env#usage */
263
+ const crossEnv: Reader = (words) => {
264
+ const reading = afterAssignments(words, 0, (value) => /^\w+=/u.test(value));
265
+ return reading?.kind === "command" ? reading : undefined;
266
+ };
267
+
268
+ /** The command a package runner hands on: its first word a package's or a command's name. */
269
+ function runnerCommand(words: readonly Word[], packaged: boolean): Reading | undefined {
270
+ const [first, ...rest] = words;
271
+ if (first?.value === undefined || first.value.startsWith("-")) {
272
+ return undefined;
273
+ }
274
+ // A package specifier names the package's command; a version picks no other command.
275
+ const name = packaged ? first.value.replace(/(?<=.)@[^/@]*$/u, "") : first.value;
276
+ return { kind: "command", words: [{ value: name, text: first.text }, ...rest] };
277
+ }
278
+
279
+ /**
280
+ * npx and npm exec, https://docs.npmjs.com/cli/v11/commands/npx and
281
+ * https://docs.npmjs.com/cli/v11/commands/npm-exec: `--package` (and npx's `-p`), `--call`,
282
+ * `--yes` and `--no`, before the first positional word, which is the package whose command
283
+ * runs unless a `--package` names one. npm exec reads options after it too, unless a `--` ends them.
284
+ */
285
+ function packageExec(shortPackage: boolean, laterOptions: boolean): Reader {
286
+ const valued = new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]);
287
+ return (words) => {
288
+ const read = optionsOf(words, { valued, flags: new Set(["-y", "--yes", "--no"]) });
289
+ if (read === undefined) {
290
+ return undefined;
291
+ }
292
+ const operands = words.slice(read.at);
293
+ if (
294
+ laterOptions &&
295
+ !read.ended &&
296
+ operands.some((word) => word.value?.startsWith("-") !== false)
297
+ ) {
298
+ return undefined;
299
+ }
300
+ const call = [...(read.values.get("-c") ?? []), ...(read.values.get("--call") ?? [])];
301
+ if (call.length > 0) {
302
+ return call.length === 1 && operands.length === 0 && call[0] !== undefined
303
+ ? { kind: "line", text: call[0] }
304
+ : undefined;
305
+ }
306
+ const packages = [...(read.values.get("--package") ?? []), ...(read.values.get("-p") ?? [])];
307
+ return runnerCommand(operands, packages.length === 0);
308
+ };
309
+ }
310
+
311
+ const npx = packageExec(true, false);
312
+ const npmExec = packageExec(false, true);
313
+
314
+ /**
315
+ * `npm run <script> [-- <arguments>]`, https://docs.npmjs.com/cli/v11/commands/npm-run-script:
316
+ * it runs another script of the same manifest, which is read on its own.
317
+ */
318
+ const npmRun: Reader = (words) => {
319
+ const [script, ...rest] = words;
320
+ const ended = rest.findIndex((word) => word.value === "--");
321
+ const before = ended < 0 ? rest : rest.slice(0, ended);
322
+ if (
323
+ script?.value === undefined ||
324
+ script.value.startsWith("-") ||
325
+ before.some((word) => word.value?.startsWith("-") !== false)
326
+ ) {
327
+ return undefined;
328
+ }
329
+ return { kind: "run", entry: undefined, preloads: [], runtime: undefined };
330
+ };
331
+
332
+ /** A tool whose second word picks the reader of the rest. */
333
+ function subcommands(readers: ReadonlyMap<string, Reader>): Reader {
334
+ return ([first, ...rest]) =>
335
+ first?.value === undefined ? undefined : readers.get(first.value)?.(rest);
336
+ }
337
+
338
+ /** `npm exec`, `npm x` and `npm run` with `run-script`. */
339
+ const npm = subcommands(
340
+ new Map([
341
+ ["exec", npmExec],
342
+ ["x", npmExec],
343
+ ["run", npmRun],
344
+ ["run-script", npmRun],
345
+ ]),
346
+ );
347
+
348
+ /**
349
+ * `pnpm exec <command>`, https://pnpm.io/cli/exec, which hands on every later word, and
350
+ * `pnpm dlx [--package <name>]... <command>`, https://pnpm.io/cli/dlx.
351
+ */
352
+ const pnpm = subcommands(
353
+ new Map<string, Reader>([
354
+ ["exec", (words) => runnerCommand(words, false)],
355
+ [
356
+ "dlx",
357
+ (words) => {
358
+ const read = optionsOf(words, { valued: new Set(["--package"]), flags: new Set() });
359
+ return read === undefined
360
+ ? undefined
361
+ : runnerCommand(words.slice(read.at), !read.values.has("--package"));
362
+ },
363
+ ],
364
+ ]),
365
+ );
366
+
367
+ /**
368
+ * `yarn node`, https://yarnpkg.com/cli/node, and `yarn dlx [-p <name>]... <command>`,
369
+ * https://yarnpkg.com/cli/dlx, whose command runs in the current directory.
370
+ */
371
+ const yarn = subcommands(
372
+ new Map<string, Reader>([
373
+ ["node", (words) => ({ kind: "command", words: [{ value: "node", text: "node" }, ...words] })],
374
+ [
375
+ "dlx",
376
+ (words) => {
377
+ const read = optionsOf(words, {
378
+ valued: new Set(["-p", "--package"]),
379
+ flags: new Set(["-q", "--quiet"]),
380
+ });
381
+ return read === undefined
382
+ ? undefined
383
+ : runnerCommand(
384
+ words.slice(read.at),
385
+ !read.values.has("-p") && !read.values.has("--package"),
386
+ );
387
+ },
388
+ ],
389
+ ]),
390
+ );
391
+
392
+ /** `bunx [-p <name>] <command>[@version] [arguments]`, https://bun.com/docs/pm/bunx */
393
+ const bunx: Reader = (words) => {
394
+ const read = optionsOf(words, { valued: new Set(["-p", "--package"]), flags: new Set() });
395
+ return read === undefined ? undefined : runnerCommand(words.slice(read.at), true);
396
+ };
397
+
398
+ /**
399
+ * The options a tool documents for naming its configuration file, each taking the file as its
400
+ * value, the next word or, for a long one, the text after `=`.
401
+ */
402
+ interface ConfigurationOptions {
403
+ readonly valued: ReadonlySet<string>;
404
+ /** Where set, the tool reads options only before its first operand, and these others take a value. */
405
+ readonly beforeOperand?: ReadonlySet<string>;
406
+ }
407
+
408
+ /** One tool's row: the reader of its command, and the options it names its configuration file by. */
409
+ interface Row {
410
+ readonly read?: Reader;
411
+ readonly configurations?: ConfigurationOptions;
412
+ }
413
+
414
+ /** The row of a tool whose reader is `read`. */
415
+ function reading(read: Reader): Row {
416
+ return { read };
417
+ }
418
+
419
+ /** The row of a tool that names its configuration file by `options` anywhere before a `--`. */
420
+ function configuredBy(...options: readonly string[]): Row {
421
+ return { configurations: { valued: new Set(options) } };
422
+ }
423
+
424
+ /** The row of each tool, by its program word: its name, or a path to it in `node_modules/.bin`. */
425
+ const TOOLS: ReadonlyMap<string, Row> = new Map<string, Row>([
426
+ ["node", reading(node)],
427
+ ["tsx", reading(tsx)],
428
+ ["ts-node", reading(tsNode)],
429
+ ["env", reading(env)],
430
+ ["cross-env", reading(crossEnv)],
431
+ [
432
+ "npx",
433
+ {
434
+ read: npx,
435
+ // `-c` takes a command line, which is read as a configuration file's name as well, so
436
+ // every configuration `fixtures/commands/observed.json` records stays named.
437
+ configurations: {
438
+ valued: new Set(["-c"]),
439
+ beforeOperand: new Set(["--package", "-p", "--call"]),
440
+ },
441
+ },
442
+ ],
443
+ ["npm", reading(npm)],
444
+ ["pnpm", reading(pnpm)],
445
+ ["yarn", reading(yarn)],
446
+ ["bunx", reading(bunx)],
447
+ // https://eslint.org/docs/latest/use/command-line-interface
448
+ ["eslint", configuredBy("-c", "--config")],
449
+ // https://prettier.io/docs/cli, whose `-c` is `--check`.
450
+ ["prettier", configuredBy("--config")],
451
+ // https://stylelint.io/user-guide/cli
452
+ ["stylelint", configuredBy("-c", "--config")],
453
+ // https://jestjs.io/docs/cli
454
+ ["jest", configuredBy("-c", "--config")],
455
+ // https://vitest.dev/guide/cli
456
+ ["vitest", configuredBy("-c", "--config")],
457
+ // https://vite.dev/guide/cli
458
+ ["vite", configuredBy("-c", "--config")],
459
+ // https://webpack.js.org/api/cli/
460
+ ["webpack", configuredBy("-c", "--config")],
461
+ // https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
462
+ ["mocha", configuredBy("--config")],
463
+ ]);
464
+
465
+ /** The row of a simple command's program word, by its name or a path to it in `node_modules/.bin`. */
466
+ function rowOf(program: Word | undefined): Row | undefined {
467
+ const name =
468
+ program?.value === undefined
469
+ ? undefined
470
+ : /^(?:.*\/)?node_modules\/\.bin\/([^/]+)$|^([^/]+)$/u.exec(program.value);
471
+ const tool = name?.[1] ?? name?.[2];
472
+ return tool === undefined ? undefined : TOOLS.get(tool);
473
+ }
474
+
475
+ /**
476
+ * What the reader of one simple command's program reads it as, or `undefined` where none
477
+ * understands it. A program word that is any other path runs that file, whatever its name.
478
+ */
479
+ export function readTool(words: readonly Word[]): Reading | undefined {
480
+ const [program, ...rest] = words;
481
+ return rowOf(program)?.read?.(rest);
482
+ }
483
+
484
+ /**
485
+ * The configuration files one simple command names, as written: each static value of an option
486
+ * its program's row names a configuration file by, and none for a program with no such row.
487
+ */
488
+ export function configurationsOf(words: readonly Word[]): readonly string[] {
489
+ const [program, ...rest] = words;
490
+ const options = rowOf(program)?.configurations;
491
+ const found: string[] = [];
492
+ for (let at = 0; options !== undefined && at < rest.length; at += 1) {
493
+ const word = rest[at]?.value;
494
+ if (word === "--" || (word === undefined && options.beforeOperand !== undefined)) {
495
+ break;
496
+ }
497
+ const equals = word?.startsWith("--") === true ? word.indexOf("=") : -1;
498
+ const name = equals < 0 ? word : word?.slice(0, equals);
499
+ if (name !== undefined && options.valued.has(name)) {
500
+ const value = equals < 0 ? rest[(at += 1)]?.value : word?.slice(equals + 1);
501
+ found.push(...(value === undefined || value === "" ? [] : [value]));
502
+ } else if (options.beforeOperand !== undefined && name !== undefined) {
503
+ if (!name.startsWith("-")) {
504
+ break;
505
+ }
506
+ at += options.beforeOperand.has(name) && equals < 0 ? 1 : 0;
507
+ }
508
+ }
509
+ return found;
510
+ }
511
+
512
+ /** One way the shell that ran a command goes on: in `dir`, `undefined` where unknown, after the status `ok`. */
513
+ export interface Outcome {
514
+ readonly dir: string | undefined;
515
+ readonly ok: boolean;
516
+ }
517
+
518
+ /**
519
+ * Each way a builtin, run in `dir`, leaves the shell running it, none where it ends that
520
+ * shell, or `undefined` where it leaves it as any other command does.
521
+ */
522
+ type Builtin = (
523
+ operands: readonly Word[],
524
+ dir: string | undefined,
525
+ ) => readonly Outcome[] | undefined;
526
+
527
+ /** Either status, in a directory the analysis does not know. */
528
+ const ELSEWHERE: readonly Outcome[] = [
529
+ { dir: undefined, ok: true },
530
+ { dir: undefined, ok: false },
531
+ ];
532
+
533
+ const elsewhere: Builtin = () => ELSEWHERE;
534
+
535
+ /** The directory a builtin's one operand states, where it is static and starts with no character of `options`. */
536
+ function statedDirectory(operands: readonly Word[], options: string): string | undefined {
537
+ const [dir] = operands;
538
+ return operands.length === 1 && dir?.value !== undefined && !options.includes(dir.value.charAt(0))
539
+ ? dir.value
540
+ : undefined;
541
+ }
542
+
543
+ /** `cd` to a stated directory is taken to enter it, as `roots.13` states. */
544
+ const cd: Builtin = (operands, dir) => {
545
+ const to = statedDirectory(operands, "-");
546
+ return to === undefined
547
+ ? ELSEWHERE
548
+ : [{ dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true }];
549
+ };
550
+
551
+ /** `pushd` enters a stated directory, or fails and leaves the shell where it was. */
552
+ const pushd: Builtin = (operands, dir) => {
553
+ const to = statedDirectory(operands, "-+");
554
+ return to === undefined
555
+ ? ELSEWHERE
556
+ : [
557
+ { dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true },
558
+ { dir, ok: false },
559
+ ];
560
+ };
561
+
562
+ /**
563
+ * `exec` with a command replaces the shell, which exits where it cannot; with none it only
564
+ * redirects: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exec
565
+ */
566
+ const exec: Builtin = ([command]) => {
567
+ if (command === undefined) {
568
+ return undefined;
569
+ }
570
+ return command.value === undefined || command.value.startsWith("-") ? ELSEWHERE : [];
571
+ };
572
+
573
+ /** `exit` ends the shell whatever its operands, in dash and in bash. */
574
+ const exit: Builtin = () => [];
575
+
576
+ /** The builtins that run another command in the same shell, where that one may be any builtin. */
577
+ const runsBuiltin: Builtin = (operands) =>
578
+ operands.some((word) => word.value === undefined || BUILTINS.has(word.value))
579
+ ? ELSEWHERE
580
+ : undefined;
581
+
582
+ /**
583
+ * The shell builtins that may move or end the shell running them: `cd`,
584
+ * https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cd.html; `pushd` and `popd`,
585
+ * https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html; `exit`,
586
+ * `exec`, and `return`, whose effect outside a function POSIX leaves unspecified,
587
+ * https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#return; and `.`,
588
+ * `eval`, `source`, `command` and `builtin`, which run other text or another builtin.
589
+ */
590
+ const BUILTINS: ReadonlyMap<string, Builtin> = new Map<string, Builtin>([
591
+ ["cd", cd],
592
+ ["pushd", pushd],
593
+ ["popd", elsewhere],
594
+ ["exit", exit],
595
+ ["exec", exec],
596
+ ["return", elsewhere],
597
+ [".", elsewhere],
598
+ ["source", elsewhere],
599
+ ["eval", elsewhere],
600
+ ["command", runsBuiltin],
601
+ ["builtin", runsBuiltin],
602
+ ]);
603
+
604
+ /**
605
+ * Each way the shell running one simple command in its own environment, in `dir`, goes on
606
+ * after a builtin that may move or end it, or `undefined` where the command is no such
607
+ * builtin. A program word the shell computes may be any builtin.
608
+ */
609
+ export function readBuiltin(
610
+ words: readonly Word[],
611
+ dir: string | undefined,
612
+ ): readonly Outcome[] | undefined {
613
+ const [program, ...rest] = words;
614
+ if (program === undefined) {
615
+ return undefined;
616
+ }
617
+ return program.value === undefined ? ELSEWHERE : BUILTINS.get(program.value)?.(rest, dir);
618
+ }
@@ -31,7 +31,8 @@ import {
31
31
  manifestAt,
32
32
  readJSON,
33
33
  } from "./fs/manifest.ts";
34
- import { shellWords } from "./text/shell.ts";
34
+ import { readCommands } from "./commands/read.ts";
35
+ import { manifestCommands } from "./manifest.ts";
35
36
 
36
37
  /** A configuration module's name, each form with the spelling a root reports it by. */
37
38
  const MODULE_FORMS: readonly { readonly name: RegExp; readonly form: string }[] = [
@@ -115,45 +116,28 @@ export interface OutsideModule {
115
116
  readonly manifestDirs: readonly string[];
116
117
  }
117
118
 
118
- /** The flags a tool's command line names its configuration file with. */
119
- const CONFIG_FLAGS: ReadonlySet<string> = new Set(["--config", "-c"]);
120
-
121
119
  /** The extensions of a module a configuration flag may name. */
122
120
  const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
123
121
 
124
122
  /**
125
- * The files the scripts of the manifest in one directory name after a configuration flag,
126
- * `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
127
- * the target root and in no `node_modules`.
123
+ * The files the command strings of the manifest in one directory name after a configuration
124
+ * flag, as `commands/read.ts` reads them against the directory each command runs in, each a
125
+ * JSON file or a module below the target root and in no `node_modules`.
128
126
  */
129
127
  function scriptConfigurations(host: Host, root: string, dir: string): readonly string[] {
130
- const scripts = manifestAt(host, dir)?.["scripts"];
131
- if (!isRecord(scripts)) {
132
- return [];
133
- }
128
+ const manifest = manifestAt(host, dir);
134
129
  const found: string[] = [];
135
- for (const command of Object.values(scripts)) {
136
- if (typeof command !== "string") {
137
- continue;
138
- }
139
- const words = shellWords(command);
140
- words.forEach((word, at) => {
141
- const named =
142
- word !== undefined && CONFIG_FLAGS.has(word)
143
- ? words[at + 1]
144
- : word === undefined
145
- ? undefined
146
- : /^--config=(.+)$/su.exec(word)?.[1];
147
- const path = named === undefined ? undefined : resolvePath(dir, named);
130
+ for (const { command } of manifest === undefined ? [] : manifestCommands(host, dir, manifest)) {
131
+ for (const { dir: runs, written } of readCommands(command, dir).configurations) {
132
+ const path = resolvePath(runs, written);
148
133
  if (
149
- path !== undefined &&
150
134
  [".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
151
135
  isProjectPath(root, path) &&
152
136
  host.kindOf(path) === "file"
153
137
  ) {
154
138
  found.push(path);
155
139
  }
156
- });
140
+ }
157
141
  }
158
142
  return found;
159
143
  }
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "6.0.0",
6
6
  result: "pass",
7
- digest: "sha256:0d33564807a356fd3e2760ddaa0645d758bdb9a96ee74a392675e444343bbf20",
7
+ digest: "sha256:58ac31fa90fcbefdbbcbcbd6d2c7a8af6dd4cd883c04d79abfe96f979ffe10e1",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;