@dbx-tools/projen 0.9.43 → 0.9.47

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 +28 -34
  2. package/index.ts +6 -7
  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 +1 -9
  18. package/src/codegen.ts +20 -17
  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 -157
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +122 -44
  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 +30 -9
  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 +77 -21
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +1 -1
  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,15 +81,16 @@ 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
+ /** Dependencies appended to the standard development group. */
87
88
  readonly devDependencies?: readonly string[];
88
89
  readonly testPaths?: readonly string[];
89
90
  readonly lintPaths?: readonly string[];
90
91
  /** Repository-relative paths Ruff must not lint or format. */
91
92
  readonly ruffExcludes?: readonly string[];
92
93
  readonly ruffPerFileIgnores?: Readonly<Record<string, readonly string[]>>;
93
- /** Generated Python implementation files Pyrefly should resolve but not type-check. */
94
- readonly pyreflyProjectExcludes?: readonly string[];
95
94
  readonly interpreterPath?: string | false;
96
95
  readonly release?: boolean | PythonReleaseOptions;
97
96
  }
@@ -102,6 +101,72 @@ const DEFAULT_DEV_DEPENDENCIES = [
102
101
  "pyyaml>=6.0,<7",
103
102
  "ruff>=0.12,<1",
104
103
  ] as const;
104
+ const DEFAULT_RUFF_TARGET = "py310";
105
+ const RELEASE_SETUP_CONDITION =
106
+ "${{ steps.release.outputs.validation == 'true' || steps.release.outputs.docs == 'true' || steps.release.outputs.pypi == 'true' }}";
107
+
108
+ /** Convert Ruff's `py311` target spelling to the setup-python `3.11` spelling. */
109
+ function pythonVersionFromRuffTarget(target: string): string {
110
+ const match = /^py(\d)(\d{1,2})$/.exec(target);
111
+ if (!match) throw new Error(`Cannot derive a workflow Python version from Ruff target ${target}`);
112
+ return `${match[1]}.${match[2]}`;
113
+ }
114
+
115
+ /** Python and uv setup steps, optionally gated by a workflow expression. */
116
+ function pythonSetupSteps(pythonVersion: string, condition?: string): JobStep[] {
117
+ return [
118
+ {
119
+ name: "Setup Python",
120
+ uses: github.ActionRefs.ACTIONS_SETUP_PYTHON,
121
+ ...(condition ? { if: condition } : {}),
122
+ with: { "python-version": pythonVersion },
123
+ },
124
+ {
125
+ ...uvSetupStep(),
126
+ ...(condition ? { if: condition } : {}),
127
+ },
128
+ ];
129
+ }
130
+
131
+ /** Materialize Projen's lazily rendered native build steps before extending them. */
132
+ function resolvedJobSteps(job: Job): JobStep[] {
133
+ const steps = (job as unknown as { steps: JobStep[] | (() => JobStep[]) }).steps;
134
+ return typeof steps === "function" ? steps() : steps;
135
+ }
136
+
137
+ function missingSetupSteps(job: Job, setupSteps: readonly JobStep[]): JobStep[] {
138
+ const actionName = (step: JobStep): string | undefined => step.uses?.split("@", 1)[0];
139
+ const existing = new Set(resolvedJobSteps(job).map(actionName));
140
+ return setupSteps.filter((step) => !existing.has(actionName(step)));
141
+ }
142
+
143
+ /**
144
+ * Ensure Python and uv are available before validation, docs, and packaging.
145
+ *
146
+ * The Python workspace owns these prerequisites. Existing action steps are
147
+ * reused so repository-specific setup can override a version without causing a
148
+ * duplicate installation.
149
+ */
150
+ function withPythonBuildSetup(job: Job, pythonVersion: string): Job {
151
+ const setupSteps = missingSetupSteps(
152
+ job,
153
+ pythonSetupSteps(pythonVersion, RELEASE_SETUP_CONDITION),
154
+ );
155
+ if (setupSteps.length === 0) return job;
156
+ const steps = resolvedJobSteps(job);
157
+ const verificationIndex = steps.findIndex((step) => step.name === "Verify release context");
158
+ const validationIndex = steps.findIndex((step) => step.name?.startsWith("Validate "));
159
+ const insertionIndex =
160
+ verificationIndex >= 0
161
+ ? verificationIndex + 1
162
+ : validationIndex < 0
163
+ ? steps.length
164
+ : validationIndex;
165
+ return {
166
+ ...job,
167
+ steps: [...steps.slice(0, insertionIndex), ...setupSteps, ...steps.slice(insertionIndex)],
168
+ };
169
+ }
105
170
 
106
171
  /** Repository-relative path for a Python package directory. */
107
172
  export function pythonPackagePath(repository: PythonRepositoryOptions, directory: string): string {
@@ -131,7 +196,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
131
196
  authorEmail: "",
132
197
  version: options.version,
133
198
  description: pkg.description,
134
- license: DBX_TOOLS_LICENSE,
199
+ license: LICENSE,
135
200
  github: false,
136
201
  sample: false,
137
202
  pytest: false,
@@ -171,7 +236,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
171
236
  },
172
237
  },
173
238
  });
174
- new License(this, { spdx: DBX_TOOLS_LICENSE });
239
+ new License(this, { spdx: LICENSE });
175
240
  this.packageOptions = pkg;
176
241
  if (!(this.packagingManager instanceof python.Uv)) {
177
242
  throw new Error(`Expected uv packaging for ${pkg.name}`);
@@ -202,12 +267,19 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
202
267
  /**
203
268
  * Generates a root uv workspace, projen-native Python member projects, Python
204
269
  * tasks, editor interpreter selection, and an optional publishing workflow.
270
+ *
271
+ * Native `PythonProject` and `PyprojectTomlFile` continue to own each package
272
+ * and TOML rendering. This component owns only the unsupported remainder:
273
+ * attaching several uv members to an existing JavaScript root and adding their
274
+ * dependency-ordered artifacts to the repository's shared tag release.
205
275
  */
206
276
  export class DBXToolsPythonWorkspace extends Component {
207
277
  readonly packages: readonly DBXToolsPythonProject[];
208
278
  readonly repository: Required<PythonRepositoryOptions>;
209
279
  readonly requiresPython: string;
210
280
  readonly version: string;
281
+ /** Exact Python version used by the shared release build. */
282
+ readonly workflowPythonVersion: string;
211
283
  readonly file: python.PyprojectTomlFile;
212
284
 
213
285
  constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
@@ -250,6 +322,9 @@ export class DBXToolsPythonWorkspace extends Component {
250
322
  }));
251
323
  const resolvedOptions = { ...options, packages };
252
324
  this.requiresPython = options.requiresPython ?? ">=3.10";
325
+ this.workflowPythonVersion =
326
+ options.workflowPythonVersion ??
327
+ pythonVersionFromRuffTarget(options.ruffTarget ?? DEFAULT_RUFF_TARGET);
253
328
  this.version = readWorkspaceVersion(project.outdir);
254
329
  this.file = this.emitWorkspace(project, resolvedOptions, scope);
255
330
  this.packages = packages.map(
@@ -285,10 +360,11 @@ export class DBXToolsPythonWorkspace extends Component {
285
360
  `${this.repository.root}/**/dist/`,
286
361
  );
287
362
  this.addTasks(project, resolvedOptions);
363
+ this.addBuildWorkflowSetup(project);
288
364
 
289
365
  const configuredReleaseOptions = options.release === true ? {} : options.release || {};
290
366
  const releaseOptions = { ...configuredReleaseOptions };
291
- this.addTrustedPublisherInstructionsTask(project, releaseOptions);
367
+ this.addTrustedPublisherInstructionsTask(project);
292
368
 
293
369
  const interpreterPath = options.interpreterPath ?? "${workspaceFolder}/.venv/bin/python";
294
370
  if (interpreterPath !== false) {
@@ -322,7 +398,7 @@ export class DBXToolsPythonWorkspace extends Component {
322
398
  dependencies: [...(options.dependencies ?? [])],
323
399
  },
324
400
  dependencyGroups: {
325
- dev: [...(options.devDependencies ?? DEFAULT_DEV_DEPENDENCIES)],
401
+ dev: [...DEFAULT_DEV_DEPENDENCIES, ...(options.devDependencies ?? [])],
326
402
  },
327
403
  tool: {
328
404
  uv: python.uvConfig.toJson_UvConfiguration({
@@ -338,7 +414,7 @@ export class DBXToolsPythonWorkspace extends Component {
338
414
  },
339
415
  },
340
416
  ruff: {
341
- "target-version": options.ruffTarget ?? "py310",
417
+ "target-version": options.ruffTarget ?? DEFAULT_RUFF_TARGET,
342
418
  "line-length": 100,
343
419
  lint: {
344
420
  "per-file-ignores": perFileIgnores,
@@ -360,18 +436,6 @@ export class DBXToolsPythonWorkspace extends Component {
360
436
  if (ruffExcludes.length) {
361
437
  file.addOverride("tool.ruff.exclude", [...new Set(ruffExcludes)]);
362
438
  }
363
- file.addOverride("tool.pyrefly.ignore-errors-in-generated-code", true);
364
- const projectExcludes = [
365
- ...(options.pyreflyProjectExcludes ?? []),
366
- ...options.packages.flatMap((pkg) =>
367
- [...(pkg.generatedSources ?? []), ...pythonNodeGeneratedSources(pkg)].map(
368
- (source) => `${this.repository.root}/${pkg.directory}/${source}`,
369
- ),
370
- ),
371
- ];
372
- if (projectExcludes.length) {
373
- file.addOverride("tool.pyrefly.project-excludes", [...new Set(projectExcludes)]);
374
- }
375
439
  file.addOverride(
376
440
  "tool.uv.sources",
377
441
  Object.fromEntries(options.packages.map((pkg) => [pkg.name, { workspace: true }])),
@@ -390,10 +454,11 @@ export class DBXToolsPythonWorkspace extends Component {
390
454
  exec: "uv run pytest",
391
455
  description: "Run Python workspace tests",
392
456
  });
393
- project.addTask("py:lint", {
457
+ const lint = project.addTask("py:lint", {
394
458
  exec: `uv run ruff check ${lintPaths.join(" ")}`,
395
459
  description: "Lint Python workspace packages",
396
460
  });
461
+ lint.exec(`uv run ruff format --check ${lintPaths.join(" ")}`);
397
462
  project.addTask("py:format", {
398
463
  exec: `uv run ruff format ${lintPaths.join(" ")}`,
399
464
  description: "Format Python workspace packages",
@@ -404,19 +469,37 @@ export class DBXToolsPythonWorkspace extends Component {
404
469
  });
405
470
  }
406
471
 
472
+ /** Add Python prerequisites to Projen's native build workflow when enabled. */
473
+ private addBuildWorkflowSetup(project: javascript.NodeProject): void {
474
+ const workflow = project.buildWorkflow?.workflow;
475
+ const jobId = project.buildWorkflowJobId;
476
+ if (!workflow || !jobId) return;
477
+ const job = workflow.getJob(jobId) as Job | undefined;
478
+ if (!job) return;
479
+ const setupSteps = missingSetupSteps(job, pythonSetupSteps(this.workflowPythonVersion));
480
+ if (setupSteps.length === 0) return;
481
+ const steps = resolvedJobSteps(job);
482
+ const buildIndex = steps.findIndex((step) => step.name === "build");
483
+ const insertionIndex = buildIndex < 0 ? steps.length : buildIndex;
484
+ workflow.updateJob(jobId, {
485
+ ...job,
486
+ steps: [...steps.slice(0, insertionIndex), ...setupSteps, ...steps.slice(insertionIndex)],
487
+ });
488
+ }
489
+
407
490
  private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
408
491
  if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
409
- const publications = this.publications(options);
492
+ const publications = this.publications();
410
493
  const allPublications = publications;
411
494
  if (allPublications.length === 0) return;
412
495
  const workflow = tryReleaseWorkflow(project);
413
496
  if (!workflow) {
414
497
  throw new Error("Python release requires the root dbx-tools release mode");
415
498
  }
416
- const build = workflow.getJob("build-release") as Job | undefined;
417
- if (!build) throw new Error("Python release requires the shared release build job");
499
+ const configuredBuild = workflow.getJob("build-release") as Job | undefined;
500
+ if (!configuredBuild) throw new Error("Python release requires the shared release build job");
501
+ const build = withPythonBuildSetup(configuredBuild, this.workflowPythonVersion);
418
502
  const pythonSteps: JobStep[] = [
419
- uvSetupStep(),
420
503
  {
421
504
  name: "Build Python distributions",
422
505
  env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
@@ -429,7 +512,7 @@ export class DBXToolsPythonWorkspace extends Component {
429
512
  },
430
513
  ...allPublications.map((publication) => ({
431
514
  name: `Upload ${publication.distribution} distributions`,
432
- uses: "actions/upload-artifact@v4",
515
+ uses: github.ActionRefs.ACTIONS_UPLOAD_ARTIFACT,
433
516
  with: {
434
517
  name: `release-python-${publication.directory}`,
435
518
  path: `.release/python/${publication.directory}`,
@@ -464,7 +547,7 @@ export class DBXToolsPythonWorkspace extends Component {
464
547
  steps: [
465
548
  {
466
549
  name: `Download ${publication.distribution} distributions`,
467
- uses: "actions/download-artifact@v4",
550
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
468
551
  with: {
469
552
  name: `release-python-${publication.directory}`,
470
553
  path: `dist/${publication.directory}`,
@@ -475,7 +558,6 @@ export class DBXToolsPythonWorkspace extends Component {
475
558
  uses: "pypa/gh-action-pypi-publish@release/v1",
476
559
  with: {
477
560
  "packages-dir": `dist/${publication.directory}`,
478
- "skip-existing": true,
479
561
  },
480
562
  },
481
563
  ],
@@ -484,22 +566,18 @@ export class DBXToolsPythonWorkspace extends Component {
484
566
  refreshReleaseDocsDependencies(project);
485
567
  }
486
568
 
487
- private publications(options: PythonReleaseOptions): readonly PythonPublication[] {
569
+ private publications(): readonly PythonPublication[] {
488
570
  return this.packages.map((pkg) => ({
489
571
  directory: pkg.packageOptions.directory,
490
572
  distribution: pkg.packageOptions.name,
491
- environment:
492
- options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
573
+ environment: pkg.packageOptions.releaseEnvironment ?? `pypi-${pkg.packageOptions.name}`,
493
574
  dependencies: pkg.packageOptions.internalDependencies,
494
575
  }));
495
576
  }
496
577
 
497
- private addTrustedPublisherInstructionsTask(
498
- project: javascript.NodeProject,
499
- options: PythonReleaseOptions,
500
- ): void {
578
+ private addTrustedPublisherInstructionsTask(project: javascript.NodeProject): void {
501
579
  const repository = this.githubRepository();
502
- const publications = this.publications(options);
580
+ const publications = this.publications();
503
581
  const releaseTag =
504
582
  isDBXToolsJavaScriptProject()(project) && tryReleaseWorkflow(project)
505
583
  ? releaseTagPattern(project)
@@ -579,7 +657,7 @@ export class DBXToolsPythonWorkspace extends Component {
579
657
  lines: stringUtils.dedent(
580
658
  // ============================================================================
581
659
  /*js*/`
582
- #!/usr/bin/env node
660
+ #!/usr/bin/env bun
583
661
  import { parseArgs } from "node:util";
584
662
 
585
663
  const { values } = parseArgs({
@@ -600,7 +678,7 @@ export class DBXToolsPythonWorkspace extends Component {
600
678
  });
601
679
  project.root.addTask("pypiTrustedPublisherInstructions", {
602
680
  description: "Print system-browser instructions for PyPI trusted publishers",
603
- exec: `node ${helper}`,
681
+ exec: `bun ${helper}`,
604
682
  receiveArgs: true,
605
683
  });
606
684
  }
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,
@@ -140,7 +153,6 @@ function releaseBuildJob(
140
153
  permissions: { contents: JobPermission.READ },
141
154
  timeoutMinutes: 60,
142
155
  env: {
143
- BUN_VERSION,
144
156
  CI: "true",
145
157
  ...(options.docs
146
158
  ? { DOCS_SITE_URL: options.docs.siteUrl, DOCS_BASE: options.docs.base ?? "/" }
@@ -162,7 +174,7 @@ function releaseBuildJob(
162
174
  fetchDepth: 0,
163
175
  },
164
176
  }),
165
- ...project.renderWorkflowSetup({ mutable: false }),
177
+ ...project.renderWorkflowSetup({ mutable: true }),
166
178
  {
167
179
  name: "Verify release context",
168
180
  id: "release",
@@ -192,6 +204,15 @@ function releaseBuildJob(
192
204
  // ============================================================================
193
205
  ),
194
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" },
195
216
  ...(options.setupSteps ?? []),
196
217
  ...releaseBuildSteps(
197
218
  "validation",
@@ -214,7 +235,7 @@ function releaseBuildJob(
214
235
  },
215
236
  {
216
237
  name: "Upload npm archives",
217
- uses: "actions/upload-artifact@v4",
238
+ uses: github.ActionRefs.ACTIONS_UPLOAD_ARTIFACT,
218
239
  with: { name: "release-npm", path: ".release/npm", "if-no-files-found": "error" },
219
240
  },
220
241
  ])),
@@ -243,11 +264,11 @@ function nodePublishJob(): Job {
243
264
  runsOn: ["ubuntu-latest"],
244
265
  permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
245
266
  timeoutMinutes: 30,
246
- env: { BUN_VERSION, CI: "true" },
267
+ env: { CI: "true" },
247
268
  steps: [
248
269
  {
249
270
  name: "Download npm archives",
250
- uses: "actions/download-artifact@v4",
271
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
251
272
  with: { name: "release-npm", path: ".release/npm" },
252
273
  },
253
274
  ...nodeReleaseSetupSteps(),
@@ -295,7 +316,7 @@ export class DBXToolsRelease extends Component {
295
316
  super(project);
296
317
  project.addGitIgnore(".release/");
297
318
  const installCondition =
298
- "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)\"";
299
320
  project.package.installTask.addCondition(installCondition);
300
321
  project.package.installCiTask.addCondition(installCondition);
301
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}). */
package/src/vscode.ts CHANGED
@@ -33,7 +33,7 @@ export function configureVsCode(scope: javascript.NodeProject): void {
33
33
  {
34
34
  label: "sync",
35
35
  detail:
36
- "projen sync --watch - projenrc (.projenrc.ts + syncResynthPaths re-synth) + barrels + openapi watchers",
36
+ "projen sync --watch - projenrc (.projenrc.ts + syncResynthPaths re-synth) + barrel watcher",
37
37
  type: "shell",
38
38
  command: "bun run sync -- --watch",
39
39
  isBackground: true,