@dbx-tools/projen 0.9.152 → 0.9.154

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,10 +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.
28
30
  Use `packageDefaults` for native options shared by every discovered package,
29
31
  such as `{ publishable: false }`; reserve `resolvePackageOptions` for genuine
30
- per-package differences.
32
+ per-package differences. Use `rootTsconfig` only for repository-wide compiler
33
+ differences; root tests, Bun/Node types, and React JSX are already included.
31
34
 
32
35
  Use `DBXToolsTypeScriptProject` itself as the root when one package needs the
33
36
  same Bun workspace, repository, task, and release defaults without a separate
@@ -107,7 +110,11 @@ new projectPy.DBXToolsPythonWorkspace(root, {
107
110
  ```
108
111
 
109
112
  Distribution names and modules come from the parent scope plus each package
110
- 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.
111
118
  `internalDependencies` renders standalone Git `#subdirectory=` requirements
112
119
  without repeating repository coordinates. Pass `repository`, `name`, or
113
120
  `module` only to override those conventions. `root.vscode` is Projen's existing
@@ -315,13 +322,18 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
315
322
  Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
316
323
  `ui`, and `app`. Repo-specific mixins layer package-specific dependencies,
317
324
  scripts, and generated files on top. `ui` is for reusable React component
318
- libraries. `app` owns a Bun-built browser application.
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.
319
328
 
320
329
  AppKit mode defaults to `auto`: when the workspace has both `app` and `server`
321
330
  packages, the engine adds the AppKit, Vite, React plugin, and Tailwind
322
331
  dependencies to the package that uses each one. It also disables AppKit's
323
332
  package migration, includes the Vite and generated-type inputs in the browser
324
- tsconfig, adds `appkit:sync`, and ignores local Databricks state. Set
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
325
337
  `appkit: false` to disable this ownership. Set `appkit: true` to require AppKit
326
338
  and fail synthesis when either package role or its required entry files are
327
339
  missing. Keep application-specific UI and server dependencies in the consuming
@@ -338,8 +350,12 @@ with concrete versions and keeps every manifest stable through upload. The
338
350
  generated working tree is never modified. The task passes an explicit profile,
339
351
  using the supplied flag or resolving one through `@dbx-tools/auth`, and fails
340
352
  before staging when neither profile nor complete environment credentials are
341
- available. A repository-owned deployment flow can run its own Bun checks and
342
- spawn `appkit:deploy` for the upload step instead of maintaining staging or
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
343
359
  materialization code.
344
360
 
345
361
  Every workspace root exposes a generic development watcher:
@@ -585,11 +601,14 @@ site. Install validation prerequisites through `releaseSetupSteps`.
585
601
  Publication jobs download the resulting artifacts rather than rebuilding the
586
602
  workspace. Each Python package retains its own publishing environment and waits
587
603
  for its package dependencies to publish. The npm job uses the bundled archive
588
- publisher without a checkout or workspace install. Pages deploys the already
589
- built site after registry publication succeeds. Local npm and Python registry publication remains available
590
- through the direct local publication tooling. Those local publishers run
591
- concurrently; a failure or release termination stops every live publisher before
592
- the command returns.
604
+ publisher without a checkout or workspace install. After all selected public
605
+ publishers finish, one verifier downloads the complete release artifact set and
606
+ polls only unresolved packages concurrently. It validates npm tarball contents
607
+ and PyPI distribution hashes before Pages deploys the already built site. Local
608
+ npm and Python registry publication remains available through the direct local
609
+ publication tooling and does not wait for registry propagation. Those local
610
+ publishers run concurrently; a failure or release termination stops every live
611
+ publisher before the command returns.
593
612
 
594
613
  The default release publishes npm, Python, and docs, then publishes to
595
614
  configured local registries. Use `bun run release --help` or the generated
package/index.ts CHANGED
@@ -3,7 +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.9.152";
6
+ export const PACKAGE_VERSION = "0.9.154";
7
7
  export * as appkitWorkspace from "./src/appkit-workspace.ts";
8
8
  export * as barrels from "./src/barrels.ts";
9
9
  export * as bunApp from "./src/bun-app.ts";
@@ -69,7 +69,7 @@ export { PYTHON_NODE_RUNTIME_DISTRIBUTION, PYTHON_NODE_RUNTIME_MODULE, PYTHON_NO
69
69
  export type { PythonNodeRuntimeOptions } from "./src/python-node-runtime.ts";
70
70
  export { DBXToolsRelease } from "./src/release.ts";
71
71
  export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
72
- export { RELEASE_VERSION, RELEASE_WORKFLOW_FILE } from "./src/release-context.ts";
72
+ export { RELEASE_VERSION, RELEASE_WORKFLOW_FILE, PUBLIC_NPM_REGISTRY_URL, PUBLIC_PYPI_REGISTRY_URL } from "./src/release-context.ts";
73
73
  export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES, ReleaseStepSelectionSchema } from "./src/release-options.ts";
74
74
  export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
75
75
  export { SHARED_CORE_BINDING_EXPORTS } from "./src/shared-core-usage.ts";
@@ -77,6 +77,7 @@ export type { SharedCoreUsageKind, SharedCoreExportUsage } from "./src/shared-co
77
77
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
78
78
  export type { PackageTag } from "./src/tags.ts";
79
79
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
80
+ export type { DBXToolsRootTsconfigOptions } from "./src/tsconfig.ts";
80
81
  export type { IgnoreGroupOptions, WatchLoopOptions } from "./src/watch.ts";
81
82
  export type { WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
82
83
  export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.ts";
package/package.json CHANGED
@@ -30,11 +30,11 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/auth": "^0.9.152",
34
- "@dbx-tools/cli-args": "^0.9.152",
35
- "@dbx-tools/core": "^0.9.152",
36
- "@dbx-tools/path": "^0.9.152",
37
- "@dbx-tools/shared-core": "^0.9.152",
33
+ "@dbx-tools/auth": "^0.9.154",
34
+ "@dbx-tools/cli-args": "^0.9.154",
35
+ "@dbx-tools/core": "^0.9.154",
36
+ "@dbx-tools/path": "^0.9.154",
37
+ "@dbx-tools/shared-core": "^0.9.154",
38
38
  "commander": "^15.0.0",
39
39
  "concurrently": "^10.0.3",
40
40
  "constructs": "^10.6.0",
@@ -56,11 +56,12 @@
56
56
  },
57
57
  "main": "index.ts",
58
58
  "license": "Apache-2.0",
59
- "version": "0.9.152",
59
+ "version": "0.9.154",
60
60
  "types": "index.ts",
61
61
  "type": "module",
62
62
  "exports": {
63
63
  ".": "./index.ts",
64
+ "./pypi-trusted-publisher-instructions": "./tasks/pypi-trusted-publisher-instructions.ts",
64
65
  "./python-release-packaging": "./tasks/publish-python.ts",
65
66
  "./release-packaging": "./tasks/publish-npm.ts",
66
67
  "./package.json": "./package.json"
@@ -4,8 +4,9 @@
4
4
  * @module
5
5
  */
6
6
  import { existsSync } from "node:fs";
7
- import { join, relative, sep } from "node:path";
8
- import { Component, type Task, javascript, typescript } from "projen";
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";
9
10
 
10
11
  /** AppKit workspace discovery and validation mode. */
11
12
  export type AppKitWorkspaceMode = "auto" | boolean;
@@ -94,9 +95,7 @@ function validateWorkspace(options: AppKitWorkspaceOptions): void {
94
95
  if (options.apps.length === 0) missing.push("an `app` package");
95
96
  if (options.servers.length === 0) missing.push("a `server` package");
96
97
  for (const app of options.apps) {
97
- for (const file of ["index.html", "vite.config.ts"]) {
98
- if (!existsSync(join(app.outdir, file))) missing.push(`${app.name}/${file}`);
99
- }
98
+ if (!existsSync(join(app.outdir, "index.html"))) missing.push(`${app.name}/index.html`);
100
99
  }
101
100
  for (const server of options.servers) {
102
101
  const entrypoint = serverEntrypoint(server);
@@ -117,11 +116,47 @@ function configureApp(project: javascript.NodeProject): void {
117
116
  "@tailwindcss/vite@catalog:",
118
117
  "tailwindcss@catalog:",
119
118
  );
120
- project.addDevDeps("vite@catalog:");
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
+ });
121
155
 
122
156
  if (!(project instanceof typescript.TypeScriptProject)) return;
123
157
  project.tsconfig?.file.addOverride("compilerOptions.rootDir", ".");
124
158
  project.tsconfig?.addInclude("vite.config.ts");
159
+ project.tsconfig?.addInclude("vite.config.override.ts");
125
160
  project.tsconfig?.addInclude("shared/appkit-types");
126
161
  }
127
162
 
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";
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
  }
@@ -64,7 +64,7 @@ const DEFAULT_CATALOG: Catalog = {
64
64
  "@types/react-dom": "^19.2.2",
65
65
  tailwindcss: "^4.3.2",
66
66
  "@tailwindcss/vite": "^4.3.2",
67
- vite: "^7.2.4",
67
+ vite: "npm:rolldown-vite@7.1.14",
68
68
  "@vitejs/plugin-react": "^5.1.1",
69
69
  // Tailwind v4 compiler for bun's dev server + `Bun.build` (the `app` tag).
70
70
  "bun-plugin-tailwind": "^0.1.2",
package/src/project-js.ts CHANGED
@@ -40,7 +40,7 @@ import { applyCompiledPublish } from "./publish.ts";
40
40
  import { generatePythonNodeBindings } from "./python-node-bindings.ts";
41
41
  import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
42
42
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
43
- import { DBXToolsRootTsconfig } from "./tsconfig.ts";
43
+ import { DBXToolsRootTsconfig, type DBXToolsRootTsconfigOptions } from "./tsconfig.ts";
44
44
  import { configureVsCode } from "./vscode.ts";
45
45
  import { readWorkspaceVersion, syncWorkspaceManifestVersion } from "./workspace-version.ts";
46
46
 
@@ -140,7 +140,15 @@ function repositoryOptions(
140
140
  options: DBXToolsJavaScriptProjectOptions,
141
141
  ): Pick<javascript.NodeProjectOptions, "repository" | "repositoryDirectory"> {
142
142
  const workspaceRoot = resolve(options.parent?.root.outdir ?? options.outdir ?? process.cwd());
143
- 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;
144
152
  const projectRoot = options.parent
145
153
  ? resolve(options.parent.outdir, options.outdir ?? ".")
146
154
  : resolve(options.outdir ?? process.cwd());
@@ -623,6 +631,8 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
623
631
  * a list = only those tags.
624
632
  */
625
633
  readonly defaultTagMixins?: false | PackageTag[];
634
+ /** Overrides for the generated repository-root TypeScript program. */
635
+ readonly rootTsconfig?: DBXToolsRootTsconfigOptions;
626
636
  /**
627
637
  * Apply AppKit conventions to `app` and `server` packages. Defaults to
628
638
  * `auto`; `true` also validates the required package roles and entry files.
@@ -1065,6 +1075,7 @@ class PrettierIgnoreGenerated extends Component {
1065
1075
  if (!(sub instanceof DBXToolsTypeScriptProject)) continue;
1066
1076
  const rel = toPosix(relative(rootAbs, sub.outdir));
1067
1077
  prettier.addIgnorePattern(`${rel}/index.ts`);
1078
+ prettier.addIgnorePattern(`${rel}/src/**/index.ts`);
1068
1079
  for (const module of codegenModulePaths(sub.dbxToolsConfig.codegenInputs)) {
1069
1080
  prettier.addIgnorePattern(`${rel}/${module}`);
1070
1081
  }
@@ -1236,7 +1247,7 @@ function initProject(
1236
1247
  }
1237
1248
 
1238
1249
  if (project instanceof DBXToolsNodeProject) {
1239
- project.rootTsconfig = new DBXToolsRootTsconfig(project);
1250
+ project.rootTsconfig = new DBXToolsRootTsconfig(project, options.rootTsconfig);
1240
1251
  }
1241
1252
  const projenrc = new typescript.ProjenrcTs(project, {
1242
1253
  runner: typescript.TypeScriptRunner.nodejs(),
@@ -1264,7 +1275,9 @@ function initProject(
1264
1275
  // lockfiles, so the CI-named install task must resolve from manifests as well.
1265
1276
  project.package.installCiTask.reset("bun install");
1266
1277
  configureRootBunRuntime(project);
1267
- 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);
1268
1281
  project.dbxToolsConfig.packageRoots = [...roots];
1269
1282
  if (options.syncResynthPaths?.length) {
1270
1283
  project.dbxToolsConfig.syncResynthPaths = [...options.syncResynthPaths];
@@ -1289,6 +1302,9 @@ function initProject(
1289
1302
  project.gitignore.addPatterns(
1290
1303
  ...[...ignore.ignorePatterns({ test: false, dot: false, lock: true })],
1291
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/**");
1292
1308
  // What the dot group was actually earning here, named explicitly: secrets and
1293
1309
  // local editor state. Both ignore CONTENTS (`.idea/*`) rather than the
1294
1310
  // directory, so a later `!` negation can still reach a file inside.
package/src/project-py.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /** Reusable uv workspace generation for Python packages hosted in a projen tree. */
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
2
4
  import * as projectUtils from "@dbx-tools/core/project-utils";
3
5
  import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
6
  import { Component, License, TextFile, type Project, github, javascript, python } from "projen";
5
7
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
6
- import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
8
+ import { header, PYTHON_GENERATED_PACKAGE } from "./generated.ts";
7
9
  import { LICENSE, projectRepositoryUrl, taskCommand } from "./project-js.ts";
8
10
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
9
11
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
@@ -17,6 +19,7 @@ import {
17
19
  PythonNodeRuntime,
18
20
  } from "./python-node-runtime.ts";
19
21
  import {
22
+ refreshPublicationVerification,
20
23
  refreshReleaseDocsDependencies,
21
24
  releaseCondition,
22
25
  releaseBuildSteps,
@@ -107,7 +110,10 @@ interface PythonPublication {
107
110
 
108
111
  /** Options for {@link DBXToolsPythonWorkspace}. */
109
112
  export interface DBXToolsPythonWorkspaceOptions {
110
- readonly packages: readonly PythonPackageOptions[];
113
+ /** Package-specific options merged over packages discovered under the Python root. */
114
+ readonly packages?: readonly PythonPackageOptions[];
115
+ /** Discover direct child directories containing `src/`. Defaults to true. */
116
+ readonly discoverPackages?: boolean;
111
117
  readonly repository?: PythonRepositoryOptions;
112
118
  /** Repository-relative Python package root. Defaults to `packages/py`. */
113
119
  readonly root?: string;
@@ -214,6 +220,15 @@ export function pythonModuleName(scope: string, directory: string): string {
214
220
  return [scope, ...directory.split("/")].map((part) => part.replaceAll("-", "_")).join(".");
215
221
  }
216
222
 
223
+ /** Discover direct Python workspace members by their conventional source directory. */
224
+ function discoverPythonPackages(root: string): string[] {
225
+ if (!existsSync(root)) return [];
226
+ return readdirSync(root, { withFileTypes: true })
227
+ .filter((entry) => entry.isDirectory() && existsSync(resolve(root, entry.name, "src")))
228
+ .map((entry) => entry.name)
229
+ .sort();
230
+ }
231
+
217
232
  /** A Python package implemented with projen's `PythonProject` and uv backend. */
218
233
  export class DBXToolsPythonProject extends python.PythonProject implements DBXToolsProject {
219
234
  readonly language = "python" as const;
@@ -349,11 +364,30 @@ export class DBXToolsPythonWorkspace extends Component {
349
364
  ref: options.repository?.ref ?? "main",
350
365
  root: options.root ?? options.repository?.root ?? "packages/py",
351
366
  };
352
- const packageIdentities: ResolvedPythonPackageOptions[] = options.packages.map((pkg) => ({
353
- ...pkg,
354
- name: pkg.name ?? `${scope}-${stringUtils.toSlug(pkg.directory)}`,
355
- module: pkg.module ?? pythonModuleName(scope, pkg.directory),
356
- }));
367
+ const configuredPackages = new Map((options.packages ?? []).map((pkg) => [pkg.directory, pkg]));
368
+ const discoveredDirectories =
369
+ options.discoverPackages === false
370
+ ? []
371
+ : discoverPythonPackages(resolve(project.outdir, this.repository.root));
372
+ const directories = [
373
+ ...configuredPackages.keys(),
374
+ ...discoveredDirectories.filter((directory) => !configuredPackages.has(directory)),
375
+ ];
376
+ const packageIdentities: ResolvedPythonPackageOptions[] = directories.map((directory) => {
377
+ const pkg = configuredPackages.get(directory);
378
+ return {
379
+ directory,
380
+ description: `${scope} ${directory} Python package.`,
381
+ ...pkg,
382
+ name: pkg?.name ?? `${scope}-${stringUtils.toSlug(directory)}`,
383
+ module: pkg?.module ?? pythonModuleName(scope, directory),
384
+ };
385
+ });
386
+ if (packageIdentities.length === 0) {
387
+ throw new Error(
388
+ `No Python packages were configured or discovered under ${this.repository.root}`,
389
+ );
390
+ }
357
391
  const configuredNodeRuntimes = packageIdentities.filter(({ nodeRuntime }) => nodeRuntime);
358
392
  if (configuredNodeRuntimes.length > 1) {
359
393
  throw new Error("A Python workspace can configure only one shared Node runtime package");
@@ -672,6 +706,7 @@ export class DBXToolsPythonWorkspace extends Component {
672
706
  ],
673
707
  });
674
708
  }
709
+ refreshPublicationVerification(project);
675
710
  refreshReleaseDocsDependencies(project);
676
711
  }
677
712
 
@@ -760,26 +795,24 @@ export class DBXToolsPythonWorkspace extends Component {
760
795
  }),
761
796
  ];
762
797
  const helper = ".projen/pypi-trusted-publisher-instructions.mjs";
798
+ const helperHeader = header({ tool: "projen", source: ".projenrc.ts" })
799
+ .trimEnd()
800
+ .replaceAll("\n", "\n ");
763
801
  new TextFile(project.root, helper, {
802
+ marker: true,
803
+ readonly: true,
764
804
  // prettier-ignore
765
805
  lines: stringUtils.dedent(
766
806
  // ============================================================================
767
807
  /*js*/`
768
808
  #!/usr/bin/env bun
769
- import { parseArgs } from "node:util";
809
+ ${helperHeader}
810
+ import { printPypiTrustedPublisherInstructions } from "@dbx-tools/projen/pypi-trusted-publisher-instructions";
770
811
 
771
- const { values } = parseArgs({
772
- options: { secretFile: { type: "string" } },
812
+ await printPypiTrustedPublisherInstructions({
813
+ beforeAuthentication: ${JSON.stringify(linesBeforeAuthentication)},
814
+ afterAuthentication: ${JSON.stringify(linesAfterAuthentication)},
773
815
  });
774
- const beforeAuthentication = ${JSON.stringify(linesBeforeAuthentication)};
775
- const afterAuthentication = ${JSON.stringify(linesAfterAuthentication)};
776
- const authentication = values.secretFile
777
- ? [
778
- \`- If browser authentication is required, read credentials from \${values.secretFile} directly into the browser without printing, logging, or exposing secret values.\`,
779
- \`- If \${values.secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.\`,
780
- ]
781
- : ["- If browser authentication is required, pause and ask the user to complete authentication."];
782
- process.stdout.write(\`\${[...beforeAuthentication, ...authentication, ...afterAuthentication].join("\\n").trimEnd()}\\n\`);
783
816
  `
784
817
  // ============================================================================
785
818
  ).split("\n"),
@@ -3,3 +3,7 @@
3
3
  export const RELEASE_VERSION = "${{ needs.build-release.outputs.release_version }}";
4
4
  /** Generated GitHub Actions workflow triggered by release tags. */
5
5
  export const RELEASE_WORKFLOW_FILE = "release.yml";
6
+ /** Public npm registry queried after npm publication completes. */
7
+ export const PUBLIC_NPM_REGISTRY_URL = "https://registry.npmjs.org";
8
+ /** Public PyPI JSON API queried after Python publication completes. */
9
+ export const PUBLIC_PYPI_REGISTRY_URL = "https://pypi.org/pypi";