@preventive/triage 1.0.0-alpha.37 → 1.0.0-alpha.39
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/README.md +16 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +6 -2
- package/common/bundle-metadata.js +63 -20
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-source-links.js +7 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +3 -1
- package/common/bundle-sources.js +47 -19
- package/common/code-language.js +16 -2
- package/common/default-scan-models.ts +2 -1
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/github-pr.ts +1 -1
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +1 -5
- package/common/managed/routes.js +123 -12
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +1 -1
- 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/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-45WIN3SW.js +1 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-6C76GAXK.js +1 -0
- package/out/chunk-7ZKDKGAH.js +1 -0
- package/out/chunk-ABW5FVSN.js +1 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-F4OMDNEY.js +1 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GMFXFEUA.js +2 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-HIV7SVI7.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-JVBB4LZX.js +1 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QSLWGJBQ.js +223 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-TINZZKP5.js +33 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-XNGEQO7Y.js +2 -0
- package/out/chunk-XRWAV3DX.js +7 -0
- package/out/chunk-XYDGU6BM.js +51 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZPD5R5RP.js +3 -0
- package/out/client-managed-import.js +3 -87
- package/out/client-managed.js +26 -338
- package/out/client-sync.js +8 -14
- package/out/graph.js +17 -30
- package/out/index.html +34 -0
- package/out/prism.js +6 -2
- package/out/terminal.js +241 -257
- package/out/view.css +1 -1
- package/out/view.js +76 -520
- package/package.json +89 -7
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +358 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +142 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +219 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-e2e/http.ts +7 -7
- package/server-e2e/npm-proxy.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +2 -2
- package/server-e2e/objstore/handlers.ts +1 -1
- package/server-e2e/objstore/tokens.ts +1 -1
- package/server-e2e/sign.ts +1 -1
- package/server-managed/bundle-advisories.ts +74 -12
- package/server-managed/bundle-cache.ts +37 -4
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-summary-cache.ts +24 -10
- package/server-managed/config.ts +4 -0
- package/server-managed/db-methods.ts +98 -11
- package/server-managed/db-neon.ts +39 -4
- package/server-managed/db-schema.ts +14 -2
- package/server-managed/db-table-names.ts +1 -1
- package/server-managed/db.ts +7 -3
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +9 -3
- package/server-managed/github-errors.ts +21 -2
- package/server-managed/github-issue-oauth.ts +5 -6
- package/server-managed/github-metadata.ts +115 -0
- package/server-managed/github-oauth.ts +38 -2
- package/server-managed/http-response.ts +26 -0
- package/server-managed/http.ts +952 -53
- package/server-managed/index.ts +11 -5
- package/server-managed/kept.ts +28 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +78 -7
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/report-sources.ts +13 -2
- package/server-managed/repository-browser.ts +19 -5
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/session.ts +16 -2
- package/server-managed/sql.ts +5 -2
- package/server-managed/storage-encryption.ts +5 -3
- package/server-managed/storage-payload.ts +4 -0
- package/server-managed/storage-stores.ts +2 -0
- package/server-managed/storage.ts +6 -3
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-feed.ts +1 -1
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +25 -4
- package/server-managed/upstream-cache.ts +147 -5
- package/server-managed/view-as.ts +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @preventive/triage
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.org/package/@preventive/triage)
|
|
4
|
+
[](#license)
|
|
5
|
+
|
|
6
|
+
Client & relay server for triaging automated reports.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npx -p @preventive/triage triage-server --mode e2e # or managed, managed-e2e, e2e-managed
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`triage-server --help` lists the environment each mode reads.
|
|
13
|
+
|
|
14
|
+
## License
|
|
15
|
+
|
|
16
|
+
[MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE), at your option.
|
|
@@ -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,522 @@
|
|
|
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 Overview's package rows: each package that differs, once, from its
|
|
357
|
+
// sizes (computeBundleDiff's `packages`) and the versions each bundle
|
|
358
|
+
// records for it (`bundlePackageVersions`). A package only one side has is
|
|
359
|
+
// removed or added, with its size and versions there; one both have is
|
|
360
|
+
// changed when its size or its versions moved, with both sides of each —
|
|
361
|
+
// own source and workspace modules carry sizes alone.
|
|
362
|
+
export function comparePackages(packages, baseVersions, otherVersions) {
|
|
363
|
+
const sizes = new Map()
|
|
364
|
+
for (const r of packages.onlyBase) sizes.set(r.pkg, { base: r.bytes, other: null })
|
|
365
|
+
for (const r of packages.onlyOther) sizes.set(r.pkg, { base: null, other: r.bytes })
|
|
366
|
+
for (const r of packages.changed) sizes.set(r.pkg, { base: r.baseBytes, other: r.otherBytes })
|
|
367
|
+
for (const r of packages.unchanged ?? []) sizes.set(r.pkg, { base: r.bytes, other: r.bytes })
|
|
368
|
+
const versionsOf = (map, pkg) => [...map.get(pkg) ?? []].toSorted(compareSemver)
|
|
369
|
+
const moved = new Set([...packages.onlyBase, ...packages.onlyOther, ...packages.changed].map(r => r.pkg))
|
|
370
|
+
for (const pkg of new Set([...baseVersions.keys(), ...otherVersions.keys()])) {
|
|
371
|
+
if (!sameVersions(versionsOf(baseVersions, pkg), versionsOf(otherVersions, pkg))) moved.add(pkg)
|
|
372
|
+
}
|
|
373
|
+
const added = [], changed = [], removed = []
|
|
374
|
+
for (const pkg of moved) {
|
|
375
|
+
const size = sizes.get(pkg) ?? { base: null, other: null }
|
|
376
|
+
const after = versionsOf(otherVersions, pkg), before = versionsOf(baseVersions, pkg)
|
|
377
|
+
const onBase = size.base !== null || before.length > 0
|
|
378
|
+
const onOther = size.other !== null || after.length > 0
|
|
379
|
+
if (onBase && !onOther) removed.push({ pkg, bytes: size.base, versions: before })
|
|
380
|
+
else if (onOther && !onBase) added.push({ pkg, bytes: size.other, versions: after })
|
|
381
|
+
else {
|
|
382
|
+
changed.push({
|
|
383
|
+
pkg, baseBytes: size.base, otherBytes: size.other,
|
|
384
|
+
delta: size.base !== null && size.other !== null ? size.other - size.base : null,
|
|
385
|
+
baseVersions: before, otherVersions: after,
|
|
386
|
+
direction: before.length > 0 && after.length > 0 && !sameVersions(before, after) ? versionDirection(before, after) : null,
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
|
|
391
|
+
return { removed: removed.toSorted(byPkg), added: added.toSorted(byPkg), changed: changed.toSorted(byPkg) }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Split a version into its dotted-numeric core and its prerelease tail,
|
|
395
|
+
// dropping any `+build` metadata (semver ignores it for precedence).
|
|
396
|
+
// `1.2.3-rc.1+sha` → { core: '1.2.3', pre: 'rc.1' }.
|
|
397
|
+
function splitVersion(v) {
|
|
398
|
+
const s = String(v).trim()
|
|
399
|
+
const noBuild = s.split('+', 1)[0]
|
|
400
|
+
const dash = noBuild.indexOf('-')
|
|
401
|
+
return dash === -1
|
|
402
|
+
? { core: noBuild, pre: '' }
|
|
403
|
+
: { core: noBuild.slice(0, dash), pre: noBuild.slice(dash + 1) }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Lightweight semver-ish comparison, enough to sort versions ascending
|
|
407
|
+
// and label an update ↑/↓. Compares the dotted-numeric core part by
|
|
408
|
+
// part (a missing part counts as 0); on an equal core a release ranks
|
|
409
|
+
// above a prerelease (`1.0.0` > `1.0.0-rc.1`) and two prereleases
|
|
410
|
+
// compare by their dot identifiers (numeric where both are numeric,
|
|
411
|
+
// else lexical) per the semver precedence rules. NOT a full
|
|
412
|
+
// implementation — a non-numeric core segment falls back to a string
|
|
413
|
+
// compare of the whole version so ordering stays total and stable.
|
|
414
|
+
export function compareSemver(a, b) {
|
|
415
|
+
if (a === b) return 0
|
|
416
|
+
const A = splitVersion(a)
|
|
417
|
+
const B = splitVersion(b)
|
|
418
|
+
const ap = A.core.split('.')
|
|
419
|
+
const bp = B.core.split('.')
|
|
420
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
421
|
+
const x = Number(ap[i] ?? 0)
|
|
422
|
+
const y = Number(bp[i] ?? 0)
|
|
423
|
+
if (Number.isNaN(x) || Number.isNaN(y)) return a < b ? -1 : 1
|
|
424
|
+
if (x !== y) return x < y ? -1 : 1
|
|
425
|
+
}
|
|
426
|
+
if (A.pre === B.pre) return 0
|
|
427
|
+
// A core with no prerelease is the higher (released) version.
|
|
428
|
+
if (!A.pre) return 1
|
|
429
|
+
if (!B.pre) return -1
|
|
430
|
+
const ai = A.pre.split('.')
|
|
431
|
+
const bi = B.pre.split('.')
|
|
432
|
+
for (let i = 0; i < Math.max(ai.length, bi.length); i++) {
|
|
433
|
+
const x = ai[i]
|
|
434
|
+
const y = bi[i]
|
|
435
|
+
if (x === undefined) return -1
|
|
436
|
+
if (y === undefined) return 1
|
|
437
|
+
if (x === y) continue
|
|
438
|
+
const xNum = /^\d+$/u.test(x)
|
|
439
|
+
const yNum = /^\d+$/u.test(y)
|
|
440
|
+
// Numeric identifiers always rank below non-numeric ones (semver),
|
|
441
|
+
// and compare numerically against each other.
|
|
442
|
+
if (xNum && yNum) return Number(x) < Number(y) ? -1 : 1
|
|
443
|
+
if (xNum !== yNum) return xNum ? -1 : 1
|
|
444
|
+
return x < y ? -1 : 1
|
|
445
|
+
}
|
|
446
|
+
return 0
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// True when two ascending-sorted version arrays hold the same members.
|
|
450
|
+
function sameVersions(a, b) {
|
|
451
|
+
if (a.length !== b.length) return false
|
|
452
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
453
|
+
return true
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Direction of a version move, from the versions dropped and the versions
|
|
457
|
+
// taken on: 'up' when everything dropped is older than everything taken
|
|
458
|
+
// on (`1.0.0` → `2.0.0`), or, with only drops or only additions, than the
|
|
459
|
+
// versions kept (`1.0.0, 2.0.0` → `2.0.0` drops the older copy, and a newer
|
|
460
|
+
// copy added beside the old is up too); 'down' the other way round; and
|
|
461
|
+
// 'changed' when the moves fall between (`1.0.0, 1.2.0, 2.0.0` → `1.0.0,
|
|
462
|
+
// 2.0.0`).
|
|
463
|
+
export function versionDirection(baseVersions, otherVersions) {
|
|
464
|
+
const dropped = baseVersions.filter(v => !otherVersions.includes(v)).toSorted(compareSemver)
|
|
465
|
+
const taken = otherVersions.filter(v => !baseVersions.includes(v)).toSorted(compareSemver)
|
|
466
|
+
const kept = baseVersions.filter(v => otherVersions.includes(v)).toSorted(compareSemver)
|
|
467
|
+
const [from, to] = dropped.length > 0 && taken.length > 0 ? [dropped, taken]
|
|
468
|
+
: dropped.length > 0 ? [dropped, kept] : [kept, taken]
|
|
469
|
+
if (from.length === 0 || to.length === 0) return 'changed'
|
|
470
|
+
// Every version of `a` older than every version of `b`.
|
|
471
|
+
const before = (a, b) => compareSemver(a.at(-1), b[0]) < 0
|
|
472
|
+
return before(from, to) ? 'up' : before(to, from) ? 'down' : 'changed'
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Diff two `Map<packageName, Set<version>>` inventories (as
|
|
476
|
+
// `bundlePackageVersions` returns for each bundle) into the dependency
|
|
477
|
+
// version changes between them. Mirrors the file/package diff framing:
|
|
478
|
+
// `base` is the open bundle, `other` the one compared against.
|
|
479
|
+
//
|
|
480
|
+
// {
|
|
481
|
+
// updated: [{ pkg, baseVersions: [...], otherVersions: [...],
|
|
482
|
+
// direction: 'up' | 'down' | 'changed' }],
|
|
483
|
+
// added: [{ pkg, versions: [...] }], // dependency only in other
|
|
484
|
+
// removed: [{ pkg, versions: [...] }], // dependency only in base
|
|
485
|
+
// totals: { baseDeps, otherDeps }, // distinct package counts
|
|
486
|
+
// }
|
|
487
|
+
//
|
|
488
|
+
// `updated` is the headline — a package present on BOTH sides whose set
|
|
489
|
+
// of versions changed (the "what did this bump pull in?" answer);
|
|
490
|
+
// packages whose versions are byte-for-byte the same are dropped.
|
|
491
|
+
// Version arrays are sorted ascending; the three lists are sorted by
|
|
492
|
+
// package name so the dependency list reads alphabetically.
|
|
493
|
+
export function computeVersionUpdates(baseVersions, otherVersions) {
|
|
494
|
+
const updated = []
|
|
495
|
+
const added = []
|
|
496
|
+
const removed = []
|
|
497
|
+
const names = new Set([...baseVersions.keys(), ...otherVersions.keys()])
|
|
498
|
+
for (const pkg of names) {
|
|
499
|
+
const bSet = baseVersions.get(pkg)
|
|
500
|
+
const oSet = otherVersions.get(pkg)
|
|
501
|
+
if (bSet && oSet) {
|
|
502
|
+
const b = [...bSet].toSorted(compareSemver)
|
|
503
|
+
const o = [...oSet].toSorted(compareSemver)
|
|
504
|
+
if (sameVersions(b, o)) continue
|
|
505
|
+
updated.push({ pkg, baseVersions: b, otherVersions: o, direction: versionDirection(b, o) })
|
|
506
|
+
} else if (bSet) {
|
|
507
|
+
removed.push({ pkg, versions: [...bSet].toSorted(compareSemver) })
|
|
508
|
+
} else {
|
|
509
|
+
added.push({ pkg, versions: [...oSet].toSorted(compareSemver) })
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
|
|
513
|
+
updated.sort(byPkg)
|
|
514
|
+
added.sort(byPkg)
|
|
515
|
+
removed.sort(byPkg)
|
|
516
|
+
return {
|
|
517
|
+
updated,
|
|
518
|
+
added,
|
|
519
|
+
removed,
|
|
520
|
+
totals: { baseDeps: baseVersions.size, otherDeps: otherVersions.size },
|
|
521
|
+
}
|
|
522
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import type { SourceMap } from '@preventive/sourcemap'
|
|
2
|
+
import type { SourcemapEdge } from './bundle-sourcemap.js'
|
|
1
3
|
export const BUNDLE_METADATA_VERSION: number
|
|
4
|
+
export const FILE_HASH: RegExp
|
|
2
5
|
export interface BundleIdentity { integrity: string; kind: string | null; size: number }
|
|
3
|
-
export interface BundleDetails extends BundleIdentity { bundle?: unknown; json?: unknown }
|
|
6
|
+
export interface BundleDetails extends BundleIdentity { bundle?: unknown; json?: unknown; map?: SourceMap; edges?: readonly SourcemapEdge[] }
|
|
4
7
|
export function parseBundleContents(text: string, identity: BundleIdentity): BundleDetails
|
|
5
8
|
export interface BundleMetadata extends Record<string, unknown> {
|
|
6
9
|
files: [string, number | null, string | null, number | null][]
|
|
7
10
|
codeStats: { files: number; lines: number; bytes: number }
|
|
8
11
|
}
|
|
9
12
|
export function createBundleMetadata(details: BundleDetails): Promise<BundleMetadata>
|
|
10
|
-
export function
|
|
13
|
+
export function parseBundleMetadata(data: unknown, integrity: string): BundleDetails
|
|
14
|
+
export function createBundleSummary(details: BundleDetails, metadata?: BundleMetadata): { files: number; codeFiles: number; lines: number; commit?: string; stasisVersion?: number }
|