universal-middleware
Version:
Write middlewares and handlers once, target [srvx](https://github.com/magne4000/universal-middleware/tree/main/packages/adapter-hono), [Express](https://github.com/magne4000/universal-middleware/tree/main/packages/adapter-express), [Cloudflare](https://gi
537 lines (535 loc) • 18.3 kB
JavaScript
// src/plugin.ts
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, parse, posix, resolve } from "node:path";
import { packageUp } from "package-up";
var defaultWrappers = [
"hono",
"express",
"hattip",
"webroute",
"fastify",
"h3",
"cloudflare-worker",
"cloudflare-pages",
"vercel-edge",
"vercel-node",
"elysia",
"srvx"
];
var maybeExternals = [
"@universal-middleware/hono",
"@universal-middleware/express",
"@universal-middleware/hattip",
"@universal-middleware/webroute",
"@universal-middleware/fastify",
"@universal-middleware/h3",
"@universal-middleware/cloudflare",
"@universal-middleware/elysia",
"@universal-middleware/srvx",
"@universal-middleware/vercel"
];
var typesByServer = {
hono: {
middleware: "HonoMiddleware",
handler: "HonoHandler"
},
express: {
middleware: "NodeMiddleware",
handler: "NodeHandler"
},
hattip: {
middleware: "HattipMiddleware",
handler: "HattipHandler"
},
fastify: {
middleware: "FastifyMiddleware",
handler: "FastifyHandler"
},
h3: {
middleware: "H3Middleware",
handler: "H3Handler"
},
webroute: {
middleware: "WebrouteMiddleware",
handler: "WebrouteHandler",
selfImports: ["type MiddlewareFactoryDataResult"],
outContext: (type) => `MiddlewareFactoryDataResult<typeof ${type}>`
},
srvx: {
middleware: "SrvxMiddleware",
handler: "SrvxHandler"
},
"cloudflare-worker": {
handler: "CloudflareHandler",
target: "cloudflare"
},
"cloudflare-pages": {
middleware: "CloudflarePagesFunction",
handler: "CloudflarePagesFunction",
typeHandler: "createPagesFunction",
typeMiddleware: "createPagesFunction",
generics: (type) => type === "handler" ? "Args, InContext, OutContext" : "Args, InContext, OutContext",
target: "cloudflare"
},
"vercel-edge": {
handler: "VercelEdgeHandler",
typeHandler: "createEdgeHandler",
target: "vercel"
},
"vercel-node": {
handler: "VercelNodeHandler",
typeHandler: "createNodeHandler",
target: "vercel"
},
elysia: {
handler: "ElysiaHandler",
middleware: "ElysiaMiddleware"
}
};
var namespace = "virtual:universal-middleware";
var versionRange = "^0";
function getVirtualInputs(type, handler, wrappers = defaultWrappers) {
const parsed = parse(handler);
return wrappers.filter((w) => Boolean(typesByServer[w][type])).map((server) => ({
server,
type,
handler,
get value() {
return `${namespace}:${this.server}:${this.type}:${this.handler}`;
},
get key() {
return join(parsed.dir, `universal-${this.server}-${this.type}-${parsed.name}`);
}
}));
}
function filterInput(input, importer) {
if (importer !== void 0 && !importer.startsWith("virtual:universal-middleware")) {
return null;
}
if (input.match(/(^|\.|\/|\\\\)handler\.[cm]?[jt]sx?$/)) {
return "handler";
}
if (input.match(/(^|\.|\/|\\\\)middleware\.[cm]?[jt]sx?$/)) {
return "middleware";
}
return null;
}
function normalizeInput(input, options) {
const keys = /* @__PURE__ */ new Set();
function getTuple(key, value) {
if (keys.has(key)) {
throw new Error(`Conflict on entry ${key}: ${value}`);
}
keys.add(key);
return [key, value];
}
if (typeof input === "string") {
const filtered = filterInput(input);
if (filtered) {
const parsed = parse(input);
const tuple = getTuple(join(parsed.dir, parsed.name), input);
return {
[tuple[0]]: tuple[1]
};
}
} else if (Array.isArray(input)) {
let i = 0;
const res = Object.fromEntries(
input.map((e) => {
if (typeof e === "string") {
if (filterInput(e)) {
i += 1;
}
const parsed = parse(e);
return getTuple(join(parsed.dir, parsed.name), e);
}
return getTuple(e.out, e.in);
})
);
if (!options?.ignoreRecommendations && i >= 2) {
console.warn("Prefer using an object for esbuild/rollup `entryPoints` instead of an array");
}
return res;
} else if (input && typeof input === "object") {
return input;
}
return null;
}
function appendVirtualInputs(input, wrappers = defaultWrappers) {
for (const v of Object.values(input)) {
const filtered = filterInput(v);
if (filtered) {
const virtualInputs = getVirtualInputs(filtered, v, wrappers);
for (const vinput of virtualInputs) {
input[vinput.key] = vinput.value;
}
}
}
}
function applyOutbase(input, outbase) {
if (!outbase) return input;
const re = new RegExp(`^(${outbase.replaceAll("\\\\", "/")}|${outbase.replaceAll("/", "\\\\")})/?`, "gu");
return Object.keys(input).reduce(
(acc, key) => {
acc[key.replace(re, "")] = input[key];
return acc;
},
{}
);
}
function shouldLoad(id) {
if (id.startsWith(namespace)) {
const [, , target, type] = id.split(":");
const info = typesByServer[target];
const t = info[type];
return Boolean(t);
}
return false;
}
function load(id, resolve2) {
const [, , target, type, handler] = id.split(":");
const info = typesByServer[target];
const fn = type === "handler" ? info.typeHandler ?? "createHandler" : info.typeMiddleware ?? "createMiddleware";
const code = `import { ${fn} } from "universal-middleware/adapters/${info.target ?? target}";
import ${type} from "${resolve2 ? resolve2(handler, type) : handler}";
export default ${fn}(${type});
`;
return { code };
}
function loadDts(id, resolve2) {
const [, , target, type, handler] = id.split(":");
const info = typesByServer[target];
const fn = type === "handler" ? info.typeHandler ?? "createHandler" : info.typeMiddleware ?? "createMiddleware";
const t = info[type];
const generics = info.generics ? info.generics(type) : type === "handler" ? "Args, InContext" : "Args, InContext, OutContext";
if (t === void 0) return;
const selfImports = [fn, `type ${t}`, ...info.selfImports ?? []];
const code = `import { type UniversalMiddleware, type RuntimeAdapterTarget } from '@universal-middleware/core';
import { ${selfImports.join(", ")} } from "universal-middleware/adapters/${info.target ?? target}";
import ${type} from "${resolve2 ? resolve2(handler, type) : handler}";
type ExtractT<T> = T extends (...args: infer X) => any ? X : never;
type ExtractInContext<T> = T extends (...args: any[]) => UniversalMiddleware<infer X> ? unknown extends X ? Universal.Context : X : {};
export type Target = '${target}';
export type RuntimeAdapter = RuntimeAdapterTarget<Target>;
export type InContext = ExtractInContext<typeof ${type}>;
export type OutContext = ${info.outContext?.(type) ?? "unknown"};
export type Args = ExtractT<typeof ${type}>;
export type Middleware = ReturnType<ReturnType<typeof ${fn}<${generics}>>>;
export default ${fn}(${type}) as (...args: Args) => Middleware;
`;
return { code };
}
function findDuplicateReports(reports) {
const exportCounts = {};
const duplicates = /* @__PURE__ */ new Map();
for (const report of reports) {
exportCounts[report.exports] = (exportCounts[report.exports] || 0) + 1;
}
for (const report of reports) {
if (exportCounts[report.exports] > 1) {
if (!duplicates.has(report.exports)) {
duplicates.set(report.exports, []);
}
duplicates.get(report.exports)?.push(report);
}
}
return duplicates;
}
function formatDuplicatesForErrorMessage(duplicates) {
let formattedMessage = "The following files have overlapping exports:\n";
duplicates.forEach((reports, exportValue) => {
formattedMessage += `exports: ${exportValue}
`;
for (const report of reports) {
formattedMessage += ` in: ${report.in}, out: ${report.out}
`;
}
});
formattedMessage += "Make sure you are using esbuild `entryPoints` object syntax or that `serversExportNames` option contains [dir].";
return formattedMessage;
}
function genBundleInfo(input, findDest) {
const entries = Object.entries(input);
return Object.fromEntries(
entries.map(([k, v]) => {
const dest = findDest(v);
const parsed = parse(dest);
return [
v,
{
in: v,
out: dest,
dts: dest.replace(/\.js$/, ".d.ts"),
id: k,
dir: parsed.dir,
name: parsed.name,
type: filterInput(v),
exports: ""
}
];
})
);
}
function fixBundleExports(bundle, options) {
for (const [k, v] of Object.entries(bundle)) {
if (!k.startsWith(namespace)) {
if (options.entryExportNames === ".") {
v.exports = ".";
} else {
v.exports = `./${posix.normalize(
options.entryExportNames.replace("[dir]", v.dir).replace("[name]", v.name).replace("[type]", v.type)
).replaceAll("\\", "/")}`;
}
}
}
for (const [k, v] of Object.entries(bundle)) {
if (k.startsWith(namespace)) {
const [, , server, type, handler] = k.split(":");
if (options.serversExportNames === ".") {
v.exports = ".";
} else {
v.exports = `./${posix.normalize(
options.serversExportNames.replace("[name]", bundle[handler].name).replace("[dir]", bundle[handler].dir).replace("[type]", type).replace("[server]", server)
).replaceAll("\\", "/")}`;
}
}
}
return bundle;
}
async function generateDts(content, outFile) {
const { isolatedDeclaration } = await import("oxc-transform");
const code = isolatedDeclaration("file.ts", content, {
sourcemap: false
});
await mkdir(dirname(outFile), { recursive: true });
await writeFile(outFile, code.code);
}
async function genDts(bundle, options) {
if (options?.dts === false) return;
for (const value of Object.values(bundle)) {
if (!value.in.startsWith(namespace)) continue;
const res = loadDts(value.in, (handler) => posix.relative(value.dts, bundle[handler].dts).replace(/^\.\./, "."));
if (!res) continue;
await generateDts(res.code, value.dts);
}
}
function genReport(bundle, options) {
const reports = Object.values(bundle).reduce((acc, curr) => {
const report = {
in: curr.in,
out: curr.out,
type: curr.type,
exports: curr.exports
};
if (options?.dts !== false) {
report.dts = curr.dts;
}
acc.push(report);
return acc;
}, []);
const duplicates = findDuplicateReports(reports);
if (duplicates.size > 0) {
const message = formatDuplicatesForErrorMessage(duplicates);
throw new Error(message);
}
return reports;
}
async function readAndEditPackageJson(reports, options) {
const packageJsonPath = await packageUp();
if (!packageJsonPath) {
throw new Error("Cannot find package.json");
}
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
if (options?.externalDependencies === true) {
packageJson.dependencies ??= {};
for (const external of maybeExternals) {
packageJson.dependencies[external] = versionRange;
}
}
packageJson.exports ??= {};
for (const report of reports) {
packageJson.exports[report.exports] = {
types: report.dts ? `./${report.dts}` : void 0,
import: `./${report.out}`,
default: `./${report.out}`
};
}
return {
path: packageJsonPath,
packageJson
};
}
var universalMiddleware = (options) => {
const serversExportNames = options?.serversExportNames ?? "./[dir]/[name]-[type]-[server]";
const entryExportNames = options?.entryExportNames ?? "./[dir]/[name]-[type]";
let normalizedInput = null;
return {
name: namespace,
enforce: "post",
rollup: {
resolveId(id, importer) {
if (id.startsWith(namespace) || filterInput(id, importer)) {
return id;
}
},
options(opts) {
normalizedInput = normalizeInput(opts.input, options);
opts.onwarn = (warning, handler) => {
if (warning.code === "THIS_IS_UNDEFINED" || warning.code === "CIRCULAR_DEPENDENCY" && warning.message.includes("typebox") || warning.code === "CIRCULAR_DEPENDENCY" && warning.message.includes("elysia")) {
return;
}
handler(warning);
};
if (normalizedInput) {
opts.input = normalizedInput;
appendVirtualInputs(opts.input, options?.servers);
if (options?.externalDependencies === true) {
if (typeof opts.external === "function") {
const orig = opts.external;
opts.external = (id, parentId, isResolved) => {
if (maybeExternals.includes(id)) return true;
return orig(id, parentId, isResolved);
};
} else if (Array.isArray(opts.external)) {
opts.external = [...opts.external, ...maybeExternals];
} else if (opts.external) {
opts.external = [opts.external, ...maybeExternals];
} else {
opts.external = [...maybeExternals];
}
}
return opts;
}
},
async generateBundle(opts, bundle) {
if (!normalizedInput) return;
const out = opts.dir ?? "dist";
const outputs = Object.entries(bundle);
let mapping = genBundleInfo(normalizedInput, (cleanV) => {
const found = outputs.find(([, value]) => {
if (value.type === "chunk" && value.isEntry) {
const cleanEntry = value.facadeModuleId;
return posix.relative(cleanEntry, cleanV) === "" || posix.relative(cleanEntry, `${namespace}:${cleanV}`) === "";
}
return false;
})?.[0];
if (!found) {
throw new Error("Error occurred while generating bundle info");
}
return found;
});
mapping = fixBundleExports(mapping, {
serversExportNames,
entryExportNames
});
for (const v of Object.values(mapping)) {
v.out = join(out, v.out);
v.dts = join(out, v.dts);
}
await genDts(mapping, options);
const report = genReport(mapping);
if (!options?.doNotEditPackageJson) {
const { path, packageJson } = await readAndEditPackageJson(report);
await writeFile(path, JSON.stringify(packageJson, void 0, 2));
}
await options?.buildEnd?.(report);
}
},
esbuild: {
setup(builder) {
if (builder.initialOptions.bundle !== true) {
throw new Error("`bundle` options must be `true` for universal-middleware to work properly");
}
if (!options?.ignoreRecommendations) {
if (builder.initialOptions.entryNames && !builder.initialOptions.entryNames.includes("[hash]") && !builder.initialOptions.entryNames.includes("[dir]")) {
console.warn(
"esbuild config specifies `entryNames` without [hash] or [dir]. This could lead to missing files"
);
}
if (!builder.initialOptions.splitting) {
console.warn("enable esbuild `splitting` option to reduce bundle size");
}
}
builder.initialOptions.metafile = true;
builder.initialOptions.external ??= [];
builder.initialOptions.external.push("node:*", "elysia");
if (builder.initialOptions.bundle && options?.externalDependencies === true) {
builder.initialOptions.external.push(...maybeExternals);
}
const normalizedInput2 = normalizeInput(builder.initialOptions.entryPoints);
if (!normalizedInput2) return;
const outbase = builder.initialOptions.outbase ?? "";
const outdir = builder.initialOptions.outdir ?? "dist";
builder.initialOptions.entryPoints = normalizedInput2;
appendVirtualInputs(builder.initialOptions.entryPoints, options?.servers);
builder.initialOptions.entryPoints = applyOutbase(builder.initialOptions.entryPoints, outbase);
builder.onResolve({ filter: /^virtual:universal-middleware/ }, (args) => {
return {
path: args.path,
namespace,
pluginData: {
resolveDir: args.resolveDir
}
};
});
builder.onResolve({ filter: /(^|\.|\/|\\\\)(handler|middleware)\./ }, (args) => {
return {
path: resolve(args.path)
};
});
builder.onLoad({ filter: /.*/, namespace }, async (args) => {
if (args.path.startsWith(namespace) && !shouldLoad(args.path)) {
return null;
}
const { code } = load(args.path);
return {
contents: code,
resolveDir: args.pluginData.resolveDir,
loader: "js"
};
});
builder.onEnd(async (result) => {
const outputs = Object.entries(result.metafile?.outputs ?? {});
let mapping = genBundleInfo(normalizedInput2, (cleanV) => {
const found = outputs.find(([, value]) => {
if (value.entryPoint) {
const cleanEntry = value.entryPoint;
return posix.relative(cleanEntry, cleanV) === "" || posix.relative(cleanEntry, `${namespace}:${cleanV}`) === "";
}
return false;
})?.[0];
if (!found) {
throw new Error("Error occurred while generating bundle info");
}
return found;
});
mapping = fixBundleExports(mapping, {
serversExportNames,
entryExportNames
});
for (const v of Object.values(mapping)) {
if (v.exports !== ".") {
v.exports = `./${posix.relative(outdir, v.exports)}`;
}
}
await genDts(mapping, options);
const report = genReport(mapping);
if (!options?.doNotEditPackageJson) {
const { path, packageJson } = await readAndEditPackageJson(report);
await writeFile(path, JSON.stringify(packageJson, void 0, 2));
}
await options?.buildEnd?.(report);
});
}
},
loadInclude(id) {
return shouldLoad(id);
},
load
};
};
var plugin_default = universalMiddleware;
export {
readAndEditPackageJson,
plugin_default
};