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
JavaScript
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 };