mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
Vercel now deploys Reactive Resume as two services in one project: `frontend` serves the static Vite build from apps/web/dist, and `backend` runs the Hono server Function from apps/server. Top-level rewrites send server-owned paths (/api, /uploads, /mcp, /.well-known, robots.txt, sitemap.xml, llms.txt, schema.json, index.html) and every path without a file extension to `backend`, so HTML shells keep their injected SEO metadata. Paths with a file extension go to `frontend`. The service builder needs a few accommodations: - apps/server/vercel.mjs replaces api/index.mjs as the entrypoint, because a service entrypoint must exist before the build runs. - `outputDirectory: "."` stops the builder from using the Docker entrypoint in dist/ as the Function handler. - The builder loads external CommonJS dependencies through pnpm links that it leaves out of the Function. ioredis and react-reconciler are now bundled with their dependencies, and bcrypt is replaced with bcryptjs, which reads and writes the same $2b$ hashes. The Vercel compatibility workflow now builds with the services framework and loads a copy of the backend Function outside the checkout, so a dependency missing from the Function fails CI. The stale PDFKit trace checks are removed. Existing Vercel installations must set Framework Preset to Services before redeploying; the self-hosting guide documents this.
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import type { TsdownPlugin } from "tsdown";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { defineConfig } from "tsdown";
|
|
|
|
const rootPackageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")) as {
|
|
version?: string;
|
|
};
|
|
|
|
// Lambda disables require(ESM) and uses stricter CJS export detection than standalone Node. Vercel's service builder
|
|
// also loads external CommonJS packages through pnpm links it leaves out of the Function, so CommonJS dependencies
|
|
// (ioredis, react-reconciler) are bundled together with their own dependencies.
|
|
const bundledInteropPackages = new Set([
|
|
"ioredis",
|
|
"@ioredis/commands",
|
|
"cluster-key-slot",
|
|
"debug",
|
|
"ms",
|
|
"denque",
|
|
"redis-errors",
|
|
"standard-as-callback",
|
|
"react-reconciler",
|
|
"scheduler",
|
|
"@uiw/color-convert",
|
|
"@babel/runtime",
|
|
"sanitize-html",
|
|
"htmlparser2",
|
|
"domhandler",
|
|
"domutils",
|
|
"domelementtype",
|
|
"dom-serializer",
|
|
"entities",
|
|
"deepmerge",
|
|
"escape-string-regexp",
|
|
"is-plain-object",
|
|
"parse-srcset",
|
|
"postcss",
|
|
"nanoid",
|
|
"picocolors",
|
|
"source-map-js",
|
|
"launder",
|
|
"dayjs",
|
|
]);
|
|
|
|
const packageNameOf = (id: string) =>
|
|
id
|
|
.split("/")
|
|
.slice(0, id.startsWith("@") ? 2 : 1)
|
|
.join("/");
|
|
|
|
// Matches subpath imports too, such as `react-reconciler/constants.js`.
|
|
const shouldBundle = (id: string) =>
|
|
id.startsWith("@reactive-resume/") || bundledInteropPackages.has(packageNameOf(id));
|
|
|
|
const shouldExternalizeThirdParty = (id: string) => {
|
|
if (shouldBundle(id)) return false;
|
|
// Subpath imports (`#…`) are resolved by the workspace package that declares them, so they're bundled with it.
|
|
if (id.startsWith("@/") || id.startsWith("#") || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0"))
|
|
return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
const aiPromptsDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../packages/ai/src/prompts");
|
|
|
|
const promptAssetsPlugin: TsdownPlugin = {
|
|
name: "prompt-assets",
|
|
buildStart() {
|
|
for (const filename of readdirSync(aiPromptsDir)) {
|
|
if (!filename.endsWith(".md")) continue;
|
|
|
|
this.emitFile({
|
|
type: "asset",
|
|
fileName: `prompts/${filename}`,
|
|
source: readFileSync(resolve(aiPromptsDir, filename), "utf-8"),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
export default defineConfig({
|
|
entry: {
|
|
index: "src/index.ts",
|
|
vercel: "src/vercel.ts",
|
|
"prepare-deployment": "src/prepare-deployment.ts",
|
|
"migrate-legacy-styles": "src/migrate-legacy-styles.ts",
|
|
},
|
|
// Keep import.meta.url-based asset lookup adjacent to the entrypoints.
|
|
outputOptions: { chunkFileNames: "[name]-[hash].mjs" },
|
|
format: "esm",
|
|
platform: "node",
|
|
target: "node24",
|
|
outDir: "dist",
|
|
clean: true,
|
|
shims: true,
|
|
dts: false,
|
|
define: { __APP_VERSION__: JSON.stringify(rootPackageJson.version ?? "0.0.0") },
|
|
// The flagged dynamic imports are deliberate: they defer evaluation of env-dependent
|
|
// modules so tests can run without env vars, not to split chunks.
|
|
suppressWarnings: [/dynamic import will not move module into another chunk/],
|
|
outExtensions: () => ({ js: ".mjs" }),
|
|
deps: {
|
|
alwaysBundle: shouldBundle,
|
|
neverBundle: shouldExternalizeThirdParty,
|
|
},
|
|
plugins: [promptAssetsPlugin],
|
|
});
|