@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
@@ -1,72 +1,59 @@
1
1
  /**
2
- * The exemption classes of the Contract's vocabulary, in its order: each class's name,
3
- * the languages it runs on, the confidence it is recorded at and, for a class that runs
4
- * on TypeScript, which private members it may retain.
5
- *
6
- * The rows are written out rather than read from the Contract at run time, for the
7
- * reason the fixed severity codes are, and a test holds them equal to the Contract
8
- * release this analyzer implements. A class name is a type, so a detector table or a
9
- * record naming a class the vocabulary does not hold, or one that runs on another
10
- * language only, does not compile.
2
+ * The exemption classes of the Contract's vocabulary, in its order: each class's name, its
3
+ * languages and, for a TypeScript class, which private members it may retain. The rows are
4
+ * written out, as the fixed severity codes are, and a test holds them equal to the Contract.
5
+ * A class name is a type, so a table or a record naming a class outside the vocabulary, or
6
+ * one of another language only, does not compile.
11
7
  */
12
8
  /** Every class of the vocabulary, in the order the Contract lists them. */
13
9
  export const EXEMPTION_CLASSES = [
14
10
  {
15
11
  class: "interface-satisfaction",
16
12
  languages: ["go", "ts"],
17
- confidence: "certain",
18
13
  typescriptVisibility: { private: false, privateName: false },
19
14
  },
20
- { class: "encoding-reflection", languages: ["go"], confidence: "certain" },
21
- { class: "format-verb-contract", languages: ["go"], confidence: "certain" },
22
- { class: "errors-duck-typing", languages: ["go"], confidence: "certain" },
15
+ { class: "encoding-reflection", languages: ["go"] },
16
+ { class: "format-verb-contract", languages: ["go"] },
17
+ { class: "errors-duck-typing", languages: ["go"] },
23
18
  {
24
19
  class: "enum-group",
25
20
  languages: ["go", "ts"],
26
- confidence: "certain",
27
21
  typescriptVisibility: { private: false, privateName: false },
28
22
  },
29
23
  {
30
24
  class: "generated-file",
31
25
  languages: ["go", "ts"],
32
- confidence: "certain",
33
26
  typescriptVisibility: { private: true, privateName: false },
34
27
  },
35
- { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
28
+ { class: "linkname-cgo-asm-plugin", languages: ["go"] },
36
29
  {
37
30
  class: "template-field",
38
31
  languages: ["go", "ts"],
39
- confidence: "possible",
40
32
  typescriptVisibility: { private: true, privateName: false },
41
33
  },
42
34
  {
43
35
  class: "reflective-lookup",
44
36
  languages: ["go", "ts"],
45
- confidence: "possible",
46
37
  typescriptVisibility: { private: true, privateName: false },
47
38
  },
48
39
  {
49
40
  class: "decorator",
50
41
  languages: ["ts"],
51
- confidence: "certain",
52
42
  typescriptVisibility: { private: true, privateName: false },
53
43
  },
54
44
  {
55
45
  class: "injection-container",
56
46
  languages: ["ts"],
57
- confidence: "certain",
58
47
  typescriptVisibility: { private: true, privateName: false },
59
48
  },
60
49
  {
61
50
  class: "framework-lifecycle",
62
51
  languages: ["ts"],
63
- confidence: "certain",
64
52
  typescriptVisibility: { private: true, privateName: false },
65
53
  },
66
54
  {
67
55
  class: "serialization-contract",
68
56
  languages: ["ts"],
69
- confidence: "certain",
70
57
  typescriptVisibility: { private: true, privateName: false },
71
58
  },
72
59
  ];
@@ -80,23 +80,11 @@ function detected(exemptionClass, detect, input) {
80
80
  .map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
81
81
  }
82
82
  }
83
- /**
84
- * The records one project's detectors found, each class in the vocabulary's order and
85
- * none of a switched-off class. Evidence on a private member the class's row says it
86
- * cannot retain is dropped first, whatever the detector found. Under a production mode
87
- * a record whose evidence is written in a test file holds nothing, as a reference a
88
- * test file makes is none there.
89
- */
90
- export function computeExemptions(input, detectors, holding) {
91
- return detectExemptions(input, detectors, holding).records;
92
- }
93
- /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
94
- export function detectExemptions(input, detectors, holding) {
95
- return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
96
- }
97
83
  /**
98
84
  * Every record one project's detectors found, one per piece of evidence, before the
99
- * mode decides which of them hold: what {@link heldRecords} reads.
85
+ * mode decides which of them hold: what {@link heldRecords} reads. Classes run in the
86
+ * vocabulary's order and a switched-off class runs not at all; evidence on a private
87
+ * member the class's row says it cannot retain is dropped, whatever the detector found.
100
88
  */
101
89
  export function evidenceRecords(input, detectors, disabled) {
102
90
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
@@ -64,6 +64,29 @@ function reachabilityClasses(input) {
64
64
  return published.has(id) ? outside : "certain";
65
65
  };
66
66
  }
67
+ /**
68
+ * The confidence ceiling of a test of dead code: the lowest reachability class among the
69
+ * target symbols it references, because its deadness is theirs, whether or not they share
70
+ * its component. Every other declaration has none below `certain`.
71
+ */
72
+ function testCeilings(input, classOf) {
73
+ const union = input.swept.matrix.union;
74
+ const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
75
+ return (id) => {
76
+ let lowest = "certain";
77
+ if (!tests.has(id)) {
78
+ return lowest;
79
+ }
80
+ for (const edge of union.out[union.at(id)] ?? []) {
81
+ const target = union.symbols[edge.to];
82
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
83
+ if (target !== undefined && !testCode && union.subject[edge.to] === true) {
84
+ lowest = capped(lowest, classOf(target.id));
85
+ }
86
+ }
87
+ return lowest;
88
+ };
89
+ }
67
90
  /**
68
91
  * The component each dead declaration of the run falls in, and none for a live one, with
69
92
  * its members listed in the canonical order where the run lists a component in full.
@@ -108,12 +131,11 @@ function findingComponents(swept, cascade) {
108
131
  }
109
132
  /**
110
133
  * Every finding of the run completed, in the order given. A dead declaration carries its
111
- * candidate's relation, counts, configurations and component; a live one carries no
112
- * relation and holds where it is declared; a file or a manifest row is `certain` and
113
- * holds in every configuration. A subject in no dead component gets one of its own,
114
- * numbered past the computed ones in finding order, so no two families mint one; nothing
115
- * falls with it, so its deletion removes no line. `kinds` is the vocabulary each code's
116
- * row is read from, the shipped one unless a caller supplies its own.
134
+ * candidate's relation, counts, configurations and component; a live one carries no relation and
135
+ * holds where it is declared; a file or a manifest row carries the class its emitter states,
136
+ * `certain` by default, and holds in every configuration. A subject in no dead component gets one
137
+ * of its own, numbered past the computed ones in finding order so no two families mint one, whose
138
+ * deletion removes no line. `kinds` defaults to the shipped vocabulary.
117
139
  */
118
140
  export function completed(input, findings, kinds = KINDS) {
119
141
  const { swept } = input;
@@ -121,6 +143,7 @@ export function completed(input, findings, kinds = KINDS) {
121
143
  const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
122
144
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
123
145
  const classOf = reachabilityClasses(input);
146
+ const ceilingOf = testCeilings(input, classOf);
124
147
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
125
148
  let minted = swept.sweep.components.length;
126
149
  const dead = new Set(swept.sweep.components.map((component) => component.id));
@@ -172,8 +195,12 @@ export function completed(input, findings, kinds = KINDS) {
172
195
  const counts = candidate === undefined
173
196
  ? union.made[at]
174
197
  : { production: candidate.productionRefs, test: candidate.testRefs };
175
- const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
198
+ const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
176
199
  const generated = input.files.generated?.has(finding.position.path) === true;
200
+ const fixability = generated ? "none" : row.fixability;
201
+ const lastUses = fixability === "deletable" && candidate !== undefined
202
+ ? input.dependencies.lastUses.get(candidate.id)
203
+ : undefined;
177
204
  return {
178
205
  code: finding.code,
179
206
  kind: row.name,
@@ -181,7 +208,7 @@ export function completed(input, findings, kinds = KINDS) {
181
208
  position: finding.position,
182
209
  symbol: finding.symbol,
183
210
  reachabilityClass,
184
- confidence: capped(reachabilityClass, row.maxClass),
211
+ confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
185
212
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
186
213
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
187
214
  generated,
@@ -192,10 +219,12 @@ export function completed(input, findings, kinds = KINDS) {
192
219
  ? swept.matrix.configurations
193
220
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
194
221
  consumersLoaded: input.boundary.consumers.loaded,
195
- fixability: generated ? "none" : row.fixability,
222
+ fixability,
196
223
  severity: severityOf(input, finding.code, row.defaultSeverity),
197
224
  message: finding.message,
198
- details: finding.details ?? {},
225
+ details: lastUses === undefined
226
+ ? (finding.details ?? {})
227
+ : { ...finding.details, removesLastUseOf: lastUses },
199
228
  };
200
229
  });
201
230
  return componentCapped(done, dead);
@@ -236,6 +265,7 @@ function dialed(finding, minConfidence) {
236
265
  export function dialsOf(input, findings) {
237
266
  const minConfidence = input.config.analysis.minConfidence;
238
267
  const classOf = reachabilityClasses(input);
268
+ const ceilingOf = testCeilings(input, classOf);
239
269
  const components = new Set(findings
240
270
  .filter((finding) => finding.component.root && dialed(finding, minConfidence))
241
271
  .map((finding) => finding.component.id));
@@ -245,7 +275,7 @@ export function dialsOf(input, findings) {
245
275
  return (row !== undefined &&
246
276
  dialed({
247
277
  severity: severityOf(input, code, row.defaultSeverity),
248
- confidence: capped(classOf(id), row.maxClass),
278
+ confidence: capped(capped(classOf(id), row.maxClass), ceilingOf(id)),
249
279
  }, minConfidence));
250
280
  },
251
281
  withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
@@ -1,3 +1,4 @@
1
+ import { declarationKey } from "../dependencies.js";
1
2
  import { renderRef } from "../ref.js";
2
3
  const UNUSED_DEPENDENCY = "DS1601";
3
4
  const MESSAGES = {
@@ -5,38 +6,14 @@ const MESSAGES = {
5
6
  "dev-dependency": "no import in the target needs the declared development dependency",
6
7
  "peer-dependency": "no import in the target needs the declared peer dependency",
7
8
  };
8
- /**
9
- * The dependencies the target needs: every package the projects' uses name, every
10
- * declared dependency that ships a command, which a script, a workflow or a shell
11
- * runs outside anything the analysis reads, and every peer a needed package
12
- * requires, which the target satisfies by declaring it.
13
- */
14
- function neededBy(dependencies) {
15
- const needed = new Set(dependencies.needed);
16
- for (const [name, installed] of dependencies.installed) {
17
- if (installed.command) {
18
- needed.add(name);
19
- }
20
- }
21
- const pending = [...needed];
22
- for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
23
- for (const peer of dependencies.installed.get(name)?.peers ?? []) {
24
- if (!needed.has(peer)) {
25
- needed.add(peer);
26
- pending.push(peer);
27
- }
28
- }
29
- }
30
- return needed;
31
- }
32
9
  /**
33
10
  * A dependency, development dependency or peer dependency of a manifest the run reads
34
11
  * that the target does not need, at the position of its key.
35
12
  */
36
13
  function unusedDependencies(dependencies) {
37
- const needed = neededBy(dependencies);
38
14
  return dependencies.declared
39
- .filter((dependency) => !needed.has(dependency.name))
15
+ .filter((dependency) => !dependencies.needed.has(dependency.name) &&
16
+ !dependencies.ran.has(declarationKey(dependency)))
40
17
  .map((dependency) => ({
41
18
  code: UNUSED_DEPENDENCY,
42
19
  position: dependency.position,
@@ -52,6 +29,9 @@ function unusedDependencies(dependencies) {
52
29
  },
53
30
  message: MESSAGES[dependency.section],
54
31
  details: { dependencyClass: dependency.section },
32
+ ...(dependencies.unbuilt.has(dependency.position.path)
33
+ ? { reachabilityClass: "possible" }
34
+ : {}),
55
35
  }));
56
36
  }
57
37
  /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
@@ -130,7 +130,3 @@ function withheldOf(input, candidates) {
130
130
  }
131
131
  return counts;
132
132
  }
133
- /** Every finding the run reports, each family's in table order. */
134
- export function findingsOf(input) {
135
- return decidedFindings(input).findings;
136
- }
@@ -1,5 +1,6 @@
1
1
  import { REPOSITORY_DOCUMENT } from "../config.js";
2
- import { STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
2
+ import { FIXED_SEVERITY_CODES } from "../kinds.js";
3
+ import { isRowCode, STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
3
4
  /** The code a configured root that matches nothing is reported under. */
4
5
  const UNMATCHED_ROOT = "DS1704";
5
6
  /** The code a configured declaration that names nothing is reported under. */
@@ -12,13 +13,6 @@ const SUPPRESSION_SUBJECT = "suppression";
12
13
  const ROOT_SUBJECT = "root";
13
14
  /** The fixed position of a finding about a document rather than about a line of one. */
14
15
  const DOCUMENT_POSITION = 1;
15
- /** A run with nothing for the self-check family to report. */
16
- export const NO_SELF_CHECK = {
17
- refusals: [],
18
- unmatchedRoots: [],
19
- rootsDocument: "",
20
- unmatchedDeclarations: [],
21
- };
22
16
  /** The words a message names a mechanism by. */
23
17
  const WRITTEN = {
24
18
  inline: "inline directive",
@@ -68,7 +62,7 @@ function suppressionRef(symbol, path, files) {
68
62
  return symbol === "" ? (files(path) ?? path) : symbol;
69
63
  }
70
64
  /** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
71
- export function refusedSuppressions(refusals, files) {
65
+ function refusedSuppressions(refusals, files) {
72
66
  return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === SUPPRESSION_WITHOUT_REASON
73
67
  ? "carries no reason"
74
68
  : "names a symbol and no path"}`, { mechanism: refused.mechanism, entry: writtenEntry(refused) }));
@@ -93,7 +87,17 @@ export function staleSuppressions(stale, files) {
93
87
  const reports = [...new Set(records.flatMap((one) => one.reports))]
94
88
  .filter((code) => !codes.includes(code))
95
89
  .sort();
96
- const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
90
+ const rows = codes.filter(isRowCode);
91
+ const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
92
+ const remedy = settable.length === 0
93
+ ? "fix what the finding names instead"
94
+ : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
95
+ const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
96
+ const instead = rows.length > 0
97
+ ? `, because ${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}${declared === "" ? "" : `; ${declared}`}`
98
+ : declared === ""
99
+ ? ""
100
+ : `, and ${declared}`;
97
101
  return {
98
102
  code: STALE_SUPPRESSION,
99
103
  mechanism: first.mechanism,
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The files the module scripts of the target's HTML files load. An HTML file is read as
3
- * text and no markup is parsed, so a `<script>` inside a comment is read as well.
2
+ * The files the scripts of the target's HTML files load: a module script's, and a
3
+ * classic script's that names its file by `src`. An HTML file is read as text and no
4
+ * markup is parsed, so a `<script>` inside a comment is read as well.
4
5
  */
5
6
  import { isExportDeclaration, isImportDeclaration, isStringLiteral, } from "@typescript/native/unstable/ast";
6
7
  import { resolveRelativeSpecifier } from "./configuration-files.js";
@@ -15,6 +16,8 @@ const SCRIPT_END = /<\/script\s*>/iu;
15
16
  const ATTRIBUTE = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/gu;
16
17
  /** A value that names a resource on another origin, or by another scheme. */
17
18
  const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
19
+ /** The `type` values a classic script carries, compared in lowercase. */
20
+ const CLASSIC_TYPES = new Set(["text/javascript", "application/javascript"]);
18
21
  /** The prefixes an inline script's specifier names a file of the target with. */
19
22
  const FILE_PREFIXES = ["/", "./", "../"];
20
23
  /** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
@@ -62,18 +65,22 @@ function specifiersOf(parse, fileName, content) {
62
65
  });
63
66
  }
64
67
  /**
65
- * Every value a module script of one HTML file names a file by: a `src` attribute's
66
- * value, or each specifier an inline script imports or re-exports that starts with `/`,
67
- * `./` or `../`.
68
+ * Every value a script of one HTML file names a file by: the `src` attribute's value of a
69
+ * module script or of a classic one, or each specifier an inline module script imports or
70
+ * re-exports that starts with `/`, `./` or `../`.
68
71
  */
69
72
  function scriptValues(parse, path, text) {
70
73
  const values = [];
71
74
  for (const match of text.matchAll(SCRIPT_TAG)) {
72
75
  const held = attributes(match[1] ?? "");
73
- if (held.get("type") !== "module") {
76
+ const type = held.get("type");
77
+ const src = held.get("src");
78
+ if (type !== "module") {
79
+ if (src !== undefined && (type === undefined || CLASSIC_TYPES.has(type.toLowerCase()))) {
80
+ values.push(src);
81
+ }
74
82
  continue;
75
83
  }
76
- const src = held.get("src");
77
84
  if (src !== undefined) {
78
85
  values.push(src);
79
86
  continue;
@@ -102,11 +109,11 @@ function manifestDirectory(host, root, dir) {
102
109
  return root;
103
110
  }
104
111
  /**
105
- * The own files the module scripts of every HTML file below the target root load. A value
112
+ * The own files the scripts of every HTML file below the target root load. A value
106
113
  * starting with a single `/` is read against the nearest manifest's directory, any other
107
- * against the HTML file's own, and either resolves as a configuration file's relative
108
- * specifier does. A value naming another origin or scheme, or no source file of the
109
- * program, loads nothing here.
114
+ * against the HTML file's own, as a configuration file's relative specifier. One naming no
115
+ * source file is read back through the project's emit mapping, and a `/` one also against
116
+ * an output directory that is the HTML file's own. Another origin or scheme loads nothing.
110
117
  */
111
118
  export function htmlEntries(host, targetRoot, files, parse) {
112
119
  const root = resolvePath(host.workingDirectory(), targetRoot);
@@ -124,12 +131,20 @@ export function htmlEntries(host, targetRoot, files, parse) {
124
131
  if (ELSEWHERE.test(value)) {
125
132
  continue;
126
133
  }
127
- const reached = value.startsWith("/")
128
- ? resolveRelativeSpecifier(host, manifestDirectory(host, root, dir), `.${value}`)
129
- : resolveRelativeSpecifier(host, dir, value);
134
+ const base = value.startsWith("/") ? manifestDirectory(host, root, dir) : dir;
135
+ const relative = value.startsWith("/") ? `.${value}` : value;
136
+ const reached = resolveRelativeSpecifier(host, base, relative);
130
137
  const file = reached === undefined ? undefined : files.byName.get(reached);
131
138
  if (file !== undefined) {
132
139
  found.push({ file, source: value });
140
+ continue;
141
+ }
142
+ const served = value.startsWith("/")
143
+ ? files.outputDirs.filter((outDir) => outDir === dir)
144
+ : [];
145
+ const emitting = [base, ...served].flatMap((against) => files.named(resolvePath(against, relative)));
146
+ for (const one of new Set(emitting)) {
147
+ found.push({ file: one, source: value });
133
148
  }
134
149
  }
135
150
  }