@dbx-tools/projen 0.9.43 → 0.9.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +28 -34
  2. package/index.ts +6 -7
  3. package/package.json +9 -10
  4. package/shims/python-node/bootstrap.ts +129 -0
  5. package/shims/python-node/child-process.ts +96 -0
  6. package/shims/python-node/crypto.ts +60 -0
  7. package/shims/python-node/fs-promises.ts +179 -0
  8. package/shims/python-node/fs.ts +173 -0
  9. package/shims/python-node/headers.ts +75 -0
  10. package/shims/python-node/host.ts +341 -0
  11. package/shims/python-node/os.ts +20 -0
  12. package/shims/python-node/process.ts +13 -0
  13. package/shims/python-node/readline.ts +37 -0
  14. package/shims/python-node/tsconfig.json +24 -0
  15. package/src/_lazy-require.ts +7 -25
  16. package/src/bun-app.ts +2 -2
  17. package/src/bun-workflow.ts +1 -9
  18. package/src/codegen.ts +20 -17
  19. package/src/dbx-tools-config.ts +52 -9
  20. package/src/generated.ts +4 -17
  21. package/src/mixin.ts +1 -5
  22. package/src/packages.ts +18 -15
  23. package/src/pnpm-workspace.ts +1 -4
  24. package/src/project-js.ts +65 -157
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +122 -44
  27. package/src/publish.ts +13 -10
  28. package/src/python-node-bindings.ts +2 -0
  29. package/src/python-node-bundle.ts +6 -0
  30. package/src/release-options.ts +17 -17
  31. package/src/release.ts +30 -9
  32. package/src/scaffold.ts +4 -7
  33. package/src/tags.ts +6 -16
  34. package/src/vscode.ts +1 -1
  35. package/src/watch.ts +5 -5
  36. package/src/workspace-lock.ts +1 -0
  37. package/src/workspace-version.ts +4 -4
  38. package/tasks/barrels.ts +3 -5
  39. package/tasks/clean.ts +1 -0
  40. package/tasks/lib/compile-workspace.ts +41 -19
  41. package/tasks/lib/local-publish.ts +0 -28
  42. package/tasks/lib/publish-npm.ts +8 -0
  43. package/tasks/lib/publish-python.ts +4 -0
  44. package/tasks/lib/python-node-bindings.ts +77 -21
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +1 -1
  48. package/tasks/projenrc.ts +1 -0
  49. package/tasks/sync.ts +4 -4
  50. package/src/openapi.ts +0 -293
  51. package/tasks/local-publish.ts +0 -6
  52. package/tasks/openapi.ts +0 -35
package/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
@@ -66,8 +65,8 @@ Repository policy stays in the consuming `.projenrc.ts`:
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.
68
+ - `releaseSynthesisCommands` runs repository-specific synth owners before the
69
+ root synthesis and clean-diff release check.
71
70
  - `extraWorkspaceMembers` declares self-synthesizing tooling packages outside
72
71
  `packageRoots`. Their version, generated entrypoint, formatting, linting, and
73
72
  workspace membership are derived from that declaration.
@@ -226,7 +225,7 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
226
225
  `project.with(...)` only when you need a predicate the filters cannot express.
227
226
 
228
227
  Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
229
- `ui`, and `openapi`. Repo-specific mixins layer package-specific dependencies,
228
+ and `ui`. Repo-specific mixins layer package-specific dependencies,
230
229
  scripts, and generated files on top.
231
230
 
232
231
  AppKit 0.81's `appkitServerConfig()` is the preferred tsdown preset for a
@@ -270,12 +269,19 @@ codegen.generateCodegen();
270
269
  barrels.generateBarrels();
271
270
  ```
272
271
 
273
- `generateCodegen()` reads `package.json` `codegen.inputs` and writes generated
274
- schema modules. They are written read-only, and the root ESLint task runs with
275
- an explicit separate fix task, so each generated module is added to
276
- `ignorePatterns` at synth - named individually via `codegen.codegenModulePaths()`,
277
- never as a blanket `<package>/src/**`. A codegen package may hold hand-written
278
- 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.
279
285
 
280
286
  `generateBarrels()` writes package-root `index.ts` barrels with module
281
287
  namespaces, flat unique type exports, and `PACKAGE_IDENTIFIER`, returning the
@@ -288,17 +294,6 @@ package is attempted even if one fails; the failures are re-thrown together as a
288
294
  `AggregateError` naming each package, rather than the first one abandoning the
289
295
  rest of the sweep.
290
296
 
291
- ## Generate OpenAPI Clients
292
-
293
- ```ts
294
- import { openapi } from "@dbx-tools/projen";
295
-
296
- const packages = await openapi.generateOpenapi();
297
- ```
298
-
299
- OpenAPI generation scans packages for tsoa controllers, emits `openapi.json`,
300
- generates TypeScript schemas, and adds an `openapi-fetch` client.
301
-
302
297
  ## Configure pnpm Catalogs
303
298
 
304
299
  ```ts
@@ -349,7 +344,6 @@ file contract as the CLI.
349
344
  - `pnpmWorkspace` - generated pnpm workspace file and catalog model.
350
345
  - `barrels` / `moduleExports` - public entrypoint generation.
351
346
  - `codegen` - `.d.ts` to zod schema generation.
352
- - `openapi` - tsoa/OpenAPI package generation.
353
347
  - `bunApp` / `tsconfig` / `vscode` - generated support files/components.
354
348
  - `generated` / `clean` / `watch` / `scaffold` - read-only file ownership,
355
349
  cleanup, watchers, and synth orchestration.
@@ -357,17 +351,17 @@ file contract as the CLI.
357
351
  - `engineRoot` - engine package root resolution for bootstrapped repos.
358
352
 
359
353
  The engine registers its commands as projen tasks on the workspace root, so run
360
- them with `bun run <task>` - `sync` (add `--watch`), `barrels`, `openapi`, and
361
- `clean`.
354
+ them with `bun run <task>` - `sync` (add `--watch`), `barrels`, and `clean`.
362
355
  [`@dbx-tools/cli`](../packages/js/cli/dbx-tools) is only needed to
363
356
  bootstrap a folder that has no `.projenrc.ts` or toolchain yet.
364
357
 
365
358
  ## Run Tasks From The ROOT
366
359
 
367
- Every repo-wide task lives on the root. `compile` batches ordinary TypeScript
368
- packages in up to four `tsc --build` processes and runs custom compile tasks alongside them;
369
- `test` delegates with `bun run --filter '*'`. Both read the current workspace
370
- 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:
371
365
 
372
366
  | Task | What it does |
373
367
  | ----------------------- | --------------------------------------------------- |
@@ -411,7 +405,7 @@ bun run release --local-registry false --local-pypi auto
411
405
  ```
412
406
 
413
407
  `--install auto` keeps Projen's normal local dependency-install behavior;
414
- `always` performs a frozen install first, and `never` uses dependencies already
408
+ `always` performs a manifest-resolved install first, and `never` uses dependencies already
415
409
  installed while still regenerating versioned sources. `--no-validation` skips
416
410
  optional task checks locally and in CI, not version or immutable-source checks.
417
411
  `--no-docs` skips building and deploying the site, not package README validation.
@@ -424,8 +418,8 @@ invokes, so there is no second place to run the same thing:
424
418
 
425
419
  - `compile` / `test` - what the root's `--filter '*'` delegation calls.
426
420
  - `prepack` - standalone-publish safety that compiles one package before packing
427
- (27 of 33 members; the workspace release driver compiles them from the root
428
- 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.
429
423
  - `watch` - a single-package `tsc --build -w`, for narrowing a long
430
424
  edit/compile loop to one package.
431
425
  - `build` / `package` - a complete compile/test/pack lifecycle when invoked in
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.43";
6
+ export const PACKAGE_VERSION = "0.9.47";
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,8 +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 { DBXToolsConfig } from "./src/dbx-tools-config.ts";
41
- 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";
42
41
  export { resolvePkgRoot } from "./src/engine-root.ts";
43
42
  export type { HeaderOpts } from "./src/generated.ts";
44
43
  export type { ConstructsMixin } from "./src/mixin.ts";
@@ -48,8 +47,8 @@ export type { RecordedPackage } from "./src/packages.ts";
48
47
  export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
49
48
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
50
49
  export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
51
- export { PackageIdentifier, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
52
- 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";
53
52
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
54
53
  export type { PythonRepositoryOptions, PythonPackageOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
55
54
  export { PROJEN_VERSION } from "./src/projen-version.ts";
@@ -61,7 +60,7 @@ export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeB
61
60
  export { DBXToolsRelease } from "./src/release.ts";
62
61
  export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
63
62
  export { RELEASE_VERSION } from "./src/release-context.ts";
64
- 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";
65
64
  export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
66
65
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
67
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.43",
36
- "@dbx-tools/path": "^0.9.43",
37
- "@dbx-tools/shared-core": "^0.9.43",
33
+ "@dbx-tools/core": "^0.9.47",
34
+ "@dbx-tools/path": "^0.9.47",
35
+ "@dbx-tools/shared-core": "^0.9.47",
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.43",
57
+ "version": "0.9.47",
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 };