@dbx-tools/projen 0.9.21 → 0.9.29

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 (111) hide show
  1. package/README.md +77 -207
  2. package/index.ts +10 -14
  3. package/package.json +4 -4
  4. package/src/barrels.ts +1 -74
  5. package/src/module-exports.ts +175 -1
  6. package/src/openapi.ts +5 -72
  7. package/src/packages.ts +45 -0
  8. package/src/project-js.ts +3 -6
  9. package/src/project-predicate.ts +1 -2
  10. package/src/project-py.ts +82 -103
  11. package/src/project.ts +2 -2
  12. package/src/python-node-bindings.ts +138 -0
  13. package/src/python-node-bundle.ts +92 -0
  14. package/src/release-catalog.ts +3 -5
  15. package/src/{release-dispatch.ts → release-context.ts} +0 -11
  16. package/src/release.ts +70 -152
  17. package/tasks/bump.ts +12 -21
  18. package/tasks/databricks-cli-assets.ts +129 -0
  19. package/tasks/local-publish.ts +23 -107
  20. package/tasks/publish-npm.ts +10 -1
  21. package/tasks/publish-python.ts +26 -11
  22. package/tasks/publish.ts +12 -48
  23. package/tasks/python-node-bindings-watch.ts +47 -0
  24. package/tasks/python-node-bindings.ts +1017 -0
  25. package/tasks/python-release.ts +74 -0
  26. package/tasks/release.ts +99 -0
  27. package/tasks/sync.ts +13 -18
  28. package/tasks/version-check.ts +1 -28
  29. package/._LICENSE +0 -0
  30. package/._README.md +0 -0
  31. package/._index.ts +0 -0
  32. package/._package.json +0 -0
  33. package/src/.__lazy-require.ts +0 -0
  34. package/src/.__release-platform.ts +0 -0
  35. package/src/.__rust-project.ts +0 -0
  36. package/src/.__rust-release-workflow.ts +0 -0
  37. package/src/.__task-command.ts +0 -0
  38. package/src/._barrels.ts +0 -0
  39. package/src/._bun-app.ts +0 -0
  40. package/src/._bun-workflow.ts +0 -0
  41. package/src/._clean.ts +0 -0
  42. package/src/._codegen.ts +0 -0
  43. package/src/._dbx-tools-config.ts +0 -0
  44. package/src/._engine-root.ts +0 -0
  45. package/src/._generated.ts +0 -0
  46. package/src/._mixin.ts +0 -0
  47. package/src/._module-exports.ts +0 -0
  48. package/src/._openapi.ts +0 -0
  49. package/src/._packages.ts +0 -0
  50. package/src/._pnpm-workspace.ts +0 -0
  51. package/src/._project-js.ts +0 -0
  52. package/src/._project-predicate.ts +0 -0
  53. package/src/._project-py.ts +0 -0
  54. package/src/._project-rs.ts +0 -0
  55. package/src/._project.ts +0 -0
  56. package/src/._projen-version.ts +0 -0
  57. package/src/._publish.ts +0 -0
  58. package/src/._release-catalog.ts +0 -0
  59. package/src/._release-dispatch.ts +0 -0
  60. package/src/._release-github.ts +0 -0
  61. package/src/._release.ts +0 -0
  62. package/src/._scaffold.ts +0 -0
  63. package/src/._tags.ts +0 -0
  64. package/src/._tsconfig.ts +0 -0
  65. package/src/._uniffi.ts +0 -0
  66. package/src/._vscode.ts +0 -0
  67. package/src/._watch.ts +0 -0
  68. package/src/._workspace-lock.ts +0 -0
  69. package/src/._workspace-version.ts +0 -0
  70. package/src/_release-platform.ts +0 -78
  71. package/src/_rust-project.ts +0 -429
  72. package/src/_rust-release-workflow.ts +0 -311
  73. package/src/project-rs.ts +0 -1054
  74. package/src/release-github.ts +0 -109
  75. package/src/uniffi.ts +0 -65
  76. package/tasks/._barrels.ts +0 -0
  77. package/tasks/._bump.ts +0 -0
  78. package/tasks/._clean.ts +0 -0
  79. package/tasks/._compile-workspace.ts +0 -0
  80. package/tasks/._local-publish.ts +0 -0
  81. package/tasks/._openapi.ts +0 -0
  82. package/tasks/._projenrc.ts +0 -0
  83. package/tasks/._publish-npm.ts +0 -0
  84. package/tasks/._publish-python.ts +0 -0
  85. package/tasks/._publish-uniffi-local.ts +0 -0
  86. package/tasks/._publish.ts +0 -0
  87. package/tasks/._python-registry.ts +0 -0
  88. package/tasks/._release-assets.ts +0 -0
  89. package/tasks/._release-candidate.ts +0 -0
  90. package/tasks/._release-manifest.ts +0 -0
  91. package/tasks/._release-pr.ts +0 -0
  92. package/tasks/._release-summary.ts +0 -0
  93. package/tasks/._release-version.ts +0 -0
  94. package/tasks/._rust.ts +0 -0
  95. package/tasks/._sync.ts +0 -0
  96. package/tasks/._uniffi-python.d.ts +0 -0
  97. package/tasks/._uniffi-python.js +0 -0
  98. package/tasks/._uniffi-release.mjs +0 -0
  99. package/tasks/._uniffi.ts +0 -0
  100. package/tasks/._version-check.ts +0 -0
  101. package/tasks/publish-uniffi-local.ts +0 -359
  102. package/tasks/release-assets.ts +0 -467
  103. package/tasks/release-candidate.ts +0 -416
  104. package/tasks/release-manifest.ts +0 -273
  105. package/tasks/release-pr.ts +0 -589
  106. package/tasks/release-summary.ts +0 -379
  107. package/tasks/rust.ts +0 -173
  108. package/tasks/uniffi-python.d.ts +0 -60
  109. package/tasks/uniffi-python.js +0 -178
  110. package/tasks/uniffi-release.mjs +0 -579
  111. package/tasks/uniffi.ts +0 -262
package/src/project-py.ts CHANGED
@@ -6,18 +6,16 @@ import { JobPermission } from "projen/lib/github/workflows-model";
6
6
  import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
7
7
  import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
8
8
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
9
+ import { hasWorkspaceNodePackage } from "./python-node-bindings.ts";
10
+ import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
9
11
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
12
+ import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
10
13
  import {
11
- RELEASE_SHA,
12
- RELEASE_TAG,
13
- RELEASE_VERSION,
14
- releaseSourceSteps,
15
- } from "./release-dispatch.ts";
16
- import {
14
+ refreshReleaseDocsDependencies,
17
15
  releaseTagPattern,
18
16
  releasePublishCondition,
19
- releaseStageCondition,
20
17
  tryReleaseWorkflow,
18
+ uvSetupStep,
21
19
  } from "./release.ts";
22
20
  import { readWorkspaceVersion } from "./workspace-version.ts";
23
21
 
@@ -33,13 +31,15 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
33
31
  readonly directory: string;
34
32
  readonly name?: string;
35
33
  readonly module?: string;
34
+ /** Python import root. Defaults to `generated-src` for generated packages, otherwise `src`. */
35
+ readonly moduleRoot?: string;
36
36
  readonly description: string;
37
37
  readonly dependencies?: readonly string[];
38
38
  /** Workspace package directories rendered as standalone Git dependencies. */
39
39
  readonly internalDependencies?: readonly string[];
40
40
  readonly scripts?: Readonly<Record<string, string>>;
41
- /** Publish this package through the Rust UniFFI release flow instead of the Python workflow. */
42
- readonly uniffi?: boolean;
41
+ /** Build-time Node package embedded through PythonMonkey. */
42
+ readonly nodeBindings?: PythonNodeBindingsOptions;
43
43
  /** Generated source files excluded from strict static analysis. Package-relative. */
44
44
  readonly generatedSources?: readonly string[];
45
45
  /** Trusted publisher used outside the standard Python release workflow. */
@@ -193,7 +193,8 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
193
193
  uv: {
194
194
  buildBackend: {
195
195
  moduleName: pkg.module,
196
- moduleRoot: "src",
196
+ moduleRoot:
197
+ pkg.moduleRoot ?? (pkg.nodeBindings?.layout === "package" ? "generated-src" : "src"),
197
198
  namespace: true,
198
199
  },
199
200
  },
@@ -210,8 +211,24 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
210
211
  if (pkg.scripts) {
211
212
  this.uv.file.addOverride("project.scripts", pkg.scripts);
212
213
  }
213
- if (pkg.uniffi !== undefined) {
214
- this.uv.file.addOverride("tool.dbx_tools.config.uniffi", pkg.uniffi);
214
+ if (pkg.nodeBindings) {
215
+ this.uv.file.addOverride("tool.dbx_tools.node_bindings", {
216
+ package: pkg.nodeBindings.package,
217
+ ...(pkg.nodeBindings.entrypoint ? { entrypoint: pkg.nodeBindings.entrypoint } : {}),
218
+ ...(pkg.nodeBindings.layout ? { layout: pkg.nodeBindings.layout } : {}),
219
+ ...(pkg.nodeBindings.private ? { private: true } : {}),
220
+ ...(pkg.nodeBindings.shimRoot ? { shim_root: pkg.nodeBindings.shimRoot } : {}),
221
+ ...(pkg.nodeBindings.functionOverrides?.length
222
+ ? {
223
+ function_overrides: pkg.nodeBindings.functionOverrides.map((override) => ({
224
+ module: override.module,
225
+ export: override.export,
226
+ handler: override.handler,
227
+ ...(override.handlerExport ? { handler_export: override.handlerExport } : {}),
228
+ })),
229
+ }
230
+ : {}),
231
+ });
215
232
  }
216
233
  this.uv.file.readonly = true;
217
234
 
@@ -293,6 +310,14 @@ export class DBXToolsPythonWorkspace extends Component {
293
310
  : this.version,
294
311
  }),
295
312
  );
313
+ for (const pkg of this.packages) {
314
+ if (!pkg.packageOptions.nodeBindings) continue;
315
+ new PythonNodeBundle(project, {
316
+ name: pkg.packageOptions.directory,
317
+ projectDirectory: pythonPackagePath(this.repository, pkg.packageOptions.directory),
318
+ watch: hasWorkspaceNodePackage(project, pkg.packageOptions.nodeBindings.package),
319
+ });
320
+ }
296
321
  if (isDBXToolsJavaScriptProject()(project)) {
297
322
  for (const pkg of this.packages) {
298
323
  project.releaseCatalog.registerProject(pkg, {
@@ -314,7 +339,7 @@ export class DBXToolsPythonWorkspace extends Component {
314
339
  return {
315
340
  target: dependency.name,
316
341
  kind: "runtime" as const,
317
- requirement: pythonReleaseRequirement(version, dependency.uniffi === true),
342
+ requirement: pythonReleaseRequirement(version),
318
343
  propagation: "outside-range" as const,
319
344
  publishOrder: true,
320
345
  internal: true,
@@ -365,7 +390,9 @@ export class DBXToolsPythonWorkspace extends Component {
365
390
 
366
391
  private emitWorkspace(
367
392
  project: javascript.NodeProject,
368
- options: DBXToolsPythonWorkspaceOptions,
393
+ options: Omit<DBXToolsPythonWorkspaceOptions, "packages"> & {
394
+ readonly packages: readonly ResolvedPythonPackageOptions[];
395
+ },
369
396
  scope: string,
370
397
  ): python.PyprojectTomlFile {
371
398
  const testPaths = options.testPaths ?? [this.repository.root];
@@ -409,7 +436,7 @@ export class DBXToolsPythonWorkspace extends Component {
409
436
  const projectExcludes = [
410
437
  ...(options.pyreflyProjectExcludes ?? []),
411
438
  ...options.packages.flatMap((pkg) =>
412
- (pkg.generatedSources ?? []).map(
439
+ [...(pkg.generatedSources ?? []), ...pythonNodeGeneratedSources(pkg)].map(
413
440
  (source) => `${this.repository.root}/${pkg.directory}/${source}`,
414
441
  ),
415
442
  ),
@@ -452,71 +479,12 @@ export class DBXToolsPythonWorkspace extends Component {
452
479
  private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
453
480
  if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
454
481
  const publications = this.publications(options);
455
- const uniffiPublications = this.uniffiPublications(options);
456
- const allPublications = [...publications, ...uniffiPublications];
482
+ const allPublications = publications;
457
483
  if (allPublications.length === 0) return;
458
484
  const workflow = tryReleaseWorkflow(project);
459
485
  if (!workflow) {
460
486
  throw new Error("Python release requires the root dbx-tools release mode");
461
487
  }
462
- workflow.addJob("build-python", {
463
- if: releaseStageCondition("python", ["needs.verify-context.result == 'success'"]),
464
- needs: ["verify-context"],
465
- runsOn: ["ubuntu-latest"],
466
- permissions: { contents: JobPermission.READ },
467
- timeoutMinutes: 20,
468
- env: { BUN_VERSION },
469
- steps: [
470
- ...releaseSourceSteps(),
471
- ...bunCacheRestoreSteps(project, {
472
- ignorePaths: project.workflowCacheIgnorePaths,
473
- }),
474
- { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
475
- { name: "Install release helpers", run: "bun install" },
476
- bunCacheSaveStep(),
477
- {
478
- name: "Download approved Python distributions",
479
- env: { GH_TOKEN: "${{ github.token }}", RELEASE_TAG },
480
- run: [
481
- "rm -rf dist/release-download",
482
- "mkdir -p dist/release-download",
483
- 'gh release download "$RELEASE_TAG" --pattern release-manifest.json --pattern SHA256SUMS --pattern "*.whl" --pattern "*.tar.gz" --dir dist/release-download',
484
- ].join("\n"),
485
- },
486
- ...allPublications.map((publication) => ({
487
- name: `Select ${publication.distribution} distributions`,
488
- env: {
489
- PACKAGE_NAME: publication.distribution.replaceAll("_", "-").toLowerCase(),
490
- RELEASE_SHA,
491
- RELEASE_TAG,
492
- RELEASE_VERSION,
493
- },
494
- run: [
495
- "bun node_modules/@dbx-tools/projen/tasks/release-manifest.ts verify \\",
496
- " --directory dist/release-download \\",
497
- ' --tag "$RELEASE_TAG" \\',
498
- ' --sha "$RELEASE_SHA" \\',
499
- ' --version "$RELEASE_VERSION" \\',
500
- " --kind pypi \\",
501
- ' --package "$PACKAGE_NAME" \\',
502
- ` --output dist/${publication.directory}`,
503
- ].join("\n"),
504
- })),
505
- {
506
- name: "Validate approved distributions",
507
- run: `find ${allPublications.map((publication) => `dist/${publication.directory}`).join(" ")} -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check`,
508
- },
509
- {
510
- name: "Upload distributions",
511
- uses: "actions/upload-artifact@v7",
512
- with: {
513
- name: "python-distributions",
514
- path: allPublications.map((publication) => `dist/${publication.directory}`).join("\n"),
515
- "retention-days": 7,
516
- },
517
- },
518
- ],
519
- });
520
488
  for (const publication of allPublications) {
521
489
  const dependencyJobs = (publication.dependencies ?? []).map(
522
490
  (dependency) => `publish-pypi-${dependency}`,
@@ -524,10 +492,9 @@ export class DBXToolsPythonWorkspace extends Component {
524
492
  workflow.addJob(`publish-pypi-${publication.directory}`, {
525
493
  if: releasePublishCondition("python", [
526
494
  "needs.verify-context.result == 'success'",
527
- "needs.build-python.result == 'success'",
528
495
  ...dependencyJobs.map((job) => `needs['${job}'].result == 'success'`),
529
496
  ]),
530
- needs: ["verify-context", "build-python", ...dependencyJobs],
497
+ needs: ["verify-context", ...dependencyJobs],
531
498
  environment: {
532
499
  name: publication.environment,
533
500
  url:
@@ -537,11 +504,24 @@ export class DBXToolsPythonWorkspace extends Component {
537
504
  runsOn: ["ubuntu-latest"],
538
505
  permissions: { idToken: JobPermission.WRITE },
539
506
  timeoutMinutes: 10,
507
+ env: { BUN_VERSION },
540
508
  steps: [
509
+ ...releaseSourceSteps(),
510
+ ...bunCacheRestoreSteps(project, {
511
+ ignorePaths: project.workflowCacheIgnorePaths,
512
+ }),
513
+ uvSetupStep(),
514
+ { name: "Install release helpers", run: "bun install --frozen-lockfile" },
515
+ bunCacheSaveStep(),
541
516
  {
542
- name: "Download distributions",
543
- uses: "actions/download-artifact@v8",
544
- with: { name: "python-distributions", path: "dist" },
517
+ name: `Build ${publication.distribution} distributions`,
518
+ env: { RELEASE_VERSION },
519
+ run: [
520
+ 'bun node_modules/@dbx-tools/projen/tasks/publish-python.ts "$RELEASE_VERSION"',
521
+ `--root ${JSON.stringify(this.repository.root)}`,
522
+ `--package ${JSON.stringify(publication.directory)}`,
523
+ `--output dist/${publication.directory}`,
524
+ ].join(" \\\n "),
545
525
  },
546
526
  {
547
527
  name: `Publish ${publication.distribution} to PyPI`,
@@ -554,32 +534,17 @@ export class DBXToolsPythonWorkspace extends Component {
554
534
  ],
555
535
  });
556
536
  }
537
+ refreshReleaseDocsDependencies(project);
557
538
  }
558
539
 
559
540
  private publications(options: PythonReleaseOptions): readonly PythonPublication[] {
560
- return this.packages
561
- .filter((pkg) => pkg.packageOptions.uniffi !== true)
562
- .map((pkg) => ({
563
- directory: pkg.packageOptions.directory,
564
- distribution: pkg.packageOptions.name,
565
- environment:
566
- options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
567
- }));
568
- }
569
-
570
- private uniffiPublications(options: PythonReleaseOptions): readonly PythonPublication[] {
571
- return this.packages
572
- .filter((pkg) => pkg.packageOptions.uniffi === true)
573
- .map((pkg) => ({
574
- directory: pkg.packageOptions.directory,
575
- distribution: pkg.packageOptions.name,
576
- environment:
577
- pkg.packageOptions.trustedPublisher?.environment ??
578
- options.environments?.[pkg.packageOptions.name] ??
579
- `pypi-${pkg.packageOptions.name}`,
580
- artifacts: pkg.packageOptions.trustedPublisher?.artifacts,
581
- dependencies: pkg.packageOptions.internalDependencies,
582
- }));
541
+ return this.packages.map((pkg) => ({
542
+ directory: pkg.packageOptions.directory,
543
+ distribution: pkg.packageOptions.name,
544
+ environment:
545
+ options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
546
+ dependencies: pkg.packageOptions.internalDependencies,
547
+ }));
583
548
  }
584
549
 
585
550
  private trustedPublisherPublications(
@@ -723,3 +688,17 @@ export class DBXToolsPythonWorkspace extends Component {
723
688
  return { owner: match[1], name: match[2] };
724
689
  }
725
690
  }
691
+
692
+ function pythonNodeGeneratedSources(pkg: ResolvedPythonPackageOptions): string[] {
693
+ if (!pkg.nodeBindings) return [];
694
+ const moduleRoot =
695
+ pkg.moduleRoot ?? (pkg.nodeBindings.layout === "package" ? "generated-src" : "src");
696
+ const moduleDirectory = `${moduleRoot}/${pkg.module.replaceAll(".", "/")}`;
697
+ const generatedDirectory =
698
+ pkg.nodeBindings.layout === "package" ? moduleDirectory : `${moduleDirectory}/_generated`;
699
+ return [
700
+ `${generatedDirectory}/_runtime.js`,
701
+ `${generatedDirectory}/node_bindings.py`,
702
+ `${generatedDirectory}/__init__.py`,
703
+ ];
704
+ }
package/src/project.ts CHANGED
@@ -12,10 +12,10 @@ import * as projectPredicate from "./project-predicate.ts";
12
12
 
13
13
  export * from "./project-js.ts";
14
14
  export * from "./project-py.ts";
15
- export * from "./project-rs.ts";
15
+ export * from "./python-node-bundle.ts";
16
16
 
17
17
  /** Runtime family implemented by a dbx-tools project. */
18
- export type DBXToolsProjectLanguage = "javascript" | "python" | "rust";
18
+ export type DBXToolsProjectLanguage = "javascript" | "python";
19
19
 
20
20
  /** Options shared by every dbx-tools project implementation. */
21
21
  export interface DBXToolsProjectOptions extends Partial<
@@ -0,0 +1,138 @@
1
+ /** Shared pyproject-backed configuration for PythonMonkey Node bindings. */
2
+ import { readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { object } from "@dbx-tools/shared-core";
5
+ import type { Project } from "projen";
6
+ import { parse } from "smol-toml";
7
+ import { workspaceDependencyDirectories } from "./packages.ts";
8
+
9
+ export interface ResolvedPythonNodeFunctionOverride {
10
+ readonly handlerExport: string;
11
+ readonly handlerFile: string;
12
+ readonly targetExport: string;
13
+ readonly targetModule: string;
14
+ }
15
+
16
+ export interface ResolvedPythonNodeBindings {
17
+ readonly bindingsPackageOutput: string;
18
+ readonly bindingsOutput: string;
19
+ readonly functionOverrides: readonly ResolvedPythonNodeFunctionOverride[];
20
+ readonly entrypoint: string;
21
+ readonly moduleDirectory: string;
22
+ readonly layout: "package" | "submodule";
23
+ readonly private: boolean;
24
+ readonly package: string;
25
+ readonly project: string;
26
+ readonly projectDirectory: string;
27
+ readonly pyproject: string;
28
+ readonly runtimeOutput: string;
29
+ readonly shimRoot?: string;
30
+ readonly workspaceDirectories: readonly string[];
31
+ }
32
+
33
+ /** Resolve one Python package's Node binding configuration and conventional outputs. */
34
+ export function resolvePythonNodeBindings(
35
+ root: string,
36
+ project: string,
37
+ ): ResolvedPythonNodeBindings {
38
+ const projectDirectory = resolve(root, project);
39
+ const pyproject = resolve(projectDirectory, "pyproject.toml");
40
+ const manifest = record(parse(readFileSync(pyproject, "utf8")), "pyproject.toml");
41
+ const tool = record(manifest.tool, "tool");
42
+ const dbxTools = record(tool.dbx_tools, "tool.dbx_tools");
43
+ const bindings = record(dbxTools.node_bindings, "tool.dbx_tools.node_bindings");
44
+ const uv = record(tool.uv, "tool.uv");
45
+ const backend = record(uv["build-backend"], "tool.uv.build-backend");
46
+ const packageName = requiredString(bindings.package, "tool.dbx_tools.node_bindings.package");
47
+ const entrypoint =
48
+ optionalString(bindings.entrypoint, "tool.dbx_tools.node_bindings.entrypoint") ?? packageName;
49
+ const layout = nodeBindingsLayout(bindings.layout);
50
+ const moduleName = requiredString(backend["module-name"], "tool.uv.build-backend.module-name");
51
+ const moduleRoot = requiredString(backend["module-root"], "tool.uv.build-backend.module-root");
52
+ const configuredOverrides = bindings.function_overrides ?? [];
53
+ if (!Array.isArray(configuredOverrides)) {
54
+ throw new Error("tool.dbx_tools.node_bindings.function_overrides must be an array");
55
+ }
56
+ const moduleDirectory = resolve(projectDirectory, moduleRoot, ...moduleName.split("."));
57
+ const generatedDirectory =
58
+ layout === "package" ? moduleDirectory : join(moduleDirectory, "_generated");
59
+ const shimRoot = optionalString(bindings.shim_root, "tool.dbx_tools.node_bindings.shim_root");
60
+ return {
61
+ project,
62
+ projectDirectory,
63
+ pyproject,
64
+ package: packageName,
65
+ entrypoint,
66
+ layout,
67
+ private: optionalBoolean(bindings.private, "tool.dbx_tools.node_bindings.private") ?? false,
68
+ moduleDirectory,
69
+ runtimeOutput: join(generatedDirectory, "_runtime.js"),
70
+ bindingsOutput: join(generatedDirectory, "node_bindings.py"),
71
+ bindingsPackageOutput: join(generatedDirectory, "__init__.py"),
72
+ ...(shimRoot ? { shimRoot: resolve(root, shimRoot) } : {}),
73
+ functionOverrides: configuredOverrides.map((candidate, index) => {
74
+ const path = `tool.dbx_tools.node_bindings.function_overrides[${index}]`;
75
+ const override = record(candidate, path);
76
+ const targetExport = requiredString(override.export, `${path}.export`);
77
+ return {
78
+ targetModule: requiredString(override.module, `${path}.module`),
79
+ targetExport,
80
+ handlerFile: resolve(root, requiredString(override.handler, `${path}.handler`)),
81
+ handlerExport:
82
+ optionalString(override.handler_export, `${path}.handler_export`) ?? targetExport,
83
+ };
84
+ }),
85
+ workspaceDirectories: workspaceDependencyDirectories(packageName, root),
86
+ };
87
+ }
88
+
89
+ function nodeBindingsLayout(value: unknown): "package" | "submodule" {
90
+ if (value === undefined) return "submodule";
91
+ if (value === "package" || value === "submodule") return value;
92
+ throw new Error("tool.dbx_tools.node_bindings.layout must be package or submodule");
93
+ }
94
+
95
+ /** All paths that can change a workspace-backed generated Node runtime. */
96
+ export function pythonNodeBindingWatchInputs(config: ResolvedPythonNodeBindings): string[] {
97
+ if (config.workspaceDirectories.length === 0) {
98
+ throw new Error(`${config.package} is not a workspace package and cannot be watched`);
99
+ }
100
+ return [
101
+ config.pyproject,
102
+ ...config.workspaceDirectories,
103
+ ...(config.shimRoot ? [config.shimRoot] : []),
104
+ ...config.functionOverrides.map(({ handlerFile }) => handlerFile),
105
+ ];
106
+ }
107
+
108
+ /** Whether a Projen tree already contains the configured Node package. */
109
+ export function hasWorkspaceNodePackage(project: Project, packageName: string): boolean {
110
+ return (
111
+ project.name === packageName || project.subprojects.some((child) => child.name === packageName)
112
+ );
113
+ }
114
+
115
+ function record(value: unknown, path: string): Record<string, unknown> {
116
+ if (!object.isRecord(value)) throw new Error(`${path} must be a table`);
117
+ return value;
118
+ }
119
+
120
+ function requiredString(value: unknown, path: string): string {
121
+ const parsed = optionalString(value, path);
122
+ if (!parsed) throw new Error(`${path} must be a non-empty string`);
123
+ return parsed;
124
+ }
125
+
126
+ function optionalString(value: unknown, path: string): string | undefined {
127
+ if (value === undefined) return undefined;
128
+ if (typeof value !== "string" || !value.trim()) {
129
+ throw new Error(`${path} must be a non-empty string`);
130
+ }
131
+ return value.trim();
132
+ }
133
+
134
+ function optionalBoolean(value: unknown, path: string): boolean | undefined {
135
+ if (value === undefined) return undefined;
136
+ if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`);
137
+ return value;
138
+ }
@@ -0,0 +1,92 @@
1
+ import { Component, type Project, type Task } from "projen";
2
+ import { taskScript } from "./project-js.ts";
3
+ import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
4
+
5
+ /** One function replaced only in a generated PythonMonkey bundle. */
6
+ export interface PythonNodeFunctionOverride {
7
+ /** Module specifier imported by the ordinary Node source. */
8
+ readonly module: string;
9
+ /** Named export to replace. */
10
+ readonly export: string;
11
+ /** Repository-relative TypeScript file containing the replacement. */
12
+ readonly handler: string;
13
+ /** Named export in {@link handler}. Defaults to {@link export}. */
14
+ readonly handlerExport?: string;
15
+ }
16
+
17
+ /** Node package embedded into one Python distribution through PythonMonkey. */
18
+ export interface PythonNodeBindingsOptions {
19
+ /** Public Node package specifier, such as `@dbx-tools/auth`. */
20
+ readonly package: string;
21
+ /** Optional portable package subpath to bind. Defaults to {@link package}. */
22
+ readonly entrypoint?: string;
23
+ /** Generated bindings own the package or live under `_generated`. Defaults to `submodule`. */
24
+ readonly layout?: "package" | "submodule";
25
+ /** Keep generated symbols out of the generated package `__init__`. Defaults to false. */
26
+ readonly private?: boolean;
27
+ /** Repository-relative directory containing Node built-in shims. */
28
+ readonly shimRoot?: string;
29
+ /** Functions replaced only while generating the Python runtime. */
30
+ readonly functionOverrides?: readonly PythonNodeFunctionOverride[];
31
+ }
32
+
33
+ /** Options for pyproject-driven PythonMonkey generation tasks. */
34
+ export interface PythonNodeBundleOptions {
35
+ /** Stable task-name prefix, such as `auth`. */
36
+ readonly name: string;
37
+ /** Repository-relative Python project containing `pyproject.toml`. */
38
+ readonly projectDirectory: string;
39
+ /** Add the freshness check to the project's test task. Defaults to true. */
40
+ readonly test?: boolean;
41
+ /** Regenerate during the root workspace watch loop. Defaults to false. */
42
+ readonly watch?: boolean;
43
+ }
44
+
45
+ /** Owns paired generation and freshness-check tasks for one Python package. */
46
+ export class PythonNodeBundle extends Component {
47
+ readonly buildTask: Task;
48
+ readonly checkTask: Task;
49
+ readonly watchTask?: Task;
50
+
51
+ constructor(project: Project, options: PythonNodeBundleOptions) {
52
+ super(project);
53
+ const command = taskScript(
54
+ project,
55
+ "python-node-bindings.ts",
56
+ `--project ${shellQuote(options.projectDirectory)}`,
57
+ );
58
+ this.buildTask = project.addTask(`${options.name}:python-runtime`, {
59
+ description: `Generate Node bindings for Python package ${options.name}`,
60
+ exec: command,
61
+ });
62
+ this.checkTask = project.addTask(`${options.name}:python-runtime:check`, {
63
+ description: `Verify generated Node bindings for Python package ${options.name}`,
64
+ exec: `${command} --check`,
65
+ });
66
+ if (options.test ?? true) project.testTask.spawn(this.checkTask);
67
+ if (options.watch) {
68
+ this.watchTask = project.addTask(`${options.name}:python-runtime:watch`, {
69
+ description: `Regenerate Node bindings for Python package ${options.name} on changes`,
70
+ exec: taskScript(
71
+ project,
72
+ "python-node-bindings-watch.ts",
73
+ `--project ${shellQuote(options.projectDirectory)}`,
74
+ ),
75
+ });
76
+ if (isDBXToolsJavaScriptProject()(project)) {
77
+ const configured = project.dbxToolsConfig.pythonNodeBindings;
78
+ const projects = Array.isArray(configured)
79
+ ? configured.filter((value): value is string => typeof value === "string")
80
+ : [];
81
+ project.dbxToolsConfig.pythonNodeBindings = [
82
+ ...new Set([...projects, options.projectDirectory]),
83
+ ];
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ function shellQuote(value: string): string {
90
+ if (/^[A-Za-z0-9_./:@=+#-]+$/.test(value)) return value;
91
+ return `'${value.replaceAll("'", `'\\''`)}'`;
92
+ }
@@ -17,10 +17,10 @@ import { toPosix } from "./packages.ts";
17
17
  import { readWorkspaceVersion } from "./workspace-version.ts";
18
18
 
19
19
  /** Runtime family represented by a release project. */
20
- export type ReleaseProjectLanguage = "javascript" | "python" | "rust";
20
+ export type ReleaseProjectLanguage = "javascript" | "python";
21
21
 
22
22
  /** Publishable output owned by one release unit. */
23
- export type ReleaseArtifactKind = "npm" | "pypi" | "cargo" | "github-binary" | "documentation";
23
+ export type ReleaseArtifactKind = "npm" | "pypi" | "documentation";
24
24
 
25
25
  /** Relationship between two projects or release units. */
26
26
  export type ReleaseEdgeKind =
@@ -172,8 +172,6 @@ const IGNORED_DIRECTORIES = new Set([
172
172
  ]);
173
173
 
174
174
  const IGNORED_FILES = new Set([
175
- "Cargo.lock",
176
- "Cargo.toml",
177
175
  "package.json",
178
176
  "pyproject.toml",
179
177
  "uv.lock",
@@ -184,7 +182,7 @@ const IGNORED_FILES = new Set([
184
182
 
185
183
  /** Stable default component id for one publishable project. */
186
184
  export function defaultReleaseUnitId(language: ReleaseProjectLanguage, identity: string): string {
187
- const prefix = language === "javascript" ? "node" : language === "python" ? "python" : "rs";
185
+ const prefix = language === "javascript" ? "node" : "python";
188
186
  const unscoped = identity.includes("/")
189
187
  ? identity.slice(identity.lastIndexOf("/") + 1)
190
188
  : identity;
@@ -4,17 +4,6 @@ import type { JobStep } from "projen/lib/github/workflows-model";
4
4
  export const RELEASE_TAG = "${{ needs.verify-context.outputs.release_tag }}";
5
5
  export const RELEASE_SHA = "${{ needs.verify-context.outputs.expected_sha }}";
6
6
  export const RELEASE_VERSION = "${{ needs.verify-context.outputs.release_version }}";
7
- export const RELEASE_SUMMARY_DIRECTORY = "docs/releases";
8
- export const RELEASE_SUMMARY_PROVIDER_NAMES = ["cursor", "codex", "claude"] as const;
9
- export type ReleaseSummaryProviderName = (typeof RELEASE_SUMMARY_PROVIDER_NAMES)[number];
10
-
11
- /** Immutable repository path for one version's release summary. */
12
- export function releaseSummaryFile(version: string, component?: string): string {
13
- const prefix = component ? `${component.replace(/[^a-zA-Z0-9-]+/g, "-")}-` : "";
14
- return `${RELEASE_SUMMARY_DIRECTORY}/${prefix}v${version.replace(/^v/, "")}.md`;
15
- }
16
-
17
- export const RELEASE_SUMMARY_FILE = releaseSummaryFile(RELEASE_VERSION);
18
7
 
19
8
  /** Check out and verify the immutable commit selected by the release tag. */
20
9
  export function releaseSourceSteps(): readonly JobStep[] {