bun-nested-workspace 0.3.2 → 0.5.0

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.
@@ -1,6 +1,197 @@
1
+ import path from "node:path";
1
2
  import JSON5 from "json5";
2
3
  import semver from "semver";
3
- import path from "node:path";
4
+ // ../lockfile/src/selector-ranges.ts
5
+ /** The lowest version: an interval without a lower bound starts there. */
6
+ const lowestVersion = new semver.SemVer("0.0.0-0");
7
+ const zeroVersion = new semver.SemVer("0.0.0");
8
+ /** Remove the leading whitespace that Bun's `trim_literal` removes from a specifier before reading it. */
9
+ function trimSpecifier(specifier) {
10
+ return specifier.replace(/^[ \t\n\r]+/, "");
11
+ }
12
+ /** Prefixes of specifiers that Bun reads as a path, a URL, or another protocol, never as a version range. */
13
+ const nonRangePrefixes = [
14
+ "./",
15
+ "../",
16
+ "/",
17
+ "~/",
18
+ "file:",
19
+ "link:",
20
+ "workspace:",
21
+ "catalog:",
22
+ "npm:",
23
+ "git+",
24
+ "git:",
25
+ "github:",
26
+ "http:",
27
+ "https:",
28
+ "ssh:"
29
+ ];
30
+ /** Whether Bun reads a specifier as a tarball by its file extension. */
31
+ function isTarballSpecifier(specifier) {
32
+ const lowerCase = specifier.toLowerCase();
33
+ return [
34
+ ".tgz",
35
+ ".tar.gz",
36
+ ".tar"
37
+ ].some((extension) => lowerCase.endsWith(extension));
38
+ }
39
+ /**
40
+ * Tell whether Bun reads a specifier as an npm version range, following the branches of its `Tag::infer`.
41
+ *
42
+ * Returns `undefined` when the answer depends on details that are not ported.
43
+ */
44
+ function isNpmRangeSpecifier(specifier) {
45
+ const text = trimSpecifier(specifier);
46
+ if (!text) return false;
47
+ const first = text[0];
48
+ if ("=><^*|".includes(first)) return true;
49
+ if (first === "~") return !text.startsWith("~/");
50
+ if ((first === "x" || first === "X") && (text.length === 1 || text[1] === ".")) return true;
51
+ if (text.startsWith("patch:")) return true;
52
+ if (/^v?\d/.test(text)) {
53
+ if (isTarballSpecifier(text)) return false;
54
+ return parseBunRange(text) ? true : void 0;
55
+ }
56
+ if (first === "v" || first === ".") return false;
57
+ if (nonRangePrefixes.some((prefix) => text.startsWith(prefix))) return false;
58
+ if (/^[a-z]:\//i.test(text)) return false;
59
+ if (!text.includes("|")) return false;
60
+ if (isTarballSpecifier(text) || text.includes("/") || text.includes(":")) return void 0;
61
+ return true;
62
+ }
63
+ /**
64
+ * Parse a semver range for override selectors, or return `undefined` when node-semver cannot parse it,
65
+ * although Bun may read it, such as `01.2.3`.
66
+ *
67
+ * Bun expands comparators as node-semver does, but it evaluates every `||` alternative on its own, so node-semver's
68
+ * normalizations across alternatives are avoided, and it skips an empty alternative.
69
+ */
70
+ function parseBunRange(text) {
71
+ const alternatives = [];
72
+ for (const alternativeText of text.split("||").filter((part) => part.trim())) {
73
+ let range;
74
+ try {
75
+ range = new semver.Range(alternativeText);
76
+ } catch {
77
+ return;
78
+ }
79
+ alternatives.push({
80
+ range,
81
+ comparators: range.set[0],
82
+ isSingle: countComparatorTokens(alternativeText) === 1
83
+ });
84
+ }
85
+ return alternatives.length ? { alternatives } : void 0;
86
+ }
87
+ /** Count the comparators written in an alternative, such as 2 in `>=1.0.0 <2.0.0`. */
88
+ function countComparatorTokens(alternativeText) {
89
+ return alternativeText.trim().replace(/(^|\s)([<>]=?|=|~>?|\^|v)\s+/g, "$1$2").replace(/\s+-\s+/g, "-").split(/\s+/).length;
90
+ }
91
+ /**
92
+ * Bun keeps a lower bound `>=0.0.0` that node-semver drops while parsing, such as the one of `0.x`.
93
+ * Adding that bound to an alternative only narrows it, so an answer computed both without it and with it in every
94
+ * alternative is Bun's answer when the two agree, and is unknown otherwise.
95
+ */
96
+ function evaluateWithZeroFloor(evaluate) {
97
+ const withoutFloor = evaluate(false);
98
+ return withoutFloor === evaluate(true) ? withoutFloor : void 0;
99
+ }
100
+ /** Tell whether a version satisfies a range, as Bun's `Group::satisfies` does, or `undefined` when unknown. */
101
+ function rangeIncludes(range, version) {
102
+ return evaluateWithZeroFloor((hasZeroFloor) => range.alternatives.some((alternative) => alternativeIncludes(alternative, version, hasZeroFloor)));
103
+ }
104
+ function alternativeIncludes(alternative, version, hasZeroFloor) {
105
+ return alternative.range.test(version) && (!hasZeroFloor || semver.compare(version, zeroVersion) >= 0);
106
+ }
107
+ /**
108
+ * Tell whether some version satisfies both ranges, as Bun 1.4.4 matches a scoped override key `name@range`
109
+ * against a declared range, or `undefined` when unknown.
110
+ *
111
+ * Ports Bun's `Group::intersects`: an exact version goes through `satisfies`, where `*` admits a prerelease too,
112
+ * and two ranges compare only their bounds, so `>=1.0.0 <2.0.0` intersects `>=2.0.0-alpha <2.0.0`.
113
+ */
114
+ function rangesIntersect(left, right) {
115
+ return evaluateWithZeroFloor((hasZeroFloor) => {
116
+ if (admitsExactVersionOf(left, right, hasZeroFloor) || admitsExactVersionOf(right, left, hasZeroFloor)) return true;
117
+ const isRange = (alternative) => exactVersion(alternative) === void 0;
118
+ for (const leftAlternative of left.alternatives.filter(isRange)) {
119
+ const base = narrow({}, leftAlternative, hasZeroFloor);
120
+ if (!isNonEmpty(base)) continue;
121
+ for (const rightAlternative of right.alternatives.filter(isRange)) if (isNonEmpty(narrow(base, rightAlternative, hasZeroFloor))) return true;
122
+ }
123
+ return false;
124
+ });
125
+ }
126
+ function isAny(comparator) {
127
+ return comparator.value === "";
128
+ }
129
+ function exactVersion({ comparators, isSingle }) {
130
+ const [comparator] = comparators;
131
+ return isSingle && comparators.length === 1 && !isAny(comparator) && (comparator.operator === "" || comparator.operator === "=") ? comparator.semver : void 0;
132
+ }
133
+ /** Whether an alternative is `*`, which Bun reads as a lone `>=0.0.0`. */
134
+ function isStar({ comparators, isSingle }) {
135
+ return isSingle && comparators.length === 1 && isAny(comparators[0]);
136
+ }
137
+ function admitsExactVersionOf(range, other, hasZeroFloor) {
138
+ return other.alternatives.some((alternative) => {
139
+ const version = exactVersion(alternative);
140
+ return version !== void 0 && (range.alternatives.some((candidate) => alternativeIncludes(candidate, version, hasZeroFloor)) || version.prerelease.length > 0 && range.alternatives.some(isStar));
141
+ });
142
+ }
143
+ function narrow(interval, alternative, hasZeroFloor) {
144
+ const result = { ...interval };
145
+ if (hasZeroFloor) raise(result, {
146
+ version: zeroVersion,
147
+ inclusive: true
148
+ });
149
+ for (const comparator of alternative.comparators) {
150
+ if (isAny(comparator)) {
151
+ raise(result, {
152
+ version: zeroVersion,
153
+ inclusive: true
154
+ });
155
+ continue;
156
+ }
157
+ const { operator, semver: version } = comparator;
158
+ if (operator === "" || operator === "=") {
159
+ raise(result, {
160
+ version,
161
+ inclusive: true
162
+ });
163
+ cap(result, {
164
+ version,
165
+ inclusive: true
166
+ });
167
+ } else if (operator.startsWith(">")) raise(result, {
168
+ version,
169
+ inclusive: operator === ">="
170
+ });
171
+ else cap(result, {
172
+ version,
173
+ inclusive: operator === "<="
174
+ });
175
+ }
176
+ return result;
177
+ }
178
+ function raise(interval, bound) {
179
+ const order = interval.lower ? semver.compare(bound.version, interval.lower.version) : 1;
180
+ if (order > 0 || order === 0 && !bound.inclusive) interval.lower = bound;
181
+ }
182
+ function cap(interval, bound) {
183
+ const order = interval.upper ? semver.compare(bound.version, interval.upper.version) : -1;
184
+ if (order < 0 || order === 0 && !bound.inclusive) interval.upper = bound;
185
+ }
186
+ function isNonEmpty({ lower, upper }) {
187
+ if (!upper) return true;
188
+ const from = lower ?? {
189
+ version: lowestVersion,
190
+ inclusive: true
191
+ };
192
+ const order = semver.compare(from.version, upper.version);
193
+ return order < 0 || order === 0 && from.inclusive && upper.inclusive;
194
+ }
4
195
  // ../lockfile/src/parse.ts
5
196
  function isObject(value) {
6
197
  return typeof value === "object" && value !== null;
@@ -8,6 +199,48 @@ function isObject(value) {
8
199
  function isGitPackageEntry(entry) {
9
200
  return isObject(entry[1]);
10
201
  }
202
+ /**
203
+ * Return whether a package entry needs `lockfileVersion` 1, because Bun 1.4.4 rejects it in a lockfile of a later version.
204
+ *
205
+ * Version 2 adds checks on reading: an npm integrity is a hash that Bun supports or empty,
206
+ * an npm package from a tarball outside the default registry has a supported integrity,
207
+ * and the checkout tag of a git package is a single safe path component.
208
+ * Bun writes version 1 when an entry fails them, and discards a lockfile of a later version that has such an entry.
209
+ * An entry that Bun rejects at every version, such as a GitHub package with an unsafe tag, does not need version 1.
210
+ */
211
+ function needsLockfileVersion1(entry) {
212
+ if (isGitPackageEntry(entry)) {
213
+ const tag = entry[2];
214
+ return (parseResolvedSpec(entry[0])?.version.startsWith("git+") ?? false) && tag !== void 0 && !isSafeGitTag(tag);
215
+ }
216
+ const [, url = "", , integrity = ""] = entry;
217
+ if (isSupportedIntegrity(integrity)) return false;
218
+ return Boolean(integrity) || Boolean(url) && !isUnderDefaultRegistry(url);
219
+ }
220
+ const defaultRegistry = "https://registry.npmjs.org";
221
+ function isUnderDefaultRegistry(url) {
222
+ return url.startsWith(defaultRegistry) && (url.length === 26 || url[26] === "/");
223
+ }
224
+ const digestLengths = {
225
+ sha1: 20,
226
+ sha256: 32,
227
+ sha384: 48,
228
+ sha512: 64
229
+ };
230
+ /** Return whether an integrity has a hash that Bun supports, as Bun's `Integrity::parse` reads it. */
231
+ function isSupportedIntegrity(integrity) {
232
+ return integrity.split(/\s/).some((hash) => {
233
+ const separator = hash.slice(0, 7).indexOf("-");
234
+ if (hash.length < 7 || separator === -1 || hash.length <= separator + 1) return false;
235
+ const digestLength = digestLengths[hash.slice(0, separator)];
236
+ const encoded = hash.slice(separator + 1).split("?")[0].replace(/=+$/, "");
237
+ return digestLength !== void 0 && /^[A-Za-z0-9+/]*$/.test(encoded) && encoded.length % 4 !== 1 && Math.floor(encoded.length * 3 / 4) <= digestLength;
238
+ });
239
+ }
240
+ /** Return whether a git checkout tag is a single safe path component, as Bun's `is_safe_resolved_tag` checks it. */
241
+ function isSafeGitTag(tag) {
242
+ return tag.length <= 256 && tag !== "." && tag !== ".." && /^[A-Za-z0-9_.][A-Za-z0-9_.-]*$/.test(tag);
243
+ }
11
244
  function packageEntryMeta(entry) {
12
245
  if (isGitPackageEntry(entry)) return entry[1];
13
246
  const maybeMeta = entry[2];
@@ -31,25 +264,13 @@ function parseResolvedSpec(spec) {
31
264
  version
32
265
  };
33
266
  }
34
- /** Error thrown when `bun.lock` has a structure that the tools do not support. */
267
+ /** Error thrown when `bun.lock` has a structure that the tools do not support, or an override they cannot evaluate. */
35
268
  var BunLockFormatError = class extends Error {
36
269
  constructor(message) {
37
270
  super(message);
38
271
  this.name = "BunLockFormatError";
39
272
  }
40
273
  };
41
- /** Parses `bun.lock` text and throws `BunLockFormatError` on unsupported structure. */
42
- function parseBunLock(lockText) {
43
- const parsed = JSON5.parse(lockText);
44
- if (!isObject(parsed)) throw new BunLockFormatError("bun.lock must parse to an object");
45
- const overrides = parsed.overrides;
46
- if (overrides !== void 0) {
47
- if (!isObject(overrides) || Array.isArray(overrides)) throw new BunLockFormatError("bun.lock overrides must be an object");
48
- for (const [name, value] of Object.entries(overrides)) if (typeof value !== "string") throw new BunLockFormatError(`bun.lock override ${JSON.stringify(name)} is scoped to a parent package or a version selector; only overrides by package name are supported`);
49
- }
50
- return parsed;
51
- }
52
- // ../lockfile/src/overrides.ts
53
274
  /**
54
275
  * Return the package and range that an `npm:` alias specifier installs, or `undefined` for any other specifier.
55
276
  *
@@ -72,6 +293,359 @@ function npmAliasTarget(specifier) {
72
293
  range: "*"
73
294
  } : void 0;
74
295
  }
296
+ const overrideRulesCache = /* @__PURE__ */ new WeakMap();
297
+ /**
298
+ * Return the rules of the `bun.lock` `overrides` section, read once per section object.
299
+ *
300
+ * Throws `BunLockFormatError` when the section has a shape or a key that Bun does not read or the tools cannot evaluate.
301
+ */
302
+ function getOverrideRules(overrides) {
303
+ if (overrides === void 0) return void 0;
304
+ if (!isObject(overrides) || Array.isArray(overrides)) throw new BunLockFormatError("bun.lock overrides must be an object");
305
+ let rules = overrideRulesCache.get(overrides);
306
+ if (!rules) {
307
+ rules = readOverrideRules(overrides);
308
+ overrideRulesCache.set(overrides, rules);
309
+ }
310
+ return rules;
311
+ }
312
+ /**
313
+ * Read the rules of the `bun.lock` `overrides` section, as Bun 1.4.4 does.
314
+ *
315
+ * A string value is an override by name.
316
+ * An object value is a group under the key `name` or `name@range`: its `"."` child overrides that package itself,
317
+ * and any other child `dep` or `dep@range` overrides a dependency of it.
318
+ */
319
+ function readOverrideRules(overrides) {
320
+ const rules = {
321
+ flat: /* @__PURE__ */ new Map(),
322
+ scopedByName: /* @__PURE__ */ new Map()
323
+ };
324
+ const addScoped = (name, rule) => {
325
+ const scoped = rules.scopedByName.get(name) ?? [];
326
+ const index = scoped.findIndex((existing) => existing.parent?.name === rule.parent?.name && existing.parent?.rangeText === rule.parent?.rangeText && existing.targetRangeText === rule.targetRangeText);
327
+ if (index === -1) scoped.push(rule);
328
+ else scoped[index] = rule;
329
+ rules.scopedByName.set(name, scoped);
330
+ };
331
+ for (const [key, value] of Object.entries(overrides)) {
332
+ if (typeof value === "string") {
333
+ rules.flat.set(key, value);
334
+ continue;
335
+ }
336
+ if (!isObject(value) || Array.isArray(value)) throw new BunLockFormatError(`bun.lock override ${JSON.stringify(key)} must be a string or an object of strings`);
337
+ const group = parseOverrideKey(key);
338
+ for (const [childKey, childValue] of Object.entries(value)) {
339
+ if (typeof childValue !== "string") throw new BunLockFormatError(`bun.lock override ${JSON.stringify(key)} must be a string or an object of strings`);
340
+ if (childKey === "." && !group.range) {
341
+ rules.flat.set(group.name, childValue);
342
+ continue;
343
+ }
344
+ const target = childKey === "." ? group : parseOverrideKey(childKey);
345
+ const parent = childKey === "." ? void 0 : group;
346
+ if (parent && !isParentSegment(parent)) throw unsupportedKey(key);
347
+ addScoped(target.name, {
348
+ key: childKey === "." ? JSON.stringify(key) : `${JSON.stringify(key)} > ${JSON.stringify(childKey)}`,
349
+ parent: parent && {
350
+ name: parent.name,
351
+ range: parent.selector?.kind === "semver" ? parent.selector.range : void 0,
352
+ rangeText: parent.range
353
+ },
354
+ targetRangeText: target.range,
355
+ targetRange: target.selector,
356
+ value: childValue
357
+ });
358
+ }
359
+ }
360
+ return rules;
361
+ }
362
+ /**
363
+ * Split an override key segment into a package name and a range, as Bun's `parse_package_segment` does.
364
+ *
365
+ * `npm:` before a range is dropped, while `npm:other` and `npm:other@range` stay aliases.
366
+ * Returns `undefined` for a segment that Bun rejects and for a range other than a semver range or a catalog,
367
+ * which the tools cannot evaluate.
368
+ */
369
+ function parseOverrideSegment(segment) {
370
+ const separator = segment.indexOf("@", 1);
371
+ const name = separator === -1 ? segment : segment.slice(0, separator);
372
+ let range = separator === -1 ? "" : segment.slice(separator + 1);
373
+ if (range.startsWith("npm:")) {
374
+ const rest = range.slice(4);
375
+ if (!rest || isNpmRangeSpecifier(rest) === true) range = rest;
376
+ }
377
+ if (!name || name.startsWith("@") && !name.includes("/") || separator !== -1 && !range) return;
378
+ if (!range) return {
379
+ name,
380
+ range
381
+ };
382
+ const selector = toSelectorRange(range);
383
+ if (!selector || selector.kind === "unparsed") return void 0;
384
+ return {
385
+ name,
386
+ range,
387
+ selector
388
+ };
389
+ }
390
+ /** Return whether a parent segment has no range or a range that Bun matches against the installed version. */
391
+ function isParentSegment({ selector }) {
392
+ return !selector || selector.kind === "semver" && selector.alias === void 0;
393
+ }
394
+ function parseOverrideKey(key) {
395
+ const segment = parseOverrideSegment(key);
396
+ if (!segment) throw unsupportedKey(key);
397
+ return segment;
398
+ }
399
+ function unsupportedKey(key) {
400
+ return new BunLockFormatError(`bun.lock override key ${JSON.stringify(key)} is not a package name with an optional semver range`);
401
+ }
402
+ /**
403
+ * Read a range in an override key, or a declared range, as Bun compares them.
404
+ *
405
+ * Returns `undefined` for a specifier that no selector range matches, such as a dist-tag or a git URL.
406
+ */
407
+ function toSelectorRange(specifier) {
408
+ const text = trimSpecifier(specifier);
409
+ if (text.startsWith("catalog:")) return {
410
+ kind: "catalog",
411
+ catalog: text.slice(8)
412
+ };
413
+ const alias = npmAliasTarget(text);
414
+ const range = alias ? alias.range : text.startsWith("npm:") ? text.slice(4) : text;
415
+ const isNpmRange = isNpmRangeSpecifier(range);
416
+ if (isNpmRange === false) return void 0;
417
+ const parsed = isNpmRange && parseBunRange(range);
418
+ return parsed ? {
419
+ kind: "semver",
420
+ range: parsed,
421
+ alias: alias?.name
422
+ } : {
423
+ kind: "unparsed",
424
+ alias: alias?.name
425
+ };
426
+ }
427
+ /** Parses `bun.lock` text and throws `BunLockFormatError` on unsupported structure. */
428
+ function parseBunLock(lockText) {
429
+ const parsed = JSON5.parse(lockText);
430
+ if (!isObject(parsed)) throw new BunLockFormatError("bun.lock must parse to an object");
431
+ getOverrideRules(parsed.overrides);
432
+ return parsed;
433
+ }
434
+ // ../lockfile/src/manifest-overrides.ts
435
+ /**
436
+ * Read the rules of a `package.json` `overrides` field, as Bun 1.4.4 does.
437
+ *
438
+ * A string value is a rule under a selector key: `name`, `name@range`, `parent>name`, or `parent/name`.
439
+ * An object value is a group under the key `name` or `name@range`: its `"."` child overrides that package itself,
440
+ * and any other child `dep` or `dep@range` overrides a dependency of it.
441
+ * `resolveValue` returns the spec that a `$name` value refers to; the rule stays under its key.
442
+ * Throws on a rule that Bun skips with a warning for its key, its nesting, or its value type,
443
+ * on the values `""`, `-`, and `patch:…`, which Bun skips too, and on a range the tools cannot evaluate.
444
+ * Does not detect a value that Bun skips because it cannot parse it as a dependency spec, such as a malformed git URL.
445
+ */
446
+ function readManifestOverrides(overrides, resolveValue, source = "package.json") {
447
+ const rules = [];
448
+ const fail = (key, problem) => /* @__PURE__ */ new Error(`${source} override ${key} ${problem}`);
449
+ const failSelector = (key) => fail(key, "is not a package name with an optional semver range or a parent>name selector");
450
+ const failDepth = (key) => fail(key, "nests deeper than one level, which Bun ignores");
451
+ const addRule = (key, parent, target, value) => {
452
+ if (typeof value !== "string") throw fail(key, "must be a string");
453
+ if (parent && !isParentSegment(parent)) throw failSelector(key);
454
+ if (!value || value === "-" || value.startsWith("patch:")) throw fail(key, `has the value ${JSON.stringify(value)}, which Bun ignores`);
455
+ rules.push({
456
+ key,
457
+ parent: parent && {
458
+ name: parent.name,
459
+ range: parent.range
460
+ },
461
+ target: target.name,
462
+ range: target.range,
463
+ value: resolveValue(target.name, value)
464
+ });
465
+ };
466
+ for (const [key, value] of Object.entries(overrides)) {
467
+ if (isCommentKey(key)) continue;
468
+ const label = JSON.stringify(key);
469
+ if (!isRecord$2(value)) {
470
+ if (typeof value !== "string") throw fail(label, "must be a string or an object of strings");
471
+ const selector = parseSelector(key);
472
+ if (selector === "too-deep") throw failDepth(label);
473
+ if (!selector) throw failSelector(label);
474
+ addRule(label, selector.parent, selector.target, value);
475
+ continue;
476
+ }
477
+ const group = parseOverrideSegment(key);
478
+ if (!group || !isParentSegment(group)) throw failSelector(label);
479
+ for (const [childKey, childValue] of Object.entries(value)) {
480
+ if (isCommentKey(childKey)) continue;
481
+ const childLabel = `${label} > ${JSON.stringify(childKey)}`;
482
+ if (isRecord$2(childValue)) throw failDepth(childLabel);
483
+ if (childKey === ".") {
484
+ addRule(childLabel, void 0, group, childValue);
485
+ continue;
486
+ }
487
+ const selector = parseSelector(childKey);
488
+ if (selector === "too-deep" || selector?.parent) throw failDepth(childLabel);
489
+ if (!selector) throw failSelector(childLabel);
490
+ addRule(childLabel, group, selector.target, childValue);
491
+ }
492
+ }
493
+ return rules;
494
+ }
495
+ /**
496
+ * Return the pairs of rules where a later rule replaces an earlier one, because both select the same packages.
497
+ *
498
+ * Bun keeps the later rule of such a pair, so the result depends on the order of the keys.
499
+ */
500
+ function findReplacedOverrideRules(rules) {
501
+ const seen = /* @__PURE__ */ new Map();
502
+ const pairs = [];
503
+ for (const rule of rules) {
504
+ const identity = getRuleIdentity(rule);
505
+ const earlier = seen.get(identity);
506
+ if (earlier) pairs.push([earlier, rule]);
507
+ seen.set(identity, rule);
508
+ }
509
+ return pairs;
510
+ }
511
+ /**
512
+ * Return the `bun.lock` `overrides` section for the rules, as Bun 1.4.4 writes it.
513
+ *
514
+ * Without scoped rules, every rule is a string by package name.
515
+ * Otherwise rules are grouped under `name` or `name@range` keys:
516
+ * a rule without a parent is the `"."` of its group, and a rule with a parent is a child of the parent's group.
517
+ * A rule by name stays a string unless its package has an unranged group.
518
+ */
519
+ function renderLockfileOverrides(rules) {
520
+ const flat = /* @__PURE__ */ new Map();
521
+ const scoped = /* @__PURE__ */ new Map();
522
+ for (const rule of rules) if (!rule.parent && !rule.range) flat.set(rule.target, rule.value);
523
+ else scoped.set(getRuleIdentity(rule), rule);
524
+ if (!scoped.size) return Object.fromEntries([...flat].sort(([a], [b]) => compareText$1(a, b)));
525
+ const groups = /* @__PURE__ */ new Map();
526
+ const getGroup = (name, range) => {
527
+ const id = JSON.stringify([name, range]);
528
+ let group = groups.get(id);
529
+ if (!group) {
530
+ group = {
531
+ name,
532
+ range,
533
+ children: []
534
+ };
535
+ groups.set(id, group);
536
+ }
537
+ return group;
538
+ };
539
+ for (const rule of scoped.values()) if (rule.parent) getGroup(rule.parent.name, rule.parent.range).children.push(rule);
540
+ else getGroup(rule.target, rule.range).dot = rule.value;
541
+ const entries = [];
542
+ for (const [name, value] of flat) {
543
+ const group = groups.get(JSON.stringify([name, ""]));
544
+ if (group) group.dot = value;
545
+ else entries.push({
546
+ name,
547
+ range: "",
548
+ value
549
+ });
550
+ }
551
+ for (const { name, range, dot, children } of groups.values()) {
552
+ children.sort((a, b) => compareText$1(a.target, b.target) || compareText$1(a.range, b.range));
553
+ const value = Object.fromEntries([...dot === void 0 ? [] : [[".", dot]], ...children.map((child) => [toSelectorKey(child.target, child.range), child.value])]);
554
+ entries.push({
555
+ name,
556
+ range,
557
+ value
558
+ });
559
+ }
560
+ entries.sort((a, b) => compareText$1(a.name, b.name) || compareText$1(a.range, b.range));
561
+ return Object.fromEntries(entries.map((entry) => [toSelectorKey(entry.name, entry.range), entry.value]));
562
+ }
563
+ /** Return the key that Bun matches a rule by when it replaces an earlier rule. */
564
+ function getRuleIdentity({ parent, target, range }) {
565
+ return JSON.stringify([
566
+ target,
567
+ range,
568
+ parent?.name,
569
+ parent?.range
570
+ ]);
571
+ }
572
+ function toSelectorKey(name, range) {
573
+ return range ? `${name}@${range}` : name;
574
+ }
575
+ function compareText$1(left, right) {
576
+ return left < right ? -1 : left > right ? 1 : 0;
577
+ }
578
+ function isCommentKey(key) {
579
+ return key.startsWith("//");
580
+ }
581
+ function isRecord$2(value) {
582
+ return typeof value === "object" && value !== null && !Array.isArray(value);
583
+ }
584
+ /**
585
+ * Parse the selector key of a string rule, as Bun's `parse_selector` does.
586
+ *
587
+ * Returns `"too-deep"` for a selector with more than one parent, which Bun skips,
588
+ * and `undefined` for any other selector that Bun skips or the tools cannot evaluate.
589
+ */
590
+ function parseSelector(key) {
591
+ const delimiter = findPnpmDelimiter(key);
592
+ if (delimiter !== void 0) {
593
+ const parentText = key.slice(0, delimiter).trim();
594
+ const targetText = key.slice(delimiter + 1).trim();
595
+ if (!targetText) return void 0;
596
+ if (findPnpmDelimiter(targetText) !== void 0) return "too-deep";
597
+ const parent = parseOverrideSegment(parentText);
598
+ const target = parseOverrideSegment(targetText);
599
+ return parent && target && {
600
+ parent,
601
+ target
602
+ };
603
+ }
604
+ const segments = splitYarnPath(key);
605
+ if (typeof segments === "string" || !segments) return segments;
606
+ const parsed = segments.map((segment) => parseOverrideSegment(segment));
607
+ if (parsed.some((segment) => !segment)) return void 0;
608
+ const [first, second] = parsed;
609
+ return second ? {
610
+ parent: first,
611
+ target: second
612
+ } : { target: first };
613
+ }
614
+ /**
615
+ * Return the position of the pnpm `parent>name` delimiter: the first `>` that does not follow a space, `|`, `@`, or `:`.
616
+ *
617
+ * Those characters put the `>` into a range, such as `name@>=1` or `name@npm:>=1`.
618
+ */
619
+ function findPnpmDelimiter(key) {
620
+ for (let index = key.indexOf(">", 1); index !== -1; index = key.indexOf(">", index + 1)) if (!" |@:".includes(key[index - 1])) return index;
621
+ }
622
+ /**
623
+ * Split a yarn `parent/name` path into at most two segments, as Bun's `parse_yarn_path` does.
624
+ *
625
+ * `**` segments are skipped, a scope joins the name after it, and the alias target in `name@npm:@scope/other`
626
+ * stays in its segment.
627
+ */
628
+ function splitYarnPath(key) {
629
+ const tokens = key.split("/");
630
+ const segments = [];
631
+ for (let index = 0; index < tokens.length; index++) {
632
+ const token = tokens[index];
633
+ const isLast = index === tokens.length - 1;
634
+ if (!token || token === "**" && isLast) return void 0;
635
+ if (token === "**") continue;
636
+ let segment = token;
637
+ if (token.startsWith("@")) {
638
+ const name = tokens[++index];
639
+ if (!name) return void 0;
640
+ segment += `/${name}`;
641
+ }
642
+ if (segment.includes("@npm:@") && index < tokens.length - 1) segment += `/${tokens[++index]}`;
643
+ if (segments.length === 2) return "too-deep";
644
+ segments.push(segment);
645
+ }
646
+ return segments.length ? segments : void 0;
647
+ }
648
+ // ../lockfile/src/overrides.ts
75
649
  /**
76
650
  * Return the package and version range that a dependency specifier requires.
77
651
  *
@@ -86,32 +660,101 @@ function dependencyRequirement(dependencyName, specifier) {
86
660
  };
87
661
  }
88
662
  /**
89
- * Read the bare-name override that Bun applies to a dependency declaration.
663
+ * Read the override that Bun applies to a dependency declaration, following Bun 1.4.4.
664
+ *
665
+ * A rule scoped to a parent applies to the direct dependencies of that parent only.
666
+ * Among the matching scoped rules, a parent with a range wins over a parent without one,
667
+ * which wins over no parent; within each of these, a rule whose target range matches wins.
668
+ * An override by name applies when no scoped rule does.
90
669
  *
91
- * Bun never overrides an `npm:` alias of a semver range.
670
+ * Bun never overrides an `npm:` alias of a semver range, unless a target range spells out the alias.
92
671
  * It overrides an alias of a dist-tag, such as `npm:debug@latest`, by the name of the aliased package,
93
672
  * so the override is returned as an alias of that package.
94
- * `parseBunLock` rejects overrides scoped to a parent package or a version selector.
95
- * Bun versions before 1.4 keep selector keys as strings without applying them,
96
- * so matching those here would diverge from the package manager.
97
- */
98
- function dependencyOverrideRange(lock, dependencyName, declaredRange) {
99
- const alias = npmAliasTarget(declaredRange);
100
- if (!alias) return bareNameOverride(lock, dependencyName);
101
- if (!isDistTag(alias.range)) return void 0;
102
- const override = bareNameOverride(lock, alias.name);
673
+ */
674
+ function dependencyOverrideRange(lock, requester, dependencyName, declaredRange) {
675
+ const rules = getOverrideRules(lock.overrides);
676
+ if (!rules) return void 0;
677
+ const alias = npmAliasTarget(trimSpecifier(declaredRange));
678
+ if (!alias || !isDistTag(alias.range)) return findOverride(rules, requester, dependencyName, declaredRange, alias !== void 0);
679
+ const override = findOverride(rules, requester, alias.name, declaredRange, false);
103
680
  if (override === void 0) return void 0;
104
681
  const requirement = dependencyRequirement(alias.name, override);
105
682
  return requirement.name !== alias.name || requirement.range.includes(":") ? override : `npm:${alias.name}@${requirement.range}`;
106
683
  }
107
- function bareNameOverride(lock, name) {
108
- const overrides = lock.overrides;
109
- if (!overrides || !Object.hasOwn(overrides, name)) return void 0;
110
- return overrides[name];
684
+ /**
685
+ * Return the value of the rule that Bun picks for a dependency looked up under `name`, if any.
686
+ *
687
+ * Throws `BunLockFormatError` when a rule that would outrank the picked one may apply,
688
+ * but the tools cannot tell whether its ranges match.
689
+ *
690
+ * @param declaredRange The declared range, which an `npm:` alias of a dist-tag never lets a target range match.
691
+ * @param isAlias Whether the declaration is an `npm:` alias that only a target range naming it can override.
692
+ */
693
+ function findOverride(rules, requester, name, declaredRange, isAlias) {
694
+ const scoped = rules.scopedByName.get(name) ?? [];
695
+ const declared = scoped.length ? toSelectorRange(declaredRange) : void 0;
696
+ let best;
697
+ let undecided;
698
+ for (const rule of scoped) {
699
+ if (!rule.targetRange && isAlias) continue;
700
+ let tier = 0;
701
+ let isDecided = true;
702
+ if (rule.parent) {
703
+ if (requester?.name === void 0 || requester.name !== rule.parent.name) continue;
704
+ if (rule.parent.range) {
705
+ if (requester.version === void 0) continue;
706
+ const includes = rangeIncludes(rule.parent.range, requester.version);
707
+ if (includes === false) continue;
708
+ isDecided &&= includes === true;
709
+ tier = 4;
710
+ } else tier = 2;
711
+ }
712
+ if (rule.targetRange) {
713
+ const selects = declared ? rangeSelects(rule.targetRange, declared) : false;
714
+ if (selects === false) continue;
715
+ isDecided &&= selects === true;
716
+ tier += 1;
717
+ }
718
+ const ranked = {
719
+ rule,
720
+ tier
721
+ };
722
+ if (!isDecided) {
723
+ if (!undecided || outranks(ranked, undecided)) undecided = ranked;
724
+ } else if (!best || outranks(ranked, best)) best = ranked;
725
+ }
726
+ if (undecided && (!best || outranks(undecided, best))) throw new BunLockFormatError(`Cannot tell whether bun.lock override ${undecided.rule.key} applies to ${name} declared as ${JSON.stringify(declaredRange)}; this tool cannot compare these ranges as Bun does, so change the override in package.json so that no range comparison is needed, such as by removing the range from its key or adding a rule scoped to the parent package, and run bun install`);
727
+ if (best) return best.rule.value;
728
+ return isAlias ? void 0 : rules.flat.get(name);
729
+ }
730
+ /** Whether a rule wins over another as Bun decides: by tier, then by the parent range text, then by the target range text. */
731
+ function outranks(left, right) {
732
+ if (left.tier !== right.tier) return left.tier > right.tier;
733
+ const leftParent = left.rule.parent?.range && left.rule.parent.rangeText;
734
+ const rightParent = right.rule.parent?.range && right.rule.parent.rangeText;
735
+ return ((leftParent && rightParent ? compareText(leftParent, rightParent) : 0) || compareText(left.rule.targetRangeText, right.rule.targetRangeText)) < 0;
736
+ }
737
+ function compareText(left, right) {
738
+ return left < right ? -1 : left > right ? 1 : 0;
739
+ }
740
+ /**
741
+ * Whether an override key `name@range` selects a declaration, as npm and pnpm do: the two ranges overlap.
742
+ *
743
+ * Returns `undefined` when the tools cannot tell.
744
+ */
745
+ function rangeSelects(range, declared) {
746
+ if (range.kind === "catalog" || declared.kind === "catalog") return range.kind === "catalog" && declared.kind === "catalog" && isSameCatalog(range, declared);
747
+ if (range.kind !== "semver" || range.alias !== declared.alias) return false;
748
+ return declared.kind === "semver" ? rangesIntersect(range.range, declared.range) : void 0;
749
+ }
750
+ /** Whether two catalog references name the same catalog; `catalog:` and `catalog:default` are the default one. */
751
+ function isSameCatalog(left, right) {
752
+ const isDefault = (catalog) => catalog === "" || catalog === "default";
753
+ return left.catalog === right.catalog || isDefault(left.catalog) && isDefault(right.catalog);
111
754
  }
112
755
  /** Whether a version of an `npm:` alias is a dist-tag, such as `latest`, rather than a range or a source. */
113
756
  function isDistTag(version) {
114
- return /^[^\s:/#@]+$/.test(version) && semver.validRange(version) === null;
757
+ return /^[^\s:/#@|]+$/.test(version) && isNpmRangeSpecifier(version) === false;
115
758
  }
116
759
  /**
117
760
  * Return the range Bun actually enforces for a dependency request.
@@ -119,10 +762,18 @@ function isDistTag(version) {
119
762
  * An override remains authoritative even when it is not valid semver;
120
763
  * compatibility then stays unknown instead of falling back to the declaration.
121
764
  */
122
- function effectiveDependencyRange(lock, dependencyName, declaredRange) {
123
- const overrideRange = dependencyOverrideRange(lock, dependencyName, declaredRange);
765
+ function effectiveDependencyRange(lock, requester, dependencyName, declaredRange) {
766
+ const overrideRange = dependencyOverrideRange(lock, requester, dependencyName, declaredRange);
124
767
  return overrideRange === void 0 ? declaredRange : overrideRange;
125
768
  }
769
+ /**
770
+ * Tell whether the range of an override parent selector `parent@range` selects a package version, as Bun 1.4.4 matches it,
771
+ * or `undefined` when the tools cannot tell.
772
+ */
773
+ function overrideParentRangeIncludes(range, version) {
774
+ const parsed = parseBunRange(range);
775
+ return parsed && rangeIncludes(parsed, version);
776
+ }
126
777
  // ../lockfile/src/render.ts
127
778
  function isRecord$1(value) {
128
779
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -182,6 +833,36 @@ function renderObjectLines(value, indentLevel, allowPackagesFormatting) {
182
833
  function renderBunLock(lock) {
183
834
  return `${renderObjectLines(lock, 0, true).join("\n")}\n`;
184
835
  }
836
+ // ../lockfile/src/requester.ts
837
+ /**
838
+ * Return the override requester for a package entry with the given spec.
839
+ *
840
+ * As in Bun, only a registry package and a workspace other than the root have a version;
841
+ * a git, tarball, or local package has none, so a parent selector with a range never matches it.
842
+ */
843
+ function packageOverrideRequester(lock, spec) {
844
+ const resolved = parseResolvedSpec(spec);
845
+ if (!resolved) return {};
846
+ if (resolved.version.startsWith("workspace:")) {
847
+ const workspace = Object.entries(lock.workspaces ?? {}).find(([workspacePath, { name }]) => workspacePath !== "" && name === resolved.name);
848
+ return {
849
+ name: resolved.name,
850
+ version: workspace?.[1].version
851
+ };
852
+ }
853
+ return {
854
+ name: resolved.name,
855
+ version: semver.valid(resolved.version) ?? void 0
856
+ };
857
+ }
858
+ /** Return the override requester for the workspace at the given path; the root workspace has no version. */
859
+ function workspaceOverrideRequester(lock, workspacePath) {
860
+ const workspace = lock.workspaces?.[workspacePath];
861
+ return {
862
+ name: workspace?.name,
863
+ version: workspacePath === "" ? void 0 : workspace?.version
864
+ };
865
+ }
185
866
  // ../lockfile/src/resolve.ts
186
867
  /**
187
868
  * Tell whether a lock key is a nested entry of a dependency: the last of several package names in the key.
@@ -330,11 +1011,13 @@ function resolveOverrideValue(manifest, name, value, source = "package.json") {
330
1011
  /**
331
1012
  * Returns the host root `package.json` with the configured root fields of the nested `package.json` merged in.
332
1013
  *
333
- * A nested entry replaces a host entry with the same name, while the other host entries stay.
1014
+ * A nested entry replaces a host entry with the same key, while the other host entries stay.
334
1015
  * Entries synced earlier and dropped by the nested repository are removed.
335
1016
  * Throws when the host and the nested repository patch the same package version,
336
1017
  * when either side defines `resolutions` while `overrides` are synced,
337
- * and when a synced `$name` override would refer to another range in the host root than in the nested root.
1018
+ * when an override is one that `readManifestOverrides` rejects, when two overrides select the same packages,
1019
+ * when a synced `$name` override would refer to another range in the host root than in the nested root,
1020
+ * and when a synced override selects the nested root by a range that its version satisfies.
338
1021
  */
339
1022
  function syncManifest(host, nested) {
340
1023
  const config = readConfig(host);
@@ -370,12 +1053,17 @@ function syncManifest(host, nested) {
370
1053
  manifest = setCatalogs(manifest, catalogs);
371
1054
  if (config.fields.includes("overrides")) {
372
1055
  for (const [source, resolutions] of [["The host package.json", host.resolutions], [`${config.dir}/package.json`, nested.resolutions]]) if (resolutions && Object.keys(resolutions).length) throw new Error(`${source} defines "resolutions", which Bun ignores once "overrides" is defined; move its entries to "overrides"`);
373
- for (const [name, value] of Object.entries(nested.overrides ?? {})) {
374
- const nestedRange = resolveOverrideValue(nested, name, value, `${config.dir}/package.json`);
1056
+ const nestedSource = `${config.dir}/package.json`;
1057
+ const nestedRules = readManifestOverrides(nested.overrides ?? {}, (name, value) => {
1058
+ const nestedRange = resolveOverrideValue(nested, name, value, nestedSource);
375
1059
  const hostRanges = getDeclaredRanges(host, name);
376
- if (value.startsWith("$") && (hostRanges.length !== 1 || hostRanges[0] !== nestedRange)) throw new Error(`${config.dir}/package.json override ${JSON.stringify(name)}: ${JSON.stringify(value)} refers to ${JSON.stringify(nestedRange)} in the nested root, but the host root ${hostRanges.length ? `declares ${hostRanges.map((range) => JSON.stringify(range)).join(", ")}` : "does not declare it"}; replace it with an explicit range`);
377
- }
1060
+ if (value.startsWith("$") && (hostRanges.length !== 1 || hostRanges[0] !== nestedRange)) throw new Error(`${nestedSource} override ${JSON.stringify(name)}: ${JSON.stringify(value)} refers to ${JSON.stringify(nestedRange)} in the nested root, but the host root ${hostRanges.length ? `declares ${hostRanges.map((range) => JSON.stringify(range)).join(", ")}` : "does not declare it"}; replace it with an explicit range`);
1061
+ return nestedRange;
1062
+ }, nestedSource);
1063
+ for (const { key, parent } of nestedRules) if (parent?.range && parent.name === nested.name && typeof nested.version === "string" && overrideParentRangeIncludes(parent.range, nested.version) !== false) throw new Error(`${nestedSource} override ${key} selects the nested root ${parent.name}@${nested.version} by a version range, which applies in the host but not in the nested repository; remove the range`);
378
1064
  const merged = mergeEntries("overrides", host.overrides, nested.overrides, synced.overrides);
1065
+ const [replaced] = findReplacedOverrideRules(readManifestOverrides(merged.entries, (_, value) => value));
1066
+ if (replaced) throw new Error(`The overrides ${replaced[0].key} and ${replaced[1].key} select the same packages, so Bun applies one of them depending on the order of the keys; keep one of them`);
379
1067
  manifest.overrides = merged.entries;
380
1068
  synced.overrides = merged.synced;
381
1069
  overridden.push(...merged.overridden);
@@ -404,14 +1092,15 @@ function syncManifest(host, nested) {
404
1092
  };
405
1093
  }
406
1094
  /**
407
- * Merges name-keyed entries of one field.
1095
+ * Merges the entries of one field by their keys.
408
1096
  *
409
1097
  * Host entries outside `synced` are the host's own.
410
1098
  * An own entry equal to the nested one stays own, so it survives if the nested repository drops it later.
411
1099
  */
412
1100
  function mergeEntries(field, host = {}, nested = {}, synced = []) {
413
1101
  const own = Object.fromEntries(Object.entries(host).filter(([name]) => !synced.includes(name)));
414
- const overridden = Object.entries(nested).filter(([name, value]) => name in own && own[name] !== value).map(([name, nestedValue]) => ({
1102
+ const isOwn = (name) => name in own && isSameValue(own[name], nested[name]);
1103
+ const overridden = Object.entries(nested).filter(([name]) => name in own && !isOwn(name)).map(([name, nestedValue]) => ({
415
1104
  field,
416
1105
  name,
417
1106
  hostValue: own[name],
@@ -422,10 +1111,15 @@ function mergeEntries(field, host = {}, nested = {}, synced = []) {
422
1111
  ...own,
423
1112
  ...nested
424
1113
  }),
425
- synced: Object.keys(nested).filter((name) => own[name] !== nested[name]).sort(),
1114
+ synced: Object.keys(nested).filter((name) => !isOwn(name)).sort(),
426
1115
  overridden
427
1116
  };
428
1117
  }
1118
+ /** Returns whether two entry values are equal, ignoring the key order of groups. */
1119
+ function isSameValue(left, right) {
1120
+ const toText = (value) => typeof value === "string" ? value : JSON.stringify(sortKeys(value));
1121
+ return typeof left === typeof right && toText(left) === toText(right);
1122
+ }
429
1123
  /** Merges host patches with the nested patches, rewriting nested patch paths through the nested directory. */
430
1124
  function mergePatches(host = {}, nested = {}, dir) {
431
1125
  const own = Object.entries(host).filter(([, patch]) => !isNestedPath(path.posix.normalize(patch), dir));
@@ -483,7 +1177,11 @@ function isRecord(value) {
483
1177
  *
484
1178
  * The format is internal to Bun, so other versions are rejected instead of being rewritten blindly.
485
1179
  */
486
- const supportedLockfileVersions = [1, 2];
1180
+ const supportedLockfileVersions = [
1181
+ 1,
1182
+ 2,
1183
+ 3
1184
+ ];
487
1185
  /** Parses `bun.lock` text and rejects lockfile versions outside `supportedLockfileVersions`. */
488
1186
  function parseLockfile(text, source = "bun.lock") {
489
1187
  let lock;
@@ -535,14 +1233,14 @@ function relocateResolution(resolution, dir) {
535
1233
  *
536
1234
  * `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
537
1235
  * so the merged lockfile must already record the synced root fields to keep the nested resolutions.
538
- * Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
1236
+ * Records overrides in the form that Bun writes to `bun.lock`, with `$name` values replaced by the range they refer to.
539
1237
  */
540
1238
  function applyManifestToLockfile(lock, manifest) {
541
1239
  const { catalog, catalogs } = getCatalogs(manifest);
542
1240
  const fields = {
543
1241
  catalog,
544
1242
  catalogs,
545
- overrides: manifest.overrides && Object.fromEntries(Object.entries(manifest.overrides).map(([name, value]) => [name, resolveOverrideValue(manifest, name, value)])),
1243
+ overrides: manifest.overrides && toLockfileOverrides(manifest),
546
1244
  patchedDependencies: manifest.patchedDependencies
547
1245
  };
548
1246
  const result = { ...lock };
@@ -551,6 +1249,34 @@ function applyManifestToLockfile(lock, manifest) {
551
1249
  return result;
552
1250
  }
553
1251
  /**
1252
+ * Returns the `bun.lock` `overrides` section for the overrides of a root `package.json`, as Bun writes it.
1253
+ *
1254
+ * Replaces `$name` values with the range they refer to, and throws when `readManifestOverrides` or `resolveOverrideValue` does.
1255
+ */
1256
+ function toLockfileOverrides(manifest, source = "package.json") {
1257
+ return renderLockfileOverrides(readManifestOverrides(manifest.overrides ?? {}, (target, value) => resolveOverrideValue(manifest, target, value, source), source));
1258
+ }
1259
+ /**
1260
+ * Throws when a lockfile does not record the `overrides` of its root `package.json` as Bun reads them.
1261
+ *
1262
+ * Bun skips with a warning an override whose value is not a dependency spec it can parse, such as a malformed git URL,
1263
+ * and it resolves every dependency again when it finds such an override in the lockfile.
1264
+ * The lockfile that Bun wrote tells which overrides it read; a difference can also mean that the lockfile is outdated.
1265
+ * Checks nothing when `package.json` has no `overrides` field, because Bun then records `resolutions` there.
1266
+ */
1267
+ function assertOverridesRecorded(manifest, lock, sources) {
1268
+ if (!manifest.overrides) return;
1269
+ const expected = toLockfileOverrides(manifest, sources.manifest);
1270
+ const recorded = lock.overrides ?? {};
1271
+ const toText = (overrides, key) => {
1272
+ if (!Object.hasOwn(overrides, key)) return void 0;
1273
+ const value = overrides[key];
1274
+ return JSON.stringify(typeof value === "string" ? value : Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
1275
+ };
1276
+ const differences = [.../* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(recorded)])].filter((key) => toText(expected, key) !== toText(recorded, key)).sort();
1277
+ if (differences.length) throw new Error(`${sources.lock} does not record the overrides of ${sources.manifest} as Bun reads them: ${differences.map((key) => JSON.stringify(key)).join(", ")}. Either Bun skipped an override that it cannot parse, which it warns about on install, or the lockfile is outdated; fix the override and update the lockfile with bun install`);
1278
+ }
1279
+ /**
554
1280
  * Merges the host lockfile with the relocated nested lockfile.
555
1281
  *
556
1282
  * Nested entries override host entries, so the nested packages keep the versions of the nested lockfile.
@@ -562,8 +1288,9 @@ function applyManifestToLockfile(lock, manifest) {
562
1288
  * so `bun install` resolves its dependency again.
563
1289
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
564
1290
  * the dependency gets the highest satisfying version recorded in either lockfile.
565
- * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
1291
+ * Dependencies are checked against the catalogs and overrides of the host lockfile, so apply the synced root fields
566
1292
  * to it first.
1293
+ * The result has the higher `lockfileVersion` of the two, or version 1 when it has an entry that needs that version.
567
1294
  */
568
1295
  function mergeLockfiles(lock, nestedLock, dir) {
569
1296
  const nestedNames = /* @__PURE__ */ new Set([...Object.keys(nestedLock.packages), ...Object.values(nestedLock.workspaces).flatMap((workspace) => workspace.name ?? [])]);
@@ -573,18 +1300,25 @@ function mergeLockfiles(lock, nestedLock, dir) {
573
1300
  ...nestedLock.packages
574
1301
  };
575
1302
  const hostWorkspaces = Object.entries(lock.workspaces).filter(([workspacePath]) => !isNestedPath(workspacePath, dir));
1303
+ const workspaces = {
1304
+ ...Object.fromEntries(hostWorkspaces),
1305
+ ...nestedLock.workspaces
1306
+ };
576
1307
  if (lock.workspaces[""]) keepHostRootResolutions(lock, nestedLock, packages);
1308
+ const toPackageDependent = (key, entry) => ({
1309
+ key,
1310
+ requester: packageOverrideRequester({ workspaces }, entry[0]),
1311
+ dependencies: getPackageDependencies(entry)
1312
+ });
577
1313
  const dependents = [...hostWorkspaces.flatMap(([workspacePath, workspace]) => workspacePath && workspace.name ? [{
578
1314
  key: workspace.name,
1315
+ requester: workspaceOverrideRequester({ workspaces }, workspacePath),
579
1316
  dependencies: getWorkspaceDependencies(workspace)
580
- }] : []), ...Object.keys(packages).map((key) => ({
581
- key,
582
- dependencies: getPackageDependencies(packages[key])
583
- }))];
1317
+ }] : []), ...Object.keys(packages).map((key) => toPackageDependent(key, packages[key]))];
584
1318
  for (let index = 0; index < dependents.length; index++) {
585
- const { key, dependencies } = dependents[index];
1319
+ const { key, requester, dependencies } = dependents[index];
586
1320
  for (const [dependency, specifier] of Object.entries(dependencies)) {
587
- const requirement = getRequirement(lock, dependency, specifier);
1321
+ const requirement = getRequirement(lock, requester, dependency, specifier);
588
1322
  const resolved = resolveDependencyLockKey(key, dependency, packages);
589
1323
  if (!requirement || !resolved || satisfies(packages[resolved], requirement)) continue;
590
1324
  const previous = resolveDependencyLockKey(key, dependency, lock.packages);
@@ -592,19 +1326,14 @@ function mergeLockfiles(lock, nestedLock, dir) {
592
1326
  if (!replacement) continue;
593
1327
  const nestedKey = `${key}/${dependency}`;
594
1328
  packages[nestedKey] = replacement;
595
- dependents.push({
596
- key: nestedKey,
597
- dependencies: getPackageDependencies(replacement)
598
- });
1329
+ dependents.push(toPackageDependent(nestedKey, replacement));
599
1330
  }
600
1331
  }
1332
+ const version = Math.max(lock.lockfileVersion, nestedLock.lockfileVersion);
601
1333
  return {
602
1334
  ...lock,
603
- lockfileVersion: Math.max(lock.lockfileVersion, nestedLock.lockfileVersion),
604
- workspaces: {
605
- ...Object.fromEntries(hostWorkspaces),
606
- ...nestedLock.workspaces
607
- },
1335
+ lockfileVersion: version > 1 && Object.values(packages).some(needsLockfileVersion1) ? 1 : version,
1336
+ workspaces,
608
1337
  packages
609
1338
  };
610
1339
  }
@@ -620,8 +1349,9 @@ function mergeLockfiles(lock, nestedLock, dir) {
620
1349
  */
621
1350
  function keepHostRootResolutions(lock, nestedLock, packages) {
622
1351
  const replaced = /* @__PURE__ */ new Set();
1352
+ const rootRequester = workspaceOverrideRequester(lock, "");
623
1353
  for (const [dependency, specifier] of Object.entries(getWorkspaceDependencies(lock.workspaces[""]))) {
624
- const requirement = getRequirement(lock, dependency, specifier);
1354
+ const requirement = getRequirement(lock, rootRequester, dependency, specifier);
625
1355
  const nestedEntry = Object.hasOwn(nestedLock.packages, dependency) ? nestedLock.packages[dependency] : void 0;
626
1356
  if (!requirement || !nestedEntry || satisfies(nestedEntry, requirement)) continue;
627
1357
  const previous = Object.hasOwn(lock.packages, dependency) ? lock.packages[dependency] : void 0;
@@ -684,11 +1414,12 @@ function getPackageDependencies(entry) {
684
1414
  /**
685
1415
  * Returns the package and version range that a dependency specifier requires.
686
1416
  *
687
- * An override replaces the specifier, as `bun install` applies it to direct and transitive dependencies alike.
1417
+ * An override replaces the specifier, as `bun install` applies it to direct and transitive dependencies alike;
1418
+ * `requester` is the package that declares the dependency.
688
1419
  * Returns `undefined` for specifiers that do not resolve by version, such as `workspace:` or git URLs.
689
1420
  */
690
- function getRequirement(lock, dependency, specifier) {
691
- return parseRequirement(lock, dependency, effectiveDependencyRange(lock, dependency, specifier));
1421
+ function getRequirement(lock, requester, dependency, specifier) {
1422
+ return parseRequirement(lock, dependency, effectiveDependencyRange(lock, requester, dependency, specifier));
692
1423
  }
693
1424
  /** Resolves `catalog:` specifiers and `npm:` aliases of `getRequirement`. */
694
1425
  function parseRequirement(lock, dependency, specifier) {
@@ -715,12 +1446,12 @@ function satisfies(entry, requirement) {
715
1446
  return spec?.name === requirement.name && Bun.semver.satisfies(spec.version, requirement.range);
716
1447
  }
717
1448
  /**
718
- * Returns whether a lockfile entry satisfies the specifier of a dependency on it.
1449
+ * Returns whether a lockfile entry satisfies the specifier of a dependency on it, declared by `requester`.
719
1450
  *
720
1451
  * Specifiers that do not resolve by version, such as `workspace:` or git URLs, are always satisfied.
721
1452
  */
722
- function satisfiesSpecifier(lock, dependency, specifier, entry) {
723
- const requirement = getRequirement(lock, dependency, specifier);
1453
+ function satisfiesSpecifier(lock, requester, dependency, specifier, entry) {
1454
+ const requirement = getRequirement(lock, requester, dependency, specifier);
724
1455
  return requirement === void 0 || satisfies(entry, requirement);
725
1456
  }
726
1457
  /** Splits a lockfile package key into the package names of its nesting chain. */
@@ -742,12 +1473,40 @@ function resolveDependency(lock, workspaceName, dependency) {
742
1473
  return key === void 0 ? void 0 : lock.packages[key][0];
743
1474
  }
744
1475
  // src/check.ts
1476
+ /** Returns the `workspaces` patterns of a root `package.json`. */
1477
+ function getWorkspacePatterns(manifest) {
1478
+ const patterns = (Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? []).map((pattern) => pattern.replace(/^(!?)\.\//, "$1").replace(/\/+$/, ""));
1479
+ return {
1480
+ included: patterns.filter((pattern) => !pattern.startsWith("!")),
1481
+ excluded: patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1))
1482
+ };
1483
+ }
1484
+ /**
1485
+ * Returns whether the patterns include a directory relative to their root, as `bun install` decides it.
1486
+ *
1487
+ * The root itself is never included.
1488
+ * A pattern without glob syntax includes its exact path regardless of exclusions.
1489
+ * A glob pattern matches the `package.json` path of the directory, so `dir/**` includes `dir` itself,
1490
+ * and skips `node_modules`, `.git`, and `CMakeFiles` directories.
1491
+ * An exclusion `dir/**` also excludes `dir` itself.
1492
+ */
1493
+ function matchesWorkspacePatterns(patterns, dir) {
1494
+ const isGlob = (pattern) => /[*?[\]{}]/.test(pattern);
1495
+ const matches = (pattern) => new Bun.Glob(pattern).match(dir);
1496
+ const matchesManifest = (pattern) => new Bun.Glob(path.posix.join(pattern, "package.json")).match(path.posix.join(dir, "package.json"));
1497
+ if (path.posix.normalize(dir) === ".") return false;
1498
+ if (patterns.included.some((pattern) => !isGlob(pattern) && path.posix.normalize(pattern) === dir)) return true;
1499
+ return !dir.split("/").some((part) => ignoredGlobDirs.has(part)) && patterns.included.some((pattern) => isGlob(pattern) && matchesManifest(pattern)) && !patterns.excluded.some((pattern) => matches(pattern) || pattern.endsWith("/**") && matches(pattern.slice(0, -3)));
1500
+ }
1501
+ const ignoredGlobDirs = /* @__PURE__ */ new Set([
1502
+ "node_modules",
1503
+ ".git",
1504
+ "CMakeFiles"
1505
+ ]);
745
1506
  /** Returns the nested workspace paths that the host `workspaces` patterns do not include. */
746
1507
  function findUncoveredWorkspaces(manifest, nestedLock) {
747
- const patterns = (Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? []).map((pattern) => pattern.replace(/^(!?)\.\//, "$1").replace(/\/+$/, ""));
748
- const included = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => new Bun.Glob(pattern));
749
- const excluded = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => new Bun.Glob(pattern.slice(1)));
750
- return Object.keys(nestedLock.workspaces).filter((workspacePath) => !included.some((glob) => glob.match(workspacePath)) || excluded.some((glob) => glob.match(workspacePath)));
1508
+ const patterns = getWorkspacePatterns(manifest);
1509
+ return Object.keys(nestedLock.workspaces).filter((workspacePath) => !matchesWorkspacePatterns(patterns, workspacePath));
751
1510
  }
752
1511
  /**
753
1512
  * Returns the paths of nested workspaces whose host lockfile entries differ from the relocated nested lockfile,
@@ -802,9 +1561,10 @@ function findTransitiveMismatches(lock, nestedLock) {
802
1561
  const { path, nestedKey, hostKey } = queue[index];
803
1562
  const entry = nestedLock.packages[nestedKey];
804
1563
  if (entry[0].includes("@workspace:")) continue;
1564
+ const requester = packageOverrideRequester(nestedLock, entry[0]);
805
1565
  for (const [dependency, specifier] of Object.entries(getPackageDependencies(entry))) {
806
1566
  const nestedDependencyKey = resolveDependencyLockKey(nestedKey, dependency, nestedLock.packages);
807
- if (!nestedDependencyKey || !satisfiesSpecifier(nestedLock, dependency, specifier, nestedLock.packages[nestedDependencyKey])) continue;
1567
+ if (!nestedDependencyKey || !satisfiesSpecifier(nestedLock, requester, dependency, specifier, nestedLock.packages[nestedDependencyKey])) continue;
808
1568
  const hostDependencyKey = resolveDependencyLockKey(hostKey, dependency, lock.packages);
809
1569
  const pair = `${nestedDependencyKey}\0${hostDependencyKey}`;
810
1570
  if (visited.has(pair)) continue;
@@ -836,6 +1596,7 @@ function findTransitiveMismatches(lock, nestedLock) {
836
1596
  */
837
1597
  function findOwnDependencyChanges(before, after, nestedLock, dir) {
838
1598
  return [...new Map([...Object.entries(before.workspaces), ...Object.entries(after.workspaces)].filter(([workspacePath]) => !isNestedPath(workspacePath, dir)))].flatMap(([workspacePath, workspace]) => {
1599
+ const requester = workspaceOverrideRequester(Object.hasOwn(after.workspaces, workspacePath) ? after : before, workspacePath);
839
1600
  const specifiers = {
840
1601
  ...getWorkspaceDependencies(before.workspaces[workspacePath] ?? {}),
841
1602
  ...getWorkspaceDependencies(after.workspaces[workspacePath] ?? {})
@@ -845,7 +1606,7 @@ function findOwnDependencyChanges(before, after, nestedLock, dir) {
845
1606
  const expected = resolveDependency(before, workspace.name, dependency);
846
1607
  const actual = resolveDependency(after, workspace.name, dependency);
847
1608
  const nestedEntry = nestedLock.packages[dependency];
848
- if (actual === expected || actual !== void 0 && actual === nestedEntry?.[0] && satisfiesSpecifier(after, dependency, specifier, nestedEntry)) return [];
1609
+ if (actual === expected || actual !== void 0 && actual === nestedEntry?.[0] && satisfiesSpecifier(after, requester, dependency, specifier, nestedEntry)) return [];
849
1610
  return [{
850
1611
  workspace: workspace.name ?? workspacePath,
851
1612
  dependency,
@@ -891,6 +1652,33 @@ async function loadNestedWorkspace(root) {
891
1652
  nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir, typeof nestedManifest.name === "string" ? nestedManifest.name : void 0)
892
1653
  };
893
1654
  }
1655
+ /**
1656
+ * Returns whether a workspace path of the host lockfile is a workspace of the nested repository:
1657
+ * its root, a workspace of the nested lockfile, or a directory that the nested `workspaces` patterns include.
1658
+ */
1659
+ function isNestedWorkspace(workspace, workspacePath) {
1660
+ const { dir } = workspace.config;
1661
+ return workspacePath === dir || workspacePath in workspace.nestedLock.workspaces || isNestedPath(workspacePath, dir) && matchesWorkspacePatterns(getWorkspacePatterns(workspace.nestedManifest), workspacePath.slice(dir.length + 1));
1662
+ }
1663
+ /**
1664
+ * Returns the first directory with a named package that the nested `workspaces` patterns include, relative to the host root.
1665
+ *
1666
+ * `bun install` treats the nested root as the workspace root there and writes the nested lockfile,
1667
+ * while in the nested root itself it writes the host lockfile.
1668
+ */
1669
+ async function findNestedPackageDir(workspace) {
1670
+ const nestedRoot = path.join(workspace.root, workspace.config.dir);
1671
+ const patterns = getWorkspacePatterns(workspace.nestedManifest);
1672
+ const dirs = /* @__PURE__ */ new Set();
1673
+ for (const pattern of patterns.included) for await (const file of new Bun.Glob(`${pattern}/package.json`).scan(nestedRoot)) {
1674
+ const dir = path.posix.normalize(path.posix.dirname(file.split(path.sep).join("/")));
1675
+ if (matchesWorkspacePatterns(patterns, dir)) dirs.add(dir);
1676
+ }
1677
+ for (const dir of [...dirs].sort()) {
1678
+ const manifest = await Bun.file(path.join(nestedRoot, dir, "package.json")).json();
1679
+ if (typeof manifest.name === "string" && manifest.name) return `${workspace.config.dir}/${dir}`;
1680
+ }
1681
+ }
894
1682
  /** Returns whether a check found no differences. */
895
1683
  function isInSync(result) {
896
1684
  return Object.values(result).every((list) => !list.length);
@@ -951,27 +1739,75 @@ async function runBunInstall(root, args = []) {
951
1739
  *
952
1740
  * Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
953
1741
  * and then compares the result with the nested checkout and with the original host resolutions.
1742
+ * When `bun install` fails or the result differs, restores both files before it throws or returns,
1743
+ * so that the next sync starts from the original host resolutions.
1744
+ * Throws an error that says so when it cannot restore them.
954
1745
  */
955
1746
  async function syncNestedWorkspace(root, options = {}) {
956
1747
  const workspace = await loadNestedWorkspace(root);
957
1748
  assertWorkspacesCovered(workspace);
958
1749
  const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
959
1750
  const { manifest } = manifestSync;
1751
+ const { dir, fields } = workspace.config;
1752
+ assertOverridesRecorded(workspace.manifest, workspace.lock, {
1753
+ manifest: "package.json",
1754
+ lock: "bun.lock"
1755
+ });
1756
+ if (fields.includes("overrides")) assertOverridesRecorded(workspace.nestedManifest, workspace.nestedLock, {
1757
+ manifest: `${dir}/package.json`,
1758
+ lock: `${dir}/bun.lock`
1759
+ });
960
1760
  const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
961
- const manifestText = formatJson(manifest, workspace.manifestText);
962
- if (manifestText !== workspace.manifestText) await Bun.write(path.join(root, "package.json"), manifestText);
963
- options.onManifestSync?.(manifestSync);
964
- await Bun.write(path.join(root, "bun.lock"), renderBunLock(merged));
965
- await runBunInstall(root, options.installArgs);
966
- const synced = await loadNestedWorkspace(root);
967
- return {
968
- ownDependencyChanges: findOwnDependencyChanges(workspace.lock, synced.lock, workspace.nestedLock, workspace.config.dir),
969
- check: checkNestedWorkspace(synced)
1761
+ const manifestPath = path.join(root, "package.json");
1762
+ const lockPath = path.join(root, "bun.lock");
1763
+ const originals = /* @__PURE__ */ new Map([[manifestPath, workspace.manifestText], [lockPath, await Bun.file(lockPath).text()]]);
1764
+ const restore = async () => {
1765
+ const failures = await restoreFiles(originals);
1766
+ if (failures.length) throw new Error([
1767
+ "The sync failed and could not restore the host files:",
1768
+ ...failures.map(({ file, error }) => ` ${path.basename(file)}: ${getErrorMessage(error)}`),
1769
+ "Restore them before running the sync again."
1770
+ ].join("\n"));
970
1771
  };
1772
+ let result;
1773
+ try {
1774
+ const manifestText = formatJson(manifest, workspace.manifestText);
1775
+ if (manifestText !== workspace.manifestText) await Bun.write(manifestPath, manifestText);
1776
+ options.onManifestSync?.(manifestSync);
1777
+ await Bun.write(lockPath, renderBunLock(merged));
1778
+ await runBunInstall(root, options.installArgs);
1779
+ const synced = await loadNestedWorkspace(root);
1780
+ result = {
1781
+ ownDependencyChanges: findOwnDependencyChanges(workspace.lock, synced.lock, workspace.nestedLock, workspace.config.dir),
1782
+ check: checkNestedWorkspace(synced)
1783
+ };
1784
+ } catch (error) {
1785
+ await restore();
1786
+ throw error;
1787
+ }
1788
+ if (result.ownDependencyChanges.length || !isInSync(result.check)) await restore();
1789
+ return result;
1790
+ }
1791
+ /**
1792
+ * Writes back the original texts of files whose contents differ from them and returns the files it could not restore.
1793
+ *
1794
+ * Restores each file separately, so that a failure does not keep the other files changed.
1795
+ */
1796
+ async function restoreFiles(originals) {
1797
+ const failures = [];
1798
+ for (const [file, text] of originals) try {
1799
+ if (await Bun.file(file).text().catch(() => void 0) !== text) await Bun.write(file, text);
1800
+ } catch (error) {
1801
+ failures.push({
1802
+ file,
1803
+ error
1804
+ });
1805
+ }
1806
+ return failures;
971
1807
  }
972
1808
  /** Serializes JSON with the indentation and final newline of the original text. */
973
1809
  function formatJson(value, original) {
974
1810
  const indent = /^[ \t]+(?=")/m.exec(original)?.[0] ?? " ";
975
1811
  return `${JSON.stringify(value, null, indent)}${original.endsWith("\n") ? "\n" : ""}`;
976
1812
  }
977
- export { syncManifest as _, syncNestedWorkspace as a, findTransitiveMismatches as c, applyManifestToLockfile as d, mergeLockfiles as f, diffManifests as g, supportedLockfileVersions as h, loadNestedWorkspace as i, findUncoveredWorkspaces as l, relocateLockfile as m, checkNestedWorkspace as n, findDirectDependencyMismatches as o, parseLockfile as p, isInSync as r, findOwnDependencyChanges as s, BunInstallError as t, findWorkspaceMismatches as u, syncedFields as v, getErrorMessage as y };
1813
+ export { supportedLockfileVersions as _, isNestedWorkspace as a, syncedFields as b, findDirectDependencyMismatches as c, findUncoveredWorkspaces as d, findWorkspaceMismatches as f, relocateLockfile as g, parseLockfile as h, isInSync as i, findOwnDependencyChanges as l, mergeLockfiles as m, checkNestedWorkspace as n, loadNestedWorkspace as o, applyManifestToLockfile as p, findNestedPackageDir as r, syncNestedWorkspace as s, BunInstallError as t, findTransitiveMismatches as u, diffManifests as v, getErrorMessage as x, syncManifest as y };