@vzn/vx-lockfile 0.0.645 → 0.0.648
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 +33 -29
- package/package.json +3 -3
- package/src/bun.ts +49 -20
- package/src/index.ts +403 -70
- package/src/json.ts +9 -0
- package/src/npm.ts +53 -29
- package/src/pnpm-settings.ts +71 -0
- package/src/pnpm.ts +135 -38
- package/src/syml.ts +91 -0
- package/src/yarn-settings.ts +26 -0
- package/src/yarn.ts +194 -18
package/src/yarn.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
// one digest is the root's reach through every entry the file resolves.
|
|
13
13
|
|
|
14
14
|
import { reachDigests, type LockfileImporter } from '@vzn/vx'
|
|
15
|
-
import { stable } from './
|
|
16
|
-
import { depsOf, record } from './json.js'
|
|
15
|
+
import { depsOf, record, stable } from './json.js'
|
|
17
16
|
import { joinPosix } from './posix.js'
|
|
17
|
+
import { parseSyml } from './syml.js'
|
|
18
18
|
|
|
19
19
|
export interface Lockfile {
|
|
20
20
|
readonly generation: 'berry' | 'classic'
|
|
@@ -41,6 +41,9 @@ export interface Lockfile {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export interface Entry {
|
|
44
|
+
/** The package's own name, from the resolution (berry; '' for classic) */
|
|
45
|
+
readonly name: string
|
|
46
|
+
readonly version: string
|
|
44
47
|
readonly deps: ReadonlyMap<string, string>
|
|
45
48
|
/** `dependencies` alone, which a workspace entry merges its devDependencies into (berry only) */
|
|
46
49
|
readonly installed: ReadonlyMap<string, string>
|
|
@@ -58,9 +61,9 @@ export function parseLockfile(text: string): Lockfile {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function parseBerry(text: string): Lockfile {
|
|
61
|
-
let doc: unknown
|
|
64
|
+
let doc: unknown = parseSyml(text)
|
|
62
65
|
try {
|
|
63
|
-
doc
|
|
66
|
+
doc ??= Bun.YAML.parse(text)
|
|
64
67
|
} catch (err) {
|
|
65
68
|
throw new Error(`yarn.lock: ${err instanceof Error ? err.message : String(err)}`)
|
|
66
69
|
}
|
|
@@ -75,15 +78,16 @@ function parseBerry(text: string): Lockfile {
|
|
|
75
78
|
if (keys === '__metadata') continue
|
|
76
79
|
const e = record(raw) ?? {}
|
|
77
80
|
const resolution = typeof e['resolution'] === 'string' ? e['resolution'] : keys
|
|
78
|
-
// Every field of the entry
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// `
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
81
|
+
// Every field of the entry, so a field this parser has not heard of
|
|
82
|
+
// moves what reaches the entry: reading the resolution and checksum
|
|
83
|
+
// alone dropped the root workspace's `dependenciesMeta` (`built`,
|
|
84
|
+
// `unplugged`), which governs the whole install (item 933). Its
|
|
85
|
+
// dependency lists too: the edges say where each lands, not under
|
|
86
|
+
// which name, and two aliases swapping targets kept the key.
|
|
87
|
+
const material = e
|
|
86
88
|
entries.set(resolution, {
|
|
89
|
+
name: resolution.slice(0, resolution.indexOf('@', 1)),
|
|
90
|
+
version: typeof e['version'] === 'string' ? e['version'] : '',
|
|
87
91
|
deps: depsOf(e, ['dependencies', 'peerDependencies']),
|
|
88
92
|
installed: depsOf(e, ['dependencies']),
|
|
89
93
|
resolution: `${resolution}\0${stable(material)}`,
|
|
@@ -102,6 +106,7 @@ function parseBerry(text: string): Lockfile {
|
|
|
102
106
|
} catch {
|
|
103
107
|
inner = patch[1]!
|
|
104
108
|
}
|
|
109
|
+
inner = npmDefault(inner)
|
|
105
110
|
const ids = patches.get(inner)
|
|
106
111
|
if (ids === undefined) patches.set(inner, [resolution])
|
|
107
112
|
else if (!ids.includes(resolution)) ids.push(resolution)
|
|
@@ -126,6 +131,15 @@ function parseBerry(text: string): Lockfile {
|
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
// Yarn 2/3 write a patch's inner descriptor with no protocol
|
|
135
|
+
// (`resolve@^1.22.0`) where Yarn 4 writes `resolve@npm:^1.22.0`; npm: is
|
|
136
|
+
// Yarn's default, so both key the same plain descriptor (turborepo#4770).
|
|
137
|
+
function npmDefault(descriptor: string): string {
|
|
138
|
+
const at = descriptor.indexOf('@', 1)
|
|
139
|
+
const range = descriptor.slice(at + 1)
|
|
140
|
+
return range.includes(':') ? descriptor : `${descriptor.slice(0, at)}@npm:${range}`
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
/** The classic format: an unquoted-YAML dialect with `key value` lines. */
|
|
130
144
|
function parseClassic(text: string): Lockfile {
|
|
131
145
|
const entries = new Map<string, Entry>()
|
|
@@ -144,6 +158,8 @@ function parseClassic(text: string): Lockfile {
|
|
|
144
158
|
// the digest did not move (item 902). Classic has no workspace entries
|
|
145
159
|
// to carry that edge, so each entry carries its own descriptors.
|
|
146
160
|
entries.set(id, {
|
|
161
|
+
name: '',
|
|
162
|
+
version: fields['version'] ?? '',
|
|
147
163
|
deps,
|
|
148
164
|
installed: deps,
|
|
149
165
|
resolution: `${fields['version'] ?? ''}\0${fields['resolved'] ?? ''}\0${fields['integrity'] ?? ''}\0${[...keys].sort().join(',')}`,
|
|
@@ -204,7 +220,8 @@ function unquote(s: string): string {
|
|
|
204
220
|
* workspace's manifest (`manifests`: importer dir → `package.json`, `.`
|
|
205
221
|
* the root). A dependency on a workspace's name is linked unless the file
|
|
206
222
|
* keys its descriptor, which Yarn writes only for the registry copy it
|
|
207
|
-
* fetched when the workspace's version misses the range
|
|
223
|
+
* fetched when the workspace's version misses the range, and a `file:`
|
|
224
|
+
* range is a copy, no link. A peer on a
|
|
208
225
|
* workspace with no installed entry of that name is a peer link.
|
|
209
226
|
*/
|
|
210
227
|
export function classicLinks(
|
|
@@ -221,6 +238,7 @@ export function classicLinks(
|
|
|
221
238
|
}
|
|
222
239
|
const workspace = (name: string): string | undefined =>
|
|
223
240
|
count.get(name) === 1 ? dirOf.get(name) : undefined
|
|
241
|
+
const forced = everywhereResolutions(manifests.get('.'))
|
|
224
242
|
const out = new Map<string, LockfileImporter>()
|
|
225
243
|
for (const [dir, pkg] of manifests) {
|
|
226
244
|
const links = { deps: {} as Record<string, string>, peers: {} as Record<string, string> }
|
|
@@ -229,7 +247,20 @@ export function classicLinks(
|
|
|
229
247
|
for (const [name, range] of depsOf(pkg, [field])) {
|
|
230
248
|
installed.add(name)
|
|
231
249
|
const target = workspace(name)
|
|
232
|
-
|
|
250
|
+
// A root resolution rewrites the request before the workspace is
|
|
251
|
+
// asked: Yarn keys the resolution's range, never the manifest's.
|
|
252
|
+
const asked = forced.get(name) ?? range
|
|
253
|
+
// A `file:` range is a copy packed at install, as berry's (`linkedDir`).
|
|
254
|
+
if (asked.startsWith('file:')) continue
|
|
255
|
+
// A `link:` symlinks its directory whatever the name: under another
|
|
256
|
+
// name (`"y": "link:../c"`) the name found no workspace.
|
|
257
|
+
if (asked.startsWith('link:')) {
|
|
258
|
+
const linked = joinPosix(dir, asked.slice('link:'.length))
|
|
259
|
+
if (linked !== undefined && linked !== dir && manifests.has(linked))
|
|
260
|
+
links.deps[name] = linked
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
if (target !== undefined && target !== dir && !lock.descriptors.has(`${name}@${asked}`)) {
|
|
233
264
|
links.deps[name] = target
|
|
234
265
|
}
|
|
235
266
|
}
|
|
@@ -243,6 +274,23 @@ export function classicLinks(
|
|
|
243
274
|
return out
|
|
244
275
|
}
|
|
245
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The root manifest's Yarn 1 `resolutions` that rewrite every request of a
|
|
279
|
+
* name (`ms`, `**\/ms`, `@s/x`), name → range, the first that names it
|
|
280
|
+
* winning as in Yarn. One scoped to a parent (`a/ms`, `a/**\/ms`) is left
|
|
281
|
+
* out: Yarn 1.22 keys its range and still hoists the workspace link over
|
|
282
|
+
* it, so the link stands.
|
|
283
|
+
*/
|
|
284
|
+
function everywhereResolutions(root: Record<string, unknown> | undefined): Map<string, string> {
|
|
285
|
+
const out = new Map<string, string>()
|
|
286
|
+
for (const [pattern, range] of depsOf(root ?? {}, ['resolutions'])) {
|
|
287
|
+
const name = pattern.replace(/^(?:\*\*\/)+/, '')
|
|
288
|
+
if (!/^(?:@[^/]+\/)?[^/@*]+$/.test(name) || out.has(name)) continue
|
|
289
|
+
out.set(name, range)
|
|
290
|
+
}
|
|
291
|
+
return out
|
|
292
|
+
}
|
|
293
|
+
|
|
246
294
|
/**
|
|
247
295
|
* `bar` + `npm:^2` → the entry keyed `bar@npm:^2`; classic: `bar@^2`. A
|
|
248
296
|
* bare range gets berry's `npm:` prefix. A `workspace:` range resolves to
|
|
@@ -266,7 +314,7 @@ function resolveDescriptor(
|
|
|
266
314
|
// A patched descriptor reaches the patched entries Yarn installs as well
|
|
267
315
|
// as the plain one it keys (item 1074).
|
|
268
316
|
const withPatch = (descriptor: string, id: string): readonly string[] => {
|
|
269
|
-
const patched = lock.patches.get(descriptor)
|
|
317
|
+
const patched = lock.patches.get(npmDefault(descriptor))
|
|
270
318
|
return patched === undefined ? [id] : [id, ...patched.filter((p) => p !== id)]
|
|
271
319
|
}
|
|
272
320
|
const direct = lock.descriptors.get(`${name}@${range}`)
|
|
@@ -392,9 +440,81 @@ function contentDirs(lock: Lockfile): Map<string, string> {
|
|
|
392
440
|
return out
|
|
393
441
|
}
|
|
394
442
|
|
|
395
|
-
/**
|
|
396
|
-
|
|
397
|
-
|
|
443
|
+
/**
|
|
444
|
+
* The directories `contentDirs` names, for the claim to hash, but a
|
|
445
|
+
* classic `link:` to a directory `workspace` says is one: Yarn 1 links
|
|
446
|
+
* that workspace as it would by its name. Berry's also `.yarnrc.yml`,
|
|
447
|
+
* whose `packageExtensions` the digest reads: as an extra file it joins
|
|
448
|
+
* the memo's identity, so an extension edit under an unchanged lockfile
|
|
449
|
+
* digests anew rather than replaying the old closure.
|
|
450
|
+
*/
|
|
451
|
+
export function fileDeps(lock: Lockfile, workspace?: (dir: string) => boolean): string[] {
|
|
452
|
+
const out = new Set<string>()
|
|
453
|
+
for (const [id, dir] of contentDirs(lock)) {
|
|
454
|
+
if (workspace !== undefined && /^.[^@]*@link:/.test(id) && workspace(dir)) continue
|
|
455
|
+
out.add(dir)
|
|
456
|
+
}
|
|
457
|
+
const dirs = [...out].sort()
|
|
458
|
+
return lock.generation === 'berry' ? [...dirs, YARNRC] : dirs
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export const YARNRC = '.yarnrc.yml'
|
|
462
|
+
|
|
463
|
+
/** One `packageExtensions` selector (`ident@range`) and the dependencies it adds. */
|
|
464
|
+
export interface Extension {
|
|
465
|
+
readonly name: string
|
|
466
|
+
readonly range: string
|
|
467
|
+
readonly deps: ReadonlyMap<string, string>
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Yarn's builtin list (yarnpkg-extensions) adds one dependency; the rest
|
|
471
|
+
// are peers, which the parent that provides them already reaches.
|
|
472
|
+
const BUILTIN: readonly Extension[] = [
|
|
473
|
+
{ name: '@babel/parser', range: '*', deps: new Map([['@babel/types', '^7.8.3']]) },
|
|
474
|
+
]
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Yarn's builtin extensions and `.yarnrc.yml`'s `packageExtensions`
|
|
478
|
+
* dependencies. A file Yarn itself would refuse adds none: the entries
|
|
479
|
+
* an extension installed are then reached by no one, and every workspace
|
|
480
|
+
* folds them (`importerDigests`).
|
|
481
|
+
*/
|
|
482
|
+
export function packageExtensions(rc: string | undefined): readonly Extension[] {
|
|
483
|
+
if (rc === undefined) return BUILTIN
|
|
484
|
+
let doc: unknown
|
|
485
|
+
try {
|
|
486
|
+
doc = Bun.YAML.parse(rc)
|
|
487
|
+
} catch {
|
|
488
|
+
return BUILTIN
|
|
489
|
+
}
|
|
490
|
+
const out = [...BUILTIN]
|
|
491
|
+
for (const [selector, raw] of Object.entries(record(record(doc)?.['packageExtensions']) ?? {})) {
|
|
492
|
+
const deps = depsOf(record(raw) ?? {}, ['dependencies'])
|
|
493
|
+
if (deps.size === 0) continue
|
|
494
|
+
const at = selector.indexOf('@', 1)
|
|
495
|
+
out.push(
|
|
496
|
+
at === -1
|
|
497
|
+
? { name: selector, range: '*', deps }
|
|
498
|
+
: { name: selector.slice(0, at), range: selector.slice(at + 1), deps },
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
return out
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Whether `version` is what the selector's range extends. Yarn matches
|
|
506
|
+
* prereleases too (`satisfiesWithPrereleases`), and a range this cannot
|
|
507
|
+
* read matches: an edge too many re-keys once too often, never too seldom.
|
|
508
|
+
*/
|
|
509
|
+
function matches(range: string, version: string): boolean {
|
|
510
|
+
const r = range.startsWith('npm:') ? range.slice(4) : range
|
|
511
|
+
try {
|
|
512
|
+
if (Bun.semver.satisfies(version, r)) return true
|
|
513
|
+
const release = version.replace(/[-+].*$/, '')
|
|
514
|
+
return release !== version && Bun.semver.satisfies(release, r)
|
|
515
|
+
} catch {
|
|
516
|
+
return true
|
|
517
|
+
}
|
|
398
518
|
}
|
|
399
519
|
|
|
400
520
|
/** The entry a descriptor the file keys resolves to: as written, bound to `from`, or with berry's `npm:` prefix. */
|
|
@@ -411,6 +531,48 @@ function keyedDescriptor(
|
|
|
411
531
|
)
|
|
412
532
|
}
|
|
413
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Yarn installs a `packageExtensions` dependency for each package its
|
|
536
|
+
* selector matches that does not declare one of that name, and records
|
|
537
|
+
* it on no entry (turborepo#14873): only the added package is keyed, so
|
|
538
|
+
* the extended package gets the edge here. Then every entry no workspace
|
|
539
|
+
* reaches (one an extension vx could not read installed) becomes an edge
|
|
540
|
+
* of every workspace: it is installed for one of them.
|
|
541
|
+
*/
|
|
542
|
+
function extend(
|
|
543
|
+
lock: Lockfile,
|
|
544
|
+
index: ReadonlyMap<string, number>,
|
|
545
|
+
edges: number[][],
|
|
546
|
+
extensions: readonly Extension[],
|
|
547
|
+
): void {
|
|
548
|
+
for (const [id, e] of lock.entries) {
|
|
549
|
+
for (const ext of extensions) {
|
|
550
|
+
if (ext.name !== e.name || !matches(ext.range, e.version)) continue
|
|
551
|
+
for (const [name, range] of ext.deps) {
|
|
552
|
+
if (e.installed.has(name)) continue
|
|
553
|
+
for (const target of resolveDescriptor(lock, id, name, range)) {
|
|
554
|
+
edges[index.get(id)!]!.push(index.get(target)!)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const roots = [...lock.workspaces.values()].map((id) => index.get(id)!)
|
|
560
|
+
const seen = new Uint8Array(edges.length)
|
|
561
|
+
const stack = [...roots]
|
|
562
|
+
for (const r of roots) seen[r] = 1
|
|
563
|
+
while (stack.length > 0) {
|
|
564
|
+
for (const w of edges[stack.pop()!]!) {
|
|
565
|
+
if (seen[w] === 0) {
|
|
566
|
+
seen[w] = 1
|
|
567
|
+
stack.push(w)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const orphans: number[] = []
|
|
572
|
+
for (let i = 0; i < seen.length; i++) if (seen[i] === 0) orphans.push(i)
|
|
573
|
+
if (orphans.length > 0) for (const r of roots) edges[r]!.push(...orphans)
|
|
574
|
+
}
|
|
575
|
+
|
|
414
576
|
/**
|
|
415
577
|
* Every workspace's digest. Berry: one node per entry, workspaces among
|
|
416
578
|
* them, keyed by dir. Classic: the file has no workspace entries, so the
|
|
@@ -420,6 +582,7 @@ function keyedDescriptor(
|
|
|
420
582
|
export function importerDigests(
|
|
421
583
|
lock: Lockfile,
|
|
422
584
|
files: ReadonlyMap<string, string> = new Map(),
|
|
585
|
+
extensions: readonly Extension[] = BUILTIN,
|
|
423
586
|
): ReadonlyMap<string, string> {
|
|
424
587
|
const index = new Map<string, number>()
|
|
425
588
|
const material: string[] = []
|
|
@@ -441,6 +604,19 @@ export function importerDigests(
|
|
|
441
604
|
if (targets.length === 0) material[from] += `\nunresolved\0${name}\0${range}`
|
|
442
605
|
}
|
|
443
606
|
}
|
|
607
|
+
if (lock.generation === 'berry') {
|
|
608
|
+
// A `link:` or `portal:` to a workspace symlinks the workspace itself:
|
|
609
|
+
// its entry lists no dependencies, and as a leaf the linked
|
|
610
|
+
// workspace's bumps moved no key of the project linking it.
|
|
611
|
+
const dirOf = new Map<string, string>()
|
|
612
|
+
for (const [dir, id] of lock.workspaces) dirOf.set(id, dir)
|
|
613
|
+
for (const id of lock.entries.keys()) {
|
|
614
|
+
const target = linkedDir(lock, dirOf, id)
|
|
615
|
+
if (target !== undefined)
|
|
616
|
+
edges[index.get(id)!]!.push(index.get(lock.workspaces.get(target)!)!)
|
|
617
|
+
}
|
|
618
|
+
extend(lock, index, edges, extensions)
|
|
619
|
+
}
|
|
444
620
|
const digests = reachDigests({ material, edges })
|
|
445
621
|
// The global digest rides as DATA: Bun's xxHash3 reads only the low 32
|
|
446
622
|
// bits of a seed, so two lockfiles' globals could share one (item 682).
|