@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.2
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/dist/src/analysis.js +6 -16
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/contextual-uses.js +41 -7
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +32 -0
- package/dist/src/dependencies.js +12 -8
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +35 -4
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/indexed-access.js +8 -2
- package/dist/src/inventory.js +3 -12
- package/dist/src/references.js +2 -2
- package/dist/src/report.js +3 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/session.js +0 -1
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +10 -30
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +1 -1
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +246 -28
- package/src/contextual-uses.ts +63 -9
- package/src/convention-rows.ts +19 -0
- package/src/conventions.ts +43 -0
- package/src/dependencies.ts +14 -10
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/entry-points.ts +1 -1
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +1 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +46 -4
- package/src/findings/emitters.ts +3 -8
- package/src/findings/self-check.ts +17 -14
- package/src/findings-pass.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/indexed-access.ts +10 -2
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +7 -6
- package/src/report.ts +8 -4
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/session.ts +0 -2
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/template-field.ts +1 -1
- package/src/value-reach.ts +1 -1
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/src/analysis.ts
CHANGED
|
@@ -62,11 +62,7 @@ import { globExpression } from "./glob.ts";
|
|
|
62
62
|
import type { Finding } from "./finding.ts";
|
|
63
63
|
import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
|
|
64
64
|
import type { EmitterInput } from "./findings/emitter.ts";
|
|
65
|
-
import {
|
|
66
|
-
NO_SELF_CHECK,
|
|
67
|
-
unmatchedRootFindings,
|
|
68
|
-
type SelfCheckFacts,
|
|
69
|
-
} from "./findings/self-check.ts";
|
|
65
|
+
import { unmatchedRootFindings, type SelfCheckFacts } from "./findings/self-check.ts";
|
|
70
66
|
import { frameworkLifecycle } from "./framework-lifecycle.ts";
|
|
71
67
|
import type { Host } from "./host.ts";
|
|
72
68
|
import { implementations, mergeImplementations } from "./implementations.ts";
|
|
@@ -194,6 +190,8 @@ interface ProjectRead {
|
|
|
194
190
|
readonly skipped: readonly SkippedUnit[];
|
|
195
191
|
/** The project's generated files, by path below the target root, each with the detail its retentions name. */
|
|
196
192
|
readonly generated: ReadonlyMap<string, string>;
|
|
193
|
+
/** The files, absolute, an applied convention row names as test files. */
|
|
194
|
+
readonly testFileNames: ReadonlySet<string>;
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
/** One target project the run analyzes, as its stage is handed it. */
|
|
@@ -460,6 +458,7 @@ function readProjects<Answer>(
|
|
|
460
458
|
.filter((file) => referenceOnly(file.fileName))
|
|
461
459
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
462
460
|
generated: generatedFilesOf(own, generatedFiles(conventions, own)),
|
|
461
|
+
testFileNames: conventions.testFiles,
|
|
463
462
|
};
|
|
464
463
|
};
|
|
465
464
|
for (const file of project.ownSourceFiles()) {
|
|
@@ -649,7 +648,7 @@ export interface ComponentWarning {
|
|
|
649
648
|
}
|
|
650
649
|
|
|
651
650
|
/** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
|
|
652
|
-
const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
651
|
+
export const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
653
652
|
["interface-satisfaction", interfaceSatisfaction],
|
|
654
653
|
["enum-group", enumGroup],
|
|
655
654
|
["generated-file", generatedFile],
|
|
@@ -739,6 +738,7 @@ function readRun<Extra>(
|
|
|
739
738
|
const projectRead = opened.read();
|
|
740
739
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
741
740
|
testFiles: config.ts.testFiles,
|
|
741
|
+
testFileNames: projectRead.testFileNames,
|
|
742
742
|
});
|
|
743
743
|
const evidence = evidenceRecords(
|
|
744
744
|
{
|
|
@@ -799,7 +799,6 @@ function readRun<Extra>(
|
|
|
799
799
|
const { projectRead } = one;
|
|
800
800
|
const satisfied = settled[index];
|
|
801
801
|
const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
|
|
802
|
-
disabled,
|
|
803
802
|
mode,
|
|
804
803
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
805
804
|
});
|
|
@@ -1049,7 +1048,7 @@ interface EmitterExtra {
|
|
|
1049
1048
|
}
|
|
1050
1049
|
|
|
1051
1050
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
1052
|
-
function readEmitterRun(
|
|
1051
|
+
export function readEmitterRun(
|
|
1053
1052
|
engine: Engine,
|
|
1054
1053
|
host: Host,
|
|
1055
1054
|
scope: Scope,
|
|
@@ -1135,7 +1134,7 @@ function nearestManifestDir(host: Host, root: string, file: string): string {
|
|
|
1135
1134
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
1136
1135
|
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
1137
1136
|
*/
|
|
1138
|
-
function emitterInputOver(
|
|
1137
|
+
export function emitterInputOver(
|
|
1139
1138
|
host: Host,
|
|
1140
1139
|
scope: Scope,
|
|
1141
1140
|
config: Config,
|
|
@@ -1190,25 +1189,6 @@ function emitterInputOver(
|
|
|
1190
1189
|
};
|
|
1191
1190
|
}
|
|
1192
1191
|
|
|
1193
|
-
/**
|
|
1194
|
-
* What every emitter reads of the run the scope and the configuration describe, swept
|
|
1195
|
-
* under the caller's marks: the run's sweep, the facts beside it each kind family reads,
|
|
1196
|
-
* and the sides of the target's declared cross-language edges that name a symbol of this
|
|
1197
|
-
* language. The run reads no suppression document, so the self-check family has nothing
|
|
1198
|
-
* to report.
|
|
1199
|
-
*/
|
|
1200
|
-
export function runEmitterInput(
|
|
1201
|
-
engine: Engine,
|
|
1202
|
-
host: Host,
|
|
1203
|
-
scope: Scope,
|
|
1204
|
-
config: Config,
|
|
1205
|
-
input: Pick<SweepInput, "marked" | "mode">,
|
|
1206
|
-
detectors: Detectors = DETECTORS,
|
|
1207
|
-
): EmitterInput {
|
|
1208
|
-
const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
|
|
1209
|
-
return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
1192
|
/** One run read for its findings: the input swept with and without its suppressions' marks. */
|
|
1213
1193
|
export interface AnalysisInputs {
|
|
1214
1194
|
/** The run swept under the marks of every bound suppression record. */
|
|
@@ -1338,7 +1318,7 @@ export interface RunProject {
|
|
|
1338
1318
|
}
|
|
1339
1319
|
|
|
1340
1320
|
/** What a report states about the run beside its findings. */
|
|
1341
|
-
|
|
1321
|
+
interface RunFacts {
|
|
1342
1322
|
/** The projects the run analyzed, in the run's order. */
|
|
1343
1323
|
readonly projects: readonly RunProject[];
|
|
1344
1324
|
/** The rules that classified files as test files, each counted over every project at once. */
|
|
@@ -1360,7 +1340,7 @@ export interface RunFacts {
|
|
|
1360
1340
|
}
|
|
1361
1341
|
|
|
1362
1342
|
/** One configuration whose component files a run cannot read. */
|
|
1363
|
-
|
|
1343
|
+
interface ComponentsUnread {
|
|
1364
1344
|
readonly configFile: string;
|
|
1365
1345
|
readonly reason: string;
|
|
1366
1346
|
}
|
package/src/calls.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { must } from "./query.ts";
|
|
|
30
30
|
import type { ProjectView } from "./session.ts";
|
|
31
31
|
|
|
32
32
|
/** One value written where a call passes it, and the node its declaration is resolved from. */
|
|
33
|
-
|
|
33
|
+
interface PassedValue {
|
|
34
34
|
/** Where the value is written, which is where a record of it sits. */
|
|
35
35
|
readonly site: Node;
|
|
36
36
|
/** The name the value is resolved through, or the shorthand assignment that holds it. */
|
|
@@ -39,7 +39,7 @@ export interface PassedValue {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** One class of the target a call passes or a decorator is attached to, and where. */
|
|
42
|
-
|
|
42
|
+
interface ClassUse {
|
|
43
43
|
/** The inventory's identifier of the class. */
|
|
44
44
|
readonly id: string;
|
|
45
45
|
readonly site: Node;
|
|
@@ -182,7 +182,7 @@ export function classesPassed<Brand>(
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/** One class declaration of the target and the decorators written on it. */
|
|
185
|
-
|
|
185
|
+
interface DecoratedClass {
|
|
186
186
|
readonly id: string;
|
|
187
187
|
readonly decorators: readonly Decorator[];
|
|
188
188
|
}
|
package/src/component-files.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** The language one block is written in, as the module's virtual extension spells it. */
|
|
10
|
-
|
|
10
|
+
type BlockLanguage = "ts" | "tsx" | "js" | "jsx";
|
|
11
11
|
|
|
12
12
|
/** One block the module holds, the frontmatter included. */
|
|
13
|
-
|
|
13
|
+
interface Block {
|
|
14
14
|
/** The offset the block's markup starts at: the `<` of its start tag, or 0 for frontmatter. */
|
|
15
15
|
readonly tagStart: number;
|
|
16
16
|
readonly contentStart: number;
|
|
@@ -20,7 +20,7 @@ export interface Block {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** One file a `<script src>` element names, which the module imports whole. */
|
|
23
|
-
|
|
23
|
+
interface SourceImport {
|
|
24
24
|
/** The specifier the module imports, relative to the component file. */
|
|
25
25
|
readonly specifier: string;
|
|
26
26
|
/** The offsets of the `src` attribute, name and value. */
|
|
@@ -29,14 +29,14 @@ export interface SourceImport {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** A `<script` start tag beginning a line that the module holds no block for. */
|
|
32
|
-
|
|
32
|
+
interface ComponentWarning {
|
|
33
33
|
/** The offset of the tag's `<`. */
|
|
34
34
|
readonly offset: number;
|
|
35
35
|
readonly reason: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** One run of markup an expression or an attribute value is written in. */
|
|
39
|
-
|
|
39
|
+
interface MarkupAction {
|
|
40
40
|
/** The offset of the run's first code unit. */
|
|
41
41
|
readonly start: number;
|
|
42
42
|
readonly text: string;
|
|
@@ -45,7 +45,7 @@ export interface MarkupAction {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** What one component file's text is read as. */
|
|
48
|
-
|
|
48
|
+
interface ComponentFile {
|
|
49
49
|
readonly blocks: readonly Block[];
|
|
50
50
|
readonly imports: readonly SourceImport[];
|
|
51
51
|
readonly warnings: readonly ComponentWarning[];
|
|
@@ -54,7 +54,7 @@ export interface ComponentFile {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** The module the compiler reads in place of one component file. */
|
|
57
|
-
|
|
57
|
+
interface ComponentModule {
|
|
58
58
|
/** The module's text: the file's length, then what the module appends. */
|
|
59
59
|
readonly text: string;
|
|
60
60
|
/** The virtual extension: the strongest language any block names, `.ts` for none. */
|
package/src/component-layer.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type { Host } from "./host.ts";
|
|
|
11
11
|
import { dirnamePath, joinPath } from "./paths.ts";
|
|
12
12
|
|
|
13
13
|
/** The name the mapper's manifest declares and every configuration's entry names. */
|
|
14
|
-
|
|
14
|
+
const MAPPER_PACKAGE = "deadset-ts-component-files";
|
|
15
15
|
|
|
16
16
|
/** The file system one snapshot is created with. */
|
|
17
|
-
|
|
17
|
+
type SnapshotFileSystem = NonNullable<CreateSnapshotParams["fileSystem"]>;
|
|
18
18
|
|
|
19
19
|
/** The manifest of the mapper's package, which runs `command`. */
|
|
20
20
|
export function mapperManifest(command: readonly string[]): string {
|
package/src/components.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface Component {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** What a report carries for one dead component under one cascade mode. */
|
|
45
|
-
|
|
45
|
+
interface Listing {
|
|
46
46
|
readonly roots: readonly string[];
|
|
47
47
|
/** Every member under the full mode, and nothing under the default one. */
|
|
48
48
|
readonly members: readonly string[];
|
package/src/config.ts
CHANGED
|
@@ -50,7 +50,7 @@ export type Cascade = "roots" | "full";
|
|
|
50
50
|
* tags in effect. It is the shape a language built under build constraints reads,
|
|
51
51
|
* and this analyzer leaves it to that analysis.
|
|
52
52
|
*/
|
|
53
|
-
|
|
53
|
+
interface PlatformConfiguration {
|
|
54
54
|
readonly shape: "platform";
|
|
55
55
|
readonly id: string;
|
|
56
56
|
readonly os: string;
|
|
@@ -97,7 +97,7 @@ export interface Analysis {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** How findings are rendered. */
|
|
100
|
-
|
|
100
|
+
interface Reporters {
|
|
101
101
|
readonly formats: readonly Format[];
|
|
102
102
|
readonly sort: Sort;
|
|
103
103
|
readonly cascade: Cascade;
|
|
@@ -154,7 +154,7 @@ export type Provider =
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
/** The analyzers the orchestrator runs. */
|
|
157
|
-
|
|
157
|
+
interface Providers {
|
|
158
158
|
readonly analyzers: readonly Provider[];
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -293,7 +293,7 @@ export interface Inputs {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/** Which refusal a {@link ConfigError} carries. */
|
|
296
|
-
|
|
296
|
+
type ErrorKind =
|
|
297
297
|
/**
|
|
298
298
|
* A document that is not one JSON instance of the closed key list: a syntax
|
|
299
299
|
* error, a value of the wrong type, a value outside the closed set a key
|
|
@@ -69,11 +69,19 @@ export const RESOLVED_EXTENSIONS = [
|
|
|
69
69
|
".cjs",
|
|
70
70
|
] as const;
|
|
71
71
|
|
|
72
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
73
|
+
const MODULES_DIR = "node_modules";
|
|
74
|
+
|
|
72
75
|
/** The file's name, the last segment of its path. */
|
|
73
76
|
function basename(path: string): string {
|
|
74
77
|
return path.slice(path.lastIndexOf("/") + 1);
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/** Whether one path is the project's own: below the target root and in no `node_modules`. */
|
|
81
|
+
function isProjectPath(root: string, path: string): boolean {
|
|
82
|
+
return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
78
86
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
87
|
}
|
|
@@ -92,6 +100,8 @@ export function isConfigurationDocumentName(name: string): boolean {
|
|
|
92
100
|
interface ConfigurationDocument {
|
|
93
101
|
/** The file, absolute. */
|
|
94
102
|
readonly path: string;
|
|
103
|
+
/** The directories of the manifests whose dependencies the strings are read against. */
|
|
104
|
+
readonly manifestDirs: readonly string[];
|
|
95
105
|
/** Every string value at any depth, in document order; a member's name is no value. */
|
|
96
106
|
readonly strings: readonly string[];
|
|
97
107
|
}
|
|
@@ -100,11 +110,169 @@ interface ConfigurationDocument {
|
|
|
100
110
|
export type ParseFile = (fileName: string, text: string) => SourceFile;
|
|
101
111
|
|
|
102
112
|
/** One file no program holds that a configuration module outside the program leads to. */
|
|
103
|
-
|
|
113
|
+
interface OutsideModule {
|
|
104
114
|
/** The file, parsed alone. */
|
|
105
115
|
readonly file: SourceFile;
|
|
106
|
-
/** The
|
|
107
|
-
readonly
|
|
116
|
+
/** The directories of the manifests of the configuration files the file was reached from. */
|
|
117
|
+
readonly manifestDirs: readonly string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The flags a tool's command line names its configuration file with. */
|
|
121
|
+
const CONFIG_FLAGS: ReadonlySet<string> = new Set(["--config", "-c"]);
|
|
122
|
+
|
|
123
|
+
/** The extensions of a module a configuration flag may name. */
|
|
124
|
+
const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
|
|
125
|
+
|
|
126
|
+
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
127
|
+
const BLANK = /[ \t\r\f\v]/u;
|
|
128
|
+
|
|
129
|
+
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
130
|
+
const OPERATOR = /[\n;&|()<>]/u;
|
|
131
|
+
|
|
132
|
+
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
133
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
137
|
+
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
138
|
+
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
139
|
+
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
140
|
+
* and each operator are undefined, so a flag's file is never read across them.
|
|
141
|
+
*/
|
|
142
|
+
function shellWords(command: string): readonly (string | undefined)[] {
|
|
143
|
+
const words: (string | undefined)[] = [];
|
|
144
|
+
let word: string | undefined;
|
|
145
|
+
let computed = false;
|
|
146
|
+
const end = (): void => {
|
|
147
|
+
if (word !== undefined) {
|
|
148
|
+
words.push(computed ? undefined : word);
|
|
149
|
+
}
|
|
150
|
+
word = undefined;
|
|
151
|
+
computed = false;
|
|
152
|
+
};
|
|
153
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
154
|
+
const char = command.charAt(at);
|
|
155
|
+
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
156
|
+
at += 1;
|
|
157
|
+
} else if (BLANK.test(char)) {
|
|
158
|
+
end();
|
|
159
|
+
} else if (OPERATOR.test(char)) {
|
|
160
|
+
end();
|
|
161
|
+
words.push(undefined);
|
|
162
|
+
} else if (char === "#" && word === undefined) {
|
|
163
|
+
const newline = command.indexOf("\n", at);
|
|
164
|
+
at = newline < 0 ? command.length : newline - 1;
|
|
165
|
+
} else if (char === "\\") {
|
|
166
|
+
at += 1;
|
|
167
|
+
word = (word ?? "") + command.charAt(at);
|
|
168
|
+
} else if (char === "'" || char === '"') {
|
|
169
|
+
const close = closingQuote(command, at);
|
|
170
|
+
if (close < 0) {
|
|
171
|
+
word ??= "";
|
|
172
|
+
computed = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const quoted =
|
|
176
|
+
char === "'"
|
|
177
|
+
? { text: command.slice(at + 1, close), computed: false }
|
|
178
|
+
: doubleQuoted(command.slice(at + 1, close));
|
|
179
|
+
word = (word ?? "") + quoted.text;
|
|
180
|
+
computed ||= quoted.computed;
|
|
181
|
+
at = close;
|
|
182
|
+
} else {
|
|
183
|
+
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
184
|
+
word = (word ?? "") + char;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
end();
|
|
188
|
+
return words;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
192
|
+
function doubleQuoted(inner: string): { readonly text: string; readonly computed: boolean } {
|
|
193
|
+
let text = "";
|
|
194
|
+
let computed = false;
|
|
195
|
+
for (let at = 0; at < inner.length; at += 1) {
|
|
196
|
+
const char = inner.charAt(at);
|
|
197
|
+
const next = inner.charAt(at + 1);
|
|
198
|
+
if (char === "\\" && next === "\n") {
|
|
199
|
+
at += 1;
|
|
200
|
+
} else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
201
|
+
text += next;
|
|
202
|
+
at += 1;
|
|
203
|
+
} else {
|
|
204
|
+
computed ||= char === "$" || char === "`";
|
|
205
|
+
text += char;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { text, computed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
212
|
+
function closingQuote(command: string, open: number): number {
|
|
213
|
+
const quote = command.charAt(open);
|
|
214
|
+
for (let at = open + 1; at < command.length; at += 1) {
|
|
215
|
+
const char = command.charAt(at);
|
|
216
|
+
if (char === quote) {
|
|
217
|
+
return at;
|
|
218
|
+
}
|
|
219
|
+
if (quote === '"' && char === "\\") {
|
|
220
|
+
at += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The files the scripts of the manifest in one directory name after a configuration flag,
|
|
228
|
+
* `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
|
|
229
|
+
* the target root and in no `node_modules`.
|
|
230
|
+
*/
|
|
231
|
+
function scriptConfigurations(host: Host, root: string, dir: string): readonly string[] {
|
|
232
|
+
let scripts: unknown;
|
|
233
|
+
try {
|
|
234
|
+
const manifest: unknown = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
|
|
235
|
+
scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(scripts)) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const found: string[] = [];
|
|
243
|
+
for (const command of Object.values(scripts)) {
|
|
244
|
+
if (typeof command !== "string") {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const words = shellWords(command);
|
|
248
|
+
words.forEach((word, at) => {
|
|
249
|
+
const named =
|
|
250
|
+
word !== undefined && CONFIG_FLAGS.has(word)
|
|
251
|
+
? words[at + 1]
|
|
252
|
+
: word === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: /^--config=(.+)$/su.exec(word)?.[1];
|
|
255
|
+
const path = named === undefined ? undefined : resolvePath(dir, named);
|
|
256
|
+
if (
|
|
257
|
+
path !== undefined &&
|
|
258
|
+
[".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
259
|
+
isProjectPath(root, path) &&
|
|
260
|
+
host.kindOf(path) === "file"
|
|
261
|
+
) {
|
|
262
|
+
found.push(path);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
|
|
270
|
+
function readDocument(host: Host, path: string): unknown {
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(host.readFile(path)) as unknown;
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
108
276
|
}
|
|
109
277
|
|
|
110
278
|
/** The configuration files one project holds and sits beside. */
|
|
@@ -117,7 +285,12 @@ interface ConfigurationFiles {
|
|
|
117
285
|
* the program does not hold either, read where a parser is given.
|
|
118
286
|
*/
|
|
119
287
|
readonly outside: readonly OutsideModule[];
|
|
120
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Every JSON configuration file in a directory of the project's files or above one, every
|
|
290
|
+
* JSON file a script's configuration flag names, and every JSON file below the target
|
|
291
|
+
* root and in no `node_modules` a relative string of either names, read against each
|
|
292
|
+
* manifest whose chain reaches it.
|
|
293
|
+
*/
|
|
121
294
|
readonly documents: readonly ConfigurationDocument[];
|
|
122
295
|
}
|
|
123
296
|
|
|
@@ -187,7 +360,8 @@ export function configurationFiles(
|
|
|
187
360
|
}
|
|
188
361
|
}
|
|
189
362
|
|
|
190
|
-
const
|
|
363
|
+
const pending: { path: string; manifestDir: string }[] = [];
|
|
364
|
+
const named: { path: string; manifestDir: string }[] = [];
|
|
191
365
|
for (const dir of [...dirs].sort()) {
|
|
192
366
|
if (!holdsManifest(dir)) {
|
|
193
367
|
continue;
|
|
@@ -200,24 +374,59 @@ export function configurationFiles(
|
|
|
200
374
|
.map((entry) => entry.name)
|
|
201
375
|
.sort();
|
|
202
376
|
} catch {
|
|
377
|
+
names = [];
|
|
378
|
+
}
|
|
379
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
380
|
+
for (const path of scriptConfigurations(host, root, dir)) {
|
|
381
|
+
const held = files.byName.get(path);
|
|
382
|
+
if (path.endsWith(".json")) {
|
|
383
|
+
pending.push({ path, manifestDir: dir });
|
|
384
|
+
} else if (held !== undefined) {
|
|
385
|
+
modules.push({ file: held, form: "a script's configuration flag" });
|
|
386
|
+
} else {
|
|
387
|
+
named.push({ path, manifestDir: dir });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// A document reached from two manifests is read against each, so a chain is keyed by both.
|
|
393
|
+
const documents = new Map<string, { path: string; manifestDirs: string[]; strings: string[] }>();
|
|
394
|
+
const unreadable = new Set<string>();
|
|
395
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
396
|
+
let document = documents.get(next.path);
|
|
397
|
+
if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
|
|
203
398
|
continue;
|
|
204
399
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
value = JSON.parse(host.readFile(path));
|
|
210
|
-
} catch {
|
|
400
|
+
if (document === undefined) {
|
|
401
|
+
const value = readDocument(host, next.path);
|
|
402
|
+
if (value === undefined) {
|
|
403
|
+
unreadable.add(next.path);
|
|
211
404
|
continue;
|
|
212
405
|
}
|
|
213
|
-
|
|
406
|
+
document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
|
|
407
|
+
documents.set(next.path, document);
|
|
408
|
+
}
|
|
409
|
+
document.manifestDirs.push(next.manifestDir);
|
|
410
|
+
for (const text of document.strings) {
|
|
411
|
+
const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
|
|
412
|
+
? resolvePath(dirnamePath(next.path), text)
|
|
413
|
+
: undefined;
|
|
414
|
+
if (
|
|
415
|
+
path?.endsWith(".json") === true &&
|
|
416
|
+
isProjectPath(root, path) &&
|
|
417
|
+
host.kindOf(path) === "file"
|
|
418
|
+
) {
|
|
419
|
+
pending.push({ path, manifestDir: next.manifestDir });
|
|
420
|
+
}
|
|
214
421
|
}
|
|
215
422
|
}
|
|
216
423
|
return {
|
|
217
424
|
modules,
|
|
218
|
-
documents,
|
|
425
|
+
documents: [...documents.values()],
|
|
219
426
|
outside:
|
|
220
|
-
parse === undefined
|
|
427
|
+
parse === undefined
|
|
428
|
+
? []
|
|
429
|
+
: outsideModules(host, [...dirs], files, holdsManifest, parse, named),
|
|
221
430
|
};
|
|
222
431
|
}
|
|
223
432
|
|
|
@@ -280,8 +489,9 @@ function outsideModules(
|
|
|
280
489
|
files: SourceFiles,
|
|
281
490
|
holdsManifest: (dir: string) => boolean,
|
|
282
491
|
parse: ParseFile,
|
|
492
|
+
named: readonly { readonly path: string; readonly manifestDir: string }[],
|
|
283
493
|
): readonly OutsideModule[] {
|
|
284
|
-
const pending: { path: string; manifestDir: string }[] = [];
|
|
494
|
+
const pending: { path: string; manifestDir: string }[] = [...named];
|
|
285
495
|
for (const dir of [...dirs].sort()) {
|
|
286
496
|
if (!holdsManifest(dir)) {
|
|
287
497
|
continue;
|
|
@@ -298,29 +508,37 @@ function outsideModules(
|
|
|
298
508
|
}
|
|
299
509
|
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
300
510
|
}
|
|
301
|
-
const found:
|
|
302
|
-
const
|
|
511
|
+
const found = new Map<string, { file: SourceFile; manifestDirs: string[] }>();
|
|
512
|
+
const unreadable = new Set<string>();
|
|
303
513
|
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
304
|
-
|
|
514
|
+
let outside = found.get(next.path);
|
|
515
|
+
if (
|
|
516
|
+
unreadable.has(next.path) ||
|
|
517
|
+
files.byName.has(next.path) ||
|
|
518
|
+
outside?.manifestDirs.includes(next.manifestDir) === true
|
|
519
|
+
) {
|
|
305
520
|
continue;
|
|
306
521
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
522
|
+
if (outside === undefined) {
|
|
523
|
+
let text: string;
|
|
524
|
+
try {
|
|
525
|
+
text = host.readFile(next.path);
|
|
526
|
+
} catch {
|
|
527
|
+
unreadable.add(next.path);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
outside = { file: parse(next.path, text), manifestDirs: [] };
|
|
531
|
+
found.set(next.path, outside);
|
|
313
532
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const specifier of relativeSpecifiers(file)) {
|
|
533
|
+
outside.manifestDirs.push(next.manifestDir);
|
|
534
|
+
for (const specifier of relativeSpecifiers(outside.file)) {
|
|
317
535
|
const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
|
|
318
536
|
if (reached !== undefined) {
|
|
319
537
|
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
320
538
|
}
|
|
321
539
|
}
|
|
322
540
|
}
|
|
323
|
-
return found;
|
|
541
|
+
return [...found.values()];
|
|
324
542
|
}
|
|
325
543
|
|
|
326
544
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|