@vzn/vx-migrate 0.0.595 → 0.0.597
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 +45 -20
- package/package.json +2 -2
- package/src/adopt.ts +1 -1
- package/src/adoption-plugin.ts +2 -3
- package/src/migrate-nx.ts +3 -2
- package/src/migrate-turbo.ts +9 -81
- package/src/migrate-vite-task.ts +40 -0
- package/src/migrate.ts +69 -14
- package/src/nx/export-graph.ts +21 -3
- package/src/nx/index.ts +6 -3
- package/src/nx/lerna.ts +60 -0
- package/src/nx/nx-inputs.ts +17 -6
- package/src/nx/nx-map.ts +17 -6
- package/src/nx/nx-outputs.ts +62 -12
- package/src/nx/nx-upstream.ts +12 -2
- package/src/nx-cache/index.ts +17 -15
- package/src/paths.ts +4 -1
- package/src/remote-breaker.ts +60 -0
- package/src/remote-deadline.ts +13 -5
- package/src/shared-outputs.ts +3 -1
- package/src/spelled-env.ts +36 -0
- package/src/tracked-outputs.ts +3 -2
- package/src/turbo/turbo-map.ts +28 -9
- package/src/turbo-cache/index.ts +90 -27
- package/src/vite-task/vite-task-map.ts +558 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Vite Task (vite-plus's `vp run`) → vx: each package's `vite.config`
|
|
2
|
+
// `run` block and its package.json scripts, as the projects a migration
|
|
3
|
+
// plan writes. The schema is vite-plus's `RunConfig` (1.1): a task is a
|
|
4
|
+
// command string, an array of them, or `{ command, cwd, dependsOn, cache }`.
|
|
5
|
+
// vite-plus 0.x put `env`, `untrackedEnv`, `input` and `output` on the task
|
|
6
|
+
// itself beside `cache: true | false`; both shapes map.
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { type GeneratedProject, type GeneratedTask, type ProjectMeta, UserError } from '@vzn/vx'
|
|
11
|
+
import { minimatchToVx, withoutTakenBack } from '../glob-grammar.js'
|
|
12
|
+
import { packageScripts, relPosix } from '../paths.js'
|
|
13
|
+
import { scriptCommand, yarnPnp } from '../script-command.js'
|
|
14
|
+
import {
|
|
15
|
+
ownFileOutput,
|
|
16
|
+
ownFileTodo,
|
|
17
|
+
takingBack,
|
|
18
|
+
wildcardOutput,
|
|
19
|
+
wildcardTodo,
|
|
20
|
+
} from '../shared-outputs.js'
|
|
21
|
+
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
22
|
+
|
|
23
|
+
/** vite-plus's `VITE_CONFIG_FILES`, in its order: the first one present is the package's. */
|
|
24
|
+
const VITE_CONFIG_FILES = [
|
|
25
|
+
'vite.config.js',
|
|
26
|
+
'vite.config.mjs',
|
|
27
|
+
'vite.config.ts',
|
|
28
|
+
'vite.config.cjs',
|
|
29
|
+
'vite.config.mts',
|
|
30
|
+
'vite.config.cts',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies'] as const
|
|
34
|
+
type DepField = (typeof DEP_FIELDS)[number]
|
|
35
|
+
|
|
36
|
+
/** A command segment that runs Vite Task: `vp run`, `vpr`, through a package runner too. */
|
|
37
|
+
const VP_RUN = /^(?:(?:pnpm(?: exec)?|npx|bunx|yarn)\s+)?(?:vp\s+run|vpr)(?:\s|$)/
|
|
38
|
+
|
|
39
|
+
/** npm's own verbs' hooks: the package manager runs them, never `vp run`'s task list. */
|
|
40
|
+
const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepublishOnly|install)$/
|
|
41
|
+
|
|
42
|
+
type Glob = string | { pattern?: unknown; base?: unknown; auto?: unknown }
|
|
43
|
+
|
|
44
|
+
interface TaskDef extends CacheDef {
|
|
45
|
+
command?: unknown
|
|
46
|
+
cwd?: unknown
|
|
47
|
+
dependsOn?: unknown
|
|
48
|
+
cache?: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface CacheDef {
|
|
52
|
+
env?: unknown
|
|
53
|
+
untrackedEnv?: unknown
|
|
54
|
+
input?: unknown
|
|
55
|
+
output?: unknown
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RunConfig {
|
|
59
|
+
tasks?: Record<string, unknown>
|
|
60
|
+
cache?: unknown
|
|
61
|
+
enablePrePostScripts?: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The `vite.config` file in `dir`, or null. */
|
|
65
|
+
function viteConfigFile(dir: string): string | null {
|
|
66
|
+
const name = VITE_CONFIG_FILES.find((f) => existsSync(path.join(dir, f)))
|
|
67
|
+
return name === undefined ? null : path.join(dir, name)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A package's `run` block, as `vp run` reads it: the config evaluated in
|
|
72
|
+
* build mode (vite-plus resolves it with `command: 'build'`), a function
|
|
73
|
+
* config called. Bun loads the TypeScript itself.
|
|
74
|
+
*/
|
|
75
|
+
async function readRunConfig(file: string, root: string): Promise<RunConfig | null> {
|
|
76
|
+
let config: unknown
|
|
77
|
+
try {
|
|
78
|
+
config = ((await import(file)) as { default?: unknown }).default
|
|
79
|
+
if (typeof config === 'function')
|
|
80
|
+
config = await (config as (env: object) => unknown)({
|
|
81
|
+
command: 'build',
|
|
82
|
+
mode: 'production',
|
|
83
|
+
isSsrBuild: false,
|
|
84
|
+
isPreview: false,
|
|
85
|
+
})
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new UserError(
|
|
88
|
+
`${relPosix(root, file)} did not load (${err instanceof Error ? err.message : String(err)}) — install the workspace's dependencies, then re-run vx-migrate`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
const run = (config as { run?: unknown } | null | undefined)?.run
|
|
92
|
+
return typeof run === 'object' && run !== null ? run : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `run.cache` at the root: what `vp run` caches with no flag. */
|
|
96
|
+
function globalCache(run: RunConfig | null): { tasks: boolean; scripts: boolean } {
|
|
97
|
+
const c = run?.cache
|
|
98
|
+
if (typeof c === 'boolean') return { tasks: c, scripts: c }
|
|
99
|
+
const o = (typeof c === 'object' && c !== null ? c : {}) as { tasks?: unknown; scripts?: unknown }
|
|
100
|
+
return { tasks: o.tasks !== false, scripts: o.scripts === true }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ViteTaskOptions {
|
|
104
|
+
/** Each project's tracked files, by root-relative dir: what a wildcard output may not claim. */
|
|
105
|
+
tracked?: (rel: string) => TrackedKinds
|
|
106
|
+
/** The config file each written task lives beside, by project dir. */
|
|
107
|
+
ownConfig?: (dir: string) => string | null
|
|
108
|
+
/** The env names the tracked files under `dirs` spell: a `*` env entry's stand-in. */
|
|
109
|
+
sourceNames?: (dirs: readonly string[]) => Promise<readonly string[]>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ViteTaskMapping {
|
|
113
|
+
projects: GeneratedProject[]
|
|
114
|
+
notes: string[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A shell word for `cd`: bare when it is safe, single-quoted otherwise. */
|
|
118
|
+
function shellWord(s: string): string {
|
|
119
|
+
return /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replaceAll("'", `'\\''`)}'`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const asStrings = (v: unknown): string[] =>
|
|
123
|
+
Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `metas` plus the workspace root when it has tasks or scripts and no
|
|
127
|
+
* member is the root: `vp run -w` runs them, so it is a project here.
|
|
128
|
+
*/
|
|
129
|
+
export async function viteTaskProjects(
|
|
130
|
+
root: string,
|
|
131
|
+
metas: readonly ProjectMeta[],
|
|
132
|
+
): Promise<{ metas: ProjectMeta[]; notes: string[] }> {
|
|
133
|
+
if (metas.some((m) => path.resolve(m.dir) === path.resolve(root)))
|
|
134
|
+
return { metas: [...metas], notes: [] }
|
|
135
|
+
const file = viteConfigFile(root)
|
|
136
|
+
const run = file === null ? null : await readRunConfig(file, root)
|
|
137
|
+
const pkg = (await Bun.file(path.join(root, 'package.json'))
|
|
138
|
+
.json()
|
|
139
|
+
.catch(() => ({}))) as { name?: unknown; scripts?: unknown }
|
|
140
|
+
const scripts = typeof pkg.scripts === 'object' && pkg.scripts !== null ? pkg.scripts : {}
|
|
141
|
+
const hasTasks = Object.keys(run?.tasks ?? {}).length > 0 || Object.keys(scripts).length > 0
|
|
142
|
+
if (!hasTasks) return { metas: [...metas], notes: [] }
|
|
143
|
+
if (typeof pkg.name !== 'string' || pkg.name === '' || metas.some((m) => m.name === pkg.name))
|
|
144
|
+
return {
|
|
145
|
+
metas: [...metas],
|
|
146
|
+
notes: [
|
|
147
|
+
'the workspace root has tasks or scripts `vp run -w` runs, and no package.json "name" of its own (or one a member takes); vx names a project by it — give it one and re-run vx-migrate',
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
metas: [...metas, { name: pkg.name, dir: root, packageJson: pkg as never, configPath: null }],
|
|
152
|
+
notes: [],
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Every project's Vite Task tasks and package.json scripts as vx tasks. */
|
|
157
|
+
export async function mapViteTaskWorkspace(
|
|
158
|
+
root: string,
|
|
159
|
+
metas: readonly ProjectMeta[],
|
|
160
|
+
opts: ViteTaskOptions = {},
|
|
161
|
+
): Promise<ViteTaskMapping> {
|
|
162
|
+
const rootFile = viteConfigFile(root)
|
|
163
|
+
const rootRun = rootFile === null ? null : await readRunConfig(rootFile, root)
|
|
164
|
+
const cacheOn = globalCache(rootRun)
|
|
165
|
+
const hooks = rootRun?.enablePrePostScripts !== false
|
|
166
|
+
const pnp = yarnPnp(root)
|
|
167
|
+
|
|
168
|
+
const runs = new Map<string, RunConfig | null>()
|
|
169
|
+
for (const m of metas) {
|
|
170
|
+
const file = viteConfigFile(m.dir)
|
|
171
|
+
runs.set(
|
|
172
|
+
m.name,
|
|
173
|
+
path.resolve(m.dir) === path.resolve(root)
|
|
174
|
+
? rootRun
|
|
175
|
+
: file === null
|
|
176
|
+
? null
|
|
177
|
+
: await readRunConfig(file, root),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
const tasksOf = (m: ProjectMeta): Record<string, unknown> => runs.get(m.name)?.tasks ?? {}
|
|
181
|
+
const defines = (m: ProjectMeta, task: string): boolean =>
|
|
182
|
+
Object.hasOwn(tasksOf(m), task) || typeof packageScripts(m)[task] === 'string'
|
|
183
|
+
const byName = new Map(metas.map((m) => [m.name, m]))
|
|
184
|
+
/** Workspace members `m` lists under each field, by name. */
|
|
185
|
+
const depsIn = (m: ProjectMeta, field: string): string[] => {
|
|
186
|
+
const deps = (m.packageJson as unknown as Record<string, unknown>)[field]
|
|
187
|
+
if (typeof deps !== 'object' || deps === null) return []
|
|
188
|
+
return Object.keys(deps).filter((n) => byName.has(n) && n !== m.name)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rootMeta = metas.find((m) => path.resolve(m.dir) === path.resolve(root))
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A `vp run` inside a command: Vite Task inlines it into its graph. A
|
|
195
|
+
* leading chain of them becomes `dependsOn` edges, and the task a group
|
|
196
|
+
* when nothing else is left; a form with no edge spelling is a TODO.
|
|
197
|
+
*/
|
|
198
|
+
function inlineRuns(t: GeneratedTask, self: ProjectMeta): void {
|
|
199
|
+
const exec = t.task?.['exec'] as { command?: unknown } | undefined
|
|
200
|
+
if (typeof exec?.command !== 'string') return
|
|
201
|
+
const parts = exec.command.split('&&').map((p) => p.trim())
|
|
202
|
+
if (!parts.some((p) => VP_RUN.test(p))) return
|
|
203
|
+
const edges: string[] = []
|
|
204
|
+
let i = 0
|
|
205
|
+
for (; i < parts.length && VP_RUN.test(parts[i]!); i++) {
|
|
206
|
+
const e = /[|;()`$<>'"\\\n]/.test(parts[i]!) ? null : runEdges(parts[i]!, self, t.name)
|
|
207
|
+
if (e === null) break
|
|
208
|
+
edges.push(...e)
|
|
209
|
+
}
|
|
210
|
+
if (parts.slice(i).some((p) => VP_RUN.test(p))) {
|
|
211
|
+
const seg = parts.find((p, j) => j >= i && VP_RUN.test(p))!
|
|
212
|
+
t.todos.push(
|
|
213
|
+
`\`${seg}\` runs Vite Task inside the command, which \`vp run\` inlined into its graph: name the tasks it runs under dependsOn and drop it from the command`,
|
|
214
|
+
)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const deps = (t.task!['dependsOn'] as string[] | undefined) ?? []
|
|
218
|
+
t.task!['dependsOn'] = [...new Set([...deps, ...edges])]
|
|
219
|
+
const rest = parts.slice(i).join(' && ')
|
|
220
|
+
if (rest === '') delete t.task!['exec']
|
|
221
|
+
else exec.command = rest
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The edges one `vp run` stands for, or null when it has none. */
|
|
225
|
+
function runEdges(segment: string, self: ProjectMeta, own: string): string[] | null {
|
|
226
|
+
const words = segment.split(/\s+/)
|
|
227
|
+
words.splice(0, words.indexOf(words[0] === 'vpr' ? 'vpr' : 'run') + 1)
|
|
228
|
+
let recursive = false
|
|
229
|
+
let workspace = false
|
|
230
|
+
const filters: string[] = []
|
|
231
|
+
const specs: string[] = []
|
|
232
|
+
for (let k = 0; k < words.length; k++) {
|
|
233
|
+
const w = words[k]!
|
|
234
|
+
if (w === '-r' || w === '--recursive') recursive = true
|
|
235
|
+
else if (w === '-w' || w === '--workspace-root') workspace = true
|
|
236
|
+
else if (w === '-F' || w === '--filter') filters.push(words[++k] ?? '')
|
|
237
|
+
else if (w.startsWith('--filter=')) filters.push(w.slice('--filter='.length))
|
|
238
|
+
else if (w === '--log' || w === '--concurrency-limit') k++
|
|
239
|
+
else if (['-v', '--verbose', '--cache', '--no-cache', '--fail-if-no-match'].includes(w))
|
|
240
|
+
continue
|
|
241
|
+
else if (w.startsWith('-')) return null
|
|
242
|
+
else specs.push(w)
|
|
243
|
+
}
|
|
244
|
+
// One task and no forwarded arguments: an edge passes none.
|
|
245
|
+
if (specs.length !== 1) return null
|
|
246
|
+
const spec = specs[0]!
|
|
247
|
+
const hash = spec.indexOf('#')
|
|
248
|
+
if (hash !== -1) {
|
|
249
|
+
const m = byName.get(spec.slice(0, hash))
|
|
250
|
+
return recursive || workspace || filters.length > 0 || m === undefined ? null : [spec]
|
|
251
|
+
}
|
|
252
|
+
let picked: ProjectMeta[]
|
|
253
|
+
if (filters.length > 0) {
|
|
254
|
+
// Package names only: pnpm's directory, glob and graph selectors are not.
|
|
255
|
+
if (filters.some((f) => !byName.has(f))) return null
|
|
256
|
+
picked = filters.map((f) => byName.get(f)!)
|
|
257
|
+
} else if (recursive) picked = [...metas]
|
|
258
|
+
else if (workspace) {
|
|
259
|
+
if (rootMeta === undefined) return null
|
|
260
|
+
picked = [rootMeta]
|
|
261
|
+
} else return defines(self, spec) && spec !== own ? [spec] : null
|
|
262
|
+
// Vite Task prunes the task's own reference (a root `build: vp run -r build`).
|
|
263
|
+
return picked
|
|
264
|
+
.filter((m) => defines(m, spec) && !(m === self && spec === own))
|
|
265
|
+
.map((m) => `${m.name}#${spec}`)
|
|
266
|
+
.sort()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const projects: GeneratedProject[] = []
|
|
270
|
+
const wildUsed = new Map<string, number>()
|
|
271
|
+
for (const meta of metas) {
|
|
272
|
+
const pkgDir = relPosix(root, meta.dir)
|
|
273
|
+
const atRoot = pkgDir === '' || pkgDir === '.'
|
|
274
|
+
const tasks: GeneratedTask[] = []
|
|
275
|
+
const own = tasksOf(meta)
|
|
276
|
+
// A `*` env entry matches names in the run's environment; a written
|
|
277
|
+
// config lists the ones this package and its workspace dependencies spell.
|
|
278
|
+
const wild = Object.values(own).some((raw) => {
|
|
279
|
+
const def = (typeof raw === 'object' && raw !== null ? raw : {}) as TaskDef
|
|
280
|
+
const c = (typeof def.cache === 'object' && def.cache !== null ? def.cache : def) as CacheDef
|
|
281
|
+
return [...asStrings(c.env), ...asStrings(c.untrackedEnv)].some(
|
|
282
|
+
(e) => !e.startsWith('!') && e.includes('*'),
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
let spelled: readonly string[] | undefined
|
|
286
|
+
if (wild && opts.sourceNames !== undefined) {
|
|
287
|
+
const closure = new Set<ProjectMeta>([meta])
|
|
288
|
+
for (const p of closure)
|
|
289
|
+
for (const field of [...DEP_FIELDS, 'optionalDependencies'])
|
|
290
|
+
for (const n of depsIn(p, field)) closure.add(byName.get(n)!)
|
|
291
|
+
spelled = await opts.sourceNames([...closure].map((p) => p.dir))
|
|
292
|
+
}
|
|
293
|
+
for (const [name, raw] of Object.entries(own)) {
|
|
294
|
+
const def: TaskDef =
|
|
295
|
+
typeof raw === 'string' || Array.isArray(raw) ? { command: raw } : (raw ?? {})
|
|
296
|
+
tasks.push(
|
|
297
|
+
mapTask(name, def, {
|
|
298
|
+
pkgDir,
|
|
299
|
+
atRoot,
|
|
300
|
+
cacheOn: cacheOn.tasks,
|
|
301
|
+
dependsOn: (from) => fromDeps(meta, from),
|
|
302
|
+
opts,
|
|
303
|
+
dir: meta.dir,
|
|
304
|
+
spelled,
|
|
305
|
+
wildUsed,
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* `{ task, from }`: the task in each direct member `from` lists that
|
|
312
|
+
* defines it. `^task` is that set while no member listed under another
|
|
313
|
+
* field defines it too (vx's `^` follows every field); else the edges.
|
|
314
|
+
*/
|
|
315
|
+
function fromDeps(m: ProjectMeta, entry: { task: string; from: unknown }): string[] {
|
|
316
|
+
const fields = (Array.isArray(entry.from) ? entry.from : [entry.from]).filter(
|
|
317
|
+
(f): f is DepField => (DEP_FIELDS as readonly unknown[]).includes(f),
|
|
318
|
+
)
|
|
319
|
+
const chosen = new Set(fields.flatMap((f) => depsIn(m, f)))
|
|
320
|
+
const others = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
321
|
+
.filter((f) => !(fields as string[]).includes(f))
|
|
322
|
+
.flatMap((f) => depsIn(m, f))
|
|
323
|
+
.filter((n) => !chosen.has(n) && defines(byName.get(n)!, entry.task))
|
|
324
|
+
if (others.length === 0) return [`^${entry.task}`]
|
|
325
|
+
return [...chosen]
|
|
326
|
+
.filter((n) => defines(byName.get(n)!, entry.task))
|
|
327
|
+
.sort()
|
|
328
|
+
.map((n) => `${n}#${entry.task}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const scripts = packageScripts(meta)
|
|
332
|
+
const has = (n: string): boolean => typeof scripts[n] === 'string' && scripts[n] !== ''
|
|
333
|
+
for (const name of Object.keys(scripts)) {
|
|
334
|
+
if (!has(name) || Object.hasOwn(own, name) || LIFECYCLE.test(name)) continue
|
|
335
|
+
// With hooks on, `vp run x` runs `prex` and `postx` around it: they
|
|
336
|
+
// ride inside x's command, as no task of their own.
|
|
337
|
+
const hookOf = hooks ? /^(pre|post)(.+)$/.exec(name) : null
|
|
338
|
+
if (hookOf !== null && has(hookOf[2]!) && !LIFECYCLE.test(hookOf[2]!)) continue
|
|
339
|
+
const command = scriptCommand(name, scripts[name] as string, hooks ? scripts : {}, pnp)
|
|
340
|
+
tasks.push({
|
|
341
|
+
name,
|
|
342
|
+
// Scripts are uncached unless `run.cache.scripts`, and then traced.
|
|
343
|
+
todos: cacheOn.scripts ? [autoTodo('inputs and outputs')] : [],
|
|
344
|
+
task: { exec: { command } },
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
for (const t of tasks) inlineRuns(t, meta)
|
|
348
|
+
if (tasks.length > 0) projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
|
|
349
|
+
}
|
|
350
|
+
const notes = [...wildUsed].map(
|
|
351
|
+
([e, n]) =>
|
|
352
|
+
`env ${JSON.stringify(e)}: the configs list the names the files of each task's package and its ` +
|
|
353
|
+
`workspace dependencies spell (${n} task${n === 1 ? '' : 's'}) — add any other a task reads to ` +
|
|
354
|
+
'cache.inputs.env and exec.env.passThrough',
|
|
355
|
+
)
|
|
356
|
+
return { projects, notes }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The TODO for what Vite Task traces and vx declares. */
|
|
360
|
+
function autoTodo(what: string, known?: Record<string, unknown>): string {
|
|
361
|
+
const have = known === undefined ? '' : ` (declared so far: \`${JSON.stringify(known)}\`)`
|
|
362
|
+
return (
|
|
363
|
+
`cache: Vite Task traced this task's ${what} and vx infers none — add ` +
|
|
364
|
+
`\`cache: { inputs: { files: [...] }, outputs: { files: [...] } }\` with the real ones${have}; ` +
|
|
365
|
+
'without it the task always runs'
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
interface TaskCtx {
|
|
370
|
+
pkgDir: string
|
|
371
|
+
atRoot: boolean
|
|
372
|
+
cacheOn: boolean
|
|
373
|
+
dir: string
|
|
374
|
+
dependsOn: (entry: { task: string; from: unknown }) => string[]
|
|
375
|
+
opts: ViteTaskOptions
|
|
376
|
+
spelled: readonly string[] | undefined
|
|
377
|
+
/** Each `*` entry the spelled names stood in for, with its task count. */
|
|
378
|
+
wildUsed: Map<string, number>
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mapTask(name: string, def: TaskDef, ctx: TaskCtx): GeneratedTask {
|
|
382
|
+
const todos: string[] = []
|
|
383
|
+
const task: Record<string, unknown> = {}
|
|
384
|
+
|
|
385
|
+
const parts = typeof def.command === 'string' ? [def.command] : asStrings(def.command)
|
|
386
|
+
const body = parts.filter((p) => p.trim() !== '').join(' && ')
|
|
387
|
+
const cwd = typeof def.cwd === 'string' && def.cwd !== '' && def.cwd !== '.' ? def.cwd : null
|
|
388
|
+
|
|
389
|
+
const deps: string[] = []
|
|
390
|
+
for (const d of Array.isArray(def.dependsOn) ? def.dependsOn : []) {
|
|
391
|
+
if (typeof d === 'string') deps.push(d)
|
|
392
|
+
else if (
|
|
393
|
+
typeof d === 'object' &&
|
|
394
|
+
d !== null &&
|
|
395
|
+
typeof (d as { task?: unknown }).task === 'string'
|
|
396
|
+
)
|
|
397
|
+
deps.push(...ctx.dependsOn(d as { task: string; from: unknown }))
|
|
398
|
+
}
|
|
399
|
+
if (deps.length > 0 || body === '') task['dependsOn'] = [...new Set(deps)]
|
|
400
|
+
// `command: []` runs nothing: a group of its dependsOn.
|
|
401
|
+
if (body === '') return { name, todos, task }
|
|
402
|
+
|
|
403
|
+
const command = cwd === null ? body : `cd ${shellWord(cwd)} && ${body}`
|
|
404
|
+
const exec: Record<string, unknown> = { command }
|
|
405
|
+
task['exec'] = exec
|
|
406
|
+
|
|
407
|
+
const cache: CacheDef | null =
|
|
408
|
+
def.cache === false || !ctx.cacheOn
|
|
409
|
+
? null
|
|
410
|
+
: typeof def.cache === 'object' && def.cache !== null
|
|
411
|
+
? def.cache
|
|
412
|
+
: def
|
|
413
|
+
|
|
414
|
+
const env = envNames('cache.env', cache?.env, todos, ctx)
|
|
415
|
+
const pass = [
|
|
416
|
+
...new Set([...env, ...envNames('cache.untrackedEnv', cache?.untrackedEnv, todos, ctx)]),
|
|
417
|
+
]
|
|
418
|
+
if (pass.length > 0) exec['env'] = { passThrough: pass }
|
|
419
|
+
if (cache === null) return { name, todos, task }
|
|
420
|
+
|
|
421
|
+
const inputs = globs('input', cache.input, ctx, todos)
|
|
422
|
+
const outputs = globs('output', cache.output, ctx, todos)
|
|
423
|
+
if (inputs.auto || outputs.auto) {
|
|
424
|
+
const known: Record<string, unknown> = {}
|
|
425
|
+
if (!inputs.auto) known['inputs'] = inputs.lists
|
|
426
|
+
if (!outputs.auto) known['outputs'] = outputs.lists
|
|
427
|
+
const what =
|
|
428
|
+
inputs.auto && outputs.auto ? 'inputs and outputs' : inputs.auto ? 'inputs' : 'outputs'
|
|
429
|
+
todos.push(autoTodo(what, Object.keys(known).length > 0 ? known : undefined))
|
|
430
|
+
return { name, todos, task }
|
|
431
|
+
}
|
|
432
|
+
if (outputs.uncached) return { name, todos, task }
|
|
433
|
+
|
|
434
|
+
const outFiles = outputs.lists['files'] as string[]
|
|
435
|
+
const wild = wildcardOutput(outFiles, ctx.opts.tracked?.(ctx.pkgDir))
|
|
436
|
+
if (wild !== undefined) {
|
|
437
|
+
todos.push(wildcardTodo(wild))
|
|
438
|
+
return { name, todos, task }
|
|
439
|
+
}
|
|
440
|
+
const ownFile = ownFileOutput(outFiles, ctx.opts.ownConfig?.(ctx.dir))
|
|
441
|
+
if (ownFile !== undefined) {
|
|
442
|
+
todos.push(ownFileTodo(ownFile))
|
|
443
|
+
return { name, todos, task }
|
|
444
|
+
}
|
|
445
|
+
const inputLists: Record<string, unknown> = { ...inputs.lists }
|
|
446
|
+
if (env.length > 0) inputLists['env'] = env
|
|
447
|
+
task['cache'] = { inputs: inputLists, outputs: outputs.lists }
|
|
448
|
+
return { name, todos, task }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* `env` / `untrackedEnv` as the exact names vx takes. `*` matches any run
|
|
453
|
+
* of characters and `!` takes names back, as Vite Task's patterns do; a `*`
|
|
454
|
+
* entry lists the names `spelled` holds that it matches (the bare prefix is
|
|
455
|
+
* no name), or is a TODO when none does.
|
|
456
|
+
*/
|
|
457
|
+
function envNames(
|
|
458
|
+
field: string,
|
|
459
|
+
v: unknown,
|
|
460
|
+
todos: string[],
|
|
461
|
+
{ spelled, wildUsed }: Pick<TaskCtx, 'spelled' | 'wildUsed'>,
|
|
462
|
+
): string[] {
|
|
463
|
+
const re = (p: string): RegExp => new RegExp(`^${p.split('*').map(RegExp.escape).join('.*')}$`)
|
|
464
|
+
const names: string[] = []
|
|
465
|
+
const dropped: RegExp[] = []
|
|
466
|
+
for (const e of asStrings(v)) {
|
|
467
|
+
if (e.startsWith('!')) {
|
|
468
|
+
dropped.push(re(e.slice(1)))
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
const hits =
|
|
472
|
+
e.includes('*') && spelled !== undefined
|
|
473
|
+
? spelled.filter((n) => n !== e.replaceAll('*', '') && re(e).test(n))
|
|
474
|
+
: []
|
|
475
|
+
if (hits.length > 0) {
|
|
476
|
+
names.push(...hits)
|
|
477
|
+
wildUsed.set(e, (wildUsed.get(e) ?? 0) + 1)
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
if (/[*?[\]{}=\0]/.test(e) || e === '') {
|
|
481
|
+
todos.push(
|
|
482
|
+
`${field} ${JSON.stringify(e)}: vx takes exact env names — list the ones the task reads in exec.env.passThrough${field === 'cache.env' ? ' and cache.inputs.env' : ''}`,
|
|
483
|
+
)
|
|
484
|
+
continue
|
|
485
|
+
}
|
|
486
|
+
names.push(e)
|
|
487
|
+
}
|
|
488
|
+
return [...new Set(names)].filter((n) => !dropped.some((d) => d.test(n)))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* `cache.input` / `cache.output`: package-relative globs (a string, or
|
|
493
|
+
* `{ pattern, base: 'package' }`) are `files`, `base: 'workspace'` ones
|
|
494
|
+
* `workspaceFiles`; a root task's are the workspace's. Omitted or holding
|
|
495
|
+
* `{ auto: true }`, the list is traced, which vx does not do.
|
|
496
|
+
*/
|
|
497
|
+
function globs(
|
|
498
|
+
kind: 'input' | 'output',
|
|
499
|
+
v: unknown,
|
|
500
|
+
ctx: TaskCtx,
|
|
501
|
+
todos: string[],
|
|
502
|
+
): { auto: boolean; uncached: boolean; lists: Record<string, string[]> } {
|
|
503
|
+
if (!Array.isArray(v)) return { auto: true, uncached: false, lists: {} }
|
|
504
|
+
let auto = false
|
|
505
|
+
let uncached = false
|
|
506
|
+
const files: string[] = []
|
|
507
|
+
const ws: string[] = []
|
|
508
|
+
for (const e of v as Glob[]) {
|
|
509
|
+
let pattern: string
|
|
510
|
+
let workspace = ctx.atRoot
|
|
511
|
+
if (typeof e === 'string') pattern = e
|
|
512
|
+
else if (typeof e === 'object' && e !== null && 'auto' in e) {
|
|
513
|
+
if (e.auto === true) auto = true
|
|
514
|
+
continue
|
|
515
|
+
} else if (typeof e === 'object' && e !== null && typeof e.pattern === 'string') {
|
|
516
|
+
pattern = e.pattern
|
|
517
|
+
if (e.base === 'workspace') workspace = true
|
|
518
|
+
} else continue
|
|
519
|
+
const neg = pattern.startsWith('!')
|
|
520
|
+
const raw = neg ? pattern.slice(1) : pattern
|
|
521
|
+
const wax = minimatchToVx(raw, neg)
|
|
522
|
+
if (wax === null) {
|
|
523
|
+
todos.push(
|
|
524
|
+
`${kind} ${JSON.stringify(pattern)}: glob syntax vx cannot take — ` +
|
|
525
|
+
(kind === 'input'
|
|
526
|
+
? 'widened to every package file'
|
|
527
|
+
: 'task runs uncached; declare the exact outputs in a vx.config to cache it'),
|
|
528
|
+
)
|
|
529
|
+
if (kind === 'output') uncached = true
|
|
530
|
+
else if (!neg) files.push('**/*')
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
let body = wax.replace(/^(\.\/)+/, '')
|
|
534
|
+
if (!workspace && body.startsWith('../')) {
|
|
535
|
+
const anchored = path.posix.normalize(path.posix.join(ctx.pkgDir, body))
|
|
536
|
+
if (anchored.startsWith('../')) {
|
|
537
|
+
todos.push(`${kind} ${JSON.stringify(pattern)}: leaves the workspace — map manually`)
|
|
538
|
+
if (kind === 'output') uncached = true
|
|
539
|
+
continue
|
|
540
|
+
}
|
|
541
|
+
body = anchored
|
|
542
|
+
workspace = true
|
|
543
|
+
}
|
|
544
|
+
;(workspace ? ws : files).push((neg ? '!' : '') + body)
|
|
545
|
+
}
|
|
546
|
+
const lists: Record<string, string[]> = {}
|
|
547
|
+
if (kind === 'input') {
|
|
548
|
+
// Exclusions alone narrow every file; core refuses a list of them alone.
|
|
549
|
+
if (files.length > 0 && files.every((f) => f.startsWith('!'))) files.unshift('**/*')
|
|
550
|
+
lists['files'] = withoutTakenBack(files)
|
|
551
|
+
if (ws.length > 0) lists['workspaceFiles'] = withoutTakenBack(ws)
|
|
552
|
+
} else {
|
|
553
|
+
lists['files'] = takingBack(files)
|
|
554
|
+
const w = takingBack(ws)
|
|
555
|
+
if (w.length > 0) lists['workspaceFiles'] = w
|
|
556
|
+
}
|
|
557
|
+
return { auto, uncached, lists }
|
|
558
|
+
}
|