@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
|
@@ -1,36 +1,29 @@
|
|
|
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
|
-
import {
|
|
7
|
+
import { pathsNamed, readCommands } from "./commands/read.js";
|
|
8
|
+
import { isRecord } from "./json-document.js";
|
|
9
|
+
import { joinPath, resolvePath } from "./paths.js";
|
|
7
10
|
import { filesIn } from "./fs/tree.js";
|
|
8
|
-
import {
|
|
11
|
+
import { yamlDocument } from "./text/yaml.js";
|
|
9
12
|
const WORKFLOWS = ".github/workflows";
|
|
10
|
-
/** A line holding a run command: indentation and one sequence marker before `run:`. */
|
|
11
|
-
const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
|
|
12
13
|
/** The directories whose every file is a git hook script, run as the shell runs it. */
|
|
13
14
|
const HOOK_DIRS = [".husky", ".githooks"];
|
|
14
|
-
/**
|
|
15
|
-
const WORKING_DIRECTORY =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/** The key setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
|
|
16
|
+
const WORKING_DIRECTORY = "working-directory";
|
|
17
|
+
/**
|
|
18
|
+
* The hook manager files, each with the key its commands are written under and the key of their
|
|
19
|
+
* directory: lefthook's `root`, https://lefthook.dev/configuration/root/
|
|
20
|
+
*/
|
|
20
21
|
const HOOK_MANAGERS = [
|
|
21
|
-
[".pre-commit-config.yaml",
|
|
22
|
-
...
|
|
22
|
+
[".pre-commit-config.yaml", "entry", undefined],
|
|
23
|
+
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map((name) => [name, "run", "root"]),
|
|
23
24
|
];
|
|
24
25
|
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
25
26
|
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"];
|
|
26
|
-
const BLOCK_SCALAR = /^[|>][+-]?$/u;
|
|
27
|
-
/** A token holding one of these is computed or a pattern, so it names no file. */
|
|
28
|
-
const COMPUTED = /[${*?[]/u;
|
|
29
|
-
const INDENT = /^\s*/u;
|
|
30
|
-
/** The files directly in one directory whose name `accepts`, by path in name order. */
|
|
31
|
-
function pathsIn(host, dir, accepts) {
|
|
32
|
-
return filesIn(host, dir, accepts).map((name) => joinPath(dir, name));
|
|
33
|
-
}
|
|
34
27
|
function readText(host, path) {
|
|
35
28
|
try {
|
|
36
29
|
return host.readFile(path);
|
|
@@ -39,144 +32,129 @@ function readText(host, path) {
|
|
|
39
32
|
return undefined;
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
/** The text a YAML scalar's value stands for, or `undefined` for a collection or a null. */
|
|
36
|
+
function scalarText(value) {
|
|
37
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
38
|
+
? String(value)
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
/** The `defaults.run.working-directory` one mapping writes, as YAML decodes it. */
|
|
42
|
+
function defaultDirectory(mapping) {
|
|
43
|
+
const defaults = mapping["defaults"];
|
|
44
|
+
const run = isRecord(defaults) ? defaults["run"] : undefined;
|
|
45
|
+
return isRecord(run) ? scalarText(run[WORKING_DIRECTORY]) : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Every command one workflow or hook manager file writes under `key`, in every mapping of the
|
|
49
|
+
* document, each with the directory it runs in: its mapping's `dirKey` value, else, in a workflow
|
|
50
|
+
* file, the innermost `defaults.run.working-directory` of a mapping holding it, else `root`. A
|
|
51
|
+
* file `yamlDocument` does not read gives none.
|
|
52
|
+
*/
|
|
53
|
+
function keyedCommands(text, root, key, dirKey) {
|
|
45
54
|
const found = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const visit = (node, inherited) => {
|
|
56
|
+
if (Array.isArray(node)) {
|
|
57
|
+
for (const item of node) {
|
|
58
|
+
visit(item, inherited);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!BLOCK_SCALAR.test(value)) {
|
|
54
|
-
found.push(value);
|
|
55
|
-
continue;
|
|
62
|
+
if (!isRecord(node)) {
|
|
63
|
+
return;
|
|
56
64
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const here = (dirKey === WORKING_DIRECTORY ? defaultDirectory(node) : undefined) ?? inherited;
|
|
66
|
+
const command = scalarText(node[key]);
|
|
67
|
+
if (command !== undefined && command.trim() !== "") {
|
|
68
|
+
const written = (dirKey === undefined ? undefined : scalarText(node[dirKey])) ?? here;
|
|
69
|
+
found.push({
|
|
70
|
+
text: command,
|
|
71
|
+
dir: written === undefined
|
|
72
|
+
? root
|
|
73
|
+
: written.includes("${{")
|
|
74
|
+
? undefined
|
|
75
|
+
: resolvePath(root, written),
|
|
76
|
+
writtenDirectory: written,
|
|
77
|
+
});
|
|
63
78
|
}
|
|
64
|
-
|
|
79
|
+
for (const value of Object.values(node)) {
|
|
80
|
+
visit(value, here);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
visit(yamlDocument(text), undefined);
|
|
65
84
|
return found;
|
|
66
85
|
}
|
|
86
|
+
/** The path of each git hook script at `root`, every file in {@link HOOK_DIRS}. */
|
|
87
|
+
function hookScripts(host, root) {
|
|
88
|
+
return HOOK_DIRS.flatMap((dir) => filesIn(host, joinPath(root, dir), () => true).map((name) => joinPath(root, dir, name)));
|
|
89
|
+
}
|
|
90
|
+
/** The makefile make reads at `root`, the first of {@link MAKEFILES} there. */
|
|
91
|
+
function makefileAt(host, root) {
|
|
92
|
+
return MAKEFILES.map((name) => joinPath(root, name)).find((candidate) => host.kindOf(candidate) === "file");
|
|
93
|
+
}
|
|
67
94
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
95
|
+
* Each recipe line of the target root's makefile: a line that starts with a tab, joined with
|
|
96
|
+
* the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it.
|
|
97
|
+
* Make runs each line in a shell of its own.
|
|
71
98
|
*/
|
|
72
99
|
function recipeLines(host, root) {
|
|
73
|
-
const path =
|
|
100
|
+
const path = makefileAt(host, root);
|
|
74
101
|
const text = path === undefined ? undefined : readText(host, path);
|
|
75
|
-
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
return text
|
|
102
|
+
return (text ?? "")
|
|
79
103
|
.replace(/\\\r?\n/gu, " ")
|
|
80
104
|
.split(/\r?\n/u)
|
|
81
105
|
.filter((line) => line.startsWith("\t"))
|
|
82
|
-
.map((line) =>
|
|
106
|
+
.map((line) => ({
|
|
107
|
+
text: line.slice(1).replace(/^[@+-]+/u, ""),
|
|
108
|
+
dir: root,
|
|
109
|
+
writtenDirectory: undefined,
|
|
110
|
+
}));
|
|
83
111
|
}
|
|
84
112
|
/**
|
|
85
|
-
* Every
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
function recipeTokens(host, root) {
|
|
90
|
-
const found = [];
|
|
91
|
-
for (const line of recipeLines(host, root)) {
|
|
92
|
-
let dir = root;
|
|
93
|
-
for (const command of line) {
|
|
94
|
-
const tokens = scriptTokens(command);
|
|
95
|
-
const [first, target, ...rest] = tokens;
|
|
96
|
-
if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
|
|
97
|
-
dir = resolvePath(dir, target);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
for (const token of tokens) {
|
|
101
|
-
found.push({ token, dir: relativePath(root, dir) ?? "" });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return found;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Every command the target root's workflow steps, git hooks and makefile run: the `run:`
|
|
109
|
-
* steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
|
|
110
|
-
* and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
|
|
111
|
-
* of a lefthook file and the makefile's recipe lines.
|
|
113
|
+
* Every command the target root's workflow steps, git hooks, hook managers and makefile run:
|
|
114
|
+
* the `run:` steps of the workflow files in `.github/workflows`, each file in `.husky` and
|
|
115
|
+
* `.githooks` as one shell script, the `entry:` commands of `.pre-commit-config.yaml`, the
|
|
116
|
+
* `run:` commands of a lefthook file and the makefile's recipe lines.
|
|
112
117
|
*/
|
|
113
118
|
export function workflowCommands(host, targetRoot) {
|
|
114
119
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Every directory the target root's workflow steps and lefthook commands are set to run in, as
|
|
119
|
-
* written: each `working-directory` of a workflow file and each `root` of a lefthook file.
|
|
120
|
-
*/
|
|
121
|
-
export function commandDirectories(host, targetRoot) {
|
|
122
|
-
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
120
|
+
const workflows = filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name));
|
|
123
121
|
return [
|
|
124
|
-
...
|
|
125
|
-
...
|
|
122
|
+
...workflows.flatMap((name) => keyedCommands(readText(host, joinPath(root, WORKFLOWS, name)) ?? "", root, "run", WORKING_DIRECTORY)),
|
|
123
|
+
...hookScripts(host, root).map((path) => ({
|
|
124
|
+
text: readText(host, path) ?? "",
|
|
125
|
+
dir: root,
|
|
126
|
+
writtenDirectory: undefined,
|
|
127
|
+
})),
|
|
128
|
+
...HOOK_MANAGERS.flatMap(([name, key, dirKey]) => keyedCommands(readText(host, joinPath(root, name)) ?? "", root, key, dirKey)),
|
|
129
|
+
...recipeLines(host, root),
|
|
126
130
|
];
|
|
127
131
|
}
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The whole text of each file {@link workflowCommands} reads a command from, each workflow file,
|
|
134
|
+
* git hook, hook manager file and makefile at the target root. Layer 3 holds it whole, so a part
|
|
135
|
+
* no command is read from, a comment included, or one the reading of the structure misses, still
|
|
136
|
+
* holds every file it names.
|
|
137
|
+
*/
|
|
138
|
+
export function commandFileTexts(host, targetRoot) {
|
|
134
139
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
135
140
|
return [
|
|
136
|
-
...
|
|
137
|
-
...
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
function stepCommands(host, root) {
|
|
142
|
-
const commands = [];
|
|
143
|
-
for (const path of workflowFiles(host, root)) {
|
|
144
|
-
commands.push(...runCommands(readText(host, path) ?? ""));
|
|
145
|
-
}
|
|
146
|
-
for (const dir of HOOK_DIRS) {
|
|
147
|
-
for (const path of pathsIn(host, joinPath(root, dir), () => true)) {
|
|
148
|
-
commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const [name, key] of HOOK_MANAGERS) {
|
|
152
|
-
commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
|
|
153
|
-
}
|
|
154
|
-
return commands;
|
|
141
|
+
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) => joinPath(root, WORKFLOWS, name)),
|
|
142
|
+
...hookScripts(host, root),
|
|
143
|
+
...HOOK_MANAGERS.map(([name]) => joinPath(root, name)),
|
|
144
|
+
...[makefileAt(host, root) ?? []].flat(),
|
|
145
|
+
].flatMap((path) => readText(host, path) ?? []);
|
|
155
146
|
}
|
|
156
147
|
/**
|
|
157
|
-
* The own files the target root's workflow steps
|
|
158
|
-
*
|
|
159
|
-
* with or without a leading `./`, whatever directory the step runs in.
|
|
148
|
+
* The own files the target root's workflow steps, git hooks and recipes run or name, as
|
|
149
|
+
* `pathsNamed` reads each command.
|
|
160
150
|
*/
|
|
161
151
|
export function workflowEntries(host, targetRoot, files) {
|
|
162
152
|
const found = [];
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (file !== undefined) {
|
|
169
|
-
found.push({ file, source: token });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
173
|
-
for (const { token, dir } of recipeTokens(host, root)) {
|
|
174
|
-
const below = COMPUTED.test(token) || dir === ""
|
|
175
|
-
? undefined
|
|
176
|
-
: relativePath(root, resolvePath(joinPath(root, dir), token));
|
|
177
|
-
const file = below === undefined ? undefined : files.byPath.get(below);
|
|
178
|
-
if (file !== undefined) {
|
|
179
|
-
found.push({ file, source: token });
|
|
153
|
+
for (const { text, dir } of workflowCommands(host, targetRoot)) {
|
|
154
|
+
for (const { path, written } of pathsNamed(readCommands(text, dir))) {
|
|
155
|
+
for (const file of files.named(path)) {
|
|
156
|
+
found.push({ file, source: written });
|
|
157
|
+
}
|
|
180
158
|
}
|
|
181
159
|
}
|
|
182
160
|
return found;
|
package/jsr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "8.0.0-dev.
|
|
3
|
+
"version": "8.0.0-dev.13",
|
|
4
4
|
"license": "GPL-3.0-or-later",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"include": [
|
|
10
10
|
"LICENSE",
|
|
11
11
|
"NOTICE",
|
|
12
|
+
"THIRD_PARTY_NOTICES.md",
|
|
12
13
|
"README.md",
|
|
13
14
|
"bin/**",
|
|
14
15
|
"src/**",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "8.0.0-dev.
|
|
3
|
+
"version": "8.0.0-dev.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc -p tsconfig.build.json",
|
|
22
22
|
"gen": "node scripts/gen-contract.ts",
|
|
23
|
+
"vendor": "node scripts/vendor-unbash.ts",
|
|
23
24
|
"prepack": "npm run build",
|
|
24
25
|
"typecheck": "tsc -p tsconfig.json",
|
|
25
26
|
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
@@ -31,22 +32,22 @@
|
|
|
31
32
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@typescript/native": "npm:typescript@7.1.0-dev.
|
|
35
|
+
"@typescript/native": "npm:typescript@7.1.0-dev.20261029.1"
|
|
35
36
|
},
|
|
36
37
|
"overrides": {
|
|
37
38
|
"qs": "^6.15.2"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@eslint/js": "10.0.1",
|
|
41
|
-
"@stryker-mutator/core": "10.
|
|
42
|
-
"@stryker-mutator/typescript-checker": "10.
|
|
43
|
-
"@stryker-mutator/vitest-runner": "10.
|
|
42
|
+
"@stryker-mutator/core": "10.1.1",
|
|
43
|
+
"@stryker-mutator/typescript-checker": "10.1.1",
|
|
44
|
+
"@stryker-mutator/vitest-runner": "10.1.1",
|
|
44
45
|
"@types/node": "24.19.2",
|
|
45
|
-
"eslint": "10.
|
|
46
|
+
"eslint": "10.13.0",
|
|
46
47
|
"fast-check": "4.10.2",
|
|
47
48
|
"prettier": "3.9.10",
|
|
48
49
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
49
|
-
"typescript-eslint": "8.
|
|
50
|
+
"typescript-eslint": "8.72.1",
|
|
50
51
|
"vitest": "5.0.5"
|
|
51
52
|
},
|
|
52
53
|
"publishConfig": {
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"files": [
|
|
73
74
|
"LICENSE",
|
|
74
75
|
"NOTICE",
|
|
76
|
+
"THIRD_PARTY_NOTICES.md",
|
|
75
77
|
"dist/",
|
|
76
78
|
"src/",
|
|
77
79
|
"!src/**/*.test.ts",
|
package/src/build-scripts.ts
CHANGED
|
@@ -40,11 +40,11 @@ import type { Host } from "./host.ts";
|
|
|
40
40
|
import { ancestors, dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
41
41
|
import type { SourceFiles } from "./source-files.ts";
|
|
42
42
|
import { bytewise } from "./text/compare.ts";
|
|
43
|
-
import { isRecord } from "./json-document.ts";
|
|
44
43
|
import { unwrapped } from "./unwrap.ts";
|
|
45
44
|
import { holdsManifest, inInstalledPackages, manifestAt } from "./fs/manifest.ts";
|
|
46
45
|
import { globExpression } from "./text/glob.ts";
|
|
47
|
-
import {
|
|
46
|
+
import { pathsNamed, readCommands } from "./commands/read.ts";
|
|
47
|
+
import { manifestCommands } from "./manifest.ts";
|
|
48
48
|
|
|
49
49
|
/** One bundler API: the module, the functions it exports that build, and the option naming entries. */
|
|
50
50
|
interface BundlerAPI {
|
|
@@ -473,19 +473,12 @@ function scriptEntries(file: SourceFile, dir: string, files: SourceFiles): reado
|
|
|
473
473
|
return found;
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
/** The modules one manifest's
|
|
476
|
+
/** The modules one manifest's command strings run or name, by absolute path, each below the target root. */
|
|
477
477
|
function scriptModules(host: Host, root: string, dir: string): readonly string[] {
|
|
478
|
-
const
|
|
479
|
-
if (!isRecord(scripts)) {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
478
|
+
const manifest = manifestAt(host, dir);
|
|
482
479
|
const found = new Set<string>();
|
|
483
|
-
for (const command of
|
|
484
|
-
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
for (const token of scriptTokens(command)) {
|
|
488
|
-
const path = resolvePath(dir, token);
|
|
480
|
+
for (const { command } of manifest === undefined ? [] : manifestCommands(host, dir, manifest)) {
|
|
481
|
+
for (const { path } of pathsNamed(readCommands(command, dir))) {
|
|
489
482
|
if (
|
|
490
483
|
MODULE_EXTENSION.test(path) &&
|
|
491
484
|
relativePath(root, path) !== undefined &&
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one reader of every command string the analysis reads. Layer 3, the holding rule
|
|
3
|
+
* `roots.ts` applies through `text/named-files.ts`, reads all of the string's text whatever
|
|
4
|
+
* the other two layers make of it; layer 1 parses the string and layer 2 reads each simple
|
|
5
|
+
* command a tool's reader understands, and they only add the files and modules it runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { joinPath, resolvePath } from "../paths.ts";
|
|
9
|
+
import { shellLine, type Pipeline, type Word } from "./shell.ts";
|
|
10
|
+
import {
|
|
11
|
+
configurationsOf,
|
|
12
|
+
preloadTarget,
|
|
13
|
+
readBuiltin,
|
|
14
|
+
readTool,
|
|
15
|
+
type Outcome,
|
|
16
|
+
type Preload,
|
|
17
|
+
type Reading,
|
|
18
|
+
} from "./tools.ts";
|
|
19
|
+
|
|
20
|
+
/** One program a read command runs. */
|
|
21
|
+
interface Run {
|
|
22
|
+
/** The directory it runs in, absolute where the string's directory is. */
|
|
23
|
+
readonly dir: string;
|
|
24
|
+
/** The path of the file it runs, as written. */
|
|
25
|
+
readonly entry: string | undefined;
|
|
26
|
+
/** The modules it loads before the entry. */
|
|
27
|
+
readonly preloads: readonly Preload[];
|
|
28
|
+
readonly runtime: "node" | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One simple command's words, `undefined` for a dynamic one, with the directory it runs in. */
|
|
32
|
+
interface CommandWords {
|
|
33
|
+
readonly dir: string;
|
|
34
|
+
readonly words: readonly (string | undefined)[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One configuration file a command names by its tool's configuration option, as written, with its command's directory. */
|
|
38
|
+
interface NamedConfiguration {
|
|
39
|
+
readonly dir: string;
|
|
40
|
+
readonly written: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What one command string runs. */
|
|
44
|
+
export interface CommandReading {
|
|
45
|
+
readonly runs: readonly Run[];
|
|
46
|
+
/**
|
|
47
|
+
* Each simple command read in the directory it runs in, and each one of the string's own
|
|
48
|
+
* line in the string's directory too, wherever and whether it runs.
|
|
49
|
+
*/
|
|
50
|
+
readonly commands: readonly CommandWords[];
|
|
51
|
+
readonly configurations: readonly NamedConfiguration[];
|
|
52
|
+
/** The text layer 3 reads: all of the string. */
|
|
53
|
+
readonly held: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The outcomes of one pipeline run in `dir`: those of the builtin that is its one member, as
|
|
58
|
+
* `readBuiltin` gives them, a directory the analysis does not know after a part layer 1 does
|
|
59
|
+
* not read, and either status in `dir` otherwise, a pipeline of several members included,
|
|
60
|
+
* whose members each run in a subshell.
|
|
61
|
+
*/
|
|
62
|
+
function outcomesOf(pipeline: Pipeline, dir: string | undefined): readonly Outcome[] {
|
|
63
|
+
const [only] = pipeline.alone ? pipeline.commands : [];
|
|
64
|
+
const outcomes = pipeline.opaque
|
|
65
|
+
? [
|
|
66
|
+
{ dir: undefined, ok: true },
|
|
67
|
+
{ dir: undefined, ok: false },
|
|
68
|
+
]
|
|
69
|
+
: ((only === undefined ? undefined : readBuiltin(only, dir)) ?? [
|
|
70
|
+
{ dir, ok: true },
|
|
71
|
+
{ dir, ok: false },
|
|
72
|
+
]);
|
|
73
|
+
return outcomes.map((one) => ({ dir: one.dir, ok: one.ok !== pipeline.negated }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The one directory every outcome is in, or `undefined` where they differ or none is known. */
|
|
77
|
+
function oneDirectory(outcomes: readonly Outcome[]): string | undefined {
|
|
78
|
+
const dirs = new Set(outcomes.map((one) => one.dir));
|
|
79
|
+
return dirs.size === 1 ? [...dirs][0] : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* What `text`, run in `dir`, `undefined` where not known, runs. A pipeline is read where every
|
|
84
|
+
* way its and-or list reaches it leaves the shell in one known directory, none after an `exit`;
|
|
85
|
+
* a background list moves and ends no later one. The words and configuration files of every
|
|
86
|
+
* simple command of the string's own line are read in `dir` as well, wherever they run.
|
|
87
|
+
*/
|
|
88
|
+
export function readCommands(text: string, dir: string | undefined): CommandReading {
|
|
89
|
+
const runs: Run[] = [];
|
|
90
|
+
const commands: CommandWords[] = [];
|
|
91
|
+
const configurations: NamedConfiguration[] = [];
|
|
92
|
+
const readCommand = (words: readonly Word[], at: string): void => {
|
|
93
|
+
commands.push({ dir: at, words: words.map((word) => word.value) });
|
|
94
|
+
let handed = words;
|
|
95
|
+
let reading: Reading | undefined = readTool(handed);
|
|
96
|
+
while (reading?.kind === "command") {
|
|
97
|
+
handed = reading.words;
|
|
98
|
+
reading = readTool(handed);
|
|
99
|
+
}
|
|
100
|
+
if (reading?.kind === "line") {
|
|
101
|
+
readLine(reading.text, at);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
configurations.push(...configurationsOf(handed).map((written) => ({ dir: at, written })));
|
|
105
|
+
if (reading !== undefined && (reading.entry !== undefined || reading.preloads.length > 0)) {
|
|
106
|
+
runs.push({
|
|
107
|
+
dir: at,
|
|
108
|
+
entry: reading.entry,
|
|
109
|
+
preloads: reading.preloads,
|
|
110
|
+
runtime: reading.runtime,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const readLine = (source: string, start: string | undefined): void => {
|
|
115
|
+
let at = start;
|
|
116
|
+
for (const list of shellLine(source)) {
|
|
117
|
+
let outcomes: readonly Outcome[] = [{ dir: at, ok: true }];
|
|
118
|
+
for (const pipeline of list.pipelines) {
|
|
119
|
+
const { operator } = pipeline;
|
|
120
|
+
const reached = outcomes.filter(
|
|
121
|
+
(one) => operator === undefined || one.ok === (operator === "&&"),
|
|
122
|
+
);
|
|
123
|
+
const where = reached.length === 0 ? undefined : oneDirectory(reached);
|
|
124
|
+
if (where !== undefined) {
|
|
125
|
+
pipeline.commands.forEach((words) => {
|
|
126
|
+
readCommand(words, where);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const next = reached.flatMap((one) => outcomesOf(pipeline, one.dir));
|
|
130
|
+
const passed = outcomes.filter((one) => !reached.includes(one));
|
|
131
|
+
outcomes = [
|
|
132
|
+
...new Map(
|
|
133
|
+
[...passed, ...next].map((one) => [`${String(one.ok)}\u0000${one.dir ?? ""}`, one]),
|
|
134
|
+
).values(),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
if (!list.background) {
|
|
138
|
+
at = oneDirectory(outcomes);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
readLine(text, dir);
|
|
143
|
+
if (dir !== undefined) {
|
|
144
|
+
for (const words of shellLine(text).flatMap((list) =>
|
|
145
|
+
list.pipelines.flatMap((pipeline) => pipeline.commands),
|
|
146
|
+
)) {
|
|
147
|
+
commands.push({ dir, words: words.map((word) => word.value) });
|
|
148
|
+
configurations.push(...configurationsOf(words).map((written) => ({ dir, written })));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const unique = <T>(all: readonly T[]): T[] => [
|
|
152
|
+
...new Map(all.map((one) => [JSON.stringify(one), one])).values(),
|
|
153
|
+
];
|
|
154
|
+
return {
|
|
155
|
+
runs,
|
|
156
|
+
commands: unique(commands),
|
|
157
|
+
configurations: unique(configurations),
|
|
158
|
+
held: text,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The extensions a runtime or a loader tries after a path written without one. */
|
|
163
|
+
const TRIED_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
|
|
164
|
+
|
|
165
|
+
/** One file a run loads, as written, and every absolute path it may be at. */
|
|
166
|
+
interface Load {
|
|
167
|
+
readonly written: string;
|
|
168
|
+
readonly paths: readonly string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The files `run` loads: its entry, and each preload written as a path. Each may be at its
|
|
173
|
+
* path, with each extension tried, and as a directory's `index` file.
|
|
174
|
+
*/
|
|
175
|
+
export function loadsOf(run: Run): readonly Load[] {
|
|
176
|
+
const written = [
|
|
177
|
+
...(run.entry === undefined
|
|
178
|
+
? []
|
|
179
|
+
: [{ written: run.entry, path: resolvePath(run.dir, run.entry) }]),
|
|
180
|
+
...run.preloads.flatMap((preload) => {
|
|
181
|
+
const target = preloadTarget(preload, run.dir);
|
|
182
|
+
return target !== undefined && "path" in target
|
|
183
|
+
? [{ written: preload.specifier, path: target.path }]
|
|
184
|
+
: [];
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
return written.map(({ written: one, path }) => ({
|
|
188
|
+
written: one,
|
|
189
|
+
paths: [
|
|
190
|
+
path,
|
|
191
|
+
...TRIED_EXTENSIONS.map((extension) => `${path}${extension}`),
|
|
192
|
+
...TRIED_EXTENSIONS.map((extension) => joinPath(path, `index${extension}`)),
|
|
193
|
+
],
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The module specifiers `run` preloads that name a package rather than a path. */
|
|
198
|
+
export function preloadedPackages(run: Run): readonly string[] {
|
|
199
|
+
return run.preloads.flatMap((preload) => {
|
|
200
|
+
const target = preloadTarget(preload, run.dir);
|
|
201
|
+
return target !== undefined && "package" in target ? [target.package] : [];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** One path a command string names, with the word or the load that names it. */
|
|
206
|
+
interface NamedPath {
|
|
207
|
+
readonly path: string;
|
|
208
|
+
readonly written: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Every path `reading` names: each static word of its commands read against the directory the
|
|
213
|
+
* command runs in, a word holding `*` aside, which a tool reads as a pattern, and every path a
|
|
214
|
+
* file a read command runs or preloads may be at.
|
|
215
|
+
*/
|
|
216
|
+
export function pathsNamed(reading: CommandReading): readonly NamedPath[] {
|
|
217
|
+
const named = [
|
|
218
|
+
...reading.commands.flatMap(({ dir, words }) =>
|
|
219
|
+
words.flatMap((word) =>
|
|
220
|
+
word === undefined || word.includes("*")
|
|
221
|
+
? []
|
|
222
|
+
: [{ path: resolvePath(dir, word), written: word }],
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
...reading.runs
|
|
226
|
+
.flatMap(loadsOf)
|
|
227
|
+
.flatMap(({ written, paths }) => paths.map((path) => ({ path, written }))),
|
|
228
|
+
];
|
|
229
|
+
return [...new Map(named.map((one) => [`${one.path}\u0000${one.written}`, one])).values()];
|
|
230
|
+
}
|