Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
120 changes: 49 additions & 71 deletions src/cli2/commands/codegen/route-types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Command } from "commander"
import fs from "node:fs/promises"
import { BaseCommand } from "../../base-command.js"
import { ResolvedWinterSpecConfig } from "src/config/utils.js"
import { extractRouteSpecsFromAST } from "src/lib/codegen/extract-route-specs-from-ast.js"
import { extractRouteSpecsFromAST } from "../../../lib/codegen/extract-route-specs-from-ast.js"
import Debug from "debug"

const debug = Debug("winterspec:CodeGenRouteTypes")
Expand All @@ -17,86 +15,66 @@ export class CodeGenRouteTypes extends BaseCommand {
.option("--root <path>", "Path to your project root")
.option("--tsconfig <path>", "Path to your tsconfig.json")
.option("--routes-directory <path>", "Path to your routes directory")
.option("--platform <platform>", "The platform to bundle for")
.action(async (options) => {
debug("Running with config", options)
const config = await this.loadConfig(options)
debug("Config loaded.")

debug("Extracting route specs from AST...")
const { project, routes, renderType } = await extractRouteSpecsFromAST({
const { routes, renderType } = await extractRouteSpecsFromAST({
tsConfigFilePath: config.tsconfigPath,
routesDirectory: config.routesDirectory,
})
debug("Route specs extracted.")

project.createSourceFile(
"manifest.ts",
`
import {z} from "zod"
const routeEntries = routes.map(
({
route,
httpMethods,
jsonResponseZodOutputType,
jsonBodyZodInputType,
commonParamsZodInputType,
queryParamsZodInputType,
urlEncodedFormDataZodInputType,
}) => {
const parts = [
`route: "${route}"`,
`method: ${httpMethods.map((m) => `"${m}"`).join(" | ")}`,
]
if (jsonResponseZodOutputType) {
parts.push(
`jsonResponse: ${renderType(jsonResponseZodOutputType)}`
)
}
if (jsonBodyZodInputType) {
parts.push(`jsonBody: ${renderType(jsonBodyZodInputType)}`)
}
if (commonParamsZodInputType) {
parts.push(
`commonParams: ${renderType(commonParamsZodInputType)}`
)
}
if (queryParamsZodInputType) {
parts.push(`queryParams: ${renderType(queryParamsZodInputType)}`)
}
if (urlEncodedFormDataZodInputType) {
parts.push(
`urlEncodedFormData: ${renderType(
urlEncodedFormDataZodInputType
)}`
)
}

export type Routes = {
${routes
.map(
({
route,
httpMethods,
jsonResponseZodOutputType,
jsonBodyZodInputType,
commonParamsZodInputType,
queryParamsZodInputType,
urlEncodedFormDataZodInputType,
}) => {
return ` "${route}": {
route: "${route}"
method: ${httpMethods.map((m) => `"${m}"`).join(" | ")}
${
jsonResponseZodOutputType
? `jsonResponse: ${renderType(jsonResponseZodOutputType)}`
: ""
}
${
jsonBodyZodInputType
? `jsonBody: ${renderType(jsonBodyZodInputType)}`
: ""
}
${
commonParamsZodInputType
? `commonParams: ${renderType(commonParamsZodInputType)}`
: ""
}
${
queryParamsZodInputType
? `queryParams: ${renderType(queryParamsZodInputType)}`
: ""
}
${
urlEncodedFormDataZodInputType
? `urlEncodedFormData: ${renderType(
urlEncodedFormDataZodInputType
)}`
: ""
}
}`
}
)
.join("\n")}
}

type ExtractOrUnknown<T, Key extends string> = Key extends keyof T ? T[Key] : unknown;

export type RouteResponse<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "jsonResponse">
export type RouteRequestBody<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "jsonBody"> & ExtractOrUnknown<Routes[Path], "commonParams">
export type RouteRequestParams<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "queryParams"> & ExtractOrUnknown<Routes[Path], "commonParams">
`
return ` "${route}": {\n ${parts.join("\n ")}\n }`
}
)

const result = project.emitToMemory({ emitOnlyDtsFiles: true })
await fs.writeFile(
options.output,
result.getFiles().find((f) => f.filePath.includes("/manifest.d.ts"))!
.text
)
const content =
`export type Routes = {\n${routeEntries.join("\n")}\n}\n\n` +
`type ExtractOrUnknown<T, Key extends string> = Key extends keyof T ? T[Key] : unknown\n\n` +
`export type RouteResponse<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "jsonResponse">\n` +
`export type RouteRequestBody<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "jsonBody"> & ExtractOrUnknown<Routes[Path], "commonParams">\n` +
`export type RouteRequestParams<Path extends keyof Routes> = ExtractOrUnknown<Routes[Path], "queryParams"> & ExtractOrUnknown<Routes[Path], "commonParams">\n`

await fs.writeFile(options.output, content)
})
}
}
25 changes: 25 additions & 0 deletions tests/cli/codegen/route-types2/api/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from "zod"
import { withWinterSpec } from "../with-winter-spec.js"

export const jsonResponse = z.object({
foo: z.object({
id: z.string().uuid(),
name: z.string(),
}),
})

export default withWinterSpec({
auth: "none",
methods: ["GET", "POST"],
jsonBody: z.object({
foo_id: z.string().uuid(),
}),
jsonResponse,
})((req) => {
return Response.json({
foo: {
id: "example-id",
name: "foo",
},
})
})
15 changes: 15 additions & 0 deletions tests/cli/codegen/route-types2/api/importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { withWinterSpec } from "../with-winter-spec.js"
import { jsonResponse } from "./foo.js"

export default withWinterSpec({
auth: "none",
methods: ["PUT"],
jsonResponse,
})((req) => {
return Response.json({
foo: {
id: "example-id",
name: "foo",
},
})
})
35 changes: 35 additions & 0 deletions tests/cli/codegen/route-types2/api/many-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { z } from "zod"
import { withWinterSpec } from "../with-winter-spec.js"

const manyParams = z.object({
this_has: z.string(),
many: z.string(),
params: z.string(),
to: z.string(),
make: z.string(),
sure: z.string(),
type_is: z.string(),
fully: z.string(),
expanded: z.string(),
})

export default withWinterSpec({
auth: "none",
methods: ["GET", "POST"],
jsonBody: manyParams.extend({
and: manyParams,
}),
jsonResponse: z.object({
foo: z.object({
id: z.string().uuid(),
name: z.string(),
}),
}),
})((req) => {
return Response.json({
foo: {
id: "example-id",
name: "foo",
},
})
})
21 changes: 21 additions & 0 deletions tests/cli/codegen/route-types2/api/param-transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from "zod"
import { withWinterSpec } from "../with-winter-spec.js"

export default withWinterSpec({
auth: "none",
methods: ["GET", "POST"],
jsonBody: z.object({
// this should be written to route types as a string rather than a number
foo_id: z.string().transform((v) => Number(v)),
}),
jsonResponse: z.object({
ok: z.boolean(),
}),
})((req) => {
return Response.json({
foo: {
id: "example-id",
name: "foo",
},
})
})
12 changes: 12 additions & 0 deletions tests/cli/codegen/route-types2/api/query-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod"
import { withWinterSpec } from "../with-winter-spec.js"

export default withWinterSpec({
auth: "none",
methods: ["GET"],
queryParams: z.object({
foo_id: z.string().uuid(),
}),
})((req) => {
return Response.json({})
})
23 changes: 23 additions & 0 deletions tests/cli/codegen/route-types2/api/union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from "zod"
import { withWinterSpec } from "../with-winter-spec.js"

export default withWinterSpec({
auth: "none",
methods: ["GET", "POST"],
jsonBody: z.object({
foo_id: z.string().uuid(),
}),
jsonResponse: z.union([
z.object({
foo_id: z.string(),
}),
z.boolean().array(),
]),
})((req) => {
return Response.json({
foo: {
id: "example-id",
name: "foo",
},
})
})
Loading