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.

114 lines (109 loc) 3.38 kB
import { join, parse, sep } from 'node:path'; import { defineNuxtModule, createResolver, addServerImports, addTemplate, updateTemplates, addImports, 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 generatedClient = addTemplate({ filename: "nuxt-procedures/api-client.ts", getContents: () => generateClientCode(nuxt, createCallerPath), write: true }); nuxt.hook("builder:watch", async (event, path) => { if (path.includes("api")) { updateTemplates({ filter: (t) => t.filename === generatedClient.filename }); } }); const imports = [{ from: generatedClient.dst, name: "apiClient" }]; addImports(imports); } }); async function buildRouterStructure(nuxt) { const procedureDirs = nuxt.options._layers.map( (l) => ( // join(l.config.serverDir ?? "server", "procedures") join(l.config.serverDir ?? "server", "api") ) ); 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(nuxt, createCallerPath) { const router = await buildRouterStructure(nuxt); 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]()}; `; } export { module as default };