bun-nested-workspace 0.3.1 → 0.4.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`.
@@ -113,7 +116,7 @@ A package version patched by both the host and the nested repository is an error
113
116
  ### Lockfile
114
117
 
115
118
  The nested lockfile entries replace the host entries, so the nested packages keep their versions.
116
- When a replaced entry no longer satisfies a dependency of a host package, the host package keeps its previous resolution as a nested lockfile entry.
119
+ When a replaced entry no longer satisfies a dependency of a host package, the host package keeps its previous resolution, and the nested packages keep theirs.
117
120
  When neither satisfies the range, which Bun lockfiles occasionally contain, the dependency gets the highest satisfying version recorded in either lockfile.
118
121
  Ranges are checked as `bun install` applies them: an override replaces the declared range, and an `npm:` alias requires the package it names.
119
122
  `bun install` then resolves new dependencies of host packages and drops unused entries.
@@ -121,7 +124,7 @@ Ranges are checked as `bun install` applies them: an override replaces the decla
121
124
  After the install, the sync compares the direct dependencies of host packages with the original host lockfile.
122
125
  A dependency may only change to the version that the nested lockfile hoists to its top level, and only within the host range.
123
126
  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
- 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.
127
+ 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
128
 
126
129
  Only `bun.lock` with `lockfileVersion` 1 or 2 is supported, because the lockfile format is internal to Bun.
127
130
 
@@ -132,7 +135,8 @@ bunx bun-nested-workspace --check
132
135
  ```
133
136
 
134
137
  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.
138
+ 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.
139
+ 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
140
  Use it in a non-blocking CI job that signals when a sync commit is due.
137
141
 
138
142
  It reports:
@@ -149,7 +153,7 @@ Limitations:
149
153
 
150
154
  - Peer dependencies are not walked.
151
155
  - 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.
156
+ - 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
157
  - 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
158
 
155
159
  ## API
@@ -173,7 +177,8 @@ if (!isInSync(check)) {
173
177
  }
174
178
  ```
175
179
 
176
- `syncNestedWorkspace` writes the files and runs `bun install` before it checks the result, so a failed sync leaves the written files for inspection.
180
+ `syncNestedWorkspace` writes the files and runs `bun install` before it checks the result.
181
+ 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
182
  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
183
 
179
184
  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-BzA7ccrb.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-DOwKm8pX.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
@@ -194,7 +194,9 @@ export declare function applyManifestToLockfile(lock: Lockfile, manifest: Packag
194
194
  * A root entry overridden by the nested lockfile might not satisfy a dependency of a host package:
195
195
  * such a dependency gets back its previous host resolution as a nested entry, so `bun install` keeps the host version
196
196
  * instead of resolving the range again.
197
- * A host workspace without a name cannot have nested entries, so `bun install` resolves its dependency again.
197
+ * A dependency of the host root package gets it back at the top level instead, as `keepHostRootResolutions` describes.
198
+ * A host workspace other than the root without a name cannot have nested entries,
199
+ * so `bun install` resolves its dependency again.
198
200
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
199
201
  * the dependency gets the highest satisfying version recorded in either lockfile.
200
202
  * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
@@ -296,11 +298,15 @@ interface SyncOptions {
296
298
  */
297
299
  onManifestSync?: (sync: ManifestSync) => void;
298
300
  }
299
- /** Result of `syncNestedWorkspace`. */
301
+ /**
302
+ * Result of `syncNestedWorkspace`.
303
+ *
304
+ * The sync failed and restored the host files when `ownDependencyChanges` is not empty or `isInSync(check)` is false.
305
+ */
300
306
  interface SyncResult {
301
- /** Own dependencies of host workspaces that the sync re-resolved; the sync failed when this is not empty. */
307
+ /** Own dependencies of host workspaces that the sync re-resolved. */
302
308
  ownDependencyChanges: DependencyMismatch[];
303
- /** Check of the synced files; the sync failed when it is not in sync. */
309
+ /** Check of the synced files. */
304
310
  check: CheckResult;
305
311
  }
306
312
  /**
@@ -308,6 +314,9 @@ interface SyncResult {
308
314
  *
309
315
  * Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
310
316
  * and then compares the result with the nested checkout and with the original host resolutions.
317
+ * When `bun install` fails or the result differs, restores both files before it throws or returns,
318
+ * so that the next sync starts from the original host resolutions.
319
+ * Throws an error that says so when it cannot restore them.
311
320
  */
312
321
  export declare function syncNestedWorkspace(root: string, options?: SyncOptions): Promise<SyncResult>;
313
322
  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-BzA7ccrb.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-DOwKm8pX.mjs";
@@ -1,6 +1,6 @@
1
+ import path from "node:path";
1
2
  import JSON5 from "json5";
2
3
  import semver from "semver";
3
- import path from "node:path";
4
4
  // ../lockfile/src/parse.ts
5
5
  function isObject(value) {
6
6
  return typeof value === "object" && value !== null;
@@ -557,7 +557,9 @@ function applyManifestToLockfile(lock, manifest) {
557
557
  * A root entry overridden by the nested lockfile might not satisfy a dependency of a host package:
558
558
  * such a dependency gets back its previous host resolution as a nested entry, so `bun install` keeps the host version
559
559
  * instead of resolving the range again.
560
- * A host workspace without a name cannot have nested entries, so `bun install` resolves its dependency again.
560
+ * A dependency of the host root package gets it back at the top level instead, as `keepHostRootResolutions` describes.
561
+ * A host workspace other than the root without a name cannot have nested entries,
562
+ * so `bun install` resolves its dependency again.
561
563
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
562
564
  * the dependency gets the highest satisfying version recorded in either lockfile.
563
565
  * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
@@ -571,16 +573,16 @@ function mergeLockfiles(lock, nestedLock, dir) {
571
573
  ...nestedLock.packages
572
574
  };
573
575
  const hostWorkspaces = Object.entries(lock.workspaces).filter(([workspacePath]) => !isNestedPath(workspacePath, dir));
574
- const dependents = [...hostWorkspaces.map(([, workspace]) => ({
576
+ if (lock.workspaces[""]) keepHostRootResolutions(lock, nestedLock, packages);
577
+ const dependents = [...hostWorkspaces.flatMap(([workspacePath, workspace]) => workspacePath && workspace.name ? [{
575
578
  key: workspace.name,
576
579
  dependencies: getWorkspaceDependencies(workspace)
577
- })), ...Object.keys(packages).map((key) => ({
580
+ }] : []), ...Object.keys(packages).map((key) => ({
578
581
  key,
579
582
  dependencies: getPackageDependencies(packages[key])
580
583
  }))];
581
584
  for (let index = 0; index < dependents.length; index++) {
582
585
  const { key, dependencies } = dependents[index];
583
- if (key === void 0) continue;
584
586
  for (const [dependency, specifier] of Object.entries(dependencies)) {
585
587
  const requirement = getRequirement(lock, dependency, specifier);
586
588
  const resolved = resolveDependencyLockKey(key, dependency, packages);
@@ -606,6 +608,63 @@ function mergeLockfiles(lock, nestedLock, dir) {
606
608
  packages
607
609
  };
608
610
  }
611
+ /**
612
+ * Gives the dependencies of the host root package back their previous host resolutions where top-level nested
613
+ * entries do not satisfy them.
614
+ *
615
+ * Bun resolves the dependencies of the root package at the top level only, so placing the host resolution under the
616
+ * root name does not pin the root dependency: the host resolution returns to the top level with its host chain instead.
617
+ * Then every dependency of a nested workspace or package that no longer resolves to its nested entry or a copy of it,
618
+ * such as one that resolved the replaced entry, gets a copy of its nested entry under the dependent,
619
+ * so the copied entries keep their whole nested dependency trees.
620
+ */
621
+ function keepHostRootResolutions(lock, nestedLock, packages) {
622
+ const replaced = /* @__PURE__ */ new Set();
623
+ for (const [dependency, specifier] of Object.entries(getWorkspaceDependencies(lock.workspaces[""]))) {
624
+ const requirement = getRequirement(lock, dependency, specifier);
625
+ const nestedEntry = Object.hasOwn(nestedLock.packages, dependency) ? nestedLock.packages[dependency] : void 0;
626
+ if (!requirement || !nestedEntry || satisfies(nestedEntry, requirement)) continue;
627
+ const previous = Object.hasOwn(lock.packages, dependency) ? lock.packages[dependency] : void 0;
628
+ const replacement = previous && satisfies(previous, requirement) ? previous : findSatisfyingEntry([nestedLock.packages, lock.packages], requirement);
629
+ if (!replacement) continue;
630
+ replaced.add(dependency);
631
+ const prefix = `${dependency}/`;
632
+ for (const key of Object.keys(packages)) if (key.startsWith(prefix)) delete packages[key];
633
+ packages[dependency] = replacement;
634
+ if (replacement === previous) {
635
+ for (const [key, entry] of Object.entries(lock.packages)) if (key.startsWith(prefix)) packages[key] = entry;
636
+ }
637
+ }
638
+ if (!replaced.size) return;
639
+ const nestedKeys = new Map(Object.keys(nestedLock.packages).filter((key) => !replaced.has(splitPackageKey(key)[0])).map((key) => [key, key]));
640
+ const queue = [...Object.values(nestedLock.workspaces).flatMap((workspace) => workspace.name ? [{
641
+ key: workspace.name,
642
+ nestedKey: workspace.name,
643
+ dependencies: getWorkspaceDependencies(workspace)
644
+ }] : []), ...[...nestedKeys.keys()].map((key) => ({
645
+ key,
646
+ nestedKey: key,
647
+ dependencies: getPackageDependencies(nestedLock.packages[key])
648
+ }))];
649
+ for (let index = 0; index < queue.length; index++) {
650
+ const { key, nestedKey, dependencies } = queue[index];
651
+ for (const dependency of Object.keys(dependencies)) {
652
+ const expected = resolveDependencyLockKey(nestedKey, dependency, nestedLock.packages);
653
+ if (expected === void 0) continue;
654
+ const actual = resolveDependencyLockKey(key, dependency, packages);
655
+ if (actual !== void 0 && nestedKeys.get(actual) === expected) continue;
656
+ const entry = nestedLock.packages[expected];
657
+ const pinnedKey = `${key}/${dependency}`;
658
+ packages[pinnedKey] = entry;
659
+ nestedKeys.set(pinnedKey, expected);
660
+ queue.push({
661
+ key: pinnedKey,
662
+ nestedKey: expected,
663
+ dependencies: getPackageDependencies(entry)
664
+ });
665
+ }
666
+ }
667
+ }
609
668
  /** Returns the installed dependencies declared by a lockfile workspace. */
610
669
  function getWorkspaceDependencies(workspace) {
611
670
  return {
@@ -683,12 +742,40 @@ function resolveDependency(lock, workspaceName, dependency) {
683
742
  return key === void 0 ? void 0 : lock.packages[key][0];
684
743
  }
685
744
  // src/check.ts
745
+ /** Returns the `workspaces` patterns of a root `package.json`. */
746
+ function getWorkspacePatterns(manifest) {
747
+ const patterns = (Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? []).map((pattern) => pattern.replace(/^(!?)\.\//, "$1").replace(/\/+$/, ""));
748
+ return {
749
+ included: patterns.filter((pattern) => !pattern.startsWith("!")),
750
+ excluded: patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1))
751
+ };
752
+ }
753
+ /**
754
+ * Returns whether the patterns include a directory relative to their root, as `bun install` decides it.
755
+ *
756
+ * The root itself is never included.
757
+ * A pattern without glob syntax includes its exact path regardless of exclusions.
758
+ * A glob pattern matches the `package.json` path of the directory, so `dir/**` includes `dir` itself,
759
+ * and skips `node_modules`, `.git`, and `CMakeFiles` directories.
760
+ * An exclusion `dir/**` also excludes `dir` itself.
761
+ */
762
+ function matchesWorkspacePatterns(patterns, dir) {
763
+ const isGlob = (pattern) => /[*?[\]{}]/.test(pattern);
764
+ const matches = (pattern) => new Bun.Glob(pattern).match(dir);
765
+ const matchesManifest = (pattern) => new Bun.Glob(path.posix.join(pattern, "package.json")).match(path.posix.join(dir, "package.json"));
766
+ if (path.posix.normalize(dir) === ".") return false;
767
+ if (patterns.included.some((pattern) => !isGlob(pattern) && path.posix.normalize(pattern) === dir)) return true;
768
+ return !dir.split("/").some((part) => ignoredGlobDirs.has(part)) && patterns.included.some((pattern) => isGlob(pattern) && matchesManifest(pattern)) && !patterns.excluded.some((pattern) => matches(pattern) || pattern.endsWith("/**") && matches(pattern.slice(0, -3)));
769
+ }
770
+ const ignoredGlobDirs = /* @__PURE__ */ new Set([
771
+ "node_modules",
772
+ ".git",
773
+ "CMakeFiles"
774
+ ]);
686
775
  /** Returns the nested workspace paths that the host `workspaces` patterns do not include. */
687
776
  function findUncoveredWorkspaces(manifest, nestedLock) {
688
- const patterns = (Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? []).map((pattern) => pattern.replace(/^(!?)\.\//, "$1").replace(/\/+$/, ""));
689
- const included = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => new Bun.Glob(pattern));
690
- const excluded = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => new Bun.Glob(pattern.slice(1)));
691
- return Object.keys(nestedLock.workspaces).filter((workspacePath) => !included.some((glob) => glob.match(workspacePath)) || excluded.some((glob) => glob.match(workspacePath)));
777
+ const patterns = getWorkspacePatterns(manifest);
778
+ return Object.keys(nestedLock.workspaces).filter((workspacePath) => !matchesWorkspacePatterns(patterns, workspacePath));
692
779
  }
693
780
  /**
694
781
  * Returns the paths of nested workspaces whose host lockfile entries differ from the relocated nested lockfile,
@@ -832,6 +919,33 @@ async function loadNestedWorkspace(root) {
832
919
  nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir, typeof nestedManifest.name === "string" ? nestedManifest.name : void 0)
833
920
  };
834
921
  }
922
+ /**
923
+ * Returns whether a workspace path of the host lockfile is a workspace of the nested repository:
924
+ * its root, a workspace of the nested lockfile, or a directory that the nested `workspaces` patterns include.
925
+ */
926
+ function isNestedWorkspace(workspace, workspacePath) {
927
+ const { dir } = workspace.config;
928
+ return workspacePath === dir || workspacePath in workspace.nestedLock.workspaces || isNestedPath(workspacePath, dir) && matchesWorkspacePatterns(getWorkspacePatterns(workspace.nestedManifest), workspacePath.slice(dir.length + 1));
929
+ }
930
+ /**
931
+ * Returns the first directory with a named package that the nested `workspaces` patterns include, relative to the host root.
932
+ *
933
+ * `bun install` treats the nested root as the workspace root there and writes the nested lockfile,
934
+ * while in the nested root itself it writes the host lockfile.
935
+ */
936
+ async function findNestedPackageDir(workspace) {
937
+ const nestedRoot = path.join(workspace.root, workspace.config.dir);
938
+ const patterns = getWorkspacePatterns(workspace.nestedManifest);
939
+ const dirs = /* @__PURE__ */ new Set();
940
+ for (const pattern of patterns.included) for await (const file of new Bun.Glob(`${pattern}/package.json`).scan(nestedRoot)) {
941
+ const dir = path.posix.normalize(path.posix.dirname(file.split(path.sep).join("/")));
942
+ if (matchesWorkspacePatterns(patterns, dir)) dirs.add(dir);
943
+ }
944
+ for (const dir of [...dirs].sort()) {
945
+ const manifest = await Bun.file(path.join(nestedRoot, dir, "package.json")).json();
946
+ if (typeof manifest.name === "string" && manifest.name) return `${workspace.config.dir}/${dir}`;
947
+ }
948
+ }
835
949
  /** Returns whether a check found no differences. */
836
950
  function isInSync(result) {
837
951
  return Object.values(result).every((list) => !list.length);
@@ -892,6 +1006,9 @@ async function runBunInstall(root, args = []) {
892
1006
  *
893
1007
  * Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
894
1008
  * and then compares the result with the nested checkout and with the original host resolutions.
1009
+ * When `bun install` fails or the result differs, restores both files before it throws or returns,
1010
+ * so that the next sync starts from the original host resolutions.
1011
+ * Throws an error that says so when it cannot restore them.
895
1012
  */
896
1013
  async function syncNestedWorkspace(root, options = {}) {
897
1014
  const workspace = await loadNestedWorkspace(root);
@@ -899,20 +1016,56 @@ async function syncNestedWorkspace(root, options = {}) {
899
1016
  const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
900
1017
  const { manifest } = manifestSync;
901
1018
  const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
902
- const manifestText = formatJson(manifest, workspace.manifestText);
903
- if (manifestText !== workspace.manifestText) await Bun.write(path.join(root, "package.json"), manifestText);
904
- options.onManifestSync?.(manifestSync);
905
- await Bun.write(path.join(root, "bun.lock"), renderBunLock(merged));
906
- await runBunInstall(root, options.installArgs);
907
- const synced = await loadNestedWorkspace(root);
908
- return {
909
- ownDependencyChanges: findOwnDependencyChanges(workspace.lock, synced.lock, workspace.nestedLock, workspace.config.dir),
910
- check: checkNestedWorkspace(synced)
1019
+ const manifestPath = path.join(root, "package.json");
1020
+ const lockPath = path.join(root, "bun.lock");
1021
+ const originals = /* @__PURE__ */ new Map([[manifestPath, workspace.manifestText], [lockPath, await Bun.file(lockPath).text()]]);
1022
+ const restore = async () => {
1023
+ const failures = await restoreFiles(originals);
1024
+ if (failures.length) throw new Error([
1025
+ "The sync failed and could not restore the host files:",
1026
+ ...failures.map(({ file, error }) => ` ${path.basename(file)}: ${getErrorMessage(error)}`),
1027
+ "Restore them before running the sync again."
1028
+ ].join("\n"));
911
1029
  };
1030
+ let result;
1031
+ try {
1032
+ const manifestText = formatJson(manifest, workspace.manifestText);
1033
+ if (manifestText !== workspace.manifestText) await Bun.write(manifestPath, manifestText);
1034
+ options.onManifestSync?.(manifestSync);
1035
+ await Bun.write(lockPath, renderBunLock(merged));
1036
+ await runBunInstall(root, options.installArgs);
1037
+ const synced = await loadNestedWorkspace(root);
1038
+ result = {
1039
+ ownDependencyChanges: findOwnDependencyChanges(workspace.lock, synced.lock, workspace.nestedLock, workspace.config.dir),
1040
+ check: checkNestedWorkspace(synced)
1041
+ };
1042
+ } catch (error) {
1043
+ await restore();
1044
+ throw error;
1045
+ }
1046
+ if (result.ownDependencyChanges.length || !isInSync(result.check)) await restore();
1047
+ return result;
1048
+ }
1049
+ /**
1050
+ * Writes back the original texts of files whose contents differ from them and returns the files it could not restore.
1051
+ *
1052
+ * Restores each file separately, so that a failure does not keep the other files changed.
1053
+ */
1054
+ async function restoreFiles(originals) {
1055
+ const failures = [];
1056
+ for (const [file, text] of originals) try {
1057
+ if (await Bun.file(file).text().catch(() => void 0) !== text) await Bun.write(file, text);
1058
+ } catch (error) {
1059
+ failures.push({
1060
+ file,
1061
+ error
1062
+ });
1063
+ }
1064
+ return failures;
912
1065
  }
913
1066
  /** Serializes JSON with the indentation and final newline of the original text. */
914
1067
  function formatJson(value, original) {
915
1068
  const indent = /^[ \t]+(?=")/m.exec(original)?.[0] ?? " ";
916
1069
  return `${JSON.stringify(value, null, indent)}${original.endsWith("\n") ? "\n" : ""}`;
917
1070
  }
918
- export { syncManifest as _, syncNestedWorkspace as a, findTransitiveMismatches as c, applyManifestToLockfile as d, mergeLockfiles as f, diffManifests as g, supportedLockfileVersions as h, loadNestedWorkspace as i, findUncoveredWorkspaces as l, relocateLockfile as m, checkNestedWorkspace as n, findDirectDependencyMismatches as o, parseLockfile as p, isInSync as r, findOwnDependencyChanges as s, BunInstallError as t, findWorkspaceMismatches as u, syncedFields as v, getErrorMessage as y };
1071
+ export { supportedLockfileVersions as _, isNestedWorkspace as a, syncedFields as b, findDirectDependencyMismatches as c, findUncoveredWorkspaces as d, findWorkspaceMismatches as f, relocateLockfile as g, parseLockfile as h, isInSync as i, findOwnDependencyChanges as l, mergeLockfiles as m, checkNestedWorkspace as n, loadNestedWorkspace as o, applyManifestToLockfile as p, findNestedPackageDir as r, syncNestedWorkspace as s, BunInstallError as t, findTransitiveMismatches as u, diffManifests as v, getErrorMessage as x, syncManifest as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-nested-workspace",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",