@dbx-tools/projen 0.6.163 → 0.6.174

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
@@ -93,13 +93,37 @@ VS Code component; dbx-tools reuses it rather than constructing a second
93
93
  configurable `root` (default `packages/rs`), generates its Cargo manifest, and
94
94
  derives the crate name and repository from the parent project. A crate containing
95
95
  `uniffi::setup_scaffolding!()`
96
- automatically wires matching private Node and Python binding packages using the
96
+ automatically wires matching public Node and Python binding packages using the
97
97
  same capability name. Repository-specific dependencies and features remain
98
98
  declarative options in `.projenrc.ts`; generated bindings are built separately
99
- from projen synthesis.
99
+ from projen synthesis. Target-independent Node binding TypeScript is committed
100
+ and remains generated/read-only, while native libraries stay ignored. Node
101
+ facades compile to `lib/` and publish JavaScript entry points that plain Node
102
+ can load from `node_modules`. A complete `bindings.ts` / `_bindings.ts` /
103
+ `_bindings-ffi.ts` triplet is exported directly from the generated package
104
+ barrel. Python keeps `bindings.py` as the generated implementation and exports
105
+ its public names from a fully generated `__init__.py` with no editable marker
106
+ blocks. Node generation fails when direct binding names conflict with another
107
+ package-root export; Python generation refuses to overwrite a non-generated
108
+ package root. Do not create a `nodeExports` binding subpath or a handwritten
109
+ type facade.
110
+
111
+ Rust dependencies between binding-enabled workspace crates become Node
112
+ `workspace:*` and Python `internalDependencies` automatically. Python generation
113
+ selects the owning crate and supplies `external_packages` imports. UBRN currently
114
+ emits every linked component; the generation task discards dependency outputs
115
+ and rewrites their imports to the owning package root, including its generated
116
+ `uniffiModule` converter table. It never copies a dependency's records or enums.
117
+ Foreign callback adapters must first be wrapped in an object created by their
118
+ own native library (`createStorageHandle` for auth), because callback registries
119
+ are library-local. Release wheels replace sibling Git requirements with matching
120
+ native-wheel versions, and dependent publishers wait for their dependencies.
121
+ Each crate builds its own `<crate>-uniffi-bindgen` executable so a workspace
122
+ build has no colliding binary outputs. Packaging runs that prebuilt executable.
100
123
 
101
124
  `sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
102
- inside an existing UniFFI crate regenerate only that crate's bindings; adding or
125
+ inside an existing UniFFI crate regenerate that crate and its dependent bindings
126
+ in dependency order; adding or
103
127
  removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
104
128
  without Rust crates start no Rust watcher. When Rust projects are detected,
105
129
  Cargo is required and the focused task fails immediately if it is unavailable.
@@ -113,44 +137,27 @@ both the tag target and `HEAD` to equal that SHA before building. Its matrix has
113
137
  one row per target. Each row installs native dependencies and restores
114
138
  Cargo/sccache once,
115
139
  builds the Rust workspace once, then packages every discovered output from that
116
- shared build. Bun and the workspace install are present only when a Node binding
117
- needs TypeScript generation; uv is present only when a Python wheel is needed.
140
+ shared build. Rust rows package native npm archives and Python wheels; they do
141
+ not install Bun or UBRN and do not build Node facades.
118
142
  A new dispatch cancels queued and in-progress release and docs workflows before
119
143
  starting, and each workflow's concurrency group also cancels its previous run.
120
- A binary-only workspace therefore installs neither. `rustVersion` remains the
144
+ A binary-only workspace therefore installs no language package tool. `rustVersion` remains the
121
145
  MSRV recorded in package manifests, while `releaseRustVersion` independently
122
- defaults release compilation to `stable`. UBRN uses that same release toolchain
123
- unless `ubrnRustVersion` explicitly selects another one.
124
-
125
- Generated build and release workflows share the Bun cache helpers from
126
- `bun-workflow.ts`. One `BUN_VERSION` value drives setup and cache keys, while a
127
- generated dependency-only fingerprint keeps release version bumps from
128
- invalidating Bun's global package cache. `node_modules` remains uncached.
129
-
130
- The final host UBRN executable is cached by pinned UBRN version, Rust toolchain,
131
- runner OS, and runner architecture. A hit skips both the workspace `bun install`
132
- and the UBRN Cargo build. Release packaging passes the cached executable directly
133
- to the Node binding generator and keeps the package barrel copied from source,
134
- so it does not need the installed projen dependency graph. A miss saves the
135
- validated executable immediately, before workspace build and packaging can fail.
136
- Bun runtime setup still runs because the binding generator is TypeScript. Cargo registry caches
146
+ defaults release compilation to `stable`.
147
+ Stable Windows rows verify and use the hosted runner's installed Rust toolchain
148
+ and select `rust-lld` for the workspace build. Cargo registry caches
137
149
  and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
138
150
  version tags. Cache keys, restore results, sccache statistics, and phase timings
139
151
  are written to each build log. Python generation executes the already-built
140
- `target/<triple>/release/uniffi-bindgen` directly. Artifact packaging therefore
152
+ `target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
141
153
  does no Rust compilation after the main workspace build.
142
154
 
143
- Artifacts identify their crate, target, and type. Download-only publication
144
- jobs publish native npm platform archives before the facade and publish
145
- platform-tagged Python wheels without checkout, Bun, or `bun install`.
146
- Non-private Cargo crates publish from a source-only job with
147
- `cargo publish --no-verify`; GitHub Release uploads likewise consume prebuilt
148
- binary artifacts without reinstalling a toolchain.
149
-
150
- Set the repository variable `LOCAL_REPOSITORIES=true` to enable generated
151
- self-hosted publication from the prebuilt artifacts. Configure `LOCAL_NPM_REGISTRY` and
152
- `LOCAL_PYPI_PUBLISH_URL` with their matching `LOCAL_*` credentials. Set
153
- `LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
155
+ Artifacts identify their crate, target, and type. `rust-release` publishes
156
+ non-private Cargo crates with `cargo publish --no-verify`, uploads requested
157
+ binary assets to the GitHub release, then dispatches the verified Rust run id
158
+ with the tag and SHA. It never publishes npm or PyPI packages.
159
+
160
+ Set `LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
154
161
  [Kellnr](https://kellnr.io/) instance and provide `LOCAL_CARGO_TOKEN`. Public
155
162
  Cargo crates also publish directly to crates.io
156
163
  with `CARGO_REGISTRY_TOKEN`. Override `releaseTargets` only when a consumer has
@@ -161,12 +168,19 @@ architecture. Omit both filters to regenerate the complete maintained matrix.
161
168
  `DBX_TOOLS_RELEASE_PLATFORMS` can select the generated matrix without repeating
162
169
  environment parsing in a consumer.
163
170
 
164
- Private Python binding projects are marked with `[tool.dbx-tools] private =
165
- true`. They stay out of the standard uv/Python release and docs surfaces;
166
- `rust-release` publishes their prebuilt native wheels directly.
167
- After Rust's public artifacts finish, it dispatches the downstream `release`
168
- event. Python, Node, standalone Node, and docs consume that event independently
169
- and start together with the same verified tag and SHA. Without Rust,
171
+ Public UniFFI facades are marked with `dbxToolsConfig.uniffi = true` in Node
172
+ manifests and `[tool.dbx_tools.config] uniffi = true` in Python manifests. The
173
+ Node release downloads and publishes native archives first, publishes normal
174
+ workspace packages, then builds and publishes facades from the committed
175
+ generated TypeScript in dependency order. The Python release downloads every
176
+ platform wheel and publishes them from one trusted-publisher job per
177
+ distribution. The docs generators include them because they are publicly installable.
178
+ When a conventional Node binding path already belongs to a root subproject,
179
+ Rust mapping reuses that project and adds binding dependencies and metadata to
180
+ its existing manifest.
181
+ After Rust's artifacts finish, it dispatches the downstream `release` event
182
+ with its run id and attempt. Python, Node, and docs consume that event
183
+ independently with the same verified tag and SHA. Without Rust,
170
184
  `release-dispatch` sends that event immediately. Configure release GitHub
171
185
  environments to permit the generated release branch; the trusted-publisher
172
186
  instructions list that branch with the workflow and environment identity PyPI
@@ -249,8 +263,9 @@ never as a blanket `<package>/src/**`. A codegen package may hold hand-written
249
263
  modules beside its generated ones, and those must stay linted.
250
264
 
251
265
  `generateBarrels()` writes package-root `index.ts` barrels with module
252
- namespaces and flat unique type exports, returning the number that actually
253
- changed. A name two modules both declare is ambiguous and stays namespace-only —
266
+ namespaces, flat unique type exports, `PACKAGE_IDENTIFIER`, and
267
+ `PACKAGE_VERSION`, returning the number that actually changed. A name two
268
+ modules both declare is ambiguous and stays namespace-only —
254
269
  except when one of them is generated: the hand-written module is the curated view
255
270
  of the generated shape (`shared-genie`'s `genie-model.ts` extends its own
256
271
  codegen'd `dashboards.ts`), so it owns the name and stays hoisted. A barrel whose export surface is unchanged is left untouched,
package/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
+ export const PACKAGE_VERSION = "0.6.173";
6
7
  export * as barrels from "./src/barrels.ts";
7
8
  export * as bunApp from "./src/bun-app.ts";
8
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -58,7 +59,7 @@ export { DOWNSTREAM_RELEASE_EVENT, RUST_RELEASE_EVENT, RELEASE_TAG, RELEASE_SHA
58
59
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
59
60
  export type { PackageTag } from "./src/tags.ts";
60
61
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
61
- export { makeDefaultedInterfaceParametersOptional, addExplicitInterfaceReexports } from "./src/uniffi.ts";
62
+ export { makeDefaultedInterfaceParametersOptional, addTypeScriptExtensionsToBindingImports, addExplicitInterfaceReexports } from "./src/uniffi.ts";
62
63
  export type { TypeScriptBindingModule } from "./src/uniffi.ts";
63
64
  export { DBXToolsVsCode } from "./src/vscode.ts";
64
65
  export type { IgnoreGroupOptions } from "./src/watch.ts";
package/package.json CHANGED
@@ -2,7 +2,6 @@
2
2
  "name": "@dbx-tools/projen",
3
3
  "scripts": {
4
4
  "build": "projen build",
5
- "bump": "projen bump",
6
5
  "clobber": "projen clobber",
7
6
  "compile": "projen compile",
8
7
  "default": "projen default",
@@ -26,9 +25,9 @@
26
25
  },
27
26
  "dependencies": {
28
27
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.163",
30
- "@dbx-tools/path": "0.6.163",
31
- "@dbx-tools/shared-core": "0.6.163",
28
+ "@dbx-tools/core": "0.6.174",
29
+ "@dbx-tools/path": "0.6.174",
30
+ "@dbx-tools/shared-core": "0.6.174",
32
31
  "commander": "^15.0.0",
33
32
  "concurrently": "^10.0.3",
34
33
  "constructs": "^10.6.0",
@@ -48,7 +47,7 @@
48
47
  },
49
48
  "main": "index.ts",
50
49
  "license": "Apache-2.0",
51
- "version": "0.6.163",
50
+ "version": "0.6.174",
52
51
  "types": "index.ts",
53
52
  "type": "module",
54
53
  "exports": {
package/src/barrels.ts CHANGED
@@ -16,6 +16,12 @@
16
16
  * takes priority. This keeps the barrel auto-generated while letting you add or
17
17
  * override individual exports.
18
18
  *
19
+ * A UniFFI source triplet (`bindings.ts`, `_bindings.ts`, and
20
+ * `_bindings-ffi.ts`) is one special case: the internal underscore modules stay
21
+ * private, while `bindings.ts` is re-exported directly instead of under a
22
+ * `bindings` namespace. Generation fails when one of those direct binding names
23
+ * conflicts with another top-level package export.
24
+ *
19
25
  * Each eligible module becomes `export * as <name> from "./src/x.ts"` (camelCase
20
26
  * namespace from its path segments; invalid identifiers suffixed with `Module`),
21
27
  * sorted by module path.
@@ -28,9 +34,10 @@
28
34
  * `export { ... }`. The module namespaces stay either way, so a namespaced call
29
35
  * site keeps working.
30
36
  *
31
- * Every generated barrel also exports `PACKAGE_IDENTIFIER` from the package's own
32
- * `package.json`. Runtime helpers can retain the package specifier without
33
- * trying to recover it from an ESM namespace object or loader function.
37
+ * Every generated barrel also exports `PACKAGE_IDENTIFIER` and
38
+ * `PACKAGE_VERSION` from the package's own `package.json`. Runtime helpers can
39
+ * retain package identity without trying to recover it from an ESM namespace
40
+ * object or loader function.
34
41
  *
35
42
  * Uniqueness is tallied over types and values TOGETHER: a name carried by two
36
43
  * modules is ambiguous whichever kind it is, and hoisting one module's value
@@ -107,10 +114,16 @@ const BARREL_HEADER: HeaderOpts = {
107
114
  source: "the exporting modules in ./src",
108
115
  };
109
116
 
110
- /** Generated package identity export, reserved against source-module hoisting. */
117
+ /** Generated package metadata exports, reserved against source-module hoisting. */
111
118
  const PACKAGE_IDENTIFIER_EXPORT = "PACKAGE_IDENTIFIER";
119
+ const PACKAGE_VERSION_EXPORT = "PACKAGE_VERSION";
112
120
  const PACKAGE_IDENTIFIER_LINE = `export const ${PACKAGE_IDENTIFIER_EXPORT} = "";`;
121
+ const PACKAGE_VERSION_LINE = `export const ${PACKAGE_VERSION_EXPORT} = "";`;
113
122
  const PACKAGE_IDENTIFIER_LINE_RE = /^export const PACKAGE_IDENTIFIER = .*;$/m;
123
+ const PACKAGE_VERSION_LINE_RE = /^export const PACKAGE_VERSION = .*;$/m;
124
+ const UNIFFI_BINDINGS_FILE = "bindings.ts";
125
+ const UNIFFI_GENERATED_FILE = "_bindings.ts";
126
+ const UNIFFI_FFI_FILE = "_bindings-ffi.ts";
114
127
 
115
128
  /** `pnpm-workspace` -> `pnpmWorkspace`; `local-fs` -> `localFS` (`fs` -> `FS`). */
116
129
  function kebabToCamel(segment: string): string {
@@ -266,8 +279,9 @@ function customExportNames(file: string): Set<string> {
266
279
  * `export * as`), then the whole module is re-exported last.
267
280
  */
268
281
  function mergeCustomExports(content: string, pkgDir: string): string {
269
- if (!existsSync(join(pkgDir, CUSTOM_EXPORTS_FILE))) return content;
270
- const overridden = customExportNames(join(pkgDir, CUSTOM_EXPORTS_FILE));
282
+ const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
283
+ if (!existsSync(customPath) || isLegacyBindingsExport(customPath)) return content;
284
+ const overridden = customExportNames(customPath);
271
285
  const kept = content.split("\n").filter((line) => {
272
286
  const ns = /^export \* as (\w+) from /.exec(line)?.[1];
273
287
  return !(ns && overridden.has(ns));
@@ -275,29 +289,101 @@ function mergeCustomExports(content: string, pkgDir: string): string {
275
289
  return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports.ts";\n`;
276
290
  }
277
291
 
278
- /** Read the authoritative npm package name emitted by the package project. */
279
- function packageIdentifier(pkgDir: string): string {
292
+ function isLegacyBindingsExport(file: string): boolean {
293
+ return readFileSync(file, "utf8").trim() === 'export * from "./src/bindings.ts";';
294
+ }
295
+
296
+ function hasUniFFIBindings(srcDir: string): boolean {
297
+ return [UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE, UNIFFI_FFI_FILE].every((file) =>
298
+ existsSync(join(srcDir, file)),
299
+ );
300
+ }
301
+
302
+ function bindingExportNames(srcDir: string): Set<string> {
303
+ return new Set(
304
+ [UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE].flatMap((file) =>
305
+ moduleExports(join(srcDir, file)).map((entry) => entry.name),
306
+ ),
307
+ );
308
+ }
309
+
310
+ function barrelExportNames(content: string, customPath: string): Set<string> {
311
+ const names = new Set<string>([PACKAGE_IDENTIFIER_EXPORT, PACKAGE_VERSION_EXPORT]);
312
+ for (const match of content.matchAll(/^export \* as (\w+) from /gm)) {
313
+ names.add(match[1]!);
314
+ }
315
+ for (const match of content.matchAll(/^export(?: type)? \{([^}]+)\} from /gm)) {
316
+ for (const specifier of match[1]!.split(",")) {
317
+ const parts = specifier.trim().split(/\s+as\s+/);
318
+ const name = parts.at(-1);
319
+ if (name) names.add(name);
320
+ }
321
+ }
322
+ if (existsSync(customPath) && !isLegacyBindingsExport(customPath)) {
323
+ const statements = moduleStatements(customPath) as ReadonlyArray<{
324
+ type: string;
325
+ exported?: unknown;
326
+ }>;
327
+ if (
328
+ statements.some(
329
+ (statement) => statement.type === "ExportAllDeclaration" && !statement.exported,
330
+ )
331
+ ) {
332
+ throw new Error(
333
+ `Cannot verify UniFFI export conflicts through a bare export in ${customPath}`,
334
+ );
335
+ }
336
+ for (const name of customExportNames(customPath)) names.add(name);
337
+ }
338
+ return names;
339
+ }
340
+
341
+ function assertNoBindingExportConflicts(srcDir: string, content: string, customPath: string): void {
342
+ const other = barrelExportNames(content, customPath);
343
+ const conflicts = [...bindingExportNames(srcDir)].filter((name) => other.has(name)).sort();
344
+ if (conflicts.length > 0) {
345
+ throw new Error(
346
+ `UniFFI binding exports conflict with package exports in ${srcDir}: ${conflicts.join(", ")}`,
347
+ );
348
+ }
349
+ }
350
+
351
+ /** Read the authoritative npm package name and version emitted by the package project. */
352
+ function packageMetadata(pkgDir: string): { identifier: string; version: string } {
280
353
  const manifestPath = join(pkgDir, "package.json");
281
- const name = existsSync(manifestPath)
282
- ? json.parseRecord(readFileSync(manifestPath, "utf8"))?.name
354
+ const manifest = existsSync(manifestPath)
355
+ ? json.parseRecord(readFileSync(manifestPath, "utf8"))
283
356
  : undefined;
357
+ const name = manifest?.name;
284
358
  if (typeof name !== "string" || !name.trim()) {
285
359
  throw new Error(`Cannot generate barrel without package.json name: ${manifestPath}`);
286
360
  }
287
- return name;
361
+ const version = manifest?.version;
362
+ if (typeof version !== "string" || !version.trim()) {
363
+ throw new Error(`Cannot generate barrel without package.json version: ${manifestPath}`);
364
+ }
365
+ return { identifier: name, version };
288
366
  }
289
367
 
290
- /** Normalize package identity so structural barrel comparisons need no manifest read. */
291
- function withoutPackageIdentifier(content: string): string {
292
- return content.replace(PACKAGE_IDENTIFIER_LINE_RE, PACKAGE_IDENTIFIER_LINE);
368
+ /** Normalize package metadata before comparing the barrel's export structure. */
369
+ function withoutPackageMetadata(content: string): string {
370
+ return content
371
+ .replace(PACKAGE_IDENTIFIER_LINE_RE, PACKAGE_IDENTIFIER_LINE)
372
+ .replace(PACKAGE_VERSION_LINE_RE, PACKAGE_VERSION_LINE);
293
373
  }
294
374
 
295
- /** Resolve and insert package identity only when a barrel is about to be written. */
296
- function withPackageIdentifier(content: string, pkgDir: string): string {
297
- const line = `export const ${PACKAGE_IDENTIFIER_EXPORT} = ${JSON.stringify(
298
- packageIdentifier(pkgDir),
299
- )};`;
300
- return content.replace(PACKAGE_IDENTIFIER_LINE_RE, () => line);
375
+ /** Resolve and insert package metadata when a barrel is about to be written. */
376
+ function withPackageMetadata(content: string, pkgDir: string): string {
377
+ const metadata = packageMetadata(pkgDir);
378
+ return content
379
+ .replace(
380
+ PACKAGE_IDENTIFIER_LINE_RE,
381
+ () => `export const ${PACKAGE_IDENTIFIER_EXPORT} = ${JSON.stringify(metadata.identifier)};`,
382
+ )
383
+ .replace(
384
+ PACKAGE_VERSION_LINE_RE,
385
+ () => `export const ${PACKAGE_VERSION_EXPORT} = ${JSON.stringify(metadata.version)};`,
386
+ );
301
387
  }
302
388
 
303
389
  /**
@@ -353,6 +439,10 @@ function generateForPackage(pkgDir: string): number {
353
439
  if (!existing || (!isSourceExt(existing) && isSourceExt(f))) byModulePath.set(stem, f);
354
440
  }
355
441
  const modulePaths = [...byModulePath.keys()].sort((a, b) => a.localeCompare(b));
442
+ const uniffiBindings = hasUniFFIBindings(srcDir);
443
+ const namespacedModulePaths = uniffiBindings
444
+ ? modulePaths.filter((modulePath) => modulePath !== "bindings")
445
+ : modulePaths;
356
446
 
357
447
  // No eligible modules -> no barrel: drop any stale root barrel and bail.
358
448
  if (modulePaths.length === 0) {
@@ -368,35 +458,45 @@ function generateForPackage(pkgDir: string): number {
368
458
  // extension is written because `tsc` rewrites it on emit
369
459
  // (`rewriteRelativeImportExtensions`) - an extensionless specifier would be
370
460
  // copied through verbatim and Node's ESM resolver cannot probe for it.
371
- const namespaceExports = modulePaths
461
+ const namespaceExports = namespacedModulePaths
372
462
  .map((stem) => {
373
463
  const modulePath = `./src/${byModulePath.get(stem)!}`;
374
464
  return `export * as ${modulePathToNamespace(modulePath)} from "${modulePath}";`;
375
465
  })
376
466
  .join("\n");
377
- let content = `${PACKAGE_IDENTIFIER_LINE}\n${namespaceExports}`;
467
+ let content = `${PACKAGE_IDENTIFIER_LINE}\n${PACKAGE_VERSION_LINE}\n${namespaceExports}`;
378
468
  // Hoist package-unique named exports to the top level. Names a hand-authored
379
469
  // `exports.ts` declares are suppressed so that file stays authoritative.
380
470
  const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
381
- const suppress = existsSync(customPath) ? customExportNames(customPath) : new Set<string>();
471
+ const suppress =
472
+ existsSync(customPath) && !isLegacyBindingsExport(customPath)
473
+ ? customExportNames(customPath)
474
+ : new Set<string>();
382
475
  suppress.add(PACKAGE_IDENTIFIER_EXPORT);
476
+ suppress.add(PACKAGE_VERSION_EXPORT);
383
477
  content = hoistUniqueExports(content, pkgDir, suppress);
384
478
  // A sibling `exports.ts` overrides/extends the generated barrel and wins on conflict.
385
479
  content = mergeCustomExports(content, pkgDir);
480
+ if (uniffiBindings) {
481
+ assertNoBindingExportConflicts(srcDir, content, customPath);
482
+ content = content.replace(
483
+ PACKAGE_VERSION_LINE,
484
+ `${PACKAGE_VERSION_LINE}\nexport * from "./src/bindings.ts";`,
485
+ );
486
+ }
386
487
 
387
- // The barrel only *changes* when its export structure does. Package identity is
388
- // blanked on both sides of this comparison, so a no-op cycle never reads
389
- // package.json merely to reconstruct a value the existing barrel already has.
390
- // If the structural result matches what's on disk, leave the file - and its
391
- // read-only bit - completely untouched and report no change (0). This keeps the
392
- // watcher quiet on ordinary in-file edits (which leave the export * as … list
393
- // identical), and it is also what keeps a no-op cycle off the read-only bit
394
- // entirely: see {@link writeBarrel} for why unlocking a barrel we are not about
395
- // to rewrite is what produced spurious EACCES failures.
488
+ // Package metadata is blanked for the structural comparison, then restored
489
+ // from package.json so a version-only change still rewrites the barrel.
490
+ // If the structural result and package metadata match what's on disk, leave
491
+ // the file and its read-only bit untouched and report no change (0). This
492
+ // keeps the watcher quiet on ordinary in-file edits while allowing a version
493
+ // bump to update PACKAGE_VERSION.
396
494
  content = `${content.replace(/\n+$/, "")}\n`;
397
495
  const template = `${header(BARREL_HEADER)}\n${content}`;
398
- if (before !== undefined && withoutPackageIdentifier(before) === template) return 0;
399
- const next = withPackageIdentifier(template, pkgDir);
496
+ const next = withPackageMetadata(template, pkgDir);
497
+ if (before !== undefined && withoutPackageMetadata(before) === template && before === next) {
498
+ return 0;
499
+ }
400
500
 
401
501
  // Written whole (header included) rather than via `stampGenerated`, which would
402
502
  // re-read and rewrite the file to prepend the same header - a second write, and
package/src/project-js.ts CHANGED
@@ -446,13 +446,13 @@ function defaultProjectOptions(
446
446
  devDeps: ["@types/node@^24.6.0"],
447
447
  ...(isRoot
448
448
  ? {
449
- prettier: true,
450
- prettierOptions: {
451
- settings: PRETTIER_SETTINGS,
452
- ignoreFile: true,
453
- ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
- },
455
- }
449
+ prettier: true,
450
+ prettierOptions: {
451
+ settings: PRETTIER_SETTINGS,
452
+ ignoreFile: true,
453
+ ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
+ },
455
+ }
456
456
  : {}),
457
457
  ...options,
458
458
  ...copiedGitIgnoreOptions(options),
@@ -495,24 +495,23 @@ function defaultTypeScriptProjectOptions(
495
495
  // ESLint is configured once on the ROOT (see initProject) and lints the whole
496
496
  // tree, so packages don't emit their own config. A caller can still override.
497
497
  eslint: false,
498
- devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", "@types/bun@^1.3.14"],
498
+ devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", `@types/bun@${BUN_VERSION}`],
499
499
  ...options,
500
500
  ...copiedGitIgnoreOptions(options),
501
501
  };
502
502
  }
503
503
 
504
- // Pinned to match the subproject defaults so bun resolves a single typescript
505
- // across the workspace (a bare name -> `*` could pull a second, newer major).
506
- // `@types/bun` gives the `Bun.*` globals the server/app tags now use.
507
- const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
504
+ // Pinned to match the subproject defaults so bun resolves one TypeScript and
505
+ // one runtime-compatible set of Bun globals across the workspace.
506
+ const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", `@types/bun@${BUN_VERSION}`];
508
507
 
509
508
  /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
510
509
  export interface DBXToolsJavaScriptProjectOptions
511
510
  extends
512
- CommonProjectOptions,
513
- Partial<javascript.NodeProjectOptions>,
514
- DBXToolsConfigOptions,
515
- DBXToolsPNPMWorkspaceOptions {
511
+ CommonProjectOptions,
512
+ Partial<javascript.NodeProjectOptions>,
513
+ DBXToolsConfigOptions,
514
+ DBXToolsPNPMWorkspaceOptions {
516
515
  /**
517
516
  * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
518
517
  * the (resolved) project name; a leading `@` is optional.
@@ -593,8 +592,7 @@ export interface DBXToolsTypeScriptProjectOptions
593
592
  */
594
593
  export class DBXToolsNodeProject
595
594
  extends javascript.NodeProject
596
- implements DBXToolsJavaScriptProject
597
- {
595
+ implements DBXToolsJavaScriptProject {
598
596
  readonly language = "javascript" as const;
599
597
  readonly scope: string;
600
598
  readonly dbxToolsConfig: DBXToolsConfig;
@@ -694,8 +692,8 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
694
692
  installDependencies(trigger: unknown): void;
695
693
  logInstallTrigger(trigger: unknown): void;
696
694
  };
697
- nodePackage.installDependencies = () => {};
698
- nodePackage.logInstallTrigger = () => {};
695
+ nodePackage.installDependencies = () => { };
696
+ nodePackage.logInstallTrigger = () => { };
699
697
  },
700
698
  );
701
699
 
@@ -708,8 +706,7 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
708
706
  */
709
707
  export class DBXToolsTypeScriptProject
710
708
  extends typescript.TypeScriptProject
711
- implements DBXToolsJavaScriptProject
712
- {
709
+ implements DBXToolsJavaScriptProject {
713
710
  readonly language = "javascript" as const;
714
711
  readonly scope: string;
715
712
  readonly dbxToolsConfig: DBXToolsConfig;