bun-nested-workspace 0.3.2 → 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 +8 -3
- package/dist/cli.mjs +20 -3
- package/dist/index.d.mts +10 -3
- package/dist/index.mjs +1 -1
- package/dist/{workspace-ChlHn5I8.mjs → workspace-DOwKm8pX.mjs} +109 -15
- package/package.json +1 -1
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`.
|
|
@@ -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
|
|
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 --
|
|
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
|
|
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
|
|
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)
|
|
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:
|
|
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
|
@@ -298,11 +298,15 @@ interface SyncOptions {
|
|
|
298
298
|
*/
|
|
299
299
|
onManifestSync?: (sync: ManifestSync) => void;
|
|
300
300
|
}
|
|
301
|
-
/**
|
|
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
|
+
*/
|
|
302
306
|
interface SyncResult {
|
|
303
|
-
/** Own dependencies of host workspaces that the sync re-resolved
|
|
307
|
+
/** Own dependencies of host workspaces that the sync re-resolved. */
|
|
304
308
|
ownDependencyChanges: DependencyMismatch[];
|
|
305
|
-
/** Check of the synced files
|
|
309
|
+
/** Check of the synced files. */
|
|
306
310
|
check: CheckResult;
|
|
307
311
|
}
|
|
308
312
|
/**
|
|
@@ -310,6 +314,9 @@ interface SyncResult {
|
|
|
310
314
|
*
|
|
311
315
|
* Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
|
|
312
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.
|
|
313
320
|
*/
|
|
314
321
|
export declare function syncNestedWorkspace(root: string, options?: SyncOptions): Promise<SyncResult>;
|
|
315
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,
|
|
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;
|
|
@@ -742,12 +742,40 @@ function resolveDependency(lock, workspaceName, dependency) {
|
|
|
742
742
|
return key === void 0 ? void 0 : lock.packages[key][0];
|
|
743
743
|
}
|
|
744
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
|
+
]);
|
|
745
775
|
/** Returns the nested workspace paths that the host `workspaces` patterns do not include. */
|
|
746
776
|
function findUncoveredWorkspaces(manifest, nestedLock) {
|
|
747
|
-
const patterns = (
|
|
748
|
-
|
|
749
|
-
const excluded = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => new Bun.Glob(pattern.slice(1)));
|
|
750
|
-
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));
|
|
751
779
|
}
|
|
752
780
|
/**
|
|
753
781
|
* Returns the paths of nested workspaces whose host lockfile entries differ from the relocated nested lockfile,
|
|
@@ -891,6 +919,33 @@ async function loadNestedWorkspace(root) {
|
|
|
891
919
|
nestedLock: relocateLockfile(await readLockfile(path.join(nestedRoot, "bun.lock")), config.dir, typeof nestedManifest.name === "string" ? nestedManifest.name : void 0)
|
|
892
920
|
};
|
|
893
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
|
+
}
|
|
894
949
|
/** Returns whether a check found no differences. */
|
|
895
950
|
function isInSync(result) {
|
|
896
951
|
return Object.values(result).every((list) => !list.length);
|
|
@@ -951,6 +1006,9 @@ async function runBunInstall(root, args = []) {
|
|
|
951
1006
|
*
|
|
952
1007
|
* Writes the synced `package.json`, writes the merged `bun.lock`, runs `bun install`,
|
|
953
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.
|
|
954
1012
|
*/
|
|
955
1013
|
async function syncNestedWorkspace(root, options = {}) {
|
|
956
1014
|
const workspace = await loadNestedWorkspace(root);
|
|
@@ -958,20 +1016,56 @@ async function syncNestedWorkspace(root, options = {}) {
|
|
|
958
1016
|
const manifestSync = syncManifest(workspace.manifest, workspace.nestedManifest);
|
|
959
1017
|
const { manifest } = manifestSync;
|
|
960
1018
|
const merged = mergeLockfiles(applyManifestToLockfile(workspace.lock, manifest), workspace.nestedLock, workspace.config.dir);
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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"));
|
|
970
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;
|
|
971
1065
|
}
|
|
972
1066
|
/** Serializes JSON with the indentation and final newline of the original text. */
|
|
973
1067
|
function formatJson(value, original) {
|
|
974
1068
|
const indent = /^[ \t]+(?=")/m.exec(original)?.[0] ?? " ";
|
|
975
1069
|
return `${JSON.stringify(value, null, indent)}${original.endsWith("\n") ? "\n" : ""}`;
|
|
976
1070
|
}
|
|
977
|
-
export {
|
|
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