monkeypatchlist 0.0.0-stage → 0.1.0

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 (221) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +61 -0
  3. package/README.md +183 -2
  4. package/catalog/catalog.json +552211 -0
  5. package/catalog/catalog.schema.json +787 -0
  6. package/catalog/changelog.json +3 -0
  7. package/dist/bin/monkeypatchlist.d.ts +2 -0
  8. package/dist/bin/monkeypatchlist.js +3 -0
  9. package/dist/catalog.d.ts +561 -0
  10. package/dist/catalog.js +58 -0
  11. package/dist/cli.d.ts +84 -0
  12. package/dist/cli.js +470 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +1 -0
  15. package/dist/lockfile-input.d.ts +9 -0
  16. package/dist/lockfile-input.js +91 -0
  17. package/dist/packages/analysis/binding.d.ts +2 -0
  18. package/dist/packages/analysis/binding.js +5 -0
  19. package/dist/packages/analysis/index.d.ts +10 -0
  20. package/dist/packages/analysis/index.js +13 -0
  21. package/dist/packages/analysis/job.d.ts +33 -0
  22. package/dist/packages/analysis/job.js +85 -0
  23. package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
  24. package/dist/packages/analysis/lib/absence-tags.js +340 -0
  25. package/dist/packages/analysis/lib/ast.d.ts +28 -0
  26. package/dist/packages/analysis/lib/ast.js +64 -0
  27. package/dist/packages/analysis/lib/binding.d.ts +36 -0
  28. package/dist/packages/analysis/lib/binding.js +66 -0
  29. package/dist/packages/analysis/lib/budget.d.ts +80 -0
  30. package/dist/packages/analysis/lib/budget.js +31 -0
  31. package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
  32. package/dist/packages/analysis/lib/catch-arms.js +351 -0
  33. package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
  34. package/dist/packages/analysis/lib/chunk-loads.js +882 -0
  35. package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
  36. package/dist/packages/analysis/lib/chunk-registry.js +334 -0
  37. package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
  38. package/dist/packages/analysis/lib/code-routes.js +921 -0
  39. package/dist/packages/analysis/lib/collisions.d.ts +22 -0
  40. package/dist/packages/analysis/lib/collisions.js +103 -0
  41. package/dist/packages/analysis/lib/definitions.d.ts +76 -0
  42. package/dist/packages/analysis/lib/definitions.js +782 -0
  43. package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
  44. package/dist/packages/analysis/lib/early-exits.js +161 -0
  45. package/dist/packages/analysis/lib/effect.d.ts +138 -0
  46. package/dist/packages/analysis/lib/effect.js +800 -0
  47. package/dist/packages/analysis/lib/global-object.d.ts +581 -0
  48. package/dist/packages/analysis/lib/global-object.js +8240 -0
  49. package/dist/packages/analysis/lib/keys.d.ts +33 -0
  50. package/dist/packages/analysis/lib/keys.js +82 -0
  51. package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
  52. package/dist/packages/analysis/lib/line-starts.js +79 -0
  53. package/dist/packages/analysis/lib/link-group.d.ts +51 -0
  54. package/dist/packages/analysis/lib/link-group.js +487 -0
  55. package/dist/packages/analysis/lib/module-links.d.ts +117 -0
  56. package/dist/packages/analysis/lib/module-links.js +1060 -0
  57. package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
  58. package/dist/packages/analysis/lib/nested-code.js +200 -0
  59. package/dist/packages/analysis/lib/numeric.d.ts +16 -0
  60. package/dist/packages/analysis/lib/numeric.js +216 -0
  61. package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
  62. package/dist/packages/analysis/lib/once-flags.js +190 -0
  63. package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
  64. package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
  65. package/dist/packages/analysis/lib/parse.d.ts +10 -0
  66. package/dist/packages/analysis/lib/parse.js +39 -0
  67. package/dist/packages/analysis/lib/partial.d.ts +60 -0
  68. package/dist/packages/analysis/lib/partial.js +518 -0
  69. package/dist/packages/analysis/lib/print.d.ts +3 -0
  70. package/dist/packages/analysis/lib/print.js +150 -0
  71. package/dist/packages/analysis/lib/reachability.d.ts +137 -0
  72. package/dist/packages/analysis/lib/reachability.js +1295 -0
  73. package/dist/packages/analysis/lib/resolver.d.ts +13 -0
  74. package/dist/packages/analysis/lib/resolver.js +2886 -0
  75. package/dist/packages/analysis/lib/restores.d.ts +42 -0
  76. package/dist/packages/analysis/lib/restores.js +482 -0
  77. package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
  78. package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
  79. package/dist/packages/analysis/lib/scope.d.ts +110 -0
  80. package/dist/packages/analysis/lib/scope.js +443 -0
  81. package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
  82. package/dist/packages/analysis/lib/script-loads.js +359 -0
  83. package/dist/packages/analysis/lib/summaries.d.ts +177 -0
  84. package/dist/packages/analysis/lib/summaries.js +498 -0
  85. package/dist/packages/analysis/lib/types.d.ts +632 -0
  86. package/dist/packages/analysis/lib/types.js +128 -0
  87. package/dist/packages/analysis/lib/wrap.d.ts +34 -0
  88. package/dist/packages/analysis/lib/wrap.js +407 -0
  89. package/dist/packages/analysis/lib/writes.d.ts +358 -0
  90. package/dist/packages/analysis/lib/writes.js +1612 -0
  91. package/dist/packages/analysis/visit-order.d.ts +3 -0
  92. package/dist/packages/analysis/visit-order.js +23 -0
  93. package/dist/packages/attribution/index.d.ts +233 -0
  94. package/dist/packages/attribution/index.js +444 -0
  95. package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
  96. package/dist/packages/attribution/lib/containing-package.js +304 -0
  97. package/dist/packages/attribution/lib/first-party.d.ts +37 -0
  98. package/dist/packages/attribution/lib/first-party.js +153 -0
  99. package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
  100. package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
  101. package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
  102. package/dist/packages/attribution/lib/lockfile.js +330 -0
  103. package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
  104. package/dist/packages/attribution/lib/path-rules.js +84 -0
  105. package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
  106. package/dist/packages/attribution/lib/pnpm.js +133 -0
  107. package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
  108. package/dist/packages/attribution/lib/remote-url.js +24 -0
  109. package/dist/packages/attribution/lib/source-map.d.ts +51 -0
  110. package/dist/packages/attribution/lib/source-map.js +125 -0
  111. package/dist/packages/attribution/lib/yaml.d.ts +9 -0
  112. package/dist/packages/attribution/lib/yaml.js +277 -0
  113. package/dist/packages/attribution/lib/yarn.d.ts +7 -0
  114. package/dist/packages/attribution/lib/yarn.js +227 -0
  115. package/dist/packages/baseline/index.d.ts +45 -0
  116. package/dist/packages/baseline/index.js +41 -0
  117. package/dist/packages/baseline/lib/error.d.ts +15 -0
  118. package/dist/packages/baseline/lib/error.js +25 -0
  119. package/dist/packages/baseline/lib/format.d.ts +49 -0
  120. package/dist/packages/baseline/lib/format.js +22 -0
  121. package/dist/packages/baseline/lib/migrations.d.ts +19 -0
  122. package/dist/packages/baseline/lib/migrations.js +7 -0
  123. package/dist/packages/baseline/lib/previous.d.ts +23 -0
  124. package/dist/packages/baseline/lib/previous.js +64 -0
  125. package/dist/packages/baseline/lib/read.d.ts +39 -0
  126. package/dist/packages/baseline/lib/read.js +219 -0
  127. package/dist/packages/baseline/lib/report.d.ts +25 -0
  128. package/dist/packages/baseline/lib/report.js +104 -0
  129. package/dist/packages/baseline/lib/skew.d.ts +51 -0
  130. package/dist/packages/baseline/lib/skew.js +75 -0
  131. package/dist/packages/compare/index.d.ts +120 -0
  132. package/dist/packages/compare/index.js +274 -0
  133. package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
  134. package/dist/packages/compare/lib/catalog-notes.js +224 -0
  135. package/dist/packages/completeness/index.d.ts +246 -0
  136. package/dist/packages/completeness/index.js +249 -0
  137. package/dist/packages/confidence/index.d.ts +54 -0
  138. package/dist/packages/confidence/index.js +71 -0
  139. package/dist/packages/config/index.d.ts +107 -0
  140. package/dist/packages/config/index.js +95 -0
  141. package/dist/packages/config/lib/command-line.d.ts +23 -0
  142. package/dist/packages/config/lib/command-line.js +239 -0
  143. package/dist/packages/config/lib/config-file.d.ts +11 -0
  144. package/dist/packages/config/lib/config-file.js +119 -0
  145. package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
  146. package/dist/packages/config/lib/scan-config-id.js +54 -0
  147. package/dist/packages/config/lib/schema.d.ts +2 -0
  148. package/dist/packages/config/lib/schema.js +31 -0
  149. package/dist/packages/config/lib/types.d.ts +64 -0
  150. package/dist/packages/config/lib/types.js +6 -0
  151. package/dist/packages/config/lib/urls.d.ts +6 -0
  152. package/dist/packages/config/lib/urls.js +9 -0
  153. package/dist/packages/discovery/index.d.ts +277 -0
  154. package/dist/packages/discovery/index.js +1950 -0
  155. package/dist/packages/discovery/lib/import-map.d.ts +68 -0
  156. package/dist/packages/discovery/lib/import-map.js +180 -0
  157. package/dist/packages/discovery/lib/requests.d.ts +58 -0
  158. package/dist/packages/discovery/lib/requests.js +591 -0
  159. package/dist/packages/discovery/lib/scopes.d.ts +6 -0
  160. package/dist/packages/discovery/lib/scopes.js +286 -0
  161. package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
  162. package/dist/packages/discovery/lib/source-maps.js +209 -0
  163. package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
  164. package/dist/packages/discovery/lib/swept-code.js +107 -0
  165. package/dist/packages/discovery/lib/syntax.d.ts +17 -0
  166. package/dist/packages/discovery/lib/syntax.js +39 -0
  167. package/dist/packages/findings/index.d.ts +106 -0
  168. package/dist/packages/findings/index.js +254 -0
  169. package/dist/packages/findings/lib/scope.d.ts +38 -0
  170. package/dist/packages/findings/lib/scope.js +110 -0
  171. package/dist/packages/parallel/index.d.ts +2 -0
  172. package/dist/packages/parallel/index.js +5 -0
  173. package/dist/packages/parallel/lib/protocol.d.ts +15 -0
  174. package/dist/packages/parallel/lib/protocol.js +2 -0
  175. package/dist/packages/parallel/lib/runner.d.ts +36 -0
  176. package/dist/packages/parallel/lib/runner.js +96 -0
  177. package/dist/packages/parallel/lib/schedule.d.ts +16 -0
  178. package/dist/packages/parallel/lib/schedule.js +52 -0
  179. package/dist/packages/parallel/lib/worker.d.ts +1 -0
  180. package/dist/packages/parallel/lib/worker.js +18 -0
  181. package/dist/packages/policy/index.d.ts +36 -0
  182. package/dist/packages/policy/index.js +22 -0
  183. package/dist/packages/report/explain.d.ts +26 -0
  184. package/dist/packages/report/explain.js +99 -0
  185. package/dist/packages/report/index.d.ts +210 -0
  186. package/dist/packages/report/index.js +388 -0
  187. package/dist/packages/report/terminal.d.ts +11 -0
  188. package/dist/packages/report/terminal.js +331 -0
  189. package/dist/packages/source-store/fixture.d.ts +22 -0
  190. package/dist/packages/source-store/fixture.js +58 -0
  191. package/dist/packages/source-store/index.d.ts +278 -0
  192. package/dist/packages/source-store/index.js +322 -0
  193. package/dist/packages/source-store/lib/addresses.d.ts +6 -0
  194. package/dist/packages/source-store/lib/addresses.js +91 -0
  195. package/dist/packages/source-store/lib/connection.d.ts +15 -0
  196. package/dist/packages/source-store/lib/connection.js +135 -0
  197. package/dist/packages/source-store/lib/content-types.d.ts +16 -0
  198. package/dist/packages/source-store/lib/content-types.js +75 -0
  199. package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
  200. package/dist/packages/source-store/lib/data-urls.js +85 -0
  201. package/dist/packages/source-store/lib/deadline.d.ts +18 -0
  202. package/dist/packages/source-store/lib/deadline.js +44 -0
  203. package/dist/packages/source-store/lib/exchange.d.ts +29 -0
  204. package/dist/packages/source-store/lib/exchange.js +112 -0
  205. package/dist/packages/source-store/lib/integrity.d.ts +19 -0
  206. package/dist/packages/source-store/lib/integrity.js +86 -0
  207. package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
  208. package/dist/packages/source-store/lib/outcomes.js +22 -0
  209. package/dist/packages/source-store/lib/proxy.d.ts +11 -0
  210. package/dist/packages/source-store/lib/proxy.js +53 -0
  211. package/dist/packages/source-store/lib/session.d.ts +12 -0
  212. package/dist/packages/source-store/lib/session.js +139 -0
  213. package/dist/packages/source-store/lib/urls.d.ts +6 -0
  214. package/dist/packages/source-store/lib/urls.js +15 -0
  215. package/dist/packages/source-store/resolve.d.ts +59 -0
  216. package/dist/packages/source-store/resolve.js +45 -0
  217. package/dist/scan.d.ts +65 -0
  218. package/dist/scan.js +592 -0
  219. package/package.json +95 -4
  220. package/schema/config.schema.json +133 -0
  221. package/schema/report.schema.json +1905 -0
@@ -0,0 +1,1950 @@
1
+ // Discovery (§19, module 3): follows references from the entry documents, builds the artifact graph (§2) and pairs
2
+ // source maps. It parses syntax only, and reads bytes only through the source store it is given.
3
+ import { createHash } from 'node:crypto';
4
+ import { posix } from 'node:path';
5
+ import { html, parse } from 'parse5';
6
+ import { alternativeIntegrity } from "../source-store/index.js";
7
+ import { emptyImportMap, mergeImportMaps, moduleIntegrity, parseImportMap, resolveSpecifier, } from "./lib/import-map.js";
8
+ import { requests } from "./lib/requests.js";
9
+ import { annotations, dataUrlText, debugIdKey, mapDebugId, rawMap, verification } from "./lib/source-maps.js";
10
+ import { sourceTypeBySyntax, sweptCode, workerMarkersAs } from "./lib/swept-code.js";
11
+ /** JavaScript MIME type essences (MIME Sniffing § JavaScript MIME type): a `script` of one of these types is classic. */
12
+ const javascriptTypes = new Set([
13
+ 'application/ecmascript',
14
+ 'application/javascript',
15
+ 'application/x-ecmascript',
16
+ 'application/x-javascript',
17
+ 'text/ecmascript',
18
+ 'text/javascript',
19
+ 'text/javascript1.0',
20
+ 'text/javascript1.1',
21
+ 'text/javascript1.2',
22
+ 'text/javascript1.3',
23
+ 'text/javascript1.4',
24
+ 'text/javascript1.5',
25
+ 'text/jscript',
26
+ 'text/livescript',
27
+ 'text/x-ecmascript',
28
+ 'text/x-javascript',
29
+ ]);
30
+ const decoder = new TextDecoder('utf-8');
31
+ const encoder = new TextEncoder();
32
+ /**
33
+ * Finds the classic and module scripts each entry document runs, following the HTML rules of §19: its inline scripts,
34
+ * and the files its `script[src]` elements name, resolved against the document's base URL. A file that only a
35
+ * `<link rel=preload as=script>` or a `<link rel=modulepreload>` names joins the graph too, and does not run. It
36
+ * counts the HTML content it does not analyze: classic `nomodule` scripts, inline event handlers and `javascript:`
37
+ * URLs.
38
+ *
39
+ * Then it follows the module requests of every script it found, and of every module those name, in turn (§2): static
40
+ * imports, re-exports and dynamic imports with a literal specifier, resolved through the document's import maps in
41
+ * browser order (§2 Import maps). An inline module script's requests resolve through the maps before it in tree order,
42
+ * and every other request through all of them. Each names a module, which runs when its importer does. A request with
43
+ * an import attribute names no JavaScript module, and is not followed. A static import or re-export with an attribute a
44
+ * browser doesn't support fails every module graph that contains it (§2 Import attributes): each root of such a graph
45
+ * (a module script element, a dynamic import or a module worker) gives nothing, and nothing is followed through it
46
+ * from a hint. A dynamic import whose specifier is not a literal, or whose options have a part that can't be read and
47
+ * could decide whether it names a JavaScript module, and a specifier that doesn't resolve, such as a bare specifier no
48
+ * import map maps, give an `unresolved-reference` record.
49
+ *
50
+ * It follows the worker scripts they load with a literal URL too (§2, §19 Discovery), where the realm has the function
51
+ * that loads them: a `new Worker` is a dedicated worker, a `new SharedWorker` a shared worker, and a
52
+ * `navigator.serviceWorker.register` a service worker, each in its own realm, and an `importScripts` loads a classic
53
+ * script into its own worker. A service worker whose script falls outside every URL mapping, like a `<link
54
+ * rel=modulepreload as=serviceworker>` outside every mapping, gives an `unresolved-reference` record instead. A
55
+ * dedicated or shared worker's script on another origin than its creator's is not followed, as a browser won't run it,
56
+ * and a `data:` worker's is code the page carries, as a `data:` script is.
57
+ *
58
+ * A file a `<link rel=preload as=script>` names that no running reference names, in any realm, is followed last, as the
59
+ * syntax of the preload's own bytes says, in URL order. Its realm is assumed to be `window` when nothing else names it,
60
+ * as it is for the modules it requests (§2 Files that don't run as referenced). One a running reference runs in another
61
+ * realm has no window artifact. One whose code has worker markers gives a `realm-uncertain` record, as a file only the
62
+ * sweep found does.
63
+ *
64
+ * A remote script is fetched with the integrity metadata its reference carries (§17 Integrity): a `script` or `link`
65
+ * element's `integrity` attribute, or, for a module with none, its URL's entry in the document's import maps. A
66
+ * fetched script's own references are fetched against the URL its bytes came from, after any redirects, though it
67
+ * keeps the URL it was requested at. They are keyed, as artifacts, by the URL resolved against the script's own URL as
68
+ * requested (its key URL), so a key doesn't change when a redirect target moves (§11 Remote URL normalization). One
69
+ * fetched URL reached under two key URLs is an artifact of each, sharing the one fetch, and one key URL reached through
70
+ * two fetched URLs is an artifact of each too, both analyzed, told apart by `retrieval.requestedUrl`.
71
+ *
72
+ * A `data:` URL is code the page carries (§2 Other script URLs): its script is an `inline` artifact of the referencing
73
+ * realm, loaded as the reference loads it, unless a browser would refuse to run it, when it gives nothing, as a `data:`
74
+ * service worker always does. A script URL with any other scheme but `http:` and `https:` (`javascript:`, `blob:`,
75
+ * `file:`, `about:`, …) gives nothing wherever it appears.
76
+ *
77
+ * An entry document or a local script that can't be read gives an `unreadable` record, and a remote script that isn't
78
+ * read gives an `offline-skipped`, `remote-refused`, `fetch-failed` or `sri-mismatch` one (§17 Outcomes); the script
79
+ * is still an artifact. Each URL, and each inline script, is an artifact of its own, whatever bytes it shares with
80
+ * another (§2 Artifacts): the scan analyzes identical bytes once, and gives each copy its findings.
81
+ *
82
+ * Last, it pairs each artifact with the first source map that verifies against it (§2 Source maps): one a fetched
83
+ * artifact's `SourceMap` header names, then one its annotation names, then its sidecar in the build directory, then a
84
+ * map there with its `debugId`. A map that fails verification is `stale`, and counted. A missing or broken map gives no
85
+ * record: it is counted in the attribution block (§13), and never changes completeness. A map the annotation of a
86
+ * local-build or inline artifact names, or its sidecar, that exists but can't be read gives an `unreadable` record. A
87
+ * remote artifact's maps are fetched only here, after every script: each one the safety floor refuses, whose fetch
88
+ * fails, or that is a build file that exists but can't be read, is only counted (§17, `unreadMaps`).
89
+ * Then each source-map directory is searched, one at a time, in the order they were declared (`pairInDirectory`): one
90
+ * that can't be listed gives an `unreadable` record, and one none of whose maps verifies against an artifact a
91
+ * `source-maps-unpaired` record.
92
+ */
93
+ export async function discover(input) {
94
+ const graph = {
95
+ store: input.store,
96
+ catalog: input.catalog,
97
+ eventHandlers: eventHandlers(input.catalog),
98
+ byRealmUrl: new Map(),
99
+ otherWay: new Map(),
100
+ reads: new Map(),
101
+ readLimitations: new Map(),
102
+ integrities: new Map(),
103
+ limitations: [],
104
+ inlineCounts: new Map(),
105
+ html: { nomodule: 0, inlineHandlers: 0 },
106
+ noModuleElements: new Set(),
107
+ visits: [],
108
+ preloads: [],
109
+ followed: new Set(),
110
+ requests: new Map(),
111
+ unresolved: new Set(),
112
+ references: new Map(),
113
+ markers: new Map(),
114
+ fetched: new Map(),
115
+ whole: new Set(),
116
+ imports: new Map(),
117
+ requested: new Set(),
118
+ named: new Map(),
119
+ ran: new Set(),
120
+ pages: new Map(),
121
+ pageOf: new Map(),
122
+ importSites: new Map(),
123
+ chunkSites: new Map(),
124
+ workerSites: new Map(),
125
+ remoteChunks: new Map(),
126
+ };
127
+ const resolved = new Set();
128
+ for (const reference of input.references ?? []) {
129
+ // Each chunk load and worker URL the resolver found, by its site: discovery judges them where it finds the site.
130
+ if (reference.kind === 'injected')
131
+ continue;
132
+ const sites = reference.kind === 'worker' ? graph.workerSites : reference.load === 'import' ? graph.importSites : graph.chunkSites;
133
+ const site = importSite(reference.from, reference.location.start);
134
+ const list = sites.get(site);
135
+ if (list === undefined)
136
+ sites.set(site, [reference]);
137
+ else
138
+ list.push(reference);
139
+ if (reference.kind === 'worker' || reference.load === 'import')
140
+ resolved.add(reference);
141
+ }
142
+ for (const [index, entry] of input.entries.entries()) {
143
+ const document = await input.store.readFile(entry.file, entry.url);
144
+ if (document.status !== 'ok') {
145
+ // A build file is never remote, so it is `ok` or `unreadable`.
146
+ if (document.status === 'unreadable')
147
+ graph.limitations.push(unreadable(document));
148
+ continue;
149
+ }
150
+ const tree = parse(decoder.decode(document.bytes));
151
+ const fields = { entry, path: document.path, index, importMap: emptyImportMap, base: entry.url, resolved: [] };
152
+ // A module graph fails or not as its requests resolve through all of the document's maps, which are known only
153
+ // once its HTML has been walked, so a first walk merges them alone (§2 Import maps, Import attributes).
154
+ const maps = { ...fields, resolved: [], finalMap: emptyImportMap };
155
+ await walk(graph, maps, tree, true);
156
+ const page = { ...fields, finalMap: maps.importMap };
157
+ if (!graph.pages.has(entry.url))
158
+ graph.pages.set(entry.url, page);
159
+ await walk(graph, page, tree);
160
+ }
161
+ // Every entry document's own references come first, then the modules they request, breadth first.
162
+ let next = 0;
163
+ const followAll = async () => {
164
+ while (next < graph.visits.length)
165
+ await follow(graph, graph.visits[next++]);
166
+ };
167
+ await followAll();
168
+ // Then each file classic preloads name that no running reference names, in any realm, as its syntax says, and what
169
+ // it requests, in URL order. Nothing a preload's visit requests runs, so the running references are all known now,
170
+ // and the judgement doesn't depend on the order the preloads were found in, or the visits followed in.
171
+ const running = runningUrls(graph);
172
+ const preloadVisits = [];
173
+ for (const preload of graph.preloads) {
174
+ const visit = await preloadVisit(graph, preload, running);
175
+ if (visit !== undefined)
176
+ preloadVisits.push(visit);
177
+ }
178
+ graph.visits.push(...preloadVisits.sort(byUrlAndPage));
179
+ await followAll();
180
+ // Then the references only the resolver can see whose code a reference found (§19), and what they reference, until
181
+ // none is left whose code is known.
182
+ const followResolved = async () => {
183
+ while (await followReferences(graph, input.references ?? [], resolved))
184
+ await followAll();
185
+ };
186
+ await followResolved();
187
+ dropRunElsewhere(graph, running);
188
+ const swept = await sweep(graph);
189
+ // Then each sweep-only file's own literal requests, as a preload-only file's are (§2 Sweep), and the resolver's
190
+ // references from code they found. A swept file a reference has since given a realm is no standalone sweep.
191
+ graph.visits.push(...sweepVisits(graph, input.entries.length));
192
+ await followAll();
193
+ await followResolved();
194
+ dropSuperseded(graph);
195
+ const { byRealmUrl, html } = graph;
196
+ // Why each artifact's bytes weren't read, as its latest read says: a reference may have read it again (`reference`).
197
+ const unread = [...graph.readLimitations.values()].filter((record) => record !== null);
198
+ const found = [...[...byRealmUrl].map(([key, artifact]) => asNamed(graph, key, artifact)), ...graph.otherWay.values()];
199
+ const { artifacts, sourceMapDirectories } = await pairSourceMaps(graph, found, input.sourceMaps ?? []);
200
+ const uncertain = artifacts.flatMap((artifact) => {
201
+ const markers = assumedMarkers(graph, artifact);
202
+ return markers.length > 0 ? [realmUncertain(graph, artifact, markers)] : [];
203
+ });
204
+ sweptImports(graph);
205
+ const importedOnly = [...graph.requested]
206
+ .filter((key) => !graph.ran.has(key))
207
+ .flatMap((key) => {
208
+ const found = byRealmUrl.get(key);
209
+ return found === undefined ? [] : [{ realm: found.realm, url: found.url }];
210
+ });
211
+ return {
212
+ artifacts,
213
+ limitations: [...graph.limitations, ...unread, ...uncertain],
214
+ html,
215
+ swept,
216
+ sourceMapDirectories,
217
+ imports: [...graph.imports.values()],
218
+ importedOnly,
219
+ documents: [...graph.named].flatMap(([key, documents]) => {
220
+ const found = byRealmUrl.get(key);
221
+ return found === undefined ? [] : [{ realm: found.realm, url: found.url, documents: [...documents] }];
222
+ }),
223
+ remoteChunks: [...graph.remoteChunks.values()],
224
+ };
225
+ }
226
+ /** The key of an `import()` site: its artifact, and the start of its specifier. */
227
+ const importSite = (from, start) => JSON.stringify([from.url, from.fetched ?? null, start]);
228
+ /**
229
+ * Whether the constant prefix `prefix` of a chunk URL that doesn't fold, resolved against `base`, is a local chunk URL
230
+ * (§2): a prefix that resolves outside every mapping, with none under it, makes it remote; otherwise the loader's
231
+ * origin decides, and an inline loader counts as local.
232
+ */
233
+ function localChunk(graph, loader, prefix, base) {
234
+ if (prefix !== '') {
235
+ const parsed = URL.parse(prefix, base);
236
+ if (parsed !== null && !graph.store.isMapped(parsed.href) && !graph.store.mapsUnder(parsed.href))
237
+ return false;
238
+ }
239
+ return loader.origin === 'local-build' || loader.origin === 'inline';
240
+ }
241
+ /** The `unresolved-reference` (§13) of a chunk load site whose URL doesn't fold, in its loader's code. */
242
+ function unresolvedChunk(graph, loader, { location }) {
243
+ const { url, path, hash } = loader;
244
+ addUnresolved(graph, {
245
+ kind: 'unresolved-reference',
246
+ artifact: { url, ...(path === null ? {} : { path }), ...(hash === null ? {} : { hash }) },
247
+ location,
248
+ detail: "a chunk loader's chunk URL that doesn't fold to a constant",
249
+ });
250
+ }
251
+ /**
252
+ * Follows each chunk reference the resolver found (§19) whose code's artifact is known, once: a created script
253
+ * element's `src` in the window, resolved against the base URL of each entry document that names the loader, or the
254
+ * loader's own URL when none does (§2), and an `importScripts` in a worker, against the loader's URL. A swept
255
+ * loader's references are followed too, once the sweep has found it: swept artifacts that add references resume the
256
+ * rounds (§19 The result doesn't depend on the rounds), and their chunks inherit its assumed basis. A chunk inside a
257
+ * mapping is a classic script of the loader's realm, which only the loader's link group runs: no reference runs it on
258
+ * its own. A swept file it names becomes its artifact. A remote chunk URL is followed the same way, fetched in `resolve`
259
+ * mode with the integrity value the code sets on its element, if that folds (§17 Integrity), and `offline-skipped`
260
+ * offline; it joins its loader's group (`Discovered.remoteChunks`). An alternative that doesn't fold is an
261
+ * `unresolved-reference` at its load site, unless it is a local chunk URL (`localChunk`). A local chunk file the
262
+ * build directory lacks gives no artifact and no record (`missingFile`). Whether any reference was followed.
263
+ */
264
+ async function followReferences(graph, references, done) {
265
+ let followed = false;
266
+ for (const resolved of references) {
267
+ if (done.has(resolved))
268
+ continue;
269
+ const { realm, from, load } = resolved;
270
+ const key = at(realm, from.url, from.fetched);
271
+ const loader = graph.byRealmUrl.get(key);
272
+ if (loader === undefined)
273
+ continue;
274
+ done.add(resolved);
275
+ followed = true;
276
+ if (resolved.kind === 'injected') {
277
+ await injectedReference(graph, resolved, key, loader);
278
+ continue;
279
+ }
280
+ // A worker URL is followed where discovery finds its call (`workerReference`).
281
+ if (resolved.kind === 'worker')
282
+ continue;
283
+ if ((load === 'element') !== (realm === 'window') || load === 'import')
284
+ continue;
285
+ const own = graph.pageOf.get(key);
286
+ const documents = load === 'element' ? [...(graph.named.get(key) ?? [])] : [];
287
+ const bases = documents.length > 0
288
+ ? documents.map((document) => [graph.pages.get(document), graph.pages.get(document)?.base ?? document])
289
+ : [[own, loader.retrieval.finalUrl ?? loader.url]];
290
+ if (resolved.partial) {
291
+ // An alternative that doesn't fold is an unresolved reference unless it is a local chunk URL, which the sweep
292
+ // covers (§2).
293
+ if (!bases.every(([, base]) => localChunk(graph, loader, resolved.url, base))) {
294
+ unresolvedChunk(graph, loader, resolved);
295
+ }
296
+ continue;
297
+ }
298
+ for (const [page, base] of bases) {
299
+ const url = URL.parse(resolved.url, base);
300
+ if (page === undefined || url === null || !scriptSchemes.has(url.protocol))
301
+ continue;
302
+ const chunk = at(realm, url.href);
303
+ const how = {
304
+ sourceType: 'script',
305
+ runs: false,
306
+ realm,
307
+ ...(load === 'import-scripts' && { imported: true }),
308
+ ...(loader.realmBasis === 'assumed' && { assumed: true }),
309
+ };
310
+ // A chunk file the build directory doesn't contain is a missing chunk (§6 Globals are not links), never
311
+ // `unreadable`: the link group raises or counts it by the chunk's id. A file an element or an import names keeps its
312
+ // record, since it reads through the same cache.
313
+ if (await missingFile(graph, page, url, how))
314
+ continue;
315
+ if (graph.byRealmUrl.get(chunk)?.discovery === 'sweep') {
316
+ graph.byRealmUrl.delete(chunk);
317
+ graph.markers.delete(url.href);
318
+ }
319
+ const worker = realm === 'window' ? undefined : { url: from.url, ...(from.fetched === undefined ? {} : { fetched: from.fetched }), sourceType: 'script' };
320
+ await reference(graph, page, url, how, from.url, worker, resolved.integrity);
321
+ if (!graph.store.isMapped(url.href) && graph.byRealmUrl.has(chunk)) {
322
+ const record = { realm, loader: from, chunk: url.href };
323
+ graph.remoteChunks.set(JSON.stringify(record), record);
324
+ }
325
+ }
326
+ }
327
+ return followed;
328
+ }
329
+ /** Whether `url` is a local file the build directory doesn't contain (§13 `unreadable`). */
330
+ async function missingFile(graph, page, url, how) {
331
+ if (!graph.store.isMapped(url.href))
332
+ return false;
333
+ const read = await readOnce(graph, page, url, how, '');
334
+ return read?.limitation?.kind === 'unreadable' && ['ENOENT', 'ENOTDIR'].includes(read.limitation.detail);
335
+ }
336
+ /**
337
+ * Follows an injected script the resolver found (§2 Injected scripts): a script element the window's code creates,
338
+ * gives a `src` and inserts. Only the window has `document.createElement`, so a group of any other realm gives nothing,
339
+ * and the script runs in the window with its realm known by reference, whatever its creator's basis. Its `src`
340
+ * resolves against the base URL of each entry document that names or first followed its creator, or the creator's own
341
+ * URL when none does, and the element's `type` decides how it loads, as an HTML element's does: one that doesn't
342
+ * fold leaves it to the script's syntax, and one a browser doesn't run gives nothing. A folded `integrity` is
343
+ * verified against each constant it may be (§17). A classic script whose `noModule` is set is not followed, as
344
+ * HTML's `<script nomodule>` is: it is counted, and a local file it names is left to the sweep. One whose
345
+ * `noModule` may be set, or whose type may be module, is followed and counted as possibly `nomodule`, and a
346
+ * module script ignores the attribute. An alternative that doesn't fold is an `unresolved-reference` at that alternative. A swept
347
+ * file it names becomes its artifact.
348
+ */
349
+ async function injectedReference(graph, resolved, key, creator) {
350
+ if (resolved.realm !== 'window')
351
+ return;
352
+ const typed = resolved.type === null ? null : typeOfScript(resolved.type, undefined);
353
+ if (typed === undefined || typed === 'importmap')
354
+ return;
355
+ if (resolved.noModule !== undefined && typed !== 'module') {
356
+ // One count per element, whatever its sources and documents; a round may find it again.
357
+ const element = JSON.stringify([resolved.from.url, resolved.from.fetched ?? null, resolved.location.start]);
358
+ if (!graph.noModuleElements.has(element)) {
359
+ graph.noModuleElements.add(element);
360
+ graph.html.nomodule++;
361
+ }
362
+ if (resolved.noModule === 'yes' && typed === 'script')
363
+ return;
364
+ }
365
+ if (resolved.partial) {
366
+ unresolvedReference(graph, creator, resolved.alternative ?? resolved.location, 'an injected script whose `src` is not a constant');
367
+ return;
368
+ }
369
+ const own = graph.pageOf.get(key);
370
+ const documents = new Set(graph.named.get(key) ?? []);
371
+ if (own !== undefined && graph.pages.get(own.entry.url) === own)
372
+ documents.add(own.entry.url);
373
+ const bases = documents.size > 0
374
+ ? [...documents].map((document) => [graph.pages.get(document), graph.pages.get(document)?.base ?? document])
375
+ : [[own, creator.retrieval.finalUrl ?? creator.url]];
376
+ for (const [page, base] of bases) {
377
+ const url = URL.parse(resolved.url, base);
378
+ if (page === undefined || url === null || !scriptSchemes.has(url.protocol))
379
+ continue;
380
+ const at_ = at('window', url.href);
381
+ if (graph.byRealmUrl.get(at_)?.discovery === 'sweep') {
382
+ graph.byRealmUrl.delete(at_);
383
+ graph.markers.delete(url.href);
384
+ }
385
+ const sourceType = typed ?? (await sourceTypeOf(graph, page, url, 'window', url));
386
+ const how = { sourceType, runs: true, realm: 'window', injected: true };
387
+ await reference(graph, page, url, how, resolved.from.url, undefined, injectedIntegrity(resolved));
388
+ }
389
+ }
390
+ /** The integrity metadata an injected script is fetched with: each constant its `integrity` may be. */
391
+ function injectedIntegrity({ integrityAlternatives = [], integrity = '' }) {
392
+ return integrityAlternatives.length === 0 ? integrity : alternativeIntegrity(integrityAlternatives);
393
+ }
394
+ /**
395
+ * A visit of each sweep-only file whose bytes were read, in URL order (§2 Sweep): its own literal requests are
396
+ * followed as a preload-only file's are. Its imports inherit its assumed realm basis, and its worker URLs are ordinary
397
+ * worker references. With no entry document, its own URL stands for one: its requests resolve against it, with no
398
+ * import map, and it is the document a fetch is made for.
399
+ */
400
+ function sweepVisits(graph, entries) {
401
+ const swept = [...graph.byRealmUrl.values()]
402
+ .filter((found) => found.discovery === 'sweep' && found.text !== null)
403
+ .sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
404
+ return swept.map(({ url, path, sourceType }, index) => {
405
+ const page = {
406
+ entry: { file: path ?? url, url },
407
+ path,
408
+ index: entries + index,
409
+ importMap: emptyImportMap,
410
+ base: url,
411
+ resolved: [],
412
+ finalMap: emptyImportMap,
413
+ };
414
+ return { url, page, base: url, sourceType, runs: false, realm: 'window', assumed: true, swept: true };
415
+ });
416
+ }
417
+ /** Whether only the sweep found the file at `url` (§2 Sweep). */
418
+ const isSwept = (graph, url) => graph.byRealmUrl.get(at('window', url))?.discovery === 'sweep';
419
+ /**
420
+ * Drops each swept file that a reference has since found, in any realm (§19 The result doesn't depend on the rounds):
421
+ * a worker the sweep found as `window` is its worker artifact alone.
422
+ */
423
+ function dropSuperseded(graph) {
424
+ const referenced = new Set();
425
+ for (const found of [...graph.byRealmUrl.values(), ...graph.otherWay.values()]) {
426
+ if (found.discovery !== 'sweep' && found.path !== null)
427
+ referenced.add(found.path);
428
+ }
429
+ for (const [key, found] of graph.byRealmUrl) {
430
+ if (found.discovery !== 'sweep' || found.path === null || !referenced.has(found.path))
431
+ continue;
432
+ graph.byRealmUrl.delete(key);
433
+ graph.markers.delete(found.url);
434
+ }
435
+ }
436
+ /**
437
+ * Records the module requests of each sweep-only file (§6 Module links): resolved against its URL with no import map,
438
+ * since no document loads it. A swept file one names is linked into the file's group (§19).
439
+ */
440
+ function sweptImports(graph) {
441
+ for (const found of graph.byRealmUrl.values()) {
442
+ if (found.discovery !== 'sweep' || found.text === null)
443
+ continue;
444
+ for (const request of followable(requestsOf(graph, found, found.sourceType).modules)) {
445
+ const resolved = resolveSpecifier(emptyImportMap, request.specifier, found.url);
446
+ if (!('error' in resolved)) {
447
+ recordImport(graph, found.realm, null, { url: found.url }, request.specifier, { url: resolved.url.href });
448
+ }
449
+ }
450
+ }
451
+ }
452
+ /**
453
+ * Records a module request that resolves (`Discovered.imports`), unless the script's code already made it for that
454
+ * document.
455
+ */
456
+ function recordImport(graph, realm, document, from, specifier, to) {
457
+ const key = JSON.stringify([realm, document, from.url, from.fetched, specifier]);
458
+ if (!graph.imports.has(key))
459
+ graph.imports.set(key, { realm, document, from, specifier, to });
460
+ }
461
+ /** Records that a reference other than a module request names the artifact at `key` for `document`. */
462
+ function recordNamed(graph, key, document) {
463
+ const documents = graph.named.get(key);
464
+ if (documents === undefined)
465
+ graph.named.set(key, new Set([document]));
466
+ else
467
+ documents.add(document);
468
+ }
469
+ /**
470
+ * The worker markers of an artifact whose realm is assumed (§2 Realm, §13 `realm-uncertain`): a sweep-only file's, read
471
+ * as the sweep typed it, or those of a file only preloads, or code whose realm is assumed, name, in its code as it is
472
+ * analyzed. None for an artifact whose realm a reference gives.
473
+ */
474
+ function assumedMarkers(graph, { url, realmBasis, discovery, sourceType, text }) {
475
+ if (realmBasis !== 'assumed' || text === null)
476
+ return [];
477
+ return discovery === 'sweep' ? (graph.markers.get(url) ?? []) : workerMarkersAs(text, sourceType);
478
+ }
479
+ /**
480
+ * An artifact as the references that name it make it (§2 Files that don't run as referenced). A file only `<link
481
+ * rel=preload as=script>` names is analyzed like a sweep-only file: its source type comes from its syntax, and its realm
482
+ * is assumed to be `window`. When only preloads name a file and they disagree, a classic one and a `modulepreload`, its
483
+ * source type comes from its syntax too. A file only the module requests of code whose realm is assumed name, such as a
484
+ * preload-only file's imports, inherits that realm basis. Any other reference decides as it found the file.
485
+ */
486
+ function asNamed(graph, key, found) {
487
+ const kinds = graph.references.get(key);
488
+ if (preloadsAlone(found, kinds)) {
489
+ const sourceType = syntaxOf(found);
490
+ return kinds.has('modulepreload') ? { ...found, sourceType } : { ...found, sourceType, realmBasis: 'assumed' };
491
+ }
492
+ return kinds !== undefined && assumedRealm(kinds) ? { ...found, realmBasis: 'assumed' } : found;
493
+ }
494
+ /**
495
+ * Whether preloads alone name a file (§2 Files that don't run as referenced): a classic preload names it, and so may a
496
+ * `modulepreload`, but nothing runs it and no other reference names it. It decides the artifact's type and realm basis,
497
+ * not whether the preload's syntax-typed visit happens (`preloadVisit`): an import that never runs
498
+ * still makes the file a `module`.
499
+ */
500
+ function preloadsAlone(found, kinds) {
501
+ return !found.runs && kinds !== undefined && kinds.has('preload') && !kinds.has('reference') && !kinds.has('assumed');
502
+ }
503
+ /**
504
+ * Whether the references that name a file leave its realm assumed: only code whose realm is assumed requests it, and
505
+ * perhaps a classic preload, which names no realm. No such reference runs a file.
506
+ */
507
+ function assumedRealm(kinds) {
508
+ return kinds.has('assumed') && !kinds.has('reference') && !kinds.has('modulepreload');
509
+ }
510
+ /** The source type of an artifact's code by its syntax (§2 Sweep): a `script` when its bytes weren't read. */
511
+ const syntaxOf = ({ text }) => text === null ? 'script' : sourceTypeBySyntax(text);
512
+ /**
513
+ * The syntax-typed visit that follows the file a classic preload names, when no running reference names it, in any
514
+ * realm (§2 Files that don't run as referenced): even when a preload-only file imports it. It
515
+ * reads the bytes of the preload's own fetch, whatever a module import of the same URL read, and its source type
516
+ * comes from their syntax. Its module requests inherit the realm basis the file's artifact has: assumed, unless a
517
+ * `modulepreload` or another reference whose realm is known names it. When a running reference runs the file, the
518
+ * preload follows that reference and adds no visit of its own.
519
+ */
520
+ async function preloadVisit(graph, preload, running) {
521
+ const key = at(preload.realm, preload.url, preload.fetched);
522
+ const found = graph.byRealmUrl.get(key);
523
+ const kinds = graph.references.get(key);
524
+ if (found === undefined || kinds === undefined || running.has(preload.url))
525
+ return undefined;
526
+ const { preloadIntegrity, ...visit } = preload;
527
+ // The store read it once for the preload, so reading it again as the preload fetched it gives that read.
528
+ const own = await readOnce(graph, visit.page, new URL(visit.fetched ?? visit.url), visit, preloadIntegrity);
529
+ if (own === null)
530
+ return undefined;
531
+ const { hash, text, retrieval, path, origin } = own;
532
+ const read = { hash, text, retrieval, path, origin };
533
+ const sourceType = syntaxOf(own);
534
+ const known = kinds.has('modulepreload') || kinds.has('reference');
535
+ return { ...visit, sourceType, read, ...(!known && { assumed: true }) };
536
+ }
537
+ /** The URLs a running reference runs, in any realm (§2 Files that don't run as referenced). */
538
+ function runningUrls(graph) {
539
+ const artifacts = [...graph.byRealmUrl.values(), ...graph.otherWay.values()];
540
+ return new Set(artifacts.flatMap(({ url, runs }) => (runs ? [url] : [])));
541
+ }
542
+ /** Orders visits by URL, then by entry document. */
543
+ function byUrlAndPage(a, b) {
544
+ return a.url < b.url ? -1 : a.url > b.url ? 1 : a.page.index - b.page.index;
545
+ }
546
+ /**
547
+ * Drops the window artifact of each file that only classic preloads name in the window, when a running reference runs
548
+ * it in another realm (§2 Files that don't run as referenced): the preload follows that reference, and
549
+ * adds no artifact or record of its own. A classically preloaded `/worker.js` that running code starts as a worker is
550
+ * analyzed only as the worker.
551
+ */
552
+ function dropRunElsewhere(graph, running) {
553
+ for (const { realm, url, fetched } of graph.preloads) {
554
+ const key = at(realm, url, fetched);
555
+ const kinds = graph.references.get(key);
556
+ if (!running.has(url) || graph.byRealmUrl.get(key)?.runs !== false)
557
+ continue;
558
+ if (kinds === undefined || [...kinds].some((kind) => kind !== 'preload'))
559
+ continue;
560
+ graph.byRealmUrl.delete(key);
561
+ graph.readLimitations.delete(key);
562
+ graph.references.delete(key);
563
+ }
564
+ }
565
+ /**
566
+ * A `realm-uncertain` record (§13) for an artifact whose realm is assumed to be `window` though its code has worker
567
+ * markers (§2 Realm): one only the sweep found, or only preloads, or code whose realm is assumed, name. It is located at
568
+ * the first marker, and its detail says why the realm is assumed, and names each kind of marker once, where it first
569
+ * appears.
570
+ */
571
+ function realmUncertain(graph, artifact, markers) {
572
+ const { url, path, hash, text } = artifact;
573
+ const first = new Map();
574
+ for (const marker of markers)
575
+ if (!first.has(marker.marker))
576
+ first.set(marker.marker, marker);
577
+ const named = [...first.values()].map(({ marker, start, end }) => {
578
+ const { line, column } = locate(text ?? '', start, end);
579
+ const what = marker === 'importScripts' ? 'top-level `importScripts`' : `\`self.addEventListener("${marker}")\``;
580
+ return `${what} at ${line}:${column}`;
581
+ });
582
+ const [{ start, end }] = markers;
583
+ const because = assumedBecause(graph, artifact);
584
+ return {
585
+ kind: 'realm-uncertain',
586
+ artifact: { url, ...(path === null ? {} : { path }), ...(hash === null ? {} : { hash }) },
587
+ location: locate(text ?? '', start, end),
588
+ detail: `${because}, so its realm is assumed to be window, but it has worker markers: ${named.join(', ')}`,
589
+ };
590
+ }
591
+ /** Why an artifact's realm is assumed (§2): what alone found or names it. */
592
+ function assumedBecause(graph, { discovery, realm, url, retrieval }) {
593
+ if (discovery === 'sweep')
594
+ return 'only the sweep found it';
595
+ return graph.references.get(at(realm, url, retrieval.requestedUrl ?? url))?.has('assumed')
596
+ ? 'only code whose realm is assumed imports it'
597
+ : 'only a `<link rel=preload as=script>` names it';
598
+ }
599
+ /**
600
+ * Sweeps the build directory (§2 Sweep): every JavaScript file no reference found, in any realm, is an artifact too,
601
+ * marked `sweep`, because chunk loaders compute URLs that discovery can't see. So is a file that only a script element
602
+ * the browser doesn't run names (`nomodule`, an ignored `type`, or a `<script src>` in `<template>` or `<noscript>`).
603
+ * It is found at the URL a mapping serves it at, and its realm is assumed to be `window`. A JavaScript or WebAssembly
604
+ * file no mapping serves is excluded, which keeps server bundles out of the scan, and a WebAssembly file a mapping
605
+ * serves is counted in `nonJs`. Every other file a mapping serves, and no reference found, is counted in `otherFiles`,
606
+ * and not analyzed. Remote artifacts are never swept.
607
+ */
608
+ async function sweep(graph) {
609
+ const { byRealmUrl, store } = graph;
610
+ const excluded = [];
611
+ let nonJs = 0;
612
+ const listing = await listed(graph);
613
+ // A directory that can't be listed, or a link whose target can't be reached, may hold files the scan should analyze,
614
+ // but only when a mapping serves its path or a path under it (§13 `unreadable`).
615
+ for (const { path, detail, link, served } of listing.unreadable) {
616
+ if (!served)
617
+ continue;
618
+ const what = link ? "the sweep can't reach the link's target" : "the sweep can't list the directory";
619
+ graph.limitations.push({ kind: 'unreadable', artifact: { path }, detail: `${what}: ${detail}` });
620
+ }
621
+ const found = new Set([...byRealmUrl.values()].flatMap(({ path }) => (path === null ? [] : [path])));
622
+ let otherFiles = 0;
623
+ for (const { path, url } of listing.files) {
624
+ if (found.has(path))
625
+ continue;
626
+ const kind = fileKind(path);
627
+ if (kind === undefined) {
628
+ if (url !== null)
629
+ otherFiles++;
630
+ }
631
+ else if (url === null)
632
+ excluded.push(path);
633
+ else if (kind === 'wasm')
634
+ nonJs++;
635
+ else {
636
+ const result = await store.readFile(path, url);
637
+ const fields = { url, path, origin: 'local-build', ...sweptFields };
638
+ if (result.status === 'ok') {
639
+ const text = decoder.decode(result.bytes);
640
+ const { sourceType, markers } = sweptCode(text);
641
+ byRealmUrl.set(at('window', url), { ...read(result.bytes, result.retrieval, text), ...fields, sourceType });
642
+ if (markers.length > 0)
643
+ graph.markers.set(url, markers);
644
+ }
645
+ else if (result.status === 'unreadable') {
646
+ // A build file is `ok` or `unreadable`.
647
+ byRealmUrl.set(at('window', url), { ...notRead(result), ...fields, sourceType: 'script' });
648
+ graph.limitations.push(unreadable(result));
649
+ }
650
+ }
651
+ }
652
+ // The listing is sorted by path, so `excluded` is too.
653
+ return { excluded, nonJs, otherFiles };
654
+ }
655
+ /** The fields of an artifact only the sweep found: it has no parent, no reference runs it, and its realm is assumed. */
656
+ const sweptFields = {
657
+ discovery: 'sweep',
658
+ realm: 'window',
659
+ realmBasis: 'assumed',
660
+ parent: null,
661
+ runs: false,
662
+ };
663
+ /**
664
+ * What the sweep takes a build file for, by its extension, ignoring ASCII case: JavaScript (`.js`, `.mjs` or `.cjs`),
665
+ * WebAssembly (`.wasm`), or neither.
666
+ */
667
+ function fileKind(path) {
668
+ const extension = asciiLowercase(posix.extname(path));
669
+ return extension === '.wasm' ? 'wasm' : ['.js', '.mjs', '.cjs'].includes(extension) ? 'js' : undefined;
670
+ }
671
+ /**
672
+ * An artifact's key in the graph: its realm and URL, and the URL it was fetched at when that isn't its URL. Code a
673
+ * redirected script references is keyed by one URL and fetched at another (§11), and one key URL reached through two
674
+ * fetched URLs is an artifact of each. A URL holds no space.
675
+ */
676
+ const at = (realm, url, fetched = url) => fetched === url ? `${realm} ${url}` : `${realm} ${url} ${fetched}`;
677
+ /**
678
+ * The artifact at `key` (its realm and URL) that a reference loading it as `sourceType` found: the one the page runs
679
+ * that way, when it runs it both ways, and otherwise the one there is.
680
+ */
681
+ function artifactAs(graph, key, sourceType) {
682
+ const other = graph.otherWay.get(key);
683
+ return other !== undefined && other.sourceType === sourceType ? other : graph.byRealmUrl.get(key);
684
+ }
685
+ /**
686
+ * Follows the requests of a script. Each module request names a module, which joins the graph in the script's realm,
687
+ * with the script as its parent, and runs when the script does. Each worker request the realm can make loads a worker
688
+ * script, the same way. A module request of code whose realm is assumed names a module whose realm is assumed too. A
689
+ * script is followed once per entry document, source type, `runs`, realm basis, realm and worker.
690
+ */
691
+ async function follow(graph, visit) {
692
+ const { url, fetched, page, sourceType, runs, realm, worker, assumed } = visit;
693
+ const own = visit.read === undefined ? '' : 'preload';
694
+ const followed = [url, fetched, page.index, sourceType, runs, assumed, realm, worker?.url, worker?.fetched];
695
+ const key = [...followed, worker?.sourceType, own].join(' ');
696
+ if (graph.followed.has(key))
697
+ return;
698
+ graph.followed.add(key);
699
+ const found = artifactAs(graph, at(realm, url, fetched), sourceType);
700
+ if (found === undefined)
701
+ return;
702
+ if (!graph.pageOf.has(at(realm, url, fetched)))
703
+ graph.pageOf.set(at(realm, url, fetched), page);
704
+ // A classic preload's syntax-typed visit parses the bytes of the preload's own fetch.
705
+ const artifact = visit.read === undefined ? found : { ...found, ...visit.read };
706
+ const requested = requestsOf(graph, artifact, sourceType);
707
+ const { modules, workers } = requested;
708
+ // A worker has its own import map, which no script element can add to, so it is always empty (HTML § Import maps).
709
+ const importMap = realm === 'window' ? (visit.importMap ?? page.importMap) : emptyImportMap;
710
+ // A fetched script's base URL is its response's URL, after any redirects (HTML § Fetching scripts), though the
711
+ // artifact keeps the URL it was requested at. An inline script's is its document's. What it references is fetched
712
+ // against that base, but keyed by the URL resolved against the script's own key URL (`visit.base`), so a key doesn't
713
+ // change when a redirect target moves (§11 Remote URL normalization, §17 What is recorded).
714
+ const base = artifact.retrieval.finalUrl ?? visit.base;
715
+ const keyBase = visit.base;
716
+ // Nothing is followed through a module graph that fails (§2 Import attributes): a hint still names its own file.
717
+ if (sourceType === 'module' && (await modulesFail(graph, page, realm, requested, importMap, base)))
718
+ return;
719
+ for (const request of modules) {
720
+ // A service worker rejects every dynamic import (HTML § HostLoadImportedModule).
721
+ if (request.dynamic && realm === 'service-worker')
722
+ continue;
723
+ // An ESM chunk loader's `import()` loads each chunk URL it folds to (§2, §6 idiom 6): each is followed as a
724
+ // literal dynamic import of it would be.
725
+ let specifiers;
726
+ const chunks = request.specifier === null;
727
+ if (request.specifier === null) {
728
+ const loads = graph.importSites.get(importSite({ url, ...(fetched === undefined ? {} : { fetched }) }, request.start));
729
+ if (loads === undefined) {
730
+ unresolvedReference(graph, artifact, request, 'a dynamic import whose specifier is not a literal');
731
+ continue;
732
+ }
733
+ // An alternative that doesn't fold is an unresolved reference, unless it is a local chunk URL, which the sweep
734
+ // covers (§2).
735
+ if (loads.some(({ url: prefix, partial }) => partial && !localChunk(graph, artifact, prefix, base))) {
736
+ unresolvedReference(graph, artifact, request, "a chunk loader's chunk URL that doesn't fold to a constant");
737
+ }
738
+ specifiers = loads.filter(({ partial }) => !partial).map((load) => load.url);
739
+ }
740
+ else
741
+ specifiers = [request.specifier];
742
+ // Import attributes that can't be read may name a module that is not JavaScript, so it is not fetched or parsed.
743
+ // A branch that names a JavaScript module still follows the import, and the unreadable one is recorded.
744
+ if (specifiers.length > 0 && !request.readableOptions) {
745
+ unresolvedReference(graph, artifact, request, "a dynamic import whose options can't be read");
746
+ if (!request.follow)
747
+ continue;
748
+ }
749
+ for (const specifier of specifiers) {
750
+ const chunk = await followImport(request, specifier);
751
+ // A remote chunk joins its loader's group (`Discovered.remoteChunks`).
752
+ if (chunks && chunk !== undefined && !graph.store.isMapped(chunk) && graph.byRealmUrl.has(at(realm, chunk))) {
753
+ const record = { realm, loader: { url, ...(fetched === undefined ? {} : { fetched }) }, chunk };
754
+ graph.remoteChunks.set(JSON.stringify(record), record);
755
+ }
756
+ }
757
+ }
758
+ for (const request of workers)
759
+ await workerReference(graph, visit, artifact, request);
760
+ /** Follows a module request with `specifier`, and gives the URL of the module it names, if it names one. */
761
+ async function followImport(request, specifier) {
762
+ const resolved = resolveSpecifier(importMap, specifier, base);
763
+ // A sweep-only file's import of another swept file links it in the file's group (§19): the module stays the
764
+ // sweep's (`sweptImports`).
765
+ if (visit.swept && !('error' in resolved) && isSwept(graph, resolved.url.href))
766
+ return undefined;
767
+ const keyed = keyBase === base ? resolved : resolveSpecifier(importMap, specifier, keyBase);
768
+ // A module request names a module in the script's realm, which is assumed when the script's is.
769
+ const how = { sourceType: 'module', runs, realm, ...(assumed && { assumed }) };
770
+ if ('error' in resolved) {
771
+ unresolvedReference(graph, artifact, request, resolved.error);
772
+ return undefined;
773
+ }
774
+ // An import is checked against the import map's integrity value for its URL (HTML § Descendant script fetch
775
+ // options).
776
+ const integrity = moduleIntegrity(importMap, resolved.url);
777
+ // A dynamic import is the root of a module graph of its own, which gives nothing when it fails.
778
+ if (request.dynamic && (await graphFails(graph, page, realm, [{ url: resolved.url, integrity }])))
779
+ return undefined;
780
+ const keyUrl = 'error' in keyed ? resolved.url : keyed.url;
781
+ const to = keyUrl.href === resolved.url.href ? { url: keyUrl.href } : { url: keyUrl.href, fetched: resolved.url.href };
782
+ recordImport(graph, realm, page.entry.url, { url, ...(fetched === undefined ? {} : { fetched }) }, specifier, to);
783
+ await reference(graph, page, resolved.url, { ...how, request: true }, url, worker, integrity, keyUrl);
784
+ return keyUrl.href === resolved.url.href ? keyUrl.href : undefined;
785
+ }
786
+ }
787
+ /** The realm each kind of worker function creates a worker in. `importScripts` loads a script into its own worker. */
788
+ const workerRealms = {
789
+ worker: 'dedicated-worker',
790
+ 'shared-worker': 'shared-worker',
791
+ 'service-worker': 'service-worker',
792
+ };
793
+ /**
794
+ * Follows a worker request of a script, when the script's realm can make it (`realmHas`): its URL resolves against
795
+ * the environment's base URL, the document's in the window and the worker script's in a worker (HTML § The Worker
796
+ * constructor, § importScripts, and Service Workers § register). A new worker's script is referenced in the worker's
797
+ * realm, and a script `importScripts` loads, in the script's own realm, as a classic script of its worker. A module
798
+ * worker's `importScripts` throws, and so does a URL that doesn't parse. A service worker's script outside every
799
+ * mapping gives an `unresolved-reference` record, and a `data:` one nothing. A dedicated or shared worker's script on
800
+ * another origin than its creator's (`creatorOrigin`) gives nothing, unless it is a `data:` URL, which is followed in
801
+ * the worker's realm (§2). A worker whose options can't be read statically is a module when its script's syntax says
802
+ * so (`sourceTypeOf`).
803
+ */
804
+ async function workerReference(graph, visit, script, request) {
805
+ const { realm, worker } = visit;
806
+ if (!callable(graph.catalog, realm, request))
807
+ return;
808
+ if (request.kind === 'import-scripts' && worker?.sourceType !== 'script')
809
+ return;
810
+ if (request.url !== null) {
811
+ await literalWorker(graph, visit, script, { ...request, url: request.url });
812
+ return;
813
+ }
814
+ // A URL that isn't a literal is each constant alternative the resolver folded it to, and each alternative that
815
+ // doesn't fold is an `unresolved-reference` there (§2 Computed URLs). A chunk loader's `importScripts`
816
+ // is followed as a chunk load instead (`followReferences`; §2). Until the resolver has folded it, the whole
817
+ // URL is one alternative that doesn't fold.
818
+ const from = { url: visit.url, ...(visit.fetched === undefined ? {} : { fetched: visit.fetched }) };
819
+ if (request.kind === 'import-scripts' && graph.chunkSites.has(importSite(from, request.start)))
820
+ return;
821
+ const folded = graph.workerSites.get(importSite(from, request.start));
822
+ const detail = 'a worker URL that is not a constant';
823
+ if (folded === undefined) {
824
+ unresolvedReference(graph, script, request, detail);
825
+ return;
826
+ }
827
+ for (const alternative of folded) {
828
+ if (alternative.none)
829
+ continue;
830
+ if (alternative.partial)
831
+ unresolvedReference(graph, script, alternative.alternative ?? request, detail);
832
+ else
833
+ await literalWorker(graph, visit, script, { ...request, url: alternative.url });
834
+ }
835
+ }
836
+ /** Follows a worker request with a constant URL (`workerReference`). */
837
+ async function literalWorker(graph, visit, script, request) {
838
+ const { realm, page, worker } = visit;
839
+ // A worker's URL is its script's response URL, after any redirects (HTML § Run a worker). What it references is
840
+ // fetched against that URL, and keyed by the URL resolved against its script's key URL (§11).
841
+ const workerUrl = worker && (graph.byRealmUrl.get(at(realm, worker.url, worker.fetched))?.retrieval.finalUrl ?? worker.url);
842
+ const url = URL.parse(request.url, workerUrl ?? page.base);
843
+ const keyUrl = (worker && URL.parse(request.url, worker.url)) ?? url;
844
+ // A script URL whose scheme is not `http:`, `https:` or `data:` gives nothing wherever it appears (§2 Other script
845
+ // URLs), not even an unresolved service worker.
846
+ if (url === null || !scriptSchemes.has(url.protocol))
847
+ return;
848
+ if (request.kind === 'import-scripts') {
849
+ const how = { sourceType: 'script', runs: visit.runs, realm, imported: true };
850
+ await reference(graph, page, url, how, visit.url, worker, '', keyUrl ?? url);
851
+ return;
852
+ }
853
+ // A browser refuses to register a `data:` service worker, so it gives nothing (§2).
854
+ if (request.kind === 'service-worker' && url.protocol === 'data:')
855
+ return;
856
+ // A service worker must be same-origin with the page, and a script outside every mapping is none of the build's.
857
+ if (request.kind === 'service-worker' && !graph.store.isMapped(url.href)) {
858
+ unresolvedReference(graph, script, request, 'a service worker whose script falls outside every URL mapping');
859
+ return;
860
+ }
861
+ // A browser refuses to run a dedicated or shared worker's script from another origin than its creator's, so it is not
862
+ // followed, and nothing is missed (§2). A `data:` worker's script runs whatever its creator, and its code is the
863
+ // page's (§2). A service worker's script is judged by the mappings alone, above.
864
+ if (request.kind !== 'service-worker' && url.protocol !== 'data:' && url.origin !== creatorOrigin(visit))
865
+ return;
866
+ const workerRealm = workerRealms[request.kind];
867
+ const sourceType = request.sourceType ?? (await sourceTypeOf(graph, page, url, workerRealm, keyUrl ?? url));
868
+ // A module worker's script is the root of a module graph, which gives nothing when it fails (§2 Import attributes).
869
+ if (sourceType === 'module' && (await graphFails(graph, page, workerRealm, [{ url, integrity: '' }])))
870
+ return;
871
+ const how = { sourceType, runs: visit.runs, realm: workerRealm };
872
+ const keyed = (keyUrl ?? url).href;
873
+ const started = { url: keyed, ...(keyed === url.href ? {} : { fetched: url.href }), sourceType };
874
+ await reference(graph, page, url, how, visit.url, started, '', keyUrl ?? url);
875
+ }
876
+ /**
877
+ * The origin of the code a visit follows, which creates its workers: the page's, in the window and in a worker whose
878
+ * script is on the page's origin, as only those are followed, and none in a `data:` worker, whose origin is opaque
879
+ * (HTML § The Worker constructor), so that no URL's origin is its.
880
+ */
881
+ function creatorOrigin({ page, worker }) {
882
+ return worker !== undefined && worker.url.startsWith('data:') ? null : new URL(page.entry.url).origin;
883
+ }
884
+ /**
885
+ * The source type of a worker's script whose options can't be read statically: its syntax decides it, as a sweep-only
886
+ * file's (§2). Its bytes are those of the artifact the realm has at its URL, if any, or else those it reads as a worker
887
+ * script: a `data:` URL's own. A script whose bytes can't be read is a classic script, the default.
888
+ */
889
+ async function sourceTypeOf(graph, page, url, realm, keyUrl) {
890
+ const found = graph.byRealmUrl.get(at(realm, keyUrl.href, url.href));
891
+ const read = found ?? (await readOnce(graph, page, url, { sourceType: 'script', realm }, ''));
892
+ const text = read?.text ?? null;
893
+ return text === null ? 'script' : sourceTypeBySyntax(text);
894
+ }
895
+ /**
896
+ * Whether a realm's global object has the function a worker request calls (§19: discovery reads the realm globals from
897
+ * the catalog), and the global name it calls it through, if any: `Worker` in the window and in dedicated and shared
898
+ * workers, `SharedWorker` in the window only, `importScripts` in workers only, and `navigator` with
899
+ * `ServiceWorkerContainer.prototype.register` in every realm; `window` only in the window.
900
+ */
901
+ function callable(catalog, realm, { kind, through }) {
902
+ if (through !== undefined && !realmHas(catalog, realm, through))
903
+ return false;
904
+ switch (kind) {
905
+ case 'worker':
906
+ return realmHas(catalog, realm, 'Worker');
907
+ case 'shared-worker':
908
+ return realmHas(catalog, realm, 'SharedWorker');
909
+ case 'import-scripts':
910
+ return realmHas(catalog, realm, 'importScripts');
911
+ case 'service-worker':
912
+ return (realmHas(catalog, realm, 'navigator') &&
913
+ Object.hasOwn(catalog.targets[realm], 'ServiceWorkerContainer.prototype.register'));
914
+ }
915
+ }
916
+ /** Whether the realm's global object has the member `name`, its own or inherited, in the catalog (§4). */
917
+ function realmHas(catalog, realm, name) {
918
+ const holders = catalog.holders[realm];
919
+ const targets = catalog.targets[realm];
920
+ let holder = Object.keys(holders).find((key) => holders[key].kind === 'global') ?? null;
921
+ for (; holder !== null; holder = holders[holder]?.proto ?? null) {
922
+ if (Object.hasOwn(targets, `${holder}.${name}`))
923
+ return true;
924
+ }
925
+ return false;
926
+ }
927
+ /**
928
+ * Records an `unresolved-reference` (§13) at a request of a script, once however many entry documents follow it. The
929
+ * record names the script's own URL and path, which its requests resolve against, whatever other URL has its bytes.
930
+ */
931
+ function unresolvedReference(graph, script, { start, end }, detail) {
932
+ const { url, path, hash, text } = script;
933
+ addUnresolved(graph, {
934
+ kind: 'unresolved-reference',
935
+ artifact: { url, ...(path === null ? {} : { path }), ...(hash === null ? {} : { hash }) },
936
+ location: locate(text ?? '', start, end),
937
+ detail,
938
+ });
939
+ }
940
+ /** Records an `unresolved-reference` (§13) once, however many entry documents or scripts raise it. */
941
+ function addUnresolved(graph, record) {
942
+ const key = JSON.stringify(record);
943
+ if (graph.unresolved.has(key))
944
+ return;
945
+ graph.unresolved.add(key);
946
+ graph.limitations.push(record);
947
+ }
948
+ /**
949
+ * The location of the UTF-16 range `[start, end)` of `text` (§5 Positions): its start as a 1-based line and a 0-based
950
+ * column. Lines end at an ECMAScript LineTerminatorSequence: LF, CR, CRLF, U+2028 or U+2029.
951
+ */
952
+ function locate(text, start, end) {
953
+ let line = 1;
954
+ let lineStart = 0;
955
+ for (let i = 0; i < start; i++) {
956
+ const code = text.charCodeAt(i);
957
+ if (code === 0x0d && text.charCodeAt(i + 1) === 0x0a)
958
+ i++;
959
+ if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) {
960
+ line++;
961
+ lineStart = i + 1;
962
+ }
963
+ }
964
+ return { start, end, line, column: start - lineStart };
965
+ }
966
+ /** The module requests that resolve through import maps: those with a literal specifier and readable options. */
967
+ function followable(modules) {
968
+ return modules.filter((request) => request.specifier !== null && request.follow);
969
+ }
970
+ /** The requests of an artifact's bytes, parsed once for each source type. None when they weren't read. */
971
+ function requestsOf(graph, { hash, text }, sourceType) {
972
+ if (hash === null || text === null)
973
+ return { modules: [], workers: [], unsupported: false };
974
+ const key = `${sourceType} ${hash}`;
975
+ let found = graph.requests.get(key);
976
+ if (found === undefined)
977
+ graph.requests.set(key, (found = requests(text, sourceType)));
978
+ return found;
979
+ }
980
+ /**
981
+ * Follows an entry document's references, element by element in tree order, as the browser would on insertion. The
982
+ * contents of a `<template>` are not its children, so they are never visited, and parse5 parses the contents of
983
+ * `<noscript>` (with scripting enabled) and `<iframe>` as text. An `iframe srcdoc` is an attribute.
984
+ */
985
+ async function walk(graph, page, root, mapsOnly = false) {
986
+ const { entry } = page;
987
+ // The first `<base href>` sets the base URL (§19). A reference before it resolves against the document's URL: the
988
+ // browser resolves a script's `src` when the parser inserts it, before a later `<base>` exists.
989
+ let base;
990
+ for (const element of elements(root)) {
991
+ if (mapsOnly) {
992
+ if (isHtml(element, 'script'))
993
+ mapScript(graph, page, element, base ?? entry.url);
994
+ }
995
+ else
996
+ graph.html.inlineHandlers += inlineHandlers(graph, element, base ?? entry.url);
997
+ if (isHtml(element, 'base') && base === undefined) {
998
+ const href = attribute(element, 'href');
999
+ if (href !== undefined)
1000
+ page.base = base = frozenBaseUrl(href, entry.url);
1001
+ }
1002
+ else if (mapsOnly)
1003
+ continue;
1004
+ else if (isHtml(element, 'script'))
1005
+ await script(graph, page, element, base ?? entry.url);
1006
+ else if (isHtml(element, 'link'))
1007
+ await link(graph, page, element, base ?? entry.url);
1008
+ }
1009
+ }
1010
+ /**
1011
+ * What an inline `script` element adds to its document's import maps (§2 Import maps): an import map merges into them,
1012
+ * and each specifier an inline module script's requests resolve through the maps before it joins the document's
1013
+ * resolved module set, which later maps can't change, whether or not the module's graph later fails. A browser rejects
1014
+ * an external import map (`src`), and an invalid one.
1015
+ */
1016
+ function mapScript(graph, page, element, base) {
1017
+ if (attribute(element, 'src') !== undefined)
1018
+ return;
1019
+ const text = inlineText(element);
1020
+ if (text === '')
1021
+ return;
1022
+ const sourceType = scriptType(element);
1023
+ if (sourceType === 'importmap') {
1024
+ const importMap = parseImportMap(text, base);
1025
+ if (importMap !== null)
1026
+ page.importMap = mergeImportMaps(page.importMap, importMap, page.resolved);
1027
+ }
1028
+ else if (sourceType === 'module') {
1029
+ for (const { specifier } of followable(inlineRequests(graph, text).modules)) {
1030
+ const resolved = resolveSpecifier(page.importMap, specifier, base);
1031
+ if ('record' in resolved)
1032
+ page.resolved.push(resolved.record);
1033
+ }
1034
+ }
1035
+ }
1036
+ /** The requests of an inline module script's text. */
1037
+ function inlineRequests(graph, text) {
1038
+ return requestsOf(graph, read(encoder.encode(text), inlineRetrieval, text), 'module');
1039
+ }
1040
+ /** An inline `script` element's text. */
1041
+ const inlineText = (element) => element.childNodes.map((child) => ('value' in child ? child.value : '')).join('');
1042
+ /**
1043
+ * Follows a `link` that preloads a script: `<link rel=preload as=script>` for a classic script, or `<link
1044
+ * rel=modulepreload>` for a module of the window or of a worker, whose `href` is resolved against `base`. Its file
1045
+ * joins the graph, but a preload never makes it run (§19 HTML rules). A service worker's module outside every URL
1046
+ * mapping gives an `unresolved-reference` record naming the entry document instead, as `register` does (§2).
1047
+ */
1048
+ async function link(graph, page, element, base) {
1049
+ const classic = { sourceType: 'script', realm: 'window' };
1050
+ const preload = isScriptPreload(element) ? classic : modulePreload(element);
1051
+ if (preload === undefined)
1052
+ return;
1053
+ const url = referenceUrl(attribute(element, 'href'), base);
1054
+ if (url === null)
1055
+ return;
1056
+ const { sourceType, realm } = preload;
1057
+ // A script URL whose scheme is not `http:`, `https:` or `data:` gives nothing wherever it appears (§2 Other script
1058
+ // URLs), not even an unresolved service worker.
1059
+ if (!scriptSchemes.has(url.protocol))
1060
+ return;
1061
+ // A browser refuses to register a `data:` service worker, so one preloaded for a service worker gives nothing (§2).
1062
+ if (realm === 'service-worker' && url.protocol === 'data:')
1063
+ return;
1064
+ // A service worker's script outside every mapping is none of the build's, as for `register` (§2).
1065
+ if (realm === 'service-worker' && !graph.store.isMapped(url.href)) {
1066
+ const { path } = page;
1067
+ addUnresolved(graph, {
1068
+ kind: 'unresolved-reference',
1069
+ artifact: { url: page.entry.url, ...(path === null ? {} : { path }) },
1070
+ detail: `a service worker module preload whose URL, ${url.href}, falls outside every URL mapping`,
1071
+ });
1072
+ return;
1073
+ }
1074
+ // A module preloaded for a worker is taken for the worker's script.
1075
+ const worker = realm === 'window' ? undefined : { url: url.href, sourceType };
1076
+ const integrity = integrityOf(element, sourceType, page, url);
1077
+ const hint = sourceType === 'script' ? 'preload' : 'modulepreload';
1078
+ await reference(graph, page, url, { sourceType, runs: false, realm, hint }, page.entry.url, worker, integrity);
1079
+ }
1080
+ /**
1081
+ * The inline event-handler attributes and `javascript:` URLs of an element, which are counted and not analyzed (§19
1082
+ * HTML rules). An event handler is an attribute named for an event handler of an element interface. A `javascript:`
1083
+ * URL counts where a browser runs one: in a link, frame or form target.
1084
+ */
1085
+ function inlineHandlers(graph, element, base) {
1086
+ let count = 0;
1087
+ for (const { name, value, prefix } of element.attrs) {
1088
+ if (prefix === undefined && graph.eventHandlers.has(name))
1089
+ count++;
1090
+ else if (runsJavascriptUrl(element, name) && URL.parse(value, base)?.protocol === 'javascript:')
1091
+ count++;
1092
+ }
1093
+ return count;
1094
+ }
1095
+ /** The HTML elements and attributes a browser navigates to, or submits a form to, so running a `javascript:` URL. */
1096
+ const javascriptUrlAttributes = {
1097
+ a: ['href'],
1098
+ area: ['href'],
1099
+ iframe: ['src'],
1100
+ frame: ['src'],
1101
+ form: ['action'],
1102
+ button: ['formaction'],
1103
+ input: ['formaction'],
1104
+ };
1105
+ /** Whether a browser runs a `javascript:` URL in the attribute `name` of `element`: a link, frame or form target. */
1106
+ function runsJavascriptUrl(element, name) {
1107
+ if (element.namespaceURI === html.NS.SVG)
1108
+ return element.tagName === 'a' && name === 'href';
1109
+ return element.namespaceURI === html.NS.HTML && (javascriptUrlAttributes[element.tagName]?.includes(name) ?? false);
1110
+ }
1111
+ /**
1112
+ * The names of event handler content attributes: the event handler attributes (§4 `assign: event-handler`) that the
1113
+ * catalog's element interfaces carry, whose prototype chains reach `Element.prototype`, such as `onclick` and, on
1114
+ * `<body>`, `onload`.
1115
+ */
1116
+ function eventHandlers(catalog) {
1117
+ const { targets, holders } = catalog;
1118
+ const isElement = (holder) => holder === 'Element.prototype' || (holder !== null && isElement(holders.window[holder]?.proto ?? null));
1119
+ const names = new Set();
1120
+ for (const [name, target] of Object.entries(targets.window)) {
1121
+ const holder = name.slice(0, name.length - target.member.length - 1);
1122
+ if (target.assign === 'event-handler' && isElement(holder))
1123
+ names.add(target.member);
1124
+ }
1125
+ return names;
1126
+ }
1127
+ /**
1128
+ * A `<base>`'s frozen base URL (HTML § The base element): its `href` resolved against the document's URL, unless that
1129
+ * fails or gives a `data:` or `javascript:` URL, which leave the document's URL.
1130
+ */
1131
+ function frozenBaseUrl(href, documentUrl) {
1132
+ const url = URL.parse(href, documentUrl);
1133
+ return url === null || url.protocol === 'data:' || url.protocol === 'javascript:' ? documentUrl : url.href;
1134
+ }
1135
+ /** Follows a `script` element: an inline script, or the file its `src` names, resolved against `base`. */
1136
+ async function script(graph, page, element, base) {
1137
+ const { entry } = page;
1138
+ const src = attribute(element, 'src');
1139
+ const inline = src === undefined ? (graph.inlineCounts.get(entry.url) ?? 0) + 1 : 0;
1140
+ if (src === undefined)
1141
+ graph.inlineCounts.set(entry.url, inline);
1142
+ const text = inlineText(element);
1143
+ // The browser's steps, in its order (HTML § Prepare the script element): an empty inline script does nothing.
1144
+ if (src === undefined && text === '')
1145
+ return;
1146
+ const sourceType = scriptType(element);
1147
+ // Every type but classic, module and import map is ignored.
1148
+ if (sourceType === undefined)
1149
+ return;
1150
+ if (sourceType === 'importmap')
1151
+ return mapScript(graph, page, element, base);
1152
+ // A classic `nomodule` script is counted, not analyzed (§19 HTML rules). A module script ignores the attribute.
1153
+ if (sourceType === 'script' && attribute(element, 'nomodule') !== undefined) {
1154
+ graph.html.nomodule++;
1155
+ return;
1156
+ }
1157
+ if (sourceType === 'script' && !legacyEventRuns(element))
1158
+ return;
1159
+ if (src === undefined) {
1160
+ const url = inlineUrl(entry.url, inline);
1161
+ const how = { sourceType, runs: true, realm: 'window' };
1162
+ const fields = { url, path: null, origin: 'inline', parent: entry.url, ...how };
1163
+ const found = { ...fields, ...read(encoder.encode(text), inlineRetrieval, text) };
1164
+ if (sourceType === 'module') {
1165
+ // An inline module script's requests resolve through the import maps before it (§2 Import maps). Its graph fails
1166
+ // as they resolve, and then the script gives nothing (§2 Import attributes).
1167
+ const importMap = page.importMap;
1168
+ mapScript(graph, page, element, base);
1169
+ if (await modulesFail(graph, page, 'window', inlineRequests(graph, text), importMap, base))
1170
+ return;
1171
+ }
1172
+ graph.byRealmUrl.set(at('window', url), found);
1173
+ recordNamed(graph, at('window', url), entry.url);
1174
+ graph.ran.add(at('window', url));
1175
+ graph.visits.push({ url, page, base, ...how, ...(sourceType === 'module' && { importMap: page.importMap }) });
1176
+ return;
1177
+ }
1178
+ // A `src` that is empty or no URL makes the script fail, and its inline text never runs.
1179
+ const url = referenceUrl(src, base);
1180
+ if (url === null)
1181
+ return;
1182
+ const integrity = integrityOf(element, sourceType, page, url);
1183
+ // A module script element is the root of a module graph, which gives nothing when it fails (§2 Import attributes).
1184
+ if (sourceType === 'module' && (await graphFails(graph, page, 'window', [{ url, integrity }])))
1185
+ return;
1186
+ await reference(graph, page, url, { sourceType, runs: true, realm: 'window' }, entry.url, undefined, integrity);
1187
+ }
1188
+ /**
1189
+ * Whether the module graph of a module fails, as its requests `requested` say, resolved through `importMap` against
1190
+ * `base` (§2 Import attributes, Principle 1): it has a static import with an unsupported attribute, or a module it
1191
+ * statically imports fails (`graphFails`).
1192
+ */
1193
+ async function modulesFail(graph, page, realm, requested, importMap, base) {
1194
+ if (requested.unsupported)
1195
+ return true;
1196
+ return graphFails(graph, page, realm, staticImports(requested.modules, importMap, base));
1197
+ }
1198
+ /** The modules that static requests name, resolved through `importMap` against `base`, with their integrity values. */
1199
+ function staticImports(modules, importMap, base) {
1200
+ return followable(modules)
1201
+ .filter(({ dynamic }) => !dynamic)
1202
+ .flatMap(({ specifier }) => {
1203
+ const resolved = resolveSpecifier(importMap, specifier, base);
1204
+ return 'error' in resolved ? [] : [{ url: resolved.url, integrity: moduleIntegrity(importMap, resolved.url) }];
1205
+ });
1206
+ }
1207
+ /**
1208
+ * Whether the module graph that `roots` start in `realm` fails (§2 Import attributes, Principle 1): a module in it,
1209
+ * found through static imports and re-exports resolved through all of the document's import maps (a worker's are
1210
+ * empty), has a static import with an unsupported attribute, which fails every graph that contains it, as Chromium
1211
+ * does. Local and `data:` modules are read before any remote one is fetched, one remote module at a time, so that no
1212
+ * remote module is fetched once a failure is found. A module whose bytes can't be read fails nothing. Reading records
1213
+ * nothing: the references that follow record what they read.
1214
+ */
1215
+ async function graphFails(graph, page, realm, roots) {
1216
+ const importMap = realm === 'window' ? page.finalMap : emptyImportMap;
1217
+ const key = (url) => wholeKey(page, realm, url);
1218
+ const seen = new Set();
1219
+ const local = [];
1220
+ const remote = [];
1221
+ const add = (root) => {
1222
+ if (!scriptSchemes.has(root.url.protocol) || seen.has(root.url.href) || graph.whole.has(key(root.url)))
1223
+ return;
1224
+ seen.add(root.url.href);
1225
+ const isLocal = root.url.protocol === 'data:' || graph.store.isMapped(root.url.href);
1226
+ (isLocal ? local : remote).push(root);
1227
+ };
1228
+ roots.forEach(add);
1229
+ for (let next = local.shift() ?? remote.shift(); next !== undefined; next = local.shift() ?? remote.shift()) {
1230
+ const read = await readOnce(graph, page, next.url, { sourceType: 'module', realm }, next.integrity);
1231
+ if (read === null)
1232
+ continue;
1233
+ const requested = requestsOf(graph, read, 'module');
1234
+ if (requested.unsupported)
1235
+ return true;
1236
+ staticImports(requested.modules, importMap, read.retrieval.finalUrl ?? next.url.href).forEach(add);
1237
+ }
1238
+ for (const href of seen)
1239
+ graph.whole.add(key(new URL(href)));
1240
+ return false;
1241
+ }
1242
+ /** A module's key in `whole`: its URL, and the import map its requests resolve through (its document's, or none). */
1243
+ const wholeKey = (page, realm, url) => `${realm === 'window' ? page.index : 'worker'} ${url.href}`;
1244
+ /**
1245
+ * The integrity metadata a `script` or `link` element fetches `url` with (§17 Integrity): its `integrity` attribute,
1246
+ * even an empty one, or for a module script or a module preload with none, the value the document's import maps give
1247
+ * `url` so far (HTML § Prepare the script element, § Link type "modulepreload"). The empty string checks nothing.
1248
+ */
1249
+ function integrityOf(element, sourceType, page, url) {
1250
+ const value = attribute(element, 'integrity');
1251
+ if (value !== undefined)
1252
+ return value;
1253
+ return sourceType === 'module' ? moduleIntegrity(page.importMap, url) : '';
1254
+ }
1255
+ /** The URL an attribute references, resolved against `base`; null when it is missing, empty or no URL. */
1256
+ function referenceUrl(value, base) {
1257
+ return value === undefined || value.trim() === '' ? null : URL.parse(value.trim(), base);
1258
+ }
1259
+ /** `<link rel=preload as=script>`: `rel` holds the token `preload`, and `as` is `script`, ignoring ASCII case. */
1260
+ function isScriptPreload(element) {
1261
+ return relTokens(element).includes('preload') && asciiLowercase(attribute(element, 'as') ?? '') === 'script';
1262
+ }
1263
+ /**
1264
+ * The keywords of a `link`'s `as` attribute (HTML § The link element): the preload destinations, and the module
1265
+ * preload destinations, which are `json`, `style`, `text` and the script-like destinations (Fetch § Requests). Any
1266
+ * other value, like a missing one, is in no state.
1267
+ */
1268
+ const linkAsKeywords = new Set([
1269
+ ...['fetch', 'font', 'image', 'script', 'style', 'track'],
1270
+ ...['json', 'text', 'audioworklet', 'paintworklet', 'serviceworker', 'sharedworker', 'worker'],
1271
+ ]);
1272
+ /** The realm a module preload's worker destination (HTML § Link type "modulepreload") preloads a module for. */
1273
+ const workerDestinations = {
1274
+ worker: 'dedicated-worker',
1275
+ sharedworker: 'shared-worker',
1276
+ serviceworker: 'service-worker',
1277
+ };
1278
+ /**
1279
+ * `<link rel=modulepreload>`, which preloads a module: `rel` holds the token `modulepreload`, and `as`, ignoring ASCII
1280
+ * case, gives the destination (HTML § Link type "modulepreload"). It is a module script of the window when `as` is
1281
+ * `script` or in no state (missing, empty or no keyword), which HTML maps to `script`, and of a worker's realm when it
1282
+ * is `worker`, `sharedworker` or `serviceworker`. A JSON, CSS or text module is not JavaScript, and any other
1283
+ * destination makes the preload fail.
1284
+ */
1285
+ function modulePreload(element) {
1286
+ if (!relTokens(element).includes('modulepreload'))
1287
+ return undefined;
1288
+ const as = asciiLowercase(attribute(element, 'as') ?? '');
1289
+ if (as === 'script' || !linkAsKeywords.has(as))
1290
+ return { sourceType: 'module', realm: 'window' };
1291
+ const realm = Object.hasOwn(workerDestinations, as) ? workerDestinations[as] : undefined;
1292
+ return realm === undefined ? undefined : { sourceType: 'module', realm };
1293
+ }
1294
+ /** The tokens of a `link`'s `rel`, in ASCII lowercase. */
1295
+ function relTokens(element) {
1296
+ return asciiLowercase(attribute(element, 'rel') ?? '').split(/[\t\n\f\r ]+/);
1297
+ }
1298
+ /**
1299
+ * Follows a reference to a script file from an entry document's element, or from a script's module or worker request:
1300
+ * a local file through the URL mappings, a `data:` URL's own bytes, or a remote `http:` or `https:` one through the
1301
+ * fetcher, with the integrity metadata the reference carries, if any (§17 Integrity). A URL with any other scheme gives
1302
+ * nothing, and so does a `data:` script a browser would refuse to run (§2 Other script URLs). A URL referenced before in
1303
+ * the same realm is the artifact found then, which a reference that runs it makes run. When that changes what it is
1304
+ * fetched as, such as a classic preload that a module import then names, it is read again as that (§17 Content types).
1305
+ * A reference that runs it the other way, as a module where it runs as a classic script or the reverse, makes it an
1306
+ * artifact that way too (§2 Files that don't run as referenced). The script its bytes hold is to be followed, as this
1307
+ * reference found it, in the worker `worker` when the realm is a worker's; a file a classic preload names is followed
1308
+ * only once every running reference has been (`preloadVisit`).
1309
+ */
1310
+ async function reference(graph, page, url, { hint, imported, assumed, request, injected, ...how }, parent, worker, integrity = '', keyUrl = url) {
1311
+ // A script URL whose scheme is not `http:`, `https:` or `data:` gives nothing wherever it appears (§2 Other script
1312
+ // URLs).
1313
+ if (!scriptSchemes.has(url.protocol))
1314
+ return;
1315
+ const { byRealmUrl } = graph;
1316
+ const key = at(how.realm, keyUrl.href, url.href);
1317
+ // How this reference fetches the file. Whether `importScripts` loads it only changes what a `data:` URL may hold.
1318
+ const fetching = imported === undefined ? how : { ...how, imported };
1319
+ const found = byRealmUrl.get(key);
1320
+ if (found === undefined) {
1321
+ const fields = await readAs(graph, page, url, fetching, key, integrity, keyUrl);
1322
+ // A `data:` script a browser refuses to run gives no artifact, no record and no findings.
1323
+ if (fields === null)
1324
+ return;
1325
+ byRealmUrl.set(key, { url: keyUrl.href, ...how, parent, ...fields, ...(injected && { discovery: 'injected' }) });
1326
+ }
1327
+ else if (!found.runs && (how.runs || how.sourceType === 'module')) {
1328
+ // A reference that runs the file gives it its source type, and so does a module reference when neither runs it: an
1329
+ // import or a `modulepreload` says how the browser loads it, where a classic preload is only a hint.
1330
+ const reread = fetchesAgain(graph, found, how, key, integrity)
1331
+ ? await readAs(graph, page, url, fetching, key, integrity, keyUrl)
1332
+ : {};
1333
+ if (reread === null)
1334
+ return;
1335
+ byRealmUrl.set(key, { ...found, ...how, ...reread });
1336
+ }
1337
+ else if (found.runs && how.runs && how.sourceType !== found.sourceType && !graph.otherWay.has(key)) {
1338
+ // A file the page runs both as a classic script and as a module is analyzed both ways (§2): the second way is an
1339
+ // artifact of its own, read as that way fetches it.
1340
+ const fields = await readAs(graph, page, url, fetching, JSON.stringify([key, how.sourceType]), integrity, keyUrl);
1341
+ if (fields === null)
1342
+ return;
1343
+ graph.otherWay.set(key, { url: keyUrl.href, ...how, parent, ...fields });
1344
+ }
1345
+ if (request !== undefined)
1346
+ graph.requested.add(key);
1347
+ else {
1348
+ recordNamed(graph, key, page.entry.url);
1349
+ if (how.runs)
1350
+ graph.ran.add(key);
1351
+ }
1352
+ const naming = hint ?? (assumed ? 'assumed' : 'reference');
1353
+ graph.references.set(key, (graph.references.get(key) ?? new Set()).add(naming));
1354
+ const fetched = url.href === keyUrl.href ? {} : { fetched: url.href };
1355
+ const visit = { url: keyUrl.href, ...fetched, page, base: keyUrl.href, ...how, ...(assumed && { assumed }) };
1356
+ // A file a classic preload names is followed only once every running reference is (`preloadVisit`).
1357
+ if (hint === 'preload')
1358
+ graph.preloads.push({ ...visit, preloadIntegrity: integrity });
1359
+ else
1360
+ graph.visits.push(worker === undefined ? visit : { ...visit, worker });
1361
+ }
1362
+ /**
1363
+ * The schemes of the script URLs a page can load (§2 Other script URLs): a remote or local file's, or a `data:` URL's,
1364
+ * which is code the page carries. A script URL with any other scheme (`javascript:`, `blob:`, `file:`, `about:`, …)
1365
+ * gives nothing wherever it appears.
1366
+ */
1367
+ const scriptSchemes = new Set(['http:', 'https:', 'data:']);
1368
+ /**
1369
+ * Whether a reference that takes over an artifact a preload or an import found fetches it again: when it fetches it as
1370
+ * another destination (§17 Content types), or it is a classic script whose own integrity value differs from the one
1371
+ * the artifact was fetched with, which keeps it from taking the preload's response (HTML § Consume a preloaded
1372
+ * resource). A module is fetched once per realm, so a module reference always takes the module found before (HTML §
1373
+ * Fetch a single module script).
1374
+ */
1375
+ function fetchesAgain(graph, found, how, key, integrity) {
1376
+ if (destination(found) !== destination(how))
1377
+ return true;
1378
+ return how.sourceType === 'script' && integrity !== '' && integrity !== graph.integrities.get(key);
1379
+ }
1380
+ /**
1381
+ * What a reference fetches a script as (§17 Content types): a classic script of the window, a module script, or a
1382
+ * classic script of a worker, which a browser checks as strictly as a module.
1383
+ */
1384
+ function destination({ sourceType, realm }) {
1385
+ return sourceType === 'module' ? 'module' : realm === 'window' ? 'script' : 'worker';
1386
+ }
1387
+ /**
1388
+ * Reads `url` as `how` fetches it, with the integrity metadata `integrity`, for the artifact at `key` (its realm and
1389
+ * URL), and gives the artifact's fields. The record of why its bytes weren't read, if they weren't, is kept as the
1390
+ * artifact's. A URL read before, in another realm, is not read again: the store gives the same bytes, and records once.
1391
+ * A remote URL is read once for each destination it is fetched as, which the safety floor judges it by (§17 Content
1392
+ * types), and each integrity value its bytes are checked against (§17 Integrity), apart for a script `importScripts`
1393
+ * loads, which a `data:` URL may not give as it gives a worker's own script. A build file is never fetched, so no
1394
+ * integrity value is checked against it.
1395
+ */
1396
+ async function readAs(graph, page, url, how, key, integrity, keyUrl) {
1397
+ const read = await readOnce(graph, page, url, how, integrity);
1398
+ if (read === null)
1399
+ return null;
1400
+ const { limitation, ...fields } = read;
1401
+ if (keyUrl.href === url.href) {
1402
+ graph.readLimitations.set(key, limitation);
1403
+ graph.integrities.set(key, integrity);
1404
+ return fields;
1405
+ }
1406
+ // A URL fetched under another key URL (§11) is an artifact of that key, sharing the one fetch: its record names the
1407
+ // key URL, and its origin comes from it, as the requested URL's does (§17 What is recorded).
1408
+ const named = limitation && { ...limitation, artifact: { ...limitation.artifact, url: keyUrl.href } };
1409
+ graph.readLimitations.set(key, named);
1410
+ graph.integrities.set(key, integrity);
1411
+ const remote = fields.origin === 'same-origin' || fields.origin === 'cross-origin';
1412
+ return remote ? { ...fields, origin: originOf(keyUrl, page) } : fields;
1413
+ }
1414
+ /** A remote URL's origin against the entry document's (§2): `same-origin` or `cross-origin`. */
1415
+ function originOf(url, page) {
1416
+ return url.origin === new URL(page.entry.url).origin ? 'same-origin' : 'cross-origin';
1417
+ }
1418
+ /**
1419
+ * Reads `url` as `how` fetches it, with the integrity metadata `integrity`, once: a local file once, and a remote or
1420
+ * `data:` URL once for each destination, whether `importScripts` loads it, and integrity value. Null for a `data:`
1421
+ * script a browser refuses to run.
1422
+ */
1423
+ async function readOnce(graph, page, url, how, integrity) {
1424
+ const imported = how.imported === true;
1425
+ const request = { destination: destination(how), document: page.entry.url, ...(imported && { imported }) };
1426
+ const mapped = graph.store.isMapped(url.href);
1427
+ const readKey = mapped ? url.href : JSON.stringify([request.destination, imported, integrity, url.href]);
1428
+ let read = graph.reads.get(readKey);
1429
+ if (read === undefined) {
1430
+ const withIntegrity = mapped || integrity === '' ? request : { ...request, integrity };
1431
+ graph.reads.set(readKey, (read = await readReference(graph, page, url, withIntegrity)));
1432
+ }
1433
+ return read;
1434
+ }
1435
+ /**
1436
+ * Reads a referenced URL through the store, as `request` asks for it, and records why when its bytes can't be read. A
1437
+ * remote URL's origin is `same-origin` or `cross-origin` against the entry document, whatever the final URL after
1438
+ * redirects is (§17).
1439
+ */
1440
+ async function readReference(graph, page, url, request) {
1441
+ const result = await graph.store.read(url.href, request);
1442
+ const remote = originOf(url, page);
1443
+ switch (result.status) {
1444
+ case 'ok': {
1445
+ const fields = { ...read(result.bytes, result.retrieval), limitation: null };
1446
+ if (result.path !== null)
1447
+ return { path: result.path, origin: 'local-build', ...fields };
1448
+ // A `data:` URL's code is the page's own (§2 Other script URLs).
1449
+ if (url.protocol === 'data:')
1450
+ return { path: null, origin: 'inline', ...fields };
1451
+ // An empty header names no map, as an empty annotation does: it would resolve to the script itself.
1452
+ const header = result.sourceMap === null || result.sourceMap.trim() === '' ? null : result.sourceMap;
1453
+ if (!graph.fetched.has(url.href))
1454
+ graph.fetched.set(url.href, { document: page.entry.url, header });
1455
+ return { path: null, origin: remote, ...fields };
1456
+ }
1457
+ case 'offline-skipped': {
1458
+ const detail = 'a remote script is not fetched offline';
1459
+ const limitation = { kind: result.status, artifact: { url: url.href }, detail };
1460
+ return { path: null, origin: remote, ...notRead(result), limitation };
1461
+ }
1462
+ case 'remote-refused':
1463
+ case 'fetch-failed':
1464
+ case 'sri-mismatch': {
1465
+ const limitation = { kind: result.status, artifact: { url: url.href }, detail: result.detail };
1466
+ return { path: null, origin: remote, ...notRead(result), limitation };
1467
+ }
1468
+ case 'unreadable':
1469
+ return { path: result.path, origin: 'local-build', ...notRead(result), limitation: unreadable(result) };
1470
+ case 'refused':
1471
+ return null;
1472
+ }
1473
+ }
1474
+ /** An `unreadable` record (§13) for a local file. */
1475
+ function unreadable(result) {
1476
+ return { kind: 'unreadable', artifact: { url: result.url, path: result.path }, detail: result.detail };
1477
+ }
1478
+ const noMap = {
1479
+ sourceMap: { method: 'none', path: null, status: 'none' },
1480
+ map: null,
1481
+ staleCandidates: 0,
1482
+ unreadMaps: { refused: 0, failed: 0, unreadable: 0 },
1483
+ };
1484
+ /**
1485
+ * The artifacts, each with its source map paired (§2 Source maps), and what became of each source-map directory's
1486
+ * maps. A URL in several realms is paired once for each bytes it was read as: one realm's read may have withheld them
1487
+ * (§17 Integrity) where another's gave them. The build directory's methods come first, then each source-map directory,
1488
+ * one at a time, in the order they were declared.
1489
+ */
1490
+ async function pairSourceMaps(graph, found, directories) {
1491
+ const byUrlAndHash = new Map();
1492
+ const key = ({ url, hash }) => JSON.stringify([url, hash]);
1493
+ for (const artifact of found) {
1494
+ if (!byUrlAndHash.has(key(artifact))) {
1495
+ byUrlAndHash.set(key(artifact), { artifact, paired: await pairSourceMap(graph, artifact) });
1496
+ }
1497
+ }
1498
+ const sourceMapDirectories = [];
1499
+ for (const dir of directories)
1500
+ sourceMapDirectories.push(await pairInDirectory(graph, dir, [...byUrlAndHash.values()]));
1501
+ const artifacts = found.map((artifact) => ({ ...artifact, ...byUrlAndHash.get(key(artifact)).paired }));
1502
+ return { artifacts, sourceMapDirectories };
1503
+ }
1504
+ /**
1505
+ * Pairs the artifacts with the maps of the source-map directory declared as `dir` (§2 Source maps), and counts what
1506
+ * became of its maps. Its maps pair by, in order: the artifact's `debugId` (any artifact), its annotation mirrored in
1507
+ * the directory, and its sidecar mirrored there (local-build artifacts only). Only a file the directory's listing
1508
+ * holds, under any spelling, is a candidate, named by its listed path, and only a `.map` file, in any case, has its
1509
+ * `debugId` read. Maps never pair by basename or by their `file` field.
1510
+ *
1511
+ * An artifact no earlier map paired takes the first candidate that verifies, and each one that fails before it is
1512
+ * `stale`. Every other candidate is verified only to count it, so that a directory counts as paired when any of its
1513
+ * maps verifies against an artifact, even one an earlier map already paired. A directory that can't be listed gives an
1514
+ * `unreadable` record, and one where nothing verifies a `source-maps-unpaired` record. A mirrored map that exists but
1515
+ * can't be read, a directory there included, gives an `unreadable` record while its artifact is being paired.
1516
+ */
1517
+ async function pairInDirectory(graph, dir, pairings) {
1518
+ const listing = await graph.store.listSourceMaps(dir);
1519
+ if (listing.status === 'unreadable') {
1520
+ graph.limitations.push({
1521
+ kind: 'unreadable',
1522
+ artifact: { path: dir },
1523
+ detail: `the source-map directory can't be read: ${listing.detail}`,
1524
+ });
1525
+ return { path: dir, found: 0, paired: 0, stale: 0, orphaned: 0, orphanedMaps: [] };
1526
+ }
1527
+ const files = new Set(listing.files);
1528
+ const directories = new Set(listing.directories);
1529
+ const outcomes = new Map();
1530
+ for (const path of listing.files)
1531
+ if (isMapName(path))
1532
+ outcomes.set(path, { verified: false, failed: false });
1533
+ // What reading each file gave, read once: its raw JSON, or why it can't be read.
1534
+ const reads = new Map();
1535
+ const readMap = async (path) => {
1536
+ let read = reads.get(path);
1537
+ if (read === undefined) {
1538
+ const result = await graph.store.readSourceMap(dir, path);
1539
+ read = result.status === 'ok' ? { raw: rawMap(decoder.decode(result.bytes)) } : { detail: result.detail };
1540
+ reads.set(path, read);
1541
+ }
1542
+ return read;
1543
+ };
1544
+ // The `.map` files with a `debugId`, by it (`debugIdKey`), each in path order; read when an artifact first needs it.
1545
+ let byDebugId;
1546
+ const debugIdCandidates = async (debugId) => {
1547
+ if (byDebugId === undefined) {
1548
+ byDebugId = new Map();
1549
+ for (const path of listing.files.filter(isMapName)) {
1550
+ const read = await readMap(path);
1551
+ const id = 'raw' in read ? mapDebugId(read.raw) : null;
1552
+ if (id !== null)
1553
+ byDebugId.set(debugIdKey(id), [...(byDebugId.get(debugIdKey(id)) ?? []), path]);
1554
+ }
1555
+ }
1556
+ return byDebugId.get(debugIdKey(debugId)) ?? [];
1557
+ };
1558
+ // The listed files and directories, by what they lead to (`sourceMapIdentity`), the first in path order for each;
1559
+ // found when a named path is first not listed as spelled.
1560
+ let byIdentity;
1561
+ /**
1562
+ * The listed path of what a pairing method names at `path`, when the directory's listing holds it (§2 Source maps):
1563
+ * a file listed as spelled, a directory listed as spelled with or without a trailing `/`, or either under another
1564
+ * spelling, as the same file by device and inode. Null when the listing doesn't hold it.
1565
+ */
1566
+ const held = async (path) => {
1567
+ const directory = path.replace(/\/+$/, '') || '.';
1568
+ if (directory === path && files.has(path))
1569
+ return path;
1570
+ if (directories.has(directory))
1571
+ return directory;
1572
+ const identity = await graph.store.sourceMapIdentity(dir, path);
1573
+ if (identity === null)
1574
+ return null;
1575
+ if (byIdentity === undefined) {
1576
+ byIdentity = new Map();
1577
+ for (const listed of [...listing.files, ...listing.directories]) {
1578
+ const each = await graph.store.sourceMapIdentity(dir, listed);
1579
+ if (each !== null && !byIdentity.has(each))
1580
+ byIdentity.set(each, listed);
1581
+ }
1582
+ }
1583
+ return byIdentity.get(identity) ?? null;
1584
+ };
1585
+ for (const pairing of pairings) {
1586
+ const { url, path, origin, text, retrieval } = pairing.artifact;
1587
+ if (text === null)
1588
+ continue;
1589
+ const named = annotations(text);
1590
+ const local = origin === 'local-build' && path !== null;
1591
+ const candidates = [];
1592
+ if (named.debugId !== null) {
1593
+ for (const map of await debugIdCandidates(named.debugId))
1594
+ candidates.push(['external-debug-id', map, null]);
1595
+ }
1596
+ if (local && named.sourceMappingURL !== null) {
1597
+ // The annotation's URL, resolved against the artifact's, then through the URL→path mapping.
1598
+ const annotated = URL.parse(named.sourceMappingURL, retrieval.finalUrl ?? url);
1599
+ const mirrored = annotated === null ? null : graph.store.buildPath(annotated.href);
1600
+ const namedBy = `the source map the annotation of ${url} names, mirrored in ${dir},`;
1601
+ if (mirrored !== null)
1602
+ candidates.push(['external-annotation', mirrored, namedBy]);
1603
+ }
1604
+ if (local) {
1605
+ candidates.push(['external-sidecar', `${path}.map`, `the source map sidecar of ${url}, mirrored in ${dir},`]);
1606
+ }
1607
+ const tried = new Set();
1608
+ for (const [method, candidate, namedBy] of candidates) {
1609
+ // A map exists when the directory's listing holds it, and is named by its listed path.
1610
+ const map = await held(candidate);
1611
+ if (map === null || tried.has(map))
1612
+ continue;
1613
+ tried.add(map);
1614
+ const pairs = pairing.paired.sourceMap.status !== 'verified';
1615
+ const read = await readMap(map);
1616
+ if (!('raw' in read)) {
1617
+ if (pairs && namedBy !== null) {
1618
+ graph.limitations.push({
1619
+ kind: 'unreadable',
1620
+ artifact: { path: posix.join(dir, map) },
1621
+ detail: `${namedBy} can't be read: ${read.detail}`,
1622
+ });
1623
+ }
1624
+ if (files.has(map) && !outcomes.has(map))
1625
+ outcomes.set(map, { verified: false, failed: false });
1626
+ continue;
1627
+ }
1628
+ const outcome = outcomes.get(map) ?? { verified: false, failed: false };
1629
+ outcomes.set(map, outcome);
1630
+ const reading = verification({ text, debugId: named.debugId }, read.raw);
1631
+ if (reading !== null)
1632
+ outcome.verified = true;
1633
+ else
1634
+ outcome.failed = true;
1635
+ if (!pairs)
1636
+ continue;
1637
+ const { sourceMap, staleCandidates, unreadMaps } = pairing.paired;
1638
+ const joined = posix.join(dir, map);
1639
+ if (reading !== null) {
1640
+ const verified = { method, path: joined, status: 'verified' };
1641
+ pairing.paired = { sourceMap: verified, map: { raw: read.raw, reading, base: map }, staleCandidates, unreadMaps };
1642
+ }
1643
+ else {
1644
+ // An artifact whose every candidate failed is `stale`, with the first one's path.
1645
+ const first = sourceMap.status === 'stale' ? sourceMap.path : joined;
1646
+ const stale = { method: 'none', path: first, status: 'stale' };
1647
+ pairing.paired = { sourceMap: stale, map: null, staleCandidates: staleCandidates + 1, unreadMaps };
1648
+ }
1649
+ }
1650
+ }
1651
+ const orphanedMaps = [...outcomes].filter(([, { verified }]) => !verified).map(([map]) => map);
1652
+ orphanedMaps.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1653
+ const paired = outcomes.size - orphanedMaps.length;
1654
+ if (paired === 0) {
1655
+ graph.limitations.push({
1656
+ kind: 'source-maps-unpaired',
1657
+ artifact: { path: dir },
1658
+ detail: `none of the source-map directory's ${count(outcomes.size, 'map')} verifies against an artifact`,
1659
+ });
1660
+ }
1661
+ const stale = orphanedMaps.filter((map) => outcomes.get(map).failed).length;
1662
+ return { path: dir, found: outcomes.size, paired, stale, orphaned: orphanedMaps.length, orphanedMaps };
1663
+ }
1664
+ /** `n` and a noun, plural unless `n` is 1. */
1665
+ const count = (n, noun) => `${n} ${n === 1 ? noun : `${noun}s`}`;
1666
+ /** Whether a file's name makes it a map (§2 Source maps): it ends in `.map`, ignoring ASCII case. */
1667
+ const isMapName = (path) => asciiLowercase(path).endsWith('.map');
1668
+ /**
1669
+ * Pairs an artifact with its source map (§2 Source maps). The methods are tried in order, each with the candidates it
1670
+ * finds: the `SourceMap` header of a fetched artifact, its `sourceMappingURL` annotation, the `<path>.map` sidecar of a
1671
+ * local-build artifact, and the build directory's maps with the artifact's `debugId`. The first candidate that verifies
1672
+ * is paired; one that fails is `stale`, and the next is tried. A candidate tried once isn't tried again, and maps are
1673
+ * never merged.
1674
+ *
1675
+ * A map the annotation of a local-build or inline artifact names, or its existing sidecar, that can't be read gives an
1676
+ * `unreadable` record, and the next method is tried (§2 Source maps). A remote artifact's map never changes
1677
+ * completeness: one that is refused, fails or can't be read is only counted (§17), and the next method is tried. Nor
1678
+ * does a map the `debugId` search meets, which isn't known to belong to the artifact.
1679
+ */
1680
+ async function pairSourceMap(graph, artifact) {
1681
+ const { url, path, origin, text, retrieval } = artifact;
1682
+ if (text === null)
1683
+ return noMap;
1684
+ const named = annotations(text);
1685
+ // A fetched artifact's header was recorded by the URL it was fetched at, which a key URL (§11) may differ from.
1686
+ const fetched = graph.fetched.get(retrieval.requestedUrl ?? url);
1687
+ // A map's URL resolves against the script's, which is the URL its bytes came from, after any redirects.
1688
+ const base = retrieval.finalUrl ?? url;
1689
+ const header = fetched?.header ?? null;
1690
+ // Only a local-build or inline artifact's annotation raises a record for a map that can't be read.
1691
+ const local = origin === 'local-build' || origin === 'inline';
1692
+ const namedBy = local ? `the source map the annotation of ${url} names` : undefined;
1693
+ const annotated = (reference) => mapAt(graph, reference, base, fetched?.document, namedBy);
1694
+ const methods = [
1695
+ ['header', async () => (header === null ? [] : mapAt(graph, header, base, fetched?.document))],
1696
+ ['annotation', async () => (named.sourceMappingURL === null ? [] : annotated(named.sourceMappingURL))],
1697
+ ['sidecar', async () => (origin === 'local-build' && path !== null ? sidecar(graph, url, path) : [])],
1698
+ ['debug-id', async () => (named.debugId === null ? [] : withDebugId(graph, named.debugId))],
1699
+ ];
1700
+ const tried = new Set();
1701
+ const stale = [];
1702
+ const unreadMaps = { refused: 0, failed: 0, unreadable: 0 };
1703
+ for (const [method, candidates] of methods) {
1704
+ for (const candidate of await candidates()) {
1705
+ if (tried.has(candidate.key))
1706
+ continue;
1707
+ tried.add(candidate.key);
1708
+ if ('unreadable' in candidate) {
1709
+ graph.limitations.push(candidate.unreadable);
1710
+ continue;
1711
+ }
1712
+ if ('unread' in candidate) {
1713
+ unreadMaps[candidate.unread]++;
1714
+ continue;
1715
+ }
1716
+ const reading = verification({ text, debugId: named.debugId }, candidate.raw);
1717
+ if (reading !== null) {
1718
+ const sourceMap = { method, path: candidate.path, status: 'verified' };
1719
+ // A `data:` URL map has no path of its own, and resolves against its script's (§10).
1720
+ const map = { raw: candidate.raw, reading, base: candidate.path ?? path };
1721
+ return { sourceMap, map, staleCandidates: stale.length, unreadMaps };
1722
+ }
1723
+ stale.push(candidate);
1724
+ }
1725
+ }
1726
+ const [first] = stale;
1727
+ if (first === undefined)
1728
+ return { ...noMap, unreadMaps };
1729
+ const sourceMap = { method: 'none', path: first.path, status: 'stale' };
1730
+ return { sourceMap, map: null, staleCandidates: stale.length, unreadMaps };
1731
+ }
1732
+ /**
1733
+ * The map a header or an annotation names, by a URL resolved against the artifact's. A `data:` URL holds the map.
1734
+ * Otherwise it is read through the store, from the build directory when a mapping covers its URL. Only the map of an
1735
+ * artifact fetched for the entry document `document` is fetched, as that document's (§17 Remote source maps). A map
1736
+ * that can't be read is no candidate. When `namedBy` names it for the record, one in the build directory that exists
1737
+ * but can't be read is an `UnreadableMap`; a URL whose path is malformed names none. A fetched artifact's map that
1738
+ * isn't read is an `UnreadMap` (§17).
1739
+ */
1740
+ async function mapAt(graph, reference, base, document, namedBy) {
1741
+ const url = URL.parse(reference, base);
1742
+ if (url === null)
1743
+ return [];
1744
+ if (url.protocol === 'data:') {
1745
+ const text = dataUrlText(url);
1746
+ return [{ key: url.href, path: null, raw: text === null ? undefined : rawMap(text) }];
1747
+ }
1748
+ if (document === undefined && !graph.store.isMapped(url.href))
1749
+ return [];
1750
+ // A mapped URL is read from the build directory, which judges no request.
1751
+ const result = await graph.store.read(url.href, { destination: 'map', document: document ?? base });
1752
+ if (result.status === 'unreadable' && namedBy !== undefined) {
1753
+ return unreadableMap(graph, result, graph.store.buildPath(url.href), namedBy);
1754
+ }
1755
+ if (result.status !== 'ok')
1756
+ return document === undefined ? [] : unreadMap(graph, result, url.href);
1757
+ return [{ key: result.path ?? url.href, path: result.path, raw: rawMap(decoder.decode(result.bytes)) }];
1758
+ }
1759
+ /**
1760
+ * The `UnreadMap` a fetched artifact's map gives when it isn't read (§17 Content types): the safety
1761
+ * floor refused it, its fetch failed, or it is a build file that exists but can't be read. A build file that isn't
1762
+ * there, which the sweep's listing doesn't hold, gives nothing, as it does for a local artifact (§2 Source maps).
1763
+ */
1764
+ async function unreadMap(graph, result, url) {
1765
+ switch (result.status) {
1766
+ case 'remote-refused':
1767
+ return [{ key: result.url, unread: 'refused' }];
1768
+ case 'fetch-failed':
1769
+ return [{ key: result.url, unread: 'failed' }];
1770
+ case 'unreadable': {
1771
+ const path = graph.store.buildPath(url);
1772
+ return path === null || !(await held(graph, path)) ? [] : [{ key: result.path, unread: 'unreadable' }];
1773
+ }
1774
+ // A map is fetched with no integrity value, only in `resolve` mode, and never from a `data:` URL.
1775
+ case 'sri-mismatch':
1776
+ case 'offline-skipped':
1777
+ case 'refused':
1778
+ return [];
1779
+ }
1780
+ }
1781
+ /**
1782
+ * The sidecar of a local-build artifact: `<path>.map` beside it in the build directory, if there is one. One that
1783
+ * exists but can't be read is an `UnreadableMap`.
1784
+ */
1785
+ async function sidecar(graph, url, path) {
1786
+ const mapUrl = new URL(url);
1787
+ mapUrl.search = '';
1788
+ mapUrl.hash = '';
1789
+ mapUrl.pathname += '.map';
1790
+ const result = await graph.store.readFile(`${path}.map`, mapUrl.href);
1791
+ if (result.status === 'unreadable') {
1792
+ return unreadableMap(graph, result, result.path, `the source map sidecar of ${url}`);
1793
+ }
1794
+ if (result.status !== 'ok' || result.path === null)
1795
+ return [];
1796
+ return [{ key: result.path, path: result.path, raw: rawMap(decoder.decode(result.bytes)) }];
1797
+ }
1798
+ /**
1799
+ * The `UnreadableMap` a build file a pairing method names at `path` gives, when it exists but can't be read, whatever
1800
+ * the error (§2 Source maps), or nothing when nothing is there: a missing map is counted only in the attribution block
1801
+ * (§13). A map exists when the sweep's listing holds its path (`held`). A null `path`, from a URL that names no path
1802
+ * in the build directory, is never held. `namedBy` names the map in the record's detail.
1803
+ */
1804
+ async function unreadableMap(graph, result, path, namedBy) {
1805
+ if (path === null || !(await held(graph, path)))
1806
+ return [];
1807
+ const { url, detail } = result;
1808
+ const unreadable = {
1809
+ kind: 'unreadable',
1810
+ artifact: { url, path },
1811
+ detail: `${namedBy} can't be read: ${detail}`,
1812
+ };
1813
+ return [{ key: path, unreadable }];
1814
+ }
1815
+ /**
1816
+ * The build directory's maps whose `debugId` is the artifact's (`debugIdKey`), in path order: its files whose names end
1817
+ * in `.map`, ignoring ASCII case.
1818
+ */
1819
+ async function withDebugId(graph, debugId) {
1820
+ graph.debugIdMaps ??= (async () => {
1821
+ const maps = new Map();
1822
+ for (const { path, url } of (await listed(graph)).files) {
1823
+ if (!asciiLowercase(path).endsWith('.map'))
1824
+ continue;
1825
+ // A file no mapping serves has no URL, so its path stands in for one.
1826
+ const result = await graph.store.readFile(path, url ?? path);
1827
+ if (result.status !== 'ok')
1828
+ continue;
1829
+ const raw = rawMap(decoder.decode(result.bytes));
1830
+ const id = mapDebugId(raw);
1831
+ if (id !== null)
1832
+ maps.set(debugIdKey(id), [...(maps.get(debugIdKey(id)) ?? []), { key: path, path, raw }]);
1833
+ }
1834
+ return maps;
1835
+ })();
1836
+ return (await graph.debugIdMaps).get(debugIdKey(debugId)) ?? [];
1837
+ }
1838
+ /** The build directory's files, listed once for the sweep, the `debugId` maps and whether a named map exists. */
1839
+ function listed(graph) {
1840
+ return (graph.listing ??= graph.store.list());
1841
+ }
1842
+ /**
1843
+ * Whether the sweep's listing holds a normalized build path (§2 Source maps): as spelled (`heldAsSpelled`), or under
1844
+ * another spelling, when it leads to the same file or directory as a path held as spelled, by device and inode.
1845
+ * So a map named in another case on a case-insensitive file system, or through a loop link, is held.
1846
+ */
1847
+ async function held(graph, path) {
1848
+ const listing = await listed(graph);
1849
+ if (heldAsSpelled(listing, path))
1850
+ return true;
1851
+ const identity = await graph.store.buildIdentity(path);
1852
+ if (identity === null)
1853
+ return false;
1854
+ graph.heldIdentities ??= (async () => {
1855
+ const identities = new Set();
1856
+ const paths = [...listing.files.map((file) => file.path), ...listing.directories];
1857
+ for (const each of paths.filter((listed) => heldAsSpelled(listing, listed))) {
1858
+ const found = await graph.store.buildIdentity(each);
1859
+ if (found !== null)
1860
+ identities.add(found);
1861
+ }
1862
+ return identities;
1863
+ })();
1864
+ return (await graph.heldIdentities).has(identity);
1865
+ }
1866
+ /**
1867
+ * Whether the sweep's listing holds a normalized build path, as spelled (§2 Source maps): a file there, a directory it
1868
+ * meets there, a loop it stops at included, or, only where the sweep records it (a mapping serves its path or a path
1869
+ * under it), a directory there it can't list or a link there whose target it can't reach. A path
1870
+ * that ends in `/` names only a directory, or a link.
1871
+ */
1872
+ function heldAsSpelled(listing, path) {
1873
+ const directory = path.replace(/\/+$/, '') || '.';
1874
+ const unreachable = listing.unreadable.find((entry) => entry.path === directory);
1875
+ if (unreachable !== undefined)
1876
+ return unreachable.served;
1877
+ return listing.files.some((file) => file.path === path) || listing.directories.includes(directory);
1878
+ }
1879
+ /** The URL of a document's `n`th inline script: the document's URL, with the fragment `inline-<n>`. */
1880
+ function inlineUrl(documentUrl, n) {
1881
+ const url = new URL(documentUrl);
1882
+ url.hash = `inline-${n}`;
1883
+ return url.href;
1884
+ }
1885
+ /**
1886
+ * The retrieval record of an inline script: part of its entry document, so never requested, and its bytes are the
1887
+ * document's.
1888
+ */
1889
+ const inlineRetrieval = { status: 'ok', requestedUrl: null, finalUrl: null, contentType: null, sri: 'none' };
1890
+ /** The fields of an artifact whose bytes were read: its content hash, text and retrieval record, and the defaults. */
1891
+ function read(bytes, retrieval, text = decoder.decode(bytes)) {
1892
+ const hash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
1893
+ return { hash, discovery: 'reference', realmBasis: 'reference', retrieval, text };
1894
+ }
1895
+ /** The fields of an artifact whose bytes could not be read: no hash or text, its retrieval record, and the defaults. */
1896
+ function notRead({ retrieval, }) {
1897
+ return { hash: null, discovery: 'reference', realmBasis: 'reference', retrieval, text: null };
1898
+ }
1899
+ /**
1900
+ * The type a browser gives a `script` element (HTML § Prepare the script element): `script` for a classic script,
1901
+ * `module`, `importmap`, or undefined for every other type, which never runs. An element with no `type` and no
1902
+ * `language`, or an empty one, is classic; otherwise `type`, stripped, or else `text/` and the `language`, must match a
1903
+ * JavaScript MIME type essence, `module` or `importmap`, ignoring ASCII case.
1904
+ */
1905
+ function scriptType(element) {
1906
+ return typeOfScript(attribute(element, 'type'), attribute(element, 'language'));
1907
+ }
1908
+ /** What a script element's `type` and `language` attributes make it (HTML § Prepare the script element). */
1909
+ function typeOfScript(type, language) {
1910
+ if (type === '' || (type === undefined && (language === undefined || language === '')))
1911
+ return 'script';
1912
+ const typeString = asciiLowercase(type === undefined ? `text/${language}` : stripAsciiWhitespace(type));
1913
+ if (javascriptTypes.has(typeString))
1914
+ return 'script';
1915
+ if (typeString === 'module' || typeString === 'importmap')
1916
+ return typeString;
1917
+ return undefined;
1918
+ }
1919
+ /**
1920
+ * Whether a classic script with `for` and `event` attributes runs: only for the window's `load` event (HTML § Prepare
1921
+ * the script element).
1922
+ */
1923
+ function legacyEventRuns(element) {
1924
+ const target = attribute(element, 'for');
1925
+ const event = attribute(element, 'event');
1926
+ if (target === undefined || event === undefined)
1927
+ return true;
1928
+ const handler = asciiLowercase(stripAsciiWhitespace(event));
1929
+ return asciiLowercase(stripAsciiWhitespace(target)) === 'window' && (handler === 'onload' || handler === 'onload()');
1930
+ }
1931
+ const stripAsciiWhitespace = (text) => text.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '');
1932
+ const asciiLowercase = (text) => text.replace(/[A-Z]/g, (letter) => String.fromCharCode(letter.charCodeAt(0) + 32));
1933
+ /** The elements under `root`, in tree order. The walk keeps its own stack, so no depth of nesting overflows it. */
1934
+ function* elements(root) {
1935
+ const stack = [...root.childNodes].reverse();
1936
+ for (let node = stack.pop(); node !== undefined; node = stack.pop()) {
1937
+ if (!('tagName' in node))
1938
+ continue;
1939
+ yield node;
1940
+ for (let i = node.childNodes.length - 1; i >= 0; i--)
1941
+ stack.push(node.childNodes[i]);
1942
+ }
1943
+ }
1944
+ /** Whether `element` is the HTML element `tagName`, rather than an SVG or MathML one. */
1945
+ function isHtml(element, tagName) {
1946
+ return element.tagName === tagName && element.namespaceURI === html.NS.HTML;
1947
+ }
1948
+ function attribute(element, name) {
1949
+ return element.attrs.find((attr) => attr.name === name)?.value;
1950
+ }