@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.
Files changed (77) hide show
  1. package/dist/src/analysis.js +6 -16
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/contextual-uses.js +41 -7
  5. package/dist/src/convention-rows.js +6 -0
  6. package/dist/src/conventions.js +32 -0
  7. package/dist/src/dependencies.js +12 -8
  8. package/dist/src/discover.js +1 -1
  9. package/dist/src/edges.js +1 -1
  10. package/dist/src/exempt-classes.js +9 -22
  11. package/dist/src/exempt.js +3 -15
  12. package/dist/src/findings/completion.js +35 -4
  13. package/dist/src/findings/emitters.js +0 -4
  14. package/dist/src/findings/self-check.js +14 -10
  15. package/dist/src/indexed-access.js +8 -2
  16. package/dist/src/inventory.js +3 -12
  17. package/dist/src/references.js +2 -2
  18. package/dist/src/report.js +3 -0
  19. package/dist/src/roots.js +0 -1
  20. package/dist/src/run.js +0 -2
  21. package/dist/src/schema.js +1 -1
  22. package/dist/src/scope.js +2 -2
  23. package/dist/src/session.js +0 -1
  24. package/dist/src/suppress-inline.js +1 -1
  25. package/dist/src/suppress.js +15 -16
  26. package/dist/src/workspace-programs.js +0 -3
  27. package/dist/src/workspace-resolution.js +0 -2
  28. package/dist/src/workspace.js +0 -3
  29. package/jsr.json +1 -1
  30. package/package.json +2 -1
  31. package/src/analysis.ts +10 -30
  32. package/src/calls.ts +3 -3
  33. package/src/component-files.ts +7 -7
  34. package/src/component-layer.ts +2 -2
  35. package/src/components.ts +1 -1
  36. package/src/config.ts +4 -4
  37. package/src/configuration-files.ts +246 -28
  38. package/src/contextual-uses.ts +63 -9
  39. package/src/convention-rows.ts +19 -0
  40. package/src/conventions.ts +43 -0
  41. package/src/dependencies.ts +14 -10
  42. package/src/discover.ts +3 -3
  43. package/src/edges.ts +3 -3
  44. package/src/entry-points.ts +1 -1
  45. package/src/exempt-classes.ts +10 -24
  46. package/src/exempt.ts +5 -29
  47. package/src/file-facts.ts +1 -1
  48. package/src/finding.ts +3 -1
  49. package/src/findings/completion.ts +46 -4
  50. package/src/findings/emitters.ts +3 -8
  51. package/src/findings/self-check.ts +17 -14
  52. package/src/findings-pass.ts +2 -2
  53. package/src/graph.ts +2 -2
  54. package/src/html-entries.ts +1 -1
  55. package/src/indexed-access.ts +10 -2
  56. package/src/inventory.ts +5 -33
  57. package/src/json-document.ts +1 -1
  58. package/src/manifest.ts +1 -1
  59. package/src/namespace-objects.ts +1 -1
  60. package/src/pnpm-workspace.ts +1 -1
  61. package/src/references.ts +7 -6
  62. package/src/report.ts +8 -4
  63. package/src/roots.ts +0 -3
  64. package/src/run.ts +0 -2
  65. package/src/schema.ts +2 -2
  66. package/src/scope.ts +2 -2
  67. package/src/session.ts +0 -2
  68. package/src/stores.ts +1 -1
  69. package/src/suppress-inline.ts +3 -3
  70. package/src/suppress.ts +21 -24
  71. package/src/template-field.ts +1 -1
  72. package/src/value-reach.ts +1 -1
  73. package/src/verbs/verb.ts +1 -4
  74. package/src/workspace-programs.ts +2 -9
  75. package/src/workspace-resolution.ts +0 -3
  76. package/src/workspace-run.ts +1 -1
  77. 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
- export interface RunFacts {
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
- export interface ComponentsUnread {
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
- export interface PassedValue {
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
- export interface ClassUse {
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
- export interface DecoratedClass {
185
+ interface DecoratedClass {
186
186
  readonly id: string;
187
187
  readonly decorators: readonly Decorator[];
188
188
  }
@@ -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
- export type BlockLanguage = "ts" | "tsx" | "js" | "jsx";
10
+ type BlockLanguage = "ts" | "tsx" | "js" | "jsx";
11
11
 
12
12
  /** One block the module holds, the frontmatter included. */
13
- export interface Block {
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
- export interface SourceImport {
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
- export interface ComponentWarning {
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
- export interface MarkupAction {
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
- export interface ComponentFile {
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
- export interface ComponentModule {
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. */
@@ -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
- export const MAPPER_PACKAGE = "deadset-ts-component-files";
14
+ const MAPPER_PACKAGE = "deadset-ts-component-files";
15
15
 
16
16
  /** The file system one snapshot is created with. */
17
- export type SnapshotFileSystem = NonNullable<CreateSnapshotParams["fileSystem"]>;
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
- export interface Listing {
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
- export interface PlatformConfiguration {
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
- export interface Reporters {
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
- export interface Providers {
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
- export type ErrorKind =
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
- export interface OutsideModule {
113
+ interface OutsideModule {
104
114
  /** The file, parsed alone. */
105
115
  readonly file: SourceFile;
106
- /** The directory of the configuration module the file was reached from, which holds its manifest. */
107
- readonly manifestDir: string;
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
- /** Every JSON configuration file in a directory of the project's files or above one. */
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 documents: ConfigurationDocument[] = [];
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
- for (const name of names) {
206
- const path = joinPath(dir, name);
207
- let value: unknown;
208
- try {
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
- documents.push({ path, strings: documentStrings(value) });
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 ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
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: OutsideModule[] = [];
302
- const seen = new Set<string>();
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
- if (seen.has(next.path) || files.byName.has(next.path)) {
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
- seen.add(next.path);
308
- let text: string;
309
- try {
310
- text = host.readFile(next.path);
311
- } catch {
312
- continue;
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
- const file = parse(next.path, text);
315
- found.push({ file, manifestDir: next.manifestDir });
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. */