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 +5 -3
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +21 -9
- package/dist/index.mjs +1 -1
- package/dist/{workspace-DOwKm8pX.mjs → workspace-CC5KeN8i.mjs} +809 -67
- package/package.json +1 -1
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`
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
/**
|
|
52
|
-
|
|
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,
|
|
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:
|
|
122
|
-
nestedValue:
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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-
|
|
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
|
|
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
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
374
|
-
|
|
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(`${
|
|
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
|
|
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
|
|
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) =>
|
|
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 = [
|
|
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
|
|
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 &&
|
|
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
|
-
*
|
|
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:
|
|
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