UNPKG

nuxt-procedures

Version:

Nuxt module to define and easily consume your backend API in a validated and type-safe way using procedures and Zod schemas.

132 lines (125 loc) 4.15 kB
import { join, parse, sep } from 'node:path'; import { defineNuxtModule, createResolver, addServerImports, addTemplate, addServerHandler, updateTemplates, addImports, getLayerDirectories, resolveFiles } from '@nuxt/kit'; import { camelCase } from 'scule'; const toCode = Symbol("toCode"); const toList = Symbol("toList"); class Procedure { constructor(file, url) { this.file = file; this.url = url; } id = "proc" + Math.random().toString(36).slice(2); [toCode]() { return `createCaller<typeof ${this.id}>("${this.url}")`; } [toList]() { return [this]; } } class Router { [toCode]() { return `{ ${Object.entries(this).map(([key, value]) => `"${camelCase(key)}": ${value[toCode]()}`).join(",\n")} }`; } [toList]() { return Object.values(this).flatMap((v) => v[toList]()); } } const module = defineNuxtModule({ meta: { name: "nuxt-procedures", configKey: "procedures" }, // Default configuration options of the Nuxt module defaults: { enabled: true }, async setup(options, nuxt) { if (!options.enabled) { return; } const { resolve } = createResolver(import.meta.url); addServerImports([ { from: resolve("./runtime/define-procedure"), name: "defineProcedure" } ]); const createCallerPath = resolve("./runtime/create-caller.ts"); const handlerPath = resolve("./runtime/handler.ts"); const generatedClient = addTemplate({ filename: "nuxt-procedures/api-client.ts", getContents: () => generateClientCode(createCallerPath), write: true }); const generatedHandler = addTemplate({ filename: "nuxt-procedures/server-handler.ts", getContents: () => generateHandlerCode(handlerPath), write: true }); addServerHandler({ route: "/procedures/**", handler: generatedHandler.dst }); nuxt.hook("builder:watch", async (event, path) => { if (path.includes("procedures")) { updateTemplates({ filter: (t) => t.filename === generatedClient.filename || t.filename === generatedHandler.filename }); } }); const imports = [{ from: generatedClient.dst, name: "apiClient" }]; addImports(imports); } }); async function buildRouterStructure() { const procedureDirs = getLayerDirectories().map((l) => join(l.server, "procedures")).reverse(); const router = new Router(); for (const procDir of procedureDirs) { const files = await resolveFiles(procDir, "**/*.ts"); for (const file of files) { const { name, dir } = parse(file); const namespace = dir.slice(procDir.length + 1); const url = namespace === "" ? `/${name}` : `/${namespace}/${name}`; if (namespace === "") { router[name] = new Procedure(file, url); } else { const parts = namespace.split(sep); let subRouter = router; for (const part of parts) { if (!subRouter[part] || subRouter[part] instanceof Procedure) { subRouter[part] = new Router(); } subRouter = subRouter[part]; } subRouter[name] = new Procedure(file, url); } } } return router; } function withoutExtension(file) { return file.slice(0, -parse(file).ext.length); } async function generateClientCode(createCallerPath) { const router = await buildRouterStructure(); const procedures = router[toList](); return ` import { createCaller } from "${withoutExtension(createCallerPath)}"; ${procedures.map((p) => `import type ${p.id} from "${withoutExtension(p.file)}";`).join("\n")} export const apiClient = ${router[toCode]()}; `; } async function generateHandlerCode(handlerPath) { const router = await buildRouterStructure(); const procedures = router[toList](); return ` import { createProceduresHandler } from "${withoutExtension(handlerPath)}"; ${procedures.map((p) => `import ${p.id} from "${withoutExtension(p.file)}";`).join("\n")} export default createProceduresHandler({ ${procedures.map((p) => `"${p.url.slice(1)}": ${p.id}`).join(",\n ")} }); `; } export { module as default };