@dbx-tools/projen 0.6.40 → 0.6.41

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/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 * as barrels from "./src/barrels.ts";
6
+ export * as bunApp from "./src/bun-app.ts";
6
7
  export * as clean from "./src/clean.ts";
7
8
  export * as codegen from "./src/codegen.ts";
8
9
  export * as dbxToolsConfig from "./src/dbx-tools-config.ts";
@@ -20,7 +21,6 @@ export * as release from "./src/release.ts";
20
21
  export * as scaffold from "./src/scaffold.ts";
21
22
  export * as tags from "./src/tags.ts";
22
23
  export * as tsconfig from "./src/tsconfig.ts";
23
- export * as vite from "./src/vite.ts";
24
24
  export * as vscode from "./src/vscode.ts";
25
25
  export * as watch from "./src/watch.ts";
26
26
  export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
@@ -41,6 +41,6 @@ export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts
41
41
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
42
42
  export type { PackageTag } from "./src/tags.ts";
43
43
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
44
- export { DEFAULT_VITE_OVERRIDES, ViteConfigFile } from "./src/vite.ts";
44
+ export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
45
45
  export { DBXToolsVsCode } from "./src/vscode.ts";
46
46
  export type { IgnoreGroupOptions } from "./src/watch.ts";
package/package.json CHANGED
@@ -1,5 +1,22 @@
1
1
  {
2
2
  "name": "@dbx-tools/projen",
3
+ "scripts": {
4
+ "build": "projen build",
5
+ "bump": "projen bump",
6
+ "clobber": "projen clobber",
7
+ "compile": "projen compile",
8
+ "default": "projen default",
9
+ "demo": "projen demo",
10
+ "eject": "projen eject",
11
+ "package": "projen package",
12
+ "post-compile": "projen post-compile",
13
+ "post-upgrade": "projen post-upgrade",
14
+ "pre-compile": "projen pre-compile",
15
+ "test": "projen test",
16
+ "upgrade": "projen upgrade",
17
+ "watch": "projen watch",
18
+ "projen": "projen"
19
+ },
3
20
  "devDependencies": {
4
21
  "@types/node": "^24.6.0",
5
22
  "constructs": "^10.0.0",
@@ -23,6 +40,7 @@
23
40
  "tsx": "^4.23.0",
24
41
  "yaml": "^2.9.0"
25
42
  },
43
+ "pnpm": {},
26
44
  "devEngines": {
27
45
  "packageManager": {
28
46
  "name": "pnpm",
@@ -32,7 +50,8 @@
32
50
  },
33
51
  "main": "index.ts",
34
52
  "license": "Apache-2.0",
35
- "version": "0.6.40",
53
+ "version": "0.6.41",
54
+ "packageManager": "pnpm@10.33.0",
36
55
  "types": "index.ts",
37
56
  "type": "module",
38
57
  "exports": {
@@ -44,22 +63,5 @@
44
63
  "src",
45
64
  "tasks"
46
65
  ],
47
- "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
48
- "scripts": {
49
- "build": "projen build",
50
- "bump": "projen bump",
51
- "clobber": "projen clobber",
52
- "compile": "projen compile",
53
- "default": "projen default",
54
- "demo": "projen demo",
55
- "eject": "projen eject",
56
- "package": "projen package",
57
- "post-compile": "projen post-compile",
58
- "post-upgrade": "projen post-upgrade",
59
- "pre-compile": "projen pre-compile",
60
- "test": "projen test",
61
- "upgrade": "projen upgrade",
62
- "watch": "projen watch",
63
- "projen": "projen"
64
- }
65
- }
66
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
67
+ }
package/src/bun-app.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Bun browser-app scaffolding as first-class projen file components.
3
+ *
4
+ * Replaces the old Vite toolchain (`vite.ts`). An `app`-tagged package gets three
5
+ * generated, read-only files that stand in for `vite dev` / `vite build`:
6
+ *
7
+ * - {@link BunfigFile} - `bunfig.toml` wiring `bun-plugin-tailwind` into both the
8
+ * dev server (`[serve.static]`) and `Bun.build` (`[build]`) so Tailwind v4's
9
+ * `@import "tailwindcss"` + `@source` directives compile with no separate CLI.
10
+ * - {@link BunDevServerFile} - `dev.ts`: a `Bun.serve()` fullstack dev server that
11
+ * imports `index.html` and serves the SPA with React hot module reloading.
12
+ * - {@link BunBuildFile} - `build.ts`: a `Bun.build()` production bundle of
13
+ * `index.html` (the `bun-plugin-tailwind` plugin must run through the JS API,
14
+ * NOT the `bun build` CLI, which does not load it).
15
+ *
16
+ * Each file supports an unmanaged OVERRIDE beside it (`bunfig.override.toml`,
17
+ * `bun-dev.override.ts`, `bun-build.override.ts`): the dev/build scripts import
18
+ * the override's default export and merge it over the generated options, so a
19
+ * package tweaks its server/bundle WITHOUT editing the projen-owned file - the
20
+ * same escape hatch the Vite generator offered via `vite.config.override.*`.
21
+ *
22
+ * bun runs these `.ts` files directly. Being package-ROOT files (not under
23
+ * `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
24
+ * usage never trips the `app` package's DOM-only `compile`.
25
+ */
26
+ import { type Project, TextFile } from "projen";
27
+
28
+ /** Unmanaged dev-server override module, merged over the generated options if present. */
29
+ export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
30
+ /** Unmanaged build override module, merged over the generated options if present. */
31
+ export const BUN_BUILD_OVERRIDE = "bun-build.override.ts";
32
+ /** Unmanaged bunfig override (documented; merging TOML is left to the user). */
33
+ export const BUN_APP_OVERRIDES = ["bunfig.override.toml", BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
34
+
35
+ /**
36
+ * Root `bunfig.toml`: pin bun's HOISTED linker workspace-wide.
37
+ *
38
+ * bun's default (isolated) linker instantiates a peer dependency once PER
39
+ * distinct peer context, so a package with many peers (`@mastra/*`) resolves to
40
+ * two peer-hash variants of the SAME `@mastra/core` version - and TypeScript then
41
+ * rejects passing a value built against one to an API typed by the other
42
+ * ("separate declarations of a private property"). The hoisted linker de-dupes to
43
+ * a single flat copy (npm-style), which is what keeps identity-based singletons
44
+ * (AppKit `CacheManager`, Mastra classes, one React) coherent - the same coherence
45
+ * the old cross-workspace `.pnpmfile.cjs` bridge used to guarantee.
46
+ */
47
+ export class RootBunfigFile extends TextFile {
48
+ constructor(project: Project) {
49
+ super(project, "bunfig.toml", {
50
+ marker: false,
51
+ readonly: true,
52
+ lines: [
53
+ '# ~~ Generated by projen. To modify, edit .projenrc.ts and run "bunx projen".',
54
+ "# Hoisted (flat) node_modules so a peer dep like @mastra/core resolves to ONE",
55
+ "# copy - the isolated default splits it into peer-hash variants and breaks",
56
+ "# identity-based singletons + cross-package types.",
57
+ "[install]",
58
+ 'linker = "hoisted"',
59
+ "",
60
+ ],
61
+ });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * `bunfig.toml`: wire `bun-plugin-tailwind` into the DEV SERVER.
67
+ *
68
+ * Only `[serve.static]` is set - that is the one config bun reads for plugins. The
69
+ * `bun build` CLI does NOT load `bunfig.toml` plugins (a documented limitation),
70
+ * so the production build goes through `Bun.build()`'s JS API in `build.ts`, which
71
+ * passes the plugin directly. A `[build]` section here would be dead config that
72
+ * falsely implies `bun build ./index.html` applies Tailwind, so it is omitted.
73
+ */
74
+ export class BunfigFile extends TextFile {
75
+ constructor(project: Project) {
76
+ super(project, "bunfig.toml", {
77
+ marker: true,
78
+ readonly: true,
79
+ lines: [
80
+ "# Tailwind v4 for the dev server (Bun.serve reads [serve.static] plugins).",
81
+ "# Production build applies the plugin via Bun.build's API in build.ts - the",
82
+ "# `bun build` CLI does not load bunfig plugins, so there is no [build] here.",
83
+ "[serve.static]",
84
+ 'plugins = ["bun-plugin-tailwind"]',
85
+ "",
86
+ ],
87
+ });
88
+ }
89
+ }
90
+
91
+ /** `dev.ts`: a `Bun.serve()` fullstack dev server with React HMR. */
92
+ export class BunDevServerFile extends TextFile {
93
+ constructor(project: Project) {
94
+ super(project, "dev.ts", {
95
+ marker: true,
96
+ readonly: true,
97
+ lines: String.raw`
98
+ import { existsSync } from "node:fs";
99
+ import index from "./index.html";
100
+
101
+ // Unmanaged override (${BUN_DEV_OVERRIDE}): its default export is merged over the
102
+ // generated serve options - later wins - so a package tweaks the dev server
103
+ // without editing this generated file.
104
+ let options: Bun.ServeFunctionOptions<unknown, {}> = {
105
+ port: Number(process.env.PORT ?? 5173),
106
+ development: { hmr: true, console: true },
107
+ routes: { "/*": index },
108
+ };
109
+
110
+ const overrideUrl = new URL("./${BUN_DEV_OVERRIDE}", import.meta.url);
111
+ if (existsSync(overrideUrl)) {
112
+ const mod = await import(overrideUrl.href);
113
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
114
+ options = { ...options, ...override };
115
+ }
116
+
117
+ const server = Bun.serve(options);
118
+ console.log("dev server running at " + server.url.href);
119
+ `
120
+ .trimStart()
121
+ .split("\n"),
122
+ });
123
+ }
124
+ }
125
+
126
+ /** `build.ts`: a `Bun.build()` production bundle (Tailwind via the plugin API). */
127
+ export class BunBuildFile extends TextFile {
128
+ constructor(project: Project) {
129
+ super(project, "build.ts", {
130
+ marker: true,
131
+ readonly: true,
132
+ lines: String.raw`
133
+ import { existsSync } from "node:fs";
134
+ import tailwind from "bun-plugin-tailwind";
135
+
136
+ // Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
137
+ // the generated build options - later wins.
138
+ let options: Bun.BuildConfig = {
139
+ entrypoints: ["./index.html"],
140
+ outdir: "./dist",
141
+ minify: true,
142
+ sourcemap: "linked",
143
+ plugins: [tailwind],
144
+ };
145
+
146
+ const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
147
+ if (existsSync(overrideUrl)) {
148
+ const mod = await import(overrideUrl.href);
149
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
150
+ options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
151
+ }
152
+
153
+ const result = await Bun.build(options);
154
+ if (!result.success) {
155
+ for (const message of result.logs) console.error(message);
156
+ process.exit(1);
157
+ }
158
+ console.log("built " + result.outputs.length + " files to " + options.outdir);
159
+ `
160
+ .trimStart()
161
+ .split("\n"),
162
+ });
163
+ }
164
+ }
package/src/packages.ts CHANGED
@@ -85,18 +85,20 @@ function nestingTagsFromSegments(segments: readonly string[]): string[] {
85
85
  /** Matches a barrel `index.<ext>` (as a basename or a posix path tail). */
86
86
  const BARREL_RE = /(^|\/)index\.(ts|tsx|js|jsx|mjs|cjs)$/;
87
87
 
88
- /** Basenames this toolchain generates (projen manifests/tsconfigs + vite config). */
88
+ /** Basenames this toolchain generates (projen manifests/tsconfigs + bun app scaffolding). */
89
89
  const GENERATED_BASENAMES = new Set([
90
90
  "package.json",
91
91
  "tsconfig.json",
92
92
  "tsconfig.dev.json",
93
- "vite.config.ts",
93
+ "bunfig.toml",
94
+ "dev.ts",
95
+ "build.ts",
94
96
  ]);
95
97
 
96
98
  /**
97
99
  * True if the file matches the watcher's generated-file heuristic: projen manifest
98
- * basenames, package-root barrels (`index.ts`), vite config, or declaration files.
99
- * Other read-only toolchain output (e.g. openapi artifacts) is not covered here.
100
+ * basenames, package-root barrels (`index.ts`), bun app scaffolding, or declaration
101
+ * files. Other read-only toolchain output (e.g. openapi artifacts) is not covered here.
100
102
  */
101
103
  export function isGeneratedFile(file: string): boolean {
102
104
  const base = file.split(sep).pop() ?? "";
@@ -67,8 +67,8 @@ const DEFAULT_CATALOG: Catalog = {
67
67
  "react-dom": "^19.2.4",
68
68
  "@types/react": "^19.2.2",
69
69
  "@types/react-dom": "^19.2.2",
70
- vite: "^7.1.14",
71
- "@vitejs/plugin-react": "^5.0.4",
70
+ // Tailwind v4 compiler for bun's dev server + `Bun.build` (the `app` tag).
71
+ "bun-plugin-tailwind": "^0.1.2",
72
72
  "@types/node": "^24.6.0",
73
73
  "@types/express": "^5.0.5",
74
74
  express: "^5.1.0",
@@ -95,14 +95,26 @@ const DEFAULT_CATALOG: Catalog = {
95
95
  };
96
96
 
97
97
  /**
98
- * Build allowances every workspace needs, because the engine itself is what
99
- * drags each one in: `esbuild` arrives with tsx (so it has to be built for any
100
- * task to run at all), and `unrs-resolver` is the native binding behind
101
- * `eslint-import-resolver-typescript`, which projen's eslint component adds to
102
- * every generated project. Leaving either unlisted greets a freshly bootstrapped
103
- * workspace with pnpm's "Ignored build scripts" warning on its first install.
98
+ * Build allowances every workspace needs. These matter for the Databricks Apps
99
+ * pnpm install (which reads `pnpm-workspace.yaml`) and for bun (mirrored into the
100
+ * root `package.json` `trustedDependencies`): `unrs-resolver` is the native
101
+ * binding behind `eslint-import-resolver-typescript`, which projen's eslint
102
+ * component adds to every generated project, and `esbuild` is still pulled in by
103
+ * parts of the toolchain. Leaving either unlisted greets a freshly bootstrapped
104
+ * workspace with an "Ignored build scripts" warning on its first install.
104
105
  */
105
- const DEFAULT_ALLOW_BUILDS: AllowBuilds = { esbuild: true, "unrs-resolver": true };
106
+ const DEFAULT_ALLOW_BUILDS: AllowBuilds = {
107
+ esbuild: true,
108
+ "unrs-resolver": true,
109
+ // The `bun` npm package (a peer of `bun-plugin-tailwind`) ships a `bun.exe`
110
+ // placeholder and downloads the real platform binary in its postinstall. Left
111
+ // unbuilt, that `.exe` shim lands in `node_modules/.bin/bun` and makes projen's
112
+ // dax PATH walk `spawn ENOEXEC` on macOS/Linux, breaking EVERY task. Building it
113
+ // replaces the shim with a runnable binary.
114
+ bun: true,
115
+ // fastembed's native ONNX runtime (via `@mastra/fastembed`).
116
+ "onnxruntime-node": true,
117
+ };
106
118
 
107
119
  /**
108
120
  * pnpm settings this engine applies to every workspace, beyond members, catalog,
@@ -207,17 +219,66 @@ export class PnpmWorkspaceState {
207
219
  }
208
220
 
209
221
  /**
210
- * Fill `packages` from the project's attached subprojects.
222
+ * Emit `pnpm-workspace.yaml` directly.
223
+ *
224
+ * Under bun, projen's base `NodePackage` never runs `configurePnpm` (that call
225
+ * site is gated to `packageManager === PNPM`), so no `PnpmWorkspaceYaml`
226
+ * component is created. This engine still needs the file for the Databricks
227
+ * Apps platform, whose build phase installs with pnpm and reads the catalog +
228
+ * `allowBuilds`. The native component is just a `YamlFile` fed by a lazy
229
+ * `toJson(options)`, so constructing it directly (with the same options object
230
+ * whose arrays/objects are mutated in place up to synth) produces the identical
231
+ * file. Idempotent: skips if a `pnpm-workspace.yaml` component already exists
232
+ * (e.g. a future projen that DOES create it under bun).
233
+ */
234
+ public attachWorkspaceFile(project: Project): void {
235
+ const exists = project.files.some((file) => file.path === "pnpm-workspace.yaml");
236
+ if (!exists) new javascript.PnpmWorkspaceYaml(project, this.options);
237
+ }
238
+
239
+ /**
240
+ * Fill `packages` from the project's attached subprojects, then mirror the
241
+ * workspace shape into the root `package.json` so bun (which reads
242
+ * `workspaces`/`catalog` from the manifest, not `pnpm-workspace.yaml`) resolves
243
+ * the same members + catalog. `trustedDependencies` (bun's `allowBuilds` analog)
244
+ * is rendered by projen from its own allowlist, so the build allowances are
245
+ * routed there via `addAllowedScripts`.
211
246
  *
212
247
  * Called from the root's `preSynthesize`, which is the earliest point every
213
248
  * discovered package is attached - the list cannot be captured at
214
249
  * construction, since the root's scan runs after it.
215
250
  */
216
- public resolveMembers(project: Project): void {
251
+ public resolveMembers(project: Project, extraMembers: readonly string[] = []): void {
217
252
  const members = project.subprojects
218
253
  .map((sub) => toPosix(relative(project.outdir, sub.outdir)))
219
254
  .filter(Boolean);
255
+ // `extraMembers` are workspace siblings NOT attached as subprojects (e.g. the
256
+ // self-synthesizing `projen/` engine), so they must be added explicitly.
257
+ const all = [...members, ...extraMembers.map((m) => toPosix(m)).filter(Boolean)];
220
258
  // Replaces the array's CONTENTS, keeping the reference projen captured.
221
- this.packages.splice(0, this.packages.length, ...new Set(members.sort()));
259
+ this.packages.splice(0, this.packages.length, ...new Set(all.sort()));
260
+ this.mirrorToPackageJson(project);
261
+ }
262
+
263
+ /**
264
+ * Write `workspaces` / `catalog` / `overrides` into the root `package.json`
265
+ * (bun's source of truth) and feed the build allowances into projen's
266
+ * allowlist so bun's native `trustedDependencies` renders them.
267
+ */
268
+ private mirrorToPackageJson(project: Project): void {
269
+ const pkg = (project as { package?: javascript.NodePackage }).package;
270
+ if (!pkg) return;
271
+ pkg.addField("workspaces", [...this.packages]);
272
+ if (Object.keys(this.catalog).length > 0) {
273
+ pkg.addField("catalog", { ...this.catalog });
274
+ }
275
+ if (Object.keys(this.overrides).length > 0) {
276
+ pkg.addField("overrides", { ...this.overrides });
277
+ }
278
+ // projen renders bun's `trustedDependencies` from `allowedScripts`.
279
+ const allowed = Object.entries(this.allowBuilds)
280
+ .filter(([, on]) => on)
281
+ .map(([name]) => name);
282
+ if (allowed.length > 0) pkg.addAllowedScripts(...allowed);
222
283
  }
223
284
  }
package/src/project.ts CHANGED
@@ -22,7 +22,13 @@ import { applyCompiledPublish } from "./publish.ts";
22
22
  import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
23
23
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
24
24
  import { DBXToolsRootTsconfig } from "./tsconfig.ts";
25
- import { DEFAULT_VITE_OVERRIDES, ViteConfigFile } from "./vite.ts";
25
+ import {
26
+ BUN_APP_OVERRIDES,
27
+ BunBuildFile,
28
+ BunDevServerFile,
29
+ BunfigFile,
30
+ RootBunfigFile,
31
+ } from "./bun-app.ts";
26
32
  import { DBXToolsVsCode } from "./vscode.ts";
27
33
  import {
28
34
  DEFAULT_PACKAGE_ROOTS,
@@ -147,11 +153,11 @@ function applyRepository(project: javascript.NodeProject, override?: string): vo
147
153
  });
148
154
  }
149
155
 
150
- /** Inherit a parent's package manager, else pnpm. */
156
+ /** Inherit a parent's package manager, else bun. */
151
157
  function inheritedPackageManager(
152
158
  parent: javascript.NodeProject | undefined,
153
159
  ): javascript.NodePackageManager {
154
- return parent?.package.packageManager ?? javascript.NodePackageManager.PNPM;
160
+ return parent?.package.packageManager ?? javascript.NodePackageManager.BUN;
155
161
  }
156
162
 
157
163
  /** Override a package's generated tsconfig `compilerOptions` (later-wins per key). */
@@ -328,12 +334,19 @@ export const PROJEN_VERSION = "^0.101.16";
328
334
  function defaultProjectOptions(options: DBXToolsProjectOptions): DBXToolsProjectOptions {
329
335
  const isRoot = options.parent === undefined;
330
336
  return {
331
- packageManager: javascript.NodePackageManager.PNPM,
337
+ // Bun owns install/run/build/test locally and in CI. projen renders
338
+ // `bun install`/`bunx` and a native `trustedDependencies` field from this.
339
+ // The engine still emits `pnpm-workspace.yaml` itself (see
340
+ // {@link PnpmWorkspaceState}) for the Databricks Apps platform, whose build
341
+ // phase installs with pnpm - so a deployed app keeps its catalog + build
342
+ // allowances even though the local/CI manager is bun.
343
+ packageManager: javascript.NodePackageManager.BUN,
332
344
  // Pinned rather than left to projen's "latest": 0.101.16 is the first release
333
- // whose `NodePackage` owns `pnpm-workspace.yaml` natively, and this engine
334
- // writes that file itself ({@link DBXToolsPNPMWorkspace}). Floating would let
335
- // an install cross that boundary silently, so the version the engine is known
336
- // to co-exist with is stated here and bumped deliberately.
345
+ // whose `NodePackage` renders bun's `trustedDependencies` natively. Under bun,
346
+ // projen does NOT create the `pnpm-workspace.yaml` component itself (that call
347
+ // site is gated to pnpm), so the engine constructs it directly ({@link
348
+ // PnpmWorkspaceState}). Floating would let an install cross that boundary
349
+ // silently, so the co-tested version is stated here and bumped deliberately.
337
350
  projenVersion: PROJEN_VERSION,
338
351
  defaultReleaseBranch: "main",
339
352
  projenrcJs: false,
@@ -403,8 +416,8 @@ function copiedGitIgnoreOptions(
403
416
  /**
404
417
  * The engine's `TypeScriptProject` defaults - a superset of {@link defaultProjectOptions}.
405
418
  * A DBXTools TS project can itself be the ROOT (a standalone compiling root), so the
406
- * same parent-based root/child logic applies; this just layers on tsx/typescript and
407
- * disables sample code.
419
+ * same parent-based root/child logic applies; this just layers on typescript +
420
+ * bun types and disables sample code. No `tsx`: bun runs `.ts` directly.
408
421
  */
409
422
  function defaultTypeScriptProjectOptions(
410
423
  options: DBXToolsTypeScriptProjectOptions,
@@ -417,15 +430,16 @@ function defaultTypeScriptProjectOptions(
417
430
  // ESLint is configured once on the ROOT (see initProject) and lints the whole
418
431
  // tree, so packages don't emit their own config. A caller can still override.
419
432
  eslint: false,
420
- devDeps: [...(base.devDeps ?? []), "tsx@^4.23.0", "typescript@^5.9.3"],
433
+ devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", "@types/bun@^1.3.14"],
421
434
  ...options,
422
435
  ...copiedGitIgnoreOptions(options),
423
436
  };
424
437
  }
425
438
 
426
- // Pinned to match the subproject defaults so pnpm resolves a single tsx/typescript
439
+ // Pinned to match the subproject defaults so bun resolves a single typescript
427
440
  // across the workspace (a bare name -> `*` could pull a second, newer major).
428
- const DEV_DEPS_ROOT: string[] = ["tsx@^4.23.0", "typescript@^5.9.3"];
441
+ // `@types/bun` gives the `Bun.*` globals the server/app tags now use.
442
+ const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
429
443
 
430
444
  /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
431
445
  export interface DBXToolsProjectOptions
@@ -478,13 +492,22 @@ export interface DBXToolsProjectOptions
478
492
  * `@dbx-tools/projen` engine in `projen/`, tagged `projen-v*`).
479
493
  */
480
494
  readonly standaloneReleases?: readonly StandaloneRelease[];
495
+ /**
496
+ * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
497
+ * config ALONGSIDE the discovered `packageRoots` members - for a package that
498
+ * is synthesized by its OWN `.projenrc.ts` (so it isn't a root subproject) but
499
+ * should still resolve as a workspace sibling. The `@dbx-tools/projen` engine in
500
+ * `projen/` is the case: it synthesizes itself (avoiding a dogfooding cycle) yet
501
+ * is a member of the single bun workspace, so the root links it from source.
502
+ */
503
+ readonly extraWorkspaceMembers?: readonly string[];
481
504
  }
482
505
 
483
506
  /** Options for {@link DBXToolsTypeScriptProject} (a package, or a compiling root). */
484
507
  export interface DBXToolsTypeScriptProjectOptions
485
508
  extends Partial<typescript.TypeScriptProjectOptions>, DBXToolsProjectOptions {
486
- /** Emit a projen-owned `vite.config.ts`. */
487
- readonly viteConfig?: boolean;
509
+ /** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
510
+ readonly bunApp?: boolean;
488
511
  }
489
512
 
490
513
  /**
@@ -498,6 +521,7 @@ export class DBXToolsNodeProject extends javascript.NodeProject implements DBXTo
498
521
  pnpmWorkspace?: PnpmWorkspaceState;
499
522
  rootTsconfig?: DBXToolsRootTsconfig;
500
523
  vsCode?: DBXToolsVsCode;
524
+ private readonly extraWorkspaceMembers: readonly string[];
501
525
 
502
526
  constructor(options: DBXToolsProjectOptions = {}) {
503
527
  const { name, scope } = resolveIdentity(options);
@@ -505,11 +529,12 @@ export class DBXToolsNodeProject extends javascript.NodeProject implements DBXTo
505
529
  options.release && options.releaseTrigger === undefined
506
530
  ? { releaseTrigger: ReleaseTrigger.tagged({ tags: ["v*"] }) }
507
531
  : {};
508
- // Before `super`, since `NodePackage` creates the native
509
- // `javascript.PnpmWorkspaceYaml` from these options inside the base
510
- // constructor and `this` is unreachable until it returns. Given nothing,
511
- // projen writes no workspace file at all - it never derives members from
512
- // `project.subprojects` (see `pnpm-workspace.ts`).
532
+ // Holds the workspace state (members/catalog/allowBuilds/overrides). Under
533
+ // bun, projen's base constructor does NOT create the `PnpmWorkspaceYaml`
534
+ // component (its `configurePnpm` call site is gated to pnpm), so this state's
535
+ // options are wired into a directly-constructed component below - AND mirrored
536
+ // into `package.json` (`workspaces`/`catalog`) for bun to read. The
537
+ // `pnpm-workspace.yaml` is still emitted for the Databricks Apps pnpm install.
513
538
  const pnpmWorkspace = new PnpmWorkspaceState(options);
514
539
  super({
515
540
  ...defaultProjectOptions(options),
@@ -522,7 +547,12 @@ export class DBXToolsNodeProject extends javascript.NodeProject implements DBXTo
522
547
  });
523
548
 
524
549
  this.pnpmWorkspace = pnpmWorkspace;
550
+ // Emit `pnpm-workspace.yaml` ourselves: under bun projen skips the native
551
+ // component, but the file is still required by the Databricks Apps platform
552
+ // (its build phase installs with pnpm and reads catalog + `allowBuilds`).
553
+ pnpmWorkspace.attachWorkspaceFile(this);
525
554
  this.scope = scope;
555
+ this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
526
556
  this.dbxToolsConfig = new DBXToolsConfig(this, options);
527
557
  initProject(this, options);
528
558
  }
@@ -531,7 +561,8 @@ export class DBXToolsNodeProject extends javascript.NodeProject implements DBXTo
531
561
  super.preSynthesize();
532
562
  // Members come from the attached subprojects, which the root's scan appends
533
563
  // after construction - so the list is filled here, not in the constructor.
534
- this.pnpmWorkspace?.resolveMembers(this);
564
+ // `extraWorkspaceMembers` adds self-synthesizing siblings (e.g. `projen/`).
565
+ this.pnpmWorkspace?.resolveMembers(this, this.extraWorkspaceMembers);
535
566
  preSynthesizeProject(this);
536
567
  }
537
568
  }
@@ -588,8 +619,19 @@ export class DBXToolsTypeScriptProject
588
619
  "./package.json": "./package.json",
589
620
  });
590
621
  addPackageFiles(this, "index.ts", "src");
591
- this.testTask.exec("tsx --test 'test/**/*.test.ts'");
592
- if (options.viteConfig ?? false) new ViteConfigFile(this);
622
+ // `bun test` intercepts `node:test` (the suites keep using node:test) and
623
+ // runs it with bun's own fast runner. Args are FILTERS, not globs; a bare
624
+ // directory auto-discovers `*.test.ts` recursively. But `bun test` EXITS 1
625
+ // when it matches no files (unlike the old `tsx --test 'glob'`, which was a
626
+ // no-op), so guard it: only invoke when a `*.test.ts` exists, else succeed.
627
+ this.testTask.exec(
628
+ 'find test -name "*.test.ts" 2>/dev/null | grep -q . && bun test test || true',
629
+ );
630
+ if (options.bunApp ?? false) {
631
+ new BunfigFile(this);
632
+ new BunDevServerFile(this);
633
+ new BunBuildFile(this);
634
+ }
593
635
  initProject(this, options);
594
636
  }
595
637
 
@@ -746,16 +788,17 @@ function registerRootTasks(project: javascript.NodeProject): void {
746
788
  }
747
789
 
748
790
  /**
749
- * `tsx node_modules/@dbx-tools/projen/tasks/<script>` command for a projen task.
791
+ * `bun node_modules/@dbx-tools/projen/tasks/<script>` command for a projen task.
750
792
  *
751
- * Use the stable package symlink, never `require.resolve()`'s physical pnpm
752
- * store path. A later install can change the peer-hash directory while leaving
753
- * the package symlink valid; persisting the physical path made every generated
754
- * task fail with ERR_MODULE_NOT_FOUND after such an update.
793
+ * Use the stable package symlink, never `require.resolve()`'s physical store
794
+ * path. A later install can change the peer-hash directory while leaving the
795
+ * package symlink valid; persisting the physical path made every generated task
796
+ * fail with ERR_MODULE_NOT_FOUND after such an update. bun runs the `.ts`
797
+ * directly (no tsx, no build step).
755
798
  */
756
799
  export function taskScript(_project: javascript.NodeProject, script: string, args = ""): string {
757
800
  const scriptPath = toPosix(join("node_modules", "@dbx-tools", "projen", "tasks", script));
758
- return args ? `tsx ${scriptPath} ${args}` : `tsx ${scriptPath}`;
801
+ return args ? `bun ${scriptPath} ${args}` : `bun ${scriptPath}`;
759
802
  }
760
803
 
761
804
  /**
@@ -788,20 +831,21 @@ function initProject(
788
831
  project.package.file.readonly = false;
789
832
 
790
833
  // NodeProject has no built-in TS projenrc support (unlike TypeScriptProject), so
791
- // wire `.projenrc.ts` through the tsx runner - this also populates the `default`
792
- // task that `pnpm exec projen` runs (and that the `sync` watcher invokes to re-synth).
834
+ // wire `.projenrc.ts` through a runner - this also populates the `default` task
835
+ // that `bunx projen` runs (and that the `sync` watcher invokes to re-synth).
836
+ // The runner choice is immaterial since the exec is reset to plain `bun` below;
837
+ // `nodejs()` avoids declaring a `ts-node`/`tsx` dependency.
793
838
  new typescript.ProjenrcTs(project, {
794
- runner: typescript.TypeScriptRunner.tsx(),
839
+ runner: typescript.TypeScriptRunner.nodejs(),
795
840
  });
796
- // ProjenrcTs wraps that step in `npx -y -p tsx -c "tsx .projenrc.ts"` because the
797
- // tsx runner declares a `tsx` dependency (so it runs even uninstalled). tsx IS a
798
- // devDep here, so that wrapper is not merely redundant but harmful: `npx -c` exports
799
- // `npm_config_call="tsx .projenrc.ts"` into the environment, which every nested
800
- // `pnpm` inherits and then dies on ("Failed parsing JSON config key call"), failing
801
- // each subproject's post-synth install; the same `npx`/`npm` process also emits the
802
- // "Unknown env config" warnings for pnpm's `catalog`/`@jsr:registry`/etc. Reset to a
803
- // plain exec (tsx resolves from `node_modules/.bin`, which pnpm puts on PATH).
804
- project.defaultTask?.reset("tsx .projenrc.ts");
841
+ // bun runs `.projenrc.ts` directly (native TS, no loader to register). Reset to
842
+ // a plain `bun` exec rather than any wrapper: the default task is spawned by
843
+ // nested installs/synths, and a wrapper that exported `npm_config_*` broke them.
844
+ project.defaultTask?.reset("bun .projenrc.ts");
845
+
846
+ // Pin bun's hoisted linker workspace-wide (see RootBunfigFile) so a peer dep
847
+ // resolves to one copy and singletons/types stay coherent.
848
+ new RootBunfigFile(project);
805
849
 
806
850
  // Only reached on a ROOT (early-returned above otherwise), so the root devDeps
807
851
  // always apply; the self-dep is added only when the engine is an installed pkg.
@@ -864,11 +908,12 @@ function initProject(
864
908
  eslint.addIgnorePattern(`${root}/openapi/**`);
865
909
  eslint.addIgnorePattern(`${root}/**/index.ts`);
866
910
  }
867
- eslint.addIgnorePattern("**/vite.config.ts");
868
- // The unmanaged vite overrides live at the package root, outside any `src/**`
869
- // tsconfig include, so the type-aware parser cannot resolve them to a project.
870
- // They are hand-authored (not generated), but ESLint still cannot parse them.
871
- for (const override of DEFAULT_VITE_OVERRIDES) {
911
+ // The generated bun app scripts + unmanaged overrides live at the package root,
912
+ // outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
913
+ // them to a project. ESLint still cannot parse them.
914
+ eslint.addIgnorePattern("**/dev.ts");
915
+ eslint.addIgnorePattern("**/build.ts");
916
+ for (const override of BUN_APP_OVERRIDES) {
872
917
  eslint.addIgnorePattern(`**/${override}`);
873
918
  }
874
919
  // Codegen packages declare `codegen.inputs` via mixins after construction; ignore