@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51

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 (368) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +3 -0
  50. package/common/managed/finding-detail.js +66 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +7 -0
  54. package/common/managed/npm-packages.js +51 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +359 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +6 -0
  64. package/common/managed/team-app.js +47 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2K6UN3PC.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-6LNQTKAV.js +52 -0
  87. package/out/chunk-7BKKYVWR.js +7 -0
  88. package/out/chunk-7C7DWL7I.js +1 -0
  89. package/out/chunk-7JBSSRD7.js +3 -0
  90. package/out/chunk-7VAWINQF.js +6 -0
  91. package/out/chunk-C4Z6ZBXV.js +10 -0
  92. package/out/chunk-FMXWXDR4.js +36 -0
  93. package/out/chunk-GDPCLSOS.js +2 -0
  94. package/out/chunk-GO7FT4OD.js +1 -0
  95. package/out/chunk-HT2SSA3V.js +1 -0
  96. package/out/chunk-IPAH4LKF.js +232 -0
  97. package/out/chunk-IYPY47MC.js +1 -0
  98. package/out/chunk-IZ7POWI3.js +18 -0
  99. package/out/chunk-JCIIBLKR.js +1 -0
  100. package/out/chunk-KOGNQDSJ.js +1 -0
  101. package/out/chunk-NRPC74AM.js +1 -0
  102. package/out/chunk-OOJM4CTU.js +1 -0
  103. package/out/chunk-PBZPYIG7.js +1 -0
  104. package/out/chunk-PNHBZGDD.js +1 -0
  105. package/out/chunk-PWFUOLZK.js +17 -0
  106. package/out/chunk-PXK7EU4V.js +1 -0
  107. package/out/chunk-QB4CBOX2.js +1 -0
  108. package/out/chunk-R3NZFXVR.js +1 -0
  109. package/out/chunk-R5O6FAW4.js +2 -0
  110. package/out/chunk-SXV2T5C4.js +80 -0
  111. package/out/chunk-TQJDYHVE.js +2 -0
  112. package/out/chunk-TUII34K2.js +1 -0
  113. package/out/chunk-VDRXHIHN.js +3 -0
  114. package/out/chunk-VFWMQ3LG.js +2 -0
  115. package/out/chunk-VH4XMZIW.js +2 -0
  116. package/out/chunk-WCPJDAYA.js +10 -0
  117. package/out/chunk-WFYLMF6Z.js +10 -0
  118. package/out/chunk-WRAJVD35.js +1 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +364 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +23 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +76 -0
  237. package/server-managed/comparison.ts +52 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +468 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +143 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +98 -0
  249. package/server-managed/dependency-reviews.ts +364 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +376 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +136 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5503 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/listed-activity.ts +39 -0
  273. package/server-managed/locked-packages.ts +152 -0
  274. package/server-managed/lockfile-scope.ts +49 -0
  275. package/server-managed/lru.ts +30 -0
  276. package/server-managed/managed-issues.ts +78 -0
  277. package/server-managed/management-catalog.ts +45 -0
  278. package/server-managed/management.ts +99 -0
  279. package/server-managed/npm-delta-worker.js +23 -0
  280. package/server-managed/npm-delta.ts +194 -0
  281. package/server-managed/npm-insights.ts +449 -0
  282. package/server-managed/npm-kept.ts +225 -0
  283. package/server-managed/npm-loads.ts +264 -0
  284. package/server-managed/npm-manifest.ts +115 -0
  285. package/server-managed/npm-packages.ts +454 -0
  286. package/server-managed/npm-patch.ts +325 -0
  287. package/server-managed/npm-provenance.ts +123 -0
  288. package/server-managed/npm-risk.ts +285 -0
  289. package/server-managed/object-storage-disk.ts +148 -0
  290. package/server-managed/object-storage-vercel.ts +67 -0
  291. package/server-managed/object-storage.ts +75 -0
  292. package/server-managed/package-entry-points.ts +51 -0
  293. package/server-managed/pretty-print.ts +434 -0
  294. package/server-managed/public-workspace.ts +238 -0
  295. package/server-managed/pull-comment-reviews.ts +161 -0
  296. package/server-managed/pull-dependencies.ts +351 -0
  297. package/server-managed/pull-dependency-reviews.ts +334 -0
  298. package/server-managed/pull-reviews.ts +205 -0
  299. package/server-managed/queue.ts +39 -0
  300. package/server-managed/release-commits.ts +84 -0
  301. package/server-managed/rename-diff-worker.js +26 -0
  302. package/server-managed/rename-diff.ts +279 -0
  303. package/server-managed/repo-path.ts +21 -0
  304. package/server-managed/report-migration.ts +35 -0
  305. package/server-managed/report-query.ts +10 -0
  306. package/server-managed/report-response.ts +16 -0
  307. package/server-managed/report-sources.ts +178 -0
  308. package/server-managed/report-store.ts +85 -0
  309. package/server-managed/repository-actions.ts +218 -0
  310. package/server-managed/repository-agents.ts +448 -0
  311. package/server-managed/repository-aliases.ts +91 -0
  312. package/server-managed/repository-browser.ts +294 -0
  313. package/server-managed/repository-changes.ts +284 -0
  314. package/server-managed/repository-code.ts +215 -0
  315. package/server-managed/repository-dependency-reviews.ts +166 -0
  316. package/server-managed/repository-discovery.ts +82 -0
  317. package/server-managed/repository-history.ts +432 -0
  318. package/server-managed/repository-issues.ts +195 -0
  319. package/server-managed/repository-policy.ts +27 -0
  320. package/server-managed/repository-pulls.ts +212 -0
  321. package/server-managed/repository-pulse.ts +460 -0
  322. package/server-managed/repository-risk-assess.ts +105 -0
  323. package/server-managed/repository-risk-audit.ts +79 -0
  324. package/server-managed/repository-risk-read.ts +170 -0
  325. package/server-managed/repository-risk.ts +491 -0
  326. package/server-managed/repository-squashes.ts +168 -0
  327. package/server-managed/request-timing.ts +63 -0
  328. package/server-managed/reviewed-dependencies.ts +149 -0
  329. package/server-managed/revisions.ts +35 -0
  330. package/server-managed/scan-downloads.ts +87 -0
  331. package/server-managed/session.ts +132 -0
  332. package/server-managed/slugs.ts +38 -0
  333. package/server-managed/solidity-entry-points.ts +71 -0
  334. package/server-managed/sql-postgres.ts +34 -0
  335. package/server-managed/sql.ts +88 -0
  336. package/server-managed/static.ts +28 -0
  337. package/server-managed/storage-command.ts +16 -0
  338. package/server-managed/storage-db.ts +183 -0
  339. package/server-managed/storage-encryption.ts +290 -0
  340. package/server-managed/storage-maintenance.ts +259 -0
  341. package/server-managed/storage-payload.ts +121 -0
  342. package/server-managed/storage-stores.ts +91 -0
  343. package/server-managed/storage.ts +82 -0
  344. package/server-managed/team-app.ts +134 -0
  345. package/server-managed/team-catalog.ts +7 -0
  346. package/server-managed/team-feed.ts +180 -0
  347. package/server-managed/team-npm-scopes.ts +106 -0
  348. package/server-managed/team-reports.ts +319 -0
  349. package/server-managed/triage-response.ts +16 -0
  350. package/server-managed/type-only-worker.js +19 -0
  351. package/server-managed/type-only.ts +105 -0
  352. package/server-managed/uploads.ts +106 -0
  353. package/server-managed/upstream-cache.ts +243 -0
  354. package/server-managed/view-as.ts +58 -0
  355. package/server-managed/workspace-shares.ts +169 -0
  356. package/server.ts +70 -0
  357. package/server/index.ts +0 -480
  358. package/server/lifecycle.ts +0 -208
  359. /package/{LICENSE → LICENSE-MIT} +0 -0
  360. /package/{server → server-e2e}/auth.ts +0 -0
  361. /package/{server → server-e2e}/config.example.json +0 -0
  362. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  363. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  364. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  365. /package/{server → server-e2e}/peer.ts +0 -0
  366. /package/{server → server-e2e}/sse-session.ts +0 -0
  367. /package/{server → server-e2e}/util.ts +0 -0
  368. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,216 @@
1
+ import { bundleFilesAsMap, bundlePackageDirs, bundleSourcesAsMap } from './bundle-sources.js'
2
+ import { bundleSourcemapSpecifiers } from './bundle-sourcemap.js'
3
+
4
+ const indexes = new WeakMap()
5
+ const manifests = new WeakMap()
6
+
7
+ // Normalize only path segments. Keep sourcemap URL roots (webpack:/// etc.)
8
+ // and leading ../ segments, and never guess extensions or basename matches.
9
+ function normalizePath(path) {
10
+ const match = /^([a-z][a-z\d+.-]*:\/\/[^/]*)(.*)$/iu.exec(path)
11
+ const prefix = match?.[1] ?? ''
12
+ const rest = match?.[2] ?? path
13
+ const absolute = rest.startsWith('/')
14
+ const parts = []
15
+ for (const part of rest.split('/')) {
16
+ if (!part || part === '.') continue
17
+ if (part === '..' && parts.length > 0 && parts.at(-1) !== '..') parts.pop()
18
+ else if (part === '..' && absolute && parts.length === 0) return null
19
+ else parts.push(part)
20
+ }
21
+ return prefix + (absolute ? '/' : '') + parts.join('/')
22
+ }
23
+
24
+ function buildIndex(sources) {
25
+ const paths = new Map()
26
+ for (const path of sources.keys()) {
27
+ const normalized = normalizePath(path)
28
+ if (normalized === null) continue
29
+ // Distinct source keys with the same normalized path are ambiguous.
30
+ paths.set(normalized, paths.has(normalized) ? null : path)
31
+ }
32
+ return paths
33
+ }
34
+
35
+ function packageScope(sources, index, parent, packageDir) {
36
+ const normalized = normalizePath(parent)
37
+ if (normalized === null) return null
38
+ const boundary = typeof packageDir === 'string' ? normalizePath(`${packageDir}/package.json`) : null
39
+ let directory = normalized.slice(0, normalized.lastIndexOf('/') + 1)
40
+ let cache = manifests.get(sources)
41
+ if (!cache) { cache = new Map(); manifests.set(sources, cache) }
42
+ for (;;) {
43
+ // A dependency without a captured manifest cannot inherit the app's imports.
44
+ if (directory.endsWith('node_modules/')) return null
45
+ const path = `${directory}package.json`
46
+ if (index.has(path)) {
47
+ if (!cache.has(path)) {
48
+ const content = sources.get(index.get(path))
49
+ let imports = null
50
+ try { imports = typeof content === 'string' ? JSON.parse(content.replace(/^\uFEFF/u, ''))?.imports ?? null : null } catch {}
51
+ cache.set(path, imports && typeof imports === 'object' && !Array.isArray(imports) ? imports : null)
52
+ }
53
+ // The nearest package defines the scope, including absent/invalid imports.
54
+ return { directory, imports: cache.get(path) }
55
+ }
56
+ // Recorded module roots are package boundaries even without a manifest.
57
+ if (path === boundary) return null
58
+ if (!directory || directory === '/' || directory.endsWith('../') || /^[a-z][a-z\d+.-]*:\/\/[^/]*\/?$/iu.test(directory)) return null
59
+ const trimmed = directory.slice(0, -1)
60
+ directory = trimmed.slice(0, trimmed.lastIndexOf('/') + 1)
61
+ }
62
+ }
63
+
64
+ function importMapping(imports, specifier) {
65
+ if (!imports) return null
66
+ if (Object.hasOwn(imports, specifier) && !specifier.includes('*')) return { value: imports[specifier], wildcard: null }
67
+ let best = null
68
+ for (const pattern of Object.keys(imports)) {
69
+ const star = pattern.indexOf('*')
70
+ if (!pattern.startsWith('#') || star === -1 || pattern.indexOf('*', star + 1) !== -1) continue
71
+ const prefix = pattern.slice(0, star), suffix = pattern.slice(star + 1)
72
+ if (specifier.length <= prefix.length + suffix.length || !specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue
73
+ // Node's pattern precedence: longest prefix, then longest complete key.
74
+ if (!best || star > best.star || star === best.star && pattern.length > best.pattern.length) best = { pattern, star, suffix }
75
+ }
76
+ return best ? { value: imports[best.pattern], wildcard: specifier.slice(best.star, specifier.length - best.suffix.length) } : null
77
+ }
78
+
79
+ function importTargets(value, wildcard, depth = 0) {
80
+ if (depth > 16) return [null]
81
+ if (typeof value === 'string') return [wildcard === null ? value : value.replaceAll('*', wildcard)]
82
+ // The viewer combines recorded conditions. Conditional/array mappings link
83
+ // only when every possible target agrees; a blocked branch stays blocked.
84
+ if (value && typeof value === 'object') return Object.values(value).flatMap(target => importTargets(target, wildcard, depth + 1))
85
+ return [null]
86
+ }
87
+
88
+ function mappedSource(target, scope, sources, index, recorded, directory) {
89
+ if (typeof target !== 'string' || /[\\\p{Cc}]/u.test(target) || /%2f|%5c/iu.test(target)) return null
90
+ const relative = target.startsWith('./')
91
+ let file = null
92
+ if (relative) {
93
+ let decoded
94
+ try { decoded = decodeURIComponent(target.slice(2)) } catch { return null }
95
+ if (/[\\\p{Cc}]/u.test(decoded) || decoded.split('/').some(part => ['.', '..', 'node_modules'].includes(part.toLowerCase()))) return null
96
+ file = normalizePath(scope.directory + decoded)
97
+ } else if (target.startsWith('../') || target.startsWith('/') || target.startsWith('#') || target.includes(':')) return null
98
+ const keys = new Set([target])
99
+ // Captures may retain the package-relative rewritten spelling or express
100
+ // that same path relative to a nested importing file.
101
+ if (relative) {
102
+ for (const key of recorded?.keys() ?? []) {
103
+ if ((key.startsWith('./') || key.startsWith('../')) && normalizePath(directory + key) === file) keys.add(key)
104
+ }
105
+ }
106
+ let found = false, resolved = null
107
+ for (const key of keys) {
108
+ if (!recorded?.has(key)) continue
109
+ const current = recorded.get(key)
110
+ if (typeof current !== 'string' || !sources.has(current) || found && current !== resolved) return null
111
+ found = true
112
+ resolved = current
113
+ }
114
+ // Exact captured files are useful too, but never guess an extension/index.
115
+ return found ? resolved : relative ? index.get(file) ?? null : null
116
+ }
117
+
118
+ function resolvePackageImport(scope, specifier, sources, index, recorded, directory) {
119
+ const mapping = importMapping(scope?.imports, specifier)
120
+ if (!mapping) return null
121
+ const targets = importTargets(mapping.value, mapping.wildcard)
122
+ let result = null
123
+ for (const target of targets) {
124
+ const resolved = mappedSource(target, scope, sources, index, recorded, directory)
125
+ if (!resolved || result && resolved !== result) return null
126
+ result = resolved
127
+ }
128
+ return result
129
+ }
130
+
131
+ // Merge conditions before linking. When exporting a restricted source set,
132
+ // include only visible parents and replace every unavailable target with null:
133
+ // its path must not leak, and relative fallback must still remain blocked.
134
+ // A sourcemap's are what its edges say each specifier names (bundle-sourcemap.js).
135
+ export function bundleSourceImports(details, sources = bundleSourcesAsMap(details)) {
136
+ const imports = new Map()
137
+ if (details?.kind === 'stasis') {
138
+ for (const byParent of details.bundle?.imports?.values() ?? []) {
139
+ for (const [parent, specifiers] of byParent) {
140
+ if (!sources.has(parent)) continue
141
+ if (!imports.has(parent)) imports.set(parent, new Map())
142
+ const merged = imports.get(parent)
143
+ for (const [specifier, target] of specifiers) {
144
+ const resolved = typeof target === 'string' && sources.has(target) ? target : null
145
+ // A platform object, missing file, or disagreement under any
146
+ // condition permanently blocks this specifier, including fallback.
147
+ merged.set(specifier, merged.has(specifier) && merged.get(specifier) !== resolved ? null : resolved)
148
+ }
149
+ }
150
+ }
151
+ } else if (details?.kind === 'sourcemap') {
152
+ for (const [parent, specifiers] of bundleSourcemapSpecifiers(details)) {
153
+ if (!sources.has(parent)) continue
154
+ imports.set(parent, new Map([...specifiers].map(([specifier, target]) => [specifier, sources.has(target) ? target : null])))
155
+ }
156
+ }
157
+ return imports
158
+ }
159
+
160
+ // Managed previews receive only report-selected bodies, often without the
161
+ // owning manifest. Resolve their quoted # aliases using the full manifest
162
+ // inventory, but export only visible targets (or null), never manifest data.
163
+ // These are candidate strings; Prism still decides which tokens become links.
164
+ export function bundleSourceLinkImports(details, sources = bundleSourcesAsMap(details)) {
165
+ const imports = bundleSourceImports(details, sources)
166
+ if (details?.kind !== 'stasis') return imports
167
+ const packageDirs = bundlePackageDirs(details), packageFiles = bundleFilesAsMap(details)
168
+ for (const [parent, content] of sources) {
169
+ const resolve = sourceLinkResolver(sources, parent, imports, packageFiles, packageDirs)
170
+ let targets = imports.get(parent)
171
+ // Look ahead so a template containing a quoted alias cannot hide that
172
+ // inner candidate. Escaped/multiline strings are not portable paths.
173
+ for (const [, , specifier] of content.matchAll(/(?=(["'`])(#[^\\\r\n]*?)\1)/gu)) {
174
+ if (targets?.has(specifier)) continue
175
+ const target = resolve(specifier)
176
+ if (!targets) { targets = new Map(); imports.set(parent, targets) }
177
+ targets.set(specifier, target)
178
+ }
179
+ }
180
+ return imports
181
+ }
182
+
183
+ export function sourceLinkResolver(sources, parent, imports = null, packageFiles = sources, packageDirs = null) {
184
+ let index = indexes.get(sources)
185
+ if (!index) { index = buildIndex(sources); indexes.set(sources, index) }
186
+ let packageIndex = indexes.get(packageFiles)
187
+ if (!packageIndex) { packageIndex = buildIndex(packageFiles); indexes.set(packageFiles, packageIndex) }
188
+ const recorded = imports?.get(parent)
189
+ const directory = parent.slice(0, parent.lastIndexOf('/') + 1)
190
+ const scope = imports ? packageScope(packageFiles, packageIndex, parent, packageDirs?.get(parent)) : null
191
+ return (specifier) => {
192
+ if (typeof specifier !== 'string' || /\p{Cc}/u.test(specifier)) return null
193
+ if (recorded?.has(specifier)) {
194
+ const target = recorded.get(specifier)
195
+ return typeof target === 'string' && sources.has(target) ? target : null
196
+ }
197
+ if (specifier.startsWith('#')) return resolvePackageImport(scope, specifier, sources, index, recorded, directory)
198
+ if (!specifier.startsWith('./') && !specifier.startsWith('../')) return null
199
+ const name = specifier.slice(specifier.lastIndexOf('/') + 1)
200
+ const dot = name.lastIndexOf('.')
201
+ if (dot <= 0 || dot === name.length - 1) return null
202
+ return index.get(normalizePath(directory + specifier)) ?? null
203
+ }
204
+ }
205
+
206
+ const bundleLinks = new WeakMap()
207
+
208
+ export function bundleSourceLinkResolver(details, parent) {
209
+ const sources = bundleSourcesAsMap(details)
210
+ let links = bundleLinks.get(sources)
211
+ if (!links) {
212
+ links = { imports: bundleSourceImports(details, sources), packageDirs: bundlePackageDirs(details) }
213
+ bundleLinks.set(sources, links)
214
+ }
215
+ return sourceLinkResolver(sources, parent, links.imports, bundleFilesAsMap(details), links.packageDirs)
216
+ }
@@ -0,0 +1,10 @@
1
+ import type { SourceMap } from '@preventive/sourcemap'
2
+ import type { Edge } from '@preventive/sourcemap/edges.js'
3
+ import type { BundleDetails } from './bundle-metadata.js'
4
+ export function parseSourcemap(text: string): { json: { version: unknown; file: unknown; sourceRoot: unknown }; map: SourceMap; namesCount: number | null }
5
+ export function sourcemapEntries(details: BundleDetails): [string, string | null][]
6
+ export type SourcemapEdge = [from: string, to: string] | [from: string, to: string, specifier: string]
7
+ export function sourcemapEdges(map: SourceMap, read: (map: SourceMap) => { edges: Edge[] }): SourcemapEdge[]
8
+ export function bundleSourcemapEdges(details: BundleDetails): readonly SourcemapEdge[]
9
+ export function bundleSourcemapImports(details: BundleDetails): Map<string, Set<string>>
10
+ export function bundleSourcemapSpecifiers(details: BundleDetails): Map<string, Map<string, string>>
@@ -0,0 +1,100 @@
1
+ // A sourcemap bundle as @preventive/sourcemap reads it: plain or indexed,
2
+ // each file once, with the edges between its files, which the graph draws
3
+ // and Code links follow by the specifier of each import. A file keeps the key
4
+ // it had before the package read it, the spelling the map lists it under
5
+ // (`sourceRoot` in front), so Code links, hashes and reports that name one
6
+ // still find it.
7
+ //
8
+ // Edges take a parser for most maps. The client goes without one: it reads
9
+ // them with edges-lite.js, which knows Metro's maps alone. The server reads
10
+ // every map's with oxc-parser (server-managed/bundle-cache.ts) and keeps
11
+ // them in the bundle's metadata, which a client takes over its own.
12
+ import { readSourceMap } from '@preventive/sourcemap'
13
+ import { bundleEdges as metroEdges } from '@preventive/sourcemap/edges-lite.js'
14
+
15
+ // The map as read, and what the Overview shows of the JSON: its header,
16
+ // and how many names it maps. The JSON itself is not kept, its mappings
17
+ // read and its sources the map's files.
18
+ export function parseSourcemap(text) {
19
+ // The `)]}'` line that guards a map against being run, which readSourceMap
20
+ // skips in text it parses itself.
21
+ const json = JSON.parse(text.replace(/^\)\]\}'[^\n]*\n/u, ''))
22
+ const map = readSourceMap(json)
23
+ const namesCount = json.sections
24
+ ? json.sections.reduce((count, section) => count + (section.map.names?.length ?? 0), 0)
25
+ : json.names?.length ?? null
26
+ return { json: { version: json.version, file: json.file, sourceRoot: json.sourceRoot }, map, namesCount }
27
+ }
28
+
29
+ // The map's files as `[key, content or null]`, in its order. Details with
30
+ // no map read (the npm viewer's, a map's arrays as written) list what their
31
+ // `sources` does, a repeated path repeated.
32
+ export function sourcemapEntries(details) {
33
+ if (details.map) return details.map.files.flatMap((file) => file.source === null ? [] : [[file.source, file.content]])
34
+ const contents = details.json?.sourcesContent ?? []
35
+ return (details.json?.sources ?? []).map((path, i) => [path, contents[i] ?? null])
36
+ }
37
+
38
+ // `read`'s edges between files of the map, as rows by key: `[from, to]`,
39
+ // and `[from, to, specifier]` for an import its specifier names. An edge to
40
+ // no file of it (a package left out, a builtin) leads nowhere here, and one
41
+ // to its own file is none, which metadata refuses to hold.
42
+ export function sourcemapEdges(map, read) {
43
+ const rows = new Map()
44
+ for (const { from, to, specifier } of read(map).edges) {
45
+ if (from.source === null || to?.source == null || to.source === from.source) continue
46
+ const row = typeof specifier === 'string' ? [from.source, to.source, specifier] : [from.source, to.source]
47
+ rows.set(row.join('\0'), row)
48
+ }
49
+ return [...rows.values()]
50
+ }
51
+
52
+ // edges-lite.js throws for a map that is not Metro's, or one with no
53
+ // sourcesContent to read: one with no edges the client can tell.
54
+ function liteEdges(map) {
55
+ try { return metroEdges(map) } catch { return { edges: [] } }
56
+ }
57
+
58
+ const NO_EDGES = Object.freeze([])
59
+
60
+ // The edges a view reads: those the metadata carries, else what the client
61
+ // reads of its own map. None without a map.
62
+ export function bundleSourcemapEdges(details) {
63
+ if (details?.edges) return details.edges
64
+ if (!details?.map) return NO_EDGES
65
+ details.edges = sourcemapEdges(details.map, liteEdges)
66
+ return details.edges
67
+ }
68
+
69
+ const importsCache = new WeakMap()
70
+ const specifiersCache = new WeakMap()
71
+
72
+ // The graph's: `Map<from, Set<to>>`.
73
+ export function bundleSourcemapImports(details) {
74
+ const rows = bundleSourcemapEdges(details)
75
+ if (!importsCache.has(rows)) {
76
+ const imports = new Map()
77
+ for (const [from, to] of rows) {
78
+ if (!imports.has(from)) imports.set(from, new Set())
79
+ imports.get(from).add(to)
80
+ }
81
+ importsCache.set(rows, imports)
82
+ }
83
+ return importsCache.get(rows)
84
+ }
85
+
86
+ // Code links': the file each import's specifier names, `Map<from,
87
+ // Map<specifier, to>>`.
88
+ export function bundleSourcemapSpecifiers(details) {
89
+ const rows = bundleSourcemapEdges(details)
90
+ if (!specifiersCache.has(rows)) {
91
+ const specifiers = new Map()
92
+ for (const [from, to, specifier] of rows) {
93
+ if (specifier === undefined) continue
94
+ if (!specifiers.has(from)) specifiers.set(from, new Map())
95
+ specifiers.get(from).set(specifier, to)
96
+ }
97
+ specifiersCache.set(rows, specifiers)
98
+ }
99
+ return specifiersCache.get(rows)
100
+ }
@@ -0,0 +1,7 @@
1
+ import type { BundleDetails } from './bundle-metadata.js'
2
+ export function bundleSourcesAsMap(details: BundleDetails): Map<string, string>
3
+ export function bundleSourceLines(content: string): number
4
+ export function bundleSourceOrder(sources: Map<string, string>): { paths: string[]; numbers: Map<string, number> }
5
+ export function bundleFilesAsMap(details: BundleDetails): Map<string, string | { format: 'base64'; data: string }>
6
+ export function bundlePackageDirs(details: BundleDetails): Map<string, string> | null
7
+ export function bundlePackageVersions(details: BundleDetails, paths?: Iterable<string> | null, keyOf?: (dir: string, info: { name: string; version: string }) => string): Map<string, Set<string>>
@@ -0,0 +1,333 @@
1
+ // Shared helper: given a parsed bundle `details`, return a
2
+ // Map<file, content-string> of its source files. Stasis bundles
3
+ // expose sources via `@exodus/stasis-core`'s `Bundle.sources` getter
4
+ // (flat Map<projectRelPath, content> from `.modules`); sourcemaps list
5
+ // their files as bundle-sourcemap.js reads them. Sourcemap sources whose
6
+ // `sourcesContent[i]` was omitted are skipped.
7
+ //
8
+ // What a stasis bundle records is not all source: `Bundle.formats`
9
+ // marks each entry, and three of those formats are files only in the
10
+ // sense that they occupy a path. `Bundle.sources` hands back the raw
11
+ // stored content for every one of them, so the format is what has to
12
+ // be asked — the content's JS type cannot tell them apart:
13
+ //
14
+ // directory a readdir capture, content `["a.js","b.js"]`
15
+ // resource:base64 an image or font, content its base64 text
16
+ // resource raw bytes, the one case not a string already
17
+ //
18
+ // A directory capture is the one that does real damage, because it
19
+ // claims a path a directory also occupies: hand it to the terminal
20
+ // and `ls` lists the name twice and stops listing what is under it.
21
+ // The base64 resources are quieter but equally wrong — a PNG read as
22
+ // a wall of text, counted toward the language shares that
23
+ // render-bundle.js says resources are kept out of.
24
+ //
25
+ // Own module so `render-bundle.js` (Code tab, finding tree, graph
26
+ // data) and `terminal-attach.js` (in-shell FS) read one definition —
27
+ // a new bundle kind or field handled here is visible to both.
28
+
29
+ import { Bundle } from '@exodus/stasis-core/bundle'
30
+ import { sourcemapEntries } from './bundle-sourcemap.js'
31
+ import { utf8ByteLength } from './utf8.js'
32
+
33
+ const sourcesCache = new WeakMap()
34
+ const filesCache = new WeakMap()
35
+ const sizesCache = new WeakMap()
36
+ const sourceSizesCache = new WeakMap()
37
+ const kindsCache = new WeakMap()
38
+ const unsizedCache = new WeakMap()
39
+
40
+ export function bundleSourcesAsMap(details) {
41
+ if (details?.metadataOnly) return new Map()
42
+ const key = details?.bundle ?? details?.json
43
+ if (key && sourcesCache.has(key)) return sourcesCache.get(key)
44
+ const result = new Map()
45
+ if (!details) return result
46
+ if (details.kind === 'stasis') {
47
+ if (!details.bundle) return result
48
+ const formats = details.bundle.formats
49
+ for (const [file, content] of details.bundle.sources) {
50
+ if (typeof content !== 'string') continue
51
+ // `isResourceFormat` is the package's own list, so a format it
52
+ // adds later is excluded here without this needing to know. A v0
53
+ // bundle records no formats at all: `get` returns undefined,
54
+ // which is not a resource format, and every entry is kept as
55
+ // before.
56
+ if (Bundle.isResourceFormat(formats?.get(file))) continue
57
+ result.set(file, content)
58
+ }
59
+ } else if (details.kind === 'sourcemap') {
60
+ for (const [path, content] of sourcemapEntries(details)) {
61
+ if (typeof content === 'string') result.set(path, content)
62
+ }
63
+ }
64
+ if (key) sourcesCache.set(key, result)
65
+ return result
66
+ }
67
+
68
+ // A source's lines as the viewer numbers them: one more than its newlines.
69
+ export function bundleSourceLines(content) {
70
+ let lines = 1
71
+ for (let at = content.indexOf('\n'); at !== -1; at = content.indexOf('\n', at + 1)) lines++
72
+ return lines
73
+ }
74
+
75
+ // A bundle's sources in path order, and each one's number in it from 1: how a
76
+ // managed Code link names a file. The content hash that names a bundle fixes
77
+ // its sources, so the numbers stay put.
78
+ const sourceOrders = new WeakMap()
79
+ export function bundleSourceOrder(sources) {
80
+ let order = sourceOrders.get(sources)
81
+ if (!order) {
82
+ const paths = [...sources.keys()].toSorted()
83
+ order = { paths, numbers: new Map(paths.map((path, index) => [path, index + 1])) }
84
+ sourceOrders.set(sources, order)
85
+ }
86
+ return order
87
+ }
88
+
89
+ // Every file the bundle carries, for a reader that wants the filesystem
90
+ // rather than the source: what `bundleSourcesAsMap` returns, plus the
91
+ // resources it leaves out, each in the form that is true to it.
92
+ //
93
+ // The two resource formats are not two encodings of the same thing.
94
+ // Stasis picks between them by whether the bytes are valid UTF-8:
95
+ // `resource` is that text, stored verbatim, so it stays a string and a
96
+ // reader sees the SVG or the licence file it is. `resource:base64` is
97
+ // the other case — bytes no string spells — and it is handed over
98
+ // still spelt that way, as `{ format: 'base64', data }`. The terminal
99
+ // takes that form since 2.0 and decodes it the first time the file is
100
+ // read, so a bundle carrying a hundred images costs a hundred strings
101
+ // rather than a hundred decoded buffers, and pays for the one that is
102
+ // opened. Either way it knows what it is holding: `ls -l` and `wc -c`
103
+ // report the real byte count rather than the length of the base64, and
104
+ // `cat` declines instead of printing mojibake.
105
+ //
106
+ // Decoding there also puts the diagnostic where the file is named: a
107
+ // spelling that does not decode is reported by the command that read
108
+ // it, under the detail `base64 source`. Decoding here could only drop
109
+ // the file and say nothing.
110
+ //
111
+ // Directory captures stay out. They are the one entry that is not a
112
+ // file in any encoding, and mounting one puts a second `lib` beside
113
+ // the directory it names.
114
+ // Files as the managed server reads a tarball's (server-managed/npm-packages.ts
115
+ // npmFileRows), `[path, bytes, text]` or `[path, bytes, null, digest]`, as
116
+ // details of `entry` in the shape of a sourcemap carrying its sources: one
117
+ // source per file, text inline, and every file's size from its bytes, so a
118
+ // file that is not text still lists with its size. Those carry a digest of
119
+ // their bytes instead (`npmBinaries`), which Compare compares them by. An npm
120
+ // version's files come so (view/npm-package.js), and a repository
121
+ // directory's (view/repository-code.js).
122
+ export function fileRowDetails(entry, rows) {
123
+ const files = Array.isArray(rows) ? rows : []
124
+ return {
125
+ integrity: entry.integrity, kind: 'sourcemap', size: entry.size,
126
+ json: { version: 3, sources: files.map(([path]) => path), sourcesContent: files.map(([, , text]) => typeof text === 'string' ? text : null) },
127
+ fileSizes: new Map(files.map(([path, size]) => [path, size])),
128
+ npmBinaries: new Map(files.filter(([, , text, digest]) => typeof text !== 'string' && typeof digest === 'string')
129
+ .map(([path, size, , digest]) => [path, { size, digest }])),
130
+ }
131
+ }
132
+
133
+ export function bundleFilesAsMap(details) {
134
+ const key = details?.bundle ?? details?.json
135
+ if (key && filesCache.has(key)) return filesCache.get(key)
136
+ const files = new Map(bundleSourcesAsMap(details))
137
+ // An npm package's files that are not text (fileRowDetails) carry
138
+ // their size and digest alone: enough for Compare to tell a changed one
139
+ // from an unchanged one, as `{ format: 'digest', size, digest }`.
140
+ for (const [path, file] of details?.npmBinaries ?? []) files.set(path, { format: 'digest', ...file })
141
+ if (details?.kind === 'stasis' && details.bundle) {
142
+ const formats = details.bundle.formats
143
+ for (const [file, content] of details.bundle.sources) {
144
+ if (typeof content !== 'string') continue
145
+ const format = formats?.get(file)
146
+ if (format === 'resource') files.set(file, content)
147
+ else if (format === 'resource:base64') files.set(file, { format: 'base64', data: content })
148
+ }
149
+ }
150
+ if (key) filesCache.set(key, files)
151
+ return files
152
+ }
153
+
154
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
155
+
156
+ // The length of the bytes a base64 spelling decodes to — three for every
157
+ // four characters, less what the padding stands for — or null when it
158
+ // decodes to none. The terminal decodes strictly (`@exodus/bytes`): RFC
159
+ // 4648's alphabet, no whitespace, padding present or left off but never
160
+ // wrong, and no stray bits in the last character. A spelling it refuses is
161
+ // a file `wc -c` cannot read, so it gets no size here rather than an
162
+ // invented one. Checked without decoding: a bundle's images are not worth
163
+ // a buffer each just to be weighed.
164
+ function base64ByteLength(text) {
165
+ const padded = text.endsWith('=')
166
+ if (padded ? text.length % 4 !== 0 || text.at(-3) === '=' : text.length % 4 === 1) return null
167
+ const data = padded ? text.slice(0, text.endsWith('==') ? -2 : -1) : text
168
+ if (/[^A-Za-z0-9+/]/u.test(data)) return null
169
+ // A trailing group of 2 or 3 characters spells 1 or 2 bytes; the bits of
170
+ // its last character past those bytes must be zero.
171
+ const tail = data.length % 4
172
+ if (tail > 1 && BASE64_ALPHABET.indexOf(data.at(-1)) & (tail === 2 ? 0x0f : 0x03)) return null
173
+ return Math.floor(data.length * 3 / 4)
174
+ }
175
+
176
+ // The byte size of one entry of `bundleFilesAsMap`, as `wc -c` reports it
177
+ // in the terminal: text weighs the UTF-8 it encodes to, and a base64
178
+ // declaration the bytes it decodes to — null when it decodes to none.
179
+ // Anything else is no file, and null too. The Compare tab sizes both
180
+ // bundles' files by this, so its totals are the Overview's.
181
+ export function bundleFileByteLength(content) {
182
+ if (typeof content === 'string') return utf8ByteLength(content)
183
+ if (content?.format === 'digest') return content.size
184
+ return content?.format === 'base64' ? base64ByteLength(content.data) : null
185
+ }
186
+
187
+ // Every path the bundle records, keyed to the byte size of the file it
188
+ // holds — the size `wc -c` reports for it in the terminal, which is to
189
+ // say the size of the entry in `bundleFilesAsMap`: a source or `resource`
190
+ // weighs the UTF-8 its text encodes to, a `resource:base64` the bytes its
191
+ // base64 decodes to. Null is a path with no size to give: a directory
192
+ // capture, which is no file; a sourcemap source whose body was left out;
193
+ // or a `resource:base64` whose spelling does not decode.
194
+ //
195
+ // The Overview and the Treemap weigh a bundle by this, so what they show
196
+ // adds up to what `du` does. Neither the source-only view nor the base64
197
+ // text will do: the first drops every image and font, the second
198
+ // inflates them by a third.
199
+ //
200
+ // Metadata views need byte sizes and paths, never the bodies. The
201
+ // persistent index supplies this map directly; full parses compute it once.
202
+ export function bundleFileSizes(details) {
203
+ if (details?.fileSizes) return details.fileSizes
204
+ const key = details?.bundle ?? details?.json
205
+ if (key && sizesCache.has(key)) return sizesCache.get(key)
206
+ const sizes = new Map()
207
+ const files = bundleFilesAsMap(details)
208
+ const paths = details?.kind === 'stasis' ? details.bundle?.sources.keys() : details && sourcemapEntries(details).map(([path]) => path)
209
+ for (const path of paths ?? []) sizes.set(path, bundleFileByteLength(files.get(path)))
210
+ if (key) sizesCache.set(key, sizes)
211
+ return sizes
212
+ }
213
+
214
+ // The files the terminal mounts that have no size: a `resource:base64`
215
+ // whose spelling does not decode. The terminal lists such a file, and says
216
+ // why it cannot read it when asked, so the views list it too — but a null
217
+ // size alone cannot tell it from an entry that is no file at all, so it is
218
+ // named here. A metadata-only open reads the set from the index, which is
219
+ // the one thing a size and a format cannot say about a path.
220
+ export function bundleUnsizedFiles(details) {
221
+ if (details?.metadataOnly) return details.unsizedFiles ?? new Set()
222
+ const sizes = bundleFileSizes(details)
223
+ if (unsizedCache.has(sizes)) return unsizedCache.get(sizes)
224
+ const unsized = new Set()
225
+ for (const path of bundleFilesAsMap(details).keys()) if (sizes.get(path) === null) unsized.add(path)
226
+ unsizedCache.set(sizes, unsized)
227
+ return unsized
228
+ }
229
+
230
+ // What each file the terminal mounts is, for a view that lists files:
231
+ // 'source', or 'resource' for an image, font or other asset — a file, sized
232
+ // like any other, but none the source viewer can show. Its keys are
233
+ // `bundleFilesAsMap`'s, and nothing else: a directory capture, or an entry
234
+ // with no body to mount (a sourcemap source whose `sourcesContent` was left
235
+ // out, a body that is no string), is no file and is absent. It reads sizes
236
+ // and formats, not bodies, so it answers for a metadata-only open as it
237
+ // does for a parsed one: a sized path is a mounted file, and so is one
238
+ // `bundleUnsizedFiles` names.
239
+ export function bundleFileKinds(details) {
240
+ const sizes = bundleFileSizes(details)
241
+ if (kindsCache.has(sizes)) return kindsCache.get(sizes)
242
+ const unsized = bundleUnsizedFiles(details)
243
+ const formats = details?.kind === 'stasis' ? details.bundle?.formats : null
244
+ const kinds = new Map()
245
+ for (const [path, size] of sizes) {
246
+ if (size === null && !unsized.has(path)) continue
247
+ // An npm package's file that is not text is a resource too: no source to view.
248
+ kinds.set(path, Bundle.isResourceFormat(formats?.get(path)) || details?.npmBinaries?.has(path) ? 'resource' : 'source')
249
+ }
250
+ kindsCache.set(sizes, kinds)
251
+ return kinds
252
+ }
253
+
254
+ // Source paths and their recorded byte sizes. Keep null for a source whose
255
+ // body is missing, but omit resources and directory captures entirely so
256
+ // graph consumers can distinguish unknown source bytes from non-source.
257
+ export function bundleSourceSizes(details) {
258
+ const sizes = bundleFileSizes(details)
259
+ const formats = details?.kind === 'stasis' ? details.bundle?.formats : null
260
+ if (!formats || formats.size === 0) return sizes
261
+ if (sourceSizesCache.has(sizes)) return sourceSizesCache.get(sizes)
262
+ const result = new Map()
263
+ for (const [path, size] of sizes) if (!Bundle.isResourceFormat(formats.get(path))) result.set(path, size)
264
+ sourceSizesCache.set(sizes, result)
265
+ return result
266
+ }
267
+
268
+ // Map each stasis bundle source path to the package directory that
269
+ // owns it. A stasis `Bundle` already records authoritative package
270
+ // boundaries in `.modules` (a `Map<dir, { name, version, files }>`
271
+ // covering both `node_modules` deps and workspace packages — the
272
+ // PHP `vendor/<vendor>/<pkg>` case, monorepo workspaces, etc.); this
273
+ // mirrors the `Bundle.sources` getter's path construction (`dir/rel`,
274
+ // or just `rel` for the `.` root) so the returned keys line up exactly
275
+ // with `bundleSourcesAsMap`'s.
276
+ //
277
+ // The package views (overview, treemap, graph) otherwise classify
278
+ // paths with `bundlePkgOf`'s string heuristic, which only knows
279
+ // `node_modules/`/`dependencies/` and treats everything else as own
280
+ // source. Feeding each path's recorded module directory into
281
+ // `bundlePkgOf` (`packageDir` option) keeps those packages separate.
282
+ //
283
+ // Returns null for sourcemap bundles (and anything without parsed
284
+ // modules) — they carry no package metadata, so callers fall back to
285
+ // the path heuristic alone. Kept by the bundle, as its sources are:
286
+ // callers only read it.
287
+ const packageDirsCache = new WeakMap()
288
+ export function bundlePackageDirs(details) {
289
+ if (details?.kind !== 'stasis' || !details.bundle?.modules) return null
290
+ if (packageDirsCache.has(details.bundle)) return packageDirsCache.get(details.bundle)
291
+ const map = new Map()
292
+ for (const [dir, info] of details.bundle.modules) {
293
+ for (const rel of Object.keys(info.files)) {
294
+ map.set(dir === '.' ? rel : `${dir}/${rel}`, dir)
295
+ }
296
+ }
297
+ packageDirsCache.set(details.bundle, map)
298
+ return map
299
+ }
300
+
301
+ // Map each named npm dependency a (stasis) bundle carries to the set of
302
+ // concrete versions present for it: `Map<name, Set<version>>`. Only
303
+ // `node_modules/...` modules that carry both a name and a non-empty
304
+ // version string count — stasis v1 `scope: 'full'` bundles merge
305
+ // workspace / own-source entries into the same `Bundle.modules` Map
306
+ // (often `version: '0.0.0'` or null), and those aren't upstream
307
+ // dependencies, so they're filtered out by the directory key. A package
308
+ // can map to more than one version (pnpm keeps duplicate majors side by
309
+ // side), hence the Set.
310
+ //
311
+ // Shared by the Advisories tab (a bulk registry lookup keyed on these
312
+ // pairs) and the Compare slide (the dependency version-update diff).
313
+ // Returns an empty Map for sourcemaps and v0 stasis bundles — neither
314
+ // carries per-module version metadata. `keyOf(dir, info)` names each
315
+ // entry, the recorded name by default; Compare keys by install directory,
316
+ // as its sizes are, so an npm alias (`node_modules/alias` recording
317
+ // `actual-package`) joins its own sizes.
318
+ export function bundlePackageVersions(details, paths = null, keyOf = (_dir, info) => info.name) {
319
+ const versions = new Map()
320
+ if (details?.kind !== 'stasis' || !details.bundle?.modules) return versions
321
+ const packageDirs = paths === null ? null : bundlePackageDirs(details)
322
+ const selectedDirs = paths === null ? null : new Set([...paths].map(path => packageDirs?.get(path)))
323
+ for (const [dir, info] of details.bundle.modules) {
324
+ if (selectedDirs && !selectedDirs.has(dir)) continue
325
+ if (!dir.includes('node_modules')) continue
326
+ if (!info?.name || typeof info.version !== 'string' || !info.version) continue
327
+ const key = keyOf(dir, info)
328
+ let set = versions.get(key)
329
+ if (!set) { set = new Set(); versions.set(key, set) }
330
+ set.add(info.version)
331
+ }
332
+ return versions
333
+ }