@dbx-tools/projen 0.9.40 → 0.9.46

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.
Files changed (52) hide show
  1. package/README.md +58 -67
  2. package/index.ts +6 -8
  3. package/package.json +9 -10
  4. package/shims/python-node/bootstrap.ts +129 -0
  5. package/shims/python-node/child-process.ts +96 -0
  6. package/shims/python-node/crypto.ts +60 -0
  7. package/shims/python-node/fs-promises.ts +179 -0
  8. package/shims/python-node/fs.ts +173 -0
  9. package/shims/python-node/headers.ts +75 -0
  10. package/shims/python-node/host.ts +341 -0
  11. package/shims/python-node/os.ts +20 -0
  12. package/shims/python-node/process.ts +13 -0
  13. package/shims/python-node/readline.ts +37 -0
  14. package/shims/python-node/tsconfig.json +24 -0
  15. package/src/_lazy-require.ts +7 -25
  16. package/src/bun-app.ts +2 -2
  17. package/src/bun-workflow.ts +2 -170
  18. package/src/codegen.ts +23 -20
  19. package/src/dbx-tools-config.ts +52 -9
  20. package/src/generated.ts +4 -17
  21. package/src/mixin.ts +1 -5
  22. package/src/packages.ts +18 -15
  23. package/src/pnpm-workspace.ts +1 -4
  24. package/src/project-js.ts +65 -196
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +127 -37
  27. package/src/publish.ts +13 -10
  28. package/src/python-node-bindings.ts +2 -0
  29. package/src/python-node-bundle.ts +6 -0
  30. package/src/release-options.ts +17 -17
  31. package/src/release.ts +31 -14
  32. package/src/scaffold.ts +4 -7
  33. package/src/tags.ts +6 -16
  34. package/src/vscode.ts +1 -1
  35. package/src/watch.ts +5 -5
  36. package/src/workspace-lock.ts +1 -0
  37. package/src/workspace-version.ts +4 -4
  38. package/tasks/barrels.ts +3 -5
  39. package/tasks/clean.ts +1 -0
  40. package/tasks/lib/compile-workspace.ts +41 -19
  41. package/tasks/lib/local-publish.ts +0 -28
  42. package/tasks/lib/publish-npm.ts +8 -0
  43. package/tasks/lib/publish-python.ts +4 -0
  44. package/tasks/lib/python-node-bindings.ts +107 -26
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +2 -2
  48. package/tasks/projenrc.ts +1 -0
  49. package/tasks/sync.ts +4 -4
  50. package/src/openapi.ts +0 -293
  51. package/tasks/local-publish.ts +0 -6
  52. package/tasks/openapi.ts +0 -35
package/src/project-py.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /** Reusable uv workspace generation for Python packages hosted in a projen tree. */
2
2
  import * as projectUtils from "@dbx-tools/core/project-utils";
3
3
  import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
- import { Component, License, TextFile, type Project, javascript, python } from "projen";
4
+ import { Component, License, TextFile, type Project, github, javascript, python } from "projen";
5
5
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
6
- import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
6
+ import { LICENSE, projectRepositoryUrl } from "./project-js.ts";
7
7
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
8
8
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
9
9
  import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
@@ -29,17 +29,17 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
29
29
  readonly directory: string;
30
30
  readonly name?: string;
31
31
  readonly module?: string;
32
- /** Python import root. Defaults to `generated-src` for generated packages, otherwise `src`. */
32
+ /** Python source root passed to uv_build. Defaults to `src`. */
33
33
  readonly moduleRoot?: string;
34
34
  readonly description: string;
35
35
  readonly dependencies?: readonly string[];
36
36
  /** Workspace package directories rendered as standalone Git dependencies. */
37
37
  readonly internalDependencies?: readonly string[];
38
38
  readonly scripts?: Readonly<Record<string, string>>;
39
+ /** GitHub environment used to publish this distribution to PyPI. */
40
+ readonly releaseEnvironment?: string;
39
41
  /** One or more build-time Node packages embedded through PythonMonkey. */
40
42
  readonly nodeBindings?: OneOrMany<PythonNodeBindingsOptions>;
41
- /** Generated source files excluded from strict static analysis. Package-relative. */
42
- readonly generatedSources?: readonly string[];
43
43
  }
44
44
 
45
45
  interface ResolvedPythonPackageOptions extends PythonPackageOptions {
@@ -59,8 +59,6 @@ export interface DBXToolsPythonProjectOptions extends DBXToolsProjectOptions {
59
59
 
60
60
  /** Python release workflow configuration. */
61
61
  export interface PythonReleaseOptions {
62
- /** GitHub environment by Python distribution name. Defaults to `pypi-<name>`. */
63
- readonly environments?: Readonly<Record<string, string>>;
64
62
  readonly environmentUrl?: string;
65
63
  }
66
64
 
@@ -83,13 +81,15 @@ export interface DBXToolsPythonWorkspaceOptions {
83
81
  /** uv strategy for repositories that intentionally use multiple trusted indexes. */
84
82
  readonly indexStrategy?: "first-index" | "unsafe-first-match" | "unsafe-best-match";
85
83
  readonly ruffTarget?: string;
84
+ /** Python version installed in validation, documentation, and release workflows. */
85
+ readonly workflowPythonVersion?: string;
86
86
  readonly workspaceName?: string;
87
87
  readonly devDependencies?: readonly string[];
88
88
  readonly testPaths?: readonly string[];
89
89
  readonly lintPaths?: readonly string[];
90
+ /** Repository-relative paths Ruff must not lint or format. */
91
+ readonly ruffExcludes?: readonly string[];
90
92
  readonly ruffPerFileIgnores?: Readonly<Record<string, readonly string[]>>;
91
- /** Generated Python implementation files Pyrefly should resolve but not type-check. */
92
- readonly pyreflyProjectExcludes?: readonly string[];
93
93
  readonly interpreterPath?: string | false;
94
94
  readonly release?: boolean | PythonReleaseOptions;
95
95
  }
@@ -100,6 +100,72 @@ const DEFAULT_DEV_DEPENDENCIES = [
100
100
  "pyyaml>=6.0,<7",
101
101
  "ruff>=0.12,<1",
102
102
  ] as const;
103
+ const DEFAULT_RUFF_TARGET = "py310";
104
+ const RELEASE_SETUP_CONDITION =
105
+ "${{ steps.release.outputs.validation == 'true' || steps.release.outputs.docs == 'true' || steps.release.outputs.pypi == 'true' }}";
106
+
107
+ /** Convert Ruff's `py311` target spelling to the setup-python `3.11` spelling. */
108
+ function pythonVersionFromRuffTarget(target: string): string {
109
+ const match = /^py(\d)(\d{1,2})$/.exec(target);
110
+ if (!match) throw new Error(`Cannot derive a workflow Python version from Ruff target ${target}`);
111
+ return `${match[1]}.${match[2]}`;
112
+ }
113
+
114
+ /** Python and uv setup steps, optionally gated by a workflow expression. */
115
+ function pythonSetupSteps(pythonVersion: string, condition?: string): JobStep[] {
116
+ return [
117
+ {
118
+ name: "Setup Python",
119
+ uses: github.ActionRefs.ACTIONS_SETUP_PYTHON,
120
+ ...(condition ? { if: condition } : {}),
121
+ with: { "python-version": pythonVersion },
122
+ },
123
+ {
124
+ ...uvSetupStep(),
125
+ ...(condition ? { if: condition } : {}),
126
+ },
127
+ ];
128
+ }
129
+
130
+ /** Materialize Projen's lazily rendered native build steps before extending them. */
131
+ function resolvedJobSteps(job: Job): JobStep[] {
132
+ const steps = (job as unknown as { steps: JobStep[] | (() => JobStep[]) }).steps;
133
+ return typeof steps === "function" ? steps() : steps;
134
+ }
135
+
136
+ function missingSetupSteps(job: Job, setupSteps: readonly JobStep[]): JobStep[] {
137
+ const actionName = (step: JobStep): string | undefined => step.uses?.split("@", 1)[0];
138
+ const existing = new Set(resolvedJobSteps(job).map(actionName));
139
+ return setupSteps.filter((step) => !existing.has(actionName(step)));
140
+ }
141
+
142
+ /**
143
+ * Ensure Python and uv are available before validation, docs, and packaging.
144
+ *
145
+ * The Python workspace owns these prerequisites. Existing action steps are
146
+ * reused so repository-specific setup can override a version without causing a
147
+ * duplicate installation.
148
+ */
149
+ function withPythonBuildSetup(job: Job, pythonVersion: string): Job {
150
+ const setupSteps = missingSetupSteps(
151
+ job,
152
+ pythonSetupSteps(pythonVersion, RELEASE_SETUP_CONDITION),
153
+ );
154
+ if (setupSteps.length === 0) return job;
155
+ const steps = resolvedJobSteps(job);
156
+ const verificationIndex = steps.findIndex((step) => step.name === "Verify release context");
157
+ const validationIndex = steps.findIndex((step) => step.name?.startsWith("Validate "));
158
+ const insertionIndex =
159
+ verificationIndex >= 0
160
+ ? verificationIndex + 1
161
+ : validationIndex < 0
162
+ ? steps.length
163
+ : validationIndex;
164
+ return {
165
+ ...job,
166
+ steps: [...steps.slice(0, insertionIndex), ...setupSteps, ...steps.slice(insertionIndex)],
167
+ };
168
+ }
103
169
 
104
170
  /** Repository-relative path for a Python package directory. */
105
171
  export function pythonPackagePath(repository: PythonRepositoryOptions, directory: string): string {
@@ -129,7 +195,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
129
195
  authorEmail: "",
130
196
  version: options.version,
131
197
  description: pkg.description,
132
- license: DBX_TOOLS_LICENSE,
198
+ license: LICENSE,
133
199
  github: false,
134
200
  sample: false,
135
201
  pytest: false,
@@ -169,7 +235,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
169
235
  },
170
236
  },
171
237
  });
172
- new License(this, { spdx: DBX_TOOLS_LICENSE });
238
+ new License(this, { spdx: LICENSE });
173
239
  this.packageOptions = pkg;
174
240
  if (!(this.packagingManager instanceof python.Uv)) {
175
241
  throw new Error(`Expected uv packaging for ${pkg.name}`);
@@ -200,12 +266,19 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
200
266
  /**
201
267
  * Generates a root uv workspace, projen-native Python member projects, Python
202
268
  * tasks, editor interpreter selection, and an optional publishing workflow.
269
+ *
270
+ * Native `PythonProject` and `PyprojectTomlFile` continue to own each package
271
+ * and TOML rendering. This component owns only the unsupported remainder:
272
+ * attaching several uv members to an existing JavaScript root and adding their
273
+ * dependency-ordered artifacts to the repository's shared tag release.
203
274
  */
204
275
  export class DBXToolsPythonWorkspace extends Component {
205
276
  readonly packages: readonly DBXToolsPythonProject[];
206
277
  readonly repository: Required<PythonRepositoryOptions>;
207
278
  readonly requiresPython: string;
208
279
  readonly version: string;
280
+ /** Exact Python version used by the shared release build. */
281
+ readonly workflowPythonVersion: string;
209
282
  readonly file: python.PyprojectTomlFile;
210
283
 
211
284
  constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
@@ -248,6 +321,9 @@ export class DBXToolsPythonWorkspace extends Component {
248
321
  }));
249
322
  const resolvedOptions = { ...options, packages };
250
323
  this.requiresPython = options.requiresPython ?? ">=3.10";
324
+ this.workflowPythonVersion =
325
+ options.workflowPythonVersion ??
326
+ pythonVersionFromRuffTarget(options.ruffTarget ?? DEFAULT_RUFF_TARGET);
251
327
  this.version = readWorkspaceVersion(project.outdir);
252
328
  this.file = this.emitWorkspace(project, resolvedOptions, scope);
253
329
  this.packages = packages.map(
@@ -283,10 +359,11 @@ export class DBXToolsPythonWorkspace extends Component {
283
359
  `${this.repository.root}/**/dist/`,
284
360
  );
285
361
  this.addTasks(project, resolvedOptions);
362
+ this.addBuildWorkflowSetup(project);
286
363
 
287
364
  const configuredReleaseOptions = options.release === true ? {} : options.release || {};
288
365
  const releaseOptions = { ...configuredReleaseOptions };
289
- this.addTrustedPublisherInstructionsTask(project, releaseOptions);
366
+ this.addTrustedPublisherInstructionsTask(project);
290
367
 
291
368
  const interpreterPath = options.interpreterPath ?? "${workspaceFolder}/.venv/bin/python";
292
369
  if (interpreterPath !== false) {
@@ -336,7 +413,7 @@ export class DBXToolsPythonWorkspace extends Component {
336
413
  },
337
414
  },
338
415
  ruff: {
339
- "target-version": options.ruffTarget ?? "py310",
416
+ "target-version": options.ruffTarget ?? DEFAULT_RUFF_TARGET,
340
417
  "line-length": 100,
341
418
  lint: {
342
419
  "per-file-ignores": perFileIgnores,
@@ -347,17 +424,16 @@ export class DBXToolsPythonWorkspace extends Component {
347
424
  if (options.indexStrategy) {
348
425
  file.addOverride("tool.uv.index-strategy", options.indexStrategy);
349
426
  }
350
- file.addOverride("tool.pyrefly.ignore-errors-in-generated-code", true);
351
- const projectExcludes = [
352
- ...(options.pyreflyProjectExcludes ?? []),
427
+ const ruffExcludes = [
428
+ ...(options.ruffExcludes ?? []),
353
429
  ...options.packages.flatMap((pkg) =>
354
- [...(pkg.generatedSources ?? []), ...pythonNodeGeneratedSources(pkg)].map(
430
+ pythonNodeGeneratedSources(pkg).map(
355
431
  (source) => `${this.repository.root}/${pkg.directory}/${source}`,
356
432
  ),
357
433
  ),
358
434
  ];
359
- if (projectExcludes.length) {
360
- file.addOverride("tool.pyrefly.project-excludes", [...new Set(projectExcludes)]);
435
+ if (ruffExcludes.length) {
436
+ file.addOverride("tool.ruff.exclude", [...new Set(ruffExcludes)]);
361
437
  }
362
438
  file.addOverride(
363
439
  "tool.uv.sources",
@@ -377,10 +453,11 @@ export class DBXToolsPythonWorkspace extends Component {
377
453
  exec: "uv run pytest",
378
454
  description: "Run Python workspace tests",
379
455
  });
380
- project.addTask("py:lint", {
456
+ const lint = project.addTask("py:lint", {
381
457
  exec: `uv run ruff check ${lintPaths.join(" ")}`,
382
458
  description: "Lint Python workspace packages",
383
459
  });
460
+ lint.exec(`uv run ruff format --check ${lintPaths.join(" ")}`);
384
461
  project.addTask("py:format", {
385
462
  exec: `uv run ruff format ${lintPaths.join(" ")}`,
386
463
  description: "Format Python workspace packages",
@@ -391,19 +468,37 @@ export class DBXToolsPythonWorkspace extends Component {
391
468
  });
392
469
  }
393
470
 
471
+ /** Add Python prerequisites to Projen's native build workflow when enabled. */
472
+ private addBuildWorkflowSetup(project: javascript.NodeProject): void {
473
+ const workflow = project.buildWorkflow?.workflow;
474
+ const jobId = project.buildWorkflowJobId;
475
+ if (!workflow || !jobId) return;
476
+ const job = workflow.getJob(jobId) as Job | undefined;
477
+ if (!job) return;
478
+ const setupSteps = missingSetupSteps(job, pythonSetupSteps(this.workflowPythonVersion));
479
+ if (setupSteps.length === 0) return;
480
+ const steps = resolvedJobSteps(job);
481
+ const buildIndex = steps.findIndex((step) => step.name === "build");
482
+ const insertionIndex = buildIndex < 0 ? steps.length : buildIndex;
483
+ workflow.updateJob(jobId, {
484
+ ...job,
485
+ steps: [...steps.slice(0, insertionIndex), ...setupSteps, ...steps.slice(insertionIndex)],
486
+ });
487
+ }
488
+
394
489
  private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
395
490
  if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
396
- const publications = this.publications(options);
491
+ const publications = this.publications();
397
492
  const allPublications = publications;
398
493
  if (allPublications.length === 0) return;
399
494
  const workflow = tryReleaseWorkflow(project);
400
495
  if (!workflow) {
401
496
  throw new Error("Python release requires the root dbx-tools release mode");
402
497
  }
403
- const build = workflow.getJob("build-release") as Job | undefined;
404
- if (!build) throw new Error("Python release requires the shared release build job");
498
+ const configuredBuild = workflow.getJob("build-release") as Job | undefined;
499
+ if (!configuredBuild) throw new Error("Python release requires the shared release build job");
500
+ const build = withPythonBuildSetup(configuredBuild, this.workflowPythonVersion);
405
501
  const pythonSteps: JobStep[] = [
406
- uvSetupStep(),
407
502
  {
408
503
  name: "Build Python distributions",
409
504
  env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
@@ -416,7 +511,7 @@ export class DBXToolsPythonWorkspace extends Component {
416
511
  },
417
512
  ...allPublications.map((publication) => ({
418
513
  name: `Upload ${publication.distribution} distributions`,
419
- uses: "actions/upload-artifact@v4",
514
+ uses: github.ActionRefs.ACTIONS_UPLOAD_ARTIFACT,
420
515
  with: {
421
516
  name: `release-python-${publication.directory}`,
422
517
  path: `.release/python/${publication.directory}`,
@@ -451,7 +546,7 @@ export class DBXToolsPythonWorkspace extends Component {
451
546
  steps: [
452
547
  {
453
548
  name: `Download ${publication.distribution} distributions`,
454
- uses: "actions/download-artifact@v4",
549
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
455
550
  with: {
456
551
  name: `release-python-${publication.directory}`,
457
552
  path: `dist/${publication.directory}`,
@@ -462,7 +557,6 @@ export class DBXToolsPythonWorkspace extends Component {
462
557
  uses: "pypa/gh-action-pypi-publish@release/v1",
463
558
  with: {
464
559
  "packages-dir": `dist/${publication.directory}`,
465
- "skip-existing": true,
466
560
  },
467
561
  },
468
562
  ],
@@ -471,22 +565,18 @@ export class DBXToolsPythonWorkspace extends Component {
471
565
  refreshReleaseDocsDependencies(project);
472
566
  }
473
567
 
474
- private publications(options: PythonReleaseOptions): readonly PythonPublication[] {
568
+ private publications(): readonly PythonPublication[] {
475
569
  return this.packages.map((pkg) => ({
476
570
  directory: pkg.packageOptions.directory,
477
571
  distribution: pkg.packageOptions.name,
478
- environment:
479
- options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
572
+ environment: pkg.packageOptions.releaseEnvironment ?? `pypi-${pkg.packageOptions.name}`,
480
573
  dependencies: pkg.packageOptions.internalDependencies,
481
574
  }));
482
575
  }
483
576
 
484
- private addTrustedPublisherInstructionsTask(
485
- project: javascript.NodeProject,
486
- options: PythonReleaseOptions,
487
- ): void {
577
+ private addTrustedPublisherInstructionsTask(project: javascript.NodeProject): void {
488
578
  const repository = this.githubRepository();
489
- const publications = this.publications(options);
579
+ const publications = this.publications();
490
580
  const releaseTag =
491
581
  isDBXToolsJavaScriptProject()(project) && tryReleaseWorkflow(project)
492
582
  ? releaseTagPattern(project)
@@ -566,7 +656,7 @@ export class DBXToolsPythonWorkspace extends Component {
566
656
  lines: stringUtils.dedent(
567
657
  // ============================================================================
568
658
  /*js*/`
569
- #!/usr/bin/env node
659
+ #!/usr/bin/env bun
570
660
  import { parseArgs } from "node:util";
571
661
 
572
662
  const { values } = parseArgs({
@@ -587,7 +677,7 @@ export class DBXToolsPythonWorkspace extends Component {
587
677
  });
588
678
  project.root.addTask("pypiTrustedPublisherInstructions", {
589
679
  description: "Print system-browser instructions for PyPI trusted publishers",
590
- exec: `node ${helper}`,
680
+ exec: `bun ${helper}`,
591
681
  receiveArgs: true,
592
682
  });
593
683
  }
package/src/publish.ts CHANGED
@@ -9,11 +9,10 @@
9
9
  * (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so a published package whose
10
10
  * entry point is `index.ts` is unloadable by anything that is not a bundler.
11
11
  *
12
- * pnpm resolves both at once. It substitutes `publishConfig`'s `main`/`types`/
13
- * `bin`/`exports` into the manifest at pack time and drops `publishConfig`
14
- * itself, so the workspace keeps its source entry points while the tarball
15
- * advertises the compiled ones. Nothing here changes how the repo builds or
16
- * type-checks; it only changes what `pnpm pack` writes.
12
+ * `publishConfig` records the compiled `main`/`types`/`bin`/`exports` tree while
13
+ * the workspace keeps its source entry points. The release packer applies that
14
+ * record inside each archive because Bun does not perform npm's publishConfig
15
+ * substitution itself. The checkout remains source-first.
17
16
  *
18
17
  * Two compiler options make the emitted tree loadable, both native `tsc`:
19
18
  *
@@ -56,14 +55,18 @@ export const COMPILED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
56
55
  * Everything does EXCEPT the `ui` tag, and the exclusion is about consumers
57
56
  * rather than convenience. The problem being solved is that Node cannot load raw
58
57
  * TypeScript - but a browser package is never loaded by Node. UI packages reach
59
- * their consumer through Vite, which reads their source happily, and they export
60
- * `./styles.css` plus raw SVG assets that `tsc` does not copy and could not
61
- * rewrite. Compiling them would mean a real asset pipeline (Vite library mode)
62
- * to solve a problem they do not have.
58
+ * their consumer through a browser bundler, which reads their source, and they
59
+ * export `./styles.css` plus raw SVG assets that `tsc` does not copy and could
60
+ * not rewrite. Compiling them would require an asset-aware library pipeline to
61
+ * solve a problem they do not have.
63
62
  */
64
63
  export function publishesCompiled(pkg: javascript.NodeProject): boolean {
65
64
  if (!(pkg instanceof typescript.TypeScriptProject) || !pkg.parent) return false;
66
- return isDBXToolsJavaScriptProject()(pkg) && !pkg.dbxToolsConfig.tags.includes("ui");
65
+ return (
66
+ isDBXToolsJavaScriptProject()(pkg) &&
67
+ pkg.dbxToolsConfig.publishable &&
68
+ !pkg.dbxToolsConfig.tags.includes("ui")
69
+ );
67
70
  }
68
71
 
69
72
  /** The `./lib/...` stem of a source path, or `undefined` if it is not TypeScript. */
@@ -8,6 +8,7 @@ import { parse } from "smol-toml";
8
8
  import { runTaskCommand } from "./_task-command.ts";
9
9
  import { resolveRepoRoot, workspaceDependencyDirectories } from "./packages.ts";
10
10
 
11
+ /** Resolved function replacement consumed while generating one PythonMonkey bundle. */
11
12
  export interface ResolvedPythonNodeFunctionOverride {
12
13
  readonly handlerExport: string;
13
14
  readonly handlerFile: string;
@@ -15,6 +16,7 @@ export interface ResolvedPythonNodeFunctionOverride {
15
16
  readonly targetModule: string;
16
17
  }
17
18
 
19
+ /** Fully resolved pyproject binding configuration consumed by the runtime generator. */
18
20
  export interface ResolvedPythonNodeBindings {
19
21
  readonly bindingDirectory: string;
20
22
  readonly bindingName: string;
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Projen task wiring for pyproject-driven PythonMonkey runtime generation.
3
+ *
4
+ * Type extraction and bundle emission remain in the executable generator; this
5
+ * component owns only typed configuration and generate/check task registration.
6
+ */
1
7
  import { Component, type Project, type Task } from "projen";
2
8
  import { taskCommand } from "./project-js.ts";
3
9
 
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import { json } from "@dbx-tools/shared-core";
11
+ import { z } from "zod";
11
12
 
12
13
  /** Publication scopes accepted by the release task; auto preserves normal publication. */
13
14
  export const RELEASE_PUBLISH_TARGETS = ["auto", "npm", "pypi", "local", "none"] as const;
@@ -22,12 +23,19 @@ export const RELEASE_INSTALL_MODES = ["auto", "always", "never"] as const;
22
23
  export type ReleaseInstallMode = (typeof RELEASE_INSTALL_MODES)[number];
23
24
 
24
25
  /** CI steps selected by an immutable release annotation. */
25
- export interface ReleaseStepSelection {
26
- readonly npm: boolean;
27
- readonly pypi: boolean;
28
- readonly docs: boolean;
29
- readonly validation: boolean;
30
- }
26
+ export const ReleaseStepSelectionSchema = z
27
+ .object({
28
+ npm: z.boolean().describe("Build and publish npm artifacts."),
29
+ pypi: z.boolean().describe("Build and publish Python artifacts."),
30
+ docs: z.boolean().describe("Build and deploy documentation."),
31
+ validation: z.boolean().describe("Run repository validation tasks."),
32
+ })
33
+ .strict()
34
+ .readonly()
35
+ .describe("Immutable CI steps recorded in an annotated release tag.");
36
+
37
+ /** CI steps selected by an immutable release annotation. */
38
+ export type ReleaseStepSelection = z.infer<typeof ReleaseStepSelectionSchema>;
31
39
 
32
40
  /** Flags consumed by the shared release-selection policy. */
33
41
  export interface ReleaseSelectionOptions {
@@ -63,17 +71,9 @@ export function parseReleaseTagAnnotation(annotation: string): ReleaseStepSelect
63
71
  if (selections.length === 0) return releaseStepSelection();
64
72
  if (selections.length !== 1) throw new Error("Release tag contains multiple step selections");
65
73
  const value = json.parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
66
- if (
67
- !value ||
68
- Object.keys(value).length !== 4 ||
69
- typeof value.npm !== "boolean" ||
70
- typeof value.pypi !== "boolean" ||
71
- typeof value.docs !== "boolean" ||
72
- typeof value.validation !== "boolean"
73
- ) {
74
- throw new Error("Invalid release tag step selection");
75
- }
76
- return { npm: value.npm, pypi: value.pypi, docs: value.docs, validation: value.validation };
74
+ const parsed = ReleaseStepSelectionSchema.safeParse(value);
75
+ if (!parsed.success) throw new Error("Invalid release tag step selection");
76
+ return parsed.data;
77
77
  }
78
78
 
79
79
  /** Whether the selected release should inspect and publish configured local registries. */
package/src/release.ts CHANGED
@@ -1,4 +1,15 @@
1
- /** Unified default-branch release workflow generation. */
1
+ /**
2
+ * Unified annotated-tag release workflow generation.
3
+ *
4
+ * Projen's native `Release` owns a different lifecycle: it calculates versions
5
+ * from commits, generates changelogs and GitHub Releases, and publishes Publib's
6
+ * ecosystem-specific artifact tree from branch workflows. This repository
7
+ * instead creates one reviewed `VERSION` commit and annotated `v*` tag locally,
8
+ * requires that tag to equal `origin/main`, omits GitHub Releases, and builds
9
+ * npm, Python, and documentation artifacts once before isolated publication
10
+ * jobs download them. `DBXToolsRelease` therefore remains separate while native
11
+ * workflow and project primitives own the YAML model and setup steps.
12
+ */
2
13
  import { stringUtils } from "@dbx-tools/shared-core";
3
14
  import { Component, github } from "projen";
4
15
  import { GithubWorkflow } from "projen/lib/github";
@@ -40,6 +51,8 @@ export interface DBXToolsReleaseOptions {
40
51
  readonly validationTasks?: readonly string[];
41
52
  /** Repository prerequisites installed before release validation and artifact builds. */
42
53
  readonly setupSteps?: readonly JobStep[];
54
+ /** Repository-specific synthesis commands run before the root Projen synthesis check. */
55
+ readonly synthesisCommands?: readonly string[];
43
56
  }
44
57
 
45
58
  /** Locate the unified workflow when release generation is enabled. */
@@ -76,12 +89,12 @@ export function nodeReleaseSetupSteps(): readonly JobStep[] {
76
89
  return [
77
90
  {
78
91
  name: "Setup Bun",
79
- uses: "oven-sh/setup-bun@v2",
92
+ uses: github.ActionRefs.OVEN_SH_SETUP_BUN,
80
93
  with: { "bun-version": BUN_VERSION },
81
94
  },
82
95
  {
83
96
  name: "Setup Node.js",
84
- uses: "actions/setup-node@v6",
97
+ uses: github.ActionRefs.ACTIONS_SETUP_NODE,
85
98
  with: {
86
99
  "node-version": NODE_VERSION,
87
100
  "registry-url": NPM_REGISTRY_URL,
@@ -92,15 +105,11 @@ export function nodeReleaseSetupSteps(): readonly JobStep[] {
92
105
  ];
93
106
  }
94
107
 
95
- /** Install uv and restore its download/build cache using Python manifests. */
108
+ /** Install uv for Python release builds. */
96
109
  export function uvSetupStep(): JobStep {
97
110
  return {
98
111
  name: "Setup uv",
99
112
  uses: "astral-sh/setup-uv@v7",
100
- with: {
101
- "enable-cache": true,
102
- "cache-dependency-glob": "**/pyproject.toml",
103
- },
104
113
  };
105
114
  }
106
115
 
@@ -144,7 +153,6 @@ function releaseBuildJob(
144
153
  permissions: { contents: JobPermission.READ },
145
154
  timeoutMinutes: 60,
146
155
  env: {
147
- BUN_VERSION,
148
156
  CI: "true",
149
157
  ...(options.docs
150
158
  ? { DOCS_SITE_URL: options.docs.siteUrl, DOCS_BASE: options.docs.base ?? "/" }
@@ -166,7 +174,7 @@ function releaseBuildJob(
166
174
  fetchDepth: 0,
167
175
  },
168
176
  }),
169
- ...project.renderWorkflowSetup({ mutable: false }),
177
+ ...project.renderWorkflowSetup({ mutable: true }),
170
178
  {
171
179
  name: "Verify release context",
172
180
  id: "release",
@@ -196,6 +204,15 @@ function releaseBuildJob(
196
204
  // ============================================================================
197
205
  ),
198
206
  },
207
+ {
208
+ name: "Verify generated sources",
209
+ run: [
210
+ ...(options.synthesisCommands ?? []),
211
+ "bunx projen",
212
+ "git diff --ignore-space-at-eol --exit-code",
213
+ ].join("\n"),
214
+ },
215
+ { name: "Verify workspace versions", run: "bun run version:check" },
199
216
  ...(options.setupSteps ?? []),
200
217
  ...releaseBuildSteps(
201
218
  "validation",
@@ -218,7 +235,7 @@ function releaseBuildJob(
218
235
  },
219
236
  {
220
237
  name: "Upload npm archives",
221
- uses: "actions/upload-artifact@v4",
238
+ uses: github.ActionRefs.ACTIONS_UPLOAD_ARTIFACT,
222
239
  with: { name: "release-npm", path: ".release/npm", "if-no-files-found": "error" },
223
240
  },
224
241
  ])),
@@ -247,11 +264,11 @@ function nodePublishJob(): Job {
247
264
  runsOn: ["ubuntu-latest"],
248
265
  permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
249
266
  timeoutMinutes: 30,
250
- env: { BUN_VERSION, CI: "true" },
267
+ env: { CI: "true" },
251
268
  steps: [
252
269
  {
253
270
  name: "Download npm archives",
254
- uses: "actions/download-artifact@v4",
271
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
255
272
  with: { name: "release-npm", path: ".release/npm" },
256
273
  },
257
274
  ...nodeReleaseSetupSteps(),
@@ -299,7 +316,7 @@ export class DBXToolsRelease extends Component {
299
316
  super(project);
300
317
  project.addGitIgnore(".release/");
301
318
  const installCondition =
302
- 'node -e "process.exit(process.env.DBX_TOOLS_RELEASE_INSTALL === \'never\' ? 1 : 0)"';
319
+ "bun -e \"process.exit(process.env.DBX_TOOLS_RELEASE_INSTALL === 'never' ? 1 : 0)\"";
303
320
  project.package.installTask.addCondition(installCondition);
304
321
  project.package.installCiTask.addCondition(installCondition);
305
322
  const tagPrefix = options.tagPrefix ?? "v";
package/src/scaffold.ts CHANGED
@@ -9,11 +9,9 @@ import { repoRoot } from "./packages.ts";
9
9
  * Re-run projen synth by executing `.projenrc.ts` with the CURRENT runtime (no
10
10
  * projen network re-exec).
11
11
  *
12
- * The TypeScript loader is chosen by which runtime is running: bun executes
13
- * `.ts` natively, while node needs `--import tsx`. Passing `--import tsx` to bun
14
- * fails outright (`Cannot find module './cjs/index.cjs'`), which is what broke
15
- * `sync` once the repo moved onto bun - `process.execPath` is bun here, so the
16
- * flag was aimed at the one runtime that cannot take it.
12
+ * dbx-tools tasks are Bun entrypoints, so the current runtime executes the
13
+ * TypeScript definition directly. A Node fallback would require an additional
14
+ * loader that no generated task or supported workflow uses.
17
15
  *
18
16
  * `post: true` runs the full flow - projen's post-synth `bun install` AND the
19
17
  * post-synth barrels component - which is what the one-shot `sync` task
@@ -33,6 +31,5 @@ export function runSynth(options: { post?: boolean } = {}): void {
33
31
  if (options.post) delete env.PROJEN_DISABLE_POST;
34
32
  else env.PROJEN_DISABLE_POST = "true";
35
33
  const projenrc = join(repoRoot, ".projenrc.ts");
36
- const args = process.versions.bun ? [projenrc] : ["--import", "tsx", projenrc];
37
- runTaskCommand(repoRoot, process.execPath, args, { env });
34
+ runTaskCommand(repoRoot, process.execPath, [projenrc], { env });
38
35
  }
package/src/tags.ts CHANGED
@@ -34,7 +34,7 @@ const NODE_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
34
34
  types: ["node"],
35
35
  };
36
36
 
37
- /** The DOM-capable lib list shared by the browser tags (`ui`, `openapi`). */
37
+ /** DOM-capable libraries shared by browser packages. */
38
38
  const DOM_LIB = ["ES2022", "DOM", "DOM.Iterable"];
39
39
 
40
40
  function removeNodeTypes(project: javascript.NodeProject): void {
@@ -144,7 +144,7 @@ export const PACKAGE_TAG_MIXINS = {
144
144
  // register and no launcher shim to generate. In a WORKSPACE checkout the
145
145
  // manifest still points at the `.ts` entry, which Node type-strips on its own
146
146
  // because the package is not under `node_modules`.
147
- p.addDeps("commander@catalog:");
147
+ p.addDeps("commander@catalog:", "zod@catalog:");
148
148
  p.addDevDeps("@types/node@catalog:");
149
149
  // A CLI compiles code OUTSIDE `src/` - its root `index.ts` barrel and the
150
150
  // `bin/` entries - which the src-only tag default doesn't reach.
@@ -163,15 +163,11 @@ export const PACKAGE_TAG_MIXINS = {
163
163
  addPackageFiles(p, "bin");
164
164
  }),
165
165
  server: create(projectPredicate.hasTag("server"), (p) => {
166
- // A Node/Express service. tsoa's decorators (@Route/@Get/...) also drive
167
- // the `openapi` task (spec + client); experimentalDecorators lets them
168
- // type-check. `dev`/`start` run the app's `src/server.ts` with tsx.
169
- p.addDeps("express@catalog:", "tsoa@catalog:");
166
+ // A Node/Express service. `dev` and `start` run `src/server.ts` directly
167
+ // through Bun, while package compilation remains native TypeScript.
168
+ p.addDeps("express@catalog:");
170
169
  p.addDevDeps("@types/node@catalog:", "@types/express@catalog:");
171
- applyCompilerOptions(p, {
172
- ...NODE_COMPILER_OPTIONS,
173
- experimentalDecorators: true,
174
- });
170
+ applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
175
171
  // bun runs the server `.ts` directly (native TS, no tsx). `--watch` restarts
176
172
  // on change - the tsx-watch replacement.
177
173
  (p.tasks.tryFind("dev") ?? p.addTask("dev")).reset("bun --watch src/server.ts");
@@ -185,12 +181,6 @@ export const PACKAGE_TAG_MIXINS = {
185
181
  removeNodeTypes(p);
186
182
  applyCompilerOptions(p, AGNOSTIC_COMPILER_OPTIONS);
187
183
  }),
188
- openapi: create(projectPredicate.hasTag("openapi"), (p) => {
189
- removeNodeTypes(p);
190
- p.addDeps("openapi-fetch@catalog:");
191
- applyCompilerOptions(p, { target: "ES2022", lib: [...DOM_LIB], types: [] });
192
- addPackageFiles(p, "openapi.json");
193
- }),
194
184
  } satisfies Record<string, ConstructsMixin>;
195
185
 
196
186
  /** A known tag name (a key of {@link PACKAGE_TAG_MIXINS}). */