@cplieger/deadset-ts 6.1.0 → 7.0.0-dev.1

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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
@@ -53,6 +53,7 @@ import {
53
53
  type HelperReading,
54
54
  type MoveReading,
55
55
  type OptionsCall,
56
+ type PackageKey,
56
57
  type ShortNameReading,
57
58
  } from "./convention-rows.ts";
58
59
  import { documentStrings } from "./configuration-files.ts";
@@ -873,6 +874,60 @@ function helperUses(
873
874
  .map((reading) => reading.package);
874
875
  }
875
876
 
877
+ /**
878
+ * The dependencies one manifest declares that an applied row's package keys use: each
879
+ * whose name a string under a member of a key's name equals, or begins followed by `/` or
880
+ * by `:` and a name. A member's own name is no such string.
881
+ */
882
+ function packageKeyUses(
883
+ host: Host,
884
+ parse: ParseSource,
885
+ dir: string,
886
+ manifest: unknown,
887
+ keys: readonly PackageKey[],
888
+ ): readonly string[] {
889
+ const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
890
+ const held = isRecord(manifest) ? manifest[section] : undefined;
891
+ return isRecord(held) ? Object.keys(held) : [];
892
+ });
893
+ const strings: string[] = [];
894
+ const collect = (node: Node): void => {
895
+ const text = literalText(node);
896
+ if (text !== undefined) {
897
+ strings.push(text);
898
+ } else if (isPropertyAssignment(node)) {
899
+ collect(node.initializer);
900
+ } else {
901
+ node.forEachChild(collect);
902
+ }
903
+ };
904
+ for (const { key, files } of keys) {
905
+ const visit = (node: Node): void => {
906
+ if (isPropertyAssignment(node) && keyOf(node) === key) {
907
+ collect(node.initializer);
908
+ }
909
+ node.forEachChild(visit);
910
+ };
911
+ for (const { file } of readingFiles(host, parse, dir, files)) {
912
+ const values = declaredValues(file);
913
+ for (const one of defaultExports(file)) {
914
+ const object = objectOf(one, values, true);
915
+ if (object !== undefined) {
916
+ visit(object);
917
+ }
918
+ }
919
+ }
920
+ }
921
+ return declared.filter((name) =>
922
+ strings.some(
923
+ (text) =>
924
+ text === name ||
925
+ text.startsWith(`${name}/`) ||
926
+ (text.startsWith(`${name}:`) && text.length > name.length + 1),
927
+ ),
928
+ );
929
+ }
930
+
876
931
  /** Two strings ordered bytewise. */
877
932
  function compare(a: string, b: string): number {
878
933
  if (a === b) {
@@ -959,6 +1014,10 @@ export function readConventions(
959
1014
  for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
960
1015
  uses.add(name);
961
1016
  }
1017
+ const keys = row.packageKeys ?? [];
1018
+ for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
1019
+ uses.add(name);
1020
+ }
962
1021
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
963
1022
  uses.add(name);
964
1023
  }
@@ -1,19 +1,22 @@
1
1
  /**
2
- * The dependencies the target's manifest and its workspace members' manifests declare, and
3
- * what the projects' own files need from outside the target. A file needs a package it
4
- * imports, re-exports from, augments or references the types of, by name or through the
5
- * file the specifier resolves to; a compiler configuration needs one it names, and a
6
- * configuration file a tool loads needs each dependency its strings spell. A package one
7
- * dependency pulls in for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and what the
3
+ * projects' own files need from outside the target. A file needs a package it imports, re-exports
4
+ * from, augments, references the types of or asks the runtime to resolve, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a configuration
6
+ * file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
7
+ * itself is that dependency's need, not the target's.
8
8
  */
9
9
 
10
10
  import {
11
+ isCallExpression,
11
12
  isExportDeclaration,
12
13
  isExternalModuleReference,
13
14
  isIdentifier,
14
15
  isImportDeclaration,
15
16
  isImportEqualsDeclaration,
17
+ isMetaProperty,
16
18
  isNamespaceExport,
19
+ isPropertyAccessExpression,
17
20
  isShorthandPropertyAssignment,
18
21
  isStringLiteral,
19
22
  isNoSubstitutionTemplateLiteral,
@@ -32,11 +35,13 @@ import {
32
35
  import type { FindingPosition } from "./finding.ts";
33
36
  import type { Host } from "./host.ts";
34
37
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
38
+ import { scriptTokens } from "./manifest.ts";
35
39
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
36
40
  import type { DependencySection, Module } from "./ref.ts";
37
41
  import { UNANSWERED } from "./query.ts";
38
42
  import type { Handle, ProjectView } from "./session.ts";
39
43
  import { sourceFilesOf } from "./source-files.ts";
44
+ import { workflowTokens } from "./workflow-steps.ts";
40
45
 
41
46
  const MANIFEST = "package.json";
42
47
  const MODULES_DIR = "node_modules";
@@ -68,16 +73,14 @@ export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
68
73
  return {
69
74
  packages: new Set(packages),
70
75
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
71
- unanswered: false,
76
+ unanswered: [],
72
77
  };
73
78
  }
74
79
 
75
80
  /** What the installed copy of one declared dependency states about itself. */
76
81
  export interface InstalledDependency {
77
- /** Whether its manifest declares a command. */
78
- readonly command: boolean;
79
- /** The peer dependencies its manifest requires: every peer not marked optional. */
80
- readonly peers: readonly string[];
82
+ /** The commands its manifest's `bin` declares, by name. */
83
+ readonly commands: readonly string[];
81
84
  }
82
85
 
83
86
  /** What one project's own files and configuration need from outside the target. */
@@ -91,20 +94,29 @@ export interface ProjectNeeds {
91
94
  */
92
95
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
93
96
  /**
94
- * Whether the checker left a module specifier unanswered, so the project may use any
95
- * dependency the manifest declares.
97
+ * The files writing a module specifier the checker left unanswered, each of which may
98
+ * use any dependency a manifest at or above it declares.
96
99
  */
97
- readonly unanswered: boolean;
100
+ readonly unanswered: readonly string[];
98
101
  }
99
102
 
100
103
  /** The run's dependency evidence. */
101
104
  export interface Dependencies {
102
105
  /** Every declared dependency, manifest by manifest, in the order each writes them. */
103
106
  readonly declared: readonly DeclaredDependency[];
104
- /** The installed copy of each declared dependency the run found, by name. */
105
- readonly installed: ReadonlyMap<string, InstalledDependency>;
106
107
  /** Every package some project needs. */
107
108
  readonly needed: ReadonlySet<string>;
109
+ /**
110
+ * The declared dependencies whose command a script entry of the manifest declaring them,
111
+ * or a workflow step for the target's manifest, runs, each by {@link declarationKey}.
112
+ */
113
+ readonly ran: ReadonlySet<string>;
114
+ /**
115
+ * The manifests, by their path below the target root, whose directory holds source only
116
+ * in configurations the run could not build, so a dependency they declare that nothing
117
+ * needs is reported at `possible`.
118
+ */
119
+ readonly unbuilt: ReadonlySet<string>;
108
120
  /**
109
121
  * Per deletion candidate, the declared dependencies whose last use in the target is
110
122
  * written inside it, sorted bytewise; a candidate holding no last use has no entry.
@@ -325,9 +337,11 @@ export function projectNeeds<Brand>(
325
337
  };
326
338
  use([PROJECT_OWNER], packages);
327
339
 
328
- let unanswered = false;
340
+ const unanswered: string[] = [];
329
341
  for (const file of project.ownSourceFiles()) {
330
- unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
342
+ if (fileNeeds(project, held, file, programPackages, use)) {
343
+ unanswered.push(file.fileName);
344
+ }
331
345
  }
332
346
 
333
347
  const beside = new Map<string, readonly string[]>();
@@ -491,9 +505,43 @@ function fileNeeds<Brand>(
491
505
  for (const reference of file.typeReferenceDirectives) {
492
506
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
493
507
  }
508
+ for (const specifier of resolvedSpecifiers(file)) {
509
+ const named = packageOfSpecifier(specifier.text);
510
+ if (named !== undefined) {
511
+ use(ownersOf(file, held, specifier), [named]);
512
+ }
513
+ }
494
514
  return resolved.slice(0, specifiers.length).includes(UNANSWERED);
495
515
  }
496
516
 
517
+ /**
518
+ * The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
519
+ * or `require.resolve`, each of which needs the package it names installed.
520
+ */
521
+ function resolvedSpecifiers(file: SourceFile): readonly Texted[] {
522
+ const found: Texted[] = [];
523
+ if (!file.text.includes(".resolve(")) {
524
+ return found;
525
+ }
526
+ const visit = (node: Node): void => {
527
+ if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
528
+ const callee = node.expression;
529
+ const [argument] = node.arguments;
530
+ const receiver = callee.expression;
531
+ const resolving =
532
+ callee.name.text === "resolve" &&
533
+ ((isMetaProperty(receiver) && receiver.name.text === "meta") ||
534
+ (isIdentifier(receiver) && receiver.text === "require"));
535
+ if (resolving && argument !== undefined && isLiteralText(argument)) {
536
+ found.push(argument);
537
+ }
538
+ }
539
+ node.forEachChild(visit);
540
+ };
541
+ visit(file);
542
+ return found;
543
+ }
544
+
497
545
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
498
546
  function valueEnd(text: string, at: number): number {
499
547
  const first = text.charAt(at);
@@ -585,21 +633,17 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
585
633
  for (let dir = root; ; dir = dirnamePath(dir)) {
586
634
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
587
635
  if (read !== undefined && isRecord(read.value)) {
588
- const manifest = read.value;
589
- const bin = manifest["bin"];
590
- const peers = manifest["peerDependencies"];
591
- const meta = isRecord(manifest["peerDependenciesMeta"])
592
- ? manifest["peerDependenciesMeta"]
593
- : {};
636
+ const bin = read.value["bin"];
637
+ // A string `bin` names one command, the package's name without its scope.
594
638
  return {
595
- command:
596
- (typeof bin === "string" && bin !== "") || (isRecord(bin) && Object.keys(bin).length > 0),
597
- peers: isRecord(peers)
598
- ? Object.keys(peers).filter((peer) => {
599
- const about = meta[peer];
600
- return !(isRecord(about) && about["optional"] === true);
601
- })
602
- : [],
639
+ commands:
640
+ typeof bin === "string"
641
+ ? bin === ""
642
+ ? []
643
+ : [name.slice(name.indexOf("/") + 1)]
644
+ : isRecord(bin)
645
+ ? Object.keys(bin)
646
+ : [],
603
647
  };
604
648
  }
605
649
  if (dirnamePath(dir) === dir) {
@@ -639,12 +683,37 @@ function declaredIn(
639
683
  return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
640
684
  }
641
685
 
686
+ /** The key one declared dependency is known by: its manifest's path and its name. */
687
+ export function declarationKey(dependency: DeclaredDependency): string {
688
+ return `${dependency.position.path}\u0000${dependency.name}`;
689
+ }
690
+
691
+ /** Every token of every value of one manifest's `scripts` member. */
692
+ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string> {
693
+ const scripts = manifest["scripts"];
694
+ const found = new Set<string>();
695
+ if (isRecord(scripts)) {
696
+ for (const command of Object.values(scripts)) {
697
+ if (typeof command === "string") {
698
+ scriptTokens(command).forEach((token) => found.add(token));
699
+ }
700
+ }
701
+ }
702
+ return found;
703
+ }
704
+
705
+ /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
706
+ function isInside(path: string, dir: string): boolean {
707
+ return dir === "" || path.startsWith(`${dir}/`);
708
+ }
709
+
642
710
  /**
643
711
  * The run's dependency evidence: the target's manifest and each workspace member's in
644
712
  * `members` below the target root, read with the position of every dependency each
645
- * declares, the installed copy of each found from its manifest's directory, what the
646
- * projects need, and the last uses each deletion candidate holds. A manifest the
647
- * program cannot read declares nothing.
713
+ * declares, what the projects and the commands its scripts and workflow steps run need,
714
+ * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
715
+ * in configurations the run could not build. A manifest the program cannot read declares
716
+ * nothing.
648
717
  */
649
718
  export function dependenciesOf(
650
719
  host: Host,
@@ -652,30 +721,43 @@ export function dependenciesOf(
652
721
  perProject: readonly ProjectNeeds[],
653
722
  candidates: readonly string[],
654
723
  members: readonly string[] = [],
724
+ unbuiltMembers: readonly string[] = [],
655
725
  ): Dependencies {
656
726
  const root = resolvePath(host.workingDirectory(), targetRoot);
657
727
  const scopeOf = packageScope(host, targetRoot);
658
728
  const manifests = new Map<string, string>([[root, MANIFEST]]);
659
- for (const dir of members) {
729
+ const unbuilt = new Set<string>();
730
+ for (const dir of [...members, ...unbuiltMembers]) {
660
731
  const below = relativePath(root, dir);
661
732
  if (below !== undefined && !manifests.has(dir)) {
662
733
  manifests.set(dir, `${below}/${MANIFEST}`);
663
734
  }
664
735
  }
736
+ for (const dir of unbuiltMembers) {
737
+ const path = manifests.get(dir);
738
+ if (path !== undefined && dir !== root) {
739
+ unbuilt.add(path);
740
+ }
741
+ }
742
+ const workflow = new Set(workflowTokens(host, targetRoot));
665
743
  const declared: DeclaredDependency[] = [];
666
- const installed = new Map<string, InstalledDependency>();
744
+ const ran = new Set<string>();
667
745
  for (const [dir, path] of manifests) {
668
746
  const read = readJSON(host, joinPath(dir, MANIFEST));
669
747
  if (read === undefined || !isRecord(read.value)) {
670
748
  continue;
671
749
  }
750
+ const tokens = scriptTokensOf(read.value);
672
751
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
673
752
  declared.push(dependency);
674
- if (!installed.has(dependency.name)) {
675
- const found = installedOf(host, dir, dependency.name);
676
- if (found !== undefined) {
677
- installed.set(dependency.name, found);
678
- }
753
+ const found = installedOf(host, dir, dependency.name);
754
+ if (found === undefined) {
755
+ continue;
756
+ }
757
+ const runs = (command: string): boolean =>
758
+ tokens.has(command) || (dir === root && workflow.has(command));
759
+ if (found.commands.some(runs)) {
760
+ ran.add(declarationKey(dependency));
679
761
  }
680
762
  }
681
763
  }
@@ -685,9 +767,15 @@ export function dependenciesOf(
685
767
  const needed = new Set<string>();
686
768
  const uses = new Map<string, Set<string>>();
687
769
  for (const project of perProject) {
688
- // A specifier the checker left unanswered may resolve to any declared dependency,
689
- // and what the configuration needs is never deleted, so it is needed there.
690
- const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
770
+ // A specifier the checker left unanswered may resolve to any dependency a manifest at
771
+ // or above its file declares, and what the configuration needs is never deleted.
772
+ const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
773
+ const unknown = declared
774
+ .filter((dependency) => {
775
+ const dir = dirnamePath(dependency.position.path);
776
+ return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
777
+ })
778
+ .map((dependency) => dependency.name);
691
779
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
692
780
  for (const [id, used] of [...project.uses, ...held]) {
693
781
  const merged = uses.get(id) ?? new Set<string>();
@@ -711,5 +799,5 @@ export function dependenciesOf(
711
799
  );
712
800
  }
713
801
  }
714
- return { declared, installed, needed, lastUses };
802
+ return { declared, needed, ran, unbuilt, lastUses };
715
803
  }
package/src/finding.ts CHANGED
@@ -83,6 +83,8 @@ export interface Finding {
83
83
  readonly message: string;
84
84
  /** Absent where the code carries nothing. */
85
85
  readonly details?: FindingDetails;
86
+ /** The reachability class of a subject that is no declaration, where it is not `certain`. */
87
+ readonly reachabilityClass?: Confidence;
86
88
  }
87
89
 
88
90
  /** The dead component a finding names, as the finding schema spells its members. */
@@ -136,12 +136,11 @@ function findingComponents(
136
136
 
137
137
  /**
138
138
  * Every finding of the run completed, in the order given. A dead declaration carries its
139
- * candidate's relation, counts, configurations and component; a live one carries no
140
- * relation and holds where it is declared; a file or a manifest row is `certain` and
141
- * holds in every configuration. A subject in no dead component gets one of its own,
142
- * numbered past the computed ones in finding order, so no two families mint one; nothing
143
- * falls with it, so its deletion removes no line. `kinds` is the vocabulary each code's
144
- * row is read from, the shipped one unless a caller supplies its own.
139
+ * candidate's relation, counts, configurations and component; a live one carries no relation and
140
+ * holds where it is declared; a file or a manifest row carries the class its emitter states,
141
+ * `certain` by default, and holds in every configuration. A subject in no dead component gets one
142
+ * of its own, numbered past the computed ones in finding order so no two families mint one, whose
143
+ * deletion removes no line. `kinds` defaults to the shipped vocabulary.
145
144
  */
146
145
  export function completed(
147
146
  input: EmitterInput,
@@ -210,7 +209,8 @@ export function completed(
210
209
  candidate === undefined
211
210
  ? union.made[at]
212
211
  : { production: candidate.productionRefs, test: candidate.testRefs };
213
- const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
212
+ const reachabilityClass =
213
+ judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
214
214
  const generated = input.files.generated?.has(finding.position.path) === true;
215
215
  return {
216
216
  code: finding.code,
@@ -1,4 +1,4 @@
1
- import type { Dependencies } from "../dependencies.ts";
1
+ import { declarationKey, type Dependencies } from "../dependencies.ts";
2
2
  import type { Finding } from "../finding.ts";
3
3
  import { renderRef, type DependencySection } from "../ref.ts";
4
4
  import type { Emitter } from "./emitter.ts";
@@ -11,39 +11,17 @@ const MESSAGES: Readonly<Record<DependencySection, string>> = {
11
11
  "peer-dependency": "no import in the target needs the declared peer dependency",
12
12
  };
13
13
 
14
- /**
15
- * The dependencies the target needs: every package the projects' uses name, every
16
- * declared dependency that ships a command, which a script, a workflow or a shell
17
- * runs outside anything the analysis reads, and every peer a needed package
18
- * requires, which the target satisfies by declaring it.
19
- */
20
- function neededBy(dependencies: Dependencies): ReadonlySet<string> {
21
- const needed = new Set(dependencies.needed);
22
- for (const [name, installed] of dependencies.installed) {
23
- if (installed.command) {
24
- needed.add(name);
25
- }
26
- }
27
- const pending = [...needed];
28
- for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
29
- for (const peer of dependencies.installed.get(name)?.peers ?? []) {
30
- if (!needed.has(peer)) {
31
- needed.add(peer);
32
- pending.push(peer);
33
- }
34
- }
35
- }
36
- return needed;
37
- }
38
-
39
14
  /**
40
15
  * A dependency, development dependency or peer dependency of a manifest the run reads
41
16
  * that the target does not need, at the position of its key.
42
17
  */
43
18
  function unusedDependencies(dependencies: Dependencies): Finding[] {
44
- const needed = neededBy(dependencies);
45
19
  return dependencies.declared
46
- .filter((dependency) => !needed.has(dependency.name))
20
+ .filter(
21
+ (dependency) =>
22
+ !dependencies.needed.has(dependency.name) &&
23
+ !dependencies.ran.has(declarationKey(dependency)),
24
+ )
47
25
  .map((dependency) => ({
48
26
  code: UNUSED_DEPENDENCY,
49
27
  position: dependency.position,
@@ -59,6 +37,9 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
59
37
  },
60
38
  message: MESSAGES[dependency.section],
61
39
  details: { dependencyClass: dependency.section },
40
+ ...(dependencies.unbuilt.has(dependency.position.path)
41
+ ? { reachabilityClass: "possible" as const }
42
+ : {}),
62
43
  }));
63
44
  }
64
45
 
@@ -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
 
6
7
  import {
@@ -14,7 +15,7 @@ import type { Host } from "./host.ts";
14
15
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
15
16
  import type { SourceFiles } from "./source-files.ts";
16
17
 
17
- /** One file a module script loads, and the value or specifier that named it. */
18
+ /** One file a script loads, and the value or specifier that named it. */
18
19
  export interface HTMLEntry {
19
20
  readonly file: SourceFile;
20
21
  readonly source: string;
@@ -35,6 +36,9 @@ const ATTRIBUTE = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)
35
36
  /** A value that names a resource on another origin, or by another scheme. */
36
37
  const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
37
38
 
39
+ /** The `type` values a classic script carries, compared in lowercase. */
40
+ const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "application/javascript"]);
41
+
38
42
  /** The prefixes an inline script's specifier names a file of the target with. */
39
43
  const FILE_PREFIXES = ["/", "./", "../"] as const;
40
44
 
@@ -85,18 +89,22 @@ function specifiersOf(parse: ParseFile, fileName: string, content: string): read
85
89
  }
86
90
 
87
91
  /**
88
- * Every value a module script of one HTML file names a file by: a `src` attribute's
89
- * value, or each specifier an inline script imports or re-exports that starts with `/`,
90
- * `./` or `../`.
92
+ * Every value a script of one HTML file names a file by: the `src` attribute's value of a
93
+ * module script or of a classic one, or each specifier an inline module script imports or
94
+ * re-exports that starts with `/`, `./` or `../`.
91
95
  */
92
96
  function scriptValues(parse: ParseFile | undefined, path: string, text: string): readonly string[] {
93
97
  const values: string[] = [];
94
98
  for (const match of text.matchAll(SCRIPT_TAG)) {
95
99
  const held = attributes(match[1] ?? "");
96
- if (held.get("type") !== "module") {
100
+ const type = held.get("type");
101
+ const src = held.get("src");
102
+ if (type !== "module") {
103
+ if (src !== undefined && (type === undefined || CLASSIC_TYPES.has(type.toLowerCase()))) {
104
+ values.push(src);
105
+ }
97
106
  continue;
98
107
  }
99
- const src = held.get("src");
100
108
  if (src !== undefined) {
101
109
  values.push(src);
102
110
  continue;
@@ -131,11 +139,11 @@ function manifestDirectory(host: Host, root: string, dir: string): string {
131
139
  }
132
140
 
133
141
  /**
134
- * The own files the module scripts of every HTML file below the target root load. A value
142
+ * The own files the scripts of every HTML file below the target root load. A value
135
143
  * starting with a single `/` is read against the nearest manifest's directory, any other
136
- * against the HTML file's own, and either resolves as a configuration file's relative
137
- * specifier does. A value naming another origin or scheme, or no source file of the
138
- * program, loads nothing here.
144
+ * against the HTML file's own, as a configuration file's relative specifier. One naming no
145
+ * source file is read back through the project's emit mapping, and a `/` one also against
146
+ * an output directory that is the HTML file's own. Another origin or scheme loads nothing.
139
147
  */
140
148
  export function htmlEntries(
141
149
  host: Host,
@@ -157,12 +165,22 @@ export function htmlEntries(
157
165
  if (ELSEWHERE.test(value)) {
158
166
  continue;
159
167
  }
160
- const reached = value.startsWith("/")
161
- ? resolveRelativeSpecifier(host, manifestDirectory(host, root, dir), `.${value}`)
162
- : resolveRelativeSpecifier(host, dir, value);
168
+ const base = value.startsWith("/") ? manifestDirectory(host, root, dir) : dir;
169
+ const relative = value.startsWith("/") ? `.${value}` : value;
170
+ const reached = resolveRelativeSpecifier(host, base, relative);
163
171
  const file = reached === undefined ? undefined : files.byName.get(reached);
164
172
  if (file !== undefined) {
165
173
  found.push({ file, source: value });
174
+ continue;
175
+ }
176
+ const served = value.startsWith("/")
177
+ ? files.outputDirs.filter((outDir) => outDir === dir)
178
+ : [];
179
+ const emitting = [base, ...served].flatMap((against) =>
180
+ files.named(resolvePath(against, relative)),
181
+ );
182
+ for (const one of new Set(emitting)) {
183
+ found.push({ file: one, source: value });
166
184
  }
167
185
  }
168
186
  }