diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts index e879ed792..51ef46859 100755 --- a/packages/open-next/src/build.ts +++ b/packages/open-next/src/build.ts @@ -35,8 +35,6 @@ import { OpenNextConfig } from "./types/open-next.js"; const require = topLevelCreateRequire(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); -let options: BuildOptions; -let config: OpenNextConfig; export type PublicFiles = { files: string[]; @@ -57,7 +55,7 @@ export async function build( ); // On Windows, we need to use file:// protocol to load the config file using import() if (process.platform === "win32") configPath = `file://${configPath}`; - config = (await import(configPath)).default as OpenNextConfig; + const config = (await import(configPath)).default as OpenNextConfig; if (!config || !config.default) { logger.error( `config.default cannot be empty, it should be at least {}, see more info here: https://open-next.js.org/config#configuration-file`, @@ -68,42 +66,38 @@ export async function build( compileOpenNextConfigEdge(tempDir, config, openNextConfigPath); - const { root: monorepoRoot, packager } = findMonorepoRoot( - path.join(process.cwd(), config.appPath || "."), - ); - // Initialize options - options = normalizeOptions(config, monorepoRoot); + const options = normalizeOptions(config); logger.setLevel(options.debug ? "debug" : "info"); // Pre-build validation - checkRunningInsideNextjsApp(); - printNextjsVersion(); - printOpenNextVersion(); + checkRunningInsideNextjsApp(options); + printNextjsVersion(options); + printOpenNextVersion(options); // Build Next.js app printHeader("Building Next.js app"); - setStandaloneBuildMode(monorepoRoot); - buildNextjsApp(packager); + setStandaloneBuildMode(options); + buildNextjsApp(options); // Generate deployable bundle printHeader("Generating bundle"); - initOutputDir(tempDir); + initOutputDir(tempDir, options); // Compile cache.ts - compileCache(); + compileCache(options); // Compile middleware - await createMiddleware(); + await createMiddleware(options); - createStaticAssets(); - await createCacheAssets(monorepoRoot); + createStaticAssets(options); + await createCacheAssets(options); - await createServerBundle(config, options); - await createRevalidationBundle(config); - await createImageOptimizationBundle(config); - await createWarmerBundle(config); - await generateOutput(options.appBuildOutputPath, config); + await createServerBundle(options); + await createRevalidationBundle(options); + await createImageOptimizationBundle(options); + await createWarmerBundle(options); + await generateOutput(options); logger.info("OpenNext build complete."); } @@ -127,7 +121,7 @@ function initTempDir() { return tempDir; } -function checkRunningInsideNextjsApp() { +function checkRunningInsideNextjsApp(options: BuildOptions) { const { appPath } = options; const extension = ["js", "cjs", "mjs", "ts"].find((ext) => fs.existsSync(path.join(appPath, `next.config.${ext}`)), @@ -140,40 +134,15 @@ function checkRunningInsideNextjsApp() { } } -function findMonorepoRoot(appPath: string) { - let currentPath = appPath; - while (currentPath !== "/") { - const found = [ - { file: "package-lock.json", packager: "npm" as const }, - { file: "yarn.lock", packager: "yarn" as const }, - { file: "pnpm-lock.yaml", packager: "pnpm" as const }, - { file: "bun.lockb", packager: "bun" as const }, - ].find((f) => fs.existsSync(path.join(currentPath, f.file))); - - if (found) { - if (currentPath !== appPath) { - logger.info("Monorepo detected at", currentPath); - } - return { root: currentPath, packager: found.packager }; - } - currentPath = path.dirname(currentPath); - } - - // note: a lock file (package-lock.json, yarn.lock, or pnpm-lock.yaml) is - // not found in the app's directory or any of its parent directories. - // We are going to assume that the app is not part of a monorepo. - return { root: appPath, packager: "npm" as const }; -} - -function setStandaloneBuildMode(monorepoRoot: string) { - // Equivalent to setting `target: "standalone"` in next.config.js +function setStandaloneBuildMode(options: BuildOptions) { + // Equivalent to setting `output: "standalone"` in next.config.js process.env.NEXT_PRIVATE_STANDALONE = "true"; // Equivalent to setting `experimental.outputFileTracingRoot` in next.config.js - process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = monorepoRoot; + process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = options.monorepoRoot; } -function buildNextjsApp(packager: "npm" | "yarn" | "pnpm" | "bun") { - const { appPackageJsonPath } = options; +function buildNextjsApp(options: BuildOptions) { + const { config, packager } = options; const command = config.buildCommand ?? (["bun", "npm"].includes(packager) @@ -181,7 +150,7 @@ function buildNextjsApp(packager: "npm" | "yarn" | "pnpm" | "bun") { : `${packager} build`); cp.execSync(command, { stdio: "inherit", - cwd: path.dirname(appPackageJsonPath), + cwd: path.dirname(options.appPackageJsonPath), }); } @@ -198,47 +167,48 @@ function printHeader(header: string) { ); } -function printNextjsVersion() { - const { nextVersion } = options; - logger.info(`Next.js version : ${nextVersion}`); +function printNextjsVersion(options: BuildOptions) { + logger.info(`Next.js version : ${options.nextVersion}`); } -function printOpenNextVersion() { - const { openNextVersion } = options; - logger.info(`OpenNext v${openNextVersion}`); +function printOpenNextVersion(options: BuildOptions) { + logger.info(`OpenNext v${options.openNextVersion}`); } -function initOutputDir(tempDir: string) { +function initOutputDir(srcTempDir: string, options: BuildOptions) { // We need to get the build relative to the cwd to find the compiled config // This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo // where the build is in the app directory, but the compiled config is in the root of the monorepo. - const { outputDir, tempDir: lTempDir } = options; const openNextConfig = readFileSync( - path.join(tempDir, "open-next.config.mjs"), + path.join(srcTempDir, "open-next.config.mjs"), "utf8", ); let openNextConfigEdge: string | null = null; - if (fs.existsSync(path.join(tempDir, "open-next.config.edge.mjs"))) { + if (fs.existsSync(path.join(srcTempDir, "open-next.config.edge.mjs"))) { openNextConfigEdge = readFileSync( - path.join(tempDir, "open-next.config.edge.mjs"), + path.join(srcTempDir, "open-next.config.edge.mjs"), "utf8", ); } - fs.rmSync(outputDir, { recursive: true, force: true }); - fs.mkdirSync(lTempDir, { recursive: true }); - fs.writeFileSync(path.join(lTempDir, "open-next.config.mjs"), openNextConfig); + fs.rmSync(options.outputDir, { recursive: true, force: true }); + const destTempDir = options.tempDir; + fs.mkdirSync(destTempDir, { recursive: true }); + fs.writeFileSync( + path.join(destTempDir, "open-next.config.mjs"), + openNextConfig, + ); if (openNextConfigEdge) { fs.writeFileSync( - path.join(lTempDir, "open-next.config.edge.mjs"), + path.join(destTempDir, "open-next.config.edge.mjs"), openNextConfigEdge, ); } } -async function createWarmerBundle(config: OpenNextConfig) { +async function createWarmerBundle(options: BuildOptions) { logger.info(`Bundling warmer function...`); - const { outputDir } = options; + const { config, outputDir } = options; // Create output folder const outputPath = path.join(outputDir, "warmer-function"); @@ -278,10 +248,10 @@ async function createWarmerBundle(config: OpenNextConfig) { ); } -async function createRevalidationBundle(config: OpenNextConfig) { +async function createRevalidationBundle(options: BuildOptions) { logger.info(`Bundling revalidation function...`); - const { appBuildOutputPath, outputDir } = options; + const { appBuildOutputPath, config, outputDir } = options; // Create output folder const outputPath = path.join(outputDir, "revalidation-function"); @@ -317,10 +287,10 @@ async function createRevalidationBundle(config: OpenNextConfig) { ); } -async function createImageOptimizationBundle(config: OpenNextConfig) { +async function createImageOptimizationBundle(options: BuildOptions) { logger.info(`Bundling image optimization function...`); - const { appPath, appBuildOutputPath, outputDir } = options; + const { appPath, appBuildOutputPath, config, outputDir } = options; // Create output folder const outputPath = path.join(outputDir, "image-optimization-function"); @@ -436,7 +406,7 @@ async function createImageOptimizationBundle(config: OpenNextConfig) { } } -function createStaticAssets() { +function createStaticAssets(options: BuildOptions) { logger.info(`Bundling static assets...`); const { appBuildOutputPath, appPublicPath, outputDir, appPath } = options; @@ -475,13 +445,14 @@ function createStaticAssets() { } } -async function createCacheAssets(monorepoRoot: string) { +async function createCacheAssets(options: BuildOptions) { + const { config } = options; if (config.dangerous?.disableIncrementalCache) return; logger.info(`Bundling cache assets...`); const { appBuildOutputPath, outputDir } = options; - const packagePath = path.relative(monorepoRoot, appBuildOutputPath); + const packagePath = path.relative(options.monorepoRoot, appBuildOutputPath); const buildId = getBuildId(appBuildOutputPath); // Copy pages to cache folder @@ -683,7 +654,11 @@ async function createCacheAssets(monorepoRoot: string) { /* Server Helper Functions */ /***************************/ -export function compileCache(format: "cjs" | "esm" = "cjs") { +export function compileCache( + options: BuildOptions, + format: "cjs" | "esm" = "cjs", +) { + const { config } = options; const ext = format === "cjs" ? "cjs" : "mjs"; const outfile = path.join(options.outputDir, ".build", `cache.${ext}`); @@ -713,10 +688,10 @@ export function compileCache(format: "cjs" | "esm" = "cjs") { return outfile; } -async function createMiddleware() { +async function createMiddleware(options: BuildOptions) { console.info(`Bundling middleware function...`); - const { appBuildOutputPath, outputDir } = options; + const { appBuildOutputPath, config, outputDir } = options; // Get middleware manifest const middlewareManifest = JSON.parse( diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 7731c8c56..dc555f364 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -30,10 +30,8 @@ import { const require = topLevelCreateRequire(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); -export async function createServerBundle( - config: OpenNextConfig, - options: BuildOptions, -) { +export async function createServerBundle(options: BuildOptions) { + const { config } = options; const foundRoutes = new Set(); // Get all functions to build const defaultFn = config.default; @@ -44,7 +42,7 @@ export async function createServerBundle( defaultFn.runtime === "deno" || functions.some(([, fn]) => fn.runtime === "deno") ) { - compileCache("esm"); + compileCache(options, "esm"); } const promises = functions.map(async ([name, fnOptions]) => { diff --git a/packages/open-next/src/build/generateOutput.ts b/packages/open-next/src/build/generateOutput.ts index fadc40f12..645b9432d 100644 --- a/packages/open-next/src/build/generateOutput.ts +++ b/packages/open-next/src/build/generateOutput.ts @@ -7,11 +7,10 @@ import { DefaultOverrideOptions, FunctionOptions, LazyLoadedOverride, - OpenNextConfig, OverrideOptions, } from "types/open-next"; -import { getBuildId } from "./helper.js"; +import { type BuildOptions, getBuildId } from "./helper.js"; type BaseFunction = { handler: string; @@ -164,10 +163,8 @@ function prefixPattern(basePath: string) { } // eslint-disable-next-line sonarjs/cognitive-complexity -export async function generateOutput( - outputPath: string, - config: OpenNextConfig, -) { +export async function generateOutput(options: BuildOptions) { + const { appBuildOutputPath, config } = options; const edgeFunctions: OpenNextOutput["edgeFunctions"] = {}; const isExternalMiddleware = config.middleware?.external ?? false; if (isExternalMiddleware) { @@ -197,7 +194,7 @@ export async function generateOutput( //Load required-server-files.json const requiredServerFiles = JSON.parse( fs.readFileSync( - path.join(outputPath, ".next", "required-server-files.json"), + path.join(appBuildOutputPath, ".next", "required-server-files.json"), "utf-8", ), ).config as NextConfig; @@ -298,7 +295,9 @@ export async function generateOutput( const patterns = "patterns" in value ? value.patterns : ["*"]; patterns.forEach((pattern) => { behaviors.push({ - pattern: prefixer(pattern.replace(/BUILD_ID/, getBuildId(outputPath))), + pattern: prefixer( + pattern.replace(/BUILD_ID/, getBuildId(appBuildOutputPath)), + ), origin: value.placement === "global" ? undefined : key, edgeFunction: value.placement === "global" @@ -323,7 +322,7 @@ export async function generateOutput( }); //Compute behaviors for assets files - const assetPath = path.join(outputPath, ".open-next", "assets"); + const assetPath = path.join(appBuildOutputPath, ".open-next", "assets"); fs.readdirSync(assetPath).forEach((item) => { if (fs.statSync(path.join(assetPath, item)).isDirectory()) { behaviors.push({ @@ -341,7 +340,9 @@ export async function generateOutput( // Check if we produced a dynamodb provider output const isTagCacheDisabled = config.dangerous?.disableTagCache || - !fs.existsSync(path.join(outputPath, ".open-next", "dynamodb-provider")); + !fs.existsSync( + path.join(appBuildOutputPath, ".open-next", "dynamodb-provider"), + ); const output: OpenNextOutput = { edgeFunctions, @@ -369,7 +370,7 @@ export async function generateOutput( }, }; fs.writeFileSync( - path.join(outputPath, ".open-next", "open-next.output.json"), + path.join(appBuildOutputPath, ".open-next", "open-next.output.json"), JSON.stringify(output), ); } diff --git a/packages/open-next/src/build/helper.ts b/packages/open-next/src/build/helper.ts index 2f41d355b..539f39acc 100644 --- a/packages/open-next/src/build/helper.ts +++ b/packages/open-next/src/build/helper.ts @@ -17,7 +17,7 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); export type BuildOptions = ReturnType; -export function normalizeOptions(config: OpenNextConfig, root: string) { +export function normalizeOptions(config: OpenNextConfig) { const appPath = path.join(process.cwd(), config.appPath || "."); const buildOutputPath = path.join( process.cwd(), @@ -25,6 +25,10 @@ export function normalizeOptions(config: OpenNextConfig, root: string) { ); const outputDir = path.join(buildOutputPath, ".open-next"); + const { root: monorepoRoot, packager } = findMonorepoRoot( + path.join(process.cwd(), config.appPath || "."), + ); + let appPackageJsonPath: string; if (config.packageJsonPath) { const _pkgPath = path.join(process.cwd(), config.packageJsonPath); @@ -32,22 +36,50 @@ export function normalizeOptions(config: OpenNextConfig, root: string) { ? _pkgPath : path.join(_pkgPath, "./package.json"); } else { - appPackageJsonPath = findNextPackageJsonPath(appPath, root); + appPackageJsonPath = findNextPackageJsonPath(appPath, monorepoRoot); } + return { - openNextVersion: getOpenNextVersion(), - nextVersion: getNextVersion(appPath), + appBuildOutputPath: buildOutputPath, appPackageJsonPath, appPath, - appBuildOutputPath: buildOutputPath, appPublicPath: path.join(appPath, "public"), + config, + debug: Boolean(process.env.OPEN_NEXT_DEBUG) ?? false, + monorepoRoot, + nextVersion: getNextVersion(appPath), + openNextVersion: getOpenNextVersion(), outputDir, + packager, tempDir: path.join(outputDir, ".build"), - debug: Boolean(process.env.OPEN_NEXT_DEBUG) ?? false, - monorepoRoot: root, }; } +function findMonorepoRoot(appPath: string) { + let currentPath = appPath; + while (currentPath !== "/") { + const found = [ + { file: "package-lock.json", packager: "npm" as const }, + { file: "yarn.lock", packager: "yarn" as const }, + { file: "pnpm-lock.yaml", packager: "pnpm" as const }, + { file: "bun.lockb", packager: "bun" as const }, + ].find((f) => fs.existsSync(path.join(currentPath, f.file))); + + if (found) { + if (currentPath !== appPath) { + logger.info("Monorepo detected at", currentPath); + } + return { root: currentPath, packager: found.packager }; + } + currentPath = path.dirname(currentPath); + } + + // note: a lock file (package-lock.json, yarn.lock, or pnpm-lock.yaml) is + // not found in the app's directory or any of its parent directories. + // We are going to assume that the app is not part of a monorepo. + return { root: appPath, packager: "npm" as const }; +} + function findNextPackageJsonPath(appPath: string, root: string) { // This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo return fs.existsSync(path.join(appPath, "./package.json"))