@dbx-tools/projen 0.6.75 → 0.6.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.75",
30
- "@dbx-tools/path": "0.6.75",
31
- "@dbx-tools/shared-core": "0.6.75",
29
+ "@dbx-tools/core": "0.6.76",
30
+ "@dbx-tools/path": "0.6.76",
31
+ "@dbx-tools/shared-core": "0.6.76",
32
32
  "commander": "^15.0.0",
33
33
  "concurrently": "^10.0.3",
34
34
  "constructs": "^10.6.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "main": "index.ts",
49
49
  "license": "Apache-2.0",
50
- "version": "0.6.75",
50
+ "version": "0.6.76",
51
51
  "types": "index.ts",
52
52
  "type": "module",
53
53
  "exports": {
package/src/bun-app.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Bun browser-app scaffolding as first-class projen file components.
3
3
  *
4
- * Replaces the old Vite toolchain (`vite.ts`). An `app`-tagged package gets three
5
- * generated, read-only files that stand in for `vite dev` / `vite build`:
4
+ * An `app`-tagged package gets three generated, read-only files that provide its
5
+ * whole dev/build toolchain:
6
6
  *
7
7
  * - {@link BunfigFile} - `bunfig.toml` wiring `bun-plugin-tailwind` into both the
8
8
  * dev server (`[serve.static]`) and `Bun.build` (`[build]`) so Tailwind v4's
@@ -17,8 +17,7 @@
17
17
  * Each file supports an unmanaged OVERRIDE beside it (`bunfig.override.toml`,
18
18
  * `bun-dev.override.ts`, `bun-build.override.ts`): the dev/build scripts import
19
19
  * the override's default export and merge it over the generated options, so a
20
- * package tweaks its server/bundle WITHOUT editing the projen-owned file - the
21
- * same escape hatch the Vite generator offered via `vite.config.override.*`.
20
+ * package tweaks its server/bundle WITHOUT editing the projen-owned file.
22
21
  *
23
22
  * bun runs these `.ts` files directly. Being package-ROOT files (not under
24
23
  * `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
@@ -42,8 +41,7 @@ export const BUN_APP_OVERRIDES = ["bunfig.override.toml", BUN_DEV_OVERRIDE, BUN_
42
41
  * rejects passing a value built against one to an API typed by the other
43
42
  * ("separate declarations of a private property"). The hoisted linker de-dupes to
44
43
  * a single flat copy (npm-style), which is what keeps identity-based singletons
45
- * (AppKit `CacheManager`, Mastra classes, one React) coherent - the same coherence
46
- * the old cross-workspace `.pnpmfile.cjs` bridge used to guarantee.
44
+ * (AppKit `CacheManager`, Mastra classes, one React) coherent.
47
45
  */
48
46
  export class RootBunfigFile extends TextFile {
49
47
  constructor(project: Project) {
package/src/openapi.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * (`from 'tsoa'` / `from '@tsoa/runtime'`) and, for each package that has them,
6
6
  * generates a read-only `<root>/openapi/<name>` package:
7
7
  *
8
- * - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, from the types).
8
+ * - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, then Speakeasy
9
+ * optimization to extract duplicate inline schemas into components).
9
10
  * - `src/schema.ts` - types generated from the spec (openapi-typescript).
10
11
  * - `src/client.ts` - a typed `openapi-fetch` client, usable client-side.
11
12
  *
@@ -18,11 +19,23 @@
18
19
  * `tsoa`, `typescript`, and `openapi-typescript` are loaded lazily (heavy, and only
19
20
  * needed for `bun run openapi`), so importing this module stays cheap. `tsoa` and
20
21
  * `typescript` are not engine dependencies at all - both are resolved out of the
21
- * consuming workspace, which is where they already live.
22
+ * consuming workspace, which is where they already live. Speakeasy's `openapi`
23
+ * binary is installed lazily through `@dbx-tools/core`'s binary cache.
22
24
  */
23
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { execFile } from "node:child_process";
26
+ import {
27
+ existsSync,
28
+ mkdirSync,
29
+ mkdtempSync,
30
+ readFileSync,
31
+ renameSync,
32
+ rmSync,
33
+ writeFileSync,
34
+ } from "node:fs";
24
35
  import { createRequire } from "node:module";
25
36
  import { join } from "node:path";
37
+ import { promisify } from "node:util";
38
+ import { bin } from "@dbx-tools/core";
26
39
  import { find } from "@dbx-tools/path";
27
40
  import { log } from "@dbx-tools/shared-core";
28
41
  import type * as ts from "typescript";
@@ -42,6 +55,9 @@ const logger = log.logger("projen:openapi");
42
55
  const OPENAPI_TAG = "openapi";
43
56
  /** Heuristic: a module file whose source imports tsoa's runtime package. */
44
57
  const TSOA_IMPORT = /from\s+['"](?:tsoa|@tsoa\/runtime)['"]/;
58
+ const SPEAKEASY_OPENAPI_VERSION = "1.24.0";
59
+ const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/releases/download/v${SPEAKEASY_OPENAPI_VERSION}`;
60
+ const execFileAsync = promisify(execFile);
45
61
 
46
62
  const CLIENT_SRC = `import createClient, { type ClientOptions } from "openapi-fetch";
47
63
  import type { paths } from "./schema";
@@ -78,6 +94,48 @@ export function isTsoaController(path: string): boolean {
78
94
  );
79
95
  }
80
96
 
97
+ /** GitHub release asset name for Speakeasy's OpenAPI binary. */
98
+ export function speakeasyOpenapiAssetName(
99
+ platform: NodeJS.Platform = process.platform,
100
+ arch: string = process.arch,
101
+ ): string {
102
+ const osName =
103
+ platform === "darwin"
104
+ ? "Darwin"
105
+ : platform === "linux"
106
+ ? "Linux"
107
+ : platform === "win32"
108
+ ? "Windows"
109
+ : undefined;
110
+ const archName = arch === "arm64" ? "arm64" : arch === "x64" ? "x86_64" : undefined;
111
+ if (!osName || !archName) {
112
+ throw new Error(`Speakeasy openapi has no supported release asset for ${platform}/${arch}`);
113
+ }
114
+ const extension = platform === "win32" ? "zip" : "tar.gz";
115
+ return `openapi_${osName}_${archName}.${extension}`;
116
+ }
117
+
118
+ async function speakeasyOpenapiPath(): Promise<string> {
119
+ const assetName = speakeasyOpenapiAssetName();
120
+ const context = await bin.ensure("openapi", `${SPEAKEASY_OPENAPI_RELEASE_URL}/${assetName}`, {
121
+ autoUnpackage: true,
122
+ minVersion: SPEAKEASY_OPENAPI_VERSION,
123
+ selector: ({ source }) =>
124
+ join(source, process.platform === "win32" ? "openapi.exe" : "openapi"),
125
+ versionParser: (output) => {
126
+ const version = bin.parseVersion(output);
127
+ return version === SPEAKEASY_OPENAPI_VERSION ? version : undefined;
128
+ },
129
+ });
130
+ return context.path;
131
+ }
132
+
133
+ /** Deduplicate inline schemas into `components.schemas` with Speakeasy. */
134
+ export async function optimizeOpenapiSpec(specPath: string, executable?: string): Promise<void> {
135
+ const openapi = executable ?? (await speakeasyOpenapiPath());
136
+ await execFileAsync(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"]);
137
+ }
138
+
81
139
  /**
82
140
  * Regenerate the `openapi` packages from every server/node package with a tsoa
83
141
  * import. Returns the package dirs it wrote so the caller can rebuild their barrels.
@@ -118,28 +176,37 @@ export async function generateOpenapi(): Promise<string[]> {
118
176
  const written: string[] = [];
119
177
  for (const p of pkgs) {
120
178
  // The generated package's folder is the source's leaf folder name (`api`), not
121
- // its npm name - `p.name` is now the (possibly-overridden) manifest name.
179
+ // its npm name - `p.name` is the (possibly-overridden) manifest name.
122
180
  const leaf = p.relPath.split("/").pop() ?? p.relPath;
123
181
  const outDir = join(repoRoot, p.root, OPENAPI_TAG, leaf);
124
182
  const srcDir = join(outDir, "src");
125
183
  mkdirSync(srcDir, { recursive: true });
126
184
 
127
- // 1) tsoa writes <outDir>/openapi.json from the controllers' decorators + types.
185
+ // 1) tsoa writes a temporary openapi.json, Speakeasy optimizes it there, then
186
+ // the complete spec moves into place so readers never observe an intermediate file.
128
187
  const specPath = join(outDir, "openapi.json");
129
- makeWritable(specPath);
130
- await generateSpec(
131
- {
132
- entryFile: "",
133
- noImplicitAdditionalProperties: "throw-on-extras",
134
- controllerPathGlobs: [join(p.dir, "src/**/*.ts")],
135
- outputDirectory: outDir,
136
- specFileBaseName: "openapi",
137
- specVersion: 3,
138
- name: `${p.relPath} API`,
139
- version: "0.0.0",
140
- },
141
- compilerOptions,
142
- );
188
+ const tempDir = mkdtempSync(join(outDir, ".openapi-"));
189
+ const tempSpecPath = join(tempDir, "openapi.json");
190
+ try {
191
+ await generateSpec(
192
+ {
193
+ entryFile: "",
194
+ noImplicitAdditionalProperties: "throw-on-extras",
195
+ controllerPathGlobs: [join(p.dir, "src/**/*.ts")],
196
+ outputDirectory: tempDir,
197
+ specFileBaseName: "openapi",
198
+ specVersion: 3,
199
+ name: `${p.relPath} API`,
200
+ version: "0.0.0",
201
+ },
202
+ compilerOptions,
203
+ );
204
+ await optimizeOpenapiSpec(tempSpecPath);
205
+ makeWritable(specPath);
206
+ renameSync(tempSpecPath, specPath);
207
+ } finally {
208
+ rmSync(tempDir, { recursive: true, force: true });
209
+ }
143
210
  makeReadonly(specPath);
144
211
 
145
212
  // 2) src/schema.ts: types generated from the spec (openapi-typescript).
@@ -148,7 +215,7 @@ export async function generateOpenapi(): Promise<string[]> {
148
215
  makeWritable(schemaPath);
149
216
  writeFileSync(schemaPath, astToString(await openapiTS(spec)));
150
217
  stampGenerated(schemaPath, {
151
- tool: "projen openapi (tsoa + openapi-typescript)",
218
+ tool: "projen openapi (tsoa + Speakeasy + openapi-typescript)",
152
219
  source: `the tsoa controllers in ${p.relPath}`,
153
220
  });
154
221
 
package/src/project.ts CHANGED
@@ -203,8 +203,8 @@ export function applyTasks(pkg: javascript.NodeProject, tasks?: Record<string, T
203
203
  */
204
204
  export function applyExports(pkg: javascript.NodeProject, exports: Record<string, string>): void {
205
205
  pkg.package.addField("exports", exports);
206
- // Keep the legacy entry points honest about the map that just replaced them.
207
- // A subpath-only surface (the `ui` tag's `./react` + `./styles.css`) has no
206
+ // Keep the `main`/`types` entry points consistent with the map. A subpath-only
207
+ // surface (the `ui` tag's `./react` + `./styles.css`) has no
208
208
  // `.` export, so the constructor's `main`/`types` would keep advertising a
209
209
  // root entry that every exports-aware resolver ignores - the contradiction
210
210
  // publint reports as "exports is missing the root entrypoint".
@@ -673,8 +673,8 @@ export class DBXToolsTypeScriptProject
673
673
  // `bun test` intercepts `node:test` (the suites keep using node:test) and
674
674
  // runs it with bun's own fast runner. Args are FILTERS, not globs; a bare
675
675
  // directory auto-discovers `*.test.ts` recursively. But `bun test` EXITS 1
676
- // when it matches no files (unlike the old `tsx --test 'glob'`, which was a
677
- // no-op), so guard it: only invoke when a `*.test.ts` exists, else succeed.
676
+ // when it matches no files, so guard it: only invoke when a `*.test.ts`
677
+ // exists, else succeed.
678
678
  this.testTask.exec("bun test test", {
679
679
  condition: 'find test -name "*.test.ts" 2>/dev/null | grep -q .',
680
680
  });
package/src/release.ts CHANGED
@@ -88,9 +88,9 @@ function publishSetupSteps(): JobStep[] {
88
88
  *
89
89
  * Declaring one also enlists it in the root's `bump`, which cuts BOTH tags at one
90
90
  * shared version. The separate tag namespace still lets it be released alone
91
- * (`cd <directory> && bun run bump`) for a consumer who wants only this package -
92
- * but a routine root bump no longer leaves it behind, which is how the engine
93
- * drifted to 0.1.24 while the packages reached 0.3.41.
91
+ * (`cd <directory> && bun run bump`) for a consumer who wants only this package.
92
+ * Enlisting it is what keeps a routine root bump from leaving it behind and letting
93
+ * its version drift away from the packages'.
94
94
  */
95
95
  export interface StandaloneRelease {
96
96
  /** Workflow name (and `.github/workflows/<name>.yml` file). E.g. `projen-release`. */
package/src/tags.ts CHANGED
@@ -105,7 +105,7 @@ export const PACKAGE_TAG_MIXINS = {
105
105
  // generated `dev.ts`/`build.ts` use; no `vite/client`.
106
106
  types: ["bun"],
107
107
  // `@/` -> `src/` alias, resolved by both tsc and bun's bundler (bun reads
108
- // tsconfig `paths`). Replaces the old Vite `resolve.alias` for `@`.
108
+ // tsconfig `paths`).
109
109
  baseUrl: ".",
110
110
  paths: { "@/*": ["./src/*"] },
111
111
  });
package/src/tsconfig.ts CHANGED
@@ -5,9 +5,9 @@ import { Component, JsonFile, type Project, javascript } from "projen";
5
5
 
6
6
  /**
7
7
  * Compiler options for the ROOT program only (`.projenrc.ts` + the engine as
8
- * seen from the root). Each package now owns its own projen-generated
8
+ * seen from the root). Each package owns its own projen-generated
9
9
  * `tsconfig.json` (scope `lib`/`jsx`/`types` overlaid on projen's defaults), so
10
- * this base no longer feeds packages - it just gives the projenrc/editor program
10
+ * this base does not feed packages - it just gives the projenrc/editor program
11
11
  * a sane ESM config. `lib` is set narrowly in the root `tsconfig.json`.
12
12
  */
13
13
  const BASE_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {