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:
+[
](https://github.com/NicolasMahe)
[
](https://github.com/shadone)
[
](https://github.com/squishykid)
[
](https://github.com/jakub-msqt)
diff --git a/express-zod-api/package.json b/express-zod-api/package.json
index 2e9da92e40..bf9eee0303 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.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": {
diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts
index c5ae09c388..7a96582f77 100644
--- a/express-zod-api/src/integration-base.ts
+++ b/express-zod-api/src/integration-base.ts
@@ -1,51 +1,18 @@
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";
import type { makeEventSchema } from "./sse";
-import {
- accessModifiers,
- ensureTypeNode,
- f,
- 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 {
+ /** @internal */
+ protected api = new TypescriptAPI();
/** @internal */
protected paths = new Set();
/** @internal */
@@ -57,65 +24,72 @@ export abstract class IntegrationBase {
>();
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"),
+ 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.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: f.createIdentifier("Input"),
- positive: f.createIdentifier("PositiveResponse"),
- negative: f.createIdentifier("NegativeResponse"),
- encoded: f.createIdentifier("EncodedResponse"),
- response: 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 = makePublicLiteralType("Method", clientMethods);
+ protected methodType = this.api.makePublicLiteralType(
+ "Method",
+ clientMethods,
+ );
/**
* @example type SomeOf = T[keyof T];
* @internal
* */
- protected someOfType = makeType("SomeOf", makeIndexed("T", makeKeyOf("T")), {
- params: ["T"],
- });
+ protected someOfType = this.api.makeType(
+ "SomeOf",
+ this.api.makeIndexed("T", this.api.makeKeyOf("T")),
+ { params: ["T"] },
+ );
/**
* @example export type Request = keyof Input;
* @internal
* */
- protected requestType = makeType(
+ protected requestType = this.api.makeType(
"Request",
- makeKeyOf(this.interfaces.input),
+ this.api.makeKeyOf(this.interfaces.input),
{ expose: true },
);
@@ -126,14 +100,14 @@ export abstract class IntegrationBase {
* @internal
**/
protected someOf = ({ name }: ts.TypeAliasDeclaration) =>
- ensureTypeNode(this.someOfType.name, [name]);
+ this.api.ensureTypeNode(this.someOfType.name, [name]);
/**
* @example export type Path = "/v1/user/retrieve" | ___;
* @internal
* */
protected makePathType = () =>
- 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; }
@@ -141,10 +115,10 @@ export abstract class IntegrationBase {
* */
protected makePublicInterfaces = () =>
(Object.keys(this.interfaces) as IOKind[]).map((kind) =>
- makeInterface(
+ this.api.makeInterface(
this.interfaces[kind],
Array.from(this.registry).map(([request, { store, isDeprecated }]) =>
- makeInterfaceProp(request, store[kind], { isDeprecated }),
+ this.api.makeInterfaceProp(request, store[kind], { isDeprecated }),
),
{ expose: true },
),
@@ -155,13 +129,15 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeEndpointTags = () =>
- makeConst(
+ this.api.makeConst(
"endpointTags",
- f.createObjectLiteralExpression(
+ this.api.f.createObjectLiteralExpression(
Array.from(this.tags).map(([request, tags]) =>
- f.createPropertyAssignment(
- makePropertyIdentifier(request),
- f.createArrayLiteralExpression(R.map(literally, tags)),
+ this.api.f.createPropertyAssignment(
+ this.api.makePropertyIdentifier(request),
+ this.api.f.createArrayLiteralExpression(
+ R.map(this.api.literally.bind(this.api), tags),
+ ),
),
),
),
@@ -173,20 +149,20 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeImplementationType = () =>
- makeType(
+ this.api.makeType(
this.#ids.implementationType,
- makeFnType(
+ this.api.makeFnType(
{
[this.#ids.methodParameter.text]: this.methodType.name,
- [this.#ids.pathParameter.text]: ts.SyntaxKind.StringKeyword,
- [this.#ids.paramsArgument.text]: recordStringAny,
+ [this.#ids.pathParameter.text]: this.api.ts.SyntaxKind.StringKeyword,
+ [this.#ids.paramsArgument.text]: this.api.makeRecordStringAny(),
[this.#ids.ctxArgument.text]: { optional: true, type: "T" },
},
- makePromise(ts.SyntaxKind.AnyKeyword),
+ this.api.makePromise(this.api.ts.SyntaxKind.AnyKeyword),
),
{
expose: true,
- params: { T: { init: ts.SyntaxKind.UnknownKeyword } },
+ params: { T: { init: this.api.ts.SyntaxKind.UnknownKeyword } },
},
);
@@ -195,18 +171,24 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeParseRequestFn = () =>
- makeConst(
+ this.api.makeConst(
this.#ids.parseRequestFn,
- makeArrowFn(
- { [this.#ids.requestParameter.text]: ts.SyntaxKind.StringKeyword },
- f.createAsExpression(
- makeCall(this.#ids.requestParameter, propOf("split"))(
- f.createRegularExpressionLiteral("/ (.+)/"), // split once
- 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
),
- f.createTupleTypeNode([
- ensureTypeNode(this.methodType.name),
- ensureTypeNode(this.#ids.pathType),
+ this.api.f.createTupleTypeNode([
+ this.api.ensureTypeNode(this.methodType.name),
+ this.api.ensureTypeNode(this.#ids.pathType),
]),
),
),
@@ -217,44 +199,47 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeSubstituteFn = () =>
- makeConst(
+ this.api.makeConst(
this.#ids.substituteFn,
- makeArrowFn(
+ this.api.makeArrowFn(
{
- [this.#ids.pathParameter.text]: ts.SyntaxKind.StringKeyword,
- [this.#ids.paramsArgument.text]: recordStringAny,
+ [this.#ids.pathParameter.text]: this.api.ts.SyntaxKind.StringKeyword,
+ [this.#ids.paramsArgument.text]: this.api.makeRecordStringAny(),
},
- f.createBlock([
- makeConst(
+ this.api.f.createBlock([
+ this.api.makeConst(
this.#ids.restConst,
- f.createObjectLiteralExpression([
- f.createSpreadAssignment(this.#ids.paramsArgument),
+ this.api.f.createObjectLiteralExpression([
+ this.api.f.createSpreadAssignment(this.#ids.paramsArgument),
]),
),
- f.createForInStatement(
- f.createVariableDeclarationList(
- [f.createVariableDeclaration(this.#ids.keyParameter)],
- 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,
- f.createBlock([
- makeAssignment(
+ this.api.f.createBlock([
+ this.api.makeAssignment(
this.#ids.pathParameter,
- makeCall(this.#ids.pathParameter, propOf("replace"))(
- makeTemplate(":", [this.#ids.keyParameter]), // `:${key}`
- makeArrowFn(
+ this.api.makeCall(
+ this.#ids.pathParameter,
+ propOf("replace"),
+ )(
+ this.api.makeTemplate(":", [this.#ids.keyParameter]), // `:${key}`
+ this.api.makeArrowFn(
[],
- f.createBlock([
- f.createExpressionStatement(
- f.createDeleteExpression(
- f.createElementAccessExpression(
+ this.api.f.createBlock([
+ this.api.f.createExpressionStatement(
+ this.api.f.createDeleteExpression(
+ this.api.f.createElementAccessExpression(
this.#ids.restConst,
this.#ids.keyParameter,
),
),
),
- f.createReturnStatement(
- f.createElementAccessExpression(
+ this.api.f.createReturnStatement(
+ this.api.f.createElementAccessExpression(
this.#ids.paramsArgument,
this.#ids.keyParameter,
),
@@ -265,13 +250,13 @@ export abstract class IntegrationBase {
),
]),
),
- f.createReturnStatement(
- f.createAsExpression(
- f.createArrayLiteralExpression([
+ this.api.f.createReturnStatement(
+ this.api.f.createAsExpression(
+ this.api.f.createArrayLiteralExpression([
this.#ids.pathParameter,
this.#ids.restConst,
]),
- ensureTypeNode("const"),
+ this.api.ensureTypeNode("const"),
),
),
]),
@@ -280,31 +265,36 @@ export abstract class IntegrationBase {
// public provide(request: K, params: Input[K]): Promise {}
#makeProvider = () =>
- makePublicMethod(
+ this.api.makePublicMethod(
this.#ids.provideMethod,
- makeParams({
+ this.api.makeParams({
[this.#ids.requestParameter.text]: "K",
- [this.#ids.paramsArgument.text]: makeIndexed(
+ [this.#ids.paramsArgument.text]: this.api.makeIndexed(
this.interfaces.input,
"K",
),
[this.#ids.ctxArgument.text]: { optional: true, type: "T" },
}),
[
- makeConst(
+ this.api.makeConst(
// const [method, path] = this.parseRequest(request);
- makeDeconstruction(
+ this.api.makeDeconstruction(
this.#ids.methodParameter,
this.#ids.pathParameter,
),
- makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter),
+ this.api.makeCall(this.#ids.parseRequestFn)(
+ this.#ids.requestParameter,
+ ),
),
// return this.implementation(___)
- f.createReturnStatement(
- makeCall(f.createThis(), this.#ids.implementationArgument)(
+ this.api.f.createReturnStatement(
+ this.api.makeCall(
+ this.api.f.createThis(),
+ this.#ids.implementationArgument,
+ )(
this.#ids.methodParameter,
- f.createSpreadElement(
- makeCall(this.#ids.substituteFn)(
+ this.api.f.createSpreadElement(
+ this.api.makeCall(this.#ids.substituteFn)(
this.#ids.pathParameter,
this.#ids.paramsArgument,
),
@@ -315,7 +305,9 @@ export abstract class IntegrationBase {
],
{
typeParams: { K: this.requestType.name },
- returns: makePromise(makeIndexed(this.interfaces.response, "K")),
+ returns: this.api.makePromise(
+ this.api.makeIndexed(this.interfaces.response, "K"),
+ ),
},
);
@@ -324,14 +316,14 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeClientClass = (name: string) =>
- makePublicClass(
+ this.api.makePublicClass(
name,
[
// public constructor(protected readonly implementation: Implementation = defaultImplementation) {}
- makePublicConstructor([
- makeParam(this.#ids.implementationArgument, {
- type: ensureTypeNode(this.#ids.implementationType, ["T"]),
- mod: 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,
}),
]),
@@ -342,18 +334,18 @@ export abstract class IntegrationBase {
// `?${new URLSearchParams(____)}`
#makeSearchParams = (from: ts.Expression) =>
- makeTemplate("?", [makeNew(URLSearchParams.name, from)]);
+ this.api.makeTemplate("?", [this.api.makeNew(URLSearchParams.name, from)]);
// new URL(`${path}${searchParams}`, "http:____")
#makeFetchURL = () =>
- makeNew(
+ this.api.makeNew(
URL.name,
- makeTemplate(
+ this.api.makeTemplate(
"",
[this.#ids.pathParameter],
[this.#ids.searchParamsConst],
),
- literally(this.serverUrl),
+ this.api.literally(this.serverUrl),
);
/**
@@ -362,20 +354,23 @@ export abstract class IntegrationBase {
* */
protected makeDefaultImplementation = () => {
// method: method.toUpperCase()
- const methodProperty = f.createPropertyAssignment(
+ const methodProperty = this.api.f.createPropertyAssignment(
propOf("method"),
- makeCall(this.#ids.methodParameter, propOf("toUpperCase"))(),
+ this.api.makeCall(
+ this.#ids.methodParameter,
+ propOf("toUpperCase"),
+ )(),
);
// headers: hasBody ? { "Content-Type": "application/json" } : undefined
- const headersProperty = f.createPropertyAssignment(
+ const headersProperty = this.api.f.createPropertyAssignment(
propOf("headers"),
- makeTernary(
+ this.api.makeTernary(
this.#ids.hasBodyConst,
- f.createObjectLiteralExpression([
- f.createPropertyAssignment(
- literally("Content-Type"),
- literally(contentTypes.json),
+ this.api.f.createObjectLiteralExpression([
+ this.api.f.createPropertyAssignment(
+ this.api.literally("Content-Type"),
+ this.api.literally(contentTypes.json),
),
]),
this.#ids.undefinedValue,
@@ -383,11 +378,11 @@ export abstract class IntegrationBase {
);
// body: hasBody ? JSON.stringify(params) : undefined
- const bodyProperty = f.createPropertyAssignment(
+ const bodyProperty = this.api.f.createPropertyAssignment(
propOf("body"),
- makeTernary(
+ this.api.makeTernary(
this.#ids.hasBodyConst,
- makeCall(
+ this.api.makeCall(
JSON[Symbol.toStringTag],
propOf("stringify"),
)(this.#ids.paramsArgument),
@@ -396,12 +391,12 @@ export abstract class IntegrationBase {
);
// const response = await fetch(new URL(`${path}${searchParams}`, "https://example.com"), { ___ });
- const responseStatement = makeConst(
+ const responseStatement = this.api.makeConst(
this.#ids.responseConst,
- f.createAwaitExpression(
- makeCall(fetch.name)(
+ this.api.f.createAwaitExpression(
+ this.api.makeCall(fetch.name)(
this.#makeFetchURL(),
- f.createObjectLiteralExpression([
+ this.api.f.createObjectLiteralExpression([
methodProperty,
headersProperty,
bodyProperty,
@@ -411,14 +406,14 @@ export abstract class IntegrationBase {
);
// const hasBody = !["get", "delete"].includes(method);
- const hasBodyStatement = makeConst(
+ const hasBodyStatement = this.api.makeConst(
this.#ids.hasBodyConst,
- f.createLogicalNot(
- makeCall(
- f.createArrayLiteralExpression([
- literally("get" satisfies ClientMethod),
- literally("head" satisfies ClientMethod),
- 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),
@@ -426,64 +421,64 @@ export abstract class IntegrationBase {
);
// const searchParams = hasBody ? "" : ___;
- const searchParamsStatement = makeConst(
+ const searchParamsStatement = this.api.makeConst(
this.#ids.searchParamsConst,
- makeTernary(
+ this.api.makeTernary(
this.#ids.hasBodyConst,
- literally(""),
+ this.api.literally(""),
this.#makeSearchParams(this.#ids.paramsArgument),
),
);
// const contentType = response.headers.get("content-type");
- const contentTypeStatement = makeConst(
+ const contentTypeStatement = this.api.makeConst(
this.#ids.contentTypeConst,
- makeCall(
+ this.api.makeCall(
this.#ids.responseConst,
propOf("headers"),
propOf("get"),
- )(literally("content-type")),
+ )(this.api.literally("content-type")),
);
// if (!contentType) return;
- const noBodyStatement = f.createIfStatement(
- f.createPrefixUnaryExpression(
- ts.SyntaxKind.ExclamationToken,
+ const noBodyStatement = this.api.f.createIfStatement(
+ this.api.f.createPrefixUnaryExpression(
+ this.api.ts.SyntaxKind.ExclamationToken,
this.#ids.contentTypeConst,
),
- f.createReturnStatement(),
+ this.api.f.createReturnStatement(),
);
// const isJSON = contentType.startsWith("application/json");
- const isJsonConst = makeConst(
+ const isJsonConst = this.api.makeConst(
this.#ids.isJsonConst,
- makeCall(
+ this.api.makeCall(
this.#ids.contentTypeConst,
propOf("startsWith"),
- )(literally(contentTypes.json)),
+ )(this.api.literally(contentTypes.json)),
);
// return response[isJSON ? "json" : "text"]();
- const returnStatement = f.createReturnStatement(
- makeCall(
+ const returnStatement = this.api.f.createReturnStatement(
+ this.api.makeCall(
this.#ids.responseConst,
- makeTernary(
+ this.api.makeTernary(
this.#ids.isJsonConst,
- literally(propOf("json")),
- literally(propOf("text")),
+ this.api.literally(propOf("json")),
+ this.api.literally(propOf("text")),
),
)(),
);
- return makeConst(
+ return this.api.makeConst(
this.#ids.defaultImplementationConst,
- makeArrowFn(
+ this.api.makeArrowFn(
[
this.#ids.methodParameter,
this.#ids.pathParameter,
this.#ids.paramsArgument,
],
- f.createBlock([
+ this.api.f.createBlock([
hasBodyStatement,
searchParamsStatement,
responseStatement,
@@ -499,76 +494,84 @@ export abstract class IntegrationBase {
};
#makeSubscriptionConstructor = () =>
- makePublicConstructor(
- makeParams({
+ this.api.makePublicConstructor(
+ this.api.makeParams({
request: "K",
- params: makeIndexed(this.interfaces.input, "K"),
+ params: this.api.makeIndexed(this.interfaces.input, "K"),
}),
[
- makeConst(
- makeDeconstruction(this.#ids.pathParameter, this.#ids.restConst),
- makeCall(this.#ids.substituteFn)(
- f.createElementAccessExpression(
- makeCall(this.#ids.parseRequestFn)(this.#ids.requestParameter),
- literally(1),
+ 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.api.literally(1),
),
this.#ids.paramsArgument,
),
),
- makeConst(
+ this.api.makeConst(
this.#ids.searchParamsConst,
this.#makeSearchParams(this.#ids.restConst),
),
- makeAssignment(
- f.createPropertyAccessExpression(
- f.createThis(),
+ this.api.makeAssignment(
+ this.api.f.createPropertyAccessExpression(
+ this.api.f.createThis(),
this.#ids.sourceProp,
),
- makeNew("EventSource", this.#makeFetchURL()),
+ this.api.makeNew("EventSource", this.#makeFetchURL()),
),
],
);
#makeEventNarrow = (value: Typeable) =>
- f.createTypeLiteralNode([
- makeInterfaceProp(propOf("event"), value),
+ this.api.f.createTypeLiteralNode([
+ this.api.makeInterfaceProp(propOf("event"), value),
]);
#makeOnMethod = () =>
- makePublicMethod(
+ this.api.makePublicMethod(
this.#ids.onMethod,
- makeParams({
+ this.api.makeParams({
[this.#ids.eventParameter.text]: "E",
- [this.#ids.handlerParameter.text]: makeFnType(
+ [this.#ids.handlerParameter.text]: this.api.makeFnType(
{
- [this.#ids.dataParameter.text]: makeIndexed(
- makeExtract("R", makeOneLine(this.#makeEventNarrow("E"))),
- makeLiteralType(propOf("data")),
+ [this.#ids.dataParameter.text]: this.api.makeIndexed(
+ this.api.makeExtract(
+ "R",
+ this.api.makeOneLine(this.#makeEventNarrow("E")),
+ ),
+ this.api.makeLiteralType(propOf("data")),
),
},
- makeMaybeAsync(ts.SyntaxKind.VoidKeyword),
+ this.api.makeMaybeAsync(this.api.ts.SyntaxKind.VoidKeyword),
),
}),
[
- f.createExpressionStatement(
- makeCall(
- f.createThis(),
+ this.api.f.createExpressionStatement(
+ this.api.makeCall(
+ this.api.f.createThis(),
this.#ids.sourceProp,
propOf("addEventListener"),
)(
this.#ids.eventParameter,
- makeArrowFn(
+ this.api.makeArrowFn(
[this.#ids.msgParameter],
- makeCall(this.#ids.handlerParameter)(
- makeCall(
+ this.api.makeCall(this.#ids.handlerParameter)(
+ this.api.makeCall(
JSON[Symbol.toStringTag],
propOf("parse"),
)(
- f.createPropertyAccessExpression(
- f.createParenthesizedExpression(
- f.createAsExpression(
+ this.api.f.createPropertyAccessExpression(
+ this.api.f.createParenthesizedExpression(
+ this.api.f.createAsExpression(
this.#ids.msgParameter,
- ensureTypeNode(MessageEvent.name),
+ this.api.ensureTypeNode(MessageEvent.name),
),
),
propOf("data"),
@@ -578,11 +581,14 @@ export abstract class IntegrationBase {
),
),
),
- f.createReturnStatement(f.createThis()),
+ this.api.f.createReturnStatement(this.api.f.createThis()),
],
{
typeParams: {
- E: makeIndexed("R", makeLiteralType(propOf("event"))),
+ E: this.api.makeIndexed(
+ "R",
+ this.api.makeLiteralType(propOf("event")),
+ ),
},
},
);
@@ -592,27 +598,32 @@ export abstract class IntegrationBase {
* @internal
* */
protected makeSubscriptionClass = (name: string) =>
- makePublicClass(
+ this.api.makePublicClass(
name,
[
- makePublicProperty(this.#ids.sourceProp, "EventSource"),
+ this.api.makePublicProperty(this.#ids.sourceProp, "EventSource"),
this.#makeSubscriptionConstructor(),
this.#makeOnMethod(),
],
{
typeParams: {
- K: makeExtract(
+ K: this.api.makeExtract(
this.requestType.name,
- f.createTemplateLiteralType(f.createTemplateHead("get "), [
- f.createTemplateLiteralTypeSpan(
- ensureTypeNode(ts.SyntaxKind.StringKeyword),
- f.createTemplateTail(""),
- ),
- ]),
+ this.api.f.createTemplateLiteralType(
+ this.api.f.createTemplateHead("get "),
+ [
+ this.api.f.createTemplateLiteralTypeSpan(
+ this.api.ensureTypeNode(this.api.ts.SyntaxKind.StringKeyword),
+ this.api.f.createTemplateTail(""),
+ ),
+ ],
+ ),
),
- R: makeExtract(
- makeIndexed(this.interfaces.positive, "K"),
- makeOneLine(this.#makeEventNarrow(ts.SyntaxKind.StringKeyword)),
+ R: this.api.makeExtract(
+ this.api.makeIndexed(this.interfaces.positive, "K"),
+ this.api.makeOneLine(
+ this.#makeEventNarrow(this.api.ts.SyntaxKind.StringKeyword),
+ ),
),
},
},
@@ -623,22 +634,28 @@ export abstract class IntegrationBase {
clientClassName: string,
subscriptionClassName: string,
): ts.Node[] => [
- makeConst(this.#ids.clientConst, 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" });
- makeCall(this.#ids.clientConst, this.#ids.provideMethod)(
- literally(`${"get" satisfies ClientMethod} /v1/user/retrieve`),
- f.createObjectLiteralExpression([
- f.createPropertyAssignment("id", 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) => {});
- makeCall(
- makeNew(
+ this.api.makeCall(
+ this.api.makeNew(
subscriptionClassName,
- literally(`${"get" satisfies ClientMethod} /v1/events/stream`),
- f.createObjectLiteralExpression(),
+ this.api.literally(`${"get" satisfies ClientMethod} /v1/events/stream`),
+ this.api.f.createObjectLiteralExpression(),
),
this.#ids.onMethod,
- )(literally("time"), makeArrowFn(["time"], 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 bdadcdec29..d184738b6a 100644
--- a/express-zod-api/src/integration.ts
+++ b/express-zod-api/src/integration.ts
@@ -1,19 +1,8 @@
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";
-import {
- f,
- 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";
@@ -82,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.api.makeLiteralType(null);
+ this.#aliases.set(key, this.api.makeType(name, temp));
+ this.#aliases.set(key, this.api.makeType(name, produce()));
}
- return ensureTypeNode(name);
+ return this.api.ensureTypeNode(name);
}
public constructor({
@@ -101,33 +90,35 @@ 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.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 = makeType(entitle("input"), zodToTs(inputSchema, ctxIn), {
- comment: request,
- });
+ const input = this.api.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.api.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.api.makeInterfaceProp(code, variantType.name),
);
}, Array.from(responses.entries()));
- const dict = makeInterface(
+ const dict = this.api.makeInterface(
entitle(responseVariant, "response", "variants"),
props,
{ comment: request },
@@ -138,18 +129,18 @@ export class Integration extends IntegrationBase {
{} as Record,
);
this.paths.add(path);
- const literalIdx = makeLiteralType(request);
+ const literalIdx = this.api.makeLiteralType(request);
const store = {
- input: ensureTypeNode(input.name),
+ input: this.api.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.api.makeUnion([
+ this.api.makeIndexed(this.interfaces.positive, literalIdx),
+ this.api.makeIndexed(this.interfaces.negative, literalIdx),
]),
- encoded: f.createIntersectionTypeNode([
- ensureTypeNode(dictionaries.positive.name),
- 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 });
@@ -191,7 +182,7 @@ export class Integration extends IntegrationBase {
.map((entry) =>
typeof entry === "string"
? entry
- : printNode(entry, printerOptions),
+ : this.api.printNode(entry, printerOptions),
)
.join("\n")
: undefined;
@@ -201,19 +192,19 @@ export class Integration extends IntegrationBase {
const usageExampleText = this.#printUsage(printerOptions);
const commentNode =
usageExampleText &&
- ts.addSyntheticLeadingComment(
- ts.addSyntheticLeadingComment(
- f.createEmptyStatement(),
- ts.SyntaxKind.SingleLineCommentTrivia,
+ this.api.ts.addSyntheticLeadingComment(
+ this.api.ts.addSyntheticLeadingComment(
+ this.api.f.createEmptyStatement(),
+ this.api.ts.SyntaxKind.SingleLineCommentTrivia,
" Usage example:",
),
- ts.SyntaxKind.MultiLineCommentTrivia,
+ this.api.ts.SyntaxKind.MultiLineCommentTrivia,
`\n${usageExampleText}`,
);
return this.#program
.concat(commentNode || [])
.map((node, index) =>
- printNode(
+ this.api.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..a196589021 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,453 @@ 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[];
+ 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
+ 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" && TypescriptAPI.#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;
+
+ /**
+ * @internal
+ * @example Record
+ * */
+ public makeRecordStringAny = () =>
+ this.ensureTypeNode("Record", [
+ this.ts.SyntaxKind.StringKeyword,
+ this.ts.SyntaxKind.AnyKeyword,
+ ]);
+
+ /**
+ * @internal
+ * 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;
diff --git a/express-zod-api/src/zts-helpers.ts b/express-zod-api/src/zts-helpers.ts
index 484c68fb0f..4778b8deac 100644
--- a/express-zod-api/src/zts-helpers.ts
+++ b/express-zod-api/src/zts-helpers.ts
@@ -1,10 +1,13 @@
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;
+ /** @internal */
+ api: TypescriptAPI;
}
export type Producer = SchemaHandler;
diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts
index 681920a2e6..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";
@@ -9,26 +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),
- [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,
@@ -41,18 +24,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(api.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 +53,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,45 +84,51 @@ 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));
-
-const makeSample = (produced: ts.TypeNode) =>
- samples?.[produced.kind as keyof typeof samples];
+ { next, api },
+) => api.makeUnion(def.options.map(next));
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[]) => {
- if (!nodes.every(ts.isTypeLiteralNode)) throw new Error("Not objects");
+ (api: TypescriptAPI, nodes: ts.TypeNode[]) => {
+ 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;
@@ -144,20 +136,31 @@ 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);
+ (
+ syntaxKind:
+ | "AnyKeyword"
+ | "BigIntKeyword"
+ | "BooleanKeyword"
+ | "NeverKeyword"
+ | "NumberKeyword"
+ | "StringKeyword"
+ | "UndefinedKeyword"
+ | "UnknownKeyword"
+ | "VoidKeyword",
+ ): Producer =>
+ ({}, { api }) =>
+ api.ensureTypeNode(api.ts.SyntaxKind[syntaxKind]);
const onWrapped: Producer = (
{
@@ -171,43 +174,55 @@ const onWrapped: Producer = (
{ next },
) => next(def.innerType);
-const getFallback = (isResponse: boolean) =>
- ensureTypeNode(
- isResponse ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.AnyKeyword,
+const getFallback = (api: TypescriptAPI, isResponse: boolean) =>
+ api.ensureTypeNode(
+ isResponse
+ ? api.ts.SyntaxKind.UnknownKeyword
+ : api.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"];
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 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);
@@ -217,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,
@@ -262,6 +277,6 @@ export const zodToTs = (
) =>
walkSchema(schema, {
rules: { ...brandHandling, ...producers },
- onMissing: ({}, { isResponse }) => getFallback(isResponse),
+ onMissing: ({}, { isResponse, api }) => getFallback(api, isResponse),
ctx,
});
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();
});
});