@preventive/triage 1.0.0-alpha.46 → 1.0.0-alpha.48
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/client/dependency-paths.d.ts +2 -1
- package/client/dependency-paths.js +16 -4
- package/common/managed/agent-tasks.ts +12 -12
- package/common/managed/github-agents.ts +29 -10
- package/out/activity.js +4 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C2AVODGZ.js +36 -0
- package/out/{chunk-I2MDGSQX.js → chunk-IO7JTZ6O.js} +1 -1
- package/out/chunk-MOACFRJ4.js +3 -0
- package/out/chunk-O6M574JI.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PENGGKX7.js +233 -0
- package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
- package/out/chunk-SZD5CW5R.js +3 -0
- package/out/{chunk-G24A42V6.js → chunk-TQJDYHVE.js} +1 -1
- package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
- package/out/{chunk-7XRR4OFU.js → chunk-WYLQWYLA.js} +1 -1
- package/out/chunk-X6PJAK3T.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/client-sync.js +2 -2
- package/out/compare.js +14 -14
- package/out/dialogs.js +40 -40
- package/out/graph.js +11 -11
- package/out/index.html +12 -12
- package/out/scan.js +2 -2
- package/out/terminal.js +215 -215
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +9 -5
- package/risk/index.d.ts +4 -2
- package/risk/npm/assess.js +15 -4
- package/risk/npm/registry.js +6 -1
- package/risk/npm/score.js +1 -1
- package/server-managed/activity.ts +24 -11
- package/server-managed/agent-merges.ts +10 -8
- package/server-managed/app-agents.ts +8 -21
- package/server-managed/bundle-build-worker.js +7 -3
- package/server-managed/bundle-build.ts +5 -3
- package/server-managed/bundle-cache.ts +9 -3
- package/server-managed/bundle.ts +3 -4
- package/server-managed/cache-storage.ts +3 -2
- package/server-managed/commit-coauthors.ts +11 -10
- package/server-managed/commit-lockfiles.ts +22 -21
- package/server-managed/db-methods.ts +338 -187
- package/server-managed/db-neon.ts +31 -8
- package/server-managed/db.ts +2 -0
- package/server-managed/dependabot-access.ts +1 -1
- package/server-managed/dependency-review-comments.ts +7 -4
- package/server-managed/dependency-reviews.ts +71 -33
- package/server-managed/github-accounts.ts +28 -1
- package/server-managed/github-app.ts +27 -23
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +8 -1
- package/server-managed/github-metadata.ts +2 -2
- package/server-managed/http-response.ts +28 -10
- package/server-managed/http.ts +736 -247
- package/server-managed/index.ts +2 -2
- package/server-managed/link-reports.ts +5 -1
- package/server-managed/managed-issues.ts +3 -0
- package/server-managed/npm-delta.ts +16 -0
- package/server-managed/npm-kept.ts +51 -10
- package/server-managed/npm-loads.ts +48 -28
- package/server-managed/npm-manifest.ts +1 -1
- package/server-managed/npm-packages.ts +31 -15
- package/server-managed/npm-risk.ts +64 -17
- package/server-managed/object-storage.ts +4 -3
- package/server-managed/pretty-print.ts +45 -12
- package/server-managed/public-workspace.ts +3 -3
- package/server-managed/pull-dependencies.ts +84 -36
- package/server-managed/pull-dependency-reviews.ts +13 -10
- package/server-managed/rename-diff-worker.js +1 -1
- package/server-managed/report-store.ts +16 -11
- package/server-managed/repository-agents.ts +9 -6
- package/server-managed/repository-browser.ts +17 -5
- package/server-managed/repository-code.ts +180 -17
- package/server-managed/repository-dependency-reviews.ts +7 -0
- package/server-managed/repository-history.ts +85 -152
- package/server-managed/repository-issues.ts +27 -13
- package/server-managed/repository-pulse.ts +9 -3
- package/server-managed/repository-squashes.ts +145 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +3 -2
- package/server-managed/session.ts +34 -12
- package/server-managed/solidity-entry-points.ts +5 -3
- package/server-managed/sql-postgres.ts +3 -0
- package/server-managed/sql.ts +17 -8
- package/server-managed/storage-db.ts +11 -3
- package/server-managed/storage-encryption.ts +23 -12
- package/server-managed/storage-maintenance.ts +5 -2
- package/server-managed/storage-payload.ts +18 -0
- package/server-managed/storage-stores.ts +3 -2
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-feed.ts +19 -12
- package/server-managed/team-npm-scopes.ts +29 -0
- package/server-managed/team-reports.ts +74 -17
- package/server-managed/uploads.ts +29 -9
- package/out/chunk-4UFAMJAX.js +0 -1
- package/out/chunk-BOIXMUU5.js +0 -3
- package/out/chunk-CC4OM6TZ.js +0 -3
- package/out/chunk-JKGQ3O7O.js +0 -6
- package/out/chunk-LE5ZWLG4.js +0 -235
- package/out/chunk-MSION3G4.js +0 -36
- package/out/chunk-S6GIXWQW.js +0 -52
- package/out/chunk-V3KWNNMY.js +0 -1
- package/out/chunk-WQ74ZBZF.js +0 -7
|
@@ -45,6 +45,10 @@ const MAX_KEPT_DETAILS_BYTES = 256 * 1024 * 1024
|
|
|
45
45
|
// Files hashed at once: one at a time, each waits its own round trip to the
|
|
46
46
|
// thread pool; all at once, every one's job is held together.
|
|
47
47
|
const HASHES_AT_ONCE = 32
|
|
48
|
+
// Kept copies a stream reads ahead of the line it is sending (readAhead):
|
|
49
|
+
// one at a time, each waits its own storage round trip; each read whole, as
|
|
50
|
+
// its line will send it.
|
|
51
|
+
const KEPT_AHEAD = 8
|
|
48
52
|
|
|
49
53
|
export class PrettyError extends Error {
|
|
50
54
|
status: number
|
|
@@ -166,6 +170,29 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// A file's line from its kept copy, `open` opening it, or null where none is
|
|
174
|
+
// kept.
|
|
175
|
+
async function keptLine(request: PrettyRequest, open: () => Promise<OpenedBlob | null>): Promise<PrettyLine | null> {
|
|
176
|
+
const copy = await open()
|
|
177
|
+
return copy && prettyLine(request, () => Promise.resolve(copy))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Each of `items` with what `read` reads of it, in their order, up to
|
|
181
|
+
// KEPT_AHEAD of them read at once ahead of the one iterated, each answered
|
|
182
|
+
// (or failing) in its turn. One iterated no further starts no more; those
|
|
183
|
+
// under way settle unread.
|
|
184
|
+
async function* readAhead<T, R>(items: readonly T[], read: (item: T) => Promise<R>): AsyncGenerator<[T, R]> {
|
|
185
|
+
const reads: Promise<R>[] = []
|
|
186
|
+
for (const [i, item] of items.entries()) {
|
|
187
|
+
while (reads.length < Math.min(items.length, i + KEPT_AHEAD)) {
|
|
188
|
+
const reading = read(items[reads.length]!)
|
|
189
|
+
reading.catch(() => {})
|
|
190
|
+
reads.push(reading)
|
|
191
|
+
}
|
|
192
|
+
yield [item, await reads[i]!]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
// The files that changed between two sides, pretty-printed: each side's
|
|
170
197
|
// formattable files the other has another text at the path of, or, where it
|
|
171
198
|
// has none there, none of their text at a path of its own (a file renamed, or
|
|
@@ -174,8 +201,9 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
174
201
|
// the viewer diffs but where it pairs files otherwise (among copies of one
|
|
175
202
|
// text, across packages), whose copies it asks for alone. As they are
|
|
176
203
|
// iterated, in path order, the base's first where both have one: those kept,
|
|
177
|
-
// at once; then the rest, formatted one after
|
|
178
|
-
// the others (needsPrettyPrint), as the viewer
|
|
204
|
+
// at once, a few read ahead (readAhead); then the rest, formatted one after
|
|
205
|
+
// another, the minified before the others (needsPrettyPrint), as the viewer
|
|
206
|
+
// prints those unasked.
|
|
179
207
|
export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
|
|
180
208
|
// Each side's texts at the paths the other hasn't: where a file moved has its own.
|
|
181
209
|
const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
|
|
@@ -185,9 +213,8 @@ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGen
|
|
|
185
213
|
}).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
|
|
186
214
|
return (async function* () {
|
|
187
215
|
const unkept: typeof changed = []
|
|
188
|
-
for (const { own, file } of changed) {
|
|
189
|
-
|
|
190
|
-
if (copy) yield { side: own.side, ...await prettyLine(file, () => Promise.resolve(copy)) }
|
|
216
|
+
for await (const [{ own, file }, line] of readAhead(changed, item => keptLine(item.file, () => item.own.kept(item.file)))) {
|
|
217
|
+
if (line) yield { side: own.side, ...line }
|
|
191
218
|
else unkept.push({ own, file })
|
|
192
219
|
}
|
|
193
220
|
const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
|
|
@@ -325,15 +352,18 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
325
352
|
return body
|
|
326
353
|
})
|
|
327
354
|
}
|
|
355
|
+
// The copy kept for a public version's file, or null where none is (and
|
|
356
|
+
// for a private version's, which none is kept for).
|
|
357
|
+
const npmKept = (doc: NpmVersionDocument, request: PrettyRequest) => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request))))
|
|
328
358
|
// A version's file pretty-printed, `read` reading its text (and checking
|
|
329
359
|
// it is the file named): from the copy kept for a public version, else
|
|
330
360
|
// made, and kept for a public one.
|
|
331
361
|
async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
362
|
+
return await npmKept(doc, request) ?? npmMade(doc, request, read)
|
|
363
|
+
}
|
|
364
|
+
// The file pretty-printed from the text `read` reads, kept for a public version.
|
|
365
|
+
async function npmMade(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
332
366
|
const file = prettyFile(request)
|
|
333
|
-
if (!doc.private) {
|
|
334
|
-
try { return await npm.open(file) }
|
|
335
|
-
catch (err) { if (!(err instanceof CacheMissError)) throw err }
|
|
336
|
-
}
|
|
337
367
|
return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
|
|
338
368
|
const body = await prettyBody(await read(), request.extension)
|
|
339
369
|
if (!doc.private) await keep(() => npm.put(file, body))
|
|
@@ -369,11 +399,14 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
369
399
|
},
|
|
370
400
|
// Every file of the version that needs pretty-printing (npmPrintable),
|
|
371
401
|
// read before this resolves; then, as they are iterated, each
|
|
372
|
-
// pretty-printed as `npm` has it, one after another
|
|
402
|
+
// pretty-printed as `npm` has it, one after another, those kept a few
|
|
403
|
+
// read ahead (readAhead).
|
|
373
404
|
async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
|
|
374
405
|
const files = await npmPrintable(doc)
|
|
375
406
|
return (async function* () {
|
|
376
|
-
for (const
|
|
407
|
+
for await (const [file, line] of readAhead(files, request => keptLine(request, () => npmKept(doc, request)))) {
|
|
408
|
+
yield line ?? await prettyLine(file, () => npmMade(doc, file, () => Promise.resolve(file.text)))
|
|
409
|
+
}
|
|
377
410
|
})()
|
|
378
411
|
},
|
|
379
412
|
// Two bundles' sides, or two versions', as a comparison reads them, read
|
|
@@ -392,7 +425,7 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
392
425
|
npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
|
|
393
426
|
const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc), (_, bytes) => npmFileHash(bytes)),
|
|
394
427
|
packageDirs: null,
|
|
395
|
-
kept: request => (doc
|
|
428
|
+
kept: request => npmKept(doc, request),
|
|
396
429
|
made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
|
|
397
430
|
return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
|
|
398
431
|
},
|
|
@@ -7,7 +7,7 @@ import type { OpenedBlob } from './blob-store.ts'
|
|
|
7
7
|
import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
|
|
8
8
|
import { sendJson, sendLines, writeResponse } from './http-response.ts'
|
|
9
9
|
import { hashToken } from './crypto.ts'
|
|
10
|
-
import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, teamReportVisibility } from './team-reports.ts'
|
|
10
|
+
import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, loadTeamTreesResponse, teamReportVisibility } from './team-reports.ts'
|
|
11
11
|
import { triageWireEntry } from './triage-response.ts'
|
|
12
12
|
import { MAX_PACKAGE_INVENTORY_BYTES } from './bundle-cache.ts'
|
|
13
13
|
import { backfillBundleSummaries, bundleSummaries } from './bundle-catalog.ts'
|
|
@@ -34,7 +34,7 @@ export async function handlePublicWorkspace(req: IncomingMessage, res: ServerRes
|
|
|
34
34
|
const method = req.method ?? 'GET'
|
|
35
35
|
if (method !== 'GET' && method !== 'HEAD') { json(res, 403, { error: 'share-read-only' }); return }
|
|
36
36
|
const shareRoute = /^\/api\/shares\/([A-Za-z0-9_-]+)\/workspace$/u.exec(url.pathname)
|
|
37
|
-
const teamRoute = /^\/api\/teams\/([^/]+)\/(shared|reports|feed|annotations)$/u.exec(url.pathname)
|
|
37
|
+
const teamRoute = /^\/api\/teams\/([^/]+)\/(shared|reports|trees|feed|annotations)$/u.exec(url.pathname)
|
|
38
38
|
const reportRoute = /^\/api\/reports\/([^/]+)\/(triage|comments|sources)$/u.exec(url.pathname)
|
|
39
39
|
const bundleRoute = /^\/api\/bundles\/([^/]+)\/(metadata|contents|download|advisories|pretty|rename-diff|type-only|comparison)$/u.exec(url.pathname)
|
|
40
40
|
if (!shareRoute && !teamRoute && !reportRoute && !bundleRoute) { json(res, 403, { error: 'share-scope-required' }); return }
|
|
@@ -84,7 +84,7 @@ export async function handlePublicWorkspace(req: IncomingMessage, res: ServerRes
|
|
|
84
84
|
await send(await loadTeamAnnotations(deps.db, deps.reportStore, snapshot, url.searchParams.get('reportId'))); return
|
|
85
85
|
}
|
|
86
86
|
if (teamRoute[2] === 'shared') { await sendTeam(); return }
|
|
87
|
-
const body = await loadTeamReportsResponse(deps.db, deps.reportStore, snapshot)
|
|
87
|
+
const body = await (teamRoute[2] === 'trees' ? loadTeamTreesResponse : loadTeamReportsResponse)(deps.db, deps.reportStore, snapshot)
|
|
88
88
|
await recheck()
|
|
89
89
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
|
|
90
90
|
writeResponse(res, body); return
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
// changes, so what is told of one is kept by them (`kept`), and each
|
|
23
23
|
// lockfile read is kept in this instance's memory by its commit
|
|
24
24
|
// (`lockfileReads`): the pull requests branching off the same commit, as a
|
|
25
|
-
// bot's do, read their base's once.
|
|
25
|
+
// bot's do, read their base's once. One read as a commit lists it, with its
|
|
26
|
+
// blob's id (commit-lockfiles.ts), is kept by its blob instead, and beyond
|
|
27
|
+
// this instance where its reader keeps it (LockfileStore): what a blob holds
|
|
28
|
+
// never changes either, and a push changing no lockfile changes none.
|
|
26
29
|
import { createClient } from '@preventive/upstream/github.js'
|
|
27
30
|
import { GithubApiError, githubJson } from './github-app.ts'
|
|
28
31
|
import { upstreamGithub } from './github-errors.ts'
|
|
@@ -46,7 +49,8 @@ export const MAX_LOCKED = 20_000
|
|
|
46
49
|
// Lockfiles read at once.
|
|
47
50
|
const READ_PARALLEL = 4
|
|
48
51
|
// The rules what is told was told by, kept with it (http.ts
|
|
49
|
-
// pullDependencyKept)
|
|
52
|
+
// pullDependencyKept), and the lockfiles read for it (lockfileKept): one
|
|
53
|
+
// told or read by others is told or read again.
|
|
50
54
|
export const PULL_DEPENDENCIES_READ = 3
|
|
51
55
|
const SHA = /^[a-f\d]{40}$/u
|
|
52
56
|
|
|
@@ -104,20 +108,42 @@ export interface PullDependencyStore {
|
|
|
104
108
|
write(key: string, value: PullDependencies): Promise<void>
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
// A lockfile's packages, or why its reader refused it, by its repository
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
type LockfileRead = { packages: LockedPackage[] } | { error: string; detail?: string }
|
|
111
|
+
// A lockfile's packages, or why its reader refused it, by its repository and
|
|
112
|
+
// readKey, read in this instance; by the packages each holds, as the scan's
|
|
113
|
+
// are (repository-risk.ts riskCaches).
|
|
114
|
+
export type LockfileRead = { packages: LockedPackage[] } | { error: string; detail?: string }
|
|
111
115
|
const lockfileReads: Lru<LockfileRead> = lru(200_000, read => 'packages' in read ? read.packages.length + 1 : 1)
|
|
112
|
-
// Those being read, by the token reading them too, lest each asking at once
|
|
113
|
-
|
|
116
|
+
// Those being read, by the token reading them too, lest each asking at once
|
|
117
|
+
// read them, with whether GitHub was asked.
|
|
118
|
+
const lockfilesReading = new Map<string, Promise<{ read: LockfileRead; github: boolean }>>()
|
|
119
|
+
// A lockfile at a commit as findRepoLockfiles lists it, by its path, with its
|
|
120
|
+
// format (lockfileFormat) and its blob's id (`sha`).
|
|
121
|
+
export interface ListedLockfile { path: string; format: Format; sha: string }
|
|
122
|
+
// What a repository's lockfiles read for its dependency review keep beyond
|
|
123
|
+
// this instance (http.ts lockfileKept), by what never changes: a commit's
|
|
124
|
+
// lockfiles as listed (`listings`, by the commit), and a lockfile's read
|
|
125
|
+
// (`lockfiles`, by its readKey). A read GitHub failed is kept nowhere.
|
|
126
|
+
export interface LockfileStore {
|
|
127
|
+
listings: { read(commit: string): Promise<ListedLockfile[] | null>; write(commit: string, listed: ListedLockfile[]): Promise<void> }
|
|
128
|
+
lockfiles: { read(key: string): Promise<LockfileRead | null>; write(key: string, read: LockfileRead): Promise<void> }
|
|
129
|
+
}
|
|
130
|
+
// Set where GitHub was asked for what a read reads (`github`), not all of it
|
|
131
|
+
// held in this instance or kept beyond it.
|
|
132
|
+
export interface GithubAsked { github: boolean }
|
|
133
|
+
|
|
134
|
+
// A lockfile's read, by its blob where it is listed with its id (`sha`),
|
|
135
|
+
// which with its format alone tell how it reads; else by the commit and its
|
|
136
|
+
// path.
|
|
137
|
+
const readKey = (ref: string, { path, format, sha }: { path: string; format: Format; sha?: string }) =>
|
|
138
|
+
sha ? `blob\0${format}\0${sha}` : `commit\0${ref}\0${path}`
|
|
114
139
|
|
|
115
140
|
const api = (fullName: string) => `https://api.github.com/repos/${fullName.split('/').map(encodeURIComponent).join('/')}`
|
|
116
141
|
|
|
117
142
|
// The pull request's base's and head's commits, as GitHub has them now: its
|
|
118
143
|
// base's moving on as it does, its head's with each push, both kept once it
|
|
119
144
|
// is merged or closed.
|
|
120
|
-
|
|
145
|
+
export interface PullCommits { base: string; head: string }
|
|
146
|
+
async function pullCommits(fullName: string, token: string, number: number, fetchImpl: typeof fetch): Promise<PullCommits> {
|
|
121
147
|
const pull = await githubJson(`${api(fullName)}/pulls/${number}`, token, fetchImpl) as
|
|
122
148
|
{ number?: unknown; base?: { sha?: unknown; repo?: { full_name?: unknown } | null }; head?: { sha?: unknown } } | null
|
|
123
149
|
const base = pull?.base?.sha, head = pull?.head?.sha, repo = pull?.base?.repo?.full_name
|
|
@@ -129,13 +155,16 @@ async function pullCommits(fullName: string, token: string, number: number, fetc
|
|
|
129
155
|
// The pull request's merge base and the files it changes from it: as GitHub
|
|
130
156
|
// compares its base with its head, else, where that lists as many as it lists
|
|
131
157
|
// at most, from the pull request's own list, a page at a time; `cut` where
|
|
132
|
-
// that lists as many as it lists at most, too.
|
|
133
|
-
|
|
158
|
+
// that lists as many as it lists at most, too. That list is of its commits
|
|
159
|
+
// now: null where they are no longer `base` and `head` (`current`).
|
|
160
|
+
async function pullFiles(fullName: string, token: string, number: number, base: string, head: string, fetchImpl: typeof fetch,
|
|
161
|
+
current: () => Promise<boolean>) {
|
|
134
162
|
const compared = await githubJson(`${api(fullName)}/compare/${base}...${head}?per_page=1`, token, fetchImpl) as { merge_base_commit?: { sha?: unknown } } | null
|
|
135
163
|
const mergeBase = compared?.merge_base_commit?.sha
|
|
136
164
|
if (typeof mergeBase !== 'string' || !SHA.test(mergeBase)) throw new GithubApiError(502, 'github-malformed')
|
|
137
165
|
const { files, more } = commitChanges(compared)
|
|
138
166
|
if (!more) return { mergeBase, files, cut: false }
|
|
167
|
+
if (!await current()) return null
|
|
139
168
|
const listed: ChangedFile[] = []
|
|
140
169
|
for (let page = 1; listed.length < PULL_FILES; page++) {
|
|
141
170
|
const data = await githubJson(`${api(fullName)}/pulls/${number}/files?per_page=${FILES_PAGE}&page=${page}`, token, fetchImpl)
|
|
@@ -171,39 +200,49 @@ function lockfilesChanged(files: ChangedFile[]) {
|
|
|
171
200
|
omitted: all.slice(MAX_DIRECTORIES).flatMap(([, directory]) => [...directory.paths]).toSorted() }
|
|
172
201
|
}
|
|
173
202
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const shared = `${token}\0${key}`
|
|
189
|
-
const reading = lockfilesReading.get(shared) ?? readOf(fullName, token, ref, lockfile, key).finally(() => lockfilesReading.delete(shared))
|
|
203
|
+
// A lockfile's packages at `ref`, as read in this instance, else as kept
|
|
204
|
+
// beyond it (`kept`), else shared with a read of it with the same token still
|
|
205
|
+
// under way, else of GitHub, and kept; a lockfile its reader refuses, why,
|
|
206
|
+
// kept as well, its reader refusing it again; `asked` told where GitHub was.
|
|
207
|
+
// Any failure to read it of GitHub but its having none there is the whole
|
|
208
|
+
// answer's, as it would be every file's; neither is kept.
|
|
209
|
+
export async function readLockfile(fullName: string, token: string, ref: string, lockfile: { path: string; format: Format; sha?: string },
|
|
210
|
+
kept: LockfileStore | null = null, asked?: GithubAsked): Promise<LockfileRead> {
|
|
211
|
+
if (lockfile.format === 'bun') return { error: 'lockfile-unsupported' }
|
|
212
|
+
const key = readKey(ref, lockfile)
|
|
213
|
+
const known = lockfileReads.get(`${fullName}\0${key}`)
|
|
214
|
+
if (known) return known
|
|
215
|
+
const shared = `${token}\0${fullName}\0${key}`
|
|
216
|
+
const reading = lockfilesReading.get(shared) ?? readOf(fullName, token, ref, lockfile, key, kept).finally(() => lockfilesReading.delete(shared))
|
|
190
217
|
lockfilesReading.set(shared, reading)
|
|
191
|
-
|
|
218
|
+
// One failing was asked of GitHub too, though a caller swallows its
|
|
219
|
+
// failure, as a baseline's does: its access is asked again all the same.
|
|
220
|
+
let told
|
|
221
|
+
try { told = await reading } catch (err) {
|
|
222
|
+
if (asked) asked.github = true
|
|
223
|
+
throw err
|
|
224
|
+
}
|
|
225
|
+
if (told.github && asked) asked.github = true
|
|
226
|
+
return told.read
|
|
192
227
|
}
|
|
193
|
-
async function readOf(fullName: string, token: string, ref: string, { path, format }: { path: string; format: Format }, key: string
|
|
228
|
+
async function readOf(fullName: string, token: string, ref: string, { path, format }: { path: string; format: Format }, key: string,
|
|
229
|
+
kept: LockfileStore | null): Promise<{ read: LockfileRead; github: boolean }> {
|
|
230
|
+
const stored = await kept?.lockfiles.read(key)
|
|
231
|
+
if (stored) { lockfileReads.set(`${fullName}\0${key}`, stored); return { read: stored, github: false } }
|
|
194
232
|
let text: string
|
|
195
233
|
try {
|
|
196
234
|
text = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).getRepoFile({ repo: fullName, path, ref }))
|
|
197
235
|
} catch (err) {
|
|
198
|
-
if (err instanceof GithubApiError && err.message === 'github-not-found') return { error: 'github-not-found' }
|
|
236
|
+
if (err instanceof GithubApiError && err.message === 'github-not-found') return { read: { error: 'github-not-found' }, github: true }
|
|
199
237
|
throw err
|
|
200
238
|
}
|
|
201
239
|
let read: LockfileRead
|
|
202
240
|
try { read = { packages: lockedPackages(format, text, { review: true }) } } catch (error) {
|
|
203
241
|
read = { error: 'lockfile-unreadable', ...error instanceof Error ? { detail: error.message.slice(0, 500) } : {} }
|
|
204
242
|
}
|
|
205
|
-
lockfileReads.set(key
|
|
206
|
-
|
|
243
|
+
lockfileReads.set(`${fullName}\0${key}`, read)
|
|
244
|
+
await kept?.lockfiles.write(key, read)
|
|
245
|
+
return { read, github: true }
|
|
207
246
|
}
|
|
208
247
|
|
|
209
248
|
// Runs `run` for each of `items`, as many at once as `parallel`, each's
|
|
@@ -278,14 +317,23 @@ export async function pullDependencies(fullName: string, token: string, number:
|
|
|
278
317
|
return (await pullDependenciesAt(fullName, token, number, kept, fetchImpl)).told
|
|
279
318
|
}
|
|
280
319
|
// The same, with the commits it is kept by (`commits`), its base's and
|
|
281
|
-
// head's, by which `kept` reads it
|
|
320
|
+
// head's, by which `kept` reads it: those `given`, as a page of its
|
|
321
|
+
// repository's open pull requests read them (repository-issues.ts), else as
|
|
322
|
+
// GitHub has them now. Commits given that its own list of its files is no
|
|
323
|
+
// longer of are read as GitHub has them now.
|
|
282
324
|
export async function pullDependenciesAt(fullName: string, token: string, number: number, kept: PullDependencyStore,
|
|
283
|
-
fetchImpl: typeof fetch = globalThis.fetch): Promise<{ commits: string; told: PullDependencies }> {
|
|
284
|
-
const { base, head } = await pullCommits(fullName, token, number, fetchImpl)
|
|
325
|
+
fetchImpl: typeof fetch = globalThis.fetch, given?: PullCommits): Promise<{ commits: string; told: PullDependencies }> {
|
|
326
|
+
const { base, head } = given ?? await pullCommits(fullName, token, number, fetchImpl)
|
|
285
327
|
const key = `${base}...${head}`
|
|
286
328
|
const held = await kept.read(key)
|
|
287
329
|
if (held) return { commits: key, told: held }
|
|
288
|
-
const
|
|
330
|
+
const current = async () => {
|
|
331
|
+
const now = given && await pullCommits(fullName, token, number, fetchImpl)
|
|
332
|
+
return !now || (now.base === base && now.head === head)
|
|
333
|
+
}
|
|
334
|
+
const pulled = await pullFiles(fullName, token, number, base, head, fetchImpl, current)
|
|
335
|
+
if (!pulled) return pullDependenciesAt(fullName, token, number, kept, fetchImpl)
|
|
336
|
+
const { mergeBase, files, cut: filesCut } = pulled
|
|
289
337
|
const { directories, cut, omitted } = lockfilesChanged(files)
|
|
290
338
|
const reads = directories.flatMap(({ before, after }) => [...before.map(lockfile => [mergeBase, lockfile] as const), ...after.map(lockfile => [head, lockfile] as const)])
|
|
291
339
|
const read = await eachOf(reads, READ_PARALLEL, ([ref, lockfile]) => readLockfile(fullName, token, ref, lockfile))
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
// changes: it is read once, every lockfile there of those read here
|
|
25
25
|
// (pull-dependencies.ts), and kept by the commit (`baselines` in this
|
|
26
26
|
// instance, `kept` beyond it), never stored as reviews.
|
|
27
|
-
import {
|
|
27
|
+
import { lockedAt, lockedIn } from './commit-lockfiles.ts'
|
|
28
28
|
import { GithubApiError } from './github-app.ts'
|
|
29
29
|
import type { LockedPackage } from './locked-packages.ts'
|
|
30
30
|
import { EVERY_LOCKFILE, type LockfileScope, heldTo, isEvery, reviewedIn } from './lockfile-scope.ts'
|
|
31
31
|
import { type Lru, lru } from './lru.ts'
|
|
32
|
-
import { type DirectoryDependencies, MAX_ADDED, type PullDependencies, lockedAs, lockedWith } from './pull-dependencies.ts'
|
|
32
|
+
import { type DirectoryDependencies, type GithubAsked, type LockfileStore, MAX_ADDED, type PullDependencies, lockedAs, lockedWith } from './pull-dependencies.ts'
|
|
33
33
|
import type { PublishedReview, RepoReviewState } from './repository-dependency-reviews.ts'
|
|
34
34
|
import { type ReviewedDependencies, type ReviewedDependency, askReviewed } from './reviewed-dependencies.ts'
|
|
35
35
|
|
|
@@ -64,17 +64,19 @@ const hashesOf = (pkg: LockedPackage) => (pkg.integrity ?? '').split(/\s+/u).fil
|
|
|
64
64
|
const baselineKeys = (pkg: LockedPackage) => hashesOf(pkg).map(hash => `${pkg.name}@${pkg.version ?? ''}\0${hash}\0${pkg.patchHash ?? ''}`)
|
|
65
65
|
|
|
66
66
|
// The versions the repository's lockfiles locked at commit `since` that are
|
|
67
|
-
// reviewed (reviewedAs), as kept, else read of GitHub (commit-lockfiles.ts
|
|
68
|
-
// and kept; with the lockfiles there its
|
|
69
|
-
// past those read (`unread`). GitHub having
|
|
70
|
-
// there is a failure of the whole, as of any
|
|
71
|
-
|
|
67
|
+
// reviewed (reviewedAs), as kept, else read of GitHub (commit-lockfiles.ts,
|
|
68
|
+
// its lockfiles as `reads` keeps them) and kept; with the lockfiles there its
|
|
69
|
+
// reader refuses, isn't one of, or past those read (`unread`). GitHub having
|
|
70
|
+
// none of a lockfile it lists there is a failure of the whole, as of any
|
|
71
|
+
// file, never kept.
|
|
72
|
+
export async function reviewBaseline(fullName: string, token: string, repoId: number, since: string, kept: ReviewBaselineStore, reads: LockfileStore | null = null,
|
|
73
|
+
asked?: GithubAsked): Promise<Baseline> {
|
|
72
74
|
const key = `${repoId}\0${since}`
|
|
73
75
|
const known = baselines.get(key)
|
|
74
76
|
if (known) return known
|
|
75
77
|
const stored = await kept.read(key)
|
|
76
78
|
if (stored) { const baseline = { versions: new Set(stored.versions), unread: stored.unread }; baselines.set(key, baseline); return baseline }
|
|
77
|
-
const read = await lockedAt(fullName, token, since, () => true, asked)
|
|
79
|
+
const read = await lockedAt(fullName, token, since, reads, () => true, asked)
|
|
78
80
|
const { packages } = lockedIn(read), { unread } = read
|
|
79
81
|
const versions = new Set<string>()
|
|
80
82
|
for (const pkg of packages) if (reviewedAs(pkg) === 'version') for (const held of baselineKeys(pkg)) versions.add(held)
|
|
@@ -306,10 +308,11 @@ export const reviewedPackages = (told: PullDependencies) => told.lockfiles
|
|
|
306
308
|
// A repository's baseline as read for a pull request: its versions and the
|
|
307
309
|
// lockfiles there left unread; none where it reviews since no commit; and
|
|
308
310
|
// where GitHub doesn't read it, none and why.
|
|
309
|
-
export async function baselineFor(fullName: string, token: string, repoId: number, state: RepoReviewState, kept: ReviewBaselineStore,
|
|
311
|
+
export async function baselineFor(fullName: string, token: string, repoId: number, state: RepoReviewState, kept: ReviewBaselineStore, reads: LockfileStore | null,
|
|
312
|
+
asked?: GithubAsked) {
|
|
310
313
|
if (!state.since) return { baseline: null, unread: [], error: null }
|
|
311
314
|
try {
|
|
312
|
-
const { versions, unread } = await reviewBaseline(fullName, token, repoId, state.since, kept, asked)
|
|
315
|
+
const { versions, unread } = await reviewBaseline(fullName, token, repoId, state.since, kept, reads, asked)
|
|
313
316
|
return { baseline: versions, unread, error: null }
|
|
314
317
|
} catch (err) {
|
|
315
318
|
if (err instanceof GithubApiError && err.status !== 429 && err.status !== 401) return { baseline: null, unread: [], error: err.message }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Compare's diff with renamed names hidden (rename-diff.ts), worked out off the
|
|
2
2
|
// server's event loop by @preventive/compare's renamedDiffs: the old side given
|
|
3
|
-
// the new side's names, formatted again where
|
|
3
|
+
// the new side's names, not formatted again (each line where the old side's was),
|
|
4
4
|
// diffed with whitespace changes shown and hidden, and the blocks checked to
|
|
5
5
|
// take it to the new side. `{ before, blocks, reason, reads, marks,
|
|
6
6
|
// ignoringWhitespace }`: the old side as diffed, the change blocks, why the
|
|
@@ -32,9 +32,7 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
|
|
|
32
32
|
if (compressedOnly.size >= COMPRESSED_ONLY_MAX) compressedOnly.delete(compressedOnly.values().next().value!)
|
|
33
33
|
compressedOnly.add(id)
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
const stored = await compressed.open(id)
|
|
37
|
-
if (!stored) return null
|
|
35
|
+
function decoded(stored: OpenedBlob): OpenedBlob {
|
|
38
36
|
const stream = createBrotliDecompress()
|
|
39
37
|
// pipeline carries storage/authentication errors into the decoded stream
|
|
40
38
|
// and closes the stored source when a reader leaves before EOF.
|
|
@@ -43,22 +41,29 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
|
|
|
43
41
|
}
|
|
44
42
|
async function open(id: string): Promise<OpenedBlob | null> {
|
|
45
43
|
if (compressedOnly.has(id)) {
|
|
46
|
-
const stored = await
|
|
47
|
-
if (stored) return stored
|
|
44
|
+
const stored = await compressed.open(id)
|
|
45
|
+
if (stored) return decoded(stored)
|
|
48
46
|
compressedOnly.delete(id)
|
|
49
47
|
}
|
|
50
48
|
// Keep the original available until conversion succeeds. Concurrent reads
|
|
51
49
|
// can finish using an already-open stream while another reader converts it.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
// Both copies are asked for at once, the original answering where it is
|
|
51
|
+
// still there: one round trip of storage for a report not known yet, as
|
|
52
|
+
// a cold instance knows none.
|
|
53
|
+
const [original, stored] = await Promise.allSettled([legacy.open(id), compressed.open(id)])
|
|
54
|
+
const unused = (copy: PromiseSettledResult<OpenedBlob | null>) => { if (copy.status === 'fulfilled') copy.value?.stream.destroy() }
|
|
55
|
+
if (original.status === 'rejected') { unused(stored); throw original.reason }
|
|
56
|
+
if (original.value) {
|
|
57
|
+
unused(stored)
|
|
58
|
+
const bytes = await readBytes(original.value)
|
|
55
59
|
await replaceLegacy(id, await encodeBrotli(bytes, 9))
|
|
56
60
|
remember(id)
|
|
57
61
|
return { size: bytes.length, stream: Readable.from([bytes]) }
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
if (stored)
|
|
61
|
-
|
|
63
|
+
if (stored.status === 'rejected') throw stored.reason
|
|
64
|
+
if (!stored.value) return null
|
|
65
|
+
remember(id)
|
|
66
|
+
return decoded(stored.value)
|
|
62
67
|
}
|
|
63
68
|
return {
|
|
64
69
|
async put(id, bytes) {
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
// an agent, and those alone are answered, never the texts. GraphQL alone
|
|
12
12
|
// gives them, so it needs a token.
|
|
13
13
|
import { type AgentAuthor, type AgentBranch, type AgentCommit, type AgentIssue, type AgentPull, type AppAgent, agentSignals } from '../common/managed/agent-tasks.ts'
|
|
14
|
+
import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId } from '../common/managed/github-agents.ts'
|
|
14
15
|
import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
|
|
15
16
|
import { GithubApiError } from './github-app.ts'
|
|
16
|
-
import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql } from './repository-history.ts'
|
|
17
|
+
import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql, linkNoreplyUsers } from './repository-history.ts'
|
|
17
18
|
import { ISSUES_CURSOR } from './repository-issues.ts'
|
|
18
19
|
|
|
19
|
-
export { APP_AGENTS }
|
|
20
|
-
|
|
21
20
|
export const AGENT_PULLS_PAGE = 50
|
|
22
21
|
export const AGENT_COMMITS_PAGE = 100
|
|
23
22
|
const AGENT_ISSUES = 100
|
|
@@ -35,7 +34,8 @@ const SHA = /^[a-f\d]{40}$/u
|
|
|
35
34
|
const CLAUDE_EMAIL = 'noreply@anthropic.com'
|
|
36
35
|
|
|
37
36
|
// The app agents' accounts' no-reply addresses, by each login of theirs.
|
|
38
|
-
const AGENT_EMAILS = APP_AGENTS.flatMap(({
|
|
37
|
+
const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
|
|
38
|
+
.flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
|
|
39
39
|
|
|
40
40
|
// An agent's branches, a page at a time, as GitHub lists them, by name;
|
|
41
41
|
// each its tip commit's time, message and author.
|
|
@@ -164,6 +164,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
|
|
|
164
164
|
for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
|
|
165
165
|
const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
|
|
166
166
|
?.repository?.refs
|
|
167
|
+
await linkNoreplyUsers((refs?.nodes ?? []).map(node => node?.target?.author), token, fetchImpl)
|
|
167
168
|
found.push(...(refs?.nodes ?? []).flatMap(node => branch(prefix)(node) ?? []))
|
|
168
169
|
const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
|
|
169
170
|
if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
|
|
@@ -175,7 +176,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
|
|
|
175
176
|
// What its answers are kept by, with their cursors (http.ts
|
|
176
177
|
// handleRepositoryAgents): bumped as what a page reads, or how it tells an
|
|
177
178
|
// agent, changes, so that no answer an older server kept is sent for a day.
|
|
178
|
-
export const AGENTS_READ_VERSION =
|
|
179
|
+
export const AGENTS_READ_VERSION = 7
|
|
179
180
|
|
|
180
181
|
// A cursor of the default branch's commits: GitHub names one by a commit
|
|
181
182
|
// and how many on from it, `<oid> <offset>`, a space between.
|
|
@@ -206,7 +207,9 @@ export async function repositoryAgents(fullName: string, token: string, cursors:
|
|
|
206
207
|
if (!repository) throw new GithubApiError(404, 'github-not-found')
|
|
207
208
|
const history = repository.defaultBranchRef?.target?.history
|
|
208
209
|
const after = { pulls: nextCursor(repository.pullRequests), commits: nextCursor(history, COMMITS_CURSOR) }
|
|
209
|
-
const
|
|
210
|
+
const nodes = [...history?.nodes ?? [], ...authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []]
|
|
211
|
+
await linkNoreplyUsers(nodes.map(node => node?.author), token, fetchImpl)
|
|
212
|
+
const commits = nodes.flatMap(node => directCommit(node) ?? [])
|
|
210
213
|
return {
|
|
211
214
|
pulls: (repository.pullRequests?.nodes ?? []).flatMap(node => pull(node) ?? []),
|
|
212
215
|
commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
|
|
@@ -171,9 +171,16 @@ async function askAccess(repo: SelectedRepo, start: string | null, userToken: st
|
|
|
171
171
|
return githubJson(base, token, fetchImpl)
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
// A repository last known private asks who the user is
|
|
175
|
-
//
|
|
176
|
-
|
|
174
|
+
// A repository last known private asks who the user is, and mints the
|
|
175
|
+
// installation token its permission is asked with, as it asks for its
|
|
176
|
+
// metadata, not after. Each is asked once a request (the token once for
|
|
177
|
+
// concurrent requests too, github-app.ts), so a failure is answered where
|
|
178
|
+
// it is awaited. One made public since mints a token it never waits for,
|
|
179
|
+
// kept for later requests.
|
|
180
|
+
if (repo.private && userToken) {
|
|
181
|
+
getIdentity().catch(() => {})
|
|
182
|
+
if (repo.installationId != null) installationToken().catch(() => {})
|
|
183
|
+
}
|
|
177
184
|
// Stored visibility can be stale (or internal with private=false). Require
|
|
178
185
|
// current, explicit public visibility or the caller's effective permission.
|
|
179
186
|
const data = await metadata() as RepoMetadata
|
|
@@ -232,6 +239,9 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
|
|
|
232
239
|
// The user's permission level on it where the gate asked GitHub of it, as
|
|
233
240
|
// for a private repository; null for a public one.
|
|
234
241
|
permission: () => level,
|
|
242
|
+
// Its default branch as GitHub named it when access was first checked,
|
|
243
|
+
// `''` where it named none: the one whose head `commit('')` reads.
|
|
244
|
+
defaultBranch,
|
|
235
245
|
async refs() {
|
|
236
246
|
// Suggestions are bounded; the input also accepts any branch or tag name.
|
|
237
247
|
const list = async (kind: string) => {
|
|
@@ -272,8 +282,10 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
|
|
|
272
282
|
const manifestPath = path ? `${path}/package.json` : 'package.json'
|
|
273
283
|
const manifest = data.find(item => item?.path === manifestPath && item.name === 'package.json' && item.type === 'file' && !item.submodule_git_url
|
|
274
284
|
&& typeof item.size === 'number' && item.size >= 0 && item.size <= MAX_PACKAGE_BYTES && typeof item.sha === 'string' && /^[a-f\d]{40}$/iu.test(item.sha))
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
// Both optional suggestions, read together: neither fails, each
|
|
286
|
+
// answering none where its reads do.
|
|
287
|
+
const [packageEntryPoints, solidity] = await Promise.all([manifest ? readPackageEntryPoints(manifest.sha, path, read) : undefined,
|
|
288
|
+
readSolidityEntryPoints(path, data, read)])
|
|
277
289
|
return { entries, limited: data.length >= 1000, ...(packageEntryPoints ? { packageEntryPoints } : {}),
|
|
278
290
|
...(solidity.paths.length > 0 || solidity.limited ? { solidityEntryPoints: solidity.paths, soliditySuggestionsLimited: solidity.limited } : {}),
|
|
279
291
|
}
|