bun-nested-workspace 0.2.1 → 0.3.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 +26 -10
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +8 -2
- package/dist/index.mjs +1 -1
- package/dist/{workspace-DOgB-vkj.mjs → workspace-DfVz3A2J.mjs} +116 -39
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
Embed a checkout of another Bun workspace repository into a subdirectory of your Bun workspace, and keep the root `package.json` and `bun.lock` of the host in sync with it.
|
|
4
4
|
|
|
5
5
|
The nested repository stays a self-contained project with its own root `package.json` and `bun.lock`.
|
|
6
|
-
Its packages, such as `nested/apps/*` and `nested/packages/*`, become workspaces of the host
|
|
7
|
-
The host installs none of the dependencies of the nested root `package.json` and cannot run its scripts.
|
|
6
|
+
Its root and its packages, such as `nested/apps/*` and `nested/packages/*`, become workspaces of the host.
|
|
8
7
|
Bun reads catalogs, overrides, patches, and trusted dependencies from the workspace root only, and resolves the whole workspace from a single lockfile.
|
|
9
8
|
This package carries those root fields over to the host and merges the nested lockfile into the host lockfile, so the nested packages build in the host with the same dependency versions as on their own.
|
|
10
9
|
|
|
@@ -16,22 +15,22 @@ The CLI runs before the host dependencies are installed, so run it with `bunx`:
|
|
|
16
15
|
bunx bun-nested-workspace
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
It requires Bun 1.
|
|
18
|
+
It requires Bun 1.4.4 or later, the first version that keeps a dependency of the host root package at its own version when the nested lockfile resolves the same package to another version.
|
|
20
19
|
|
|
21
20
|
## Setup
|
|
22
21
|
|
|
23
|
-
Add the nested packages to the host workspaces and name the nested directory in the `bun-nested-workspace` field of the host `package.json`:
|
|
22
|
+
Add the nested directory and its packages to the host workspaces and name the nested directory in the `bun-nested-workspace` field of the host `package.json`:
|
|
24
23
|
|
|
25
24
|
```json
|
|
26
25
|
{
|
|
27
|
-
"workspaces": ["apps/*", "nested/apps/*", "nested/packages/*"],
|
|
26
|
+
"workspaces": ["apps/*", "nested", "nested/apps/*", "nested/packages/*"],
|
|
28
27
|
"bun-nested-workspace": {
|
|
29
28
|
"dir": "nested"
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
Both modes fail when the host `workspaces` patterns miss a workspace package
|
|
33
|
+
Both modes fail when the host `workspaces` patterns miss the nested root or a nested workspace package, or when the nested root `package.json` has no `name`.
|
|
35
34
|
|
|
36
35
|
The config takes these fields:
|
|
37
36
|
|
|
@@ -39,6 +38,24 @@ The config takes these fields:
|
|
|
39
38
|
- `fields`: the root fields to sync, a subset of `catalog`, `catalogs`, `overrides`, `patchedDependencies`, and `trustedDependencies`; all of them when omitted.
|
|
40
39
|
- `synced`: the names of entries that came from the nested repository, maintained by the sync; commit it and do not edit it by hand.
|
|
41
40
|
|
|
41
|
+
## Nested checkout
|
|
42
|
+
|
|
43
|
+
`bun install` in the host installs the dependencies of the nested root `package.json` in the versions of the nested lockfile, so the scripts and Git hooks of the nested repository work in the checkout.
|
|
44
|
+
It also runs the `prepare` script of the nested root.
|
|
45
|
+
|
|
46
|
+
Change the dependencies of the nested repository by editing its `package.json` files.
|
|
47
|
+
Then update the nested lockfile with `bun install --lockfile-only` in the directory of any nested workspace package, and run the sync in the host:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
(cd nested/packages/core && bun install --lockfile-only)
|
|
51
|
+
bunx bun-nested-workspace
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Do not run other `bun install`, `bun add`, or `bun remove` commands in the nested directory or in its package directories:
|
|
55
|
+
|
|
56
|
+
- In the nested root, Bun runs them for the host: it writes the host lockfile and leaves the nested one stale.
|
|
57
|
+
- In a nested package directory, Bun installs a separate `node_modules` of the nested repository over the host install.
|
|
58
|
+
|
|
42
59
|
## Sync
|
|
43
60
|
|
|
44
61
|
Run the CLI in the host root after switching the nested checkout to another commit:
|
|
@@ -57,6 +74,7 @@ The sync:
|
|
|
57
74
|
Commit `package.json` and `bun.lock` after the sync.
|
|
58
75
|
|
|
59
76
|
In a Docker build, run the sync with `--lockfile-only` in a stage that contains only the dependency files, before installing dependencies.
|
|
77
|
+
The dependency install runs the `prepare` script of the nested root unless it gets `--ignore-scripts`.
|
|
60
78
|
`bun install` also needs the patch files and the install configuration, such as `bunfig.toml` and `.npmrc`, so keep them, along with any files they reference.
|
|
61
79
|
A separate stage can strip the build context down to these files, so the Dockerfile does not list every workspace package:
|
|
62
80
|
|
|
@@ -91,8 +109,6 @@ A host entry equal to the nested one stays the host's own, so it survives when t
|
|
|
91
109
|
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.
|
|
92
110
|
A package version patched by both the host and the nested repository is an error.
|
|
93
111
|
|
|
94
|
-
Bun warns when a nested patch targets a package version that the host does not install, such as a patched tool used only by the nested root.
|
|
95
|
-
|
|
96
112
|
### Lockfile
|
|
97
113
|
|
|
98
114
|
The nested lockfile entries replace the host entries, so the nested packages keep their versions.
|
|
@@ -102,7 +118,7 @@ Ranges are checked as `bun install` applies them: an override replaces the decla
|
|
|
102
118
|
`bun install` then resolves new dependencies of host packages and drops unused entries.
|
|
103
119
|
|
|
104
120
|
After the install, the sync compares the direct dependencies of host packages with the original host lockfile.
|
|
105
|
-
A dependency may only change to the version that the nested lockfile
|
|
121
|
+
A dependency may only change to the version that the nested lockfile hoists to its top level, and only within the host range.
|
|
106
122
|
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.
|
|
107
123
|
The latter happens to a host workspace without a name, because Bun records workspace-specific versions under the workspace name; name the workspace or align its range with the nested version.
|
|
108
124
|
|
|
@@ -115,7 +131,7 @@ bunx bun-nested-workspace --check
|
|
|
115
131
|
```
|
|
116
132
|
|
|
117
133
|
The check writes nothing, does not run `bun install`, and exits with code 1 when the host files lag behind the nested checkout.
|
|
118
|
-
It reads the root `package.json` and `bun.lock` of the host and of the nested checkout only, so it relies on the nested `bun.lock` being up to date with the nested packages; `bun install --frozen-lockfile` in the nested
|
|
134
|
+
It reads the root `package.json` and `bun.lock` of the host and of the nested checkout only, so it relies on the nested `bun.lock` being up to date with the nested packages; `bun install --frozen-lockfile` in the CI of the nested repository verifies that.
|
|
119
135
|
Use it in a non-blocking CI job that signals when a sync commit is due.
|
|
120
136
|
|
|
121
137
|
It reports:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { a as syncNestedWorkspace, i as loadNestedWorkspace, n as checkNestedWorkspace, r as isInSync, t as BunInstallError, y as getErrorMessage } from "./workspace-
|
|
2
|
+
import { a as syncNestedWorkspace, i as loadNestedWorkspace, n as checkNestedWorkspace, r as isInSync, t as BunInstallError, y as getErrorMessage } from "./workspace-DfVz3A2J.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
|
|
@@ -52,7 +52,7 @@ async function runSync(root, installArgs) {
|
|
|
52
52
|
console.error([
|
|
53
53
|
"The sync changed own dependencies of host packages.",
|
|
54
54
|
"Either the host bun.lock did not pin them: restore bun.lock, run bun install, commit bun.lock, and sync again.",
|
|
55
|
-
"Or the nested
|
|
55
|
+
"Or the nested lockfile resolves them outside the host range: align the host range with the nested version.",
|
|
56
56
|
...ownDependencyChanges.map(({ workspace, dependency, expected, actual }) => formatChange(`${workspace} > ${dependency}`, expected, actual))
|
|
57
57
|
].join("\n"));
|
|
58
58
|
exitCode = 1;
|
package/dist/index.d.mts
CHANGED
|
@@ -92,6 +92,10 @@ interface ResolvedConfig {
|
|
|
92
92
|
type Catalog = Record<string, string>;
|
|
93
93
|
/** Root `package.json` fields this package reads or rewrites; other fields are preserved as is. */
|
|
94
94
|
interface PackageManifest {
|
|
95
|
+
dependencies?: Record<string, string>;
|
|
96
|
+
devDependencies?: Record<string, string>;
|
|
97
|
+
optionalDependencies?: Record<string, string>;
|
|
98
|
+
peerDependencies?: Record<string, string>;
|
|
95
99
|
workspaces?: string[] | WorkspacesConfig;
|
|
96
100
|
catalog?: Catalog;
|
|
97
101
|
catalogs?: Record<string, Catalog>;
|
|
@@ -135,7 +139,8 @@ interface ManifestSync {
|
|
|
135
139
|
* A nested entry replaces a host entry with the same name, while the other host entries stay.
|
|
136
140
|
* Entries synced earlier and dropped by the nested repository are removed.
|
|
137
141
|
* Throws when the host and the nested repository patch the same package version,
|
|
138
|
-
*
|
|
142
|
+
* 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.
|
|
139
144
|
*/
|
|
140
145
|
export declare function syncManifest(host: PackageManifest, nested: PackageManifest): ManifestSync;
|
|
141
146
|
/** Difference between the expected and the actual value at a `package.json` path. */
|
|
@@ -168,7 +173,7 @@ export declare function parseLockfile(text: string, source?: string): Lockfile;
|
|
|
168
173
|
/**
|
|
169
174
|
* Returns the nested lockfile with workspace and local paths relative to the host root.
|
|
170
175
|
*
|
|
171
|
-
* The nested root workspace
|
|
176
|
+
* The nested root workspace moves to the nested directory, where it is a workspace of the host.
|
|
172
177
|
*/
|
|
173
178
|
export declare function relocateLockfile(lock: Lockfile, dir: string): Lockfile;
|
|
174
179
|
/**
|
|
@@ -176,6 +181,7 @@ export declare function relocateLockfile(lock: Lockfile, dir: string): Lockfile;
|
|
|
176
181
|
*
|
|
177
182
|
* `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
|
|
178
183
|
* so the merged lockfile must already record the synced root fields to keep the nested resolutions.
|
|
184
|
+
* Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
|
|
179
185
|
*/
|
|
180
186
|
export declare function applyManifestToLockfile(lock: Lockfile, manifest: PackageManifest): Lockfile;
|
|
181
187
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as BunInstallError, d as applyManifestToLockfile, n as checkNestedWorkspace, g as diffManifests, o as findDirectDependencyMismatches, s as findOwnDependencyChanges, c as findTransitiveMismatches, l as findUncoveredWorkspaces, u as findWorkspaceMismatches, r as isInSync, i as loadNestedWorkspace, f as mergeLockfiles, p as parseLockfile, m as relocateLockfile, h as supportedLockfileVersions, _ as syncManifest, a as syncNestedWorkspace, v as syncedFields } from "./workspace-
|
|
1
|
+
export { t as BunInstallError, d as applyManifestToLockfile, n as checkNestedWorkspace, g as diffManifests, o as findDirectDependencyMismatches, s as findOwnDependencyChanges, c as findTransitiveMismatches, l as findUncoveredWorkspaces, u as findWorkspaceMismatches, r as isInSync, i as loadNestedWorkspace, f as mergeLockfiles, p as parseLockfile, m as relocateLockfile, h as supportedLockfileVersions, _ as syncManifest, a as syncNestedWorkspace, v as syncedFields } from "./workspace-DfVz3A2J.mjs";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import JSON5 from "json5";
|
|
2
|
+
import semver from "semver";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
// ../lockfile/src/parse.ts
|
|
4
5
|
function isObject(value) {
|
|
@@ -13,11 +14,14 @@ function packageEntryMeta(entry) {
|
|
|
13
14
|
return isObject(maybeMeta) ? maybeMeta : void 0;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
|
-
* Splits Bun's resolved package spec (`name@version`)
|
|
17
|
-
*
|
|
17
|
+
* Splits Bun's resolved package spec (`name@version`) at the first `@` after
|
|
18
|
+
* the leading `@` of a scoped name, as Bun does.
|
|
19
|
+
*
|
|
20
|
+
* The version part may contain `@` itself, as in a git, tarball, or workspace
|
|
21
|
+
* source such as `pkg@git+ssh://git@host/repo.git#sha`.
|
|
18
22
|
*/
|
|
19
23
|
function parseResolvedSpec(spec) {
|
|
20
|
-
const atIndex = spec.
|
|
24
|
+
const atIndex = spec.indexOf("@", 1);
|
|
21
25
|
if (atIndex <= 0 || atIndex >= spec.length - 1) return null;
|
|
22
26
|
const name = spec.slice(0, atIndex);
|
|
23
27
|
const version = spec.slice(atIndex + 1);
|
|
@@ -47,17 +51,67 @@ function parseBunLock(lockText) {
|
|
|
47
51
|
}
|
|
48
52
|
// ../lockfile/src/overrides.ts
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
54
|
+
* Return the package and range that an `npm:` alias specifier installs, or `undefined` for any other specifier.
|
|
55
|
+
*
|
|
56
|
+
* Follows Bun: `npm:<name>@<version>` always names a package.
|
|
57
|
+
* Without a version, a target with a range operator or an exact version, such as `npm:^1.0.0`, `npm:1.0.0`, or
|
|
58
|
+
* `npm:v1.0.0`, is a range of the dependency itself; any other target, such as `npm:x`, `npm:1`, or `npm:1.x`, is a
|
|
59
|
+
* package name.
|
|
60
|
+
*/
|
|
61
|
+
function npmAliasTarget(specifier) {
|
|
62
|
+
if (!specifier.startsWith("npm:")) return void 0;
|
|
63
|
+
const target = specifier.slice(4);
|
|
64
|
+
const separator = target.indexOf("@", 1);
|
|
65
|
+
if (separator !== -1) return {
|
|
66
|
+
name: target.slice(0, separator),
|
|
67
|
+
range: target.slice(separator + 1)
|
|
68
|
+
};
|
|
69
|
+
const isRange = /[\^~><=*| ]/.test(target) || /^v?\d+\.\d+\.\d+(?:[-+][\w.+-]*)?$/i.test(target);
|
|
70
|
+
return target && !isRange ? {
|
|
71
|
+
name: target,
|
|
72
|
+
range: "*"
|
|
73
|
+
} : void 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Return the package and version range that a dependency specifier requires.
|
|
77
|
+
*
|
|
78
|
+
* An `npm:` alias requires the aliased package, and `npm:<range>` is a range of the dependency itself.
|
|
79
|
+
*/
|
|
80
|
+
function dependencyRequirement(dependencyName, specifier) {
|
|
81
|
+
const alias = npmAliasTarget(specifier);
|
|
82
|
+
if (alias) return alias;
|
|
83
|
+
return {
|
|
84
|
+
name: dependencyName,
|
|
85
|
+
range: specifier.startsWith("npm:") ? specifier.slice(4) : specifier
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read the bare-name override that Bun applies to a dependency declaration.
|
|
51
90
|
*
|
|
91
|
+
* Bun never overrides an `npm:` alias of a semver range.
|
|
92
|
+
* It overrides an alias of a dist-tag, such as `npm:debug@latest`, by the name of the aliased package,
|
|
93
|
+
* so the override is returned as an alias of that package.
|
|
52
94
|
* `parseBunLock` rejects overrides scoped to a parent package or a version selector.
|
|
53
95
|
* Bun versions before 1.4 keep selector keys as strings without applying them,
|
|
54
96
|
* so matching those here would diverge from the package manager.
|
|
55
97
|
*/
|
|
56
|
-
function dependencyOverrideRange(lock, dependencyName) {
|
|
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);
|
|
103
|
+
if (override === void 0) return void 0;
|
|
104
|
+
const requirement = dependencyRequirement(alias.name, override);
|
|
105
|
+
return requirement.name !== alias.name || requirement.range.includes(":") ? override : `npm:${alias.name}@${requirement.range}`;
|
|
106
|
+
}
|
|
107
|
+
function bareNameOverride(lock, name) {
|
|
57
108
|
const overrides = lock.overrides;
|
|
58
|
-
if (!overrides) return void 0;
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
if (!overrides || !Object.hasOwn(overrides, name)) return void 0;
|
|
110
|
+
return overrides[name];
|
|
111
|
+
}
|
|
112
|
+
/** Whether a version of an `npm:` alias is a dist-tag, such as `latest`, rather than a range or a source. */
|
|
113
|
+
function isDistTag(version) {
|
|
114
|
+
return /^[^\s:/#@]+$/.test(version) && semver.validRange(version) === null;
|
|
61
115
|
}
|
|
62
116
|
/**
|
|
63
117
|
* Return the range Bun actually enforces for a dependency request.
|
|
@@ -66,7 +120,7 @@ function dependencyOverrideRange(lock, dependencyName) {
|
|
|
66
120
|
* compatibility then stays unknown instead of falling back to the declaration.
|
|
67
121
|
*/
|
|
68
122
|
function effectiveDependencyRange(lock, dependencyName, declaredRange) {
|
|
69
|
-
const overrideRange = dependencyOverrideRange(lock, dependencyName);
|
|
123
|
+
const overrideRange = dependencyOverrideRange(lock, dependencyName, declaredRange);
|
|
70
124
|
return overrideRange === void 0 ? declaredRange : overrideRange;
|
|
71
125
|
}
|
|
72
126
|
// ../lockfile/src/render.ts
|
|
@@ -130,15 +184,14 @@ function renderBunLock(lock) {
|
|
|
130
184
|
}
|
|
131
185
|
// ../lockfile/src/resolve.ts
|
|
132
186
|
/**
|
|
133
|
-
* Tell nested dependency
|
|
187
|
+
* Tell whether a lock key is a nested entry of a dependency: the last of several package names in the key.
|
|
134
188
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* such as `@scope/requester/name`.
|
|
189
|
+
* A suffix match is not enough: `@scope/name` is a root package key, and `app/@alias/name` is a nested entry
|
|
190
|
+
* of the dependency `@alias/name`, such as an `npm:` alias.
|
|
138
191
|
*/
|
|
139
192
|
function isNestedDependencyLockKey(lockKey, dependencyName) {
|
|
140
|
-
|
|
141
|
-
return
|
|
193
|
+
const names = splitLockKey(lockKey);
|
|
194
|
+
return names.length > 1 && names.at(-1) === dependencyName;
|
|
142
195
|
}
|
|
143
196
|
/**
|
|
144
197
|
* Resolve a dependency the way Bun lock keys are structured:
|
|
@@ -249,13 +302,39 @@ function setCatalogs(manifest, catalogs) {
|
|
|
249
302
|
if (manifest.workspaces !== void 0 || Object.keys(workspaces).length) result.workspaces = Array.isArray(manifest.workspaces) && Object.keys(workspaces).length === 1 ? workspaces.packages : workspaces;
|
|
250
303
|
return result;
|
|
251
304
|
}
|
|
305
|
+
/** Returns the distinct ranges that the root `package.json` declares for a package across its dependency groups. */
|
|
306
|
+
function getDeclaredRanges(manifest, name) {
|
|
307
|
+
const groups = [
|
|
308
|
+
manifest.dependencies,
|
|
309
|
+
manifest.devDependencies,
|
|
310
|
+
manifest.optionalDependencies,
|
|
311
|
+
manifest.peerDependencies
|
|
312
|
+
];
|
|
313
|
+
return [...new Set(groups.flatMap((group) => group && Object.hasOwn(group, name) ? [group[name]] : []))];
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Returns the range that a `$name` override value refers to, or the value itself when it is not a reference.
|
|
317
|
+
*
|
|
318
|
+
* Bun replaces `$name` with the range that the root `package.json` declares for `name`, as written.
|
|
319
|
+
* Resolves only references to the root's own declarations: throws when the reference names another package,
|
|
320
|
+
* which Bun versions resolve differently, and when the dependency groups declare no range or different ranges for it.
|
|
321
|
+
*/
|
|
322
|
+
function resolveOverrideValue(manifest, name, value, source = "package.json") {
|
|
323
|
+
if (!value.startsWith("$")) return value;
|
|
324
|
+
const entry = `${source} override ${JSON.stringify(name)}: ${JSON.stringify(value)}`;
|
|
325
|
+
if (value.slice(1) !== name) throw new Error(`${entry} refers to another package, which Bun versions resolve differently; replace it with an explicit range`);
|
|
326
|
+
const ranges = getDeclaredRanges(manifest, name);
|
|
327
|
+
if (ranges.length !== 1) throw new Error(`${entry} refers to ${ranges.length ? "different ranges in the dependency groups" : "a dependency that the root does not declare"}; replace it with an explicit range`);
|
|
328
|
+
return ranges[0];
|
|
329
|
+
}
|
|
252
330
|
/**
|
|
253
331
|
* Returns the host root `package.json` with the configured root fields of the nested `package.json` merged in.
|
|
254
332
|
*
|
|
255
333
|
* A nested entry replaces a host entry with the same name, while the other host entries stay.
|
|
256
334
|
* Entries synced earlier and dropped by the nested repository are removed.
|
|
257
335
|
* Throws when the host and the nested repository patch the same package version,
|
|
258
|
-
*
|
|
336
|
+
* 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.
|
|
259
338
|
*/
|
|
260
339
|
function syncManifest(host, nested) {
|
|
261
340
|
const config = readConfig(host);
|
|
@@ -291,6 +370,11 @@ function syncManifest(host, nested) {
|
|
|
291
370
|
manifest = setCatalogs(manifest, catalogs);
|
|
292
371
|
if (config.fields.includes("overrides")) {
|
|
293
372
|
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`);
|
|
375
|
+
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
|
+
}
|
|
294
378
|
const merged = mergeEntries("overrides", host.overrides, nested.overrides, synced.overrides);
|
|
295
379
|
manifest.overrides = merged.entries;
|
|
296
380
|
synced.overrides = merged.synced;
|
|
@@ -422,12 +506,12 @@ async function readLockfile(file) {
|
|
|
422
506
|
/**
|
|
423
507
|
* Returns the nested lockfile with workspace and local paths relative to the host root.
|
|
424
508
|
*
|
|
425
|
-
* The nested root workspace
|
|
509
|
+
* The nested root workspace moves to the nested directory, where it is a workspace of the host.
|
|
426
510
|
*/
|
|
427
511
|
function relocateLockfile(lock, dir) {
|
|
428
512
|
return {
|
|
429
513
|
...lock,
|
|
430
|
-
workspaces: Object.fromEntries(Object.entries(lock.workspaces).
|
|
514
|
+
workspaces: Object.fromEntries(Object.entries(lock.workspaces).map(([workspacePath, workspace]) => [path.posix.join(dir, workspacePath), workspace])),
|
|
431
515
|
packages: Object.fromEntries(Object.entries(lock.packages).map(([key, [resolution, ...rest]]) => [key, [relocateResolution(resolution, dir), ...rest]]))
|
|
432
516
|
};
|
|
433
517
|
}
|
|
@@ -445,13 +529,14 @@ function relocateResolution(resolution, dir) {
|
|
|
445
529
|
*
|
|
446
530
|
* `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
|
|
447
531
|
* so the merged lockfile must already record the synced root fields to keep the nested resolutions.
|
|
532
|
+
* Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
|
|
448
533
|
*/
|
|
449
534
|
function applyManifestToLockfile(lock, manifest) {
|
|
450
535
|
const { catalog, catalogs } = getCatalogs(manifest);
|
|
451
536
|
const fields = {
|
|
452
537
|
catalog,
|
|
453
538
|
catalogs,
|
|
454
|
-
overrides: manifest.overrides,
|
|
539
|
+
overrides: manifest.overrides && Object.fromEntries(Object.entries(manifest.overrides).map(([name, value]) => [name, resolveOverrideValue(manifest, name, value)])),
|
|
455
540
|
patchedDependencies: manifest.patchedDependencies
|
|
456
541
|
};
|
|
457
542
|
const result = { ...lock };
|
|
@@ -473,7 +558,8 @@ function applyManifestToLockfile(lock, manifest) {
|
|
|
473
558
|
* to it first.
|
|
474
559
|
*/
|
|
475
560
|
function mergeLockfiles(lock, nestedLock, dir) {
|
|
476
|
-
const
|
|
561
|
+
const nestedNames = /* @__PURE__ */ new Set([...Object.keys(nestedLock.packages), ...Object.values(nestedLock.workspaces).flatMap((workspace) => workspace.name ?? [])]);
|
|
562
|
+
const hostPackages = Object.entries(lock.packages).filter(([key]) => !nestedNames.has(splitPackageKey(key)[0]));
|
|
477
563
|
const packages = {
|
|
478
564
|
...Object.fromEntries(hostPackages),
|
|
479
565
|
...nestedLock.packages
|
|
@@ -546,21 +632,8 @@ function parseRequirement(lock, dependency, specifier) {
|
|
|
546
632
|
const range = (catalogName && catalogName !== "default" ? lock.catalogs?.[catalogName] : lock.catalog)?.[dependency];
|
|
547
633
|
return range === void 0 ? void 0 : parseRequirement(lock, dependency, range);
|
|
548
634
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const separator = target.lastIndexOf("@");
|
|
552
|
-
return separator > 0 ? {
|
|
553
|
-
name: target.slice(0, separator),
|
|
554
|
-
range: target.slice(separator + 1)
|
|
555
|
-
} : {
|
|
556
|
-
name: target,
|
|
557
|
-
range: "*"
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
return /^[a-z+]+:|\//i.test(specifier) ? void 0 : {
|
|
561
|
-
name: dependency,
|
|
562
|
-
range: specifier
|
|
563
|
-
};
|
|
635
|
+
const requirement = dependencyRequirement(dependency, specifier);
|
|
636
|
+
return /^[a-z+]+:|\//i.test(requirement.range) ? void 0 : requirement;
|
|
564
637
|
}
|
|
565
638
|
/** Returns the entry with the highest version of the required package that satisfies its range. */
|
|
566
639
|
function findSatisfyingEntry(sources, requirement) {
|
|
@@ -569,7 +642,7 @@ function findSatisfyingEntry(sources, requirement) {
|
|
|
569
642
|
return best;
|
|
570
643
|
}
|
|
571
644
|
function getVersion([resolution]) {
|
|
572
|
-
return
|
|
645
|
+
return parseResolvedSpec(resolution)?.version;
|
|
573
646
|
}
|
|
574
647
|
/** Returns whether an entry resolves the required package to a version in its range. */
|
|
575
648
|
function satisfies(entry, requirement) {
|
|
@@ -616,7 +689,9 @@ function findUncoveredWorkspaces(manifest, nestedLock) {
|
|
|
616
689
|
* including host entries of workspaces that the nested repository no longer has.
|
|
617
690
|
*/
|
|
618
691
|
function findWorkspaceMismatches(lock, nestedLock, dir) {
|
|
619
|
-
|
|
692
|
+
const paths = /* @__PURE__ */ new Set([...Object.keys(nestedLock.workspaces), ...Object.keys(lock.workspaces).filter((workspacePath) => isNestedPath(workspacePath, dir))]);
|
|
693
|
+
const comparable = (workspacePath, workspace) => workspacePath === dir && workspace ? Object.fromEntries(Object.entries(workspace).filter(([key]) => key !== "version" && key !== "bin")) : workspace;
|
|
694
|
+
return [...paths].sort().filter((workspacePath) => toCanonicalJson(comparable(workspacePath, lock.workspaces[workspacePath])) !== toCanonicalJson(comparable(workspacePath, nestedLock.workspaces[workspacePath])));
|
|
620
695
|
}
|
|
621
696
|
/**
|
|
622
697
|
* Returns the direct dependencies of nested workspaces that the host lockfile resolves differently than the nested one.
|
|
@@ -755,11 +830,13 @@ function isInSync(result) {
|
|
|
755
830
|
return Object.values(result).every((list) => !list.length);
|
|
756
831
|
}
|
|
757
832
|
/**
|
|
758
|
-
* Throws when the host `workspaces` patterns miss nested workspaces.
|
|
833
|
+
* Throws when the host `workspaces` patterns miss nested workspaces or the nested root has no name.
|
|
759
834
|
*
|
|
760
835
|
* Bun would drop such workspaces from the merged lockfile, so neither mode can sync or check them.
|
|
836
|
+
* Bun skips a workspace package without a name, or fails on it in versions before 1.4.
|
|
761
837
|
*/
|
|
762
838
|
function assertWorkspacesCovered(workspace) {
|
|
839
|
+
if (typeof workspace.nestedManifest.name !== "string" || !workspace.nestedManifest.name) throw new Error(`${workspace.config.dir}/package.json needs a "name" field to be a workspace of the host`);
|
|
763
840
|
const uncovered = findUncoveredWorkspaces(workspace.manifest, workspace.nestedLock);
|
|
764
841
|
if (uncovered.length) throw new Error(`The host "workspaces" field does not include nested workspaces: ${uncovered.join(", ")}`);
|
|
765
842
|
}
|
|
@@ -814,10 +891,10 @@ async function syncNestedWorkspace(root, options = {}) {
|
|
|
814
891
|
assertWorkspacesCovered(workspace);
|
|
815
892
|
const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
|
|
816
893
|
const { manifest } = manifestSync;
|
|
894
|
+
const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
|
|
817
895
|
const manifestText = formatJson(manifest, workspace.manifestText);
|
|
818
896
|
if (manifestText !== workspace.manifestText) await Bun.write(path.join(root, "package.json"), manifestText);
|
|
819
897
|
options.onManifestSync?.(manifestSync);
|
|
820
|
-
const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
|
|
821
898
|
await Bun.write(path.join(root, "bun.lock"), renderBunLock(merged));
|
|
822
899
|
await runBunInstall(root, options.installArgs);
|
|
823
900
|
const synced = await loadNestedWorkspace(root);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-nested-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"prepublishOnly": "bun run build"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"json5": "^2.2.3"
|
|
46
|
+
"json5": "^2.2.3",
|
|
47
|
+
"semver": "^7.7.4"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@arethetypeswrong/core": "catalog:",
|
|
@@ -54,6 +55,6 @@
|
|
|
54
55
|
"tsdown": "catalog:"
|
|
55
56
|
},
|
|
56
57
|
"engines": {
|
|
57
|
-
"bun": ">=1.
|
|
58
|
+
"bun": ">=1.4.4"
|
|
58
59
|
}
|
|
59
60
|
}
|