@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.
- package/README.md +2 -0
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/src/build-scripts.js +6 -13
- package/dist/src/commands/read.js +156 -0
- package/dist/src/commands/shell.js +75 -0
- package/dist/src/commands/tools.js +460 -0
- package/dist/src/configuration-files.js +11 -25
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/dependencies.js +67 -115
- package/dist/src/load/synthesize.js +12 -5
- package/dist/src/manifest.js +27 -14
- package/dist/src/roots.js +18 -11
- package/dist/src/text/named-files.js +24 -3
- package/dist/src/text/yaml.js +464 -173
- package/dist/src/vendor/unbash/ansi-c.js +132 -0
- package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
- package/dist/src/vendor/unbash/arithmetic.js +591 -0
- package/dist/src/vendor/unbash/assignment.js +131 -0
- package/dist/src/vendor/unbash/chars.js +48 -0
- package/dist/src/vendor/unbash/command.js +41 -0
- package/dist/src/vendor/unbash/expansion.js +80 -0
- package/dist/src/vendor/unbash/glob.js +268 -0
- package/dist/src/vendor/unbash/heredoc.js +27 -0
- package/dist/src/vendor/unbash/internal-types.js +1 -0
- package/dist/src/vendor/unbash/lexer.js +4274 -0
- package/dist/src/vendor/unbash/limits.js +3 -0
- package/dist/src/vendor/unbash/parser.js +1366 -0
- package/dist/src/vendor/unbash/parts.js +57 -0
- package/dist/src/vendor/unbash/substitution-context.js +8 -0
- package/dist/src/vendor/unbash/traversal.js +212 -0
- package/dist/src/vendor/unbash/types.js +1 -0
- package/dist/src/vendor/unbash/word-value.js +61 -0
- package/dist/src/vendor/unbash/word.js +40 -0
- package/dist/src/workflow-steps.js +111 -133
- package/jsr.json +2 -1
- package/package.json +4 -2
- package/src/build-scripts.ts +6 -13
- package/src/commands/read.ts +230 -0
- package/src/commands/shell.ts +126 -0
- package/src/commands/tools.ts +618 -0
- package/src/configuration-files.ts +10 -26
- package/src/conformance-record.ts +1 -1
- package/src/dependencies.ts +99 -123
- package/src/load/synthesize.ts +18 -5
- package/src/manifest.ts +43 -25
- package/src/roots.ts +20 -13
- package/src/text/named-files.ts +26 -3
- package/src/text/yaml.ts +503 -179
- package/src/vendor/unbash/LICENSE +15 -0
- package/src/vendor/unbash/VENDORED.md +16 -0
- package/src/vendor/unbash/ansi-c.ts +132 -0
- package/src/vendor/unbash/arithmetic-parts.ts +53 -0
- package/src/vendor/unbash/arithmetic.ts +641 -0
- package/src/vendor/unbash/assignment.ts +163 -0
- package/src/vendor/unbash/chars.ts +48 -0
- package/src/vendor/unbash/command.ts +47 -0
- package/src/vendor/unbash/expansion.ts +144 -0
- package/src/vendor/unbash/glob.ts +284 -0
- package/src/vendor/unbash/heredoc.ts +34 -0
- package/src/vendor/unbash/internal-types.ts +94 -0
- package/src/vendor/unbash/lexer.ts +4352 -0
- package/src/vendor/unbash/limits.ts +3 -0
- package/src/vendor/unbash/parser.ts +1612 -0
- package/src/vendor/unbash/parts.ts +96 -0
- package/src/vendor/unbash/substitution-context.ts +20 -0
- package/src/vendor/unbash/traversal.ts +182 -0
- package/src/vendor/unbash/types.ts +660 -0
- package/src/vendor/unbash/word-value.ts +64 -0
- package/src/vendor/unbash/word.ts +50 -0
- package/src/workflow-steps.ts +153 -154
- package/dist/src/text/shell.js +0 -159
- package/src/text/named-files.inputs.json +0 -1522
- 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 {
|
|
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
|
|
126
|
-
*
|
|
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
|
|
131
|
-
if (!isRecord(scripts)) {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
128
|
+
const manifest = manifestAt(host, dir);
|
|
134
129
|
const found: string[] = [];
|
|
135
|
-
for (const command of
|
|
136
|
-
|
|
137
|
-
|
|
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:
|
|
7
|
+
digest: "sha256:58ac31fa90fcbefdbbcbcbd6d2c7a8af6dd4cd883c04d79abfe96f979ffe10e1",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|