bun-nested-workspace 0.3.0 → 0.3.2

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
@@ -102,6 +102,7 @@ Exclude `node_modules` and other large directories from the build context with `
102
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.
103
103
  The sync warns when it adds the field to the host.
104
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.
105
106
 
106
107
  A nested entry replaces a host entry with the same name, while the other host entries stay.
107
108
  The sync lists the host entries it replaced.
@@ -112,7 +113,7 @@ A package version patched by both the host and the nested repository is an error
112
113
  ### Lockfile
113
114
 
114
115
  The nested lockfile entries replace the host entries, so the nested packages keep their versions.
115
- 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.
116
+ 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.
116
117
  When neither satisfies the range, which Bun lockfiles occasionally contain, the dependency gets the highest satisfying version recorded in either lockfile.
117
118
  Ranges are checked as `bun install` applies them: an override replaces the declared range, and an `npm:` alias requires the package it names.
118
119
  `bun install` then resolves new dependencies of host packages and drops unused entries.
@@ -120,7 +121,7 @@ Ranges are checked as `bun install` applies them: an override replaces the decla
120
121
  After the install, the sync compares the direct dependencies of host packages with the original host lockfile.
121
122
  A dependency may only change to the version that the nested lockfile hoists to its top level, and only within the host range.
122
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.
123
- The latter happens to a host workspace without a name, because Bun records workspace-specific versions under the workspace name; name the workspace or align its range with the nested version.
124
+ 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.
124
125
 
125
126
  Only `bun.lock` with `lockfileVersion` 1 or 2 is supported, because the lockfile format is internal to Bun.
126
127
 
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-DfVz3A2J.mjs";
2
+ import { a as syncNestedWorkspace, i as loadNestedWorkspace, n as checkNestedWorkspace, r as isInSync, t as BunInstallError, y as getErrorMessage } from "./workspace-ChlHn5I8.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
package/dist/index.d.mts CHANGED
@@ -173,9 +173,12 @@ export declare function parseLockfile(text: string, source?: string): Lockfile;
173
173
  /**
174
174
  * Returns the nested lockfile with workspace and local paths relative to the host root.
175
175
  *
176
- * The nested root workspace moves to the nested directory, where it is a workspace of the host.
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.
177
180
  */
178
- export declare function relocateLockfile(lock: Lockfile, dir: string): Lockfile;
181
+ export declare function relocateLockfile(lock: Lockfile, dir: string, rootName?: string): Lockfile;
179
182
  /**
180
183
  * Returns the lockfile with root fields that affect resolution taken from the root `package.json`.
181
184
  *
@@ -191,7 +194,9 @@ export declare function applyManifestToLockfile(lock: Lockfile, manifest: Packag
191
194
  * A root entry overridden by the nested lockfile might not satisfy a dependency of a host package:
192
195
  * such a dependency gets back its previous host resolution as a nested entry, so `bun install` keeps the host version
193
196
  * instead of resolving the range again.
194
- * 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.
195
200
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
196
201
  * the dependency gets the highest satisfying version recorded in either lockfile.
197
202
  * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
@@ -257,7 +262,7 @@ interface NestedWorkspace {
257
262
  manifestText: string;
258
263
  nestedManifest: PackageManifest;
259
264
  lock: Lockfile;
260
- /** Nested lockfile relocated with `relocateLockfile`. */
265
+ /** Nested lockfile relocated with `relocateLockfile`, with the root name of `nestedManifest`. */
261
266
  nestedLock: Lockfile;
262
267
  }
263
268
  /** 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-DfVz3A2J.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-ChlHn5I8.mjs";
@@ -506,12 +506,18 @@ async function readLockfile(file) {
506
506
  /**
507
507
  * Returns the nested lockfile with workspace and local paths relative to the host root.
508
508
  *
509
- * The nested root workspace moves to the nested directory, where it is a workspace of the host.
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.
510
513
  */
511
- function relocateLockfile(lock, dir) {
514
+ function relocateLockfile(lock, dir, rootName) {
512
515
  return {
513
516
  ...lock,
514
- workspaces: Object.fromEntries(Object.entries(lock.workspaces).map(([workspacePath, workspace]) => [path.posix.join(dir, workspacePath), workspace])),
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])),
515
521
  packages: Object.fromEntries(Object.entries(lock.packages).map(([key, [resolution, ...rest]]) => [key, [relocateResolution(resolution, dir), ...rest]]))
516
522
  };
517
523
  }
@@ -551,7 +557,9 @@ function applyManifestToLockfile(lock, manifest) {
551
557
  * A root entry overridden by the nested lockfile might not satisfy a dependency of a host package:
552
558
  * such a dependency gets back its previous host resolution as a nested entry, so `bun install` keeps the host version
553
559
  * instead of resolving the range again.
554
- * 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.
555
563
  * When the previous host resolution does not satisfy the range either, which Bun lockfiles occasionally contain,
556
564
  * the dependency gets the highest satisfying version recorded in either lockfile.
557
565
  * Catalog dependencies are checked against the catalogs of the host lockfile, so apply the synced root fields
@@ -565,16 +573,16 @@ function mergeLockfiles(lock, nestedLock, dir) {
565
573
  ...nestedLock.packages
566
574
  };
567
575
  const hostWorkspaces = Object.entries(lock.workspaces).filter(([workspacePath]) => !isNestedPath(workspacePath, dir));
568
- const dependents = [...hostWorkspaces.map(([, workspace]) => ({
576
+ if (lock.workspaces[""]) keepHostRootResolutions(lock, nestedLock, packages);
577
+ const dependents = [...hostWorkspaces.flatMap(([workspacePath, workspace]) => workspacePath && workspace.name ? [{
569
578
  key: workspace.name,
570
579
  dependencies: getWorkspaceDependencies(workspace)
571
- })), ...Object.keys(packages).map((key) => ({
580
+ }] : []), ...Object.keys(packages).map((key) => ({
572
581
  key,
573
582
  dependencies: getPackageDependencies(packages[key])
574
583
  }))];
575
584
  for (let index = 0; index < dependents.length; index++) {
576
585
  const { key, dependencies } = dependents[index];
577
- if (key === void 0) continue;
578
586
  for (const [dependency, specifier] of Object.entries(dependencies)) {
579
587
  const requirement = getRequirement(lock, dependency, specifier);
580
588
  const resolved = resolveDependencyLockKey(key, dependency, packages);
@@ -600,6 +608,63 @@ function mergeLockfiles(lock, nestedLock, dir) {
600
608
  packages
601
609
  };
602
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
+ }
603
668
  /** Returns the installed dependencies declared by a lockfile workspace. */
604
669
  function getWorkspaceDependencies(workspace) {
605
670
  return {
@@ -815,14 +880,15 @@ async function loadNestedWorkspace(root) {
815
880
  const manifest = JSON.parse(manifestText);
816
881
  const config = readConfig(manifest);
817
882
  const nestedRoot = path.join(root, config.dir);
883
+ const nestedManifest = await Bun.file(path.join(nestedRoot, "package.json")).json();
818
884
  return {
819
885
  root,
820
886
  config,
821
887
  manifest,
822
888
  manifestText,
823
- nestedManifest: await Bun.file(path.join(nestedRoot, "package.json")).json(),
889
+ nestedManifest,
824
890
  lock: await readLockfile(path.join(root, "bun.lock")),
825
- nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir)
891
+ nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir, typeof nestedManifest.name === "string" ? nestedManifest.name : void 0)
826
892
  };
827
893
  }
828
894
  /** Returns whether a check found no differences. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-nested-workspace",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",