290 lines
9.9 KiB
JavaScript
290 lines
9.9 KiB
JavaScript
import { existsSync } from "node:fs";
|
|
import { readFile } from "node:fs/promises";
|
|
import { relative, dirname, extname } from "node:path";
|
|
import { writeFile } from "nitropack/kit";
|
|
import { parseTOML, parseJSONC } from "confbox";
|
|
import { readGitConfig, readPackageJSON, findNearestFile } from "pkg-types";
|
|
import { defu } from "defu";
|
|
import { globby } from "globby";
|
|
import { provider } from "std-env";
|
|
import { join, resolve } from "pathe";
|
|
import {
|
|
joinURL,
|
|
hasProtocol,
|
|
withLeadingSlash,
|
|
withTrailingSlash,
|
|
withoutLeadingSlash
|
|
} from "ufo";
|
|
import {
|
|
workerdHybridNodeCompatPlugin,
|
|
unenvWorkerdWithNodeCompat
|
|
} from "../_unenv/preset-workerd.mjs";
|
|
export async function writeCFRoutes(nitro) {
|
|
const _cfPagesConfig = nitro.options.cloudflare?.pages || {};
|
|
const routes = {
|
|
version: _cfPagesConfig.routes?.version || 1,
|
|
include: _cfPagesConfig.routes?.include || ["/*"],
|
|
exclude: _cfPagesConfig.routes?.exclude || []
|
|
};
|
|
const writeRoutes = () => writeFile(
|
|
resolve(nitro.options.output.dir, "_routes.json"),
|
|
JSON.stringify(routes, void 0, 2),
|
|
true
|
|
);
|
|
if (_cfPagesConfig.defaultRoutes === false) {
|
|
await writeRoutes();
|
|
return;
|
|
}
|
|
const explicitPublicAssets = nitro.options.publicAssets.filter(
|
|
(dir, index, array) => {
|
|
if (dir.fallthrough || !dir.baseURL) {
|
|
return false;
|
|
}
|
|
const normalizedBase = withoutLeadingSlash(dir.baseURL);
|
|
return !array.some(
|
|
(otherDir, otherIndex) => otherIndex !== index && normalizedBase.startsWith(
|
|
withoutLeadingSlash(withTrailingSlash(otherDir.baseURL))
|
|
)
|
|
);
|
|
}
|
|
);
|
|
routes.exclude.push(
|
|
...explicitPublicAssets.map((asset) => joinURL(nitro.options.baseURL, asset.baseURL || "/", "*")).sort(comparePaths)
|
|
);
|
|
const publicAssetFiles = await globby("**", {
|
|
cwd: nitro.options.output.dir,
|
|
absolute: false,
|
|
dot: true,
|
|
ignore: [
|
|
"_worker.js",
|
|
"_worker.js.map",
|
|
"nitro.json",
|
|
...routes.exclude.map(
|
|
(path) => withoutLeadingSlash(path.replace(/\/\*$/, "/**"))
|
|
)
|
|
]
|
|
});
|
|
routes.exclude.push(
|
|
...publicAssetFiles.map(
|
|
(i) => withLeadingSlash(i).replace(/\/index\.html$/, "").replace(/\.html$/, "") || "/"
|
|
).sort(comparePaths)
|
|
);
|
|
routes.exclude.splice(100 - routes.include.length);
|
|
await writeRoutes();
|
|
}
|
|
function comparePaths(a, b) {
|
|
return a.split("/").length - b.split("/").length || a.localeCompare(b);
|
|
}
|
|
export async function writeCFHeaders(nitro, outdir) {
|
|
const headersPath = join(
|
|
outdir === "public" ? nitro.options.output.publicDir : nitro.options.output.dir,
|
|
"_headers"
|
|
);
|
|
const contents = [];
|
|
const rules = Object.entries(nitro.options.routeRules).sort(
|
|
(a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length
|
|
);
|
|
for (const [path, routeRules] of rules.filter(
|
|
([_, routeRules2]) => routeRules2.headers
|
|
)) {
|
|
const headers = [
|
|
joinURL(nitro.options.baseURL, path.replace("/**", "/*")),
|
|
...Object.entries({ ...routeRules.headers }).map(
|
|
([header, value]) => ` ${header}: ${value}`
|
|
)
|
|
].join("\n");
|
|
contents.push(headers);
|
|
}
|
|
if (existsSync(headersPath)) {
|
|
const currentHeaders = await readFile(headersPath, "utf8");
|
|
if (/^\/\* /m.test(currentHeaders)) {
|
|
nitro.logger.info(
|
|
"Not adding Nitro fallback to `_headers` (as an existing fallback was found)."
|
|
);
|
|
return;
|
|
}
|
|
nitro.logger.info(
|
|
"Adding Nitro fallback to `_headers` to handle all unmatched routes."
|
|
);
|
|
contents.unshift(currentHeaders);
|
|
}
|
|
await writeFile(headersPath, contents.join("\n"), true);
|
|
}
|
|
export async function writeCFPagesRedirects(nitro) {
|
|
const redirectsPath = join(nitro.options.output.dir, "_redirects");
|
|
const staticFallback = existsSync(
|
|
join(nitro.options.output.publicDir, "404.html")
|
|
) ? `${joinURL(nitro.options.baseURL, "/*")} ${joinURL(nitro.options.baseURL, "/404.html")} 404` : "";
|
|
const contents = [staticFallback];
|
|
const rules = Object.entries(nitro.options.routeRules).sort(
|
|
(a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length
|
|
);
|
|
for (const [key, routeRules] of rules.filter(
|
|
([_, routeRules2]) => routeRules2.redirect
|
|
)) {
|
|
const code = routeRules.redirect.statusCode;
|
|
const from = joinURL(nitro.options.baseURL, key.replace("/**", "/*"));
|
|
const to = hasProtocol(routeRules.redirect.to, { acceptRelative: true }) ? routeRules.redirect.to : joinURL(nitro.options.baseURL, routeRules.redirect.to);
|
|
contents.unshift(`${from} ${to} ${code}`);
|
|
}
|
|
if (existsSync(redirectsPath)) {
|
|
const currentRedirects = await readFile(redirectsPath, "utf8");
|
|
if (/^\/\* /m.test(currentRedirects)) {
|
|
nitro.logger.info(
|
|
"Not adding Nitro fallback to `_redirects` (as an existing fallback was found)."
|
|
);
|
|
return;
|
|
}
|
|
nitro.logger.info(
|
|
"Adding Nitro fallback to `_redirects` to handle all unmatched routes."
|
|
);
|
|
contents.unshift(currentRedirects);
|
|
}
|
|
await writeFile(redirectsPath, contents.join("\n"), true);
|
|
}
|
|
export async function enableNodeCompat(nitro) {
|
|
nitro.options.cloudflare ??= {};
|
|
if (nitro.options.cloudflare.deployConfig === void 0 && provider === "cloudflare_workers") {
|
|
nitro.options.cloudflare.deployConfig = true;
|
|
}
|
|
if (nitro.options.cloudflare.nodeCompat === void 0) {
|
|
const { config } = await readWranglerConfig(nitro);
|
|
const userCompatibilityFlags = new Set(config?.compatibility_flags || []);
|
|
if (userCompatibilityFlags.has("nodejs_compat") || userCompatibilityFlags.has("nodejs_compat_v2") || nitro.options.cloudflare.deployConfig) {
|
|
nitro.options.cloudflare.nodeCompat = true;
|
|
}
|
|
}
|
|
if (!nitro.options.cloudflare.nodeCompat) {
|
|
if (nitro.options.cloudflare.nodeCompat === void 0) {
|
|
nitro.logger.warn("[cloudflare] Node.js compatibility is not enabled.");
|
|
}
|
|
return;
|
|
}
|
|
nitro.options.unenv.push(unenvWorkerdWithNodeCompat);
|
|
nitro.options.rollupConfig.plugins ??= [];
|
|
nitro.options.rollupConfig.plugins.push(
|
|
workerdHybridNodeCompatPlugin
|
|
);
|
|
}
|
|
const extensionParsers = {
|
|
".json": JSON.parse,
|
|
".jsonc": parseJSONC,
|
|
".toml": parseTOML
|
|
};
|
|
async function readWranglerConfig(nitro) {
|
|
const configPath = await findNearestFile(
|
|
["wrangler.json", "wrangler.jsonc", "wrangler.toml"],
|
|
{
|
|
startingFrom: nitro.options.rootDir
|
|
}
|
|
).catch(() => void 0);
|
|
if (!configPath) {
|
|
return {};
|
|
}
|
|
const userConfigText = await readFile(configPath, "utf8");
|
|
const parser = extensionParsers[extname(configPath)];
|
|
if (!parser) {
|
|
throw new Error(`Unsupported config file format: ${configPath}`);
|
|
}
|
|
const config = parser(userConfigText);
|
|
return { configPath, config };
|
|
}
|
|
export async function writeWranglerConfig(nitro, cfTarget) {
|
|
if (!nitro.options.cloudflare?.deployConfig) {
|
|
return;
|
|
}
|
|
const wranglerConfigDir = nitro.options.output.serverDir;
|
|
const wranglerConfigPath = join(wranglerConfigDir, "wrangler.json");
|
|
const defaults = {};
|
|
const overrides = {};
|
|
defaults.compatibility_date = nitro.options.compatibilityDate.cloudflare || nitro.options.compatibilityDate.default;
|
|
if (cfTarget === "pages") {
|
|
overrides.pages_build_output_dir = relative(
|
|
wranglerConfigDir,
|
|
nitro.options.output.dir
|
|
);
|
|
} else {
|
|
overrides.main = relative(
|
|
wranglerConfigDir,
|
|
join(nitro.options.output.serverDir, "index.mjs")
|
|
);
|
|
overrides.assets = {
|
|
binding: "ASSETS",
|
|
directory: relative(
|
|
wranglerConfigDir,
|
|
resolve(
|
|
nitro.options.output.publicDir,
|
|
"..".repeat(nitro.options.baseURL.split("/").filter(Boolean).length)
|
|
)
|
|
)
|
|
};
|
|
}
|
|
const { config: userConfig = {} } = await readWranglerConfig(nitro);
|
|
const ctxConfig = nitro.options.cloudflare?.wrangler || {};
|
|
for (const key in overrides) {
|
|
if (key in userConfig || key in ctxConfig) {
|
|
nitro.logger.warn(
|
|
`[cloudflare] Wrangler config \`${key}\`${key in ctxConfig ? "set by config or modules" : ""} is overridden and will be ignored.`
|
|
);
|
|
}
|
|
}
|
|
const wranglerConfig = defu(
|
|
overrides,
|
|
ctxConfig,
|
|
userConfig,
|
|
defaults
|
|
);
|
|
if (!wranglerConfig.name) {
|
|
wranglerConfig.name = await generateWorkerName(nitro);
|
|
nitro.logger.info(
|
|
`Using auto generated worker name: \`${wranglerConfig.name}\``
|
|
);
|
|
}
|
|
const compatFlags = new Set(wranglerConfig.compatibility_flags || []);
|
|
if (nitro.options.cloudflare?.nodeCompat) {
|
|
if (compatFlags.has("nodejs_compat_v2") && compatFlags.has("no_nodejs_compat_v2")) {
|
|
nitro.logger.warn(
|
|
"[cloudflare] Wrangler config `compatibility_flags` contains both `nodejs_compat_v2` and `no_nodejs_compat_v2`. Ignoring `nodejs_compat_v2`."
|
|
);
|
|
compatFlags.delete("nodejs_compat_v2");
|
|
}
|
|
if (compatFlags.has("nodejs_compat_v2")) {
|
|
nitro.logger.warn(
|
|
"[cloudflare] Please consider replacing `nodejs_compat_v2` with `nodejs_compat` in your `compatibility_flags` or USE IT AT YOUR OWN RISK as it can cause issues with nitro."
|
|
);
|
|
} else {
|
|
compatFlags.add("nodejs_compat");
|
|
compatFlags.add("no_nodejs_compat_v2");
|
|
}
|
|
}
|
|
wranglerConfig.compatibility_flags = [...compatFlags];
|
|
await writeFile(
|
|
wranglerConfigPath,
|
|
JSON.stringify(wranglerConfig, null, 2),
|
|
true
|
|
);
|
|
const configPath = join(
|
|
nitro.options.rootDir,
|
|
".wrangler/deploy/config.json"
|
|
);
|
|
await writeFile(
|
|
configPath,
|
|
JSON.stringify({
|
|
configPath: relative(dirname(configPath), wranglerConfigPath)
|
|
}),
|
|
true
|
|
);
|
|
}
|
|
async function generateWorkerName(nitro) {
|
|
const gitConfig = await readGitConfig(nitro.options.rootDir).catch(
|
|
() => void 0
|
|
);
|
|
const gitRepo = gitConfig?.remote?.origin?.url?.replace(/\.git$/, "").match(/[/:]([^/]+\/[^/]+)$/)?.[1];
|
|
const pkgJSON = await readPackageJSON(nitro.options.rootDir).catch(
|
|
() => void 0
|
|
);
|
|
const pkgName = pkgJSON?.name;
|
|
const subpath = relative(nitro.options.workspaceDir, nitro.options.rootDir);
|
|
return `${gitRepo || pkgName}/${subpath}`.toLowerCase().replace(/[^a-zA-Z0-9-]/g, "-").replace(/-$/, "");
|
|
}
|