@vzn/vx-lockfile 0.0.636 → 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/pnpm.ts
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Bun.YAML is the parser: no dependency, and the file is plain YAML.
|
|
13
13
|
|
|
14
|
-
import { reachDigests } from '@vzn/vx'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import type { PruneScope } from './scope.js'
|
|
14
|
+
import { reachDigests, type LockfileImporter } from '@vzn/vx'
|
|
15
|
+
import { record } from './json.js'
|
|
16
|
+
import { joinPosix } from './posix.js'
|
|
18
17
|
|
|
19
18
|
export interface Lockfile {
|
|
20
19
|
readonly version: string
|
|
@@ -257,7 +256,8 @@ function buildGraph(lock: Lockfile): Graph {
|
|
|
257
256
|
const link = (from: number, dir: string, name: string, version: string): void => {
|
|
258
257
|
if (version.startsWith('link:')) {
|
|
259
258
|
const target = joinPosix(dir, version.slice('link:'.length))
|
|
260
|
-
if (lock.importers.has(target))
|
|
259
|
+
if (target !== undefined && lock.importers.has(target))
|
|
260
|
+
edges[from]!.push(index.get(importerNode(target))!)
|
|
261
261
|
else material[from] += `\nlink\0${name}\0${version}`
|
|
262
262
|
return
|
|
263
263
|
}
|
|
@@ -278,113 +278,23 @@ function buildGraph(lock: Lockfile): Graph {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
281
|
+
* Each importer's `link:` dependencies on other importers. pnpm records
|
|
282
|
+
* an auto-installed peer (`autoInstallPeers`, the default) under
|
|
283
|
+
* `dependencies` like any other, so `peers` is empty here: the plugin
|
|
284
|
+
* asks the importer's manifest which of these are peers.
|
|
285
285
|
*/
|
|
286
|
-
export function
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
// What production struck from each kept importer's manifest.
|
|
291
|
-
const skip = new Map<string, Set<string>>()
|
|
292
|
-
if (dropped.size > 0) {
|
|
293
|
-
const docs: unknown = Bun.YAML.parse(text)
|
|
294
|
-
const doc = record(Array.isArray(docs) ? docs.at(-1) : docs)
|
|
295
|
-
for (const [dir, entry] of Object.entries(record(doc?.['importers']) ?? {})) {
|
|
296
|
-
if (dirs.has(dir)) skip.set(dir, devOnly(record(entry), dropped))
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const reached = new Set<string>()
|
|
300
|
-
const visit = (dir: string, deps: ReadonlyMap<string, string>, importer = false): void => {
|
|
286
|
+
export function importerLinks(lock: Lockfile): ReadonlyMap<string, LockfileImporter> {
|
|
287
|
+
const out = new Map<string, LockfileImporter>()
|
|
288
|
+
for (const [dir, deps] of lock.importers) {
|
|
289
|
+
const links: Record<string, string> = {}
|
|
301
290
|
for (const [name, version] of deps) {
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
throw new Error(`pnpm-lock.yaml: ${dir} links ${target}, which the subset leaves out`)
|
|
307
|
-
}
|
|
308
|
-
continue
|
|
309
|
-
}
|
|
310
|
-
const key = snapshotKey(lock, name, version)
|
|
311
|
-
if (reached.has(key)) continue
|
|
312
|
-
reached.add(key)
|
|
313
|
-
visit('.', lock.snapshots.get(key) ?? new Map())
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
for (const [dir, deps] of lock.importers) if (dirs.has(dir)) visit(dir, deps, true)
|
|
317
|
-
const v9 = Number.parseInt(lock.version, 10) >= 9
|
|
318
|
-
const packages = new Set([...reached].map((key) => (v9 ? packageKey(key) : key)))
|
|
319
|
-
return (
|
|
320
|
-
text.slice(0, cut) +
|
|
321
|
-
dropImporterDev(
|
|
322
|
-
pruneSections(text.slice(cut), {
|
|
323
|
-
importers: (dir) => dirs.has(dir),
|
|
324
|
-
packages: (key) => packages.has(key),
|
|
325
|
-
snapshots: (key) => reached.has(key),
|
|
326
|
-
}),
|
|
327
|
-
skip,
|
|
328
|
-
)
|
|
329
|
-
)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Each importer's `devDependencies` without the `dropped` names, cut as
|
|
334
|
-
* lines like every other prune: the subsection goes when it empties, and
|
|
335
|
-
* an importer left with nothing is `{}`, as pnpm writes one.
|
|
336
|
-
*/
|
|
337
|
-
function dropImporterDev(text: string, skip: ReadonlyMap<string, ReadonlySet<string>>): string {
|
|
338
|
-
if (![...skip.values()].some((s) => s.size > 0)) return text
|
|
339
|
-
const lines = text.split('\n')
|
|
340
|
-
const start = lines.findIndex((l) => /^importers:\s*$/.test(l))
|
|
341
|
-
if (start === -1) return text
|
|
342
|
-
let end = start + 1
|
|
343
|
-
while (end < lines.length && !/^\S/.test(lines[end]!)) end++
|
|
344
|
-
const indented = (l: string, n: number): boolean =>
|
|
345
|
-
l.startsWith(' '.repeat(n)) && /^[^\s#]/.test(l.slice(n))
|
|
346
|
-
const out = lines.slice(0, start + 1)
|
|
347
|
-
let i = start + 1
|
|
348
|
-
while (i < end) {
|
|
349
|
-
if (!indented(lines[i]!, 2)) {
|
|
350
|
-
out.push(lines[i++]!)
|
|
351
|
-
continue
|
|
352
|
-
}
|
|
353
|
-
const key = i
|
|
354
|
-
const dropped = skip.get(yamlKey(lines[key]!)) ?? new Set<string>()
|
|
355
|
-
let stop = i + 1
|
|
356
|
-
while (stop < end && !indented(lines[stop]!, 2)) stop++
|
|
357
|
-
const body: string[] = []
|
|
358
|
-
for (let j = key + 1; j < stop;) {
|
|
359
|
-
if (lines[j] !== ' devDependencies:') {
|
|
360
|
-
body.push(lines[j++]!)
|
|
361
|
-
continue
|
|
291
|
+
if (!version.startsWith('link:')) continue
|
|
292
|
+
const target = joinPosix(dir, version.slice('link:'.length))
|
|
293
|
+
if (target !== undefined && target !== dir && lock.importers.has(target)) {
|
|
294
|
+
links[name] = target
|
|
362
295
|
}
|
|
363
|
-
let sub = j + 1
|
|
364
|
-
while (sub < stop && (lines[sub]!.trim() === '' || /^ {5,}/.test(lines[sub]!))) sub++
|
|
365
|
-
const kept = pruneEntries(lines, j + 1, sub, 6, (l) => !dropped.has(yamlKey(l)))
|
|
366
|
-
if (kept.some((l) => indented(l, 6))) body.push(lines[j]!, ...kept)
|
|
367
|
-
else body.push(...kept.filter((l) => l.trim() === ''))
|
|
368
|
-
j = sub
|
|
369
296
|
}
|
|
370
|
-
|
|
371
|
-
const entry = !emptied
|
|
372
|
-
? [lines[key]!, ...body]
|
|
373
|
-
: [`${lines[key]!.replace(/:\s*$/, '')}: {}`, ...body.filter((l) => l.trim() === '')]
|
|
374
|
-
out.push(...entry)
|
|
375
|
-
i = stop
|
|
297
|
+
out.set(dir, { deps: links, peers: {} })
|
|
376
298
|
}
|
|
377
|
-
out
|
|
378
|
-
return out.join('\n')
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** `packages/a` + `../b` → `packages/b`; `.` + `packages/a` → `packages/a`. POSIX, as the lockfile writes paths. */
|
|
382
|
-
function joinPosix(base: string, rel: string): string {
|
|
383
|
-
const parts: string[] = []
|
|
384
|
-
for (const seg of `${base}/${rel}`.split('/')) {
|
|
385
|
-
if (seg === '' || seg === '.') continue
|
|
386
|
-
if (seg === '..') parts.pop()
|
|
387
|
-
else parts.push(seg)
|
|
388
|
-
}
|
|
389
|
-
return parts.length === 0 ? '.' : parts.join('/')
|
|
299
|
+
return out
|
|
390
300
|
}
|
package/src/posix.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `packages/a` + `../b` → `packages/b`; `.` + `packages/a` → `packages/a`.
|
|
3
|
+
* POSIX, as lockfiles write paths. Undefined for a path above the root: it
|
|
4
|
+
* dropped each `..` past it, so `link:../ext` from the root named `ext`.
|
|
5
|
+
*/
|
|
6
|
+
export function joinPosix(base: string, rel: string): string | undefined {
|
|
7
|
+
const parts: string[] = []
|
|
8
|
+
for (const seg of `${base}/${rel}`.split('/')) {
|
|
9
|
+
if (seg === '' || seg === '.') continue
|
|
10
|
+
if (seg !== '..') parts.push(seg)
|
|
11
|
+
else if (parts.pop() === undefined) return undefined
|
|
12
|
+
}
|
|
13
|
+
return parts.length === 0 ? '.' : parts.join('/')
|
|
14
|
+
}
|
package/src/yarn.ts
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
// lockfile's root descriptors only, so a workspace's digest is the root's
|
|
12
12
|
// reach through the descriptors the file resolves.
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import { reachDigests } from '@vzn/vx'
|
|
14
|
+
import { reachDigests, type LockfileImporter } from '@vzn/vx'
|
|
16
15
|
import { stable } from './pnpm.js'
|
|
17
|
-
import { pruneEntries, yamlKey } from './blocks.js'
|
|
18
16
|
import { depsOf, record } from './json.js'
|
|
19
|
-
import
|
|
17
|
+
import { joinPosix } from './posix.js'
|
|
18
|
+
|
|
19
|
+
/** What yarn() warns of a classic lockfile, after its name: the static part a test holds. */
|
|
20
|
+
export const CLASSIC_EDGES =
|
|
21
|
+
': yarn classic (v1) records no workspaces, so yarn() gives vx no project edges — upgrade to Yarn 2+ (`yarn set version stable`)'
|
|
20
22
|
|
|
21
23
|
export interface Lockfile {
|
|
22
24
|
readonly generation: 'berry' | 'classic'
|
|
@@ -40,8 +42,8 @@ export interface Lockfile {
|
|
|
40
42
|
|
|
41
43
|
export interface Entry {
|
|
42
44
|
readonly deps: ReadonlyMap<string, string>
|
|
43
|
-
/**
|
|
44
|
-
readonly
|
|
45
|
+
/** `dependencies` alone, which a workspace entry merges its devDependencies into (berry only) */
|
|
46
|
+
readonly installed: ReadonlyMap<string, string>
|
|
45
47
|
/** resolution + checksum / version + resolved + integrity */
|
|
46
48
|
readonly resolution: string
|
|
47
49
|
}
|
|
@@ -83,7 +85,7 @@ function parseBerry(text: string): Lockfile {
|
|
|
83
85
|
)
|
|
84
86
|
entries.set(resolution, {
|
|
85
87
|
deps: depsOf(e, ['dependencies', 'peerDependencies']),
|
|
86
|
-
|
|
88
|
+
installed: depsOf(e, ['dependencies']),
|
|
87
89
|
resolution: `${resolution}\0${stable(material)}`,
|
|
88
90
|
})
|
|
89
91
|
// Yarn joins a key's descriptors with ', '; a range's own trailing
|
|
@@ -141,7 +143,7 @@ function parseClassic(text: string): Lockfile {
|
|
|
141
143
|
// to carry that edge, so each entry carries its own descriptors.
|
|
142
144
|
entries.set(id, {
|
|
143
145
|
deps,
|
|
144
|
-
|
|
146
|
+
installed: deps,
|
|
145
147
|
resolution: `${fields['version'] ?? ''}\0${fields['resolved'] ?? ''}\0${fields['integrity'] ?? ''}\0${[...keys].sort().join(',')}`,
|
|
146
148
|
})
|
|
147
149
|
for (const k of keys) descriptors.set(k, id)
|
|
@@ -247,6 +249,88 @@ function resolveDescriptor(
|
|
|
247
249
|
return lock.names.get(name) ?? []
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Each berry workspace's dependencies whose descriptor the file resolves
|
|
254
|
+
* to a workspace entry (a `workspace:` range, or a range Yarn's
|
|
255
|
+
* transparent workspaces satisfied with one), and its peers on a
|
|
256
|
+
* workspace of that name, which Yarn hoists whatever the range. Only a
|
|
257
|
+
* descriptor the file keys counts: the name fallbacks the digest takes
|
|
258
|
+
* (a catalog, an override) name no link. Classic records no workspaces,
|
|
259
|
+
* so it has none.
|
|
260
|
+
*/
|
|
261
|
+
export function importerLinks(lock: Lockfile): ReadonlyMap<string, LockfileImporter> {
|
|
262
|
+
const out = new Map<string, LockfileImporter>()
|
|
263
|
+
const dirOf = new Map<string, string>()
|
|
264
|
+
const byName = new Map<string, string>()
|
|
265
|
+
for (const [dir, id] of lock.workspaces) {
|
|
266
|
+
dirOf.set(id, dir)
|
|
267
|
+
byName.set(id.slice(0, id.indexOf('@workspace:')), dir)
|
|
268
|
+
}
|
|
269
|
+
for (const [dir, id] of lock.workspaces) {
|
|
270
|
+
const entry = lock.entries.get(id)!
|
|
271
|
+
const links = { deps: {} as Record<string, string>, peers: {} as Record<string, string> }
|
|
272
|
+
const add = (kind: 'deps' | 'peers', name: string, target: string | undefined): void => {
|
|
273
|
+
if (target !== undefined && target !== dir) links[kind][name] = target
|
|
274
|
+
}
|
|
275
|
+
for (const [name, range] of entry.installed) {
|
|
276
|
+
const keyed = keyedDescriptor(lock, id, name, range)
|
|
277
|
+
add(
|
|
278
|
+
'deps',
|
|
279
|
+
name,
|
|
280
|
+
keyed === undefined
|
|
281
|
+
? range.startsWith('workspace:')
|
|
282
|
+
? byName.get(name)
|
|
283
|
+
: undefined
|
|
284
|
+
: (dirOf.get(keyed) ?? linkedDir(lock, dirOf, keyed)),
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
for (const name of entry.deps.keys()) {
|
|
288
|
+
if (!entry.installed.has(name)) add('peers', name, byName.get(name))
|
|
289
|
+
}
|
|
290
|
+
out.set(dir, links)
|
|
291
|
+
}
|
|
292
|
+
return out
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The workspace a `link:` or `portal:` entry symlinks, from its resolution
|
|
297
|
+
* (`b@link:../b::locator=a%40workspace%3Apackages%2Fa`): the path, from the
|
|
298
|
+
* locating workspace's dir. A `file:` entry is a copy packed at install,
|
|
299
|
+
* no link to the workspace's build.
|
|
300
|
+
*/
|
|
301
|
+
function linkedDir(
|
|
302
|
+
lock: Lockfile,
|
|
303
|
+
dirOf: ReadonlyMap<string, string>,
|
|
304
|
+
id: string,
|
|
305
|
+
): string | undefined {
|
|
306
|
+
const m = /^(?:@[^@/]+\/)?[^@]+@(?:link|portal):([^:]*)::locator=([^&]+)/.exec(id)
|
|
307
|
+
if (m === null) return undefined
|
|
308
|
+
let locator: string
|
|
309
|
+
try {
|
|
310
|
+
locator = decodeURIComponent(m[2]!)
|
|
311
|
+
} catch {
|
|
312
|
+
return undefined
|
|
313
|
+
}
|
|
314
|
+
const from = dirOf.get(locator)
|
|
315
|
+
if (from === undefined) return undefined
|
|
316
|
+
const target = joinPosix(from, m[1]!)
|
|
317
|
+
return target !== undefined && lock.workspaces.has(target) ? target : undefined
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The entry a descriptor the file keys resolves to: as written, bound to `from`, or with berry's `npm:` prefix. */
|
|
321
|
+
function keyedDescriptor(
|
|
322
|
+
lock: Lockfile,
|
|
323
|
+
from: string,
|
|
324
|
+
name: string,
|
|
325
|
+
range: string,
|
|
326
|
+
): string | undefined {
|
|
327
|
+
return (
|
|
328
|
+
lock.descriptors.get(`${name}@${range}`) ??
|
|
329
|
+
lock.descriptors.get(`${name}@${range}::locator=${encodeURIComponent(from)}`) ??
|
|
330
|
+
(range.includes(':') ? undefined : lock.descriptors.get(`${name}@npm:${range}`))
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
250
334
|
/**
|
|
251
335
|
* Every workspace's digest. Berry: one node per entry, workspaces among
|
|
252
336
|
* them, keyed by dir. Classic: the file has no workspace entries, so the
|
|
@@ -284,88 +368,3 @@ export function importerDigests(lock: Lockfile): ReadonlyMap<string, string> {
|
|
|
284
368
|
for (const [dir, id] of lock.workspaces) out.set(dir, fold(digests[index.get(id)!]!))
|
|
285
369
|
return out
|
|
286
370
|
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* The lockfile cut to the entries the workspaces at `dirs` install, each
|
|
290
|
-
* entry's text as written. Berry walks from the kept workspace entries;
|
|
291
|
-
* classic records no workspaces, so its walk starts at each kept
|
|
292
|
-
* manifest's dependencies (a workspace sibling has no entry and is
|
|
293
|
-
* skipped).
|
|
294
|
-
*/
|
|
295
|
-
export function pruneLockfile(text: string, scope: PruneScope): string {
|
|
296
|
-
const { dirs, members, manifests, dropped } = scope
|
|
297
|
-
// Berry merges a workspace's dev dependencies into its entry's
|
|
298
|
-
// `dependencies`, so a struck name is told by its link: a dropped
|
|
299
|
-
// workspace package is reached through a `workspace:` range.
|
|
300
|
-
const isDropped = (name: string, range: string): boolean =>
|
|
301
|
-
dropped.has(name) && range.startsWith('workspace:')
|
|
302
|
-
const lock = parseLockfile(text)
|
|
303
|
-
const reached = new Set<string>()
|
|
304
|
-
const visit = (id: string | undefined): void => {
|
|
305
|
-
if (id === undefined || reached.has(id)) return
|
|
306
|
-
const ws = id.indexOf('@workspace:')
|
|
307
|
-
const dir = ws === -1 ? undefined : id.slice(ws + '@workspace:'.length)
|
|
308
|
-
if (dir !== undefined && members.has(dir) && !dirs.has(dir)) {
|
|
309
|
-
throw new Error(`yarn.lock: ${id} is a workspace the subset leaves out`)
|
|
310
|
-
}
|
|
311
|
-
reached.add(id)
|
|
312
|
-
const kept = dir !== undefined && dirs.has(dir)
|
|
313
|
-
for (const [name, range] of lock.entries.get(id)!.installs) {
|
|
314
|
-
if (kept && isDropped(name, range)) continue
|
|
315
|
-
for (const target of resolveDescriptor(lock, id, name, range)) visit(target)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const berry = lock.generation === 'berry'
|
|
319
|
-
// Berry records a workspace's dev dependencies in its entry's
|
|
320
|
-
// `dependencies`; classic records no workspace, and its manifests are
|
|
321
|
-
// already stripped.
|
|
322
|
-
const keptWorkspaces = new Set<string>()
|
|
323
|
-
if (berry) {
|
|
324
|
-
for (const [dir, id] of lock.workspaces) {
|
|
325
|
-
if (!dirs.has(dir)) continue
|
|
326
|
-
keptWorkspaces.add(id)
|
|
327
|
-
visit(id)
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
for (const [dir, deps] of manifests) {
|
|
331
|
-
if (!dirs.has(dir)) continue
|
|
332
|
-
for (const [name, range] of deps) {
|
|
333
|
-
// Classic keys a workspace's `file:` / `link:` path from the root:
|
|
334
|
-
// `file:./vendor/x` in packages/a is `file:./packages/a/vendor/x`.
|
|
335
|
-
const local = /^(file|link):(?!\/)(.*)$/.exec(range)
|
|
336
|
-
const keyed =
|
|
337
|
-
local === null || dir === '.' ? range : `${local[1]}:./${path.posix.join(dir, local[2]!)}`
|
|
338
|
-
visit(lock.descriptors.get(`${name}@${keyed}`))
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
const lines = text.split('\n')
|
|
343
|
-
const first = (line: string): string =>
|
|
344
|
-
berry
|
|
345
|
-
? yamlKey(line).split(',')[0]!.trimStart()
|
|
346
|
-
: unquote(line.replace(/:\s*$/, '').split(',')[0]!.trim())
|
|
347
|
-
const pruned = pruneEntries(lines, 0, lines.length, 0, (line) => {
|
|
348
|
-
if (line.startsWith('__metadata:')) return true
|
|
349
|
-
const id = lock.descriptors.get(first(line))
|
|
350
|
-
return id !== undefined && reached.has(id)
|
|
351
|
-
})
|
|
352
|
-
if (!berry || dropped.size === 0) return pruned.join('\n')
|
|
353
|
-
const out: string[] = []
|
|
354
|
-
let inKept = false
|
|
355
|
-
for (let i = 0; i < pruned.length; i++) {
|
|
356
|
-
const line = pruned[i]!
|
|
357
|
-
if (/^\S/.test(line)) inKept = keptWorkspaces.has(lock.descriptors.get(first(line)) ?? '')
|
|
358
|
-
if (!inKept || line !== ' dependencies:') {
|
|
359
|
-
out.push(line)
|
|
360
|
-
continue
|
|
361
|
-
}
|
|
362
|
-
const deps: string[] = []
|
|
363
|
-
while (i + 1 < pruned.length && pruned[i + 1]!.startsWith(' ')) deps.push(pruned[++i]!)
|
|
364
|
-
const kept = deps.filter((l) => {
|
|
365
|
-
const [name, range] = Object.entries(record(Bun.YAML.parse(l.trim())) ?? {})[0] ?? []
|
|
366
|
-
return name === undefined || !isDropped(name, String(range))
|
|
367
|
-
})
|
|
368
|
-
if (kept.length > 0) out.push(line, ...kept)
|
|
369
|
-
}
|
|
370
|
-
return out.join('\n')
|
|
371
|
-
}
|
package/src/blocks.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
// Line-level pruning for the YAML-shaped lockfiles (pnpm-lock.yaml, yarn.lock
|
|
2
|
-
// berry and classic). Rewriting the parsed document would drop the package
|
|
3
|
-
// manager's own layout — its quoting, its blank lines, its key order — so a
|
|
4
|
-
// pruned file is the original with whole entries cut out, and a pruned
|
|
5
|
-
// lockfile diffs against the source as removals only.
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `lines` with every entry at `indent` spaces between `from` and `to` that
|
|
9
|
-
* `keep` refuses cut out. An entry is its key line and every line after it
|
|
10
|
-
* up to the next key line at that indent; the blank lines separating
|
|
11
|
-
* entries are kept as they were between the survivors, and those after
|
|
12
|
-
* the last entry stay after the last survivor.
|
|
13
|
-
*/
|
|
14
|
-
export function pruneEntries(
|
|
15
|
-
lines: readonly string[],
|
|
16
|
-
from: number,
|
|
17
|
-
to: number,
|
|
18
|
-
indent: number,
|
|
19
|
-
keep: (keyLine: string) => boolean,
|
|
20
|
-
): string[] {
|
|
21
|
-
const pad = ' '.repeat(indent)
|
|
22
|
-
const isKey = (l: string): boolean => l.startsWith(pad) && /^[^\s#]/.test(l.slice(indent))
|
|
23
|
-
const head: string[] = []
|
|
24
|
-
let i = from
|
|
25
|
-
while (i < to && !isKey(lines[i]!)) head.push(lines[i++]!)
|
|
26
|
-
const entries: string[][] = []
|
|
27
|
-
let separator: string[] | undefined
|
|
28
|
-
while (i < to) {
|
|
29
|
-
const entry = [lines[i++]!]
|
|
30
|
-
while (i < to && !isKey(lines[i]!)) entry.push(lines[i++]!)
|
|
31
|
-
entries.push(entry)
|
|
32
|
-
}
|
|
33
|
-
const blanks = (e: string[]): string[] => {
|
|
34
|
-
let n = e.length
|
|
35
|
-
while (n > 1 && e[n - 1]!.trim() === '') n--
|
|
36
|
-
return e.splice(n)
|
|
37
|
-
}
|
|
38
|
-
const tail = entries.length === 0 ? [] : blanks(entries.at(-1)!)
|
|
39
|
-
for (const e of entries.slice(0, -1)) {
|
|
40
|
-
const gap = blanks(e)
|
|
41
|
-
separator ??= gap
|
|
42
|
-
}
|
|
43
|
-
const kept = entries.filter((e) => keep(e[0]!))
|
|
44
|
-
const out = [...head]
|
|
45
|
-
for (const [n, e] of kept.entries()) {
|
|
46
|
-
if (n > 0) out.push(...(separator ?? []))
|
|
47
|
-
out.push(...e)
|
|
48
|
-
}
|
|
49
|
-
out.push(...tail)
|
|
50
|
-
return out
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The key a YAML mapping line opens: ` 'a@1': {}` → `a@1`, ` .:` → `.`. */
|
|
54
|
-
export function yamlKey(line: string): string {
|
|
55
|
-
const parsed: unknown = Bun.YAML.parse(line.trim())
|
|
56
|
-
return parsed !== null && typeof parsed === 'object' ? (Object.keys(parsed)[0] ?? '') : ''
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* `text` with each top-level section named in `sections` pruned to the
|
|
61
|
-
* entries its predicate keeps; every other line as it was.
|
|
62
|
-
*/
|
|
63
|
-
export function pruneSections(
|
|
64
|
-
text: string,
|
|
65
|
-
sections: Readonly<Record<string, (key: string) => boolean>>,
|
|
66
|
-
): string {
|
|
67
|
-
const lines = text.split('\n')
|
|
68
|
-
const out: string[] = []
|
|
69
|
-
let i = 0
|
|
70
|
-
while (i < lines.length) {
|
|
71
|
-
const line = lines[i]!
|
|
72
|
-
out.push(line)
|
|
73
|
-
i++
|
|
74
|
-
const name = /^([A-Za-z]+):\s*$/.exec(line)?.[1]
|
|
75
|
-
const keep = name === undefined ? undefined : sections[name]
|
|
76
|
-
if (keep === undefined) continue
|
|
77
|
-
let end = i
|
|
78
|
-
while (end < lines.length && !/^\S/.test(lines[end]!)) end++
|
|
79
|
-
out.push(...pruneEntries(lines, i, end, 2, (l) => keep(yamlKey(l))))
|
|
80
|
-
i = end
|
|
81
|
-
}
|
|
82
|
-
return out.join('\n')
|
|
83
|
-
}
|