@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,172 @@
|
|
|
1
|
+
// Dependency versions two bundles record, compared: sorting versions as
|
|
2
|
+
// semver orders them, which way a package moved, and the Overview's package
|
|
3
|
+
// rows from computeBundleDiff's sizes. No @preventive/compare here, so a view
|
|
4
|
+
// that only orders versions loads none of the comparison's pairing.
|
|
5
|
+
|
|
6
|
+
// The Overview's package rows: each package that differs, once, from its
|
|
7
|
+
// sizes (computeBundleDiff's `packages`) and the versions each bundle
|
|
8
|
+
// records for it (`bundlePackageVersions`). A package only one side has is
|
|
9
|
+
// removed or added, with its size and versions there; one both have is
|
|
10
|
+
// changed when its size or its versions moved, with both sides of each —
|
|
11
|
+
// own source and workspace modules carry sizes alone.
|
|
12
|
+
export function comparePackages(packages, baseVersions, otherVersions) {
|
|
13
|
+
const sizes = new Map()
|
|
14
|
+
for (const r of packages.onlyBase) sizes.set(r.pkg, { base: r.bytes, other: null })
|
|
15
|
+
for (const r of packages.onlyOther) sizes.set(r.pkg, { base: null, other: r.bytes })
|
|
16
|
+
for (const r of packages.changed) sizes.set(r.pkg, { base: r.baseBytes, other: r.otherBytes })
|
|
17
|
+
for (const r of packages.unchanged ?? []) sizes.set(r.pkg, { base: r.bytes, other: r.bytes })
|
|
18
|
+
const versionsOf = (map, pkg) => [...map.get(pkg) ?? []].toSorted(compareSemver)
|
|
19
|
+
const moved = new Set([...packages.onlyBase, ...packages.onlyOther, ...packages.changed].map(r => r.pkg))
|
|
20
|
+
for (const pkg of new Set([...baseVersions.keys(), ...otherVersions.keys()])) {
|
|
21
|
+
if (!sameVersions(versionsOf(baseVersions, pkg), versionsOf(otherVersions, pkg))) moved.add(pkg)
|
|
22
|
+
}
|
|
23
|
+
const added = [], changed = [], removed = []
|
|
24
|
+
for (const pkg of moved) {
|
|
25
|
+
const size = sizes.get(pkg) ?? { base: null, other: null }
|
|
26
|
+
const after = versionsOf(otherVersions, pkg), before = versionsOf(baseVersions, pkg)
|
|
27
|
+
const onBase = size.base !== null || before.length > 0
|
|
28
|
+
const onOther = size.other !== null || after.length > 0
|
|
29
|
+
if (onBase && !onOther) removed.push({ pkg, bytes: size.base, versions: before })
|
|
30
|
+
else if (onOther && !onBase) added.push({ pkg, bytes: size.other, versions: after })
|
|
31
|
+
else {
|
|
32
|
+
changed.push({
|
|
33
|
+
pkg, baseBytes: size.base, otherBytes: size.other,
|
|
34
|
+
delta: size.base !== null && size.other !== null ? size.other - size.base : null,
|
|
35
|
+
baseVersions: before, otherVersions: after,
|
|
36
|
+
direction: before.length > 0 && after.length > 0 && !sameVersions(before, after) ? versionDirection(before, after) : null,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
|
|
41
|
+
return { removed: removed.toSorted(byPkg), added: added.toSorted(byPkg), changed: changed.toSorted(byPkg) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Split a version into its dotted-numeric core and its prerelease tail,
|
|
45
|
+
// dropping any `+build` metadata (semver ignores it for precedence).
|
|
46
|
+
// `1.2.3-rc.1+sha` → { core: '1.2.3', pre: 'rc.1' }.
|
|
47
|
+
function splitVersion(v) {
|
|
48
|
+
const s = String(v).trim()
|
|
49
|
+
const noBuild = s.split('+', 1)[0]
|
|
50
|
+
const dash = noBuild.indexOf('-')
|
|
51
|
+
return dash === -1
|
|
52
|
+
? { core: noBuild, pre: '' }
|
|
53
|
+
: { core: noBuild.slice(0, dash), pre: noBuild.slice(dash + 1) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Lightweight semver-ish comparison, enough to sort versions ascending
|
|
57
|
+
// and label an update ↑/↓. Compares the dotted-numeric core part by
|
|
58
|
+
// part (a missing part counts as 0); on an equal core a release ranks
|
|
59
|
+
// above a prerelease (`1.0.0` > `1.0.0-rc.1`) and two prereleases
|
|
60
|
+
// compare by their dot identifiers (numeric where both are numeric,
|
|
61
|
+
// else lexical) per the semver precedence rules. NOT a full
|
|
62
|
+
// implementation — a non-numeric core segment falls back to a string
|
|
63
|
+
// compare of the whole version so ordering stays total and stable.
|
|
64
|
+
export function compareSemver(a, b) {
|
|
65
|
+
if (a === b) return 0
|
|
66
|
+
const A = splitVersion(a)
|
|
67
|
+
const B = splitVersion(b)
|
|
68
|
+
const ap = A.core.split('.')
|
|
69
|
+
const bp = B.core.split('.')
|
|
70
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
71
|
+
const x = Number(ap[i] ?? 0)
|
|
72
|
+
const y = Number(bp[i] ?? 0)
|
|
73
|
+
if (Number.isNaN(x) || Number.isNaN(y)) return a < b ? -1 : 1
|
|
74
|
+
if (x !== y) return x < y ? -1 : 1
|
|
75
|
+
}
|
|
76
|
+
if (A.pre === B.pre) return 0
|
|
77
|
+
// A core with no prerelease is the higher (released) version.
|
|
78
|
+
if (!A.pre) return 1
|
|
79
|
+
if (!B.pre) return -1
|
|
80
|
+
const ai = A.pre.split('.')
|
|
81
|
+
const bi = B.pre.split('.')
|
|
82
|
+
for (let i = 0; i < Math.max(ai.length, bi.length); i++) {
|
|
83
|
+
const x = ai[i]
|
|
84
|
+
const y = bi[i]
|
|
85
|
+
if (x === undefined) return -1
|
|
86
|
+
if (y === undefined) return 1
|
|
87
|
+
if (x === y) continue
|
|
88
|
+
const xNum = /^\d+$/u.test(x)
|
|
89
|
+
const yNum = /^\d+$/u.test(y)
|
|
90
|
+
// Numeric identifiers always rank below non-numeric ones (semver),
|
|
91
|
+
// and compare numerically against each other.
|
|
92
|
+
if (xNum && yNum) return Number(x) < Number(y) ? -1 : 1
|
|
93
|
+
if (xNum !== yNum) return xNum ? -1 : 1
|
|
94
|
+
return x < y ? -1 : 1
|
|
95
|
+
}
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// True when two ascending-sorted version arrays hold the same members.
|
|
100
|
+
function sameVersions(a, b) {
|
|
101
|
+
if (a.length !== b.length) return false
|
|
102
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Direction of a version move, from the versions dropped and the versions
|
|
107
|
+
// taken on: 'up' when everything dropped is older than everything taken
|
|
108
|
+
// on (`1.0.0` → `2.0.0`), or, with only drops or only additions, than the
|
|
109
|
+
// versions kept (`1.0.0, 2.0.0` → `2.0.0` drops the older copy, and a newer
|
|
110
|
+
// copy added beside the old is up too); 'down' the other way round; and
|
|
111
|
+
// 'changed' when the moves fall between (`1.0.0, 1.2.0, 2.0.0` → `1.0.0,
|
|
112
|
+
// 2.0.0`).
|
|
113
|
+
export function versionDirection(baseVersions, otherVersions) {
|
|
114
|
+
const dropped = baseVersions.filter(v => !otherVersions.includes(v)).toSorted(compareSemver)
|
|
115
|
+
const taken = otherVersions.filter(v => !baseVersions.includes(v)).toSorted(compareSemver)
|
|
116
|
+
const kept = baseVersions.filter(v => otherVersions.includes(v)).toSorted(compareSemver)
|
|
117
|
+
const [from, to] = dropped.length > 0 && taken.length > 0 ? [dropped, taken]
|
|
118
|
+
: dropped.length > 0 ? [dropped, kept] : [kept, taken]
|
|
119
|
+
if (from.length === 0 || to.length === 0) return 'changed'
|
|
120
|
+
// Every version of `a` older than every version of `b`.
|
|
121
|
+
const before = (a, b) => compareSemver(a.at(-1), b[0]) < 0
|
|
122
|
+
return before(from, to) ? 'up' : before(to, from) ? 'down' : 'changed'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Diff two `Map<packageName, Set<version>>` inventories (as
|
|
126
|
+
// `bundlePackageVersions` returns for each bundle) into the dependency
|
|
127
|
+
// version changes between them. Mirrors the file/package diff framing:
|
|
128
|
+
// `base` is the open bundle, `other` the one compared against.
|
|
129
|
+
//
|
|
130
|
+
// {
|
|
131
|
+
// updated: [{ pkg, baseVersions: [...], otherVersions: [...],
|
|
132
|
+
// direction: 'up' | 'down' | 'changed' }],
|
|
133
|
+
// added: [{ pkg, versions: [...] }], // dependency only in other
|
|
134
|
+
// removed: [{ pkg, versions: [...] }], // dependency only in base
|
|
135
|
+
// totals: { baseDeps, otherDeps }, // distinct package counts
|
|
136
|
+
// }
|
|
137
|
+
//
|
|
138
|
+
// `updated` is the headline — a package present on BOTH sides whose set
|
|
139
|
+
// of versions changed (the "what did this bump pull in?" answer);
|
|
140
|
+
// packages whose versions are byte-for-byte the same are dropped.
|
|
141
|
+
// Version arrays are sorted ascending; the three lists are sorted by
|
|
142
|
+
// package name so the dependency list reads alphabetically.
|
|
143
|
+
export function computeVersionUpdates(baseVersions, otherVersions) {
|
|
144
|
+
const updated = []
|
|
145
|
+
const added = []
|
|
146
|
+
const removed = []
|
|
147
|
+
const names = new Set([...baseVersions.keys(), ...otherVersions.keys()])
|
|
148
|
+
for (const pkg of names) {
|
|
149
|
+
const bSet = baseVersions.get(pkg)
|
|
150
|
+
const oSet = otherVersions.get(pkg)
|
|
151
|
+
if (bSet && oSet) {
|
|
152
|
+
const b = [...bSet].toSorted(compareSemver)
|
|
153
|
+
const o = [...oSet].toSorted(compareSemver)
|
|
154
|
+
if (sameVersions(b, o)) continue
|
|
155
|
+
updated.push({ pkg, baseVersions: b, otherVersions: o, direction: versionDirection(b, o) })
|
|
156
|
+
} else if (bSet) {
|
|
157
|
+
removed.push({ pkg, versions: [...bSet].toSorted(compareSemver) })
|
|
158
|
+
} else {
|
|
159
|
+
added.push({ pkg, versions: [...oSet].toSorted(compareSemver) })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
|
|
163
|
+
updated.sort(byPkg)
|
|
164
|
+
added.sort(byPkg)
|
|
165
|
+
removed.sort(byPkg)
|
|
166
|
+
return {
|
|
167
|
+
updated,
|
|
168
|
+
added,
|
|
169
|
+
removed,
|
|
170
|
+
totals: { baseDeps: baseVersions.size, otherDeps: otherVersions.size },
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Root } from '@preventive/compare'
|
|
2
|
+
import type { BundleDetails } from './bundle-metadata.js'
|
|
3
|
+
// A comparison as Compare and Review read it; `files.basePaths` maps every
|
|
4
|
+
// base file to how it reads as the other side lays its files out.
|
|
5
|
+
export interface Comparison extends Record<string, unknown> {
|
|
6
|
+
files: Record<string, unknown> & { root: Root | null; basePaths: Map<string, string> }
|
|
7
|
+
}
|
|
8
|
+
export function prepareComparison(base: BundleDetails, other: BundleDetails, scope?: string | null): Comparison
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// One comparison model for Compare and Review: bytes, metadata, resolutions,
|
|
2
|
+
// dependency versions and line counts, its files paired by
|
|
3
|
+
// @preventive/compare (computeBundleDiff, treeLayout). Worked out by the
|
|
4
|
+
// server for two managed bundles or two versions of a package
|
|
5
|
+
// (server-managed/comparison.ts), and by the viewer only for bundles it opened
|
|
6
|
+
// itself, which no server holds (ui/view/bundle-comparison.js): a managed
|
|
7
|
+
// viewer loads none of it.
|
|
8
|
+
import { layoutPackageDirs, treeLayout } from '@preventive/compare'
|
|
9
|
+
import { bundlePkgOf } from './bundle-pkg-of.js'
|
|
10
|
+
import { bundlePackageDirs, bundlePackageVersions } from './bundle-sources.js'
|
|
11
|
+
import { bundleFileMeta, computeBundleDiff, computeResolutionDiff } from './bundle-compare-diff.js'
|
|
12
|
+
import { comparePackages, computeVersionUpdates } from './bundle-compare-versions.js'
|
|
13
|
+
import { bundleCompareFiles, bundleCompareResolutions } from './bundle-compare-inputs.js'
|
|
14
|
+
import { bundleLineCounts } from './bundle-metadata.js'
|
|
15
|
+
|
|
16
|
+
export function prepareComparison(base, other, scope = null) {
|
|
17
|
+
// Every file each side mounts, resources included: a diff of
|
|
18
|
+
// source alone would call two bundles identical when only their
|
|
19
|
+
// images differ, and total less than the Overview does.
|
|
20
|
+
// From the bundle picked to the open one.
|
|
21
|
+
const [from, to] = [base, other]
|
|
22
|
+
const baseSources = bundleCompareFiles(from, scope)
|
|
23
|
+
const otherSources = bundleCompareFiles(to, scope)
|
|
24
|
+
// Classify original paths and preserve recorded Stasis package
|
|
25
|
+
// boundaries from either side, as the open one lays its files out
|
|
26
|
+
// where their roots differ. Own source has one identity.
|
|
27
|
+
const layout = treeLayout(baseSources.keys(), otherSources.keys())
|
|
28
|
+
const packageDirs = layoutPackageDirs(bundlePackageDirs(from), bundlePackageDirs(to), layout)
|
|
29
|
+
const pkgOf = (p) => bundlePkgOf(p, { packageDir: packageDirs?.get(p) })
|
|
30
|
+
const diff = computeBundleDiff(baseSources, otherSources, pkgOf, layout, [bundleFileMeta(from), bundleFileMeta(to)])
|
|
31
|
+
// Each side's lines of code (bundleLineCounts), on its rows and in
|
|
32
|
+
// the totals; a file that isn't text has none.
|
|
33
|
+
const baseLines = bundleLineCounts(from), otherLines = bundleLineCounts(to)
|
|
34
|
+
const linesOf = (lines, paths) => { let total = 0; for (const path of paths) total += lines.get(path) ?? 0; return total }
|
|
35
|
+
Object.assign(diff.totals, { baseLines: linesOf(baseLines, baseSources.keys()), otherLines: linesOf(otherLines, otherSources.keys()) })
|
|
36
|
+
for (const row of diff.files.onlyBase) row.lines = baseLines.get(row.basePath ?? row.path) ?? 0
|
|
37
|
+
for (const row of diff.files.onlyOther) row.lines = otherLines.get(row.path) ?? 0
|
|
38
|
+
for (const row of diff.files.changed) Object.assign(row, { baseLines: baseLines.get(row.basePath ?? row.path) ?? 0, otherLines: otherLines.get(row.path) ?? 0 })
|
|
39
|
+
diff.resolutions = computeResolutionDiff(
|
|
40
|
+
bundleCompareResolutions(from, scope),
|
|
41
|
+
bundleCompareResolutions(to, scope),
|
|
42
|
+
new Map(diff.files.changed.filter(row => row.renamedFrom != null).map(row => [row.basePath, row.path])),
|
|
43
|
+
diff.files.basePaths,
|
|
44
|
+
diff.files.root,
|
|
45
|
+
)
|
|
46
|
+
// Dependency versions come from the stasis per-module `{ name,
|
|
47
|
+
// version }` metadata, not the path/byte walk above, so they're
|
|
48
|
+
// computed alongside and hung off the same memo: the package rows
|
|
49
|
+
// join them to the sizes, the summary counts them. Empty for
|
|
50
|
+
// sourcemap / v0 pairs (no version metadata), whose rows carry sizes
|
|
51
|
+
// alone.
|
|
52
|
+
// Keyed by install directory, as the sizes are (`bundlePkgOf`).
|
|
53
|
+
const versionKey = dir => bundlePkgOf('', { packageDir: dir })
|
|
54
|
+
const baseVersions = bundlePackageVersions(from, scope ? baseSources.keys() : null, versionKey)
|
|
55
|
+
const otherVersions = bundlePackageVersions(to, scope ? otherSources.keys() : null, versionKey)
|
|
56
|
+
diff.versionUpdates = computeVersionUpdates(baseVersions, otherVersions)
|
|
57
|
+
diff.packageRows = comparePackages(diff.packages, baseVersions, otherVersions)
|
|
58
|
+
// The reason scope its files are within, for what the server works out of
|
|
59
|
+
// them alike (rename-diff.js, type-only.js).
|
|
60
|
+
diff.scope = scope ?? ''
|
|
61
|
+
return diff
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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 function computeBundleFileHashes(details: BundleDetails): Promise<Map<string, string>>
|
|
9
|
+
export interface BundleMetadata extends Record<string, unknown> {
|
|
10
|
+
files: [string, number | null, string | null, number | null][]
|
|
11
|
+
codeStats: { files: number; lines: number; bytes: number }
|
|
12
|
+
}
|
|
13
|
+
export function createBundleMetadata(details: BundleDetails): Promise<BundleMetadata>
|
|
14
|
+
export function parseBundleMetadata(data: unknown, integrity: string): BundleDetails
|
|
15
|
+
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.fromJSON(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
|