@dbx-tools/projen 0.9.51 → 0.9.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,6 +135,13 @@ in consuming repositories after `@dbx-tools/projen` is installed.
135
135
  new project.DBXToolsPythonWorkspace(root, {
136
136
  root: "packages/py",
137
137
  packages: [
138
+ {
139
+ directory: "node-runtime",
140
+ name: "dbx-tools-node-runtime",
141
+ module: "dbx_tools.node_runtime",
142
+ description: "Shared PythonMonkey runtime and lazy Node.js bootstrap",
143
+ nodeRuntime: true,
144
+ },
138
145
  {
139
146
  directory: "postgres",
140
147
  description: "Python bindings for example Postgres helpers",
@@ -180,18 +187,40 @@ types with typed async methods, and primitives, arrays, maps, promises, and
180
187
  optional values retain their corresponding Python annotations.
181
188
 
182
189
  The generator resolves portable Node built-ins through standard browser
183
- polyfills. Python-host-backed adapters for process execution, files, crypto,
184
- OS values, and process state are included automatically, so packages do not
185
- configure a shim directory.
186
-
187
- The workspace creates `<name>:python-runtime`, `<name>:python-runtime:check`,
188
- and `<name>:python-runtime:watch`. The focused watcher regenerates a Python
189
- project when its bound Node package, a transitive workspace dependency, its
190
- binding configuration, a function override, or a shared shim changes.
191
- `bun run sync --watch` supervises this watcher alongside Projen and barrel
192
- generation. Full synthesis regenerates bindings and removes stale
193
- `_generated/node` trees by reconciling them with the current `pyproject.toml`
194
- files, including the resynthesis triggered by a `.projenrc.ts` change.
190
+ polyfills. Generated bundles proxy Python-host-backed built-ins to
191
+ `dbx-tools-node-runtime`, which owns the process, file, crypto, OS, abort,
192
+ headers, and runtime bootstrap shims. Packages do not configure or copy a shim
193
+ directory, and their generated Python loader imports the shared runtime package.
194
+
195
+ `nodeRuntime: true` identifies the one workspace package that owns
196
+ `build-runtime.ts`, `shims/`, the generated `runtime.js`, PythonMonkey bootstrap,
197
+ and bundle loading. Its distribution and module must be
198
+ `dbx-tools-node-runtime` and `dbx_tools.node_runtime`. Packages with
199
+ `nodeBindings` automatically depend on that workspace member. When the workspace
200
+ does not publish the runtime itself, they receive the matching external
201
+ `dbx-tools-node-runtime` dependency instead.
202
+
203
+ The workspace creates one repository-wide binding lifecycle:
204
+
205
+ - `python-node-bindings` generates every configured bridge.
206
+ - `python-node-bindings:check` verifies generated files and fails on stale
207
+ `_generated/node` directories.
208
+ - `python-node-bindings:watch` watches all configured bridges and regenerates
209
+ only Python projects affected by a Node package, transitive workspace
210
+ dependency, binding manifest, or function override change.
211
+
212
+ When a runtime owner is configured, the workspace also creates one shared
213
+ runtime lifecycle:
214
+
215
+ - `python-node-runtime` runs the package-owned `build-runtime.ts`.
216
+ - `python-node-runtime:check` verifies the committed `runtime.js`.
217
+ - `python-node-runtime:watch` watches `build-runtime.ts` and `shims/` and runs
218
+ the same package-owned builder.
219
+
220
+ `bun run sync --watch` supervises both generic watchers alongside the existing
221
+ `projenrc` and `barrels` watchers. No per-Python-package watcher tasks are
222
+ created. Full synthesis regenerates every binding and reconciles stale
223
+ `_generated/node` trees with the current `pyproject.toml` files.
195
224
  Built-in shims can make ordinary Node imports work under PythonMonkey.
196
225
  `functionOverrides` replaces named exports only in the generated runtime, so the
197
226
  Node package does not gain Python callbacks or alternate source files. A
@@ -215,7 +244,7 @@ subset of a Git repository instead of exposing a direct URL dependency:
215
244
  include: ["driver.py", "operations/**/*.py"],
216
245
  exclude: ["**/test_*.py"],
217
246
  replace: {
218
- "from driver.": "from example.service._generated.sync.upstream_driver.",
247
+ "DEFAULT_TIMEOUT = 30": "DEFAULT_TIMEOUT = 120",
219
248
  },
220
249
  },
221
250
  ],
@@ -225,8 +254,18 @@ subset of a Git repository instead of exposing a direct URL dependency:
225
254
  Each source is written beneath
226
255
  `<python-module>/_generated/sync/<name-or-owner-repository-hash>`. The generated
227
256
  package task parses pip-style Git sources, filters files with the configured
228
- globs, verifies every replacement matched, records the options and resolved
229
- commit in `.sync.json`, and recursively marks the result read-only.
257
+ globs, applies and verifies every literal `replace` patch, records the options
258
+ and resolved commit in `.sync.json`, and recursively marks the result read-only.
259
+
260
+ Do not hard-code the generated package path in `replace`. `localizeImports`
261
+ (on by default for identifier names) parses each file with the Python AST and
262
+ rewrites absolute imports of synchronized modules, whether upstream imports them
263
+ bare (`from operations.base import X`) or under the subdirectory's dotted path
264
+ (`from src.driver.operations.base import X`). It also rewrites constant
265
+ `__import__` and `importlib.import_module` arguments. Pass a list of upstream
266
+ module names to rewrite only those (each must match), or `false` to keep upstream
267
+ imports. The import prefix comes from the same constants as the output path, so
268
+ renaming the module or generated directory flows through without config edits.
230
269
 
231
270
  Synchronization uses a check-lock-check sequence and an atomic directory
232
271
  replacement. A current pinned commit performs no network request. Branches and
@@ -390,7 +429,8 @@ file contract as the CLI.
390
429
  - `engineRoot` - engine package root resolution for bootstrapped repos.
391
430
 
392
431
  The engine registers its commands as projen tasks on the workspace root, so run
393
- them with `bun run <task>` - `sync` (add `--watch`), `barrels`, and `clean`.
432
+ them with `bun run <task>` - `sync` (add `--watch`), `barrels`, `clean`, and the
433
+ configured `python-node-bindings:*` and `python-node-runtime:*` lifecycles.
394
434
  [`@dbx-tools/cli`](../packages/js/cli/dbx-tools) is only needed to
395
435
  bootstrap a folder that has no `.projenrc.ts` or toolchain yet.
396
436
 
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.51";
6
+ export const PACKAGE_VERSION = "0.9.58";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -23,7 +23,8 @@ export * as projectPy from "./src/project-py.ts";
23
23
  export * as projenVersion from "./src/projen-version.ts";
24
24
  export * as publish from "./src/publish.ts";
25
25
  export * as pythonNodeBindings from "./src/python-node-bindings.ts";
26
- export * as pythonNodeBundle from "./src/python-node-bundle.ts";
26
+ export * as pythonNodeBindingsComponent from "./src/python-node-bindings-component.ts";
27
+ export * as pythonNodeRuntime from "./src/python-node-runtime.ts";
27
28
  export * as release from "./src/release.ts";
28
29
  export * as releaseContext from "./src/release-context.ts";
29
30
  export * as releaseOptions from "./src/release-options.ts";
@@ -39,6 +40,7 @@ export { BUN_VERSION } from "./src/bun-workflow.ts";
39
40
  export { DBXToolsConfigDataSchema, DBXToolsConfig } from "./src/dbx-tools-config.ts";
40
41
  export type { DBXToolsConfigOptions, DBXToolsConfigData } from "./src/dbx-tools-config.ts";
41
42
  export { resolvePkgRoot } from "./src/engine-root.ts";
43
+ export { PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE } from "./src/generated.ts";
42
44
  export type { HeaderOpts } from "./src/generated.ts";
43
45
  export type { ConstructsMixin } from "./src/mixin.ts";
44
46
  export type { ModuleStatement, ModuleExport, PublicFunctionExport } from "./src/module-exports.ts";
@@ -53,10 +55,11 @@ export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py
53
55
  export type { PythonRepositoryOptions, PythonPackageOptions, PythonSourceSyncOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
54
56
  export { PROJEN_VERSION } from "./src/projen-version.ts";
55
57
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
56
- export { PYTHON_NODE_SHIM_ROOT } from "./src/python-node-bindings.ts";
57
- export type { ResolvedPythonNodeFunctionOverride, ResolvedPythonNodeBindings } from "./src/python-node-bindings.ts";
58
- export { PythonNodeBundle } from "./src/python-node-bundle.ts";
59
- export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeBundleOptions } from "./src/python-node-bundle.ts";
58
+ export type { ResolvedPythonNodeFunctionOverride, ResolvedPythonNodeBindings, GeneratePythonNodeBindingsOptions } from "./src/python-node-bindings.ts";
59
+ export { PYTHON_NODE_BINDINGS_TASK, PYTHON_NODE_BINDINGS_CHECK_TASK, PYTHON_NODE_BINDINGS_WATCH_TASK, PythonNodeBindings } from "./src/python-node-bindings-component.ts";
60
+ export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeBindingsComponentOptions } from "./src/python-node-bindings-component.ts";
61
+ export { PYTHON_NODE_RUNTIME_DISTRIBUTION, PYTHON_NODE_RUNTIME_MODULE, PYTHON_NODE_RUNTIME_TASK, PYTHON_NODE_RUNTIME_CHECK_TASK, PYTHON_NODE_RUNTIME_WATCH_TASK, PythonNodeRuntime } from "./src/python-node-runtime.ts";
62
+ export type { PythonNodeRuntimeOptions } from "./src/python-node-runtime.ts";
60
63
  export { DBXToolsRelease } from "./src/release.ts";
61
64
  export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
62
65
  export { RELEASE_VERSION } from "./src/release-context.ts";
package/package.json CHANGED
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/core": "^0.9.51",
34
- "@dbx-tools/path": "^0.9.51",
35
- "@dbx-tools/shared-core": "^0.9.51",
33
+ "@dbx-tools/core": "^0.9.58",
34
+ "@dbx-tools/path": "^0.9.58",
35
+ "@dbx-tools/shared-core": "^0.9.58",
36
36
  "commander": "^15.0.0",
37
37
  "concurrently": "^10.0.3",
38
38
  "constructs": "^10.6.0",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "main": "index.ts",
56
56
  "license": "Apache-2.0",
57
- "version": "0.9.51",
57
+ "version": "0.9.58",
58
58
  "types": "index.ts",
59
59
  "type": "module",
60
60
  "exports": {
@@ -65,8 +65,7 @@
65
65
  "files": [
66
66
  "index.ts",
67
67
  "src",
68
- "tasks",
69
- "shims/python-node"
68
+ "tasks"
70
69
  ],
71
70
  "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
72
71
  }
@@ -38,6 +38,11 @@ export const DBXToolsConfigDataSchema = z
38
38
  .readonly()
39
39
  .optional()
40
40
  .describe("Additional paths that trigger watcher resynthesis."),
41
+ syncWatchTasks: z
42
+ .array(z.string().min(1))
43
+ .readonly()
44
+ .optional()
45
+ .describe("Projen tasks supervised by `sync --watch`."),
41
46
  })
42
47
  .strict()
43
48
  .readonly()
@@ -58,6 +63,8 @@ export class DBXToolsConfig {
58
63
  publishable: boolean;
59
64
  /** Additional repository paths that trigger watcher resynthesis. */
60
65
  syncResynthPaths?: string[];
66
+ /** Projen tasks supervised by `sync --watch`. */
67
+ readonly syncWatchTasks: string[] = [];
61
68
 
62
69
  constructor(
63
70
  readonly project: javascript.NodeProject,
@@ -82,6 +89,7 @@ export class DBXToolsConfig {
82
89
  ...(!this.publishable ? { publishable: false } : {}),
83
90
  ...(this.packageRoots?.length ? { packageRoots: this.packageRoots } : {}),
84
91
  ...(this.syncResynthPaths?.length ? { syncResynthPaths: this.syncResynthPaths } : {}),
92
+ ...(this.syncWatchTasks.length ? { syncWatchTasks: [...new Set(this.syncWatchTasks)] } : {}),
85
93
  });
86
94
  }
87
95
  }
package/src/generated.ts CHANGED
@@ -13,6 +13,12 @@ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
13
13
  /** First line of every banner {@link header} builds. */
14
14
  const MARKER = "// GENERATED by";
15
15
 
16
+ /** Python subpackage, under a package's module directory, that holds all toolchain output. */
17
+ export const PYTHON_GENERATED_PACKAGE = "_generated";
18
+
19
+ /** {@link PYTHON_GENERATED_PACKAGE} child holding upstream sources written by `python-sync`. */
20
+ export const PYTHON_SYNC_PACKAGE = "sync";
21
+
16
22
  const READONLY = 0o444;
17
23
  const WRITABLE = 0o644;
18
24
 
package/src/packages.ts CHANGED
@@ -280,6 +280,12 @@ export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): strin
280
280
  return Array.isArray(paths) ? stringUtils.parseList(paths.map((p) => String(p))) : [];
281
281
  }
282
282
 
283
+ /** Projen task names supervised as focused processes by `sync --watch`. */
284
+ export function syncWatchTasks(projectRoot: string = resolveRepoRoot()): string[] {
285
+ const tasks = readDbxToolsConfig(projectRoot)?.syncWatchTasks;
286
+ return Array.isArray(tasks) ? stringUtils.parseList(tasks.map((task) => String(task))) : [];
287
+ }
288
+
283
289
  /**
284
290
  * The recorded workspace members from `pnpm-workspace.yaml` (the source of truth),
285
291
  * each augmented with the `tags` read back from its `package.json`. This is what
package/src/project-py.ts CHANGED
@@ -3,10 +3,19 @@ import * as projectUtils from "@dbx-tools/core/project-utils";
3
3
  import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
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 { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
6
7
  import { LICENSE, projectRepositoryUrl, taskCommand } from "./project-js.ts";
7
8
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
8
9
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
9
- import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
10
+ import {
11
+ PythonNodeBindings,
12
+ type PythonNodeBindingsOptions,
13
+ } from "./python-node-bindings-component.ts";
14
+ import {
15
+ PYTHON_NODE_RUNTIME_DISTRIBUTION,
16
+ PYTHON_NODE_RUNTIME_MODULE,
17
+ PythonNodeRuntime,
18
+ } from "./python-node-runtime.ts";
10
19
  import {
11
20
  refreshReleaseDocsDependencies,
12
21
  releaseCondition,
@@ -33,6 +42,8 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
33
42
  readonly moduleRoot?: string;
34
43
  readonly description: string;
35
44
  readonly dependencies?: readonly string[];
45
+ /** Named PEP 621 optional dependency groups emitted under `project.optional-dependencies`. */
46
+ readonly optionalDependencies?: Readonly<Record<string, readonly string[]>>;
36
47
  /** Development dependencies emitted only in this package's dependency group. */
37
48
  readonly devDependencies?: readonly string[];
38
49
  /** Workspace package directories rendered as standalone Git dependencies. */
@@ -40,6 +51,8 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
40
51
  readonly scripts?: Readonly<Record<string, string>>;
41
52
  /** GitHub environment used to publish this distribution to PyPI. */
42
53
  readonly releaseEnvironment?: string;
54
+ /** Mark this package as the shared PythonMonkey runtime and Node shim owner. */
55
+ readonly nodeRuntime?: boolean;
43
56
  /** One or more build-time Node packages embedded through PythonMonkey. */
44
57
  readonly nodeBindings?: OneOrMany<PythonNodeBindingsOptions>;
45
58
  /** Pinned Git source subsets synchronized into this package's generated tree. */
@@ -52,7 +65,17 @@ export interface PythonSourceSyncOptions {
52
65
  readonly source: string;
53
66
  readonly include?: readonly string[];
54
67
  readonly exclude?: readonly string[];
68
+ /** Literal text patches applied to each synchronized `.py` file before import localization. */
55
69
  readonly replace?: Readonly<Record<string, string>>;
70
+ /**
71
+ * Rewrite absolute imports of synchronized modules to the generated package they now
72
+ * live in, using the Python AST. `true` detects every top-level synchronized module,
73
+ * importable either bare (`config.schema`) or under the source subdirectory's dotted
74
+ * path (`graphiti_core.driver.postgraph`). A list names the upstream modules to rewrite
75
+ * instead, and each entry must match at least one import. Defaults to `true` when the
76
+ * sync `name` is a Python identifier; set `false` to keep upstream imports unchanged.
77
+ */
78
+ readonly localizeImports?: boolean | readonly string[];
56
79
  }
57
80
 
58
81
  interface ResolvedPythonPackageOptions extends PythonPackageOptions {
@@ -231,6 +254,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
231
254
  licenseFiles: ["LICENSE"],
232
255
  requiresPython: options.requiresPython,
233
256
  dependencies: [...(pkg.dependencies ?? [])],
257
+ optionalDependencies: pkg.optionalDependencies,
234
258
  scripts: pkg.scripts,
235
259
  urls: {
236
260
  Source: `${options.repository.url.replace(/\.git$/, "")}/tree/${options.repository.ref}/${pythonPackagePath(options.repository, pkg.directory)}`,
@@ -271,7 +295,13 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
271
295
  );
272
296
  }
273
297
  if (pkg.sync?.length) {
274
- this.uv.file.addOverride("tool.dbx_tools.sync", [...pkg.sync]);
298
+ this.uv.file.addOverride(
299
+ "tool.dbx_tools.sync",
300
+ pkg.sync.map(({ localizeImports, ...sync }) => ({
301
+ ...sync,
302
+ ...(localizeImports === undefined ? {} : { localize_imports: localizeImports }),
303
+ })),
304
+ );
275
305
  }
276
306
  this.uv.file.readonly = true;
277
307
 
@@ -324,28 +354,64 @@ export class DBXToolsPythonWorkspace extends Component {
324
354
  name: pkg.name ?? `${scope}-${stringUtils.toSlug(pkg.directory)}`,
325
355
  module: pkg.module ?? pythonModuleName(scope, pkg.directory),
326
356
  }));
357
+ const configuredNodeRuntimes = packageIdentities.filter(({ nodeRuntime }) => nodeRuntime);
358
+ if (configuredNodeRuntimes.length > 1) {
359
+ throw new Error("A Python workspace can configure only one shared Node runtime package");
360
+ }
361
+ const configuredNodeRuntime = configuredNodeRuntimes[0];
362
+ if (
363
+ configuredNodeRuntime &&
364
+ (configuredNodeRuntime.name !== PYTHON_NODE_RUNTIME_DISTRIBUTION ||
365
+ configuredNodeRuntime.module !== PYTHON_NODE_RUNTIME_MODULE)
366
+ ) {
367
+ throw new Error(
368
+ `The shared Node runtime package must use distribution ${PYTHON_NODE_RUNTIME_DISTRIBUTION} ` +
369
+ `and module ${PYTHON_NODE_RUNTIME_MODULE}`,
370
+ );
371
+ }
372
+ this.version = readWorkspaceVersion(project.outdir);
327
373
  const packagesByDirectory = new Map(packageIdentities.map((pkg) => [pkg.directory, pkg]));
328
- const packages = packageIdentities.map((pkg) => ({
329
- ...pkg,
330
- dependencies: [
374
+ const nodeRuntimePackage = configuredNodeRuntime;
375
+ const packages = packageIdentities.map((pkg) => {
376
+ const usesNodeRuntime = pythonNodeBindings(pkg).length > 0;
377
+ const internalDependencies = [
378
+ ...new Set([
379
+ ...(pkg.internalDependencies ?? []),
380
+ ...(usesNodeRuntime &&
381
+ nodeRuntimePackage &&
382
+ nodeRuntimePackage.directory !== pkg.directory
383
+ ? [nodeRuntimePackage.directory]
384
+ : []),
385
+ ]),
386
+ ];
387
+ const dependencies = [
331
388
  ...(pkg.dependencies ?? []),
332
- ...(pkg.internalDependencies ?? []).map((directory) => {
389
+ ...internalDependencies.map((directory) => {
333
390
  const dependency = packagesByDirectory.get(directory);
334
391
  if (!dependency) {
335
392
  throw new Error(
336
393
  `Python package ${pkg.directory} references unknown internal package ${directory}`,
337
394
  );
338
395
  }
339
- return dependency.name;
396
+ return usesNodeRuntime && dependency.name === PYTHON_NODE_RUNTIME_DISTRIBUTION
397
+ ? `${dependency.name}==${this.version}`
398
+ : dependency.name;
340
399
  }),
341
- ],
342
- }));
400
+ ...(usesNodeRuntime && !nodeRuntimePackage
401
+ ? [`${PYTHON_NODE_RUNTIME_DISTRIBUTION}==${this.version}`]
402
+ : []),
403
+ ];
404
+ return {
405
+ ...pkg,
406
+ dependencies: [...new Set(dependencies)],
407
+ internalDependencies,
408
+ };
409
+ });
343
410
  const resolvedOptions = { ...options, packages };
344
411
  this.requiresPython = options.requiresPython ?? ">=3.10";
345
412
  this.workflowPythonVersion =
346
413
  options.workflowPythonVersion ??
347
414
  pythonVersionFromRuffTarget(options.ruffTarget ?? DEFAULT_RUFF_TARGET);
348
- this.version = readWorkspaceVersion(project.outdir);
349
415
  this.file = this.emitWorkspace(project, resolvedOptions, scope);
350
416
  this.packages = packages.map(
351
417
  (pkg) =>
@@ -357,13 +423,19 @@ export class DBXToolsPythonWorkspace extends Component {
357
423
  version: this.version,
358
424
  }),
359
425
  );
360
- for (const pkg of this.packages) {
361
- const bindings = pythonNodeBindings(pkg.packageOptions);
362
- if (bindings.length === 0) continue;
363
- new PythonNodeBundle(project, {
364
- name: pkg.packageOptions.directory,
365
- projectDirectory: pythonPackagePath(this.repository, pkg.packageOptions.directory),
426
+ const syncWatchTasks: string[] = [];
427
+ if (configuredNodeRuntime) {
428
+ const runtime = new PythonNodeRuntime(project, {
429
+ projectDirectory: pythonPackagePath(this.repository, configuredNodeRuntime.directory),
366
430
  });
431
+ syncWatchTasks.push(runtime.watchTask.name);
432
+ }
433
+ if (this.packages.some((pkg) => pythonNodeBindings(pkg.packageOptions).length > 0)) {
434
+ const bindings = new PythonNodeBindings(project);
435
+ syncWatchTasks.push(bindings.watchTask.name);
436
+ }
437
+ if (syncWatchTasks.length && isDBXToolsJavaScriptProject()(project)) {
438
+ project.dbxToolsConfig.syncWatchTasks.push(...syncWatchTasks);
367
439
  }
368
440
  for (const pkg of this.packages) {
369
441
  if (!pkg.packageOptions.sync?.length) continue;
@@ -681,7 +753,6 @@ export class DBXToolsPythonWorkspace extends Component {
681
753
  `- Repository name: ${repository.name}`,
682
754
  "- Workflow name: release.yml",
683
755
  `- Environment name: ${publication.environment}`,
684
- `- GitHub environment tag: ${releaseTag}`,
685
756
  "",
686
757
  ];
687
758
  }),
@@ -759,5 +830,5 @@ function pythonNodeGeneratedSources(pkg: ResolvedPythonPackageOptions): string[]
759
830
  if (bindings.length === 0) return [];
760
831
  const moduleRoot = pkg.moduleRoot ?? "src";
761
832
  const moduleDirectory = `${moduleRoot}/${pkg.module.replaceAll(".", "/")}`;
762
- return [`${moduleDirectory}/_generated/**`];
833
+ return [`${moduleDirectory}/${PYTHON_GENERATED_PACKAGE}/**`];
763
834
  }
package/src/project.ts CHANGED
@@ -12,7 +12,8 @@ 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 "./python-node-bundle.ts";
15
+ export * from "./python-node-bindings-component.ts";
16
+ export * from "./python-node-runtime.ts";
16
17
 
17
18
  /** Runtime family implemented by a dbx-tools project. */
18
19
  export type DBXToolsProjectLanguage = "javascript" | "python";
@@ -0,0 +1,71 @@
1
+ /** Projen task wiring for repository-wide PythonMonkey Node binding generation. */
2
+ import { Component, type Project, type Task } from "projen";
3
+ import { taskCommand } from "./project-js.ts";
4
+
5
+ /** Repository-wide task that regenerates every configured Python Node bridge. */
6
+ export const PYTHON_NODE_BINDINGS_TASK = "python-node-bindings";
7
+
8
+ /** Repository-wide task that verifies every generated Python Node bridge. */
9
+ export const PYTHON_NODE_BINDINGS_CHECK_TASK = `${PYTHON_NODE_BINDINGS_TASK}:check`;
10
+
11
+ /** Repository-wide task that watches every configured Python Node bridge. */
12
+ export const PYTHON_NODE_BINDINGS_WATCH_TASK = `${PYTHON_NODE_BINDINGS_TASK}:watch`;
13
+
14
+ /** One function replaced only in a generated PythonMonkey bundle. */
15
+ export interface PythonNodeFunctionOverride {
16
+ /** Module specifier imported by the ordinary Node source. */
17
+ readonly module: string;
18
+ /** Named export to replace. */
19
+ readonly export: string;
20
+ /** Repository-relative TypeScript file containing the replacement. */
21
+ readonly handler: string;
22
+ /** Named export in {@link handler}. Defaults to {@link export}. */
23
+ readonly handlerExport?: string;
24
+ }
25
+
26
+ /** Node package embedded into one Python distribution through PythonMonkey. */
27
+ export interface PythonNodeBindingsOptions {
28
+ /** Public Node package specifier, such as `@dbx-tools/auth`. */
29
+ readonly package: string;
30
+ /** Optional portable package subpath to bind. Defaults to {@link package}. */
31
+ readonly entrypoint?: string;
32
+ /** Automatically generated root namespace modules to bind independently. */
33
+ readonly modules?: readonly string[];
34
+ /** Functions replaced only while generating the Python runtime. */
35
+ readonly functionOverrides?: readonly PythonNodeFunctionOverride[];
36
+ }
37
+
38
+ /** Options for repository-wide PythonMonkey Node binding tasks. */
39
+ export interface PythonNodeBindingsComponentOptions {
40
+ /** Add the repository-wide freshness check to the test task. Defaults to true. */
41
+ readonly test?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Registers one build, freshness, and watch lifecycle for every configured
46
+ * `[tool.dbx_tools.node_bindings]` table in the repository.
47
+ */
48
+ export class PythonNodeBindings extends Component {
49
+ readonly buildTask: Task;
50
+ readonly checkTask: Task;
51
+ readonly watchTask: Task;
52
+
53
+ constructor(project: Project, options: PythonNodeBindingsComponentOptions = {}) {
54
+ super(project);
55
+ const command = taskCommand("python-node-bindings.ts");
56
+ this.buildTask = project.addTask(PYTHON_NODE_BINDINGS_TASK, {
57
+ description: "Generate every configured Python Node binding",
58
+ execArgs: command,
59
+ });
60
+ this.checkTask = project.addTask(PYTHON_NODE_BINDINGS_CHECK_TASK, {
61
+ description: "Verify every configured Python Node binding is current",
62
+ execArgs: [...command, "--check"],
63
+ });
64
+ this.watchTask = project.addTask(PYTHON_NODE_BINDINGS_WATCH_TASK, {
65
+ description: "Watch and regenerate affected Python Node bindings",
66
+ execArgs: taskCommand("python-node-bindings-watch.ts"),
67
+ });
68
+ project.preCompileTask.spawn(this.buildTask);
69
+ if (options.test ?? true) project.testTask.spawn(this.checkTask);
70
+ }
71
+ }
@@ -32,14 +32,17 @@ export interface ResolvedPythonNodeBindings {
32
32
  readonly workspaceDirectories: readonly string[];
33
33
  }
34
34
 
35
- /** Standard PythonMonkey host adapters shipped with the binding generator. */
36
- export const PYTHON_NODE_SHIM_ROOT = resolve(
37
- dirname(fileURLToPath(import.meta.url)),
38
- "../shims/python-node",
39
- );
40
-
41
- /** Regenerate every pyproject-configured Node binding in a repository. */
42
- export function generatePythonNodeBindings(projectRoot: string = resolveRepoRoot()): void {
35
+ /** Options for repository-wide Python Node binding generation. */
36
+ export interface GeneratePythonNodeBindingsOptions {
37
+ /** Verify generated output without changing it. */
38
+ readonly check?: boolean;
39
+ }
40
+
41
+ /** Generate or verify every pyproject-configured Node binding in a repository. */
42
+ export function generatePythonNodeBindings(
43
+ projectRoot: string = resolveRepoRoot(),
44
+ options: GeneratePythonNodeBindingsOptions = {},
45
+ ): void {
43
46
  const task = resolve(dirname(fileURLToPath(import.meta.url)), "../tasks/python-node-bindings.ts");
44
47
  const configuredProjects = pythonNodeBindingProjects(projectRoot);
45
48
  const directories = new Set<string>();
@@ -48,16 +51,38 @@ export function generatePythonNodeBindings(projectRoot: string = resolveRepoRoot
48
51
  directories.add(relative(projectRoot, join(config.moduleDirectory, "_generated", "node")));
49
52
  }
50
53
  }
51
- for (const directory of existingBindingDirectories(projectRoot)) {
52
- if (!directories.has(directory)) {
53
- rmSync(resolve(projectRoot, directory), { recursive: true, force: true });
54
- }
54
+ const stale = existingBindingDirectories(projectRoot).filter(
55
+ (directory) => !directories.has(directory),
56
+ );
57
+ if (stale.length > 0 && options.check) {
58
+ throw new Error(
59
+ `Generated Node binding directories are stale:\n${stale.map((directory) => ` ${directory}`).join("\n")}`,
60
+ );
61
+ }
62
+ for (const directory of stale) {
63
+ rmSync(resolve(projectRoot, directory), { recursive: true, force: true });
55
64
  }
56
65
  for (const project of configuredProjects) {
57
- runTaskCommand(projectRoot, "bun", [task, "--root", projectRoot, "--project", project]);
66
+ runTaskCommand(projectRoot, "bun", [
67
+ task,
68
+ "--root",
69
+ projectRoot,
70
+ "--project",
71
+ project,
72
+ ...(options.check ? ["--check"] : []),
73
+ ]);
58
74
  }
59
75
  }
60
76
 
77
+ /** Resolve every configured binding in a repository. */
78
+ export function resolveAllPythonNodeBindings(
79
+ projectRoot: string = resolveRepoRoot(),
80
+ ): ResolvedPythonNodeBindings[] {
81
+ return pythonNodeBindingProjects(projectRoot).flatMap((project) =>
82
+ resolvePythonNodeBindings(projectRoot, project),
83
+ );
84
+ }
85
+
61
86
  /** Repository-relative Python projects that configure generated Node bindings. */
62
87
  export function pythonNodeBindingProjects(projectRoot: string): string[] {
63
88
  return [
@@ -165,7 +190,6 @@ export function pythonNodeBindingWatchInputs(config: ResolvedPythonNodeBindings)
165
190
  return [
166
191
  config.pyproject,
167
192
  ...config.workspaceDirectories,
168
- PYTHON_NODE_SHIM_ROOT,
169
193
  ...config.functionOverrides.map(({ handlerFile }) => handlerFile),
170
194
  ];
171
195
  }
@@ -0,0 +1,56 @@
1
+ /** Projen task wiring for the shared PythonMonkey Node runtime package. */
2
+ import { Component, type Project, type Task } from "projen";
3
+ import { taskCommand } from "./project-js.ts";
4
+
5
+ /** Published Python distribution imported by every generated Node binding loader. */
6
+ export const PYTHON_NODE_RUNTIME_DISTRIBUTION = "dbx-tools-node-runtime";
7
+
8
+ /** Python module that owns the shared PythonMonkey runtime and Node shims. */
9
+ export const PYTHON_NODE_RUNTIME_MODULE = "dbx_tools.node_runtime";
10
+
11
+ /** Repository-wide task that builds the shared Python Node runtime. */
12
+ export const PYTHON_NODE_RUNTIME_TASK = "python-node-runtime";
13
+
14
+ /** Repository-wide task that verifies the shared Python Node runtime. */
15
+ export const PYTHON_NODE_RUNTIME_CHECK_TASK = `${PYTHON_NODE_RUNTIME_TASK}:check`;
16
+
17
+ /** Repository-wide task that watches the shared Python Node runtime. */
18
+ export const PYTHON_NODE_RUNTIME_WATCH_TASK = `${PYTHON_NODE_RUNTIME_TASK}:watch`;
19
+
20
+ /** Options for the package that owns the shared PythonMonkey runtime. */
21
+ export interface PythonNodeRuntimeOptions {
22
+ /** Repository-relative Python project containing `build-runtime.ts` and `shims/`. */
23
+ readonly projectDirectory: string;
24
+ }
25
+
26
+ /**
27
+ * Registers the shared runtime's build, freshness, and watch lifecycle.
28
+ *
29
+ * The runtime package owns the build script, shim sources, generated `runtime.js`,
30
+ * PythonMonkey bootstrap, and bundle loader. This component only connects those
31
+ * package-owned operations to Projen's native task graph.
32
+ */
33
+ export class PythonNodeRuntime extends Component {
34
+ readonly buildTask: Task;
35
+ readonly checkTask: Task;
36
+ readonly watchTask: Task;
37
+
38
+ constructor(project: Project, options: PythonNodeRuntimeOptions) {
39
+ super(project);
40
+ const buildCommand = ["bun", `${options.projectDirectory}/build-runtime.ts`];
41
+ this.buildTask = project.addTask(PYTHON_NODE_RUNTIME_TASK, {
42
+ description: "Build the shared PythonMonkey Node runtime",
43
+ execArgs: buildCommand,
44
+ });
45
+ this.checkTask = project.addTask(PYTHON_NODE_RUNTIME_CHECK_TASK, {
46
+ description: "Verify the shared PythonMonkey Node runtime is current",
47
+ execArgs: [...buildCommand, "--check"],
48
+ });
49
+ this.watchTask = project.addTask(PYTHON_NODE_RUNTIME_WATCH_TASK, {
50
+ description: "Watch and rebuild the shared PythonMonkey Node runtime",
51
+ execArgs: taskCommand("python-node-runtime-watch.ts", "--project", options.projectDirectory),
52
+ });
53
+ project.preCompileTask.spawn(this.buildTask);
54
+ project.testTask.spawn(this.checkTask);
55
+ }
56
+ }