@dbx-tools/projen 0.9.151 → 0.9.153

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
@@ -24,7 +24,13 @@ packages. It can also generate Zod schemas from declarations and coordinate
24
24
  generated-file cleanup and watch tasks.
25
25
 
26
26
  Every `src`-bearing folder under the configured roots becomes a
27
- `DBXToolsTypeScriptProject`. Folder path drives package name and runtime tags.
27
+ `DBXToolsTypeScriptProject`. `packages/js` is selected automatically when it
28
+ exists; otherwise discovery uses `packages`. Folder path drives package name and
29
+ runtime tags.
30
+ Use `packageDefaults` for native options shared by every discovered package,
31
+ such as `{ publishable: false }`; reserve `resolvePackageOptions` for genuine
32
+ per-package differences. Use `rootTsconfig` only for repository-wide compiler
33
+ differences; root tests, Bun/Node types, and React JSX are already included.
28
34
 
29
35
  Use `DBXToolsTypeScriptProject` itself as the root when one package needs the
30
36
  same Bun workspace, repository, task, and release defaults without a separate
@@ -104,7 +110,11 @@ new projectPy.DBXToolsPythonWorkspace(root, {
104
110
  ```
105
111
 
106
112
  Distribution names and modules come from the parent scope plus each package
107
- directory. The repository comes from the parent project metadata or Git remote.
113
+ directory. Direct children of the Python root that contain `src/` are discovered
114
+ automatically; `packages` only needs entries whose metadata differs from the
115
+ generated defaults. Set `discoverPackages: false` for a fully explicit list.
116
+ The repository comes from the parent project metadata, existing generated root
117
+ manifest, or Git remote.
108
118
  `internalDependencies` renders standalone Git `#subdirectory=` requirements
109
119
  without repeating repository coordinates. Pass `repository`, `name`, or
110
120
  `module` only to override those conventions. `root.vscode` is Projen's existing
@@ -310,8 +320,43 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
310
320
  `project.with(...)` only when you need a predicate the filters cannot express.
311
321
 
312
322
  Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
313
- and `ui`. Repo-specific mixins layer package-specific dependencies,
314
- scripts, and generated files on top.
323
+ `ui`, and `app`. Repo-specific mixins layer package-specific dependencies,
324
+ scripts, and generated files on top. `ui` is for reusable React component
325
+ libraries. `app` owns a Bun-built browser application. Terminal
326
+ `packages/**/app` and `packages/**/server` directories select those specific
327
+ roles instead of inheriting a generic `ui` or `node` ancestor.
328
+
329
+ AppKit mode defaults to `auto`: when the workspace has both `app` and `server`
330
+ packages, the engine adds the AppKit, Vite, React plugin, and Tailwind
331
+ dependencies to the package that uses each one. It also disables AppKit's
332
+ package migration, includes the Vite and generated-type inputs in the browser
333
+ tsconfig, generates a read-only monorepo-safe `vite.config.ts`, adds
334
+ `appkit:sync`, and ignores local Databricks state. Put repository-specific Vite
335
+ settings in the optional unmanaged `vite.config.override.ts`; the generated
336
+ config merges it over the engine defaults. Set
337
+ `appkit: false` to disable this ownership. Set `appkit: true` to require AppKit
338
+ and fail synthesis when either package role or its required entry files are
339
+ missing. Keep application-specific UI and server dependencies in the consuming
340
+ `.projenrc.ts`. A workspace with one `server` package also receives standard
341
+ `check`, `dev`, and `start` tasks unless the repository defines them.
342
+
343
+ The generated `deploy` task validates with the native Bun build, then delegates
344
+ to `appkit:deploy` unless the repository already owns `deploy`. The AppKit task
345
+ runs `databricks apps deploy --skip-validation` with all forwarded arguments
346
+ from an isolated source snapshot, so the Databricks CLI never runs npm locally.
347
+ Because the remote builder selects npm when no portable pnpm lockfile is
348
+ uploaded, the snapshot replaces `catalog:` and `workspace:` dependency protocols
349
+ with concrete versions and keeps every manifest stable through upload. The
350
+ generated working tree is never modified. The task passes an explicit profile,
351
+ using the supplied flag or resolving one through `@dbx-tools/auth`, and fails
352
+ before staging when neither profile nor complete environment credentials are
353
+ available. It validates the bundle strictly, removes only stale direct-engine
354
+ state from the temporary snapshot, and defaults to active-run protection while
355
+ skipping duplicate npm validation and tests. If deployment or app startup
356
+ fails, the task prints the current Databricks App logs before returning the
357
+ original failure. A repository-owned deployment flow can run its own Bun checks
358
+ and spawn `appkit:deploy` for the upload step instead of maintaining staging or
359
+ materialization code.
315
360
 
316
361
  Every workspace root exposes a generic development watcher:
317
362
 
package/index.ts CHANGED
@@ -3,7 +3,8 @@
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.9.151";
6
+ export const PACKAGE_VERSION = "0.9.153";
7
+ export * as appkitWorkspace from "./src/appkit-workspace.ts";
7
8
  export * as barrels from "./src/barrels.ts";
8
9
  export * as bunApp from "./src/bun-app.ts";
9
10
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -36,7 +37,10 @@ export * as tsconfig from "./src/tsconfig.ts";
36
37
  export * as vscode from "./src/vscode.ts";
37
38
  export * as watch from "./src/watch.ts";
38
39
  export * as workspaceLock from "./src/workspace-lock.ts";
40
+ export * as workspaceManifest from "./src/workspace-manifest.ts";
39
41
  export * as workspaceVersion from "./src/workspace-version.ts";
42
+ export { AppKitWorkspace } from "./src/appkit-workspace.ts";
43
+ export type { AppKitWorkspaceMode, AppKitWorkspaceOptions } from "./src/appkit-workspace.ts";
40
44
  export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
41
45
  export { BUN_VERSION, MINIMUM_BUN_VERSION, BUN_VERSION_CHECK_SCRIPT } from "./src/bun-workflow.ts";
42
46
  export { DBXToolsConfigDataSchema, DBXToolsConfig } from "./src/dbx-tools-config.ts";
@@ -73,6 +77,7 @@ export type { SharedCoreUsageKind, SharedCoreExportUsage } from "./src/shared-co
73
77
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
74
78
  export type { PackageTag } from "./src/tags.ts";
75
79
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
80
+ export type { DBXToolsRootTsconfigOptions } from "./src/tsconfig.ts";
76
81
  export type { IgnoreGroupOptions, WatchLoopOptions } from "./src/watch.ts";
77
82
  export type { WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
78
83
  export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.ts";
package/package.json CHANGED
@@ -30,10 +30,11 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/cli-args": "^0.9.151",
34
- "@dbx-tools/core": "^0.9.151",
35
- "@dbx-tools/path": "^0.9.151",
36
- "@dbx-tools/shared-core": "^0.9.151",
33
+ "@dbx-tools/auth": "^0.9.153",
34
+ "@dbx-tools/cli-args": "^0.9.153",
35
+ "@dbx-tools/core": "^0.9.153",
36
+ "@dbx-tools/path": "^0.9.153",
37
+ "@dbx-tools/shared-core": "^0.9.153",
37
38
  "commander": "^15.0.0",
38
39
  "concurrently": "^10.0.3",
39
40
  "constructs": "^10.6.0",
@@ -55,11 +56,12 @@
55
56
  },
56
57
  "main": "index.ts",
57
58
  "license": "Apache-2.0",
58
- "version": "0.9.151",
59
+ "version": "0.9.153",
59
60
  "types": "index.ts",
60
61
  "type": "module",
61
62
  "exports": {
62
63
  ".": "./index.ts",
64
+ "./pypi-trusted-publisher-instructions": "./tasks/pypi-trusted-publisher-instructions.ts",
63
65
  "./python-release-packaging": "./tasks/publish-python.ts",
64
66
  "./release-packaging": "./tasks/publish-npm.ts",
65
67
  "./package.json": "./package.json"
@@ -0,0 +1,181 @@
1
+ /**
2
+ * AppKit workspace conventions for paired browser and server packages.
3
+ *
4
+ * @module
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { basename, join, relative, sep } from "node:path";
8
+ import { Component, type Task, TextFile, javascript, typescript } from "projen";
9
+ import { headerLines } from "./generated.ts";
10
+
11
+ /** AppKit workspace discovery and validation mode. */
12
+ export type AppKitWorkspaceMode = "auto" | boolean;
13
+
14
+ /** Projects configured by the AppKit workspace owner. */
15
+ export interface AppKitWorkspaceOptions {
16
+ /** Browser application packages carrying the `app` tag. */
17
+ readonly apps: readonly javascript.NodeProject[];
18
+ /** AppKit backend packages carrying the `server` tag. */
19
+ readonly servers: readonly javascript.NodeProject[];
20
+ /** Fail synthesis when an AppKit package or required entry file is missing. */
21
+ readonly validate?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Own the dependencies and generated configuration shared by AppKit workspaces.
26
+ */
27
+ export class AppKitWorkspace extends Component {
28
+ private readonly deployTask: Task;
29
+ private readonly serverEntrypoint?: string;
30
+
31
+ constructor(project: javascript.NodeProject, options: AppKitWorkspaceOptions) {
32
+ super(project);
33
+
34
+ if (options.validate) validateWorkspace(options);
35
+ project.gitignore.addPatterns(".databricks");
36
+ project.addTask("appkit:sync", {
37
+ description: "Synchronize installed AppKit plugin metadata",
38
+ exec: "appkit plugin sync --write --silent",
39
+ });
40
+ this.deployTask = project.addTask("appkit:deploy", {
41
+ description: "Deploy the AppKit workspace from an npm-compatible source snapshot",
42
+ execArgs: ["bun", "node_modules/@dbx-tools/projen/tasks/appkit-deploy.ts"],
43
+ receiveArgs: true,
44
+ });
45
+
46
+ for (const app of options.apps) configureApp(app);
47
+ for (const server of options.servers) configureServer(server);
48
+ if (options.servers.length === 1) {
49
+ const server = options.servers[0]!;
50
+ const entrypoint = serverEntrypoint(server);
51
+ this.serverEntrypoint = relative(project.outdir, join(server.outdir, entrypoint))
52
+ .split(sep)
53
+ .join("/");
54
+ }
55
+ }
56
+
57
+ public override preSynthesize(): void {
58
+ let check = this.project.tasks.tryFind("check");
59
+ if (!check) {
60
+ check = this.project.addTask("check", {
61
+ description: "Validate the generated workspace with Bun",
62
+ });
63
+ check.spawn(this.project.buildTask);
64
+ }
65
+ if (!this.project.tasks.tryFind("deploy")) {
66
+ const deploy = this.project.addTask("deploy", {
67
+ description: "Validate with Bun and deploy the AppKit workspace",
68
+ });
69
+ deploy.spawn(check);
70
+ deploy.spawn(this.deployTask);
71
+ }
72
+ if (!this.serverEntrypoint) return;
73
+ if (!this.project.tasks.tryFind("dev")) {
74
+ this.project.addTask("dev", {
75
+ description: "Run the AppKit server in development mode",
76
+ env: { NODE_ENV: "development" },
77
+ exec: `bun --watch ${this.serverEntrypoint}`,
78
+ receiveArgs: true,
79
+ });
80
+ }
81
+ if (!this.project.tasks.tryFind("start")) {
82
+ this.project.addTask("start", {
83
+ description: "Run the AppKit server in production mode",
84
+ env: { NODE_ENV: "production" },
85
+ exec: `bun ${this.serverEntrypoint}`,
86
+ receiveArgs: true,
87
+ });
88
+ }
89
+ }
90
+ }
91
+
92
+ /** Validate the explicit AppKit convention before generating dependent config. */
93
+ function validateWorkspace(options: AppKitWorkspaceOptions): void {
94
+ const missing: string[] = [];
95
+ if (options.apps.length === 0) missing.push("an `app` package");
96
+ if (options.servers.length === 0) missing.push("a `server` package");
97
+ for (const app of options.apps) {
98
+ if (!existsSync(join(app.outdir, "index.html"))) missing.push(`${app.name}/index.html`);
99
+ }
100
+ for (const server of options.servers) {
101
+ const entrypoint = serverEntrypoint(server);
102
+ if (!existsSync(join(server.outdir, entrypoint))) {
103
+ missing.push(`${server.name}/${entrypoint}`);
104
+ }
105
+ }
106
+ if (missing.length > 0) {
107
+ throw new Error(`appkit: true requires ${missing.join(", ")}`);
108
+ }
109
+ }
110
+
111
+ /** Configure one AppKit browser application without broadening React libraries. */
112
+ function configureApp(project: javascript.NodeProject): void {
113
+ project.package.addField("appkit", { autoMigrate: false });
114
+ project.addDeps(
115
+ "@databricks/appkit-ui@catalog:",
116
+ "@tailwindcss/vite@catalog:",
117
+ "tailwindcss@catalog:",
118
+ );
119
+ project.addDevDeps("@databricks/appkit@catalog:", "vite@catalog:");
120
+ const typesDirectory = `${basename(project.outdir)}/shared/appkit-types`;
121
+ new TextFile(project, "vite.config.ts", {
122
+ marker: true,
123
+ readonly: true,
124
+ lines: [
125
+ ...headerLines({ tool: "projen", source: ".projenrc.ts" }),
126
+ 'import { existsSync } from "node:fs";',
127
+ 'import { appKitServingTypesPlugin, appKitTypesPlugin } from "@databricks/appkit";',
128
+ 'import tailwindcss from "@tailwindcss/vite";',
129
+ 'import { defineConfig, mergeConfig, type UserConfig } from "vite";',
130
+ "",
131
+ `const appKitTypesDirectory = ${JSON.stringify(typesDirectory)};`,
132
+ 'const overrideUrl = new URL("./vite.config.override.ts", import.meta.url);',
133
+ "const override = existsSync(overrideUrl)",
134
+ " ? ((await import(overrideUrl.href)).default as UserConfig)",
135
+ " : {};",
136
+ "",
137
+ "export default defineConfig(",
138
+ " mergeConfig(",
139
+ " {",
140
+ " plugins: [",
141
+ " tailwindcss(),",
142
+ " appKitTypesPlugin({",
143
+ " outFile: `${appKitTypesDirectory}/analytics.d.ts`,",
144
+ " }),",
145
+ " appKitServingTypesPlugin({",
146
+ " outFile: `${appKitTypesDirectory}/serving.d.ts`,",
147
+ " }),",
148
+ " ],",
149
+ " },",
150
+ " override,",
151
+ " ),",
152
+ ");",
153
+ ],
154
+ });
155
+
156
+ if (!(project instanceof typescript.TypeScriptProject)) return;
157
+ project.tsconfig?.file.addOverride("compilerOptions.rootDir", ".");
158
+ project.tsconfig?.addInclude("vite.config.ts");
159
+ project.tsconfig?.addInclude("vite.config.override.ts");
160
+ project.tsconfig?.addInclude("shared/appkit-types");
161
+ }
162
+
163
+ /** Configure one AppKit server with the runtime dependency its Vite loader imports. */
164
+ function configureServer(project: javascript.NodeProject): void {
165
+ const entrypoint = serverEntrypoint(project);
166
+ if (entrypoint !== project.package.manifest.main) {
167
+ project.package.addField("main", entrypoint);
168
+ project.package.addField("types", entrypoint);
169
+ }
170
+ project.addDeps("@databricks/appkit@catalog:", "@vitejs/plugin-react@catalog:");
171
+ }
172
+
173
+ /** Prefer the canonical AppKit server entry when a package still has its default barrel. */
174
+ function serverEntrypoint(project: javascript.NodeProject): string {
175
+ const configured =
176
+ typeof project.package.manifest.main === "string" ? project.package.manifest.main : "index.ts";
177
+ const canonical = "src/server.ts";
178
+ return configured === "index.ts" && existsSync(join(project.outdir, canonical))
179
+ ? canonical
180
+ : configured;
181
+ }
package/src/bun-app.ts CHANGED
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import { stringUtils } from "@dbx-tools/shared-core";
26
26
  import { type Project, TextFile } from "projen";
27
+ import { header, headerLines } from "./generated.ts";
27
28
 
28
29
  /** Unmanaged dev-server override module, merged over the generated options if present. */
29
30
  export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
@@ -31,6 +32,8 @@ export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
31
32
  export const BUN_BUILD_OVERRIDE = "bun-build.override.ts";
32
33
  /** Unmanaged generated-script overrides excluded from formatting and linting. */
33
34
  export const BUN_APP_OVERRIDES = [BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
35
+ const PROJEN_HEADER = { tool: "projen", source: ".projenrc.ts" } as const;
36
+ const PROJEN_TS_HEADER = header(PROJEN_HEADER).trimEnd().replaceAll("\n", "\n ");
34
37
 
35
38
  /**
36
39
  * Root `bunfig.toml`: pin bun's HOISTED linker workspace-wide.
@@ -49,7 +52,7 @@ export class RootBunfigFile extends TextFile {
49
52
  marker: false,
50
53
  readonly: true,
51
54
  lines: [
52
- '# ~~ Generated by projen. To modify, edit .projenrc.ts and run "bunx projen".',
55
+ ...headerLines({ ...PROJEN_HEADER, comment: "#" }),
53
56
  "# Hoisted (flat) node_modules so a peer dep like @mastra/core resolves to ONE",
54
57
  "# copy - the isolated default splits it into peer-hash variants and breaks",
55
58
  "# identity-based singletons + cross-package types.",
@@ -76,6 +79,7 @@ export class BunfigFile extends TextFile {
76
79
  marker: true,
77
80
  readonly: true,
78
81
  lines: [
82
+ ...headerLines({ ...PROJEN_HEADER, comment: "#" }),
79
83
  "# Tailwind v4 for the dev server (Bun.serve reads [serve.static] plugins).",
80
84
  "# Production build applies the plugin via Bun.build's API in build.ts - the",
81
85
  "# `bun build` CLI does not load bunfig plugins, so there is no [build] here.",
@@ -94,6 +98,7 @@ export class BunDevServerFile extends TextFile {
94
98
  const source = (
95
99
  // ============================================================================
96
100
  /*ts*/`
101
+ ${PROJEN_TS_HEADER}
97
102
  import { existsSync } from "node:fs";
98
103
  import index from "./index.html";
99
104
 
@@ -135,6 +140,7 @@ export class BunBuildFile extends TextFile {
135
140
  String.raw
136
141
  // ============================================================================
137
142
  /*ts*/`
143
+ ${PROJEN_TS_HEADER}
138
144
  import { existsSync } from "node:fs";
139
145
  import { cp, rm } from "node:fs/promises";
140
146
  import { basename } from "node:path";
@@ -73,6 +73,7 @@ export class DBXToolsConfig {
73
73
  this.tags = [...(options.tags ?? [])];
74
74
  this.codegenInputs = [...(options.codegenInputs ?? [])];
75
75
  this.publishable = options.publishable ?? true;
76
+ project.package.addField("private", () => (this.publishable ? undefined : true));
76
77
  project.package.addField(CONFIG_KEY, () => {
77
78
  const data = this.data();
78
79
  return Object.keys(data).length === 0 ? undefined : data;
package/src/generated.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  */
11
11
  import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
12
12
 
13
- /** First line of every banner {@link header} builds. */
14
- const MARKER = "// GENERATED by";
13
+ /** Tool-owned marker text shared by every supported comment style. */
14
+ const MARKER = "GENERATED by";
15
15
 
16
16
  /** Python subpackage, under a package's module directory, that holds all toolchain output. */
17
17
  export const PYTHON_GENERATED_PACKAGE = "_generated";
@@ -34,6 +34,8 @@ export function makeReadonly(file: string): void {
34
34
 
35
35
  /** Inputs for the durable generated-file banner. */
36
36
  export interface HeaderOpts {
37
+ /** Line-comment prefix. Defaults to `//`. */
38
+ readonly comment?: "//" | "#";
37
39
  /** What produced the file, e.g. `"projen watch"`. */
38
40
  readonly tool: string;
39
41
  /** What the file is derived from, e.g. `"the exporting modules beside it"`. */
@@ -42,12 +44,18 @@ export interface HeaderOpts {
42
44
 
43
45
  /** Build the do-not-edit banner (line comments, for TS/JS). */
44
46
  export function header(opts: HeaderOpts): string {
45
- const lines = [`${MARKER} ${opts.tool} - DO NOT EDIT.`];
46
- if (opts.source) lines.push(`// Regenerated from ${opts.source}.`);
47
- lines.push("// Hand edits are overwritten on the next watch; this file is read-only.");
47
+ const comment = opts.comment ?? "//";
48
+ const lines = [`${comment} ${MARKER} ${opts.tool} - DO NOT EDIT.`];
49
+ if (opts.source) lines.push(`${comment} Regenerated from ${opts.source}.`);
50
+ lines.push(`${comment} Hand edits are overwritten on the next watch; this file is read-only.`);
48
51
  return `${lines.join("\n")}\n`;
49
52
  }
50
53
 
54
+ /** Build a generated banner as lines for Projen file components. */
55
+ export function headerLines(opts: HeaderOpts): string[] {
56
+ return header(opts).trimEnd().split("\n");
57
+ }
58
+
51
59
  /**
52
60
  * True if the file exists and carries the do-not-edit banner {@link header}
53
61
  * writes - i.e. a toolchain wrote it and a hand edit would be overwritten.
@@ -61,7 +69,8 @@ export function isGenerated(file: string): boolean {
61
69
  try {
62
70
  const source = readFileSync(file, "utf8");
63
71
  return (
64
- source.startsWith(MARKER) ||
72
+ source.startsWith(`// ${MARKER}`) ||
73
+ source.startsWith(`# ${MARKER}`) ||
65
74
  source.startsWith("// ~~ Generated by projen") ||
66
75
  source.startsWith("# ~~ Generated by projen") ||
67
76
  /"\/\/"\s*:\s*"~~ Generated by projen/.test(source)
@@ -78,7 +87,7 @@ export function isGenerated(file: string): boolean {
78
87
  export function stampGenerated(file: string, opts: HeaderOpts): void {
79
88
  makeWritable(file);
80
89
  const body = readFileSync(file, "utf8");
81
- const next = body.startsWith(MARKER) ? body : `${header(opts)}\n${body}`;
90
+ const next = isGenerated(file) ? body : `${header(opts)}\n${body}`;
82
91
  writeFileSync(file, next);
83
92
  makeReadonly(file);
84
93
  }
package/src/packages.ts CHANGED
@@ -146,11 +146,16 @@ export class DiscoveredPackage {
146
146
 
147
147
  /**
148
148
  * Tag candidates from nesting under the discovery root: cumulative kebab-case join
149
- * of every ancestor folder, excluding the leaf package folder when depth >= 2
149
+ * of every ancestor folder, excluding the leaf package folder when depth >= 2.
150
+ * Terminal `app` and `server` directories are runtime roles and replace their
151
+ * generic ancestor (`ui/app` -> `app`, `node/server` -> `server`).
152
+ * Other paths keep cumulative tags
150
153
  * (`shared/path/coolDude/another` -> `[shared, shared-path, shared-path-cool-dude]`).
151
154
  * Matched against `packageTagPaths` to resolve applied mixin tags.
152
155
  */
153
156
  get tagCandidates(): string[] {
157
+ const leaf = stringUtils.toSlug(this.name);
158
+ if (leaf === "app" || leaf === "server") return [leaf];
154
159
  return nestingTagsFromSegments(this.relSegments);
155
160
  }
156
161
  }
@@ -62,6 +62,10 @@ const DEFAULT_CATALOG: Catalog = {
62
62
  "react-dom": "~19.2.4",
63
63
  "@types/react": "^19.2.2",
64
64
  "@types/react-dom": "^19.2.2",
65
+ tailwindcss: "^4.3.2",
66
+ "@tailwindcss/vite": "^4.3.2",
67
+ vite: "npm:rolldown-vite@7.1.14",
68
+ "@vitejs/plugin-react": "^5.1.1",
65
69
  // Tailwind v4 compiler for bun's dev server + `Bun.build` (the `app` tag).
66
70
  "bun-plugin-tailwind": "^0.1.2",
67
71
  "@types/node": "^24.6.0",
package/src/project-js.ts CHANGED
@@ -15,6 +15,7 @@ import { type IConstruct } from "constructs";
15
15
  import { Component, LogLevel, Project, type Task, TextFile, javascript, typescript } from "projen";
16
16
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
17
17
  import type { ReleaseProjectOptions } from "projen/lib/release";
18
+ import { AppKitWorkspace, type AppKitWorkspaceMode } from "./appkit-workspace.ts";
18
19
  import { generateBarrels, isGeneratedBarrel } from "./barrels.ts";
19
20
 
20
21
  import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
@@ -39,7 +40,7 @@ import { applyCompiledPublish } from "./publish.ts";
39
40
  import { generatePythonNodeBindings } from "./python-node-bindings.ts";
40
41
  import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
41
42
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
42
- import { DBXToolsRootTsconfig } from "./tsconfig.ts";
43
+ import { DBXToolsRootTsconfig, type DBXToolsRootTsconfigOptions } from "./tsconfig.ts";
43
44
  import { configureVsCode } from "./vscode.ts";
44
45
  import { readWorkspaceVersion, syncWorkspaceManifestVersion } from "./workspace-version.ts";
45
46
 
@@ -139,7 +140,15 @@ function repositoryOptions(
139
140
  options: DBXToolsJavaScriptProjectOptions,
140
141
  ): Pick<javascript.NodeProjectOptions, "repository" | "repositoryDirectory"> {
141
142
  const workspaceRoot = resolve(options.parent?.root.outdir ?? options.outdir ?? process.cwd());
142
- const repository = options.repository ?? projectUtils.repositoryUrl(workspaceRoot, "npm");
143
+ const existingRepository = readPackageManifest(workspaceRoot)?.repository;
144
+ const existingRepositoryUrl =
145
+ typeof existingRepository === "string"
146
+ ? existingRepository
147
+ : object.isRecord(existingRepository)
148
+ ? stringUtils.trimToNull(existingRepository.url)
149
+ : undefined;
150
+ const repository =
151
+ options.repository ?? projectUtils.repositoryUrl(workspaceRoot, "npm") ?? existingRepositoryUrl;
143
152
  const projectRoot = options.parent
144
153
  ? resolve(options.parent.outdir, options.outdir ?? ".")
145
154
  : resolve(options.outdir ?? process.cwd());
@@ -603,6 +612,8 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
603
612
  * disable. Defaults to `"node"`.
604
613
  */
605
614
  readonly omitRelativePrefix?: OneOrMany<string>;
615
+ /** Native defaults merged into every filesystem-discovered package. */
616
+ readonly packageDefaults?: Omit<DiscoveredTypeScriptPackageOptions, "name">;
606
617
  /** Resolve native constructor fields before a discovered package is constructed. */
607
618
  readonly resolvePackageOptions?: (
608
619
  pkg: DiscoveredPackage,
@@ -620,6 +631,13 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
620
631
  * a list = only those tags.
621
632
  */
622
633
  readonly defaultTagMixins?: false | PackageTag[];
634
+ /** Overrides for the generated repository-root TypeScript program. */
635
+ readonly rootTsconfig?: DBXToolsRootTsconfigOptions;
636
+ /**
637
+ * Apply AppKit conventions to `app` and `server` packages. Defaults to
638
+ * `auto`; `true` also validates the required package roles and entry files.
639
+ */
640
+ readonly appkit?: AppKitWorkspaceMode;
623
641
  /**
624
642
  * Extra repo-root paths that trigger a full re-synth during `sync --watch`
625
643
  * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
@@ -679,7 +697,7 @@ export class DBXToolsNodeProject
679
697
  readonly language = "javascript" as const;
680
698
  readonly scope: string;
681
699
  readonly dbxToolsConfig: DBXToolsConfig;
682
- pnpmWorkspace?: PnpmWorkspaceState;
700
+ pnpmWorkspace: PnpmWorkspaceState;
683
701
  rootTsconfig?: DBXToolsRootTsconfig;
684
702
  readonly extraWorkspaceMembers: readonly string[];
685
703
  readonly releaseBranch: string;
@@ -1007,7 +1025,10 @@ class WorkspaceValidationTasks extends Component {
1007
1025
  return;
1008
1026
  }
1009
1027
  project.compileTask.execArgs(taskCommand("compile-workspace.ts"));
1010
- project.testTask.spawn(project.tasks.tryFind("test:all")!);
1028
+ const testAll = project.tasks.tryFind("test:all")!;
1029
+ const pythonTests = project.tasks.tryFind("py:test");
1030
+ if (pythonTests) testAll.spawn(pythonTests);
1031
+ project.testTask.spawn(testAll);
1011
1032
  }
1012
1033
  }
1013
1034
 
@@ -1054,6 +1075,7 @@ class PrettierIgnoreGenerated extends Component {
1054
1075
  if (!(sub instanceof DBXToolsTypeScriptProject)) continue;
1055
1076
  const rel = toPosix(relative(rootAbs, sub.outdir));
1056
1077
  prettier.addIgnorePattern(`${rel}/index.ts`);
1078
+ prettier.addIgnorePattern(`${rel}/src/**/index.ts`);
1057
1079
  for (const module of codegenModulePaths(sub.dbxToolsConfig.codegenInputs)) {
1058
1080
  prettier.addIgnorePattern(`${rel}/${module}`);
1059
1081
  }
@@ -1225,7 +1247,7 @@ function initProject(
1225
1247
  }
1226
1248
 
1227
1249
  if (project instanceof DBXToolsNodeProject) {
1228
- project.rootTsconfig = new DBXToolsRootTsconfig(project);
1250
+ project.rootTsconfig = new DBXToolsRootTsconfig(project, options.rootTsconfig);
1229
1251
  }
1230
1252
  const projenrc = new typescript.ProjenrcTs(project, {
1231
1253
  runner: typescript.TypeScriptRunner.nodejs(),
@@ -1253,7 +1275,9 @@ function initProject(
1253
1275
  // lockfiles, so the CI-named install task must resolve from manifests as well.
1254
1276
  project.package.installCiTask.reset("bun install");
1255
1277
  configureRootBunRuntime(project);
1256
- const roots = options.packageRoots ?? DEFAULT_PACKAGE_ROOTS;
1278
+ const roots =
1279
+ options.packageRoots ??
1280
+ (existsSync(resolve(project.outdir, "packages/js")) ? ["packages/js"] : DEFAULT_PACKAGE_ROOTS);
1257
1281
  project.dbxToolsConfig.packageRoots = [...roots];
1258
1282
  if (options.syncResynthPaths?.length) {
1259
1283
  project.dbxToolsConfig.syncResynthPaths = [...options.syncResynthPaths];
@@ -1278,10 +1302,20 @@ function initProject(
1278
1302
  project.gitignore.addPatterns(
1279
1303
  ...[...ignore.ignorePatterns({ test: false, dot: false, lock: true })],
1280
1304
  );
1305
+ // The generic compiled-output pattern `**/lib/**` must not hide authored
1306
+ // modules beneath a package's `src/lib/`.
1307
+ project.gitignore.include("**/src/lib/**");
1281
1308
  // What the dot group was actually earning here, named explicitly: secrets and
1282
1309
  // local editor state. Both ignore CONTENTS (`.idea/*`) rather than the
1283
1310
  // directory, so a later `!` negation can still reach a file inside.
1284
- project.gitignore.addPatterns(".env", ".env.*", "!.env.example", "!.env.sample", ".idea/*");
1311
+ project.gitignore.addPatterns(
1312
+ ".env",
1313
+ ".env.*",
1314
+ "!.env.example",
1315
+ "!.env.sample",
1316
+ ".idea/*",
1317
+ ".npmrc",
1318
+ );
1285
1319
  for (const root of roots) {
1286
1320
  project.annotateGenerated(`/${root}/**/index.ts`);
1287
1321
  project.annotateGenerated(`/${root}/**/src/generated/**`);
@@ -1396,9 +1430,11 @@ function initProject(
1396
1430
  continue;
1397
1431
  }
1398
1432
  const defaultPackageName = packageNameFor(project.scope, p.relPath, omitPrefixes);
1399
- const packageOptions =
1400
- options.resolvePackageOptions?.(p, { name: defaultPackageName }) ??
1401
- ({ name: defaultPackageName } satisfies DiscoveredTypeScriptPackageOptions);
1433
+ const defaults = {
1434
+ name: defaultPackageName,
1435
+ ...options.packageDefaults,
1436
+ } satisfies DiscoveredTypeScriptPackageOptions;
1437
+ const packageOptions = options.resolvePackageOptions?.(p, defaults) ?? defaults;
1402
1438
  new DBXToolsTypeScriptProject({
1403
1439
  parent: project,
1404
1440
  outdir: p.memberPath,
@@ -1417,6 +1453,25 @@ function initProject(
1417
1453
  if (enabledTagMixins.length) {
1418
1454
  project.with(...enabledTagMixins.map((t) => PACKAGE_TAG_MIXINS[t]));
1419
1455
  }
1456
+ const appkitMode = options.appkit ?? "auto";
1457
+ if (appkitMode !== false) {
1458
+ const projects = [project, ...project.subprojects].filter(
1459
+ (candidate): candidate is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
1460
+ candidate instanceof DBXToolsNodeProject || candidate instanceof DBXToolsTypeScriptProject,
1461
+ );
1462
+ const apps = projects.filter((candidate) => candidate.dbxToolsConfig.tags.includes("app"));
1463
+ const servers = projects.filter((candidate) =>
1464
+ candidate.dbxToolsConfig.tags.includes("server"),
1465
+ );
1466
+ const enabled = appkitMode === true || (apps.length > 0 && servers.length > 0);
1467
+ if (enabled) {
1468
+ new AppKitWorkspace(project, {
1469
+ apps,
1470
+ servers,
1471
+ validate: appkitMode === true,
1472
+ });
1473
+ }
1474
+ }
1420
1475
 
1421
1476
  new WorkspaceValidationTasks(project);
1422
1477
  new PrettierIgnoreGenerated(project);