@dbx-tools/projen 0.9.43 → 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 +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 +120 -43
  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/src/openapi.ts DELETED
@@ -1,293 +0,0 @@
1
- /**
2
- * OpenAPI generator for tsoa controllers.
3
- *
4
- * TypeScript packages are discovered from tsoa imports and feed the optimized generated package:
5
- *
6
- * - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, then Speakeasy
7
- * optimization to extract duplicate inline schemas into components).
8
- * - `src/schema.ts` - types generated from the spec (openapi-typescript).
9
- * - `src/client.ts` - a typed `openapi-fetch` client, usable client-side.
10
- *
11
- * `generateSpec` then reads the actual controller decorators + TypeScript types from
12
- * those files, so the API surface is annotated on the methods and nothing is
13
- * hand-written twice. The generated client stack is openapi-typescript +
14
- * openapi-fetch (openapi-ts.dev), the best-of-2026 choice since AppKit ships no
15
- * OpenAPI client generator.
16
- *
17
- * `tsoa`, `typescript`, and `openapi-typescript` are loaded lazily (heavy, and only
18
- * needed for `bun run openapi`), so importing this module stays cheap. `tsoa` and
19
- * `typescript` are not engine dependencies at all - both are resolved out of the
20
- * consuming workspace, which is where they already live. Speakeasy's `openapi`
21
- * binary is installed lazily through `@dbx-tools/core`'s binary cache.
22
- */
23
- import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
24
- import { createRequire } from "node:module";
25
- import { isAbsolute, join, relative, resolve } from "node:path";
26
- import * as bin from "@dbx-tools/core/bin";
27
- import * as exec from "@dbx-tools/core/exec";
28
- import { find } from "@dbx-tools/path";
29
- import { log, object, stringUtils } from "@dbx-tools/shared-core";
30
- import type * as ts from "typescript";
31
- import { lazyRequire } from "./_lazy-require.ts";
32
- import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
33
- import {
34
- type RecordedPackage,
35
- isModuleFile,
36
- repoRoot,
37
- toPosix,
38
- recordedPackages,
39
- } from "./packages.ts";
40
- import { readWorkspaceVersion } from "./workspace-version.ts";
41
-
42
- const logger = log.logger("projen:openapi");
43
-
44
- /** The tag (and folder) the generated openapi client packages are written under. */
45
- const OPENAPI_TAG = "openapi";
46
- /** Heuristic: a module file whose source imports tsoa's runtime package. */
47
- const TSOA_IMPORT = /from\s+['"](?:tsoa|@tsoa\/runtime)['"]/;
48
- const SPEAKEASY_OPENAPI_VERSION = "1.24.0";
49
- const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/releases/download/v${SPEAKEASY_OPENAPI_VERSION}`;
50
-
51
- // prettier-ignore
52
- const CLIENT_SRC =
53
- stringUtils.dedent(
54
- // ============================================================================
55
- /*ts*/`
56
- import createClient, { type ClientOptions } from "openapi-fetch";
57
- import type { paths } from "./schema";
58
-
59
- /** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
60
- export function createApiClient(options?: ClientOptions) {
61
- return createClient<paths>(options);
62
- }
63
- `
64
- // ============================================================================
65
- ) + "\n";
66
-
67
- /** True if any module file in `<pkg>/src` matches {@link TSOA_IMPORT}. */
68
- function hasTsoaControllers(pkg: Pick<RecordedPackage, "dir">): boolean {
69
- const srcDir = tsoaSource(pkg);
70
- return object
71
- .sequence(find.findFiles("**/*", { cwd: srcDir }))
72
- .filter(isModuleFile)
73
- .some((file) => TSOA_IMPORT.test(readFileSync(join(srcDir, file), "utf8")));
74
- }
75
-
76
- function tsoaSource(pkg: Pick<RecordedPackage, "dir">): string {
77
- return join(pkg.dir, "src");
78
- }
79
-
80
- function isTsoaCandidate(pkg: RecordedPackage): boolean {
81
- return pkg.tags.includes("server") || pkg.tags.includes("node");
82
- }
83
-
84
- /** `server`/`node` packages (never the generated `openapi` tag) with a tsoa import. */
85
- function controllerPackages(): object.Sequence<RecordedPackage> {
86
- return object.sequence(recordedPackages()).filter(isTsoaCandidate).filter(hasTsoaControllers);
87
- }
88
-
89
- function inside(path: string, directory: string): boolean {
90
- const child = relative(resolve(directory), resolve(path));
91
- return child === "" || (!child.startsWith("..") && !isAbsolute(child));
92
- }
93
-
94
- /** Source roots consumed by TypeScript OpenAPI producers. */
95
- export function openapiWatchRoots(): string[] {
96
- return [
97
- ...controllerPackages()
98
- .map((pkg) => resolve(tsoaSource(pkg)))
99
- .distinct(),
100
- ];
101
- }
102
-
103
- /** Whether one changed source belongs to an OpenAPI-producing package. */
104
- export function isOpenapiSource(path: string): boolean {
105
- const absolute = resolve(path);
106
- const segments = toPosix(relative(repoRoot, absolute)).split("/");
107
- if (segments.includes(OPENAPI_TAG)) return false;
108
- return object
109
- .sequence(recordedPackages())
110
- .filter(isTsoaCandidate)
111
- .filter((pkg) => inside(absolute, tsoaSource(pkg)))
112
- .some(hasTsoaControllers);
113
- }
114
-
115
- /** GitHub release asset name for Speakeasy's OpenAPI binary. */
116
- export function speakeasyOpenapiAssetName(
117
- platform: NodeJS.Platform = process.platform,
118
- arch: string = process.arch,
119
- ): string {
120
- const osName =
121
- platform === "darwin"
122
- ? "Darwin"
123
- : platform === "linux"
124
- ? "Linux"
125
- : platform === "win32"
126
- ? "Windows"
127
- : undefined;
128
- const archName = arch === "arm64" ? "arm64" : arch === "x64" ? "x86_64" : undefined;
129
- if (!osName || !archName) {
130
- throw new Error(`Speakeasy openapi has no supported release asset for ${platform}/${arch}`);
131
- }
132
- const extension = platform === "win32" ? "zip" : "tar.gz";
133
- return `openapi_${osName}_${archName}.${extension}`;
134
- }
135
-
136
- async function speakeasyOpenapiPath(): Promise<string> {
137
- const assetName = speakeasyOpenapiAssetName();
138
- const context = await bin.ensure("openapi", `${SPEAKEASY_OPENAPI_RELEASE_URL}/${assetName}`, {
139
- autoUnpackage: true,
140
- minVersion: SPEAKEASY_OPENAPI_VERSION,
141
- selector: ({ source }) =>
142
- join(source, process.platform === "win32" ? "openapi.exe" : "openapi"),
143
- versionParser: (output) => {
144
- const version = bin.parseVersion(output);
145
- return version === SPEAKEASY_OPENAPI_VERSION ? version : undefined;
146
- },
147
- });
148
- return context.path;
149
- }
150
-
151
- /** Deduplicate inline schemas into `components.schemas` with Speakeasy. */
152
- export async function optimizeOpenapiSpec(specPath: string, executable?: string): Promise<void> {
153
- const openapi = executable ?? (await speakeasyOpenapiPath());
154
- await exec.spawn(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"], {
155
- stdin: "ignore",
156
- check: true,
157
- });
158
- }
159
-
160
- type OpenApiTypeTools = {
161
- openapiTS: typeof import("openapi-typescript").default;
162
- astToString: typeof import("openapi-typescript").astToString;
163
- };
164
-
165
- async function writeClientPackage(
166
- outDir: string,
167
- source: string,
168
- tools: OpenApiTypeTools,
169
- ): Promise<void> {
170
- const srcDir = join(outDir, "src");
171
- mkdirSync(srcDir, { recursive: true });
172
- const specPath = join(outDir, "openapi.json");
173
- const spec = JSON.parse(readFileSync(specPath, "utf8"));
174
- const schemaPath = join(srcDir, "schema.ts");
175
- makeWritable(schemaPath);
176
- writeFileSync(schemaPath, tools.astToString(await tools.openapiTS(spec)));
177
- stampGenerated(schemaPath, {
178
- tool: "projen openapi (Speakeasy + openapi-typescript)",
179
- source,
180
- });
181
-
182
- const clientPath = join(srcDir, "client.ts");
183
- makeWritable(clientPath);
184
- writeFileSync(clientPath, CLIENT_SRC);
185
- stampGenerated(clientPath, {
186
- tool: "projen openapi (openapi-fetch)",
187
- source: "./schema",
188
- });
189
- }
190
-
191
- /**
192
- * Regenerate the `openapi` packages from every server/node package with a tsoa
193
- * import. Returns the package dirs it wrote so the caller can rebuild their barrels.
194
- * A separate projen synth is still needed before new openapi folders become workspace
195
- * members in `pnpm-workspace.yaml`.
196
- */
197
- export async function generateOpenapi(): Promise<string[]> {
198
- const pkgs = controllerPackages().toArray();
199
- // Same reasoning as codegen's empty case: a workspace with no tsoa controllers
200
- // is not a condition worth a line on every synth.
201
- if (pkgs.length === 0) {
202
- logger.debug("no TypeScript OpenAPI producers found");
203
- return [];
204
- }
205
-
206
- // Lazy, resilient loads: tsoa + typescript are CJS (require), openapi-typescript
207
- // is ESM (dynamic import).
208
- const { default: openapiTS, astToString } = await import("openapi-typescript");
209
- const tools = { openapiTS, astToString };
210
-
211
- const typescript =
212
- pkgs.length > 0
213
- ? (() => {
214
- const require = createRequire(import.meta.url);
215
- const { generateSpec } = lazyRequire<typeof import("tsoa")>(
216
- require,
217
- "tsoa",
218
- "openapi generation",
219
- );
220
- const runtime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
221
- return {
222
- generateSpec,
223
- runtime,
224
- };
225
- })()
226
- : undefined;
227
-
228
- const version = readWorkspaceVersion(repoRoot);
229
- const written: string[] = [];
230
- for (const p of pkgs) {
231
- // The generated package's folder is the source's leaf folder name (`api`), not
232
- // its npm name - `p.name` is the (possibly-overridden) manifest name.
233
- const leaf = p.relPath.split("/").pop() ?? p.relPath;
234
- const outDir = join(repoRoot, p.root, OPENAPI_TAG, leaf);
235
- mkdirSync(outDir, { recursive: true });
236
-
237
- // 1) tsoa writes a temporary openapi.json, Speakeasy optimizes it there, then
238
- // the complete spec moves into place so readers never observe an intermediate file.
239
- const specPath = join(outDir, "openapi.json");
240
- const tempDir = mkdtempSync(join(outDir, ".openapi-"));
241
- const tempSpecPath = join(tempDir, "openapi.json");
242
- try {
243
- const tsconfigPath = join(p.dir, "tsconfig.json");
244
- const config = typescript!.runtime.readConfigFile(
245
- tsconfigPath,
246
- typescript!.runtime.sys.readFile,
247
- );
248
- if (config.error) {
249
- throw new Error(
250
- typescript!.runtime.flattenDiagnosticMessageText(config.error.messageText, "\n"),
251
- );
252
- }
253
- const parsed = typescript!.runtime.parseJsonConfigFileContent(
254
- config.config,
255
- typescript!.runtime.sys,
256
- p.dir,
257
- );
258
- if (parsed.errors.length > 0) {
259
- throw new Error(
260
- parsed.errors
261
- .map((error) =>
262
- typescript!.runtime.flattenDiagnosticMessageText(error.messageText, "\n"),
263
- )
264
- .join("\n"),
265
- );
266
- }
267
- await typescript!.generateSpec(
268
- {
269
- entryFile: "",
270
- noImplicitAdditionalProperties: "throw-on-extras",
271
- controllerPathGlobs: [join(p.dir, "src/**/*.ts")],
272
- outputDirectory: tempDir,
273
- specFileBaseName: "openapi",
274
- specVersion: 3,
275
- name: `${p.relPath} API`,
276
- version,
277
- },
278
- parsed.options,
279
- );
280
- await optimizeOpenapiSpec(tempSpecPath);
281
- makeWritable(specPath);
282
- renameSync(tempSpecPath, specPath);
283
- } finally {
284
- rmSync(tempDir, { recursive: true, force: true });
285
- }
286
- makeReadonly(specPath);
287
- await writeClientPackage(outDir, `the tsoa controllers in ${p.relPath}`, tools);
288
-
289
- written.push(outDir);
290
- logger.success(`openapi/${leaf} (from ${p.relPath})`);
291
- }
292
- return written;
293
- }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- import { main } from "./lib/local-publish.ts";
3
-
4
- export * from "./lib/local-publish.ts";
5
-
6
- if (import.meta.main) await main();
package/tasks/openapi.ts DELETED
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- import { log, object, stringUtils } from "@dbx-tools/shared-core";
3
- import { generateBarrels } from "../src/barrels.ts";
4
- import { generateOpenapi, isOpenapiSource, openapiWatchRoots } from "../src/openapi.ts";
5
- import { runSynth } from "../src/scaffold.ts";
6
- import { watchLoop } from "../src/watch.ts";
7
-
8
- const logger = log.logger("projen:openapi");
9
-
10
- export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
11
- if (args.includes("--watch")) {
12
- // Watch the package roots; a changed tsoa controller regenerates the openapi
13
- // packages (spec + client) and rebuilds just their barrels. The projenrc watcher
14
- // (alongside under `concurrently`) owns full re-synth, so no re-synth here.
15
- watchLoop(
16
- "openapi",
17
- openapiWatchRoots(),
18
- async () => {
19
- const dirs = await generateOpenapi();
20
- if (dirs.length) {
21
- generateBarrels({ dirs });
22
- logger.success(`regenerated openapi (${stringUtils.pluralize(dirs.length, "package")})`);
23
- }
24
- },
25
- { check: (changed) => object.sequence(changed).some(isOpenapiSource) },
26
- );
27
- } else {
28
- // One-shot: regenerate, then re-synth so any newly created openapi folder becomes a
29
- // workspace member (a new package needs install + linking, which the watch path skips).
30
- const dirs = await generateOpenapi();
31
- if (dirs.length > 0) runSynth({ post: true });
32
- }
33
- }
34
-
35
- if (import.meta.main) await main();