@vzn/vx 0.0.102 → 0.0.103
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/package.json +5 -5
- package/src/cli/help.ts +2 -1
- package/src/cli/last.ts +29 -3
- package/src/exec/sandbox-runtime.ts +18 -0
- package/src/workspace/config-imports.ts +97 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.103",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.103",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.103",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.103",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.103"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/help.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
49
49
|
' vx show [PROJECT[#TASK] | TASK] [--format pretty|json]',
|
|
50
50
|
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
51
51
|
' vx why [TASK | PKG#TASK] [--run <runId>] [--format pretty|json] [--cache-dir <path>]',
|
|
52
|
-
' vx last [RUNID] [--list[=N]] [--format pretty|json] [--cache-dir <path>]',
|
|
52
|
+
' vx last [RUNID] [--list[=N]] [--failed] [--format pretty|json] [--cache-dir <path>]',
|
|
53
53
|
' vx completions bash|zsh|fish',
|
|
54
54
|
' vx help [VERB]',
|
|
55
55
|
' vx version',
|
|
@@ -150,6 +150,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
150
150
|
' header (command, when, branch, counts) plus the per-task',
|
|
151
151
|
' table, failures first. No re-execution, no cache probe.',
|
|
152
152
|
' --list[=N] List the N most recent runs (default 10) with run ids.',
|
|
153
|
+
' --failed Only failed runs: the latest one, or with --list the list.',
|
|
153
154
|
' --format <fmt> pretty (default) | json.',
|
|
154
155
|
'',
|
|
155
156
|
'Migration:',
|
package/src/cli/last.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// history in cache.db is the only replay surface, and this verb reads it.
|
|
5
5
|
// Read-only — no config evaluation, no re-hash, no cache probe.
|
|
6
6
|
|
|
7
|
+
import type { Database } from 'bun:sqlite'
|
|
7
8
|
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
8
9
|
import { formatBytes } from './format.js'
|
|
9
10
|
import { flagHint, seeHelp } from './help.js'
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
exitSignal,
|
|
12
13
|
getInvocation,
|
|
13
14
|
getRun,
|
|
15
|
+
type InvocationDetail,
|
|
14
16
|
listInvocations,
|
|
15
17
|
type RunSummaryRow,
|
|
16
18
|
} from '../orchestrator/index.js'
|
|
@@ -22,6 +24,8 @@ import { resolveRunId, shortRunId } from './run-id.js'
|
|
|
22
24
|
interface LastArgs {
|
|
23
25
|
runId?: string
|
|
24
26
|
list?: number
|
|
27
|
+
/** Only runs that failed: the latest one replayed, or the list narrowed. */
|
|
28
|
+
failed?: boolean
|
|
25
29
|
format: 'pretty' | 'json'
|
|
26
30
|
/** `--cache-dir`: read the history a run with the same flag wrote. */
|
|
27
31
|
cacheDir?: string
|
|
@@ -48,6 +52,10 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
48
52
|
out.list = n
|
|
49
53
|
continue
|
|
50
54
|
}
|
|
55
|
+
if (a === '--failed') {
|
|
56
|
+
out.failed = true
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
51
59
|
if (a === '--format' || a.startsWith('--format=')) {
|
|
52
60
|
const fv = a === '--format' ? args[++i] : a.slice(9)
|
|
53
61
|
if (fv !== 'pretty' && fv !== 'json') {
|
|
@@ -82,9 +90,20 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
82
90
|
error: `a run id and --list do not combine: replay ${out.runId}, or list runs`,
|
|
83
91
|
}
|
|
84
92
|
}
|
|
93
|
+
if (out.runId !== undefined && out.failed === true) {
|
|
94
|
+
return { ...out, error: `a run id and --failed do not combine: replay ${out.runId}` }
|
|
95
|
+
}
|
|
85
96
|
return out
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
/** The `limit` most recent runs that failed, newest first. */
|
|
100
|
+
function failedInvocations(db: Database, limit: number): InvocationDetail[] {
|
|
101
|
+
const ids = db
|
|
102
|
+
.query('SELECT run_id FROM invocations WHERE exit_ok = 0 ORDER BY rowid DESC LIMIT ?')
|
|
103
|
+
.all(limit) as Array<{ run_id: string }>
|
|
104
|
+
return ids.map((r) => getInvocation(db, r.run_id)).filter((i) => i !== null)
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
89
108
|
|
|
90
109
|
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
@@ -190,7 +209,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
190
209
|
const db = cache.dbHandle()
|
|
191
210
|
|
|
192
211
|
if (parsed.list !== undefined) {
|
|
193
|
-
const invocations =
|
|
212
|
+
const invocations =
|
|
213
|
+
parsed.failed === true
|
|
214
|
+
? failedInvocations(db, parsed.list)
|
|
215
|
+
: listInvocations(db, { limit: parsed.list })
|
|
194
216
|
if (parsed.format === 'json') {
|
|
195
217
|
process.stdout.write(`${JSON.stringify(invocations)}\n`)
|
|
196
218
|
return 0
|
|
@@ -213,12 +235,16 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
213
235
|
const inv =
|
|
214
236
|
parsed.runId !== undefined
|
|
215
237
|
? getInvocation(db, resolveRunId(db, parsed.runId, 'vx last') ?? parsed.runId)
|
|
216
|
-
:
|
|
238
|
+
: parsed.failed === true
|
|
239
|
+
? (failedInvocations(db, 1)[0] ?? null)
|
|
240
|
+
: (listInvocations(db, { limit: 1 })[0] ?? null)
|
|
217
241
|
if (inv === null || inv === undefined) {
|
|
218
242
|
throw new UserError(
|
|
219
243
|
parsed.runId !== undefined
|
|
220
244
|
? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
|
|
221
|
-
:
|
|
245
|
+
: parsed.failed === true
|
|
246
|
+
? 'vx last: no recorded run failed'
|
|
247
|
+
: 'vx last: no recorded runs yet — run something first',
|
|
222
248
|
)
|
|
223
249
|
}
|
|
224
250
|
const detail = getRun(db, inv.runId)
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
chmodSync,
|
|
30
30
|
closeSync,
|
|
31
31
|
lstatSync,
|
|
32
|
+
existsSync,
|
|
32
33
|
mkdirSync,
|
|
33
34
|
openSync,
|
|
34
35
|
readlinkSync,
|
|
@@ -461,6 +462,21 @@ function linuxToolPaths(): { bwrapPath?: string; socatPath?: string } {
|
|
|
461
462
|
return paths
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Whether this Linux host speaks IPv6. SRT's in-sandbox network bridge is
|
|
467
|
+
* `socat TCP-LISTEN:3128`, and socat 1.8 opens that as an IPv6 socket: on
|
|
468
|
+
* a host without IPv6 it failed ("Address family not supported by
|
|
469
|
+
* protocol"), its error went to /dev/null, and every networked task met
|
|
470
|
+
* only "connection refused" on the proxy (J-33's lead, B-22). Without
|
|
471
|
+
* IPv6 the wrapped command sets `SOCAT_DEFAULT_LISTEN_IP=4`, socat's own
|
|
472
|
+
* switch for the listen family; with it nothing changes. Asked once.
|
|
473
|
+
*/
|
|
474
|
+
let hasIpv6: boolean | undefined
|
|
475
|
+
function hostHasIpv6(): boolean {
|
|
476
|
+
hasIpv6 ??= existsSync('/proc/net/if_inet6')
|
|
477
|
+
return hasIpv6
|
|
478
|
+
}
|
|
479
|
+
|
|
464
480
|
/**
|
|
465
481
|
* One-time SRT initialization per orchestrator run. Starts the proxy
|
|
466
482
|
* servers + (on macOS) the violation log monitor. Safe to call repeatedly
|
|
@@ -922,6 +938,8 @@ export async function wrapSandboxedCommand(
|
|
|
922
938
|
// the namespace goes with vx, a `setsid` daemon inside included, a
|
|
923
939
|
// traced one-shot task too: its strace runs inside (B-11).
|
|
924
940
|
if (process.platform === 'linux' && /^\S*bwrap /.test(wrapped)) wrapped = `exec ${wrapped}`
|
|
941
|
+
if (process.platform === 'linux' && !hostHasIpv6())
|
|
942
|
+
wrapped = `SOCAT_DEFAULT_LISTEN_IP=4 ${wrapped}`
|
|
925
943
|
if (args.server === true) liveServers.add(tag)
|
|
926
944
|
if (ports.length > 0) spawnHostBridges(ports, tag)
|
|
927
945
|
return {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
//
|
|
19
19
|
// - RELATIVE specifiers only. A bare specifier is a package; it moves when
|
|
20
20
|
// the lockfile moves, which the workspace fingerprint already covers.
|
|
21
|
+
// A tsconfig `paths` / `baseUrl` alias is the exception: Bun loads it
|
|
22
|
+
// from disk (D-27).
|
|
21
23
|
// - Descend only through files owned by NO project. A config reaching into
|
|
22
24
|
// another project (say a site's `vx.config.ts` importing
|
|
23
25
|
// `../core/src/index.ts`) records that edge and STOPS there — following
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
// closure, and the containment channel already selects the project that
|
|
26
28
|
// owns it.
|
|
27
29
|
|
|
28
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
30
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
29
31
|
import { realpath } from 'node:fs/promises'
|
|
30
32
|
import { builtinModules } from 'node:module'
|
|
31
33
|
import path from 'node:path'
|
|
@@ -137,18 +139,6 @@ export function unprovidedBareImports(
|
|
|
137
139
|
return out
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
/** The nearest `tsconfig.json`, else `jsconfig.json`, at or above `dir`: Bun reads that one alone. */
|
|
141
|
-
function nearestTsconfig(dir: string): string | undefined {
|
|
142
|
-
for (let d = path.resolve(dir); ;) {
|
|
143
|
-
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
144
|
-
if (existsSync(path.join(d, name))) return path.join(d, name)
|
|
145
|
-
}
|
|
146
|
-
const up = path.dirname(d)
|
|
147
|
-
if (up === d) return undefined
|
|
148
|
-
d = up
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
142
|
interface CompilerPaths {
|
|
153
143
|
paths?: Record<string, unknown>
|
|
154
144
|
/** What `paths` targets resolve against: `baseUrl`, else the config's own dir. */
|
|
@@ -161,7 +151,9 @@ interface CompilerPaths {
|
|
|
161
151
|
* against the config that set it. A package `extends` is not followed: its
|
|
162
152
|
* paths are unseen, so a specifier only they map stays refused.
|
|
163
153
|
*/
|
|
164
|
-
function compilerPaths(file: string, depth = 0): CompilerPaths {
|
|
154
|
+
function compilerPaths(file: string, memo?: TsconfigMemo, depth = 0): CompilerPaths {
|
|
155
|
+
const known = memo?.get(file)
|
|
156
|
+
if (known !== undefined) return known
|
|
165
157
|
let json: unknown
|
|
166
158
|
try {
|
|
167
159
|
json = Bun.JSONC.parse(readFileSync(file, 'utf8'))
|
|
@@ -175,7 +167,7 @@ function compilerPaths(file: string, depth = 0): CompilerPaths {
|
|
|
175
167
|
for (const base of typeof ext === 'string' ? [ext] : Array.isArray(ext) ? ext : []) {
|
|
176
168
|
if (depth > 8 || typeof base !== 'string' || !base.startsWith('.')) continue
|
|
177
169
|
const f = path.resolve(dir, base)
|
|
178
|
-
Object.assign(out, compilerPaths(f.endsWith('.json') ? f : f + '.json', depth + 1))
|
|
170
|
+
Object.assign(out, compilerPaths(f.endsWith('.json') ? f : f + '.json', memo, depth + 1))
|
|
179
171
|
}
|
|
180
172
|
const opts = (json as { compilerOptions?: unknown }).compilerOptions
|
|
181
173
|
if (typeof opts !== 'object' || opts === null) return out
|
|
@@ -187,6 +179,7 @@ function compilerPaths(file: string, depth = 0): CompilerPaths {
|
|
|
187
179
|
} else if (out.paths !== undefined && out.baseUrl !== undefined) {
|
|
188
180
|
out.pathsBase = out.baseUrl
|
|
189
181
|
}
|
|
182
|
+
memo?.set(file, out)
|
|
190
183
|
return out
|
|
191
184
|
}
|
|
192
185
|
|
|
@@ -204,15 +197,45 @@ function fileAt(target: string): string | undefined {
|
|
|
204
197
|
return undefined
|
|
205
198
|
}
|
|
206
199
|
|
|
200
|
+
/** Paths per directory and per tsconfig file, memoised across one walk. */
|
|
201
|
+
type TsconfigMemo = Map<string, CompilerPaths>
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The paths of the nearest `tsconfig.json`, else `jsconfig.json`, at or
|
|
205
|
+
* above `dir`: Bun reads that one alone.
|
|
206
|
+
*/
|
|
207
|
+
function pathsAt(dir: string, memo?: TsconfigMemo): CompilerPaths {
|
|
208
|
+
const known = memo?.get(dir)
|
|
209
|
+
if (known !== undefined) return known
|
|
210
|
+
const file = ['tsconfig.json', 'jsconfig.json'].map((n) => path.join(dir, n)).find(existsSync)
|
|
211
|
+
const up = path.dirname(dir)
|
|
212
|
+
const cp = file !== undefined ? compilerPaths(file, memo) : up === dir ? {} : pathsAt(up, memo)
|
|
213
|
+
memo?.set(dir, cp)
|
|
214
|
+
return cp
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
218
|
+
* Whether `source` quotes a bare specifier the nearest tsconfig maps: the
|
|
219
|
+
* textual pass that keeps a config importing only packages unscanned, as
|
|
220
|
+
* D-23's keeps one importing nothing relative (D-27).
|
|
211
221
|
*/
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
function hasAliasCandidate(source: string, fromDir: string, memo: TsconfigMemo): boolean {
|
|
223
|
+
SPECIFIER.lastIndex = 0
|
|
224
|
+
for (let m = SPECIFIER.exec(source); m !== null; m = SPECIFIER.exec(source)) {
|
|
225
|
+
const spec = m[1] ?? m[2] ?? ''
|
|
226
|
+
if (needsLookup(spec) && tsconfigBases(spec, fromDir, memo).length > 0) return true
|
|
227
|
+
}
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The paths a bare specifier names through the nearest tsconfig's `paths`
|
|
233
|
+
* (each matching target, in order) or `baseUrl`, before extensions: Bun
|
|
234
|
+
* tries them ahead of `node_modules`.
|
|
235
|
+
*/
|
|
236
|
+
function tsconfigBases(spec: string, fromDir: string, memo?: TsconfigMemo): string[] {
|
|
237
|
+
const { paths, pathsBase, baseUrl } = pathsAt(path.resolve(fromDir), memo)
|
|
238
|
+
const out: string[] = []
|
|
216
239
|
for (const [key, targets] of Object.entries(paths ?? {})) {
|
|
217
240
|
const star = key.indexOf('*')
|
|
218
241
|
let hole: string
|
|
@@ -226,12 +249,39 @@ function tsconfigTarget(spec: string, fromDir: string): string | undefined {
|
|
|
226
249
|
hole = spec.slice(head.length, spec.length - tail.length)
|
|
227
250
|
}
|
|
228
251
|
for (const t of Array.isArray(targets) ? targets : []) {
|
|
229
|
-
if (typeof t
|
|
230
|
-
const hit = fileAt(path.resolve(pathsBase!, t.replace('*', hole)))
|
|
231
|
-
if (hit !== undefined) return hit
|
|
252
|
+
if (typeof t === 'string') out.push(path.resolve(pathsBase!, t.replace('*', hole)))
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
|
-
|
|
255
|
+
if (baseUrl !== undefined) out.push(path.resolve(baseUrl, spec))
|
|
256
|
+
return out
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The local file a bare specifier names through the nearest tsconfig, as
|
|
261
|
+
* Bun resolves it (D-26): an alias whose target exists loads from disk and
|
|
262
|
+
* never reaches the registry.
|
|
263
|
+
*/
|
|
264
|
+
function tsconfigTarget(spec: string, fromDir: string, memo?: TsconfigMemo): string | undefined {
|
|
265
|
+
for (const base of tsconfigBases(spec, fromDir, memo)) {
|
|
266
|
+
const hit = fileAt(base)
|
|
267
|
+
if (hit !== undefined) return hit
|
|
268
|
+
}
|
|
269
|
+
return undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A path as Bun names it: realpath'd when it exists. */
|
|
273
|
+
function realpathOr(file: string): string {
|
|
274
|
+
try {
|
|
275
|
+
return realpathSync(file)
|
|
276
|
+
} catch {
|
|
277
|
+
return file
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** A path and, bare of an extension, the files Bun would try for it. */
|
|
282
|
+
function expansions(named: string): string[] {
|
|
283
|
+
if (path.extname(named) !== '') return [named]
|
|
284
|
+
return [named, ...RESOLVED_EXTENSIONS.flatMap((e) => [named + e, path.join(named, 'index' + e)])]
|
|
235
285
|
}
|
|
236
286
|
|
|
237
287
|
/**
|
|
@@ -251,8 +301,14 @@ const RELATIVE_CANDIDATE = /["'`](?:\.\.?\/|[^"'`\n]*\\)/
|
|
|
251
301
|
* reason. `import type` is erased by `scanImports` and so contributes no edge,
|
|
252
302
|
* which is correct: an erased import cannot move a resolved value.
|
|
253
303
|
*/
|
|
254
|
-
function scanLocalImports(
|
|
255
|
-
|
|
304
|
+
function scanLocalImports(
|
|
305
|
+
source: string,
|
|
306
|
+
fromDir: string,
|
|
307
|
+
loader: 'ts' | 'js',
|
|
308
|
+
memo: TsconfigMemo,
|
|
309
|
+
): string[] {
|
|
310
|
+
const aliased = hasAliasCandidate(source, fromDir, memo)
|
|
311
|
+
if (!aliased && !RELATIVE_CANDIDATE.test(source)) return []
|
|
256
312
|
let specifiers: string[]
|
|
257
313
|
try {
|
|
258
314
|
specifiers = scanner(loader)
|
|
@@ -263,7 +319,16 @@ function scanLocalImports(source: string, fromDir: string, loader: 'ts' | 'js'):
|
|
|
263
319
|
}
|
|
264
320
|
const out: string[] = []
|
|
265
321
|
for (const spec of specifiers) {
|
|
266
|
-
if (!spec.startsWith('./') && !spec.startsWith('../'))
|
|
322
|
+
if (!spec.startsWith('./') && !spec.startsWith('../')) {
|
|
323
|
+
// A tsconfig alias is a local import Bun resolves by path (D-27); a
|
|
324
|
+
// target the change deleted still names its paths, as below.
|
|
325
|
+
if (!aliased || spec.startsWith('.') || spec.startsWith('/') || !needsLookup(spec)) continue
|
|
326
|
+
const bases = tsconfigBases(spec, fromDir, memo)
|
|
327
|
+
const hit = bases.map(fileAt).find((f) => f !== undefined)
|
|
328
|
+
if (hit !== undefined) out.push(realpathOr(hit))
|
|
329
|
+
else out.push(...bases.flatMap(expansions))
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
267
332
|
try {
|
|
268
333
|
out.push(Bun.resolveSync(spec, fromDir))
|
|
269
334
|
} catch {
|
|
@@ -273,12 +338,7 @@ function scanLocalImports(source: string, fromDir: string, loader: 'ts' | 'js'):
|
|
|
273
338
|
// of an extension, the files Bun would have tried), so the deleted
|
|
274
339
|
// path in the diff still reaches its importer (item 958). Selection
|
|
275
340
|
// may widen; it is never hashed.
|
|
276
|
-
|
|
277
|
-
out.push(named)
|
|
278
|
-
if (path.extname(spec) === '') {
|
|
279
|
-
for (const ext of RESOLVED_EXTENSIONS)
|
|
280
|
-
out.push(named + ext, path.join(named, 'index' + ext))
|
|
281
|
-
}
|
|
341
|
+
out.push(...expansions(path.resolve(fromDir, spec)))
|
|
282
342
|
}
|
|
283
343
|
}
|
|
284
344
|
return out
|
|
@@ -361,6 +421,7 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
361
421
|
|
|
362
422
|
// target → the files that import it. Reversed up front so one BFS from the
|
|
363
423
|
// changed set answers every root at once, instead of a walk per root.
|
|
424
|
+
const tsconfigs: TsconfigMemo = new Map()
|
|
364
425
|
const importedBy = new Map<string, string[]>()
|
|
365
426
|
const visited = new Set<string>()
|
|
366
427
|
// A level at a time, its files read together: one awaited read per
|
|
@@ -383,7 +444,7 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
383
444
|
if (entry === null) continue
|
|
384
445
|
const { file, source } = entry
|
|
385
446
|
const loader = TS_EXT.has(path.extname(file)) ? 'ts' : 'js'
|
|
386
|
-
for (const target of scanLocalImports(source, path.dirname(file), loader)) {
|
|
447
|
+
for (const target of scanLocalImports(source, path.dirname(file), loader, tsconfigs)) {
|
|
387
448
|
if (!target.startsWith(workspaceRoot + path.sep)) continue
|
|
388
449
|
if (target.split(path.sep).includes('node_modules')) continue
|
|
389
450
|
const list = importedBy.get(target)
|