@vzn/vx 0.0.18 → 0.0.21
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/LICENSE +1 -1
- package/README.md +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/schedule-history/index.ts +0 -109
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { Dirent } from 'node:fs'
|
|
1
2
|
import { readdir, stat } from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import type { ProjectConfig, WorkspaceConfig } from '../config.js'
|
|
4
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
5
|
+
import { relPosix, UserError, normalizeGlob } from '../util/index.js'
|
|
5
6
|
|
|
6
7
|
export interface PackageJson {
|
|
7
8
|
name: string
|
|
@@ -89,8 +90,9 @@ export async function findWorkspaceRoot(start: string): Promise<string> {
|
|
|
89
90
|
/** True when one of `below` (dirs under `root`, toward `start`) is a member. */
|
|
90
91
|
function claimsMember(root: string, below: readonly string[], globs: readonly string[]): boolean {
|
|
91
92
|
if (below.length === 0 || globs.length === 0) return false
|
|
92
|
-
const
|
|
93
|
-
|
|
93
|
+
const { positive, negative } = splitPackageGlobs(globs)
|
|
94
|
+
const rels = below.map((d) => relPosix(root, d)).filter((rel) => !excludedBy(rel, negative))
|
|
95
|
+
for (const pattern of positive) {
|
|
94
96
|
const normalized = pattern.replace(/\/+$/, '')
|
|
95
97
|
// `.` means the root itself is the project — never a directory below it.
|
|
96
98
|
if (normalized === '' || normalized === '.') continue
|
|
@@ -100,6 +102,37 @@ function claimsMember(root: string, below: readonly string[], globs: readonly st
|
|
|
100
102
|
return false
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
/**
|
|
106
|
+
* pnpm, npm, yarn and Bun all take `!packages/fixtures` in the package
|
|
107
|
+
* list. Handed to `Bun.Glob` raw, a leading `!` negates the WHOLE pattern,
|
|
108
|
+
* so `!packages/fixtures/package.json` matched every manifest in the tree
|
|
109
|
+
* and every directory holding one became a project — the excluded package
|
|
110
|
+
* ran under `--all`, and a fixture repeating a name killed the run with
|
|
111
|
+
* "Duplicate package name". Negations subtract from what the positive
|
|
112
|
+
* globs found; a literal one excludes its tree.
|
|
113
|
+
*/
|
|
114
|
+
function splitPackageGlobs(globs: readonly string[]): { positive: string[]; negative: string[] } {
|
|
115
|
+
const positive: string[] = []
|
|
116
|
+
const negative: string[] = []
|
|
117
|
+
// `!./packages/legacy` and `!packages//legacy` excluded nothing, silently
|
|
118
|
+
// (2026-09-10): the same spellings the input globs normalize.
|
|
119
|
+
for (const raw of globs) {
|
|
120
|
+
const g = normalizeGlob(raw)
|
|
121
|
+
if (g.startsWith('!')) negative.push(g.slice(1).replace(/\/+$/, ''))
|
|
122
|
+
else positive.push(g)
|
|
123
|
+
}
|
|
124
|
+
return { positive, negative }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function excludedBy(rel: string, negative: readonly string[]): boolean {
|
|
128
|
+
for (const neg of negative) {
|
|
129
|
+
if (neg.length === 0) continue
|
|
130
|
+
if (rel === neg || rel.startsWith(`${neg}/`)) return true
|
|
131
|
+
if (/[*?[\]{}]/.test(neg) && new Bun.Glob(neg).match(rel)) return true
|
|
132
|
+
}
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
103
136
|
/**
|
|
104
137
|
* Package globs declared by `dir`, or `null` when `dir` is not a root
|
|
105
138
|
* candidate. A bare `package.json` (no `workspaces`) is single-project mode:
|
|
@@ -188,6 +221,23 @@ export async function loadWorkspace(root: string): Promise<Workspace> {
|
|
|
188
221
|
// path.
|
|
189
222
|
const SIMPLE_STAR_RE = /^[^*?{}[\]!]+\/\*$/
|
|
190
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The directory each `<dir>/*` package glob names — where a member comes
|
|
226
|
+
* and goes as one directory entry, so `vx watch` can hear a package added
|
|
227
|
+
* or removed without walking anything. A glob of another shape (`apps/**`)
|
|
228
|
+
* has no such directory and contributes none.
|
|
229
|
+
*/
|
|
230
|
+
export function memberBaseDirs(workspace: Workspace): string[] {
|
|
231
|
+
const bases = new Set<string>()
|
|
232
|
+
for (const pattern of splitPackageGlobs(workspace.packageGlobs).positive) {
|
|
233
|
+
const normalized = pattern === '.' ? '' : pattern.replace(/\/$/, '')
|
|
234
|
+
if (SIMPLE_STAR_RE.test(normalized)) {
|
|
235
|
+
bases.add(path.resolve(workspace.root, normalized.slice(0, -2)))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [...bases]
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
/**
|
|
192
242
|
* The directories a workspace glob names. For the `<dir>/*` shape this is
|
|
193
243
|
* one readdir of `<dir>` — the same answer `Bun.Glob` gives, at a third of
|
|
@@ -237,31 +287,66 @@ async function memberDirs(root: string, pattern: string): Promise<string[]> {
|
|
|
237
287
|
|
|
238
288
|
/**
|
|
239
289
|
* The project's config file, by `CONFIG_FILENAMES` precedence, or null.
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
290
|
+
*
|
|
291
|
+
* Two shapes, one per platform, each the measured winner there across
|
|
292
|
+
* 1,000 projects with the manifest read in flight alongside:
|
|
293
|
+
* - macOS stats the candidates in order: a readdir each cost 9.6 ms
|
|
294
|
+
* against 6.3 ms of stats even with the config at the LAST name
|
|
295
|
+
* (2026-09-03), and a `.ts` config — the first name — pays one stat.
|
|
296
|
+
* - Linux lists the directory once: a readdir each is 12 ms against
|
|
297
|
+
* 43 ms of stats at the last name and ties the one-stat case
|
|
298
|
+
* (2026-09-09, a `vx.config.mjs` workspace; stats there cost a
|
|
299
|
+
* thread-pool round trip apiece).
|
|
300
|
+
* Both answer identically: a directory or dangling link under a config
|
|
301
|
+
* name is skipped for the next name, a symlink to a file counts.
|
|
244
302
|
*/
|
|
245
303
|
async function findConfigFile(dir: string): Promise<string | null> {
|
|
246
|
-
|
|
247
|
-
|
|
304
|
+
if (process.platform === 'linux') {
|
|
305
|
+
let entries: Dirent[]
|
|
248
306
|
try {
|
|
249
|
-
|
|
307
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
250
308
|
} catch {
|
|
251
|
-
|
|
309
|
+
return null
|
|
252
310
|
}
|
|
311
|
+
const byName = new Map(entries.map((e) => [e.name, e]))
|
|
312
|
+
for (const name of CONFIG_FILENAMES) {
|
|
313
|
+
const entry = byName.get(name)
|
|
314
|
+
if (entry === undefined) continue
|
|
315
|
+
const candidate = path.join(dir, name)
|
|
316
|
+
if (entry.isFile()) return candidate
|
|
317
|
+
if (entry.isSymbolicLink() && (await isFile(candidate))) return candidate
|
|
318
|
+
}
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
for (const name of CONFIG_FILENAMES) {
|
|
322
|
+
const candidate = path.join(dir, name)
|
|
323
|
+
if (await isFile(candidate)) return candidate
|
|
253
324
|
}
|
|
254
325
|
return null
|
|
255
326
|
}
|
|
256
327
|
|
|
328
|
+
async function isFile(p: string): Promise<boolean> {
|
|
329
|
+
try {
|
|
330
|
+
return (await stat(p)).isFile()
|
|
331
|
+
} catch {
|
|
332
|
+
return false
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
257
336
|
export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
258
337
|
// Run all package globs concurrently. Disk-bound walks parallelize
|
|
259
338
|
// well; serializing them just stretches the discovery phase by N×.
|
|
339
|
+
const { positive, negative } = splitPackageGlobs(workspace.packageGlobs)
|
|
260
340
|
const perPattern = await Promise.all(
|
|
261
|
-
|
|
341
|
+
positive.map((pattern) => memberDirs(workspace.root, pattern)),
|
|
262
342
|
)
|
|
263
343
|
const matches = new Set<string>()
|
|
264
|
-
for (const arr of perPattern)
|
|
344
|
+
for (const arr of perPattern) {
|
|
345
|
+
for (const m of arr) {
|
|
346
|
+
if (negative.length > 0 && excludedBy(relPosix(workspace.root, m), negative)) continue
|
|
347
|
+
matches.add(m)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
265
350
|
|
|
266
351
|
// Per-project discovery: the manifest read answers "is there a
|
|
267
352
|
// package.json" (an ENOENT is the "not a member" answer, at the cost of
|
package/src/cache/cas-backend.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// CASBackend — pluggable content-addressed storage beneath the cache.
|
|
2
|
-
//
|
|
3
|
-
// Today's Cache + LayeredCache both bundle "where do the bytes live"
|
|
4
|
-
// with "how do we look up entries metadata + key derivation."
|
|
5
|
-
// CASBackend separates the former so a future R2/S3 backend
|
|
6
|
-
// or REAPI CAS bridge can drop in without touching the
|
|
7
|
-
// orchestrator or the SQL entries index.
|
|
8
|
-
//
|
|
9
|
-
// Two reference implementations in this file:
|
|
10
|
-
// - MemoryCASBackend — testing + in-memory builds. Holds raw bytes.
|
|
11
|
-
// - FsCASBackend — writes bytes to a directory; mirrors what
|
|
12
|
-
// Cache.save does today. Bun-native using Bun.file
|
|
13
|
-
// for the read path so big artifacts stream
|
|
14
|
-
// without slurping into memory.
|
|
15
|
-
//
|
|
16
|
-
// Cache.ts has NOT yet been rewired to use a CASBackend — that's a
|
|
17
|
-
// follow-up (Phase 1b). This file ships the abstraction + the two
|
|
18
|
-
// reference impls so downstream work (R2, otel-bridge, distributed-ci)
|
|
19
|
-
// can rely on the type. Byte-identical behaviour today.
|
|
20
|
-
|
|
21
|
-
import { mkdir, rename, rm } from 'node:fs/promises'
|
|
22
|
-
import path from 'node:path'
|
|
23
|
-
import type { Digest } from './digest.js'
|
|
24
|
-
|
|
25
|
-
/** Per-process counter for put() temp names; uniqueness only. */
|
|
26
|
-
let casTmpSeq = 0
|
|
27
|
-
|
|
28
|
-
export interface CASBackend {
|
|
29
|
-
/** Write bytes under `digest`. Idempotent — putting the same digest twice is a no-op. */
|
|
30
|
-
put(digest: Digest, bytes: Uint8Array): Promise<void>
|
|
31
|
-
/** Read bytes under `digest`, or null if absent. */
|
|
32
|
-
get(digest: Digest): Promise<Uint8Array | null>
|
|
33
|
-
/** Cheap existence probe — no bytes round-tripped if possible. */
|
|
34
|
-
has(digest: Digest): Promise<boolean>
|
|
35
|
-
/** Drop one entry (eviction). No-op if absent. */
|
|
36
|
-
remove(digest: Digest): Promise<void>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** In-memory backend, useful for tests and ephemeral runs. */
|
|
40
|
-
export class MemoryCASBackend implements CASBackend {
|
|
41
|
-
private readonly store = new Map<string, Uint8Array>()
|
|
42
|
-
|
|
43
|
-
async put(digest: Digest, bytes: Uint8Array): Promise<void> {
|
|
44
|
-
if (bytes.byteLength !== digest.sizeBytes) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`MemoryCASBackend.put: sizeBytes mismatch (digest=${digest.sizeBytes}, actual=${bytes.byteLength})`,
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
this.store.set(digest.hash, bytes)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async get(digest: Digest): Promise<Uint8Array | null> {
|
|
53
|
-
return this.store.get(digest.hash) ?? null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async has(digest: Digest): Promise<boolean> {
|
|
57
|
-
return this.store.has(digest.hash)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async remove(digest: Digest): Promise<void> {
|
|
61
|
-
this.store.delete(digest.hash)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Test-only: how many entries are held. */
|
|
65
|
-
size(): number {
|
|
66
|
-
return this.store.size
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Filesystem-backed backend writing `<rootDir>/<hash>.tar.zst` files. */
|
|
71
|
-
export class FsCASBackend implements CASBackend {
|
|
72
|
-
constructor(private readonly rootDir: string) {}
|
|
73
|
-
|
|
74
|
-
private pathFor(digest: Digest): string {
|
|
75
|
-
return path.join(this.rootDir, `${digest.hash}.tar.zst`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async put(digest: Digest, bytes: Uint8Array): Promise<void> {
|
|
79
|
-
if (bytes.byteLength !== digest.sizeBytes) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`FsCASBackend.put: sizeBytes mismatch (digest=${digest.sizeBytes}, actual=${bytes.byteLength})`,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
// Temp + rename, the same shape `Cache.writeArtifactAndIndex` and the
|
|
85
|
-
// archive extractor use: two writers of the SAME content-addressed
|
|
86
|
-
// blob are expected (that is what content-addressing invites), and a
|
|
87
|
-
// plain write lets a reader observe a half-written file under a name
|
|
88
|
-
// that promises complete bytes. rename(2) is atomic, so a concurrent
|
|
89
|
-
// reader sees either the old file or the whole new one.
|
|
90
|
-
const final = this.pathFor(digest)
|
|
91
|
-
const tmp = `${final}.tmp-${process.pid.toString(36)}-${(casTmpSeq++).toString(36)}`
|
|
92
|
-
await mkdir(this.rootDir, { recursive: true })
|
|
93
|
-
try {
|
|
94
|
-
await Bun.write(tmp, bytes)
|
|
95
|
-
await rename(tmp, final)
|
|
96
|
-
} catch (err) {
|
|
97
|
-
await rm(tmp, { force: true })
|
|
98
|
-
throw err
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async get(digest: Digest): Promise<Uint8Array | null> {
|
|
103
|
-
const file = Bun.file(this.pathFor(digest))
|
|
104
|
-
if (!(await file.exists())) return null
|
|
105
|
-
return new Uint8Array(await file.arrayBuffer())
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async has(digest: Digest): Promise<boolean> {
|
|
109
|
-
return Bun.file(this.pathFor(digest)).exists()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async remove(digest: Digest): Promise<void> {
|
|
113
|
-
await rm(this.pathFor(digest), { force: true })
|
|
114
|
-
}
|
|
115
|
-
}
|
package/src/cache/digest.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// Digest — the explicit content-address used by every cache layer.
|
|
2
|
-
//
|
|
3
|
-
// Lifted as a first-class type (architecture-review-2026-06.md §4.3)
|
|
4
|
-
// so storage backends become pluggable beneath CacheLayer: local FS,
|
|
5
|
-
// R2, S3, or a future REAPI CAS bridge all speak (hash, sizeBytes).
|
|
6
|
-
//
|
|
7
|
-
// Two practical wins from making this explicit:
|
|
8
|
-
// 1. sizeBytes is the truncation check at transport boundaries (HTTP
|
|
9
|
-
// Content-Length, R2 ETag-by-size). Free correctness, free corrupt-
|
|
10
|
-
// artifact-early-fail.
|
|
11
|
-
// 2. CASBackend.has(digest) can answer with just the metadata, no
|
|
12
|
-
// bytes round-trip — what existence probes want.
|
|
13
|
-
//
|
|
14
|
-
// Why sizeBytes is `number` and not `bigint`: Bun.file().size returns
|
|
15
|
-
// number; JS Number safely represents integers to 2^53-1 (~9 PB); no
|
|
16
|
-
// real cache artifact gets there. Bigint would create the same
|
|
17
|
-
// JSON.stringify-throws nuisance we already navigated for ns timestamps.
|
|
18
|
-
|
|
19
|
-
export interface Digest {
|
|
20
|
-
readonly hash: string
|
|
21
|
-
readonly sizeBytes: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function makeDigest(hash: string, sizeBytes: number): Digest {
|
|
25
|
-
if (!/^[0-9a-f]+$/i.test(hash)) {
|
|
26
|
-
throw new Error(`invalid digest hash: ${JSON.stringify(hash)}`)
|
|
27
|
-
}
|
|
28
|
-
if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {
|
|
29
|
-
throw new Error(`invalid digest sizeBytes: ${sizeBytes}`)
|
|
30
|
-
}
|
|
31
|
-
return { hash, sizeBytes }
|
|
32
|
-
}
|
package/src/cli/migrate-emit.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Shared TS-literal emission helper for the migrate mappers. Lives in its own
|
|
2
|
-
// leaf module so both migrate.ts (the config emitter) and migrate-turbo.ts (the
|
|
3
|
-
// preset renderer) can escape strings identically without a dependency cycle
|
|
4
|
-
// (migrate.ts already imports the turbo/nx mappers, so a back-import of a value
|
|
5
|
-
// from migrate.ts would close a runtime cycle).
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Escape an arbitrary string into a single-quoted TS literal. Escapes
|
|
9
|
-
* backslash + quote AND raw newlines/CR — a value with an embedded newline
|
|
10
|
-
* (legal JSON, e.g. a script `"echo a\necho b"`, or a glob with a `'`) would
|
|
11
|
-
* otherwise splice into a single-quoted literal as an unterminated / malformed
|
|
12
|
-
* string that fails to load (generated files must round-trip through the
|
|
13
|
-
* loader).
|
|
14
|
-
*/
|
|
15
|
-
export function quote(s: string): string {
|
|
16
|
-
return `'${s
|
|
17
|
-
.replaceAll('\\', '\\\\')
|
|
18
|
-
.replaceAll("'", "\\'")
|
|
19
|
-
.replaceAll('\n', '\\n')
|
|
20
|
-
.replaceAll('\r', '\\r')}'`
|
|
21
|
-
}
|