Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/example-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export namespace Root {
features?: Type1[] | undefined;
};
export interface Emission {
time: (currentIsoTime: string) => void;
time: (currentIsoTime: unknown) => void;
Comment thread
RobinTail marked this conversation as resolved.
Outdated
chat: (
message: string,
extraInfo: {
Expand Down
3 changes: 2 additions & 1 deletion example/generate-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { writeFile } from "node:fs/promises";
import { Integration } from "zod-sockets";
import { actions } from "./actions";
import { config } from "./config";
import typescript from "typescript";

await writeFile(
"example-client.ts",
new Integration({ config, actions }).print(),
new Integration({ typescript, config, actions }).print(),
"utf-8",
);
5 changes: 5 additions & 0 deletions zod-sockets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
"typescript": "catalog:peer",
"zod": "catalog:peer"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"devDependencies": {
"@types/ramda": "^0.31.0",
"socket.io": "catalog:dev",
Expand Down
10 changes: 9 additions & 1 deletion zod-sockets/src/__snapshots__/zts.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = `
] | bigint)[];
};
}>;
looseRecord: Record<"one" | "two", boolean>;
looseRecord: Record<"one" | "two", boolean> & Record<PropertyKey, boolean>;
map: any;
set: any;
intersection: (string & number) | bigint;
Expand All @@ -77,6 +77,13 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = `
catch: number;
pipeline: string;
readonly: string;
extended: {
hex: string;
hash: string;
};
codec: string;
slug: string;
xor: string | number;
}"
`;

Expand Down Expand Up @@ -277,6 +284,7 @@ exports[`zod-to-ts > z.optional() > Zod 4: should add question mark only to opti
required: string;
}
] | undefined;
exact?: string;
}"
`;

Expand Down
6 changes: 4 additions & 2 deletions zod-sockets/src/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ts from "typescript";
import { z } from "zod";
import { ActionsFactory } from "./actions-factory";
import { Config, createSimpleConfig } from "./config";
Expand All @@ -16,6 +17,7 @@ describe("Integration", () => {
});
const input = z.tuple([feature.array()]);
const instance = new Integration({
typescript: ts,
config: sampleConfig,
actions: [
new ActionsFactory(sampleConfig).build({
Expand All @@ -28,7 +30,7 @@ describe("Integration", () => {
expect(instance.print()).toMatchSnapshot();
});

test("should handle namespaces with emission", () => {
test("should handle namespaces with emission", async () => {
const configWithEmission = new Config().addNamespace({
path: "/test",
emission: {
Expand All @@ -38,7 +40,7 @@ describe("Integration", () => {
},
},
});
const instance = new Integration({
const instance = await Integration.create({
config: configWithEmission,
actions: [],
});
Expand Down
127 changes: 60 additions & 67 deletions zod-sockets/src/integration.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import ts from "typescript";
import type ts from "typescript";
import { z } from "zod";
import { AbstractAction } from "./action";
import { makeCleanId } from "./common-helpers";
import { Config } from "./config";
import { makeEventFnSchema } from "./integration-helpers";
import { Namespaces, normalizeNS } from "./namespace";
import { zodToTs } from "./zts";
import {
addJsDoc,
makeType,
printNode,
f,
exportModifier,
} from "./typescript-api";
import { TypescriptAPI } from "./typescript-api";

interface IntegrationProps {
interface IntegrationParams {
typescript: typeof ts;
config: Config<Namespaces>;
actions: AbstractAction[];
/**
Expand All @@ -30,18 +25,20 @@ const fallbackNs = "root";
const registryScopes = ["emission", "actions"];

export class Integration {
/** @internal */
protected readonly api: TypescriptAPI;
#program: ts.Node[] = [];
#aliases: Record<
string, // namespace
Map<object, ts.TypeAliasDeclaration>
> = {};
#ids = {
path: f.createIdentifier("path"),
socket: f.createIdentifier("Socket"),
socketBase: f.createIdentifier("SocketBase"),
ioClient: f.createStringLiteral("socket.io-client"),
emission: f.createIdentifier(makeCleanId(registryScopes[0])),
actions: f.createIdentifier(makeCleanId(registryScopes[1])),
path: "path",
socket: "Socket",
socketBase: "SocketBase",
ioClient: "socket.io-client",
emission: makeCleanId(registryScopes[0]),
actions: makeCleanId(registryScopes[1]),
};
protected registry: Record<
string, // namespace
Expand All @@ -51,48 +48,49 @@ export class Integration {
>
> = {};

#makeAlias(
ns: string,
key: object,
produce: () => ts.TypeNode,
): ts.TypeReferenceNode {
#makeAlias(ns: string, key: object, produce: () => ts.TypeNode): ts.TypeNode {
let name = this.#aliases[ns].get(key)?.name?.text;
if (!name) {
name = `Type${this.#aliases[ns].size + 1}`;
const temp = f.createLiteralTypeNode(f.createNull());
this.#aliases[ns].set(key, makeType(name, temp));
this.#aliases[ns].set(key, makeType(name, produce()));
const temp = this.api.makeLiteralType(null);
this.#aliases[ns].set(key, this.api.makeType(name, temp));
this.#aliases[ns].set(key, this.api.makeType(name, produce()));
}
return f.createTypeReferenceNode(name);
return this.api.ensureTypeNode(name);
}

constructor({
typescript,
config: { namespaces },
actions,
maxOverloads = 3,
}: IntegrationProps) {
}: IntegrationParams) {
this.api = new TypescriptAPI(typescript);
this.#program.push(
f.createImportDeclaration(
this.api.f.createImportDeclaration(
undefined,
f.createImportClause(
true,
this.api.f.createImportClause(
this.api.ts.SyntaxKind.TypeKeyword,
undefined,
f.createNamedImports([
f.createImportSpecifier(
this.api.f.createNamedImports([
this.api.f.createImportSpecifier(
false,
this.#ids.socket,
this.#ids.socketBase,
this.api.f.createIdentifier(this.#ids.socket),
this.api.f.createIdentifier(this.#ids.socketBase),
),
]),
),
this.#ids.ioClient,
this.api.f.createStringLiteral(this.#ids.ioClient),
),
);

for (const [ns, { emission }] of Object.entries(namespaces)) {
this.#aliases[ns] = new Map<z.ZodTypeAny, ts.TypeAliasDeclaration>();
this.registry[ns] = { emission: [], actions: [] };
const commons = { makeAlias: this.#makeAlias.bind(this, ns) };
const commons = {
makeAlias: this.#makeAlias.bind(this, ns),
api: this.api,
};
for (const [event, { schema, ack }] of Object.entries(emission)) {
const node = zodToTs(makeEventFnSchema(schema, ack, maxOverloads), {
isResponse: true,
Expand All @@ -115,69 +113,64 @@ export class Integration {
for (const ns in this.registry) {
const publicName = makeCleanId(ns) || makeCleanId(fallbackNs);

const nsNameNode = f.createVariableStatement(
exportModifier,
f.createVariableDeclarationList(
[
f.createVariableDeclaration(
this.#ids.path,
undefined,
undefined,
f.createStringLiteral(normalizeNS(ns)),
),
],
ts.NodeFlags.Const,
),
const nsNameNode = this.api.makeConst(
this.#ids.path,
this.api.f.createStringLiteral(normalizeNS(ns)),
{ expose: true },
);
addJsDoc(
this.api.addJsDoc(
nsNameNode,
`@desc The actual path of the ${publicName} namespace`,
);

const interfaces = Object.entries(this.registry[ns]).map(
([scope, events]) =>
f.createInterfaceDeclaration(
exportModifier,
this.api.makeInterface(
makeCleanId(scope),
undefined,
undefined,
events.map(({ event, node }) =>
f.createPropertySignature(undefined, event, undefined, node),
this.api.makeInterfaceProp(event, node),
),
{ expose: true },
),
);
const socketNode = f.createTypeAliasDeclaration(
exportModifier,
const socketNode = this.api.makeType(
this.#ids.socket,
undefined,
f.createTypeReferenceNode(this.#ids.socketBase, [
f.createTypeReferenceNode(this.#ids.emission),
f.createTypeReferenceNode(this.#ids.actions),
this.api.ensureTypeNode(this.#ids.socketBase, [
this.#ids.emission,
this.#ids.actions,
]),
{ expose: true },
);
addJsDoc(
this.api.addJsDoc(
socketNode,
`@example const socket: ${publicName}.${this.#ids.socket.text} = io(${publicName}.${this.#ids.path.text})`,
`@example const socket: ${publicName}.${this.#ids.socket} = io(${publicName}.${this.#ids.path})`,
);
this.#program.push(
f.createModuleDeclaration(
exportModifier,
f.createIdentifier(publicName),
f.createModuleBlock([
this.api.f.createModuleDeclaration(
this.api.exportModifier,
this.api.f.createIdentifier(publicName),
this.api.f.createModuleBlock([
nsNameNode,
...this.#aliases[ns].values(),
...interfaces,
socketNode,
]),
ts.NodeFlags.Namespace,
this.api.ts.NodeFlags.Namespace,
),
);
}
}

public static async create(params: Omit<IntegrationParams, "typescript">) {
return new Integration({
...params,
typescript: (await import("typescript"))["default"],
});
}

public print(printerOptions?: ts.PrinterOptions) {
return this.#program
.map((node) => printNode(node, printerOptions))
.map((node) => this.api.printNode(node, printerOptions))
.join("\n\n");
}
}
Loading