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