@vzn/vx 0.0.612 → 0.0.613

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.612",
3
+ "version": "0.0.613",
4
4
  "description": "The fastest 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.612",
25
- "@vzn/vx-linux-arm64": "0.0.612",
26
- "@vzn/vx-darwin-x64": "0.0.612",
27
- "@vzn/vx-darwin-arm64": "0.0.612"
24
+ "@vzn/vx-linux-x64": "0.0.613",
25
+ "@vzn/vx-linux-arm64": "0.0.613",
26
+ "@vzn/vx-darwin-x64": "0.0.613",
27
+ "@vzn/vx-darwin-arm64": "0.0.613"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -34,7 +34,16 @@ function describeValue(v: unknown): string {
34
34
  if (v === null) return 'null'
35
35
  if (Array.isArray(v)) return 'an array'
36
36
  const t = typeof v
37
- return t === 'object' ? 'an object' : `a ${t}`
37
+ if (t !== 'object') return `a ${t}`
38
+ const proto: unknown = Object.getPrototypeOf(v)
39
+ if (proto === Object.prototype || proto === null) return 'an object'
40
+ return `a ${(v as object).constructor?.name || 'class instance'}`
41
+ }
42
+
43
+ function isRecord(v: unknown): v is Record<string, unknown> {
44
+ if (typeof v !== 'object' || v === null) return false
45
+ const proto: unknown = Object.getPrototypeOf(v)
46
+ return proto === Object.prototype || proto === null
38
47
  }
39
48
 
40
49
  /** Every method `CacheLayer` requires (the optional ones are probed with `?.`). */
@@ -197,7 +206,8 @@ export async function applyProjectHooks(
197
206
  * against the LAST plugin that ran: usually the one whose edit made it so,
198
207
  * but an earlier plugin's edit that a later one left in place is blamed on
199
208
  * the later one, since the structure is not checked between plugins.
200
- * Each node's task config is (`afterEach`), like the `project` stage's.
209
+ * Each node's task config is re-validated after EACH plugin (`afterEach`),
210
+ * like the `project` stage's.
201
211
  */
202
212
  export async function applyGraphHooks(
203
213
  plugins: readonly VxPlugin[],
@@ -271,8 +281,10 @@ export async function applyKeyHooks(
271
281
  if (material === undefined) continue
272
282
  // `Object.entries` over a string yields its characters as string
273
283
  // values, so a plugin returning `'v22'` used to fold parts named
274
- // '0', '1', '2' into every key — silently, and permanently.
275
- if (typeof material !== 'object' || material === null || Array.isArray(material)) {
284
+ // '0', '1', '2' into every key — silently, and permanently. A Map or a
285
+ // class instance keeps its data off own enumerable keys, so
286
+ // `Object.entries` read it as no material at all: a silent miskey.
287
+ if (!isRecord(material)) {
276
288
  throw new UserError(
277
289
  `plugin '${plugin.name}' failed in key: returned ${describeValue(material)}, not a record of string values`,
278
290
  )
@@ -254,12 +254,16 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
254
254
  }
255
255
  for (const cap of PLUGIN_FUNCTION_HOOKS) {
256
256
  if (plug[cap] !== undefined && typeof plug[cap] !== 'function') {
257
- throw new UserError(`${configPath}: \`plugins[${i}].${cap}\` must be a function`)
257
+ throw new UserError(
258
+ `${configPath}: \`plugins[${i}].${cap}\` of plugin '${pkg}' must be a function`,
259
+ )
258
260
  }
259
261
  }
260
262
  if (plug.commands !== undefined) {
261
263
  if (plug.commands === null || typeof plug.commands !== 'object') {
262
- throw new UserError(`${configPath}: \`plugins[${i}].commands\` must be an object`)
264
+ throw new UserError(
265
+ `${configPath}: \`plugins[${i}].commands\` of plugin '${pkg}' must be an object`,
266
+ )
263
267
  }
264
268
  // A list of commands loaded as the verbs `0`, `1`, … that help listed.
265
269
  if (Array.isArray(plug.commands)) {
@@ -276,7 +280,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
276
280
  typeof c.description !== 'string'
277
281
  ) {
278
282
  throw new UserError(
279
- `${configPath}: \`plugins[${i}].commands.${verb}\` must be { description: string, run: function }`,
283
+ `${configPath}: \`plugins[${i}].commands.${verb}\` of plugin '${pkg}' must be { description: string, run: function }`,
280
284
  )
281
285
  }
282
286
  // A verb the dispatcher would never reach is refused, not left
@@ -287,10 +291,12 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
287
291
  // package: two plugins one package exports (`bun()` and `pnpm()`
288
292
  // both carry `prune`) are one owner, and the first declared runs.
289
293
  // `vx --version` and `vx ''` never reach a plugin: a flag is
290
- // core's, and an empty word is no verb (X-21).
291
- if (verb.trim() === '' || verb.startsWith('-')) {
294
+ // core's, and an empty word is no verb (X-21). `vx completions`
295
+ // writes verbs unquoted into a shell script, so a space or a `$(`
296
+ // in one would break or inject into the user's rc.
297
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(verb)) {
292
298
  throw new UserError(
293
- `${configPath}: plugin '${plug.name}' declares command '${verb}', which no command line reaches — a verb is a word, not a flag or empty`,
299
+ `${configPath}: plugin '${plug.name}' declares command '${verb}', which is not a word — a verb is a letter or digit, then letters, digits, ':', '.', '_' or '-'`,
294
300
  )
295
301
  }
296
302
  if ((CORE_VERBS as readonly string[]).includes(verb)) {
@@ -317,7 +323,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
317
323
  typeof claim.affected !== 'function'
318
324
  ) {
319
325
  throw new UserError(
320
- `${configPath}: \`plugins[${i}].fingerprint\` must be { files: [name, …], affected: function }`,
326
+ `${configPath}: \`plugins[${i}].fingerprint\` of plugin '${pkg}' must be { files: [name, …], affected: function }`,
321
327
  )
322
328
  }
323
329
  for (const file of claim.files as unknown[]) {
@@ -357,7 +363,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
357
363
  // — an empty `{ name }` object is a no-op authoring mistake.
358
364
  if (PLUGIN_HOOKS.every((hook) => plug[hook] === undefined)) {
359
365
  throw new UserError(
360
- `${configPath}: \`plugins[${i}]\` must contribute at least one of ${PLUGIN_HOOKS.join('/')}`,
366
+ `${configPath}: \`plugins[${i}]\` (plugin '${pkg}') must contribute at least one of ${PLUGIN_HOOKS.join('/')}`,
361
367
  )
362
368
  }
363
369
  }