@preventive/triage 1.0.0-alpha.43 → 1.0.0-alpha.45

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 (186) hide show
  1. package/common/bundle-compare-diff.js +2 -167
  2. package/common/bundle-compare-versions.js +172 -0
  3. package/common/bundle-comparison.d.ts +8 -0
  4. package/common/bundle-comparison.js +62 -0
  5. package/common/bundle-sources.js +20 -1
  6. package/common/default-scan-models.ts +1 -1
  7. package/common/dependency-reviews.d.ts +0 -5
  8. package/common/dependency-reviews.js +0 -17
  9. package/common/managed/agent-tasks.ts +292 -0
  10. package/common/managed/github-agents.ts +92 -0
  11. package/common/managed/github-logins.ts +49 -0
  12. package/common/managed/roles.ts +7 -0
  13. package/common/managed/routes.js +91 -10
  14. package/common/managed/team-app.d.ts +5 -2
  15. package/common/managed/team-app.js +2 -2
  16. package/common/server-info.ts +11 -0
  17. package/common/workspace-app.js +19 -3
  18. package/out/brotli-fallback.js +3 -3
  19. package/out/chunk-2XUEZHIP.js +18 -18
  20. package/out/chunk-2ZDZWLFY.js +1 -0
  21. package/out/chunk-3PS4KWD2.js +10 -0
  22. package/out/chunk-5DHL5NG3.js +2 -0
  23. package/out/chunk-6HNXNIQY.js +52 -0
  24. package/out/chunk-7C7DWL7I.js +1 -0
  25. package/out/chunk-AFLZH7WG.js +1 -0
  26. package/out/chunk-BC2UCLFJ.js +1 -0
  27. package/out/chunk-C4Z6ZBXV.js +6 -6
  28. package/out/chunk-GDPCLSOS.js +2 -0
  29. package/out/chunk-GO7FT4OD.js +1 -0
  30. package/out/chunk-HMAJDVXN.js +1 -0
  31. package/out/chunk-I2MDGSQX.js +3 -0
  32. package/out/chunk-IYPY47MC.js +1 -0
  33. package/out/chunk-IZ7POWI3.js +18 -0
  34. package/out/chunk-JCIIBLKR.js +1 -0
  35. package/out/chunk-NPW52BM2.js +17 -0
  36. package/out/chunk-NRPC74AM.js +1 -1
  37. package/out/chunk-NV7ES4EW.js +1 -0
  38. package/out/chunk-OKRLLE5I.js +17 -0
  39. package/out/chunk-OOJM4CTU.js +1 -1
  40. package/out/chunk-PNHBZGDD.js +1 -1
  41. package/out/chunk-PTGDYLNS.js +36 -0
  42. package/out/chunk-PXK7EU4V.js +1 -0
  43. package/out/chunk-Q6MVYGYV.js +3 -0
  44. package/out/chunk-QB4CBOX2.js +1 -0
  45. package/out/chunk-QRD6GIG5.js +229 -0
  46. package/out/chunk-R3NZFXVR.js +1 -0
  47. package/out/chunk-R5O6FAW4.js +1 -1
  48. package/out/chunk-SXV2T5C4.js +80 -0
  49. package/out/chunk-TUII34K2.js +1 -1
  50. package/out/chunk-VFWMQ3LG.js +2 -0
  51. package/out/chunk-VH4XMZIW.js +2 -0
  52. package/out/chunk-WFYLMF6Z.js +10 -0
  53. package/out/chunk-WI7Z5A2T.js +6 -0
  54. package/out/chunk-WQ74ZBZF.js +7 -0
  55. package/out/chunk-XVBEOGOS.js +3 -0
  56. package/out/client-managed-import.js +3 -3
  57. package/out/client-managed.js +31 -35
  58. package/out/client-sync.js +11 -11
  59. package/out/compare.js +35 -0
  60. package/out/dialogs.js +51 -0
  61. package/out/github-issue-authorized.html +1 -1
  62. package/out/github-issue-authorized.js +1 -1
  63. package/out/graph.js +17 -17
  64. package/out/index.html +31 -31
  65. package/out/prism.js +14 -14
  66. package/out/scan.js +2 -0
  67. package/out/terminal.js +294 -293
  68. package/out/view.css +1 -1
  69. package/out/view.js +16 -107
  70. package/package.json +55 -6
  71. package/risk/actions/findings.js +156 -0
  72. package/risk/actions/resolve.js +219 -0
  73. package/risk/actions/scripts/commands.js +182 -0
  74. package/risk/actions/scripts/grade.js +113 -0
  75. package/risk/actions/scripts/packages.js +123 -0
  76. package/risk/actions/scripts/shell.js +165 -0
  77. package/risk/actions/uses.js +76 -0
  78. package/risk/actions/workflow.js +110 -0
  79. package/risk/files/hosts.js +6 -6
  80. package/risk/files/readability.js +23 -17
  81. package/risk/files/runs-code.js +18 -6
  82. package/risk/files/script.js +7 -3
  83. package/risk/files/stamps.js +8 -7
  84. package/risk/index.d.ts +68 -1
  85. package/risk/index.js +6 -2
  86. package/risk/npm/assess.js +13 -8
  87. package/risk/npm/compare.js +34 -10
  88. package/risk/npm/dependencies.js +11 -0
  89. package/risk/npm/files.js +413 -2
  90. package/risk/npm/tarball.js +56 -73
  91. package/server-managed/agent-merges.ts +198 -0
  92. package/server-managed/app-agents.ts +30 -0
  93. package/server-managed/avatar-store.ts +1 -0
  94. package/server-managed/commit-coauthors.ts +83 -0
  95. package/server-managed/commit-lockfiles.ts +62 -0
  96. package/server-managed/comparison.ts +41 -0
  97. package/server-managed/db-methods.ts +29 -10
  98. package/server-managed/db-neon.ts +30 -2
  99. package/server-managed/db-schema.ts +9 -2
  100. package/server-managed/db.ts +7 -0
  101. package/server-managed/dependabot-access.ts +60 -0
  102. package/server-managed/dependabot-alerts.ts +185 -0
  103. package/server-managed/dependency-review-comments.ts +97 -0
  104. package/server-managed/dependency-reviews.ts +65 -15
  105. package/server-managed/github-accounts.ts +33 -0
  106. package/server-managed/github-actions.ts +71 -0
  107. package/server-managed/github-app.ts +23 -9
  108. package/server-managed/github-errors.ts +2 -0
  109. package/server-managed/github-link-oauth.ts +69 -0
  110. package/server-managed/github-links.ts +204 -0
  111. package/server-managed/github-oauth.ts +5 -2
  112. package/server-managed/github-users.ts +30 -0
  113. package/server-managed/http.ts +1219 -81
  114. package/server-managed/index.ts +6 -2
  115. package/server-managed/locked-packages.ts +48 -16
  116. package/server-managed/lockfile-scope.ts +49 -0
  117. package/server-managed/npm-delta-worker.js +23 -0
  118. package/server-managed/npm-delta.ts +173 -0
  119. package/server-managed/npm-insights.ts +20 -11
  120. package/server-managed/npm-kept.ts +51 -4
  121. package/server-managed/npm-loads.ts +20 -13
  122. package/server-managed/npm-packages.ts +24 -64
  123. package/server-managed/pretty-print.ts +14 -9
  124. package/server-managed/public-workspace.ts +26 -8
  125. package/server-managed/pull-comment-reviews.ts +161 -0
  126. package/server-managed/pull-dependencies.ts +280 -0
  127. package/server-managed/pull-dependency-reviews.ts +318 -0
  128. package/server-managed/pull-reviews.ts +205 -0
  129. package/server-managed/release-commits.ts +84 -0
  130. package/server-managed/repository-actions.ts +209 -0
  131. package/server-managed/repository-agents.ts +220 -0
  132. package/server-managed/repository-browser.ts +54 -13
  133. package/server-managed/repository-changes.ts +284 -0
  134. package/server-managed/repository-code.ts +52 -0
  135. package/server-managed/repository-dependency-reviews.ts +135 -0
  136. package/server-managed/repository-history.ts +445 -0
  137. package/server-managed/repository-issues.ts +181 -0
  138. package/server-managed/repository-pulse.ts +432 -0
  139. package/server-managed/repository-risk-read.ts +165 -0
  140. package/server-managed/repository-risk.ts +116 -106
  141. package/server-managed/reviewed-dependencies.ts +149 -0
  142. package/server-managed/session.ts +9 -5
  143. package/server-managed/sql.ts +1 -1
  144. package/server-managed/storage-stores.ts +5 -0
  145. package/server-managed/team-app.ts +54 -14
  146. package/server-managed/workspace-shares.ts +4 -1
  147. package/server.ts +11 -0
  148. package/out/chunk-2KBF5TZ7.js +0 -2
  149. package/out/chunk-35VE6CDH.js +0 -2
  150. package/out/chunk-3BZ7JERB.js +0 -1
  151. package/out/chunk-4F6JSF57.js +0 -3
  152. package/out/chunk-4XCDFGDP.js +0 -10
  153. package/out/chunk-7RU5EDRO.js +0 -226
  154. package/out/chunk-A7M4GV7M.js +0 -1
  155. package/out/chunk-CLPKNNC7.js +0 -51
  156. package/out/chunk-CMQA4DM6.js +0 -1
  157. package/out/chunk-CVVJAXVY.js +0 -2
  158. package/out/chunk-DIJJNTRH.js +0 -10
  159. package/out/chunk-GJ4IUCNS.js +0 -1
  160. package/out/chunk-GNXOKIGP.js +0 -80
  161. package/out/chunk-GZ4MS3TE.js +0 -13
  162. package/out/chunk-IU3CCINR.js +0 -3
  163. package/out/chunk-K3X5EJBQ.js +0 -2
  164. package/out/chunk-KEX2ATEW.js +0 -2
  165. package/out/chunk-KYCNJVB7.js +0 -1
  166. package/out/chunk-LAAB5SZT.js +0 -2
  167. package/out/chunk-NWZ4PVPN.js +0 -1
  168. package/out/chunk-OSIMGFUF.js +0 -1
  169. package/out/chunk-RFZQQXSG.js +0 -17
  170. package/out/chunk-RSHMVYNS.js +0 -1
  171. package/out/chunk-TZ5KB2Y5.js +0 -10
  172. package/out/chunk-UNTRGE26.js +0 -2
  173. package/out/chunk-UXNUAIIC.js +0 -33
  174. package/out/chunk-UYAXVRVL.js +0 -1
  175. package/out/chunk-V3ME6FI5.js +0 -2
  176. package/out/chunk-V5O7M2YT.js +0 -1
  177. package/out/chunk-VIHGO77Y.js +0 -1
  178. package/out/chunk-VQJCXGIY.js +0 -7
  179. package/out/chunk-W4J3MGLB.js +0 -1
  180. package/out/chunk-WGG7KQUH.js +0 -1
  181. package/out/chunk-WZDTZE64.js +0 -1
  182. package/out/chunk-XOCAPDCM.js +0 -1
  183. package/out/chunk-XYFXXB2G.js +0 -10
  184. package/out/chunk-YVZH5SHP.js +0 -1
  185. package/out/chunk-ZBBOBQ43.js +0 -1
  186. package/out/chunk-ZMQQCGF6.js +0 -2
@@ -8,10 +8,11 @@
8
8
  // files the open version adds, and `changed`, those it changes, each `{ path,
9
9
  // renamedFrom?, modified? }`, by its path in the open version.
10
10
  import { bundleFileSizes, bundleFilesAsMap } from '../../common/bundle-sources.js'
11
- import { npmVersionFileRisk, npmVersionHosts } from './files.js'
11
+ import { npmLoadedScripts, npmVersionFileRisk, npmVersionHosts } from './files.js'
12
12
  import { npmHostFinding } from './held.js'
13
13
  import { HOST_TAGS, npmHostSeverity, npmHostsTagged } from '../files/hosts.js'
14
14
  import { npmBecameUnreadable, npmBinaryCode, npmLargeCode, npmUnreadable } from '../files/readability.js'
15
+ import { npmIsScript } from '../files/script.js'
15
16
  import { SEVERITIES } from '../files/stamps.js'
16
17
  import { CREDENTIAL_TAGS, npmFoundAdded, npmRunsCodeFlags, npmRunsCodeTagged, npmRunsCodeWithout } from '../files/runs-code.js'
17
18
  import { npmDependencySpecs } from './dependencies.js'
@@ -19,6 +20,21 @@ import { npmFilesDiff } from './diff.js'
19
20
  import { npmInstallScripts } from './install-scripts.js'
20
21
  import { npmInstallScriptsChange, npmProvenanceLost, npmPublisherChange } from './release.js'
21
22
 
23
+ // The files the open version (`other`) runs as scripts that the one compared
24
+ // with (`base`) held, changed or not, but didn't run, by their names
25
+ // (script.js npmIsScript, a `.js` renamed `.txt`) or as code loaded them
26
+ // (npm/files.js npmLoadedScripts), as its code now loads them, each a row as npmFilesDiff's
27
+ // `changed` holds one, its own where it has one: what node runs of them is
28
+ // new, as a changed file's is.
29
+ function npmWokenFiles(base, other, files) {
30
+ const added = new Set(files.onlyOther.map(row => row.path)), rows = new Map(files.changed.map(row => [row.path, row]))
31
+ const before = bundleFilesAsMap(base), loadedBefore = npmLoadedScripts(base)
32
+ // A file that isn't text by its name alone (`payload.js`).
33
+ const ran = path => loadedBefore.has(path) || (before.has(path) && npmIsScript(path, typeof before.get(path) === 'string' ? before.get(path) : ''))
34
+ return [...npmLoadedScripts(other)].filter(path => !added.has(path)).map(path => rows.get(path) ?? { path })
35
+ .filter(row => !ran(row.renamedFrom ?? row.path))
36
+ }
37
+
22
38
  // What the open version does to its files that calls for a look, as red
23
39
  // findings: code that runs beyond itself (runs-code.js), new to a file it
24
40
  // adds or changes, the kinds with their lines (`runs-code`), those reaching
@@ -26,7 +42,8 @@ import { npmInstallScriptsChange, npmProvenanceLost, npmPublisherChange } from '
26
42
  // one of the hosts' tags new to a file it adds or changes (hosts.js
27
43
  // npmExfiltration), a finding a tag a level higher than its own
28
44
  // (npmHostSeverity: `exfiltration` and `remote` red, `ip-lookup` orange); a
29
- // code file of 256 KiB or more it adds (npmLargeCode, `large-code`); a file
45
+ // code file of 256 KiB or more it adds, one node runs as a script whatever
46
+ // its name too (npmLargeCode, `large-code`); a file
30
47
  // it changes that had no line over 200 characters and now has one over 2,000
31
48
  // (npmBecameUnreadable, `now-unreadable`); a file it adds or changes with
32
49
  // an obfuscator's fingerprint where the one compared with had none
@@ -39,13 +56,17 @@ import { npmInstallScriptsChange, npmProvenanceLost, npmPublisherChange } from '
39
56
  // where its source map was rebuilt with it (`rebuilt`, rebuiltWithMap), as a
40
57
  // build minifies a file and writes its map together, and in a `major`
41
58
  // release (npmMajorRelease): `now-unreadable` orange where every such file is
42
- // either, else red.
59
+ // either, else red. A file it held, changed or not, that node now runs as a
60
+ // script (npmWokenFiles) counts as one it changes, large code where it is
61
+ // one, and unreadable now where it wasn't as no script, whatever its lines
62
+ // were.
43
63
  function npmFileFindings(base, other, files, major) {
44
64
  const added = new Set(files.onlyOther)
45
65
  const after = row => npmVersionFileRisk(other, row.path)
46
66
  const before = row => added.has(row) ? null : npmVersionFileRisk(base, row.renamedFrom ?? row.path)
47
67
  const changed = files.changed.filter(row => row.renamedFrom == null || row.modified)
48
- const touched = [...files.onlyOther, ...changed]
68
+ const woken = npmWokenFiles(base, other, files)
69
+ const touched = [...new Set([...files.onlyOther, ...changed, ...woken])]
49
70
  const sizes = bundleFileSizes(other)
50
71
  // The files that hold what they didn't before, each with that.
51
72
  const gained = key => touched.map(row => ({ path: row.path, added: npmFoundAdded(after(row)[key], before(row)?.[key] ?? null) })).filter(row => row.added)
@@ -61,13 +82,16 @@ function npmFileFindings(base, other, files, major) {
61
82
  const rows = exfiltration.map(row => ({ path: row.path, added: npmHostsTagged(row.added, tag) })).filter(row => row.added)
62
83
  return { id: npmHostFinding(tag), severity: rows.length > 0 ? npmHostSeverity(tag, rows[0].added, null) : HOST_TAGS.get(tag), files: rows }
63
84
  }),
64
- { id: 'large-code', severity: 'critical', files: files.onlyOther.filter(row => npmLargeCode(row.path, sizes.get(row.path))).map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
65
- { id: 'now-unreadable', ...unreadableFinding(major, changed.filter(row => npmBecameUnreadable(row.path, before(row)?.readability, after(row).readability))
85
+ { id: 'large-code', severity: 'critical', files: [...files.onlyOther, ...woken].filter(row => npmLargeCode(row.path, sizes.get(row.path), npmLoadedScripts(other).has(row.path)))
86
+ .map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
87
+ { id: 'now-unreadable', ...unreadableFinding(major, [...new Set([...changed.filter(row => npmBecameUnreadable(row.path, before(row)?.readability, after(row).readability, npmLoadedScripts(other).has(row.path))),
88
+ ...woken.filter(row => npmUnreadable(row.path, after(row).readability, true) && !npmUnreadable(row.renamedFrom ?? row.path, before(row).readability))])]
66
89
  .map(row => ({ path: row.path, was: before(row).readability.longest, now: after(row).readability.longest,
67
90
  ...rebuiltWithMap(base, other, row.path, row.renamedFrom ?? row.path) ? { rebuilt: true } : {} }))) },
68
91
  { id: 'obfuscated', severity: 'critical', files: touched.filter(row => after(row).readability.obfuscation && !before(row)?.readability.obfuscation)
69
92
  .map(row => ({ path: row.path, obfuscation: after(row).readability.obfuscation })) },
70
- { id: 'binary-code', severity: 'critical', files: touched.filter(row => npmBinaryCode(row.path, after(row).readability) && !npmBinaryCode(row.renamedFrom ?? row.path, before(row)?.readability))
93
+ { id: 'binary-code', severity: 'critical', files: touched.filter(row => npmBinaryCode(row.path, after(row).readability, npmLoadedScripts(other).has(row.path))
94
+ && !npmBinaryCode(row.renamedFrom ?? row.path, before(row)?.readability, npmLoadedScripts(base).has(row.renamedFrom ?? row.path)))
71
95
  .map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
72
96
  { id: 'kept-runs-code', severity: 'high', files: ofKinds(kept, 'found', npmRunsCodeWithout) },
73
97
  { id: 'kept-credentials', severity: 'high', files: ofKinds(kept, 'found', npmRunsCodeTagged) },
@@ -149,7 +173,7 @@ const unreadableFinding = (major, files) => ({ severity: major || files.every(ro
149
173
  // adds or changes hold all it names anew.
150
174
  function npmNewHosts(base, other, files) {
151
175
  const found = new Map(), known = npmVersionHosts(base)
152
- for (const { path } of [...files.onlyOther, ...files.changed]) {
176
+ for (const path of new Set([...files.onlyOther, ...files.changed, ...npmWokenFiles(base, other, files)].map(row => row.path))) {
153
177
  for (const [host, line] of npmVersionFileRisk(other, path).hosts ?? []) {
154
178
  if (known.has(host) || ALLOWED_HOSTS.has(host)) continue
155
179
  const place = { path, line }
@@ -179,8 +203,8 @@ export function npmBaseFilesTell(other, manifest) {
179
203
  const sizes = bundleFileSizes(other)
180
204
  return [...bundleFilesAsMap(other).keys()].some(path => {
181
205
  const { runsCode, exfiltration, readability, hosts } = npmVersionFileRisk(other, path)
182
- return Boolean(runsCode || exfiltration || readability.obfuscation) || npmUnreadable(path, readability) || npmLargeCode(path, sizes.get(path))
183
- || npmBinaryCode(path, readability)
206
+ return Boolean(runsCode || exfiltration || readability.obfuscation) || npmUnreadable(path, readability, npmLoadedScripts(other).has(path)) || npmLargeCode(path, sizes.get(path), npmLoadedScripts(other).has(path))
207
+ || npmBinaryCode(path, readability, npmLoadedScripts(other).has(path))
184
208
  || [...hosts?.keys() ?? []].some(host => !ALLOWED_HOSTS.has(host))
185
209
  })
186
210
  }
@@ -125,6 +125,17 @@ export function npmDependencyUnimported(details, { name, kind, range, to }) {
125
125
  && !commands.some(command => commandNames(command, name))
126
126
  }
127
127
 
128
+ // Whether a dependency nothing of a version imports (`{ name, range }`,
129
+ // npmDependencyUnimported) counts toward its risk (assess.js): not lodash,
130
+ // which packages keep naming long after their code stopped importing it, nor
131
+ // Babel's own presets (`@babel/preset-*`), which a config names by a string
132
+ // (an ESLint config's parser options); each where npm installs that package
133
+ // from its registry (installsAs), not another under its name.
134
+ export function npmUnusedDependencyCounted({ name, range }) {
135
+ const installs = installsAs(name, range)
136
+ return !(name === 'lodash' && installs === 'lodash') && !(name.startsWith('@babel/preset-') && installs?.startsWith('@babel/preset-'))
137
+ }
138
+
128
139
  // Where a source takes a dependency from, in a sentence.
129
140
  const SOURCE_NAMES = new Map([['URL', 'a URL'], ['git', 'a git repository']])
130
141
 
package/risk/npm/files.js CHANGED
@@ -2,21 +2,432 @@
2
2
  // npmFileRisk), and every host its scripts name: for compare.js's findings
3
3
  // and held.js's, and for the viewer, which reads its files' tags from it.
4
4
  import { bundleFileSizes, bundleFilesAsMap } from '../../common/bundle-sources.js'
5
+ import { NPM_PROSE } from '../files/readability.js'
6
+ import { npmIsScript } from '../files/script.js'
5
7
  import { npmFileRisk } from '../files/stamps.js'
6
8
 
7
9
  // What a version's files are kept by: its bundle where it is one, else its
8
10
  // files' list, else itself.
9
11
  const keyOf = version => version.bundle ?? version.json ?? version
10
12
 
13
+ // A string's escapes as JavaScript reads them (`\x2e`, `\u002e`, `\u{2e}`, a
14
+ // control's letter, and an octal's, `\056`, as code outside strict mode
15
+ // reads it), a line continuation for nothing, any other for itself: `\X2E`
16
+ // and `\U002E` too, their letters in lowercase alone introducing a code.
17
+ const ESCAPE = /\\(?:x([\dA-Fa-f]{2})|u([\dA-Fa-f]{4})|u\{([\dA-Fa-f]{1,6})\}|([0-3][0-7]{0,2}|[4-7][0-7]?)|(\r\n|[\s\S]))/gu
18
+ const CONTROLS = { b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' }
19
+ const unescaped = body => body.replaceAll(ESCAPE, (escape, x, u, braced, octal, other) => {
20
+ if (octal !== undefined) return String.fromCodePoint(Number.parseInt(octal, 8))
21
+ if (other !== undefined) return /^(?:\r\n|[\n\r\u2028\u2029])$/u.test(other) ? '' : CONTROLS[other] ?? other
22
+ const code = Number.parseInt(x ?? u ?? braced, 16)
23
+ return code <= 0x10FFFF ? String.fromCodePoint(code) : escape
24
+ })
25
+
26
+ // The characters a filesystem telling neither case apart (folded) takes for
27
+ // one of a package's name, which npm keeps to ASCII: its cases, and those
28
+ // Unicode cases into it (`ı`, `ſ`, the Kelvin sign).
29
+ const FOLDING_INTO = { i: '\u0131', k: '\u212A', s: '\u017F' }
30
+ const foldsInto = char => new Set([char, char.toLowerCase(), char.toUpperCase(), ...FOLDING_INTO[char.toLowerCase()] ?? ''])
31
+
32
+ // Each string a text writes out that may name a file of its package, its
33
+ // value starting `.`, with an escape, or with one of `firsts` (its name's
34
+ // first character, foldsInto, and `#` where its package.json files map
35
+ // specifiers of its own): from any quote no backslash escapes (an even run of them
36
+ // before it, or none) to the next of its kind none escapes, a template's
37
+ // across lines, the others' within their line, a line continuation aside.
38
+ // Tried from every such quote, not only where code's tokens say a string
39
+ // starts, which code can be written to have read wrong (a regular
40
+ // expression, a comment or a template's text taken for another): so one in
41
+ // a comment, or in another string, is read too. Linear on any text, each
42
+ // quote's string read to the next quote of its kind.
43
+ function quoted(firsts) {
44
+ const starts = String.raw`(?=[.\\${[...firsts].map(char => String.raw`\u{${char.codePointAt(0).toString(16)}}`).join('')}])`
45
+ const line = quote => String.raw`${quote}(?=${starts}((?:[^${quote}\\\n]|\\(?:\r\n|[\s\S]))*)${quote})`
46
+ return new RegExp(String.raw`(?<=(?:^|[^\\])(?:\\\\)*)(?:${line('"')}|${line("'")}|\x60(?=${starts}((?:[^\x60\\]|\\[\s\S])*)\x60))`, 'gu')
47
+ }
48
+
49
+ // A string's values, from its quote and what it holds: as JavaScript reads
50
+ // its escapes (unescaped), and a template's as written too, as a tag reads it
51
+ // (`String.raw`); none for a template with something put in it, a `${` no
52
+ // backslash escapes (`\${` its text).
53
+ function valuesOf(quote, body) {
54
+ if (!body.includes('\\')) return quote === '`' && body.includes('${') ? [] : [body]
55
+ if (quote !== '`') return [unescaped(body)]
56
+ return body.replaceAll(/\\[\s\S]/gu, '').includes('${') ? [] : [unescaped(body), body]
57
+ }
58
+
59
+ // Where a string is what `import` takes, rather than what `require` may: a
60
+ // declaration's (`import './x'`, `from './x'`), on the keyword's line with
61
+ // blanks alone between, as a line break could begin another statement
62
+ // there (`from\n'./x'.split(',').forEach(require)`), and not a template,
63
+ // which `from` would be called with as a tag; or the whole first argument of
64
+ // `import(…)`, on its line too.
65
+ const DECLARED = /(?<=(?<![\p{ID_Continue}$.#])(?:import|from)[ \t]*)/uy
66
+ const CALLED = /(?<=(?<![\p{ID_Continue}$.#])import[ \t]*\([ \t]*)/uy
67
+ const ARGUMENT_END = /[ \t]*[,)]/uy
68
+ const matchesAt = (regex, text, index) => {
69
+ regex.lastIndex = index
70
+ return regex.test(text)
71
+ }
72
+ const imported = (text, quote, start, end) => (quote !== '`' && matchesAt(DECLARED, text, start)) || (matchesAt(CALLED, text, start) && matchesAt(ARGUMENT_END, text, end))
73
+
74
+ // A relative path: from `./` or `../`, or `.` or `..` alone.
75
+ const RELATIVE = /^\.\.?(?:\/|$)/u
76
+ // What `require` doesn't run as JavaScript: JSON, and a native addon, by
77
+ // their names as it knows them, in lowercase: `.JSON` it runs.
78
+ const NOT_RUN = /\.(?:json|node)$/u
79
+ // The extensions `require` tries a path with that names no file.
80
+ const TRIED = ['.js', '.json', '.node']
81
+ // A path `require` takes as a directory's alone, never a file's: one ending
82
+ // in `/`, or in `.` or `..` after one, or either alone, as node tells it, by
83
+ // `/` alone, on Windows too (`./payload/`, not `.\payload\`).
84
+ const DIRECTORY = /(?:^|\/)\.{0,2}$/u
85
+ // A name with an extension, which `import` runs only where it knows it: not
86
+ // `.npmignore`, nor `payload`.
87
+ const EXTENSION = /[^/.][^/]*\.[^/]*$/u
88
+
89
+ // A path's parts, `spec`'s from `parts`, each `..` a directory up; null for
90
+ // one leading past the top.
91
+ export function resolved(parts, spec) {
92
+ const all = [...parts]
93
+ for (const part of spec.split('/')) {
94
+ if (part === '..' && all.pop() === undefined) return null
95
+ if (part !== '..' && part !== '.' && part !== '') all.push(part)
96
+ }
97
+ return all
98
+ }
99
+ // Those parts as a path in the package where it is installed (`root`, its
100
+ // directory's parts, `node_modules/<name>`), told as a filesystem telling
101
+ // neither case nor Unicode's normalization apart tells them (folded,
102
+ // `../LOADS/x` for `loads`): null for one outside it.
103
+ const within = (root, parts) => parts !== null && root.every((part, index) => index < parts.length && (parts[index] === part || folded(parts[index]) === part)) ? parts.slice(root.length).join('/') : null
104
+ // `spec` from the directory `dir` ('' for the package's top) as a path in the
105
+ // package, one leading out of it and back in by its name too
106
+ // (`../<name>/x`); null for one leading elsewhere.
107
+ const joined = (root, dir, spec) => within(root, resolved([...root, ...dir === '' ? [] : dir.split('/')], spec))
108
+ // A path as `require` takes it: its `\` a separator on Windows, a name's
109
+ // character elsewhere, so read both ways.
110
+ const separated = spec => new Set([spec, spec.replaceAll('\\', '/')])
111
+ // Each relative one of those (RELATIVE), from `dir`, as a path in the package
112
+ // (joined).
113
+ const requiredPaths = (root, dir, spec) => [...separated(spec)].filter(path => RELATIVE.test(path)).map(path => joined(root, dir, path))
114
+ // What a URL reads otherwise than a path: an escape, a query, a fragment, a
115
+ // `\`, a tab or a line break, or a blank or a control at its end, which it
116
+ // drops.
117
+ const URL_READ = /[%?#\\\t\n\r]|[\0- ]$/u
118
+ // A relative path as `import` takes it, and `exports` and `imports` targets,
119
+ // a URL's: its query and fragment dropped, tabs and line breaks too, `\` a
120
+ // separator, its escapes read (`%2E`); null for one escaping a separator
121
+ // (`%2F`), as node refuses, or leading out of the package (joined).
122
+ function urlJoined(root, dir, spec) {
123
+ if (!RELATIVE.test(spec)) return null
124
+ if (!URL_READ.test(spec)) return joined(root, dir, spec)
125
+ try {
126
+ const base = [...root, ...dir === '' ? [] : dir.split('/')].map(part => `${encodeURIComponent(part)}/`).join('')
127
+ const { pathname } = new URL(spec, `file:///${base}`)
128
+ return /%(?:2f|5c)/iu.test(pathname) ? null : within(root, resolved([], decodeURIComponent(pathname)))
129
+ } catch {
130
+ return null
131
+ }
132
+ }
133
+
134
+ // Where a package is installed (`node_modules/<name>`), as parts, folded
135
+ // (within); none for one with no name, or one no directory could be.
136
+ const NAME = /^(?:@[^/]+\/)?[^/@.][^/]*$/u
137
+ const rootOf = name => typeof name === 'string' && NAME.test(name) && name.split('/').every(part => part !== '.' && part !== '..') ? ['node_modules', ...name.split('/')].map(folded) : []
138
+
139
+ // A path as a filesystem telling neither case nor Unicode's normalization
140
+ // apart takes it, as Windows' and macOS's do by default: `Payload.txt` for
141
+ // `payload.txt`; a lone surrogate the U+FFFD node writes a name with for it.
142
+ const folded = path => path.toWellFormed().normalize('NFC').toUpperCase().toLowerCase()
143
+ // The files a path names once installed, as a filesystem tells them
144
+ // (folded): its own, and for a .npmignore, a .gitignore npm installs as that;
145
+ // and the filesystems node may find them on, each telling the files a path
146
+ // names (`named`) by the path as it takes it (`fold`): one telling case
147
+ // apart (`cased`), as Linux's does, and one telling neither case nor
148
+ // Unicode's normalization apart (`folding`, folded), a path folded naming
149
+ // those with what follows it in ASCII (`.json`, `/index`), which folds as
150
+ // it is, every file the other names among them.
151
+ function installedOf(files) {
152
+ const exact = new Map(), index = new Map()
153
+ const put = (map, key, path) => map.has(key) ? map.get(key).push(path) : map.set(key, [path])
154
+ for (const path of files.keys()) {
155
+ const aliases = /(?:^|\/)\.gitignore$/u.test(path) ? [path, path.replace(/\.gitignore$/u, '.npmignore')] : [path]
156
+ for (const alias of aliases) {
157
+ put(index, folded(alias), path)
158
+ put(exact, alias, path)
159
+ }
160
+ }
161
+ const keyed = key => index.get(key) ?? []
162
+ return { installed: path => path === null ? [] : keyed(folded(path)), cased: { named: key => exact.get(key) ?? [], fold: path => path }, folding: { named: keyed, fold: folded } }
163
+ }
164
+
165
+ // Every string a JSON value holds, however deep it nests them, read
166
+ // without recursing, which nesting deeper than a call stack would end.
167
+ function leaves(value) {
168
+ const found = [], pending = [value]
169
+ while (pending.length > 0) {
170
+ const item = pending.pop()
171
+ if (typeof item === 'string') found.push(item)
172
+ else if (item && typeof item === 'object') for (const each of Object.values(item)) pending.push(each)
173
+ }
174
+ return found
175
+ }
176
+
177
+ // The targets an `exports` or `imports` map holds (leaves), by its keys
178
+ // (those `subpaths` tells, `./x`, `#x`), each a pattern's where its key is
179
+ // one (`./lib/*`), as node puts what a consumer asks for in a target's `*`
180
+ // only then; a map with none of those keys its `.`'s whole (`"exports":
181
+ // "./x"`, or conditions alone).
182
+ function mapped(map, subpaths) {
183
+ const keyed = map && typeof map === 'object' && !Array.isArray(map) && Object.keys(map).some(subpaths) ? Object.entries(map) : [['.', map]]
184
+ return keyed.flatMap(([key, value]) => leaves(value).map(target => ({ key, pattern: key.includes('*'), target })))
185
+ }
186
+
187
+ // A package.json's `name`, `main`, and the paths its `exports` and `imports`
188
+ // name (`./x`, mapped); and its maps by their keys (`./x`, `#x`), its
189
+ // `imports` to the package's own name too (`<name>/x`), read past a byte
190
+ // order mark as node reads it; none where it is no JSON object.
191
+ function entriesOf(text) {
192
+ const none = { name: null, main: null, targets: [], exports: [], hasExports: false, imports: [] }
193
+ if (typeof text !== 'string') return none
194
+ try {
195
+ const { name, main, exports, imports } = JSON.parse(text.replace(/^\uFEFF/u, ''))
196
+ const subpaths = mapped(exports, key => key.startsWith('.')).filter(({ key, target }) => key.startsWith('.') && target.startsWith('./'))
197
+ const own = mapped(imports, key => key.startsWith('#')).filter(({ key }) => key.startsWith('#'))
198
+ const targets = [...subpaths, ...own.filter(({ target }) => target.startsWith('./'))]
199
+ return { name, main: typeof main === 'string' && main !== '' ? main : null, targets, exports: subpaths, hasExports: exports !== undefined && exports !== null, imports: own }
200
+ } catch {
201
+ return none
202
+ }
203
+ }
204
+
205
+ // A path as a filesystem reads it, from the package's top: each run of `/`
206
+ // one, none leading.
207
+ const collapsed = path => path.replaceAll(/\/+/gu, '/').replace(/^\//u, '')
208
+
209
+ // The paths an `exports` or `imports` target names, from the directory of the
210
+ // package.json naming it, read as a URL (urlJoined): its own, `*` and all,
211
+ // or where its key is a pattern (`pattern`, mapped) each file it matches, as a filesystem tells them (folded, collapsed), each of
212
+ // its `*` the same text, as node puts what a consumer asks for (`pkg/x`) in
213
+ // every one, where the pattern names what follows the last (`./lib/*.txt`);
214
+ // where it names nothing (`./*`), only one with no extension that isn't prose
215
+ // by its name (not `LICENSE`), the rest a consumer reaches only asking for
216
+ // them by their names. What a `*` stands for may be text with a `/` before
217
+ // it, after it, both or neither, or a `/` alone, the filesystem folding it
218
+ // into a `/` beside it (`pkg//` reaching `lib/.txt` through `./lib/*.txt`):
219
+ // each such shape's length told by one `x`'s, a file of the pattern's is the
220
+ // pattern with its text in each; a `/` before or after it counted apart only
221
+ // where it folds into a `/` of the pattern's, or the top, beside one `*` and
222
+ // not another, else its text holds it, or it folds alike at every one.
223
+ function exported(names, root, dir, target, pattern) {
224
+ const path = urlJoined(root, dir, target)
225
+ if (path === null || !pattern || !path.includes('*')) return [path]
226
+ const parts = folded(path).split('*'), stars = parts.length - 1
227
+ const head = parts[0].replace(/\/+$/u, ''), slashes = collapsed(parts.join('/')), tail = parts.at(-1).replace(/^\/+/u, '')
228
+ const both = sides => sides.includes(true) && sides.includes(false) ? ['', '/'] : ['']
229
+ const afters = both(parts.slice(1).map(part => part.startsWith('/'))), befores = both(parts.slice(0, -1).map((part, index) => part.endsWith('/') || (index === 0 && part === '')))
230
+ const shapes = befores.flatMap(before => afters.map(after => ({ after, at: collapsed(`${parts[0]}${before}x`).length - 1, before, length: collapsed(parts.join(`${before}x${after}`)).length })))
231
+ return names.filter(([name, file]) => {
232
+ if (!file.startsWith(head) || !file.endsWith(tail) || (parts.at(-1) === '' && (EXTENSION.test(name) || NPM_PROSE.test(name)))) return false
233
+ return file === slashes || shapes.some(({ after, at, before, length }) => {
234
+ const core = (file.length - length) / stars + 1
235
+ if (!Number.isInteger(core) || core <= 0) return false
236
+ const built = parts.join(`${before}${file.slice(at, at + core)}${after}`)
237
+ return built === file || ((built.includes('//') || built.startsWith('/')) && collapsed(built) === file)
238
+ })
239
+ }).map(([name]) => name)
240
+ }
241
+
242
+ // What a map's entries (`{ dir, key, target }` each) take `value` to, as
243
+ // node maps a specifier through `exports` or `imports`: the target of a key
244
+ // that is it, or of a pattern's (`#lib/*`, `./lib/*`, one `*`) it matches,
245
+ // what it matches put in each of the target's `*`; EVERY for one that would
246
+ // put in more than MAX_EXPANSION characters, which only a package written to
247
+ // hold up its reading has (`*` thousands of times over a long specifier), and
248
+ // whose path a URL's reading could shorten again (tabs, a query, `//`).
249
+ const MAX_EXPANSION = 64 * 1024
250
+ const EVERY = Symbol('every file')
251
+ const mappedTo = (entries, value) => entries.flatMap(({ dir, key, target }) => {
252
+ const star = key.indexOf('*')
253
+ if (star === -1 ? key !== value : star !== key.lastIndexOf('*') || value.length < key.length || !value.startsWith(key.slice(0, star)) || !value.endsWith(key.slice(star + 1))) return []
254
+ if (star === -1) return [{ dir, target }]
255
+ const sub = value.slice(star, value.length - (key.length - star - 1))
256
+ return [{ dir, target: (target.split('*').length - 1) * sub.length > MAX_EXPANSION ? EVERY : target.replaceAll('*', sub) }]
257
+ })
258
+
259
+ // A package's own subpath (`./x`, `.`) as its `exports` map it (mappedTo), as
260
+ // node resolves a specifier by its own name (`<name>/x`), read as URLs; with
261
+ // no `exports` at all, the path itself, as node finds the package where it
262
+ // is installed (`root`).
263
+ const selfResolved = (root, { exports, hasExports }, spec) => hasExports
264
+ ? mappedTo(exports.map(entry => ({ ...entry, dir: '' })), spec).map(({ target }) => target === EVERY ? EVERY : urlJoined(root, '', target))
265
+ : [urlJoined(root, '', spec)]
266
+
267
+ // The path of the first of `extensions` a path takes that names a file of a
268
+ // version's on a filesystem (`system`, installedOf's), each it tells for it
269
+ // its own: none at the package's top, which is no file.
270
+ function firstNamed({ named, fold }, path, extensions) {
271
+ const key = path === '' ? null : fold(path)
272
+ const extension = key === null ? undefined : extensions.find(each => named(`${key}${each}`).length > 0)
273
+ return extension === undefined ? null : `${path}${extension}`
274
+ }
275
+ const indexFile = (system, path) => firstNamed(system, path === '' ? 'index' : `${path}/index`, TRIED)
276
+
277
+ // The files a path `require` takes finds, the first it tries that names a
278
+ // file: its own, one by its name and an extension (`.js`, `.json`, `.node`,
279
+ // `payload.JSON` as a filesystem folding case finds it), but where it takes
280
+ // it as a directory's (`directory`, DIRECTORY); else a directory's
281
+ // package.json `main`, found so or by its `index` and an extension; else the
282
+ // directory's `index` so, as node falls back to it; on a filesystem
283
+ // (`system`, installedOf's), as each finds its own first.
284
+ function requiredFiles(files, system, root, path, directory) {
285
+ if (path === null) return []
286
+ const file = directory ? null : firstNamed(system, path, ['', ...TRIED])
287
+ if (file !== null) return [file]
288
+ const mains = system.named(path === '' ? 'package.json' : `${system.fold(path)}/package.json`).map(manifest => entriesOf(files.get(manifest)).main).filter(main => main !== null)
289
+ .flatMap(main => [...separated(main)].map(spec => joined(root, path, spec))).filter(at => at !== null)
290
+ .map(at => firstNamed(system, at, ['', ...TRIED]) ?? indexFile(system, at)).filter(found => found !== null)
291
+ return mains.length > 0 ? mains : [indexFile(system, path)].filter(found => found !== null)
292
+ }
293
+
294
+ // The `#` specifiers each package.json of a version's files maps
295
+ // (`imports`), each from its directory.
296
+ const specifiersOf = files => [...files].filter(([path]) => /(?:^|\/)package\.json$/iu.test(path))
297
+ .flatMap(([path, text]) => entriesOf(text).imports.map(({ key, target }) => ({ dir: path.slice(0, -'package.json'.length - 1), key, target })))
298
+
299
+ // How many `exports` and `imports` patterns a package's package.json files
300
+ // have each matched against its files (exported): more than a package names
301
+ // (4 at most of popular ones). One naming more has every file read as one it
302
+ // may load.
303
+ const MAX_PATTERNS = 64
304
+
305
+ // The files of a version node runs as JavaScript though their names say
306
+ // otherwise (script.js npmIsScript), read as scripts all the same
307
+ // (stamps.js npmFileRisk's `script`): any a script of it, or a file read so,
308
+ // names by a relative path written out whole as a string (quoted), anywhere
309
+ // in its text, whatever takes it, or by the package's own name
310
+ // (`'<name>/x.txt'`); as `require` runs any but JSON and a native addon
311
+ // (`require('./data.txt')`, `require('./.npmignore')`, the .gitignore npm
312
+ // installs as that), and the `main` of a directory's package.json; and as
313
+ // `import` runs one whose name has no extension (`import('./payload')`), as a
314
+ // URL (urlJoined), a string an import declaration or `import(…)` takes whole
315
+ // read as it alone (imported). And the package's own `main`, and what the
316
+ // `exports` and `imports` of its package.json files name, their patterns'
317
+ // too, which a `require` reaching them runs whatever their names. A path a
318
+ // script builds is not followed (`'./x' + '.txt'`, a template with something
319
+ // put in it), nor one written in a string run as code, which runs-code.js
320
+ // tells. Each path names the files a filesystem telling neither case nor
321
+ // Unicode's normalization apart finds by it (folded), as Windows' and
322
+ // macOS's do, a loader running one where its name or theirs says it does;
323
+ // one that isn't text among them too, which node runs decoded as UTF-8 and
324
+ // readability.js npmBinaryCode tells, though nothing it loads is read, but
325
+ // where it can't parse (tarball.js npmRunnable's `runnable`: an image).
326
+ const loadedByVersion = new WeakMap()
327
+ export function npmLoadedScripts(version) {
328
+ let loaded = loadedByVersion.get(keyOf(version))
329
+ if (loaded) return loaded
330
+ loaded = new Set()
331
+ const files = bundleFilesAsMap(version), { installed, cased, folding } = installedOf(files), queue = []
332
+ const top = entriesOf(files.get(files.has('package.json') ? 'package.json' : installed('package.json')[0]))
333
+ const root = rootOf(top.name)
334
+ // What a loader takes (`by`) runs, as the name it is asked by tells, or
335
+ // the name of the file it finds.
336
+ const runs = (path, by) => !(by === 'require' ? NOT_RUN : EXTENSION).test(path)
337
+ // Each path once a loader and whether its name runs: the files it names
338
+ // are the same however else it is cased.
339
+ const tried = new Set()
340
+ const add = (path, by) => {
341
+ const key = path === null ? null : `${by}\0${runs(path, by)}\0${folded(path)}`
342
+ if (key === null || tried.has(key)) return
343
+ tried.add(key)
344
+ for (const file of installed(path)) {
345
+ const text = files.get(file)
346
+ if (loaded.has(file) || npmIsScript(file, text) || text?.runnable === false || (!runs(path, by) && !runs(file, by))) continue
347
+ loaded.add(file)
348
+ if (typeof text === 'string') queue.push(file)
349
+ }
350
+ }
351
+ // On either filesystem: where one folding case finds none, neither does
352
+ // one telling case apart, which finds fewer.
353
+ const requires = (path, directory) => {
354
+ const found = requiredFiles(files, folding, root, path, directory)
355
+ if (found.length > 0) for (const file of [...found, ...requiredFiles(files, cased, root, path, directory)]) add(file, 'require')
356
+ }
357
+ requires('', true)
358
+ const targets = [...files].filter(([path]) => /(?:^|\/)package\.json$/iu.test(path))
359
+ .flatMap(([path, text]) => entriesOf(text).targets.map(({ pattern, target }) => [path.slice(0, -'package.json'.length - 1), target, pattern && target.includes('*')]))
360
+ if (targets.filter(([, , pattern]) => pattern).length > MAX_PATTERNS) for (const path of files.keys()) add(path, 'require')
361
+ else {
362
+ // Each file's path and its path as a filesystem tells it (folded), for
363
+ // the patterns to match.
364
+ const names = targets.some(([, , pattern]) => pattern) ? [...files.keys()].map(name => [name, folded(name)]) : []
365
+ for (const [dir, target, pattern] of targets) for (const file of exported(names, root, dir, target, pattern)) add(file, 'require')
366
+ }
367
+ for (const [path, text] of files) if (npmIsScript(path, text)) queue.push(path)
368
+ const specifiers = specifiersOf(files)
369
+ // The package's own name, as a filesystem tells it (rootOf), a path from
370
+ // it after a `/`, or a `\\` as on Windows.
371
+ const named = value => root.length > 0 && value.length > top.name.length && '/\\'.includes(value[top.name.length])
372
+ && folded(value.slice(0, top.name.length)) === root.slice(1).join('/')
373
+ const seen = new Set(), strings = quoted([...root.length > 0 ? foldsInto(top.name[0]) : [], ...specifiers.length > 0 ? ['#'] : []])
374
+ const subpathsOf = spec => selfResolved(root, top, spec)
375
+ // A path loaded, or for EVERY, every file, once (mappedTo).
376
+ let every = false
377
+ const reach = (path, by) => {
378
+ if (path !== EVERY) return add(path, by)
379
+ if (!every) for (const file of files.keys()) add(file, 'require')
380
+ every = true
381
+ }
382
+ // A `#` specifier as the package's `imports` map it (mappedTo), to a path
383
+ // of its own, read as a URL (urlJoined), or to its own name, as its
384
+ // `exports` map that (subpathsOf), loaded as code takes it (`by`), once
385
+ // each.
386
+ const own = (value, by) => {
387
+ if (seen.has(`${by}\0#\0${value}`)) return
388
+ seen.add(`${by}\0#\0${value}`)
389
+ for (const { dir, target } of mappedTo(specifiers, value)) {
390
+ const paths = target === EVERY ? [EVERY] : target.startsWith('./') ? [urlJoined(root, dir, target)] : named(target) ? subpathsOf(`./${target.slice(top.name.length + 1)}`)
391
+ : root.length > 0 && target === top.name ? subpathsOf('.') : []
392
+ for (const path of paths) reach(path, by)
393
+ }
394
+ }
395
+ // A string's value from the directory `dir`, a load `import` takes where
396
+ // its place says so (`importing`), else one `require` may take, once each.
397
+ const follow = (value, dir, importing) => {
398
+ if (value.startsWith('#')) return own(value, importing() ? 'import' : 'require')
399
+ const [from, spec] = named(value) ? ['', `./${value.slice(top.name.length + 1)}`] : [dir, value]
400
+ if (!RELATIVE.test(spec.replaceAll('\\', '/'))) return
401
+ const by = importing() ? 'import' : 'require', key = `${by}\0${from}\0${spec}`
402
+ if (seen.has(key)) return
403
+ seen.add(key)
404
+ if (named(value)) for (const path of subpathsOf(spec)) reach(path, by)
405
+ if (named(value) && top.hasExports) return
406
+ if (by === 'require') for (const required of requiredPaths(root, from, spec)) requires(required, DIRECTORY.test(spec))
407
+ add(urlJoined(root, from, spec), 'import')
408
+ }
409
+ for (let index = 0; index < queue.length; index++) {
410
+ const path = queue[index], text = files.get(path)
411
+ const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : ''
412
+ for (const { 1: double, 2: single, 3: template, index: start } of text.matchAll(strings)) {
413
+ const body = double ?? single ?? template, quote = text[start]
414
+ for (const value of valuesOf(quote, body)) follow(value, dir, () => imported(text, quote, start, start + body.length + 2))
415
+ }
416
+ }
417
+ loadedByVersion.set(keyOf(version), loaded)
418
+ return loaded
419
+ }
420
+
11
421
  // What risk reads of each file of a version (stamps.js npmFileRisk), once a
12
- // version and path.
422
+ // version and path: as a script too where node runs it as one though its name
423
+ // says otherwise (npmLoadedScripts).
13
424
  const reads = new WeakMap()
14
425
  export function npmVersionFileRisk(version, path) {
15
426
  let files = reads.get(keyOf(version))
16
427
  if (!files) reads.set(keyOf(version), files = new Map())
17
428
  if (!files.has(path)) {
18
429
  const text = bundleFilesAsMap(version).get(path)
19
- files.set(path, npmFileRisk(path, typeof text === 'string' ? text : null, bundleFileSizes(version).get(path)))
430
+ files.set(path, npmFileRisk(path, typeof text === 'string' ? text : null, bundleFileSizes(version).get(path), npmLoadedScripts(version).has(path)))
20
431
  }
21
432
  return files.get(path)
22
433
  }