@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41

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 (312) 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 +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -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 +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,65 @@
1
+ import { bundleFilesAsMap } from './bundle-sources.js'
2
+
3
+ // Use the union of named trees. A tree absent from one stasis bundle contains
4
+ // no files there; falling back to all files would invent additions/removals.
5
+ // A bundle that names no trees (a sourcemap) is compared whole under any of
6
+ // them. Unlike a single-bundle graph, retain a scope that includes every file
7
+ // on one side.
8
+ export function bundleCompareScopes(...details) {
9
+ const names = new Set()
10
+ for (const item of details) {
11
+ const files = bundleFilesAsMap(item)
12
+ for (const [name, paths] of Object.entries(item?.bundle?.reason ?? {})) {
13
+ if (Array.isArray(paths) && paths.some(path => files.has(path))) names.add(name)
14
+ }
15
+ }
16
+ // A scope is only useful as a filter if it changes the files compared on
17
+ // some side. Keep every scope when at least one does, otherwise hide the
18
+ // selector, as single-bundle views do (see common/bundle-reasons.js).
19
+ const narrows = name => details.some(item => {
20
+ if (item?.kind !== 'stasis') return false
21
+ const files = bundleFilesAsMap(item)
22
+ const paths = item?.bundle?.reason?.[name]
23
+ return new Set(Array.isArray(paths) ? paths.filter(path => files.has(path)) : []).size < files.size
24
+ })
25
+ if (![...names].some(narrows)) return []
26
+ return [...names].toSorted().map(name => ({ id: `reason:${name}`, label: name }))
27
+ }
28
+
29
+ export function bundleCompareFiles(details, scope = '') {
30
+ const files = bundleFilesAsMap(details)
31
+ if (!scope || details?.kind !== 'stasis') return files
32
+ const paths = details?.bundle?.reason?.[scope.replace(/^reason:/u, '')]
33
+ const selected = new Set(Array.isArray(paths) ? paths : [])
34
+ return new Map([...files].filter(([path]) => selected.has(path)))
35
+ }
36
+
37
+ // A resolution's identity: its importer, specifier, conditions (import
38
+ // attributes included), and Metro platform.
39
+ export function resolutionKey(parent, specifier, conditions, platform) {
40
+ return JSON.stringify([parent, specifier, conditions, platform])
41
+ }
42
+
43
+ // Keep the full resolution identity: collapsing to graph edges would lose
44
+ // specifier, condition/import-attribute, and Metro platform changes.
45
+ export function bundleCompareResolutions(details, scope = '') {
46
+ const result = new Map()
47
+ if (details?.kind !== 'stasis' || !details.bundle) return result
48
+ const files = scope ? bundleFilesAsMap(details) : null
49
+ const selected = scope ? bundleCompareFiles(details, scope) : null
50
+ for (const [conditions, byParent] of details.bundle.imports) {
51
+ for (const [parent, specifiers] of byParent) {
52
+ for (const [specifier, resolved] of specifiers) {
53
+ const targets = typeof resolved === 'string' ? [[null, resolved]] : resolved
54
+ for (const [platform, target] of targets) {
55
+ // Include imports from scoped files, plus imports into the scope
56
+ // from uncaptured parents (e.g. an app in a dependencies-only bundle).
57
+ if (selected && !selected.has(parent) && (files.has(parent) || !selected.has(target))) continue
58
+ const key = resolutionKey(parent, specifier, conditions, platform)
59
+ result.set(key, { key, parent, specifier, conditions, platform, target })
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return result
65
+ }
@@ -0,0 +1,14 @@
1
+ import type { SourceMap } from '@preventive/sourcemap'
2
+ import type { SourcemapEdge } from './bundle-sourcemap.js'
3
+ export const BUNDLE_METADATA_VERSION: number
4
+ export const FILE_HASH: RegExp
5
+ export interface BundleIdentity { integrity: string; kind: string | null; size: number }
6
+ export interface BundleDetails extends BundleIdentity { bundle?: unknown; json?: unknown; map?: SourceMap; edges?: readonly SourcemapEdge[] }
7
+ export function parseBundleContents(text: string, identity: BundleIdentity): BundleDetails
8
+ export interface BundleMetadata extends Record<string, unknown> {
9
+ files: [string, number | null, string | null, number | null][]
10
+ codeStats: { files: number; lines: number; bytes: number }
11
+ }
12
+ export function createBundleMetadata(details: BundleDetails): Promise<BundleMetadata>
13
+ export function parseBundleMetadata(data: unknown, integrity: string): BundleDetails
14
+ export function createBundleSummary(details: BundleDetails, metadata?: BundleMetadata): { files: number; codeFiles: number; lines: number; commit?: string; stasisVersion?: number }
@@ -0,0 +1,226 @@
1
+ import { bundleCodeStats } from './bundle-stats.js'
2
+ import { bundleCommitHash } from './bundle-commit.js'
3
+ import { Bundle } from '@exodus/stasis-core/bundle'
4
+ import { computeFileHash } from '@preventive/report'
5
+ import { utf8ByteLength } from './utf8.js'
6
+ import { bundleFileSizes, bundleSourcesAsMap, bundleUnsizedFiles } from './bundle-sources.js'
7
+ import { bundleSourcemapEdges, parseSourcemap, sourcemapEntries } from './bundle-sourcemap.js'
8
+
9
+ // Version 2 sizes every file by its bytes, resources included. A version 1
10
+ // index sized by what the source map held when it was written: before the
11
+ // map asked each entry's format, that took a directory capture for a file
12
+ // and a base64 resource for its spelling; after, it left every resource
13
+ // out. Either way its sizes are not file sizes, so it is read for its
14
+ // hashes alone and rebuilt on open.
15
+ // Version 3 also retains the bundle's repository and package origin metadata.
16
+ // Version 4 retains dependency repositories as well.
17
+ // Version 5 counts lines of code, leaving blank lines out.
18
+ // Version 6 reads a sourcemap whole (bundle-sourcemap.js): an index map's
19
+ // files too, each once, keyed with `sourceRoot` in front, and the edges
20
+ // between them.
21
+ export const BUNDLE_METADATA_VERSION = 6
22
+ const INDEX_VERSION = BUNDLE_METADATA_VERSION
23
+ const hashJobs = new WeakMap()
24
+ const SOURCE_TABS = new Set(['terminal', 'code', 'search', 'compare'])
25
+
26
+ export function bundleNeedsSources(tab, sourceFile = null) {
27
+ return Boolean(sourceFile) || SOURCE_TABS.has(tab)
28
+ }
29
+
30
+ // A file's hash as metadata names it: computeFileHash's sha512 of its text.
31
+ export const FILE_HASH = /^sha512-[A-Za-z0-9+/]{86}==$/u
32
+
33
+ // Bound outstanding WebCrypto jobs: serial awaits incur one task round-trip
34
+ // per file, while an unbounded Promise.all holds every encoded body at once.
35
+ export function computeBundleFileHashes(details) {
36
+ if (details.fileHashes) return Promise.resolve(details.fileHashes)
37
+ if (hashJobs.has(details)) return hashJobs.get(details)
38
+ const job = (async () => {
39
+ const entries = [...bundleSourcesAsMap(details)]
40
+ const result = new Map()
41
+ for (let i = 0; i < entries.length; i += 32) {
42
+ const hashes = await Promise.all(entries.slice(i, i + 32).map(async ([file, content]) => [file, await computeFileHash(content)]))
43
+ for (const [file, hash] of hashes) result.set(file, hash)
44
+ }
45
+ details.fileHashes = result
46
+ return result
47
+ })()
48
+ hashJobs.set(details, job)
49
+ job.catch(() => hashJobs.delete(details))
50
+ return job
51
+ }
52
+
53
+ // Count lines of code: lines with anything but whitespace on them, comments
54
+ // included for now, split at \r\n, \r or \n. Each such line is counted at
55
+ // its first non-whitespace character, its rest passed over to the next line
56
+ // break: no match runs a line's length, which past some 9 million characters
57
+ // (a bundled payload's one line, as Shai-Hulud's 10 MB bun_environment.js)
58
+ // overflows the stack a Unicode regular expression backtracks with in a
59
+ // string holding any character past U+00FF.
60
+ // The source map intentionally excludes non-text resources, so a null entry
61
+ // remains the metadata marker for images, fonts, and other binary payloads.
62
+ export function bundleSourceLineCount(content) {
63
+ if (typeof content !== 'string') return 0
64
+ const code = /\S/gu, end = /[\r\n]/gu
65
+ let lines = 0
66
+ while (code.test(content)) {
67
+ lines++
68
+ end.lastIndex = code.lastIndex
69
+ if (!end.test(content)) break
70
+ code.lastIndex = end.lastIndex
71
+ }
72
+ return lines
73
+ }
74
+
75
+ // Each source's lines of code, by path: the index's where `details` has them,
76
+ // else counted once and kept on it. `bundleSourcesAsMap` holds only textual
77
+ // sources: Stasis resources (images, fonts, and other binary payloads) have
78
+ // no lines, so they cannot distort the language shares.
79
+ export function bundleLineCounts(details) {
80
+ return details.lineCounts ??= new Map([...bundleSourcesAsMap(details)].map(([path, content]) => [path, bundleSourceLineCount(content)]))
81
+ }
82
+
83
+ function mapObject(value) {
84
+ return value instanceof Map ? Object.fromEntries([...value].map(([key, v]) => [key, mapObject(v)])) : value
85
+ }
86
+
87
+ // A private, versioned derivative of the bundle, not a Stasis lock. Source
88
+ // bodies are omitted; file sizes and hashes keep metadata views self-contained.
89
+ // A row is `[path, bytes, hash, lines]`: a resource has bytes but no hash or
90
+ // lines, since it is no source; a directory capture has none of the three.
91
+ // `unsized` names the mounted files with no bytes to give (a base64 spelling
92
+ // that does not decode), which a null size alone cannot tell from no file.
93
+ export async function createBundleMetadata(details) {
94
+ const hashes = await computeBundleFileHashes(details)
95
+ const sizes = bundleFileSizes(details)
96
+ const sourceLines = bundleLineCounts(details)
97
+ const result = { version: INDEX_VERSION, integrity: details.integrity, kind: details.kind, size: details.size, codeStats: bundleCodeStats(sourceLines, sizes),
98
+ files: [...sizes].map(([path, size]) => [path, size, hashes.get(path) ?? null, sourceLines.get(path) ?? null]) }
99
+ const unsized = bundleUnsizedFiles(details)
100
+ if (unsized.size > 0) result.unsized = [...unsized]
101
+ if (details.kind === 'sourcemap') {
102
+ const { version, file, sourceRoot, names } = details.json
103
+ const entries = sourcemapEntries(details)
104
+ result.json = { version, file, sourceRoot, sources: entries.map(([path]) => path) }
105
+ // A map's arrays as written can repeat a path with different/absent
106
+ // content. Keep the Overview's positional inventory, alongside the
107
+ // path-keyed graph index.
108
+ result.sourceSizes = entries.map(([, content]) => typeof content === 'string' ? utf8ByteLength(content) : null)
109
+ result.namesCount = details.namesCount ?? names?.length ?? null
110
+ // `[from, to]` or `[from, to, specifier]`, `from` and `to` rows of `files`.
111
+ const rows = new Map(result.files.map(([path], i) => [path, i]))
112
+ result.edges = bundleSourcemapEdges(details).map(([from, to, ...specifier]) => [rows.get(from), rows.get(to), ...specifier])
113
+ } else {
114
+ const b = details.bundle
115
+ const bundle = { version: b.version, config: b.config, repo: b.repo, package: b.package, formats: mapObject(b.formats), imports: mapObject(b.imports), reason: b.reason, executable: [...b.executable] }
116
+ if (b.version === 0) {
117
+ bundle.sources = Object.fromEntries([...sizes.keys()].map((path) => [path, null]))
118
+ } else {
119
+ const modules = {}, sources = {}
120
+ for (const [dir, info] of b.modules) {
121
+ const target = dir.split('/').includes('node_modules') ? modules : sources
122
+ Object.defineProperty(target, dir, { enumerable: true, value: {
123
+ name: info.name, version: info.version, ecosystem: info.ecosystem, repo: info.repo,
124
+ files: Object.fromEntries(Object.keys(info.files).map((path) => [path, null])),
125
+ } })
126
+ }
127
+ bundle.modules = modules
128
+ if (b.config.scope === 'full') { bundle.sources = sources; bundle.entries = [...b.entries] }
129
+ }
130
+ result.bundle = bundle
131
+ }
132
+ return result
133
+ }
134
+
135
+ // Catalogs need counts without the per-file inventory. Match scan inputs:
136
+ // textual resources count as files (with zero LoC); binary resources do not
137
+ // count as Code inputs, and missing source bodies/directories are not files.
138
+ export function createBundleSummary(details, metadata) {
139
+ const files = (metadata?.files ?? [...bundleFileSizes(details)]).filter(([, size]) => size !== null)
140
+ const formats = details.kind === 'stasis' ? details.bundle.formats : null
141
+ const lines = metadata?.codeStats.lines ?? [...bundleSourcesAsMap(details).values()].reduce((sum, source) => sum + bundleSourceLineCount(source), 0)
142
+ const commit = details.kind === 'stasis' ? bundleCommitHash(details.bundle.repo?.commit) : null
143
+ return { files: files.length, lines,
144
+ codeFiles: files.filter(([path]) => !['resource:base64', 'directory'].includes(formats?.get(path))).length,
145
+ ...(commit ? { commit } : {}),
146
+ // The Stasis format version: 0 for legacy bundles, which record no package versions.
147
+ ...(details.kind === 'stasis' ? { stasisVersion: details.bundle.version } : {}) }
148
+ }
149
+
150
+ // `stale` marks an index this version did not write: its hashes still
151
+ // answer report lookups, but an open rebuilds it for current sizes, line
152
+ // counts and origin metadata.
153
+ export function parseBundleMetadata(data, integrity) {
154
+ if (![1, 2, 3, 4, 5, INDEX_VERSION].includes(data?.version) || data.integrity !== integrity || !['stasis', 'sourcemap'].includes(data.kind)
155
+ || !Number.isSafeInteger(data.size) || data.size < 0 || !Array.isArray(data.files)) throw new Error('Invalid bundle metadata')
156
+ const stale = data.version !== INDEX_VERSION
157
+ const legacySizes = data.version === 1
158
+ const fileHashes = new Map(), fileSizes = new Map(), lineCounts = new Map()
159
+ for (const row of data.files) {
160
+ // Version 1 rows predate the line count, and gave every sized entry a hash.
161
+ if (!Array.isArray(row) || (row.length !== 4 && !(legacySizes && row.length === 3))) throw new Error('Invalid bundle metadata file')
162
+ const [path, size, hash, lines] = row
163
+ if (typeof path !== 'string' || fileSizes.has(path) || (size !== null && (!Number.isSafeInteger(size) || size < 0))
164
+ || (hash !== null && (typeof hash !== 'string' || !FILE_HASH.test(hash)))
165
+ || (size === null && hash !== null) || (legacySizes && size !== null && hash === null)
166
+ || (lines !== undefined && lines !== null && (!Number.isSafeInteger(lines) || lines < 0))) throw new Error('Invalid bundle metadata file')
167
+ fileSizes.set(path, size)
168
+ if (hash !== null) fileHashes.set(path, hash)
169
+ if (lines !== undefined && lines !== null) lineCounts.set(path, lines)
170
+ }
171
+ // Only a sizeless row can name a mounted file with no size, and only once.
172
+ const unsized = new Set(data.unsized ?? [])
173
+ if ((data.unsized !== undefined && (legacySizes || !Array.isArray(data.unsized) || unsized.size !== data.unsized.length))
174
+ || [...unsized].some((path) => typeof path !== 'string' || !fileSizes.has(path) || fileSizes.get(path) !== null)) throw new Error('Invalid bundle metadata')
175
+ const details = { integrity, kind: data.kind, size: data.size, metadataOnly: true, fileSizes, fileHashes, lineCounts, unsizedFiles: unsized, stale }
176
+ if (data.kind === 'stasis') {
177
+ details.bundle = Bundle.parse(JSON.stringify(data.bundle))
178
+ const paths = details.bundle.sources
179
+ if (paths.size !== fileSizes.size || [...paths.keys()].some((path) => !fileSizes.has(path))) throw new Error('Invalid bundle metadata inventory')
180
+ // A file is hashed exactly when it is source: a resource has bytes and
181
+ // no hash. And only a base64 resource can be mounted without a size.
182
+ // Both are checked against the formats, which only the bundle carries.
183
+ const formats = details.bundle.formats
184
+ if (!legacySizes && [...fileSizes].some(([path, size]) => fileHashes.has(path) !== (size !== null && !Bundle.isResourceFormat(formats.get(path))))) {
185
+ throw new Error('Invalid bundle metadata inventory')
186
+ }
187
+ if ([...unsized].some((path) => formats.get(path) !== 'resource:base64')) throw new Error('Invalid bundle metadata inventory')
188
+ } else {
189
+ if (!data.json || typeof data.json !== 'object' || ![null, undefined].includes(data.namesCount) && (!Number.isSafeInteger(data.namesCount) || data.namesCount < 0)) throw new Error('Invalid sourcemap metadata')
190
+ if (!Array.isArray(data.json.sources) || data.json.sources.some((path) => !fileSizes.has(path))
191
+ || !Array.isArray(data.sourceSizes) || data.sourceSizes.length !== data.json.sources.length
192
+ || data.sourceSizes.some((size) => size !== null && (!Number.isSafeInteger(size) || size < 0))
193
+ || [...fileSizes].some(([path, size]) => fileHashes.has(path) !== (size !== null)) || unsized.size > 0) throw new Error('Invalid sourcemap inventory')
194
+ details.json = data.json
195
+ details.sourceSizes = data.sourceSizes
196
+ details.namesCount = data.namesCount
197
+ // An index before version 6 has none: the view reads no edges for it.
198
+ if (data.edges !== undefined) details.edges = parseSourcemapEdges(data.edges, data.files)
199
+ }
200
+ if (data.codeStats === undefined) details.codeStats = bundleCodeStats(lineCounts, fileSizes)
201
+ else {
202
+ const stats = data.codeStats
203
+ const validCount = value => Number.isSafeInteger(value) && value >= 0
204
+ if (!stats || ![stats.files, stats.lines, stats.bytes].every(validCount) || !Array.isArray(stats.languages)
205
+ || stats.languages.some(row => !row || typeof row.key !== 'string' || typeof row.label !== 'string'
206
+ || ![row.files, row.lines, row.bytes].every(validCount))) throw new Error('Invalid bundle code stats')
207
+ details.codeStats = stats
208
+ }
209
+ return details
210
+ }
211
+
212
+ function parseSourcemapEdges(rows, files) {
213
+ if (!Array.isArray(rows)) throw new Error('Invalid sourcemap edges')
214
+ return rows.map((row) => {
215
+ if (!Array.isArray(row) || ![2, 3].includes(row.length) || row[0] === row[1] || (row.length === 3 && typeof row[2] !== 'string')
216
+ || row.slice(0, 2).some((i) => !Number.isSafeInteger(i) || i < 0 || i >= files.length)) throw new Error('Invalid sourcemap edges')
217
+ return [files[row[0]][0], files[row[1]][0], ...row.slice(2)]
218
+ })
219
+ }
220
+
221
+ // HTTP content encoding handles compression before this shared JSON parser.
222
+ export function parseBundleContents(text, { integrity, kind, size }) {
223
+ if (kind === 'stasis') return { integrity, kind, size, bundle: Bundle.parse(text) }
224
+ if (kind !== 'sourcemap') throw new Error('Unsupported bundle format')
225
+ return { integrity, kind, size, ...parseSourcemap(text) }
226
+ }
@@ -0,0 +1 @@
1
+ export function bundlePkgOf(path: string, options?: { packageDir?: string | null | undefined }): string
@@ -0,0 +1,90 @@
1
+ // Package classifier for bundle paths. Pulled out of
2
+ // `render-bundle.js` (which drags in lit + the source-viewer + a
3
+ // circular `render.js` import, none of which this pure string logic
4
+ // needs) so it's a DOM-free leaf the bundle graph / treemap / compare
5
+ // views — and the test suite — can import on its own, and the server pair a
6
+ // comparison's files as Compare does (server-managed/rename-diff.ts).
7
+
8
+ // Bucket a bundle source path into a "package":
9
+ // - files under `node_modules/<pkg>/...` return `<pkg>` (scoped names
10
+ // included), as do Solidity files under Soldeer's
11
+ // `dependencies/<pkg>/...`, the one ecosystem installing there;
12
+ // - own (first-party) source returns the single `__own__` bucket: an
13
+ // ordinary `dependencies/` folder of anything else is own code.
14
+ //
15
+ // pnpm wraps each install in
16
+ // `node_modules/.pnpm/<name>@<version>/node_modules/<name>/...` —
17
+ // matching the first occurrence would bucket every dep under `.pnpm`,
18
+ // so when we hit that synthetic dir we walk past it to the inner
19
+ // `node_modules/<pkg>` segment that names the actual package.
20
+ //
21
+ // `packageDir` is this path's authoritative stasis package directory
22
+ // (from `bundlePackageDirs` in bundle-sources.js), when one is known.
23
+ // It takes precedence over path heuristics: a module can contain its own
24
+ // `dependencies/` directory without those files becoming new packages.
25
+ // Recorded `node_modules/<pkg>` directories use the bare package name;
26
+ // other named module directories stay separate under their full dir, and
27
+ // `.` identifies own source. Only bundles without this metadata use
28
+ // the path heuristic.
29
+ //
30
+ // The result is the package's identity — what files, sizes and edges are
31
+ // grouped by — so a vendored package keeps its dir here: Cargo's
32
+ // `vendor/log` is not npm's `log`, and one bundle can carry both. It is
33
+ // `pkgLabel` that shows it by name.
34
+ export function bundlePkgOf(path, { packageDir = null } = {}) {
35
+ if (packageDir) {
36
+ if (packageDir !== '.') {
37
+ const npm = packageDir.match(/(?:^|\/)node_modules\/(@[^/]+\/[^/]+|[^/]+)$/u)
38
+ return npm && npm[1] !== '.pnpm' ? npm[1] : packageDir
39
+ }
40
+ } else {
41
+ const re = /\.sol$/iu.test(path)
42
+ ? /(?:^|\/)(?:node_modules\/(@[^/]+\/[^/]+|[^/]+)|dependencies\/(@[^/]+\/[^/]+|[^/]+)\/)/gu
43
+ : /(?:^|\/)node_modules\/(@[^/]+\/[^/]+|[^/]+)/gu
44
+ let m
45
+ while ((m = re.exec(path)) !== null) {
46
+ const name = m[1] ?? m[2]
47
+ if (name !== '.pnpm') return name
48
+ }
49
+ }
50
+ return '__own__'
51
+ }
52
+
53
+ // First-party code: own source, and named workspace modules (a recorded
54
+ // package dir outside `node_modules/`, `dependencies/` and `vendor/`).
55
+ // Workspace modules stay separate packages in `bundlePkgOf`, but are
56
+ // own code even when another workspace imports them.
57
+ export function isOwnSourcePath(path, packageDir = null) {
58
+ return bundlePkgOf(path, { packageDir }) === '__own__'
59
+ || Boolean(packageDir) && !/(?:^|\/)(?:node_modules|dependencies|vendor)(?:\/|$)/u.test(packageDir)
60
+ }
61
+
62
+ // Search order for the bundle Code rail and Search tab: own code
63
+ // first, then dependencies, each keeping the order `paths` came in.
64
+ // Matches in the bundle's own code are usually what a search is after,
65
+ // and package code shouldn't push them past the result caps.
66
+ // `packageDirs` is `bundlePackageDirs` output, or null for the path
67
+ // heuristic alone.
68
+ export function ownSourceFirst(paths, packageDirs = null) {
69
+ const own = []
70
+ const deps = []
71
+ for (const path of paths) (isOwnSourcePath(path, packageDirs?.get(path)) ? own : deps).push(path)
72
+ return [...own, ...deps]
73
+ }
74
+
75
+ // Display label for a package bucket: `__own__` is the sentinel for
76
+ // own-source (non-dependency) files, spelled out as "Own source" in
77
+ // package lists and tooltips.
78
+ //
79
+ // A vendored package is shown by the path after the (last) `vendor/`,
80
+ // which in each vendoring layout is what the package is called: `cargo
81
+ // vendor` puts a crate at `vendor/<crate>`, Composer a package at
82
+ // `vendor/<vendor>/<pkg>`, Go a module at `vendor/<module path>`. Only
83
+ // the label is shortened; the bucket keeps the dir (see `bundlePkgOf`).
84
+ // Anything but a package key (the graph asks with no package focused)
85
+ // passes through as it came.
86
+ export function pkgLabel(pkg) {
87
+ if (pkg === '__own__') return 'Own source'
88
+ if (typeof pkg !== 'string') return pkg
89
+ return pkg.match(/^(?:.*\/)?vendor\/(.+)$/u)?.[1] ?? pkg
90
+ }
@@ -0,0 +1,2 @@
1
+ import type { BundleDetails } from './bundle-metadata.js'
2
+ export function bundleReasons(details: BundleDetails, sourcePaths?: Iterable<string>): Map<string, Set<string>>
@@ -0,0 +1,21 @@
1
+ import { bundleFileSizes } from './bundle-sources.js'
2
+
3
+ // Reason metadata attributes files to consumers (run, build plugins, etc.).
4
+ // It is informational and can be absent on older/single-consumer bundles.
5
+ export function bundleReasons(details, sourcePaths) {
6
+ const reasons = new Map()
7
+ const raw = details?.kind === 'stasis' ? details.bundle?.reason : null
8
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return reasons
9
+ const paths = new Set(sourcePaths ?? [...bundleFileSizes(details)].filter(([, size]) => size != null).map(([path]) => path))
10
+ for (const [reason, files] of Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b))) {
11
+ if (!reason || !Array.isArray(files)) continue
12
+ const present = new Set(files.filter((file) => typeof file === 'string' && paths.has(file)))
13
+ if (present.size > 0) reasons.set(reason, present)
14
+ }
15
+ // A reason is only useful as a filter if it changes the set of files.
16
+ // Keep all named options when at least one differs, otherwise hide the
17
+ // selector (and discard any stale selection) in every visualization.
18
+ if (![...reasons.values()].some((files) => files.size < paths.size)) reasons.clear()
19
+ return reasons
20
+ }
21
+
@@ -0,0 +1,6 @@
1
+ import type { BundleDetails } from './bundle-metadata.js'
2
+
3
+ export function bundleSourceImports(details: BundleDetails, sources?: Map<string, string>): Map<string, Map<string, string | null>>
4
+ export function bundleSourceLinkImports(details: BundleDetails, sources?: Map<string, string>): Map<string, Map<string, string | null>>
5
+ export function sourceLinkResolver(sources: Map<string, string>, parent: string, imports?: Map<string, Map<string, string | null>> | null, packageFiles?: Map<string, unknown>, packageDirs?: Map<string, string> | null): (specifier: string) => string | null
6
+ export function bundleSourceLinkResolver(details: BundleDetails, parent: string): (specifier: string) => string | null
@@ -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
+ }