@dbx-tools/projen 0.9.12 → 0.9.17
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 +20 -15
- package/index.ts +4 -3
- package/package.json +4 -4
- package/src/_rust-project.ts +37 -13
- package/src/_rust-release-workflow.ts +267 -164
- package/src/_rust-release.ts +38 -10
- package/src/openapi.ts +191 -79
- package/src/project-py.ts +4 -4
- package/src/project-rs.ts +57 -6
- package/src/release-plan.ts +57 -16
- package/src/release.ts +2 -1
- package/src/watch.ts +19 -2
- package/src/workspace-lock.ts +32 -6
- package/tasks/barrels.ts +47 -30
- package/tasks/openapi.ts +15 -11
- package/tasks/release-plan.ts +2 -1
- package/tasks/release-pr.ts +1 -1
- package/tasks/rust-release.d.ts +9 -5
- package/tasks/rust-release.mjs +20 -11
- package/tasks/rust.ts +62 -33
- package/tasks/uniffi-release.mjs +5 -1
- package/tasks/uniffi.ts +2 -0
package/src/_rust-release.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { parseArgs } from "node:util";
|
|
|
29
29
|
import * as exec from "@dbx-tools/core/exec";
|
|
30
30
|
import { parse, stringify, type TomlTable } from "smol-toml";
|
|
31
31
|
|
|
32
|
-
export const FINGERPRINT_SCHEMA =
|
|
32
|
+
export const FINGERPRINT_SCHEMA = 4;
|
|
33
33
|
export const VERSION_SLOT_SCHEMA = 1;
|
|
34
34
|
export const VERSION_CAPACITY = 64;
|
|
35
35
|
|
|
@@ -161,11 +161,18 @@ function toPosix(path: string): string {
|
|
|
161
161
|
return path.split(sep).join("/");
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function sourceRoots(
|
|
164
|
+
function sourceRoots(
|
|
165
|
+
root: string,
|
|
166
|
+
metadata: CargoMetadata,
|
|
167
|
+
sources: readonly string[],
|
|
168
|
+
includeWorkspace: boolean,
|
|
169
|
+
): string[] {
|
|
165
170
|
const members = new Set(metadata.workspace_members);
|
|
166
|
-
const discovered =
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
const discovered = includeWorkspace
|
|
172
|
+
? metadata.packages
|
|
173
|
+
.filter((candidate) => members.has(candidate.id))
|
|
174
|
+
.map((candidate) => dirname(candidate.manifest_path))
|
|
175
|
+
: [];
|
|
169
176
|
return [...new Set([...discovered, ...sources.map((source) => resolve(root, source))])]
|
|
170
177
|
.map((path) => toPosix(relative(root, path)) || ".")
|
|
171
178
|
.sort();
|
|
@@ -243,8 +250,12 @@ function normalizedInput(
|
|
|
243
250
|
return content;
|
|
244
251
|
}
|
|
245
252
|
|
|
246
|
-
/** Hash Cargo
|
|
247
|
-
export function sourceHash(
|
|
253
|
+
/** Hash shared Cargo inputs plus selected or Cargo-discovered workspace sources. */
|
|
254
|
+
export function sourceHash(
|
|
255
|
+
root: string,
|
|
256
|
+
sources: readonly string[] = [],
|
|
257
|
+
includeWorkspace = true,
|
|
258
|
+
): string {
|
|
248
259
|
const resolvedRoot = resolve(root);
|
|
249
260
|
const metadata = cargoMetadata(resolvedRoot);
|
|
250
261
|
const members = new Set(metadata.workspace_members);
|
|
@@ -254,7 +265,10 @@ export function sourceHash(root: string, sources: readonly string[] = []): strin
|
|
|
254
265
|
.map((candidate) => candidate.name),
|
|
255
266
|
);
|
|
256
267
|
const hash = createHash("sha256");
|
|
257
|
-
for (const file of inputFiles(
|
|
268
|
+
for (const file of inputFiles(
|
|
269
|
+
resolvedRoot,
|
|
270
|
+
sourceRoots(resolvedRoot, metadata, sources, includeWorkspace),
|
|
271
|
+
)) {
|
|
258
272
|
const absolute = join(resolvedRoot, file);
|
|
259
273
|
const stat = lstatSync(absolute);
|
|
260
274
|
hash.update(file);
|
|
@@ -280,6 +294,7 @@ export function linkerIdentity(target: string): string {
|
|
|
280
294
|
|
|
281
295
|
export interface TargetKeyOptions {
|
|
282
296
|
readonly rustSourceHash: string;
|
|
297
|
+
readonly namespace?: string;
|
|
283
298
|
readonly target: string;
|
|
284
299
|
readonly targetConfig?: string;
|
|
285
300
|
readonly toolchain?: string;
|
|
@@ -288,6 +303,7 @@ export interface TargetKeyOptions {
|
|
|
288
303
|
|
|
289
304
|
export function targetKey({
|
|
290
305
|
rustSourceHash,
|
|
306
|
+
namespace = "workspace",
|
|
291
307
|
target,
|
|
292
308
|
targetConfig = "",
|
|
293
309
|
toolchain = "stable",
|
|
@@ -297,6 +313,7 @@ export function targetKey({
|
|
|
297
313
|
for (const value of [
|
|
298
314
|
String(FINGERPRINT_SCHEMA),
|
|
299
315
|
String(VERSION_SLOT_SCHEMA),
|
|
316
|
+
namespace,
|
|
300
317
|
rustSourceHash,
|
|
301
318
|
target,
|
|
302
319
|
targetConfig,
|
|
@@ -327,11 +344,14 @@ export interface FingerprintOptions {
|
|
|
327
344
|
readonly toolchain?: string;
|
|
328
345
|
readonly portable?: boolean;
|
|
329
346
|
readonly sources?: readonly string[];
|
|
347
|
+
readonly sourceOnly?: boolean;
|
|
348
|
+
readonly namespace?: string;
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
export interface RustBuildFingerprint {
|
|
333
352
|
readonly schemaVersion: number;
|
|
334
353
|
readonly versionSlotSchema: number;
|
|
354
|
+
readonly namespace: string;
|
|
335
355
|
readonly rustSourceHash: string;
|
|
336
356
|
readonly targets: Readonly<Record<string, string>>;
|
|
337
357
|
}
|
|
@@ -344,10 +364,12 @@ export function fingerprint({
|
|
|
344
364
|
toolchain = "stable",
|
|
345
365
|
portable = false,
|
|
346
366
|
sources = [],
|
|
367
|
+
sourceOnly = false,
|
|
368
|
+
namespace = "workspace",
|
|
347
369
|
}: FingerprintOptions): RustBuildFingerprint {
|
|
348
370
|
if (!targets.length) throw new Error("at least one --target is required");
|
|
349
371
|
const resolvedRoot = resolve(root);
|
|
350
|
-
const rustSourceHash = sourceHash(resolvedRoot, sources);
|
|
372
|
+
const rustSourceHash = sourceHash(resolvedRoot, sources, !sourceOnly);
|
|
351
373
|
const rustc = portable ? undefined : rustcIdentity();
|
|
352
374
|
const targetEntries = targets
|
|
353
375
|
.map((specification) => {
|
|
@@ -356,13 +378,14 @@ export function fingerprint({
|
|
|
356
378
|
const targetConfig = separator < 0 ? "" : specification.slice(separator + 1);
|
|
357
379
|
return [
|
|
358
380
|
target,
|
|
359
|
-
targetKey({ rustSourceHash, target, targetConfig, toolchain, rustc }),
|
|
381
|
+
targetKey({ rustSourceHash, namespace, target, targetConfig, toolchain, rustc }),
|
|
360
382
|
] as const;
|
|
361
383
|
})
|
|
362
384
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
363
385
|
const manifest = {
|
|
364
386
|
schemaVersion: FINGERPRINT_SCHEMA,
|
|
365
387
|
versionSlotSchema: VERSION_SLOT_SCHEMA,
|
|
388
|
+
namespace,
|
|
366
389
|
rustSourceHash,
|
|
367
390
|
targets: Object.fromEntries(targetEntries),
|
|
368
391
|
};
|
|
@@ -373,6 +396,7 @@ export function fingerprint({
|
|
|
373
396
|
if (
|
|
374
397
|
current.schemaVersion !== manifest.schemaVersion ||
|
|
375
398
|
current.versionSlotSchema !== manifest.versionSlotSchema ||
|
|
399
|
+
current.namespace !== manifest.namespace ||
|
|
376
400
|
current.rustSourceHash !== manifest.rustSourceHash ||
|
|
377
401
|
!targetsMatch
|
|
378
402
|
) {
|
|
@@ -538,6 +562,8 @@ function fingerprintCommand(args: readonly string[]): void {
|
|
|
538
562
|
toolchain: { type: "string", default: "stable" },
|
|
539
563
|
portable: { type: "boolean", default: false },
|
|
540
564
|
source: { type: "string", multiple: true },
|
|
565
|
+
"source-only": { type: "boolean", default: false },
|
|
566
|
+
namespace: { type: "string", default: "workspace" },
|
|
541
567
|
},
|
|
542
568
|
strict: true,
|
|
543
569
|
});
|
|
@@ -549,6 +575,8 @@ function fingerprintCommand(args: readonly string[]): void {
|
|
|
549
575
|
toolchain: values.toolchain,
|
|
550
576
|
portable: values.portable,
|
|
551
577
|
sources: values.source ?? [],
|
|
578
|
+
sourceOnly: values["source-only"],
|
|
579
|
+
namespace: values.namespace,
|
|
552
580
|
});
|
|
553
581
|
}
|
|
554
582
|
|
package/src/openapi.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAPI generator
|
|
2
|
+
* OpenAPI generator for tsoa controllers and code-first Rust producers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* TypeScript packages are discovered from tsoa imports. Rust producers are
|
|
5
|
+
* recorded by `DBXToolsRustWorkspace`. Both feed the same optimized generated
|
|
6
|
+
* package:
|
|
7
7
|
*
|
|
8
8
|
* - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, then Speakeasy
|
|
9
9
|
* optimization to extract duplicate inline schemas into components).
|
|
@@ -33,11 +33,11 @@ import {
|
|
|
33
33
|
writeFileSync,
|
|
34
34
|
} from "node:fs";
|
|
35
35
|
import { createRequire } from "node:module";
|
|
36
|
-
import { join } from "node:path";
|
|
36
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
37
37
|
import { promisify } from "node:util";
|
|
38
38
|
import * as bin from "@dbx-tools/core/bin";
|
|
39
39
|
import { find } from "@dbx-tools/path";
|
|
40
|
-
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
40
|
+
import { log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
41
41
|
import type * as ts from "typescript";
|
|
42
42
|
import { lazyRequire } from "./_lazy-require.ts";
|
|
43
43
|
import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
toPosix,
|
|
49
49
|
recordedPackages,
|
|
50
50
|
} from "./packages.ts";
|
|
51
|
+
import type { RustOpenApiMapping, RustWorkspaceMapping } from "./project-rs.ts";
|
|
51
52
|
import type { ReleaseUnitGraph } from "./release-catalog.ts";
|
|
52
53
|
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
53
54
|
|
|
@@ -62,45 +63,95 @@ const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/
|
|
|
62
63
|
const execFileAsync = promisify(execFile);
|
|
63
64
|
|
|
64
65
|
// prettier-ignore
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const CLIENT_SRC =
|
|
67
|
+
stringUtils.dedent(
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/*ts*/`
|
|
70
|
+
import createClient, { type ClientOptions } from "openapi-fetch";
|
|
71
|
+
import type { paths } from "./schema";
|
|
72
|
+
|
|
73
|
+
/** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
|
|
74
|
+
export function createApiClient(options?: ClientOptions) {
|
|
75
|
+
return createClient<paths>(options);
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
// ============================================================================
|
|
79
|
+
) + "\n";
|
|
79
80
|
|
|
80
81
|
/** True if any module file in `<pkg>/src` matches {@link TSOA_IMPORT}. */
|
|
81
82
|
function hasTsoaControllers(pkg: Pick<RecordedPackage, "dir">): boolean {
|
|
82
|
-
const srcDir =
|
|
83
|
-
return
|
|
83
|
+
const srcDir = tsoaSource(pkg);
|
|
84
|
+
return object
|
|
85
|
+
.sequence(find.findFiles("**/*", { cwd: srcDir }))
|
|
84
86
|
.filter(isModuleFile)
|
|
85
|
-
.some((
|
|
87
|
+
.some((file) => TSOA_IMPORT.test(readFileSync(join(srcDir, file), "utf8")));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tsoaSource(pkg: Pick<RecordedPackage, "dir">): string {
|
|
91
|
+
return join(pkg.dir, "src");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isTsoaCandidate(pkg: RecordedPackage): boolean {
|
|
95
|
+
return pkg.tags.includes("server") || pkg.tags.includes("node");
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/** `server`/`node` packages (never the generated `openapi` tag) with a tsoa import. */
|
|
89
|
-
function controllerPackages(): RecordedPackage
|
|
90
|
-
return recordedPackages().filter(
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
function controllerPackages(): object.Sequence<RecordedPackage> {
|
|
100
|
+
return object.sequence(recordedPackages()).filter(isTsoaCandidate).filter(hasTsoaControllers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rustOpenapiMappings(): RustOpenApiMapping[] {
|
|
104
|
+
const manifest = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as unknown;
|
|
105
|
+
if (!object.isRecord(manifest)) return [];
|
|
106
|
+
const config = manifest.dbxToolsConfig;
|
|
107
|
+
if (!object.isRecord(config) || !object.isRecord(config.rust)) return [];
|
|
108
|
+
const mapping = config.rust as unknown as Partial<RustWorkspaceMapping>;
|
|
109
|
+
return Array.isArray(mapping.openapi) ? [...mapping.openapi] : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inside(path: string, directory: string): boolean {
|
|
113
|
+
const child = relative(resolve(directory), resolve(path));
|
|
114
|
+
return child === "" || (!child.startsWith("..") && !isAbsolute(child));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether one changed source belongs to any producing package directory. */
|
|
118
|
+
export function isOpenapiProducerSource(path: string, producers: readonly string[]): boolean {
|
|
119
|
+
const absolute = resolve(path);
|
|
120
|
+
return object.sequence(producers).some((producer) => inside(absolute, producer));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rustOpenapiSources(mapping: RustOpenApiMapping): string[] {
|
|
124
|
+
return [resolve(repoRoot, mapping.rust, "src"), resolve(repoRoot, mapping.rust, "Cargo.toml")];
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
/**
|
|
96
|
-
export function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
/** Source roots consumed by TypeScript and Rust OpenAPI producers. */
|
|
128
|
+
export function openapiWatchRoots(): string[] {
|
|
129
|
+
return [
|
|
130
|
+
...controllerPackages()
|
|
131
|
+
.map((pkg) => resolve(tsoaSource(pkg)))
|
|
132
|
+
.join(object.sequence(rustOpenapiMappings()).flatMap(rustOpenapiSources))
|
|
133
|
+
.distinct(),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether one changed source belongs to an OpenAPI-producing package. */
|
|
138
|
+
export function isOpenapiSource(path: string): boolean {
|
|
139
|
+
const absolute = resolve(path);
|
|
140
|
+
const segments = toPosix(relative(repoRoot, absolute)).split("/");
|
|
141
|
+
if (segments.includes(OPENAPI_TAG)) return false;
|
|
142
|
+
if (
|
|
143
|
+
object
|
|
144
|
+
.sequence(rustOpenapiMappings())
|
|
145
|
+
.flatMap(rustOpenapiSources)
|
|
146
|
+
.some((source) => inside(absolute, source))
|
|
147
|
+
) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return object
|
|
151
|
+
.sequence(recordedPackages())
|
|
152
|
+
.filter(isTsoaCandidate)
|
|
153
|
+
.filter((pkg) => inside(absolute, tsoaSource(pkg)))
|
|
154
|
+
.some(hasTsoaControllers);
|
|
104
155
|
}
|
|
105
156
|
|
|
106
157
|
/** GitHub release asset name for Speakeasy's OpenAPI binary. */
|
|
@@ -145,6 +196,59 @@ export async function optimizeOpenapiSpec(specPath: string, executable?: string)
|
|
|
145
196
|
await execFileAsync(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"]);
|
|
146
197
|
}
|
|
147
198
|
|
|
199
|
+
/** Cargo arguments that export one Rust producer's aide document. */
|
|
200
|
+
export function rustOpenapiArgs(
|
|
201
|
+
mapping: RustOpenApiMapping,
|
|
202
|
+
output: string,
|
|
203
|
+
targetDir = join(repoRoot, "target/openapi"),
|
|
204
|
+
): string[] {
|
|
205
|
+
return [
|
|
206
|
+
"run",
|
|
207
|
+
"--quiet",
|
|
208
|
+
"--target-dir",
|
|
209
|
+
targetDir,
|
|
210
|
+
"--package",
|
|
211
|
+
mapping.crate,
|
|
212
|
+
...(mapping.noDefaultFeatures ? ["--no-default-features"] : []),
|
|
213
|
+
...(mapping.features.length ? ["--features", mapping.features.join(",")] : []),
|
|
214
|
+
...(mapping.binary ? ["--bin", mapping.binary] : []),
|
|
215
|
+
"--",
|
|
216
|
+
"--generate-spec",
|
|
217
|
+
output,
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
type OpenApiTypeTools = {
|
|
222
|
+
openapiTS: typeof import("openapi-typescript").default;
|
|
223
|
+
astToString: typeof import("openapi-typescript").astToString;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
async function writeClientPackage(
|
|
227
|
+
outDir: string,
|
|
228
|
+
source: string,
|
|
229
|
+
tools: OpenApiTypeTools,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const srcDir = join(outDir, "src");
|
|
232
|
+
mkdirSync(srcDir, { recursive: true });
|
|
233
|
+
const specPath = join(outDir, "openapi.json");
|
|
234
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
235
|
+
const schemaPath = join(srcDir, "schema.ts");
|
|
236
|
+
makeWritable(schemaPath);
|
|
237
|
+
writeFileSync(schemaPath, tools.astToString(await tools.openapiTS(spec)));
|
|
238
|
+
stampGenerated(schemaPath, {
|
|
239
|
+
tool: "projen openapi (Speakeasy + openapi-typescript)",
|
|
240
|
+
source,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const clientPath = join(srcDir, "client.ts");
|
|
244
|
+
makeWritable(clientPath);
|
|
245
|
+
writeFileSync(clientPath, CLIENT_SRC);
|
|
246
|
+
stampGenerated(clientPath, {
|
|
247
|
+
tool: "projen openapi (openapi-fetch)",
|
|
248
|
+
source: "./schema",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
148
252
|
/**
|
|
149
253
|
* Regenerate the `openapi` packages from every server/node package with a tsoa
|
|
150
254
|
* import. Returns the package dirs it wrote so the caller can rebuild their barrels.
|
|
@@ -152,35 +256,43 @@ export async function optimizeOpenapiSpec(specPath: string, executable?: string)
|
|
|
152
256
|
* members in `pnpm-workspace.yaml`.
|
|
153
257
|
*/
|
|
154
258
|
export async function generateOpenapi(): Promise<string[]> {
|
|
155
|
-
const pkgs = controllerPackages();
|
|
259
|
+
const pkgs = controllerPackages().toArray();
|
|
260
|
+
const rustMappings = rustOpenapiMappings();
|
|
156
261
|
// Same reasoning as codegen's empty case: a workspace with no tsoa controllers
|
|
157
262
|
// is not a condition worth a line on every synth.
|
|
158
|
-
if (pkgs.length === 0) {
|
|
159
|
-
logger.debug("no
|
|
263
|
+
if (pkgs.length === 0 && rustMappings.length === 0) {
|
|
264
|
+
logger.debug("no TypeScript or Rust OpenAPI producers found");
|
|
160
265
|
return [];
|
|
161
266
|
}
|
|
162
267
|
|
|
163
268
|
// Lazy, resilient loads: tsoa + typescript are CJS (require), openapi-typescript
|
|
164
269
|
// is ESM (dynamic import).
|
|
165
|
-
const require = createRequire(import.meta.url);
|
|
166
|
-
const { generateSpec } = lazyRequire<typeof import("tsoa")>(
|
|
167
|
-
require,
|
|
168
|
-
"tsoa",
|
|
169
|
-
"openapi generation",
|
|
170
|
-
);
|
|
171
|
-
const tsRuntime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
|
|
172
270
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
271
|
+
const tools = { openapiTS, astToString };
|
|
173
272
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
273
|
+
const typescript =
|
|
274
|
+
pkgs.length > 0
|
|
275
|
+
? (() => {
|
|
276
|
+
const require = createRequire(import.meta.url);
|
|
277
|
+
const { generateSpec } = lazyRequire<typeof import("tsoa")>(
|
|
278
|
+
require,
|
|
279
|
+
"tsoa",
|
|
280
|
+
"openapi generation",
|
|
281
|
+
);
|
|
282
|
+
const runtime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
|
|
283
|
+
return {
|
|
284
|
+
generateSpec,
|
|
285
|
+
compilerOptions: {
|
|
286
|
+
experimentalDecorators: true,
|
|
287
|
+
target: runtime.ScriptTarget.ES2022,
|
|
288
|
+
module: runtime.ModuleKind.ESNext,
|
|
289
|
+
moduleResolution: runtime.ModuleResolutionKind.Bundler,
|
|
290
|
+
esModuleInterop: true,
|
|
291
|
+
skipLibCheck: true,
|
|
292
|
+
} satisfies ts.CompilerOptions,
|
|
293
|
+
};
|
|
294
|
+
})()
|
|
295
|
+
: undefined;
|
|
184
296
|
|
|
185
297
|
const releaseGraphPath = join(repoRoot, ".projen/release-units.json");
|
|
186
298
|
const releaseGraph = existsSync(releaseGraphPath)
|
|
@@ -196,8 +308,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
196
308
|
// its npm name - `p.name` is the (possibly-overridden) manifest name.
|
|
197
309
|
const leaf = p.relPath.split("/").pop() ?? p.relPath;
|
|
198
310
|
const outDir = join(repoRoot, p.root, OPENAPI_TAG, leaf);
|
|
199
|
-
|
|
200
|
-
mkdirSync(srcDir, { recursive: true });
|
|
311
|
+
mkdirSync(outDir, { recursive: true });
|
|
201
312
|
|
|
202
313
|
// 1) tsoa writes a temporary openapi.json, Speakeasy optimizes it there, then
|
|
203
314
|
// the complete spec moves into place so readers never observe an intermediate file.
|
|
@@ -205,7 +316,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
205
316
|
const tempDir = mkdtempSync(join(outDir, ".openapi-"));
|
|
206
317
|
const tempSpecPath = join(tempDir, "openapi.json");
|
|
207
318
|
try {
|
|
208
|
-
await generateSpec(
|
|
319
|
+
await typescript!.generateSpec(
|
|
209
320
|
{
|
|
210
321
|
entryFile: "",
|
|
211
322
|
noImplicitAdditionalProperties: "throw-on-extras",
|
|
@@ -216,7 +327,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
216
327
|
name: `${p.relPath} API`,
|
|
217
328
|
version: specVersion,
|
|
218
329
|
},
|
|
219
|
-
compilerOptions,
|
|
330
|
+
typescript!.compilerOptions,
|
|
220
331
|
);
|
|
221
332
|
await optimizeOpenapiSpec(tempSpecPath);
|
|
222
333
|
makeWritable(specPath);
|
|
@@ -225,28 +336,29 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
225
336
|
rmSync(tempDir, { recursive: true, force: true });
|
|
226
337
|
}
|
|
227
338
|
makeReadonly(specPath);
|
|
228
|
-
|
|
229
|
-
// 2) src/schema.ts: types generated from the spec (openapi-typescript).
|
|
230
|
-
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
231
|
-
const schemaPath = join(srcDir, "schema.ts");
|
|
232
|
-
makeWritable(schemaPath);
|
|
233
|
-
writeFileSync(schemaPath, astToString(await openapiTS(spec)));
|
|
234
|
-
stampGenerated(schemaPath, {
|
|
235
|
-
tool: "projen openapi (tsoa + Speakeasy + openapi-typescript)",
|
|
236
|
-
source: `the tsoa controllers in ${p.relPath}`,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// 3) src/client.ts: a typed openapi-fetch client over those types.
|
|
240
|
-
const clientPath = join(srcDir, "client.ts");
|
|
241
|
-
makeWritable(clientPath);
|
|
242
|
-
writeFileSync(clientPath, CLIENT_SRC);
|
|
243
|
-
stampGenerated(clientPath, {
|
|
244
|
-
tool: "projen openapi (openapi-fetch)",
|
|
245
|
-
source: "./schema",
|
|
246
|
-
});
|
|
339
|
+
await writeClientPackage(outDir, `the tsoa controllers in ${p.relPath}`, tools);
|
|
247
340
|
|
|
248
341
|
written.push(outDir);
|
|
249
342
|
logger.success(`openapi/${leaf} (from ${p.relPath})`);
|
|
250
343
|
}
|
|
344
|
+
for (const mapping of rustMappings) {
|
|
345
|
+
const outDir = resolve(repoRoot, mapping.output);
|
|
346
|
+
mkdirSync(outDir, { recursive: true });
|
|
347
|
+
const specPath = join(outDir, "openapi.json");
|
|
348
|
+
const tempDir = mkdtempSync(join(outDir, ".openapi-"));
|
|
349
|
+
const tempSpecPath = join(tempDir, "openapi.json");
|
|
350
|
+
try {
|
|
351
|
+
await execFileAsync("cargo", rustOpenapiArgs(mapping, tempSpecPath), { cwd: repoRoot });
|
|
352
|
+
await optimizeOpenapiSpec(tempSpecPath);
|
|
353
|
+
makeWritable(specPath);
|
|
354
|
+
renameSync(tempSpecPath, specPath);
|
|
355
|
+
} finally {
|
|
356
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
makeReadonly(specPath);
|
|
359
|
+
await writeClientPackage(outDir, `the aide routes in ${mapping.rust}`, tools);
|
|
360
|
+
written.push(outDir);
|
|
361
|
+
logger.success(`${mapping.output} (from ${mapping.rust})`);
|
|
362
|
+
}
|
|
251
363
|
return written;
|
|
252
364
|
}
|
package/src/project-py.ts
CHANGED
|
@@ -465,9 +465,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
465
465
|
if (project.releaseCatalog.mode === "independent") {
|
|
466
466
|
workflow.addJob("build-python", {
|
|
467
467
|
if: usesRustArtifacts
|
|
468
|
-
? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-
|
|
468
|
+
? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-uniffi.result != 'failure' && needs.rust-uniffi.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
|
|
469
469
|
: "${{ needs.release-plan.outputs.python == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}",
|
|
470
|
-
needs: ["release-plan", ...(usesRustArtifacts ? ["rust-
|
|
470
|
+
needs: ["release-plan", ...(usesRustArtifacts ? ["rust-uniffi"] : [])],
|
|
471
471
|
runsOn: ["ubuntu-latest"],
|
|
472
472
|
permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
|
|
473
473
|
timeoutMinutes: 20,
|
|
@@ -546,9 +546,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
546
546
|
}
|
|
547
547
|
workflow.addJob("build-python", {
|
|
548
548
|
if: usesRustArtifacts
|
|
549
|
-
? "${{ always() && needs.verify-context.result == 'success' && needs.rust-
|
|
549
|
+
? "${{ always() && needs.verify-context.result == 'success' && needs.rust-uniffi.result != 'failure' && needs.rust-uniffi.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
|
|
550
550
|
: releaseStageCondition("python"),
|
|
551
|
-
needs: ["verify-context", ...(usesRustArtifacts ? ["rust-
|
|
551
|
+
needs: ["verify-context", ...(usesRustArtifacts ? ["rust-uniffi"] : [])],
|
|
552
552
|
runsOn: ["ubuntu-latest"],
|
|
553
553
|
permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
|
|
554
554
|
timeoutMinutes: 20,
|