2025-09-05 14:59:21 +08:00

307 lines
9.7 KiB
JavaScript

import fsp from "node:fs/promises";
import { defu } from "defu";
import { writeFile } from "nitropack/kit";
import { dirname, relative, resolve } from "pathe";
import { joinURL, withoutLeadingSlash } from "ufo";
import { isTest } from "std-env";
const SUPPORTED_NODE_VERSIONS = [18, 20, 22];
function getSystemNodeVersion() {
const systemNodeVersion = Number.parseInt(
process.versions.node.split(".")[0]
);
return Number.isNaN(systemNodeVersion) ? 22 : systemNodeVersion;
}
export async function generateFunctionFiles(nitro) {
const o11Routes = getObservabilityRoutes(nitro);
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
const buildConfig = generateBuildConfig(nitro, o11Routes);
await writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2));
const systemNodeVersion = getSystemNodeVersion();
const usedNodeVersion = SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? SUPPORTED_NODE_VERSIONS.at(-1);
const runtimeVersion = `nodejs${usedNodeVersion}.x`;
const functionConfigPath = resolve(
nitro.options.output.serverDir,
".vc-config.json"
);
const functionConfig = {
runtime: runtimeVersion,
handler: "index.mjs",
launcherType: "Nodejs",
shouldAddHelpers: false,
supportsResponseStreaming: true,
...nitro.options.vercel?.functions
};
await writeFile(functionConfigPath, JSON.stringify(functionConfig, null, 2));
for (const [key, value] of Object.entries(nitro.options.routeRules)) {
if (!value.isr) {
continue;
}
let isrConfig = value.isr;
if (typeof isrConfig === "number") {
isrConfig = { expiration: isrConfig };
} else if (isrConfig === true) {
isrConfig = { expiration: false };
} else {
isrConfig = { ...isrConfig };
}
const prerenderConfig = {
expiration: isrConfig.expiration ?? false,
bypassToken: nitro.options.vercel?.config?.bypassToken,
...isrConfig
};
if (key.includes("/**")) {
isrConfig.allowQuery = isrConfig.allowQuery || [];
if (!isrConfig.allowQuery.includes("url")) {
isrConfig.allowQuery.push("url");
}
}
const funcPrefix = resolve(
nitro.options.output.serverDir,
".." + generateEndpoint(key)
);
await fsp.mkdir(dirname(funcPrefix), { recursive: true });
await fsp.symlink(
"./" + relative(dirname(funcPrefix), nitro.options.output.serverDir),
funcPrefix + ".func",
"junction"
);
await writeFile(
funcPrefix + ".prerender-config.json",
JSON.stringify(prerenderConfig, null, 2)
);
}
for (const route of o11Routes) {
const funcPrefix = resolve(
nitro.options.output.serverDir,
"..",
route.dest
);
await fsp.mkdir(dirname(funcPrefix), { recursive: true });
await fsp.symlink(
"./" + relative(dirname(funcPrefix), nitro.options.output.serverDir),
funcPrefix + ".func",
"junction"
);
}
}
export async function generateEdgeFunctionFiles(nitro) {
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
const buildConfig = generateBuildConfig(nitro);
await writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2));
const functionConfigPath = resolve(
nitro.options.output.serverDir,
".vc-config.json"
);
const functionConfig = {
runtime: "edge",
entrypoint: "index.mjs",
regions: nitro.options.vercel?.regions
};
await writeFile(functionConfigPath, JSON.stringify(functionConfig, null, 2));
}
export async function generateStaticFiles(nitro) {
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
const buildConfig = generateBuildConfig(nitro);
await writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2));
}
function generateBuildConfig(nitro, o11Routes) {
const rules = Object.entries(nitro.options.routeRules).sort(
(a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length
);
const config = defu(nitro.options.vercel?.config, {
version: 3,
overrides: {
// Nitro static prerendered route overrides
...Object.fromEntries(
(nitro._prerenderedRoutes?.filter((r) => r.fileName !== r.route) || []).map(({ route, fileName }) => [
withoutLeadingSlash(fileName),
{ path: route.replace(/^\//, "") }
])
)
},
routes: [
// Redirect and header rules
...rules.filter(([_, routeRules]) => routeRules.redirect || routeRules.headers).map(([path, routeRules]) => {
let route = {
src: path.replace("/**", "/(.*)")
};
if (routeRules.redirect) {
route = defu(route, {
status: routeRules.redirect.statusCode,
headers: {
Location: routeRules.redirect.to.replace("/**", "/$1")
}
});
}
if (routeRules.headers) {
route = defu(route, { headers: routeRules.headers });
}
return route;
}),
// Public asset rules
...nitro.options.publicAssets.filter((asset) => !asset.fallthrough).map((asset) => joinURL(nitro.options.baseURL, asset.baseURL || "/")).map((baseURL) => ({
src: baseURL + "(.*)",
headers: {
"cache-control": "public,max-age=31536000,immutable"
},
continue: true
})),
{ handle: "filesystem" }
]
});
if (nitro.options.static) {
return config;
}
config.routes.push(
...rules.filter(
([key, value]) => (
// value.isr === false || (value.isr && key.includes("/**"))
value.isr !== void 0 && key !== "/"
)
).map(([key, value]) => {
const src = key.replace(/^(.*)\/\*\*/, "(?<url>$1/.*)");
if (value.isr === false) {
return {
src,
dest: "/__fallback"
};
}
return {
src,
dest: nitro.options.preset === "vercel-edge" ? "/__fallback?url=$url" : generateEndpoint(key) + "?url=$url"
};
}),
...nitro.options.routeRules["/"]?.isr ? [
{
src: "(?<url>/)",
dest: "/__fallback-index?url=$url"
}
] : [],
...(o11Routes || []).map((route) => ({
src: joinURL(nitro.options.baseURL, route.src),
dest: "/" + route.dest
})),
...nitro.options.routeRules["/**"]?.isr ? [] : [
{
src: "/(.*)",
dest: "/__fallback"
}
]
);
return config;
}
function generateEndpoint(url) {
if (url === "/") {
return "/__fallback-index";
}
return url.includes("/**") ? "/__fallback-" + withoutLeadingSlash(url.replace(/\/\*\*.*/, "").replace(/[^a-z]/g, "-")) : url;
}
export function deprecateSWR(nitro) {
if (nitro.options.future.nativeSWR) {
return;
}
let hasLegacyOptions = false;
for (const [key, value] of Object.entries(nitro.options.routeRules)) {
if (_hasProp(value, "isr")) {
continue;
}
if (value.cache === false) {
value.isr = false;
}
if (_hasProp(value, "static")) {
value.isr = !value.static;
hasLegacyOptions = true;
}
if (value.cache && _hasProp(value.cache, "swr")) {
value.isr = value.cache.swr;
hasLegacyOptions = true;
}
}
if (hasLegacyOptions && !isTest) {
nitro.logger.warn(
"Nitro now uses `isr` option to configure ISR behavior on Vercel. Backwards-compatible support for `static` and `swr` options within the Vercel Build Options API will be removed in the future versions. Set `future.nativeSWR: true` nitro config disable this warning."
);
}
}
function _hasProp(obj, prop) {
return obj && typeof obj === "object" && prop in obj;
}
function getObservabilityRoutes(nitro) {
const compatDate = nitro.options.compatibilityDate.vercel || nitro.options.compatibilityDate.default;
if (compatDate < "2025-07-15") {
return [];
}
const routePatterns = [
.../* @__PURE__ */ new Set([
...nitro.options.ssrRoutes || [],
...[...nitro.scannedHandlers, ...nitro.options.handlers].filter((h) => !h.middleware && h.route).map((h) => h.route)
])
];
const staticRoutes = [];
const dynamicRoutes = [];
const catchAllRoutes = [];
for (const route of routePatterns) {
if (route.includes("**")) {
catchAllRoutes.push(route);
} else if (route.includes(":") || route.includes("*")) {
dynamicRoutes.push(route);
} else {
staticRoutes.push(route);
}
}
return [
...normalizeRoutes(staticRoutes),
...normalizeRoutes(dynamicRoutes),
...normalizeRoutes(catchAllRoutes)
];
}
function normalizeRoutes(routes) {
return routes.sort(
(a, b) => (
// a.split("/").length - b.split("/").length ||
b.localeCompare(a)
)
).map((route) => ({
src: normalizeRouteSrc(route),
dest: normalizeRouteDest(route)
}));
}
function normalizeRouteSrc(route) {
let idCtr = 0;
return route.split("/").map((segment) => {
if (segment.startsWith("**")) {
return segment === "**" ? "(?:.*)" : `?(?<${namedGroup(segment.slice(3))}>.+)`;
}
if (segment === "*") {
return `(?<_${idCtr++}>[^/]*)`;
}
if (segment.includes(":")) {
return segment.replace(/:(\w+)/g, (_, id) => `(?<${namedGroup(id)}>[^/]+)`).replace(/\./g, String.raw`\.`);
}
return segment;
}).join("/");
}
function namedGroup(input = "") {
if (/\d/.test(input[0])) {
input = `_${input}`;
}
return input.replace(/[^a-zA-Z0-9_]/g, "") || "_";
}
function normalizeRouteDest(route) {
return route.split("/").slice(1).map((segment) => {
if (segment.startsWith("**")) {
return `[...${segment.replace(/[*:]/g, "")}]`;
}
if (segment === "*") {
return "[-]";
}
if (segment.startsWith(":")) {
return `[${segment.slice(1)}]`;
}
if (segment.includes(":")) {
return `[${segment.replace(/:/g, "_")}]`;
}
return segment;
}).map((segment) => segment.replace(/[^a-zA-Z0-9_.[\]]/g, "-")).join("/") || "index";
}