Skip to content

Commit 1d2cb59

Browse files
authored
Merge branch 'main' into dependabot/github_actions/jdx/mise-action-4.3.0
2 parents 42541c4 + 1d8c1ea commit 1d2cb59

26 files changed

Lines changed: 2405 additions & 30 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"swagger-typescript-api": patch
3+
---
4+
5+
Fix `extractRequestParams` overwriting a component schema whose name matches `<operationId>Params`.
6+
7+
Since path-only routes started producing an extracted params type (13.2.9), a spec with an operation
8+
`getOrder` and a component schema `GetOrderParams` (for example the operation's request body) would have
9+
the model silently replaced by the route's path/query params, so the generated method typed its `data`
10+
argument as the path params instead of the body.
11+
12+
`createRequestParamsSchema` now goes through the same schema-key collision guard already used by
13+
`extractResponseBody` and `extractResponseError`, so the existing model keeps its name and the route
14+
params fall back to the next free name (`GetOrderParams1`).
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"swagger-typescript-api": minor
3+
---
4+
5+
Add `importFileExtension` and `typeOnlyImports` options
6+
7+
`importFileExtension` (`""` | `".js"` | `".ts"`) appends a file extension to
8+
generated relative imports, for projects using `moduleResolution: node16`/`nodenext`
9+
(`.js`) or `allowImportingTsExtensions` (`.ts`).
10+
11+
`typeOnlyImports` emits `import type` for type-only imports (and inline `type` on
12+
mixed imports such as the http-client import, where `HttpClient` stays a value
13+
import) for projects using `verbatimModuleSyntax` / `isolatedModules`. `ContentType`
14+
is only marked `type` for `enumStyle: "union"`, where it is a pure type.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
/docs/
33
/node_modules/
44
/tmp-issue-463-run/
5+
/.idea/
6+
/.serena/

index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,18 @@ const generateCommand = defineCommand({
199199
'enum output style: "enum" (default), "union" (T1 | T2 | TN), "const" (as const object + type alias), or "const-enum" (const enum)',
200200
default: codeGenBaseConfig.enumStyle,
201201
},
202+
"import-file-extension": {
203+
type: "string",
204+
description:
205+
'extension appended to generated relative imports: "" (default), ".js" (moduleResolution node16/nodenext), or ".ts" (allowImportingTsExtensions)',
206+
default: codeGenBaseConfig.importFileExtension,
207+
},
208+
"type-only-imports": {
209+
type: "boolean",
210+
description:
211+
"emit `import type` / inline `type` for type-only imports (verbatimModuleSyntax / isolatedModules)",
212+
default: codeGenBaseConfig.typeOnlyImports,
213+
},
202214
"http-client": {
203215
type: "string",
204216
description: `http client type (possible values: ${Object.values(
@@ -349,6 +361,12 @@ const generateCommand = defineCommand({
349361
| "const"
350362
| "const-enum"
351363
| undefined,
364+
importFileExtension: args["import-file-extension"] as
365+
| ""
366+
| ".js"
367+
| ".ts"
368+
| undefined,
369+
typeOnlyImports: args["type-only-imports"],
352370
httpClientType:
353371
args["http-client"] || args.axios
354372
? HTTP_CLIENT.AXIOS

src/configuration.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export class CodeGenConfig {
6161
enumStyle: "enum" | "union" | "const" | "const-enum" = "enum";
6262
/** @deprecated Use enumStyle: "union" instead */
6363
generateUnionEnums = false;
64+
/** CLI flag. Extension appended to generated relative imports: "" (default), ".js", or ".ts". */
65+
importFileExtension: "" | ".js" | ".ts" = "";
66+
/** CLI flag. Emit `import type` / inline `type` for type-only imports. */
67+
typeOnlyImports = false;
6468
/** CLI flag */
6569
addReadonly = false;
6670
enumNamesAsValues = false;
@@ -466,6 +470,12 @@ export class CodeGenConfig {
466470
>,
467471
) => {
468472
objectAssign(this, update);
473+
this.importFileExtension ??= "";
474+
if (!["", ".js", ".ts"].includes(this.importFileExtension)) {
475+
throw new Error(
476+
`Invalid \`importFileExtension\` value "${this.importFileExtension}". Expected "", ".js", or ".ts".`,
477+
);
478+
}
469479
if (this.enumNamesAsValues) {
470480
this.extractEnums = true;
471481
}

src/schema-parser/base-schema-parsers/complex.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export class ComplexSchemaParser extends MonoSchemaParser {
1818
complexType
1919
](this.schema);
2020

21+
// A $ref alongside `not`, `allOf` and friends is a sibling in OpenAPI 3.1: the
22+
// keywords apply on top of the referenced schema. Parsing the reference too keeps
23+
// the type, which would otherwise be lost with the complex keyword that cannot be
24+
// expressed in TypeScript.
25+
const shouldParseSimpleSchema =
26+
this.schemaUtils.getInternalSchemaType(simpleSchema) ===
27+
SCHEMA_TYPES.OBJECT || this.schemaUtils.isRefSchema(simpleSchema);
28+
2129
return {
2230
...(typeof this.schema === "object" ? this.schema : {}),
2331
$schemaPath: this.schemaPath.slice(),
@@ -33,19 +41,21 @@ export class ComplexSchemaParser extends MonoSchemaParser {
3341
),
3442
content:
3543
this.config.Ts.IntersectionType(
36-
compact([
37-
this.config.Ts.ExpressionGroup(complexSchemaContent),
38-
this.schemaUtils.getInternalSchemaType(simpleSchema) ===
39-
SCHEMA_TYPES.OBJECT &&
40-
this.config.Ts.ExpressionGroup(
41-
this.schemaParserFabric
42-
.createSchemaParser({
43-
schema: simpleSchema,
44-
schemaPath: this.schemaPath,
45-
})
46-
.getInlineParseContent(),
47-
),
48-
]),
44+
this.schemaUtils
45+
.filterSchemaContents(
46+
compact([
47+
complexSchemaContent,
48+
shouldParseSimpleSchema &&
49+
this.schemaParserFabric
50+
.createSchemaParser({
51+
schema: simpleSchema,
52+
schemaPath: this.schemaPath,
53+
})
54+
.getInlineParseContent(),
55+
]),
56+
(content) => content !== this.config.Ts.Keyword.Any,
57+
)
58+
.map((content) => this.config.Ts.ExpressionGroup(content)),
4959
) || this.config.Ts.Keyword.Any,
5060
};
5161
}

src/schema-routes/schema-routes.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,12 @@ export class SchemaRoutes {
7272
}
7373

7474
/**
75-
* `extractResponseBody` / `extractResponseError` call `createParsedComponent`, which
76-
* registers `#/components/schemas/<typeName>`. If that key already exists (e.g.
77-
* `MergeFluffyData` in definitions), the map entry would be overwritten unless we
78-
* pick another name via `resolveTypeName` after reserving the colliding one.
75+
* `extractResponseBody` / `extractResponseError` / `extractRequestParams` call
76+
* `createParsedComponent`, which registers `#/components/schemas/<typeName>`. If that
77+
* key already exists (e.g. `MergeFluffyData` in definitions, or a request-body model
78+
* named `GetOrderParams` next to a `getOrder` operation), the map entry would be
79+
* overwritten unless we pick another name via `resolveTypeName` after reserving the
80+
* colliding one.
7981
*
8082
* `getComponents` may be missing in narrow unit tests that pass a stub map.
8183
*
@@ -859,7 +861,7 @@ export class SchemaRoutes {
859861
if (fixedSchema) return fixedSchema;
860862

861863
if (extractRequestParams) {
862-
const generatedTypeName = this.schemaUtils.resolveTypeName(
864+
const generatedTypeName = this.extractTypeNameWithoutSchemaKeyCollision(
863865
routeName.usage,
864866
{
865867
suffixes: this.config.extractingOptions.requestParamsSuffix,

templates/default/procedure-call.ejs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,21 @@ const rawWrapperArgs = config.extractRequestParams ?
4646
requestConfigParam,
4747
])
4848
49+
// Before sorting, promote any optional arg that has a defaultValue and is
50+
// followed by a required arg to "positionally required". The sort still pushes
51+
// truly-optional (`?:`) args to the end, but defaultable args ahead of a
52+
// required arg stay in declaration order, so callers' positional arguments
53+
// don't silently shift when a previously-required query becomes optional.
54+
const positionedWrapperArgs = rawWrapperArgs.map((arg, i) => {
55+
if (arg.optional && arg.defaultValue && rawWrapperArgs.slice(i + 1).some(a => !a.optional)) {
56+
return { ...arg, optional: false };
57+
}
58+
return arg;
59+
})
60+
4961
const wrapperArgs = _
5062
// Sort by optionality
51-
.sortBy(rawWrapperArgs, [o => o.optional])
63+
.sortBy(positionedWrapperArgs, [o => o.optional])
5264
.map(argToTmpl)
5365
.join(', ')
5466

templates/default/route-types.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const dataContracts = config.modular ? _.map(modelTypes, "name") : [];
55
%>
66

77
<% if (dataContracts.length) { %>
8-
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
8+
import <%~ config.typeOnlyImports ? "type " : "" %>{ <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %><%~ config.importFileExtension %>"
99
<% } %>
1010

1111
<%

templates/modular/api.ejs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ const { _, pascalCase, require } = utils;
44
const apiClassName = pascalCase(route.moduleName);
55
const routes = route.routes;
66
const dataContracts = _.map(modelTypes, "name");
7+
const importExt = config.importFileExtension;
8+
const typeModifier = config.typeOnlyImports ? "type " : "";
9+
// ContentType is a pure type only for the "union" style; otherwise it is a runtime value.
10+
const contentTypeSpecifier = config.enumStyle === "union" ? "type ContentType" : "ContentType";
11+
const httpClientSpecifiers = ["HttpClient", `${typeModifier}RequestParams`, contentTypeSpecifier, `${typeModifier}HttpResponse`].join(", ");
712
%>
813

914
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
1015

11-
<% if (config.enumStyle === "union") { %>
12-
import { HttpClient, RequestParams, type ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>";
13-
<% } else { %>
14-
import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>";
15-
<% } %>
16+
import { <%~ httpClientSpecifiers %> } from "./<%~ config.fileNames.httpClient %><%~ importExt %>";
1617
<% if (dataContracts.length) { %>
17-
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
18+
import <%~ typeModifier %>{ <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %><%~ importExt %>"
1819
<% } %>
1920

2021
export class <%= apiClassName %><SecurityDataType = unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {

0 commit comments

Comments
 (0)