@cplieger/deadset-ts 8.0.0-dev.11 → 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 +9 -7
- 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,460 @@
|
|
|
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
|
+
import { resolvePath } from "../paths.js";
|
|
9
|
+
/** The options `words` start with, or `undefined` where one is unhandled or a needed word is dynamic. */
|
|
10
|
+
function optionsOf(words, options) {
|
|
11
|
+
const values = new Map();
|
|
12
|
+
const keep = (name, value) => {
|
|
13
|
+
values.set(name, [...(values.get(name) ?? []), value]);
|
|
14
|
+
};
|
|
15
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
16
|
+
const word = words[at]?.value;
|
|
17
|
+
if (word === undefined) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (word === "--") {
|
|
21
|
+
return { values, at: at + 1, ended: true };
|
|
22
|
+
}
|
|
23
|
+
if (!word.startsWith("-") || options.flags.has(word)) {
|
|
24
|
+
if (!word.startsWith("-")) {
|
|
25
|
+
return { values, at, ended: false };
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const equals = word.startsWith("--") ? word.indexOf("=") : -1;
|
|
30
|
+
const name = equals < 0 ? word : word.slice(0, equals);
|
|
31
|
+
if (options.valued.has(name)) {
|
|
32
|
+
const value = equals < 0 ? words[(at += 1)]?.value : word.slice(equals + 1);
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
keep(name, value);
|
|
37
|
+
}
|
|
38
|
+
else if (options.attached === true &&
|
|
39
|
+
options.valued.has(word.slice(0, 2)) &&
|
|
40
|
+
!word.startsWith("--")) {
|
|
41
|
+
keep(word.slice(0, 2), word.slice(2));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { values, at: words.length, ended: false };
|
|
48
|
+
}
|
|
49
|
+
/** A specifier that starts with a URL scheme, `file:` and `node:` among them. */
|
|
50
|
+
const SCHEME = /^[a-z][a-z\d+.-]*:/iu;
|
|
51
|
+
/** A specifier written as a path: `.`, `..`, or one starting `./`, `../` or `/`. */
|
|
52
|
+
const RELATIVE = /^(?:\.{1,2}(?:\/|$)|\/)/u;
|
|
53
|
+
/**
|
|
54
|
+
* The path a URL's path text names, percent-decoded as Node's `fileURLToPath` does, with a
|
|
55
|
+
* backslash read as the URL parser reads it, or `undefined` where it does not decode or
|
|
56
|
+
* encodes a separator: https://nodejs.org/api/url.html#urlfileurltopathurl-options
|
|
57
|
+
*/
|
|
58
|
+
function urlPath(text) {
|
|
59
|
+
const path = text.replace(/[?#].*$/su, "").replaceAll("\\", "/");
|
|
60
|
+
if (/%2f|%5c/iu.test(path)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent(path);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Where one preload is, read in `dir`: the absolute path of a file, a package's name, or
|
|
72
|
+
* `undefined` where it names neither. `require` reads a path as written; an ECMAScript
|
|
73
|
+
* specifier is a URL, so a relative one or a `file:` one is percent-decoded and any other
|
|
74
|
+
* scheme loads no file: https://nodejs.org/api/esm.html#urls
|
|
75
|
+
*/
|
|
76
|
+
export function preloadTarget(preload, dir) {
|
|
77
|
+
const { specifier, resolution } = preload;
|
|
78
|
+
if (resolution === "require") {
|
|
79
|
+
return RELATIVE.test(specifier)
|
|
80
|
+
? { path: resolvePath(dir, specifier) }
|
|
81
|
+
: SCHEME.test(specifier)
|
|
82
|
+
? undefined
|
|
83
|
+
: { package: specifier };
|
|
84
|
+
}
|
|
85
|
+
if (!RELATIVE.test(specifier) && !SCHEME.test(specifier)) {
|
|
86
|
+
return { package: specifier };
|
|
87
|
+
}
|
|
88
|
+
const local = /^file:(?:\/\/(?=\/))?/iu.exec(specifier);
|
|
89
|
+
const rest = specifier.slice(local?.[0].length ?? 0);
|
|
90
|
+
// `file://host/…` names a file on another host.
|
|
91
|
+
if ((SCHEME.test(specifier) && local === null) || rest.startsWith("//")) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const path = urlPath(rest);
|
|
95
|
+
return path === undefined ? undefined : { path: resolvePath(dir, path) };
|
|
96
|
+
}
|
|
97
|
+
/** A runtime: its preload options, then its entry, and every later word handed to the program. */
|
|
98
|
+
function runtime(preloads, flags, runsNode, subcommand) {
|
|
99
|
+
const options = { valued: new Set(preloads.keys()), flags: new Set(flags) };
|
|
100
|
+
return (words) => {
|
|
101
|
+
const from = subcommand !== undefined && words[0]?.value === subcommand ? 1 : 0;
|
|
102
|
+
const read = optionsOf(words.slice(from), options);
|
|
103
|
+
if (read === undefined) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const entry = words[from + read.at];
|
|
107
|
+
const loads = [...preloads].flatMap(([name, resolution]) => (read.values.get(name) ?? []).map((specifier) => ({ specifier, resolution })));
|
|
108
|
+
if ((entry !== undefined && entry.value === undefined) ||
|
|
109
|
+
loads.some((one) => preloadTarget(one, "/") === undefined)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
kind: "run",
|
|
114
|
+
entry: entry?.value,
|
|
115
|
+
preloads: loads,
|
|
116
|
+
runtime: runsNode && entry !== undefined ? "node" : undefined,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `node [options] [entry] [arguments]`, https://nodejs.org/api/cli.html: the options that load
|
|
122
|
+
* a module before the entry, `-r` as `require` does and the rest as an ECMAScript module.
|
|
123
|
+
* `-e`, `-p` and `-` run text that is no file, so they are unhandled.
|
|
124
|
+
*/
|
|
125
|
+
const node = runtime(new Map([
|
|
126
|
+
["-r", "require"],
|
|
127
|
+
["--require", "require"],
|
|
128
|
+
["--import", "import"],
|
|
129
|
+
["--loader", "import"],
|
|
130
|
+
["--experimental-loader", "import"],
|
|
131
|
+
]), [], true);
|
|
132
|
+
/** `tsx [watch] [options] <file> [arguments]`, https://tsx.is/usage, https://tsx.is/node-enhancement */
|
|
133
|
+
const tsx = runtime(new Map([
|
|
134
|
+
["-r", "require"],
|
|
135
|
+
["--require", "require"],
|
|
136
|
+
["--import", "import"],
|
|
137
|
+
]), [], false, "watch");
|
|
138
|
+
/** `ts-node [options] [script] [arguments]`, https://typestrong.org/ts-node/docs/options; `--esm` picks no file. */
|
|
139
|
+
const tsNode = runtime(new Map([
|
|
140
|
+
["-r", "require"],
|
|
141
|
+
["--require", "require"],
|
|
142
|
+
]), ["--esm"], false);
|
|
143
|
+
/** Hands on the command `words` from `at`, after the assignments `assigns` accepts. */
|
|
144
|
+
function afterAssignments(words, at, assigns) {
|
|
145
|
+
let end = at;
|
|
146
|
+
while (end < words.length && words[end]?.value?.includes("=") === true) {
|
|
147
|
+
if (!assigns(words[end]?.value ?? "")) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
end += 1;
|
|
151
|
+
}
|
|
152
|
+
const program = words[end];
|
|
153
|
+
if (program !== undefined && (program.value === undefined || program.value.startsWith("-"))) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return program === undefined
|
|
157
|
+
? { kind: "run", entry: undefined, preloads: [], runtime: undefined }
|
|
158
|
+
: { kind: "command", words: words.slice(end) };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `env [option]... [name=value]... [command [argument]...]`,
|
|
162
|
+
* https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html: the options that
|
|
163
|
+
* change only the environment, `-i`, `-u` and `-v` in each spelling. GNU env takes any word
|
|
164
|
+
* holding an `=` after its first character as an assignment, and fails before running anything
|
|
165
|
+
* on a `-u` name that is empty or holds an `=`.
|
|
166
|
+
*/
|
|
167
|
+
const env = (words) => {
|
|
168
|
+
const read = optionsOf(words, {
|
|
169
|
+
valued: new Set(["-u", "--unset"]),
|
|
170
|
+
flags: new Set(["-i", "--ignore-environment", "-", "-v", "--debug"]),
|
|
171
|
+
attached: true,
|
|
172
|
+
});
|
|
173
|
+
const unset = [...(read?.values.get("-u") ?? []), ...(read?.values.get("--unset") ?? [])];
|
|
174
|
+
return read === undefined || unset.some((name) => name === "" || name.includes("="))
|
|
175
|
+
? undefined
|
|
176
|
+
: afterAssignments(words, read.at, (value) => !value.startsWith("="));
|
|
177
|
+
};
|
|
178
|
+
/** `cross-env [name=value]... command [argument]...`, https://github.com/kentcdodds/cross-env#usage */
|
|
179
|
+
const crossEnv = (words) => {
|
|
180
|
+
const reading = afterAssignments(words, 0, (value) => /^\w+=/u.test(value));
|
|
181
|
+
return reading?.kind === "command" ? reading : undefined;
|
|
182
|
+
};
|
|
183
|
+
/** The command a package runner hands on: its first word a package's or a command's name. */
|
|
184
|
+
function runnerCommand(words, packaged) {
|
|
185
|
+
const [first, ...rest] = words;
|
|
186
|
+
if (first?.value === undefined || first.value.startsWith("-")) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
// A package specifier names the package's command; a version picks no other command.
|
|
190
|
+
const name = packaged ? first.value.replace(/(?<=.)@[^/@]*$/u, "") : first.value;
|
|
191
|
+
return { kind: "command", words: [{ value: name, text: first.text }, ...rest] };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* npx and npm exec, https://docs.npmjs.com/cli/v11/commands/npx and
|
|
195
|
+
* https://docs.npmjs.com/cli/v11/commands/npm-exec: `--package` (and npx's `-p`), `--call`,
|
|
196
|
+
* `--yes` and `--no`, before the first positional word, which is the package whose command
|
|
197
|
+
* runs unless a `--package` names one. npm exec reads options after it too, unless a `--` ends them.
|
|
198
|
+
*/
|
|
199
|
+
function packageExec(shortPackage, laterOptions) {
|
|
200
|
+
const valued = new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]);
|
|
201
|
+
return (words) => {
|
|
202
|
+
const read = optionsOf(words, { valued, flags: new Set(["-y", "--yes", "--no"]) });
|
|
203
|
+
if (read === undefined) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const operands = words.slice(read.at);
|
|
207
|
+
if (laterOptions &&
|
|
208
|
+
!read.ended &&
|
|
209
|
+
operands.some((word) => word.value?.startsWith("-") !== false)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const call = [...(read.values.get("-c") ?? []), ...(read.values.get("--call") ?? [])];
|
|
213
|
+
if (call.length > 0) {
|
|
214
|
+
return call.length === 1 && operands.length === 0 && call[0] !== undefined
|
|
215
|
+
? { kind: "line", text: call[0] }
|
|
216
|
+
: undefined;
|
|
217
|
+
}
|
|
218
|
+
const packages = [...(read.values.get("--package") ?? []), ...(read.values.get("-p") ?? [])];
|
|
219
|
+
return runnerCommand(operands, packages.length === 0);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const npx = packageExec(true, false);
|
|
223
|
+
const npmExec = packageExec(false, true);
|
|
224
|
+
/**
|
|
225
|
+
* `npm run <script> [-- <arguments>]`, https://docs.npmjs.com/cli/v11/commands/npm-run-script:
|
|
226
|
+
* it runs another script of the same manifest, which is read on its own.
|
|
227
|
+
*/
|
|
228
|
+
const npmRun = (words) => {
|
|
229
|
+
const [script, ...rest] = words;
|
|
230
|
+
const ended = rest.findIndex((word) => word.value === "--");
|
|
231
|
+
const before = ended < 0 ? rest : rest.slice(0, ended);
|
|
232
|
+
if (script?.value === undefined ||
|
|
233
|
+
script.value.startsWith("-") ||
|
|
234
|
+
before.some((word) => word.value?.startsWith("-") !== false)) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return { kind: "run", entry: undefined, preloads: [], runtime: undefined };
|
|
238
|
+
};
|
|
239
|
+
/** A tool whose second word picks the reader of the rest. */
|
|
240
|
+
function subcommands(readers) {
|
|
241
|
+
return ([first, ...rest]) => first?.value === undefined ? undefined : readers.get(first.value)?.(rest);
|
|
242
|
+
}
|
|
243
|
+
/** `npm exec`, `npm x` and `npm run` with `run-script`. */
|
|
244
|
+
const npm = subcommands(new Map([
|
|
245
|
+
["exec", npmExec],
|
|
246
|
+
["x", npmExec],
|
|
247
|
+
["run", npmRun],
|
|
248
|
+
["run-script", npmRun],
|
|
249
|
+
]));
|
|
250
|
+
/**
|
|
251
|
+
* `pnpm exec <command>`, https://pnpm.io/cli/exec, which hands on every later word, and
|
|
252
|
+
* `pnpm dlx [--package <name>]... <command>`, https://pnpm.io/cli/dlx.
|
|
253
|
+
*/
|
|
254
|
+
const pnpm = subcommands(new Map([
|
|
255
|
+
["exec", (words) => runnerCommand(words, false)],
|
|
256
|
+
[
|
|
257
|
+
"dlx",
|
|
258
|
+
(words) => {
|
|
259
|
+
const read = optionsOf(words, { valued: new Set(["--package"]), flags: new Set() });
|
|
260
|
+
return read === undefined
|
|
261
|
+
? undefined
|
|
262
|
+
: runnerCommand(words.slice(read.at), !read.values.has("--package"));
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
]));
|
|
266
|
+
/**
|
|
267
|
+
* `yarn node`, https://yarnpkg.com/cli/node, and `yarn dlx [-p <name>]... <command>`,
|
|
268
|
+
* https://yarnpkg.com/cli/dlx, whose command runs in the current directory.
|
|
269
|
+
*/
|
|
270
|
+
const yarn = subcommands(new Map([
|
|
271
|
+
["node", (words) => ({ kind: "command", words: [{ value: "node", text: "node" }, ...words] })],
|
|
272
|
+
[
|
|
273
|
+
"dlx",
|
|
274
|
+
(words) => {
|
|
275
|
+
const read = optionsOf(words, {
|
|
276
|
+
valued: new Set(["-p", "--package"]),
|
|
277
|
+
flags: new Set(["-q", "--quiet"]),
|
|
278
|
+
});
|
|
279
|
+
return read === undefined
|
|
280
|
+
? undefined
|
|
281
|
+
: runnerCommand(words.slice(read.at), !read.values.has("-p") && !read.values.has("--package"));
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
]));
|
|
285
|
+
/** `bunx [-p <name>] <command>[@version] [arguments]`, https://bun.com/docs/pm/bunx */
|
|
286
|
+
const bunx = (words) => {
|
|
287
|
+
const read = optionsOf(words, { valued: new Set(["-p", "--package"]), flags: new Set() });
|
|
288
|
+
return read === undefined ? undefined : runnerCommand(words.slice(read.at), true);
|
|
289
|
+
};
|
|
290
|
+
/** The row of a tool whose reader is `read`. */
|
|
291
|
+
function reading(read) {
|
|
292
|
+
return { read };
|
|
293
|
+
}
|
|
294
|
+
/** The row of a tool that names its configuration file by `options` anywhere before a `--`. */
|
|
295
|
+
function configuredBy(...options) {
|
|
296
|
+
return { configurations: { valued: new Set(options) } };
|
|
297
|
+
}
|
|
298
|
+
/** The row of each tool, by its program word: its name, or a path to it in `node_modules/.bin`. */
|
|
299
|
+
const TOOLS = new Map([
|
|
300
|
+
["node", reading(node)],
|
|
301
|
+
["tsx", reading(tsx)],
|
|
302
|
+
["ts-node", reading(tsNode)],
|
|
303
|
+
["env", reading(env)],
|
|
304
|
+
["cross-env", reading(crossEnv)],
|
|
305
|
+
[
|
|
306
|
+
"npx",
|
|
307
|
+
{
|
|
308
|
+
read: npx,
|
|
309
|
+
// `-c` takes a command line, which is read as a configuration file's name as well, so
|
|
310
|
+
// every configuration `fixtures/commands/observed.json` records stays named.
|
|
311
|
+
configurations: {
|
|
312
|
+
valued: new Set(["-c"]),
|
|
313
|
+
beforeOperand: new Set(["--package", "-p", "--call"]),
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
["npm", reading(npm)],
|
|
318
|
+
["pnpm", reading(pnpm)],
|
|
319
|
+
["yarn", reading(yarn)],
|
|
320
|
+
["bunx", reading(bunx)],
|
|
321
|
+
// https://eslint.org/docs/latest/use/command-line-interface
|
|
322
|
+
["eslint", configuredBy("-c", "--config")],
|
|
323
|
+
// https://prettier.io/docs/cli, whose `-c` is `--check`.
|
|
324
|
+
["prettier", configuredBy("--config")],
|
|
325
|
+
// https://stylelint.io/user-guide/cli
|
|
326
|
+
["stylelint", configuredBy("-c", "--config")],
|
|
327
|
+
// https://jestjs.io/docs/cli
|
|
328
|
+
["jest", configuredBy("-c", "--config")],
|
|
329
|
+
// https://vitest.dev/guide/cli
|
|
330
|
+
["vitest", configuredBy("-c", "--config")],
|
|
331
|
+
// https://vite.dev/guide/cli
|
|
332
|
+
["vite", configuredBy("-c", "--config")],
|
|
333
|
+
// https://webpack.js.org/api/cli/
|
|
334
|
+
["webpack", configuredBy("-c", "--config")],
|
|
335
|
+
// https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
|
|
336
|
+
["mocha", configuredBy("--config")],
|
|
337
|
+
]);
|
|
338
|
+
/** The row of a simple command's program word, by its name or a path to it in `node_modules/.bin`. */
|
|
339
|
+
function rowOf(program) {
|
|
340
|
+
const name = program?.value === undefined
|
|
341
|
+
? undefined
|
|
342
|
+
: /^(?:.*\/)?node_modules\/\.bin\/([^/]+)$|^([^/]+)$/u.exec(program.value);
|
|
343
|
+
const tool = name?.[1] ?? name?.[2];
|
|
344
|
+
return tool === undefined ? undefined : TOOLS.get(tool);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* What the reader of one simple command's program reads it as, or `undefined` where none
|
|
348
|
+
* understands it. A program word that is any other path runs that file, whatever its name.
|
|
349
|
+
*/
|
|
350
|
+
export function readTool(words) {
|
|
351
|
+
const [program, ...rest] = words;
|
|
352
|
+
return rowOf(program)?.read?.(rest);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The configuration files one simple command names, as written: each static value of an option
|
|
356
|
+
* its program's row names a configuration file by, and none for a program with no such row.
|
|
357
|
+
*/
|
|
358
|
+
export function configurationsOf(words) {
|
|
359
|
+
const [program, ...rest] = words;
|
|
360
|
+
const options = rowOf(program)?.configurations;
|
|
361
|
+
const found = [];
|
|
362
|
+
for (let at = 0; options !== undefined && at < rest.length; at += 1) {
|
|
363
|
+
const word = rest[at]?.value;
|
|
364
|
+
if (word === "--" || (word === undefined && options.beforeOperand !== undefined)) {
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
const equals = word?.startsWith("--") === true ? word.indexOf("=") : -1;
|
|
368
|
+
const name = equals < 0 ? word : word?.slice(0, equals);
|
|
369
|
+
if (name !== undefined && options.valued.has(name)) {
|
|
370
|
+
const value = equals < 0 ? rest[(at += 1)]?.value : word?.slice(equals + 1);
|
|
371
|
+
found.push(...(value === undefined || value === "" ? [] : [value]));
|
|
372
|
+
}
|
|
373
|
+
else if (options.beforeOperand !== undefined && name !== undefined) {
|
|
374
|
+
if (!name.startsWith("-")) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
at += options.beforeOperand.has(name) && equals < 0 ? 1 : 0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return found;
|
|
381
|
+
}
|
|
382
|
+
/** Either status, in a directory the analysis does not know. */
|
|
383
|
+
const ELSEWHERE = [
|
|
384
|
+
{ dir: undefined, ok: true },
|
|
385
|
+
{ dir: undefined, ok: false },
|
|
386
|
+
];
|
|
387
|
+
const elsewhere = () => ELSEWHERE;
|
|
388
|
+
/** The directory a builtin's one operand states, where it is static and starts with no character of `options`. */
|
|
389
|
+
function statedDirectory(operands, options) {
|
|
390
|
+
const [dir] = operands;
|
|
391
|
+
return operands.length === 1 && dir?.value !== undefined && !options.includes(dir.value.charAt(0))
|
|
392
|
+
? dir.value
|
|
393
|
+
: undefined;
|
|
394
|
+
}
|
|
395
|
+
/** `cd` to a stated directory is taken to enter it, as `roots.13` states. */
|
|
396
|
+
const cd = (operands, dir) => {
|
|
397
|
+
const to = statedDirectory(operands, "-");
|
|
398
|
+
return to === undefined
|
|
399
|
+
? ELSEWHERE
|
|
400
|
+
: [{ dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true }];
|
|
401
|
+
};
|
|
402
|
+
/** `pushd` enters a stated directory, or fails and leaves the shell where it was. */
|
|
403
|
+
const pushd = (operands, dir) => {
|
|
404
|
+
const to = statedDirectory(operands, "-+");
|
|
405
|
+
return to === undefined
|
|
406
|
+
? ELSEWHERE
|
|
407
|
+
: [
|
|
408
|
+
{ dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true },
|
|
409
|
+
{ dir, ok: false },
|
|
410
|
+
];
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* `exec` with a command replaces the shell, which exits where it cannot; with none it only
|
|
414
|
+
* redirects: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exec
|
|
415
|
+
*/
|
|
416
|
+
const exec = ([command]) => {
|
|
417
|
+
if (command === undefined) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
return command.value === undefined || command.value.startsWith("-") ? ELSEWHERE : [];
|
|
421
|
+
};
|
|
422
|
+
/** `exit` ends the shell whatever its operands, in dash and in bash. */
|
|
423
|
+
const exit = () => [];
|
|
424
|
+
/** The builtins that run another command in the same shell, where that one may be any builtin. */
|
|
425
|
+
const runsBuiltin = (operands) => operands.some((word) => word.value === undefined || BUILTINS.has(word.value))
|
|
426
|
+
? ELSEWHERE
|
|
427
|
+
: undefined;
|
|
428
|
+
/**
|
|
429
|
+
* The shell builtins that may move or end the shell running them: `cd`,
|
|
430
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cd.html; `pushd` and `popd`,
|
|
431
|
+
* https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html; `exit`,
|
|
432
|
+
* `exec`, and `return`, whose effect outside a function POSIX leaves unspecified,
|
|
433
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#return; and `.`,
|
|
434
|
+
* `eval`, `source`, `command` and `builtin`, which run other text or another builtin.
|
|
435
|
+
*/
|
|
436
|
+
const BUILTINS = new Map([
|
|
437
|
+
["cd", cd],
|
|
438
|
+
["pushd", pushd],
|
|
439
|
+
["popd", elsewhere],
|
|
440
|
+
["exit", exit],
|
|
441
|
+
["exec", exec],
|
|
442
|
+
["return", elsewhere],
|
|
443
|
+
[".", elsewhere],
|
|
444
|
+
["source", elsewhere],
|
|
445
|
+
["eval", elsewhere],
|
|
446
|
+
["command", runsBuiltin],
|
|
447
|
+
["builtin", runsBuiltin],
|
|
448
|
+
]);
|
|
449
|
+
/**
|
|
450
|
+
* Each way the shell running one simple command in its own environment, in `dir`, goes on
|
|
451
|
+
* after a builtin that may move or end it, or `undefined` where the command is no such
|
|
452
|
+
* builtin. A program word the shell computes may be any builtin.
|
|
453
|
+
*/
|
|
454
|
+
export function readBuiltin(words, dir) {
|
|
455
|
+
const [program, ...rest] = words;
|
|
456
|
+
if (program === undefined) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
return program.value === undefined ? ELSEWHERE : BUILTINS.get(program.value)?.(rest, dir);
|
|
460
|
+
}
|
|
@@ -12,7 +12,8 @@ import { bytewise } from "./text/compare.js";
|
|
|
12
12
|
import { isRecord } from "./json-document.js";
|
|
13
13
|
import { filesIn } from "./fs/tree.js";
|
|
14
14
|
import { DEPENDENCY_SECTIONS, holdsManifest, inInstalledPackages, manifestAt, readJSON, } from "./fs/manifest.js";
|
|
15
|
-
import {
|
|
15
|
+
import { readCommands } from "./commands/read.js";
|
|
16
|
+
import { manifestCommands } from "./manifest.js";
|
|
16
17
|
/** A configuration module's name, each form with the spelling a root reports it by. */
|
|
17
18
|
const MODULE_FORMS = [
|
|
18
19
|
{ name: /^[\w-]+\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u, form: "<stem>.config.<ext>" },
|
|
@@ -64,40 +65,25 @@ export function configurationModuleForm(name) {
|
|
|
64
65
|
export function isConfigurationDocumentName(name) {
|
|
65
66
|
return DOCUMENT_NAME.test(name);
|
|
66
67
|
}
|
|
67
|
-
/** The flags a tool's command line names its configuration file with. */
|
|
68
|
-
const CONFIG_FLAGS = new Set(["--config", "-c"]);
|
|
69
68
|
/** The extensions of a module a configuration flag may name. */
|
|
70
69
|
const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
71
70
|
/**
|
|
72
|
-
* The files the
|
|
73
|
-
*
|
|
74
|
-
* the target root and in no `node_modules`.
|
|
71
|
+
* The files the command strings of the manifest in one directory name after a configuration
|
|
72
|
+
* flag, as `commands/read.ts` reads them against the directory each command runs in, each a
|
|
73
|
+
* JSON file or a module below the target root and in no `node_modules`.
|
|
75
74
|
*/
|
|
76
75
|
function scriptConfigurations(host, root, dir) {
|
|
77
|
-
const
|
|
78
|
-
if (!isRecord(scripts)) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
76
|
+
const manifest = manifestAt(host, dir);
|
|
81
77
|
const found = [];
|
|
82
|
-
for (const command of
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const words = shellWords(command);
|
|
87
|
-
words.forEach((word, at) => {
|
|
88
|
-
const named = word !== undefined && CONFIG_FLAGS.has(word)
|
|
89
|
-
? words[at + 1]
|
|
90
|
-
: word === undefined
|
|
91
|
-
? undefined
|
|
92
|
-
: /^--config=(.+)$/su.exec(word)?.[1];
|
|
93
|
-
const path = named === undefined ? undefined : resolvePath(dir, named);
|
|
94
|
-
if (path !== undefined &&
|
|
95
|
-
[".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
78
|
+
for (const { command } of manifest === undefined ? [] : manifestCommands(host, dir, manifest)) {
|
|
79
|
+
for (const { dir: runs, written } of readCommands(command, dir).configurations) {
|
|
80
|
+
const path = resolvePath(runs, written);
|
|
81
|
+
if ([".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
96
82
|
isProjectPath(root, path) &&
|
|
97
83
|
host.kindOf(path) === "file") {
|
|
98
84
|
found.push(path);
|
|
99
85
|
}
|
|
100
|
-
}
|
|
86
|
+
}
|
|
101
87
|
}
|
|
102
88
|
return found;
|
|
103
89
|
}
|
|
@@ -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
|
};
|