bun-nested-workspace 0.1.0 → 0.2.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 CHANGED
@@ -4,15 +4,16 @@ Embed a checkout of another Bun workspace repository into a subdirectory of your
4
4
 
5
5
  The nested repository stays a self-contained project with its own root `package.json` and `bun.lock`.
6
6
  Its packages, such as `nested/apps/*` and `nested/packages/*`, become workspaces of the host, while its root does not.
7
+ The host installs none of the dependencies of the nested root `package.json` and cannot run its scripts.
7
8
  Bun reads catalogs, overrides, patches, and trusted dependencies from the workspace root only, and resolves the whole workspace from a single lockfile.
8
9
  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.
9
10
 
10
11
  ## Install
11
12
 
12
- The CLI runs before the host dependencies are installed, so run it with `bunx` and pin the version:
13
+ The CLI runs before the host dependencies are installed, so run it with `bunx`:
13
14
 
14
15
  ```sh
15
- bunx bun-nested-workspace@0.1.0
16
+ bunx bun-nested-workspace
16
17
  ```
17
18
 
18
19
  It requires Bun 1.2.19 or later, the first version that resolves `catalog:` dependencies from both catalog locations.
@@ -43,7 +44,7 @@ The config takes these fields:
43
44
  Run the CLI in the host root after switching the nested checkout to another commit:
44
45
 
45
46
  ```sh
46
- bunx bun-nested-workspace@0.1.0
47
+ bunx bun-nested-workspace
47
48
  ```
48
49
 
49
50
  The sync:
@@ -55,22 +56,33 @@ The sync:
55
56
 
56
57
  Commit `package.json` and `bun.lock` after the sync.
57
58
 
58
- In a Docker build, run the sync with `--lockfile-only` in a stage that contains only the dependency files, before installing dependencies:
59
+ In a Docker build, run the sync with `--lockfile-only` in a stage that contains only the dependency files, before installing dependencies.
60
+ `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
+ A separate stage can strip the build context down to these files, so the Dockerfile does not list every workspace package:
59
62
 
60
63
  ```dockerfile
61
- COPY package.json bun.lock ./
62
- COPY nested/package.json nested/bun.lock nested/
63
- COPY nested/packages/core/package.json nested/packages/core/
64
- RUN bunx bun-nested-workspace@0.1.0 --lockfile-only
64
+ FROM oven/bun:1 AS manifests
65
+ WORKDIR /app
66
+ COPY . .
67
+ RUN find . -type f ! -name package.json ! -name bun.lock ! -name '*.patch' ! -name bunfig.toml ! -name .npmrc -delete
68
+
69
+ FROM oven/bun:1 AS deps
70
+ WORKDIR /app
71
+ COPY --from=manifests /app ./
72
+ RUN bunx bun-nested-workspace --lockfile-only
65
73
  ```
66
74
 
75
+ Exclude `node_modules` and other large directories from the build context with `.dockerignore`.
76
+
67
77
  ### Root fields
68
78
 
69
79
  - `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`.
70
80
  An array `workspaces` field becomes an object when a catalog needs it.
71
81
  - `overrides` and `trustedDependencies` are merged by package name.
82
+ - `resolutions` on either side is an error while `overrides` are synced: Bun ignores `resolutions` once `overrides` is defined, so move its entries to `overrides`.
72
83
  - `trustedDependencies` stays in the host `package.json`, even empty, when either side defines it.
73
84
  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
+ The sync warns when it adds the field to the host.
74
86
  - `patchedDependencies` paths are rewritten through the nested directory, such as `nested/patches/exceljs@4.4.0.patch`.
75
87
 
76
88
  A nested entry replaces a host entry with the same name, while the other host entries stay.
@@ -99,7 +111,7 @@ Only `bun.lock` with `lockfileVersion` 1 or 2 is supported, because the lockfile
99
111
  ## Check
100
112
 
101
113
  ```sh
102
- bunx bun-nested-workspace@0.1.0 --check
114
+ bunx bun-nested-workspace --check
103
115
  ```
104
116
 
105
117
  The check writes nothing, does not run `bun install`, and exits with code 1 when the host files lag behind the nested checkout.
@@ -145,5 +157,6 @@ if (!isInSync(check)) {
145
157
  ```
146
158
 
147
159
  `syncNestedWorkspace` writes the files and runs `bun install` before it checks the result, so a failed sync leaves the written files for inspection.
160
+ Its `onManifestSync` option receives the replaced host entries and whether the sync added `trustedDependencies`, as soon as `package.json` is written and even when `bun install` then fails.
148
161
 
149
162
  Lower-level functions work on parsed files without touching the disk: `syncManifest`, `diffManifests`, `parseLockfile`, `relocateLockfile`, `applyManifestToLockfile`, `mergeLockfiles`, and the `find*` checks.
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-DJ2dh3q7.mjs";
2
+ import { a as syncNestedWorkspace, i as loadNestedWorkspace, n as checkNestedWorkspace, r as isInSync, t as BunInstallError, y as getErrorMessage } from "./workspace-DOgB-vkj.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
@@ -43,8 +43,10 @@ async function runCheck(root) {
43
43
  return 1;
44
44
  }
45
45
  async function runSync(root, installArgs) {
46
- const { overridden, ownDependencyChanges, check } = await syncNestedWorkspace(root, { installArgs });
47
- if (overridden.length) console.log(["Nested entries replaced host entries:", ...overridden.map(formatOverridden)].join("\n"));
46
+ const { ownDependencyChanges, check } = await syncNestedWorkspace(root, {
47
+ installArgs,
48
+ onManifestSync: printManifestSync
49
+ });
48
50
  let exitCode = 0;
49
51
  if (ownDependencyChanges.length) {
50
52
  console.error([
@@ -61,6 +63,14 @@ async function runSync(root, installArgs) {
61
63
  }
62
64
  return exitCode;
63
65
  }
66
+ function printManifestSync({ overridden, addedTrustedDependencies }) {
67
+ if (overridden.length) console.log(["Nested entries replaced host entries:", ...overridden.map(formatOverridden)].join("\n"));
68
+ if (addedTrustedDependencies) console.warn([
69
+ "The sync added trustedDependencies to the host package.json.",
70
+ "Bun no longer applies its default trusted list, so host dependencies that relied on it no longer run their lifecycle scripts.",
71
+ "Add the ones the host needs to trustedDependencies."
72
+ ].join("\n"));
73
+ }
64
74
  /** Formats the differences of a failed check as indented lines grouped by kind. */
65
75
  function formatCheckResult(result) {
66
76
  const section = (title, lines) => lines.length ? [` ${title}:`, ...lines.map((line) => ` ${line}`)] : [];
package/dist/index.d.mts CHANGED
@@ -96,6 +96,7 @@ interface PackageManifest {
96
96
  catalog?: Catalog;
97
97
  catalogs?: Record<string, Catalog>;
98
98
  overrides?: Record<string, string>;
99
+ resolutions?: Record<string, string>;
99
100
  patchedDependencies?: Record<string, string>;
100
101
  trustedDependencies?: string[];
101
102
  [configField]?: NestedWorkspaceConfig;
@@ -120,13 +121,21 @@ interface OverriddenEntry {
120
121
  interface ManifestSync {
121
122
  manifest: PackageManifest;
122
123
  overridden: OverriddenEntry[];
124
+ /**
125
+ * Whether the sync added `trustedDependencies` to a host that did not define it.
126
+ *
127
+ * The explicit list replaces the default trusted list of Bun,
128
+ * so host dependencies that relied on the default list no longer run their lifecycle scripts.
129
+ */
130
+ addedTrustedDependencies: boolean;
123
131
  }
124
132
  /**
125
133
  * Returns the host root `package.json` with the configured root fields of the nested `package.json` merged in.
126
134
  *
127
135
  * A nested entry replaces a host entry with the same name, while the other host entries stay.
128
136
  * Entries synced earlier and dropped by the nested repository are removed.
129
- * Throws when the host and the nested repository patch the same package version.
137
+ * Throws when the host and the nested repository patch the same package version,
138
+ * and when either side defines `resolutions` while `overrides` are synced.
130
139
  */
131
140
  export declare function syncManifest(host: PackageManifest, nested: PackageManifest): ManifestSync;
132
141
  /** Difference between the expected and the actual value at a `package.json` path. */
@@ -271,10 +280,15 @@ export declare class BunInstallError extends Error {
271
280
  interface SyncOptions {
272
281
  /** Arguments passed to `bun install`, such as `--lockfile-only`. */
273
282
  installArgs?: readonly string[];
283
+ /**
284
+ * Called with the result of `syncManifest` once the host `package.json` is written, before `bun install` runs.
285
+ *
286
+ * It reports the changes of `package.json` even when `bun install` fails and the sync throws.
287
+ */
288
+ onManifestSync?: (sync: ManifestSync) => void;
274
289
  }
275
290
  /** Result of `syncNestedWorkspace`. */
276
291
  interface SyncResult {
277
- overridden: OverriddenEntry[];
278
292
  /** Own dependencies of host workspaces that the sync re-resolved; the sync failed when this is not empty. */
279
293
  ownDependencyChanges: DependencyMismatch[];
280
294
  /** Check of the synced files; the sync failed when it is not in sync. */
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-DJ2dh3q7.mjs";
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-DOgB-vkj.mjs";
@@ -27,13 +27,21 @@ function parseResolvedSpec(spec) {
27
27
  version
28
28
  };
29
29
  }
30
+ /** Error thrown when `bun.lock` has a structure that the tools do not support. */
31
+ var BunLockFormatError = class extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "BunLockFormatError";
35
+ }
36
+ };
37
+ /** Parses `bun.lock` text and throws `BunLockFormatError` on unsupported structure. */
30
38
  function parseBunLock(lockText) {
31
39
  const parsed = JSON5.parse(lockText);
32
- if (!isObject(parsed)) throw new Error("bun.lock must parse to an object");
40
+ if (!isObject(parsed)) throw new BunLockFormatError("bun.lock must parse to an object");
33
41
  const overrides = parsed.overrides;
34
42
  if (overrides !== void 0) {
35
- if (!isObject(overrides) || Array.isArray(overrides)) throw new Error("bun.lock overrides must be an object");
36
- for (const [name, value] of Object.entries(overrides)) if (typeof value !== "string") throw new Error(`bun.lock override for ${JSON.stringify(name)} must be a string`);
43
+ if (!isObject(overrides) || Array.isArray(overrides)) throw new BunLockFormatError("bun.lock overrides must be an object");
44
+ 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`);
37
45
  }
38
46
  return parsed;
39
47
  }
@@ -41,7 +49,8 @@ function parseBunLock(lockText) {
41
49
  /**
42
50
  * Read the exact bare-name override Bun stores for a dependency.
43
51
  *
44
- * Bun 1.3.14 ignores selector keys and omits unsupported nested/path forms,
52
+ * `parseBunLock` rejects overrides scoped to a parent package or a version selector.
53
+ * Bun versions before 1.4 keep selector keys as strings without applying them,
45
54
  * so matching those here would diverge from the package manager.
46
55
  */
47
56
  function dependencyOverrideRange(lock, dependencyName) {
@@ -245,7 +254,8 @@ function setCatalogs(manifest, catalogs) {
245
254
  *
246
255
  * A nested entry replaces a host entry with the same name, while the other host entries stay.
247
256
  * Entries synced earlier and dropped by the nested repository are removed.
248
- * Throws when the host and the nested repository patch the same package version.
257
+ * Throws when the host and the nested repository patch the same package version,
258
+ * and when either side defines `resolutions` while `overrides` are synced.
249
259
  */
250
260
  function syncManifest(host, nested) {
251
261
  const config = readConfig(host);
@@ -280,6 +290,7 @@ function syncManifest(host, nested) {
280
290
  }
281
291
  manifest = setCatalogs(manifest, catalogs);
282
292
  if (config.fields.includes("overrides")) {
293
+ 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"`);
283
294
  const merged = mergeEntries("overrides", host.overrides, nested.overrides, synced.overrides);
284
295
  manifest.overrides = merged.entries;
285
296
  synced.overrides = merged.synced;
@@ -304,7 +315,8 @@ function syncManifest(host, nested) {
304
315
  } : rest;
305
316
  return {
306
317
  manifest,
307
- overridden
318
+ overridden,
319
+ addedTrustedDependencies: host.trustedDependencies === void 0 && manifest.trustedDependencies !== void 0
308
320
  };
309
321
  }
310
322
  /**
@@ -800,15 +812,16 @@ async function runBunInstall(root, args = []) {
800
812
  async function syncNestedWorkspace(root, options = {}) {
801
813
  const workspace = await loadNestedWorkspace(root);
802
814
  assertWorkspacesCovered(workspace);
803
- const { manifest, overridden } = syncManifest(workspace.manifest, workspace.nestedManifest);
815
+ const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
816
+ const { manifest } = manifestSync;
804
817
  const manifestText = formatJson(manifest, workspace.manifestText);
805
818
  if (manifestText !== workspace.manifestText) await Bun.write(path.join(root, "package.json"), manifestText);
819
+ options.onManifestSync?.(manifestSync);
806
820
  const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
807
821
  await Bun.write(path.join(root, "bun.lock"), renderBunLock(merged));
808
822
  await runBunInstall(root, options.installArgs);
809
823
  const synced = await loadNestedWorkspace(root);
810
824
  return {
811
- overridden,
812
825
  ownDependencyChanges: findOwnDependencyChanges(workspace.lock, synced.lock, workspace.nestedLock, workspace.config.dir),
813
826
  check: checkNestedWorkspace(synced)
814
827
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-nested-workspace",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",