@vzn/vx 0.0.341 → 0.0.342
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 +1 -1
- package/package.json +5 -5
- package/src/exec/runner.ts +10 -4
- package/src/orchestrator/hit-restore.ts +55 -35
- package/src/orchestrator/run.ts +20 -3
- package/src/orchestrator/summary.ts +4 -1
- package/src/workspace/filter.ts +10 -3
- package/src/workspace/migrate-scripts.ts +39 -6
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ key by key: [Turbo and Nx config support](https://vznjs.github.io/vx/compare/tur
|
|
|
100
100
|
## Install
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
npm install -D @vzn/vx # or: pnpm add -D · yarn add -D · bun add -d
|
|
103
|
+
npm install -D @vzn/vx # or: pnpm add -D -w · yarn add -D (-W on Yarn 1) · bun add -d
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
The package ships a prebuilt binary for Linux and macOS, x64 and arm64
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.342",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.342",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.342",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.342",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.342"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/runner.ts
CHANGED
|
@@ -784,6 +784,8 @@ export function droppedOutputLine(dropped: number): string {
|
|
|
784
784
|
)
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
+
const isHighSurrogate = (code: number): boolean => code >= 0xd800 && code <= 0xdbff
|
|
788
|
+
|
|
787
789
|
/**
|
|
788
790
|
* A head-and-tail accumulator: the head fills once, the tail is a ring
|
|
789
791
|
* of chunks trimmed from the front, so memory is bounded by the two
|
|
@@ -797,11 +799,14 @@ class BoundedCapture {
|
|
|
797
799
|
|
|
798
800
|
push(chunk: string): void {
|
|
799
801
|
if (this.head.length < CAPTURE_HEAD_CHARS) {
|
|
800
|
-
|
|
802
|
+
let room = CAPTURE_HEAD_CHARS - this.head.length
|
|
801
803
|
if (chunk.length <= room) {
|
|
802
804
|
this.head += chunk
|
|
803
805
|
return
|
|
804
806
|
}
|
|
807
|
+
// Never between a surrogate pair's halves: the bounds count UTF-16
|
|
808
|
+
// units, and a halved character read U+FFFD in the replay.
|
|
809
|
+
if (isHighSurrogate(chunk.charCodeAt(room - 1))) room--
|
|
805
810
|
this.head += chunk.slice(0, room)
|
|
806
811
|
chunk = chunk.slice(room)
|
|
807
812
|
}
|
|
@@ -815,9 +820,10 @@ class BoundedCapture {
|
|
|
815
820
|
this.tailLen -= first.length
|
|
816
821
|
this.dropped += first.length
|
|
817
822
|
} else {
|
|
818
|
-
|
|
819
|
-
this.
|
|
820
|
-
this.
|
|
823
|
+
const cut = isHighSurrogate(first.charCodeAt(excess - 1)) ? excess + 1 : excess
|
|
824
|
+
this.tail[0] = first.slice(cut)
|
|
825
|
+
this.tailLen -= cut
|
|
826
|
+
this.dropped += cut
|
|
821
827
|
}
|
|
822
828
|
}
|
|
823
829
|
}
|
|
@@ -61,7 +61,7 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
61
61
|
// The directories the positive globs cover whole. A `!` entry inside one
|
|
62
62
|
// leaves the snapshot sound: a file added or removed there still bumps a
|
|
63
63
|
// recorded directory, and the walk that follows applies the `!` (A-46).
|
|
64
|
-
const dirPrefixes =
|
|
64
|
+
const dirPrefixes = dirPrefixesOf(outputs)
|
|
65
65
|
const cleanArgs = {
|
|
66
66
|
projectDir: node.projectDir,
|
|
67
67
|
outputs,
|
|
@@ -133,52 +133,54 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
133
133
|
setKnown = covers && (await args.cache.outputDirsCurrent!(node.projectDir, dirRows))
|
|
134
134
|
endDirs()
|
|
135
135
|
}
|
|
136
|
-
let
|
|
137
|
-
let actualWsAbs: string[]
|
|
136
|
+
let treeMatches: boolean
|
|
138
137
|
if (setKnown) {
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// The snapshot proved the tree holds exactly the entry's project
|
|
139
|
+
// rows (one per path, the table's key) and no workspace output is
|
|
140
|
+
// declared, so the comparisons below hold by construction: mapping
|
|
141
|
+
// each row to a path and back was ~5 ms per 1,000 warm hits.
|
|
142
|
+
treeMatches = wsExpected.length === 0
|
|
141
143
|
} else {
|
|
142
144
|
const endGlob = span('output glob')
|
|
143
|
-
actualAbs = await resolveOutputs({
|
|
145
|
+
const actualAbs = await resolveOutputs({
|
|
144
146
|
projectDir: node.projectDir,
|
|
145
147
|
outputs,
|
|
146
148
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
147
149
|
})
|
|
148
|
-
actualWsAbs = await resolveWorkspaceOutputs({
|
|
150
|
+
const actualWsAbs = await resolveWorkspaceOutputs({
|
|
149
151
|
workspaceRoot: args.workspaceRoot,
|
|
150
152
|
outputs: wsOutputs,
|
|
151
153
|
})
|
|
152
154
|
endGlob()
|
|
155
|
+
const setsMatch = (
|
|
156
|
+
actual: readonly string[],
|
|
157
|
+
exp: ReadonlyArray<{ path: string }>,
|
|
158
|
+
): boolean => {
|
|
159
|
+
const expSet = new Set(exp.map((e) => e.path))
|
|
160
|
+
return actual.length === expSet.size && actual.every((r) => expSet.has(r))
|
|
161
|
+
}
|
|
162
|
+
// A stray a dependant's glob could have added is not a stray; a path
|
|
163
|
+
// this entry recorded is never dropped, even where that glob covers
|
|
164
|
+
// the whole tree (strapi's shape: both on `dist`).
|
|
165
|
+
const expectedRels = new Set(projExpected.map((e) => e.path))
|
|
166
|
+
const expectedWsRels = new Set(wsExpected.map((e) => e.path))
|
|
167
|
+
const actualRels = actualAbs
|
|
168
|
+
.map((p) => path.relative(node.projectDir, p).split(path.sep).join('/'))
|
|
169
|
+
.filter((rel) => expectedRels.has(rel) || !isAddition(rel))
|
|
170
|
+
const actualWsRels = actualWsAbs
|
|
171
|
+
.map((p) => path.relative(args.workspaceRoot, p).split(path.sep).join('/'))
|
|
172
|
+
.filter((rel) => expectedWsRels.has(rel) || !isAddition(rel))
|
|
173
|
+
const rowsPresent = (
|
|
174
|
+
actual: readonly string[],
|
|
175
|
+
exp: ReadonlyArray<{ path: string }>,
|
|
176
|
+
): boolean => {
|
|
177
|
+
const have = new Set(actual)
|
|
178
|
+
return exp.every((e) => have.has(e.path))
|
|
179
|
+
}
|
|
180
|
+
treeMatches = additive
|
|
181
|
+
? rowsPresent(actualRels, projExpected) && rowsPresent(actualWsRels, wsExpected)
|
|
182
|
+
: setsMatch(actualRels, projExpected) && setsMatch(actualWsRels, wsExpected)
|
|
153
183
|
}
|
|
154
|
-
const setsMatch = (
|
|
155
|
-
actual: readonly string[],
|
|
156
|
-
exp: ReadonlyArray<{ path: string }>,
|
|
157
|
-
): boolean => {
|
|
158
|
-
const expSet = new Set(exp.map((e) => e.path))
|
|
159
|
-
return actual.length === expSet.size && actual.every((r) => expSet.has(r))
|
|
160
|
-
}
|
|
161
|
-
// A stray a dependant's glob could have added is not a stray; a path
|
|
162
|
-
// this entry recorded is never dropped, even where that glob covers
|
|
163
|
-
// the whole tree (strapi's shape: both on `dist`).
|
|
164
|
-
const expectedRels = new Set(projExpected.map((e) => e.path))
|
|
165
|
-
const expectedWsRels = new Set(wsExpected.map((e) => e.path))
|
|
166
|
-
const actualRels = actualAbs
|
|
167
|
-
.map((p) => path.relative(node.projectDir, p).split(path.sep).join('/'))
|
|
168
|
-
.filter((rel) => expectedRels.has(rel) || !isAddition(rel))
|
|
169
|
-
const actualWsRels = actualWsAbs
|
|
170
|
-
.map((p) => path.relative(args.workspaceRoot, p).split(path.sep).join('/'))
|
|
171
|
-
.filter((rel) => expectedWsRels.has(rel) || !isAddition(rel))
|
|
172
|
-
const rowsPresent = (
|
|
173
|
-
actual: readonly string[],
|
|
174
|
-
exp: ReadonlyArray<{ path: string }>,
|
|
175
|
-
): boolean => {
|
|
176
|
-
const have = new Set(actual)
|
|
177
|
-
return exp.every((e) => have.has(e.path))
|
|
178
|
-
}
|
|
179
|
-
const treeMatches = additive
|
|
180
|
-
? rowsPresent(actualRels, projExpected) && rowsPresent(actualWsRels, wsExpected)
|
|
181
|
-
: setsMatch(actualRels, projExpected) && setsMatch(actualWsRels, wsExpected)
|
|
182
184
|
if (treeMatches) {
|
|
183
185
|
const endStat = span('output stat')
|
|
184
186
|
skipRestore =
|
|
@@ -307,3 +309,21 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
|
|
|
307
309
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
308
310
|
}
|
|
309
311
|
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The whole-subtree directories of a task's output globs, memoised by the
|
|
315
|
+
* list: a function of the declared globs alone, and a workspace declares a
|
|
316
|
+
* handful of output lists over thousands of tasks (~5 ms of a 1,000-hit
|
|
317
|
+
* warm run, unmemoised). Callers only read the result.
|
|
318
|
+
*/
|
|
319
|
+
function dirPrefixesOf(outputs: readonly string[]): readonly string[] | null {
|
|
320
|
+
const key = JSON.stringify(outputs)
|
|
321
|
+
let prefixes = prefixMemo.get(key)
|
|
322
|
+
if (prefixes === undefined) {
|
|
323
|
+
prefixes = wholeSubtreePrefixes(splitNegations(outputs).positive)
|
|
324
|
+
prefixMemo.set(key, prefixes)
|
|
325
|
+
}
|
|
326
|
+
return prefixes
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const prefixMemo = new Map<string, readonly string[] | null>()
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -219,12 +219,29 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
219
219
|
// it twice (item 635). Otherwise a fresh internal bus.
|
|
220
220
|
const bus = options.bus ?? createEventBus()
|
|
221
221
|
const unsubscribeTerminal = bus.subscribe(terminalSubscriber(sink))
|
|
222
|
-
|
|
223
|
-
return await runOnBus(options, bus, colors, () => terminal?.failureRecap() ?? [])
|
|
224
|
-
} finally {
|
|
222
|
+
const detach = (): void => {
|
|
225
223
|
unsubscribeTerminal()
|
|
226
224
|
terminal?.settle()
|
|
227
225
|
}
|
|
226
|
+
let held = false
|
|
227
|
+
try {
|
|
228
|
+
const summary = await runOnBus(options, bus, colors, () => terminal?.failureRecap() ?? [])
|
|
229
|
+
if (summary.persistent === undefined) return summary
|
|
230
|
+
// Servers handed back still running still write: `vx watch dev` printed
|
|
231
|
+
// none of its server's log while it idled, the renderer gone with this
|
|
232
|
+
// return (C-57). It stays until the caller stops them.
|
|
233
|
+
held = true
|
|
234
|
+
const { stop } = summary.persistent
|
|
235
|
+
return {
|
|
236
|
+
...summary,
|
|
237
|
+
persistent: {
|
|
238
|
+
...summary.persistent,
|
|
239
|
+
stop: (signal) => stop(signal).finally(detach),
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
if (!held) detach()
|
|
244
|
+
}
|
|
228
245
|
}
|
|
229
246
|
|
|
230
247
|
async function runOnBus(
|
|
@@ -419,9 +419,12 @@ export function formatSkippedSection(outcomes: readonly TaskOutcome[]): string[]
|
|
|
419
419
|
list.push(o.node.id)
|
|
420
420
|
groups.set(cause, list)
|
|
421
421
|
}
|
|
422
|
+
// "Blocked upstream" only when every skip was: a fail-fast skip had no
|
|
423
|
+
// blocker, and the header claimed one over its own cause line (C-55).
|
|
424
|
+
const blocked = skipped.every((o) => o.blockedBy !== undefined) ? ' \u2014 blocked upstream' : ''
|
|
422
425
|
const lines = [
|
|
423
426
|
'',
|
|
424
|
-
` Skipped: ${skipped.length} task${skipped.length === 1 ? '' : 's'} never started
|
|
427
|
+
` Skipped: ${skipped.length} task${skipped.length === 1 ? '' : 's'} never started${blocked}`,
|
|
425
428
|
]
|
|
426
429
|
const NAMES = 8
|
|
427
430
|
for (const [cause, ids] of [...groups].sort((a, b) => b[1].length - a[1].length)) {
|
package/src/workspace/filter.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface ParsedFilter {
|
|
|
49
49
|
sinceViaDeps?: true
|
|
50
50
|
/** A path form carrying a glob (`./packages/*`): matched over the root-relative project dir. */
|
|
51
51
|
pathGlob?: Bun.Glob
|
|
52
|
+
/** `./a/**`'s `a`: a trailing `**` matches zero dirs, and `Bun.Glob` does not (D-84). */
|
|
53
|
+
pathGlobBase?: Bun.Glob
|
|
52
54
|
/** The workspace root `pathGlob` is relative to. */
|
|
53
55
|
pathRoot?: string
|
|
54
56
|
/** `//`: the project at `matcher` itself, never the ones under it. */
|
|
@@ -142,6 +144,7 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
|
142
144
|
let isPath = false
|
|
143
145
|
let matcher = s
|
|
144
146
|
let pathGlob: Bun.Glob | undefined
|
|
147
|
+
let pathGlobBase: Bun.Glob | undefined
|
|
145
148
|
const pathForm =
|
|
146
149
|
s.startsWith('./') || s === '.'
|
|
147
150
|
? s
|
|
@@ -157,7 +160,9 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
|
157
160
|
// A member glob, not a task glob: `Bun.Glob`'s alphabet, the class included.
|
|
158
161
|
if (BUN_GLOB_WILDCARDS.test(pathForm)) {
|
|
159
162
|
const rel = path.relative(workspaceRoot, matcher).split(path.sep).join('/')
|
|
160
|
-
|
|
163
|
+
const glob = rel.replace(/\/+$/, '')
|
|
164
|
+
pathGlob = new Bun.Glob(glob)
|
|
165
|
+
if (glob.endsWith('/**')) pathGlobBase = new Bun.Glob(glob.slice(0, -3))
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -171,6 +176,7 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
|
171
176
|
isPath,
|
|
172
177
|
matcher,
|
|
173
178
|
...(pathGlob !== undefined ? { pathGlob, pathRoot: workspaceRoot } : {}),
|
|
179
|
+
...(pathGlobBase !== undefined ? { pathGlobBase } : {}),
|
|
174
180
|
...(gitSince !== undefined ? { gitSince } : {}),
|
|
175
181
|
...(sinceViaDeps ? { sinceViaDeps: true as const } : {}),
|
|
176
182
|
}
|
|
@@ -221,13 +227,14 @@ function matchSelector(filter: ParsedFilter, projects: ProjectMeta[]): string[]
|
|
|
221
227
|
if (filter.pathGlob !== undefined) {
|
|
222
228
|
// The glob is matched against the project's own dir, as pnpm and
|
|
223
229
|
// Turbo do: `./packages/*` is the packages directly under `packages`,
|
|
224
|
-
// `./packages/**` reaches the nested ones too
|
|
230
|
+
// `./packages/**` reaches the nested ones too, and `./packages/kit/**`
|
|
231
|
+
// kit itself (kit's `check`: pnpm and Turbo read `**` as zero dirs too).
|
|
225
232
|
for (const p of projects) {
|
|
226
233
|
const rel = path
|
|
227
234
|
.relative(filter.pathRoot ?? '', p.dir)
|
|
228
235
|
.split(path.sep)
|
|
229
236
|
.join('/')
|
|
230
|
-
if (filter.pathGlob.match(rel)) out.push(p.name)
|
|
237
|
+
if (filter.pathGlob.match(rel) || filter.pathGlobBase?.match(rel) === true) out.push(p.name)
|
|
231
238
|
}
|
|
232
239
|
}
|
|
233
240
|
return out
|
|
@@ -253,7 +253,7 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
|
|
|
253
253
|
* task again beside the member's own (D-45).
|
|
254
254
|
*/
|
|
255
255
|
const RUNS_MEMBERS =
|
|
256
|
-
/(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\bworkspaces?\s+(foreach|run)\b|\byarn\s+workspace\s|\bvp\s+run\s
|
|
256
|
+
/(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\bworkspaces?\s+(foreach|run)\b|\byarn\s+workspace\s|\bvp\s+run\s/
|
|
257
257
|
|
|
258
258
|
/** A workspace flag, read only where a package manager takes it (`pmRunsMembers`). */
|
|
259
259
|
const MEMBER_FLAG =
|
|
@@ -263,14 +263,15 @@ const MEMBER_FLAG =
|
|
|
263
263
|
* The workspace flags are a package manager's: on the program it runs they
|
|
264
264
|
* mean something else, and berry's `bench` (`yarn node -r ./setup.ts …`,
|
|
265
265
|
* node's `--require`) was left out as running the members (D-81). With no
|
|
266
|
-
* manager named,
|
|
266
|
+
* manager named, `node <bin> run` is one (npm/cli's own npm), and not
|
|
267
|
+
* `docker run -w` (D-83).
|
|
267
268
|
*/
|
|
268
269
|
function pmRunsMembers(script: string): boolean {
|
|
269
270
|
for (const segment of script.split(/&&|\|\||[;|()]/)) {
|
|
270
271
|
const words = segment.trim().split(/\s+/)
|
|
271
272
|
let i = words.findIndex((w) => /^(pnpm|npm|yarn|bun)$/.test(w))
|
|
272
273
|
// npm/cli runs itself: `node . run test --workspaces`.
|
|
273
|
-
if (i < 0) i = words.
|
|
274
|
+
if (i < 0) i = words.findIndex((w, j) => w === 'run' && words[j - 2] === 'node')
|
|
274
275
|
if (i < 0) continue
|
|
275
276
|
// `exec`, `dlx` and `x` take the manager's flags up to the program name.
|
|
276
277
|
let program = false
|
|
@@ -285,6 +286,25 @@ function pmRunsMembers(script: string): boolean {
|
|
|
285
286
|
return false
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
const CD = /(?:^|[\s;&|(])cd\s+("[^"]*"|'[^']*'|[^\s;&|()]+)/g
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* A `cd` into a member's directory, or one holding members, runs that
|
|
293
|
+
* member's work (kit's `cd packages/kit && vitest run`); bun's
|
|
294
|
+
* `typecheck` (`cd test && bun run typecheck`) enters no member and was
|
|
295
|
+
* left out (D-83). A target vx cannot read (`$DIR`, `~`) still counts.
|
|
296
|
+
*/
|
|
297
|
+
function cdsToMembers(script: string, rootDir: string, memberDirs: readonly string[]): boolean {
|
|
298
|
+
for (const m of script.matchAll(CD)) {
|
|
299
|
+
const target = m[1]!.replace(/^(["'])(.*)\1$/, '$2')
|
|
300
|
+
if (/[$`~*?]/.test(target) || target === '-') return true
|
|
301
|
+
const dir = path.resolve(rootDir, target)
|
|
302
|
+
const inside = (a: string, b: string): boolean => a === b || a.startsWith(b + path.sep)
|
|
303
|
+
if (memberDirs.some((d) => inside(d, dir) || inside(dir, d))) return true
|
|
304
|
+
}
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
|
|
288
308
|
/** A package manager running a script by name: `pnpm build`, `npm run x`, `bun run x`. */
|
|
289
309
|
const RUNS_SCRIPT =
|
|
290
310
|
/(?:^|[\s;&|(])(?:pnpm|npm|yarn|bun)\s+(?:run(?:-script)?\s+)?([^\s;&|()'"-][^\s;&|()'"]*)/g
|
|
@@ -294,12 +314,21 @@ const RUNS_SCRIPT =
|
|
|
294
314
|
* scripts: vite's `ci-docs` (`pnpm build && pnpm docs-build`) runs each
|
|
295
315
|
* member's build through the root's own `build` (`pnpm -r … run build`).
|
|
296
316
|
*/
|
|
297
|
-
function runningMembers(
|
|
317
|
+
function runningMembers(
|
|
318
|
+
scripts: Readonly<Record<string, unknown>>,
|
|
319
|
+
rootDir: string,
|
|
320
|
+
memberDirs: readonly string[],
|
|
321
|
+
): Set<string> {
|
|
298
322
|
const text = Object.entries(scripts).filter(
|
|
299
323
|
(e): e is [string, string] => typeof e[1] === 'string',
|
|
300
324
|
)
|
|
301
325
|
const out = new Set(
|
|
302
|
-
text
|
|
326
|
+
text
|
|
327
|
+
.filter(
|
|
328
|
+
([, v]) =>
|
|
329
|
+
RUNS_MEMBERS.test(` ${v}`) || pmRunsMembers(v) || cdsToMembers(v, rootDir, memberDirs),
|
|
330
|
+
)
|
|
331
|
+
.map(([n]) => n),
|
|
303
332
|
)
|
|
304
333
|
for (let grew = true; grew;) {
|
|
305
334
|
grew = false
|
|
@@ -358,7 +387,11 @@ export function migrateScripts(
|
|
|
358
387
|
)
|
|
359
388
|
const rootScripts = (meta: ProjectMeta): Record<string, unknown> => {
|
|
360
389
|
const scripts = scriptsOf(meta)
|
|
361
|
-
const runs = runningMembers(
|
|
390
|
+
const runs = runningMembers(
|
|
391
|
+
scripts,
|
|
392
|
+
meta.dir,
|
|
393
|
+
metas.filter((m) => m !== root).map((m) => m.dir),
|
|
394
|
+
)
|
|
362
395
|
return Object.fromEntries(
|
|
363
396
|
Object.entries(scripts).filter(
|
|
364
397
|
([n, v]) => typeof v === 'string' && !memberTasks.has(n) && !runs.has(n),
|