@preventive/triage 1.0.0-alpha.36 → 1.0.0-alpha.38
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-4XCDFGDP.js +10 -0
- package/out/chunk-5FKRIJGF.js +1 -0
- package/out/chunk-6Z5XLSPJ.js +1 -0
- package/out/chunk-ABW5FVSN.js +1 -0
- package/out/chunk-BJKRVROW.js +1 -0
- package/out/chunk-BYUEDJOM.js +7 -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-DCQX2J3C.js +223 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-F4OMDNEY.js +1 -0
- package/out/chunk-FU4MTW7F.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-JAKWIZWL.js +2 -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-OJ5DGP5E.js +33 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-SD2WJAXN.js +48 -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-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 +88 -7
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +164 -0
- package/risk/files/runs-code.js +329 -0
- package/risk/files/script.js +112 -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 +222 -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 +33 -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 +68 -11
- package/server-managed/bundle-build-worker.js +1 -1
- package/server-managed/bundle-build.ts +6 -2
- 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-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 +943 -52
- 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 +413 -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
|
@@ -4,6 +4,7 @@ import { Bundle } from '@exodus/stasis-core/bundle'
|
|
|
4
4
|
import { computeFileHash } from '@preventive/report'
|
|
5
5
|
import { utf8ByteLength } from './utf8.js'
|
|
6
6
|
import { bundleFileSizes, bundleSourcesAsMap, bundleUnsizedFiles } from './bundle-sources.js'
|
|
7
|
+
import { bundleSourcemapEdges, parseSourcemap, sourcemapEntries } from './bundle-sourcemap.js'
|
|
7
8
|
|
|
8
9
|
// Version 2 sizes every file by its bytes, resources included. A version 1
|
|
9
10
|
// index sized by what the source map held when it was written: before the
|
|
@@ -13,7 +14,11 @@ import { bundleFileSizes, bundleSourcesAsMap, bundleUnsizedFiles } from './bundl
|
|
|
13
14
|
// hashes alone and rebuilt on open.
|
|
14
15
|
// Version 3 also retains the bundle's repository and package origin metadata.
|
|
15
16
|
// Version 4 retains dependency repositories as well.
|
|
16
|
-
|
|
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
|
|
17
22
|
const INDEX_VERSION = BUNDLE_METADATA_VERSION
|
|
18
23
|
const hashJobs = new WeakMap()
|
|
19
24
|
const SOURCE_TABS = new Set(['terminal', 'code', 'search', 'compare'])
|
|
@@ -22,6 +27,9 @@ export function bundleNeedsSources(tab, sourceFile = null) {
|
|
|
22
27
|
return Boolean(sourceFile) || SOURCE_TABS.has(tab)
|
|
23
28
|
}
|
|
24
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
|
+
|
|
25
33
|
// Bound outstanding WebCrypto jobs: serial awaits incur one task round-trip
|
|
26
34
|
// per file, while an unbounded Promise.all holds every encoded body at once.
|
|
27
35
|
export function computeBundleFileHashes(details) {
|
|
@@ -42,13 +50,34 @@ export function computeBundleFileHashes(details) {
|
|
|
42
50
|
return job
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
// Count
|
|
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.
|
|
46
60
|
// The source map intentionally excludes non-text resources, so a null entry
|
|
47
61
|
// remains the metadata marker for images, fonts, and other binary payloads.
|
|
48
62
|
export function bundleSourceLineCount(content) {
|
|
49
|
-
if (typeof content !== 'string'
|
|
50
|
-
const
|
|
51
|
-
|
|
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)]))
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
function mapObject(value) {
|
|
@@ -64,19 +93,23 @@ function mapObject(value) {
|
|
|
64
93
|
export async function createBundleMetadata(details) {
|
|
65
94
|
const hashes = await computeBundleFileHashes(details)
|
|
66
95
|
const sizes = bundleFileSizes(details)
|
|
67
|
-
const sourceLines =
|
|
68
|
-
.map(([path, content]) => [path, bundleSourceLineCount(content)]))
|
|
96
|
+
const sourceLines = bundleLineCounts(details)
|
|
69
97
|
const result = { version: INDEX_VERSION, integrity: details.integrity, kind: details.kind, size: details.size, codeStats: bundleCodeStats(sourceLines, sizes),
|
|
70
98
|
files: [...sizes].map(([path, size]) => [path, size, hashes.get(path) ?? null, sourceLines.get(path) ?? null]) }
|
|
71
99
|
const unsized = bundleUnsizedFiles(details)
|
|
72
100
|
if (unsized.size > 0) result.unsized = [...unsized]
|
|
73
101
|
if (details.kind === 'sourcemap') {
|
|
74
|
-
const { version, file, sourceRoot, names
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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])
|
|
80
113
|
} else {
|
|
81
114
|
const b = details.bundle
|
|
82
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] }
|
|
@@ -109,14 +142,16 @@ export function createBundleSummary(details, metadata) {
|
|
|
109
142
|
const commit = details.kind === 'stasis' ? bundleCommitHash(details.bundle.repo?.commit) : null
|
|
110
143
|
return { files: files.length, lines,
|
|
111
144
|
codeFiles: files.filter(([path]) => !['resource:base64', 'directory'].includes(formats?.get(path))).length,
|
|
112
|
-
...(commit ? { commit } : {})
|
|
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 } : {}) }
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
// `stale` marks an index this version did not write: its hashes still
|
|
116
151
|
// answer report lookups, but an open rebuilds it for current sizes, line
|
|
117
152
|
// counts and origin metadata.
|
|
118
153
|
export function parseBundleMetadata(data, integrity) {
|
|
119
|
-
if (![1, 2, 3, INDEX_VERSION].includes(data?.version) || data.integrity !== integrity || !['stasis', 'sourcemap'].includes(data.kind)
|
|
154
|
+
if (![1, 2, 3, 4, 5, INDEX_VERSION].includes(data?.version) || data.integrity !== integrity || !['stasis', 'sourcemap'].includes(data.kind)
|
|
120
155
|
|| !Number.isSafeInteger(data.size) || data.size < 0 || !Array.isArray(data.files)) throw new Error('Invalid bundle metadata')
|
|
121
156
|
const stale = data.version !== INDEX_VERSION
|
|
122
157
|
const legacySizes = data.version === 1
|
|
@@ -126,7 +161,7 @@ export function parseBundleMetadata(data, integrity) {
|
|
|
126
161
|
if (!Array.isArray(row) || (row.length !== 4 && !(legacySizes && row.length === 3))) throw new Error('Invalid bundle metadata file')
|
|
127
162
|
const [path, size, hash, lines] = row
|
|
128
163
|
if (typeof path !== 'string' || fileSizes.has(path) || (size !== null && (!Number.isSafeInteger(size) || size < 0))
|
|
129
|
-
|| (hash !== null && (typeof hash !== 'string' ||
|
|
164
|
+
|| (hash !== null && (typeof hash !== 'string' || !FILE_HASH.test(hash)))
|
|
130
165
|
|| (size === null && hash !== null) || (legacySizes && size !== null && hash === null)
|
|
131
166
|
|| (lines !== undefined && lines !== null && (!Number.isSafeInteger(lines) || lines < 0))) throw new Error('Invalid bundle metadata file')
|
|
132
167
|
fileSizes.set(path, size)
|
|
@@ -159,6 +194,8 @@ export function parseBundleMetadata(data, integrity) {
|
|
|
159
194
|
details.json = data.json
|
|
160
195
|
details.sourceSizes = data.sourceSizes
|
|
161
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)
|
|
162
199
|
}
|
|
163
200
|
if (data.codeStats === undefined) details.codeStats = bundleCodeStats(lineCounts, fileSizes)
|
|
164
201
|
else {
|
|
@@ -172,12 +209,18 @@ export function parseBundleMetadata(data, integrity) {
|
|
|
172
209
|
return details
|
|
173
210
|
}
|
|
174
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
|
+
|
|
175
221
|
// HTTP content encoding handles compression before this shared JSON parser.
|
|
176
222
|
export function parseBundleContents(text, { integrity, kind, size }) {
|
|
177
223
|
if (kind === 'stasis') return { integrity, kind, size, bundle: Bundle.parse(text) }
|
|
178
224
|
if (kind !== 'sourcemap') throw new Error('Unsupported bundle format')
|
|
179
|
-
|
|
180
|
-
if (!Array.isArray(json?.sources) || json.sources.some(path => typeof path !== 'string')
|
|
181
|
-
|| (json.sourcesContent !== undefined && (!Array.isArray(json.sourcesContent) || json.sourcesContent.some(content => content !== null && typeof content !== 'string')))) throw new Error('Invalid sourcemap')
|
|
182
|
-
return { integrity, kind, size, json }
|
|
225
|
+
return { integrity, kind, size, ...parseSourcemap(text) }
|
|
183
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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bundleFilesAsMap, bundlePackageDirs, bundleSourcesAsMap } from './bundle-sources.js'
|
|
2
|
+
import { bundleSourcemapSpecifiers } from './bundle-sourcemap.js'
|
|
2
3
|
|
|
3
4
|
const indexes = new WeakMap()
|
|
4
5
|
const manifests = new WeakMap()
|
|
@@ -130,6 +131,7 @@ function resolvePackageImport(scope, specifier, sources, index, recorded, direct
|
|
|
130
131
|
// Merge conditions before linking. When exporting a restricted source set,
|
|
131
132
|
// include only visible parents and replace every unavailable target with null:
|
|
132
133
|
// its path must not leak, and relative fallback must still remain blocked.
|
|
134
|
+
// A sourcemap's are what its edges say each specifier names (bundle-sourcemap.js).
|
|
133
135
|
export function bundleSourceImports(details, sources = bundleSourcesAsMap(details)) {
|
|
134
136
|
const imports = new Map()
|
|
135
137
|
if (details?.kind === 'stasis') {
|
|
@@ -146,6 +148,11 @@ export function bundleSourceImports(details, sources = bundleSourcesAsMap(detail
|
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
}
|
|
151
|
+
} else if (details?.kind === 'sourcemap') {
|
|
152
|
+
for (const [parent, specifiers] of bundleSourcemapSpecifiers(details)) {
|
|
153
|
+
if (!sources.has(parent)) continue
|
|
154
|
+
imports.set(parent, new Map([...specifiers].map(([specifier, target]) => [specifier, sources.has(target) ? target : null])))
|
|
155
|
+
}
|
|
149
156
|
}
|
|
150
157
|
return imports
|
|
151
158
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SourceMap } from '@preventive/sourcemap'
|
|
2
|
+
import type { Edge } from '@preventive/sourcemap/edges.js'
|
|
3
|
+
import type { BundleDetails } from './bundle-metadata.js'
|
|
4
|
+
export function parseSourcemap(text: string): { json: { version: unknown; file: unknown; sourceRoot: unknown }; map: SourceMap; namesCount: number | null }
|
|
5
|
+
export function sourcemapEntries(details: BundleDetails): [string, string | null][]
|
|
6
|
+
export type SourcemapEdge = [from: string, to: string] | [from: string, to: string, specifier: string]
|
|
7
|
+
export function sourcemapEdges(map: SourceMap, read: (map: SourceMap) => { edges: Edge[] }): SourcemapEdge[]
|
|
8
|
+
export function bundleSourcemapEdges(details: BundleDetails): readonly SourcemapEdge[]
|
|
9
|
+
export function bundleSourcemapImports(details: BundleDetails): Map<string, Set<string>>
|
|
10
|
+
export function bundleSourcemapSpecifiers(details: BundleDetails): Map<string, Map<string, string>>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A sourcemap bundle as @preventive/sourcemap reads it: plain or indexed,
|
|
2
|
+
// each file once, with the edges between its files, which the graph draws
|
|
3
|
+
// and Code links follow by the specifier of each import. A file keeps the key
|
|
4
|
+
// it had before the package read it, the spelling the map lists it under
|
|
5
|
+
// (`sourceRoot` in front), so Code links, hashes and reports that name one
|
|
6
|
+
// still find it.
|
|
7
|
+
//
|
|
8
|
+
// Edges take a parser for most maps. The client goes without one: it reads
|
|
9
|
+
// them with edges-lite.js, which knows Metro's maps alone. The server reads
|
|
10
|
+
// every map's with oxc-parser (server-managed/bundle-cache.ts) and keeps
|
|
11
|
+
// them in the bundle's metadata, which a client takes over its own.
|
|
12
|
+
import { readSourceMap } from '@preventive/sourcemap'
|
|
13
|
+
import { bundleEdges as metroEdges } from '@preventive/sourcemap/edges-lite.js'
|
|
14
|
+
|
|
15
|
+
// The map as read, and what the Overview shows of the JSON: its header,
|
|
16
|
+
// and how many names it maps. The JSON itself is not kept, its mappings
|
|
17
|
+
// read and its sources the map's files.
|
|
18
|
+
export function parseSourcemap(text) {
|
|
19
|
+
// The `)]}'` line that guards a map against being run, which readSourceMap
|
|
20
|
+
// skips in text it parses itself.
|
|
21
|
+
const json = JSON.parse(text.replace(/^\)\]\}'[^\n]*\n/u, ''))
|
|
22
|
+
const map = readSourceMap(json)
|
|
23
|
+
const namesCount = json.sections
|
|
24
|
+
? json.sections.reduce((count, section) => count + (section.map.names?.length ?? 0), 0)
|
|
25
|
+
: json.names?.length ?? null
|
|
26
|
+
return { json: { version: json.version, file: json.file, sourceRoot: json.sourceRoot }, map, namesCount }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The map's files as `[key, content or null]`, in its order. Details with
|
|
30
|
+
// no map read (the npm viewer's, a map's arrays as written) list what their
|
|
31
|
+
// `sources` does, a repeated path repeated.
|
|
32
|
+
export function sourcemapEntries(details) {
|
|
33
|
+
if (details.map) return details.map.files.flatMap((file) => file.source === null ? [] : [[file.source, file.content]])
|
|
34
|
+
const contents = details.json?.sourcesContent ?? []
|
|
35
|
+
return (details.json?.sources ?? []).map((path, i) => [path, contents[i] ?? null])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `read`'s edges between files of the map, as rows by key: `[from, to]`,
|
|
39
|
+
// and `[from, to, specifier]` for an import its specifier names. An edge to
|
|
40
|
+
// no file of it (a package left out, a builtin) leads nowhere here, and one
|
|
41
|
+
// to its own file is none, which metadata refuses to hold.
|
|
42
|
+
export function sourcemapEdges(map, read) {
|
|
43
|
+
const rows = new Map()
|
|
44
|
+
for (const { from, to, specifier } of read(map).edges) {
|
|
45
|
+
if (from.source === null || to?.source == null || to.source === from.source) continue
|
|
46
|
+
const row = typeof specifier === 'string' ? [from.source, to.source, specifier] : [from.source, to.source]
|
|
47
|
+
rows.set(row.join('\0'), row)
|
|
48
|
+
}
|
|
49
|
+
return [...rows.values()]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// edges-lite.js throws for a map that is not Metro's, or one with no
|
|
53
|
+
// sourcesContent to read: one with no edges the client can tell.
|
|
54
|
+
function liteEdges(map) {
|
|
55
|
+
try { return metroEdges(map) } catch { return { edges: [] } }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const NO_EDGES = Object.freeze([])
|
|
59
|
+
|
|
60
|
+
// The edges a view reads: those the metadata carries, else what the client
|
|
61
|
+
// reads of its own map. None without a map.
|
|
62
|
+
export function bundleSourcemapEdges(details) {
|
|
63
|
+
if (details?.edges) return details.edges
|
|
64
|
+
if (!details?.map) return NO_EDGES
|
|
65
|
+
details.edges = sourcemapEdges(details.map, liteEdges)
|
|
66
|
+
return details.edges
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const importsCache = new WeakMap()
|
|
70
|
+
const specifiersCache = new WeakMap()
|
|
71
|
+
|
|
72
|
+
// The graph's: `Map<from, Set<to>>`.
|
|
73
|
+
export function bundleSourcemapImports(details) {
|
|
74
|
+
const rows = bundleSourcemapEdges(details)
|
|
75
|
+
if (!importsCache.has(rows)) {
|
|
76
|
+
const imports = new Map()
|
|
77
|
+
for (const [from, to] of rows) {
|
|
78
|
+
if (!imports.has(from)) imports.set(from, new Set())
|
|
79
|
+
imports.get(from).add(to)
|
|
80
|
+
}
|
|
81
|
+
importsCache.set(rows, imports)
|
|
82
|
+
}
|
|
83
|
+
return importsCache.get(rows)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Code links': the file each import's specifier names, `Map<from,
|
|
87
|
+
// Map<specifier, to>>`.
|
|
88
|
+
export function bundleSourcemapSpecifiers(details) {
|
|
89
|
+
const rows = bundleSourcemapEdges(details)
|
|
90
|
+
if (!specifiersCache.has(rows)) {
|
|
91
|
+
const specifiers = new Map()
|
|
92
|
+
for (const [from, to, specifier] of rows) {
|
|
93
|
+
if (specifier === undefined) continue
|
|
94
|
+
if (!specifiers.has(from)) specifiers.set(from, new Map())
|
|
95
|
+
specifiers.get(from).set(specifier, to)
|
|
96
|
+
}
|
|
97
|
+
specifiersCache.set(rows, specifiers)
|
|
98
|
+
}
|
|
99
|
+
return specifiersCache.get(rows)
|
|
100
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BundleDetails } from './bundle-metadata.js'
|
|
2
2
|
export function bundleSourcesAsMap(details: BundleDetails): Map<string, string>
|
|
3
|
+
export function bundleSourceLines(content: string): number
|
|
4
|
+
export function bundleSourceOrder(sources: Map<string, string>): { paths: string[]; numbers: Map<string, number> }
|
|
3
5
|
export function bundleFilesAsMap(details: BundleDetails): Map<string, string | { format: 'base64'; data: string }>
|
|
4
6
|
export function bundlePackageDirs(details: BundleDetails): Map<string, string> | null
|
|
5
|
-
export function bundlePackageVersions(details: BundleDetails, paths?: Iterable<string> | null): Map<string, Set<string>>
|
|
7
|
+
export function bundlePackageVersions(details: BundleDetails, paths?: Iterable<string> | null, keyOf?: (dir: string, info: { name: string; version: string }) => string): Map<string, Set<string>>
|
package/common/bundle-sources.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Shared helper: given a parsed bundle `details`, return a
|
|
2
2
|
// Map<file, content-string> of its source files. Stasis bundles
|
|
3
3
|
// expose sources via `@exodus/stasis-core`'s `Bundle.sources` getter
|
|
4
|
-
// (flat Map<projectRelPath, content> from `.modules`); sourcemaps
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// omitted are skipped.
|
|
4
|
+
// (flat Map<projectRelPath, content> from `.modules`); sourcemaps list
|
|
5
|
+
// their files as bundle-sourcemap.js reads them. Sourcemap sources whose
|
|
6
|
+
// `sourcesContent[i]` was omitted are skipped.
|
|
8
7
|
//
|
|
9
8
|
// What a stasis bundle records is not all source: `Bundle.formats`
|
|
10
9
|
// marks each entry, and three of those formats are files only in the
|
|
@@ -28,6 +27,7 @@
|
|
|
28
27
|
// a new bundle kind or field handled here is visible to both.
|
|
29
28
|
|
|
30
29
|
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
30
|
+
import { sourcemapEntries } from './bundle-sourcemap.js'
|
|
31
31
|
import { utf8ByteLength } from './utf8.js'
|
|
32
32
|
|
|
33
33
|
const sourcesCache = new WeakMap()
|
|
@@ -57,17 +57,35 @@ export function bundleSourcesAsMap(details) {
|
|
|
57
57
|
result.set(file, content)
|
|
58
58
|
}
|
|
59
59
|
} else if (details.kind === 'sourcemap') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const contents = details.json.sourcesContent ?? []
|
|
63
|
-
for (let i = 0; i < srcs.length; i++) {
|
|
64
|
-
if (typeof contents[i] === 'string') result.set(srcs[i], contents[i])
|
|
60
|
+
for (const [path, content] of sourcemapEntries(details)) {
|
|
61
|
+
if (typeof content === 'string') result.set(path, content)
|
|
65
62
|
}
|
|
66
63
|
}
|
|
67
64
|
if (key) sourcesCache.set(key, result)
|
|
68
65
|
return result
|
|
69
66
|
}
|
|
70
67
|
|
|
68
|
+
// A source's lines as the viewer numbers them: one more than its newlines.
|
|
69
|
+
export function bundleSourceLines(content) {
|
|
70
|
+
let lines = 1
|
|
71
|
+
for (let at = content.indexOf('\n'); at !== -1; at = content.indexOf('\n', at + 1)) lines++
|
|
72
|
+
return lines
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A bundle's sources in path order, and each one's number in it from 1: how a
|
|
76
|
+
// managed Code link names a file. The content hash that names a bundle fixes
|
|
77
|
+
// its sources, so the numbers stay put.
|
|
78
|
+
const sourceOrders = new WeakMap()
|
|
79
|
+
export function bundleSourceOrder(sources) {
|
|
80
|
+
let order = sourceOrders.get(sources)
|
|
81
|
+
if (!order) {
|
|
82
|
+
const paths = [...sources.keys()].toSorted()
|
|
83
|
+
order = { paths, numbers: new Map(paths.map((path, index) => [path, index + 1])) }
|
|
84
|
+
sourceOrders.set(sources, order)
|
|
85
|
+
}
|
|
86
|
+
return order
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
// Every file the bundle carries, for a reader that wants the filesystem
|
|
72
90
|
// rather than the source: what `bundleSourcesAsMap` returns, plus the
|
|
73
91
|
// resources it leaves out, each in the form that is true to it.
|
|
@@ -97,6 +115,10 @@ export function bundleFilesAsMap(details) {
|
|
|
97
115
|
const key = details?.bundle ?? details?.json
|
|
98
116
|
if (key && filesCache.has(key)) return filesCache.get(key)
|
|
99
117
|
const files = new Map(bundleSourcesAsMap(details))
|
|
118
|
+
// An npm package's files that are not text (view/npm-package.js) carry
|
|
119
|
+
// their size and digest alone: enough for Compare to tell a changed one
|
|
120
|
+
// from an unchanged one, as `{ format: 'digest', size, digest }`.
|
|
121
|
+
for (const [path, file] of details?.npmBinaries ?? []) files.set(path, { format: 'digest', ...file })
|
|
100
122
|
if (details?.kind === 'stasis' && details.bundle) {
|
|
101
123
|
const formats = details.bundle.formats
|
|
102
124
|
for (const [file, content] of details.bundle.sources) {
|
|
@@ -139,6 +161,7 @@ function base64ByteLength(text) {
|
|
|
139
161
|
// bundles' files by this, so its totals are the Overview's.
|
|
140
162
|
export function bundleFileByteLength(content) {
|
|
141
163
|
if (typeof content === 'string') return utf8ByteLength(content)
|
|
164
|
+
if (content?.format === 'digest') return content.size
|
|
142
165
|
return content?.format === 'base64' ? base64ByteLength(content.data) : null
|
|
143
166
|
}
|
|
144
167
|
|
|
@@ -163,7 +186,7 @@ export function bundleFileSizes(details) {
|
|
|
163
186
|
if (key && sizesCache.has(key)) return sizesCache.get(key)
|
|
164
187
|
const sizes = new Map()
|
|
165
188
|
const files = bundleFilesAsMap(details)
|
|
166
|
-
const paths = details?.kind === 'stasis' ? details.bundle?.sources.keys() : details
|
|
189
|
+
const paths = details?.kind === 'stasis' ? details.bundle?.sources.keys() : details && sourcemapEntries(details).map(([path]) => path)
|
|
167
190
|
for (const path of paths ?? []) sizes.set(path, bundleFileByteLength(files.get(path)))
|
|
168
191
|
if (key) sizesCache.set(key, sizes)
|
|
169
192
|
return sizes
|
|
@@ -202,22 +225,23 @@ export function bundleFileKinds(details) {
|
|
|
202
225
|
const kinds = new Map()
|
|
203
226
|
for (const [path, size] of sizes) {
|
|
204
227
|
if (size === null && !unsized.has(path)) continue
|
|
205
|
-
|
|
228
|
+
// An npm package's file that is not text is a resource too: no source to view.
|
|
229
|
+
kinds.set(path, Bundle.isResourceFormat(formats?.get(path)) || details?.npmBinaries?.has(path) ? 'resource' : 'source')
|
|
206
230
|
}
|
|
207
231
|
kindsCache.set(sizes, kinds)
|
|
208
232
|
return kinds
|
|
209
233
|
}
|
|
210
234
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
235
|
+
// Source paths and their recorded byte sizes. Keep null for a source whose
|
|
236
|
+
// body is missing, but omit resources and directory captures entirely so
|
|
237
|
+
// graph consumers can distinguish unknown source bytes from non-source.
|
|
214
238
|
export function bundleSourceSizes(details) {
|
|
215
239
|
const sizes = bundleFileSizes(details)
|
|
216
240
|
const formats = details?.kind === 'stasis' ? details.bundle?.formats : null
|
|
217
241
|
if (!formats || formats.size === 0) return sizes
|
|
218
242
|
if (sourceSizesCache.has(sizes)) return sourceSizesCache.get(sizes)
|
|
219
243
|
const result = new Map()
|
|
220
|
-
for (const [path, size] of sizes)
|
|
244
|
+
for (const [path, size] of sizes) if (!Bundle.isResourceFormat(formats.get(path))) result.set(path, size)
|
|
221
245
|
sourceSizesCache.set(sizes, result)
|
|
222
246
|
return result
|
|
223
247
|
}
|
|
@@ -264,8 +288,11 @@ export function bundlePackageDirs(details) {
|
|
|
264
288
|
// Shared by the Advisories tab (a bulk registry lookup keyed on these
|
|
265
289
|
// pairs) and the Compare slide (the dependency version-update diff).
|
|
266
290
|
// Returns an empty Map for sourcemaps and v0 stasis bundles — neither
|
|
267
|
-
// carries per-module version metadata.
|
|
268
|
-
|
|
291
|
+
// carries per-module version metadata. `keyOf(dir, info)` names each
|
|
292
|
+
// entry, the recorded name by default; Compare keys by install directory,
|
|
293
|
+
// as its sizes are, so an npm alias (`node_modules/alias` recording
|
|
294
|
+
// `actual-package`) joins its own sizes.
|
|
295
|
+
export function bundlePackageVersions(details, paths = null, keyOf = (_dir, info) => info.name) {
|
|
269
296
|
const versions = new Map()
|
|
270
297
|
if (details?.kind !== 'stasis' || !details.bundle?.modules) return versions
|
|
271
298
|
const packageDirs = paths === null ? null : bundlePackageDirs(details)
|
|
@@ -274,8 +301,9 @@ export function bundlePackageVersions(details, paths = null) {
|
|
|
274
301
|
if (selectedDirs && !selectedDirs.has(dir)) continue
|
|
275
302
|
if (!dir.includes('node_modules')) continue
|
|
276
303
|
if (!info?.name || typeof info.version !== 'string' || !info.version) continue
|
|
277
|
-
|
|
278
|
-
|
|
304
|
+
const key = keyOf(dir, info)
|
|
305
|
+
let set = versions.get(key)
|
|
306
|
+
if (!set) { set = new Set(); versions.set(key, set) }
|
|
279
307
|
set.add(info.version)
|
|
280
308
|
}
|
|
281
309
|
return versions
|
package/common/code-language.js
CHANGED
|
@@ -39,7 +39,19 @@ const FORMAT_TO_LANG = {
|
|
|
39
39
|
'podfile-lock': 'yaml', xml: 'markup',
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Whether a text is a JSON object by its ends: `{` first and `}` last,
|
|
43
|
+
// whitespace aside. Read from both ends, never the whole of a large file.
|
|
44
|
+
function objectText(content) {
|
|
45
|
+
if (typeof content !== 'string') return false
|
|
46
|
+
let end = content.length - 1, start = 0
|
|
47
|
+
while (start < end && /\s/u.test(content[start])) start++
|
|
48
|
+
while (end > start && /\s/u.test(content[end])) end--
|
|
49
|
+
return start < end && content[start] === '{' && content[end] === '}'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `content`, where the caller has it, settles what the name alone cannot: a
|
|
53
|
+
// `.map` file is a source map, JSON, when its text is an object.
|
|
54
|
+
export function langForPath(path, format, content) {
|
|
43
55
|
if (typeof format === 'string' && FORMAT_TO_LANG[format]) return FORMAT_TO_LANG[format]
|
|
44
56
|
if (typeof path !== 'string') return null
|
|
45
57
|
// Only the filename can contribute an extension. A dotted directory such
|
|
@@ -47,5 +59,7 @@ export function langForPath(path, format) {
|
|
|
47
59
|
const basename = path.slice(path.lastIndexOf('/') + 1)
|
|
48
60
|
const dot = basename.lastIndexOf('.')
|
|
49
61
|
if (dot <= 0) return null
|
|
50
|
-
|
|
62
|
+
const ext = basename.slice(dot + 1).toLowerCase()
|
|
63
|
+
if (ext === 'map') return objectText(content) ? 'json' : null
|
|
64
|
+
return EXT_TO_LANG[ext] ?? null
|
|
51
65
|
}
|
|
@@ -5,7 +5,7 @@ export function defaultScanModels(provider: string | null = null) {
|
|
|
5
5
|
'anthropic/claude-opus-5.5',
|
|
6
6
|
'anthropic/claude-fable-5.1',
|
|
7
7
|
'anthropic/claude-sonnet-5.5',
|
|
8
|
-
'anthropic/claude-haiku-
|
|
8
|
+
'anthropic/claude-haiku-5.5',
|
|
9
9
|
'openai/gpt-6-astra',
|
|
10
10
|
'openai/gpt-6-astra-pro',
|
|
11
11
|
'openai/gpt-6.1-sol',
|
|
@@ -20,6 +20,7 @@ export function defaultScanModels(provider: string | null = null) {
|
|
|
20
20
|
'qwen/qwen3.8-max',
|
|
21
21
|
'deepseek/deepseek-v4.1-flash',
|
|
22
22
|
'deepseek/deepseek-v4-pro',
|
|
23
|
+
'mistralai/mistral-large-4-0',
|
|
23
24
|
]
|
|
24
25
|
// OpenRouter routes all listed providers. Direct-provider credentials only
|
|
25
26
|
// expose that provider's models; no selection leaves the catalogue intact.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ReviewVerdict = 'accept' | 'accept-with-nits' | 'request-changes' | 'reject'
|
|
2
|
+
export interface ReviewSummary {
|
|
3
|
+
coverage: 'full' | 'partial' | null
|
|
4
|
+
checkedFiles: number | null
|
|
5
|
+
totalFiles: number | null
|
|
6
|
+
verdictLimit: ReviewVerdict
|
|
7
|
+
}
|
|
8
|
+
export const REVIEW_VERDICTS: Record<ReviewVerdict, string>
|
|
9
|
+
export function isReviewableFile(before: unknown, after: unknown): boolean
|
|
10
|
+
export function summarizeReview(entries: { file: string; status: ReviewVerdict | null }[], files: string[] | null, nonTextFiles?: string[] | null): ReviewSummary
|
|
11
|
+
export function allowedReviewVerdicts(entries: { file: string; status: ReviewVerdict | null }[], limit?: ReviewVerdict): ReviewVerdict[]
|
|
12
|
+
interface ReviewNpmVersion { version: string; private?: boolean; manifest?: Record<string, unknown> }
|
|
13
|
+
export function npmTakenDown(entry: { npm: ReviewNpmVersion }): boolean
|
|
14
|
+
export function npmReviewVerdictLimit(npm: ReviewNpmVersion | null | undefined,
|
|
15
|
+
advisoryList?: { versions: string[]; advisories: { id?: string; title?: string; severity?: string; cwe?: string[]; affected: number[] }[] } | null,
|
|
16
|
+
socket?: { alerts: { type: string }[] } | null, risk?: { findings: { id: string }[] } | null): ReviewVerdict
|
|
17
|
+
export function needsPackageJsonReview(entries: { file: string; status: ReviewVerdict | null }[], files: string[], nonTextFiles?: string[] | null): boolean
|
|
18
|
+
export function parseReviewNotes(comment: string): { text: string; bullet: boolean }[]
|
|
19
|
+
export function auditMarkdown(review: ReviewSummary & {
|
|
20
|
+
package: string; version: string; integrity: string; basePackage: string | null; baseVersion: string | null
|
|
21
|
+
publishedAt: number | null; revokedAt: number | null; entries: { file: string; comment: string }[]
|
|
22
|
+
status: ReviewVerdict | null; comment: string
|
|
23
|
+
}): string | null
|