@dbx-tools/projen 0.9.40 → 0.9.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +58 -67
  2. package/index.ts +6 -8
  3. package/package.json +9 -10
  4. package/shims/python-node/bootstrap.ts +129 -0
  5. package/shims/python-node/child-process.ts +96 -0
  6. package/shims/python-node/crypto.ts +60 -0
  7. package/shims/python-node/fs-promises.ts +179 -0
  8. package/shims/python-node/fs.ts +173 -0
  9. package/shims/python-node/headers.ts +75 -0
  10. package/shims/python-node/host.ts +341 -0
  11. package/shims/python-node/os.ts +20 -0
  12. package/shims/python-node/process.ts +13 -0
  13. package/shims/python-node/readline.ts +37 -0
  14. package/shims/python-node/tsconfig.json +24 -0
  15. package/src/_lazy-require.ts +7 -25
  16. package/src/bun-app.ts +2 -2
  17. package/src/bun-workflow.ts +2 -170
  18. package/src/codegen.ts +23 -20
  19. package/src/dbx-tools-config.ts +52 -9
  20. package/src/generated.ts +4 -17
  21. package/src/mixin.ts +1 -5
  22. package/src/packages.ts +18 -15
  23. package/src/pnpm-workspace.ts +1 -4
  24. package/src/project-js.ts +65 -196
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +127 -37
  27. package/src/publish.ts +13 -10
  28. package/src/python-node-bindings.ts +2 -0
  29. package/src/python-node-bundle.ts +6 -0
  30. package/src/release-options.ts +17 -17
  31. package/src/release.ts +31 -14
  32. package/src/scaffold.ts +4 -7
  33. package/src/tags.ts +6 -16
  34. package/src/vscode.ts +1 -1
  35. package/src/watch.ts +5 -5
  36. package/src/workspace-lock.ts +1 -0
  37. package/src/workspace-version.ts +4 -4
  38. package/tasks/barrels.ts +3 -5
  39. package/tasks/clean.ts +1 -0
  40. package/tasks/lib/compile-workspace.ts +41 -19
  41. package/tasks/lib/local-publish.ts +0 -28
  42. package/tasks/lib/publish-npm.ts +8 -0
  43. package/tasks/lib/publish-python.ts +4 -0
  44. package/tasks/lib/python-node-bindings.ts +107 -26
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +2 -2
  48. package/tasks/projenrc.ts +1 -0
  49. package/tasks/sync.ts +4 -4
  50. package/src/openapi.ts +0 -293
  51. package/tasks/local-publish.ts +0 -6
  52. package/tasks/openapi.ts +0 -35
package/README.md CHANGED
@@ -4,21 +4,20 @@ Projen engine for Bun-first dbx-tools workspaces.
4
4
 
5
5
  Import this package from `.projenrc.ts` when a repository should discover
6
6
  packages from the filesystem and generate manifests, tsconfigs,
7
- barrels, OpenAPI clients, codegen outputs, and release tasks.
7
+ barrels, codegen outputs, and release tasks.
8
8
 
9
9
  Key features:
10
10
 
11
11
  - Filesystem package discovery: every `src`-bearing folder under configured
12
12
  workspace roots becomes a TypeScript package.
13
13
  - Tag-driven runtime defaults for shared libraries, Node packages, CLIs,
14
- servers, OpenAPI clients, and React/browser UI packages.
14
+ servers, and React/browser UI packages.
15
15
  - Generated package manifests, tsconfigs, package-root barrels, Bun app configs,
16
16
  VS Code settings, and a committed `pnpm-workspace.yaml` retained for
17
17
  Databricks Apps deployment.
18
18
  - Extensible mixin system so repositories can add deps, tasks, or generated
19
19
  files based on package predicates.
20
- - OpenAPI client generation from tsoa controllers and zod schema generation from
21
- `.d.ts` inputs.
20
+ - Zod schema generation from `.d.ts` inputs.
22
21
  - Read-only generated-file ownership, cleanup, and watch-loop helpers.
23
22
 
24
23
  ## Define A Workspace Root
@@ -58,18 +57,16 @@ workflow.
58
57
  Repository policy stays in the consuming `.projenrc.ts`:
59
58
 
60
59
  - `releaseDocs` supplies repository-defined preparation and build steps plus
61
- the Pages artifact path. The engine adds release checkout, Bun caching,
62
- artifact upload, and deployment without naming a docs script or output tree.
60
+ the Pages artifact path. The engine adds release checkout, artifact upload,
61
+ and deployment without naming a docs script or output tree.
63
62
  - `releasePythonRoot` passes the actual Python package root to local release
64
63
  preparation. Omit it when the workspace has no standard Python packages.
65
64
  - `releaseValidationTasks` names repository tasks that must pass in tag CI
66
65
  before release artifacts are built and published.
67
66
  - `releaseSetupSteps` installs any extra validation/build prerequisites in the
68
67
  shared release job before the validation tasks run.
69
- - `pullRequestTitlePolicy` configures semantic title types and scope policy.
70
- Omit it or pass `false` to disable the title job.
71
- - `workflowCacheIgnorePaths` excludes generated output trees that may contain
72
- package manifests from the dependency-only Bun cache key.
68
+ - `releaseSynthesisCommands` runs repository-specific synth owners before the
69
+ root synthesis and clean-diff release check.
73
70
  - `extraWorkspaceMembers` declares self-synthesizing tooling packages outside
74
71
  `packageRoots`. Their version, generated entrypoint, formatting, linting, and
75
72
  workspace membership are derived from that declaration.
@@ -228,7 +225,7 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
228
225
  `project.with(...)` only when you need a predicate the filters cannot express.
229
226
 
230
227
  Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
231
- `ui`, and `openapi`. Repo-specific mixins layer package-specific dependencies,
228
+ and `ui`. Repo-specific mixins layer package-specific dependencies,
232
229
  scripts, and generated files on top.
233
230
 
234
231
  AppKit 0.81's `appkitServerConfig()` is the preferred tsdown preset for a
@@ -272,12 +269,19 @@ codegen.generateCodegen();
272
269
  barrels.generateBarrels();
273
270
  ```
274
271
 
275
- `generateCodegen()` reads `package.json` `codegen.inputs` and writes generated
276
- schema modules. They are written read-only, and the root ESLint task runs with
277
- an explicit separate fix task, so each generated module is added to
278
- `ignorePatterns` at synth - named individually via `codegen.codegenModulePaths()`,
279
- never as a blanket `<package>/src/**`. A codegen package may hold hand-written
280
- modules beside its generated ones, and those must stay linted.
272
+ Add declaration inputs through the owning package's typed config:
273
+
274
+ ```ts
275
+ pkg.dbxToolsConfig.codegenInputs.push("node_modules/example/model.d.ts");
276
+ ```
277
+
278
+ `generateCodegen()` reads the serialized `dbxToolsConfig.codegenInputs` and
279
+ writes generated schema modules. They are written read-only, and the root
280
+ ESLint task runs with an explicit separate fix task, so each generated module is
281
+ added to `ignorePatterns` at synth - named individually via
282
+ `codegen.codegenModulePaths()`, never as a blanket `<package>/src/**`. A codegen
283
+ package may hold hand-written modules beside its generated ones, and those must
284
+ stay linted.
281
285
 
282
286
  `generateBarrels()` writes package-root `index.ts` barrels with module
283
287
  namespaces, flat unique type exports, and `PACKAGE_IDENTIFIER`, returning the
@@ -290,17 +294,6 @@ package is attempted even if one fails; the failures are re-thrown together as a
290
294
  `AggregateError` naming each package, rather than the first one abandoning the
291
295
  rest of the sweep.
292
296
 
293
- ## Generate OpenAPI Clients
294
-
295
- ```ts
296
- import { openapi } from "@dbx-tools/projen";
297
-
298
- const packages = await openapi.generateOpenapi();
299
- ```
300
-
301
- OpenAPI generation scans packages for tsoa controllers, emits `openapi.json`,
302
- generates TypeScript schemas, and adds an `openapi-fetch` client.
303
-
304
297
  ## Configure pnpm Catalogs
305
298
 
306
299
  ```ts
@@ -351,7 +344,6 @@ file contract as the CLI.
351
344
  - `pnpmWorkspace` - generated pnpm workspace file and catalog model.
352
345
  - `barrels` / `moduleExports` - public entrypoint generation.
353
346
  - `codegen` - `.d.ts` to zod schema generation.
354
- - `openapi` - tsoa/OpenAPI package generation.
355
347
  - `bunApp` / `tsconfig` / `vscode` - generated support files/components.
356
348
  - `generated` / `clean` / `watch` / `scaffold` - read-only file ownership,
357
349
  cleanup, watchers, and synth orchestration.
@@ -359,17 +351,17 @@ file contract as the CLI.
359
351
  - `engineRoot` - engine package root resolution for bootstrapped repos.
360
352
 
361
353
  The engine registers its commands as projen tasks on the workspace root, so run
362
- them with `bun run <task>` - `sync` (add `--watch`), `barrels`, `openapi`, and
363
- `clean`.
354
+ them with `bun run <task>` - `sync` (add `--watch`), `barrels`, and `clean`.
364
355
  [`@dbx-tools/cli`](../packages/js/cli/dbx-tools) is only needed to
365
356
  bootstrap a folder that has no `.projenrc.ts` or toolchain yet.
366
357
 
367
358
  ## Run Tasks From The ROOT
368
359
 
369
- Every repo-wide task lives on the root. `compile` batches ordinary TypeScript
370
- packages in up to four `tsc --build` processes and runs custom compile tasks alongside them;
371
- `test` delegates with `bun run --filter '*'`. Both read the current workspace
372
- list, so a new package is covered without a re-synth. Work from the root:
360
+ Every repo-wide task lives on the root. `compile` batches package tsconfigs in
361
+ up to four `tsc --build` processes and runs package-owned compile lifecycles
362
+ alongside them; `test` delegates with `bun run --filter '*'`. Both read the
363
+ current workspace list, so a new package is covered without a re-synth. Work
364
+ from the root:
373
365
 
374
366
  | Task | What it does |
375
367
  | ----------------------- | --------------------------------------------------- |
@@ -389,10 +381,10 @@ diverged. From `main`, it calls the existing `bump` task, commits the generated
389
381
  version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
390
382
  Pass `--no-bump` only to release an existing synchronized local bump. The tag
391
383
  workflow verifies that the tag points at the exact `origin/main` commit. One
392
- `build-release` job restores the Bun cache and installs workspace dependencies,
393
- runs validation, packs the npm archives, builds all Python distributions, and
394
- builds the documentation site. Install validation prerequisites through
395
- `releaseSetupSteps`; `releaseDocs.prepareSteps` runs after validation.
384
+ `build-release` job installs workspace dependencies, runs validation, packs the
385
+ npm archives, builds all Python distributions, and builds the documentation
386
+ site. Install validation prerequisites through `releaseSetupSteps`;
387
+ `releaseDocs.prepareSteps` runs after validation.
396
388
 
397
389
  Publication jobs download the resulting artifacts rather than rebuilding the
398
390
  workspace. Each Python package retains its own publishing environment and waits
@@ -413,7 +405,7 @@ bun run release --local-registry false --local-pypi auto
413
405
  ```
414
406
 
415
407
  `--install auto` keeps Projen's normal local dependency-install behavior;
416
- `always` performs a frozen install first, and `never` uses dependencies already
408
+ `always` performs a manifest-resolved install first, and `never` uses dependencies already
417
409
  installed while still regenerating versioned sources. `--no-validation` skips
418
410
  optional task checks locally and in CI, not version or immutable-source checks.
419
411
  `--no-docs` skips building and deploying the site, not package README validation.
@@ -426,8 +418,8 @@ invokes, so there is no second place to run the same thing:
426
418
 
427
419
  - `compile` / `test` - what the root's `--filter '*'` delegation calls.
428
420
  - `prepack` - standalone-publish safety that compiles one package before packing
429
- (27 of 33 members; the workspace release driver compiles them from the root
430
- and publishes with lifecycle scripts disabled).
421
+ when that package has a compiled publication surface; the workspace release
422
+ driver compiles from the root and publishes with lifecycle scripts disabled.
431
423
  - `watch` - a single-package `tsc --build -w`, for narrowing a long
432
424
  edit/compile loop to one package.
433
425
  - `build` / `package` - a complete compile/test/pack lifecycle when invoked in
@@ -458,33 +450,32 @@ workflow order without introducing another version owner.
458
450
 
459
451
  ### `release`
460
452
 
461
- ```text
462
- Usage: release [options]
463
-
464
453
  Prepare an annotated release and select its build and publication steps
465
454
 
466
- Options:
467
- --root <path> repository root
468
- --branch <name> release branch (default: "main")
469
- --prefix <prefix> release tag prefix (default: "v")
470
- --remote <name> git remote (default: "origin")
471
- --python-root <path> Python package root for local publish (default: "packages/py")
472
- --validate <task> task to run before pushing (default: [])
473
- --no-bump use an existing synchronized local version bump
474
- --publish <target> publication scope (choices: "auto", "npm", "pypi", "local",
475
- "none", default: "auto")
476
- --install <mode> local workspace dependency installation (choices: "auto",
477
- "always", "never", default: "auto")
478
- --no-npm skip npm build and publication, including local npm publication
479
- --no-pypi skip Python build and publication, including local Python
480
- publication
481
- --docs build and deploy docs for a selected scope
482
- --no-docs skip documentation build and deployment
483
- --no-validation skip optional release validation tasks; version/source checks
484
- remain mandatory
485
- --no-local-publish skip publishing to configured local registries
486
- --local-registry <auto|false|url> local npm registry selection (default: "auto")
487
- --local-pypi <auto|false|url> local devpi registry selection (default: "auto")
455
+ ```sh
456
+ release [options]
488
457
  ```
489
458
 
459
+ #### Options
460
+
461
+ | Option | Description |
462
+ | ------------------------------------- | --------------------------------------------------------------------------------------------- |
463
+ | `--root <path>` | repository root |
464
+ | `--branch <name>` | release branch (default: "main") |
465
+ | `--prefix <prefix>` | release tag prefix (default: "v") |
466
+ | `--remote <name>` | git remote (default: "origin") |
467
+ | `--python-root <path>` | Python package root for local publish (default: "packages/py") |
468
+ | `--validate <task>` | task to run before pushing (default: []) |
469
+ | `--no-bump` | use an existing synchronized local version bump |
470
+ | `--publish <target>` | publication scope (choices: "auto", "npm", "pypi", "local", "none", default: "auto") |
471
+ | `--install <mode>` | local workspace dependency installation (choices: "auto", "always", "never", default: "auto") |
472
+ | `--no-npm` | skip npm build and publication, including local npm publication |
473
+ | `--no-pypi` | skip Python build and publication, including local Python publication |
474
+ | `--docs` | build and deploy docs for a selected scope |
475
+ | `--no-docs` | skip documentation build and deployment |
476
+ | `--no-validation` | skip optional release validation tasks; version/source checks remain mandatory |
477
+ | `--no-local-publish` | skip publishing to configured local registries |
478
+ | `--local-registry <auto\|false\|url>` | local npm registry selection (default: "auto") |
479
+ | `--local-pypi <auto\|false\|url>` | local devpi registry selection (default: "auto") |
480
+
490
481
  <!-- cli-reference:end -->
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.40";
6
+ export const PACKAGE_VERSION = "0.9.46";
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";
@@ -14,7 +14,6 @@ export * as engineRoot from "./src/engine-root.ts";
14
14
  export * as generated from "./src/generated.ts";
15
15
  export * as mixin from "./src/mixin.ts";
16
16
  export * as moduleExports from "./src/module-exports.ts";
17
- export * as openapi from "./src/openapi.ts";
18
17
  export * as packages from "./src/packages.ts";
19
18
  export * as pnpmWorkspace from "./src/pnpm-workspace.ts";
20
19
  export * as project from "./src/project.ts";
@@ -37,9 +36,8 @@ export * as workspaceLock from "./src/workspace-lock.ts";
37
36
  export * as workspaceVersion from "./src/workspace-version.ts";
38
37
  export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
39
38
  export { BUN_VERSION } from "./src/bun-workflow.ts";
40
- export type { BunWorkflowCacheOptions } from "./src/bun-workflow.ts";
41
- export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
42
- export type { DBXToolsConfigOptions } from "./src/dbx-tools-config.ts";
39
+ export { DBXToolsConfigDataSchema, DBXToolsConfig } from "./src/dbx-tools-config.ts";
40
+ export type { DBXToolsConfigOptions, DBXToolsConfigData } from "./src/dbx-tools-config.ts";
43
41
  export { resolvePkgRoot } from "./src/engine-root.ts";
44
42
  export type { HeaderOpts } from "./src/generated.ts";
45
43
  export type { ConstructsMixin } from "./src/mixin.ts";
@@ -49,8 +47,8 @@ export type { RecordedPackage } from "./src/packages.ts";
49
47
  export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
50
48
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
51
49
  export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
52
- export { PackageIdentifier, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
53
- export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
50
+ export { PackageIdentifier, LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
51
+ export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, DiscoveredTypeScriptPackageOptions, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
54
52
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
55
53
  export type { PythonRepositoryOptions, PythonPackageOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
56
54
  export { PROJEN_VERSION } from "./src/projen-version.ts";
@@ -62,7 +60,7 @@ export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeB
62
60
  export { DBXToolsRelease } from "./src/release.ts";
63
61
  export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
64
62
  export { RELEASE_VERSION } from "./src/release-context.ts";
65
- export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES } from "./src/release-options.ts";
63
+ export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES, ReleaseStepSelectionSchema } from "./src/release-options.ts";
66
64
  export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
67
65
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
68
66
  export type { PackageTag } from "./src/tags.ts";
package/package.json CHANGED
@@ -16,7 +16,6 @@
16
16
  "post-upgrade": "projen post-upgrade",
17
17
  "pre-compile": "projen pre-compile",
18
18
  "test": "projen test",
19
- "test:external-consumer": "projen test:external-consumer",
20
19
  "upgrade": "projen upgrade",
21
20
  "watch": "projen watch",
22
21
  "projen": "projen"
@@ -24,29 +23,28 @@
24
23
  "devDependencies": {
25
24
  "@types/node": "^24.6.0",
26
25
  "@types/semver": "^7.7.1",
27
- "projen": "0.103.27",
28
- "tsoa": "^6.6.0"
26
+ "projen": "0.103.27"
29
27
  },
30
28
  "peerDependencies": {
31
29
  "projen": "^0.103.27"
32
30
  },
33
31
  "dependencies": {
34
32
  "@clack/prompts": "^1.7.0",
35
- "@dbx-tools/core": "^0.9.40",
36
- "@dbx-tools/path": "^0.9.40",
37
- "@dbx-tools/shared-core": "^0.9.40",
33
+ "@dbx-tools/core": "^0.9.46",
34
+ "@dbx-tools/path": "^0.9.46",
35
+ "@dbx-tools/shared-core": "^0.9.46",
38
36
  "commander": "^15.0.0",
39
37
  "concurrently": "^10.0.3",
40
38
  "constructs": "^10.6.0",
41
39
  "is-identifier": "^1",
42
40
  "node-stdlib-browser": "^1.3.1",
43
- "openapi-typescript": "^7.13.0",
44
41
  "oxc-parser": "^0.90.0",
45
42
  "semver": "^7.7.3",
46
43
  "smol-toml": "1.8.0",
47
44
  "ts-to-zod": "^5.1.0",
48
45
  "typescript": "^5.9.3",
49
- "yaml": "^2.9.0"
46
+ "yaml": "^2.9.0",
47
+ "zod": "^4.3.6"
50
48
  },
51
49
  "devEngines": {
52
50
  "packageManager": {
@@ -56,7 +54,7 @@
56
54
  },
57
55
  "main": "index.ts",
58
56
  "license": "Apache-2.0",
59
- "version": "0.9.40",
57
+ "version": "0.9.46",
60
58
  "types": "index.ts",
61
59
  "type": "module",
62
60
  "exports": {
@@ -67,7 +65,8 @@
67
65
  "files": [
68
66
  "index.ts",
69
67
  "src",
70
- "tasks"
68
+ "tasks",
69
+ "shims/python-node"
71
70
  ],
72
71
  "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
73
72
  }
@@ -0,0 +1,129 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ import { PythonHeaders } from "./headers.ts";
4
+ import { installPythonGlobals, pythonHost } from "./host.ts";
5
+
6
+ installPythonGlobals();
7
+
8
+ (globalThis as typeof globalThis & { Buffer?: typeof Buffer }).Buffer = Buffer;
9
+
10
+ const globals = globalThis as typeof globalThis & {
11
+ AbortController?: typeof AbortController;
12
+ Headers?: typeof Headers;
13
+ Request?: typeof Request;
14
+ Response?: typeof Response;
15
+ structuredClone?: typeof structuredClone;
16
+ TextDecoder?: typeof TextDecoder;
17
+ TextEncoder?: typeof TextEncoder;
18
+ fetch?: typeof fetch;
19
+ };
20
+ globals.AbortController ??= class AbortController {
21
+ readonly signal: AbortSignal;
22
+ readonly #listeners = new Set<() => void>();
23
+
24
+ constructor() {
25
+ this.signal = {
26
+ aborted: false,
27
+ addEventListener: (name: string, listener: EventListenerOrEventListenerObject) => {
28
+ if (name === "abort") {
29
+ this.#listeners.add(
30
+ typeof listener === "function"
31
+ ? () => listener(new Event("abort"))
32
+ : () => listener.handleEvent(new Event("abort")),
33
+ );
34
+ }
35
+ },
36
+ removeEventListener: () => {},
37
+ } as AbortSignal;
38
+ }
39
+
40
+ abort(): void {
41
+ if (this.signal.aborted) return;
42
+ Object.defineProperty(this.signal, "aborted", { value: true });
43
+ for (const listener of this.#listeners) listener();
44
+ this.#listeners.clear();
45
+ }
46
+ } as typeof AbortController;
47
+
48
+ globals.Headers ??= PythonHeaders as unknown as typeof Headers;
49
+
50
+ class PythonResponse {
51
+ readonly headers: Headers;
52
+ readonly ok: boolean;
53
+ readonly status: number;
54
+ readonly #body: Uint8Array;
55
+
56
+ constructor(body: Uint8Array, init: ResponseInit = {}) {
57
+ this.#body = body;
58
+ this.status = init.status ?? 200;
59
+ this.ok = this.status >= 200 && this.status < 300;
60
+ this.headers = new Headers(init.headers);
61
+ }
62
+
63
+ async arrayBuffer(): Promise<ArrayBuffer> {
64
+ return this.#body.slice().buffer;
65
+ }
66
+
67
+ async text(): Promise<string> {
68
+ return new TextDecoder().decode(this.#body);
69
+ }
70
+
71
+ async json(): Promise<unknown> {
72
+ return JSON.parse(await this.text());
73
+ }
74
+ }
75
+
76
+ globals.Response ??= PythonResponse as unknown as typeof Response;
77
+ globals.structuredClone ??= ((value: unknown) => cloneStructured(value)) as typeof structuredClone;
78
+ globals.TextEncoder ??= class TextEncoder {
79
+ encode(value: string): Uint8Array {
80
+ const encoded = unescape(encodeURIComponent(String(value)));
81
+ return Uint8Array.from(encoded, (character) => character.charCodeAt(0));
82
+ }
83
+ } as typeof TextEncoder;
84
+ globals.TextDecoder ??= class TextDecoder {
85
+ decode(value?: ArrayBuffer | ArrayBufferView): string {
86
+ const bytes =
87
+ value instanceof ArrayBuffer
88
+ ? new Uint8Array(value)
89
+ : value
90
+ ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
91
+ : new Uint8Array();
92
+ const encoded = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
93
+ return decodeURIComponent(escape(encoded));
94
+ }
95
+ } as typeof TextDecoder;
96
+ globals.fetch ??= (async (input: string | URL | Request, init: RequestInit = {}) => {
97
+ const request = typeof input === "object" && "url" in input ? input : undefined;
98
+ const headers = init.headers ?? request?.headers;
99
+ const response = await pythonHost().http.fetch(
100
+ request?.url ?? String(input),
101
+ init.method ?? request?.method,
102
+ headers ? Object.fromEntries(new Headers(headers).entries()) : undefined,
103
+ typeof init.body === "string" ? init.body : undefined,
104
+ );
105
+ return new Response(Uint8Array.from(response.body), {
106
+ status: response.status,
107
+ headers: response.headers,
108
+ });
109
+ }) as typeof fetch;
110
+
111
+ function cloneStructured(value: unknown, seen = new Map<object, unknown>()): unknown {
112
+ if (value === null || typeof value !== "object") return value;
113
+ const cached = seen.get(value);
114
+ if (cached !== undefined) return cached;
115
+ if (value instanceof Date) return new Date(value.getTime());
116
+ if (Array.isArray(value)) {
117
+ const clone: unknown[] = [];
118
+ seen.set(value, clone);
119
+ for (const item of value) clone.push(cloneStructured(item, seen));
120
+ return clone;
121
+ }
122
+ if (Object.getPrototypeOf(value) === Object.prototype) {
123
+ const clone: Record<string, unknown> = {};
124
+ seen.set(value, clone);
125
+ for (const [key, item] of Object.entries(value)) clone[key] = cloneStructured(item, seen);
126
+ return clone;
127
+ }
128
+ throw new TypeError(`PythonMonkey structuredClone does not support ${value.constructor.name}`);
129
+ }
@@ -0,0 +1,96 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { Readable, Writable } from "node:stream";
3
+
4
+ import { pythonHost, type PythonProcessHandle } from "./host.ts";
5
+
6
+ interface SpawnOptions {
7
+ env?: Record<string, string>;
8
+ signal?: AbortSignal;
9
+ }
10
+
11
+ class PythonChildProcess extends EventEmitter {
12
+ readonly pid = undefined;
13
+ readonly stdin = new Writable({ write: (_chunk, _encoding, callback) => callback() });
14
+ readonly stdout = new Readable({ read() {} });
15
+ readonly stderr = new Readable({ read() {} });
16
+ private active = true;
17
+ private readonly handle: PythonProcessHandle;
18
+
19
+ constructor(command: string, args: string[], options: SpawnOptions) {
20
+ super();
21
+ this.handle = pythonHost().process.start(command, args, options.env, undefined);
22
+ options.signal?.addEventListener("abort", () => {
23
+ if (!this.active) return;
24
+ this.active = false;
25
+ // Actually terminate the Python child so an aborted call leaks nothing.
26
+ this.handle.kill();
27
+ this.stdout.push(null);
28
+ this.stderr.push(null);
29
+ this.emit(
30
+ "error",
31
+ Object.assign(new Error("Process execution was aborted"), { code: "ABORT_ERR" }),
32
+ );
33
+ });
34
+ void this.handle.wait().then(
35
+ (result) => {
36
+ if (!this.active) return;
37
+ this.active = false;
38
+ if (result.stdout) this.stdout.push(result.stdout);
39
+ if (result.stderr) this.stderr.push(result.stderr);
40
+ this.stdout.push(null);
41
+ this.stderr.push(null);
42
+ this.emit("close", result.exitCode);
43
+ this.emit("exit", result.exitCode);
44
+ },
45
+ (error) => {
46
+ if (!this.active) return;
47
+ this.active = false;
48
+ this.stdout.push(null);
49
+ this.stderr.push(null);
50
+ this.emit("error", error);
51
+ },
52
+ );
53
+ }
54
+
55
+ kill(): boolean {
56
+ this.handle.kill();
57
+ return true;
58
+ }
59
+ }
60
+
61
+ export function spawn(command: string, args: string[] = [], options: SpawnOptions = {}) {
62
+ return new PythonChildProcess(command, args, options);
63
+ }
64
+
65
+ export function execFile(
66
+ command: string,
67
+ args: string[],
68
+ _options: object,
69
+ callback: (error: Error | null, result?: { stdout: string; stderr: string }) => void,
70
+ ): void {
71
+ void pythonHost()
72
+ .process.start(command, args, undefined, undefined)
73
+ .wait()
74
+ .then(
75
+ (result) => {
76
+ if (result.exitCode === 0) {
77
+ callback(null, { stdout: result.stdout ?? "", stderr: result.stderr ?? "" });
78
+ } else {
79
+ callback(
80
+ Object.assign(new Error(result.stderr || `Command exited ${result.exitCode}`), {
81
+ code: result.exitCode,
82
+ }),
83
+ );
84
+ }
85
+ },
86
+ (error) => callback(error instanceof Error ? error : new Error(String(error))),
87
+ );
88
+ }
89
+
90
+ export function spawnSync(): never {
91
+ throw Object.assign(new Error("Synchronous subprocesses are unavailable in PythonMonkey"), {
92
+ code: "ENOSYS",
93
+ });
94
+ }
95
+
96
+ export default { execFile, spawn, spawnSync };
@@ -0,0 +1,60 @@
1
+ import { pythonHost } from "./host.ts";
2
+
3
+ class PythonHash {
4
+ private readonly content: number[] = [];
5
+
6
+ constructor(private readonly algorithm: string) {}
7
+
8
+ update(value: string | ArrayBuffer | ArrayBufferView): this {
9
+ const bytes =
10
+ typeof value === "string"
11
+ ? new TextEncoder().encode(value)
12
+ : value instanceof ArrayBuffer
13
+ ? new Uint8Array(value)
14
+ : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
15
+ this.content.push(...bytes);
16
+ return this;
17
+ }
18
+
19
+ digest(encoding?: "hex" | "base64"): string | Uint8Array {
20
+ const digest = pythonHost().crypto.digest(this.algorithm, this.content, encoding);
21
+ if (typeof digest === "string") return digest;
22
+ const bytes = Uint8Array.from(digest) as Uint8Array & {
23
+ readBigInt64BE(offset: number): bigint;
24
+ };
25
+ bytes.readBigInt64BE = (offset) => {
26
+ let value = 0n;
27
+ for (const byte of bytes.slice(offset, offset + 8)) {
28
+ value = (value << 8n) | BigInt(byte);
29
+ }
30
+ return BigInt.asIntN(64, value);
31
+ };
32
+ return bytes;
33
+ }
34
+ }
35
+
36
+ export function createHash(algorithm: string): PythonHash {
37
+ const normalized = algorithm.toLowerCase();
38
+ if (!getHashes().includes(normalized)) {
39
+ throw new Error(`Unsupported hash algorithm: ${algorithm}`);
40
+ }
41
+ return new PythonHash(normalized);
42
+ }
43
+
44
+ export function getHashes(): string[] {
45
+ return pythonHost().crypto.hashes();
46
+ }
47
+
48
+ export function randomBytes(length: number): Uint8Array {
49
+ return Uint8Array.from(pythonHost().crypto.randomBytes(length));
50
+ }
51
+
52
+ export function randomUUID(): string {
53
+ const bytes = randomBytes(16);
54
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40;
55
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
56
+ const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
57
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
58
+ }
59
+
60
+ export default { createHash, getHashes, randomBytes, randomUUID };