@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,50 @@
|
|
|
1
|
+
// Code generated by scripts/vendor-unbash.ts from unbash 6.0.0 (968c14992a43dc06ce29c9f51858abd4a87851ea). DO NOT EDIT.
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import type { Word, WordPart } from "./internal-types.ts";
|
|
4
|
+
import { unescapeBareValue, wordPartsValue } from "./word-value.ts";
|
|
5
|
+
|
|
6
|
+
export type PartsResolver = (source: string, word: Word, depth: number) => WordPart[] | undefined;
|
|
7
|
+
|
|
8
|
+
export class WordImpl implements Word {
|
|
9
|
+
type = "Word" as const;
|
|
10
|
+
text: string;
|
|
11
|
+
pos: number;
|
|
12
|
+
end: number;
|
|
13
|
+
#source: string | undefined;
|
|
14
|
+
#resolver: PartsResolver | undefined;
|
|
15
|
+
#depth: number;
|
|
16
|
+
#parts: WordPart[] | undefined | null;
|
|
17
|
+
#value: string | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(text: string, pos: number, end: number, source?: string, resolver?: PartsResolver, depth = 0) {
|
|
20
|
+
this.text = text;
|
|
21
|
+
this.pos = pos;
|
|
22
|
+
this.end = end;
|
|
23
|
+
this.#source = source;
|
|
24
|
+
this.#resolver = resolver;
|
|
25
|
+
this.#depth = depth;
|
|
26
|
+
this.#parts = resolver === undefined ? undefined : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get value(): string {
|
|
30
|
+
if (this.#value === null) {
|
|
31
|
+
const parts = this.#source === undefined && this.#parts === null ? undefined : this.parts;
|
|
32
|
+
this.#value = parts ? wordPartsValue(parts) : unescapeBareValue(this.text);
|
|
33
|
+
}
|
|
34
|
+
return this.#value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get parts(): WordPart[] | undefined {
|
|
38
|
+
if (this.#parts === null) {
|
|
39
|
+
this.#parts = this.#resolver?.(this.#source ?? "", this, this.#depth) ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
return this.#parts;
|
|
42
|
+
}
|
|
43
|
+
set parts(v: WordPart[] | undefined) {
|
|
44
|
+
this.#parts = v ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
toJSON() {
|
|
48
|
+
return { type: this.type, text: this.text, pos: this.pos, end: this.end, parts: this.parts, value: this.value };
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/workflow-steps.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The command strings beside a target root's packages: a CI workflow's `run:` steps, the git
|
|
3
|
+
* hooks, the hook managers' commands and the makefile's recipe lines, each with the directory
|
|
4
|
+
* it runs in. A workflow or hook manager file is read by `text/yaml.ts`, and one it does not
|
|
5
|
+
* read gives no command; each command is read by `commands/read.ts`.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { SourceFile } from "@typescript/native/ast";
|
|
9
|
+
import { pathsNamed, readCommands } from "./commands/read.ts";
|
|
8
10
|
import type { Host } from "./host.ts";
|
|
9
|
-
import {
|
|
11
|
+
import { isRecord } from "./json-document.ts";
|
|
12
|
+
import { joinPath, resolvePath } from "./paths.ts";
|
|
10
13
|
import type { SourceFiles } from "./source-files.ts";
|
|
11
14
|
import { filesIn } from "./fs/tree.ts";
|
|
12
|
-
import {
|
|
15
|
+
import { yamlDocument } from "./text/yaml.ts";
|
|
13
16
|
|
|
14
|
-
/** One file a workflow step names. */
|
|
17
|
+
/** One file a workflow step, git hook or recipe runs or names. */
|
|
15
18
|
interface WorkflowEntry {
|
|
16
19
|
readonly file: SourceFile;
|
|
17
|
-
/** The
|
|
20
|
+
/** The word or the path that named it, as the command writes it. */
|
|
18
21
|
readonly source: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
/** One command string and the directory it runs in. */
|
|
25
|
+
interface CommandString {
|
|
26
|
+
readonly text: string;
|
|
27
|
+
/**
|
|
28
|
+
* The directory it runs in, absolute, or `undefined` where the file does not state what runs
|
|
29
|
+
* where: a directory holding a `${{ }}` expression, which GitHub Actions evaluates.
|
|
30
|
+
*/
|
|
31
|
+
readonly dir: string | undefined;
|
|
32
|
+
/** The directory the file writes for it, as YAML decodes it, which layer 3 holds beside the text. */
|
|
33
|
+
readonly writtenDirectory: string | undefined;
|
|
34
|
+
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
|
|
36
|
+
const WORKFLOWS = ".github/workflows";
|
|
25
37
|
|
|
26
38
|
/** The directories whose every file is a git hook script, run as the shell runs it. */
|
|
27
39
|
const HOOK_DIRS = [".husky", ".githooks"] as const;
|
|
28
40
|
|
|
29
|
-
/**
|
|
30
|
-
const WORKING_DIRECTORY =
|
|
31
|
-
|
|
32
|
-
const LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml"] as const;
|
|
41
|
+
/** The key setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
|
|
42
|
+
const WORKING_DIRECTORY = "working-directory";
|
|
33
43
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const HOOK_MANAGERS: readonly (readonly [string,
|
|
39
|
-
[".pre-commit-config.yaml",
|
|
40
|
-
...
|
|
44
|
+
/**
|
|
45
|
+
* The hook manager files, each with the key its commands are written under and the key of their
|
|
46
|
+
* directory: lefthook's `root`, https://lefthook.dev/configuration/root/
|
|
47
|
+
*/
|
|
48
|
+
const HOOK_MANAGERS: readonly (readonly [string, string, string | undefined])[] = [
|
|
49
|
+
[".pre-commit-config.yaml", "entry", undefined],
|
|
50
|
+
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map(
|
|
51
|
+
(name) => [name, "run", "root"] as const,
|
|
52
|
+
),
|
|
41
53
|
];
|
|
42
54
|
|
|
43
55
|
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
44
56
|
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"] as const;
|
|
45
57
|
|
|
46
|
-
const BLOCK_SCALAR = /^[|>][+-]?$/u;
|
|
47
|
-
|
|
48
|
-
/** A token holding one of these is computed or a pattern, so it names no file. */
|
|
49
|
-
const COMPUTED = /[${*?[]/u;
|
|
50
|
-
|
|
51
|
-
const INDENT = /^\s*/u;
|
|
52
|
-
|
|
53
|
-
/** The files directly in one directory whose name `accepts`, by path in name order. */
|
|
54
|
-
function pathsIn(host: Host, dir: string, accepts: (name: string) => boolean): readonly string[] {
|
|
55
|
-
return filesIn(host, dir, accepts).map((name) => joinPath(dir, name));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
58
|
function readText(host: Host, path: string): string | undefined {
|
|
59
59
|
try {
|
|
60
60
|
return host.readFile(path);
|
|
@@ -63,138 +63,150 @@ function readText(host: Host, path: string): string | undefined {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const match = key.exec(lines[at] ?? "");
|
|
72
|
-
if (match === null) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const column = (match[1] ?? "").length;
|
|
76
|
-
const value = unquoted((match[2] ?? "").trim());
|
|
77
|
-
if (!BLOCK_SCALAR.test(value)) {
|
|
78
|
-
found.push(value);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
for (let next = at + 1; next < lines.length; next += 1) {
|
|
82
|
-
const line = lines[next] ?? "";
|
|
83
|
-
if (line.trim() !== "" && (INDENT.exec(line)?.[0].length ?? 0) <= column) {
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
found.push(line);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return found;
|
|
66
|
+
/** The text a YAML scalar's value stands for, or `undefined` for a collection or a null. */
|
|
67
|
+
function scalarText(value: unknown): string | undefined {
|
|
68
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
69
|
+
? String(value)
|
|
70
|
+
: undefined;
|
|
90
71
|
}
|
|
91
72
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
function recipeLines(host: Host, root: string): readonly (readonly string[])[] {
|
|
98
|
-
const path = MAKEFILES.map((name) => joinPath(root, name)).find(
|
|
99
|
-
(candidate) => host.kindOf(candidate) === "file",
|
|
100
|
-
);
|
|
101
|
-
const text = path === undefined ? undefined : readText(host, path);
|
|
102
|
-
if (text === undefined) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
return text
|
|
106
|
-
.replace(/\\\r?\n/gu, " ")
|
|
107
|
-
.split(/\r?\n/u)
|
|
108
|
-
.filter((line) => line.startsWith("\t"))
|
|
109
|
-
.map((line) => commandsOf(withoutComment(line.slice(1).replace(/^[@+-]+/u, ""))));
|
|
73
|
+
/** The `defaults.run.working-directory` one mapping writes, as YAML decodes it. */
|
|
74
|
+
function defaultDirectory(mapping: Record<string, unknown>): string | undefined {
|
|
75
|
+
const defaults = mapping["defaults"];
|
|
76
|
+
const run = isRecord(defaults) ? defaults["run"] : undefined;
|
|
77
|
+
return isRecord(run) ? scalarText(run[WORKING_DIRECTORY]) : undefined;
|
|
110
78
|
}
|
|
111
79
|
|
|
112
80
|
/**
|
|
113
|
-
* Every
|
|
114
|
-
*
|
|
115
|
-
*
|
|
81
|
+
* Every command one workflow or hook manager file writes under `key`, in every mapping of the
|
|
82
|
+
* document, each with the directory it runs in: its mapping's `dirKey` value, else, in a workflow
|
|
83
|
+
* file, the innermost `defaults.run.working-directory` of a mapping holding it, else `root`. A
|
|
84
|
+
* file `yamlDocument` does not read gives none.
|
|
116
85
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (const token of tokens) {
|
|
129
|
-
found.push({ token, dir: relativePath(root, dir) ?? "" });
|
|
86
|
+
function keyedCommands(
|
|
87
|
+
text: string,
|
|
88
|
+
root: string,
|
|
89
|
+
key: string,
|
|
90
|
+
dirKey: string | undefined,
|
|
91
|
+
): readonly CommandString[] {
|
|
92
|
+
const found: CommandString[] = [];
|
|
93
|
+
const visit = (node: unknown, inherited: string | undefined): void => {
|
|
94
|
+
if (Array.isArray(node)) {
|
|
95
|
+
for (const item of node) {
|
|
96
|
+
visit(item, inherited);
|
|
130
97
|
}
|
|
98
|
+
return;
|
|
131
99
|
}
|
|
132
|
-
|
|
100
|
+
if (!isRecord(node)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const here = (dirKey === WORKING_DIRECTORY ? defaultDirectory(node) : undefined) ?? inherited;
|
|
104
|
+
const command = scalarText(node[key]);
|
|
105
|
+
if (command !== undefined && command.trim() !== "") {
|
|
106
|
+
const written = (dirKey === undefined ? undefined : scalarText(node[dirKey])) ?? here;
|
|
107
|
+
found.push({
|
|
108
|
+
text: command,
|
|
109
|
+
dir:
|
|
110
|
+
written === undefined
|
|
111
|
+
? root
|
|
112
|
+
: written.includes("${{")
|
|
113
|
+
? undefined
|
|
114
|
+
: resolvePath(root, written),
|
|
115
|
+
writtenDirectory: written,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const value of Object.values(node)) {
|
|
119
|
+
visit(value, here);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
visit(yamlDocument(text), undefined);
|
|
133
123
|
return found;
|
|
134
124
|
}
|
|
135
125
|
|
|
126
|
+
/** The path of each git hook script at `root`, every file in {@link HOOK_DIRS}. */
|
|
127
|
+
function hookScripts(host: Host, root: string): readonly string[] {
|
|
128
|
+
return HOOK_DIRS.flatMap((dir) =>
|
|
129
|
+
filesIn(host, joinPath(root, dir), () => true).map((name) => joinPath(root, dir, name)),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The makefile make reads at `root`, the first of {@link MAKEFILES} there. */
|
|
134
|
+
function makefileAt(host: Host, root: string): string | undefined {
|
|
135
|
+
return MAKEFILES.map((name) => joinPath(root, name)).find(
|
|
136
|
+
(candidate) => host.kindOf(candidate) === "file",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* of a lefthook file and the makefile's recipe lines.
|
|
141
|
+
* Each recipe line of the target root's makefile: a line that starts with a tab, joined with
|
|
142
|
+
* the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it.
|
|
143
|
+
* Make runs each line in a shell of its own.
|
|
141
144
|
*/
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
+
function recipeLines(host: Host, root: string): readonly CommandString[] {
|
|
146
|
+
const path = makefileAt(host, root);
|
|
147
|
+
const text = path === undefined ? undefined : readText(host, path);
|
|
148
|
+
return (text ?? "")
|
|
149
|
+
.replace(/\\\r?\n/gu, " ")
|
|
150
|
+
.split(/\r?\n/u)
|
|
151
|
+
.filter((line) => line.startsWith("\t"))
|
|
152
|
+
.map((line) => ({
|
|
153
|
+
text: line.slice(1).replace(/^[@+-]+/u, ""),
|
|
154
|
+
dir: root,
|
|
155
|
+
writtenDirectory: undefined,
|
|
156
|
+
}));
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
/**
|
|
148
|
-
* Every
|
|
149
|
-
*
|
|
160
|
+
* Every command the target root's workflow steps, git hooks, hook managers and makefile run:
|
|
161
|
+
* the `run:` steps of the workflow files in `.github/workflows`, each file in `.husky` and
|
|
162
|
+
* `.githooks` as one shell script, the `entry:` commands of `.pre-commit-config.yaml`, the
|
|
163
|
+
* `run:` commands of a lefthook file and the makefile's recipe lines.
|
|
150
164
|
*/
|
|
151
|
-
export function
|
|
165
|
+
export function workflowCommands(host: Host, targetRoot: string): readonly CommandString[] {
|
|
152
166
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
167
|
+
const workflows = filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name));
|
|
153
168
|
return [
|
|
154
|
-
...
|
|
155
|
-
|
|
169
|
+
...workflows.flatMap((name) =>
|
|
170
|
+
keyedCommands(
|
|
171
|
+
readText(host, joinPath(root, WORKFLOWS, name)) ?? "",
|
|
172
|
+
root,
|
|
173
|
+
"run",
|
|
174
|
+
WORKING_DIRECTORY,
|
|
175
|
+
),
|
|
156
176
|
),
|
|
157
|
-
...
|
|
158
|
-
|
|
177
|
+
...hookScripts(host, root).map((path): CommandString => ({
|
|
178
|
+
text: readText(host, path) ?? "",
|
|
179
|
+
dir: root,
|
|
180
|
+
writtenDirectory: undefined,
|
|
181
|
+
})),
|
|
182
|
+
...HOOK_MANAGERS.flatMap(([name, key, dirKey]) =>
|
|
183
|
+
keyedCommands(readText(host, joinPath(root, name)) ?? "", root, key, dirKey),
|
|
159
184
|
),
|
|
185
|
+
...recipeLines(host, root),
|
|
160
186
|
];
|
|
161
187
|
}
|
|
162
188
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
export function
|
|
189
|
+
/**
|
|
190
|
+
* The whole text of each file {@link workflowCommands} reads a command from, each workflow file,
|
|
191
|
+
* git hook, hook manager file and makefile at the target root. Layer 3 holds it whole, so a part
|
|
192
|
+
* no command is read from, a comment included, or one the reading of the structure misses, still
|
|
193
|
+
* holds every file it names.
|
|
194
|
+
*/
|
|
195
|
+
export function commandFileTexts(host: Host, targetRoot: string): readonly string[] {
|
|
170
196
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
171
197
|
return [
|
|
172
|
-
...
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const commands: string[] = [];
|
|
180
|
-
for (const path of workflowFiles(host, root)) {
|
|
181
|
-
commands.push(...runCommands(readText(host, path) ?? ""));
|
|
182
|
-
}
|
|
183
|
-
for (const dir of HOOK_DIRS) {
|
|
184
|
-
for (const path of pathsIn(host, joinPath(root, dir), () => true)) {
|
|
185
|
-
commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
for (const [name, key] of HOOK_MANAGERS) {
|
|
189
|
-
commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
|
|
190
|
-
}
|
|
191
|
-
return commands;
|
|
198
|
+
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) =>
|
|
199
|
+
joinPath(root, WORKFLOWS, name),
|
|
200
|
+
),
|
|
201
|
+
...hookScripts(host, root),
|
|
202
|
+
...HOOK_MANAGERS.map(([name]) => joinPath(root, name)),
|
|
203
|
+
...[makefileAt(host, root) ?? []].flat(),
|
|
204
|
+
].flatMap((path) => readText(host, path) ?? []);
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
195
|
-
* The own files the target root's workflow steps
|
|
196
|
-
*
|
|
197
|
-
* with or without a leading `./`, whatever directory the step runs in.
|
|
208
|
+
* The own files the target root's workflow steps, git hooks and recipes run or name, as
|
|
209
|
+
* `pathsNamed` reads each command.
|
|
198
210
|
*/
|
|
199
211
|
export function workflowEntries(
|
|
200
212
|
host: Host,
|
|
@@ -202,24 +214,11 @@ export function workflowEntries(
|
|
|
202
214
|
files: SourceFiles,
|
|
203
215
|
): readonly WorkflowEntry[] {
|
|
204
216
|
const found: WorkflowEntry[] = [];
|
|
205
|
-
for (const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (file !== undefined) {
|
|
211
|
-
found.push({ file, source: token });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
215
|
-
for (const { token, dir } of recipeTokens(host, root)) {
|
|
216
|
-
const below =
|
|
217
|
-
COMPUTED.test(token) || dir === ""
|
|
218
|
-
? undefined
|
|
219
|
-
: relativePath(root, resolvePath(joinPath(root, dir), token));
|
|
220
|
-
const file = below === undefined ? undefined : files.byPath.get(below);
|
|
221
|
-
if (file !== undefined) {
|
|
222
|
-
found.push({ file, source: token });
|
|
217
|
+
for (const { text, dir } of workflowCommands(host, targetRoot)) {
|
|
218
|
+
for (const { path, written } of pathsNamed(readCommands(text, dir))) {
|
|
219
|
+
for (const file of files.named(path)) {
|
|
220
|
+
found.push({ file, source: written });
|
|
221
|
+
}
|
|
223
222
|
}
|
|
224
223
|
}
|
|
225
224
|
return found;
|
package/dist/src/text/shell.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell command lines read as text, never run: the commands a line runs, the tokens the
|
|
3
|
-
* Contract splits a command into, and the words POSIX token recognition reads.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* The shell's list and pipeline operators, each of which ends one command. An `&` beside a
|
|
7
|
-
* redirection's `<` or `>` (`2>&1`, and bash's `&>log`) is part of the redirection.
|
|
8
|
-
*/
|
|
9
|
-
const COMMAND_OPERATORS = /&&|\|\||;|\||(?<![<>])&(?!>)/u;
|
|
10
|
-
/** Where a command splits into tokens: white space and the operators the Contract names. */
|
|
11
|
-
const TOKEN_SEPARATORS = /\s+|&&|\|\||;|\|/u;
|
|
12
|
-
/** A text written inside one pair of quotes. */
|
|
13
|
-
const QUOTED = /^(["'])(.*)\1$/su;
|
|
14
|
-
/** The commands one line runs, the text between two of the shell's list and pipeline operators. */
|
|
15
|
-
export function commandsOf(line) {
|
|
16
|
-
return line.split(COMMAND_OPERATORS);
|
|
17
|
-
}
|
|
18
|
-
/** One text with one pair of enclosing quotes removed. */
|
|
19
|
-
export function unquoted(text) {
|
|
20
|
-
return text.replace(QUOTED, "$2");
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Every token one script's command line holds, each with one pair of enclosing quotes
|
|
24
|
-
* removed. A token holding a wildcard is a pattern rather than a path, and names no
|
|
25
|
-
* entry point.
|
|
26
|
-
*/
|
|
27
|
-
export function scriptTokens(command) {
|
|
28
|
-
return command
|
|
29
|
-
.split(TOKEN_SEPARATORS)
|
|
30
|
-
.map(unquoted)
|
|
31
|
-
.filter((token) => token !== "" && !token.includes("*"));
|
|
32
|
-
}
|
|
33
|
-
/** One shell line up to its comment: a `#` that starts a word outside quotes. */
|
|
34
|
-
export function withoutComment(line) {
|
|
35
|
-
let quote = "";
|
|
36
|
-
for (let at = 0; at < line.length; at += 1) {
|
|
37
|
-
const char = line[at];
|
|
38
|
-
if (quote !== "") {
|
|
39
|
-
if (char === quote) {
|
|
40
|
-
quote = "";
|
|
41
|
-
}
|
|
42
|
-
else if (char === "\\" && quote === '"') {
|
|
43
|
-
at += 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (char === "'" || char === '"') {
|
|
47
|
-
quote = char;
|
|
48
|
-
}
|
|
49
|
-
else if (char === "\\") {
|
|
50
|
-
at += 1;
|
|
51
|
-
}
|
|
52
|
-
else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
|
|
53
|
-
return line.slice(0, at);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return line;
|
|
57
|
-
}
|
|
58
|
-
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
59
|
-
// Stryker disable next-line Regex: its \v to \V mutant is no valid escape under the u flag, so the whole module fails to parse
|
|
60
|
-
const BLANK = /[ \t\r\f\v]/u;
|
|
61
|
-
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
62
|
-
const OPERATOR = /[\n;&|()<>]/u;
|
|
63
|
-
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
64
|
-
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
65
|
-
/**
|
|
66
|
-
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
67
|
-
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
68
|
-
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
69
|
-
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
70
|
-
* and each operator are undefined, so a flag's file is never read across them.
|
|
71
|
-
*/
|
|
72
|
-
export function shellWords(command) {
|
|
73
|
-
const words = [];
|
|
74
|
-
let word;
|
|
75
|
-
let computed = false;
|
|
76
|
-
const end = () => {
|
|
77
|
-
if (word !== undefined) {
|
|
78
|
-
words.push(computed ? undefined : word);
|
|
79
|
-
}
|
|
80
|
-
word = undefined;
|
|
81
|
-
computed = false;
|
|
82
|
-
};
|
|
83
|
-
for (let at = 0; at < command.length; at += 1) {
|
|
84
|
-
const char = command.charAt(at);
|
|
85
|
-
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
86
|
-
at += 1;
|
|
87
|
-
}
|
|
88
|
-
else if (BLANK.test(char)) {
|
|
89
|
-
end();
|
|
90
|
-
}
|
|
91
|
-
else if (OPERATOR.test(char)) {
|
|
92
|
-
end();
|
|
93
|
-
words.push(undefined);
|
|
94
|
-
}
|
|
95
|
-
else if (char === "#" && word === undefined) {
|
|
96
|
-
const newline = command.indexOf("\n", at);
|
|
97
|
-
at = newline < 0 ? command.length : newline - 1;
|
|
98
|
-
}
|
|
99
|
-
else if (char === "\\") {
|
|
100
|
-
at += 1;
|
|
101
|
-
word = (word ?? "") + command.charAt(at);
|
|
102
|
-
}
|
|
103
|
-
else if (char === "'" || char === '"') {
|
|
104
|
-
const close = closingQuote(command, at);
|
|
105
|
-
if (close < 0) {
|
|
106
|
-
word ??= "";
|
|
107
|
-
computed = true;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
const quoted = char === "'"
|
|
111
|
-
? { text: command.slice(at + 1, close), computed: false }
|
|
112
|
-
: doubleQuoted(command.slice(at + 1, close));
|
|
113
|
-
word = (word ?? "") + quoted.text;
|
|
114
|
-
computed ||= quoted.computed;
|
|
115
|
-
at = close;
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
119
|
-
word = (word ?? "") + char;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
end();
|
|
123
|
-
return words;
|
|
124
|
-
}
|
|
125
|
-
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
126
|
-
function doubleQuoted(inner) {
|
|
127
|
-
let text = "";
|
|
128
|
-
let computed = false;
|
|
129
|
-
for (let at = 0; at < inner.length; at += 1) {
|
|
130
|
-
const char = inner.charAt(at);
|
|
131
|
-
const next = inner.charAt(at + 1);
|
|
132
|
-
if (char === "\\" && next === "\n") {
|
|
133
|
-
at += 1;
|
|
134
|
-
}
|
|
135
|
-
else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
136
|
-
text += next;
|
|
137
|
-
at += 1;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
computed ||= char === "$" || char === "`";
|
|
141
|
-
text += char;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return { text, computed };
|
|
145
|
-
}
|
|
146
|
-
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
147
|
-
function closingQuote(command, open) {
|
|
148
|
-
const quote = command.charAt(open);
|
|
149
|
-
for (let at = open + 1; at < command.length; at += 1) {
|
|
150
|
-
const char = command.charAt(at);
|
|
151
|
-
if (char === quote) {
|
|
152
|
-
return at;
|
|
153
|
-
}
|
|
154
|
-
if (quote === '"' && char === "\\") {
|
|
155
|
-
at += 1;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return -1;
|
|
159
|
-
}
|