@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,115 @@
|
|
|
1
|
+
// The "links" file — a small JSON document that says which findings
|
|
2
|
+
// are the SAME finding, reported twice:
|
|
3
|
+
//
|
|
4
|
+
// [["a", "b"], ["c", "d", "e"]]
|
|
5
|
+
// Entries may also be objects such as {"id":"a"}, for existing exports.
|
|
6
|
+
//
|
|
7
|
+
// One inner array per link. Every finding named in it is a duplicate
|
|
8
|
+
// of every other one in that array, and the file says nothing else:
|
|
9
|
+
// no severity, no title, no file, no prose. That is the whole format.
|
|
10
|
+
//
|
|
11
|
+
// It is NOT a report, and this module deliberately sits outside
|
|
12
|
+
// `@preventive/report` because of it. A report carries findings; a links file
|
|
13
|
+
// carries only ids of findings that must already live in reports the
|
|
14
|
+
// reader holds. So it never reaches `ingestReport` / `state.reports`,
|
|
15
|
+
// it has its own view (`ui/view/render-links.js`) that points at the
|
|
16
|
+
// findings. It also supplies the "Duplicates:" row on finding cards
|
|
17
|
+
// and groups explicitly linked rows in workspace App view.
|
|
18
|
+
//
|
|
19
|
+
// Pure — no storage, no DOM, no app state. `linked-findings-index.js`
|
|
20
|
+
// beside this file is the OPFS-wide store built on top; everything
|
|
21
|
+
// here is text in, data out, so the recognition rules are testable on
|
|
22
|
+
// their own.
|
|
23
|
+
|
|
24
|
+
import { isLinkableFindingId } from './finding-link.js'
|
|
25
|
+
|
|
26
|
+
// The `source` marker a links file is filed under — the same slot a
|
|
27
|
+
// report's producer ('deepsec', 'piolium', …) occupies in the counts
|
|
28
|
+
// cache, so `getKind(name)` answers "what kind of file is this" for
|
|
29
|
+
// links and reports alike and the sidebar can bucket both from one
|
|
30
|
+
// lookup. No report format can collide with it: this value comes from
|
|
31
|
+
// here, never from a document's own content.
|
|
32
|
+
export const LINKS_KIND = 'links'
|
|
33
|
+
|
|
34
|
+
// Read a string ID or an existing object entry — `{"id": "…"}`. Extra fields are allowed and ignored: an exporter that
|
|
35
|
+
// writes the title or the report alongside the id is writing a
|
|
36
|
+
// superset of this format, not a different one.
|
|
37
|
+
function entryId(entry) {
|
|
38
|
+
if (typeof entry === 'string') return entry.length > 0 ? entry : null
|
|
39
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null
|
|
40
|
+
const { id } = entry
|
|
41
|
+
return typeof id === 'string' && id.length > 0 ? id : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Recognise `content` as a links file and normalise it.
|
|
45
|
+
//
|
|
46
|
+
// Returns `{ groups, skipped }` — `groups` is one `string[]` of finding
|
|
47
|
+
// ids per link, and `skipped` counts ids the app could never follow (a
|
|
48
|
+
// session-local numeric id, a control character, something absurdly
|
|
49
|
+
// long — `isLinkableFindingId` draws the same line the per-finding Link
|
|
50
|
+
// button draws). Returns null when the text isn't this format at all.
|
|
51
|
+
//
|
|
52
|
+
// Recognition is on SHAPE, and it is strict, because this parser runs
|
|
53
|
+
// against every dropped file before the report readers get their turn:
|
|
54
|
+
// a top-level array, holding only arrays, holding string IDs or objects with a
|
|
55
|
+
// string `id`. Nothing else this app reads is a bare JSON array, so
|
|
56
|
+
// nothing else can be mistaken for one — and an empty top-level array
|
|
57
|
+
// is refused rather than claimed, since `[]` is every empty JSON list
|
|
58
|
+
// in the world, not distinctively a links file.
|
|
59
|
+
//
|
|
60
|
+
// Normalisation drops what carries no information: ids repeated inside
|
|
61
|
+
// one link, ids that can't be linked, and then any link left naming
|
|
62
|
+
// fewer than two findings — a "link" to a single finding links nothing.
|
|
63
|
+
// A file whose every link normalises away is still a links file; it
|
|
64
|
+
// just has no links, which its view says plainly.
|
|
65
|
+
export function parseLinkedFindings(content) {
|
|
66
|
+
// Only text opening an array can parse to one, so a report — which
|
|
67
|
+
// opens an object — is turned away without being parsed twice.
|
|
68
|
+
if (typeof content === 'string' && !/^\s*\[/u.test(content)) return null
|
|
69
|
+
let data
|
|
70
|
+
try { data = JSON.parse(content) } catch { return null }
|
|
71
|
+
if (!Array.isArray(data) || data.length === 0) return null
|
|
72
|
+
const groups = []
|
|
73
|
+
let skipped = 0
|
|
74
|
+
for (const raw of data) {
|
|
75
|
+
if (!Array.isArray(raw)) return null
|
|
76
|
+
const ids = new Set()
|
|
77
|
+
for (const entry of raw) {
|
|
78
|
+
const id = entryId(entry)
|
|
79
|
+
if (id === null) return null
|
|
80
|
+
if (isLinkableFindingId(id)) ids.add(id)
|
|
81
|
+
else skipped++
|
|
82
|
+
}
|
|
83
|
+
if (ids.size > 1) groups.push([...ids])
|
|
84
|
+
}
|
|
85
|
+
return { groups, skipped }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// How many findings the links in `groups` name, counted once each — a
|
|
89
|
+
// finding named by two links is one linked finding. What the sidebar
|
|
90
|
+
// badge and the view's header report alongside the link count.
|
|
91
|
+
export function countLinkedIds(groups) {
|
|
92
|
+
const ids = new Set()
|
|
93
|
+
for (const group of groups) for (const id of group) ids.add(id)
|
|
94
|
+
return ids.size
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fold links into `index` (a `Map<id, Set<id>>`): every id in a link
|
|
98
|
+
// gains every OTHER id in it as a duplicate. Called once per links
|
|
99
|
+
// file, so an id linked by two files ends up with the union of what
|
|
100
|
+
// both said about it.
|
|
101
|
+
//
|
|
102
|
+
// Union, NOT transitive closure. If one file links a↔b and another
|
|
103
|
+
// links b↔c, then b has two duplicates and a has one — a and c were
|
|
104
|
+
// never said to be the same finding, and inferring it would put a
|
|
105
|
+
// claim in the reader's card that no file they dropped ever made.
|
|
106
|
+
export function collectDuplicates(groups, index) {
|
|
107
|
+
for (const group of groups) {
|
|
108
|
+
for (const id of group) {
|
|
109
|
+
let set = index.get(id)
|
|
110
|
+
if (!set) index.set(id, set = new Set())
|
|
111
|
+
for (const other of group) if (other !== id) set.add(other)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return index
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function bundleCommitHash(value: unknown): string | null
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function computeBundleDiff(base: Map<string, unknown>, other: Map<string, unknown>, pkgOf: (path: string) => string, layout?: import('@preventive/compare').Layout): {
|
|
2
|
+
files: { onlyBase: { path: string; basePath?: string }[]; onlyOther: { path: string }[]; changed: { path: string; basePath?: string; renamedFrom?: string }[] }
|
|
3
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Shared bundle comparison — no Lit, no DOM, no `state`. Given two
|
|
2
|
+
// `Map<path, content>` file maps (the shape `bundleFilesAsMap` returns
|
|
3
|
+
// for either a sourcemap or a stasis bundle: every file the terminal
|
|
4
|
+
// mounts, source or resource, with no directory captures) plus a
|
|
5
|
+
// `pkgOf(path)` bucketing function, it computes a structural diff:
|
|
6
|
+
// which files exist only in one side, which exist in both but changed,
|
|
7
|
+
// per-package size deltas, and the roll-up totals.
|
|
8
|
+
//
|
|
9
|
+
// Kept free of Lit, the DOM and the OPFS parse pipeline on purpose, so
|
|
10
|
+
// `<bundle-compare>` (the Compare slide in the bundles view) and its
|
|
11
|
+
// unit test can both consume the same logic — the test exercises this
|
|
12
|
+
// module directly. Its imports are the file sizer the Overview uses and
|
|
13
|
+
// @preventive/compare's renames and layout, all pure. The component is a
|
|
14
|
+
// thin rendering shell around `computeBundleDiff`.
|
|
15
|
+
//
|
|
16
|
+
// `base` is the currently-open bundle (the tab you're viewing);
|
|
17
|
+
// `other` is the bundle picked to compare against. The result names
|
|
18
|
+
// the two sides `onlyBase` / `onlyOther` rather than added / removed
|
|
19
|
+
// so the UI can label them with each bundle's actual name — "added"
|
|
20
|
+
// is ambiguous without knowing which side is newer.
|
|
21
|
+
|
|
22
|
+
import { compareTrees, treeLayout } from '@preventive/compare'
|
|
23
|
+
import { bundleFileByteLength } from './bundle-sources.js'
|
|
24
|
+
import { resolutionKey } from './bundle-compare-inputs.js'
|
|
25
|
+
|
|
26
|
+
// Resolutions compare independently from file bytes: identical files can be
|
|
27
|
+
// wired together differently. Inputs are keyed by importer, specifier,
|
|
28
|
+
// conditions (including import attributes), and platform. Only existing
|
|
29
|
+
// resolutions whose targets changed are reported; additions/removals are omitted.
|
|
30
|
+
// `renames` (base path → other path, see renamesOf) names base files as
|
|
31
|
+
// the other bundle does: a renamed importer's resolutions pair with its new
|
|
32
|
+
// path's, reported under it, and a target renamed is the same target.
|
|
33
|
+
// `basePaths` and `root` (computeBundleDiff's `files`) name the rest where
|
|
34
|
+
// the bundles' roots differ, a base target shown as the other bundle would
|
|
35
|
+
// lay it out; a path no file has (an importer the bundle didn't capture, a
|
|
36
|
+
// builtin) may read either as it is or through the root.
|
|
37
|
+
export function computeResolutionDiff(base, other, renames = new Map(), basePaths = new Map(), root = null) {
|
|
38
|
+
const names = path => {
|
|
39
|
+
const known = renames.get(path) ?? basePaths.get(path)
|
|
40
|
+
return known === undefined ? [...new Set([path, through(root, path) ?? path])] : [known]
|
|
41
|
+
}
|
|
42
|
+
const changed = []
|
|
43
|
+
for (const before of base.values()) {
|
|
44
|
+
const after = names(before.parent).map(parent => other.get(resolutionKey(parent, before.specifier, before.conditions, before.platform))).find(Boolean)
|
|
45
|
+
if (after && !names(before.target).includes(after.target)) {
|
|
46
|
+
const { target: otherTarget, ...identity } = after
|
|
47
|
+
changed.push({ ...identity, baseTarget: basePaths.get(before.target) ?? before.target, otherTarget })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
changed.sort((a, b) => a.key.localeCompare(b.key))
|
|
51
|
+
return { changed, totalChanges: changed.length }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A file's size, by the measure the Overview and Treemap use: text by the
|
|
55
|
+
// UTF-8 it encodes to, a base64 resource by the bytes it decodes to. A
|
|
56
|
+
// file with no size to give (a base64 spelling that does not decode)
|
|
57
|
+
// counts as zero bytes, but is still compared.
|
|
58
|
+
function byteLen(content) {
|
|
59
|
+
return bundleFileByteLength(content) ?? 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Whether two sides hold the same file. Text compares as text; a base64
|
|
63
|
+
// resource arrives as a fresh `{ format: 'base64', data }` on each side,
|
|
64
|
+
// so it compares by its spelling — identity would call every image
|
|
65
|
+
// changed. A file that is text on one side and base64 on the other has
|
|
66
|
+
// changed. An npm package's binary file compares by its digest.
|
|
67
|
+
function sameContent(a, b) {
|
|
68
|
+
return a === b || (a?.format === 'base64' && b?.format === 'base64' && a.data === b.data)
|
|
69
|
+
|| (a?.format === 'digest' && b?.format === 'digest' && a.digest === b.digest)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// What a stasis bundle records of a file beside its bytes: the format it
|
|
73
|
+
// loads it in (a resource is carried as data, the rest run as code), whether
|
|
74
|
+
// it carries an execute bit, and whether the bundle starts from it. A
|
|
75
|
+
// function of the file's path; null for another kind of bundle, which
|
|
76
|
+
// records none of it.
|
|
77
|
+
export function bundleFileMeta(details) {
|
|
78
|
+
if (details?.kind !== 'stasis' || !details.bundle) return null
|
|
79
|
+
const { formats, executable, entries } = details.bundle
|
|
80
|
+
return path => ({ format: formats?.get(path), executable: executable?.has(path) ?? false, entry: entries?.has(path) ?? false })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// How a file's record changed from one stasis bundle to the other, as
|
|
84
|
+
// `{ field, from, to }` (bundleFileMeta's fields): none where either side
|
|
85
|
+
// records nothing. A format left unsaid is one too: stasis runs such a
|
|
86
|
+
// file as code, so a resource whose format is dropped runs.
|
|
87
|
+
function metaChanges(before, after) {
|
|
88
|
+
if (!before || !after) return []
|
|
89
|
+
return ['format', 'executable', 'entry'].filter(field => before[field] !== after[field]).map(field => ({ field, from: before[field], to: after[field] }))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Comparator: largest absolute delta first, then path/label ascending
|
|
93
|
+
// so equal-magnitude rows stay stably ordered. Used for the changed
|
|
94
|
+
// file + package lists, where the biggest mover is the most
|
|
95
|
+
// interesting row.
|
|
96
|
+
function byAbsDeltaThenKey(key) {
|
|
97
|
+
return (a, b) => {
|
|
98
|
+
const d = Math.abs(b.delta) - Math.abs(a.delta)
|
|
99
|
+
// `d || …` falls through to the tiebreak only when the magnitudes
|
|
100
|
+
// match (d === 0); a non-zero d is returned as-is.
|
|
101
|
+
return d || a[key].localeCompare(b[key])
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Largest byte size first, then key ascending — for the only-in-one-
|
|
106
|
+
// side lists, where there's no delta to rank by.
|
|
107
|
+
function byBytesThenKey(key) {
|
|
108
|
+
return (a, b) => {
|
|
109
|
+
const d = b.bytes - a.bytes
|
|
110
|
+
return d || a[key].localeCompare(b[key])
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A file's content as compareTrees reads it to find renames, each kind tagged
|
|
115
|
+
// apart: text as it is (a lone surrogate, which no UTF-8 holds, as U+FFFD); a
|
|
116
|
+
// resource by its spelling or its digest; and one with none, by its place in
|
|
117
|
+
// its list alone, so that only its path pairs it.
|
|
118
|
+
function treeData(content, at) {
|
|
119
|
+
if (typeof content === 'string') return `\0text\0${content.toWellFormed()}`
|
|
120
|
+
if (content?.format === 'base64') return `\0base64\0${content.data}`
|
|
121
|
+
return content?.format === 'digest' ? `\0digest\0${content.digest}` : `\0none\0${at}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// git's diff.renameLimit for a package's renames: past this many removed files
|
|
125
|
+
// by added ones, squared, no pair is scored, as that would hold the page for
|
|
126
|
+
// seconds, and only identical content and paths pair them.
|
|
127
|
+
const RENAME_LIMIT = 250
|
|
128
|
+
|
|
129
|
+
// The files of `onlyBase` renamed to one of `onlyOther`, both by path as the
|
|
130
|
+
// other bundle lays them out, within a package: `Map<base path, other path>`,
|
|
131
|
+
// as @preventive/compare's compareTrees finds them, by content as git diff -M
|
|
132
|
+
// does and where their paths say so. Whether a renamed file changed is
|
|
133
|
+
// sameContent's to say, not this reading's. A path no UTF-8 holds is left out.
|
|
134
|
+
function renamesOf(onlyBase, onlyOther, base, other, pkgOf) {
|
|
135
|
+
const entries = (rows, files, side) => rows.filter(({ path }) => path !== '' && path.isWellFormed())
|
|
136
|
+
.map(({ path }, at) => ({ name: path, data: treeData(files.get(path), `${side}${at}`) }))
|
|
137
|
+
const options = { paths: 'keys', renameGroup: pkgOf, renameLimit: RENAME_LIMIT, lines: false }
|
|
138
|
+
const changes = compareTrees(entries(onlyBase, base, 'a'), entries(onlyOther, other, 'b'), options)
|
|
139
|
+
return new Map(changes.filter(({ status }) => status === 'renamed').map(change => [change.old.path, change.new.path]))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A base path through `root` as treeLayout reads it, or null for one outside
|
|
143
|
+
// a base root.
|
|
144
|
+
function through(root, path) {
|
|
145
|
+
if (!root) return path
|
|
146
|
+
if (root.base === '') return `${root.other}${path}`
|
|
147
|
+
return path.startsWith(root.base) ? path.slice(root.base.length) : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Per-package accumulator factory. Tracks each side's byte total plus
|
|
151
|
+
// per-bucket file counts so a package is flagged `changed` whenever
|
|
152
|
+
// ANY of its files moved — not only when the byte total happens to
|
|
153
|
+
// differ (two versions can shuffle content while landing on the same
|
|
154
|
+
// size).
|
|
155
|
+
function emptyPkgAcc() {
|
|
156
|
+
return {
|
|
157
|
+
baseBytes: 0,
|
|
158
|
+
otherBytes: 0,
|
|
159
|
+
onlyBaseFiles: 0,
|
|
160
|
+
onlyOtherFiles: 0,
|
|
161
|
+
changedFiles: 0,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Compare two bundle source maps. Returns:
|
|
166
|
+
// {
|
|
167
|
+
// totals: { baseFiles, baseBytes, otherFiles, otherBytes,
|
|
168
|
+
// onlyBaseFiles, onlyBaseBytes, onlyOtherFiles,
|
|
169
|
+
// onlyOtherBytes, changedFiles, changedDelta,
|
|
170
|
+
// unchangedFiles, fileDelta, byteDelta, identical },
|
|
171
|
+
// files: {
|
|
172
|
+
// onlyBase: [{ path, bytes }],
|
|
173
|
+
// onlyOther: [{ path, bytes }],
|
|
174
|
+
// changed: [{ path, baseBytes, otherBytes, delta, basePath?, modified?, meta? }],
|
|
175
|
+
// },
|
|
176
|
+
// packages: {
|
|
177
|
+
// onlyBase: [{ pkg, bytes }],
|
|
178
|
+
// onlyOther: [{ pkg, bytes }],
|
|
179
|
+
// changed: [{ pkg, baseBytes, otherBytes, delta }],
|
|
180
|
+
// },
|
|
181
|
+
// }
|
|
182
|
+
//
|
|
183
|
+
// `delta` is always `other − base` (positive = the compared bundle is
|
|
184
|
+
// larger). A file renamed (see renamesOf) is changed, not
|
|
185
|
+
// removed and added: its row's `path` is the other side's, `renamedFrom`
|
|
186
|
+
// the base's, `modified` whether its contents changed too, and it counts in
|
|
187
|
+
// `renamedFiles` as well, its contents moved or not.
|
|
188
|
+
// Where one bundle's files sit under a directory of the other's (`root`, see
|
|
189
|
+
// treeLayout), the base's are compared as the other lays them out: a row's
|
|
190
|
+
// `path` (and `renamedFrom`) reads that way, and `basePath` names the file
|
|
191
|
+
// in the base bundle wherever that differs from `path`. `files.basePaths`
|
|
192
|
+
// maps every base file to how it reads. `layout` (treeLayout) may come
|
|
193
|
+
// from the caller, to key `pkgOf` by the same paths: it is asked of files
|
|
194
|
+
// as the other bundle lays them out.
|
|
195
|
+
// `meta`, each side's bundleFileMeta where it has one, compares what two
|
|
196
|
+
// stasis bundles record of a file beside its bytes: a row whose record
|
|
197
|
+
// changed carries how (`meta`, metaChanges), and a file whose record alone
|
|
198
|
+
// changed is changed too, `modified: false`, its bytes the same.
|
|
199
|
+
// `identical` is true when the two bundles carry the same files with
|
|
200
|
+
// byte-identical content, resources included, each recorded alike.
|
|
201
|
+
export function computeBundleDiff(base, other, pkgOf, layout = treeLayout(base.keys(), other.keys()), meta = []) {
|
|
202
|
+
const { root, paths: basePaths } = layout
|
|
203
|
+
// The base's files as the other lays them out, and back.
|
|
204
|
+
const baseFile = new Map([...basePaths].map(([from, to]) => [to, from]))
|
|
205
|
+
base = new Map([...base].map(([path, content]) => [basePaths.get(path), content]))
|
|
206
|
+
const atBase = path => baseFile.get(path) === path ? {} : { basePath: baseFile.get(path) }
|
|
207
|
+
// How a file's record changed, by its base path and its path, as a row's fields.
|
|
208
|
+
const recorded = (from, to) => {
|
|
209
|
+
const changes = metaChanges(meta[0]?.(from), meta[1]?.(to))
|
|
210
|
+
return changes.length > 0 ? { meta: changes } : {}
|
|
211
|
+
}
|
|
212
|
+
const onlyBase = []
|
|
213
|
+
const onlyOther = []
|
|
214
|
+
const changed = []
|
|
215
|
+
let baseBytes = 0
|
|
216
|
+
let otherBytes = 0
|
|
217
|
+
let onlyBaseBytes = 0
|
|
218
|
+
let onlyOtherBytes = 0
|
|
219
|
+
let changedDelta = 0
|
|
220
|
+
let unchangedFiles = 0
|
|
221
|
+
const pkgs = new Map()
|
|
222
|
+
const pkgAcc = (path) => {
|
|
223
|
+
const key = pkgOf(path)
|
|
224
|
+
let acc = pkgs.get(key)
|
|
225
|
+
if (!acc) { acc = emptyPkgAcc(); pkgs.set(key, acc) }
|
|
226
|
+
return acc
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const allPaths = new Set([...base.keys(), ...other.keys()])
|
|
230
|
+
for (const path of allPaths) {
|
|
231
|
+
const inBase = base.has(path)
|
|
232
|
+
const inOther = other.has(path)
|
|
233
|
+
const acc = pkgAcc(path)
|
|
234
|
+
if (inBase && inOther) {
|
|
235
|
+
const bC = base.get(path)
|
|
236
|
+
const oC = other.get(path)
|
|
237
|
+
const same = sameContent(bC, oC)
|
|
238
|
+
const record = recorded(baseFile.get(path), path)
|
|
239
|
+
const bB = byteLen(bC)
|
|
240
|
+
// Identical content shares the byte count, so only measure the
|
|
241
|
+
// other side when the files actually differ.
|
|
242
|
+
const oB = same ? bB : byteLen(oC)
|
|
243
|
+
baseBytes += bB
|
|
244
|
+
otherBytes += oB
|
|
245
|
+
acc.baseBytes += bB
|
|
246
|
+
acc.otherBytes += oB
|
|
247
|
+
if (same && !record.meta) {
|
|
248
|
+
unchangedFiles++
|
|
249
|
+
} else {
|
|
250
|
+
const delta = oB - bB
|
|
251
|
+
changed.push({ path, ...atBase(path), baseBytes: bB, otherBytes: oB, delta, ...record, ...same ? { modified: false } : {} })
|
|
252
|
+
changedDelta += delta
|
|
253
|
+
acc.changedFiles++
|
|
254
|
+
}
|
|
255
|
+
} else if (inBase) {
|
|
256
|
+
const bB = byteLen(base.get(path))
|
|
257
|
+
baseBytes += bB
|
|
258
|
+
onlyBaseBytes += bB
|
|
259
|
+
acc.baseBytes += bB
|
|
260
|
+
acc.onlyBaseFiles++
|
|
261
|
+
onlyBase.push({ path, ...atBase(path), bytes: bB })
|
|
262
|
+
} else {
|
|
263
|
+
const oB = byteLen(other.get(path))
|
|
264
|
+
otherBytes += oB
|
|
265
|
+
onlyOtherBytes += oB
|
|
266
|
+
acc.otherBytes += oB
|
|
267
|
+
acc.onlyOtherFiles++
|
|
268
|
+
onlyOther.push({ path, bytes: oB })
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const renames = renamesOf(onlyBase, onlyOther, base, other, pkgOf)
|
|
273
|
+
if (renames.size > 0) {
|
|
274
|
+
const renamedTo = new Set(renames.values())
|
|
275
|
+
for (const [from, to] of renames) {
|
|
276
|
+
const bC = base.get(from), oC = other.get(to)
|
|
277
|
+
const same = sameContent(bC, oC)
|
|
278
|
+
const bB = byteLen(bC), oB = same ? bB : byteLen(oC)
|
|
279
|
+
onlyBaseBytes -= bB
|
|
280
|
+
onlyOtherBytes -= oB
|
|
281
|
+
changed.push({ path: to, renamedFrom: from, basePath: baseFile.get(from), baseBytes: bB, otherBytes: oB, delta: oB - bB, modified: !same, ...recorded(baseFile.get(from), to) })
|
|
282
|
+
changedDelta += oB - bB
|
|
283
|
+
// Both in one package (its key is in the rename's).
|
|
284
|
+
const acc = pkgAcc(from)
|
|
285
|
+
acc.onlyBaseFiles--
|
|
286
|
+
acc.onlyOtherFiles--
|
|
287
|
+
acc.changedFiles++
|
|
288
|
+
}
|
|
289
|
+
onlyBase.splice(0, onlyBase.length, ...onlyBase.filter(row => !renames.has(row.path)))
|
|
290
|
+
onlyOther.splice(0, onlyOther.length, ...onlyOther.filter(row => !renamedTo.has(row.path)))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Classify each package from its accumulator: present on exactly one
|
|
294
|
+
// side → onlyBase / onlyOther; present on both with any moved file →
|
|
295
|
+
// changed; otherwise unchanged (dropped — the lists surface only
|
|
296
|
+
// what differs).
|
|
297
|
+
const pkgOnlyBase = []
|
|
298
|
+
const pkgOnlyOther = []
|
|
299
|
+
const pkgChanged = []
|
|
300
|
+
const pkgUnchanged = []
|
|
301
|
+
for (const [pkg, acc] of pkgs) {
|
|
302
|
+
// Present on a side if any of its files contributed there — its
|
|
303
|
+
// byte total, an only-this-side file, or a changed file (which
|
|
304
|
+
// exists on both). A changed file alone is enough to count as
|
|
305
|
+
// present even when that side's bytes net to zero.
|
|
306
|
+
const onBase = acc.baseBytes > 0 || acc.onlyBaseFiles > 0 || acc.changedFiles > 0
|
|
307
|
+
const onOther = acc.otherBytes > 0 || acc.onlyOtherFiles > 0 || acc.changedFiles > 0
|
|
308
|
+
const movedFiles = acc.onlyBaseFiles + acc.onlyOtherFiles + acc.changedFiles
|
|
309
|
+
if (onBase && !onOther) {
|
|
310
|
+
pkgOnlyBase.push({ pkg, bytes: acc.baseBytes })
|
|
311
|
+
} else if (onOther && !onBase) {
|
|
312
|
+
pkgOnlyOther.push({ pkg, bytes: acc.otherBytes })
|
|
313
|
+
} else if (movedFiles > 0 || acc.baseBytes !== acc.otherBytes) {
|
|
314
|
+
pkgChanged.push({
|
|
315
|
+
pkg,
|
|
316
|
+
baseBytes: acc.baseBytes,
|
|
317
|
+
otherBytes: acc.otherBytes,
|
|
318
|
+
delta: acc.otherBytes - acc.baseBytes,
|
|
319
|
+
})
|
|
320
|
+
} else {
|
|
321
|
+
pkgUnchanged.push({ pkg, bytes: acc.baseBytes })
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
onlyBase.sort(byBytesThenKey('path'))
|
|
326
|
+
onlyOther.sort(byBytesThenKey('path'))
|
|
327
|
+
changed.sort(byAbsDeltaThenKey('path'))
|
|
328
|
+
pkgOnlyBase.sort(byBytesThenKey('pkg'))
|
|
329
|
+
pkgOnlyOther.sort(byBytesThenKey('pkg'))
|
|
330
|
+
pkgChanged.sort(byAbsDeltaThenKey('pkg'))
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
totals: {
|
|
334
|
+
baseFiles: base.size,
|
|
335
|
+
baseBytes,
|
|
336
|
+
otherFiles: other.size,
|
|
337
|
+
otherBytes,
|
|
338
|
+
onlyBaseFiles: onlyBase.length,
|
|
339
|
+
onlyBaseBytes,
|
|
340
|
+
onlyOtherFiles: onlyOther.length,
|
|
341
|
+
onlyOtherBytes,
|
|
342
|
+
changedFiles: changed.length,
|
|
343
|
+
renamedFiles: renames.size,
|
|
344
|
+
changedDelta,
|
|
345
|
+
unchangedFiles,
|
|
346
|
+
fileDelta: other.size - base.size,
|
|
347
|
+
byteDelta: otherBytes - baseBytes,
|
|
348
|
+
identical: onlyBase.length === 0 && onlyOther.length === 0 && changed.length === 0,
|
|
349
|
+
},
|
|
350
|
+
files: { onlyBase, onlyOther, changed, root, basePaths },
|
|
351
|
+
// `unchanged` lends a version-only change its (equal) sizes.
|
|
352
|
+
packages: { onlyBase: pkgOnlyBase, onlyOther: pkgOnlyOther, changed: pkgChanged, unchanged: pkgUnchanged },
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The versions' part (bundle-compare-versions.js), as one comparison reads it.
|
|
357
|
+
export { comparePackages, compareSemver, computeVersionUpdates, versionDirection } from './bundle-compare-versions.js'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { bundleFilesAsMap } from './bundle-sources.js'
|
|
2
|
+
|
|
3
|
+
// Use the union of named trees. A tree absent from one stasis bundle contains
|
|
4
|
+
// no files there; falling back to all files would invent additions/removals.
|
|
5
|
+
// A bundle that names no trees (a sourcemap) is compared whole under any of
|
|
6
|
+
// them. Unlike a single-bundle graph, retain a scope that includes every file
|
|
7
|
+
// on one side.
|
|
8
|
+
export function bundleCompareScopes(...details) {
|
|
9
|
+
const names = new Set()
|
|
10
|
+
for (const item of details) {
|
|
11
|
+
const files = bundleFilesAsMap(item)
|
|
12
|
+
for (const [name, paths] of Object.entries(item?.bundle?.reason ?? {})) {
|
|
13
|
+
if (Array.isArray(paths) && paths.some(path => files.has(path))) names.add(name)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// A scope is only useful as a filter if it changes the files compared on
|
|
17
|
+
// some side. Keep every scope when at least one does, otherwise hide the
|
|
18
|
+
// selector, as single-bundle views do (see common/bundle-reasons.js).
|
|
19
|
+
const narrows = name => details.some(item => {
|
|
20
|
+
if (item?.kind !== 'stasis') return false
|
|
21
|
+
const files = bundleFilesAsMap(item)
|
|
22
|
+
const paths = item?.bundle?.reason?.[name]
|
|
23
|
+
return new Set(Array.isArray(paths) ? paths.filter(path => files.has(path)) : []).size < files.size
|
|
24
|
+
})
|
|
25
|
+
if (![...names].some(narrows)) return []
|
|
26
|
+
return [...names].toSorted().map(name => ({ id: `reason:${name}`, label: name }))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function bundleCompareFiles(details, scope = '') {
|
|
30
|
+
const files = bundleFilesAsMap(details)
|
|
31
|
+
if (!scope || details?.kind !== 'stasis') return files
|
|
32
|
+
const paths = details?.bundle?.reason?.[scope.replace(/^reason:/u, '')]
|
|
33
|
+
const selected = new Set(Array.isArray(paths) ? paths : [])
|
|
34
|
+
return new Map([...files].filter(([path]) => selected.has(path)))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A resolution's identity: its importer, specifier, conditions (import
|
|
38
|
+
// attributes included), and Metro platform.
|
|
39
|
+
export function resolutionKey(parent, specifier, conditions, platform) {
|
|
40
|
+
return JSON.stringify([parent, specifier, conditions, platform])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Keep the full resolution identity: collapsing to graph edges would lose
|
|
44
|
+
// specifier, condition/import-attribute, and Metro platform changes.
|
|
45
|
+
export function bundleCompareResolutions(details, scope = '') {
|
|
46
|
+
const result = new Map()
|
|
47
|
+
if (details?.kind !== 'stasis' || !details.bundle) return result
|
|
48
|
+
const files = scope ? bundleFilesAsMap(details) : null
|
|
49
|
+
const selected = scope ? bundleCompareFiles(details, scope) : null
|
|
50
|
+
for (const [conditions, byParent] of details.bundle.imports) {
|
|
51
|
+
for (const [parent, specifiers] of byParent) {
|
|
52
|
+
for (const [specifier, resolved] of specifiers) {
|
|
53
|
+
const targets = typeof resolved === 'string' ? [[null, resolved]] : resolved
|
|
54
|
+
for (const [platform, target] of targets) {
|
|
55
|
+
// Include imports from scoped files, plus imports into the scope
|
|
56
|
+
// from uncaptured parents (e.g. an app in a dependencies-only bundle).
|
|
57
|
+
if (selected && !selected.has(parent) && (files.has(parent) || !selected.has(target))) continue
|
|
58
|
+
const key = resolutionKey(parent, specifier, conditions, platform)
|
|
59
|
+
result.set(key, { key, parent, specifier, conditions, platform, target })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
}
|