Skip to content

Commit d4536d1

Browse files
committed
fix: use import type when possible
this is needed in order to support ```json "verbatimModuleSyntax": true, ``` otherwise, you wind up with errors like: ```shell TS1484: ExpressRuntimeResponder is a type and must be imported using a type-only import when verbatimModuleSyntax is enabled. ```
1 parent f7e374f commit d4536d1

File tree

10 files changed

+119
-57
lines changed

10 files changed

+119
-57
lines changed

packages/openapi-code-generator/src/typescript/common/import-builder.spec.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ describe("typescript/common/import-builder", () => {
1212
it("can import individual exports", () => {
1313
const builder = new ImportBuilder()
1414

15-
builder.addSingle("Cat", "./models.ts")
16-
builder.addSingle("Dog", "./models.ts")
15+
builder.addSingle("Cat", "./models.ts", false)
16+
builder.addSingle("Dog", "./models.ts", false)
1717

1818
expect(builder.toString()).toBe("import {Cat, Dog} from './models'")
1919
})
2020

2121
it("can import a whole module, and individual exports", () => {
2222
const builder = new ImportBuilder()
2323

24-
builder.addSingle("Next", "koa")
25-
builder.addSingle("Context", "koa")
24+
builder.addSingle("Next", "koa", false)
25+
builder.addSingle("Context", "koa", false)
2626
builder.addModule("koa", "koa")
2727

2828
expect(builder.toString()).toBe("import koa, {Context, Next} from 'koa'")
@@ -32,33 +32,44 @@ describe("typescript/common/import-builder", () => {
3232
it("same directory", () => {
3333
const builder = new ImportBuilder({filename: "./foo/example.ts"})
3434

35-
builder.addSingle("Cat", "./foo/models.ts")
35+
builder.addSingle("Cat", "./foo/models.ts", false)
3636

3737
expect(builder.toString()).toBe("import {Cat} from './models'")
3838
})
3939

4040
it("parent directory", () => {
4141
const builder = new ImportBuilder({filename: "./foo/example.ts"})
4242

43-
builder.addSingle("Cat", "./models.ts")
43+
builder.addSingle("Cat", "./models.ts", false)
4444

4545
expect(builder.toString()).toBe("import {Cat} from '../models'")
4646
})
4747

4848
it("child directory", () => {
4949
const builder = new ImportBuilder({filename: "./example.ts"})
5050

51-
builder.addSingle("Cat", "./foo/models.ts")
51+
builder.addSingle("Cat", "./foo/models.ts", false)
5252

5353
expect(builder.toString()).toBe("import {Cat} from './foo/models'")
5454
})
5555

5656
it("sibling directory", () => {
5757
const builder = new ImportBuilder({filename: "./foo/example.ts"})
5858

59-
builder.addSingle("Cat", "./bar/models.ts")
59+
builder.addSingle("Cat", "./bar/models.ts", false)
6060

6161
expect(builder.toString()).toBe("import {Cat} from '../bar/models'")
6262
})
6363
})
64+
65+
describe("type imports", () => {
66+
it("can import types", () => {
67+
const builder = new ImportBuilder()
68+
69+
builder.addSingle("Cat", "./models.ts", false)
70+
builder.addSingle("Dog", "./models.ts", true)
71+
72+
expect(builder.toString()).toBe("import {Cat, type Dog} from './models'")
73+
})
74+
})
6475
})

packages/openapi-code-generator/src/typescript/common/import-builder.ts

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
import path from "node:path"
22

33
export class ImportBuilder {
4-
private readonly imports: Record<string, Set<string>> = {}
4+
private readonly imports: Record<
5+
string,
6+
{values: Set<string>; types: Set<string>}
7+
> = {}
58
private readonly importAll: Record<string, string> = {}
69

710
constructor(private readonly unit?: {filename: string}) {}
811

912
from(from: string) {
10-
return {
11-
add: (...names: string[]): this => {
13+
const chain = {
14+
add: (...names: string[]) => {
1215
for (const it of names) {
13-
this.addSingle(it, from)
16+
this.addSingle(it, from, false)
1417
}
15-
return this
18+
return chain
1619
},
17-
all: (name: string): this => {
20+
addType: (...names: string[]) => {
21+
for (const it of names) {
22+
this.addSingle(it, from, true)
23+
}
24+
return chain
25+
},
26+
all: (name: string) => {
1827
this.addModule(name, from)
19-
return this
28+
return chain
2029
},
2130
}
31+
return chain
2232
}
2333

24-
addSingle(name: string, from: string): void {
34+
addSingle(name: string, from: string, isType: boolean): void {
2535
if (!name) {
2636
throw new Error(`cannot addSingle with name '${name}'`)
2737
}
@@ -30,7 +40,7 @@ export class ImportBuilder {
3040
throw new Error(`cannot addSingle with from '${from}'`)
3141
}
3242

33-
this.add(name, from, false)
43+
this.add(name, from, false, isType)
3444
}
3545

3646
addModule(name: string, from: string): void {
@@ -42,7 +52,8 @@ export class ImportBuilder {
4252
throw new Error(`cannot addModule with from '${from}'`)
4353
}
4454

45-
this.add(name, from, true)
55+
// todo: add support for importing whole module as a type
56+
this.add(name, from, true, false)
4657
}
4758

4859
static merge(
@@ -51,42 +62,71 @@ export class ImportBuilder {
5162
): ImportBuilder {
5263
const result = new ImportBuilder(unit)
5364

54-
// biome-ignore lint/complexity/noForEach: todo
55-
builders.forEach((builder) => {
56-
// biome-ignore lint/complexity/noForEach: todo
57-
Object.entries(builder.imports).forEach(([key, value]) => {
58-
// biome-ignore lint/suspicious/noAssignInExpressions: todo
59-
const imports = (result.imports[key] = result.imports[key] ?? new Set())
65+
for (const builder of builders) {
66+
for (const [key, {values, types}] of Object.entries(builder.imports)) {
67+
if (!result.imports[key]) {
68+
result.imports[key] = {
69+
values: new Set(),
70+
types: new Set(),
71+
}
72+
}
73+
74+
const imports = result.imports[key]
6075

61-
for (const it of value) {
62-
imports.add(it)
76+
for (const it of values) {
77+
imports.values.add(it)
6378
}
64-
})
79+
for (const it of types) {
80+
imports.types.add(it)
81+
}
82+
}
6583

66-
// biome-ignore lint/complexity/noForEach: todo
67-
Object.entries(builder.importAll).forEach(([key, value]) => {
84+
for (const [key, value] of Object.entries(builder.importAll)) {
6885
if (result.importAll[key] && result.importAll[key] !== value) {
6986
throw new Error("cannot merge imports with colliding importAlls")
7087
}
7188

7289
result.importAll[key] = value
73-
})
74-
})
90+
}
91+
}
7592

7693
return result
7794
}
7895

79-
private add(name: string, from: string, isAll: boolean): void {
96+
private add(
97+
name: string,
98+
from: string,
99+
isAll: boolean,
100+
isType: boolean,
101+
): void {
80102
// biome-ignore lint/style/noParameterAssign: normalization
81103
from = this.normalizeFrom(from)
82-
// biome-ignore lint/suspicious/noAssignInExpressions: init
83-
const imports = (this.imports[from] =
84-
this.imports[from] ?? new Set<string>())
104+
105+
if (!this.imports[from]) {
106+
this.imports[from] = {
107+
values: new Set(),
108+
types: new Set(),
109+
}
110+
}
111+
112+
let imports = this.imports[from]
113+
114+
if (!imports) {
115+
imports = {
116+
values: new Set(),
117+
types: new Set(),
118+
}
119+
this.imports[from] = imports
120+
}
85121

86122
if (isAll) {
87123
this.importAll[from] = name
88124
} else {
89-
imports.add(name)
125+
if (isType) {
126+
imports.types.add(name)
127+
} else {
128+
imports.values.add(name)
129+
}
90130
}
91131
}
92132

@@ -140,10 +180,19 @@ export class ImportBuilder {
140180
)
141181
.sort()
142182
.map((from) => {
143-
// biome-ignore lint/style/noNonNullAssertion: todo
144-
const individualImports = Array.from(this.imports[from]!.values())
145-
.sort()
146-
.filter(hasImport)
183+
const valueImports = Array.from(
184+
(this.imports[from]?.values ?? new Set()).values(),
185+
)
186+
const typeImports = Array.from(
187+
(this.imports[from]?.types ?? new Set()).values(),
188+
)
189+
190+
const individualImports = valueImports
191+
.map((it) => ({name: it, isType: false}))
192+
.concat(typeImports.map((it) => ({name: it, isType: true})))
193+
.sort((a, b) => (a.name < b.name ? -1 : 1))
194+
.filter((it) => hasImport(it.name))
195+
.map((it) => (it.isType ? `type ${it.name}` : it.name))
147196
.join(", ")
148197

149198
const importAll = this.importAll[from]

packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export abstract class AbstractSchemaBuilder<
6565
this.referenced[name] = reference
6666

6767
if (this.imports) {
68-
this.imports.addSingle(name, this.filename)
68+
this.imports.addSingle(name, this.filename, false)
6969
}
7070

7171
return name
@@ -78,7 +78,7 @@ export abstract class AbstractSchemaBuilder<
7878

7979
this.parent?.addStaticSchema(name)
8080
this.referencedStaticSchemas.add(name)
81-
this.imports?.addSingle(name, this.filename)
81+
this.imports?.addSingle(name, this.filename, false)
8282

8383
return name
8484
}

packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder<
224224
this.schemaBuilderImports.addSingle(
225225
"UnknownEnumNumberValue",
226226
"./models",
227+
false,
227228
)
228229
return [
229230
this.union([
@@ -275,6 +276,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder<
275276
this.schemaBuilderImports.addSingle(
276277
"UnknownEnumStringValue",
277278
"./models",
279+
false,
278280
)
279281
return this.union([
280282
this.stringEnum(model),

packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder<
227227
this.schemaBuilderImports.addSingle(
228228
"UnknownEnumNumberValue",
229229
"./models",
230+
false,
230231
)
231232
return [
232233
this.union([
@@ -278,6 +279,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder<
278279
this.schemaBuilderImports.addSingle(
279280
"UnknownEnumStringValue",
280281
"./models",
282+
false,
281283
)
282284
return this.union([
283285
this.stringEnum(model),

packages/openapi-code-generator/src/typescript/common/type-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ export class TypeBuilder implements ICompilable {
7777

7878
const name = this.getTypeNameFromRef({$ref})
7979

80-
this.imports?.addSingle(name, this.filename)
80+
this.imports?.addSingle(name, this.filename, true)
8181

8282
return name
8383
}
8484

8585
protected addStaticType(name: StaticType): string {
8686
this.parent?.addStaticType(name)
8787
this.referencedStaticTypes.add(name)
88-
this.imports?.addSingle(name, this.filename)
88+
this.imports?.addSingle(name, this.filename, true)
8989

9090
return name
9191
}

packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-router-builder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ export class ExpressRouterBuilder extends AbstractRouterBuilder {
3333
protected buildImports(): void {
3434
this.imports
3535
.from("express")
36-
.add("Router", "Request", "Response", "NextFunction")
36+
.add("Router")
37+
.addType("Request", "Response", "NextFunction")
3738

3839
this.imports.from("express").all("express")
3940

4041
this.imports
4142
.from("@nahkies/typescript-express-runtime/server")
42-
.add(
43+
.add("ExpressRuntimeResponse", "SkipResponse")
44+
.addType(
4345
"ExpressRuntimeResponder",
44-
"ExpressRuntimeResponse",
4546
"Params",
46-
"SkipResponse",
4747
"StatusCode",
4848
"StatusCode1xx",
4949
"StatusCode2xx",

packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-server-builder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export class ExpressServerBuilder implements ICompilable {
1212
) {
1313
this.imports
1414
.from("@nahkies/typescript-express-runtime/server")
15-
.add("startServer", "ServerConfig")
15+
.add("startServer")
16+
.addType("ServerConfig")
1617
}
1718

1819
toString(): string {

packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,24 @@ export class KoaRouterBuilder extends AbstractRouterBuilder {
3434
// todo: unsure why, but adding an export at `.` of index.ts doesn't work properly
3535
this.imports
3636
.from("@nahkies/typescript-koa-runtime/server")
37-
.add(
37+
.add("KoaRuntimeResponse", "SkipResponse", "startServer")
38+
.addType(
3839
"KoaRuntimeResponder",
39-
"KoaRuntimeResponse",
4040
"Params",
4141
"Response",
42-
"ServerConfig",
43-
"SkipResponse",
4442
"StatusCode",
4543
"StatusCode2xx",
4644
"StatusCode3xx",
4745
"StatusCode4xx",
4846
"StatusCode5xx",
49-
"startServer",
5047
)
5148

5249
this.imports
5350
.from("@nahkies/typescript-koa-runtime/errors")
5451
.add("KoaRuntimeError", "RequestInputType")
5552

5653
this.imports.from("koa").add("Next")
57-
this.imports.addModule("KoaRouter", "@koa/router")
58-
this.imports.from("@koa/router").add("RouterContext")
54+
this.imports.from("@koa/router").addType("RouterContext").all("KoaRouter")
5955

6056
const schemaBuilderType = this.schemaBuilder.type
6157

packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-server-builder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export class KoaServerBuilder implements ICompilable {
1212
) {
1313
this.imports
1414
.from("@nahkies/typescript-koa-runtime/server")
15-
.add("startServer", "ServerConfig")
15+
.add("startServer")
16+
.addType("ServerConfig")
1617
}
1718

1819
toString(): string {

0 commit comments

Comments
 (0)