UNPKG

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