@vzn/vx 0.0.37 → 0.0.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "An open, extensible monorepo task runner.",
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.37",
21
- "@vzn/vx-linux-arm64": "0.0.37",
22
- "@vzn/vx-darwin-x64": "0.0.37",
23
- "@vzn/vx-darwin-arm64": "0.0.37"
20
+ "@vzn/vx-linux-x64": "0.0.38",
21
+ "@vzn/vx-linux-arm64": "0.0.38",
22
+ "@vzn/vx-darwin-x64": "0.0.38",
23
+ "@vzn/vx-darwin-arm64": "0.0.38"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -822,18 +822,7 @@ function isEnvName(name: unknown): name is string {
822
822
  * trimmed away by the reader (item 1000).
823
823
  */
824
824
  function assertTaskName(name: string, configPath: string): void {
825
- const why =
826
- name.trim().length === 0
827
- ? 'is empty'
828
- : name.trim() !== name
829
- ? 'has surrounding whitespace'
830
- : name.includes('#')
831
- ? "holds '#', which separates a project from its task"
832
- : name.includes('*')
833
- ? "holds '*', which makes it a pattern"
834
- : name.startsWith('^') || name.startsWith('!')
835
- ? `starts with '${name[0]}', which names dependencies' tasks or negates`
836
- : null
825
+ const why = taskNameProblem(name)
837
826
  if (why === null) return
838
827
  throw new UserError(
839
828
  `${configPath}: task name ${JSON.stringify(name)} ${why} — dependsOn, cache.inputs.tasks and ` +
@@ -841,6 +830,21 @@ function assertTaskName(name: string, configPath: string): void {
841
830
  )
842
831
  }
843
832
 
833
+ /** Why `name` cannot be a task name, or null; `vx init` skips such a script (item 1033). */
834
+ export function taskNameProblem(name: string): string | null {
835
+ return name.trim().length === 0
836
+ ? 'is empty'
837
+ : name.trim() !== name
838
+ ? 'has surrounding whitespace'
839
+ : name.includes('#')
840
+ ? "holds '#', which separates a project from its task"
841
+ : name.includes('*')
842
+ ? "holds '*', which makes it a pattern"
843
+ : name.startsWith('^') || name.startsWith('!')
844
+ ? `starts with '${name[0]}', which names dependencies' tasks or negates`
845
+ : null
846
+ }
847
+
844
848
  /** The graph's `*`-only task glob (`compileTaskPattern`), mirrored: `*` is the sole metacharacter. */
845
849
  function taskPatternRegExp(pattern: string): RegExp {
846
850
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
@@ -24,6 +24,7 @@
24
24
  // `<pm> run <other>` becomes a GROUP over `<other>`, so the graph sees the
25
25
  // dependency instead of a package-manager subprocess it cannot cache.
26
26
 
27
+ import { taskNameProblem } from './config-schema.js'
27
28
  import type { ProjectMeta } from './workspace.js'
28
29
  import {
29
30
  foldScriptHooks,
@@ -116,10 +117,19 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
116
117
  const projects: GeneratedProject[] = []
117
118
  for (const meta of metas) {
118
119
  const scripts = scriptsOf(meta)
119
- const names = Object.keys(scripts).filter(
120
+ const runnable = Object.keys(scripts).filter(
120
121
  (n) => typeof scripts[n] === 'string' && scripts[n] !== '',
121
122
  )
122
- if (names.length === 0) continue
123
+ // A script whose name no task may carry (`lint#fix`, `^up`) was written
124
+ // as one, and the next run refused the whole config (item 1033). It is
125
+ // left out, with a TODO, and a script delegating to it keeps its
126
+ // command rather than naming it as an edge.
127
+ const refused = runnable.flatMap((n) => {
128
+ const why = taskNameProblem(n)
129
+ return why === null ? [] : [[n, why] as const]
130
+ })
131
+ const names = runnable.filter((n) => taskNameProblem(n) === null)
132
+ if (runnable.length === 0) continue
123
133
  const hasBuild = names.includes('build')
124
134
  const has = (n: string): boolean => names.includes(n)
125
135
  const tasks: GeneratedTask[] = []
@@ -173,6 +183,15 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
173
183
  }
174
184
  tasks.push({ name, todos, task })
175
185
  }
186
+ for (const [name, why] of refused) {
187
+ tasks.push({
188
+ name,
189
+ todos: [
190
+ `script ${JSON.stringify(name)} not migrated: its name ${why} — rename the script, or add it by hand under another name`,
191
+ ],
192
+ task: null,
193
+ })
194
+ }
176
195
  upstreamBuildOnWorker(tasks)
177
196
  if (tasks.length > 0) projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
178
197
  }
@@ -5,10 +5,12 @@
5
5
  // tool wrote it. Core knows no source format here: a mapper returns a
6
6
  // `MigrationPlan` and this file does the rest.
7
7
 
8
+ import { unlink } from 'node:fs/promises'
8
9
  import path from 'node:path'
9
10
  import { relPosix, UserError } from '../util/index.js'
10
11
  import type { ProjectMeta } from './workspace.js'
11
12
  import { loadWorkspace, unreachedHint, unreachedPackages } from './workspace.js'
13
+ import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
12
14
 
13
15
  /**
14
16
  * Escape an arbitrary string into a single-quoted TS literal. Escapes
@@ -164,13 +166,10 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
164
166
  }
165
167
  // A migrated workspace must declare its executor and cache — nothing is
166
168
  // applied by default. Emit the workspace file unless the repo already has
167
- // one in any supported extension.
169
+ // one in any extension the loader reads: a hand-written `.mts` was missed,
170
+ // and the `.ts` written beside it won by load order (item 1033).
168
171
  const hasWorkspaceFile = (
169
- await Promise.all(
170
- ['vx.workspace.ts', 'vx.workspace.mjs', 'vx.workspace.js'].map((n) =>
171
- Bun.file(path.join(root, n)).exists(),
172
- ),
173
- )
172
+ await Promise.all(WORKSPACE_CONFIG_FILENAMES.map((n) => Bun.file(path.join(root, n)).exists()))
174
173
  ).some(Boolean)
175
174
  if (!hasWorkspaceFile) {
176
175
  const abs = path.join(root, workspaceName)
@@ -201,12 +200,23 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
201
200
  }
202
201
  }
203
202
 
203
+ // Under --force a project's config of another extension is REPLACED: the
204
+ // new one was written beside it, the loader read one of the two by its
205
+ // order, and the report said "written" for a file the run never loaded
206
+ // (item 1033).
207
+ const replaced: string[] = []
208
+ for (const p of plan.projects) {
209
+ if (p.tasks.length === 0) continue
210
+ const existing = metas.find((m) => m.dir === p.dir)?.configPath
211
+ if (existing && existing !== path.join(p.dir, configName)) replaced.push(existing)
212
+ }
204
213
  if (dry) {
205
214
  for (const f of files) {
206
215
  process.stdout.write(`── ${f.relPath} ──\n${f.contents}\n`)
207
216
  }
208
217
  } else {
209
218
  for (const f of files) await Bun.write(f.abs, f.contents)
219
+ for (const f of replaced) await unlink(f)
210
220
  }
211
221
 
212
222
  const todoList: string[] = []
@@ -258,6 +268,10 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
258
268
  report.push(...plan.notes)
259
269
  report.push(dry ? 'files (dry run, nothing written):' : 'files written:')
260
270
  for (const f of files) report.push(` ${f.relPath}`)
271
+ if (replaced.length > 0) {
272
+ report.push(dry ? 'would replace (dry run):' : 'replaced:')
273
+ for (const f of replaced) report.push(` ${relPosix(root, f)}`)
274
+ }
261
275
  }
262
276
  const firstTask =
263
277
  plan.projects.flatMap((p) => p.tasks.map((t) => t.name)).find((n) => n === 'build') ??
@@ -345,7 +359,14 @@ function renderConfigFile(
345
359
  for (const t of p.tasks) {
346
360
  for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
347
361
  if (t.task === null) continue // skipped target — the TODO above explains
348
- const key = IDENT.test(t.name) ? t.name : quoteTsLiteral(t.name)
362
+ // `__proto__: {…}` in a literal SETS the prototype, quoted or not, and
363
+ // the run refused the config (item 1033); a computed key is a property.
364
+ const key =
365
+ t.name === '__proto__'
366
+ ? `[${quoteTsLiteral(t.name)}]`
367
+ : IDENT.test(t.name)
368
+ ? t.name
369
+ : quoteTsLiteral(t.name)
349
370
  lines.push(` ${key}: ${renderValue(t.task, ' ')},`)
350
371
  }
351
372
  lines.push(' },', format === 'ts' ? '} satisfies ProjectConfig' : '}', '')