bun-nested-workspace 0.4.0 → 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.
package/README.md CHANGED
@@ -99,7 +99,9 @@ Exclude `node_modules` and other large directories from the build context with `
99
99
 
100
100
  - `catalog` and `catalogs` each stay where the host defines them; a field the host lacks goes next to the other catalog field, or into `workspaces`.
101
101
  An array `workspaces` field becomes an object when a catalog needs it.
102
- - `overrides` and `trustedDependencies` are merged by package name.
102
+ - `overrides` are merged by their keys, so a group such as `"micromatch": { "picomatch": "^2.3.2" }` replaces the host group with the same key as a whole.
103
+ The sync requires each `bun.lock` to be up to date with the overrides of its `package.json`.
104
+ - `trustedDependencies` are merged by package name.
103
105
  - `resolutions` on either side is an error while `overrides` are synced: Bun ignores `resolutions` once `overrides` is defined, so move its entries to `overrides`.
104
106
  - `trustedDependencies` stays in the host `package.json`, even empty, when either side defines it.
105
107
  An explicit list replaces the default trusted list of Bun, so when only one side defines the field, the packages of the other side lose the default list; list the ones whose lifecycle scripts they need.
@@ -107,7 +109,7 @@ Exclude `node_modules` and other large directories from the build context with `
107
109
  - `patchedDependencies` paths are rewritten through the nested directory, such as `nested/patches/exceljs@4.4.0.patch`.
108
110
  Bun warns on every install that it ignores `patchedDependencies` of the nested root package; keep them there, because the host applies the synced copies.
109
111
 
110
- A nested entry replaces a host entry with the same name, while the other host entries stay.
112
+ A nested entry replaces a host entry with the same key, while the other host entries stay.
111
113
  The sync lists the host entries it replaced.
112
114
  A host entry equal to the nested one stays the host's own, so it survives when the nested repository drops it.
113
115
  An entry that came from the nested repository and is no longer there is removed; for patches, these are the patches inside the nested directory.
@@ -126,7 +128,7 @@ A dependency may only change to the version that the nested lockfile hoists to i
126
128
  Any other change means either that the host lockfile did not pin the host dependencies, for example after an edit of a host `package.json` without `bun install`, or that the lockfile cannot record the host version next to the nested one.
127
129
  The latter happens to a host workspace other than the root without a name, because Bun records workspace-specific versions under the workspace name; name the workspace or align its range with the nested version.
128
130
 
129
- Only `bun.lock` with `lockfileVersion` 1 or 2 is supported, because the lockfile format is internal to Bun.
131
+ Only `bun.lock` with `lockfileVersion` 1, 2, or 3 is supported, because the lockfile format is internal to Bun.
130
132
 
131
133
  ## Check
132
134
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { a as isNestedWorkspace, i as isInSync, n as checkNestedWorkspace, o as loadNestedWorkspace, r as findNestedPackageDir, s as syncNestedWorkspace, t as BunInstallError, x as getErrorMessage } from "./workspace-DOwKm8pX.mjs";
2
+ import { a as isNestedWorkspace, i as isInSync, n as checkNestedWorkspace, o as loadNestedWorkspace, r as findNestedPackageDir, s as syncNestedWorkspace, t as BunInstallError, x as getErrorMessage } from "./workspace-CC5KeN8i.mjs";
3
3
  // src/cli.ts
4
4
  const usage = `Usage:
5
5
  bun-nested-workspace [bun install options] Sync package.json and bun.lock, then run bun install
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import semver from "semver";
1
2
  type DependencyMap = Record<string, string>;
2
3
  type BunLockWorkspace = {
3
4
  /**
@@ -5,6 +6,8 @@ type BunLockWorkspace = {
5
6
  * context for workspace-scoped lock keys.
6
7
  */
7
8
  name?: string;
9
+ /** Version of a workspace package; Bun leaves it out for the root workspace. */
10
+ version?: string;
8
11
  dependencies?: DependencyMap;
9
12
  devDependencies?: DependencyMap;
10
13
  optionalDependencies?: DependencyMap;
@@ -48,8 +51,12 @@ type BunLockFile = {
48
51
  catalog?: DependencyMap;
49
52
  /** Named catalogs of the root `package.json`. */
50
53
  catalogs?: Record<string, DependencyMap>;
51
- /** Canonical map Bun writes for package.json overrides and resolutions. */
52
- overrides?: DependencyMap;
54
+ /**
55
+ * Canonical map Bun writes for package.json overrides and resolutions.
56
+ *
57
+ * A string value overrides a package by name; an object value groups the rules scoped to a package or its range.
58
+ */
59
+ overrides?: Record<string, string | DependencyMap>;
53
60
  /** Package specs whose local patches may change their dependency declarations. */
54
61
  patchedDependencies?: Record<string, string>;
55
62
  packages?: Record<string, BunPackageEntry>;
@@ -90,6 +97,8 @@ interface ResolvedConfig {
90
97
  synced: SyncedEntries;
91
98
  }
92
99
  type Catalog = Record<string, string>;
100
+ /** Value of an `overrides` entry: a version spec, or a group of rules keyed by `"."` and dependency selectors. */
101
+ type OverrideValue = string | Record<string, string>;
93
102
  /** Root `package.json` fields this package reads or rewrites; other fields are preserved as is. */
94
103
  interface PackageManifest {
95
104
  dependencies?: Record<string, string>;
@@ -99,7 +108,7 @@ interface PackageManifest {
99
108
  workspaces?: string[] | WorkspacesConfig;
100
109
  catalog?: Catalog;
101
110
  catalogs?: Record<string, Catalog>;
102
- overrides?: Record<string, string>;
111
+ overrides?: Record<string, OverrideValue>;
103
112
  resolutions?: Record<string, string>;
104
113
  patchedDependencies?: Record<string, string>;
105
114
  trustedDependencies?: string[];
@@ -118,8 +127,8 @@ interface OverriddenEntry {
118
127
  /** Field path, such as `catalog`, `catalogs.testing`, or `overrides`. */
119
128
  field: string;
120
129
  name: string;
121
- hostValue: string;
122
- nestedValue: string;
130
+ hostValue: OverrideValue;
131
+ nestedValue: OverrideValue;
123
132
  }
124
133
  /** Result of `syncManifest`. */
125
134
  interface ManifestSync {
@@ -136,11 +145,13 @@ interface ManifestSync {
136
145
  /**
137
146
  * Returns the host root `package.json` with the configured root fields of the nested `package.json` merged in.
138
147
  *
139
- * A nested entry replaces a host entry with the same name, while the other host entries stay.
148
+ * A nested entry replaces a host entry with the same key, while the other host entries stay.
140
149
  * Entries synced earlier and dropped by the nested repository are removed.
141
150
  * Throws when the host and the nested repository patch the same package version,
142
151
  * when either side defines `resolutions` while `overrides` are synced,
143
- * and when a synced `$name` override would refer to another range in the host root than in the nested root.
152
+ * when an override is one that `readManifestOverrides` rejects, when two overrides select the same packages,
153
+ * when a synced `$name` override would refer to another range in the host root than in the nested root,
154
+ * and when a synced override selects the nested root by a range that its version satisfies.
144
155
  */
145
156
  export declare function syncManifest(host: PackageManifest, nested: PackageManifest): ManifestSync;
146
157
  /** Difference between the expected and the actual value at a `package.json` path. */
@@ -184,7 +195,7 @@ export declare function relocateLockfile(lock: Lockfile, dir: string, rootName?:
184
195
  *
185
196
  * `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
186
197
  * so the merged lockfile must already record the synced root fields to keep the nested resolutions.
187
- * Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
198
+ * Records overrides in the form that Bun writes to `bun.lock`, with `$name` values replaced by the range they refer to.
188
199
  */
189
200
  export declare function applyManifestToLockfile(lock: Lockfile, manifest: PackageManifest): Lockfile;
190
201
  /**
@@ -199,8 +210,9 @@ export declare function applyManifestToLockfile(lock: Lockfile, manifest: Packag
199
210
  * so `bun install` resolves its dependency again.
200
211
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
201
212
  * the dependency gets the highest satisfying version recorded in either lockfile.
202
- * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
213
+ * Dependencies are checked against the catalogs and overrides of the host lockfile, so apply the synced root fields
203
214
  * to it first.
215
+ * The result has the higher `lockfileVersion` of the two, or version 1 when it has an entry that needs that version.
204
216
  */
205
217
  export declare function mergeLockfiles(lock: Lockfile, nestedLock: Lockfile, dir: string): Lockfile;
206
218
  /** Dependency of a workspace package resolved differently than expected. */
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as BunInstallError, p as applyManifestToLockfile, n as checkNestedWorkspace, v as diffManifests, c as findDirectDependencyMismatches, l as findOwnDependencyChanges, u as findTransitiveMismatches, d as findUncoveredWorkspaces, f as findWorkspaceMismatches, i as isInSync, o as loadNestedWorkspace, m as mergeLockfiles, h as parseLockfile, g as relocateLockfile, _ as supportedLockfileVersions, y as syncManifest, s as syncNestedWorkspace, b as syncedFields } from "./workspace-DOwKm8pX.mjs";
1
+ export { t as BunInstallError, p as applyManifestToLockfile, n as checkNestedWorkspace, v as diffManifests, c as findDirectDependencyMismatches, l as findOwnDependencyChanges, u as findTransitiveMismatches, d as findUncoveredWorkspaces, f as findWorkspaceMismatches, i as isInSync, o as loadNestedWorkspace, m as mergeLockfiles, h as parseLockfile, g as relocateLockfile, _ as supportedLockfileVersions, y as syncManifest, s as syncNestedWorkspace, b as syncedFields } from "./workspace-CC5KeN8i.mjs";
@@ -1,6 +1,197 @@
1
1
  import path from "node:path";
2
2
  import JSON5 from "json5";
3
3
  import semver from "semver";
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
673
  */
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);
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. */
@@ -830,9 +1561,10 @@ function findTransitiveMismatches(lock, nestedLock) {
830
1561
  const { path, nestedKey, hostKey } = queue[index];
831
1562
  const entry = nestedLock.packages[nestedKey];
832
1563
  if (entry[0].includes("@workspace:")) continue;
1564
+ const requester = packageOverrideRequester(nestedLock, entry[0]);
833
1565
  for (const [dependency, specifier] of Object.entries(getPackageDependencies(entry))) {
834
1566
  const nestedDependencyKey = resolveDependencyLockKey(nestedKey, dependency, nestedLock.packages);
835
- if (!nestedDependencyKey || !satisfiesSpecifier(nestedLock, dependency, specifier, nestedLock.packages[nestedDependencyKey])) continue;
1567
+ if (!nestedDependencyKey || !satisfiesSpecifier(nestedLock, requester, dependency, specifier, nestedLock.packages[nestedDependencyKey])) continue;
836
1568
  const hostDependencyKey = resolveDependencyLockKey(hostKey, dependency, lock.packages);
837
1569
  const pair = `${nestedDependencyKey}\0${hostDependencyKey}`;
838
1570
  if (visited.has(pair)) continue;
@@ -864,6 +1596,7 @@ function findTransitiveMismatches(lock, nestedLock) {
864
1596
  */
865
1597
  function findOwnDependencyChanges(before, after, nestedLock, dir) {
866
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);
867
1600
  const specifiers = {
868
1601
  ...getWorkspaceDependencies(before.workspaces[workspacePath] ?? {}),
869
1602
  ...getWorkspaceDependencies(after.workspaces[workspacePath] ?? {})
@@ -873,7 +1606,7 @@ function findOwnDependencyChanges(before, after, nestedLock, dir) {
873
1606
  const expected = resolveDependency(before, workspace.name, dependency);
874
1607
  const actual = resolveDependency(after, workspace.name, dependency);
875
1608
  const nestedEntry = nestedLock.packages[dependency];
876
- 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 [];
877
1610
  return [{
878
1611
  workspace: workspace.name ?? workspacePath,
879
1612
  dependency,
@@ -1015,6 +1748,15 @@ async function syncNestedWorkspace(root, options = {}) {
1015
1748
  assertWorkspacesCovered(workspace);
1016
1749
  const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
1017
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
+ });
1018
1760
  const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
1019
1761
  const manifestPath = path.join(root, "package.json");
1020
1762
  const lockPath = path.join(root, "bun.lock");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-nested-workspace",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Embed a checkout of another Bun workspace repository into a Bun workspace and keep the root package.json and bun.lock in sync with it",
5
5
  "keywords": [
6
6
  "bun",