one
Version:
One is a new React Framework that makes Vite serve both native and web.
1,078 lines (1,064 loc) • 39.7 kB
JavaScript
import { createRequire } from "node:module";
import { cpus } from "node:os";
import Path, { join, relative, resolve } from "node:path";
import { resolvePath } from "@vxrn/resolve";
import FSExtra from "fs-extra";
import MicroMatch from "micromatch";
import { mergeConfig, build as viteBuild } from "vite";
import { fillOptions, getOptimizeDeps, rollupRemoveUnusedImportsPlugin, build as vxrnBuild } from "vxrn";
import * as constants from "../constants.mjs";
import { setServerGlobals } from "../server/setServerGlobals.mjs";
import { getPathnameFromFilePath } from "../utils/getPathnameFromFilePath.mjs";
import { getRouterRootFromOneOptions } from "../utils/getRouterRootFromOneOptions.mjs";
import { isRolldown } from "../utils/isRolldown.mjs";
import { toAbsolute } from "../utils/toAbsolute.mjs";
import { buildVercelOutputDirectory } from "../vercel/build/buildVercelOutputDirectory.mjs";
import { getManifest } from "../vite/getManifest.mjs";
import { loadUserOneOptions } from "../vite/loadConfig.mjs";
import { runWithAsyncLocalContext } from "../vite/one-server-only.mjs";
import { buildPage, printBuildTimings } from "./buildPage.mjs";
import { checkNodeVersion } from "./checkNodeVersion.mjs";
import { getWorkerPool, terminateWorkerPool } from "./workerPool.mjs";
import { generateSitemap } from "./generateSitemap.mjs";
import { labelProcess } from "./label-process.mjs";
import { pLimit } from "../utils/pLimit.mjs";
import { getCriticalCSSOutputPaths } from "../vite/plugins/criticalCSSPlugin.mjs";
const {
ensureDir,
writeJSON
} = FSExtra;
function normalizeDeploy(deploy) {
if (!deploy) return void 0;
if (typeof deploy === "string") return {
target: deploy
};
return deploy;
}
const GENERATED_CLOUDFLARE_WRANGLER_RULES = [{
type: "ESModule",
globs: ["./server/**/*.js"],
fallthrough: true
}, {
type: "ESModule",
globs: ["./api/**/*.js"],
fallthrough: true
}, {
type: "ESModule",
globs: ["./middlewares/**/*.js"],
fallthrough: true
}, {
type: "ESModule",
globs: ["./assets/**/*.js"],
fallthrough: true
}];
function isPlainObject(value) {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function mergeJsonObjects(base, overrides) {
const merged = {
...base
};
for (const [key, value] of Object.entries(overrides)) {
const baseValue = merged[key];
if (isPlainObject(baseValue) && isPlainObject(value)) {
merged[key] = mergeJsonObjects(baseValue, value);
} else {
merged[key] = value;
}
}
return merged;
}
function dedupeJsonValues(values) {
const seen = /* @__PURE__ */new Set();
return values.filter(value => {
const key = JSON.stringify(value);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function mergeCloudflareCompatibilityFlags(flags) {
const userFlags = Array.isArray(flags) ? flags.filter(flag => typeof flag === "string") : [];
return dedupeJsonValues(["nodejs_compat", ...userFlags]);
}
function mergeCloudflareRules(rules) {
const userRules = Array.isArray(rules) ? rules.filter(rule => isPlainObject(rule)) : [];
return dedupeJsonValues([...GENERATED_CLOUDFLARE_WRANGLER_RULES, ...userRules]);
}
function parseJsonc(text) {
let out = "";
let i = 0;
let inString = false;
let quote = "";
while (i < text.length) {
const ch = text[i];
const next = text[i + 1];
if (inString) {
if (ch === "\\") {
out += ch + (next ?? "");
i += 2;
continue;
}
if (ch === quote) inString = false;
out += ch;
i++;
continue;
}
if (ch === '"' || ch === "'") {
inString = true;
quote = ch;
out += ch;
i++;
continue;
}
if (ch === "/" && next === "/") {
while (i < text.length && text[i] !== "\n") i++;
continue;
}
if (ch === "/" && next === "*") {
i += 2;
while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) i++;
i += 2;
continue;
}
out += ch;
i++;
}
return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
}
async function loadUserWranglerConfig(root) {
const candidateRoots = [... /* @__PURE__ */new Set([root, process.cwd()])];
for (const candidateRoot of candidateRoots) {
for (const fileName of ["wrangler.jsonc", "wrangler.json"]) {
const configPath = join(candidateRoot, fileName);
if (!(await FSExtra.pathExists(configPath))) {
continue;
}
const contents = await FSExtra.readFile(configPath, "utf-8");
let parsed;
try {
parsed = parseJsonc(contents);
} catch (err) {
throw new Error(`Failed to parse ${relative(process.cwd(), configPath)}: ${err.message}`);
}
if (!isPlainObject(parsed)) {
throw new Error(`Expected ${relative(process.cwd(), configPath)} to contain a top-level JSON object`);
}
return {
path: configPath,
config: parsed
};
}
}
return null;
}
function createCloudflareWranglerConfig(projectName, userConfig) {
const generatedConfig = {
name: projectName,
main: "worker.js",
compatibility_date: "2024-12-05",
compatibility_flags: ["nodejs_compat"],
find_additional_modules: true,
rules: GENERATED_CLOUDFLARE_WRANGLER_RULES,
assets: {
directory: "client",
binding: "ASSETS",
run_worker_first: true
}
};
const mergedConfig = userConfig ? mergeJsonObjects(generatedConfig, userConfig) : generatedConfig;
mergedConfig.main = "worker.js";
mergedConfig.find_additional_modules = true;
mergedConfig.compatibility_flags = mergeCloudflareCompatibilityFlags(mergedConfig.compatibility_flags);
mergedConfig.rules = mergeCloudflareRules(mergedConfig.rules);
mergedConfig.assets = {
...(isPlainObject(mergedConfig.assets) ? mergedConfig.assets : {}),
directory: "client",
binding: "ASSETS",
run_worker_first: true
};
return mergedConfig;
}
async function getCloudflareProjectName(root) {
try {
const pkg = JSON.parse(await FSExtra.readFile(join(root, "package.json"), "utf-8"));
if (pkg.name) {
return pkg.name.replace(/^@[^/]+\//, "");
}
} catch {}
return "one-app";
}
process.env.ONE_CACHE_KEY = constants.CACHE_KEY;
const BUILD_CONCURRENCY = process.env.ONE_BUILD_CONCURRENCY ? Math.max(1, parseInt(process.env.ONE_BUILD_CONCURRENCY, 10)) : Math.max(1, Math.min(cpus().length, 8));
function shouldUseWorkers(oneOptions) {
if (process.env.ONE_BUILD_WORKERS === "0") return false;
if (process.env.ONE_BUILD_WORKERS === "1") return true;
return oneOptions?.build?.workers !== false;
}
process.on("uncaughtException", err => {
console.error(err?.message || err);
});
const HOOK_KEYS = ["resolveId", "load", "transform", "renderChunk", "generateBundle", "writeBundle", "buildStart", "buildEnd", "moduleParsed"];
function clonePluginHooks(config) {
if (!config.plugins) return config;
return {
...config,
plugins: config.plugins.map(p => {
if (!p || typeof p !== "object") return p;
const cloned = {
...p
};
for (const key of HOOK_KEYS) {
if (cloned[key] && typeof cloned[key] === "object" && "handler" in cloned[key]) {
cloned[key] = {
...cloned[key]
};
}
}
return cloned;
})
};
}
async function build(args) {
process.env.IS_VXRN_CLI = "true";
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
} else if (process.env.NODE_ENV !== "production") {
console.warn(`
\u26A0\uFE0F Warning: NODE_ENV is set to "${process.env.NODE_ENV}" (builds default to "production")
`);
}
labelProcess("build");
checkNodeVersion();
setServerGlobals();
const {
oneOptions,
config: viteLoadedConfig
} = await loadUserOneOptions("build");
const routerRoot = getRouterRootFromOneOptions(oneOptions);
if (oneOptions.web?.defaultRenderMode) {
process.env.ONE_DEFAULT_RENDER_MODE = oneOptions.web.defaultRenderMode;
}
const deployConfig = normalizeDeploy(oneOptions.web?.deploy);
if (!process.env.ONE_SERVER_URL && deployConfig) {
const url = deployConfig.url ?? (deployConfig.target === "cloudflare" ? `https://${await getCloudflareProjectName(process.cwd())}.workers.dev` : void 0);
if (url) {
process.env.ONE_SERVER_URL = url;
console.info(`
\u2601\uFE0F ONE_SERVER_URL: ${url}
`);
}
}
const outDir = viteLoadedConfig?.config?.build?.outDir ?? "dist";
const manifest = getManifest({
routerRoot,
ignoredRouteFiles: oneOptions.router?.ignoredRouteFiles
});
const serverOutputFormat = oneOptions.build?.server === false ? "esm" : oneOptions.build?.server?.outputFormat ?? "esm";
const buildStartTime = performance.now();
const vxrnOutput = await vxrnBuild({
skipEnv: args.skipEnv ?? oneOptions.skipEnv,
server: oneOptions.server,
build: {
analyze: true,
server: oneOptions.build?.server === false ? false : {
outputFormat: serverOutputFormat
}
}
}, args);
const bundleTime = performance.now() - buildStartTime;
console.info(`
\u23F1\uFE0F vite bundle: ${(bundleTime / 1e3).toFixed(2)}s
`);
if (!vxrnOutput || args.platform !== "web") {
return;
}
const options = await fillOptions(vxrnOutput.options, {
mode: "prod"
});
const {
optimizeDeps
} = getOptimizeDeps("build");
const {
rolldownOptions: _rolldownOptions,
...optimizeDepsNoRolldown
} = optimizeDeps;
const clonedWebBuildConfig = clonePluginHooks(vxrnOutput.webBuildConfig);
const apiBuildConfig = mergeConfig(
// feels like this should build off the *server* build config not web
clonedWebBuildConfig, {
configFile: false,
appType: "custom",
optimizeDeps: optimizeDepsNoRolldown,
environments: {
client: {
optimizeDeps: {
rolldownOptions: _rolldownOptions
}
}
}
});
async function buildCustomRoutes(subFolder, routes) {
const input = routes.reduce((entries, {
page,
file
}) => {
entries[page.slice(1) + ".js"] = join(routerRoot, file);
return entries;
}, {});
const outputFormat = oneOptions?.build?.api?.outputFormat ?? serverOutputFormat;
const treeshake = oneOptions?.build?.api?.treeshake;
const mergedConfig = mergeConfig(apiBuildConfig, {
appType: "custom",
configFile: false,
// plugins: [
// nodeExternals({
// exclude: optimizeDeps.include,
// }) as any,
// ],
define: vxrnOutput.processEnvDefines,
ssr: {
noExternal: true,
external: ["react", "react-dom"],
optimizeDeps: optimizeDepsNoRolldown
},
environments: {
ssr: {
optimizeDeps: {
rolldownOptions: _rolldownOptions
}
}
},
build: {
ssr: true,
emptyOutDir: false,
outDir: `${outDir}/${subFolder}`,
copyPublicDir: false,
minify: false,
rolldownOptions: {
treeshake: treeshake ?? {
moduleSideEffects: false
},
plugins: [
// otherwise rollup is leaving commonjs-only top level imports...
outputFormat === "esm" ? rollupRemoveUnusedImportsPlugin : null].filter(Boolean),
// too many issues
// treeshake: {
// moduleSideEffects: false,
// },
// prevents it from shaking out the exports
preserveEntrySignatures: "strict",
input,
external: [],
output: {
entryFileNames: "[name]",
exports: "auto",
...(outputFormat === "esm" ? {
format: "esm",
esModule: true
} : {
format: "cjs",
// Preserve folder structure and use .cjs extension
entryFileNames: chunkInfo => {
const name = chunkInfo.name.replace(/\.js$/, ".cjs");
return name;
},
chunkFileNames: chunkInfo => {
const dir = Path.dirname(chunkInfo.name);
const name = Path.basename(chunkInfo.name, Path.extname(chunkInfo.name));
return Path.join(dir, `${name}-[hash].cjs`);
},
assetFileNames: assetInfo => {
const name = assetInfo.name ?? "";
const dir = Path.dirname(name);
const baseName = Path.basename(name, Path.extname(name));
const ext = Path.extname(name);
return Path.join(dir, `${baseName}-[hash]${ext}`);
}
})
}
}
}
});
const userApiBuildConf = oneOptions.build?.api?.config;
const finalApiBuildConf = userApiBuildConf ? mergeConfig(mergedConfig, userApiBuildConf) : mergedConfig;
const output = await viteBuild(
// allow user merging api build config
finalApiBuildConf);
return output;
}
const builtMiddlewares = {};
const apiPromise = manifest.apiRoutes.length ? (console.info(`
\u{1F528} build api routes
`), buildCustomRoutes("api", manifest.apiRoutes)) : Promise.resolve(null);
const middlewarePromise = manifest.middlewareRoutes.length ? (console.info(`
\u{1F528} build middlewares
`), buildCustomRoutes("middlewares", manifest.middlewareRoutes)) : Promise.resolve(null);
const [apiOutput, middlewareBuildInfo] = await Promise.all([apiPromise, middlewarePromise]);
if (middlewareBuildInfo) {
for (const middleware of manifest.middlewareRoutes) {
const absoluteRoot = resolve(process.cwd(), options.root);
const fullPath = join(absoluteRoot, routerRoot, middleware.file);
const outChunks = middlewareBuildInfo.output.filter(x => x.type === "chunk");
const chunk = outChunks.find(x => x.facadeModuleId === fullPath);
if (!chunk) throw new Error(`internal err finding middleware`);
builtMiddlewares[middleware.file] = join(outDir, "middlewares", chunk.fileName);
}
}
globalThis["require"] = createRequire(join(import.meta.url, ".."));
const assets = [];
const builtRoutes = [];
const sitemapData = [];
const collectImportsCache = /* @__PURE__ */new Map();
const cssFileContentsCache = /* @__PURE__ */new Map();
const criticalCSSOutputPaths = getCriticalCSSOutputPaths(vxrnOutput.clientManifest);
const limit = pLimit(BUILD_CONCURRENCY);
const useWorkers = shouldUseWorkers(oneOptions);
const workerPool = useWorkers ? getWorkerPool(BUILD_CONCURRENCY) : null;
if (workerPool) {
const serializableOptions = JSON.parse(JSON.stringify(oneOptions, (_key, value) => typeof value === "function" ? void 0 : value));
await workerPool.initialize(serializableOptions);
}
const staticStartTime = performance.now();
const modeLabel = useWorkers ? `workers: ${workerPool?.size}` : `concurrency: ${BUILD_CONCURRENCY}`;
console.info(`
\u{1F528} build static routes (${modeLabel})
`);
const staticDir = join(`${outDir}/static`);
const clientDir = join(`${outDir}/client`);
await ensureDir(staticDir);
if (!vxrnOutput.serverOutput) {
throw new Error(`No server output`);
}
const clientChunksBySource = /* @__PURE__ */new Map();
if (vxrnOutput.clientOutput) {
for (const chunk of vxrnOutput.clientOutput) {
if (chunk.type === "chunk" && chunk.facadeModuleId) {
clientChunksBySource.set(chunk.facadeModuleId, {
fileName: chunk.fileName,
imports: chunk.imports || []
});
}
}
}
const outputEntries = [...vxrnOutput.serverOutput.entries()];
const layoutServerPaths = /* @__PURE__ */new Map();
for (const [, output] of outputEntries) {
if (output.type === "asset") continue;
const id = output.facadeModuleId || "";
const file = Path.basename(id);
if (file.startsWith("_layout") && id.includes(`/${routerRoot}/`)) {
const relativePath = relative(process.cwd(), id).replace(`${routerRoot}/`, "");
const contextKey = `./${relativePath}`;
layoutServerPaths.set(contextKey, output.fileName);
}
}
const routeByPath = /* @__PURE__ */new Map();
for (const route of manifest.pageRoutes) {
if (route.file) {
const routePath = `${routerRoot}${route.file.slice(1)}`;
routeByPath.set(routePath, route);
}
}
for (const [, output] of outputEntries) {
if (output.type === "asset") {
assets.push(output);
}
}
const moduleIdToServerChunk = /* @__PURE__ */new Map();
for (const [, output] of outputEntries) {
if (output.type === "asset") continue;
const moduleIds = output.moduleIds || (output.facadeModuleId ? [output.facadeModuleId] : []);
for (const moduleId of moduleIds) {
moduleIdToServerChunk.set(moduleId, output.fileName);
}
}
for (const foundRoute of manifest.pageRoutes) {
let collectImports = function (entry, {
type = "js"
} = {}) {
const {
imports = [],
css
} = entry;
const cacheKey = `${entry.file || imports.join(",")}:${type}`;
const cached = collectImportsCache.get(cacheKey);
if (cached) return cached;
const result = [...new Set([...(type === "js" ? imports : css || []), ...imports.flatMap(name => {
const found = vxrnOutput.clientManifest[name];
if (!found) {
console.warn(`No found imports`, name, vxrnOutput.clientManifest);
}
return collectImports(found, {
type
});
})].flat().filter(x => x && (type === "css" || x.endsWith(".js"))).map(x => type === "css" ? x : x.startsWith("assets/") ? x : `assets/${x.slice(1)}`))];
collectImportsCache.set(cacheKey, result);
return result;
};
if (!foundRoute.file) {
continue;
}
const routeModulePath = join(resolve(process.cwd(), options.root), routerRoot, foundRoute.file.slice(2));
const serverFileName = moduleIdToServerChunk.get(routeModulePath);
if (!serverFileName) {
if (foundRoute.type === "spa") {
continue;
}
console.warn(`[one] No server chunk found for route: ${foundRoute.file}`);
continue;
}
const onlyBuild = vxrnOutput.buildArgs?.only;
if (onlyBuild) {
const relativeId2 = foundRoute.file.slice(1);
if (!MicroMatch.contains(relativeId2, onlyBuild)) {
continue;
}
}
const clientChunk = clientChunksBySource.get(routeModulePath);
const manifestKey = `${routerRoot}${foundRoute.file.slice(1)}`;
const clientManifestEntry = vxrnOutput.clientManifest[manifestKey];
if (!clientChunk && foundRoute.type !== "spa" && foundRoute.type !== "ssg") {
console.warn(`No client chunk found for route: ${routeModulePath}`);
continue;
}
foundRoute.loaderServerPath = serverFileName;
const relativeId = foundRoute.file.replace(/^\.\//, "/");
if (foundRoute.layouts) {
for (const layout of foundRoute.layouts) {
const serverPath = layoutServerPaths.get(layout.contextKey);
if (serverPath) {
layout.loaderServerPath = serverPath;
}
}
}
const entryImports = collectImports(clientManifestEntry || {});
const layoutEntries = foundRoute.layouts?.flatMap(layout => {
const clientKey = `${routerRoot}${layout.contextKey.slice(1)}`;
const found = vxrnOutput.clientManifest[clientKey];
return found ? found : [];
}) ?? [];
const layoutImports = layoutEntries.flatMap(entry => {
return [entry.file, ...collectImports(entry)];
});
const routePreloads = {};
const rootLayoutKey = `${routerRoot}/_layout.tsx`;
const rootLayoutEntry = vxrnOutput.clientManifest[rootLayoutKey];
if (rootLayoutEntry) {
routePreloads[`/${rootLayoutKey}`] = `/${rootLayoutEntry.file}`;
}
if (foundRoute.layouts) {
for (const layout of foundRoute.layouts) {
const clientKey = `${routerRoot}${layout.contextKey.slice(1)}`;
const entry = vxrnOutput.clientManifest[clientKey];
if (entry) {
routePreloads[`/${clientKey}`] = `/${entry.file}`;
}
}
}
if (clientChunk) {
const routeKey = `/${routerRoot}${foundRoute.file.slice(1)}`;
routePreloads[routeKey] = `/${clientChunk.fileName}`;
} else if (clientManifestEntry) {
const routeKey = `/${routerRoot}${foundRoute.file.slice(1)}`;
routePreloads[routeKey] = `/${clientManifestEntry.file}`;
}
const preloadSetupFilePreloads = (() => {
if (!oneOptions.setupFile) return [];
const clientSetupFile = typeof oneOptions.setupFile === "string" ? oneOptions.setupFile : oneOptions.setupFile.client;
if (!clientSetupFile) return [];
const needle = clientSetupFile.replace(/^\.\//, "");
for (const file in vxrnOutput.clientManifest) {
if (file === needle) {
const entry = vxrnOutput.clientManifest[file];
return [entry.file
// getting 404s for preloading the imports as well?
// ...(entry.imports as string[])
];
}
}
return [];
})();
const allPreloads = [... /* @__PURE__ */new Set([...preloadSetupFilePreloads,
// add the route entry js (like ./app/index.ts) - prefer direct chunk lookup
...(clientChunk ? [clientChunk.fileName] : clientManifestEntry ? [clientManifestEntry.file] : []),
// add the virtual entry
vxrnOutput.clientManifest["virtual:one-entry"].file, ...entryImports, ...layoutImports])].map(path => `/${path}`);
const scriptLoadingMode = oneOptions.web?.experimental_scriptLoading;
const useDeferredLoading = scriptLoadingMode === "defer-non-critical";
const useAggressiveLCP = scriptLoadingMode === "after-lcp-aggressive";
const needsSeparatedPreloads = useDeferredLoading || useAggressiveLCP;
const criticalPreloads = needsSeparatedPreloads ? [... /* @__PURE__ */new Set([...preloadSetupFilePreloads,
// add the virtual entry (framework bootstrap)
vxrnOutput.clientManifest["virtual:one-entry"].file,
// add the route entry js (like ./app/index.ts) - prefer direct chunk lookup
...(clientChunk ? [clientChunk.fileName] : clientManifestEntry ? [clientManifestEntry.file] : []),
// add layout files (but not their deep imports)
...layoutEntries.map(entry => entry.file)])].map(path => `/${path}`) : void 0;
const deferredPreloads = needsSeparatedPreloads ? [... /* @__PURE__ */new Set([...entryImports, ...layoutEntries.flatMap(entry => collectImports(entry))])].filter(path => !criticalPreloads.includes(`/${path}`)).map(path => `/${path}`) : void 0;
const preloads2 = needsSeparatedPreloads ? [...criticalPreloads, ...deferredPreloads] : allPreloads;
const allEntries = [clientManifestEntry, ...layoutEntries].filter(Boolean);
const layoutCSS = [...new Set(layoutEntries.flatMap(entry => collectImports(entry, {
type: "css"
})).map(path => `/${path}`))];
const allCSS = [... /* @__PURE__ */new Set([...layoutCSS,
// css from page entry
...(clientManifestEntry ? collectImports(clientManifestEntry, {
type: "css"
}).map(path => `/${path}`) : []),
// root-level css (handles cssCodeSplit: false)
...Object.entries(vxrnOutput.clientManifest).filter(([key]) => key.endsWith(".css")).map(([, entry]) => `/${entry.file}`)])];
const hasCriticalCSS = allCSS.some(p => criticalCSSOutputPaths.has(p));
const needsCSSContents = oneOptions.web?.inlineLayoutCSS || hasCriticalCSS;
let allCSSContents;
if (needsCSSContents) {
allCSSContents = await Promise.all(allCSS.map(async cssPath => {
if (!oneOptions.web?.inlineLayoutCSS && !criticalCSSOutputPaths.has(cssPath)) {
return "";
}
const cached = cssFileContentsCache.get(cssPath);
if (cached !== void 0) return cached;
const filePath = join(clientDir, cssPath);
try {
const content = await FSExtra.readFile(filePath, "utf-8");
cssFileContentsCache.set(cssPath, content);
return content;
} catch (err) {
console.warn(`[one] Warning: Could not read CSS file ${filePath}`);
cssFileContentsCache.set(cssPath, "");
return "";
}
}));
}
if (process.env.DEBUG) {
console.info("[one] building routes", {
foundRoute,
layoutEntries,
allEntries,
allCSS
});
}
const serverJsPath = join(`${outDir}/server`, serverFileName);
let exported;
try {
exported = await import(toAbsolute(serverJsPath));
} catch (err) {
console.error(`Error importing page (original error)`, err);
throw new Error(`Error importing page: ${serverJsPath}`, {
cause: err
});
}
foundRoute.hasLoader = typeof exported.loader === "function";
const isDynamic = !!Object.keys(foundRoute.routeKeys).length;
if (foundRoute.type === "ssg" && isDynamic && !foundRoute.page.includes("+not-found") && !foundRoute.page.includes("_sitemap") && !exported.generateStaticParams) {
throw new Error(`[one] Error: Missing generateStaticParams
Route ${foundRoute.page} of type ${foundRoute.type} must export generateStaticParams so build can complete.
See docs on generateStaticParams:
https://onestack.dev/docs/routing-exports#generatestaticparams
`);
}
const paramsList = (await exported.generateStaticParams?.()) ?? [{}];
console.info(`
[build] page ${relativeId} (with ${paramsList.length} routes)
`);
if (process.env.DEBUG) {
console.info(`paramsList`, JSON.stringify(paramsList, null, 2));
}
const routeSitemapExport = exported.sitemap;
const isAfterLCPMode = scriptLoadingMode === "after-lcp" || scriptLoadingMode === "after-lcp-aggressive";
const useAfterLCP = foundRoute.type === "ssg" && isAfterLCPMode;
const useAfterLCPAggressive = foundRoute.type === "ssg" && scriptLoadingMode === "after-lcp-aggressive";
const shouldCollectSitemap = foundRoute.type !== "api" && foundRoute.type !== "layout" && !foundRoute.isNotFound && !foundRoute.page.includes("+not-found") && !foundRoute.page.includes("_sitemap");
const pageBuilds = paramsList.map(params => {
const path = getPathnameFromFilePath(relativeId, params, foundRoute.type === "ssg");
if (workerPool) {
console.info(` \u21A6 route ${path}`);
return workerPool.buildPage({
serverEntry: vxrnOutput.serverEntry,
path,
relativeId,
params,
foundRoute,
clientManifestEntry,
staticDir,
clientDir,
builtMiddlewares,
serverJsPath,
preloads: preloads2,
allCSS,
layoutCSS,
routePreloads,
allCSSContents,
criticalPreloads,
deferredPreloads,
useAfterLCP,
useAfterLCPAggressive
}).then(built => ({
built,
path
})).catch(err => {
console.warn(` \u26A0 skipping page ${path}: ${err.message}`);
return null;
});
}
return limit(async () => {
console.info(` \u21A6 route ${path}`);
try {
const built = await runWithAsyncLocalContext(async () => {
return await buildPage(vxrnOutput.serverEntry, path, relativeId, params, foundRoute, clientManifestEntry, staticDir, clientDir, builtMiddlewares, serverJsPath, preloads2, allCSS, layoutCSS, routePreloads, allCSSContents, criticalPreloads, deferredPreloads, useAfterLCP, useAfterLCPAggressive);
});
return {
built,
path
};
} catch (err) {
console.warn(` \u26A0 skipping page ${path}: ${err.message}`);
return null;
}
});
});
const results = (await Promise.all(pageBuilds)).filter(Boolean);
for (const {
built,
path
} of results) {
builtRoutes.push(built);
if (shouldCollectSitemap) {
sitemapData.push({
path,
routeExport: routeSitemapExport
});
}
}
}
if (workerPool) {
await terminateWorkerPool();
}
const staticTime = performance.now() - staticStartTime;
console.info(`
\u23F1\uFE0F static routes: ${(staticTime / 1e3).toFixed(2)}s (${builtRoutes.length} pages)
`);
printBuildTimings();
await moveAllFiles(staticDir, clientDir);
await FSExtra.rm(staticDir, {
force: true,
recursive: true
});
const routeMap = {};
const routeToBuildInfo = {};
const pathToRoute = {};
const preloads = {};
const cssPreloads = {};
const loaders = {};
for (const route of builtRoutes) {
if (!route.cleanPath.includes("*")) {
routeMap[route.cleanPath] = route.htmlPath;
}
const {
// dont include loaderData it can be huge
loaderData: _loaderData,
...rest
} = route;
routeToBuildInfo[route.routeFile] = rest;
for (const p of getCleanPaths([route.path, route.cleanPath])) {
pathToRoute[p] = route.routeFile;
}
preloads[route.preloadPath] = true;
cssPreloads[route.cssPreloadPath] = true;
loaders[route.loaderPath] = true;
}
function createBuildManifestRoute(route) {
const {
layouts,
...built
} = route;
if (layouts?.length) {
;
built.layouts = layouts.map(layout => ({
contextKey: layout.contextKey,
loaderServerPath: layout.loaderServerPath
}));
}
const buildInfo = builtRoutes.find(x => x.routeFile === route.file);
if (built.middlewares && buildInfo?.middlewares) {
for (const [index, mw] of built.middlewares.entries()) {
mw.contextKey = buildInfo.middlewares[index];
}
}
if (buildInfo) {
built.loaderPath = buildInfo.loaderPath;
}
return built;
}
const buildInfoForWriting = {
outDir,
oneOptions,
routeToBuildInfo,
pathToRoute,
manifest: {
pageRoutes: manifest.pageRoutes.map(createBuildManifestRoute),
apiRoutes: manifest.apiRoutes.map(createBuildManifestRoute),
allRoutes: manifest.allRoutes.map(createBuildManifestRoute)
},
routeMap,
constants: JSON.parse(JSON.stringify({
...constants
})),
preloads,
cssPreloads,
loaders,
useRolldown: await isRolldown()
};
await writeJSON(toAbsolute(`${outDir}/buildInfo.json`), buildInfoForWriting);
await FSExtra.writeFile(join(clientDir, "version.json"), JSON.stringify({
version: constants.CACHE_KEY
}));
console.info(`
\u{1F6E1} skew protection: emitted version.json
`);
const sitemapConfig = oneOptions.web?.sitemap;
if (sitemapConfig) {
const sitemapOptions = typeof sitemapConfig === "boolean" ? {} : sitemapConfig;
const sitemapXml = generateSitemap(sitemapData, sitemapOptions);
const sitemapPath = join(clientDir, "sitemap.xml");
await FSExtra.writeFile(sitemapPath, sitemapXml);
console.info(`
\u{1F4C4} generated sitemap.xml (${sitemapData.length} URLs)
`);
}
const postBuildLogs = [];
const platform = deployConfig?.target;
if (platform) {
postBuildLogs.push(`[one.build] platform ${platform}`);
}
switch (platform) {
case "vercel":
{
const vercelJsonPath = join(options.root, "vercel.json");
if (FSExtra.existsSync(vercelJsonPath)) {
try {
const vercelConfig = JSON.parse(FSExtra.readFileSync(vercelJsonPath, "utf-8"));
if (!vercelConfig.cleanUrls) {
console.warn(`
\u26A0\uFE0F Warning: Your vercel.json is missing "cleanUrls": true`);
console.warn(` Without this, direct navigation to SSG pages will 404.`);
console.warn(` Add "cleanUrls": true to your vercel.json to fix this.
`);
}
} catch {}
}
await buildVercelOutputDirectory({
apiOutput,
buildInfoForWriting,
clientDir,
oneOptionsRoot: options.root,
postBuildLogs
});
break;
}
case "cloudflare":
{
const pageRouteMap = [];
const apiRouteMap = [];
const middlewareRouteMap = [];
for (const [routeFile, info] of Object.entries(buildInfoForWriting.routeToBuildInfo)) {
if (info.serverJsPath) {
const importPath = "./" + info.serverJsPath.replace(new RegExp(`^${outDir}/`), "");
pageRouteMap.push(` '${routeFile}': () => import('${importPath}')`);
}
}
for (const route of buildInfoForWriting.manifest.apiRoutes) {
if (route.file) {
const apiFileName = route.page.slice(1).replace(/\[/g, "_").replace(/\]/g, "_");
const importPath = `./api/${apiFileName}.js`;
apiRouteMap.push(` '${route.page}': () => import('${importPath}')`);
}
}
for (const [, builtPath] of Object.entries(builtMiddlewares)) {
const importPath = "./" + builtPath.replace(new RegExp(`^${outDir}/`), "");
middlewareRouteMap.push(` '${builtPath}': () => import('${importPath}')`);
}
const workerSrcPath = join(options.root, outDir, "_worker-src.js");
const workerCode = `// Polyfill MessageChannel for React SSR (not available in Cloudflare Workers by default)
if (typeof MessageChannel === 'undefined') {
globalThis.MessageChannel = class MessageChannel {
constructor() {
this.port1 = { postMessage: () => {}, onmessage: null, close: () => {} }
this.port2 = { postMessage: () => {}, onmessage: null, close: () => {} }
}
}
}
import { serve, setFetchStaticHtml } from 'one/serve-worker'
// Lazy import map - modules load on-demand when route is matched
const lazyRoutes = {
serverEntry: () => import('./server/_virtual_one-entry.js'),
pages: {
${pageRouteMap.join(",\n")}
},
api: {
${apiRouteMap.join(",\n")}
},
middlewares: {
${middlewareRouteMap.join(",\n")}
}
}
const buildInfo = ${JSON.stringify(buildInfoForWriting)}
let server
export default {
async fetch(request, env, ctx) {
if (!server) {
server = await serve(buildInfo, lazyRoutes)
}
// set up static HTML fetcher for this request (uses ASSETS binding)
if (env.ASSETS) {
setFetchStaticHtml(async (path) => {
try {
const url = new URL(request.url)
url.pathname = path
const assetResponse = await env.ASSETS.fetch(new Request(url))
if (assetResponse && assetResponse.ok) {
return await assetResponse.text()
}
} catch (e) {
// asset not found
}
return null
})
}
try {
const response = await server.fetch(request)
// no route matched or 404 \u2192 try static assets
if (!response || response.status === 404) {
if (env.ASSETS) {
try {
const assetResponse = await env.ASSETS.fetch(request)
if (assetResponse && assetResponse.status !== 404) {
return assetResponse
}
} catch (e) {
// asset not found, continue with original response
}
}
}
return response
} finally {
setFetchStaticHtml(null)
}
}
}
`;
await FSExtra.writeFile(workerSrcPath, workerCode);
console.info("\n [cloudflare] Bundling worker...");
await viteBuild({
root: options.root,
mode: "production",
logLevel: "warn",
build: {
outDir,
emptyOutDir: false,
// Use SSR mode with node target for proper Node.js module resolution
ssr: workerSrcPath,
rolldownOptions: {
external: [
// React Native dev tools - not needed in production
"@react-native/dev-middleware", "@react-native/debugger-shell", "metro", "metro-core", "metro-runtime",
// Native modules that can't run in workers
/\.node$/],
output: {
entryFileNames: "worker.js",
format: "es",
// Keep dynamic imports separate for lazy loading
inlineDynamicImports: false
}
},
minify: true,
target: "esnext"
},
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
"process.env.VITE_ENVIRONMENT": JSON.stringify("ssr")
},
resolve: {
conditions: ["workerd", "worker", "node", "module", "default"],
alias: [
// rolldown can't parse react-native's Flow syntax; alias to react-native-web for ssr
{
find: /^react-native\/Libraries\/.*/,
replacement: resolvePath("@vxrn/vite-plugin-metro/empty", options.root)
}, {
find: "react-native/package.json",
replacement: resolvePath("react-native-web/package.json", options.root)
}, {
find: "react-native",
replacement: resolvePath("react-native-web", options.root)
}, {
find: "react-native-safe-area-context",
replacement: resolvePath("@vxrn/safe-area", options.root)
}]
},
ssr: {
target: "node",
noExternal: true
}
});
await FSExtra.remove(workerSrcPath);
const projectName = await getCloudflareProjectName(options.root);
const userWranglerConfig = await loadUserWranglerConfig(options.root);
const wranglerConfig = createCloudflareWranglerConfig(projectName, userWranglerConfig?.config);
if (userWranglerConfig) {
console.info(` [cloudflare] Merging ${relative(options.root, userWranglerConfig.path)} into ${outDir}/wrangler.jsonc`);
}
await FSExtra.writeFile(join(options.root, outDir, "wrangler.jsonc"), `${JSON.stringify(wranglerConfig, null, 2)}
`);
postBuildLogs.push(`Cloudflare worker bundled at ${outDir}/worker.js`);
postBuildLogs.push(`To deploy: cd ${outDir} && wrangler deploy`);
break;
}
}
const securityScanOption = oneOptions.build?.securityScan;
const securityScanLevel = securityScanOption === false ? null : securityScanOption === true || securityScanOption === void 0 ? "warn" : typeof securityScanOption === "string" ? securityScanOption : securityScanOption.level ?? "warn";
const securitySafePatterns = typeof securityScanOption === "object" && securityScanOption !== null ? securityScanOption.safePatterns : void 0;
if (securityScanLevel) {
const {
runSecurityScan
} = await import("./securityScan.mjs");
const passed = await runSecurityScan(clientDir, securityScanLevel, securitySafePatterns);
if (!passed) {
process.exit(1);
}
}
if (postBuildLogs.length) {
console.info(`
`);
postBuildLogs.forEach(log => {
console.info(` \xB7 ${log}`);
});
}
console.info(`
\u{1F49B} build complete
`);
}
const TRAILING_INDEX_REGEX = /\/index(\.(web))?/;
function getCleanPaths(possiblePaths) {
return Array.from(new Set(Array.from(new Set(possiblePaths)).flatMap(p => {
const paths = [p];
if (p.match(TRAILING_INDEX_REGEX)) {
const pathWithTrailingIndexRemoved = p.replace(TRAILING_INDEX_REGEX, "");
paths.push(pathWithTrailingIndexRemoved);
paths.push(pathWithTrailingIndexRemoved + "/");
}
return paths;
})));
}
async function moveAllFiles(src, dest) {
try {
await FSExtra.copy(src, dest, {
overwrite: true,
errorOnExist: false
});
} catch (err) {
console.error("Error moving files:", err);
}
}
export { build };
//# sourceMappingURL=build.mjs.map