From 25dcf06ee7e80b62c4f5a7ea3db832a8577b051a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 15:21:16 +0100 Subject: [PATCH 01/18] Require typescript in IntegrationBase. --- express-zod-api/src/integration-base.ts | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index c5ae09c388..1bd771c03d 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -1,5 +1,6 @@ +import { createRequire } from "node:module"; import * as R from "ramda"; -import ts from "typescript"; +import type ts from "typescript"; import { ResponseVariant } from "./api-response"; import { contentTypes } from "./content-type"; import { ClientMethod, clientMethods } from "./method"; @@ -55,6 +56,7 @@ export abstract class IntegrationBase { string, { store: Store; isDeprecated: boolean } >(); + protected ts: typeof ts; readonly #ids = { pathType: f.createIdentifier("Path"), @@ -119,7 +121,9 @@ export abstract class IntegrationBase { { expose: true }, ); - protected constructor(private readonly serverUrl: string) {} + protected constructor(private readonly serverUrl: string) { + this.ts = createRequire(import.meta.url)("typescript"); // @todo replace with a dynamic import in next major + } /** * @example SomeOf<_> @@ -178,15 +182,15 @@ export abstract class IntegrationBase { makeFnType( { [this.#ids.methodParameter.text]: this.methodType.name, - [this.#ids.pathParameter.text]: ts.SyntaxKind.StringKeyword, + [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, [this.#ids.paramsArgument.text]: recordStringAny, [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, }, - makePromise(ts.SyntaxKind.AnyKeyword), + makePromise(this.ts.SyntaxKind.AnyKeyword), ), { expose: true, - params: { T: { init: ts.SyntaxKind.UnknownKeyword } }, + params: { T: { init: this.ts.SyntaxKind.UnknownKeyword } }, }, ); @@ -198,7 +202,7 @@ export abstract class IntegrationBase { makeConst( this.#ids.parseRequestFn, makeArrowFn( - { [this.#ids.requestParameter.text]: ts.SyntaxKind.StringKeyword }, + { [this.#ids.requestParameter.text]: this.ts.SyntaxKind.StringKeyword }, f.createAsExpression( makeCall(this.#ids.requestParameter, propOf("split"))( f.createRegularExpressionLiteral("/ (.+)/"), // split once @@ -221,7 +225,7 @@ export abstract class IntegrationBase { this.#ids.substituteFn, makeArrowFn( { - [this.#ids.pathParameter.text]: ts.SyntaxKind.StringKeyword, + [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, [this.#ids.paramsArgument.text]: recordStringAny, }, f.createBlock([ @@ -234,7 +238,7 @@ export abstract class IntegrationBase { f.createForInStatement( f.createVariableDeclarationList( [f.createVariableDeclaration(this.#ids.keyParameter)], - ts.NodeFlags.Const, + this.ts.NodeFlags.Const, ), this.#ids.paramsArgument, f.createBlock([ @@ -448,7 +452,7 @@ export abstract class IntegrationBase { // if (!contentType) return; const noBodyStatement = f.createIfStatement( f.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, + this.ts.SyntaxKind.ExclamationToken, this.#ids.contentTypeConst, ), f.createReturnStatement(), @@ -546,7 +550,7 @@ export abstract class IntegrationBase { makeLiteralType(propOf("data")), ), }, - makeMaybeAsync(ts.SyntaxKind.VoidKeyword), + makeMaybeAsync(this.ts.SyntaxKind.VoidKeyword), ), }), [ @@ -605,14 +609,16 @@ export abstract class IntegrationBase { this.requestType.name, f.createTemplateLiteralType(f.createTemplateHead("get "), [ f.createTemplateLiteralTypeSpan( - ensureTypeNode(ts.SyntaxKind.StringKeyword), + ensureTypeNode(this.ts.SyntaxKind.StringKeyword), f.createTemplateTail(""), ), ]), ), R: makeExtract( makeIndexed(this.interfaces.positive, "K"), - makeOneLine(this.#makeEventNarrow(ts.SyntaxKind.StringKeyword)), + makeOneLine( + this.#makeEventNarrow(this.ts.SyntaxKind.StringKeyword), + ), ), }, }, From ab3b7e1e6e4ea6e0cabcf595a205bd2adc548b11 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 15:26:08 +0100 Subject: [PATCH 02/18] Adjusting the Integration class. --- express-zod-api/src/integration.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index bdadcdec29..b428c2b931 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -1,5 +1,5 @@ import * as R from "ramda"; -import ts from "typescript"; +import type ts from "typescript"; import { z } from "zod"; import { ResponseVariant, responseVariants } from "./api-response"; import { IntegrationBase } from "./integration-base"; @@ -201,13 +201,13 @@ export class Integration extends IntegrationBase { const usageExampleText = this.#printUsage(printerOptions); const commentNode = usageExampleText && - ts.addSyntheticLeadingComment( - ts.addSyntheticLeadingComment( + this.ts.addSyntheticLeadingComment( + this.ts.addSyntheticLeadingComment( f.createEmptyStatement(), - ts.SyntaxKind.SingleLineCommentTrivia, + this.ts.SyntaxKind.SingleLineCommentTrivia, " Usage example:", ), - ts.SyntaxKind.MultiLineCommentTrivia, + this.ts.SyntaxKind.MultiLineCommentTrivia, `\n${usageExampleText}`, ); return this.#program From 68b681a47a01f594a39d2fbbaf91e26a57d66a1e Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 16:03:12 +0100 Subject: [PATCH 03/18] Calling createIdentifier in methods of IntegrationBase. --- express-zod-api/src/integration-base.ts | 188 +++++++++++++----------- 1 file changed, 106 insertions(+), 82 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index 1bd771c03d..294aa203c3 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -59,34 +59,34 @@ export abstract class IntegrationBase { protected ts: typeof ts; readonly #ids = { - pathType: f.createIdentifier("Path"), - implementationType: f.createIdentifier("Implementation"), - keyParameter: f.createIdentifier("key"), - pathParameter: f.createIdentifier("path"), - paramsArgument: f.createIdentifier("params"), - ctxArgument: f.createIdentifier("ctx"), - methodParameter: f.createIdentifier("method"), - requestParameter: f.createIdentifier("request"), - eventParameter: f.createIdentifier("event"), - dataParameter: f.createIdentifier("data"), - handlerParameter: f.createIdentifier("handler"), - msgParameter: f.createIdentifier("msg"), - parseRequestFn: f.createIdentifier("parseRequest"), - substituteFn: f.createIdentifier("substitute"), - provideMethod: f.createIdentifier("provide"), - onMethod: f.createIdentifier("on"), - implementationArgument: f.createIdentifier("implementation"), - hasBodyConst: f.createIdentifier("hasBody"), - undefinedValue: f.createIdentifier("undefined"), - responseConst: f.createIdentifier("response"), - restConst: f.createIdentifier("rest"), - searchParamsConst: f.createIdentifier("searchParams"), - defaultImplementationConst: f.createIdentifier("defaultImplementation"), - clientConst: f.createIdentifier("client"), - contentTypeConst: f.createIdentifier("contentType"), - isJsonConst: f.createIdentifier("isJSON"), - sourceProp: f.createIdentifier("source"), - } satisfies Record; + pathType: "Path", + implementationType: "Implementation", + keyParameter: "key", + pathParameter: "path", + paramsArgument: "params", + ctxArgument: "ctx", + methodParameter: "method", + requestParameter: "request", + eventParameter: "event", + dataParameter: "data", + handlerParameter: "handler", + msgParameter: "msg", + parseRequestFn: "parseRequest", + substituteFn: "substitute", + provideMethod: "provide", + onMethod: "on", + implementationArgument: "implementation", + hasBodyConst: "hasBody", + undefinedValue: "undefined", + responseConst: "response", + restConst: "rest", + searchParamsConst: "searchParams", + defaultImplementationConst: "defaultImplementation", + clientConst: "client", + contentTypeConst: "contentType", + isJsonConst: "isJSON", + sourceProp: "source", + } satisfies Record; /** @internal */ protected interfaces: Record = { @@ -181,10 +181,10 @@ export abstract class IntegrationBase { this.#ids.implementationType, makeFnType( { - [this.#ids.methodParameter.text]: this.methodType.name, - [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument.text]: recordStringAny, - [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, + [this.#ids.methodParameter]: this.methodType.name, + [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument]: recordStringAny, + [this.#ids.ctxArgument]: { optional: true, type: "T" }, }, makePromise(this.ts.SyntaxKind.AnyKeyword), ), @@ -202,7 +202,7 @@ export abstract class IntegrationBase { makeConst( this.#ids.parseRequestFn, makeArrowFn( - { [this.#ids.requestParameter.text]: this.ts.SyntaxKind.StringKeyword }, + { [this.#ids.requestParameter]: this.ts.SyntaxKind.StringKeyword }, f.createAsExpression( makeCall(this.#ids.requestParameter, propOf("split"))( f.createRegularExpressionLiteral("/ (.+)/"), // split once @@ -225,14 +225,16 @@ export abstract class IntegrationBase { this.#ids.substituteFn, makeArrowFn( { - [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument.text]: recordStringAny, + [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument]: recordStringAny, }, f.createBlock([ makeConst( this.#ids.restConst, f.createObjectLiteralExpression([ - f.createSpreadAssignment(this.#ids.paramsArgument), + f.createSpreadAssignment( + this.ts.factory.createIdentifier(this.#ids.paramsArgument), + ), ]), ), f.createForInStatement( @@ -240,27 +242,37 @@ export abstract class IntegrationBase { [f.createVariableDeclaration(this.#ids.keyParameter)], this.ts.NodeFlags.Const, ), - this.#ids.paramsArgument, + this.ts.factory.createIdentifier(this.#ids.paramsArgument), f.createBlock([ makeAssignment( - this.#ids.pathParameter, + this.ts.factory.createIdentifier(this.#ids.pathParameter), makeCall(this.#ids.pathParameter, propOf("replace"))( - makeTemplate(":", [this.#ids.keyParameter]), // `:${key}` + makeTemplate(":", [ + this.ts.factory.createIdentifier(this.#ids.keyParameter), + ]), // `:${key}` makeArrowFn( [], f.createBlock([ f.createExpressionStatement( f.createDeleteExpression( f.createElementAccessExpression( - this.#ids.restConst, - this.#ids.keyParameter, + this.ts.factory.createIdentifier( + this.#ids.restConst, + ), + this.ts.factory.createIdentifier( + this.#ids.keyParameter, + ), ), ), ), f.createReturnStatement( f.createElementAccessExpression( - this.#ids.paramsArgument, - this.#ids.keyParameter, + this.ts.factory.createIdentifier( + this.#ids.paramsArgument, + ), + this.ts.factory.createIdentifier( + this.#ids.keyParameter, + ), ), ), ]), @@ -272,8 +284,8 @@ export abstract class IntegrationBase { f.createReturnStatement( f.createAsExpression( f.createArrayLiteralExpression([ - this.#ids.pathParameter, - this.#ids.restConst, + this.ts.factory.createIdentifier(this.#ids.pathParameter), + this.ts.factory.createIdentifier(this.#ids.restConst), ]), ensureTypeNode("const"), ), @@ -285,35 +297,34 @@ export abstract class IntegrationBase { // public provide(request: K, params: Input[K]): Promise {} #makeProvider = () => makePublicMethod( - this.#ids.provideMethod, + this.ts.factory.createIdentifier(this.#ids.provideMethod), makeParams({ - [this.#ids.requestParameter.text]: "K", - [this.#ids.paramsArgument.text]: makeIndexed( - this.interfaces.input, - "K", - ), - [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, + [this.#ids.requestParameter]: "K", + [this.#ids.paramsArgument]: makeIndexed(this.interfaces.input, "K"), + [this.#ids.ctxArgument]: { optional: true, type: "T" }, }), [ makeConst( // const [method, path] = this.parseRequest(request); makeDeconstruction( - this.#ids.methodParameter, - this.#ids.pathParameter, + this.ts.factory.createIdentifier(this.#ids.methodParameter), + this.ts.factory.createIdentifier(this.#ids.pathParameter), + ), + makeCall(this.#ids.parseRequestFn)( + this.ts.factory.createIdentifier(this.#ids.requestParameter), ), - makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter), ), // return this.implementation(___) f.createReturnStatement( makeCall(f.createThis(), this.#ids.implementationArgument)( - this.#ids.methodParameter, + this.ts.factory.createIdentifier(this.#ids.methodParameter), f.createSpreadElement( makeCall(this.#ids.substituteFn)( - this.#ids.pathParameter, - this.#ids.paramsArgument, + this.ts.factory.createIdentifier(this.#ids.pathParameter), + this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), ), - this.#ids.ctxArgument, + this.ts.factory.createIdentifier(this.#ids.ctxArgument), ), ), ], @@ -336,7 +347,9 @@ export abstract class IntegrationBase { makeParam(this.#ids.implementationArgument, { type: ensureTypeNode(this.#ids.implementationType, ["T"]), mod: accessModifiers.protectedReadonly, - init: this.#ids.defaultImplementationConst, + init: this.ts.factory.createIdentifier( + this.#ids.defaultImplementationConst, + ), }), ]), this.#makeProvider(), @@ -354,8 +367,8 @@ export abstract class IntegrationBase { URL.name, makeTemplate( "", - [this.#ids.pathParameter], - [this.#ids.searchParamsConst], + [this.ts.factory.createIdentifier(this.#ids.pathParameter)], + [this.ts.factory.createIdentifier(this.#ids.searchParamsConst)], ), literally(this.serverUrl), ); @@ -375,14 +388,14 @@ export abstract class IntegrationBase { const headersProperty = f.createPropertyAssignment( propOf("headers"), makeTernary( - this.#ids.hasBodyConst, + this.ts.factory.createIdentifier(this.#ids.hasBodyConst), f.createObjectLiteralExpression([ f.createPropertyAssignment( literally("Content-Type"), literally(contentTypes.json), ), ]), - this.#ids.undefinedValue, + this.ts.factory.createIdentifier(this.#ids.undefinedValue), ), ); @@ -390,12 +403,12 @@ export abstract class IntegrationBase { const bodyProperty = f.createPropertyAssignment( propOf("body"), makeTernary( - this.#ids.hasBodyConst, + this.ts.factory.createIdentifier(this.#ids.hasBodyConst), makeCall( JSON[Symbol.toStringTag], propOf("stringify"), - )(this.#ids.paramsArgument), - this.#ids.undefinedValue, + )(this.ts.factory.createIdentifier(this.#ids.paramsArgument)), + this.ts.factory.createIdentifier(this.#ids.undefinedValue), ), ); @@ -425,7 +438,7 @@ export abstract class IntegrationBase { literally("delete" satisfies ClientMethod), ]), propOf("includes"), - )(this.#ids.methodParameter), + )(this.ts.factory.createIdentifier(this.#ids.methodParameter)), ), ); @@ -433,9 +446,11 @@ export abstract class IntegrationBase { const searchParamsStatement = makeConst( this.#ids.searchParamsConst, makeTernary( - this.#ids.hasBodyConst, + this.ts.factory.createIdentifier(this.#ids.hasBodyConst), literally(""), - this.#makeSearchParams(this.#ids.paramsArgument), + this.#makeSearchParams( + this.ts.factory.createIdentifier(this.#ids.paramsArgument), + ), ), ); @@ -453,7 +468,7 @@ export abstract class IntegrationBase { const noBodyStatement = f.createIfStatement( f.createPrefixUnaryExpression( this.ts.SyntaxKind.ExclamationToken, - this.#ids.contentTypeConst, + this.ts.factory.createIdentifier(this.#ids.contentTypeConst), ), f.createReturnStatement(), ); @@ -472,7 +487,7 @@ export abstract class IntegrationBase { makeCall( this.#ids.responseConst, makeTernary( - this.#ids.isJsonConst, + this.ts.factory.createIdentifier(this.#ids.isJsonConst), literally(propOf("json")), literally(propOf("text")), ), @@ -510,18 +525,25 @@ export abstract class IntegrationBase { }), [ makeConst( - makeDeconstruction(this.#ids.pathParameter, this.#ids.restConst), + makeDeconstruction( + this.ts.factory.createIdentifier(this.#ids.pathParameter), + this.ts.factory.createIdentifier(this.#ids.restConst), + ), makeCall(this.#ids.substituteFn)( f.createElementAccessExpression( - makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter), + makeCall(this.#ids.parseRequestFn)( + this.ts.factory.createIdentifier(this.#ids.requestParameter), + ), literally(1), ), - this.#ids.paramsArgument, + this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), ), makeConst( this.#ids.searchParamsConst, - this.#makeSearchParams(this.#ids.restConst), + this.#makeSearchParams( + this.ts.factory.createIdentifier(this.#ids.restConst), + ), ), makeAssignment( f.createPropertyAccessExpression( @@ -540,12 +562,12 @@ export abstract class IntegrationBase { #makeOnMethod = () => makePublicMethod( - this.#ids.onMethod, + this.ts.factory.createIdentifier(this.#ids.onMethod), makeParams({ - [this.#ids.eventParameter.text]: "E", - [this.#ids.handlerParameter.text]: makeFnType( + [this.#ids.eventParameter]: "E", + [this.#ids.handlerParameter]: makeFnType( { - [this.#ids.dataParameter.text]: makeIndexed( + [this.#ids.dataParameter]: makeIndexed( makeExtract("R", makeOneLine(this.#makeEventNarrow("E"))), makeLiteralType(propOf("data")), ), @@ -560,7 +582,7 @@ export abstract class IntegrationBase { this.#ids.sourceProp, propOf("addEventListener"), )( - this.#ids.eventParameter, + this.ts.factory.createIdentifier(this.#ids.eventParameter), makeArrowFn( [this.#ids.msgParameter], makeCall(this.#ids.handlerParameter)( @@ -571,7 +593,9 @@ export abstract class IntegrationBase { f.createPropertyAccessExpression( f.createParenthesizedExpression( f.createAsExpression( - this.#ids.msgParameter, + this.ts.factory.createIdentifier( + this.#ids.msgParameter, + ), ensureTypeNode(MessageEvent.name), ), ), From a7beada3dc7dbd55450955e0d90c5362aefc52c4 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 16:06:47 +0100 Subject: [PATCH 04/18] Changeing IntegrationBase::interfaces to record of strings. --- express-zod-api/src/integration-base.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index 294aa203c3..b92e89de3a 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -89,12 +89,12 @@ export abstract class IntegrationBase { } satisfies Record; /** @internal */ - protected interfaces: Record = { - input: f.createIdentifier("Input"), - positive: f.createIdentifier("PositiveResponse"), - negative: f.createIdentifier("NegativeResponse"), - encoded: f.createIdentifier("EncodedResponse"), - response: f.createIdentifier("Response"), + protected interfaces: Record = { + input: "Input", + positive: "PositiveResponse", + negative: "NegativeResponse", + encoded: "EncodedResponse", + response: "Response", }; /** From e37a35414553441a10051b5d3a292b51b633757f Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 16:10:45 +0100 Subject: [PATCH 05/18] rm import factory to IntegrationBase. --- express-zod-api/src/integration-base.ts | 124 ++++++++++++------------ 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index b92e89de3a..d147125e07 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -8,7 +8,6 @@ import type { makeEventSchema } from "./sse"; import { accessModifiers, ensureTypeNode, - f, makeArrowFn, makeConst, makeDeconstruction, @@ -57,6 +56,7 @@ export abstract class IntegrationBase { { store: Store; isDeprecated: boolean } >(); protected ts: typeof ts; + protected f: typeof ts.factory; readonly #ids = { pathType: "Path", @@ -123,6 +123,7 @@ export abstract class IntegrationBase { protected constructor(private readonly serverUrl: string) { this.ts = createRequire(import.meta.url)("typescript"); // @todo replace with a dynamic import in next major + this.f = this.ts.factory; } /** @@ -161,11 +162,11 @@ export abstract class IntegrationBase { protected makeEndpointTags = () => makeConst( "endpointTags", - f.createObjectLiteralExpression( + this.f.createObjectLiteralExpression( Array.from(this.tags).map(([request, tags]) => - f.createPropertyAssignment( + this.f.createPropertyAssignment( makePropertyIdentifier(request), - f.createArrayLiteralExpression(R.map(literally, tags)), + this.f.createArrayLiteralExpression(R.map(literally, tags)), ), ), ), @@ -203,12 +204,12 @@ export abstract class IntegrationBase { this.#ids.parseRequestFn, makeArrowFn( { [this.#ids.requestParameter]: this.ts.SyntaxKind.StringKeyword }, - f.createAsExpression( + this.f.createAsExpression( makeCall(this.#ids.requestParameter, propOf("split"))( - f.createRegularExpressionLiteral("/ (.+)/"), // split once + this.f.createRegularExpressionLiteral("/ (.+)/"), // split once literally(2), // excludes third empty element ), - f.createTupleTypeNode([ + this.f.createTupleTypeNode([ ensureTypeNode(this.methodType.name), ensureTypeNode(this.#ids.pathType), ]), @@ -228,22 +229,22 @@ export abstract class IntegrationBase { [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, [this.#ids.paramsArgument]: recordStringAny, }, - f.createBlock([ + this.f.createBlock([ makeConst( this.#ids.restConst, - f.createObjectLiteralExpression([ - f.createSpreadAssignment( + this.f.createObjectLiteralExpression([ + this.f.createSpreadAssignment( this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), ]), ), - f.createForInStatement( - f.createVariableDeclarationList( - [f.createVariableDeclaration(this.#ids.keyParameter)], + this.f.createForInStatement( + this.f.createVariableDeclarationList( + [this.f.createVariableDeclaration(this.#ids.keyParameter)], this.ts.NodeFlags.Const, ), this.ts.factory.createIdentifier(this.#ids.paramsArgument), - f.createBlock([ + this.f.createBlock([ makeAssignment( this.ts.factory.createIdentifier(this.#ids.pathParameter), makeCall(this.#ids.pathParameter, propOf("replace"))( @@ -252,10 +253,10 @@ export abstract class IntegrationBase { ]), // `:${key}` makeArrowFn( [], - f.createBlock([ - f.createExpressionStatement( - f.createDeleteExpression( - f.createElementAccessExpression( + this.f.createBlock([ + this.f.createExpressionStatement( + this.f.createDeleteExpression( + this.f.createElementAccessExpression( this.ts.factory.createIdentifier( this.#ids.restConst, ), @@ -265,8 +266,8 @@ export abstract class IntegrationBase { ), ), ), - f.createReturnStatement( - f.createElementAccessExpression( + this.f.createReturnStatement( + this.f.createElementAccessExpression( this.ts.factory.createIdentifier( this.#ids.paramsArgument, ), @@ -281,9 +282,9 @@ export abstract class IntegrationBase { ), ]), ), - f.createReturnStatement( - f.createAsExpression( - f.createArrayLiteralExpression([ + this.f.createReturnStatement( + this.f.createAsExpression( + this.f.createArrayLiteralExpression([ this.ts.factory.createIdentifier(this.#ids.pathParameter), this.ts.factory.createIdentifier(this.#ids.restConst), ]), @@ -315,10 +316,10 @@ export abstract class IntegrationBase { ), ), // return this.implementation(___) - f.createReturnStatement( - makeCall(f.createThis(), this.#ids.implementationArgument)( + this.f.createReturnStatement( + makeCall(this.f.createThis(), this.#ids.implementationArgument)( this.ts.factory.createIdentifier(this.#ids.methodParameter), - f.createSpreadElement( + this.f.createSpreadElement( makeCall(this.#ids.substituteFn)( this.ts.factory.createIdentifier(this.#ids.pathParameter), this.ts.factory.createIdentifier(this.#ids.paramsArgument), @@ -379,18 +380,18 @@ export abstract class IntegrationBase { * */ protected makeDefaultImplementation = () => { // method: method.toUpperCase() - const methodProperty = f.createPropertyAssignment( + const methodProperty = this.f.createPropertyAssignment( propOf("method"), makeCall(this.#ids.methodParameter, propOf("toUpperCase"))(), ); // headers: hasBody ? { "Content-Type": "application/json" } : undefined - const headersProperty = f.createPropertyAssignment( + const headersProperty = this.f.createPropertyAssignment( propOf("headers"), makeTernary( this.ts.factory.createIdentifier(this.#ids.hasBodyConst), - f.createObjectLiteralExpression([ - f.createPropertyAssignment( + this.f.createObjectLiteralExpression([ + this.f.createPropertyAssignment( literally("Content-Type"), literally(contentTypes.json), ), @@ -400,7 +401,7 @@ export abstract class IntegrationBase { ); // body: hasBody ? JSON.stringify(params) : undefined - const bodyProperty = f.createPropertyAssignment( + const bodyProperty = this.f.createPropertyAssignment( propOf("body"), makeTernary( this.ts.factory.createIdentifier(this.#ids.hasBodyConst), @@ -415,10 +416,10 @@ export abstract class IntegrationBase { // const response = await fetch(new URL(`${path}${searchParams}`, "https://example.com"), { ___ }); const responseStatement = makeConst( this.#ids.responseConst, - f.createAwaitExpression( + this.f.createAwaitExpression( makeCall(fetch.name)( this.#makeFetchURL(), - f.createObjectLiteralExpression([ + this.f.createObjectLiteralExpression([ methodProperty, headersProperty, bodyProperty, @@ -430,9 +431,9 @@ export abstract class IntegrationBase { // const hasBody = !["get", "delete"].includes(method); const hasBodyStatement = makeConst( this.#ids.hasBodyConst, - f.createLogicalNot( + this.f.createLogicalNot( makeCall( - f.createArrayLiteralExpression([ + this.f.createArrayLiteralExpression([ literally("get" satisfies ClientMethod), literally("head" satisfies ClientMethod), literally("delete" satisfies ClientMethod), @@ -465,12 +466,12 @@ export abstract class IntegrationBase { ); // if (!contentType) return; - const noBodyStatement = f.createIfStatement( - f.createPrefixUnaryExpression( + const noBodyStatement = this.f.createIfStatement( + this.f.createPrefixUnaryExpression( this.ts.SyntaxKind.ExclamationToken, this.ts.factory.createIdentifier(this.#ids.contentTypeConst), ), - f.createReturnStatement(), + this.f.createReturnStatement(), ); // const isJSON = contentType.startsWith("application/json"); @@ -483,7 +484,7 @@ export abstract class IntegrationBase { ); // return response[isJSON ? "json" : "text"](); - const returnStatement = f.createReturnStatement( + const returnStatement = this.f.createReturnStatement( makeCall( this.#ids.responseConst, makeTernary( @@ -502,7 +503,7 @@ export abstract class IntegrationBase { this.#ids.pathParameter, this.#ids.paramsArgument, ], - f.createBlock([ + this.f.createBlock([ hasBodyStatement, searchParamsStatement, responseStatement, @@ -530,7 +531,7 @@ export abstract class IntegrationBase { this.ts.factory.createIdentifier(this.#ids.restConst), ), makeCall(this.#ids.substituteFn)( - f.createElementAccessExpression( + this.f.createElementAccessExpression( makeCall(this.#ids.parseRequestFn)( this.ts.factory.createIdentifier(this.#ids.requestParameter), ), @@ -546,8 +547,8 @@ export abstract class IntegrationBase { ), ), makeAssignment( - f.createPropertyAccessExpression( - f.createThis(), + this.f.createPropertyAccessExpression( + this.f.createThis(), this.#ids.sourceProp, ), makeNew("EventSource", this.#makeFetchURL()), @@ -556,7 +557,7 @@ export abstract class IntegrationBase { ); #makeEventNarrow = (value: Typeable) => - f.createTypeLiteralNode([ + this.f.createTypeLiteralNode([ makeInterfaceProp(propOf("event"), value), ]); @@ -576,9 +577,9 @@ export abstract class IntegrationBase { ), }), [ - f.createExpressionStatement( + this.f.createExpressionStatement( makeCall( - f.createThis(), + this.f.createThis(), this.#ids.sourceProp, propOf("addEventListener"), )( @@ -590,9 +591,9 @@ export abstract class IntegrationBase { JSON[Symbol.toStringTag], propOf("parse"), )( - f.createPropertyAccessExpression( - f.createParenthesizedExpression( - f.createAsExpression( + this.f.createPropertyAccessExpression( + this.f.createParenthesizedExpression( + this.f.createAsExpression( this.ts.factory.createIdentifier( this.#ids.msgParameter, ), @@ -606,7 +607,7 @@ export abstract class IntegrationBase { ), ), ), - f.createReturnStatement(f.createThis()), + this.f.createReturnStatement(this.f.createThis()), ], { typeParams: { @@ -631,12 +632,15 @@ export abstract class IntegrationBase { typeParams: { K: makeExtract( this.requestType.name, - f.createTemplateLiteralType(f.createTemplateHead("get "), [ - f.createTemplateLiteralTypeSpan( - ensureTypeNode(this.ts.SyntaxKind.StringKeyword), - f.createTemplateTail(""), - ), - ]), + this.f.createTemplateLiteralType( + this.f.createTemplateHead("get "), + [ + this.f.createTemplateLiteralTypeSpan( + ensureTypeNode(this.ts.SyntaxKind.StringKeyword), + this.f.createTemplateTail(""), + ), + ], + ), ), R: makeExtract( makeIndexed(this.interfaces.positive, "K"), @@ -657,8 +661,8 @@ export abstract class IntegrationBase { // client.provide("get /v1/user/retrieve", { id: "10" }); makeCall(this.#ids.clientConst, this.#ids.provideMethod)( literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`), - f.createObjectLiteralExpression([ - f.createPropertyAssignment("id", literally("10")), + this.f.createObjectLiteralExpression([ + this.f.createPropertyAssignment("id", literally("10")), ]), ), // new Subscription("get /v1/events/stream", {}).on("time", (time) => {}); @@ -666,9 +670,9 @@ export abstract class IntegrationBase { makeNew( subscriptionClassName, literally(`${"get" satisfies ClientMethod} /v1/events/stream`), - f.createObjectLiteralExpression(), + this.f.createObjectLiteralExpression(), ), this.#ids.onMethod, - )(literally("time"), makeArrowFn(["time"], f.createBlock([]))), + )(literally("time"), makeArrowFn(["time"], this.f.createBlock([]))), ]; } From 9721e9652b1adb8f9db75fd2d30fc1155c2ec529 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 17:27:32 +0100 Subject: [PATCH 06/18] Adjusting factory usage in Integration. --- express-zod-api/src/integration.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index b428c2b931..89da646400 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -4,7 +4,6 @@ import { z } from "zod"; import { ResponseVariant, responseVariants } from "./api-response"; import { IntegrationBase } from "./integration-base"; import { - f, makeInterfaceProp, makeInterface, makeType, @@ -147,7 +146,7 @@ export class Integration extends IntegrationBase { makeIndexed(this.interfaces.positive, literalIdx), makeIndexed(this.interfaces.negative, literalIdx), ]), - encoded: f.createIntersectionTypeNode([ + encoded: this.f.createIntersectionTypeNode([ ensureTypeNode(dictionaries.positive.name), ensureTypeNode(dictionaries.negative.name), ]), @@ -203,7 +202,7 @@ export class Integration extends IntegrationBase { usageExampleText && this.ts.addSyntheticLeadingComment( this.ts.addSyntheticLeadingComment( - f.createEmptyStatement(), + this.f.createEmptyStatement(), this.ts.SyntaxKind.SingleLineCommentTrivia, " Usage example:", ), From 61f9b1ae9aca0d561216697f83d689641a0349ed Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 20:42:23 +0100 Subject: [PATCH 07/18] REF: moving ts and f into new class TypescriptAPI. --- express-zod-api/src/integration-base.ts | 301 ++++----- express-zod-api/src/integration.ts | 50 +- express-zod-api/src/typescript-api.ts | 828 +++++++++++++----------- 3 files changed, 596 insertions(+), 583 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index d147125e07..f410457061 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -1,51 +1,16 @@ -import { createRequire } from "node:module"; import * as R from "ramda"; import type ts from "typescript"; import { ResponseVariant } from "./api-response"; import { contentTypes } from "./content-type"; import { ClientMethod, clientMethods } from "./method"; import type { makeEventSchema } from "./sse"; -import { - accessModifiers, - ensureTypeNode, - makeArrowFn, - makeConst, - makeDeconstruction, - makeExtract, - makeInterface, - makeInterfaceProp, - makeKeyOf, - makeNew, - makeOneLine, - makeParam, - makeParams, - makePromise, - makeCall, - makePropertyIdentifier, - makePublicConstructor, - makePublicClass, - makePublicLiteralType, - makePublicMethod, - makeTemplate, - makeTernary, - makeType, - propOf, - recordStringAny, - makeAssignment, - makePublicProperty, - makeIndexed, - makeMaybeAsync, - Typeable, - makeFnType, - makeLiteralType, - literally, -} from "./typescript-api"; +import { propOf, Typeable, TypescriptAPI } from "./typescript-api"; type IOKind = "input" | "response" | ResponseVariant | "encoded"; type SSEShape = ReturnType["shape"]; type Store = Record; -export abstract class IntegrationBase { +export abstract class IntegrationBase extends TypescriptAPI { /** @internal */ protected paths = new Set(); /** @internal */ @@ -55,8 +20,6 @@ export abstract class IntegrationBase { string, { store: Store; isDeprecated: boolean } >(); - protected ts: typeof ts; - protected f: typeof ts.factory; readonly #ids = { pathType: "Path", @@ -101,29 +64,30 @@ export abstract class IntegrationBase { * @example export type Method = "get" | "post" | "put" | "delete" | "patch" | "head"; * @internal * */ - protected methodType = makePublicLiteralType("Method", clientMethods); + protected methodType = this.makePublicLiteralType("Method", clientMethods); /** * @example type SomeOf = T[keyof T]; * @internal * */ - protected someOfType = makeType("SomeOf", makeIndexed("T", makeKeyOf("T")), { - params: ["T"], - }); + protected someOfType = this.makeType( + "SomeOf", + this.makeIndexed("T", this.makeKeyOf("T")), + { params: ["T"] }, + ); /** * @example export type Request = keyof Input; * @internal * */ - protected requestType = makeType( + protected requestType = this.makeType( "Request", - makeKeyOf(this.interfaces.input), + this.makeKeyOf(this.interfaces.input), { expose: true }, ); protected constructor(private readonly serverUrl: string) { - this.ts = createRequire(import.meta.url)("typescript"); // @todo replace with a dynamic import in next major - this.f = this.ts.factory; + super(); } /** @@ -131,14 +95,14 @@ export abstract class IntegrationBase { * @internal **/ protected someOf = ({ name }: ts.TypeAliasDeclaration) => - ensureTypeNode(this.someOfType.name, [name]); + this.ensureTypeNode(this.someOfType.name, [name]); /** * @example export type Path = "/v1/user/retrieve" | ___; * @internal * */ protected makePathType = () => - makePublicLiteralType(this.#ids.pathType, Array.from(this.paths)); + this.makePublicLiteralType(this.#ids.pathType, Array.from(this.paths)); /** * @example export interface Input { "get /v1/user/retrieve": GetV1UserRetrieveInput; } @@ -146,10 +110,10 @@ export abstract class IntegrationBase { * */ protected makePublicInterfaces = () => (Object.keys(this.interfaces) as IOKind[]).map((kind) => - makeInterface( + this.makeInterface( this.interfaces[kind], Array.from(this.registry).map(([request, { store, isDeprecated }]) => - makeInterfaceProp(request, store[kind], { isDeprecated }), + this.makeInterfaceProp(request, store[kind], { isDeprecated }), ), { expose: true }, ), @@ -160,13 +124,15 @@ export abstract class IntegrationBase { * @internal * */ protected makeEndpointTags = () => - makeConst( + this.makeConst( "endpointTags", this.f.createObjectLiteralExpression( Array.from(this.tags).map(([request, tags]) => this.f.createPropertyAssignment( - makePropertyIdentifier(request), - this.f.createArrayLiteralExpression(R.map(literally, tags)), + this.makePropertyIdentifier(request), + this.f.createArrayLiteralExpression( + R.map(this.literally.bind(this), tags), + ), ), ), ), @@ -178,16 +144,16 @@ export abstract class IntegrationBase { * @internal * */ protected makeImplementationType = () => - makeType( + this.makeType( this.#ids.implementationType, - makeFnType( + this.makeFnType( { [this.#ids.methodParameter]: this.methodType.name, [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument]: recordStringAny, + [this.#ids.paramsArgument]: this.makeRecordStringAny(), [this.#ids.ctxArgument]: { optional: true, type: "T" }, }, - makePromise(this.ts.SyntaxKind.AnyKeyword), + this.makePromise(this.ts.SyntaxKind.AnyKeyword), ), { expose: true, @@ -200,18 +166,18 @@ export abstract class IntegrationBase { * @internal * */ protected makeParseRequestFn = () => - makeConst( + this.makeConst( this.#ids.parseRequestFn, - makeArrowFn( + this.makeArrowFn( { [this.#ids.requestParameter]: this.ts.SyntaxKind.StringKeyword }, this.f.createAsExpression( - makeCall(this.#ids.requestParameter, propOf("split"))( + this.makeCall(this.#ids.requestParameter, propOf("split"))( this.f.createRegularExpressionLiteral("/ (.+)/"), // split once - literally(2), // excludes third empty element + this.literally(2), // excludes third empty element ), this.f.createTupleTypeNode([ - ensureTypeNode(this.methodType.name), - ensureTypeNode(this.#ids.pathType), + this.ensureTypeNode(this.methodType.name), + this.ensureTypeNode(this.#ids.pathType), ]), ), ), @@ -222,15 +188,15 @@ export abstract class IntegrationBase { * @internal * */ protected makeSubstituteFn = () => - makeConst( + this.makeConst( this.#ids.substituteFn, - makeArrowFn( + this.makeArrowFn( { [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument]: recordStringAny, + [this.#ids.paramsArgument]: this.makeRecordStringAny(), }, this.f.createBlock([ - makeConst( + this.makeConst( this.#ids.restConst, this.f.createObjectLiteralExpression([ this.f.createSpreadAssignment( @@ -245,13 +211,16 @@ export abstract class IntegrationBase { ), this.ts.factory.createIdentifier(this.#ids.paramsArgument), this.f.createBlock([ - makeAssignment( + this.makeAssignment( this.ts.factory.createIdentifier(this.#ids.pathParameter), - makeCall(this.#ids.pathParameter, propOf("replace"))( - makeTemplate(":", [ + this.makeCall( + this.#ids.pathParameter, + propOf("replace"), + )( + this.makeTemplate(":", [ this.ts.factory.createIdentifier(this.#ids.keyParameter), ]), // `:${key}` - makeArrowFn( + this.makeArrowFn( [], this.f.createBlock([ this.f.createExpressionStatement( @@ -288,7 +257,7 @@ export abstract class IntegrationBase { this.ts.factory.createIdentifier(this.#ids.pathParameter), this.ts.factory.createIdentifier(this.#ids.restConst), ]), - ensureTypeNode("const"), + this.ensureTypeNode("const"), ), ), ]), @@ -297,30 +266,33 @@ export abstract class IntegrationBase { // public provide(request: K, params: Input[K]): Promise {} #makeProvider = () => - makePublicMethod( + this.makePublicMethod( this.ts.factory.createIdentifier(this.#ids.provideMethod), - makeParams({ + this.makeParams({ [this.#ids.requestParameter]: "K", - [this.#ids.paramsArgument]: makeIndexed(this.interfaces.input, "K"), + [this.#ids.paramsArgument]: this.makeIndexed( + this.interfaces.input, + "K", + ), [this.#ids.ctxArgument]: { optional: true, type: "T" }, }), [ - makeConst( + this.makeConst( // const [method, path] = this.parseRequest(request); - makeDeconstruction( + this.makeDeconstruction( this.ts.factory.createIdentifier(this.#ids.methodParameter), this.ts.factory.createIdentifier(this.#ids.pathParameter), ), - makeCall(this.#ids.parseRequestFn)( + this.makeCall(this.#ids.parseRequestFn)( this.ts.factory.createIdentifier(this.#ids.requestParameter), ), ), // return this.implementation(___) this.f.createReturnStatement( - makeCall(this.f.createThis(), this.#ids.implementationArgument)( + this.makeCall(this.f.createThis(), this.#ids.implementationArgument)( this.ts.factory.createIdentifier(this.#ids.methodParameter), this.f.createSpreadElement( - makeCall(this.#ids.substituteFn)( + this.makeCall(this.#ids.substituteFn)( this.ts.factory.createIdentifier(this.#ids.pathParameter), this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), @@ -331,7 +303,9 @@ export abstract class IntegrationBase { ], { typeParams: { K: this.requestType.name }, - returns: makePromise(makeIndexed(this.interfaces.response, "K")), + returns: this.makePromise( + this.makeIndexed(this.interfaces.response, "K"), + ), }, ); @@ -340,14 +314,14 @@ export abstract class IntegrationBase { * @internal * */ protected makeClientClass = (name: string) => - makePublicClass( + this.makePublicClass( name, [ // public constructor(protected readonly implementation: Implementation = defaultImplementation) {} - makePublicConstructor([ - makeParam(this.#ids.implementationArgument, { - type: ensureTypeNode(this.#ids.implementationType, ["T"]), - mod: accessModifiers.protectedReadonly, + this.makePublicConstructor([ + this.makeParam(this.#ids.implementationArgument, { + type: this.ensureTypeNode(this.#ids.implementationType, ["T"]), + mod: this.accessModifiers.protectedReadonly, init: this.ts.factory.createIdentifier( this.#ids.defaultImplementationConst, ), @@ -360,18 +334,18 @@ export abstract class IntegrationBase { // `?${new URLSearchParams(____)}` #makeSearchParams = (from: ts.Expression) => - makeTemplate("?", [makeNew(URLSearchParams.name, from)]); + this.makeTemplate("?", [this.makeNew(URLSearchParams.name, from)]); // new URL(`${path}${searchParams}`, "http:____") #makeFetchURL = () => - makeNew( + this.makeNew( URL.name, - makeTemplate( + this.makeTemplate( "", [this.ts.factory.createIdentifier(this.#ids.pathParameter)], [this.ts.factory.createIdentifier(this.#ids.searchParamsConst)], ), - literally(this.serverUrl), + this.literally(this.serverUrl), ); /** @@ -382,18 +356,18 @@ export abstract class IntegrationBase { // method: method.toUpperCase() const methodProperty = this.f.createPropertyAssignment( propOf("method"), - makeCall(this.#ids.methodParameter, propOf("toUpperCase"))(), + this.makeCall(this.#ids.methodParameter, propOf("toUpperCase"))(), ); // headers: hasBody ? { "Content-Type": "application/json" } : undefined const headersProperty = this.f.createPropertyAssignment( propOf("headers"), - makeTernary( + this.makeTernary( this.ts.factory.createIdentifier(this.#ids.hasBodyConst), this.f.createObjectLiteralExpression([ this.f.createPropertyAssignment( - literally("Content-Type"), - literally(contentTypes.json), + this.literally("Content-Type"), + this.literally(contentTypes.json), ), ]), this.ts.factory.createIdentifier(this.#ids.undefinedValue), @@ -403,9 +377,9 @@ export abstract class IntegrationBase { // body: hasBody ? JSON.stringify(params) : undefined const bodyProperty = this.f.createPropertyAssignment( propOf("body"), - makeTernary( + this.makeTernary( this.ts.factory.createIdentifier(this.#ids.hasBodyConst), - makeCall( + this.makeCall( JSON[Symbol.toStringTag], propOf("stringify"), )(this.ts.factory.createIdentifier(this.#ids.paramsArgument)), @@ -414,10 +388,10 @@ export abstract class IntegrationBase { ); // const response = await fetch(new URL(`${path}${searchParams}`, "https://example.com"), { ___ }); - const responseStatement = makeConst( + const responseStatement = this.makeConst( this.#ids.responseConst, this.f.createAwaitExpression( - makeCall(fetch.name)( + this.makeCall(fetch.name)( this.#makeFetchURL(), this.f.createObjectLiteralExpression([ methodProperty, @@ -429,14 +403,14 @@ export abstract class IntegrationBase { ); // const hasBody = !["get", "delete"].includes(method); - const hasBodyStatement = makeConst( + const hasBodyStatement = this.makeConst( this.#ids.hasBodyConst, this.f.createLogicalNot( - makeCall( + this.makeCall( this.f.createArrayLiteralExpression([ - literally("get" satisfies ClientMethod), - literally("head" satisfies ClientMethod), - literally("delete" satisfies ClientMethod), + this.literally("get" satisfies ClientMethod), + this.literally("head" satisfies ClientMethod), + this.literally("delete" satisfies ClientMethod), ]), propOf("includes"), )(this.ts.factory.createIdentifier(this.#ids.methodParameter)), @@ -444,11 +418,11 @@ export abstract class IntegrationBase { ); // const searchParams = hasBody ? "" : ___; - const searchParamsStatement = makeConst( + const searchParamsStatement = this.makeConst( this.#ids.searchParamsConst, - makeTernary( + this.makeTernary( this.ts.factory.createIdentifier(this.#ids.hasBodyConst), - literally(""), + this.literally(""), this.#makeSearchParams( this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), @@ -456,13 +430,13 @@ export abstract class IntegrationBase { ); // const contentType = response.headers.get("content-type"); - const contentTypeStatement = makeConst( + const contentTypeStatement = this.makeConst( this.#ids.contentTypeConst, - makeCall( + this.makeCall( this.#ids.responseConst, propOf("headers"), propOf("get"), - )(literally("content-type")), + )(this.literally("content-type")), ); // if (!contentType) return; @@ -475,29 +449,29 @@ export abstract class IntegrationBase { ); // const isJSON = contentType.startsWith("application/json"); - const isJsonConst = makeConst( + const isJsonConst = this.makeConst( this.#ids.isJsonConst, - makeCall( + this.makeCall( this.#ids.contentTypeConst, propOf("startsWith"), - )(literally(contentTypes.json)), + )(this.literally(contentTypes.json)), ); // return response[isJSON ? "json" : "text"](); const returnStatement = this.f.createReturnStatement( - makeCall( + this.makeCall( this.#ids.responseConst, - makeTernary( + this.makeTernary( this.ts.factory.createIdentifier(this.#ids.isJsonConst), - literally(propOf("json")), - literally(propOf("text")), + this.literally(propOf("json")), + this.literally(propOf("text")), ), )(), ); - return makeConst( + return this.makeConst( this.#ids.defaultImplementationConst, - makeArrowFn( + this.makeArrowFn( [ this.#ids.methodParameter, this.#ids.pathParameter, @@ -519,75 +493,78 @@ export abstract class IntegrationBase { }; #makeSubscriptionConstructor = () => - makePublicConstructor( - makeParams({ + this.makePublicConstructor( + this.makeParams({ request: "K", - params: makeIndexed(this.interfaces.input, "K"), + params: this.makeIndexed(this.interfaces.input, "K"), }), [ - makeConst( - makeDeconstruction( + this.makeConst( + this.makeDeconstruction( this.ts.factory.createIdentifier(this.#ids.pathParameter), this.ts.factory.createIdentifier(this.#ids.restConst), ), - makeCall(this.#ids.substituteFn)( + this.makeCall(this.#ids.substituteFn)( this.f.createElementAccessExpression( - makeCall(this.#ids.parseRequestFn)( + this.makeCall(this.#ids.parseRequestFn)( this.ts.factory.createIdentifier(this.#ids.requestParameter), ), - literally(1), + this.literally(1), ), this.ts.factory.createIdentifier(this.#ids.paramsArgument), ), ), - makeConst( + this.makeConst( this.#ids.searchParamsConst, this.#makeSearchParams( this.ts.factory.createIdentifier(this.#ids.restConst), ), ), - makeAssignment( + this.makeAssignment( this.f.createPropertyAccessExpression( this.f.createThis(), this.#ids.sourceProp, ), - makeNew("EventSource", this.#makeFetchURL()), + this.makeNew("EventSource", this.#makeFetchURL()), ), ], ); #makeEventNarrow = (value: Typeable) => this.f.createTypeLiteralNode([ - makeInterfaceProp(propOf("event"), value), + this.makeInterfaceProp(propOf("event"), value), ]); #makeOnMethod = () => - makePublicMethod( + this.makePublicMethod( this.ts.factory.createIdentifier(this.#ids.onMethod), - makeParams({ + this.makeParams({ [this.#ids.eventParameter]: "E", - [this.#ids.handlerParameter]: makeFnType( + [this.#ids.handlerParameter]: this.makeFnType( { - [this.#ids.dataParameter]: makeIndexed( - makeExtract("R", makeOneLine(this.#makeEventNarrow("E"))), - makeLiteralType(propOf("data")), + [this.#ids.dataParameter]: this.makeIndexed( + this.makeExtract( + "R", + this.makeOneLine(this.#makeEventNarrow("E")), + ), + this.makeLiteralType(propOf("data")), ), }, - makeMaybeAsync(this.ts.SyntaxKind.VoidKeyword), + this.makeMaybeAsync(this.ts.SyntaxKind.VoidKeyword), ), }), [ this.f.createExpressionStatement( - makeCall( + this.makeCall( this.f.createThis(), this.#ids.sourceProp, propOf("addEventListener"), )( this.ts.factory.createIdentifier(this.#ids.eventParameter), - makeArrowFn( + this.makeArrowFn( [this.#ids.msgParameter], - makeCall(this.#ids.handlerParameter)( - makeCall( + this.makeCall(this.#ids.handlerParameter)( + this.makeCall( JSON[Symbol.toStringTag], propOf("parse"), )( @@ -597,7 +574,7 @@ export abstract class IntegrationBase { this.ts.factory.createIdentifier( this.#ids.msgParameter, ), - ensureTypeNode(MessageEvent.name), + this.ensureTypeNode(MessageEvent.name), ), ), propOf("data"), @@ -611,7 +588,10 @@ export abstract class IntegrationBase { ], { typeParams: { - E: makeIndexed("R", makeLiteralType(propOf("event"))), + E: this.makeIndexed( + "R", + this.makeLiteralType(propOf("event")), + ), }, }, ); @@ -621,30 +601,30 @@ export abstract class IntegrationBase { * @internal * */ protected makeSubscriptionClass = (name: string) => - makePublicClass( + this.makePublicClass( name, [ - makePublicProperty(this.#ids.sourceProp, "EventSource"), + this.makePublicProperty(this.#ids.sourceProp, "EventSource"), this.#makeSubscriptionConstructor(), this.#makeOnMethod(), ], { typeParams: { - K: makeExtract( + K: this.makeExtract( this.requestType.name, this.f.createTemplateLiteralType( this.f.createTemplateHead("get "), [ this.f.createTemplateLiteralTypeSpan( - ensureTypeNode(this.ts.SyntaxKind.StringKeyword), + this.ensureTypeNode(this.ts.SyntaxKind.StringKeyword), this.f.createTemplateTail(""), ), ], ), ), - R: makeExtract( - makeIndexed(this.interfaces.positive, "K"), - makeOneLine( + R: this.makeExtract( + this.makeIndexed(this.interfaces.positive, "K"), + this.makeOneLine( this.#makeEventNarrow(this.ts.SyntaxKind.StringKeyword), ), ), @@ -657,22 +637,25 @@ export abstract class IntegrationBase { clientClassName: string, subscriptionClassName: string, ): ts.Node[] => [ - makeConst(this.#ids.clientConst, makeNew(clientClassName)), // const client = new Client(); + this.makeConst(this.#ids.clientConst, this.makeNew(clientClassName)), // const client = new Client(); // client.provide("get /v1/user/retrieve", { id: "10" }); - makeCall(this.#ids.clientConst, this.#ids.provideMethod)( - literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`), + this.makeCall(this.#ids.clientConst, this.#ids.provideMethod)( + this.literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`), this.f.createObjectLiteralExpression([ - this.f.createPropertyAssignment("id", literally("10")), + this.f.createPropertyAssignment("id", this.literally("10")), ]), ), // new Subscription("get /v1/events/stream", {}).on("time", (time) => {}); - makeCall( - makeNew( + this.makeCall( + this.makeNew( subscriptionClassName, - literally(`${"get" satisfies ClientMethod} /v1/events/stream`), + this.literally(`${"get" satisfies ClientMethod} /v1/events/stream`), this.f.createObjectLiteralExpression(), ), this.#ids.onMethod, - )(literally("time"), makeArrowFn(["time"], this.f.createBlock([]))), + )( + this.literally("time"), + this.makeArrowFn(["time"], this.f.createBlock([])), + ), ]; } diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index 89da646400..fd1f2368c0 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -3,16 +3,6 @@ import type ts from "typescript"; import { z } from "zod"; import { ResponseVariant, responseVariants } from "./api-response"; import { IntegrationBase } from "./integration-base"; -import { - makeInterfaceProp, - makeInterface, - makeType, - printNode, - ensureTypeNode, - makeIndexed, - makeLiteralType, - makeUnion, -} from "./typescript-api"; import { shouldHaveContent, makeCleanId } from "./common-helpers"; import { loadPeer } from "./peer-helpers"; import { Routing } from "./routing"; @@ -81,11 +71,11 @@ export class Integration extends IntegrationBase { let name = this.#aliases.get(key)?.name?.text; if (!name) { name = `Type${this.#aliases.size + 1}`; - const temp = makeLiteralType(null); - this.#aliases.set(key, makeType(name, temp)); - this.#aliases.set(key, makeType(name, produce())); + const temp = this.makeLiteralType(null); + this.#aliases.set(key, this.makeType(name, temp)); + this.#aliases.set(key, this.makeType(name, produce())); } - return ensureTypeNode(name); + return this.ensureTypeNode(name); } public constructor({ @@ -107,26 +97,28 @@ export class Integration extends IntegrationBase { const entitle = makeCleanId.bind(null, method, path); // clean id with method+path prefix const { isDeprecated, inputSchema, tags } = endpoint; const request = `${method} ${path}`; - const input = makeType(entitle("input"), zodToTs(inputSchema, ctxIn), { - comment: request, - }); + const input = this.makeType( + entitle("input"), + zodToTs(inputSchema, ctxIn), + { comment: request }, + ); this.#program.push(input); const dictionaries = responseVariants.reduce( (agg, responseVariant) => { const responses = endpoint.getResponses(responseVariant); const props = R.chain(([idx, { schema, mimeTypes, statusCodes }]) => { const hasContent = shouldHaveContent(method, mimeTypes); - const variantType = makeType( + const variantType = this.makeType( entitle(responseVariant, "variant", `${idx + 1}`), zodToTs(hasContent ? schema : noContent, ctxOut), { comment: request }, ); this.#program.push(variantType); return statusCodes.map((code) => - makeInterfaceProp(code, variantType.name), + this.makeInterfaceProp(code, variantType.name), ); }, Array.from(responses.entries())); - const dict = makeInterface( + const dict = this.makeInterface( entitle(responseVariant, "response", "variants"), props, { comment: request }, @@ -137,18 +129,18 @@ export class Integration extends IntegrationBase { {} as Record, ); this.paths.add(path); - const literalIdx = makeLiteralType(request); + const literalIdx = this.makeLiteralType(request); const store = { - input: ensureTypeNode(input.name), + input: this.ensureTypeNode(input.name), positive: this.someOf(dictionaries.positive), negative: this.someOf(dictionaries.negative), - response: makeUnion([ - makeIndexed(this.interfaces.positive, literalIdx), - makeIndexed(this.interfaces.negative, literalIdx), + response: this.makeUnion([ + this.makeIndexed(this.interfaces.positive, literalIdx), + this.makeIndexed(this.interfaces.negative, literalIdx), ]), encoded: this.f.createIntersectionTypeNode([ - ensureTypeNode(dictionaries.positive.name), - ensureTypeNode(dictionaries.negative.name), + this.ensureTypeNode(dictionaries.positive.name), + this.ensureTypeNode(dictionaries.negative.name), ]), }; this.registry.set(request, { isDeprecated, store }); @@ -190,7 +182,7 @@ export class Integration extends IntegrationBase { .map((entry) => typeof entry === "string" ? entry - : printNode(entry, printerOptions), + : this.printNode(entry, printerOptions), ) .join("\n") : undefined; @@ -212,7 +204,7 @@ export class Integration extends IntegrationBase { return this.#program .concat(commentNode || []) .map((node, index) => - printNode( + this.printNode( node, index < this.#program.length ? printerOptions diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index b4aa43df9b..21d81ded7e 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -1,5 +1,6 @@ +import { createRequire } from "node:module"; import * as R from "ramda"; -import ts from "typescript"; +import type ts from "typescript"; export type Typeable = | ts.TypeNode @@ -11,410 +12,447 @@ type TypeParams = | string[] | Partial>; -export const f = ts.factory; - -const exportModifier = [f.createModifier(ts.SyntaxKind.ExportKeyword)]; - -const asyncModifier = [f.createModifier(ts.SyntaxKind.AsyncKeyword)]; - -export const accessModifiers = { - public: [f.createModifier(ts.SyntaxKind.PublicKeyword)], - protectedReadonly: [ - f.createModifier(ts.SyntaxKind.ProtectedKeyword), - f.createModifier(ts.SyntaxKind.ReadonlyKeyword), - ], -}; - -export const addJsDoc = (node: T, text: string) => - ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - `* ${text} `, - true, - ); - -export const printNode = ( - node: ts.Node, - printerOptions?: ts.PrinterOptions, -) => { - const sourceFile = ts.createSourceFile( - "print.ts", - "", - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS, - ); - const printer = ts.createPrinter(printerOptions); - return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); -}; - -const safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; -export const makePropertyIdentifier = (name: string | number) => - typeof name === "string" && safePropRegex.test(name) - ? f.createIdentifier(name) - : literally(name); - -export const makeTemplate = ( - head: string, - ...rest: ([ts.Expression] | [ts.Expression, string])[] -) => - f.createTemplateExpression( - f.createTemplateHead(head), - rest.map(([id, str = ""], idx) => - f.createTemplateSpan( - id, - idx === rest.length - 1 - ? f.createTemplateTail(str) - : f.createTemplateMiddle(str), +export class TypescriptAPI { + public ts: typeof ts; + public f: typeof ts.factory; + public exportModifier: ts.ModifierToken[]; + public asyncModifier: ts.ModifierToken[]; + public accessModifiers: Record<"public" | "protectedReadonly", ts.Modifier[]>; + #primitives: ts.KeywordTypeSyntaxKind[]; + #safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + + constructor() { + this.ts = createRequire(import.meta.url)("typescript"); // @todo replace with a dynamic import in next major + this.f = this.ts.factory; + this.exportModifier = [ + this.f.createModifier(this.ts.SyntaxKind.ExportKeyword), + ]; + this.asyncModifier = [ + this.f.createModifier(this.ts.SyntaxKind.AsyncKeyword), + ]; + this.accessModifiers = { + public: [this.f.createModifier(this.ts.SyntaxKind.PublicKeyword)], + protectedReadonly: [ + this.f.createModifier(this.ts.SyntaxKind.ProtectedKeyword), + this.f.createModifier(this.ts.SyntaxKind.ReadonlyKeyword), + ], + }; + this.#primitives = [ + this.ts.SyntaxKind.AnyKeyword, + this.ts.SyntaxKind.BigIntKeyword, + this.ts.SyntaxKind.BooleanKeyword, + this.ts.SyntaxKind.NeverKeyword, + this.ts.SyntaxKind.NumberKeyword, + this.ts.SyntaxKind.ObjectKeyword, + this.ts.SyntaxKind.StringKeyword, + this.ts.SyntaxKind.SymbolKeyword, + this.ts.SyntaxKind.UndefinedKeyword, + this.ts.SyntaxKind.UnknownKeyword, + this.ts.SyntaxKind.VoidKeyword, + ]; + } + + public addJsDoc = (node: T, text: string) => + this.ts.addSyntheticLeadingComment( + node, + this.ts.SyntaxKind.MultiLineCommentTrivia, + `* ${text} `, + true, + ); + + public printNode = (node: ts.Node, printerOptions?: ts.PrinterOptions) => { + const sourceFile = this.ts.createSourceFile( + "print.ts", + "", + this.ts.ScriptTarget.Latest, + false, + this.ts.ScriptKind.TS, + ); + const printer = this.ts.createPrinter(printerOptions); + return printer.printNode(this.ts.EmitHint.Unspecified, node, sourceFile); + }; + + public makePropertyIdentifier = (name: string | number) => + typeof name === "string" && this.#safePropRegex.test(name) + ? this.f.createIdentifier(name) + : this.literally(name); + + public makeTemplate = ( + head: string, + ...rest: ([ts.Expression] | [ts.Expression, string])[] + ) => + this.f.createTemplateExpression( + this.f.createTemplateHead(head), + rest.map(([id, str = ""], idx) => + this.f.createTemplateSpan( + id, + idx === rest.length - 1 + ? this.f.createTemplateTail(str) + : this.f.createTemplateMiddle(str), + ), ), - ), - ); - -export const makeParam = ( - name: string | ts.Identifier, - { - type, - mod, - init, - optional, - }: { - type?: Typeable; - mod?: ts.Modifier[]; - init?: ts.Expression; - optional?: boolean; - } = {}, -) => - f.createParameterDeclaration( - mod, - undefined, - name, - optional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, - type ? ensureTypeNode(type) : undefined, - init, - ); - -export const makeParams = ( - params: Partial[1]>>, -) => - Object.entries(params).map(([name, value]) => - makeParam( + ); + + public makeParam = ( + name: string | ts.Identifier, + { + type, + mod, + init, + optional, + }: { + type?: Typeable; + mod?: ts.Modifier[]; + init?: ts.Expression; + optional?: boolean; + } = {}, + ) => + this.f.createParameterDeclaration( + mod, + undefined, name, - typeof value === "string" || - typeof value === "number" || - (typeof value === "object" && "kind" in value) - ? { type: value } - : value, - ), - ); - -export const makePublicConstructor = ( - params: ts.ParameterDeclaration[], - statements: ts.Statement[] = [], -) => - f.createConstructorDeclaration( - accessModifiers.public, - params, - f.createBlock(statements), - ); - -export const ensureTypeNode = ( - subject: Typeable, - args?: Typeable[], // only for string and id -): ts.TypeNode => - typeof subject === "number" - ? f.createKeywordTypeNode(subject) - : typeof subject === "string" || ts.isIdentifier(subject) - ? f.createTypeReferenceNode(subject, args && R.map(ensureTypeNode, args)) - : subject; - -// Record -export const recordStringAny = ensureTypeNode("Record", [ - ts.SyntaxKind.StringKeyword, - ts.SyntaxKind.AnyKeyword, -]); - -/** ensures distinct union (unique primitives) */ -export const makeUnion = (entries: ts.TypeNode[]) => { - const nodes = new Map(); - for (const entry of entries) - nodes.set(isPrimitive(entry) ? entry.kind : entry, entry); - return f.createUnionTypeNode(Array.from(nodes.values())); -}; - -export const makeInterfaceProp = ( - name: string | number, - value: Typeable, - { - isOptional, - isDeprecated, - comment, - }: { isOptional?: boolean; isDeprecated?: boolean; comment?: string } = {}, -) => { - const propType = ensureTypeNode(value); - const node = f.createPropertySignature( - undefined, - makePropertyIdentifier(name), - isOptional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, - isOptional - ? makeUnion([propType, ensureTypeNode(ts.SyntaxKind.UndefinedKeyword)]) - : propType, - ); - const jsdoc = R.reject(R.isNil, [ - isDeprecated ? "@deprecated" : undefined, - comment, - ]); - return jsdoc.length ? addJsDoc(node, jsdoc.join(" ")) : node; -}; - -export const makeOneLine = (subject: ts.TypeNode) => - ts.setEmitFlags(subject, ts.EmitFlags.SingleLine); - -export const makeDeconstruction = ( - ...names: ts.Identifier[] -): ts.ArrayBindingPattern => - f.createArrayBindingPattern( - names.map( - (name) => f.createBindingElement(undefined, undefined, name), // can also add default value at last - ), - ); - -export const makeConst = ( - name: string | ts.Identifier | ts.ArrayBindingPattern, - value: ts.Expression, - { type, expose }: { type?: Typeable; expose?: true } = {}, -) => - f.createVariableStatement( - expose && exportModifier, - f.createVariableDeclarationList( - [ - f.createVariableDeclaration( - name, - undefined, - type ? ensureTypeNode(type) : undefined, - value, - ), - ], - ts.NodeFlags.Const, - ), - ); - -export const makePublicLiteralType = ( - name: ts.Identifier | string, - literals: string[], -) => - makeType(name, makeUnion(R.map(makeLiteralType, literals)), { expose: true }); - -export const makeType = ( - name: ts.Identifier | string, - value: ts.TypeNode, - { - expose, - comment, - params, - }: { expose?: boolean; comment?: string; params?: TypeParams } = {}, -) => { - const node = f.createTypeAliasDeclaration( - expose ? exportModifier : undefined, - name, - params && makeTypeParams(params), - value, - ); - return comment ? addJsDoc(node, comment) : node; -}; - -export const makePublicProperty = ( - name: string | ts.PropertyName, - type: Typeable, -) => - f.createPropertyDeclaration( - accessModifiers.public, - name, - undefined, - ensureTypeNode(type), - undefined, - ); - -export const makePublicMethod = ( - name: ts.Identifier, - params: ts.ParameterDeclaration[], - statements: ts.Statement[], - { - typeParams, - returns, - }: { typeParams?: TypeParams; returns?: ts.TypeNode } = {}, -) => - f.createMethodDeclaration( - accessModifiers.public, - undefined, - name, - undefined, - typeParams && makeTypeParams(typeParams), - params, - returns, - f.createBlock(statements), - ); - -export const makePublicClass = ( - name: string, - statements: ts.ClassElement[], - { typeParams }: { typeParams?: TypeParams } = {}, -) => - f.createClassDeclaration( - exportModifier, - name, - typeParams && makeTypeParams(typeParams), - undefined, - statements, - ); - -export const makeKeyOf = (subj: Typeable) => - f.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, ensureTypeNode(subj)); - -export const makePromise = (subject: Typeable) => - ensureTypeNode(Promise.name, [subject]); - -export const makeInterface = ( - name: ts.Identifier | string, - props: ts.PropertySignature[], - { expose, comment }: { expose?: boolean; comment?: string } = {}, -) => { - const node = f.createInterfaceDeclaration( - expose ? exportModifier : undefined, - name, - undefined, - undefined, - props, - ); - return comment ? addJsDoc(node, comment) : node; -}; - -export const makeTypeParams = ( - params: - | string[] - | Partial< - Record - >, -) => - (Array.isArray(params) - ? params.map((name) => R.pair(name, undefined)) - : Object.entries(params) - ).map(([name, val]) => { - const { type, init } = - typeof val === "object" && "init" in val ? val : { type: val }; - return f.createTypeParameterDeclaration( - [], + optional + ? this.f.createToken(this.ts.SyntaxKind.QuestionToken) + : undefined, + type ? this.ensureTypeNode(type) : undefined, + init, + ); + + public makeParams = ( + params: Partial< + Record[1]> + >, + ) => + Object.entries(params).map(([name, value]) => + this.makeParam( + name, + typeof value === "string" || + typeof value === "number" || + (typeof value === "object" && "kind" in value) + ? { type: value } + : value, + ), + ); + + public makePublicConstructor = ( + params: ts.ParameterDeclaration[], + statements: ts.Statement[] = [], + ) => + this.f.createConstructorDeclaration( + this.accessModifiers.public, + params, + this.f.createBlock(statements), + ); + + public ensureTypeNode = ( + subject: Typeable, + args?: Typeable[], // only for string and id + ): ts.TypeNode => + typeof subject === "number" + ? this.f.createKeywordTypeNode(subject) + : typeof subject === "string" || this.ts.isIdentifier(subject) + ? this.f.createTypeReferenceNode( + subject, + args && R.map(this.ensureTypeNode.bind(this), args), + ) + : subject; + + // Record + public makeRecordStringAny = () => + this.ensureTypeNode("Record", [ + this.ts.SyntaxKind.StringKeyword, + this.ts.SyntaxKind.AnyKeyword, + ]); + + /** ensures distinct union (unique primitives) */ + public makeUnion = (entries: ts.TypeNode[]) => { + const nodes = new Map< + ts.TypeNode | ts.KeywordTypeSyntaxKind, + ts.TypeNode + >(); + for (const entry of entries) + nodes.set(this.isPrimitive(entry) ? entry.kind : entry, entry); + return this.f.createUnionTypeNode(Array.from(nodes.values())); + }; + + public makeInterfaceProp = ( + name: string | number, + value: Typeable, + { + isOptional, + isDeprecated, + comment, + }: { isOptional?: boolean; isDeprecated?: boolean; comment?: string } = {}, + ) => { + const propType = this.ensureTypeNode(value); + const node = this.f.createPropertySignature( + undefined, + this.makePropertyIdentifier(name), + isOptional + ? this.f.createToken(this.ts.SyntaxKind.QuestionToken) + : undefined, + isOptional + ? this.makeUnion([ + propType, + this.ensureTypeNode(this.ts.SyntaxKind.UndefinedKeyword), + ]) + : propType, + ); + const jsdoc = R.reject(R.isNil, [ + isDeprecated ? "@deprecated" : undefined, + comment, + ]); + return jsdoc.length ? this.addJsDoc(node, jsdoc.join(" ")) : node; + }; + + public makeOneLine = (subject: ts.TypeNode) => + this.ts.setEmitFlags(subject, this.ts.EmitFlags.SingleLine); + + public makeDeconstruction = ( + ...names: ts.Identifier[] + ): ts.ArrayBindingPattern => + this.f.createArrayBindingPattern( + names.map( + (name) => this.f.createBindingElement(undefined, undefined, name), // can also add default value at last + ), + ); + + public makeConst = ( + name: string | ts.Identifier | ts.ArrayBindingPattern, + value: ts.Expression, + { type, expose }: { type?: Typeable; expose?: true } = {}, + ) => + this.f.createVariableStatement( + expose && this.exportModifier, + this.f.createVariableDeclarationList( + [ + this.f.createVariableDeclaration( + name, + undefined, + type ? this.ensureTypeNode(type) : undefined, + value, + ), + ], + this.ts.NodeFlags.Const, + ), + ); + + public makePublicLiteralType = ( + name: ts.Identifier | string, + literals: string[], + ) => + this.makeType( name, - type ? ensureTypeNode(type) : undefined, - init ? ensureTypeNode(init) : undefined, + this.makeUnion(R.map(this.makeLiteralType.bind(this), literals)), + { expose: true }, ); - }); - -export const makeArrowFn = ( - params: - | Array[0]> - | Parameters[0], - body: ts.ConciseBody, - { isAsync }: { isAsync?: boolean } = {}, -) => - f.createArrowFunction( - isAsync ? asyncModifier : undefined, - undefined, - Array.isArray(params) ? R.map(makeParam, params) : makeParams(params), - undefined, - undefined, - body, - ); -export const propOf = (name: keyof NoInfer) => name as string; + public makeType = ( + name: ts.Identifier | string, + value: ts.TypeNode, + { + expose, + comment, + params, + }: { expose?: boolean; comment?: string; params?: TypeParams } = {}, + ) => { + const node = this.f.createTypeAliasDeclaration( + expose ? this.exportModifier : undefined, + name, + params && this.makeTypeParams(params), + value, + ); + return comment ? this.addJsDoc(node, comment) : node; + }; + + public makePublicProperty = ( + name: string | ts.PropertyName, + type: Typeable, + ) => + this.f.createPropertyDeclaration( + this.accessModifiers.public, + name, + undefined, + this.ensureTypeNode(type), + undefined, + ); + + public makePublicMethod = ( + name: ts.Identifier, + params: ts.ParameterDeclaration[], + statements: ts.Statement[], + { + typeParams, + returns, + }: { typeParams?: TypeParams; returns?: ts.TypeNode } = {}, + ) => + this.f.createMethodDeclaration( + this.accessModifiers.public, + undefined, + name, + undefined, + typeParams && this.makeTypeParams(typeParams), + params, + returns, + this.f.createBlock(statements), + ); + + public makePublicClass = ( + name: string, + statements: ts.ClassElement[], + { typeParams }: { typeParams?: TypeParams } = {}, + ) => + this.f.createClassDeclaration( + this.exportModifier, + name, + typeParams && this.makeTypeParams(typeParams), + undefined, + statements, + ); + + public makeKeyOf = (subj: Typeable) => + this.f.createTypeOperatorNode( + this.ts.SyntaxKind.KeyOfKeyword, + this.ensureTypeNode(subj), + ); + + public makePromise = (subject: Typeable) => + this.ensureTypeNode(Promise.name, [subject]); -export const makeTernary = ( - condition: ts.Expression, - positive: ts.Expression, - negative: ts.Expression, -) => - f.createConditionalExpression( - condition, - f.createToken(ts.SyntaxKind.QuestionToken), - positive, - f.createToken(ts.SyntaxKind.ColonToken), - negative, - ); - -export const makeCall = - ( - first: ts.Expression | string, - ...rest: Array + public makeInterface = ( + name: ts.Identifier | string, + props: ts.PropertySignature[], + { expose, comment }: { expose?: boolean; comment?: string } = {}, + ) => { + const node = this.f.createInterfaceDeclaration( + expose ? this.exportModifier : undefined, + name, + undefined, + undefined, + props, + ); + return comment ? this.addJsDoc(node, comment) : node; + }; + + public makeTypeParams = ( + params: + | string[] + | Partial< + Record + >, + ) => + (Array.isArray(params) + ? params.map((name) => R.pair(name, undefined)) + : Object.entries(params) + ).map(([name, val]) => { + const { type, init } = + typeof val === "object" && "init" in val ? val : { type: val }; + return this.f.createTypeParameterDeclaration( + [], + name, + type ? this.ensureTypeNode(type) : undefined, + init ? this.ensureTypeNode(init) : undefined, + ); + }); + + public makeArrowFn = ( + params: + | Array[0]> + | Parameters[0], + body: ts.ConciseBody, + { isAsync }: { isAsync?: boolean } = {}, ) => - (...args: ts.Expression[]) => - f.createCallExpression( - rest.reduce( - (acc, entry) => - typeof entry === "string" || ts.isIdentifier(entry) - ? f.createPropertyAccessExpression(acc, entry) - : f.createElementAccessExpression(acc, entry), - typeof first === "string" ? f.createIdentifier(first) : first, + this.f.createArrowFunction( + isAsync ? this.asyncModifier : undefined, + undefined, + Array.isArray(params) + ? R.map(this.makeParam.bind(this), params) + : this.makeParams(params), + undefined, + undefined, + body, + ); + + public makeTernary = ( + condition: ts.Expression, + positive: ts.Expression, + negative: ts.Expression, + ) => + this.f.createConditionalExpression( + condition, + this.f.createToken(this.ts.SyntaxKind.QuestionToken), + positive, + this.f.createToken(this.ts.SyntaxKind.ColonToken), + negative, + ); + + public makeCall = + ( + first: ts.Expression | string, + ...rest: Array + ) => + (...args: ts.Expression[]) => + this.f.createCallExpression( + rest.reduce( + (acc, entry) => + typeof entry === "string" || this.ts.isIdentifier(entry) + ? this.f.createPropertyAccessExpression(acc, entry) + : this.f.createElementAccessExpression(acc, entry), + typeof first === "string" ? this.f.createIdentifier(first) : first, + ), + undefined, + args, + ); + + public makeNew = (cls: string, ...args: ts.Expression[]) => + this.f.createNewExpression(this.f.createIdentifier(cls), undefined, args); + + public makeExtract = (base: Typeable, narrow: ts.TypeNode) => + this.ensureTypeNode("Extract", [base, narrow]); + + public makeAssignment = (left: ts.Expression, right: ts.Expression) => + this.f.createExpressionStatement( + this.f.createBinaryExpression( + left, + this.f.createToken(this.ts.SyntaxKind.EqualsToken), + right, ), + ); + + public makeIndexed = (subject: Typeable, index: Typeable) => + this.f.createIndexedAccessTypeNode( + this.ensureTypeNode(subject), + this.ensureTypeNode(index), + ); + + public makeMaybeAsync = (subj: Typeable) => + this.makeUnion([this.ensureTypeNode(subj), this.makePromise(subj)]); + + public makeFnType = ( + params: Parameters[0], + returns: Typeable, + ) => + this.f.createFunctionTypeNode( undefined, - args, + this.makeParams(params), + this.ensureTypeNode(returns), ); -export const makeNew = (cls: string, ...args: ts.Expression[]) => - f.createNewExpression(f.createIdentifier(cls), undefined, args); - -export const makeExtract = (base: Typeable, narrow: ts.TypeNode) => - ensureTypeNode("Extract", [base, narrow]); - -export const makeAssignment = (left: ts.Expression, right: ts.Expression) => - f.createExpressionStatement( - f.createBinaryExpression( - left, - f.createToken(ts.SyntaxKind.EqualsToken), - right, - ), - ); - -export const makeIndexed = (subject: Typeable, index: Typeable) => - f.createIndexedAccessTypeNode(ensureTypeNode(subject), ensureTypeNode(index)); - -export const makeMaybeAsync = (subj: Typeable) => - makeUnion([ensureTypeNode(subj), makePromise(subj)]); - -export const makeFnType = ( - params: Parameters[0], - returns: Typeable, -) => - f.createFunctionTypeNode( - undefined, - makeParams(params), - ensureTypeNode(returns), - ); - -/* eslint-disable prettier/prettier -- shorter and works better this way than overrides */ -export const literally = (subj: T) => ( - typeof subj === "number" ? f.createNumericLiteral(subj) - : typeof subj === "bigint" ? f.createBigIntLiteral(subj.toString()) - : typeof subj === "boolean" ? subj ? f.createTrue() : f.createFalse() - : subj === null ? f.createNull() : f.createStringLiteral(subj) + /* eslint-disable prettier/prettier -- shorter and works better this way than overrides */ + public literally = (subj: T) => ( + typeof subj === "number" ? this.f.createNumericLiteral(subj) + : typeof subj === "bigint" ? this.f.createBigIntLiteral(subj.toString()) + : typeof subj === "boolean" ? subj ? this.f.createTrue() : this.f.createFalse() + : subj === null ? this.f.createNull() : this.f.createStringLiteral(subj) ) as T extends string ? ts.StringLiteral : T extends number ? ts.NumericLiteral : T extends boolean ? ts.BooleanLiteral : ts.NullLiteral; -/* eslint-enable prettier/prettier */ - -export const makeLiteralType = (subj: Parameters[0]) => - f.createLiteralTypeNode(literally(subj)); - -const primitives: ts.KeywordTypeSyntaxKind[] = [ - ts.SyntaxKind.AnyKeyword, - ts.SyntaxKind.BigIntKeyword, - ts.SyntaxKind.BooleanKeyword, - ts.SyntaxKind.NeverKeyword, - ts.SyntaxKind.NumberKeyword, - ts.SyntaxKind.ObjectKeyword, - ts.SyntaxKind.StringKeyword, - ts.SyntaxKind.SymbolKeyword, - ts.SyntaxKind.UndefinedKeyword, - ts.SyntaxKind.UnknownKeyword, - ts.SyntaxKind.VoidKeyword, -]; - -const isPrimitive = (node: ts.TypeNode): node is ts.KeywordTypeNode => - (primitives as ts.SyntaxKind[]).includes(node.kind); + /* eslint-enable prettier/prettier */ + + public makeLiteralType = (subj: Parameters[0]) => + this.f.createLiteralTypeNode(this.literally(subj)); + + public isPrimitive = (node: ts.TypeNode): node is ts.KeywordTypeNode => + (this.#primitives as ts.SyntaxKind[]).includes(node.kind); +} + +export const propOf = (name: keyof NoInfer) => name as string; From a80f5694353c18dec3eb5f6f76a1ef1caaadd563 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 21:07:38 +0100 Subject: [PATCH 08/18] REF: Adjusting ZTS to take TypescriptAPI from context. --- express-zod-api/src/integration.ts | 2 +- express-zod-api/src/zts-helpers.ts | 2 + express-zod-api/src/zts.ts | 105 +++++++++++++++-------------- 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index fd1f2368c0..a346fead5a 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -90,7 +90,7 @@ export class Integration extends IntegrationBase { hasHeadMethod = true, }: IntegrationParams) { super(serverUrl); - const commons = { makeAlias: this.#makeAlias.bind(this) }; + const commons = { makeAlias: this.#makeAlias.bind(this), api: this }; const ctxIn = { brandHandling, ctx: { ...commons, isResponse: false } }; const ctxOut = { brandHandling, ctx: { ...commons, isResponse: true } }; const onEndpoint: OnEndpoint = (method, path, endpoint) => { diff --git a/express-zod-api/src/zts-helpers.ts b/express-zod-api/src/zts-helpers.ts index 484c68fb0f..6a6eb336a6 100644 --- a/express-zod-api/src/zts-helpers.ts +++ b/express-zod-api/src/zts-helpers.ts @@ -1,10 +1,12 @@ import type ts from "typescript"; import { FlatObject } from "./common-helpers"; import { SchemaHandler } from "./schema-walker"; +import type { TypescriptAPI } from "./typescript-api"; export interface ZTSContext extends FlatObject { isResponse: boolean; makeAlias: (key: object, produce: () => ts.TypeNode) => ts.TypeNode; + api: TypescriptAPI; } export type Producer = SchemaHandler; diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts index 681920a2e6..fce044f70c 100644 --- a/express-zod-api/src/zts.ts +++ b/express-zod-api/src/zts.ts @@ -9,16 +9,9 @@ import { hasCycle } from "./deep-checks"; import { ProprietaryBrand } from "./proprietary-schemas"; import { ezRawBrand, RawSchema } from "./raw-schema"; import { FirstPartyKind, HandlingRules, walkSchema } from "./schema-walker"; -import { - ensureTypeNode, - makeInterfaceProp, - makeLiteralType, - makeUnion, -} from "./typescript-api"; +import type { TypescriptAPI } from "./typescript-api"; import { Producer, ZTSContext } from "./zts-helpers"; -const { factory: f } = ts; - const samples = { [ts.SyntaxKind.AnyKeyword]: "", [ts.SyntaxKind.BigIntKeyword]: BigInt(0), @@ -41,18 +34,21 @@ const nodePath = { optional: R.path(["questionToken" satisfies keyof ts.TypeElement]), }; -const onLiteral: Producer = ({ _zod: { def } }: z.core.$ZodLiteral) => { +const onLiteral: Producer = ( + { _zod: { def } }: z.core.$ZodLiteral, + { api }, +) => { const values = def.values.map((entry) => entry === undefined - ? ensureTypeNode(ts.SyntaxKind.UndefinedKeyword) - : makeLiteralType(entry), + ? api.ensureTypeNode(ts.SyntaxKind.UndefinedKeyword) + : api.makeLiteralType(entry), ); - return values.length === 1 ? values[0] : makeUnion(values); + return values.length === 1 ? values[0] : api.makeUnion(values); }; const onTemplateLiteral: Producer = ( { _zod: { def } }: z.core.$ZodTemplateLiteral, - { next }, + { next, api }, ) => { const parts = [...def.parts]; const shiftText = () => { @@ -67,30 +63,30 @@ const onTemplateLiteral: Producer = ( } return text; }; - const head = f.createTemplateHead(shiftText()); + const head = api.f.createTemplateHead(shiftText()); const spans: ts.TemplateLiteralTypeSpan[] = []; while (parts.length) { const schema = next(parts.shift() as z.core.$ZodType); const text = shiftText(); const textWrapper = parts.length - ? f.createTemplateMiddle - : f.createTemplateTail; - spans.push(f.createTemplateLiteralTypeSpan(schema, textWrapper(text))); + ? api.f.createTemplateMiddle + : api.f.createTemplateTail; + spans.push(api.f.createTemplateLiteralTypeSpan(schema, textWrapper(text))); } - if (!spans.length) return makeLiteralType(head.text); - return f.createTemplateLiteralType(head, spans); + if (!spans.length) return api.makeLiteralType(head.text); + return api.f.createTemplateLiteralType(head, spans); }; const onObject: Producer = ( obj: z.core.$ZodObject, - { isResponse, next, makeAlias }, + { isResponse, next, makeAlias, api }, ) => { const produce = () => { const members = Object.entries(obj._zod.def.shape).map( ([key, value]) => { const { description: comment, deprecated: isDeprecated } = globalRegistry.get(value) || {}; - return makeInterfaceProp(key, next(value), { + return api.makeInterfaceProp(key, next(value), { comment, isDeprecated, isOptional: @@ -98,44 +94,53 @@ const onObject: Producer = ( }); }, ); - return f.createTypeLiteralNode(members); + return api.f.createTypeLiteralNode(members); }; return hasCycle(obj, { io: isResponse ? "output" : "input" }) ? makeAlias(obj, produce) : produce(); }; -const onArray: Producer = ({ _zod: { def } }: z.core.$ZodArray, { next }) => - f.createArrayTypeNode(next(def.element)); +const onArray: Producer = ( + { _zod: { def } }: z.core.$ZodArray, + { next, api }, +) => api.f.createArrayTypeNode(next(def.element)); -const onEnum: Producer = ({ _zod: { def } }: z.core.$ZodEnum) => - makeUnion(Object.values(def.entries).map(makeLiteralType)); +const onEnum: Producer = ({ _zod: { def } }: z.core.$ZodEnum, { api }) => + api.makeUnion(Object.values(def.entries).map(api.makeLiteralType.bind(api))); const onSomeUnion: Producer = ( { _zod: { def } }: z.core.$ZodUnion | z.core.$ZodDiscriminatedUnion, - { next }, -) => makeUnion(def.options.map(next)); + { next, api }, +) => api.makeUnion(def.options.map(next)); const makeSample = (produced: ts.TypeNode) => samples?.[produced.kind as keyof typeof samples]; const onNullable: Producer = ( { _zod: { def } }: z.core.$ZodNullable, - { next }, -) => makeUnion([next(def.innerType), makeLiteralType(null)]); + { next, api }, +) => api.makeUnion([next(def.innerType), api.makeLiteralType(null)]); -const onTuple: Producer = ({ _zod: { def } }: z.core.$ZodTuple, { next }) => - f.createTupleTypeNode( +const onTuple: Producer = ( + { _zod: { def } }: z.core.$ZodTuple, + { next, api }, +) => + api.f.createTupleTypeNode( def.items .map(next) - .concat(def.rest === null ? [] : f.createRestTypeNode(next(def.rest))), + .concat( + def.rest === null ? [] : api.f.createRestTypeNode(next(def.rest)), + ), ); -const onRecord: Producer = ({ _zod: { def } }: z.core.$ZodRecord, { next }) => - ensureTypeNode("Record", [def.keyType, def.valueType].map(next)); +const onRecord: Producer = ( + { _zod: { def } }: z.core.$ZodRecord, + { next, api }, +) => api.ensureTypeNode("Record", [def.keyType, def.valueType].map(next)); const intersect = R.tryCatch( - (nodes: ts.TypeNode[]) => { + (api: TypescriptAPI, nodes: ts.TypeNode[]) => { if (!nodes.every(ts.isTypeLiteralNode)) throw new Error("Not objects"); const members = R.chain(R.prop("members"), nodes); const uniqs = R.uniqWith((...props) => { @@ -144,20 +149,20 @@ const intersect = R.tryCatch( return true; throw new Error("Has conflicting prop"); }, members); - return f.createTypeLiteralNode(uniqs); + return api.f.createTypeLiteralNode(uniqs); }, - (_err, nodes) => f.createIntersectionTypeNode(nodes), + (_err, api, nodes) => api.f.createIntersectionTypeNode(nodes), ); const onIntersection: Producer = ( { _zod: { def } }: z.core.$ZodIntersection, - { next }, -) => intersect([def.left, def.right].map(next)); + { next, api }, +) => intersect(api, [def.left, def.right].map(next)); const onPrimitive = (syntaxKind: ts.KeywordTypeSyntaxKind): Producer => - () => - ensureTypeNode(syntaxKind); + ({}, { api }) => + api.ensureTypeNode(syntaxKind); const onWrapped: Producer = ( { @@ -171,14 +176,14 @@ const onWrapped: Producer = ( { next }, ) => next(def.innerType); -const getFallback = (isResponse: boolean) => - ensureTypeNode( +const getFallback = (api: TypescriptAPI, isResponse: boolean) => + api.ensureTypeNode( isResponse ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.AnyKeyword, ); const onPipeline: Producer = ( { _zod: { def } }: z.core.$ZodPipe, - { next, isResponse }, + { next, isResponse, api }, ) => { const target = def[isResponse ? "out" : "in"]; const opposite = def[isResponse ? "in" : "out"]; @@ -195,19 +200,19 @@ const onPipeline: Producer = ( undefined: ts.SyntaxKind.UndefinedKeyword, object: ts.SyntaxKind.ObjectKeyword, }; - return ensureTypeNode( - (targetType && resolutions[targetType]) || getFallback(isResponse), + return api.ensureTypeNode( + (targetType && resolutions[targetType]) || getFallback(api, isResponse), ); }; -const onNull: Producer = () => makeLiteralType(null); +const onNull: Producer = ({}, { api }) => api.makeLiteralType(null); const onLazy: Producer = ( { _zod: { def } }: z.core.$ZodLazy, { makeAlias, next }, ) => makeAlias(def.getter, () => next(def.getter())); -const onBuffer: Producer = () => ensureTypeNode("Buffer"); +const onBuffer: Producer = ({}, { api }) => api.ensureTypeNode("Buffer"); const onRaw: Producer = (schema: RawSchema, { next }) => next(schema._zod.def.shape.raw); @@ -262,6 +267,6 @@ export const zodToTs = ( ) => walkSchema(schema, { rules: { ...brandHandling, ...producers }, - onMissing: ({}, { isResponse }) => getFallback(isResponse), + onMissing: ({}, { isResponse, api }) => getFallback(api, isResponse), ctx, }); From cd10ad3bf86fdc8a658c14c750265a361be4633d Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 21:10:01 +0100 Subject: [PATCH 09/18] Adjusting ZTS test. --- express-zod-api/tests/zts.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/express-zod-api/tests/zts.spec.ts b/express-zod-api/tests/zts.spec.ts index 218aeab24a..21ed739f2c 100644 --- a/express-zod-api/tests/zts.spec.ts +++ b/express-zod-api/tests/zts.spec.ts @@ -1,16 +1,18 @@ import ts from "typescript"; import { z } from "zod"; import { ez } from "../src"; -import { f, printNode } from "../src/typescript-api"; +import { TypescriptAPI } from "../src/typescript-api"; import { zodToTs } from "../src/zts"; import { ZTSContext } from "../src/zts-helpers"; describe("zod-to-ts", () => { + const api = new TypescriptAPI(); const printNodeTest = (node: ts.Node) => - printNode(node, { newLine: ts.NewLineKind.LineFeed }); + api.printNode(node, { newLine: ts.NewLineKind.LineFeed }); const ctx: ZTSContext = { isResponse: false, - makeAlias: vi.fn(() => f.createTypeReferenceNode("SomeType")), + makeAlias: vi.fn(() => api.f.createTypeReferenceNode("SomeType")), + api, }; describe("z.array()", () => { @@ -173,7 +175,7 @@ describe("zod-to-ts", () => { test("should produce the expected results", () => { const node = zodToTs(example, { ctx }); - expect(printNode(node)).toMatchSnapshot(); + expect(api.printNode(node)).toMatchSnapshot(); }); }); From 8cad4b1c39ed6b6195643d5ac273fbe274b95581 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 21:20:20 +0100 Subject: [PATCH 10/18] Rev: changes to identifiers. --- express-zod-api/src/integration-base.ts | 197 +++++++++++------------- 1 file changed, 87 insertions(+), 110 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index f410457061..1a4e3dc09a 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -22,42 +22,44 @@ export abstract class IntegrationBase extends TypescriptAPI { >(); readonly #ids = { - pathType: "Path", - implementationType: "Implementation", - keyParameter: "key", - pathParameter: "path", - paramsArgument: "params", - ctxArgument: "ctx", - methodParameter: "method", - requestParameter: "request", - eventParameter: "event", - dataParameter: "data", - handlerParameter: "handler", - msgParameter: "msg", - parseRequestFn: "parseRequest", - substituteFn: "substitute", - provideMethod: "provide", - onMethod: "on", - implementationArgument: "implementation", - hasBodyConst: "hasBody", - undefinedValue: "undefined", - responseConst: "response", - restConst: "rest", - searchParamsConst: "searchParams", - defaultImplementationConst: "defaultImplementation", - clientConst: "client", - contentTypeConst: "contentType", - isJsonConst: "isJSON", - sourceProp: "source", - } satisfies Record; + pathType: this.f.createIdentifier("Path"), + implementationType: this.f.createIdentifier("Implementation"), + keyParameter: this.f.createIdentifier("key"), + pathParameter: this.f.createIdentifier("path"), + paramsArgument: this.f.createIdentifier("params"), + ctxArgument: this.f.createIdentifier("ctx"), + methodParameter: this.f.createIdentifier("method"), + requestParameter: this.f.createIdentifier("request"), + eventParameter: this.f.createIdentifier("event"), + dataParameter: this.f.createIdentifier("data"), + handlerParameter: this.f.createIdentifier("handler"), + msgParameter: this.f.createIdentifier("msg"), + parseRequestFn: this.f.createIdentifier("parseRequest"), + substituteFn: this.f.createIdentifier("substitute"), + provideMethod: this.f.createIdentifier("provide"), + onMethod: this.f.createIdentifier("on"), + implementationArgument: this.f.createIdentifier("implementation"), + hasBodyConst: this.f.createIdentifier("hasBody"), + undefinedValue: this.f.createIdentifier("undefined"), + responseConst: this.f.createIdentifier("response"), + restConst: this.f.createIdentifier("rest"), + searchParamsConst: this.f.createIdentifier("searchParams"), + defaultImplementationConst: this.f.createIdentifier( + "defaultImplementation", + ), + clientConst: this.f.createIdentifier("client"), + contentTypeConst: this.f.createIdentifier("contentType"), + isJsonConst: this.f.createIdentifier("isJSON"), + sourceProp: this.f.createIdentifier("source"), + } satisfies Record; /** @internal */ - protected interfaces: Record = { - input: "Input", - positive: "PositiveResponse", - negative: "NegativeResponse", - encoded: "EncodedResponse", - response: "Response", + protected interfaces: Record = { + input: this.f.createIdentifier("Input"), + positive: this.f.createIdentifier("PositiveResponse"), + negative: this.f.createIdentifier("NegativeResponse"), + encoded: this.f.createIdentifier("EncodedResponse"), + response: this.f.createIdentifier("Response"), }; /** @@ -148,10 +150,10 @@ export abstract class IntegrationBase extends TypescriptAPI { this.#ids.implementationType, this.makeFnType( { - [this.#ids.methodParameter]: this.methodType.name, - [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument]: this.makeRecordStringAny(), - [this.#ids.ctxArgument]: { optional: true, type: "T" }, + [this.#ids.methodParameter.text]: this.methodType.name, + [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument.text]: this.makeRecordStringAny(), + [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, }, this.makePromise(this.ts.SyntaxKind.AnyKeyword), ), @@ -169,7 +171,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.makeConst( this.#ids.parseRequestFn, this.makeArrowFn( - { [this.#ids.requestParameter]: this.ts.SyntaxKind.StringKeyword }, + { [this.#ids.requestParameter.text]: this.ts.SyntaxKind.StringKeyword }, this.f.createAsExpression( this.makeCall(this.#ids.requestParameter, propOf("split"))( this.f.createRegularExpressionLiteral("/ (.+)/"), // split once @@ -192,16 +194,14 @@ export abstract class IntegrationBase extends TypescriptAPI { this.#ids.substituteFn, this.makeArrowFn( { - [this.#ids.pathParameter]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument]: this.makeRecordStringAny(), + [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument.text]: this.makeRecordStringAny(), }, this.f.createBlock([ this.makeConst( this.#ids.restConst, this.f.createObjectLiteralExpression([ - this.f.createSpreadAssignment( - this.ts.factory.createIdentifier(this.#ids.paramsArgument), - ), + this.f.createSpreadAssignment(this.#ids.paramsArgument), ]), ), this.f.createForInStatement( @@ -209,40 +209,30 @@ export abstract class IntegrationBase extends TypescriptAPI { [this.f.createVariableDeclaration(this.#ids.keyParameter)], this.ts.NodeFlags.Const, ), - this.ts.factory.createIdentifier(this.#ids.paramsArgument), + this.#ids.paramsArgument, this.f.createBlock([ this.makeAssignment( - this.ts.factory.createIdentifier(this.#ids.pathParameter), + this.#ids.pathParameter, this.makeCall( this.#ids.pathParameter, propOf("replace"), )( - this.makeTemplate(":", [ - this.ts.factory.createIdentifier(this.#ids.keyParameter), - ]), // `:${key}` + this.makeTemplate(":", [this.#ids.keyParameter]), // `:${key}` this.makeArrowFn( [], this.f.createBlock([ this.f.createExpressionStatement( this.f.createDeleteExpression( this.f.createElementAccessExpression( - this.ts.factory.createIdentifier( - this.#ids.restConst, - ), - this.ts.factory.createIdentifier( - this.#ids.keyParameter, - ), + this.#ids.restConst, + this.#ids.keyParameter, ), ), ), this.f.createReturnStatement( this.f.createElementAccessExpression( - this.ts.factory.createIdentifier( - this.#ids.paramsArgument, - ), - this.ts.factory.createIdentifier( - this.#ids.keyParameter, - ), + this.#ids.paramsArgument, + this.#ids.keyParameter, ), ), ]), @@ -254,8 +244,8 @@ export abstract class IntegrationBase extends TypescriptAPI { this.f.createReturnStatement( this.f.createAsExpression( this.f.createArrayLiteralExpression([ - this.ts.factory.createIdentifier(this.#ids.pathParameter), - this.ts.factory.createIdentifier(this.#ids.restConst), + this.#ids.pathParameter, + this.#ids.restConst, ]), this.ensureTypeNode("const"), ), @@ -267,37 +257,35 @@ export abstract class IntegrationBase extends TypescriptAPI { // public provide(request: K, params: Input[K]): Promise {} #makeProvider = () => this.makePublicMethod( - this.ts.factory.createIdentifier(this.#ids.provideMethod), + this.#ids.provideMethod, this.makeParams({ - [this.#ids.requestParameter]: "K", - [this.#ids.paramsArgument]: this.makeIndexed( + [this.#ids.requestParameter.text]: "K", + [this.#ids.paramsArgument.text]: this.makeIndexed( this.interfaces.input, "K", ), - [this.#ids.ctxArgument]: { optional: true, type: "T" }, + [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, }), [ this.makeConst( // const [method, path] = this.parseRequest(request); this.makeDeconstruction( - this.ts.factory.createIdentifier(this.#ids.methodParameter), - this.ts.factory.createIdentifier(this.#ids.pathParameter), - ), - this.makeCall(this.#ids.parseRequestFn)( - this.ts.factory.createIdentifier(this.#ids.requestParameter), + this.#ids.methodParameter, + this.#ids.pathParameter, ), + this.makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter), ), // return this.implementation(___) this.f.createReturnStatement( this.makeCall(this.f.createThis(), this.#ids.implementationArgument)( - this.ts.factory.createIdentifier(this.#ids.methodParameter), + this.#ids.methodParameter, this.f.createSpreadElement( this.makeCall(this.#ids.substituteFn)( - this.ts.factory.createIdentifier(this.#ids.pathParameter), - this.ts.factory.createIdentifier(this.#ids.paramsArgument), + this.#ids.pathParameter, + this.#ids.paramsArgument, ), ), - this.ts.factory.createIdentifier(this.#ids.ctxArgument), + this.#ids.ctxArgument, ), ), ], @@ -322,9 +310,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.makeParam(this.#ids.implementationArgument, { type: this.ensureTypeNode(this.#ids.implementationType, ["T"]), mod: this.accessModifiers.protectedReadonly, - init: this.ts.factory.createIdentifier( - this.#ids.defaultImplementationConst, - ), + init: this.#ids.defaultImplementationConst, }), ]), this.#makeProvider(), @@ -342,8 +328,8 @@ export abstract class IntegrationBase extends TypescriptAPI { URL.name, this.makeTemplate( "", - [this.ts.factory.createIdentifier(this.#ids.pathParameter)], - [this.ts.factory.createIdentifier(this.#ids.searchParamsConst)], + [this.#ids.pathParameter], + [this.#ids.searchParamsConst], ), this.literally(this.serverUrl), ); @@ -363,14 +349,14 @@ export abstract class IntegrationBase extends TypescriptAPI { const headersProperty = this.f.createPropertyAssignment( propOf("headers"), this.makeTernary( - this.ts.factory.createIdentifier(this.#ids.hasBodyConst), + this.#ids.hasBodyConst, this.f.createObjectLiteralExpression([ this.f.createPropertyAssignment( this.literally("Content-Type"), this.literally(contentTypes.json), ), ]), - this.ts.factory.createIdentifier(this.#ids.undefinedValue), + this.#ids.undefinedValue, ), ); @@ -378,12 +364,12 @@ export abstract class IntegrationBase extends TypescriptAPI { const bodyProperty = this.f.createPropertyAssignment( propOf("body"), this.makeTernary( - this.ts.factory.createIdentifier(this.#ids.hasBodyConst), + this.#ids.hasBodyConst, this.makeCall( JSON[Symbol.toStringTag], propOf("stringify"), - )(this.ts.factory.createIdentifier(this.#ids.paramsArgument)), - this.ts.factory.createIdentifier(this.#ids.undefinedValue), + )(this.#ids.paramsArgument), + this.#ids.undefinedValue, ), ); @@ -413,7 +399,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.literally("delete" satisfies ClientMethod), ]), propOf("includes"), - )(this.ts.factory.createIdentifier(this.#ids.methodParameter)), + )(this.#ids.methodParameter), ), ); @@ -421,11 +407,9 @@ export abstract class IntegrationBase extends TypescriptAPI { const searchParamsStatement = this.makeConst( this.#ids.searchParamsConst, this.makeTernary( - this.ts.factory.createIdentifier(this.#ids.hasBodyConst), + this.#ids.hasBodyConst, this.literally(""), - this.#makeSearchParams( - this.ts.factory.createIdentifier(this.#ids.paramsArgument), - ), + this.#makeSearchParams(this.#ids.paramsArgument), ), ); @@ -443,7 +427,7 @@ export abstract class IntegrationBase extends TypescriptAPI { const noBodyStatement = this.f.createIfStatement( this.f.createPrefixUnaryExpression( this.ts.SyntaxKind.ExclamationToken, - this.ts.factory.createIdentifier(this.#ids.contentTypeConst), + this.#ids.contentTypeConst, ), this.f.createReturnStatement(), ); @@ -462,7 +446,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.makeCall( this.#ids.responseConst, this.makeTernary( - this.ts.factory.createIdentifier(this.#ids.isJsonConst), + this.#ids.isJsonConst, this.literally(propOf("json")), this.literally(propOf("text")), ), @@ -500,25 +484,20 @@ export abstract class IntegrationBase extends TypescriptAPI { }), [ this.makeConst( - this.makeDeconstruction( - this.ts.factory.createIdentifier(this.#ids.pathParameter), - this.ts.factory.createIdentifier(this.#ids.restConst), - ), + this.makeDeconstruction(this.#ids.pathParameter, this.#ids.restConst), this.makeCall(this.#ids.substituteFn)( this.f.createElementAccessExpression( this.makeCall(this.#ids.parseRequestFn)( - this.ts.factory.createIdentifier(this.#ids.requestParameter), + this.#ids.requestParameter, ), this.literally(1), ), - this.ts.factory.createIdentifier(this.#ids.paramsArgument), + this.#ids.paramsArgument, ), ), this.makeConst( this.#ids.searchParamsConst, - this.#makeSearchParams( - this.ts.factory.createIdentifier(this.#ids.restConst), - ), + this.#makeSearchParams(this.#ids.restConst), ), this.makeAssignment( this.f.createPropertyAccessExpression( @@ -537,12 +516,12 @@ export abstract class IntegrationBase extends TypescriptAPI { #makeOnMethod = () => this.makePublicMethod( - this.ts.factory.createIdentifier(this.#ids.onMethod), + this.#ids.onMethod, this.makeParams({ - [this.#ids.eventParameter]: "E", - [this.#ids.handlerParameter]: this.makeFnType( + [this.#ids.eventParameter.text]: "E", + [this.#ids.handlerParameter.text]: this.makeFnType( { - [this.#ids.dataParameter]: this.makeIndexed( + [this.#ids.dataParameter.text]: this.makeIndexed( this.makeExtract( "R", this.makeOneLine(this.#makeEventNarrow("E")), @@ -560,7 +539,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.#ids.sourceProp, propOf("addEventListener"), )( - this.ts.factory.createIdentifier(this.#ids.eventParameter), + this.#ids.eventParameter, this.makeArrowFn( [this.#ids.msgParameter], this.makeCall(this.#ids.handlerParameter)( @@ -571,9 +550,7 @@ export abstract class IntegrationBase extends TypescriptAPI { this.f.createPropertyAccessExpression( this.f.createParenthesizedExpression( this.f.createAsExpression( - this.ts.factory.createIdentifier( - this.#ids.msgParameter, - ), + this.#ids.msgParameter, this.ensureTypeNode(MessageEvent.name), ), ), From 0abbd6362052503e280e12f75137dd05382a7137 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 16 Dec 2025 21:32:54 +0100 Subject: [PATCH 11/18] REF: marking all public methods and props of TypescriptAPI as internal. --- express-zod-api/src/typescript-api.ts | 48 +++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index 21d81ded7e..fd08dc6a42 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -13,10 +13,15 @@ type TypeParams = | Partial>; export class TypescriptAPI { + /** @internal */ public ts: typeof ts; + /** @internal */ public f: typeof ts.factory; + /** @internal */ public exportModifier: ts.ModifierToken[]; + /** @internal */ public asyncModifier: ts.ModifierToken[]; + /** @internal */ public accessModifiers: Record<"public" | "protectedReadonly", ts.Modifier[]>; #primitives: ts.KeywordTypeSyntaxKind[]; #safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; @@ -52,6 +57,7 @@ export class TypescriptAPI { ]; } + /** @internal */ public addJsDoc = (node: T, text: string) => this.ts.addSyntheticLeadingComment( node, @@ -60,6 +66,7 @@ export class TypescriptAPI { true, ); + /** @internal */ public printNode = (node: ts.Node, printerOptions?: ts.PrinterOptions) => { const sourceFile = this.ts.createSourceFile( "print.ts", @@ -72,11 +79,13 @@ export class TypescriptAPI { return printer.printNode(this.ts.EmitHint.Unspecified, node, sourceFile); }; + /** @internal */ public makePropertyIdentifier = (name: string | number) => typeof name === "string" && this.#safePropRegex.test(name) ? this.f.createIdentifier(name) : this.literally(name); + /** @internal */ public makeTemplate = ( head: string, ...rest: ([ts.Expression] | [ts.Expression, string])[] @@ -93,6 +102,7 @@ export class TypescriptAPI { ), ); + /** @internal */ public makeParam = ( name: string | ts.Identifier, { @@ -118,6 +128,7 @@ export class TypescriptAPI { init, ); + /** @internal */ public makeParams = ( params: Partial< Record[1]> @@ -134,6 +145,7 @@ export class TypescriptAPI { ), ); + /** @internal */ public makePublicConstructor = ( params: ts.ParameterDeclaration[], statements: ts.Statement[] = [], @@ -144,6 +156,7 @@ export class TypescriptAPI { this.f.createBlock(statements), ); + /** @internal */ public ensureTypeNode = ( subject: Typeable, args?: Typeable[], // only for string and id @@ -157,14 +170,20 @@ export class TypescriptAPI { ) : subject; - // Record + /** + * @internal + * @example Record + * */ public makeRecordStringAny = () => this.ensureTypeNode("Record", [ this.ts.SyntaxKind.StringKeyword, this.ts.SyntaxKind.AnyKeyword, ]); - /** ensures distinct union (unique primitives) */ + /** + * @internal + * ensures distinct union (unique primitives) + * */ public makeUnion = (entries: ts.TypeNode[]) => { const nodes = new Map< ts.TypeNode | ts.KeywordTypeSyntaxKind, @@ -175,6 +194,7 @@ export class TypescriptAPI { return this.f.createUnionTypeNode(Array.from(nodes.values())); }; + /** @internal */ public makeInterfaceProp = ( name: string | number, value: Typeable, @@ -205,9 +225,11 @@ export class TypescriptAPI { return jsdoc.length ? this.addJsDoc(node, jsdoc.join(" ")) : node; }; + /** @internal */ public makeOneLine = (subject: ts.TypeNode) => this.ts.setEmitFlags(subject, this.ts.EmitFlags.SingleLine); + /** @internal */ public makeDeconstruction = ( ...names: ts.Identifier[] ): ts.ArrayBindingPattern => @@ -217,6 +239,7 @@ export class TypescriptAPI { ), ); + /** @internal */ public makeConst = ( name: string | ts.Identifier | ts.ArrayBindingPattern, value: ts.Expression, @@ -237,6 +260,7 @@ export class TypescriptAPI { ), ); + /** @internal */ public makePublicLiteralType = ( name: ts.Identifier | string, literals: string[], @@ -247,6 +271,7 @@ export class TypescriptAPI { { expose: true }, ); + /** @internal */ public makeType = ( name: ts.Identifier | string, value: ts.TypeNode, @@ -265,6 +290,7 @@ export class TypescriptAPI { return comment ? this.addJsDoc(node, comment) : node; }; + /** @internal */ public makePublicProperty = ( name: string | ts.PropertyName, type: Typeable, @@ -277,6 +303,7 @@ export class TypescriptAPI { undefined, ); + /** @internal */ public makePublicMethod = ( name: ts.Identifier, params: ts.ParameterDeclaration[], @@ -297,6 +324,7 @@ export class TypescriptAPI { this.f.createBlock(statements), ); + /** @internal */ public makePublicClass = ( name: string, statements: ts.ClassElement[], @@ -310,15 +338,18 @@ export class TypescriptAPI { statements, ); + /** @internal */ public makeKeyOf = (subj: Typeable) => this.f.createTypeOperatorNode( this.ts.SyntaxKind.KeyOfKeyword, this.ensureTypeNode(subj), ); + /** @internal */ public makePromise = (subject: Typeable) => this.ensureTypeNode(Promise.name, [subject]); + /** @internal */ public makeInterface = ( name: ts.Identifier | string, props: ts.PropertySignature[], @@ -334,6 +365,7 @@ export class TypescriptAPI { return comment ? this.addJsDoc(node, comment) : node; }; + /** @internal */ public makeTypeParams = ( params: | string[] @@ -355,6 +387,7 @@ export class TypescriptAPI { ); }); + /** @internal */ public makeArrowFn = ( params: | Array[0]> @@ -373,6 +406,7 @@ export class TypescriptAPI { body, ); + /** @internal */ public makeTernary = ( condition: ts.Expression, positive: ts.Expression, @@ -386,6 +420,7 @@ export class TypescriptAPI { negative, ); + /** @internal */ public makeCall = ( first: ts.Expression | string, @@ -404,12 +439,15 @@ export class TypescriptAPI { args, ); + /** @internal */ public makeNew = (cls: string, ...args: ts.Expression[]) => this.f.createNewExpression(this.f.createIdentifier(cls), undefined, args); + /** @internal */ public makeExtract = (base: Typeable, narrow: ts.TypeNode) => this.ensureTypeNode("Extract", [base, narrow]); + /** @internal */ public makeAssignment = (left: ts.Expression, right: ts.Expression) => this.f.createExpressionStatement( this.f.createBinaryExpression( @@ -419,15 +457,18 @@ export class TypescriptAPI { ), ); + /** @internal */ public makeIndexed = (subject: Typeable, index: Typeable) => this.f.createIndexedAccessTypeNode( this.ensureTypeNode(subject), this.ensureTypeNode(index), ); + /** @internal */ public makeMaybeAsync = (subj: Typeable) => this.makeUnion([this.ensureTypeNode(subj), this.makePromise(subj)]); + /** @internal */ public makeFnType = ( params: Parameters[0], returns: Typeable, @@ -439,6 +480,7 @@ export class TypescriptAPI { ); /* eslint-disable prettier/prettier -- shorter and works better this way than overrides */ + /** @internal */ public literally = (subj: T) => ( typeof subj === "number" ? this.f.createNumericLiteral(subj) : typeof subj === "bigint" ? this.f.createBigIntLiteral(subj.toString()) @@ -448,9 +490,11 @@ export class TypescriptAPI { : T extends boolean ? ts.BooleanLiteral : ts.NullLiteral; /* eslint-enable prettier/prettier */ + /** @internal */ public makeLiteralType = (subj: Parameters[0]) => this.f.createLiteralTypeNode(this.literally(subj)); + /** @internal */ public isPrimitive = (node: ts.TypeNode): node is ts.KeywordTypeNode => (this.#primitives as ts.SyntaxKind[]).includes(node.kind); } From b9b925f91311a570109a46bdf878daad5211819e Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Wed, 17 Dec 2025 09:00:55 +0100 Subject: [PATCH 12/18] Changelog: 26.1.0. --- CHANGELOG.md | 8 ++++++++ README.md | 1 + 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c77ed0c243..6ed8c670e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Version 26 +### v26.1.0 + +- Optimization to the memory consumption for your API: + - It has been discovered that static import of `typescript` within the framework consumes memory unnecessarily; + - Importing `typescript` is only necessary to generate an Integration; + - This version avoids static importing, but the solution is temporary in order to avoid breaking changes; + - The issue was found, investigated and reported by [@NicolasMahe](https://github.com/NicolasMahe). + ### v26.0.0 - Supported `http-errors` versions: `^2.0.1`; diff --git a/README.md b/README.md index fb19fd754f..4a7d044953 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas: +[@NicolasMahe](https://github.com/NicolasMahe) [@shadone](https://github.com/shadone) [@squishykid](https://github.com/squishykid) [@jakub-msqt](https://github.com/jakub-msqt) From 25c92ad98962ae51705b13cc815533ad3ade92f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:38:22 +0000 Subject: [PATCH 13/18] express-zod-api version 26.1.0-beta.1 --- express-zod-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/package.json b/express-zod-api/package.json index 2e9da92e40..c4dd92e56b 100644 --- a/express-zod-api/package.json +++ b/express-zod-api/package.json @@ -1,6 +1,6 @@ { "name": "express-zod-api", - "version": "26.0.0", + "version": "26.1.0-beta.1", "description": "A Typescript framework to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.", "license": "MIT", "repository": { From a371e1b793baff5e42a54da6c4913ec620042c36 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 23 Dec 2025 14:33:09 +0100 Subject: [PATCH 14/18] Fix import of typescript in zts, adjusting implementation to use api argument instead. --- express-zod-api/src/zts.ts | 84 +++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts index fce044f70c..dc47637ae9 100644 --- a/express-zod-api/src/zts.ts +++ b/express-zod-api/src/zts.ts @@ -1,5 +1,5 @@ import * as R from "ramda"; -import ts from "typescript"; +import type ts from "typescript"; import { globalRegistry, z } from "zod"; import { ezBufferBrand } from "./buffer-schema"; import { getTransformedType, isSchema } from "./common-helpers"; @@ -12,16 +12,6 @@ import { FirstPartyKind, HandlingRules, walkSchema } from "./schema-walker"; import type { TypescriptAPI } from "./typescript-api"; import { Producer, ZTSContext } from "./zts-helpers"; -const samples = { - [ts.SyntaxKind.AnyKeyword]: "", - [ts.SyntaxKind.BigIntKeyword]: BigInt(0), - [ts.SyntaxKind.BooleanKeyword]: false, - [ts.SyntaxKind.NumberKeyword]: 0, - [ts.SyntaxKind.ObjectKeyword]: {}, - [ts.SyntaxKind.StringKeyword]: "", - [ts.SyntaxKind.UndefinedKeyword]: undefined, -} satisfies Partial>; - const nodePath = { name: R.path([ "name" satisfies keyof ts.TypeElement, @@ -40,7 +30,7 @@ const onLiteral: Producer = ( ) => { const values = def.values.map((entry) => entry === undefined - ? api.ensureTypeNode(ts.SyntaxKind.UndefinedKeyword) + ? api.ensureTypeNode(api.ts.SyntaxKind.UndefinedKeyword) : api.makeLiteralType(entry), ); return values.length === 1 ? values[0] : api.makeUnion(values); @@ -114,9 +104,6 @@ const onSomeUnion: Producer = ( { next, api }, ) => api.makeUnion(def.options.map(next)); -const makeSample = (produced: ts.TypeNode) => - samples?.[produced.kind as keyof typeof samples]; - const onNullable: Producer = ( { _zod: { def } }: z.core.$ZodNullable, { next, api }, @@ -141,7 +128,7 @@ const onRecord: Producer = ( const intersect = R.tryCatch( (api: TypescriptAPI, nodes: ts.TypeNode[]) => { - if (!nodes.every(ts.isTypeLiteralNode)) throw new Error("Not objects"); + if (!nodes.every(api.ts.isTypeLiteralNode)) throw new Error("Not objects"); const members = R.chain(R.prop("members"), nodes); const uniqs = R.uniqWith((...props) => { if (!R.eqBy(nodePath.name, ...props)) return false; @@ -160,9 +147,20 @@ const onIntersection: Producer = ( ) => intersect(api, [def.left, def.right].map(next)); const onPrimitive = - (syntaxKind: ts.KeywordTypeSyntaxKind): Producer => + ( + syntaxKind: + | "AnyKeyword" + | "BigIntKeyword" + | "BooleanKeyword" + | "NeverKeyword" + | "NumberKeyword" + | "StringKeyword" + | "UndefinedKeyword" + | "UnknownKeyword" + | "VoidKeyword", + ): Producer => ({}, { api }) => - api.ensureTypeNode(syntaxKind); + api.ensureTypeNode(api.ts.SyntaxKind[syntaxKind]); const onWrapped: Producer = ( { @@ -178,7 +176,9 @@ const onWrapped: Producer = ( const getFallback = (api: TypescriptAPI, isResponse: boolean) => api.ensureTypeNode( - isResponse ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.AnyKeyword, + isResponse + ? api.ts.SyntaxKind.UnknownKeyword + : api.ts.SyntaxKind.AnyKeyword, ); const onPipeline: Producer = ( @@ -189,16 +189,26 @@ const onPipeline: Producer = ( const opposite = def[isResponse ? "in" : "out"]; if (!isSchema(target, "transform")) return next(target); const opposingType = next(opposite); - const targetType = getTransformedType(target, makeSample(opposingType)); + const samples = { + [api.ts.SyntaxKind.AnyKeyword]: "", + [api.ts.SyntaxKind.BigIntKeyword]: BigInt(0), + [api.ts.SyntaxKind.BooleanKeyword]: false, + [api.ts.SyntaxKind.NumberKeyword]: 0, + [api.ts.SyntaxKind.ObjectKeyword]: {}, + [api.ts.SyntaxKind.StringKeyword]: "", + [api.ts.SyntaxKind.UndefinedKeyword]: undefined, + } satisfies Partial>; + const sample = samples[opposingType.kind as keyof typeof samples]; + const targetType = getTransformedType(target, sample); const resolutions: Partial< Record, ts.KeywordTypeSyntaxKind> > = { - number: ts.SyntaxKind.NumberKeyword, - bigint: ts.SyntaxKind.BigIntKeyword, - boolean: ts.SyntaxKind.BooleanKeyword, - string: ts.SyntaxKind.StringKeyword, - undefined: ts.SyntaxKind.UndefinedKeyword, - object: ts.SyntaxKind.ObjectKeyword, + number: api.ts.SyntaxKind.NumberKeyword, + bigint: api.ts.SyntaxKind.BigIntKeyword, + boolean: api.ts.SyntaxKind.BooleanKeyword, + string: api.ts.SyntaxKind.StringKeyword, + undefined: api.ts.SyntaxKind.UndefinedKeyword, + object: api.ts.SyntaxKind.ObjectKeyword, }; return api.ensureTypeNode( (targetType && resolutions[targetType]) || getFallback(api, isResponse), @@ -222,17 +232,17 @@ const producers: HandlingRules< ZTSContext, FirstPartyKind | ProprietaryBrand > = { - string: onPrimitive(ts.SyntaxKind.StringKeyword), - number: onPrimitive(ts.SyntaxKind.NumberKeyword), - bigint: onPrimitive(ts.SyntaxKind.BigIntKeyword), - boolean: onPrimitive(ts.SyntaxKind.BooleanKeyword), - any: onPrimitive(ts.SyntaxKind.AnyKeyword), - undefined: onPrimitive(ts.SyntaxKind.UndefinedKeyword), - [ezDateInBrand]: onPrimitive(ts.SyntaxKind.StringKeyword), - [ezDateOutBrand]: onPrimitive(ts.SyntaxKind.StringKeyword), - never: onPrimitive(ts.SyntaxKind.NeverKeyword), - void: onPrimitive(ts.SyntaxKind.UndefinedKeyword), - unknown: onPrimitive(ts.SyntaxKind.UnknownKeyword), + string: onPrimitive("StringKeyword"), + number: onPrimitive("NumberKeyword"), + bigint: onPrimitive("BigIntKeyword"), + boolean: onPrimitive("BooleanKeyword"), + any: onPrimitive("AnyKeyword"), + undefined: onPrimitive("UndefinedKeyword"), + [ezDateInBrand]: onPrimitive("StringKeyword"), + [ezDateOutBrand]: onPrimitive("StringKeyword"), + never: onPrimitive("NeverKeyword"), + void: onPrimitive("UndefinedKeyword"), + unknown: onPrimitive("UnknownKeyword"), null: onNull, array: onArray, tuple: onTuple, From 8861ccb885937f9a9e4bf31cb19144cc77322cb3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:46:06 +0000 Subject: [PATCH 15/18] express-zod-api version 26.1.0-beta.2 --- express-zod-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/package.json b/express-zod-api/package.json index c4dd92e56b..bf9eee0303 100644 --- a/express-zod-api/package.json +++ b/express-zod-api/package.json @@ -1,6 +1,6 @@ { "name": "express-zod-api", - "version": "26.1.0-beta.1", + "version": "26.1.0-beta.2", "description": "A Typescript framework to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.", "license": "MIT", "repository": { From f94dd5377ff9ad3f27ac99903cada232dd809cdf Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 25 Dec 2025 13:06:37 +0100 Subject: [PATCH 16/18] Fix: no inheritance from TypescriptAPI and avoid spoiling the whole Integration into the ZTSContext. --- express-zod-api/src/integration-base.ts | 459 +++++++++++++----------- express-zod-api/src/integration.ts | 48 +-- 2 files changed, 265 insertions(+), 242 deletions(-) diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index 1a4e3dc09a..7a96582f77 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -10,7 +10,9 @@ type IOKind = "input" | "response" | ResponseVariant | "encoded"; type SSEShape = ReturnType["shape"]; type Store = Record; -export abstract class IntegrationBase extends TypescriptAPI { +export abstract class IntegrationBase { + /** @internal */ + protected api = new TypescriptAPI(); /** @internal */ protected paths = new Set(); /** @internal */ @@ -22,59 +24,62 @@ export abstract class IntegrationBase extends TypescriptAPI { >(); readonly #ids = { - pathType: this.f.createIdentifier("Path"), - implementationType: this.f.createIdentifier("Implementation"), - keyParameter: this.f.createIdentifier("key"), - pathParameter: this.f.createIdentifier("path"), - paramsArgument: this.f.createIdentifier("params"), - ctxArgument: this.f.createIdentifier("ctx"), - methodParameter: this.f.createIdentifier("method"), - requestParameter: this.f.createIdentifier("request"), - eventParameter: this.f.createIdentifier("event"), - dataParameter: this.f.createIdentifier("data"), - handlerParameter: this.f.createIdentifier("handler"), - msgParameter: this.f.createIdentifier("msg"), - parseRequestFn: this.f.createIdentifier("parseRequest"), - substituteFn: this.f.createIdentifier("substitute"), - provideMethod: this.f.createIdentifier("provide"), - onMethod: this.f.createIdentifier("on"), - implementationArgument: this.f.createIdentifier("implementation"), - hasBodyConst: this.f.createIdentifier("hasBody"), - undefinedValue: this.f.createIdentifier("undefined"), - responseConst: this.f.createIdentifier("response"), - restConst: this.f.createIdentifier("rest"), - searchParamsConst: this.f.createIdentifier("searchParams"), - defaultImplementationConst: this.f.createIdentifier( + pathType: this.api.f.createIdentifier("Path"), + implementationType: this.api.f.createIdentifier("Implementation"), + keyParameter: this.api.f.createIdentifier("key"), + pathParameter: this.api.f.createIdentifier("path"), + paramsArgument: this.api.f.createIdentifier("params"), + ctxArgument: this.api.f.createIdentifier("ctx"), + methodParameter: this.api.f.createIdentifier("method"), + requestParameter: this.api.f.createIdentifier("request"), + eventParameter: this.api.f.createIdentifier("event"), + dataParameter: this.api.f.createIdentifier("data"), + handlerParameter: this.api.f.createIdentifier("handler"), + msgParameter: this.api.f.createIdentifier("msg"), + parseRequestFn: this.api.f.createIdentifier("parseRequest"), + substituteFn: this.api.f.createIdentifier("substitute"), + provideMethod: this.api.f.createIdentifier("provide"), + onMethod: this.api.f.createIdentifier("on"), + implementationArgument: this.api.f.createIdentifier("implementation"), + hasBodyConst: this.api.f.createIdentifier("hasBody"), + undefinedValue: this.api.f.createIdentifier("undefined"), + responseConst: this.api.f.createIdentifier("response"), + restConst: this.api.f.createIdentifier("rest"), + searchParamsConst: this.api.f.createIdentifier("searchParams"), + defaultImplementationConst: this.api.f.createIdentifier( "defaultImplementation", ), - clientConst: this.f.createIdentifier("client"), - contentTypeConst: this.f.createIdentifier("contentType"), - isJsonConst: this.f.createIdentifier("isJSON"), - sourceProp: this.f.createIdentifier("source"), + clientConst: this.api.f.createIdentifier("client"), + contentTypeConst: this.api.f.createIdentifier("contentType"), + isJsonConst: this.api.f.createIdentifier("isJSON"), + sourceProp: this.api.f.createIdentifier("source"), } satisfies Record; /** @internal */ protected interfaces: Record = { - input: this.f.createIdentifier("Input"), - positive: this.f.createIdentifier("PositiveResponse"), - negative: this.f.createIdentifier("NegativeResponse"), - encoded: this.f.createIdentifier("EncodedResponse"), - response: this.f.createIdentifier("Response"), + input: this.api.f.createIdentifier("Input"), + positive: this.api.f.createIdentifier("PositiveResponse"), + negative: this.api.f.createIdentifier("NegativeResponse"), + encoded: this.api.f.createIdentifier("EncodedResponse"), + response: this.api.f.createIdentifier("Response"), }; /** * @example export type Method = "get" | "post" | "put" | "delete" | "patch" | "head"; * @internal * */ - protected methodType = this.makePublicLiteralType("Method", clientMethods); + protected methodType = this.api.makePublicLiteralType( + "Method", + clientMethods, + ); /** * @example type SomeOf = T[keyof T]; * @internal * */ - protected someOfType = this.makeType( + protected someOfType = this.api.makeType( "SomeOf", - this.makeIndexed("T", this.makeKeyOf("T")), + this.api.makeIndexed("T", this.api.makeKeyOf("T")), { params: ["T"] }, ); @@ -82,29 +87,27 @@ export abstract class IntegrationBase extends TypescriptAPI { * @example export type Request = keyof Input; * @internal * */ - protected requestType = this.makeType( + protected requestType = this.api.makeType( "Request", - this.makeKeyOf(this.interfaces.input), + this.api.makeKeyOf(this.interfaces.input), { expose: true }, ); - protected constructor(private readonly serverUrl: string) { - super(); - } + protected constructor(private readonly serverUrl: string) {} /** * @example SomeOf<_> * @internal **/ protected someOf = ({ name }: ts.TypeAliasDeclaration) => - this.ensureTypeNode(this.someOfType.name, [name]); + this.api.ensureTypeNode(this.someOfType.name, [name]); /** * @example export type Path = "/v1/user/retrieve" | ___; * @internal * */ protected makePathType = () => - this.makePublicLiteralType(this.#ids.pathType, Array.from(this.paths)); + this.api.makePublicLiteralType(this.#ids.pathType, Array.from(this.paths)); /** * @example export interface Input { "get /v1/user/retrieve": GetV1UserRetrieveInput; } @@ -112,10 +115,10 @@ export abstract class IntegrationBase extends TypescriptAPI { * */ protected makePublicInterfaces = () => (Object.keys(this.interfaces) as IOKind[]).map((kind) => - this.makeInterface( + this.api.makeInterface( this.interfaces[kind], Array.from(this.registry).map(([request, { store, isDeprecated }]) => - this.makeInterfaceProp(request, store[kind], { isDeprecated }), + this.api.makeInterfaceProp(request, store[kind], { isDeprecated }), ), { expose: true }, ), @@ -126,14 +129,14 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeEndpointTags = () => - this.makeConst( + this.api.makeConst( "endpointTags", - this.f.createObjectLiteralExpression( + this.api.f.createObjectLiteralExpression( Array.from(this.tags).map(([request, tags]) => - this.f.createPropertyAssignment( - this.makePropertyIdentifier(request), - this.f.createArrayLiteralExpression( - R.map(this.literally.bind(this), tags), + this.api.f.createPropertyAssignment( + this.api.makePropertyIdentifier(request), + this.api.f.createArrayLiteralExpression( + R.map(this.api.literally.bind(this.api), tags), ), ), ), @@ -146,20 +149,20 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeImplementationType = () => - this.makeType( + this.api.makeType( this.#ids.implementationType, - this.makeFnType( + this.api.makeFnType( { [this.#ids.methodParameter.text]: this.methodType.name, - [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument.text]: this.makeRecordStringAny(), + [this.#ids.pathParameter.text]: this.api.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument.text]: this.api.makeRecordStringAny(), [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, }, - this.makePromise(this.ts.SyntaxKind.AnyKeyword), + this.api.makePromise(this.api.ts.SyntaxKind.AnyKeyword), ), { expose: true, - params: { T: { init: this.ts.SyntaxKind.UnknownKeyword } }, + params: { T: { init: this.api.ts.SyntaxKind.UnknownKeyword } }, }, ); @@ -168,18 +171,24 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeParseRequestFn = () => - this.makeConst( + this.api.makeConst( this.#ids.parseRequestFn, - this.makeArrowFn( - { [this.#ids.requestParameter.text]: this.ts.SyntaxKind.StringKeyword }, - this.f.createAsExpression( - this.makeCall(this.#ids.requestParameter, propOf("split"))( - this.f.createRegularExpressionLiteral("/ (.+)/"), // split once - this.literally(2), // excludes third empty element + this.api.makeArrowFn( + { + [this.#ids.requestParameter.text]: + this.api.ts.SyntaxKind.StringKeyword, + }, + this.api.f.createAsExpression( + this.api.makeCall( + this.#ids.requestParameter, + propOf("split"), + )( + this.api.f.createRegularExpressionLiteral("/ (.+)/"), // split once + this.api.literally(2), // excludes third empty element ), - this.f.createTupleTypeNode([ - this.ensureTypeNode(this.methodType.name), - this.ensureTypeNode(this.#ids.pathType), + this.api.f.createTupleTypeNode([ + this.api.ensureTypeNode(this.methodType.name), + this.api.ensureTypeNode(this.#ids.pathType), ]), ), ), @@ -190,47 +199,47 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeSubstituteFn = () => - this.makeConst( + this.api.makeConst( this.#ids.substituteFn, - this.makeArrowFn( + this.api.makeArrowFn( { - [this.#ids.pathParameter.text]: this.ts.SyntaxKind.StringKeyword, - [this.#ids.paramsArgument.text]: this.makeRecordStringAny(), + [this.#ids.pathParameter.text]: this.api.ts.SyntaxKind.StringKeyword, + [this.#ids.paramsArgument.text]: this.api.makeRecordStringAny(), }, - this.f.createBlock([ - this.makeConst( + this.api.f.createBlock([ + this.api.makeConst( this.#ids.restConst, - this.f.createObjectLiteralExpression([ - this.f.createSpreadAssignment(this.#ids.paramsArgument), + this.api.f.createObjectLiteralExpression([ + this.api.f.createSpreadAssignment(this.#ids.paramsArgument), ]), ), - this.f.createForInStatement( - this.f.createVariableDeclarationList( - [this.f.createVariableDeclaration(this.#ids.keyParameter)], - this.ts.NodeFlags.Const, + this.api.f.createForInStatement( + this.api.f.createVariableDeclarationList( + [this.api.f.createVariableDeclaration(this.#ids.keyParameter)], + this.api.ts.NodeFlags.Const, ), this.#ids.paramsArgument, - this.f.createBlock([ - this.makeAssignment( + this.api.f.createBlock([ + this.api.makeAssignment( this.#ids.pathParameter, - this.makeCall( + this.api.makeCall( this.#ids.pathParameter, propOf("replace"), )( - this.makeTemplate(":", [this.#ids.keyParameter]), // `:${key}` - this.makeArrowFn( + this.api.makeTemplate(":", [this.#ids.keyParameter]), // `:${key}` + this.api.makeArrowFn( [], - this.f.createBlock([ - this.f.createExpressionStatement( - this.f.createDeleteExpression( - this.f.createElementAccessExpression( + this.api.f.createBlock([ + this.api.f.createExpressionStatement( + this.api.f.createDeleteExpression( + this.api.f.createElementAccessExpression( this.#ids.restConst, this.#ids.keyParameter, ), ), ), - this.f.createReturnStatement( - this.f.createElementAccessExpression( + this.api.f.createReturnStatement( + this.api.f.createElementAccessExpression( this.#ids.paramsArgument, this.#ids.keyParameter, ), @@ -241,13 +250,13 @@ export abstract class IntegrationBase extends TypescriptAPI { ), ]), ), - this.f.createReturnStatement( - this.f.createAsExpression( - this.f.createArrayLiteralExpression([ + this.api.f.createReturnStatement( + this.api.f.createAsExpression( + this.api.f.createArrayLiteralExpression([ this.#ids.pathParameter, this.#ids.restConst, ]), - this.ensureTypeNode("const"), + this.api.ensureTypeNode("const"), ), ), ]), @@ -256,31 +265,36 @@ export abstract class IntegrationBase extends TypescriptAPI { // public provide(request: K, params: Input[K]): Promise {} #makeProvider = () => - this.makePublicMethod( + this.api.makePublicMethod( this.#ids.provideMethod, - this.makeParams({ + this.api.makeParams({ [this.#ids.requestParameter.text]: "K", - [this.#ids.paramsArgument.text]: this.makeIndexed( + [this.#ids.paramsArgument.text]: this.api.makeIndexed( this.interfaces.input, "K", ), [this.#ids.ctxArgument.text]: { optional: true, type: "T" }, }), [ - this.makeConst( + this.api.makeConst( // const [method, path] = this.parseRequest(request); - this.makeDeconstruction( + this.api.makeDeconstruction( this.#ids.methodParameter, this.#ids.pathParameter, ), - this.makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter), + this.api.makeCall(this.#ids.parseRequestFn)( + this.#ids.requestParameter, + ), ), // return this.implementation(___) - this.f.createReturnStatement( - this.makeCall(this.f.createThis(), this.#ids.implementationArgument)( + this.api.f.createReturnStatement( + this.api.makeCall( + this.api.f.createThis(), + this.#ids.implementationArgument, + )( this.#ids.methodParameter, - this.f.createSpreadElement( - this.makeCall(this.#ids.substituteFn)( + this.api.f.createSpreadElement( + this.api.makeCall(this.#ids.substituteFn)( this.#ids.pathParameter, this.#ids.paramsArgument, ), @@ -291,8 +305,8 @@ export abstract class IntegrationBase extends TypescriptAPI { ], { typeParams: { K: this.requestType.name }, - returns: this.makePromise( - this.makeIndexed(this.interfaces.response, "K"), + returns: this.api.makePromise( + this.api.makeIndexed(this.interfaces.response, "K"), ), }, ); @@ -302,14 +316,14 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeClientClass = (name: string) => - this.makePublicClass( + this.api.makePublicClass( name, [ // public constructor(protected readonly implementation: Implementation = defaultImplementation) {} - this.makePublicConstructor([ - this.makeParam(this.#ids.implementationArgument, { - type: this.ensureTypeNode(this.#ids.implementationType, ["T"]), - mod: this.accessModifiers.protectedReadonly, + this.api.makePublicConstructor([ + this.api.makeParam(this.#ids.implementationArgument, { + type: this.api.ensureTypeNode(this.#ids.implementationType, ["T"]), + mod: this.api.accessModifiers.protectedReadonly, init: this.#ids.defaultImplementationConst, }), ]), @@ -320,18 +334,18 @@ export abstract class IntegrationBase extends TypescriptAPI { // `?${new URLSearchParams(____)}` #makeSearchParams = (from: ts.Expression) => - this.makeTemplate("?", [this.makeNew(URLSearchParams.name, from)]); + this.api.makeTemplate("?", [this.api.makeNew(URLSearchParams.name, from)]); // new URL(`${path}${searchParams}`, "http:____") #makeFetchURL = () => - this.makeNew( + this.api.makeNew( URL.name, - this.makeTemplate( + this.api.makeTemplate( "", [this.#ids.pathParameter], [this.#ids.searchParamsConst], ), - this.literally(this.serverUrl), + this.api.literally(this.serverUrl), ); /** @@ -340,20 +354,23 @@ export abstract class IntegrationBase extends TypescriptAPI { * */ protected makeDefaultImplementation = () => { // method: method.toUpperCase() - const methodProperty = this.f.createPropertyAssignment( + const methodProperty = this.api.f.createPropertyAssignment( propOf("method"), - this.makeCall(this.#ids.methodParameter, propOf("toUpperCase"))(), + this.api.makeCall( + this.#ids.methodParameter, + propOf("toUpperCase"), + )(), ); // headers: hasBody ? { "Content-Type": "application/json" } : undefined - const headersProperty = this.f.createPropertyAssignment( + const headersProperty = this.api.f.createPropertyAssignment( propOf("headers"), - this.makeTernary( + this.api.makeTernary( this.#ids.hasBodyConst, - this.f.createObjectLiteralExpression([ - this.f.createPropertyAssignment( - this.literally("Content-Type"), - this.literally(contentTypes.json), + this.api.f.createObjectLiteralExpression([ + this.api.f.createPropertyAssignment( + this.api.literally("Content-Type"), + this.api.literally(contentTypes.json), ), ]), this.#ids.undefinedValue, @@ -361,11 +378,11 @@ export abstract class IntegrationBase extends TypescriptAPI { ); // body: hasBody ? JSON.stringify(params) : undefined - const bodyProperty = this.f.createPropertyAssignment( + const bodyProperty = this.api.f.createPropertyAssignment( propOf("body"), - this.makeTernary( + this.api.makeTernary( this.#ids.hasBodyConst, - this.makeCall( + this.api.makeCall( JSON[Symbol.toStringTag], propOf("stringify"), )(this.#ids.paramsArgument), @@ -374,12 +391,12 @@ export abstract class IntegrationBase extends TypescriptAPI { ); // const response = await fetch(new URL(`${path}${searchParams}`, "https://example.com"), { ___ }); - const responseStatement = this.makeConst( + const responseStatement = this.api.makeConst( this.#ids.responseConst, - this.f.createAwaitExpression( - this.makeCall(fetch.name)( + this.api.f.createAwaitExpression( + this.api.makeCall(fetch.name)( this.#makeFetchURL(), - this.f.createObjectLiteralExpression([ + this.api.f.createObjectLiteralExpression([ methodProperty, headersProperty, bodyProperty, @@ -389,14 +406,14 @@ export abstract class IntegrationBase extends TypescriptAPI { ); // const hasBody = !["get", "delete"].includes(method); - const hasBodyStatement = this.makeConst( + const hasBodyStatement = this.api.makeConst( this.#ids.hasBodyConst, - this.f.createLogicalNot( - this.makeCall( - this.f.createArrayLiteralExpression([ - this.literally("get" satisfies ClientMethod), - this.literally("head" satisfies ClientMethod), - this.literally("delete" satisfies ClientMethod), + this.api.f.createLogicalNot( + this.api.makeCall( + this.api.f.createArrayLiteralExpression([ + this.api.literally("get" satisfies ClientMethod), + this.api.literally("head" satisfies ClientMethod), + this.api.literally("delete" satisfies ClientMethod), ]), propOf("includes"), )(this.#ids.methodParameter), @@ -404,64 +421,64 @@ export abstract class IntegrationBase extends TypescriptAPI { ); // const searchParams = hasBody ? "" : ___; - const searchParamsStatement = this.makeConst( + const searchParamsStatement = this.api.makeConst( this.#ids.searchParamsConst, - this.makeTernary( + this.api.makeTernary( this.#ids.hasBodyConst, - this.literally(""), + this.api.literally(""), this.#makeSearchParams(this.#ids.paramsArgument), ), ); // const contentType = response.headers.get("content-type"); - const contentTypeStatement = this.makeConst( + const contentTypeStatement = this.api.makeConst( this.#ids.contentTypeConst, - this.makeCall( + this.api.makeCall( this.#ids.responseConst, propOf("headers"), propOf("get"), - )(this.literally("content-type")), + )(this.api.literally("content-type")), ); // if (!contentType) return; - const noBodyStatement = this.f.createIfStatement( - this.f.createPrefixUnaryExpression( - this.ts.SyntaxKind.ExclamationToken, + const noBodyStatement = this.api.f.createIfStatement( + this.api.f.createPrefixUnaryExpression( + this.api.ts.SyntaxKind.ExclamationToken, this.#ids.contentTypeConst, ), - this.f.createReturnStatement(), + this.api.f.createReturnStatement(), ); // const isJSON = contentType.startsWith("application/json"); - const isJsonConst = this.makeConst( + const isJsonConst = this.api.makeConst( this.#ids.isJsonConst, - this.makeCall( + this.api.makeCall( this.#ids.contentTypeConst, propOf("startsWith"), - )(this.literally(contentTypes.json)), + )(this.api.literally(contentTypes.json)), ); // return response[isJSON ? "json" : "text"](); - const returnStatement = this.f.createReturnStatement( - this.makeCall( + const returnStatement = this.api.f.createReturnStatement( + this.api.makeCall( this.#ids.responseConst, - this.makeTernary( + this.api.makeTernary( this.#ids.isJsonConst, - this.literally(propOf("json")), - this.literally(propOf("text")), + this.api.literally(propOf("json")), + this.api.literally(propOf("text")), ), )(), ); - return this.makeConst( + return this.api.makeConst( this.#ids.defaultImplementationConst, - this.makeArrowFn( + this.api.makeArrowFn( [ this.#ids.methodParameter, this.#ids.pathParameter, this.#ids.paramsArgument, ], - this.f.createBlock([ + this.api.f.createBlock([ hasBodyStatement, searchParamsStatement, responseStatement, @@ -477,81 +494,84 @@ export abstract class IntegrationBase extends TypescriptAPI { }; #makeSubscriptionConstructor = () => - this.makePublicConstructor( - this.makeParams({ + this.api.makePublicConstructor( + this.api.makeParams({ request: "K", - params: this.makeIndexed(this.interfaces.input, "K"), + params: this.api.makeIndexed(this.interfaces.input, "K"), }), [ - this.makeConst( - this.makeDeconstruction(this.#ids.pathParameter, this.#ids.restConst), - this.makeCall(this.#ids.substituteFn)( - this.f.createElementAccessExpression( - this.makeCall(this.#ids.parseRequestFn)( + this.api.makeConst( + this.api.makeDeconstruction( + this.#ids.pathParameter, + this.#ids.restConst, + ), + this.api.makeCall(this.#ids.substituteFn)( + this.api.f.createElementAccessExpression( + this.api.makeCall(this.#ids.parseRequestFn)( this.#ids.requestParameter, ), - this.literally(1), + this.api.literally(1), ), this.#ids.paramsArgument, ), ), - this.makeConst( + this.api.makeConst( this.#ids.searchParamsConst, this.#makeSearchParams(this.#ids.restConst), ), - this.makeAssignment( - this.f.createPropertyAccessExpression( - this.f.createThis(), + this.api.makeAssignment( + this.api.f.createPropertyAccessExpression( + this.api.f.createThis(), this.#ids.sourceProp, ), - this.makeNew("EventSource", this.#makeFetchURL()), + this.api.makeNew("EventSource", this.#makeFetchURL()), ), ], ); #makeEventNarrow = (value: Typeable) => - this.f.createTypeLiteralNode([ - this.makeInterfaceProp(propOf("event"), value), + this.api.f.createTypeLiteralNode([ + this.api.makeInterfaceProp(propOf("event"), value), ]); #makeOnMethod = () => - this.makePublicMethod( + this.api.makePublicMethod( this.#ids.onMethod, - this.makeParams({ + this.api.makeParams({ [this.#ids.eventParameter.text]: "E", - [this.#ids.handlerParameter.text]: this.makeFnType( + [this.#ids.handlerParameter.text]: this.api.makeFnType( { - [this.#ids.dataParameter.text]: this.makeIndexed( - this.makeExtract( + [this.#ids.dataParameter.text]: this.api.makeIndexed( + this.api.makeExtract( "R", - this.makeOneLine(this.#makeEventNarrow("E")), + this.api.makeOneLine(this.#makeEventNarrow("E")), ), - this.makeLiteralType(propOf("data")), + this.api.makeLiteralType(propOf("data")), ), }, - this.makeMaybeAsync(this.ts.SyntaxKind.VoidKeyword), + this.api.makeMaybeAsync(this.api.ts.SyntaxKind.VoidKeyword), ), }), [ - this.f.createExpressionStatement( - this.makeCall( - this.f.createThis(), + this.api.f.createExpressionStatement( + this.api.makeCall( + this.api.f.createThis(), this.#ids.sourceProp, propOf("addEventListener"), )( this.#ids.eventParameter, - this.makeArrowFn( + this.api.makeArrowFn( [this.#ids.msgParameter], - this.makeCall(this.#ids.handlerParameter)( - this.makeCall( + this.api.makeCall(this.#ids.handlerParameter)( + this.api.makeCall( JSON[Symbol.toStringTag], propOf("parse"), )( - this.f.createPropertyAccessExpression( - this.f.createParenthesizedExpression( - this.f.createAsExpression( + this.api.f.createPropertyAccessExpression( + this.api.f.createParenthesizedExpression( + this.api.f.createAsExpression( this.#ids.msgParameter, - this.ensureTypeNode(MessageEvent.name), + this.api.ensureTypeNode(MessageEvent.name), ), ), propOf("data"), @@ -561,13 +581,13 @@ export abstract class IntegrationBase extends TypescriptAPI { ), ), ), - this.f.createReturnStatement(this.f.createThis()), + this.api.f.createReturnStatement(this.api.f.createThis()), ], { typeParams: { - E: this.makeIndexed( + E: this.api.makeIndexed( "R", - this.makeLiteralType(propOf("event")), + this.api.makeLiteralType(propOf("event")), ), }, }, @@ -578,31 +598,31 @@ export abstract class IntegrationBase extends TypescriptAPI { * @internal * */ protected makeSubscriptionClass = (name: string) => - this.makePublicClass( + this.api.makePublicClass( name, [ - this.makePublicProperty(this.#ids.sourceProp, "EventSource"), + this.api.makePublicProperty(this.#ids.sourceProp, "EventSource"), this.#makeSubscriptionConstructor(), this.#makeOnMethod(), ], { typeParams: { - K: this.makeExtract( + K: this.api.makeExtract( this.requestType.name, - this.f.createTemplateLiteralType( - this.f.createTemplateHead("get "), + this.api.f.createTemplateLiteralType( + this.api.f.createTemplateHead("get "), [ - this.f.createTemplateLiteralTypeSpan( - this.ensureTypeNode(this.ts.SyntaxKind.StringKeyword), - this.f.createTemplateTail(""), + this.api.f.createTemplateLiteralTypeSpan( + this.api.ensureTypeNode(this.api.ts.SyntaxKind.StringKeyword), + this.api.f.createTemplateTail(""), ), ], ), ), - R: this.makeExtract( - this.makeIndexed(this.interfaces.positive, "K"), - this.makeOneLine( - this.#makeEventNarrow(this.ts.SyntaxKind.StringKeyword), + R: this.api.makeExtract( + this.api.makeIndexed(this.interfaces.positive, "K"), + this.api.makeOneLine( + this.#makeEventNarrow(this.api.ts.SyntaxKind.StringKeyword), ), ), }, @@ -614,25 +634,28 @@ export abstract class IntegrationBase extends TypescriptAPI { clientClassName: string, subscriptionClassName: string, ): ts.Node[] => [ - this.makeConst(this.#ids.clientConst, this.makeNew(clientClassName)), // const client = new Client(); + this.api.makeConst( + this.#ids.clientConst, + this.api.makeNew(clientClassName), + ), // const client = new Client(); // client.provide("get /v1/user/retrieve", { id: "10" }); - this.makeCall(this.#ids.clientConst, this.#ids.provideMethod)( - this.literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`), - this.f.createObjectLiteralExpression([ - this.f.createPropertyAssignment("id", this.literally("10")), + this.api.makeCall(this.#ids.clientConst, this.#ids.provideMethod)( + this.api.literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`), + this.api.f.createObjectLiteralExpression([ + this.api.f.createPropertyAssignment("id", this.api.literally("10")), ]), ), // new Subscription("get /v1/events/stream", {}).on("time", (time) => {}); - this.makeCall( - this.makeNew( + this.api.makeCall( + this.api.makeNew( subscriptionClassName, - this.literally(`${"get" satisfies ClientMethod} /v1/events/stream`), - this.f.createObjectLiteralExpression(), + this.api.literally(`${"get" satisfies ClientMethod} /v1/events/stream`), + this.api.f.createObjectLiteralExpression(), ), this.#ids.onMethod, )( - this.literally("time"), - this.makeArrowFn(["time"], this.f.createBlock([])), + this.api.literally("time"), + this.api.makeArrowFn(["time"], this.api.f.createBlock([])), ), ]; } diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index a346fead5a..d184738b6a 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -71,11 +71,11 @@ export class Integration extends IntegrationBase { let name = this.#aliases.get(key)?.name?.text; if (!name) { name = `Type${this.#aliases.size + 1}`; - const temp = this.makeLiteralType(null); - this.#aliases.set(key, this.makeType(name, temp)); - this.#aliases.set(key, this.makeType(name, produce())); + const temp = this.api.makeLiteralType(null); + this.#aliases.set(key, this.api.makeType(name, temp)); + this.#aliases.set(key, this.api.makeType(name, produce())); } - return this.ensureTypeNode(name); + return this.api.ensureTypeNode(name); } public constructor({ @@ -90,14 +90,14 @@ export class Integration extends IntegrationBase { hasHeadMethod = true, }: IntegrationParams) { super(serverUrl); - const commons = { makeAlias: this.#makeAlias.bind(this), api: this }; + const commons = { makeAlias: this.#makeAlias.bind(this), api: this.api }; const ctxIn = { brandHandling, ctx: { ...commons, isResponse: false } }; const ctxOut = { brandHandling, ctx: { ...commons, isResponse: true } }; const onEndpoint: OnEndpoint = (method, path, endpoint) => { const entitle = makeCleanId.bind(null, method, path); // clean id with method+path prefix const { isDeprecated, inputSchema, tags } = endpoint; const request = `${method} ${path}`; - const input = this.makeType( + const input = this.api.makeType( entitle("input"), zodToTs(inputSchema, ctxIn), { comment: request }, @@ -108,17 +108,17 @@ export class Integration extends IntegrationBase { const responses = endpoint.getResponses(responseVariant); const props = R.chain(([idx, { schema, mimeTypes, statusCodes }]) => { const hasContent = shouldHaveContent(method, mimeTypes); - const variantType = this.makeType( + const variantType = this.api.makeType( entitle(responseVariant, "variant", `${idx + 1}`), zodToTs(hasContent ? schema : noContent, ctxOut), { comment: request }, ); this.#program.push(variantType); return statusCodes.map((code) => - this.makeInterfaceProp(code, variantType.name), + this.api.makeInterfaceProp(code, variantType.name), ); }, Array.from(responses.entries())); - const dict = this.makeInterface( + const dict = this.api.makeInterface( entitle(responseVariant, "response", "variants"), props, { comment: request }, @@ -129,18 +129,18 @@ export class Integration extends IntegrationBase { {} as Record, ); this.paths.add(path); - const literalIdx = this.makeLiteralType(request); + const literalIdx = this.api.makeLiteralType(request); const store = { - input: this.ensureTypeNode(input.name), + input: this.api.ensureTypeNode(input.name), positive: this.someOf(dictionaries.positive), negative: this.someOf(dictionaries.negative), - response: this.makeUnion([ - this.makeIndexed(this.interfaces.positive, literalIdx), - this.makeIndexed(this.interfaces.negative, literalIdx), + response: this.api.makeUnion([ + this.api.makeIndexed(this.interfaces.positive, literalIdx), + this.api.makeIndexed(this.interfaces.negative, literalIdx), ]), - encoded: this.f.createIntersectionTypeNode([ - this.ensureTypeNode(dictionaries.positive.name), - this.ensureTypeNode(dictionaries.negative.name), + encoded: this.api.f.createIntersectionTypeNode([ + this.api.ensureTypeNode(dictionaries.positive.name), + this.api.ensureTypeNode(dictionaries.negative.name), ]), }; this.registry.set(request, { isDeprecated, store }); @@ -182,7 +182,7 @@ export class Integration extends IntegrationBase { .map((entry) => typeof entry === "string" ? entry - : this.printNode(entry, printerOptions), + : this.api.printNode(entry, printerOptions), ) .join("\n") : undefined; @@ -192,19 +192,19 @@ export class Integration extends IntegrationBase { const usageExampleText = this.#printUsage(printerOptions); const commentNode = usageExampleText && - this.ts.addSyntheticLeadingComment( - this.ts.addSyntheticLeadingComment( - this.f.createEmptyStatement(), - this.ts.SyntaxKind.SingleLineCommentTrivia, + this.api.ts.addSyntheticLeadingComment( + this.api.ts.addSyntheticLeadingComment( + this.api.f.createEmptyStatement(), + this.api.ts.SyntaxKind.SingleLineCommentTrivia, " Usage example:", ), - this.ts.SyntaxKind.MultiLineCommentTrivia, + this.api.ts.SyntaxKind.MultiLineCommentTrivia, `\n${usageExampleText}`, ); return this.#program .concat(commentNode || []) .map((node, index) => - this.printNode( + this.api.printNode( node, index < this.#program.length ? printerOptions From 81118c8ef3cd028237e901a253a0ee8448775c12 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 25 Dec 2025 13:15:25 +0100 Subject: [PATCH 17/18] Ref: marking api prop internal in ZTSContext. --- express-zod-api/src/typescript-api.ts | 38 --------------------------- express-zod-api/src/zts-helpers.ts | 1 + 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index fd08dc6a42..d83cf49b1a 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -13,15 +13,10 @@ type TypeParams = | Partial>; export class TypescriptAPI { - /** @internal */ public ts: typeof ts; - /** @internal */ public f: typeof ts.factory; - /** @internal */ public exportModifier: ts.ModifierToken[]; - /** @internal */ public asyncModifier: ts.ModifierToken[]; - /** @internal */ public accessModifiers: Record<"public" | "protectedReadonly", ts.Modifier[]>; #primitives: ts.KeywordTypeSyntaxKind[]; #safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; @@ -57,7 +52,6 @@ export class TypescriptAPI { ]; } - /** @internal */ public addJsDoc = (node: T, text: string) => this.ts.addSyntheticLeadingComment( node, @@ -66,7 +60,6 @@ export class TypescriptAPI { true, ); - /** @internal */ public printNode = (node: ts.Node, printerOptions?: ts.PrinterOptions) => { const sourceFile = this.ts.createSourceFile( "print.ts", @@ -79,13 +72,11 @@ export class TypescriptAPI { return printer.printNode(this.ts.EmitHint.Unspecified, node, sourceFile); }; - /** @internal */ public makePropertyIdentifier = (name: string | number) => typeof name === "string" && this.#safePropRegex.test(name) ? this.f.createIdentifier(name) : this.literally(name); - /** @internal */ public makeTemplate = ( head: string, ...rest: ([ts.Expression] | [ts.Expression, string])[] @@ -102,7 +93,6 @@ export class TypescriptAPI { ), ); - /** @internal */ public makeParam = ( name: string | ts.Identifier, { @@ -128,7 +118,6 @@ export class TypescriptAPI { init, ); - /** @internal */ public makeParams = ( params: Partial< Record[1]> @@ -145,7 +134,6 @@ export class TypescriptAPI { ), ); - /** @internal */ public makePublicConstructor = ( params: ts.ParameterDeclaration[], statements: ts.Statement[] = [], @@ -156,7 +144,6 @@ export class TypescriptAPI { this.f.createBlock(statements), ); - /** @internal */ public ensureTypeNode = ( subject: Typeable, args?: Typeable[], // only for string and id @@ -194,7 +181,6 @@ export class TypescriptAPI { return this.f.createUnionTypeNode(Array.from(nodes.values())); }; - /** @internal */ public makeInterfaceProp = ( name: string | number, value: Typeable, @@ -225,11 +211,9 @@ export class TypescriptAPI { return jsdoc.length ? this.addJsDoc(node, jsdoc.join(" ")) : node; }; - /** @internal */ public makeOneLine = (subject: ts.TypeNode) => this.ts.setEmitFlags(subject, this.ts.EmitFlags.SingleLine); - /** @internal */ public makeDeconstruction = ( ...names: ts.Identifier[] ): ts.ArrayBindingPattern => @@ -239,7 +223,6 @@ export class TypescriptAPI { ), ); - /** @internal */ public makeConst = ( name: string | ts.Identifier | ts.ArrayBindingPattern, value: ts.Expression, @@ -260,7 +243,6 @@ export class TypescriptAPI { ), ); - /** @internal */ public makePublicLiteralType = ( name: ts.Identifier | string, literals: string[], @@ -271,7 +253,6 @@ export class TypescriptAPI { { expose: true }, ); - /** @internal */ public makeType = ( name: ts.Identifier | string, value: ts.TypeNode, @@ -290,7 +271,6 @@ export class TypescriptAPI { return comment ? this.addJsDoc(node, comment) : node; }; - /** @internal */ public makePublicProperty = ( name: string | ts.PropertyName, type: Typeable, @@ -303,7 +283,6 @@ export class TypescriptAPI { undefined, ); - /** @internal */ public makePublicMethod = ( name: ts.Identifier, params: ts.ParameterDeclaration[], @@ -324,7 +303,6 @@ export class TypescriptAPI { this.f.createBlock(statements), ); - /** @internal */ public makePublicClass = ( name: string, statements: ts.ClassElement[], @@ -338,18 +316,15 @@ export class TypescriptAPI { statements, ); - /** @internal */ public makeKeyOf = (subj: Typeable) => this.f.createTypeOperatorNode( this.ts.SyntaxKind.KeyOfKeyword, this.ensureTypeNode(subj), ); - /** @internal */ public makePromise = (subject: Typeable) => this.ensureTypeNode(Promise.name, [subject]); - /** @internal */ public makeInterface = ( name: ts.Identifier | string, props: ts.PropertySignature[], @@ -365,7 +340,6 @@ export class TypescriptAPI { return comment ? this.addJsDoc(node, comment) : node; }; - /** @internal */ public makeTypeParams = ( params: | string[] @@ -387,7 +361,6 @@ export class TypescriptAPI { ); }); - /** @internal */ public makeArrowFn = ( params: | Array[0]> @@ -406,7 +379,6 @@ export class TypescriptAPI { body, ); - /** @internal */ public makeTernary = ( condition: ts.Expression, positive: ts.Expression, @@ -420,7 +392,6 @@ export class TypescriptAPI { negative, ); - /** @internal */ public makeCall = ( first: ts.Expression | string, @@ -439,15 +410,12 @@ export class TypescriptAPI { args, ); - /** @internal */ public makeNew = (cls: string, ...args: ts.Expression[]) => this.f.createNewExpression(this.f.createIdentifier(cls), undefined, args); - /** @internal */ public makeExtract = (base: Typeable, narrow: ts.TypeNode) => this.ensureTypeNode("Extract", [base, narrow]); - /** @internal */ public makeAssignment = (left: ts.Expression, right: ts.Expression) => this.f.createExpressionStatement( this.f.createBinaryExpression( @@ -457,18 +425,15 @@ export class TypescriptAPI { ), ); - /** @internal */ public makeIndexed = (subject: Typeable, index: Typeable) => this.f.createIndexedAccessTypeNode( this.ensureTypeNode(subject), this.ensureTypeNode(index), ); - /** @internal */ public makeMaybeAsync = (subj: Typeable) => this.makeUnion([this.ensureTypeNode(subj), this.makePromise(subj)]); - /** @internal */ public makeFnType = ( params: Parameters[0], returns: Typeable, @@ -480,7 +445,6 @@ export class TypescriptAPI { ); /* eslint-disable prettier/prettier -- shorter and works better this way than overrides */ - /** @internal */ public literally = (subj: T) => ( typeof subj === "number" ? this.f.createNumericLiteral(subj) : typeof subj === "bigint" ? this.f.createBigIntLiteral(subj.toString()) @@ -490,11 +454,9 @@ export class TypescriptAPI { : T extends boolean ? ts.BooleanLiteral : ts.NullLiteral; /* eslint-enable prettier/prettier */ - /** @internal */ public makeLiteralType = (subj: Parameters[0]) => this.f.createLiteralTypeNode(this.literally(subj)); - /** @internal */ public isPrimitive = (node: ts.TypeNode): node is ts.KeywordTypeNode => (this.#primitives as ts.SyntaxKind[]).includes(node.kind); } diff --git a/express-zod-api/src/zts-helpers.ts b/express-zod-api/src/zts-helpers.ts index 6a6eb336a6..4778b8deac 100644 --- a/express-zod-api/src/zts-helpers.ts +++ b/express-zod-api/src/zts-helpers.ts @@ -6,6 +6,7 @@ import type { TypescriptAPI } from "./typescript-api"; export interface ZTSContext extends FlatObject { isResponse: boolean; makeAlias: (key: object, produce: () => ts.TypeNode) => ts.TypeNode; + /** @internal */ api: TypescriptAPI; } From 4e0c8d37f910e9d493f7a44dcebfd25d2763bac7 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 25 Dec 2025 13:22:50 +0100 Subject: [PATCH 18/18] Ref: making safePropRegex static. --- express-zod-api/src/typescript-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index d83cf49b1a..a196589021 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -19,7 +19,7 @@ export class TypescriptAPI { public asyncModifier: ts.ModifierToken[]; public accessModifiers: Record<"public" | "protectedReadonly", ts.Modifier[]>; #primitives: ts.KeywordTypeSyntaxKind[]; - #safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + static #safePropRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/; constructor() { this.ts = createRequire(import.meta.url)("typescript"); // @todo replace with a dynamic import in next major @@ -73,7 +73,7 @@ export class TypescriptAPI { }; public makePropertyIdentifier = (name: string | number) => - typeof name === "string" && this.#safePropRegex.test(name) + typeof name === "string" && TypescriptAPI.#safePropRegex.test(name) ? this.f.createIdentifier(name) : this.literally(name);