@dbx-tools/projen 0.9.32 → 0.9.36

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 (55) hide show
  1. package/README.md +71 -65
  2. package/index.ts +5 -8
  3. package/package.json +8 -7
  4. package/src/_task-command.ts +0 -75
  5. package/src/barrels.ts +15 -23
  6. package/src/bun-app.ts +5 -6
  7. package/src/clean.ts +41 -15
  8. package/src/codegen.ts +2 -3
  9. package/src/dbx-tools-config.ts +23 -64
  10. package/src/generated.ts +7 -1
  11. package/src/module-exports.ts +35 -3
  12. package/src/openapi.ts +34 -38
  13. package/src/packages.ts +3 -30
  14. package/src/pnpm-workspace.ts +21 -26
  15. package/src/project-js.ts +152 -365
  16. package/src/project-predicate.ts +4 -5
  17. package/src/project-py.ts +55 -144
  18. package/src/publish.ts +1 -1
  19. package/src/python-node-bindings.ts +140 -67
  20. package/src/python-node-bundle.ts +6 -42
  21. package/src/release-context.ts +4 -4
  22. package/src/release.ts +88 -86
  23. package/src/tags.ts +20 -18
  24. package/src/tsconfig.ts +9 -5
  25. package/src/vscode.ts +47 -58
  26. package/src/watch.ts +2 -8
  27. package/src/workspace-version.ts +1 -6
  28. package/tasks/barrels.ts +43 -38
  29. package/tasks/bump.ts +24 -20
  30. package/tasks/clean.ts +54 -50
  31. package/tasks/compile-workspace.ts +2 -117
  32. package/tasks/lib/compile-workspace.ts +119 -0
  33. package/tasks/lib/local-publish.ts +122 -0
  34. package/tasks/lib/publish-npm.ts +458 -0
  35. package/tasks/lib/publish-python.ts +195 -0
  36. package/tasks/lib/publish.ts +264 -0
  37. package/tasks/lib/python-node-bindings.ts +1262 -0
  38. package/tasks/{python-registry.ts → lib/python-registry.ts} +21 -30
  39. package/tasks/{python-release.ts → lib/python-release.ts} +1 -0
  40. package/tasks/lib/release-version.ts +66 -0
  41. package/tasks/lib/release.ts +212 -0
  42. package/tasks/local-publish.ts +4 -118
  43. package/tasks/openapi.ts +25 -21
  44. package/tasks/projenrc.ts +14 -12
  45. package/tasks/publish-npm.ts +3 -364
  46. package/tasks/publish-python.ts +3 -184
  47. package/tasks/publish.ts +3 -279
  48. package/tasks/python-node-bindings.ts +3 -1014
  49. package/tasks/release-version.ts +3 -62
  50. package/tasks/release.ts +3 -121
  51. package/tasks/sync.ts +71 -70
  52. package/tasks/version-check.ts +3 -10
  53. package/src/release-catalog.ts +0 -684
  54. package/tasks/databricks-cli-assets.ts +0 -129
  55. package/tasks/python-node-bindings-watch.ts +0 -47
@@ -9,9 +9,8 @@ import {
9
9
  } from "@dbx-tools/shared-core";
10
10
  import { IConstruct } from "constructs";
11
11
  import { Project } from "projen";
12
- import { project } from "..";
13
12
  import { toPosix } from "./packages.ts";
14
- import type { DBXToolsJavaScriptProject } from "./project-js.ts";
13
+ import { identifier, type DBXToolsJavaScriptProject } from "./project-js.ts";
15
14
  import type { DBXToolsProject } from "./project.ts";
16
15
 
17
16
  /**
@@ -77,7 +76,7 @@ export function hasIdentifierPackageName(
77
76
  ): Predicate<IConstruct, Project> {
78
77
  const matchers = projectMatchers(...patterns);
79
78
  return isProject().and((p) => {
80
- const packageName = project.identifier(p).packageName;
79
+ const packageName = identifier(p).packageName;
81
80
  return matchers.every((matcher) => matcher(packageName));
82
81
  });
83
82
  }
@@ -88,7 +87,7 @@ export function hasIdentifierName(
88
87
  ): Predicate<IConstruct, Project> {
89
88
  const matchers = projectMatchers(...patterns);
90
89
  return isProject().and((p) => {
91
- const name = project.identifier(p).name;
90
+ const name = identifier(p).name;
92
91
  return matchers.every((matcher) => matcher(name));
93
92
  });
94
93
  }
@@ -99,7 +98,7 @@ export function hasIdentifierScope(
99
98
  ): Predicate<IConstruct, Project> {
100
99
  const matchers = projectMatchers(...patterns);
101
100
  return isProject().and((p) => {
102
- const scope = project.identifier(p).scope;
101
+ const scope = identifier(p).scope;
103
102
  return scope && matchers.every((matcher) => matcher(scope));
104
103
  });
105
104
  }
package/src/project-py.ts CHANGED
@@ -1,19 +1,18 @@
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
- import { stringUtils } from "@dbx-tools/shared-core";
4
- import { Component, License, TextFile, type Project, javascript, python, vscode } from "projen";
3
+ import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
+ import { Component, License, TextFile, type Project, javascript, python } from "projen";
5
5
  import { JobPermission } from "projen/lib/github/workflows-model";
6
- import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
6
+ import { BUN_VERSION } 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";
11
9
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
10
+ import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
12
11
  import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
13
12
  import {
14
13
  refreshReleaseDocsDependencies,
14
+ releaseCondition,
15
15
  releaseTagPattern,
16
- releasePublishCondition,
17
16
  tryReleaseWorkflow,
18
17
  uvSetupStep,
19
18
  } from "./release.ts";
@@ -38,12 +37,10 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
38
37
  /** Workspace package directories rendered as standalone Git dependencies. */
39
38
  readonly internalDependencies?: readonly string[];
40
39
  readonly scripts?: Readonly<Record<string, string>>;
41
- /** Build-time Node package embedded through PythonMonkey. */
42
- readonly nodeBindings?: PythonNodeBindingsOptions;
40
+ /** One or more build-time Node packages embedded through PythonMonkey. */
41
+ readonly nodeBindings?: OneOrMany<PythonNodeBindingsOptions>;
43
42
  /** Generated source files excluded from strict static analysis. Package-relative. */
44
43
  readonly generatedSources?: readonly string[];
45
- /** Trusted publisher used outside the standard Python release workflow. */
46
- readonly trustedPublisher?: PythonTrustedPublisherOptions;
47
44
  }
48
45
 
49
46
  interface ResolvedPythonPackageOptions extends PythonPackageOptions {
@@ -51,12 +48,6 @@ interface ResolvedPythonPackageOptions extends PythonPackageOptions {
51
48
  readonly module: string;
52
49
  }
53
50
 
54
- /** GitHub Actions publisher for a Python package released by another workflow. */
55
- export interface PythonTrustedPublisherOptions {
56
- readonly environment: string;
57
- readonly artifacts?: string;
58
- }
59
-
60
51
  /** Options for one projen-native Python workspace member. */
61
52
  export interface DBXToolsPythonProjectOptions extends DBXToolsProjectOptions {
62
53
  readonly parent: Project;
@@ -78,7 +69,6 @@ interface PythonPublication {
78
69
  readonly directory: string;
79
70
  readonly distribution: string;
80
71
  readonly environment: string;
81
- readonly artifacts?: string;
82
72
  readonly dependencies?: readonly string[];
83
73
  }
84
74
 
@@ -117,32 +107,11 @@ export function pythonPackagePath(repository: PythonRepositoryOptions, directory
117
107
  return `${repository.root ?? "packages/py"}/${directory}`;
118
108
  }
119
109
 
120
- /** PEP 508 dependency pointing at a sibling package in a Git repository. */
121
- export function pythonGitDependency(
122
- repository: PythonRepositoryOptions,
123
- name: string,
124
- directory: string,
125
- ): string {
126
- return `${name} @ git+${repository.url}@${repository.ref ?? "main"}#subdirectory=${pythonPackagePath(repository, directory)}`;
127
- }
128
-
129
110
  /** Derive a dotted Python module from an npm-style scope and package directory. */
130
111
  export function pythonModuleName(scope: string, directory: string): string {
131
112
  return [scope, ...directory.split("/")].map((part) => part.replaceAll("-", "_")).join(".");
132
113
  }
133
114
 
134
- /** Published requirement for one internal Python dependency. */
135
- export function pythonReleaseRequirement(version: string, exact = false): string {
136
- if (exact) return `==${version}`;
137
- const [major, minor] = version.split(".").map(Number);
138
- const upper = major === 0 ? `0.${minor + 1}.0` : `${major + 1}.0.0`;
139
- return `>=${version},<${upper}`;
140
- }
141
-
142
- function projectVscode(project: Project): vscode.VsCode | undefined {
143
- return (project as Project & { readonly vscode?: vscode.VsCode }).vscode;
144
- }
145
-
146
115
  /** A Python package implemented with projen's `PythonProject` and uv backend. */
147
116
  export class DBXToolsPythonProject extends python.PythonProject implements DBXToolsProject {
148
117
  readonly language = "python" as const;
@@ -151,6 +120,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
151
120
 
152
121
  constructor(options: DBXToolsPythonProjectOptions) {
153
122
  const pkg = options.package;
123
+ const nodeBindings = pythonNodeBindings(pkg);
154
124
  super({
155
125
  parent: options.parent,
156
126
  outdir: pythonPackagePath(options.repository, pkg.directory),
@@ -182,6 +152,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
182
152
  licenseFiles: ["LICENSE"],
183
153
  requiresPython: options.requiresPython,
184
154
  dependencies: [...(pkg.dependencies ?? [])],
155
+ scripts: pkg.scripts,
185
156
  urls: {
186
157
  Source: `${options.repository.url.replace(/\.git$/, "")}/tree/${options.repository.ref}/${pythonPackagePath(options.repository, pkg.directory)}`,
187
158
  },
@@ -193,8 +164,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
193
164
  uv: {
194
165
  buildBackend: {
195
166
  moduleName: pkg.module,
196
- moduleRoot:
197
- pkg.moduleRoot ?? (pkg.nodeBindings?.layout === "package" ? "generated-src" : "src"),
167
+ moduleRoot: pkg.moduleRoot ?? "src",
198
168
  namespace: true,
199
169
  },
200
170
  },
@@ -206,29 +176,16 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
206
176
  throw new Error(`Expected uv packaging for ${pkg.name}`);
207
177
  }
208
178
  this.uv = this.packagingManager;
179
+ this.tasks.removeTask("publish");
180
+ this.tasks.removeTask("publish:test");
209
181
  this.uv.file.addDeletionOverride("project.authors");
210
182
  this.uv.file.addDeletionOverride("dependency-groups");
211
- if (pkg.scripts) {
212
- this.uv.file.addOverride("project.scripts", pkg.scripts);
213
- }
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
- });
183
+ if (nodeBindings.length > 0) {
184
+ const rendered = nodeBindings.map(renderPythonNodeBindings);
185
+ this.uv.file.addOverride(
186
+ "tool.dbx_tools.node_bindings",
187
+ rendered.length === 1 ? rendered[0] : rendered,
188
+ );
232
189
  }
233
190
  this.uv.file.readonly = true;
234
191
 
@@ -286,7 +243,7 @@ export class DBXToolsPythonWorkspace extends Component {
286
243
  `Python package ${pkg.directory} references unknown internal package ${directory}`,
287
244
  );
288
245
  }
289
- return pythonGitDependency(this.repository, dependency.name, dependency.directory);
246
+ return dependency.name;
290
247
  }),
291
248
  ],
292
249
  }));
@@ -301,53 +258,17 @@ export class DBXToolsPythonWorkspace extends Component {
301
258
  package: pkg,
302
259
  repository: this.repository,
303
260
  requiresPython: this.requiresPython,
304
- version: isDBXToolsJavaScriptProject()(project)
305
- ? project.releaseCatalog.versionForRegistration(
306
- "python",
307
- pkg.name,
308
- pythonPackagePath(this.repository, pkg.directory),
309
- )
310
- : this.version,
261
+ version: this.version,
311
262
  }),
312
263
  );
313
264
  for (const pkg of this.packages) {
314
- if (!pkg.packageOptions.nodeBindings) continue;
265
+ const bindings = pythonNodeBindings(pkg.packageOptions);
266
+ if (bindings.length === 0) continue;
315
267
  new PythonNodeBundle(project, {
316
268
  name: pkg.packageOptions.directory,
317
269
  projectDirectory: pythonPackagePath(this.repository, pkg.packageOptions.directory),
318
- watch: hasWorkspaceNodePackage(project, pkg.packageOptions.nodeBindings.package),
319
270
  });
320
271
  }
321
- if (isDBXToolsJavaScriptProject()(project)) {
322
- for (const pkg of this.packages) {
323
- project.releaseCatalog.registerProject(pkg, {
324
- language: "python",
325
- identity: pkg.packageOptions.name,
326
- sourcePaths: [`${pythonPackagePath(this.repository, pkg.packageOptions.directory)}/src`],
327
- dependencies: (pkg.packageOptions.internalDependencies ?? []).map((directory) => {
328
- const dependency = packagesByDirectory.get(directory);
329
- if (!dependency) {
330
- throw new Error(
331
- `Python package ${pkg.packageOptions.directory} references unknown internal package ${directory}`,
332
- );
333
- }
334
- const version = project.releaseCatalog.versionForRegistration(
335
- "python",
336
- dependency.name,
337
- pythonPackagePath(this.repository, dependency.directory),
338
- );
339
- return {
340
- target: dependency.name,
341
- kind: "runtime" as const,
342
- requirement: pythonReleaseRequirement(version),
343
- propagation: "outside-range" as const,
344
- publishOrder: true,
345
- internal: true,
346
- };
347
- }),
348
- });
349
- }
350
- }
351
272
  for (const pkg of this.packages) {
352
273
  const pyproject = `/${pythonPackagePath(this.repository, pkg.packageOptions.directory)}/pyproject.toml`;
353
274
  project.gitignore.include(pyproject);
@@ -370,7 +291,7 @@ export class DBXToolsPythonWorkspace extends Component {
370
291
 
371
292
  const interpreterPath = options.interpreterPath ?? "${workspaceFolder}/.venv/bin/python";
372
293
  if (interpreterPath !== false) {
373
- projectVscode(project)?.settings.addSetting("python.defaultInterpreterPath", interpreterPath);
294
+ project.vscode?.settings.addSetting("python.defaultInterpreterPath", interpreterPath);
374
295
  }
375
296
 
376
297
  if (options.release && this.packages.length > 0) {
@@ -383,11 +304,6 @@ export class DBXToolsPythonWorkspace extends Component {
383
304
  return pythonPackagePath(this.repository, directory);
384
305
  }
385
306
 
386
- /** PEP 508 dependency pointing at a sibling package in the configured repository. */
387
- gitDependency(name: string, directory: string): string {
388
- return pythonGitDependency(this.repository, name, directory);
389
- }
390
-
391
307
  private emitWorkspace(
392
308
  project: javascript.NodeProject,
393
309
  options: Omit<DBXToolsPythonWorkspaceOptions, "packages"> & {
@@ -490,7 +406,7 @@ export class DBXToolsPythonWorkspace extends Component {
490
406
  (dependency) => `publish-pypi-${dependency}`,
491
407
  );
492
408
  workflow.addJob(`publish-pypi-${publication.directory}`, {
493
- if: releasePublishCondition("python", [
409
+ if: releaseCondition([
494
410
  "needs.verify-context.result == 'success'",
495
411
  ...dependencyJobs.map((job) => `needs['${job}'].result == 'success'`),
496
412
  ]),
@@ -507,12 +423,8 @@ export class DBXToolsPythonWorkspace extends Component {
507
423
  env: { BUN_VERSION },
508
424
  steps: [
509
425
  ...releaseSourceSteps(),
510
- ...bunCacheRestoreSteps(project, {
511
- ignorePaths: project.workflowCacheIgnorePaths,
512
- }),
426
+ ...project.renderWorkflowSetup({ mutable: false }),
513
427
  uvSetupStep(),
514
- { name: "Install release helpers", run: "bun install --frozen-lockfile" },
515
- bunCacheSaveStep(),
516
428
  {
517
429
  name: `Build ${publication.distribution} distributions`,
518
430
  env: { RELEASE_VERSION },
@@ -547,31 +459,12 @@ export class DBXToolsPythonWorkspace extends Component {
547
459
  }));
548
460
  }
549
461
 
550
- private trustedPublisherPublications(
551
- options: PythonReleaseOptions,
552
- ): readonly PythonPublication[] {
553
- const standard = new Map(
554
- this.publications(options).map((publication) => [publication.distribution, publication]),
555
- );
556
- for (const pkg of this.packages) {
557
- const publisher = pkg.packageOptions.trustedPublisher;
558
- if (!publisher || standard.has(pkg.packageOptions.name)) continue;
559
- standard.set(pkg.packageOptions.name, {
560
- directory: pkg.packageOptions.directory,
561
- distribution: pkg.packageOptions.name,
562
- environment: publisher.environment,
563
- artifacts: publisher.artifacts,
564
- });
565
- }
566
- return [...standard.values()];
567
- }
568
-
569
462
  private addTrustedPublisherInstructionsTask(
570
463
  project: javascript.NodeProject,
571
464
  options: PythonReleaseOptions,
572
465
  ): void {
573
466
  const repository = this.githubRepository();
574
- const publications = this.trustedPublisherPublications(options);
467
+ const publications = this.publications(options);
575
468
  const releaseTag =
576
469
  isDBXToolsJavaScriptProject()(project) && tryReleaseWorkflow(project)
577
470
  ? releaseTagPattern(project)
@@ -641,7 +534,6 @@ export class DBXToolsPythonWorkspace extends Component {
641
534
  "- Workflow name: release.yml",
642
535
  `- Environment name: ${publication.environment}`,
643
536
  `- GitHub environment tag: ${releaseTag}`,
644
- ...(publication.artifacts ? [`- Artifacts: ${publication.artifacts}`] : []),
645
537
  "",
646
538
  ];
647
539
  }),
@@ -689,16 +581,35 @@ export class DBXToolsPythonWorkspace extends Component {
689
581
  }
690
582
  }
691
583
 
692
- function pythonNodeGeneratedSources(pkg: ResolvedPythonPackageOptions): string[] {
584
+ function pythonNodeBindings(
585
+ pkg: Pick<PythonPackageOptions, "nodeBindings">,
586
+ ): readonly PythonNodeBindingsOptions[] {
693
587
  if (!pkg.nodeBindings) return [];
694
- const moduleRoot =
695
- pkg.moduleRoot ?? (pkg.nodeBindings.layout === "package" ? "generated-src" : "src");
588
+ return Array.isArray(pkg.nodeBindings) ? pkg.nodeBindings : [pkg.nodeBindings];
589
+ }
590
+
591
+ function renderPythonNodeBindings(binding: PythonNodeBindingsOptions): Record<string, unknown> {
592
+ return {
593
+ package: binding.package,
594
+ ...(binding.entrypoint ? { entrypoint: binding.entrypoint } : {}),
595
+ ...(binding.modules?.length ? { modules: binding.modules } : {}),
596
+ ...(binding.functionOverrides?.length
597
+ ? {
598
+ function_overrides: binding.functionOverrides.map((override) => ({
599
+ module: override.module,
600
+ export: override.export,
601
+ handler: override.handler,
602
+ ...(override.handlerExport ? { handler_export: override.handlerExport } : {}),
603
+ })),
604
+ }
605
+ : {}),
606
+ };
607
+ }
608
+
609
+ function pythonNodeGeneratedSources(pkg: ResolvedPythonPackageOptions): string[] {
610
+ const bindings = pythonNodeBindings(pkg);
611
+ if (bindings.length === 0) return [];
612
+ const moduleRoot = pkg.moduleRoot ?? "src";
696
613
  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
- ];
614
+ return [`${moduleDirectory}/_generated/**`];
704
615
  }
package/src/publish.ts CHANGED
@@ -29,8 +29,8 @@
29
29
  */
30
30
  import type { javascript } from "projen";
31
31
  import { typescript } from "projen";
32
+ import { addPackageFiles, applyCompilerOptions, applyIncludes } from "./project-js.ts";
32
33
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
33
- import { addPackageFiles, applyCompilerOptions, applyIncludes } from "./project.ts";
34
34
 
35
35
  /** Directory `tsc` emits into, and the root of every published entry point. */
36
36
  export const COMPILED_DIR = "lib";
@@ -1,10 +1,12 @@
1
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";
2
+ import { readFileSync, rmSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { find } from "@dbx-tools/path";
6
+ import { object, stringUtils } from "@dbx-tools/shared-core";
6
7
  import { parse } from "smol-toml";
7
- import { workspaceDependencyDirectories } from "./packages.ts";
8
+ import { runTaskCommand } from "./_task-command.ts";
9
+ import { resolveRepoRoot, workspaceDependencyDirectories } from "./packages.ts";
8
10
 
9
11
  export interface ResolvedPythonNodeFunctionOverride {
10
12
  readonly handlerExport: string;
@@ -14,82 +16,142 @@ export interface ResolvedPythonNodeFunctionOverride {
14
16
  }
15
17
 
16
18
  export interface ResolvedPythonNodeBindings {
17
- readonly bindingsPackageOutput: string;
18
- readonly bindingsOutput: string;
19
+ readonly bindingDirectory: string;
20
+ readonly bindingName: string;
19
21
  readonly functionOverrides: readonly ResolvedPythonNodeFunctionOverride[];
20
22
  readonly entrypoint: string;
21
23
  readonly moduleDirectory: string;
22
- readonly layout: "package" | "submodule";
23
- readonly private: boolean;
24
+ readonly modules: readonly string[];
24
25
  readonly package: string;
25
26
  readonly project: string;
26
27
  readonly projectDirectory: string;
27
28
  readonly pyproject: string;
28
29
  readonly runtimeOutput: string;
29
- readonly shimRoot?: string;
30
30
  readonly workspaceDirectories: readonly string[];
31
31
  }
32
32
 
33
- /** Resolve one Python package's Node binding configuration and conventional outputs. */
33
+ /** Standard PythonMonkey host adapters shipped with the binding generator. */
34
+ export const PYTHON_NODE_SHIM_ROOT = resolve(
35
+ dirname(fileURLToPath(import.meta.url)),
36
+ "../shims/python-node",
37
+ );
38
+
39
+ /** Regenerate every pyproject-configured Node binding in a repository. */
40
+ export function generatePythonNodeBindings(projectRoot: string = resolveRepoRoot()): void {
41
+ const task = resolve(dirname(fileURLToPath(import.meta.url)), "../tasks/python-node-bindings.ts");
42
+ const configuredProjects = pythonNodeBindingProjects(projectRoot);
43
+ const directories = new Set<string>();
44
+ for (const project of configuredProjects) {
45
+ for (const config of resolvePythonNodeBindings(projectRoot, project)) {
46
+ directories.add(relative(projectRoot, join(config.moduleDirectory, "_generated", "node")));
47
+ }
48
+ }
49
+ for (const directory of existingBindingDirectories(projectRoot)) {
50
+ if (!directories.has(directory)) {
51
+ rmSync(resolve(projectRoot, directory), { recursive: true, force: true });
52
+ }
53
+ }
54
+ for (const project of configuredProjects) {
55
+ runTaskCommand(projectRoot, "bun", [task, "--root", projectRoot, "--project", project]);
56
+ }
57
+ }
58
+
59
+ function pythonNodeBindingProjects(projectRoot: string): string[] {
60
+ return [
61
+ ...find
62
+ .findFiles("**/pyproject.toml", { cwd: projectRoot })
63
+ .filter((file) => {
64
+ const manifest = parse(readFileSync(resolve(projectRoot, file), "utf8")) as {
65
+ tool?: { dbx_tools?: { node_bindings?: unknown } };
66
+ };
67
+ return manifest.tool?.dbx_tools?.node_bindings !== undefined;
68
+ })
69
+ .map((file) => dirname(file)),
70
+ ].sort();
71
+ }
72
+
73
+ function existingBindingDirectories(projectRoot: string): string[] {
74
+ const directories = new Set<string>();
75
+ for (const file of find.findFiles("**/_generated/node/**/*", {
76
+ cwd: projectRoot,
77
+ ignore: () => false,
78
+ })) {
79
+ const segments = file.split("/");
80
+ const generated = segments.lastIndexOf("_generated");
81
+ if (generated >= 0 && segments[generated + 1] === "node") {
82
+ directories.add(segments.slice(0, generated + 2).join("/"));
83
+ }
84
+ }
85
+ return [...directories].sort();
86
+ }
87
+
88
+ /** Resolve one Python package's Node binding configurations and conventional outputs. */
34
89
  export function resolvePythonNodeBindings(
35
90
  root: string,
36
91
  project: string,
37
- ): ResolvedPythonNodeBindings {
92
+ ): ResolvedPythonNodeBindings[] {
38
93
  const projectDirectory = resolve(root, project);
39
94
  const pyproject = resolve(projectDirectory, "pyproject.toml");
40
95
  const manifest = record(parse(readFileSync(pyproject, "utf8")), "pyproject.toml");
41
96
  const tool = record(manifest.tool, "tool");
42
97
  const dbxTools = record(tool.dbx_tools, "tool.dbx_tools");
43
- const bindings = record(dbxTools.node_bindings, "tool.dbx_tools.node_bindings");
98
+ const configuredBindings = Array.isArray(dbxTools.node_bindings)
99
+ ? dbxTools.node_bindings
100
+ : [dbxTools.node_bindings];
101
+ if (configuredBindings.length === 0) {
102
+ throw new Error("tool.dbx_tools.node_bindings must contain at least one table");
103
+ }
44
104
  const uv = record(tool.uv, "tool.uv");
45
105
  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
106
  const moduleName = requiredString(backend["module-name"], "tool.uv.build-backend.module-name");
51
107
  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
108
  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");
109
+ const configs = configuredBindings.map((candidate, bindingIndex) => {
110
+ const bindingPath = Array.isArray(dbxTools.node_bindings)
111
+ ? `tool.dbx_tools.node_bindings[${bindingIndex}]`
112
+ : "tool.dbx_tools.node_bindings";
113
+ const bindings = record(candidate, bindingPath);
114
+ const packageName = requiredString(bindings.package, `${bindingPath}.package`);
115
+ const entrypoint =
116
+ optionalString(bindings.entrypoint, `${bindingPath}.entrypoint`) ?? packageName;
117
+ const modules = optionalStrings(bindings.modules, `${bindingPath}.modules`);
118
+ if (bindings.layout !== undefined || bindings.private !== undefined) {
119
+ throw new Error(`${bindingPath} no longer supports layout or private`);
120
+ }
121
+ const configuredOverrides = bindings.function_overrides ?? [];
122
+ if (!Array.isArray(configuredOverrides)) {
123
+ throw new Error(`${bindingPath}.function_overrides must be an array`);
124
+ }
125
+ const bindingName = nodePackageName(packageName);
126
+ const bindingDirectory = join(moduleDirectory, "_generated", "node", bindingName);
127
+ return {
128
+ project,
129
+ projectDirectory,
130
+ pyproject,
131
+ package: packageName,
132
+ entrypoint,
133
+ modules,
134
+ bindingName,
135
+ bindingDirectory,
136
+ moduleDirectory,
137
+ runtimeOutput: join(moduleDirectory, "_generated", "node", "_runtime.js"),
138
+ functionOverrides: configuredOverrides.map((overrideCandidate, overrideIndex) => {
139
+ const path = `${bindingPath}.function_overrides[${overrideIndex}]`;
140
+ const override = record(overrideCandidate, path);
141
+ const targetExport = requiredString(override.export, `${path}.export`);
142
+ return {
143
+ targetModule: requiredString(override.module, `${path}.module`),
144
+ targetExport,
145
+ handlerFile: resolve(root, requiredString(override.handler, `${path}.handler`)),
146
+ handlerExport:
147
+ optionalString(override.handler_export, `${path}.handler_export`) ?? targetExport,
148
+ };
149
+ }),
150
+ workspaceDirectories: workspaceDependencyDirectories(packageName, root),
151
+ };
152
+ });
153
+ validateBindings(configs);
154
+ return configs;
93
155
  }
94
156
 
95
157
  /** All paths that can change a workspace-backed generated Node runtime. */
@@ -100,16 +162,26 @@ export function pythonNodeBindingWatchInputs(config: ResolvedPythonNodeBindings)
100
162
  return [
101
163
  config.pyproject,
102
164
  ...config.workspaceDirectories,
103
- ...(config.shimRoot ? [config.shimRoot] : []),
165
+ PYTHON_NODE_SHIM_ROOT,
104
166
  ...config.functionOverrides.map(({ handlerFile }) => handlerFile),
105
167
  ];
106
168
  }
107
169
 
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
- );
170
+ function validateBindings(configs: readonly ResolvedPythonNodeBindings[]): void {
171
+ const names = configs.map(({ bindingName }) => bindingName);
172
+ if (new Set(names).size !== names.length) {
173
+ throw new Error("tool.dbx_tools.node_bindings package names must be unique");
174
+ }
175
+ for (const config of configs) {
176
+ if (new Set(config.modules).size !== config.modules.length) {
177
+ throw new Error(`${config.package} Node binding module names must be unique`);
178
+ }
179
+ }
180
+ }
181
+
182
+ function nodePackageName(packageName: string): string {
183
+ const unscoped = packageName.split("/").at(-1) ?? packageName;
184
+ return stringUtils.toIdentifierWithOptions({ delimiter: "_" }, unscoped);
113
185
  }
114
186
 
115
187
  function record(value: unknown, path: string): Record<string, unknown> {
@@ -131,8 +203,9 @@ function optionalString(value: unknown, path: string): string | undefined {
131
203
  return value.trim();
132
204
  }
133
205
 
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;
206
+ function optionalStrings(value: unknown, path: string): string[] {
207
+ if (value === undefined) return [];
208
+ if (typeof value === "string") return [requiredString(value, path)];
209
+ if (!Array.isArray(value)) throw new Error(`${path} must be a string or array`);
210
+ return value.map((candidate, index) => requiredString(candidate, `${path}[${index}]`));
138
211
  }