diff --git a/e2e/jest.config.js b/e2e/jest.config.js index d47497655..22a43f492 100644 --- a/e2e/jest.config.js +++ b/e2e/jest.config.js @@ -1,12 +1,12 @@ -const base = require("../jest.base") -const {name: displayName} = require("./package.json") +import base from "../jest.base.js" +import pkg from "./package.json" with {type: "json"} /** * @type { import('@jest/types').Config.ProjectConfig } */ const config = { ...base, - displayName, + displayName: pkg.name, } -module.exports = config +export default config diff --git a/e2e/package.json b/e2e/package.json index 27d41cfc1..3c2076336 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -5,6 +5,7 @@ "author": "Michael Nahkies", "license": "MIT", "private": true, + "type": "module", "scripts": { "clean": "rm -rf ./dist && rm -rf ./src/generated", "generate": "./scripts/generate.sh", diff --git a/e2e/src/express.entrypoint.ts b/e2e/src/express.entrypoint.ts index bf9889194..9cff20eac 100644 --- a/e2e/src/express.entrypoint.ts +++ b/e2e/src/express.entrypoint.ts @@ -1,10 +1,10 @@ import {type NextFunction, type Request, type Response, Router} from "express" -import {bootstrap} from "./generated/server/express" -import {createEscapeHatchesRouter} from "./routes/express/escape-hatches" -import {createMediaTypesRouter} from "./routes/express/media-types" -import {createRequestHeadersRouter} from "./routes/express/request-headers" -import {createValidationRouter} from "./routes/express/validation" -import {createErrorResponse} from "./shared" +import {bootstrap} from "./generated/server/express/index.ts" +import {createEscapeHatchesRouter} from "./routes/express/escape-hatches.ts" +import {createMediaTypesRouter} from "./routes/express/media-types.ts" +import {createRequestHeadersRouter} from "./routes/express/request-headers.ts" +import {createValidationRouter} from "./routes/express/validation.ts" +import {createErrorResponse} from "./shared.ts" function createRouter() { const router = Router() diff --git a/e2e/src/generated/client/axios/client.ts b/e2e/src/generated/client/axios/client.ts index 257bfc4ea..2c2eb8df3 100644 --- a/e2e/src/generated/client/axios/client.ts +++ b/e2e/src/generated/client/axios/client.ts @@ -17,7 +17,7 @@ import type { t_postValidationOptionalBodyJson200Response, t_postValidationOptionalBodyJsonRequestBody, t_RandomNumber, -} from "./models" +} from "./models.ts" import { s_Enumerations, s_getHeadersRequestJson200Response, @@ -25,7 +25,7 @@ import { s_ProductOrder, s_postValidationOptionalBodyJson200Response, s_RandomNumber, -} from "./schemas" +} from "./schemas.ts" export class E2ETestClientServers { static default(): Server<"E2ETestClient"> { diff --git a/e2e/src/generated/client/axios/schemas.ts b/e2e/src/generated/client/axios/schemas.ts index 549bc25ca..1c8a844f2 100644 --- a/e2e/src/generated/client/axios/schemas.ts +++ b/e2e/src/generated/client/axios/schemas.ts @@ -3,7 +3,7 @@ /* eslint-disable */ import {z} from "zod/v4" -import type {UnknownEnumNumberValue, UnknownEnumStringValue} from "./models" +import type {UnknownEnumNumberValue, UnknownEnumStringValue} from "./models.ts" export const s_Enumerations = z.object({ colors: z.union([ diff --git a/e2e/src/generated/client/fetch/client.ts b/e2e/src/generated/client/fetch/client.ts index 88470ae2a..efcb1cfec 100644 --- a/e2e/src/generated/client/fetch/client.ts +++ b/e2e/src/generated/client/fetch/client.ts @@ -18,7 +18,7 @@ import type { t_postValidationOptionalBodyJson200Response, t_postValidationOptionalBodyJsonRequestBody, t_RandomNumber, -} from "./models" +} from "./models.ts" import { s_Enumerations, s_getHeadersRequestJson200Response, @@ -26,7 +26,7 @@ import { s_ProductOrder, s_postValidationOptionalBodyJson200Response, s_RandomNumber, -} from "./schemas" +} from "./schemas.ts" export class E2ETestClientServers { static default(): Server<"E2ETestClient"> { diff --git a/e2e/src/generated/client/fetch/schemas.ts b/e2e/src/generated/client/fetch/schemas.ts index 549bc25ca..1c8a844f2 100644 --- a/e2e/src/generated/client/fetch/schemas.ts +++ b/e2e/src/generated/client/fetch/schemas.ts @@ -3,7 +3,7 @@ /* eslint-disable */ import {z} from "zod/v4" -import type {UnknownEnumNumberValue, UnknownEnumStringValue} from "./models" +import type {UnknownEnumNumberValue, UnknownEnumStringValue} from "./models.ts" export const s_Enumerations = z.object({ colors: z.union([ diff --git a/e2e/src/generated/server/express/routes/media-types.ts b/e2e/src/generated/server/express/routes/media-types.ts index 4158ff8f1..b6df76a85 100644 --- a/e2e/src/generated/server/express/routes/media-types.ts +++ b/e2e/src/generated/server/express/routes/media-types.ts @@ -23,8 +23,8 @@ import type { t_PostMediaTypesTextRequestBodySchema, t_PostMediaTypesXWwwFormUrlencodedRequestBodySchema, t_ProductOrder, -} from "../models" -import {s_ProductOrder} from "../schemas" +} from "../models.ts" +import {s_ProductOrder} from "../schemas.ts" export type PostMediaTypesTextResponder = { with200(): ExpressRuntimeResponse diff --git a/e2e/src/generated/server/express/routes/request-headers.ts b/e2e/src/generated/server/express/routes/request-headers.ts index 93d7f6e40..ec42247c8 100644 --- a/e2e/src/generated/server/express/routes/request-headers.ts +++ b/e2e/src/generated/server/express/routes/request-headers.ts @@ -23,12 +23,12 @@ import type { t_GetHeadersRequestRequestHeaderSchema, t_getHeadersRequestJson200Response, t_getHeadersUndeclaredJson200Response, -} from "../models" +} from "../models.ts" import { PermissiveBoolean, s_getHeadersRequestJson200Response, s_getHeadersUndeclaredJson200Response, -} from "../schemas" +} from "../schemas.ts" export type GetHeadersUndeclaredResponder = { with200(): ExpressRuntimeResponse diff --git a/e2e/src/generated/server/express/routes/validation.ts b/e2e/src/generated/server/express/routes/validation.ts index 67b0afbaa..42a94bf78 100644 --- a/e2e/src/generated/server/express/routes/validation.ts +++ b/e2e/src/generated/server/express/routes/validation.ts @@ -26,13 +26,13 @@ import type { t_PostValidationOptionalBodyRequestBodySchema, t_postValidationOptionalBodyJson200Response, t_RandomNumber, -} from "../models" +} from "../models.ts" import { s_Enumerations, s_postValidationOptionalBodyJson200Response, s_postValidationOptionalBodyJsonRequestBody, s_RandomNumber, -} from "../schemas" +} from "../schemas.ts" export type GetValidationNumbersRandomNumberResponder = { with200(): ExpressRuntimeResponse diff --git a/e2e/src/generated/server/koa/routes/media-types.ts b/e2e/src/generated/server/koa/routes/media-types.ts index 18a86fc3b..fbfba1317 100644 --- a/e2e/src/generated/server/koa/routes/media-types.ts +++ b/e2e/src/generated/server/koa/routes/media-types.ts @@ -25,8 +25,8 @@ import type { t_PostMediaTypesTextBodySchema, t_PostMediaTypesXWwwFormUrlencodedBodySchema, t_ProductOrder, -} from "../models" -import {s_ProductOrder} from "../schemas" +} from "../models.ts" +import {s_ProductOrder} from "../schemas.ts" export type PostMediaTypesTextResponder = { with200(): KoaRuntimeResponse diff --git a/e2e/src/generated/server/koa/routes/request-headers.ts b/e2e/src/generated/server/koa/routes/request-headers.ts index d28b88597..0723c7756 100644 --- a/e2e/src/generated/server/koa/routes/request-headers.ts +++ b/e2e/src/generated/server/koa/routes/request-headers.ts @@ -25,12 +25,12 @@ import type { t_GetHeadersRequestHeaderSchema, t_getHeadersRequestJson200Response, t_getHeadersUndeclaredJson200Response, -} from "../models" +} from "../models.ts" import { PermissiveBoolean, s_getHeadersRequestJson200Response, s_getHeadersUndeclaredJson200Response, -} from "../schemas" +} from "../schemas.ts" export type GetHeadersUndeclaredResponder = { with200(): KoaRuntimeResponse diff --git a/e2e/src/generated/server/koa/routes/validation.ts b/e2e/src/generated/server/koa/routes/validation.ts index 58d66a88c..f98d32b49 100644 --- a/e2e/src/generated/server/koa/routes/validation.ts +++ b/e2e/src/generated/server/koa/routes/validation.ts @@ -28,13 +28,13 @@ import type { t_PostValidationOptionalBodyBodySchema, t_postValidationOptionalBodyJson200Response, t_RandomNumber, -} from "../models" +} from "../models.ts" import { s_Enumerations, s_postValidationOptionalBodyJson200Response, s_postValidationOptionalBodyJsonRequestBody, s_RandomNumber, -} from "../schemas" +} from "../schemas.ts" export type GetValidationNumbersRandomNumberResponder = { with200(): KoaRuntimeResponse diff --git a/e2e/src/index.axios.spec.ts b/e2e/src/index.axios.spec.ts index 9845bb30b..7c7fbc89b 100644 --- a/e2e/src/index.axios.spec.ts +++ b/e2e/src/index.axios.spec.ts @@ -1,10 +1,13 @@ import type {Server} from "node:http" import {afterAll, beforeAll, describe, expect, it} from "@jest/globals" import type {AxiosError} from "axios" -import {ApiClient, E2ETestClientServers} from "./generated/client/axios/client" -import type {t_ProductOrder} from "./generated/client/axios/models" -import {startServerFunctions} from "./index" -import {numberBetween} from "./test-utils" +import { + ApiClient, + E2ETestClientServers, +} from "./generated/client/axios/client.ts" +import type {t_ProductOrder} from "./generated/client/axios/models.ts" +import {startServerFunctions} from "./index.ts" +import {numberBetween} from "./test-utils.ts" describe.each(startServerFunctions)( "e2e - typescript-axios client against $name server", diff --git a/e2e/src/index.fetch.spec.ts b/e2e/src/index.fetch.spec.ts index b268c99e0..1c45fae6d 100644 --- a/e2e/src/index.fetch.spec.ts +++ b/e2e/src/index.fetch.spec.ts @@ -1,9 +1,12 @@ import type {Server} from "node:http" import {afterAll, beforeAll, describe, expect, it} from "@jest/globals" -import {ApiClient, E2ETestClientServers} from "./generated/client/fetch/client" -import type {t_ProductOrder} from "./generated/client/fetch/models" -import {startServerFunctions} from "./index" -import {numberBetween} from "./test-utils" +import { + ApiClient, + E2ETestClientServers, +} from "./generated/client/fetch/client.ts" +import type {t_ProductOrder} from "./generated/client/fetch/models.ts" +import {startServerFunctions} from "./index.ts" +import {numberBetween} from "./test-utils.ts" describe.each(startServerFunctions)( "e2e - typescript-fetch client against $name server", diff --git a/e2e/src/index.ts b/e2e/src/index.ts index c2c6a1842..ca7d2a44f 100644 --- a/e2e/src/index.ts +++ b/e2e/src/index.ts @@ -1,5 +1,5 @@ -import {startExpressServer} from "./express.entrypoint" -import {startKoaServer} from "./koa.entrypoint" +import {startExpressServer} from "./express.entrypoint.ts" +import {startKoaServer} from "./koa.entrypoint.ts" type StartServerFunction = { name: string diff --git a/e2e/src/koa.entrypoint.ts b/e2e/src/koa.entrypoint.ts index d77ffbb1e..c40e49474 100644 --- a/e2e/src/koa.entrypoint.ts +++ b/e2e/src/koa.entrypoint.ts @@ -1,10 +1,10 @@ import Router from "@koa/router" -import {bootstrap} from "./generated/server/koa" -import {createEscapeHatchesRouter} from "./routes/koa/escape-hatches" -import {createMediaTypesRouter} from "./routes/koa/media-types" -import {createRequestHeadersRouter} from "./routes/koa/request-headers" -import {createValidationRouter} from "./routes/koa/validation" -import {createErrorResponse} from "./shared" +import {bootstrap} from "./generated/server/koa/index.ts" +import {createEscapeHatchesRouter} from "./routes/koa/escape-hatches.ts" +import {createMediaTypesRouter} from "./routes/koa/media-types.ts" +import {createRequestHeadersRouter} from "./routes/koa/request-headers.ts" +import {createValidationRouter} from "./routes/koa/validation.ts" +import {createErrorResponse} from "./shared.ts" function createRouter() { const router = new Router() diff --git a/e2e/src/routes/express/escape-hatches.ts b/e2e/src/routes/express/escape-hatches.ts index 128f0c06d..ec24357fe 100644 --- a/e2e/src/routes/express/escape-hatches.ts +++ b/e2e/src/routes/express/escape-hatches.ts @@ -2,7 +2,7 @@ import {SkipResponse} from "@nahkies/typescript-express-runtime/server" import { createRouter, type GetEscapeHatchesPlainText, -} from "../../generated/server/express/routes/escape-hatches" +} from "../../generated/server/express/routes/escape-hatches.ts" const getEscapeHatchesPlainText: GetEscapeHatchesPlainText = async ( _, diff --git a/e2e/src/routes/express/media-types.ts b/e2e/src/routes/express/media-types.ts index 08587e495..8211a4438 100644 --- a/e2e/src/routes/express/media-types.ts +++ b/e2e/src/routes/express/media-types.ts @@ -3,7 +3,7 @@ import { createRouter, type PostMediaTypesText, type PostMediaTypesXWwwFormUrlencoded, -} from "../../generated/server/express/routes/media-types" +} from "../../generated/server/express/routes/media-types.ts" const postMediaTypesText: PostMediaTypesText = async ( {body}, diff --git a/e2e/src/routes/express/request-headers.ts b/e2e/src/routes/express/request-headers.ts index 4abc6a467..f3ae9c5fe 100644 --- a/e2e/src/routes/express/request-headers.ts +++ b/e2e/src/routes/express/request-headers.ts @@ -2,7 +2,7 @@ import { createRouter, type GetHeadersRequest, type GetHeadersUndeclared, -} from "../../generated/server/express/routes/request-headers" +} from "../../generated/server/express/routes/request-headers.ts" const getHeadersUndeclared: GetHeadersUndeclared = async ( {headers}, diff --git a/e2e/src/routes/express/validation.ts b/e2e/src/routes/express/validation.ts index 766be5ad9..70c0c25df 100644 --- a/e2e/src/routes/express/validation.ts +++ b/e2e/src/routes/express/validation.ts @@ -5,7 +5,7 @@ import { type GetValidationNumbersRandomNumber, type PostValidationEnums, type PostValidationOptionalBody, -} from "../../generated/server/express/routes/validation" +} from "../../generated/server/express/routes/validation.ts" const postValidationEnums: PostValidationEnums = async ({body}, respond) => { return respond.with200().body(body) diff --git a/e2e/src/routes/koa/escape-hatches.ts b/e2e/src/routes/koa/escape-hatches.ts index bd16949c0..c70bfc011 100644 --- a/e2e/src/routes/koa/escape-hatches.ts +++ b/e2e/src/routes/koa/escape-hatches.ts @@ -2,7 +2,7 @@ import {SkipResponse} from "@nahkies/typescript-koa-runtime/server" import { createRouter, type GetEscapeHatchesPlainText, -} from "../../generated/server/koa/routes/escape-hatches" +} from "../../generated/server/koa/routes/escape-hatches.ts" const getEscapeHatchesPlainText: GetEscapeHatchesPlainText = async ( _, diff --git a/e2e/src/routes/koa/media-types.ts b/e2e/src/routes/koa/media-types.ts index a601b7c5c..557e7df4b 100644 --- a/e2e/src/routes/koa/media-types.ts +++ b/e2e/src/routes/koa/media-types.ts @@ -3,7 +3,7 @@ import { createRouter, type PostMediaTypesText, type PostMediaTypesXWwwFormUrlencoded, -} from "../../generated/server/koa/routes/media-types" +} from "../../generated/server/koa/routes/media-types.ts" const postMediaTypesText: PostMediaTypesText = async ( {body}, diff --git a/e2e/src/routes/koa/request-headers.ts b/e2e/src/routes/koa/request-headers.ts index 82dce7b81..f10d5d0db 100644 --- a/e2e/src/routes/koa/request-headers.ts +++ b/e2e/src/routes/koa/request-headers.ts @@ -2,7 +2,7 @@ import { createRouter, type GetHeadersRequest, type GetHeadersUndeclared, -} from "../../generated/server/koa/routes/request-headers" +} from "../../generated/server/koa/routes/request-headers.ts" const getHeadersUndeclared: GetHeadersUndeclared = async ( {headers}, diff --git a/e2e/src/routes/koa/validation.ts b/e2e/src/routes/koa/validation.ts index 8e940d21e..75fc94784 100644 --- a/e2e/src/routes/koa/validation.ts +++ b/e2e/src/routes/koa/validation.ts @@ -5,7 +5,7 @@ import { type GetValidationNumbersRandomNumber, type PostValidationEnums, type PostValidationOptionalBody, -} from "../../generated/server/koa/routes/validation" +} from "../../generated/server/koa/routes/validation.ts" const postValidationEnums: PostValidationEnums = async ({body}, respond) => { return respond.with200().body(body) diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 9991e411f..6d6492706 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "composite": false, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "module": "NodeNext", + "target": "ESNext", + "verbatimModuleSyntax": true, + "rewriteRelativeImportExtensions": true }, "include": ["src/**/*"] } diff --git a/integration-tests/typescript-express/package.json b/integration-tests/typescript-express/package.json index 8414c0bb9..65fd2293a 100644 --- a/integration-tests/typescript-express/package.json +++ b/integration-tests/typescript-express/package.json @@ -5,6 +5,7 @@ "author": "Michael Nahkies", "license": "MIT", "private": true, + "type": "module", "scripts": { "clean": "rm -rf ./dist && rm -rf ./src/generated", "validate": "tsc -p ./tsconfig.json" diff --git a/integration-tests/typescript-express/src/generated/api.github.com.yaml/generated.ts b/integration-tests/typescript-express/src/generated/api.github.com.yaml/generated.ts index 537ba15db..4f8710471 100644 --- a/integration-tests/typescript-express/src/generated/api.github.com.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/api.github.com.yaml/generated.ts @@ -1832,7 +1832,7 @@ import type { t_workflow_run, t_workflow_run_usage, t_workflow_usage, -} from "./models" +} from "./models.ts" import { PermissiveBoolean, s_actions_billing_usage, @@ -2170,7 +2170,7 @@ import { s_workflow_run, s_workflow_run_usage, s_workflow_usage, -} from "./schemas" +} from "./schemas.ts" export type MetaRootResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/generated.ts b/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/generated.ts index 324d08d37..f2d6c153b 100644 --- a/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/generated.ts +++ b/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/generated.ts @@ -96,7 +96,7 @@ import type { t_WidgetsUpdateAnalyticsQuerySchema, t_WidgetsUpdateAnalyticsRequestBodySchema, t_WidgetsUpdateAnalyticsRequestHeaderSchema, -} from "./models" +} from "./models.ts" import { s_Azure_Core_Foundations_Error, s_Azure_Core_Foundations_ErrorResponse, @@ -114,7 +114,7 @@ import { s_WidgetPartReorderRequest, s_WidgetRepairRequest, s_WidgetRepairState, -} from "./schemas" +} from "./schemas.ts" export type GetServiceStatusResponder = { with200(): ExpressRuntimeResponse<{ diff --git a/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/schemas.ts b/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/schemas.ts index 4a2d53d8f..2908565af 100644 --- a/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/schemas.ts +++ b/integration-tests/typescript-express/src/generated/azure-core-data-plane-service.tsp/schemas.ts @@ -7,7 +7,7 @@ import type { t_Azure_Core_Foundations_Error, t_Azure_Core_Foundations_ErrorResponse, t_Azure_Core_Foundations_InnerError, -} from "./models" +} from "./models.ts" export const s_Azure_Core_Foundations_OperationState = z.union([ z.enum(["NotStarted", "Running", "Succeeded", "Failed", "Canceled"]), diff --git a/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/generated.ts b/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/generated.ts index 1d3722d09..b489ba96b 100644 --- a/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/generated.ts +++ b/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/generated.ts @@ -47,7 +47,7 @@ import type { t_MoveResponse, t_OperationListResult, t_OperationsListQuerySchema, -} from "./models" +} from "./models.ts" import { s_Azure_Core_uuid, s_Azure_ResourceManager_CommonTypes_ErrorResponse, @@ -57,7 +57,7 @@ import { s_MoveRequest, s_MoveResponse, s_OperationListResult, -} from "./schemas" +} from "./schemas.ts" export type OperationsListResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/schemas.ts b/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/schemas.ts index 2838ffc07..2fa51a8eb 100644 --- a/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/schemas.ts +++ b/integration-tests/typescript-express/src/generated/azure-resource-manager.tsp/schemas.ts @@ -6,7 +6,7 @@ import {z} from "zod/v4" import type { t_Azure_ResourceManager_CommonTypes_ErrorDetail, t_Azure_ResourceManager_CommonTypes_ErrorResponse, -} from "./models" +} from "./models.ts" export const PermissiveBoolean = z.preprocess((value) => { if (typeof value === "string" && (value === "true" || value === "false")) { diff --git a/integration-tests/typescript-express/src/generated/okta.idp.yaml/generated.ts b/integration-tests/typescript-express/src/generated/okta.idp.yaml/generated.ts index 7bb855643..0b02090b5 100644 --- a/integration-tests/typescript-express/src/generated/okta.idp.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/okta.idp.yaml/generated.ts @@ -66,7 +66,7 @@ import type { t_VerifyEmailOtpRequestBodySchema, t_VerifyPhoneChallengeParamSchema, t_VerifyPhoneChallengeRequestBodySchema, -} from "./models" +} from "./models.ts" import { PermissiveBoolean, s_AppAuthenticatorEnrollment, @@ -85,7 +85,7 @@ import { s_Schema, s_UpdateAppAuthenticatorEnrollmentRequest, s_UpdateAuthenticatorEnrollmentRequest, -} from "./schemas" +} from "./schemas.ts" export type CreateAppAuthenticatorEnrollmentResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/okta.oauth.yaml/generated.ts b/integration-tests/typescript-express/src/generated/okta.oauth.yaml/generated.ts index 28a7944b9..17cff795b 100644 --- a/integration-tests/typescript-express/src/generated/okta.oauth.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/okta.oauth.yaml/generated.ts @@ -94,7 +94,7 @@ import type { t_TokenResponse, t_UserInfo, t_UserinfoCustomAsParamSchema, -} from "./models" +} from "./models.ts" import { s_AcrValue, s_AmrValue, @@ -127,7 +127,7 @@ import { s_TokenRequest, s_TokenResponse, s_UserInfo, -} from "./schemas" +} from "./schemas.ts" export type GetWellKnownOpenIdConfigurationResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/petstore-expanded.yaml/generated.ts b/integration-tests/typescript-express/src/generated/petstore-expanded.yaml/generated.ts index 955c2df5d..4147cd66f 100644 --- a/integration-tests/typescript-express/src/generated/petstore-expanded.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/petstore-expanded.yaml/generated.ts @@ -28,8 +28,8 @@ import type { t_FindPetByIdParamSchema, t_FindPetsQuerySchema, t_Pet, -} from "./models" -import {s_Error, s_NewPet, s_Pet} from "./schemas" +} from "./models.ts" +import {s_Error, s_NewPet, s_Pet} from "./schemas.ts" export type FindPetsResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/stripe.yaml/generated.ts b/integration-tests/typescript-express/src/generated/stripe.yaml/generated.ts index af1a72b6b..0fc5c278f 100644 --- a/integration-tests/typescript-express/src/generated/stripe.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/stripe.yaml/generated.ts @@ -1104,7 +1104,7 @@ import type { t_treasury_transaction, t_treasury_transaction_entry, t_webhook_endpoint, -} from "./models" +} from "./models.ts" import { PermissiveBoolean, s_account, @@ -1264,7 +1264,7 @@ import { s_treasury_transaction, s_treasury_transaction_entry, s_webhook_endpoint, -} from "./schemas" +} from "./schemas.ts" export type GetAccountResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/src/generated/stripe.yaml/schemas.ts b/integration-tests/typescript-express/src/generated/stripe.yaml/schemas.ts index 2564e0e7c..6b4bdac1f 100644 --- a/integration-tests/typescript-express/src/generated/stripe.yaml/schemas.ts +++ b/integration-tests/typescript-express/src/generated/stripe.yaml/schemas.ts @@ -200,7 +200,7 @@ import type { t_treasury_transaction, t_treasury_transaction_entry, t_treasury_transactions_resource_flow_details, -} from "./models" +} from "./models.ts" export const PermissiveBoolean = z.preprocess((value) => { if (typeof value === "string" && (value === "true" || value === "false")) { diff --git a/integration-tests/typescript-express/src/generated/todo-lists.yaml/generated.ts b/integration-tests/typescript-express/src/generated/todo-lists.yaml/generated.ts index 469d4cebc..c5d94dd7f 100644 --- a/integration-tests/typescript-express/src/generated/todo-lists.yaml/generated.ts +++ b/integration-tests/typescript-express/src/generated/todo-lists.yaml/generated.ts @@ -36,14 +36,14 @@ import type { t_UpdateTodoListByIdParamSchema, t_UpdateTodoListByIdRequestBodySchema, t_UploadAttachmentRequestBodySchema, -} from "./models" +} from "./models.ts" import { s_CreateUpdateTodoList, s_Error, s_Statuses, s_TodoList, s_UnknownObject, -} from "./schemas" +} from "./schemas.ts" export type GetTodoListsResponder = { with200(): ExpressRuntimeResponse diff --git a/integration-tests/typescript-express/tsconfig.json b/integration-tests/typescript-express/tsconfig.json index b0cd1fe3a..3b3996dfd 100644 --- a/integration-tests/typescript-express/tsconfig.json +++ b/integration-tests/typescript-express/tsconfig.json @@ -6,6 +6,8 @@ "types": [], /* exercise the code path where exactOptionalPropertyTypes is enabled */ "exactOptionalPropertyTypes": true, - "noUnusedLocals": true + "verbatimModuleSyntax": true, + "noUnusedLocals": true, + "rewriteRelativeImportExtensions": true } } diff --git a/integration-tests/typescript-fetch/package.json b/integration-tests/typescript-fetch/package.json index 9e6f0b289..1c2ce1bf9 100644 --- a/integration-tests/typescript-fetch/package.json +++ b/integration-tests/typescript-fetch/package.json @@ -9,6 +9,7 @@ "clean": "rm -rf ./dist && rm -rf ./src/generated", "validate": "tsc -p ./tsconfig.json" }, + "type": "module", "dependencies": { "@nahkies/typescript-fetch-runtime": "workspace:*", "dotenv": "^17.2.3", diff --git a/integration-tests/typescript-fetch/src/generate-release-notes.ts b/integration-tests/typescript-fetch/src/generate-release-notes.ts index b94caa4d4..1a556cc3a 100644 --- a/integration-tests/typescript-fetch/src/generate-release-notes.ts +++ b/integration-tests/typescript-fetch/src/generate-release-notes.ts @@ -3,7 +3,7 @@ import path from "node:path" import { GitHubV3RestApi, GitHubV3RestApiServers, -} from "./generated/api.github.com.yaml/client" +} from "./generated/api.github.com.yaml/client.ts" function formatDate(dateStr: string | null) { if (!dateStr) return "" diff --git a/integration-tests/typescript-fetch/src/generated/api.github.com.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/api.github.com.yaml/client.ts index fca3760b0..c90317340 100644 --- a/integration-tests/typescript-fetch/src/generated/api.github.com.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/api.github.com.yaml/client.ts @@ -346,7 +346,7 @@ import type { t_workflow_run_usage, t_workflow_usage, UnknownEnumStringValue, -} from "./models" +} from "./models.ts" export class GitHubV3RestApiServersOperations { static reposUploadReleaseAsset( diff --git a/integration-tests/typescript-fetch/src/generated/azure-core-data-plane-service.tsp/client.ts b/integration-tests/typescript-fetch/src/generated/azure-core-data-plane-service.tsp/client.ts index d0614626f..1296be146 100644 --- a/integration-tests/typescript-fetch/src/generated/azure-core-data-plane-service.tsp/client.ts +++ b/integration-tests/typescript-fetch/src/generated/azure-core-data-plane-service.tsp/client.ts @@ -26,7 +26,7 @@ import type { t_WidgetPartReorderRequest, t_WidgetRepairRequest, t_WidgetRepairState, -} from "./models" +} from "./models.ts" export class ContosoWidgetManagerServers { static server(url: "{endpoint}/widget" = "{endpoint}/widget"): { diff --git a/integration-tests/typescript-fetch/src/generated/azure-resource-manager.tsp/client.ts b/integration-tests/typescript-fetch/src/generated/azure-resource-manager.tsp/client.ts index 27ad37bef..b8fdfcd92 100644 --- a/integration-tests/typescript-fetch/src/generated/azure-resource-manager.tsp/client.ts +++ b/integration-tests/typescript-fetch/src/generated/azure-resource-manager.tsp/client.ts @@ -18,7 +18,7 @@ import type { t_MoveRequest, t_MoveResponse, t_OperationListResult, -} from "./models" +} from "./models.ts" export class ContosoProviderHubClientServers { static default(): Server<"ContosoProviderHubClient"> { diff --git a/integration-tests/typescript-fetch/src/generated/okta.idp.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/okta.idp.yaml/client.ts index faea394cf..167f31a25 100644 --- a/integration-tests/typescript-fetch/src/generated/okta.idp.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/okta.idp.yaml/client.ts @@ -27,7 +27,7 @@ import type { t_UpdateAppAuthenticatorEnrollmentRequest, t_UpdateAuthenticatorEnrollmentRequest, UnknownEnumStringValue, -} from "./models" +} from "./models.ts" export class MyAccountManagementServers { static default(): Server<"MyAccountManagement"> { diff --git a/integration-tests/typescript-fetch/src/generated/okta.oauth.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/okta.oauth.yaml/client.ts index 38b79e1ee..7d5d25c5c 100644 --- a/integration-tests/typescript-fetch/src/generated/okta.oauth.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/okta.oauth.yaml/client.ts @@ -40,7 +40,7 @@ import type { t_TokenRequest, t_TokenResponse, t_UserInfo, -} from "./models" +} from "./models.ts" export class OktaOpenIdConnectOAuth20Servers { static default(): Server<"OktaOpenIdConnectOAuth20"> { diff --git a/integration-tests/typescript-fetch/src/generated/petstore-expanded.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/petstore-expanded.yaml/client.ts index c5991fdb8..e83528792 100644 --- a/integration-tests/typescript-fetch/src/generated/petstore-expanded.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/petstore-expanded.yaml/client.ts @@ -9,7 +9,7 @@ import { type Server, type StatusCode, } from "@nahkies/typescript-fetch-runtime/main" -import type {t_Error, t_NewPet, t_Pet} from "./models" +import type {t_Error, t_NewPet, t_Pet} from "./models.ts" export class SwaggerPetstoreServers { static default(): Server<"SwaggerPetstore"> { diff --git a/integration-tests/typescript-fetch/src/generated/stripe.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/stripe.yaml/client.ts index 53e196915..8ebe2521b 100644 --- a/integration-tests/typescript-fetch/src/generated/stripe.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/stripe.yaml/client.ts @@ -169,7 +169,7 @@ import type { t_treasury_transaction_entry, t_webhook_endpoint, UnknownEnumStringValue, -} from "./models" +} from "./models.ts" export class StripeApiServersOperations { static postFiles( diff --git a/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts index d2e1fc123..2bed2a748 100644 --- a/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts +++ b/integration-tests/typescript-fetch/src/generated/todo-lists.yaml/client.ts @@ -17,7 +17,7 @@ import type { t_Statuses, t_TodoList, t_UnknownObject, -} from "./models" +} from "./models.ts" export class TodoListsExampleApiServersOperations { static listAttachments(url?: "{schema}://{tenant}.attachments.example.com"): { diff --git a/integration-tests/typescript-fetch/src/todo-lists.type-tests.ts b/integration-tests/typescript-fetch/src/todo-lists.type-tests.ts index db075063b..f274d170d 100644 --- a/integration-tests/typescript-fetch/src/todo-lists.type-tests.ts +++ b/integration-tests/typescript-fetch/src/todo-lists.type-tests.ts @@ -1,4 +1,4 @@ -import {TodoListsExampleApiServers} from "./generated/todo-lists.yaml/client" +import {TodoListsExampleApiServers} from "./generated/todo-lists.yaml/client.ts" TodoListsExampleApiServers.server() diff --git a/integration-tests/typescript-fetch/src/uniform-github-repositories.ts b/integration-tests/typescript-fetch/src/uniform-github-repositories.ts index d450aeeef..dad83396f 100644 --- a/integration-tests/typescript-fetch/src/uniform-github-repositories.ts +++ b/integration-tests/typescript-fetch/src/uniform-github-repositories.ts @@ -2,8 +2,8 @@ import dotenv from "dotenv" dotenv.config() -import {ApiClient} from "./generated/api.github.com.yaml/client" -import type {t_repository} from "./generated/api.github.com.yaml/models" +import {ApiClient} from "./generated/api.github.com.yaml/client.ts" +import type {t_repository} from "./generated/api.github.com.yaml/models.ts" const {writeHeapSnapshot} = require("node:v8") diff --git a/integration-tests/typescript-fetch/tsconfig.json b/integration-tests/typescript-fetch/tsconfig.json index e0f0c1a7c..eea5bf44e 100644 --- a/integration-tests/typescript-fetch/tsconfig.json +++ b/integration-tests/typescript-fetch/tsconfig.json @@ -6,6 +6,8 @@ "rootDir": "./src", /* exercise the code path where exactOptionalPropertyTypes is disabled */ "exactOptionalPropertyTypes": false, - "noUnusedLocals": true + "noUnusedLocals": true, + "verbatimModuleSyntax": true, + "rewriteRelativeImportExtensions": true } } diff --git a/packages/documentation/src/app/guides/esm-support/page.mdx b/packages/documentation/src/app/guides/esm-support/page.mdx new file mode 100644 index 000000000..4c4e97d15 --- /dev/null +++ b/packages/documentation/src/app/guides/esm-support/page.mdx @@ -0,0 +1,65 @@ +--- +title: ESM Support +description: Explanation of how to use the code generator in projects using ESM +--- + +# ESM Support + +The generated code should work with projects using ESM and modern versions of nodejs. + +It will try to detect this from the `package.json` file, based on whether the `type` field is set to `module`. +You can override this by passing the `--ts-is-esm-project ` flag to the code generator, +or using the `OPENAPI_TS_IS_ESM_PROJECT` environment variable. + +## Project Configuration + +### package.json +Ensure that you have `"type": "module"` in your `package.json` file. + +```json5 +{ + "type": "module" +} +``` + +### tsconfig.json +Ensure that you have `"module": "nodenext"` in your `tsconfig.json` file, +and have rewriteRelativeImportExtensions set to true. + +```json5 +{ + "compilerOptions": { + "module": "nodenext", + "target": "esnext", + // make typescript replace .ts file extensions in imports with .js in transpiled code + "rewriteRelativeImportExtensions": true, + // optional + "verbatimModuleSyntax": true, + } +} +``` + +## How does it work? + +The code generator CLI itself, and the runtime packages still ship as CommonJS modules. +However, NodeJS supports loading CommonJS from either ESM or CommonJS contexts (ref: [interoperability-with-commonjs](https://nodejs.org/api/esm.html#interoperability-with-commonjs)) + +This means you can run the generator, and import the runtime code from both ESM and CommonJS projects. + +We do have to change one thing in the generated code, for it to work with ESM, and that's including file extensions in imports. + +Eg: +```typescript +import { + // schema names +} from "./schemas" +``` + +Becomes +```typescript +import { + // schema names +} from "./schemas.ts" +``` + +This is controlled by the `--ts-is-esm-project true` flag mentioned above, though in most cases it should be detected automatically. diff --git a/packages/documentation/src/app/reference/cli-options/page.mdx b/packages/documentation/src/app/reference/cli-options/page.mdx index 80665b546..d62eebb22 100644 --- a/packages/documentation/src/app/reference/cli-options/page.mdx +++ b/packages/documentation/src/app/reference/cli-options/page.mdx @@ -129,6 +129,15 @@ See [enums](../guides/concepts/enums) for more information. Default: `open` is used for client templates, and `closed` for server templates. +#### `--ts-is-esm-project ` +As environment variable `OPENAPI_TS_IS_ESM_PROJECT` + +Allows overriding the auto-detected value for whether the generated code is part of an ESM project. + +See [ESM Support](../guides/esm-support) for more information. + +Default: Auto-detected. + #### `--ts-allow-any` As environment variable `OPENAPI_TS_ALLOW_ANY` diff --git a/packages/documentation/src/lib/playground.tsx b/packages/documentation/src/lib/playground.tsx index 459ccf256..bac51e2a4 100644 --- a/packages/documentation/src/lib/playground.tsx +++ b/packages/documentation/src/lib/playground.tsx @@ -57,6 +57,7 @@ const defaultConfig = { tsCompilerOptions: {exactOptionalPropertyTypes: false}, enableTypedBasePaths: true, filenameConvention: "kebab-case", + tsIsEsmProject: false, tsServerImplementationMethod: "type", enumExtensibility: "", } satisfies Config diff --git a/packages/documentation/src/lib/playground/config-form.tsx b/packages/documentation/src/lib/playground/config-form.tsx index 262075d50..88b0046f6 100644 --- a/packages/documentation/src/lib/playground/config-form.tsx +++ b/packages/documentation/src/lib/playground/config-form.tsx @@ -17,6 +17,7 @@ const schema = configSchema.pick({ groupingStrategy: true, tsAllowAny: true, tsServerImplementationMethod: true, + tsIsEsmProject: true, enumExtensibility: true, }) @@ -37,6 +38,7 @@ export const ConfigForm: React.FC<{ groupingStrategy: config.groupingStrategy, tsAllowAny: config.tsAllowAny, tsServerImplementationMethod: config.tsServerImplementationMethod, + tsIsEsmProject: config.tsIsEsmProject, enumExtensibility: config.enumExtensibility, } as const, }) @@ -83,6 +85,11 @@ export const ConfigForm: React.FC<{ name={"tsAllowAny"} control={control} /> + { +const optionalBoolParser = (arg: string): boolean | undefined => { const TRUTHY_VALUES = ["true", "1", "on"] const FALSY_VALUES = ["false", "0", "off", ""] @@ -41,6 +42,10 @@ export const boolParser = (arg: string): boolean => { return false } + if (!arg) { + return undefined + } + throw new InvalidArgumentError( `'${arg}' is not a valid boolean parameter. Valid truthy values are: ${TRUTHY_VALUES.map( (it) => JSON.stringify(it), @@ -50,6 +55,16 @@ export const boolParser = (arg: string): boolean => { ) } +export const boolParser = (arg: string): boolean => { + const result = optionalBoolParser(arg) + + if (result === undefined) { + throw new InvalidArgumentError(`'${arg}' is not a valid boolean parameter.`) + } + + return result +} + export const remoteSpecRequestHeadersParser = (arg: string) => { return z .preprocess( @@ -123,6 +138,14 @@ const program = new Command() .argParser(boolParser) .default(false), ) + .addOption( + new Option( + "--ts-is-esm-project [bool]", + `(typescript) whether the target project uss esm or commonjs. auto-detected from package.json when omitted.`, + ) + .env("OPENAPI_TS_IS_ESM_PROJECT") + .argParser(optionalBoolParser), + ) .addOption( new Option( "--ts-server-implementation-method ", @@ -285,12 +308,16 @@ async function main() { ) const outputPath = path.join(process.cwd(), config.output) + const formatterOptions = await loadTypescriptFormatterConfig( outputPath, fsAdaptor, ) + const formatter = await formatterFactory(formatterOptions) + const projectPackageJson = await loadPackageJson(outputPath, fsAdaptor) + const compilerOptions = await loadTsConfigCompilerOptions( outputPath, fsAdaptor, @@ -298,6 +325,8 @@ async function main() { await generate( configSchema.parse({ + // can be overridden by config spread + tsIsEsmProject: projectPackageJson.type === "module", ...config, tsCompilerOptions: compilerOptions, }), diff --git a/packages/openapi-code-generator/src/config.ts b/packages/openapi-code-generator/src/config.ts index 538ce53ef..1a9e226fc 100644 --- a/packages/openapi-code-generator/src/config.ts +++ b/packages/openapi-code-generator/src/config.ts @@ -27,6 +27,7 @@ export type Config = { enumExtensibility: "" | "open" | "closed" tsAllowAny: boolean tsServerImplementationMethod: ServerImplementationMethod + tsIsEsmProject: boolean tsCompilerOptions: CompilerOptions remoteSpecRequestHeaders?: GenericLoaderRequestHeaders | undefined } @@ -64,6 +65,7 @@ export const configSchema = z.object({ enumExtensibility: z.enum(["", "open", "closed"]), tsAllowAny: z.boolean(), tsServerImplementationMethod: tsServerImplementationSchema, + tsIsEsmProject: z.boolean(), tsCompilerOptions: tsconfigSchema.shape.compilerOptions, remoteSpecRequestHeaders: z .record( diff --git a/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts b/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts index f9fcfd473..f4cc79108 100644 --- a/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts +++ b/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts @@ -13,8 +13,19 @@ export class NodeFsAdaptor implements IFsAdaptor { } async exists(path: string) { - const stat = await fs.stat(path) - return stat.isFile() + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (err) { + if ( + typeof err === "object" && + err !== null && + Reflect.get(err, "code") === "ENOENT" + ) { + return false + } + throw err + } } existsSync(path: string) { diff --git a/packages/openapi-code-generator/src/core/loaders/package.json.loader.spec.ts b/packages/openapi-code-generator/src/core/loaders/package.json.loader.spec.ts new file mode 100644 index 000000000..8f46e3e29 --- /dev/null +++ b/packages/openapi-code-generator/src/core/loaders/package.json.loader.spec.ts @@ -0,0 +1,33 @@ +import path from "node:path" +import {describe, expect, it} from "@jest/globals" +import {NodeFsAdaptor} from "../file-system/node-fs-adaptor" +import {loadPackageJson} from "./package.json.loader" + +describe("core/loaders/package.json.loader", () => { + const fsAdaptor = new NodeFsAdaptor() + + it("should load the nearest package.json (commonjs)", async () => { + const actual = await loadPackageJson(__dirname, fsAdaptor) + + expect(actual).toEqual({ + type: "commonjs", + }) + }) + + it("should load the nearest package.json (esm)", async () => { + const actual = await loadPackageJson( + path.join(__dirname, "../../../../../e2e"), + fsAdaptor, + ) + expect(actual).toEqual({ + type: "module", + }) + }) + + it("should default to commonjs if no package.json is found", async () => { + const actual = await loadPackageJson("/tmp/foo/bla", fsAdaptor) + expect(actual).toEqual({ + type: "commonjs", + }) + }) +}) diff --git a/packages/openapi-code-generator/src/core/loaders/package.json.loader.ts b/packages/openapi-code-generator/src/core/loaders/package.json.loader.ts new file mode 100644 index 000000000..11bfa19b2 --- /dev/null +++ b/packages/openapi-code-generator/src/core/loaders/package.json.loader.ts @@ -0,0 +1,25 @@ +import json5 from "json5" +import {z} from "zod/v4" +import type {IFsAdaptor} from "../file-system/fs-adaptor" +import {logger} from "../logger" +import {loadFileUp} from "./utils" + +const schema = z.object({ + type: z.enum(["module", "commonjs"]).optional().default("commonjs"), +}) + +export async function loadPackageJson( + outputPath: string, + fsAdaptor: IFsAdaptor, +) { + let rawJson = await loadFileUp("package.json", outputPath, fsAdaptor) + + if (!rawJson) { + logger.warn("no package.json found, using defaults") + rawJson = "{}" + } + + const packageJson = json5.parse(rawJson) + + return schema.parse(packageJson) +} diff --git a/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts b/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts index 9d4f8afe6..5fe3d6776 100644 --- a/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts +++ b/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts @@ -11,14 +11,17 @@ import { export type CompilerOptions = Pick< TsCompilerOptions, - "exactOptionalPropertyTypes" + "exactOptionalPropertyTypes" | "rewriteRelativeImportExtensions" > export async function loadTsConfigCompilerOptions( searchPath: string, fsAdaptor: IFsAdaptor, ): Promise { - const defaults = {exactOptionalPropertyTypes: false} + const defaults = { + exactOptionalPropertyTypes: false, + rewriteRelativeImportExtensions: false, + } try { const path = ts.findConfigFile(searchPath, (it) => fsAdaptor.existsSync(it)) diff --git a/packages/openapi-code-generator/src/core/loaders/utils.ts b/packages/openapi-code-generator/src/core/loaders/utils.ts index 93ffb3b61..08bf8adaf 100644 --- a/packages/openapi-code-generator/src/core/loaders/utils.ts +++ b/packages/openapi-code-generator/src/core/loaders/utils.ts @@ -1,3 +1,25 @@ +import path from "node:path" +import type {IFsAdaptor} from "../file-system/fs-adaptor" + export function isRemote(location: string): boolean { return location.startsWith("http://") || location.startsWith("https://") } + +export async function loadFileUp( + filename: string, + directory: string, + fsAdaptor: IFsAdaptor, +): Promise { + while (directory !== "/") { + const filePath = path.join(directory, filename) + + if (await fsAdaptor.exists(filePath)) { + return fsAdaptor.readFile(filePath) + } + + // biome-ignore lint/style/noParameterAssign: walk + directory = path.dirname(directory) + } + + return undefined +} diff --git a/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts b/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts index 61ac0a27d..d0dcd9e94 100644 --- a/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts +++ b/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts @@ -35,6 +35,8 @@ export const tsconfigSchema = z.object({ noPropertyAccessFromIndexSignature: z.boolean(), allowUnusedLabels: z.boolean(), allowUnreachableCode: z.boolean(), + rewriteRelativeImportExtensions: z.boolean(), + verbatimModuleSyntax: z.boolean(), }) .partial(), }) diff --git a/packages/openapi-code-generator/src/index.ts b/packages/openapi-code-generator/src/index.ts index fdf6d92e1..c3073ef2f 100644 --- a/packages/openapi-code-generator/src/index.ts +++ b/packages/openapi-code-generator/src/index.ts @@ -76,6 +76,15 @@ export async function generate( allowUnusedImports: config.allowUnusedImports, }) + if ( + config.tsIsEsmProject && + !config.tsCompilerOptions.rewriteRelativeImportExtensions + ) { + logger.warn( + `Generating in ESM mode, but typescript compiler option "rewriteRelativeImportExtensions" is not set to true. This may result in broken imports.`, + ) + } + await generator.run({ input, emitter, @@ -85,6 +94,7 @@ export async function generate( compilerOptions: config.tsCompilerOptions, groupingStrategy: config.groupingStrategy, filenameConvention: config.filenameConvention, + isEsmProject: config.tsIsEsmProject, allowAny: config.tsAllowAny, serverImplementationMethod: config.tsServerImplementationMethod, }) diff --git a/packages/openapi-code-generator/src/templates.types.ts b/packages/openapi-code-generator/src/templates.types.ts index 567f31bc8..2f3c8bbf5 100644 --- a/packages/openapi-code-generator/src/templates.types.ts +++ b/packages/openapi-code-generator/src/templates.types.ts @@ -27,6 +27,10 @@ export interface OpenapiTypescriptGeneratorConfig * Which runtime schema parsing library to use */ schemaBuilder: SchemaBuilderType + /** + * Whether the project is using ESM, or CommonJS + */ + isEsmProject: boolean /** * Sub-set of typescript compiler options relevant to codegen */ diff --git a/packages/openapi-code-generator/src/typescript/client/client-servers-builder.spec.ts b/packages/openapi-code-generator/src/typescript/client/client-servers-builder.spec.ts index 3b3877de6..20b628ea1 100644 --- a/packages/openapi-code-generator/src/typescript/client/client-servers-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/client/client-servers-builder.spec.ts @@ -19,7 +19,7 @@ async function runTest( "unit-test.ts", "UnitTest", servers, - new ImportBuilder(), + new ImportBuilder({includeFileExtensions: false}), ) for (const it of operations) { diff --git a/packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-module-builder.ts b/packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-module-builder.ts index 901efae58..3eb04867a 100644 --- a/packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-module-builder.ts +++ b/packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-module-builder.ts @@ -1,9 +1,7 @@ import {CompilationUnit, type ICompilable} from "../../common/compilation-units" -import {ImportBuilder} from "../../common/import-builder" +import type {ImportBuilder} from "../../common/import-builder" export class AngularModuleBuilder implements ICompilable { - private readonly tsImports: ImportBuilder - private readonly ngImports = new Set() private readonly ngDeclarations = new Set() private readonly ngExports = new Set() @@ -12,9 +10,8 @@ export class AngularModuleBuilder implements ICompilable { constructor( readonly filename: string, public readonly exportName: string, + private readonly tsImports: ImportBuilder, ) { - this.tsImports = new ImportBuilder() - this.tsImports.from("@angular/core").add("NgModule") } diff --git a/packages/openapi-code-generator/src/typescript/client/typescript-angular/typescript-angular.generator.ts b/packages/openapi-code-generator/src/typescript/client/typescript-angular/typescript-angular.generator.ts index cc9a2c840..271dfcb0c 100644 --- a/packages/openapi-code-generator/src/typescript/client/typescript-angular/typescript-angular.generator.ts +++ b/packages/openapi-code-generator/src/typescript/client/typescript-angular/typescript-angular.generator.ts @@ -11,22 +11,28 @@ export async function generateTypescriptAngular( ): Promise { const {input, emitter, allowAny} = config + const importBuilderConfig = {includeFileExtensions: config.isEsmProject} + + const schemaBuilderImports = new ImportBuilder(importBuilderConfig) + const moduleImports = new ImportBuilder(importBuilderConfig) + const serviceImports = new ImportBuilder(importBuilderConfig) + const rootTypeBuilder = await TypeBuilder.fromInput( "./models.ts", input, config.compilerOptions, {allowAny}, ) + const rootSchemaBuilder = await schemaBuilderFactory( "./schemas.ts", input, config.schemaBuilder, {allowAny}, + schemaBuilderImports, rootTypeBuilder, ) - const imports = new ImportBuilder() - const exportName = titleCase(input.name()) const serviceExportName = `${exportName}Service` const moduleExportName = `${exportName}Module` @@ -35,9 +41,9 @@ export async function generateTypescriptAngular( "client.service.ts", serviceExportName, input, - imports, - rootTypeBuilder.withImports(imports), - rootSchemaBuilder.withImports(imports), + serviceImports, + rootTypeBuilder.withImports(serviceImports), + rootSchemaBuilder.withImports(serviceImports), { enableRuntimeResponseValidation: config.enableRuntimeResponseValidation, enableTypedBasePaths: config.enableTypedBasePaths, @@ -46,7 +52,11 @@ export async function generateTypescriptAngular( input.allOperations().map((it) => client.add(it)) - const module = new AngularModuleBuilder("api.module.ts", moduleExportName) + const module = new AngularModuleBuilder( + "api.module.ts", + moduleExportName, + moduleImports, + ) module.provides(`./${client.filename}`).add(client.exportName) diff --git a/packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios.generator.ts b/packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios.generator.ts index 2f6057b10..79e29af13 100644 --- a/packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios.generator.ts +++ b/packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios.generator.ts @@ -10,6 +10,11 @@ export async function generateTypescriptAxios( ): Promise { const {input, emitter, allowAny} = config + const importBuilderConfig = {includeFileExtensions: config.isEsmProject} + + const schemaBuilderImports = new ImportBuilder(importBuilderConfig) + const clientImports = new ImportBuilder(importBuilderConfig) + const rootTypeBuilder = await TypeBuilder.fromInput( "./models.ts", input, @@ -22,11 +27,10 @@ export async function generateTypescriptAxios( input, config.schemaBuilder, {allowAny}, + schemaBuilderImports, rootTypeBuilder, ) - const imports = new ImportBuilder() - const filename = "client.ts" const exportName = titleCase(input.name()) @@ -34,9 +38,9 @@ export async function generateTypescriptAxios( filename, exportName, input, - imports, - rootTypeBuilder.withImports(imports), - rootSchemaBuilder.withImports(imports), + clientImports, + rootTypeBuilder.withImports(clientImports), + rootSchemaBuilder.withImports(clientImports), { enableRuntimeResponseValidation: config.enableRuntimeResponseValidation, enableTypedBasePaths: config.enableTypedBasePaths, diff --git a/packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch.generator.ts b/packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch.generator.ts index 67a52fa98..42ba872c3 100644 --- a/packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch.generator.ts +++ b/packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch.generator.ts @@ -10,6 +10,10 @@ export async function generateTypescriptFetch( ): Promise { const {input, emitter, allowAny} = config + const importBuilderConfig = {includeFileExtensions: config.isEsmProject} + + const schemaBuilderImports = new ImportBuilder(importBuilderConfig) + const rootTypeBuilder = await TypeBuilder.fromInput( "./models.ts", input, @@ -21,10 +25,11 @@ export async function generateTypescriptFetch( input, config.schemaBuilder, {allowAny}, + schemaBuilderImports, rootTypeBuilder, ) - const imports = new ImportBuilder() + const imports = new ImportBuilder(importBuilderConfig) const filename = "client.ts" const exportName = titleCase(input.name()) diff --git a/packages/openapi-code-generator/src/typescript/common/import-builder.spec.ts b/packages/openapi-code-generator/src/typescript/common/import-builder.spec.ts index 68e17dc40..24ed45a8f 100644 --- a/packages/openapi-code-generator/src/typescript/common/import-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/import-builder.spec.ts @@ -11,14 +11,14 @@ describe("typescript/common/import-builder", () => { }) it("can import whole modules", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addModule("_", "lodash") expect(builder.toString()).toBe("import _ from 'lodash'") }) it("can import individual exports", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("Cat", "./models.ts", false) builder.addSingle("Dog", "./models.ts", false) @@ -27,7 +27,7 @@ describe("typescript/common/import-builder", () => { }) it("can import a whole module, and individual exports", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("Next", "koa", false) builder.addSingle("Context", "koa", false) @@ -38,7 +38,12 @@ describe("typescript/common/import-builder", () => { describe("relative path handling", () => { it("same directory", () => { - const builder = new ImportBuilder({filename: "./foo/example.ts"}) + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: false, + }) builder.addSingle("Cat", "./foo/models.ts", false) @@ -46,7 +51,12 @@ describe("typescript/common/import-builder", () => { }) it("parent directory", () => { - const builder = new ImportBuilder({filename: "./foo/example.ts"}) + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: false, + }) builder.addSingle("Cat", "./models.ts", false) @@ -54,7 +64,12 @@ describe("typescript/common/import-builder", () => { }) it("child directory", () => { - const builder = new ImportBuilder({filename: "./example.ts"}) + const builder = new ImportBuilder({ + unit: { + filename: "./example", + }, + includeFileExtensions: false, + }) builder.addSingle("Cat", "./foo/models.ts", false) @@ -62,7 +77,12 @@ describe("typescript/common/import-builder", () => { }) it("sibling directory", () => { - const builder = new ImportBuilder({filename: "./foo/example.ts"}) + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: false, + }) builder.addSingle("Cat", "./bar/models.ts", false) @@ -72,7 +92,7 @@ describe("typescript/common/import-builder", () => { describe("type imports", () => { it("can import types", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("Cat", "./models.ts", false) builder.addSingle("Dog", "./models.ts", true) @@ -81,7 +101,7 @@ describe("typescript/common/import-builder", () => { }) it("formats all-type named imports as 'import type {A, B}'", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("A", "lib", true) builder.addSingle("B", "lib", true) @@ -90,7 +110,7 @@ describe("typescript/common/import-builder", () => { }) it("mixes default import with only-type named imports", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addModule("Default", "lib") builder.addSingle("A", "lib", true) @@ -102,7 +122,7 @@ describe("typescript/common/import-builder", () => { }) it("deduplicates names when added as both value and type (prefers value)", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("Overlap", "pkg", false) builder.addSingle("Overlap", "pkg", true) @@ -116,7 +136,7 @@ describe("typescript/common/import-builder", () => { describe("from() chaining API", () => { it("supports add, addType, and all in a single chain", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.from("koa").add("Context", "Next").addType("State").all("koa") @@ -128,7 +148,7 @@ describe("typescript/common/import-builder", () => { describe("usage-based pruning", () => { it("omits unused named and default imports based on provided code", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addModule("_, defaultExport", "ignore-me") // ensure not used builder.addModule("Lodash", "lodash") @@ -154,7 +174,7 @@ describe("typescript/common/import-builder", () => { describe("sorting", () => { it("sorts named imports alphabetically and groups types correctly", () => { - const builder = new ImportBuilder() + const builder = new ImportBuilder({includeFileExtensions: false}) builder.addSingle("Bravo", "pkg", false) builder.addSingle("Alpha", "pkg", false) @@ -167,7 +187,12 @@ describe("typescript/common/import-builder", () => { }) it("orders sources by Biome distance (URL > protocol pkg > pkg > alias > paths)", () => { - const builder = new ImportBuilder({filename: "./foo/example.ts"}) + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: false, + }) builder.addModule("S1", "./file.js") // sibling builder.addModule("A1", "#alias") // alias @@ -201,11 +226,11 @@ describe("typescript/common/import-builder", () => { describe("merge()", () => { it("merges multiple builders and preserves types/values", () => { - const a = new ImportBuilder() + const a = new ImportBuilder({includeFileExtensions: false}) a.addSingle("A", "x", false) a.addSingle("TA", "x", true) - const b = new ImportBuilder() + const b = new ImportBuilder({includeFileExtensions: false}) b.addModule("Def", "x") b.addSingle("B", "x", false) @@ -215,10 +240,10 @@ describe("typescript/common/import-builder", () => { }) it("throws when merging builders with conflicting default imports for same module", () => { - const a = new ImportBuilder() + const a = new ImportBuilder({includeFileExtensions: false}) a.addModule("DefA", "x") - const b = new ImportBuilder() + const b = new ImportBuilder({includeFileExtensions: false}) b.addModule("DefB", "x") expect(() => ImportBuilder.merge(undefined, a, b)).toThrow( @@ -226,4 +251,104 @@ describe("typescript/common/import-builder", () => { ) }) }) + + describe("includeFileExtensions = true", () => { + it("keeps .ts extensions in named imports", () => { + const builder = new ImportBuilder({includeFileExtensions: true}) + builder.addSingle("Cat", "./models.ts", false) + builder.addSingle("Dog", "./models.ts", false) + expect(builder.toString()).toBe("import {Cat, Dog} from './models.ts'") + }) + + it("keeps .ts extension for default (module) imports from relative files", () => { + const builder = new ImportBuilder({includeFileExtensions: true}) + builder.addModule("Util", "./util.ts") + expect(builder.toString()).toBe("import Util from './util.ts'") + }) + + describe("relative path handling", () => { + it("same directory keeps extension", () => { + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: true, + }) + + builder.addSingle("Cat", "./foo/models.ts", false) + + expect(builder.toString()).toBe("import {Cat} from './models.ts'") + }) + + it("parent directory keeps extension", () => { + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: true, + }) + + builder.addSingle("Cat", "./models.ts", false) + + expect(builder.toString()).toBe("import {Cat} from '../models.ts'") + }) + + it("child directory keeps extension", () => { + const builder = new ImportBuilder({ + unit: { + filename: "./example", + }, + includeFileExtensions: true, + }) + + builder.addSingle("Cat", "./foo/models.ts", false) + + expect(builder.toString()).toBe("import {Cat} from './foo/models.ts'") + }) + + it("sibling directory keeps extension", () => { + const builder = new ImportBuilder({ + unit: { + filename: "./foo/example", + }, + includeFileExtensions: true, + }) + + builder.addSingle("Cat", "./bar/models.ts", false) + + expect(builder.toString()).toBe("import {Cat} from '../bar/models.ts'") + }) + }) + + it("supports types with extensions", () => { + const builder = new ImportBuilder({includeFileExtensions: true}) + builder.addSingle("Cat", "./models.ts", false) + builder.addSingle("Dog", "./models.ts", true) + expect(builder.toString()).toBe( + "import {Cat, type Dog} from './models.ts'", + ) + }) + + it("usage-based pruning still works with extensions", () => { + const builder = new ImportBuilder({includeFileExtensions: true}) + builder.addModule("Lodash", "lodash") + builder.addSingle("Cat", "./models.ts", false) + builder.addSingle("Dog", "./models.ts", true) + builder.addSingle("Unused", "./models.ts", false) + + const code = [ + "function demo() {", + " const x: Dog = {} as any;", + " console.log(Lodash, Cat)", + "}", + ].join("\n") + + expect(builder.toString(code)).toBe( + [ + "import Lodash from 'lodash'", + "import {Cat, type Dog} from './models.ts'", + ].join("\n"), + ) + }) + }) }) diff --git a/packages/openapi-code-generator/src/typescript/common/import-builder.ts b/packages/openapi-code-generator/src/typescript/common/import-builder.ts index ac67eaa42..32a212d84 100644 --- a/packages/openapi-code-generator/src/typescript/common/import-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/import-builder.ts @@ -82,6 +82,11 @@ export function categorizeImportSource(source: string): ImportCategory { return ImportCategory.PATH } +export type ImportBuilderConfig = { + unit?: {filename: string} | undefined + includeFileExtensions: boolean +} + export class ImportBuilder { private readonly imports: Record< string, @@ -89,7 +94,7 @@ export class ImportBuilder { > = {} private readonly importAll: Record = {} - constructor(private readonly unit?: {filename: string}) {} + constructor(private readonly config: ImportBuilderConfig) {} from(from: string) { const chain = { @@ -142,7 +147,14 @@ export class ImportBuilder { unit: {filename: string} | undefined, ...builders: ImportBuilder[] ): ImportBuilder { - const result = new ImportBuilder(unit) + const config = builders[0]?.config + + if (!config) { + // todo: validate all config options are the same? + throw new Error("cannot merge imports without config") + } + + const result = new ImportBuilder({...config, unit}) for (const builder of builders) { for (const [key, {values, types}] of Object.entries(builder.imports)) { @@ -213,13 +225,13 @@ export class ImportBuilder { } private normalizeFrom(from: string) { - if (from.endsWith(".ts")) { + if (!this.config.includeFileExtensions && from.endsWith(".ts")) { // biome-ignore lint/style/noParameterAssign: normalization from = from.substring(0, from.length - ".ts".length) } - if (this.unit && from.startsWith("./")) { - const unitDirname = path.dirname(this.unit.filename) + if (this.config.unit && from.startsWith("./")) { + const unitDirname = path.dirname(this.config.unit.filename) const fromDirname = path.dirname(from) const relative = path.relative(unitDirname, fromDirname) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts index 4fdd65177..60737f594 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts @@ -18,7 +18,7 @@ import type { import {getSchemaNameFromRef, isRef} from "../../../core/openapi-utils" import {hasSingleElement} from "../../../core/utils" import {CompilationUnit, type ICompilable} from "../compilation-units" -import {ImportBuilder} from "../import-builder" +import type {ImportBuilder} from "../import-builder" import type {TypeBuilder} from "../type-builder" import {buildExport, type ExportDefinition} from "../typescript-common" import type {SchemaBuilderType} from "./schema-builder" @@ -37,12 +37,12 @@ export abstract class AbstractSchemaBuilder< private readonly graph: DependencyGraph protected readonly typeBuilder: TypeBuilder - protected readonly schemaBuilderImports = new ImportBuilder() protected constructor( public readonly filename: string, protected readonly input: Input, protected readonly config: SchemaBuilderConfig, + protected readonly schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, private readonly availableStaticSchemas: StaticSchemas, private readonly referenced: Record = {}, diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts index f2ad31896..e715eeb63 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts @@ -34,12 +34,14 @@ export class JoiBuilder extends AbstractSchemaBuilder< filename: string, input: Input, schemaBuilderConfig: SchemaBuilderConfig, + schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new JoiBuilder( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, staticSchemas, ) @@ -50,6 +52,7 @@ export class JoiBuilder extends AbstractSchemaBuilder< this.filename, this.input, this.config, + this.schemaBuilderImports, this.typeBuilder, staticSchemas, {}, diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts index db3943bb4..1801e1645 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts @@ -46,7 +46,7 @@ export function schemaBuilderTestHarness( ) { const formatter = await TypescriptFormatterBiome.createNodeFormatter() - const imports = new ImportBuilder() + const imports = new ImportBuilder({includeFileExtensions: false}) const typeBuilder = await TypeBuilder.fromInput( "./unit-test.types.ts", @@ -60,6 +60,7 @@ export function schemaBuilderTestHarness( input, schemaBuilderType, config, + new ImportBuilder({includeFileExtensions: false}), typeBuilder, ) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts index 83b139d3a..ea936b7bc 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts @@ -1,4 +1,5 @@ import type {Input} from "../../../core/input" +import type {ImportBuilder} from "../import-builder" import type {TypeBuilder} from "../type-builder" import type {SchemaBuilderConfig} from "./abstract-schema-builder" import {JoiBuilder} from "./joi-schema-builder" @@ -13,6 +14,7 @@ export function schemaBuilderFactory( input: Input, schemaBuilderType: SchemaBuilderType, schemaBuilderConfig: SchemaBuilderConfig, + schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { switch (schemaBuilderType) { @@ -21,6 +23,7 @@ export function schemaBuilderFactory( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, ) } @@ -30,6 +33,7 @@ export function schemaBuilderFactory( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, ) } @@ -39,6 +43,7 @@ export function schemaBuilderFactory( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, ) } diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.spec.ts index bb1543c5c..f46e6c846 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.spec.ts @@ -825,7 +825,8 @@ describe.each(testVersions)( } function inlineStaticSchemas(code: string) { - const importRegex = /import { ([^}]+) } from "\.\/unit-test\.schemas"\n/ + const importRegex = + /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ const match = code.match(importRegex)?.[1] diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts index 3ffe686f6..bae2b203e 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts @@ -49,12 +49,14 @@ export class ZodV3Builder extends AbstractSchemaBuilder< filename: string, input: Input, schemaBuilderConfig: SchemaBuilderConfig, + schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new ZodV3Builder( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, staticSchemas, ) @@ -65,6 +67,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder< this.filename, this.input, this.config, + this.schemaBuilderImports, this.typeBuilder, staticSchemas, {}, @@ -223,7 +226,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder< if (model["x-enum-extensibility"] === "open") { this.schemaBuilderImports.addSingle( "UnknownEnumNumberValue", - "./models", + this.typeBuilder.filename, true, ) return [ @@ -275,7 +278,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder< if (model["x-enum-extensibility"] === "open") { this.schemaBuilderImports.addSingle( "UnknownEnumStringValue", - "./models", + this.typeBuilder.filename, true, ) return this.union([ diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.spec.ts index 571fec227..414971189 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.spec.ts @@ -835,7 +835,8 @@ describe.each(testVersions)( } function inlineStaticSchemas(code: string) { - const importRegex = /import { ([^}]+) } from "\.\/unit-test\.schemas"\n/ + const importRegex = + /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ const match = code.match(importRegex)?.[1] diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts index ea866d763..ae0f9d548 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts @@ -50,12 +50,14 @@ export class ZodV4Builder extends AbstractSchemaBuilder< filename: string, input: Input, schemaBuilderConfig: SchemaBuilderConfig, + schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new ZodV4Builder( filename, input, schemaBuilderConfig, + schemaBuilderImports, typeBuilder, staticSchemas, ) @@ -66,6 +68,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder< this.filename, this.input, this.config, + this.schemaBuilderImports, this.typeBuilder, staticSchemas, {}, @@ -226,7 +229,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder< if (model["x-enum-extensibility"] === "open") { this.schemaBuilderImports.addSingle( "UnknownEnumNumberValue", - "./models", + this.typeBuilder.filename, true, ) return [ @@ -278,7 +281,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder< if (model["x-enum-extensibility"] === "open") { this.schemaBuilderImports.addSingle( "UnknownEnumStringValue", - "./models", + this.typeBuilder.filename, true, ) return this.union([ diff --git a/packages/openapi-code-generator/src/typescript/common/type-builder.spec.ts b/packages/openapi-code-generator/src/typescript/common/type-builder.spec.ts index b21c5be67..958ed3904 100644 --- a/packages/openapi-code-generator/src/typescript/common/type-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/type-builder.spec.ts @@ -441,7 +441,7 @@ describe.each(testVersions)( const {input, file} = await unitTestInput(version) const schema = {$ref: `${file}#/${path}`} - const imports = new ImportBuilder() + const imports = new ImportBuilder({includeFileExtensions: false}) const builder = await TypeBuilder.fromInput( "./unit-test.types.ts", diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-server-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-server-builder.ts index e2cb3aee6..8fb8de023 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-server-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-server-builder.ts @@ -1,6 +1,6 @@ import type {Input} from "../../../core/input" import {CompilationUnit, type ICompilable} from "../../common/compilation-units" -import {ImportBuilder} from "../../common/import-builder" +import type {ImportBuilder} from "../../common/import-builder" export class ExpressServerBuilder implements ICompilable { constructor( @@ -8,7 +8,7 @@ export class ExpressServerBuilder implements ICompilable { private readonly name: string, // biome-ignore lint/correctness/noUnusedPrivateClassMembers: future private readonly input: Input, - private readonly imports: ImportBuilder = new ImportBuilder(), + private readonly imports: ImportBuilder, ) { this.imports .from("@nahkies/typescript-express-runtime/server") diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express.generator.ts index 3f24c2f4e..90ad56988 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express.generator.ts @@ -16,6 +16,10 @@ export async function generateTypescriptExpress( const routesDirectory = config.groupingStrategy === "none" ? "./" : "./routes/" + const importBuilderConfig = {includeFileExtensions: config.isEsmProject} + + const schemaBuilderImports = new ImportBuilder(importBuilderConfig) + const rootTypeBuilder = await TypeBuilder.fromInput( "./models.ts", input, @@ -28,6 +32,7 @@ export async function generateTypescriptExpress( input, config.schemaBuilder, {allowAny}, + schemaBuilderImports, rootTypeBuilder, ) @@ -35,7 +40,7 @@ export async function generateTypescriptExpress( "index.ts", input.name(), input, - new ImportBuilder(), + new ImportBuilder(importBuilderConfig), ) const routers = await Promise.all( @@ -44,7 +49,12 @@ export async function generateTypescriptExpress( `${path.join(routesDirectory, group.name)}.ts`, config.filenameConvention, ) - const imports = new ImportBuilder({filename}) + const imports = new ImportBuilder({ + ...importBuilderConfig, + unit: { + filename, + }, + }) // Create router with imports and types const routerBuilder = new ExpressRouterBuilder( diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.spec.ts b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.spec.ts index e7278a896..d1a88b02f 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.spec.ts @@ -52,7 +52,11 @@ describe("typescript/server/typescript-koa/koa-router-builder", () => { const formatter = await TypescriptFormatterBiome.createNodeFormatter() - const imports = new ImportBuilder() + const imports = new ImportBuilder({includeFileExtensions: false}) + const schemaBuilderImports = new ImportBuilder({ + includeFileExtensions: false, + }) + const typeBuilder = await TypeBuilder.fromInput( "./unit-test.types.ts", input, @@ -64,6 +68,7 @@ describe("typescript/server/typescript-koa/koa-router-builder", () => { input, "zod-v4", {allowAny: true}, + schemaBuilderImports, typeBuilder, ) const serverRouterBuilder = new KoaRouterBuilder( diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-server-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-server-builder.ts index d1447dbe4..63ae80f9d 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-server-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-server-builder.ts @@ -1,6 +1,6 @@ import type {Input} from "../../../core/input" import {CompilationUnit, type ICompilable} from "../../common/compilation-units" -import {ImportBuilder} from "../../common/import-builder" +import type {ImportBuilder} from "../../common/import-builder" export class KoaServerBuilder implements ICompilable { constructor( @@ -8,7 +8,7 @@ export class KoaServerBuilder implements ICompilable { private readonly name: string, // biome-ignore lint/correctness/noUnusedPrivateClassMembers: future private readonly input: Input, - private readonly imports: ImportBuilder = new ImportBuilder(), + private readonly imports: ImportBuilder, ) { this.imports .from("@nahkies/typescript-koa-runtime/server") diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa.generator.ts index 0611c750a..043f75bc2 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa.generator.ts @@ -16,6 +16,10 @@ export async function generateTypescriptKoa( const routesDirectory = config.groupingStrategy === "none" ? "./" : "./routes/" + const importBuilderConfig = {includeFileExtensions: config.isEsmProject} + + const schemaBuilderImports = new ImportBuilder(importBuilderConfig) + const rootTypeBuilder = await TypeBuilder.fromInput( "./models.ts", input, @@ -28,6 +32,7 @@ export async function generateTypescriptKoa( input, config.schemaBuilder, {allowAny}, + schemaBuilderImports, rootTypeBuilder, ) @@ -35,7 +40,7 @@ export async function generateTypescriptKoa( "index.ts", input.name(), input, - new ImportBuilder(), + new ImportBuilder(importBuilderConfig), ) const routers = await Promise.all( @@ -44,7 +49,12 @@ export async function generateTypescriptKoa( `${path.join(routesDirectory, group.name)}.ts`, config.filenameConvention, ) - const imports = new ImportBuilder({filename}) + const imports = new ImportBuilder({ + ...importBuilderConfig, + unit: { + filename, + }, + }) const routerBuilder = new KoaRouterBuilder( filename,