@vzn/vx 0.0.243 → 0.0.245

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.243",
3
+ "version": "0.0.245",
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.243",
21
- "@vzn/vx-linux-arm64": "0.0.243",
22
- "@vzn/vx-darwin-x64": "0.0.243",
23
- "@vzn/vx-darwin-arm64": "0.0.243"
20
+ "@vzn/vx-linux-x64": "0.0.245",
21
+ "@vzn/vx-linux-arm64": "0.0.245",
22
+ "@vzn/vx-darwin-x64": "0.0.245",
23
+ "@vzn/vx-darwin-arm64": "0.0.245"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -21,7 +21,7 @@ import { LOCKFILE_NAME } from './lockfile.js'
21
21
  import { configImportOwners } from './config-imports.js'
22
22
  import { configImports } from './config-cache.js'
23
23
  import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
24
- import { WORKSPACE_FINGERPRINT_FILES } from './fingerprint.js'
24
+ import { bunPatchFiles, WORKSPACE_FINGERPRINT_FILES } from './fingerprint.js'
25
25
  import { buildPackageGraph } from './package-graph.js'
26
26
  import type { PackageJson, ProjectMeta } from './workspace.js'
27
27
 
@@ -212,6 +212,16 @@ export async function affectedProjects(args: AffectedArgs): Promise<Set<string>>
212
212
  return new Set(args.projects.map((p) => p.name))
213
213
  }
214
214
  const fingerprintChanged = changed.filter((p) => FINGERPRINT_SET.has(p))
215
+ // A patch bun.lock names folds into every key, core's fingerprint and
216
+ // `bun()`'s claim alike, so its edit widens as a lockfile edit does. A
217
+ // patch the lockfile no longer names moved bun.lock itself.
218
+ if (changed.length > 0) {
219
+ const lock = await bytesOrNull(path.join(args.workspaceRoot, 'bun.lock'))
220
+ if (lock !== null) {
221
+ const patches = new Set(bunPatchFiles(lock))
222
+ if (changed.some((p) => patches.has(p))) return new Set(args.projects.map((p) => p.name))
223
+ }
224
+ }
215
225
  const claimedOwned = new Set<string>()
216
226
  // A claim may also name a root file core does not fold (`turbo.json`,
217
227
  // read by `turbo()`'s stages): it re-keys tasks through their resolved
@@ -106,9 +106,54 @@ export async function computeWorkspaceFingerprints(
106
106
  unclaimed = xxh3(`${f}\0`, unclaimed)
107
107
  unclaimed = xxh3(bytes, unclaimed)
108
108
  }
109
+ // bun.lock names a patch by PATH, never by its content: an edited patch
110
+ // left the lockfile byte-identical and every key unmoved while the
111
+ // install applied the new one (item 1014, where only `bun()`'s claim
112
+ // learned it). Each file it names folds here; a claimed bun.lock's
113
+ // plugin folds them itself.
114
+ const bunLock = files.get('bun.lock')
115
+ if (bunLock !== undefined) {
116
+ for (const rel of bunPatchFiles(bunLock)) {
117
+ const bytes = await readOnce(reads, path.join(workspaceRoot, rel))
118
+ const mark = `patch\0${rel}\0${bytes === null ? 'gone' : 'present'}`
119
+ all = xxh3(mark, all)
120
+ if (bytes !== null) all = xxh3(bytes, all)
121
+ if (claimed.has('bun.lock')) continue
122
+ unclaimed = xxh3(mark, unclaimed)
123
+ if (bytes !== null) unclaimed = xxh3(bytes, unclaimed)
124
+ }
125
+ }
109
126
  return { all: hex(all), unclaimed: hex(unclaimed), files }
110
127
  }
111
128
 
129
+ const PATCHED = new TextEncoder().encode('"patchedDependencies"')
130
+
131
+ /**
132
+ * The workspace-relative patch files a `bun.lock` names, sorted; none
133
+ * outside the workspace. A lockfile without the key is not parsed (the
134
+ * common case costs one byte search), and one that will not parse names
135
+ * none: its bytes still fold whole.
136
+ */
137
+ export function bunPatchFiles(bytes: Uint8Array): string[] {
138
+ if (Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).indexOf(PATCHED) < 0) return []
139
+ let doc: unknown
140
+ try {
141
+ doc = Bun.JSONC.parse(new TextDecoder().decode(bytes))
142
+ } catch {
143
+ return []
144
+ }
145
+ const patched = (doc as { patchedDependencies?: unknown } | null)?.patchedDependencies
146
+ if (patched === null || typeof patched !== 'object') return []
147
+ const out = new Set<string>()
148
+ for (const v of Object.values(patched)) {
149
+ if (typeof v !== 'string') continue
150
+ const rel = path.posix.normalize(v.split(path.sep).join('/'))
151
+ if (rel.startsWith('../') || rel === '..' || path.posix.isAbsolute(rel)) continue
152
+ out.add(rel)
153
+ }
154
+ return [...out].sort()
155
+ }
156
+
112
157
  function hex(h: bigint): string {
113
158
  return h.toString(16).padStart(16, '0')
114
159
  }
@@ -96,8 +96,8 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
96
96
  }
97
97
 
98
98
  /**
99
- * Whether the package manager that owns `dir` runs `pre` / `post` hooks
100
- * around a script. The manager is the nearest `packageManager` field or
99
+ * The package manager that owns `dir`, when it runs `pre` / `post` hooks
100
+ * around a script; null when it runs none. The manager is the nearest `packageManager` field or
101
101
  * lockfile, and its settings are read beside it, as each reads them for a
102
102
  * member too. Probed with a `prebuild` / `build` / `postbuild` trio: Yarn
103
103
  * 2+ runs none (D-31); npm runs none under `ignore-scripts=true` in
@@ -105,7 +105,7 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
105
105
  * `enablePrePostScripts: false` in `pnpm-workspace.yaml` (D-33); Bun and
106
106
  * Yarn 1 run them whatever those say. No manager found is npm's default.
107
107
  */
108
- function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
108
+ function runsScriptHooks(dir: string, memo: Map<string, string | null>): string | null {
109
109
  const known = memo.get(dir)
110
110
  if (known !== undefined) return known
111
111
  const read = (f: string): string | undefined => {
@@ -129,15 +129,15 @@ function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
129
129
  else if (existsSync(path.join(dir, 'package-lock.json'))) manager = 'npm'
130
130
  else if (['bun.lock', 'bun.lockb'].some((f) => existsSync(path.join(dir, f)))) manager = 'bun'
131
131
  }
132
- let answer: boolean
132
+ let answer: string | null
133
133
  if (manager === undefined) {
134
134
  const up = path.dirname(dir)
135
- answer = up === dir ? true : runsScriptHooks(up, memo)
135
+ answer = up === dir ? 'npm' : runsScriptHooks(up, memo)
136
136
  } else {
137
137
  const npmrc = read('.npmrc') ?? ''
138
138
  const set = (key: string, value: string): boolean =>
139
139
  new RegExp(`^\\s*${key}\\s*=\\s*${value}\\s*$`, 'm').test(npmrc)
140
- answer =
140
+ const runs =
141
141
  manager === 'berry'
142
142
  ? false
143
143
  : manager === 'npm'
@@ -146,6 +146,7 @@ function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
146
146
  ? !set('enable-pre-post-scripts', 'false') &&
147
147
  !/^enablePrePostScripts:\s*false\s*$/m.test(read('pnpm-workspace.yaml') ?? '')
148
148
  : true
149
+ answer = runs ? manager : null
149
150
  }
150
151
  memo.set(dir, answer)
151
152
  return answer
@@ -242,7 +243,7 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
242
243
 
243
244
  export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
244
245
  const projects: GeneratedProject[] = []
245
- const hookMemo = new Map<string, boolean>()
246
+ const hookMemo = new Map<string, string | null>()
246
247
  // The workspace root among members: its scripts run the workspace
247
248
  // (`npm run build --workspaces`, `pnpm -r build`), and a root task made
248
249
  // of one ran every member's build again under `--all`. Since D-39 a
@@ -258,7 +259,8 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
258
259
  for (const meta of metas) {
259
260
  if (meta === root) continue
260
261
  const scripts = scriptsOf(meta)
261
- const runsHooks = runsScriptHooks(meta.dir, hookMemo)
262
+ const hooksBy = runsScriptHooks(meta.dir, hookMemo)
263
+ const runsHooks = hooksBy !== null
262
264
  const runnable = Object.keys(scripts).filter(
263
265
  (n) => typeof scripts[n] === 'string' && scripts[n] !== '',
264
266
  )
@@ -314,7 +316,7 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
314
316
  )
315
317
  if (hooks.length > 0) {
316
318
  todos.push(
317
- `npm ran ${hooks.map((h) => `\`${h}\``).join(' and ')} around this script without being asked; folded into the command in that order`,
319
+ `${hooksBy} ran ${hooks.map((h) => `\`${h}\``).join(' and ')} around this script without being asked; folded into the command in that order`,
318
320
  )
319
321
  }
320
322
  const exec: Record<string, unknown> = { command }