@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.
- package/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +310 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2AFQKRAM.js +51 -0
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KVEUXIEU.js +1 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QZ3A376W.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TY4RZD3Z.js +226 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-U6OPEXIA.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-UZAZM2SX.js +2 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XL35ZV6R.js +1 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +298 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +595 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +156 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +223 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +116 -18
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +420 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /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,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
|
+
}
|