@vzn/vx-lockfile 0.0.635 → 0.0.637
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 +35 -30
- package/package.json +4 -6
- package/src/bun.ts +88 -92
- package/src/index.ts +349 -73
- package/src/json.ts +6 -22
- package/src/npm.ts +23 -48
- package/src/pnpm.ts +19 -109
- package/src/posix.ts +14 -0
- package/src/yarn.ts +92 -93
- package/src/blocks.ts +0 -83
- package/src/prune.ts +0 -400
- package/src/scope.ts +0 -17
package/src/prune.ts
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
// `vx prune <project...> [--out-dir <dir>] [--docker] [--production]`: the workspace cut to
|
|
2
|
-
// the named projects and their transitive workspace dependencies, for a
|
|
3
|
-
// Docker build that installs and builds only what it ships (`turbo prune`).
|
|
4
|
-
// Every lockfile here is cut to what the subset installs, so the copy
|
|
5
|
-
// installs with a frozen lockfile and a lockfile line the subset cannot
|
|
6
|
-
// reach does not bust the install layer. `--production` follows no
|
|
7
|
-
// devDependencies edge between workspace packages: a package only a dev
|
|
8
|
-
// dependency pulls in is left out, and the kept manifests and lockfile no
|
|
9
|
-
// longer name it (`turbo prune --production`).
|
|
10
|
-
//
|
|
11
|
-
// <out>/ the subset (with --docker: <out>/full/)
|
|
12
|
-
// package.json `workspaces` rewritten to the subset's dirs
|
|
13
|
-
// pnpm-workspace.yaml `packages` rewritten the same way, the rest as written
|
|
14
|
-
// <lockfile> pruned
|
|
15
|
-
// vx.workspace.*, the root vx.config.*, install config, patch files
|
|
16
|
-
// <project dirs> whole, less node_modules / .git / .vx / .turbo
|
|
17
|
-
// <out>/json/ (--docker) the install layer: root install files and
|
|
18
|
-
// each project's package.json, nothing else
|
|
19
|
-
|
|
20
|
-
import { cp, mkdir, readdir, stat, writeFile } from 'node:fs/promises'
|
|
21
|
-
import path from 'node:path'
|
|
22
|
-
import {
|
|
23
|
-
buildPackageGraph,
|
|
24
|
-
listProjectMetas,
|
|
25
|
-
loadWorkspace,
|
|
26
|
-
UserError,
|
|
27
|
-
type CommandContext,
|
|
28
|
-
type ProjectMeta,
|
|
29
|
-
} from '@vzn/vx'
|
|
30
|
-
import * as bunLock from './bun.js'
|
|
31
|
-
import { record } from './json.js'
|
|
32
|
-
import * as npmLock from './npm.js'
|
|
33
|
-
import * as pnpmLock from './pnpm.js'
|
|
34
|
-
import * as yarnLock from './yarn.js'
|
|
35
|
-
import type { PruneScope } from './scope.js'
|
|
36
|
-
|
|
37
|
-
const USAGE = 'usage: vx prune <project...> [--out-dir <dir>] [--docker] [--production]'
|
|
38
|
-
|
|
39
|
-
const LOCKFILES: Readonly<Record<string, (text: string, scope: PruneScope) => string>> = {
|
|
40
|
-
'pnpm-lock.yaml': pnpmLock.pruneLockfile,
|
|
41
|
-
'bun.lock': bunLock.pruneLockfile,
|
|
42
|
-
'package-lock.json': npmLock.pruneLockfile,
|
|
43
|
-
'yarn.lock': yarnLock.pruneLockfile,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Root files an install reads, copied when present (the manifest and workspace file are rewritten). */
|
|
47
|
-
const INSTALL_FILES = [
|
|
48
|
-
'.npmrc',
|
|
49
|
-
'.yarnrc.yml',
|
|
50
|
-
'.pnpmfile.cjs',
|
|
51
|
-
'bunfig.toml',
|
|
52
|
-
'.nvmrc',
|
|
53
|
-
'.node-version',
|
|
54
|
-
]
|
|
55
|
-
const VX_FILES = [
|
|
56
|
-
'vx.workspace.ts',
|
|
57
|
-
'vx.workspace.mts',
|
|
58
|
-
'vx.workspace.js',
|
|
59
|
-
'vx.workspace.mjs',
|
|
60
|
-
'vx.config.ts',
|
|
61
|
-
'vx.config.mts',
|
|
62
|
-
'vx.config.js',
|
|
63
|
-
'vx.config.mjs',
|
|
64
|
-
'vx.config.cts',
|
|
65
|
-
'vx.config.cjs',
|
|
66
|
-
]
|
|
67
|
-
const COPY_EXCLUDES = new Set(['node_modules', '.git', '.vx', '.turbo'])
|
|
68
|
-
const DEP_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'] as const
|
|
69
|
-
/** `from '…'`, `import '…'`, `import('…')`: static specifiers only. */
|
|
70
|
-
const SPECIFIER_RE = /(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g
|
|
71
|
-
|
|
72
|
-
interface Args {
|
|
73
|
-
readonly projects: readonly string[]
|
|
74
|
-
readonly outDir: string
|
|
75
|
-
readonly docker: boolean
|
|
76
|
-
readonly production: boolean
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseArgs(argv: readonly string[]): Args | null {
|
|
80
|
-
const projects: string[] = []
|
|
81
|
-
let outDir = 'out'
|
|
82
|
-
let docker = false
|
|
83
|
-
let production = false
|
|
84
|
-
for (let i = 0; i < argv.length; i++) {
|
|
85
|
-
const a = argv[i]!
|
|
86
|
-
if (a === '--help' || a === '-h') return null
|
|
87
|
-
if (a === '--docker') docker = true
|
|
88
|
-
else if (a === '--production') production = true
|
|
89
|
-
else if (a === '--out-dir' || a.startsWith('--out-dir=')) {
|
|
90
|
-
const v = a === '--out-dir' ? argv[++i] : a.slice('--out-dir='.length)
|
|
91
|
-
if (v === undefined || v === '')
|
|
92
|
-
throw new UserError(`vx prune: --out-dir needs a path\n${USAGE}`)
|
|
93
|
-
// `--out-dir --docker` wrote to a directory named `--docker` and lost
|
|
94
|
-
// docker mode; a path that starts with '-' is spelt `--out-dir=-x`.
|
|
95
|
-
if (a === '--out-dir' && v.startsWith('-'))
|
|
96
|
-
throw new UserError(`vx prune: --out-dir needs a path, got the flag ${v}\n${USAGE}`)
|
|
97
|
-
outDir = v
|
|
98
|
-
} else if (a.startsWith('-')) throw new UserError(`vx prune: unknown flag ${a}\n${USAGE}`)
|
|
99
|
-
else projects.push(a)
|
|
100
|
-
}
|
|
101
|
-
if (projects.length === 0) throw new UserError(`vx prune: name at least one project\n${USAGE}`)
|
|
102
|
-
return { projects, outDir, docker, production }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function exists(p: string): Promise<boolean> {
|
|
106
|
-
return stat(p).then(
|
|
107
|
-
() => true,
|
|
108
|
-
() => false,
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const posix = (p: string): string => p.split(path.sep).join('/')
|
|
113
|
-
|
|
114
|
-
/** `a` within `dir`, or `dir` itself. */
|
|
115
|
-
const within = (a: string, dir: string): boolean => a === dir || a.startsWith(dir + path.sep)
|
|
116
|
-
|
|
117
|
-
export async function prune(argv: readonly string[], ctx: CommandContext): Promise<number> {
|
|
118
|
-
const args = parseArgs(argv)
|
|
119
|
-
if (args === null) {
|
|
120
|
-
process.stdout.write(`${USAGE}\n`)
|
|
121
|
-
return 0
|
|
122
|
-
}
|
|
123
|
-
const root = ctx.workspaceRoot
|
|
124
|
-
const projects = (await listProjectMetas(await loadWorkspace(root))).filter((p) => p.dir !== root)
|
|
125
|
-
const byName = new Map(projects.map((p) => [p.name, p]))
|
|
126
|
-
const unknown = args.projects.filter((n) => !byName.has(n))
|
|
127
|
-
if (unknown.length > 0) {
|
|
128
|
-
throw new UserError(
|
|
129
|
-
`vx prune: no project named ${unknown.map((n) => JSON.stringify(n)).join(', ')} (the root is always kept; name the projects under it)`,
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
// Production: the graph without dev edges, so a package reached only
|
|
133
|
-
// through a devDependencies entry is never added. The linking rules are
|
|
134
|
-
// the graph's own either way.
|
|
135
|
-
const graph = buildPackageGraph(
|
|
136
|
-
args.production
|
|
137
|
-
? projects.map((p) => ({ ...p, packageJson: { ...p.packageJson, devDependencies: {} } }))
|
|
138
|
-
: projects,
|
|
139
|
-
)
|
|
140
|
-
const names = new Set<string>()
|
|
141
|
-
const add = (name: string): void => {
|
|
142
|
-
if (names.has(name) || !byName.has(name)) return
|
|
143
|
-
names.add(name)
|
|
144
|
-
for (const d of graph.transitiveDeps(name)) add(d)
|
|
145
|
-
}
|
|
146
|
-
for (const n of args.projects) add(n)
|
|
147
|
-
// A vx config runs before any task, so a workspace package it imports (a
|
|
148
|
-
// local plugin) is as load-bearing as a dependency: without it `vx run`
|
|
149
|
-
// in the image cannot load the config. Pulled in with its closure, until
|
|
150
|
-
// no config names another.
|
|
151
|
-
const owner = (spec: string): ProjectMeta | undefined =>
|
|
152
|
-
projects.find((p) => spec === p.name || spec.startsWith(`${p.name}/`))
|
|
153
|
-
const escapes: string[] = []
|
|
154
|
-
const scanned = new Set<string>()
|
|
155
|
-
const scan = async (dir: string, label: string): Promise<void> => {
|
|
156
|
-
for (const f of VX_FILES) {
|
|
157
|
-
const file = path.join(dir, f)
|
|
158
|
-
if (!(await exists(file))) continue
|
|
159
|
-
for (const [, spec] of (await Bun.file(file).text()).matchAll(SPECIFIER_RE)) {
|
|
160
|
-
if (spec!.startsWith('.')) {
|
|
161
|
-
const target = path.resolve(dir, spec!)
|
|
162
|
-
const kept =
|
|
163
|
-
[...names].some((n) => within(target, byName.get(n)!.dir)) ||
|
|
164
|
-
path.dirname(target) === root
|
|
165
|
-
if (!kept) escapes.push(`${label}: ${spec} resolves outside the subset`)
|
|
166
|
-
} else {
|
|
167
|
-
const p = owner(spec!)
|
|
168
|
-
if (p !== undefined) add(p.name)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
await scan(root, 'the workspace root')
|
|
174
|
-
for (let grew = true; grew;) {
|
|
175
|
-
grew = false
|
|
176
|
-
for (const n of [...names]) {
|
|
177
|
-
if (scanned.has(n)) continue
|
|
178
|
-
scanned.add(n)
|
|
179
|
-
grew = true
|
|
180
|
-
await scan(byName.get(n)!.dir, n)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const subset = [...names].map((n) => byName.get(n)!).sort((a, b) => a.name.localeCompare(b.name))
|
|
184
|
-
const rels = subset.map((p) => posix(path.relative(root, p.dir)))
|
|
185
|
-
|
|
186
|
-
const out = path.resolve(process.cwd(), args.outDir)
|
|
187
|
-
if (within(root, out)) {
|
|
188
|
-
throw new UserError(`vx prune: --out-dir ${args.outDir} is or contains the workspace root`)
|
|
189
|
-
}
|
|
190
|
-
for (const p of subset) {
|
|
191
|
-
if (within(out, p.dir)) {
|
|
192
|
-
throw new UserError(
|
|
193
|
-
`vx prune: --out-dir ${args.outDir} is inside ${p.name}, which the subset copies — pick a path outside it`,
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const held = await readdir(out).then(
|
|
198
|
-
(entries) => entries.length > 0,
|
|
199
|
-
(err: NodeJS.ErrnoException) => err.code !== 'ENOENT',
|
|
200
|
-
)
|
|
201
|
-
if (held) {
|
|
202
|
-
throw new UserError(
|
|
203
|
-
`vx prune: --out-dir ${args.outDir} already has content — remove it or name an empty directory`,
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const rootManifest = JSON.parse(await Bun.file(path.join(root, 'package.json')).text()) as Record<
|
|
208
|
-
string,
|
|
209
|
-
unknown
|
|
210
|
-
>
|
|
211
|
-
const field = rootManifest['workspaces']
|
|
212
|
-
const listed = Array.isArray(field)
|
|
213
|
-
? (field as string[])
|
|
214
|
-
: ((field as { packages?: string[] } | undefined)?.packages ?? undefined)
|
|
215
|
-
const patterns = rels.map(literalGlob)
|
|
216
|
-
const workspaces = listed?.includes('.') ? ['.', ...patterns] : patterns
|
|
217
|
-
if (listed !== undefined) {
|
|
218
|
-
rootManifest['workspaces'] = Array.isArray(field)
|
|
219
|
-
? workspaces
|
|
220
|
-
: { ...(field as object), packages: workspaces }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Production: each kept manifest (the root's too) without the
|
|
224
|
-
// devDependencies entries naming a workspace package the subset leaves
|
|
225
|
-
// out, written over the copy; the lockfiles drop the same entries.
|
|
226
|
-
const dropped = new Set<string>()
|
|
227
|
-
const rewritten = new Map<string, Record<string, unknown>>()
|
|
228
|
-
if (args.production) {
|
|
229
|
-
const strip = (rel: string, pkg: Record<string, unknown>): void => {
|
|
230
|
-
const dev = record(pkg['devDependencies'])
|
|
231
|
-
const gone = Object.keys(dev ?? {}).filter((n) => byName.has(n) && !names.has(n))
|
|
232
|
-
if (dev === undefined || gone.length === 0) return
|
|
233
|
-
for (const n of gone) dropped.add(n)
|
|
234
|
-
const { devDependencies: _, ...rest } = pkg
|
|
235
|
-
const kept = Object.entries(dev).filter(([n]) => !gone.includes(n))
|
|
236
|
-
rewritten.set(
|
|
237
|
-
rel,
|
|
238
|
-
kept.length === 0 ? rest : { ...pkg, devDependencies: Object.fromEntries(kept) },
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
strip('.', rootManifest)
|
|
242
|
-
for (const [i, p] of subset.entries()) {
|
|
243
|
-
strip(rels[i]!, JSON.parse(await Bun.file(path.join(p.dir, 'package.json')).text()))
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const manifests = new Map<string, ReadonlyMap<string, string>>()
|
|
247
|
-
type Deps = Partial<Record<(typeof DEP_FIELDS)[number], Record<string, string>>>
|
|
248
|
-
const depsOf = (pkg: Deps): ReadonlyMap<string, string> =>
|
|
249
|
-
new Map(DEP_FIELDS.flatMap((f) => Object.entries(pkg[f] ?? {})))
|
|
250
|
-
manifests.set('.', depsOf((rewritten.get('.') ?? rootManifest) as Deps))
|
|
251
|
-
for (const [i, p] of subset.entries()) {
|
|
252
|
-
manifests.set(rels[i]!, depsOf((rewritten.get(rels[i]!) ?? p.packageJson) as Deps))
|
|
253
|
-
}
|
|
254
|
-
const scope: PruneScope = {
|
|
255
|
-
dirs: new Set(['.', ...rels]),
|
|
256
|
-
members: new Set(projects.map((p) => posix(path.relative(root, p.dir)))),
|
|
257
|
-
workspaces,
|
|
258
|
-
manifests,
|
|
259
|
-
dropped,
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
(await exists(path.join(root, 'bun.lockb'))) &&
|
|
264
|
-
!(await exists(path.join(root, 'bun.lock')))
|
|
265
|
-
) {
|
|
266
|
-
throw new UserError(
|
|
267
|
-
'vx prune: bun.lockb is binary and cannot be pruned — `bun install --save-text-lockfile` writes bun.lock',
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
const written = new Map<string, string>()
|
|
271
|
-
for (const [file, pruneLock] of Object.entries(LOCKFILES)) {
|
|
272
|
-
const src = path.join(root, file)
|
|
273
|
-
if (!(await exists(src))) continue
|
|
274
|
-
try {
|
|
275
|
-
written.set(file, pruneLock(await Bun.file(src).text(), scope))
|
|
276
|
-
} catch (err) {
|
|
277
|
-
throw new UserError(`vx prune: ${err instanceof Error ? err.message : String(err)}`)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const workspaceYaml = path.join(root, 'pnpm-workspace.yaml')
|
|
281
|
-
if (await exists(workspaceYaml)) {
|
|
282
|
-
written.set(
|
|
283
|
-
'pnpm-workspace.yaml',
|
|
284
|
-
rewriteWorkspaceYaml(await Bun.file(workspaceYaml).text(), patterns),
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
// A manifest with no `workspaces` (pnpm's) is copied byte for byte
|
|
288
|
-
// unless production rewrote it.
|
|
289
|
-
const rootOut = rewritten.get('.') ?? (listed !== undefined ? rootManifest : undefined)
|
|
290
|
-
if (rootOut !== undefined) written.set('package.json', `${JSON.stringify(rootOut, null, 2)}\n`)
|
|
291
|
-
const installFiles = [
|
|
292
|
-
...(rootOut === undefined ? ['package.json'] : []),
|
|
293
|
-
...INSTALL_FILES,
|
|
294
|
-
...(await namedFiles(root, rootManifest)),
|
|
295
|
-
]
|
|
296
|
-
|
|
297
|
-
const emit = async (dest: string, extra: readonly string[]): Promise<void> => {
|
|
298
|
-
await mkdir(dest, { recursive: true })
|
|
299
|
-
for (const [file, text] of written) await writeFile(path.join(dest, file), text)
|
|
300
|
-
for (const f of [...installFiles, ...extra]) {
|
|
301
|
-
const src = path.join(root, f)
|
|
302
|
-
if (!(await exists(src))) continue
|
|
303
|
-
await mkdir(path.dirname(path.join(dest, f)), { recursive: true })
|
|
304
|
-
await cp(src, path.join(dest, f), { recursive: true })
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const full = args.docker ? path.join(out, 'full') : out
|
|
308
|
-
await emit(full, VX_FILES)
|
|
309
|
-
for (const [i, p] of subset.entries()) {
|
|
310
|
-
await cp(p.dir, path.join(full, rels[i]!), {
|
|
311
|
-
recursive: true,
|
|
312
|
-
filter: (src) => !COPY_EXCLUDES.has(path.basename(src)),
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
const manifestText = (rel: string): string | undefined => {
|
|
316
|
-
const pkg = rel === '.' ? undefined : rewritten.get(rel)
|
|
317
|
-
return pkg === undefined ? undefined : `${JSON.stringify(pkg, null, 2)}\n`
|
|
318
|
-
}
|
|
319
|
-
for (const rel of rels) {
|
|
320
|
-
const text = manifestText(rel)
|
|
321
|
-
if (text !== undefined) await writeFile(path.join(full, rel, 'package.json'), text)
|
|
322
|
-
}
|
|
323
|
-
if (args.docker) {
|
|
324
|
-
const json = path.join(out, 'json')
|
|
325
|
-
await emit(json, [])
|
|
326
|
-
for (const rel of rels) {
|
|
327
|
-
await mkdir(path.join(json, rel), { recursive: true })
|
|
328
|
-
const text = manifestText(rel)
|
|
329
|
-
if (text !== undefined) await writeFile(path.join(json, rel, 'package.json'), text)
|
|
330
|
-
else await cp(path.join(root, rel, 'package.json'), path.join(json, rel, 'package.json'))
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const lockfiles = Object.keys(LOCKFILES).filter((f) => written.has(f))
|
|
335
|
-
process.stdout.write(
|
|
336
|
-
`vx prune: ${subset.length} project${subset.length === 1 ? '' : 's'} → ${args.outDir}` +
|
|
337
|
-
`${args.docker ? ' (json/ + full/)' : ''}` +
|
|
338
|
-
`${args.production ? ' (production)' : ''}` +
|
|
339
|
-
`${lockfiles.length > 0 ? `, ${lockfiles.join(', ')} pruned` : ''}\n` +
|
|
340
|
-
subset.map((p, i) => ` ${p.name} (${rels[i]})\n`).join(''),
|
|
341
|
-
)
|
|
342
|
-
for (const e of escapes) process.stderr.write(`vx prune: warning: ${e}\n`)
|
|
343
|
-
return 0
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* A dir as a workspace pattern that matches only itself: every package
|
|
348
|
-
* manager and vx read the list as globs, and `packages/a[1]` matched
|
|
349
|
-
* `packages/a1`, so the pruned copy had no such member. A class of one
|
|
350
|
-
* (`[[]`) is the escape bun, npm, pnpm, yarn and `Bun.Glob` all read;
|
|
351
|
-
* a backslash is not (bun found no workspace).
|
|
352
|
-
*/
|
|
353
|
-
function literalGlob(rel: string): string {
|
|
354
|
-
return rel.replace(/[*?[{]/g, '[$&]')
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** `packages:` cut to the subset's member patterns; every other key as written. */
|
|
358
|
-
function rewriteWorkspaceYaml(text: string, members: readonly string[]): string {
|
|
359
|
-
const lines = text.split('\n')
|
|
360
|
-
const list = ['packages:', ...members.map((r) => ` - ${JSON.stringify(r)}`)]
|
|
361
|
-
const at = lines.findIndex((l) => /^packages\s*:/.test(l))
|
|
362
|
-
if (at === -1) return [...list, ...lines].join('\n')
|
|
363
|
-
let end = at + 1
|
|
364
|
-
if (/^packages\s*:\s*$/.test(lines[at]!)) {
|
|
365
|
-
while (end < lines.length && (lines[end] === '' || /^[\s-]/.test(lines[end]!))) end++
|
|
366
|
-
while (end > at + 1 && lines[end - 1]!.trim() === '') end--
|
|
367
|
-
}
|
|
368
|
-
return [...lines.slice(0, at), ...list, ...lines.slice(end)].join('\n')
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Root files the install config names: patch files (bun's and pnpm's
|
|
373
|
-
* `patchedDependencies`, in the manifest or pnpm-workspace.yaml) and
|
|
374
|
-
* Yarn's `yarnPath` and plugins. A path outside the root is not the
|
|
375
|
-
* workspace's to copy.
|
|
376
|
-
*/
|
|
377
|
-
async function namedFiles(root: string, manifest: Record<string, unknown>): Promise<string[]> {
|
|
378
|
-
const values = (v: unknown): unknown[] =>
|
|
379
|
-
v !== null && typeof v === 'object' ? Object.values(v as Record<string, unknown>) : []
|
|
380
|
-
const named: unknown[] = [
|
|
381
|
-
...values(manifest['patchedDependencies']),
|
|
382
|
-
...values((manifest['pnpm'] as Record<string, unknown> | undefined)?.['patchedDependencies']),
|
|
383
|
-
]
|
|
384
|
-
const yaml = async (file: string): Promise<Record<string, unknown>> => {
|
|
385
|
-
const p = path.join(root, file)
|
|
386
|
-
if (!(await exists(p))) return {}
|
|
387
|
-
const doc: unknown = Bun.YAML.parse(await Bun.file(p).text())
|
|
388
|
-
return record(doc) ?? {}
|
|
389
|
-
}
|
|
390
|
-
named.push(...values((await yaml('pnpm-workspace.yaml'))['patchedDependencies']))
|
|
391
|
-
const yarnrc = await yaml('.yarnrc.yml')
|
|
392
|
-
named.push(yarnrc['yarnPath'])
|
|
393
|
-
for (const p of Array.isArray(yarnrc['plugins']) ? yarnrc['plugins'] : []) {
|
|
394
|
-
named.push(typeof p === 'string' ? p : (p as Record<string, unknown> | null)?.['path'])
|
|
395
|
-
}
|
|
396
|
-
return named.filter(
|
|
397
|
-
(f): f is string =>
|
|
398
|
-
typeof f === 'string' && !path.isAbsolute(f) && !path.normalize(f).startsWith('..'),
|
|
399
|
-
)
|
|
400
|
-
}
|
package/src/scope.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** What a lockfile is pruned to. Dirs are workspace-relative, POSIX, `.` the root. */
|
|
2
|
-
export interface PruneScope {
|
|
3
|
-
/** The workspaces kept: the root and every pruned project. */
|
|
4
|
-
readonly dirs: ReadonlySet<string>
|
|
5
|
-
/** Every workspace project, kept or not: a path the lockfile reaches that is one of these and not kept is a broken subset. */
|
|
6
|
-
readonly members: ReadonlySet<string>
|
|
7
|
-
/** The root manifest's rewritten `workspaces` list. */
|
|
8
|
-
readonly workspaces: readonly string[]
|
|
9
|
-
/** Each kept dir's manifest dependencies (every bucket): yarn classic records no workspaces, so its walk starts here. */
|
|
10
|
-
readonly manifests: ReadonlyMap<string, ReadonlyMap<string, string>>
|
|
11
|
-
/**
|
|
12
|
-
* Workspace package names `--production` struck from the kept manifests'
|
|
13
|
-
* devDependencies: the lockfile's kept workspaces stop naming them too.
|
|
14
|
-
* Empty otherwise.
|
|
15
|
-
*/
|
|
16
|
-
readonly dropped: ReadonlySet<string>
|
|
17
|
-
}
|