bun-nested-workspace 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,8 @@ Then update the nested lockfile with `bun install --lockfile-only` in the direct
51
51
  bunx bun-nested-workspace
52
52
  ```
53
53
 
54
+ A nested repository without workspace packages updates its lockfile in a separate clone.
55
+
54
56
  Do not run other `bun install`, `bun add`, or `bun remove` commands in the nested directory or in its package directories:
55
57
 
56
58
  - In the nested root, Bun runs them for the host: it writes the host lockfile and leaves the nested one stale.
@@ -72,6 +74,7 @@ The sync:
72
74
  4. Fails if the install re-resolved own dependencies of host packages, or if the host does not resolve the nested packages as the nested lockfile does.
73
75
 
74
76
  Commit `package.json` and `bun.lock` after the sync.
77
+ A failed sync restores `package.json` and `bun.lock` to their contents before the run, while `node_modules` stays as the install left it.
75
78
 
76
79
  In a Docker build, run the sync with `--lockfile-only` in a stage that contains only the dependency files, before installing dependencies.
77
80
  The dependency install runs the `prepare` script of the nested root unless it gets `--ignore-scripts`.
@@ -96,7 +99,9 @@ Exclude `node_modules` and other large directories from the build context with `
96
99
 
97
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`.
98
101
  An array `workspaces` field becomes an object when a catalog needs it.
99
- - `overrides` and `trustedDependencies` are merged by package name.
102
+ - `overrides` are merged by their keys, so a group such as `"micromatch": { "picomatch": "^2.3.2" }` replaces the host group with the same key as a whole.
103
+ The sync requires each `bun.lock` to be up to date with the overrides of its `package.json`.
104
+ - `trustedDependencies` are merged by package name.
100
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`.
101
106
  - `trustedDependencies` stays in the host `package.json`, even empty, when either side defines it.
102
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.
@@ -104,7 +109,7 @@ Exclude `node_modules` and other large directories from the build context with `
104
109
  - `patchedDependencies` paths are rewritten through the nested directory, such as `nested/patches/exceljs@4.4.0.patch`.
105
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.
106
111
 
107
- A nested entry replaces a host entry with the same name, while the other host entries stay.
112
+ A nested entry replaces a host entry with the same key, while the other host entries stay.
108
113
  The sync lists the host entries it replaced.
109
114
  A host entry equal to the nested one stays the host's own, so it survives when the nested repository drops it.
110
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.
@@ -123,7 +128,7 @@ A dependency may only change to the version that the nested lockfile hoists to i
123
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.
124
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.
125
130
 
126
- Only `bun.lock` with `lockfileVersion` 1 or 2 is supported, because the lockfile format is internal to Bun.
131
+ Only `bun.lock` with `lockfileVersion` 1, 2, or 3 is supported, because the lockfile format is internal to Bun.
127
132
 
128
133
  ## Check
129
134
 
@@ -132,7 +137,8 @@ bunx bun-nested-workspace --check
132
137
  ```
133
138
 
134
139
  The check writes nothing, does not run `bun install`, and exits with code 1 when the host files lag behind the nested checkout.
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.
140
+ 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.
141
+ Verify that in the CI of the nested repository with `bun install --lockfile-only` followed by `git diff --exit-code bun.lock`: `bun install --frozen-lockfile` misses some changes, such as a narrowed range that the locked version still satisfies.
136
142
  Use it in a non-blocking CI job that signals when a sync commit is due.
137
143
 
138
144
  It reports:
@@ -149,7 +155,7 @@ Limitations:
149
155
 
150
156
  - Peer dependencies are not walked.
151
157
  - Edges that the nested lockfile itself resolves outside their range are skipped, because the sync repairs them in the host.
152
- - The check does not verify that the host lockfile matches the host packages; `bun install --frozen-lockfile` does.
158
+ - The check does not verify that the host lockfile matches the host packages; `bun install --lockfile-only` followed by `git diff --exit-code bun.lock` does.
153
159
  - The check does not predict whether a sync would re-resolve own dependencies of host packages; only the sync detects it after `bun install`.
154
160
 
155
161
  ## API
@@ -173,7 +179,8 @@ if (!isInSync(check)) {
173
179
  }
174
180
  ```
175
181
 
176
- `syncNestedWorkspace` writes the files and runs `bun install` before it checks the result, so a failed sync leaves the written files for inspection.
182
+ `syncNestedWorkspace` writes the files and runs `bun install` before it checks the result.
183
+ When `bun install` fails or the result shows differences, it restores both files before it throws or returns, and throws an error saying so when it cannot restore them.
177
184
  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.
178
185
 
179
186
  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,11 +1,12 @@
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-ChlHn5I8.mjs";
2
+ import { a as isNestedWorkspace, i as isInSync, n as checkNestedWorkspace, o as loadNestedWorkspace, r as findNestedPackageDir, s as syncNestedWorkspace, t as BunInstallError, x as getErrorMessage } from "./workspace-CC5KeN8i.mjs";
3
3
  // src/cli.ts
4
4
  const usage = `Usage:
5
5
  bun-nested-workspace [bun install options] Sync package.json and bun.lock, then run bun install
6
6
  bun-nested-workspace --check Exit with 1 if package.json or bun.lock lag behind the nested checkout
7
7
 
8
8
  Configure the nested directory in the "bun-nested-workspace" field of the host package.json.`;
9
+ const restoredMessage = "Restored package.json and bun.lock to their contents before the sync.";
9
10
  /** Runs the CLI with the given arguments in the given host root and returns the exit code. */
10
11
  async function runCli(args, root = process.cwd()) {
11
12
  if (args.includes("--help") || args.includes("-h")) {
@@ -23,7 +24,10 @@ async function runCli(args, root = process.cwd()) {
23
24
  }
24
25
  return await runSync(root, args);
25
26
  } catch (error) {
26
- if (error instanceof BunInstallError) return error.exitCode;
27
+ if (error instanceof BunInstallError) {
28
+ console.error(restoredMessage);
29
+ return error.exitCode;
30
+ }
27
31
  console.error(getErrorMessage(error));
28
32
  return 1;
29
33
  }
@@ -51,7 +55,7 @@ async function runSync(root, installArgs) {
51
55
  if (ownDependencyChanges.length) {
52
56
  console.error([
53
57
  "The sync changed own dependencies of host packages.",
54
- "Either the host bun.lock did not pin them: restore bun.lock, run bun install, commit bun.lock, and sync again.",
58
+ "Either the host bun.lock did not pin them: run bun install, commit bun.lock, and sync again.",
55
59
  "Or the nested lockfile resolves them outside the host range: align the host range with the nested version.",
56
60
  ...ownDependencyChanges.map(({ workspace, dependency, expected, actual }) => formatChange(`${workspace} > ${dependency}`, expected, actual))
57
61
  ].join("\n"));
@@ -59,10 +63,23 @@ async function runSync(root, installArgs) {
59
63
  }
60
64
  if (!isInSync(check)) {
61
65
  console.error(["bun install did not keep the nested resolutions:", ...formatCheckResult(check)].join("\n"));
66
+ const workspace = await loadNestedWorkspace(root);
67
+ if (check.workspaces.some((workspacePath) => isNestedWorkspace(workspace, workspacePath))) console.error(await formatNestedLockfileUpdate(workspace));
62
68
  exitCode = 1;
63
69
  }
70
+ if (exitCode) console.error(restoredMessage);
64
71
  return exitCode;
65
72
  }
73
+ /** Formats how to update a nested lockfile that lags behind the nested package.json files. */
74
+ async function formatNestedLockfileUpdate(workspace) {
75
+ const { dir } = workspace.config;
76
+ const packageDir = await findNestedPackageDir(workspace);
77
+ return packageDir ? `${dir}/bun.lock lags behind the package.json files of ${dir}. Update it and sync again:\n (cd ${quoteShellArgument(packageDir)} && bun install --lockfile-only)` : `${dir}/bun.lock lags behind the package.json files of ${dir}. Update it with bun install --lockfile-only in a separate clone of the nested repository and sync again.`;
78
+ }
79
+ /** Quotes a value for a POSIX shell when it contains characters other than those of plain paths. */
80
+ function quoteShellArgument(value) {
81
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
82
+ }
66
83
  function printManifestSync({ overridden, addedTrustedDependencies }) {
67
84
  if (overridden.length) console.log(["Nested entries replaced host entries:", ...overridden.map(formatOverridden)].join("\n"));
68
85
  if (addedTrustedDependencies) console.warn([
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import semver from "semver";
1
2
  type DependencyMap = Record<string, string>;
2
3
  type BunLockWorkspace = {
3
4
  /**
@@ -5,6 +6,8 @@ type BunLockWorkspace = {
5
6
  * context for workspace-scoped lock keys.
6
7
  */
7
8
  name?: string;
9
+ /** Version of a workspace package; Bun leaves it out for the root workspace. */
10
+ version?: string;
8
11
  dependencies?: DependencyMap;
9
12
  devDependencies?: DependencyMap;
10
13
  optionalDependencies?: DependencyMap;
@@ -48,8 +51,12 @@ type BunLockFile = {
48
51
  catalog?: DependencyMap;
49
52
  /** Named catalogs of the root `package.json`. */
50
53
  catalogs?: Record<string, DependencyMap>;
51
- /** Canonical map Bun writes for package.json overrides and resolutions. */
52
- overrides?: DependencyMap;
54
+ /**
55
+ * Canonical map Bun writes for package.json overrides and resolutions.
56
+ *
57
+ * A string value overrides a package by name; an object value groups the rules scoped to a package or its range.
58
+ */
59
+ overrides?: Record<string, string | DependencyMap>;
53
60
  /** Package specs whose local patches may change their dependency declarations. */
54
61
  patchedDependencies?: Record<string, string>;
55
62
  packages?: Record<string, BunPackageEntry>;
@@ -90,6 +97,8 @@ interface ResolvedConfig {
90
97
  synced: SyncedEntries;
91
98
  }
92
99
  type Catalog = Record<string, string>;
100
+ /** Value of an `overrides` entry: a version spec, or a group of rules keyed by `"."` and dependency selectors. */
101
+ type OverrideValue = string | Record<string, string>;
93
102
  /** Root `package.json` fields this package reads or rewrites; other fields are preserved as is. */
94
103
  interface PackageManifest {
95
104
  dependencies?: Record<string, string>;
@@ -99,7 +108,7 @@ interface PackageManifest {
99
108
  workspaces?: string[] | WorkspacesConfig;
100
109
  catalog?: Catalog;
101
110
  catalogs?: Record<string, Catalog>;
102
- overrides?: Record<string, string>;
111
+ overrides?: Record<string, OverrideValue>;
103
112
  resolutions?: Record<string, string>;
104
113
  patchedDependencies?: Record<string, string>;
105
114
  trustedDependencies?: string[];
@@ -118,8 +127,8 @@ interface OverriddenEntry {
118
127
  /** Field path, such as `catalog`, `catalogs.testing`, or `overrides`. */
119
128
  field: string;
120
129
  name: string;
121
- hostValue: string;
122
- nestedValue: string;
130
+ hostValue: OverrideValue;
131
+ nestedValue: OverrideValue;
123
132
  }
124
133
  /** Result of `syncManifest`. */
125
134
  interface ManifestSync {
@@ -136,11 +145,13 @@ interface ManifestSync {
136
145
  /**
137
146
  * Returns the host root `package.json` with the configured root fields of the nested `package.json` merged in.
138
147
  *
139
- * A nested entry replaces a host entry with the same name, while the other host entries stay.
148
+ * A nested entry replaces a host entry with the same key, while the other host entries stay.
140
149
  * Entries synced earlier and dropped by the nested repository are removed.
141
150
  * Throws when the host and the nested repository patch the same package version,
142
151
  * when either side defines `resolutions` while `overrides` are synced,
143
- * and when a synced `$name` override would refer to another range in the host root than in the nested root.
152
+ * when an override is one that `readManifestOverrides` rejects, when two overrides select the same packages,
153
+ * when a synced `$name` override would refer to another range in the host root than in the nested root,
154
+ * and when a synced override selects the nested root by a range that its version satisfies.
144
155
  */
145
156
  export declare function syncManifest(host: PackageManifest, nested: PackageManifest): ManifestSync;
146
157
  /** Difference between the expected and the actual value at a `package.json` path. */
@@ -184,7 +195,7 @@ export declare function relocateLockfile(lock: Lockfile, dir: string, rootName?:
184
195
  *
185
196
  * `bun install` re-resolves dependencies whose catalog entry, override, or patch differs from the lockfile,
186
197
  * so the merged lockfile must already record the synced root fields to keep the nested resolutions.
187
- * Records `$name` overrides with the range they refer to, as Bun does, and throws when `resolveOverrideValue` does.
198
+ * Records overrides in the form that Bun writes to `bun.lock`, with `$name` values replaced by the range they refer to.
188
199
  */
189
200
  export declare function applyManifestToLockfile(lock: Lockfile, manifest: PackageManifest): Lockfile;
190
201
  /**
@@ -199,8 +210,9 @@ export declare function applyManifestToLockfile(lock: Lockfile, manifest: Packag
199
210
  * so `bun install` resolves its dependency again.
200
211
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
201
212
  * the dependency gets the highest satisfying version recorded in either lockfile.
202
- * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
213
+ * Dependencies are checked against the catalogs and overrides of the host lockfile, so apply the synced root fields
203
214
  * to it first.
215
+ * The result has the higher `lockfileVersion` of the two, or version 1 when it has an entry that needs that version.
204
216
  */
205
217
  export declare function mergeLockfiles(lock: Lockfile, nestedLock: Lockfile, dir: string): Lockfile;
206
218
  /** Dependency of a workspace package resolved differently than expected. */
@@ -298,11 +310,15 @@ interface SyncOptions {
298
310
  */
299
311
  onManifestSync?: (sync: ManifestSync) => void;
300
312
  }
301
- /** Result of `syncNestedWorkspace`. */
313
+ /**
314
+ * Result of `syncNestedWorkspace`.
315
+ *
316
+ * The sync failed and restored the host files when `ownDependencyChanges` is not empty or `isInSync(check)` is false.
317
+ */
302
318
  interface SyncResult {
303
- /** Own dependencies of host workspaces that the sync re-resolved; the sync failed when this is not empty. */
319
+ /** Own dependencies of host workspaces that the sync re-resolved. */
304
320
  ownDependencyChanges: DependencyMismatch[];
305
- /** Check of the synced files; the sync failed when it is not in sync. */
321
+ /** Check of the synced files. */
306
322
  check: CheckResult;
307
323
  }
308
324
  /**
@@ -310,6 +326,9 @@ interface SyncResult {
310
326
  *
311
327
  * Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
312
328
  * and then compares the result with the nested checkout and with the original host resolutions.
329
+ * When `bun install` fails or the result differs, restores both files before it throws or returns,
330
+ * so that the next sync starts from the original host resolutions.
331
+ * Throws an error that says so when it cannot restore them.
313
332
  */
314
333
  export declare function syncNestedWorkspace(root: string, options?: SyncOptions): Promise<SyncResult>;
315
334
  export type { CheckResult, DependencyMismatch, Lockfile, ManifestDifference, ManifestSync, NestedWorkspace, NestedWorkspaceConfig, OverriddenEntry, PackageManifest, PackageMismatch, ResolvedConfig, SyncOptions, SyncResult, SyncedEntries, SyncedField, WorkspacesConfig };
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-ChlHn5I8.mjs";
1
+ export { t as BunInstallError, p as applyManifestToLockfile, n as checkNestedWorkspace, v as diffManifests, c as findDirectDependencyMismatches, l as findOwnDependencyChanges, u as findTransitiveMismatches, d as findUncoveredWorkspaces, f as findWorkspaceMismatches, i as isInSync, o as loadNestedWorkspace, m as mergeLockfiles, h as parseLockfile, g as relocateLockfile, _ as supportedLockfileVersions, y as syncManifest, s as syncNestedWorkspace, b as syncedFields } from "./workspace-CC5KeN8i.mjs";