@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/tasks/sync.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ /** Synthesize once or supervise the focused Projen and barrel watch loops. */
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { log } from "@dbx-tools/shared-core";
4
5
  import concurrently from "concurrently";
@@ -46,8 +47,8 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
46
47
  // Watch: one initial full synth to bring the tree up to date, then focused
47
48
  // watchers under `concurrently`. The projenrc watcher is the intelligent stand-in
48
49
  // for stock `projen --watch` - it re-synths (+install) ONLY when `.projenrc.ts` or
49
- // a configured `syncResynthPaths` entry changes, while barrels/openapi keep generated
50
- // OUTPUT fresh on source edits with no full synth.
50
+ // a configured `syncResynthPaths` entry changes, while the barrel watcher keeps
51
+ // generated package exports fresh on source edits with no full synth.
51
52
  //
52
53
  // VS Code auto-runs this on folder open, so from here down nothing is allowed to be
53
54
  // fatal: errors are logged and retried, and only a stop signal ends the task.
@@ -67,11 +68,10 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
67
68
  const watchers = [
68
69
  watcher("projenrc.ts", "projenrc", "magenta"),
69
70
  watcher("barrels.ts", "barrels", "cyan", "--watch"),
70
- watcher("openapi.ts", "openapi", "green", "--watch"),
71
71
  ];
72
72
  const { result } = concurrently(watchers, {
73
73
  prefix: "name",
74
- // No `killOthersOn`: one watcher falling over is no reason to tear the other two
74
+ // No `killOthersOn`: one watcher falling over is no reason to tear the other
75
75
  // down. `-1` is concurrently's spelling for "restart forever", so a crashed
76
76
  // watcher comes back instead of silently leaving its outputs stale.
77
77
  restartTries: -1,
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();