@dbx-tools/projen 0.9.38 → 0.9.43

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Per-run release policy shared by the Projen task and tag-triggered workflow.
3
+ *
4
+ * This module owns defaults and the annotated-tag contract. Reuse these values
5
+ * when adding release steps so local flags and CI cannot drift. Registry URLs
6
+ * and local installation choices never belong in the public tag annotation.
7
+ *
8
+ * @module
9
+ */
10
+ import { json } from "@dbx-tools/shared-core";
11
+
12
+ /** Publication scopes accepted by the release task; auto preserves normal publication. */
13
+ export const RELEASE_PUBLISH_TARGETS = ["auto", "npm", "pypi", "local", "none"] as const;
14
+
15
+ /** One publication scope accepted by the release task. */
16
+ export type ReleasePublishTarget = (typeof RELEASE_PUBLISH_TARGETS)[number];
17
+
18
+ /** Native Projen dependency-install modes available during local release preparation. */
19
+ export const RELEASE_INSTALL_MODES = ["auto", "always", "never"] as const;
20
+
21
+ /** Local dependency-install behavior; auto retains Projen's normal trigger policy. */
22
+ export type ReleaseInstallMode = (typeof RELEASE_INSTALL_MODES)[number];
23
+
24
+ /** CI steps selected by an immutable release annotation. */
25
+ export interface ReleaseStepSelection {
26
+ readonly npm: boolean;
27
+ readonly pypi: boolean;
28
+ readonly docs: boolean;
29
+ readonly validation: boolean;
30
+ }
31
+
32
+ /** Flags consumed by the shared release-selection policy. */
33
+ export interface ReleaseSelectionOptions {
34
+ readonly publish?: ReleasePublishTarget;
35
+ readonly npm?: boolean;
36
+ readonly pypi?: boolean;
37
+ readonly docs?: boolean;
38
+ readonly validation?: boolean;
39
+ }
40
+
41
+ const ANNOTATION_MARKER = "dbx-tools-release:";
42
+
43
+ /** Derive public build/publication steps without mutating repository configuration. */
44
+ export function releaseStepSelection(options: ReleaseSelectionOptions = {}): ReleaseStepSelection {
45
+ const target = options.publish ?? "auto";
46
+ return {
47
+ npm: (target === "auto" || target === "npm") && options.npm !== false,
48
+ pypi: (target === "auto" || target === "pypi") && options.pypi !== false,
49
+ docs: options.docs ?? (target !== "local" && target !== "none"),
50
+ validation: options.validation ?? true,
51
+ };
52
+ }
53
+
54
+ /** Record non-default CI selections alongside the existing annotated release tag. */
55
+ export function releaseTagAnnotation(tag: string, selection: ReleaseStepSelection): string {
56
+ if (selection.npm && selection.pypi && selection.docs && selection.validation) return tag;
57
+ return `${tag}\n\n${ANNOTATION_MARKER} ${JSON.stringify(selection)}`;
58
+ }
59
+
60
+ /** Read the release selection, rejecting malformed or ambiguous annotated policy. */
61
+ export function parseReleaseTagAnnotation(annotation: string): ReleaseStepSelection {
62
+ const selections = annotation.split("\n").filter((line) => line.startsWith(ANNOTATION_MARKER));
63
+ if (selections.length === 0) return releaseStepSelection();
64
+ if (selections.length !== 1) throw new Error("Release tag contains multiple step selections");
65
+ const value = json.parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
66
+ if (
67
+ !value ||
68
+ Object.keys(value).length !== 4 ||
69
+ typeof value.npm !== "boolean" ||
70
+ typeof value.pypi !== "boolean" ||
71
+ typeof value.docs !== "boolean" ||
72
+ typeof value.validation !== "boolean"
73
+ ) {
74
+ throw new Error("Invalid release tag step selection");
75
+ }
76
+ return { npm: value.npm, pypi: value.pypi, docs: value.docs, validation: value.validation };
77
+ }
78
+
79
+ /** Whether the selected release should inspect and publish configured local registries. */
80
+ export function releasePublishesLocally(target: ReleasePublishTarget = "auto"): boolean {
81
+ return target === "auto" || target === "local";
82
+ }
package/src/release.ts CHANGED
@@ -5,7 +5,8 @@ import { GithubWorkflow } from "projen/lib/github";
5
5
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
6
6
  import { BUN_VERSION } from "./bun-workflow.ts";
7
7
  import { projectReleaseBranch, taskCommand, type DBXToolsJavaScriptProject } from "./project-js.ts";
8
- import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
8
+ import { RELEASE_VERSION } from "./release-context.ts";
9
+ import type { ReleaseStepSelection } from "./release-options.ts";
9
10
 
10
11
  const NODE_VERSION = "24";
11
12
  const NPM_VERSION = "11.4.2";
@@ -17,7 +18,7 @@ const releaseWorkflows = new WeakMap<DBXToolsJavaScriptProject, GithubWorkflow>(
17
18
  export interface ReleaseDocsOptions {
18
19
  readonly siteUrl: string;
19
20
  readonly base?: string;
20
- /** Repository-defined setup and generation steps run before saving the Bun cache. */
21
+ /** Documentation generation steps run in the shared release build after validation. */
21
22
  readonly prepareSteps: readonly JobStep[];
22
23
  /** Repository-defined documentation build and validation steps. */
23
24
  readonly buildSteps: readonly JobStep[];
@@ -37,6 +38,8 @@ export interface DBXToolsReleaseOptions {
37
38
  readonly pythonRoot?: string;
38
39
  /** Repository task names run after VERSION verification and before publication. */
39
40
  readonly validationTasks?: readonly string[];
41
+ /** Repository prerequisites installed before release validation and artifact builds. */
42
+ readonly setupSteps?: readonly JobStep[];
40
43
  }
41
44
 
42
45
  /** Locate the unified workflow when release generation is enabled. */
@@ -44,7 +47,7 @@ export function tryReleaseWorkflow(project: DBXToolsJavaScriptProject): GithubWo
44
47
  return releaseWorkflows.get(project);
45
48
  }
46
49
 
47
- /** Tag pattern created by release jobs and accepted by manual recovery. */
50
+ /** Tag pattern accepted by the unified release workflow. */
48
51
  export function releaseTagPattern(project: DBXToolsJavaScriptProject): string {
49
52
  const prefix = releaseTagPrefixes.get(project);
50
53
  if (!prefix) throw new Error("Release workflow is not configured");
@@ -57,11 +60,25 @@ export function releaseCondition(prerequisites: readonly string[] = []): string
57
60
  return `\${{ always() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} }}`;
58
61
  }
59
62
 
60
- /** Native project setup plus npm publication authentication. */
61
- export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): readonly JobStep[] {
63
+ /** Gate shared-build steps using the same per-run flag, preserving their existing conditions. */
64
+ export function releaseBuildSteps(
65
+ flag: keyof ReleaseStepSelection,
66
+ steps: readonly JobStep[],
67
+ ): JobStep[] {
68
+ return steps.map((step) => ({
69
+ ...step,
70
+ if: `\${{ success() && steps.release.outputs.${flag} == 'true'${step.if ? ` && (${step.if.replace(/^\s*\$\{\{\s*|\s*\}\}\s*$/g, "")})` : ""} }}`,
71
+ }));
72
+ }
73
+
74
+ /** Artifact-only publication runtime and npm authentication; no workspace installation. */
75
+ export function nodeReleaseSetupSteps(): readonly JobStep[] {
62
76
  return [
63
- ...releaseSourceSteps(),
64
- ...project.renderWorkflowSetup({ mutable: false }),
77
+ {
78
+ name: "Setup Bun",
79
+ uses: "oven-sh/setup-bun@v2",
80
+ with: { "bun-version": BUN_VERSION },
81
+ },
65
82
  {
66
83
  name: "Setup Node.js",
67
84
  uses: "actions/setup-node@v6",
@@ -75,26 +92,32 @@ export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): reado
75
92
  ];
76
93
  }
77
94
 
78
- /** Install uv and restore its download/build cache using Python manifests. */
95
+ /** Install uv for Python release builds. */
79
96
  export function uvSetupStep(): JobStep {
80
97
  return {
81
98
  name: "Setup uv",
82
99
  uses: "astral-sh/setup-uv@v7",
83
- with: {
84
- "enable-cache": true,
85
- "cache-dependency-glob": "**/pyproject.toml",
86
- },
87
100
  };
88
101
  }
89
102
 
90
103
  function refreshDocsRegistryDependencies(workflow: GithubWorkflow): void {
91
- const docs = workflow.getJob("build-docs") as Job | undefined;
104
+ const docs = workflow.getJob("deploy-docs") as Job | undefined;
92
105
  if (!docs) return;
93
106
  const registryJobs = Object.keys(workflow.jobs).filter(
94
107
  (name) => name === "publish-node" || name.startsWith("publish-pypi-"),
95
108
  );
96
109
  if (registryJobs.length > 0) {
97
- workflow.updateJob("build-docs", { ...docs, needs: ["verify-context", ...registryJobs] });
110
+ workflow.updateJob("deploy-docs", {
111
+ ...docs,
112
+ needs: ["build-release", ...registryJobs],
113
+ if: releaseCondition([
114
+ "needs['build-release'].result == 'success'",
115
+ "needs['build-release'].outputs.docs == 'true'",
116
+ ...registryJobs.map(
117
+ (name) => `needs['${name}'].result == 'success' || needs['${name}'].result == 'skipped'`,
118
+ ),
119
+ ]),
120
+ });
98
121
  }
99
122
  }
100
123
 
@@ -106,7 +129,7 @@ export function refreshReleaseDocsDependencies(project: DBXToolsJavaScriptProjec
106
129
  }
107
130
  }
108
131
 
109
- function verifyContextJob(
132
+ function releaseBuildJob(
110
133
  project: DBXToolsJavaScriptProject,
111
134
  tagPrefix: string,
112
135
  releaseBranch: string,
@@ -115,12 +138,21 @@ function verifyContextJob(
115
138
  return {
116
139
  runsOn: ["ubuntu-latest"],
117
140
  permissions: { contents: JobPermission.READ },
118
- timeoutMinutes: 30,
119
- env: { BUN_VERSION, CI: "true" },
141
+ timeoutMinutes: 60,
142
+ env: {
143
+ BUN_VERSION,
144
+ CI: "true",
145
+ ...(options.docs
146
+ ? { DOCS_SITE_URL: options.docs.siteUrl, DOCS_BASE: options.docs.base ?? "/" }
147
+ : {}),
148
+ },
120
149
  outputs: {
121
150
  release_tag: { stepId: "release", outputName: "release_tag" },
122
151
  expected_sha: { stepId: "release", outputName: "expected_sha" },
123
152
  release_version: { stepId: "release", outputName: "release_version" },
153
+ npm: { stepId: "release", outputName: "npm" },
154
+ pypi: { stepId: "release", outputName: "pypi" },
155
+ docs: { stepId: "release", outputName: "docs" },
124
156
  },
125
157
  steps: [
126
158
  github.WorkflowSteps.checkout({
@@ -155,28 +187,70 @@ function verifyContextJob(
155
187
  echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
156
188
  echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
157
189
  echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
190
+ bun node_modules/@dbx-tools/projen/tasks/release-options.ts --tag "$RELEASE_TAG" --output "$GITHUB_OUTPUT"
158
191
  `
159
192
  // ============================================================================
160
193
  ),
161
194
  },
162
- ...(options.validationTasks ?? []).map((task) => ({
163
- name: `Validate ${task}`,
164
- run: `bun run ${task}`,
165
- })),
195
+ ...(options.setupSteps ?? []),
196
+ ...releaseBuildSteps(
197
+ "validation",
198
+ (options.validationTasks ?? []).map((task) => ({
199
+ name: `Validate ${task}`,
200
+ run: `bun run ${task}`,
201
+ })),
202
+ ),
203
+ ...(options.nodeRelease === false
204
+ ? []
205
+ : releaseBuildSteps("npm", [
206
+ {
207
+ name: "Build npm archives",
208
+ env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
209
+ run: [
210
+ "bun run compile",
211
+ 'bun node_modules/@dbx-tools/projen/tasks/publish.ts "$RELEASE_VERSION" --skip-compile --output .release/npm',
212
+ "bun build node_modules/@dbx-tools/projen/tasks/publish-npm.ts --target=bun --outfile=.release/npm/publish-npm.mjs",
213
+ ].join("\n"),
214
+ },
215
+ {
216
+ name: "Upload npm archives",
217
+ uses: "actions/upload-artifact@v4",
218
+ with: { name: "release-npm", path: ".release/npm", "if-no-files-found": "error" },
219
+ },
220
+ ])),
221
+ ...(options.docs
222
+ ? releaseBuildSteps("docs", [
223
+ ...options.docs.prepareSteps,
224
+ ...options.docs.buildSteps,
225
+ {
226
+ name: "Upload Pages artifact",
227
+ uses: "actions/upload-pages-artifact@v4",
228
+ with: { path: options.docs.artifactPath },
229
+ },
230
+ ])
231
+ : []),
166
232
  ],
167
233
  };
168
234
  }
169
235
 
170
- function nodePublishJob(project: DBXToolsJavaScriptProject): Job {
236
+ function nodePublishJob(): Job {
171
237
  return {
172
- if: releaseCondition(),
173
- needs: ["verify-context"],
238
+ if: releaseCondition([
239
+ "needs['build-release'].result == 'success'",
240
+ "needs['build-release'].outputs.npm == 'true'",
241
+ ]),
242
+ needs: ["build-release"],
174
243
  runsOn: ["ubuntu-latest"],
175
244
  permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
176
245
  timeoutMinutes: 30,
177
246
  env: { BUN_VERSION, CI: "true" },
178
247
  steps: [
179
- ...nodeReleaseSetupSteps(project),
248
+ {
249
+ name: "Download npm archives",
250
+ uses: "actions/download-artifact@v4",
251
+ with: { name: "release-npm", path: ".release/npm" },
252
+ },
253
+ ...nodeReleaseSetupSteps(),
180
254
  {
181
255
  name: "Publish npm workspace",
182
256
  env: {
@@ -184,47 +258,19 @@ function nodePublishJob(project: DBXToolsJavaScriptProject): Job {
184
258
  NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
185
259
  NPM_CONFIG_PROVENANCE: "true",
186
260
  },
187
- run: 'bun node_modules/@dbx-tools/projen/tasks/publish.ts "$RELEASE_VERSION" $DRY_RUN',
261
+ run: 'bun .release/npm/publish-npm.mjs --directory .release/npm --version "$RELEASE_VERSION"',
188
262
  },
189
263
  ],
190
264
  };
191
265
  }
192
266
 
193
- function addDocsJobs(
194
- workflow: GithubWorkflow,
195
- project: DBXToolsJavaScriptProject,
196
- options: ReleaseDocsOptions,
197
- ): void {
198
- workflow.addJob("build-docs", {
199
- if: releaseCondition(),
200
- needs: ["verify-context"],
201
- runsOn: ["ubuntu-latest"],
202
- permissions: {
203
- contents: JobPermission.READ,
204
- pages: JobPermission.WRITE,
205
- idToken: JobPermission.WRITE,
206
- },
207
- timeoutMinutes: 30,
208
- env: {
209
- BUN_VERSION,
210
- DOCS_SITE_URL: options.siteUrl,
211
- DOCS_BASE: options.base ?? "/",
212
- },
213
- steps: [
214
- ...releaseSourceSteps(),
215
- ...project.renderWorkflowSetup({ mutable: false }),
216
- ...options.prepareSteps,
217
- ...options.buildSteps,
218
- {
219
- name: "Upload Pages artifact",
220
- uses: "actions/upload-pages-artifact@v4",
221
- with: { path: options.artifactPath },
222
- },
223
- ],
224
- });
267
+ function addDocsJobs(workflow: GithubWorkflow): void {
225
268
  workflow.addJob("deploy-docs", {
226
- if: releaseCondition(),
227
- needs: ["build-docs"],
269
+ if: releaseCondition([
270
+ "needs['build-release'].result == 'success'",
271
+ "needs['build-release'].outputs.docs == 'true'",
272
+ ]),
273
+ needs: ["build-release"],
228
274
  environment: {
229
275
  name: "github-pages",
230
276
  url: "${{ steps.deployment.outputs.page_url }}",
@@ -247,6 +293,11 @@ function addDocsJobs(
247
293
  export class DBXToolsRelease extends Component {
248
294
  constructor(project: DBXToolsJavaScriptProject, options: DBXToolsReleaseOptions = {}) {
249
295
  super(project);
296
+ project.addGitIgnore(".release/");
297
+ const installCondition =
298
+ "node -e \"process.exit(process.env.DBX_TOOLS_RELEASE_INSTALL === 'never' ? 1 : 0)\"";
299
+ project.package.installTask.addCondition(installCondition);
300
+ project.package.installCiTask.addCondition(installCondition);
250
301
  const tagPrefix = options.tagPrefix ?? "v";
251
302
  const releaseBranch = projectReleaseBranch(project);
252
303
  releaseTagPrefixes.set(project, tagPrefix);
@@ -290,12 +341,12 @@ export class DBXToolsRelease extends Component {
290
341
  push: { tags: [`${tagPrefix}*`] },
291
342
  });
292
343
  workflow.file?.addOverride("permissions.contents", "read");
293
- workflow.addJob("verify-context", verifyContextJob(project, tagPrefix, releaseBranch, options));
344
+ workflow.addJob("build-release", releaseBuildJob(project, tagPrefix, releaseBranch, options));
294
345
  if (options.nodeRelease !== false) {
295
- workflow.addJob("publish-node", nodePublishJob(project));
346
+ workflow.addJob("publish-node", nodePublishJob());
296
347
  }
297
348
  if (options.docs) {
298
- addDocsJobs(workflow, project, options.docs);
349
+ addDocsJobs(workflow);
299
350
  }
300
351
  refreshReleaseDocsDependencies(project);
301
352
  }
@@ -94,6 +94,8 @@ export function buildPythonProjects(options: {
94
94
  readonly allowEmpty?: boolean;
95
95
  readonly output: string;
96
96
  readonly packages?: readonly string[];
97
+ /** Place each package's artifacts in its directory beneath output. */
98
+ readonly packageDirectories?: boolean;
97
99
  readonly root: string;
98
100
  readonly version: string;
99
101
  }): void {
@@ -136,9 +138,15 @@ export function buildPythonProjects(options: {
136
138
  version: options.version,
137
139
  }),
138
140
  );
139
- runTaskCommand(root, "uv", ["build", "--out-dir", output, packageRoot]);
141
+ const packageOutput = options.packageDirectories ? join(output, project.directory) : output;
142
+ runTaskCommand(root, "uv", ["build", "--out-dir", packageOutput, packageRoot]);
143
+ if (pythonDistributionPaths(packageOutput).length === 0) {
144
+ throw new Error(`No Python distributions found for ${project.name}`);
145
+ }
140
146
  }
141
- const distributions = pythonDistributionPaths(output);
147
+ const distributions = options.packageDirectories
148
+ ? projects.flatMap((project) => pythonDistributionPaths(join(output, project.directory)))
149
+ : pythonDistributionPaths(output);
142
150
  if (distributions.length === 0) {
143
151
  throw new Error(`No Python distributions found in ${output}`);
144
152
  }
@@ -157,6 +165,10 @@ export async function main(): Promise<void> {
157
165
  .option("--root <path>", "Python workspace package root", "packages/py")
158
166
  .option("--package <directory...>", "Build only selected package directories")
159
167
  .option("--output <path>", "Build distributions into a directory without publishing")
168
+ .option(
169
+ "--package-directories",
170
+ "Keep each package's distributions in its own output directory",
171
+ )
160
172
  .option("--dry-run", "build and inspect distributions without uploading")
161
173
  .action(
162
174
  (
@@ -165,6 +177,7 @@ export async function main(): Promise<void> {
165
177
  dryRun?: boolean;
166
178
  indexUrl?: string;
167
179
  package?: string[];
180
+ packageDirectories?: boolean;
168
181
  publishUrl?: string;
169
182
  root: string;
170
183
  output?: string;
@@ -174,6 +187,7 @@ export async function main(): Promise<void> {
174
187
  buildPythonProjects({
175
188
  output: options.output,
176
189
  packages: options.package,
190
+ packageDirectories: options.packageDirectories,
177
191
  root: options.root,
178
192
  version,
179
193
  });
@@ -270,6 +270,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
270
270
  };
271
271
  });
272
272
  build.onResolve({ filter: /.*/ }, ({ path }) => {
273
+ if (path === "zod") {
274
+ return {
275
+ path: resolve(dirname(Bun.resolveSync("zod/package.json", root)), "index.cjs"),
276
+ };
277
+ }
273
278
  if (overridesByModule.has(path)) {
274
279
  return { path, namespace: functionOverrideNamespace };
275
280
  }
@@ -586,7 +591,13 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
586
591
  }
587
592
  const parameterType = withoutUndefined(checker.getTypeAtLocation(parameter));
588
593
  const path = `${exported.sourceName}.${parameter.name.text}`;
589
- const record = pythonRecordType(checker, parameterType, records, path);
594
+ const record = pythonRecordType(
595
+ checker,
596
+ parameterType,
597
+ records,
598
+ path,
599
+ declaredTypeName(parameter.type),
600
+ );
590
601
  const required = !parameter.questionToken && !parameter.initializer;
591
602
  return {
592
603
  javascriptName: parameter.name.text,
@@ -606,6 +617,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
606
617
  responses,
607
618
  protocols,
608
619
  `${exported.sourceName}.return`,
620
+ declaredTypeName(declaration.type),
609
621
  );
610
622
  typesByFunction.set(`${exported.sourceFile}#${exported.sourceName}`, {
611
623
  parameters,
@@ -636,8 +648,10 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
636
648
  type: ts.Type,
637
649
  records: Map<string, PythonRecord>,
638
650
  path: string,
651
+ preferredName?: string,
639
652
  ): PythonRecord {
640
- const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
653
+ const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
654
+ const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
641
655
  if (!name || name === "__type") throw new Error(`${path} must reference a named object type`);
642
656
  const existing = records.get(name);
643
657
  if (existing) return existing;
@@ -665,13 +679,23 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
665
679
  type: ts.Type,
666
680
  records: Map<string, PythonRecord>,
667
681
  path: string,
682
+ preferredName?: string,
668
683
  ): PythonRecord | undefined {
669
684
  if (!(type.flags & ts.TypeFlags.Object)) return undefined;
670
685
  if (checker.isArrayType(type) || type.getCallSignatures().length > 0) return undefined;
671
686
  if (checker.getIndexTypeOfType(type, ts.IndexKind.String)) return undefined;
672
- const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
687
+ const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
688
+ const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
673
689
  if (!name || name === "__type") return undefined;
674
- return pythonRecord(checker, type, records, path);
690
+ return pythonRecord(checker, type, records, path, preferredName);
691
+ }
692
+
693
+ function declaredTypeName(node: ts.TypeNode | undefined): string | undefined {
694
+ if (!node) return undefined;
695
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
696
+ return node.typeName.text;
697
+ }
698
+ return undefined;
675
699
  }
676
700
 
677
701
  function pythonType(
@@ -715,6 +739,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
715
739
  responses: Map<string, PythonResponse>,
716
740
  protocols: Map<string, PythonProtocol>,
717
741
  path: string,
742
+ preferredName?: string,
718
743
  ): string {
719
744
  if (type.isUnion()) {
720
745
  return pythonUnion(
@@ -839,7 +864,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
839
864
  protocols.set(name, { name, methods: protocolMethods, properties: protocolProperties });
840
865
  return name;
841
866
  }
842
- const name = `${pythonObjectName(type, path, "Result")}Response`;
867
+ const name = `${preferredName ?? pythonObjectName(type, path, "Result")}Response`;
843
868
  if (responses.has(name)) return name;
844
869
  responses.set(name, { name, fields: [] });
845
870
  const fields = properties.map((property) => {