1414// dependency range (see the @stll/docx-core ^0.4.0-vs-0.5.0 incident this
1515// script was added to prevent).
1616//
17- // The only fix once it drifts is `rm bun.lock && bun install` (a full
18- // regenerate). This script cross-checks each packages/*/package.json
19- // `version` against the version bun.lock has cached for that workspace, so
20- // the drift itself gets caught in CI instead of silently persisting.
17+ // This script is the single owner of workspace self-version synchronization:
18+ // check-only by default for CI, or byte-preserving repair with `--write`.
2119
2220import { panic } from "better-result" ;
2321import { readdir } from "node:fs/promises" ;
2422import { join } from "node:path" ;
23+ import { syncWorkspaceVersions } from "./lib/bun-lock-workspace-versions" ;
2524
2625const ROOT = join ( import . meta. dirname , ".." ) ;
2726
@@ -37,31 +36,13 @@ const workspaceDirs = entries
3736
3837const lockText = await Bun . file ( join ( ROOT , "bun.lock" ) ) . text ( ) ;
3938
40- // bun.lock is JSON-with-trailing-commas ("JSONC"-flavored), not strict JSON,
41- // so a plain JSON.parse fails on it as-is. Trailing commas only ever appear
42- // directly before a closing ` }`/`]`, and that exact sequence cannot occur
43- // inside any of bun.lock's own string values (workspace paths, package
44- // names/versions/specifiers, or the base64 `sha512-...` integrity hashes),
45- // so stripping them is a safe, structure-preserving normalize. Parsing the
46- // whole file once and reading `workspaces[dir].version` from the resulting
47- // object is immune to bun's key ordering or nested-object shape — unlike a
48- // per-block regex, there is no block to mis-extract.
49- const isRecord = ( value : unknown ) : value is Record < string , unknown > =>
50- typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
51-
52- const parsedLock : unknown = JSON . parse ( lockText . replace ( / , ( \s * [ } \] ] ) / g, "$1" ) ) ;
53- if ( ! isRecord ( parsedLock ) || ! isRecord ( parsedLock . workspaces ) ) {
54- panic ( "bun.lock did not parse into the expected { workspaces: {...} } shape" ) ;
39+ const args = process . argv . slice ( 2 ) ;
40+ if ( args . some ( ( arg ) => arg !== "--write" ) || args . filter ( ( arg ) => arg === "--write" ) . length > 1 ) {
41+ panic ( "Usage: bun scripts/check-lockfile-workspace-versions.ts [--write]" ) ;
5542}
56- const { workspaces : lockWorkspaces } = parsedLock ;
57-
58- const versionForWorkspace = ( workspacePath : string ) : string | null => {
59- const entry = lockWorkspaces [ workspacePath ] ;
60- if ( ! isRecord ( entry ) || typeof entry . version !== "string" ) return null ;
61- return entry . version ;
62- } ;
63-
64- const mismatches : string [ ] = [ ] ;
43+ const write = args [ 0 ] === "--write" ;
44+ const expectedVersions = new Map < string , string > ( ) ;
45+ const packageNames = new Map < string , string > ( ) ;
6546
6647for ( const workspaceDir of workspaceDirs ) {
6748 // A directory under packages/ is not necessarily a real workspace: skip
@@ -73,35 +54,47 @@ for (const workspaceDir of workspaceDirs) {
7354 const version = pkg . version ;
7455 if ( typeof name !== "string" || typeof version !== "string" ) continue ;
7556
76- const lockedVersion = versionForWorkspace ( workspaceDir ) ;
77- if ( lockedVersion === null ) {
78- mismatches . push ( `${ name } (${ workspaceDir } ): no bun.lock entry found` ) ;
79- continue ;
80- }
81- if ( lockedVersion !== version ) {
82- mismatches . push (
83- `${ name } (${ workspaceDir } ): package.json is ${ version } , bun.lock has ${ lockedVersion } ` ,
84- ) ;
85- }
57+ expectedVersions . set ( workspaceDir , version ) ;
58+ packageNames . set ( workspaceDir , name ) ;
8659}
8760
61+ const result = syncWorkspaceVersions ( lockText , expectedVersions ) ;
62+ const unrepairable = result . mismatches . filter ( ( { actual } ) => actual === null ) ;
63+
64+ if ( write && unrepairable . length === 0 && result . text !== lockText ) {
65+ await Bun . write ( join ( ROOT , "bun.lock" ) , result . text ) ;
66+ console . log ( `bun.lock workspace-version sync: updated ${ result . mismatches . length } workspace(s).` ) ;
67+ process . exit ( 0 ) ;
68+ }
69+
70+ const mismatches = result . mismatches . map ( ( { workspace, expected, actual } ) =>
71+ actual === null
72+ ? `${ packageNames . get ( workspace ) } (${ workspace } ): no writable bun.lock version entry found`
73+ : `${ packageNames . get ( workspace ) } (${ workspace } ): package.json is ${ expected } , bun.lock has ${ actual } ` ,
74+ ) ;
75+
8876if ( mismatches . length > 0 ) {
8977 console . error (
9078 [
9179 "bun.lock workspace-version drift detected:" ,
9280 "" ,
9381 ...mismatches . map ( ( line ) => ` - ${ line } ` ) ,
9482 "" ,
95- "A plain `bun install` will not fix this (it doesn't rewrite cached" ,
96- "workspace versions for entries that already exist). Regenerate the" ,
97- "lockfile instead :",
83+ write
84+ ? "The lockfile shape is incomplete; workspace entries must exist before they can be synchronized."
85+ : "A plain `bun install` will not fix cached workspace self-versions. Synchronize them with :",
9886 "" ,
99- " rm bun.lock && bun install" ,
87+ " bun scripts/check-lockfile-workspace-versions.ts --write" ,
88+ " bun install --frozen-lockfile" ,
10089 "" ,
10190 "Then commit the refreshed bun.lock." ,
10291 ] . join ( "\n" ) ,
10392 ) ;
10493 process . exit ( 1 ) ;
10594}
10695
107- console . log ( "bun.lock workspace-version check: all workspace versions match. OK." ) ;
96+ console . log (
97+ write
98+ ? "bun.lock workspace-version sync: already current. OK."
99+ : "bun.lock workspace-version check: all workspace versions match. OK." ,
100+ ) ;
0 commit comments