bun-nested-workspace 0.2.1 → 0.3.1
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 +27 -10
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +1 -1
- package/dist/{workspace-DOgB-vkj.mjs → workspace-BzA7ccrb.mjs} +126 -42
- 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
|
|
|
@@ -84,6 +102,7 @@ Exclude `node_modules` and other large directories from the build context with `
|
|
|
84
102
|
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.
|
|
85
103
|
The sync warns when it adds the field to the host.
|
|
86
104
|
- `patchedDependencies` paths are rewritten through the nested directory, such as `nested/patches/exceljs@4.4.0.patch`.
|
|
105
|
+
Bun warns on every install that it ignores `patchedDependencies` of the nested root package; keep them there, because the host applies the synced copies.
|
|
87
106
|
|
|
88
107
|
A nested entry replaces a host entry with the same name, while the other host entries stay.
|
|
89
108
|
The sync lists the host entries it replaced.
|
|
@@ -91,8 +110,6 @@ A host entry equal to the nested one stays the host's own, so it survives when t
|
|
|
91
110
|
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
111
|
A package version patched by both the host and the nested repository is an error.
|
|
93
112
|
|
|
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
113
|
### Lockfile
|
|
97
114
|
|
|
98
115
|
The nested lockfile entries replace the host entries, so the nested packages keep their versions.
|
|
@@ -102,7 +119,7 @@ Ranges are checked as `bun install` applies them: an override replaces the decla
|
|
|
102
119
|
`bun install` then resolves new dependencies of host packages and drops unused entries.
|
|
103
120
|
|
|
104
121
|
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
|
|
122
|
+
A dependency may only change to the version that the nested lockfile hoists to its top level, and only within the host range.
|
|
106
123
|
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
124
|
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
125
|
|
|
@@ -115,7 +132,7 @@ bunx bun-nested-workspace --check
|
|
|
115
132
|
```
|
|
116
133
|
|
|
117
134
|
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
|
|
135
|
+
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
136
|
Use it in a non-blocking CI job that signals when a sync commit is due.
|
|
120
137
|
|
|
121
138
|
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-BzA7ccrb.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,14 +173,18 @@ 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,
|
|
177
|
+
* and takes `rootName`, the name of the nested root `package.json`, when it is given.
|
|
178
|
+
* Bun requires the name of a workspace package entry but keeps a root entry without the name
|
|
179
|
+
* when the name was added to `package.json` after the lockfile.
|
|
172
180
|
*/
|
|
173
|
-
export declare function relocateLockfile(lock: Lockfile, dir: string): Lockfile;
|
|
181
|
+
export declare function relocateLockfile(lock: Lockfile, dir: string, rootName?: string): Lockfile;
|
|
174
182
|
/**
|
|
175
183
|
* Returns the lockfile with root fields that affect resolution taken from the root `package.json`.
|
|
176
184
|
*
|
|
177
185
|
* `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
|
|
178
186
|
* so the merged lockfile must already record the synced root fields to keep the nested resolutions.
|
|
187
|
+
* Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
|
|
179
188
|
*/
|
|
180
189
|
export declare function applyManifestToLockfile(lock: Lockfile, manifest: PackageManifest): Lockfile;
|
|
181
190
|
/**
|
|
@@ -251,7 +260,7 @@ interface NestedWorkspace {
|
|
|
251
260
|
manifestText: string;
|
|
252
261
|
nestedManifest: PackageManifest;
|
|
253
262
|
lock: Lockfile;
|
|
254
|
-
/** Nested lockfile relocated with `relocateLockfile`. */
|
|
263
|
+
/** Nested lockfile relocated with `relocateLockfile`, with the root name of `nestedManifest`. */
|
|
255
264
|
nestedLock: Lockfile;
|
|
256
265
|
}
|
|
257
266
|
/** Loads the root `package.json` and `bun.lock` files of the host and of the nested checkout. */
|
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-BzA7ccrb.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,18 @@ 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,
|
|
510
|
+
* and takes `rootName`, the name of the nested root `package.json`, when it is given.
|
|
511
|
+
* Bun requires the name of a workspace package entry but keeps a root entry without the name
|
|
512
|
+
* when the name was added to `package.json` after the lockfile.
|
|
426
513
|
*/
|
|
427
|
-
function relocateLockfile(lock, dir) {
|
|
514
|
+
function relocateLockfile(lock, dir, rootName) {
|
|
428
515
|
return {
|
|
429
516
|
...lock,
|
|
430
|
-
workspaces: Object.fromEntries(Object.entries(lock.workspaces).
|
|
517
|
+
workspaces: Object.fromEntries(Object.entries(lock.workspaces).map(([workspacePath, workspace]) => [path.posix.join(dir, workspacePath), workspacePath === "" && rootName !== void 0 ? {
|
|
518
|
+
...workspace,
|
|
519
|
+
name: rootName
|
|
520
|
+
} : workspace])),
|
|
431
521
|
packages: Object.fromEntries(Object.entries(lock.packages).map(([key, [resolution, ...rest]]) => [key, [relocateResolution(resolution, dir), ...rest]]))
|
|
432
522
|
};
|
|
433
523
|
}
|
|
@@ -445,13 +535,14 @@ function relocateResolution(resolution, dir) {
|
|
|
445
535
|
*
|
|
446
536
|
* `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
|
|
447
537
|
* so the merged lockfile must already record the synced root fields to keep the nested resolutions.
|
|
538
|
+
* Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
|
|
448
539
|
*/
|
|
449
540
|
function applyManifestToLockfile(lock, manifest) {
|
|
450
541
|
const { catalog, catalogs } = getCatalogs(manifest);
|
|
451
542
|
const fields = {
|
|
452
543
|
catalog,
|
|
453
544
|
catalogs,
|
|
454
|
-
overrides: manifest.overrides,
|
|
545
|
+
overrides: manifest.overrides && Object.fromEntries(Object.entries(manifest.overrides).map(([name, value]) => [name, resolveOverrideValue(manifest, name, value)])),
|
|
455
546
|
patchedDependencies: manifest.patchedDependencies
|
|
456
547
|
};
|
|
457
548
|
const result = { ...lock };
|
|
@@ -473,7 +564,8 @@ function applyManifestToLockfile(lock, manifest) {
|
|
|
473
564
|
* to it first.
|
|
474
565
|
*/
|
|
475
566
|
function mergeLockfiles(lock, nestedLock, dir) {
|
|
476
|
-
const
|
|
567
|
+
const nestedNames = /* @__PURE__ */ new Set([...Object.keys(nestedLock.packages), ...Object.values(nestedLock.workspaces).flatMap((workspace) => workspace.name ?? [])]);
|
|
568
|
+
const hostPackages = Object.entries(lock.packages).filter(([key]) => !nestedNames.has(splitPackageKey(key)[0]));
|
|
477
569
|
const packages = {
|
|
478
570
|
...Object.fromEntries(hostPackages),
|
|
479
571
|
...nestedLock.packages
|
|
@@ -546,21 +638,8 @@ function parseRequirement(lock, dependency, specifier) {
|
|
|
546
638
|
const range = (catalogName && catalogName !== "default" ? lock.catalogs?.[catalogName] : lock.catalog)?.[dependency];
|
|
547
639
|
return range === void 0 ? void 0 : parseRequirement(lock, dependency, range);
|
|
548
640
|
}
|
|
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
|
-
};
|
|
641
|
+
const requirement = dependencyRequirement(dependency, specifier);
|
|
642
|
+
return /^[a-z+]+:|\//i.test(requirement.range) ? void 0 : requirement;
|
|
564
643
|
}
|
|
565
644
|
/** Returns the entry with the highest version of the required package that satisfies its range. */
|
|
566
645
|
function findSatisfyingEntry(sources, requirement) {
|
|
@@ -569,7 +648,7 @@ function findSatisfyingEntry(sources, requirement) {
|
|
|
569
648
|
return best;
|
|
570
649
|
}
|
|
571
650
|
function getVersion([resolution]) {
|
|
572
|
-
return
|
|
651
|
+
return parseResolvedSpec(resolution)?.version;
|
|
573
652
|
}
|
|
574
653
|
/** Returns whether an entry resolves the required package to a version in its range. */
|
|
575
654
|
function satisfies(entry, requirement) {
|
|
@@ -616,7 +695,9 @@ function findUncoveredWorkspaces(manifest, nestedLock) {
|
|
|
616
695
|
* including host entries of workspaces that the nested repository no longer has.
|
|
617
696
|
*/
|
|
618
697
|
function findWorkspaceMismatches(lock, nestedLock, dir) {
|
|
619
|
-
|
|
698
|
+
const paths = /* @__PURE__ */ new Set([...Object.keys(nestedLock.workspaces), ...Object.keys(lock.workspaces).filter((workspacePath) => isNestedPath(workspacePath, dir))]);
|
|
699
|
+
const comparable = (workspacePath, workspace) => workspacePath === dir && workspace ? Object.fromEntries(Object.entries(workspace).filter(([key]) => key !== "version" && key !== "bin")) : workspace;
|
|
700
|
+
return [...paths].sort().filter((workspacePath) => toCanonicalJson(comparable(workspacePath, lock.workspaces[workspacePath])) !== toCanonicalJson(comparable(workspacePath, nestedLock.workspaces[workspacePath])));
|
|
620
701
|
}
|
|
621
702
|
/**
|
|
622
703
|
* Returns the direct dependencies of nested workspaces that the host lockfile resolves differently than the nested one.
|
|
@@ -740,14 +821,15 @@ async function loadNestedWorkspace(root) {
|
|
|
740
821
|
const manifest = JSON.parse(manifestText);
|
|
741
822
|
const config = readConfig(manifest);
|
|
742
823
|
const nestedRoot = path.join(root, config.dir);
|
|
824
|
+
const nestedManifest = await Bun.file(path.join(nestedRoot, "package.json")).json();
|
|
743
825
|
return {
|
|
744
826
|
root,
|
|
745
827
|
config,
|
|
746
828
|
manifest,
|
|
747
829
|
manifestText,
|
|
748
|
-
nestedManifest
|
|
830
|
+
nestedManifest,
|
|
749
831
|
lock: await readLockfile(path.join(root, "bun.lock")),
|
|
750
|
-
nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir)
|
|
832
|
+
nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir, typeof nestedManifest.name === "string" ? nestedManifest.name : void 0)
|
|
751
833
|
};
|
|
752
834
|
}
|
|
753
835
|
/** Returns whether a check found no differences. */
|
|
@@ -755,11 +837,13 @@ function isInSync(result) {
|
|
|
755
837
|
return Object.values(result).every((list) => !list.length);
|
|
756
838
|
}
|
|
757
839
|
/**
|
|
758
|
-
* Throws when the host `workspaces` patterns miss nested workspaces.
|
|
840
|
+
* Throws when the host `workspaces` patterns miss nested workspaces or the nested root has no name.
|
|
759
841
|
*
|
|
760
842
|
* Bun would drop such workspaces from the merged lockfile, so neither mode can sync or check them.
|
|
843
|
+
* Bun skips a workspace package without a name, or fails on it in versions before 1.4.
|
|
761
844
|
*/
|
|
762
845
|
function assertWorkspacesCovered(workspace) {
|
|
846
|
+
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
847
|
const uncovered = findUncoveredWorkspaces(workspace.manifest, workspace.nestedLock);
|
|
764
848
|
if (uncovered.length) throw new Error(`The host "workspaces" field does not include nested workspaces: ${uncovered.join(", ")}`);
|
|
765
849
|
}
|
|
@@ -814,10 +898,10 @@ async function syncNestedWorkspace(root, options = {}) {
|
|
|
814
898
|
assertWorkspacesCovered(workspace);
|
|
815
899
|
const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
|
|
816
900
|
const { manifest } = manifestSync;
|
|
901
|
+
const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
|
|
817
902
|
const manifestText = formatJson(manifest, workspace.manifestText);
|
|
818
903
|
if (manifestText !== workspace.manifestText) await Bun.write(path.join(root, "package.json"), manifestText);
|
|
819
904
|
options.onManifestSync?.(manifestSync);
|
|
820
|
-
const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
|
|
821
905
|
await Bun.write(path.join(root, "bun.lock"), renderBunLock(merged));
|
|
822
906
|
await runBunInstall(root, options.installArgs);
|
|
823
907
|
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.1",
|
|
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
|
}
|