@vzn/vx-lockfile 0.0.646 → 0.0.649
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 +32 -28
- package/package.json +3 -3
- package/src/bun.ts +49 -20
- package/src/index.ts +353 -65
- package/src/npm.ts +53 -29
- package/src/pnpm-settings.ts +50 -5
- package/src/pnpm.ts +120 -20
- package/src/syml.ts +91 -0
- package/src/yarn-settings.ts +26 -0
- package/src/yarn.ts +193 -16
package/src/yarn.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { reachDigests, type LockfileImporter } from '@vzn/vx'
|
|
15
15
|
import { depsOf, record, stable } from './json.js'
|
|
16
16
|
import { joinPosix } from './posix.js'
|
|
17
|
+
import { parseSyml } from './syml.js'
|
|
17
18
|
|
|
18
19
|
export interface Lockfile {
|
|
19
20
|
readonly generation: 'berry' | 'classic'
|
|
@@ -40,6 +41,9 @@ export interface Lockfile {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export interface Entry {
|
|
44
|
+
/** The package's own name, from the resolution (berry; '' for classic) */
|
|
45
|
+
readonly name: string
|
|
46
|
+
readonly version: string
|
|
43
47
|
readonly deps: ReadonlyMap<string, string>
|
|
44
48
|
/** `dependencies` alone, which a workspace entry merges its devDependencies into (berry only) */
|
|
45
49
|
readonly installed: ReadonlyMap<string, string>
|
|
@@ -57,9 +61,9 @@ export function parseLockfile(text: string): Lockfile {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function parseBerry(text: string): Lockfile {
|
|
60
|
-
let doc: unknown
|
|
64
|
+
let doc: unknown = parseSyml(text)
|
|
61
65
|
try {
|
|
62
|
-
doc
|
|
66
|
+
doc ??= Bun.YAML.parse(text)
|
|
63
67
|
} catch (err) {
|
|
64
68
|
throw new Error(`yarn.lock: ${err instanceof Error ? err.message : String(err)}`)
|
|
65
69
|
}
|
|
@@ -74,15 +78,16 @@ function parseBerry(text: string): Lockfile {
|
|
|
74
78
|
if (keys === '__metadata') continue
|
|
75
79
|
const e = record(raw) ?? {}
|
|
76
80
|
const resolution = typeof e['resolution'] === 'string' ? e['resolution'] : keys
|
|
77
|
-
// Every field of the entry
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// `
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
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
|
|
85
88
|
entries.set(resolution, {
|
|
89
|
+
name: resolution.slice(0, resolution.indexOf('@', 1)),
|
|
90
|
+
version: typeof e['version'] === 'string' ? e['version'] : '',
|
|
86
91
|
deps: depsOf(e, ['dependencies', 'peerDependencies']),
|
|
87
92
|
installed: depsOf(e, ['dependencies']),
|
|
88
93
|
resolution: `${resolution}\0${stable(material)}`,
|
|
@@ -101,6 +106,7 @@ function parseBerry(text: string): Lockfile {
|
|
|
101
106
|
} catch {
|
|
102
107
|
inner = patch[1]!
|
|
103
108
|
}
|
|
109
|
+
inner = npmDefault(inner)
|
|
104
110
|
const ids = patches.get(inner)
|
|
105
111
|
if (ids === undefined) patches.set(inner, [resolution])
|
|
106
112
|
else if (!ids.includes(resolution)) ids.push(resolution)
|
|
@@ -125,6 +131,15 @@ function parseBerry(text: string): Lockfile {
|
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
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
|
+
|
|
128
143
|
/** The classic format: an unquoted-YAML dialect with `key value` lines. */
|
|
129
144
|
function parseClassic(text: string): Lockfile {
|
|
130
145
|
const entries = new Map<string, Entry>()
|
|
@@ -143,6 +158,8 @@ function parseClassic(text: string): Lockfile {
|
|
|
143
158
|
// the digest did not move (item 902). Classic has no workspace entries
|
|
144
159
|
// to carry that edge, so each entry carries its own descriptors.
|
|
145
160
|
entries.set(id, {
|
|
161
|
+
name: '',
|
|
162
|
+
version: fields['version'] ?? '',
|
|
146
163
|
deps,
|
|
147
164
|
installed: deps,
|
|
148
165
|
resolution: `${fields['version'] ?? ''}\0${fields['resolved'] ?? ''}\0${fields['integrity'] ?? ''}\0${[...keys].sort().join(',')}`,
|
|
@@ -203,7 +220,8 @@ function unquote(s: string): string {
|
|
|
203
220
|
* workspace's manifest (`manifests`: importer dir → `package.json`, `.`
|
|
204
221
|
* the root). A dependency on a workspace's name is linked unless the file
|
|
205
222
|
* keys its descriptor, which Yarn writes only for the registry copy it
|
|
206
|
-
* 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
|
|
207
225
|
* workspace with no installed entry of that name is a peer link.
|
|
208
226
|
*/
|
|
209
227
|
export function classicLinks(
|
|
@@ -220,6 +238,7 @@ export function classicLinks(
|
|
|
220
238
|
}
|
|
221
239
|
const workspace = (name: string): string | undefined =>
|
|
222
240
|
count.get(name) === 1 ? dirOf.get(name) : undefined
|
|
241
|
+
const forced = everywhereResolutions(manifests.get('.'))
|
|
223
242
|
const out = new Map<string, LockfileImporter>()
|
|
224
243
|
for (const [dir, pkg] of manifests) {
|
|
225
244
|
const links = { deps: {} as Record<string, string>, peers: {} as Record<string, string> }
|
|
@@ -228,7 +247,20 @@ export function classicLinks(
|
|
|
228
247
|
for (const [name, range] of depsOf(pkg, [field])) {
|
|
229
248
|
installed.add(name)
|
|
230
249
|
const target = workspace(name)
|
|
231
|
-
|
|
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}`)) {
|
|
232
264
|
links.deps[name] = target
|
|
233
265
|
}
|
|
234
266
|
}
|
|
@@ -242,6 +274,23 @@ export function classicLinks(
|
|
|
242
274
|
return out
|
|
243
275
|
}
|
|
244
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
|
+
|
|
245
294
|
/**
|
|
246
295
|
* `bar` + `npm:^2` → the entry keyed `bar@npm:^2`; classic: `bar@^2`. A
|
|
247
296
|
* bare range gets berry's `npm:` prefix. A `workspace:` range resolves to
|
|
@@ -265,7 +314,7 @@ function resolveDescriptor(
|
|
|
265
314
|
// A patched descriptor reaches the patched entries Yarn installs as well
|
|
266
315
|
// as the plain one it keys (item 1074).
|
|
267
316
|
const withPatch = (descriptor: string, id: string): readonly string[] => {
|
|
268
|
-
const patched = lock.patches.get(descriptor)
|
|
317
|
+
const patched = lock.patches.get(npmDefault(descriptor))
|
|
269
318
|
return patched === undefined ? [id] : [id, ...patched.filter((p) => p !== id)]
|
|
270
319
|
}
|
|
271
320
|
const direct = lock.descriptors.get(`${name}@${range}`)
|
|
@@ -391,9 +440,81 @@ function contentDirs(lock: Lockfile): Map<string, string> {
|
|
|
391
440
|
return out
|
|
392
441
|
}
|
|
393
442
|
|
|
394
|
-
/**
|
|
395
|
-
|
|
396
|
-
|
|
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
|
+
}
|
|
397
518
|
}
|
|
398
519
|
|
|
399
520
|
/** The entry a descriptor the file keys resolves to: as written, bound to `from`, or with berry's `npm:` prefix. */
|
|
@@ -410,6 +531,48 @@ function keyedDescriptor(
|
|
|
410
531
|
)
|
|
411
532
|
}
|
|
412
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
|
+
|
|
413
576
|
/**
|
|
414
577
|
* Every workspace's digest. Berry: one node per entry, workspaces among
|
|
415
578
|
* them, keyed by dir. Classic: the file has no workspace entries, so the
|
|
@@ -419,6 +582,7 @@ function keyedDescriptor(
|
|
|
419
582
|
export function importerDigests(
|
|
420
583
|
lock: Lockfile,
|
|
421
584
|
files: ReadonlyMap<string, string> = new Map(),
|
|
585
|
+
extensions: readonly Extension[] = BUILTIN,
|
|
422
586
|
): ReadonlyMap<string, string> {
|
|
423
587
|
const index = new Map<string, number>()
|
|
424
588
|
const material: string[] = []
|
|
@@ -440,6 +604,19 @@ export function importerDigests(
|
|
|
440
604
|
if (targets.length === 0) material[from] += `\nunresolved\0${name}\0${range}`
|
|
441
605
|
}
|
|
442
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
|
+
}
|
|
443
620
|
const digests = reachDigests({ material, edges })
|
|
444
621
|
// The global digest rides as DATA: Bun's xxHash3 reads only the low 32
|
|
445
622
|
// bits of a seed, so two lockfiles' globals could share one (item 682).
|