rvlib-shipkit 1.7.0 → 1.9.0
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/CHANGELOG.md +48 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +31 -14
- package/src/assets/gen/generate-roll.ts +20 -7
- package/src/assets/kit-config.ts +9 -0
- package/src/assets/kits/identity-kit.ts +7 -1
- package/src/assets/run-assets.ts +3 -3
- package/src/assets/server/studio-server.ts +3 -3
- package/src/checks/SK027-agent-wiki-links.check.ts +6 -2
- package/src/checks/SK033-global-contribution.check.ts +47 -0
- package/src/cli/cmd_Ci.ts +5 -4
- package/src/cli/cmd_GlobalContext.ts +47 -0
- package/src/cli/cmd_GlobalProcesses.ts +38 -1
- package/src/cli.ts +4 -1
- package/src/collections/providers.ts +1 -1
- package/src/core/check-runner.ts +13 -4
- package/src/core/discover-checks.ts +37 -8
- package/src/core/fix-scope.ts +5 -0
- package/src/core/floor-resolve.ts +2 -1
- package/src/core/git-root.ts +22 -12
- package/src/core/global-contribution.ts +42 -0
- package/src/core/one-lib.ts +4 -2
- package/src/core/run-ci.ts +12 -11
- package/src/core/slots.ts +36 -6
- package/src/home/global-context.ts +145 -0
- package/src/home/gui-app/ShipkitGui.swift +96 -0
- package/src/home/home-routes.ts +10 -0
- package/src/home/resources.ts +3 -0
- package/src/plan/planner.ts +21 -4
- package/src/plan/repo-source.ts +7 -3
- package/src/release/run-release.ts +17 -8
- package/src/release/unit-gate.ts +12 -5
- package/src/repo-config.ts +7 -0
- package/src/serve/hub-runtime.ts +34 -23
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +138 -0
- package/src/smoke/run-smoke.ts +13 -6
- package/src/suite/judge-suites.ts +79 -11
- package/src/suite/reads-env.ts +8 -0
- package/src/suite/select.ts +510 -0
- package/src/suite/suite-cli.ts +213 -23
- package/src/suite/suite-reads-preload.ts +335 -0
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// WHAT EACH MODULE READ, preloaded into every test worker and every bun
|
|
2
|
+
// process a suite spawns. The quick tier runs only the suites whose reads
|
|
3
|
+
// changed since the last green run (select.ts), and a module's static imports
|
|
4
|
+
// are only part of what it reads: a suite also reads files by path, lists
|
|
5
|
+
// dirs, globs and spawns. This preload records each of those, attributed to
|
|
6
|
+
// every module of the repo on the call stack. A memo filled by one suite and
|
|
7
|
+
// used by the next is still charged to the module that holds it, and every
|
|
8
|
+
// suite whose import closure holds that module depends on the read.
|
|
9
|
+
//
|
|
10
|
+
// Lines, appended to the file SHIPKIT_SUITE_READS names: `<kind>\t<module>\t<path>`.
|
|
11
|
+
// r a file read, probed or stat'ed (its content, or its absence, matters)
|
|
12
|
+
// d a dir listed (its names matter)
|
|
13
|
+
// g a tree walked by a glob (every name under it matters)
|
|
14
|
+
// o a process spawned that reads what nobody records (git or a shell run
|
|
15
|
+
// in the repo): the module is opaque, and every suite that reaches it
|
|
16
|
+
// runs every time
|
|
17
|
+
// l a repo module loaded at runtime from a call this module was on the
|
|
18
|
+
// stack of (a require or an import() in a function body)
|
|
19
|
+
// L a repo module loaded while this module itself was being loaded
|
|
20
|
+
// A read with no repo frame on its stack is charged to `-`, which every suite
|
|
21
|
+
// depends on. A bun child records under the module that spawned it.
|
|
22
|
+
//
|
|
23
|
+
// node:fs is replaced through bun:test's mock.module, the one hook that also
|
|
24
|
+
// reaches a named ESM import, and it updates modules imported before it.
|
|
25
|
+
import { mock, afterAll } from 'bun:test'
|
|
26
|
+
import * as fsLive from 'node:fs'
|
|
27
|
+
import * as fspLive from 'node:fs/promises'
|
|
28
|
+
import * as cpLive from 'node:child_process'
|
|
29
|
+
import { appendFileSync, realpathSync } from 'node:fs'
|
|
30
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
import { SUITE_OWNER_ENV, SUITE_READS_ENV, SUITE_ROOT_ENV } from './reads-env.ts'
|
|
33
|
+
|
|
34
|
+
const out = process.env[SUITE_READS_ENV]
|
|
35
|
+
const root = process.env[SUITE_ROOT_ENV]
|
|
36
|
+
const owner = process.env[SUITE_OWNER_ENV]
|
|
37
|
+
const SELF = import.meta.path
|
|
38
|
+
|
|
39
|
+
if (out != null && out !== '' && root != null && root !== '') install(out, resolve(root))
|
|
40
|
+
|
|
41
|
+
function install(outFile: string, rootDir: string): void {
|
|
42
|
+
const realFs = { ...fsLive } as Record<string, unknown>
|
|
43
|
+
const realFsp = { ...fspLive } as Record<string, unknown>
|
|
44
|
+
const realCp = { ...cpLive } as Record<string, unknown>
|
|
45
|
+
// the root as git prints it is a realpath, and a path a module names can
|
|
46
|
+
// go through a symlink to it (macOS: /var is /private/var), so each
|
|
47
|
+
// spelling of the root counts
|
|
48
|
+
const real = ((): string => { try { return realpathSync(rootDir) } catch { return rootDir } })()
|
|
49
|
+
const roots = [...new Set([rootDir, real, ...[rootDir, real].filter((r) => r.startsWith('/private/')).map((r) => r.slice('/private'.length))])]
|
|
50
|
+
const seen = new Set<string>()
|
|
51
|
+
let buf: string[] = []
|
|
52
|
+
|
|
53
|
+
/** repo-relative posix path, null outside the repo, in an install, or in git's own dir */
|
|
54
|
+
const rel = (p: unknown): string | null => {
|
|
55
|
+
let s: string
|
|
56
|
+
if (typeof p === 'string') s = p
|
|
57
|
+
else if (p instanceof URL) { if (p.protocol !== 'file:') return null; s = fileURLToPath(p) }
|
|
58
|
+
else if (p instanceof Uint8Array) s = Buffer.from(p).toString()
|
|
59
|
+
else return null
|
|
60
|
+
if (s.startsWith('file:')) { try { s = fileURLToPath(s) } catch { return null } }
|
|
61
|
+
const abs = isAbsolute(s) ? resolve(s) : resolve(process.cwd(), s)
|
|
62
|
+
const base = roots.find((r) => abs === r || abs.startsWith(r.endsWith(sep) ? r : `${r}${sep}`))
|
|
63
|
+
if (base == null) return null
|
|
64
|
+
const r = relative(base, abs).split(sep).join('/')
|
|
65
|
+
if (r === '' ) return '.'
|
|
66
|
+
if (r === 'node_modules' || r.startsWith('node_modules/') || r.includes('/node_modules/')) return null
|
|
67
|
+
return r
|
|
68
|
+
}
|
|
69
|
+
/** the innermost repo module on the stack, outside this file */
|
|
70
|
+
// a read is charged to EVERY repo module on the stack: the one that read
|
|
71
|
+
// it, the ones that hold a memo of what it read, and the one that asked. A
|
|
72
|
+
// cache a preload installs is asked through the module that holds its
|
|
73
|
+
// hook, and every suite reusing the memo calls that module again
|
|
74
|
+
const callers = (): string[] => {
|
|
75
|
+
if (owner != null && owner !== '') return [owner]
|
|
76
|
+
const limit = Error.stackTraceLimit
|
|
77
|
+
Error.stackTraceLimit = 80
|
|
78
|
+
const stack = new Error().stack ?? ''
|
|
79
|
+
Error.stackTraceLimit = limit
|
|
80
|
+
const out: string[] = []
|
|
81
|
+
for (const line of stack.split('\n')) {
|
|
82
|
+
const m = /\(?((?:\/|[A-Za-z]:\\)[^():]+):\d+:\d+\)?\s*$/.exec(line)
|
|
83
|
+
const file = m?.[1]
|
|
84
|
+
if (file == null || file === SELF) continue
|
|
85
|
+
const r = rel(file)
|
|
86
|
+
if (r != null && r !== '.' && !out.includes(r)) out.push(r)
|
|
87
|
+
}
|
|
88
|
+
return out.length > 0 ? out : ['-']
|
|
89
|
+
}
|
|
90
|
+
/** the module a child records under: the innermost one that spawned it */
|
|
91
|
+
const caller = (): string => callers()[0]!
|
|
92
|
+
/** a read through a symlink reads its target too */
|
|
93
|
+
const target = (path: string): string | null => {
|
|
94
|
+
try {
|
|
95
|
+
const real = rel((realFs['realpathSync'] as (p: string) => string)(join(rootDir, path)))
|
|
96
|
+
return real != null && real !== path ? real : null
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const note = (kind: string, path: string | null, mods: string | string[] = callers()): void => {
|
|
102
|
+
if (path == null) return
|
|
103
|
+
if (kind !== 'o') {
|
|
104
|
+
const t = target(path)
|
|
105
|
+
if (t != null) note(kind, t, mods)
|
|
106
|
+
}
|
|
107
|
+
for (const mod of typeof mods === 'string' ? [mods] : mods) {
|
|
108
|
+
const line = `${kind}\t${mod}\t${path}`
|
|
109
|
+
if (seen.has(line)) continue
|
|
110
|
+
seen.add(line)
|
|
111
|
+
buf.push(line)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const flush = (): void => {
|
|
115
|
+
if (buf.length === 0) return
|
|
116
|
+
try { appendFileSync(outFile, `${buf.join('\n')}\n`) } catch { /* no record: the next full run fills it */ }
|
|
117
|
+
buf = []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const READS = ['readFileSync', 'readFile', 'existsSync', 'exists', 'statSync', 'stat', 'lstatSync', 'lstat', 'accessSync', 'access', 'openSync', 'open', 'createReadStream', 'readlinkSync', 'readlink', 'realpathSync', 'realpath', 'opendirSync', 'opendir', 'copyFileSync', 'copyFile', 'watchFile', 'statfsSync', 'statfs']
|
|
121
|
+
// a copy or a watch of a dir takes the whole tree under it
|
|
122
|
+
const TREES = ['cpSync', 'cp', 'watch']
|
|
123
|
+
const LISTS = ['readdirSync', 'readdir']
|
|
124
|
+
const GLOBS = ['globSync', 'glob']
|
|
125
|
+
const wrap = (real: Record<string, unknown>, kind: string, names: string[]): Record<string, unknown> => {
|
|
126
|
+
const shim: Record<string, unknown> = {}
|
|
127
|
+
for (const n of names) {
|
|
128
|
+
const f = real[n]
|
|
129
|
+
if (typeof f !== 'function') continue
|
|
130
|
+
const g = function (this: unknown, ...a: unknown[]): unknown {
|
|
131
|
+
if (kind === 'g') note('g', globRoot(a[0], a[1]))
|
|
132
|
+
else if (kind === 't') {
|
|
133
|
+
const r = rel(a[0])
|
|
134
|
+
note(r != null && exists(join(rootDir, r), false) ? 'r' : 'g', r)
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const r = rel(a[0])
|
|
138
|
+
// a recursive listing is a walk of the whole tree
|
|
139
|
+
const recursive = kind === 'd' && typeof a[1] === 'object' && a[1] != null && (a[1] as { recursive?: unknown }).recursive === true
|
|
140
|
+
note(recursive ? 'g' : n.startsWith('opendir') ? 'd' : kind, r)
|
|
141
|
+
}
|
|
142
|
+
return (f as (...x: unknown[]) => unknown).apply(this, a)
|
|
143
|
+
}
|
|
144
|
+
Object.assign(g, f)
|
|
145
|
+
shim[n] = g
|
|
146
|
+
}
|
|
147
|
+
return shim
|
|
148
|
+
}
|
|
149
|
+
const globRoot = (pattern: unknown, opts: unknown): string | null => {
|
|
150
|
+
const pats = Array.isArray(pattern) ? pattern : [pattern]
|
|
151
|
+
const cwd = typeof opts === 'object' && opts != null && typeof (opts as { cwd?: unknown }).cwd === 'string' ? (opts as { cwd: string }).cwd : process.cwd()
|
|
152
|
+
const roots = pats.filter((p): p is string => typeof p === 'string').map((p) => {
|
|
153
|
+
const lit = p.split('/').filter((_, i, a) => !/[*?[{]/.test(a.slice(0, i + 1).join('/'))).join('/')
|
|
154
|
+
return rel(join(cwd, lit))
|
|
155
|
+
})
|
|
156
|
+
return roots.length === 1 ? roots[0]! : rel(cwd)
|
|
157
|
+
}
|
|
158
|
+
const fsShim: Record<string, unknown> = { ...realFs, ...wrap(realFs, 'r', READS), ...wrap(realFs, 'd', LISTS), ...wrap(realFs, 'g', GLOBS), ...wrap(realFs, 't', TREES) }
|
|
159
|
+
const fspShim: Record<string, unknown> = { ...realFsp, ...wrap(realFsp, 'r', READS), ...wrap(realFsp, 'd', LISTS), ...wrap(realFsp, 'g', GLOBS), ...wrap(realFsp, 't', TREES) }
|
|
160
|
+
fsShim['promises'] = fspShim
|
|
161
|
+
mock.module('node:fs', () => ({ ...fsShim, default: fsShim }))
|
|
162
|
+
mock.module('fs', () => ({ ...fsShim, default: fsShim }))
|
|
163
|
+
mock.module('node:fs/promises', () => ({ ...fspShim, default: fspShim }))
|
|
164
|
+
mock.module('fs/promises', () => ({ ...fspShim, default: fspShim }))
|
|
165
|
+
|
|
166
|
+
// a bun child runs this same preload and records under the spawning module.
|
|
167
|
+
// Any other child reads what nobody records
|
|
168
|
+
/** a path that stands there (a file, or a dir when `dir`) */
|
|
169
|
+
const exists = (abs: string, dir: boolean): boolean => {
|
|
170
|
+
try {
|
|
171
|
+
const st = (realFs['statSync'] as (p: string) => { isFile(): boolean; isDirectory(): boolean })(abs)
|
|
172
|
+
return st.isFile() || (dir && st.isDirectory())
|
|
173
|
+
} catch { return false }
|
|
174
|
+
}
|
|
175
|
+
const SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'fish', 'env', 'xargs', 'nice', 'time', 'timeout'])
|
|
176
|
+
// tools that read only the files their arguments name, nothing they find through them
|
|
177
|
+
const FILE_TOOLS = new Set(['true', 'false', 'echo', 'sleep', 'kill', 'rm', 'mkdir', 'touch', 'cwebp', 'dwebp'])
|
|
178
|
+
const isBun = (bin: unknown): boolean => typeof bin === 'string' && (bin === 'bun' || bin === process.execPath || /(^|\/)bun$/.test(bin))
|
|
179
|
+
const childEnv = (env: unknown): Record<string, string | undefined> => {
|
|
180
|
+
const base = (typeof env === 'object' && env != null ? env : process.env) as Record<string, string | undefined>
|
|
181
|
+
const opts = base['BUN_OPTIONS'] ?? ''
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
[SUITE_READS_ENV]: outFile,
|
|
185
|
+
[SUITE_ROOT_ENV]: rootDir,
|
|
186
|
+
[SUITE_OWNER_ENV]: caller(),
|
|
187
|
+
BUN_OPTIONS: opts.includes(SELF) ? opts : `${opts} --preload=${SELF}`.trim(),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// a child that is not bun reads what nobody records. It is trusted only
|
|
191
|
+
// when it runs outside the repo and names nothing in it (git in a temp
|
|
192
|
+
// repo); a tool known to read only the files it names (cwebp) records
|
|
193
|
+
// them; any other child in or about the repo makes its module opaque
|
|
194
|
+
const spawned = (cmd: unknown, cwdOpt: unknown): void => {
|
|
195
|
+
const argv = (Array.isArray(cmd) ? cmd : typeof cmd === 'string' ? [cmd] : []).map(String)
|
|
196
|
+
if (isBun(argv[0])) return
|
|
197
|
+
const tool = (argv[0] ?? '?').split('/').pop() ?? '?'
|
|
198
|
+
const cwd = typeof cwdOpt === 'string' ? resolve(cwdOpt) : cwdOpt instanceof URL ? fileURLToPath(cwdOpt) : process.cwd()
|
|
199
|
+
const shell = SHELLS.has(tool)
|
|
200
|
+
const words = argv.slice(1).flatMap((a) => (shell ? a.split(/[\s;&|<>()'"`=]+/) : [a.replace(/^--?[\w-]+=/, '')])).filter((w) => w !== '' && !w.startsWith('-'))
|
|
201
|
+
const inRepo: string[] = []
|
|
202
|
+
for (const w of words) {
|
|
203
|
+
const abs = isAbsolute(w) ? w : join(cwd, w)
|
|
204
|
+
if (!isAbsolute(w) && !w.includes('/') && !exists(abs, true)) continue
|
|
205
|
+
const r = rel(abs)
|
|
206
|
+
if (r != null) inRepo.push(r)
|
|
207
|
+
}
|
|
208
|
+
// a tool known to read only the files it names records them; any other
|
|
209
|
+
// tool is trusted only far from the repo: run outside it, naming nothing in it
|
|
210
|
+
if (!shell && FILE_TOOLS.has(tool)) {
|
|
211
|
+
for (const r of inRepo) note(exists(join(rootDir, r), false) ? 'r' : 'g', r)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (rel(cwd) != null || inRepo.length > 0) note('o', tool)
|
|
215
|
+
}
|
|
216
|
+
const realSpawn = Bun.spawn
|
|
217
|
+
const realSpawnSync = Bun.spawnSync
|
|
218
|
+
const viaBun = (real: typeof Bun.spawn | typeof Bun.spawnSync) => function (this: unknown, ...a: unknown[]): unknown {
|
|
219
|
+
const first = a[0]
|
|
220
|
+
const cmd = Array.isArray(first) ? first : (first as { cmd?: unknown } | undefined)?.cmd
|
|
221
|
+
spawned(cmd, ((Array.isArray(first) ? a[1] : first) as { cwd?: unknown } | undefined)?.cwd)
|
|
222
|
+
if (Array.isArray(first)) {
|
|
223
|
+
const opts = (a[1] ?? {}) as { env?: unknown }
|
|
224
|
+
return (real as (...x: unknown[]) => unknown).call(this, first, { ...opts, env: childEnv(opts.env) })
|
|
225
|
+
}
|
|
226
|
+
const opts = (first ?? {}) as { env?: unknown }
|
|
227
|
+
return (real as (...x: unknown[]) => unknown).call(this, { ...opts, env: childEnv(opts.env) })
|
|
228
|
+
}
|
|
229
|
+
Bun.spawn = viaBun(realSpawn) as typeof Bun.spawn
|
|
230
|
+
Bun.spawnSync = viaBun(realSpawnSync) as typeof Bun.spawnSync
|
|
231
|
+
const cpWrap = (n: string, cmdAt: (a: unknown[]) => unknown, optsAt: number | ((a: unknown[]) => number)): unknown => {
|
|
232
|
+
const f = realCp[n]
|
|
233
|
+
if (typeof f !== 'function') return f
|
|
234
|
+
return function (this: unknown, ...a: unknown[]): unknown {
|
|
235
|
+
const cmd = cmdAt(a)
|
|
236
|
+
const i = typeof optsAt === 'number' ? optsAt : optsAt(a)
|
|
237
|
+
const opts = (typeof a[i] === 'object' && a[i] != null && !Array.isArray(a[i]) ? a[i] : {}) as { env?: unknown; cwd?: unknown }
|
|
238
|
+
if (n === 'exec' || n === 'execSync') spawned(['sh', '-c', cmd], opts.cwd)
|
|
239
|
+
else spawned([cmd, ...(Array.isArray(a[1]) ? a[1] : [])], opts.cwd)
|
|
240
|
+
const args = [...a]
|
|
241
|
+
while (args.length < i) args.push(undefined)
|
|
242
|
+
args[i] = { ...opts, env: childEnv(opts.env) }
|
|
243
|
+
return (f as (...x: unknown[]) => unknown).apply(this, args)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const withArgs = (a: unknown[]): number => (Array.isArray(a[1]) ? 2 : 1)
|
|
247
|
+
const cpShim: Record<string, unknown> = {
|
|
248
|
+
...realCp,
|
|
249
|
+
spawn: cpWrap('spawn', (a) => a[0], withArgs),
|
|
250
|
+
spawnSync: cpWrap('spawnSync', (a) => a[0], withArgs),
|
|
251
|
+
execFile: cpWrap('execFile', (a) => a[0], withArgs),
|
|
252
|
+
execFileSync: cpWrap('execFileSync', (a) => a[0], withArgs),
|
|
253
|
+
exec: cpWrap('exec', (a) => a[0], 1),
|
|
254
|
+
execSync: cpWrap('execSync', (a) => a[0], 1),
|
|
255
|
+
fork: cpWrap('fork', () => 'bun', withArgs),
|
|
256
|
+
}
|
|
257
|
+
mock.module('node:child_process', () => ({ ...cpShim, default: cpShim }))
|
|
258
|
+
mock.module('child_process', () => ({ ...cpShim, default: cpShim }))
|
|
259
|
+
const realShell = Bun.$
|
|
260
|
+
Bun.$ = new Proxy(realShell, { apply: (t, self, a) => { note('o', 'sh'); return Reflect.apply(t, self, a) } })
|
|
261
|
+
|
|
262
|
+
// a file:// fetch is a read; a Worker runs code no preload of ours sees
|
|
263
|
+
const realFetch = globalThis.fetch
|
|
264
|
+
globalThis.fetch = Object.assign(function (this: unknown, input: unknown, init?: unknown): unknown {
|
|
265
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as { url?: unknown } | null)?.url
|
|
266
|
+
if (typeof url === 'string' && url.startsWith('file:')) note('r', rel(url))
|
|
267
|
+
return (realFetch as (...x: unknown[]) => unknown).call(this, input, init)
|
|
268
|
+
}, realFetch) as typeof fetch
|
|
269
|
+
for (const name of ['Worker', 'SharedWorker']) {
|
|
270
|
+
const Real = (globalThis as Record<string, unknown>)[name] as (new (...a: unknown[]) => object) | undefined
|
|
271
|
+
if (typeof Real !== 'function') continue
|
|
272
|
+
const Wrapped = function (this: unknown, ...a: unknown[]): object {
|
|
273
|
+
note('o', name.toLowerCase())
|
|
274
|
+
return new Real(...a)
|
|
275
|
+
} as unknown as typeof Real
|
|
276
|
+
Wrapped.prototype = Real.prototype
|
|
277
|
+
;(globalThis as Record<string, unknown>)[name] = Wrapped
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const realFile = Bun.file
|
|
281
|
+
Bun.file = function (this: unknown, p: unknown, o?: unknown): unknown {
|
|
282
|
+
note('r', rel(p))
|
|
283
|
+
return (realFile as (...x: unknown[]) => unknown).call(this, p, o)
|
|
284
|
+
} as typeof Bun.file
|
|
285
|
+
// a Glob keeps no pattern a caller can read, so each one remembers its own:
|
|
286
|
+
// the walked tree is the cwd joined with the pattern's literal head
|
|
287
|
+
const RealGlob = Bun.Glob
|
|
288
|
+
const patterns = new WeakMap<object, string>()
|
|
289
|
+
const Glob = function (this: unknown, pattern: string): unknown {
|
|
290
|
+
const g = new RealGlob(pattern)
|
|
291
|
+
patterns.set(g, pattern)
|
|
292
|
+
return g
|
|
293
|
+
} as unknown as typeof Bun.Glob
|
|
294
|
+
Glob.prototype = RealGlob.prototype
|
|
295
|
+
Bun.Glob = Glob
|
|
296
|
+
const G = RealGlob.prototype as unknown as Record<string, (...a: unknown[]) => unknown>
|
|
297
|
+
for (const n of ['scan', 'scanSync']) {
|
|
298
|
+
const f = G[n]
|
|
299
|
+
if (typeof f !== 'function') continue
|
|
300
|
+
G[n] = function (this: object, opts?: unknown): unknown {
|
|
301
|
+
const cwd = typeof opts === 'string' ? opts : typeof opts === 'object' && opts != null && typeof (opts as { cwd?: unknown }).cwd === 'string' ? (opts as { cwd: string }).cwd : process.cwd()
|
|
302
|
+
note('g', globRoot(patterns.get(this) ?? '**', { cwd }))
|
|
303
|
+
return f.call(this, opts)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// every import resolved at runtime, as an edge from its importer and from
|
|
308
|
+
// every module on the stack: a module a computed import loads is in no
|
|
309
|
+
// static closure, and the edge charges it wherever the load was asked
|
|
310
|
+
// from, whichever suite ran first. A key already seen asks nothing again
|
|
311
|
+
const edges = new Set<string>()
|
|
312
|
+
const imported = (spec: string, importer: string | undefined): void => {
|
|
313
|
+
if (importer == null || importer === '' || spec.startsWith('node:') || spec.startsWith('bun:')) return
|
|
314
|
+
const from = rel(importer)
|
|
315
|
+
if (from == null || from === '.') return
|
|
316
|
+
const key = `${from}\0${spec}\0${new Error().stack ?? ''}`
|
|
317
|
+
if (edges.has(key)) return
|
|
318
|
+
edges.add(key)
|
|
319
|
+
const bare = spec.replace(/\?.*$/, '')
|
|
320
|
+
const base = isAbsolute(bare) ? bare : bare.startsWith('.') ? join(importer, '..', bare) : join(rootDir, bare)
|
|
321
|
+
const abs = [base, `${base}.ts`, `${base}.tsx`, join(base, 'index.ts')].find((x) => exists(x, false))
|
|
322
|
+
if (abs == null) return
|
|
323
|
+
const to = rel(abs)
|
|
324
|
+
// charged like a read: a lazy load inside a hook a preload installed
|
|
325
|
+
// is reached through the modules that called the hook
|
|
326
|
+
if (to == null || to === from) return
|
|
327
|
+
const asked = callers().filter((c) => c !== from && c !== '-')
|
|
328
|
+
if (asked.length === 0) note('L', to, from)
|
|
329
|
+
else note('l', to, [from, ...asked])
|
|
330
|
+
}
|
|
331
|
+
Bun.plugin({ name: 'shipkit-suite-reads', setup(b) { b.onResolve({ filter: /.*/ }, (a) => { imported(a.path, a.importer); return undefined }) } })
|
|
332
|
+
|
|
333
|
+
try { afterAll(flush) } catch { /* not under bun test: the exit hook below records it */ }
|
|
334
|
+
process.on('exit', flush)
|
|
335
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// the gate's typecheck keeps its own incremental cache, one file per project.
|
|
2
|
+
// a tsconfig's tsBuildInfoFile is shared by every tsc that runs that config (a
|
|
3
|
+
// watcher task, a raw script), and a verdict read off a cache another process
|
|
4
|
+
// writes is not a verdict on this tree. no fs here: it only names paths.
|
|
5
|
+
import { basename, dirname, join } from 'path'
|
|
6
|
+
|
|
7
|
+
export const gateBuildInfoArgs = (tsconfig: string): string[] => [
|
|
8
|
+
'--incremental',
|
|
9
|
+
'--tsBuildInfoFile',
|
|
10
|
+
join(dirname(tsconfig), 'node_modules/.cache/shipkit', `${basename(tsconfig, '.json')}.tsbuildinfo`),
|
|
11
|
+
]
|
package/src/ts/typecheck-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// who fixes the one error tsc showed and re-runs into a second one has been
|
|
11
11
|
// told the truth twice and believed it once.
|
|
12
12
|
import { resolveTypecheckProjects } from 'rvlib-shipkit/src/ts/tsconfig-projects.ts'
|
|
13
|
+
import { gateBuildInfoArgs } from 'rvlib-shipkit/src/ts/typecheck-cache.ts'
|
|
13
14
|
|
|
14
15
|
export function typecheckCli(argv: string[]): number {
|
|
15
16
|
const [tsc, tsconfig] = argv
|
|
@@ -30,7 +31,7 @@ export function typecheckCli(argv: string[]): number {
|
|
|
30
31
|
|
|
31
32
|
let worst = 0
|
|
32
33
|
for (const p of resolved.projects) {
|
|
33
|
-
const r = Bun.spawnSync([tsc, '--noEmit', '-p', p], { stdout: 'inherit', stderr: 'inherit' })
|
|
34
|
+
const r = Bun.spawnSync([tsc, '--noEmit', '-p', p, ...gateBuildInfoArgs(p)], { stdout: 'inherit', stderr: 'inherit' })
|
|
34
35
|
if (r.exitCode !== 0) {
|
|
35
36
|
console.log(`FAIL: ${p}`)
|
|
36
37
|
worst = r.exitCode ?? 1
|