@cplieger/deadset-ts 6.1.1-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 (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. package/src/workspace.ts +1 -9
@@ -23,7 +23,7 @@ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhi
23
23
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
24
24
  import { globExpression } from "./glob.js";
25
25
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
26
- import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
26
+ import { unmatchedRootFindings } from "./findings/self-check.js";
27
27
  import { frameworkLifecycle } from "./framework-lifecycle.js";
28
28
  import { implementations, mergeImplementations } from "./implementations.js";
29
29
  import { injectionContainer } from "./injection-container.js";
@@ -32,7 +32,7 @@ import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
32
32
  import { inventory } from "./inventory.js";
33
33
  import { packageEntries, readManifest } from "./manifest.js";
34
34
  import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
35
- import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
35
+ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
36
36
  import { byPosition, isComponentFile } from "./position.js";
37
37
  import { SetupError, setupLine } from "./setup-failure.js";
38
38
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
@@ -41,6 +41,7 @@ import { references, testFileRulesOf } from "./references.js";
41
41
  import { roots, unmatchedEverywhere } from "./roots.js";
42
42
  import { serializationContract } from "./serialization-contract.js";
43
43
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
44
+ import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
44
45
  import { supportReferences, testSupportFiles } from "./test-support.js";
45
46
  import { diagnosticsOf, ownedAs, runSession } from "./session.js";
46
47
  import { accessorsOf, storesOf } from "./stores.js";
@@ -119,6 +120,7 @@ function readProjects(engine, host, scope, config, stage) {
119
120
  testFiles: config.ts.testFiles,
120
121
  parse: (fileName, text) => engine.parseSourceFile(fileName, text),
121
122
  publishedAPI: config.targetKind === "library",
123
+ publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
122
124
  };
123
125
  const notBuilt = [...discovered.notBuilt];
124
126
  const dropped = (configFile, error) => {
@@ -144,6 +146,10 @@ function readProjects(engine, host, scope, config, stage) {
144
146
  unbuildable.push(...met);
145
147
  }
146
148
  };
149
+ const testPatterns = config.ts.testFiles.map(globExpression);
150
+ const joins = discovered.derived
151
+ ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
152
+ : new Map();
147
153
  const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
148
154
  const consumer = consumerOf.get(opened.configFile);
149
155
  const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
@@ -237,15 +243,17 @@ function readProjects(engine, host, scope, config, stage) {
237
243
  .filter((file) => referenceOnly(file.fileName))
238
244
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
239
245
  generated: generatedFilesOf(own, generatedFiles(conventions, own)),
246
+ testFileNames: conventions.testFiles,
240
247
  };
241
248
  };
242
249
  for (const file of project.ownSourceFiles()) {
243
250
  heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
244
251
  }
245
252
  return stage(project, { configuration, read });
246
- }, discovered.derived ? "record" : "refuse", workspace?.programs, () => {
247
- const patterns = config.ts.testFiles.map(globExpression);
248
- outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path))), options.parse);
253
+ }, discovered.derived ? "record" : "refuse", joins.size === 0
254
+ ? workspace?.programs
255
+ : withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins), () => {
256
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), options.parse);
249
257
  });
250
258
  for (const configFile of session.unopened) {
251
259
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -339,7 +347,7 @@ export function runRoots(engine, host, scope, config, provenance) {
339
347
  };
340
348
  }
341
349
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
342
- const DETECTORS = new Map([
350
+ export const DETECTORS = new Map([
343
351
  ["interface-satisfaction", interfaceSatisfaction],
344
352
  ["enum-group", enumGroup],
345
353
  ["generated-file", generatedFile],
@@ -367,6 +375,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
367
375
  const projectRead = opened.read();
368
376
  const resolved = references(project, projectRead.held, targetRoot, {
369
377
  testFiles: config.ts.testFiles,
378
+ testFileNames: projectRead.testFileNames,
370
379
  });
371
380
  const evidence = evidenceRecords({
372
381
  project,
@@ -412,7 +421,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
412
421
  const { projectRead } = one;
413
422
  const satisfied = settled[index];
414
423
  const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
415
- disabled,
416
424
  mode,
417
425
  testFiles: new Set([...one.testFiles, ...support]),
418
426
  });
@@ -616,7 +624,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
616
624
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
617
625
  }
618
626
  /** Reads a run's projects with every per-project fact an emitter reads. */
619
- function readEmitterRun(engine, host, scope, config, mode, detectors) {
627
+ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
620
628
  const targetRoot = scope.target.path;
621
629
  const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
622
630
  deprecation: deprecatedDeclarations(project, read.held),
@@ -638,32 +646,48 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
638
646
  : { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
639
647
  }
640
648
  /**
641
- * The workspace members whose manifest the run reads whole enough to judge its
642
- * dependencies: a built project holds a file below the member's directory, and no derived
643
- * configuration at or below it was dropped. A file no built project holds may import any
644
- * dependency the member declares.
649
+ * The workspace members whose dependencies the run judges: `read` holds those a built
650
+ * project holds a file below, `unbuilt` the others whose own directory, the nearest
651
+ * manifest's above the configuration, holds a configuration the run could not build. A
652
+ * member no configuration holds a file of is in neither.
645
653
  */
646
- function membersWhollyRead(host, targetRoot, read) {
654
+ function judgedMembers(host, targetRoot, read) {
647
655
  const root = resolvePath(host.workingDirectory(), targetRoot);
648
656
  const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
649
- const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
650
- return read.memberDirs.filter((dir) => {
657
+ const owners = new Set(read.notBuilt.map((one) => nearestManifestDir(host, root, one.configFile)));
658
+ const judged = { read: [], unbuilt: [] };
659
+ for (const dir of read.memberDirs) {
651
660
  const below = relativePath(root, dir);
652
661
  if (below === undefined) {
653
- return false;
662
+ continue;
654
663
  }
655
- const inside = (path) => path.startsWith(`${below}/`);
656
- return [...held].some(inside) && !dropped.some(inside);
657
- });
664
+ if ([...held].some((path) => path.startsWith(`${below}/`))) {
665
+ judged.read.push(dir);
666
+ }
667
+ else if (owners.has(dir)) {
668
+ judged.unbuilt.push(dir);
669
+ }
670
+ }
671
+ return judged;
672
+ }
673
+ /** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
674
+ function nearestManifestDir(host, root, file) {
675
+ for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
676
+ if (host.kindOf(joinPath(dir, "package.json")) === "file") {
677
+ return dir;
678
+ }
679
+ }
680
+ return root;
658
681
  }
659
682
  /**
660
683
  * The input every emitter reads over one sweep of a read run: the facts beside the sweep
661
684
  * each kind family reads, every per-project one read in the same session as the sweep.
662
685
  */
663
- function emitterInputOver(host, scope, config, read, input, selfCheck) {
686
+ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
664
687
  const targetRoot = scope.target.path;
665
688
  const swept = sweptOf(read, input);
666
689
  const { projects } = read;
690
+ const members = judgedMembers(host, targetRoot, read);
667
691
  return {
668
692
  config,
669
693
  swept,
@@ -672,7 +696,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
672
696
  references: one.configured.references,
673
697
  accessors: one.extra.accessors,
674
698
  })), swept.exempt, input.mode),
675
- dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), membersWhollyRead(host, targetRoot, read)),
699
+ dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt),
676
700
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
677
701
  files: {
678
702
  tree: sourceTree(host, targetRoot),
@@ -691,17 +715,6 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
691
715
  intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
692
716
  };
693
717
  }
694
- /**
695
- * What every emitter reads of the run the scope and the configuration describe, swept
696
- * under the caller's marks: the run's sweep, the facts beside it each kind family reads,
697
- * and the sides of the target's declared cross-language edges that name a symbol of this
698
- * language. The run reads no suppression document, so the self-check family has nothing
699
- * to report.
700
- */
701
- export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
702
- const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
703
- return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
704
- }
705
718
  /**
706
719
  * Reads the run the scope and the configuration describe for its findings. The inline
707
720
  * directives, the ignore file's entries and the baseline's rows are bound before any
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { dirnamePath, joinPath } from "./paths.js";
9
9
  /** The name the mapper's manifest declares and every configuration's entry names. */
10
- export const MAPPER_PACKAGE = "deadset-ts-component-files";
10
+ const MAPPER_PACKAGE = "deadset-ts-component-files";
11
11
  /** The manifest of the mapper's package, which runs `command`. */
12
12
  export function mapperManifest(command) {
13
13
  return `${JSON.stringify({
@@ -50,10 +50,16 @@ export const RESOLVED_EXTENSIONS = [
50
50
  ".mjs",
51
51
  ".cjs",
52
52
  ];
53
+ /** The directory a package manager installs packages into, whose files are no project's own. */
54
+ const MODULES_DIR = "node_modules";
53
55
  /** The file's name, the last segment of its path. */
54
56
  function basename(path) {
55
57
  return path.slice(path.lastIndexOf("/") + 1);
56
58
  }
59
+ /** Whether one path is the project's own: below the target root and in no `node_modules`. */
60
+ function isProjectPath(root, path) {
61
+ return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
62
+ }
57
63
  function isRecord(value) {
58
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
59
65
  }
@@ -65,6 +71,160 @@ export function configurationModuleForm(name) {
65
71
  export function isConfigurationDocumentName(name) {
66
72
  return DOCUMENT_NAME.test(name);
67
73
  }
74
+ /** The flags a tool's command line names its configuration file with. */
75
+ const CONFIG_FLAGS = new Set(["--config", "-c"]);
76
+ /** The extensions of a module a configuration flag may name. */
77
+ const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
78
+ /** The characters that end a word outside quotes and separate nothing: blanks. */
79
+ const BLANK = /[ \t\r\f\v]/u;
80
+ /** The characters that end a word outside quotes and begin an operator: newline included. */
81
+ const OPERATOR = /[\n;&|()<>]/u;
82
+ /** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
83
+ const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
84
+ /**
85
+ * The words of one command line, as POSIX token recognition reads quotes, backslashes,
86
+ * line continuations, comments and operators. A word an expansion may change (an unquoted
87
+ * `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
88
+ * is computed, and so is the rest of a line whose quote is never closed. A computed word
89
+ * and each operator are undefined, so a flag's file is never read across them.
90
+ */
91
+ function shellWords(command) {
92
+ const words = [];
93
+ let word;
94
+ let computed = false;
95
+ const end = () => {
96
+ if (word !== undefined) {
97
+ words.push(computed ? undefined : word);
98
+ }
99
+ word = undefined;
100
+ computed = false;
101
+ };
102
+ for (let at = 0; at < command.length; at += 1) {
103
+ const char = command.charAt(at);
104
+ if (char === "\\" && command.charAt(at + 1) === "\n") {
105
+ at += 1;
106
+ }
107
+ else if (BLANK.test(char)) {
108
+ end();
109
+ }
110
+ else if (OPERATOR.test(char)) {
111
+ end();
112
+ words.push(undefined);
113
+ }
114
+ else if (char === "#" && word === undefined) {
115
+ const newline = command.indexOf("\n", at);
116
+ at = newline < 0 ? command.length : newline - 1;
117
+ }
118
+ else if (char === "\\") {
119
+ at += 1;
120
+ word = (word ?? "") + command.charAt(at);
121
+ }
122
+ else if (char === "'" || char === '"') {
123
+ const close = closingQuote(command, at);
124
+ if (close < 0) {
125
+ word ??= "";
126
+ computed = true;
127
+ break;
128
+ }
129
+ const quoted = char === "'"
130
+ ? { text: command.slice(at + 1, close), computed: false }
131
+ : doubleQuoted(command.slice(at + 1, close));
132
+ word = (word ?? "") + quoted.text;
133
+ computed ||= quoted.computed;
134
+ at = close;
135
+ }
136
+ else {
137
+ computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
138
+ word = (word ?? "") + char;
139
+ }
140
+ }
141
+ end();
142
+ return words;
143
+ }
144
+ /** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
145
+ function doubleQuoted(inner) {
146
+ let text = "";
147
+ let computed = false;
148
+ for (let at = 0; at < inner.length; at += 1) {
149
+ const char = inner.charAt(at);
150
+ const next = inner.charAt(at + 1);
151
+ if (char === "\\" && next === "\n") {
152
+ at += 1;
153
+ }
154
+ else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
155
+ text += next;
156
+ at += 1;
157
+ }
158
+ else {
159
+ computed ||= char === "$" || char === "`";
160
+ text += char;
161
+ }
162
+ }
163
+ return { text, computed };
164
+ }
165
+ /** The index of the quote that closes the one at `open`, or -1 where none does. */
166
+ function closingQuote(command, open) {
167
+ const quote = command.charAt(open);
168
+ for (let at = open + 1; at < command.length; at += 1) {
169
+ const char = command.charAt(at);
170
+ if (char === quote) {
171
+ return at;
172
+ }
173
+ if (quote === '"' && char === "\\") {
174
+ at += 1;
175
+ }
176
+ }
177
+ return -1;
178
+ }
179
+ /**
180
+ * The files the scripts of the manifest in one directory name after a configuration flag,
181
+ * `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
182
+ * the target root and in no `node_modules`.
183
+ */
184
+ function scriptConfigurations(host, root, dir) {
185
+ let scripts;
186
+ try {
187
+ const manifest = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
188
+ scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ if (!isRecord(scripts)) {
194
+ return [];
195
+ }
196
+ const found = [];
197
+ for (const command of Object.values(scripts)) {
198
+ if (typeof command !== "string") {
199
+ continue;
200
+ }
201
+ const words = shellWords(command);
202
+ words.forEach((word, at) => {
203
+ const named = word !== undefined && CONFIG_FLAGS.has(word)
204
+ ? words[at + 1]
205
+ : word === undefined
206
+ ? undefined
207
+ : /^--config=(.+)$/su.exec(word)?.[1];
208
+ const path = named === undefined ? undefined : resolvePath(dir, named);
209
+ if (path !== undefined &&
210
+ [".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
211
+ isProjectPath(root, path) &&
212
+ host.kindOf(path) === "file") {
213
+ found.push(path);
214
+ }
215
+ });
216
+ }
217
+ return found;
218
+ }
219
+ /** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
220
+ function readDocument(host, path) {
221
+ try {
222
+ return JSON.parse(host.readFile(path));
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ }
68
228
  /** Every string value one decoded JSON value holds at any depth. */
69
229
  export function documentStrings(value) {
70
230
  if (typeof value === "string") {
@@ -116,7 +276,8 @@ export function configurationFiles(host, targetRoot, files, parse) {
116
276
  dirs.add(dir);
117
277
  }
118
278
  }
119
- const documents = [];
279
+ const pending = [];
280
+ const named = [];
120
281
  for (const dir of [...dirs].sort()) {
121
282
  if (!holdsManifest(dir)) {
122
283
  continue;
@@ -130,24 +291,57 @@ export function configurationFiles(host, targetRoot, files, parse) {
130
291
  .sort();
131
292
  }
132
293
  catch {
133
- continue;
294
+ names = [];
134
295
  }
135
- for (const name of names) {
136
- const path = joinPath(dir, name);
137
- let value;
138
- try {
139
- value = JSON.parse(host.readFile(path));
296
+ pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
297
+ for (const path of scriptConfigurations(host, root, dir)) {
298
+ const held = files.byName.get(path);
299
+ if (path.endsWith(".json")) {
300
+ pending.push({ path, manifestDir: dir });
140
301
  }
141
- catch {
302
+ else if (held !== undefined) {
303
+ modules.push({ file: held, form: "a script's configuration flag" });
304
+ }
305
+ else {
306
+ named.push({ path, manifestDir: dir });
307
+ }
308
+ }
309
+ }
310
+ // A document reached from two manifests is read against each, so a chain is keyed by both.
311
+ const documents = new Map();
312
+ const unreadable = new Set();
313
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
314
+ let document = documents.get(next.path);
315
+ if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
316
+ continue;
317
+ }
318
+ if (document === undefined) {
319
+ const value = readDocument(host, next.path);
320
+ if (value === undefined) {
321
+ unreadable.add(next.path);
142
322
  continue;
143
323
  }
144
- documents.push({ path, strings: documentStrings(value) });
324
+ document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
325
+ documents.set(next.path, document);
326
+ }
327
+ document.manifestDirs.push(next.manifestDir);
328
+ for (const text of document.strings) {
329
+ const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
330
+ ? resolvePath(dirnamePath(next.path), text)
331
+ : undefined;
332
+ if (path?.endsWith(".json") === true &&
333
+ isProjectPath(root, path) &&
334
+ host.kindOf(path) === "file") {
335
+ pending.push({ path, manifestDir: next.manifestDir });
336
+ }
145
337
  }
146
338
  }
147
339
  return {
148
340
  modules,
149
- documents,
150
- outside: parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
341
+ documents: [...documents.values()],
342
+ outside: parse === undefined
343
+ ? []
344
+ : outsideModules(host, [...dirs], files, holdsManifest, parse, named),
151
345
  };
152
346
  }
153
347
  /** The relative specifiers one file's imports and re-exports name. */
@@ -195,8 +389,8 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
195
389
  * does not hold, and the files they import by a relative specifier, until no further
196
390
  * file joins. A file that cannot be read is skipped.
197
391
  */
198
- function outsideModules(host, dirs, files, holdsManifest, parse) {
199
- const pending = [];
392
+ function outsideModules(host, dirs, files, holdsManifest, parse, named) {
393
+ const pending = [...named];
200
394
  for (const dir of [...dirs].sort()) {
201
395
  if (!holdsManifest(dir)) {
202
396
  continue;
@@ -214,30 +408,36 @@ function outsideModules(host, dirs, files, holdsManifest, parse) {
214
408
  }
215
409
  pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
216
410
  }
217
- const found = [];
218
- const seen = new Set();
411
+ const found = new Map();
412
+ const unreadable = new Set();
219
413
  for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
220
- if (seen.has(next.path) || files.byName.has(next.path)) {
414
+ let outside = found.get(next.path);
415
+ if (unreadable.has(next.path) ||
416
+ files.byName.has(next.path) ||
417
+ outside?.manifestDirs.includes(next.manifestDir) === true) {
221
418
  continue;
222
419
  }
223
- seen.add(next.path);
224
- let text;
225
- try {
226
- text = host.readFile(next.path);
227
- }
228
- catch {
229
- continue;
420
+ if (outside === undefined) {
421
+ let text;
422
+ try {
423
+ text = host.readFile(next.path);
424
+ }
425
+ catch {
426
+ unreadable.add(next.path);
427
+ continue;
428
+ }
429
+ outside = { file: parse(next.path, text), manifestDirs: [] };
430
+ found.set(next.path, outside);
230
431
  }
231
- const file = parse(next.path, text);
232
- found.push({ file, manifestDir: next.manifestDir });
233
- for (const specifier of relativeSpecifiers(file)) {
432
+ outside.manifestDirs.push(next.manifestDir);
433
+ for (const specifier of relativeSpecifiers(outside.file)) {
234
434
  const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
235
435
  if (reached !== undefined) {
236
436
  pending.push({ path: reached, manifestDir: next.manifestDir });
237
437
  }
238
438
  }
239
439
  }
240
- return found;
440
+ return [...found.values()];
241
441
  }
242
442
  /** Every dependency the manifest in one directory declares, or none where it holds none to read. */
243
443
  export function declaredIn(host, dir) {
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "4.1.0",
5
+ corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
7
+ digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
8
8
  },
9
9
  gaps: [],
10
10
  };