vite-plugin-api-routes
Version:
A Vite.js plugin that creates API routes by mapping the directory structure, similar to Next.js API Routes. This plugin enhances the functionality for backend development using Vite.
663 lines (647 loc) • 18.5 kB
JavaScript
// src/model.ts
import path from "slash-path";
var assertConfig = (opts) => {
let {
moduleId = "@api",
mode = "legacy",
root = process.cwd(),
cacheDir = ".api",
server = path.join(cacheDir, "server.js"),
handler = path.join(cacheDir, "handler.js"),
configure = path.join(cacheDir, "configure.js"),
routeBase = "api",
dirs = [{ dir: "src/api", route: "", exclude: [], skip: false }],
include = ["**/*.ts", "**/*.js"],
exclude = [],
mapper = {},
filePriority = 100,
paramPriority = 110,
forceRestart = false,
disableBuild = false,
clientOutDir = "dist/client",
clientMinify = false,
clientBuild = (config) => config,
serverOutDir = "dist",
serverMinify = false,
serverBuild = (config) => config
} = opts;
if (moduleId !== "@api") {
console.warn("The moduleId will be remove in the next release");
}
if (cacheDir !== ".api") {
console.warn("The cacheDir will be remove in the next release");
}
cacheDir = path.join(root, cacheDir);
dirs = dirs.map((it) => {
it.dir = path.join(root, it.dir);
return it;
});
mapper = {
default: { method: "use", priority: 10 },
USE: { method: "use", priority: 20 },
GET: { method: "get", priority: 30 },
POST: { method: "post", priority: 40 },
PATCH: { method: "patch", priority: 50 },
PUT: { method: "put", priority: 60 },
DELETE: { method: "delete", priority: 70 },
// Overwrite
...mapper
};
if (mode === "isolated") {
delete mapper.default;
}
routeBase = path.join("/", routeBase);
clientOutDir = path.join(root, clientOutDir);
serverOutDir = path.join(root, serverOutDir);
const serverFile = path.join(root, server);
const handlerFile = path.join(root, handler);
const routersFile = path.join(cacheDir, "routers.js");
const configureFile = path.join(root, configure);
const mapperList = Object.entries(mapper).map(([name, value]) => {
if (value === false) {
return {
name,
method: "",
priority: "000"
};
}
if (typeof value === "string") {
return {
name,
method: value,
priority: "010"
};
}
return {
name,
method: value.method,
priority: value.priority.toString().padStart(3, "0")
};
}).filter((it) => it.method !== "");
if (mode === "isolated") {
include = mapperList.map((it) => `**/${it.name}.{js,ts}`);
}
const watcherList = dirs.map((it) => it.dir);
watcherList.push(configureFile);
watcherList.push(handlerFile);
return {
mode,
moduleId,
server,
handler,
configure,
root,
serverFile,
handlerFile,
routersFile,
filePriority: filePriority.toString().padStart(3, "0"),
paramPriority: paramPriority.toString().padStart(3, "0"),
configureFile,
routeBase,
dirs,
include,
exclude,
mapperList,
watcherList,
cacheDir,
forceRestart,
disableBuild,
clientOutDir,
clientMinify,
clientBuild,
serverOutDir,
serverMinify,
serverBuild
};
};
// src/plugin-build/index.ts
import path2 from "slash-path";
import { build } from "vite";
var ENTRY_NONE = "____.html";
var simplePath = (filePath) => filePath.replace(/(\.[^\.]+)$/, "").replace(/^[\/\\]/gi, "").replace(/[\/\\]$/, "");
var doBuildServer = async (apiConfig, viteConfig) => {
const {
root,
serverOutDir,
clientOutDir,
serverMinify,
serverBuild,
routeBase,
serverFile,
configureFile,
handlerFile,
routersFile,
cacheDir
} = apiConfig;
const binFiles = [configureFile, handlerFile, routersFile];
const clientDir = path2.relative(serverOutDir, clientOutDir);
const viteServer = await serverBuild({
appType: "custom",
root,
publicDir: "private",
define: {
"API_ROUTES.BASE": JSON.stringify(viteConfig.base),
"API_ROUTES.BASE_API": JSON.stringify(
path2.join(viteConfig.base, routeBase)
),
"API_ROUTES.PUBLIC_DIR": JSON.stringify(clientDir)
},
build: {
outDir: serverOutDir,
ssr: serverFile,
write: true,
minify: serverMinify,
target: "esnext",
emptyOutDir: true,
rollupOptions: {
external: viteConfig.build?.rollupOptions?.external,
output: {
format: "es",
entryFileNames: "app.js",
chunkFileNames: "bin/[name]-[hash].js",
assetFileNames: "assets/[name].[ext]",
manualChunks: (id) => {
const isApi = apiConfig.dirs.find((it) => id.includes(it.dir));
if (isApi) {
const file = path2.join(
apiConfig.routeBase || "",
isApi.route || "",
path2.relative(isApi.dir, id)
);
return simplePath(file);
}
const isBin = binFiles.find((bin) => id.includes(bin));
if (isBin) {
return "index";
}
if (id.includes("node_modules")) {
const pkg = id.split("node_modules/")[1];
const lib = path2.join("libs", pkg);
return simplePath(lib);
}
}
},
onwarn: (warning, handler) => {
if (warning.code === "MISSING_EXPORT" && warning.id.startsWith(cacheDir)) {
return;
}
handler(warning);
}
}
}
});
await build(viteServer);
};
var doBuildClient = async (apiConfig, viteConfig) => {
const { root, clientOutDir, clientMinify, clientBuild } = apiConfig;
const preloadFiles = [
"modulepreload-polyfill",
"commonjsHelpers",
"vite/",
"installHook"
];
const viteClient = await clientBuild({
root,
build: {
outDir: clientOutDir,
write: true,
minify: clientMinify,
emptyOutDir: true,
rollupOptions: {
external: viteConfig.build?.rollupOptions?.external,
output: {
manualChunks: (id) => {
const isInternal = preloadFiles.find((it) => id.includes(it));
if (isInternal) {
return "preload";
}
if (id.includes("node_modules")) {
return "vendor";
}
}
}
}
}
});
await build(viteClient);
};
var apiRoutesBuild = (apiConfig) => {
if (apiConfig.disableBuild) {
return null;
}
let viteConfig = {};
return {
name: "vite-plugin-api-routes:build",
enforce: "pre",
apply: "build",
config: () => {
if (process.env.IS_API_ROUTES_BUILD) return {};
return {
build: {
emptyOutDir: true,
copyPublicDir: false,
write: false,
rollupOptions: {
input: {
main: ENTRY_NONE
}
}
}
};
},
resolveId: (id) => {
if (id === ENTRY_NONE) {
return id;
}
return null;
},
load: (id) => {
if (id === ENTRY_NONE) {
return "";
}
return null;
},
configResolved: (config) => {
viteConfig = config;
},
buildStart: async () => {
if (process.env.IS_API_ROUTES_BUILD) return;
process.env.IS_API_ROUTES_BUILD = "true";
viteConfig.logger.info("");
viteConfig.logger.info("\x1B[1m\x1B[31mSERVER BUILD\x1B[0m");
await doBuildServer(apiConfig, viteConfig);
viteConfig.logger.info("");
viteConfig.logger.info("\x1B[1m\x1B[31mCLIENT BUILD\x1B[0m");
await doBuildClient(apiConfig, viteConfig);
viteConfig.logger.info("");
}
};
};
// src/plugin-route/index.ts
import path5 from "slash-path";
// src/plugin-route/routersFile.ts
import fs from "fs";
import path4 from "slash-path";
// src/plugin-route/common.ts
import fg from "fast-glob";
import path3 from "slash-path";
var byKey = (a, b) => a.key.localeCompare(b.key);
var isParam = (name) => /:|\[|\$/.test(name);
var createKeyRoute = (route, apiConfig) => {
return route.split("/").filter((it) => it).map((n) => {
const s = "_" + n;
const p = apiConfig.mapperList.find((r) => r.name === n);
if (p) {
return p.priority + s;
} else if (isParam(n)) {
return apiConfig.paramPriority + s;
}
return apiConfig.filePriority + s;
}).join("_");
};
var parseFileToRoute = (names) => {
const route = names.split("/").map((name) => {
name = name.replaceAll("$", ":").replaceAll("[", ":").replaceAll("]", "");
return name;
}).join("/");
return route.replace(/\.[^.]+$/, "").replaceAll(/index$/gi, "").replaceAll(/_index$/gi, "");
};
var getAllFileRouters = (apiConfig) => {
let { dirs, include, exclude } = apiConfig;
const currentMode = process.env.NODE_ENV;
return dirs.filter((dir) => {
if (dir.skip === true || dir.skip === currentMode) {
return false;
}
return true;
}).flatMap((it) => {
it.exclude = it.exclude || [];
const ignore = [...exclude, ...it.exclude];
const files = fg.sync(include, {
ignore,
onlyDirectories: false,
dot: true,
unique: true,
cwd: it.dir
});
return files.map((file) => {
const routeFile = path3.join("/", it.route, file);
const route = parseFileToRoute(routeFile);
const key = createKeyRoute(route, apiConfig);
const relativeFile = path3.relative(
apiConfig.root,
path3.join(it.dir, file)
);
return {
name: path3.basename(routeFile),
file: relativeFile,
route,
key
};
});
}).map((it) => ({
varName: "API_",
name: it.name,
file: it.file,
route: it.route,
key: it.key
})).sort(byKey).map((it, ix) => {
it.varName = "API_" + ix.toString().padStart(3, "0");
return it;
});
};
var parseMethodRouters = (fileRoutes, apiConfig) => {
if (apiConfig.mode === "isolated") {
return fileRoutes.map((r) => {
const m = apiConfig.mapperList.find(
(m2) => r.route.endsWith(`/${m2.name}`)
);
if (m == null) {
return null;
}
const re = new RegExp(`${m.name}$`);
const route = r.route.replace(re, "");
return {
key: r.key,
source: r.file,
method: m.method,
route,
url: path3.join("/", apiConfig.routeBase, route),
cb: r.varName + ".default"
};
}).filter((r) => !!r);
}
return fileRoutes.flatMap((it) => {
return apiConfig.mapperList.map((m) => {
const route = path3.join(it.route, m.name);
const key = createKeyRoute(route, apiConfig);
return {
...it,
key,
name: m.name,
method: m.method
};
});
}).sort(byKey).map((it) => {
let cb = it.varName + "." + it.name;
const source = it.file + "?fn=" + it.name;
const route = it.route;
return {
key: it.key,
source,
method: it.method,
route,
url: path3.join("/", apiConfig.routeBase, route),
cb
};
});
};
// src/plugin-route/routersFile.ts
var writeRoutersFile = (apiConfig, vite) => {
const { moduleId, cacheDir, routersFile } = apiConfig;
if (routersFile.startsWith(cacheDir)) {
const fileRouters = getAllFileRouters(apiConfig);
const methodRouters = parseMethodRouters(fileRouters, apiConfig);
const max = methodRouters.map((it) => {
it.url = path4.join("/", vite.base, apiConfig.routeBase, it.route);
return it;
}).reduce(
(max2, it) => {
const cb = it.cb.length;
const url = it.url.length;
const route = it.route.length;
const method = it.method.length;
const source = it.source.length;
max2.cb = cb > max2.cb ? cb : max2.cb;
max2.url = url > max2.url ? url : max2.url;
max2.route = route > max2.route ? route : max2.route;
max2.method = method > max2.method ? method : max2.method;
max2.source = source > max2.source ? source : max2.source;
return max2;
},
{ cb: 0, method: 0, route: 0, url: 0, source: 0 }
);
const debug = methodRouters.map(
(it) => "// " + it.method.toUpperCase().padEnd(max.method + 1, " ") + it.url.padEnd(max.url + 4, " ") + it.source
).join("\n").trim();
const writeRouter = (c) => {
return [
" ",
`${c.cb}`.padEnd(max.cb + 1, " "),
` && { cb: `,
`${c.cb}`.padEnd(max.cb + 1, " "),
", method: ",
`"${c.method}"`.padEnd(max.method + 3, " "),
`, route: `,
`"${c.route}"`.padEnd(max.route + 3, " "),
", url: ",
`"${c.url}"`.padEnd(max.url + 3, " "),
", source: ",
`"${c.source}"`.padEnd(max.source + 3, " "),
"}"
].join("");
};
const importFiles = fileRouters.map(
(it) => `import * as ${it.varName} from "${moduleId}/root/${it.file}";`
).join("\n");
const internalRouter = methodRouters.map((c) => writeRouter(c)).join(",\n");
const code = `
// Files Imports
import * as configure from "${moduleId}/configure";
${importFiles}
// Public RESTful API Methods and Paths
// This section describes the available HTTP methods and their corresponding endpoints (paths).
${debug}
const internal = [
${internalRouter}
].filter(it => it);
export const routers = internal.map((it) => {
const { method, route, url, source } = it;
return { method, url, route, source };
});
export const endpoints = internal.map(
(it) => it.method?.toUpperCase() + "\\t" + it.url
);
export const applyRouters = (applyRouter) => {
internal.forEach((it) => {
it.cb = configure.callbackBefore?.(it.cb, it) || it.cb;
applyRouter(it);
});
};
`;
fs.writeFileSync(routersFile, code);
}
};
// src/plugin-route/index.ts
var apiRoutesRoute = (apiConfig) => {
const isReload = (file) => {
file = path5.slash(file);
return apiConfig.watcherList.find((it) => file.startsWith(it));
};
return {
name: "vite-plugin-api-routes:route",
enforce: "pre",
config: () => {
return {
resolve: {
alias: {
[`${apiConfig.moduleId}/root`]: apiConfig.root,
[`${apiConfig.moduleId}/server`]: apiConfig.serverFile,
[`${apiConfig.moduleId}/handler`]: apiConfig.handlerFile,
[`${apiConfig.moduleId}/routers`]: apiConfig.routersFile,
[`${apiConfig.moduleId}/configure`]: apiConfig.configureFile
}
}
};
},
configResolved: (viteConfig) => {
writeRoutersFile(apiConfig, viteConfig);
},
handleHotUpdate: async (data) => {
if (isReload(data.file)) {
return [];
}
},
configureServer: async (devServer) => {
const {
//
watcher,
restart,
config: viteConfig
} = devServer;
const onReload = (file) => {
if (isReload(file)) {
writeRoutersFile(apiConfig, viteConfig);
watcher.off("add", onReload);
watcher.off("change", onReload);
if (apiConfig.forceRestart) {
restart(true);
}
}
};
watcher.on("add", onReload);
watcher.on("change", onReload);
}
};
};
// src/plugin-serve/index.ts
import express from "express";
import path6 from "slash-path";
var apiRoutesServe = (config) => {
return {
name: "vite-plugin-api-routes:serve",
enforce: "pre",
apply: "serve",
configureServer: async (devServer) => {
const {
//
config: vite,
middlewares,
ssrLoadModule,
ssrFixStacktrace
} = devServer;
const baseApi = path6.join(vite.base, config.routeBase);
const { viteServerBefore, viteServerAfter } = await ssrLoadModule(
config.configure,
{
fixStacktrace: true
}
);
var appServer = express();
viteServerBefore?.(appServer, devServer, vite);
appServer.use("/", async (req, res, next) => {
try {
const { handler } = await ssrLoadModule(config.handler, {
fixStacktrace: true
});
handler(req, res, next);
} catch (error) {
ssrFixStacktrace(error);
process.exitCode = 1;
next(error);
}
});
viteServerAfter?.(appServer, devServer, vite);
middlewares.use(baseApi, appServer);
}
};
};
// src/utils.ts
import fs2 from "fs-extra";
import path7 from "path";
import { fileURLToPath } from "url";
var getPluginDirectory = () => {
if (typeof __dirname === "undefined") {
const filename = fileURLToPath(import.meta.url);
return path7.dirname(filename);
} else {
return __dirname;
}
};
var findDirPlugin = (dirname, max = 5) => {
const basedir = getPluginDirectory();
let relative = "/";
let dirPath = "";
for (var i = 0; i < max; i++) {
dirPath = path7.join(basedir, relative, dirname);
if (fs2.existsSync(dirPath)) {
return dirPath;
}
relative += "../";
}
throw Error(`Not found: ${dirPath}`);
};
var cleanDirectory = (target) => {
if (fs2.existsSync(target)) {
fs2.rmSync(target, { recursive: true, force: true });
}
fs2.mkdirSync(target, { recursive: true });
};
var copyFilesDirectory = (origin, target, {
files = [],
oldId = "",
newId = ""
}) => {
files.forEach((file) => {
const sourceFilePath = path7.join(origin, file);
const targetFilePath = path7.join(target, file);
if (oldId !== newId) {
let fileContent = fs2.readFileSync(sourceFilePath, "utf-8");
fileContent = fileContent.replace(new RegExp(oldId, "g"), newId);
fs2.writeFileSync(targetFilePath, fileContent, "utf-8");
} else {
fs2.copySync(sourceFilePath, targetFilePath, { overwrite: true });
}
});
};
// src/index.ts
var pluginAPIRoutes = (opts = {}) => {
const apiConfig = assertConfig(opts);
const apiDir = findDirPlugin(".api");
cleanDirectory(apiConfig.cacheDir);
copyFilesDirectory(apiDir, apiConfig.cacheDir, {
files: ["configure.js", "handler.js", "server.js"],
oldId: "vite-plugin-api-routes",
newId: apiConfig.moduleId
});
copyFilesDirectory(apiDir, apiConfig.cacheDir, {
files: ["env.d.ts"],
oldId: "@api",
newId: apiConfig.moduleId
});
return [
//
apiRoutesRoute(apiConfig),
apiRoutesServe(apiConfig),
apiRoutesBuild(apiConfig)
];
};
var pluginAPI = pluginAPIRoutes;
var createAPI = pluginAPIRoutes;
var index_default = pluginAPIRoutes;
export {
createAPI,
index_default as default,
pluginAPI,
pluginAPIRoutes
};