UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

347 lines (340 loc) 14.2 kB
import FSExtra from "fs-extra"; import MicroMatch from "micromatch"; import { createRequire } from "node:module"; import Path, { join, relative, resolve } from "node:path"; 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 { toAbsolute } from "../utils/toAbsolute.mjs"; import { getManifest } from "../vite/getManifest.mjs"; import { loadUserOneOptions } from "../vite/loadConfig.mjs"; import { runWithAsyncLocalContext } from "../vite/one-server-only.mjs"; import { buildVercelOutputDirectory } from "../vercel/build/buildVercelOutputDirectory.mjs"; import { getRouterRootFromOneOptions } from "../utils/getRouterRootFromOneOptions.mjs"; import { buildPage } from "./buildPage.mjs"; import { checkNodeVersion } from "./checkNodeVersion.mjs"; import { labelProcess } from "./label-process.mjs"; import { getPathnameFromFilePath } from "../utils/getPathnameFromFilePath.mjs"; const { ensureDir, writeJSON } = FSExtra; process.on("uncaughtException", err => { console.error(err?.message || err); }); async function build(args) { process.env.IS_VXRN_CLI = "true", process.env.NODE_ENV = "production", labelProcess("build"), checkNodeVersion(), setServerGlobals(); const { oneOptions } = await loadUserOneOptions("build"), routerRoot = getRouterRootFromOneOptions(oneOptions), routerRootRegexp = new RegExp(`^${routerRoot}`), manifest = getManifest({ routerRoot }), serverOutputFormat = oneOptions.build?.server === !1 ? "esm" : oneOptions.build?.server?.outputFormat ?? "esm", vxrnOutput = await vxrnBuild({ server: oneOptions.server, build: { analyze: !0, server: oneOptions.build?.server === !1 ? !1 : { outputFormat: serverOutputFormat } } }, args); if (!vxrnOutput || args.platform !== "web") return; const options = await fillOptions(vxrnOutput.options), { optimizeDeps } = getOptimizeDeps("build"), apiBuildConfig = mergeConfig( // feels like this should build off the *server* build config not web vxrnOutput.webBuildConfig, { configFile: !1, appType: "custom", optimizeDeps }); async function buildCustomRoutes(subFolder, routes) { const input = routes.reduce((entries, { page, file }) => (entries[page.slice(1) + ".js"] = join(routerRoot, file), entries), {}), outputFormat = oneOptions?.build?.api?.outputFormat ?? serverOutputFormat, treeshake = oneOptions?.build?.api?.treeshake, mergedConfig = mergeConfig(apiBuildConfig, { appType: "custom", configFile: !1, // plugins: [ // nodeExternals({ // exclude: optimizeDeps.include, // }) as any, // ], define: { ...vxrnOutput.processEnvDefines }, ssr: { noExternal: !0, external: ["react", "react-dom"], optimizeDeps }, build: { ssr: !0, emptyOutDir: !1, outDir: `dist/${subFolder}`, copyPublicDir: !1, minify: !1, rollupOptions: { treeshake: treeshake ?? { moduleSideEffects: !1 }, 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: id => !1, output: { entryFileNames: "[name]", exports: "auto", ...(outputFormat === "esm" ? { format: "esm", esModule: !0 } : { format: "cjs", // Preserve folder structure and use .cjs extension entryFileNames: chunkInfo => chunkInfo.name.replace(/\.js$/, ".cjs"), chunkFileNames: chunkInfo => { const dir = Path.dirname(chunkInfo.name), name = Path.basename(chunkInfo.name, Path.extname(chunkInfo.name)); return Path.join(dir, `${name}-[hash].cjs`); }, assetFileNames: assetInfo => { const name = assetInfo.name ?? "", dir = Path.dirname(name), baseName = Path.basename(name, Path.extname(name)), ext = Path.extname(name); return Path.join(dir, `${baseName}-[hash]${ext}`); } }) } } } }), userApiBuildConf = oneOptions.build?.api?.config, finalApiBuildConf = userApiBuildConf ? mergeConfig(mergedConfig, userApiBuildConf) : mergedConfig; return await viteBuild( // allow user merging api build config finalApiBuildConf); } let apiOutput = null; manifest.apiRoutes.length && (console.info(` \u{1F528} build api routes `), apiOutput = await buildCustomRoutes("api", manifest.apiRoutes)); const builtMiddlewares = {}; if (manifest.middlewareRoutes.length) { console.info(` \u{1F528} build middlewares `); const middlewareBuildInfo = await buildCustomRoutes("middlewares", manifest.middlewareRoutes); for (const middleware of manifest.middlewareRoutes) { const absoluteRoot = resolve(process.cwd(), options.root), fullPath = join(absoluteRoot, routerRoot, middleware.file), chunk = middlewareBuildInfo.output.filter(x => x.type === "chunk").find(x => x.facadeModuleId === fullPath); if (!chunk) throw new Error("internal err finding middleware"); builtMiddlewares[middleware.file] = join("dist", "middlewares", chunk.fileName); } } globalThis.require = createRequire(join(import.meta.url, "..")); const assets = [], builtRoutes = []; console.info(` \u{1F528} build static routes `); const staticDir = join("dist/static"), clientDir = join("dist/client"); if (await ensureDir(staticDir), !vxrnOutput.serverOutput) throw new Error("No server output"); const outputEntries = [...vxrnOutput.serverOutput.entries()]; for (const [index, output] of outputEntries) { let collectImports = function ({ imports = [], css }, { type = "js" } = {}) { return [...new Set([...(type === "js" ? imports : css || []), ...imports.flatMap(name => { const found = vxrnOutput.clientManifest[name]; return found || console.warn("No found imports", name, vxrnOutput.clientManifest), collectImports(found, { type }); })].flat().filter(x => x && (type === "css" || x.endsWith(".js"))).map(x => type === "css" || x.startsWith("assets/") ? x : `assets/${x.slice(1)}`))]; }; if (output.type === "asset") { assets.push(output); continue; } const id = output.facadeModuleId || "", file = Path.basename(id); if (!id || file[0] === "_" || file.includes("entry-server") || id.includes("+api") || !id.includes(`/${routerRoot}/`)) continue; const relativeId = relative(process.cwd(), id).replace(`${routerRoot}/`, "/"), onlyBuild = vxrnOutput.buildArgs?.only; if (onlyBuild && !MicroMatch.contains(relativeId, onlyBuild)) continue; const clientManifestKey = Object.keys(vxrnOutput.clientManifest).find(key => id.endsWith(key)) || ""; if (!clientManifestKey) continue; const clientManifestEntry = vxrnOutput.clientManifest[clientManifestKey], foundRoute = manifest.pageRoutes.find(route => route.file && clientManifestKey.replace(routerRootRegexp, "") === route.file.slice(1)); if (!foundRoute) continue; foundRoute.loaderServerPath = output.fileName, clientManifestEntry || console.warn(`No client manifest entry found: ${clientManifestKey} in manifest ${JSON.stringify(vxrnOutput.clientManifest, null, 2)}`); const entryImports = collectImports(clientManifestEntry || {}), layoutEntries = foundRoute.layouts?.flatMap(layout => { const clientKey = `${routerRoot}${layout.contextKey.slice(1)}`; return vxrnOutput.clientManifest[clientKey]; }) ?? [], layoutImports = layoutEntries.flatMap(entry => [entry.file, ...collectImports(entry)]), preloadSetupFilePreloads = (() => { if (oneOptions.setupFile) { const needle = oneOptions.setupFile.replace(/^\.\//, ""); for (const file2 in vxrnOutput.clientManifest) if (file2 === needle) return [vxrnOutput.clientManifest[file2].file // getting 404s for preloading the imports as well? // ...(entry.imports as string[]) ]; } return []; })(), preloads2 = [... /* @__PURE__ */new Set([...preloadSetupFilePreloads, // add the route entry js (like ./app/index.ts) clientManifestEntry.file, // add the virtual entry vxrnOutput.clientManifest["virtual:one-entry"].file, ...entryImports, ...layoutImports])].map(path => `/${path}`), allEntries = [clientManifestEntry, ...layoutEntries], allCSS = allEntries.flatMap(entry => collectImports(entry, { type: "css" })).map(path => `/${path}`); process.env.DEBUG && console.info("[one] building routes", { foundRoute, layoutEntries, allEntries, allCSS }); const serverJsPath = join("dist/server", output.fileName); let exported; try { exported = await import(toAbsolute(serverJsPath)); } catch (err) { throw console.error("Error importing page (original error)", err), new Error(`Error importing page: ${serverJsPath}`, { cause: err }); } 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) `), process.env.DEBUG && console.info("paramsList", JSON.stringify(paramsList, null, 2)); for (const params of paramsList) { const path = getPathnameFromFilePath(relativeId, params, foundRoute.type === "ssg"); console.info(` \u21A6 route ${path}`); const built = await runWithAsyncLocalContext(async () => await buildPage(vxrnOutput.serverEntry, path, relativeId, params, foundRoute, clientManifestEntry, staticDir, clientDir, builtMiddlewares, serverJsPath, preloads2, allCSS)); builtRoutes.push(built); } } await moveAllFiles(staticDir, clientDir), await FSExtra.rm(staticDir, { force: !0, recursive: !0 }); const routeMap = {}, routeToBuildInfo = {}, pathToRoute = {}, preloads = {}, loaders = {}; for (const route of builtRoutes) { 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 (let p of getCleanPaths([route.path, route.cleanPath])) pathToRoute[p] = route.routeFile; preloads[route.preloadPath] = !0, loaders[route.loaderPath] = !0; } function createBuildManifestRoute(route) { const { layouts, ...built } = route, 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]; return buildInfo && (built.loaderPath = buildInfo.loaderPath), built; } const buildInfoForWriting = { 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, loaders }; await writeJSON(toAbsolute("dist/buildInfo.json"), buildInfoForWriting); let postBuildLogs = []; const platform = oneOptions.web?.deploy; switch (platform && postBuildLogs.push(`[one.build] platform ${platform}`), platform) { case "vercel": { await buildVercelOutputDirectory({ apiOutput, buildInfoForWriting, clientDir, oneOptionsRoot: options.root, postBuildLogs }); break; } } process.env.VXRN_ANALYZE_BUNDLE && postBuildLogs.push(`client build report: ${toAbsolute("dist/report.html")}`), 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: !0, errorOnExist: !1 }); } catch (err) { console.error("Error moving files:", err); } } export { build }; //# sourceMappingURL=build.mjs.map