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