@dbx-tools/projen 0.6.75 → 0.6.77

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/README.md CHANGED
@@ -47,6 +47,44 @@ The engine treats generated barrels, tests, declaration files, and folders
47
47
  without exported source modules as implementation details. They do not create
48
48
  new package membership.
49
49
 
50
+ ## Add A Python uv Workspace
51
+
52
+ `DBXToolsPythonWorkspace` attaches Python packaging to an existing projen root
53
+ without turning Python packages into JavaScript workspace projects. It generates
54
+ the root and member `pyproject.toml` files, standard `py:*` tasks, the VS Code
55
+ interpreter setting, and an optional manual PyPI trusted-publishing workflow.
56
+
57
+ ```ts
58
+ import { project, projectPy } from "@dbx-tools/projen";
59
+
60
+ const root = new project.DBXToolsNodeProject({ name: "my-apps" });
61
+ const repository = {
62
+ url: "https://github.com/example/my-apps.git",
63
+ ref: "main",
64
+ root: "python/packages",
65
+ } as const;
66
+
67
+ new projectPy.DBXToolsPythonWorkspace(root, {
68
+ repository,
69
+ packages: [
70
+ {
71
+ directory: "core",
72
+ name: "my-apps-core",
73
+ module: "my_apps.core",
74
+ description: "Shared Python helpers",
75
+ },
76
+ ],
77
+ interpreterPath: "${workspaceFolder}/python/.venv/bin/python",
78
+ release: true,
79
+ });
80
+ ```
81
+
82
+ Use `projectPy.pythonGitDependency(repository, name, directory)` for an internal
83
+ dependency that must also resolve when a package is installed directly through a
84
+ Git `#subdirectory=` URL. `root.vscode` is projen's existing VS Code component;
85
+ dbx-tools reuses it rather than constructing a second `.vscode/settings.json`
86
+ owner.
87
+
50
88
  ## Customize Packages With Mixins
51
89
 
52
90
  ```ts
package/index.ts CHANGED
@@ -15,7 +15,9 @@ export * as openapi from "./src/openapi.ts";
15
15
  export * as packages from "./src/packages.ts";
16
16
  export * as pnpmWorkspace from "./src/pnpm-workspace.ts";
17
17
  export * as project from "./src/project.ts";
18
+ export * as projectJs from "./src/project-js.ts";
18
19
  export * as projectPredicate from "./src/project-predicate.ts";
20
+ export * as projectPy from "./src/project-py.ts";
19
21
  export * as publish from "./src/publish.ts";
20
22
  export * as release from "./src/release.ts";
21
23
  export * as scaffold from "./src/scaffold.ts";
@@ -23,15 +25,7 @@ export * as tags from "./src/tags.ts";
23
25
  export * as tsconfig from "./src/tsconfig.ts";
24
26
  export * as vscode from "./src/vscode.ts";
25
27
  export * as watch from "./src/watch.ts";
26
- export {
27
- BUN_DEV_OVERRIDE,
28
- BUN_BUILD_OVERRIDE,
29
- BUN_APP_OVERRIDES,
30
- RootBunfigFile,
31
- BunfigFile,
32
- BunDevServerFile,
33
- BunBuildFile,
34
- } from "./src/bun-app.ts";
28
+ export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
35
29
  export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
36
30
  export type { DBXToolsConfigOptions } from "./src/dbx-tools-config.ts";
37
31
  export { resolvePkgRoot } from "./src/engine-root.ts";
@@ -42,18 +36,11 @@ export { repoRoot, DEFAULT_PACKAGE_ROOTS, DiscoveredPackage } from "./src/packag
42
36
  export type { RecordedPackage } from "./src/packages.ts";
43
37
  export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
44
38
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
45
- export {
46
- PackageIdentifier,
47
- PROJEN_VERSION,
48
- DBXToolsNodeProject,
49
- DBXToolsTypeScriptProject,
50
- } from "./src/project.ts";
51
- export type {
52
- DBXToolsProject,
53
- DBXToolsProjectOptions,
54
- DBXToolsTypeScriptProjectOptions,
55
- ApplyToProjectsOptions,
56
- } from "./src/project.ts";
39
+ export type { ApplyToProjectsOptions } from "./src/project.ts";
40
+ export { PackageIdentifier, PROJEN_VERSION, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
41
+ export type { DBXToolsProject, DBXToolsProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
42
+ export { DBXToolsPythonWorkspace } from "./src/project-py.ts";
43
+ export type { PythonRepositoryOptions, PythonPackageOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
57
44
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
58
45
  export { DBXToolsRelease } from "./src/release.ts";
59
46
  export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts";
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.77",
30
+ "@dbx-tools/path": "0.6.77",
31
+ "@dbx-tools/shared-core": "0.6.77",
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.77",
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