diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts index 4d8b57fc4..9c927801a 100755 --- a/packages/open-next/src/build.ts +++ b/packages/open-next/src/build.ts @@ -29,35 +29,27 @@ import { OpenNextConfig } from "./types/open-next.js"; const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); let options: BuildOptions; +let config: OpenNextConfig; export type PublicFiles = { files: string[]; }; export async function build(openNextConfigPath?: string) { - const outputTmpPath = path.join(process.cwd(), ".open-next", ".build"); + showWindowsWarning(); - if (os.platform() === "win32") { - logger.error( - "OpenNext is not properly supported on Windows. On windows you should use WSL. It might works or it might fail in unpredictable way at runtime", - ); - // Wait 10s here so that the user see this message - await new Promise((resolve) => setTimeout(resolve, 10000)); - } - - // Compile open-next.config.ts - createOpenNextConfigBundle(outputTmpPath, openNextConfigPath); - - const config = await import(outputTmpPath + "/open-next.config.mjs"); - const opts = config.default as OpenNextConfig; - validateConfig(opts); + // Load open-next.config.ts + const tempDir = initTempDir(); + const configPath = compileOpenNextConfig(tempDir, openNextConfigPath); + config = (await import(configPath)).default as OpenNextConfig; + validateConfig(config); const { root: monorepoRoot, packager } = findMonorepoRoot( - path.join(process.cwd(), opts.appPath || "."), + path.join(process.cwd(), config.appPath || "."), ); // Initialize options - options = normalizeOptions(opts, monorepoRoot); + options = normalizeOptions(config, monorepoRoot); logger.setLevel(options.debug ? "debug" : "info"); // Pre-build validation @@ -75,57 +67,70 @@ export async function build(openNextConfigPath?: string) { initOutputDir(); // Compile cache.ts - compileCache(options); + compileCache(); // Compile middleware - await createMiddleware(opts); + await createMiddleware(); createStaticAssets(); - if (!options.dangerous?.disableIncrementalCache) { - await createCacheAssets(monorepoRoot, options.dangerous?.disableTagCache); - } - await createServerBundle(opts, options); + await createCacheAssets(monorepoRoot); + await createServerBundle(config, options); await createRevalidationBundle(); createImageOptimizationBundle(); await createWarmerBundle(); - await generateOutput(options.appBuildOutputPath, opts); + await generateOutput(options.appBuildOutputPath, config); } -function createOpenNextConfigBundle( - tempDir: string, - openNextConfigPath?: string, -) { - //Check if open-next.config.ts exists - const pathToOpenNextConfig = path.join( +function showWindowsWarning() { + if (os.platform() !== "win32") return; + + logger.warn("OpenNext is not fully compatible with Windows."); + logger.warn( + "For optimal performance, it is recommended to use Windows Subsystem for Linux (WSL).", + ); + logger.warn( + "While OpenNext may function on Windows, it could encounter unpredictable failures during runtime.", + ); +} + +function initTempDir() { + const dir = path.join(process.cwd(), ".open-next"); + const tempDir = path.join(dir, ".build"); + fs.rmSync(dir, { recursive: true, force: true }); + fs.mkdirSync(tempDir, { recursive: true }); + return tempDir; +} + +function compileOpenNextConfig(tempDir: string, openNextConfigPath?: string) { + const sourcePath = path.join( process.cwd(), openNextConfigPath ?? "open-next.config.ts", ); - if (!fs.existsSync(pathToOpenNextConfig)) { + const outputPath = path.join(tempDir, "open-next.config.mjs"); + + //Check if open-next.config.ts exists + if (!fs.existsSync(sourcePath)) { //Create a simple open-next.config.mjs file - logger.warn( - "You don't have an open-next.config.ts file. Using default configuration.", - ); + logger.debug("Cannot find open-next.config.ts. Using default config."); fs.writeFileSync( - path.join(tempDir, "open-next.config.mjs"), - `var config = { - default: { - }, - }; - var open_next_config_default = config; - export { - open_next_config_default as default - }; - `, + outputPath, + [ + "var config = { default: { } };", + "var open_next_config_default = config;", + "export { open_next_config_default as default };", + ].join("\n"), ); } else { buildSync({ - entryPoints: [pathToOpenNextConfig], - outfile: path.join(tempDir, "open-next.config.mjs"), + entryPoints: [sourcePath], + outfile: outputPath, bundle: true, format: "esm", target: ["node18"], }); } + + return outputPath; } function checkRunningInsideNextjsApp() { @@ -176,7 +181,7 @@ function setStandaloneBuildMode(monorepoRoot: string) { function buildNextjsApp(packager: "npm" | "yarn" | "pnpm" | "bun") { const { nextPackageJsonPath } = options; const command = - options.buildCommand ?? + config.buildCommand ?? (["bun", "npm"].includes(packager) ? `${packager} run build` : `${packager} build`); @@ -430,10 +435,9 @@ function createStaticAssets() { } } -async function createCacheAssets( - monorepoRoot: string, - disableDynamoDBCache = false, -) { +async function createCacheAssets(monorepoRoot: string) { + if (config.dangerous?.disableIncrementalCache) return; + logger.info(`Bundling cache assets...`); const { appBuildOutputPath, outputDir } = options; @@ -527,7 +531,7 @@ async function createCacheAssets( fs.writeFileSync(cacheFilePath, JSON.stringify(cacheFileContent)); }); - if (!disableDynamoDBCache) { + if (!config.dangerous?.disableTagCache) { // Generate dynamodb data // We need to traverse the cache to find every .meta file const metaFiles: { @@ -635,9 +639,8 @@ async function createCacheAssets( /* Server Helper Functions */ /***************************/ -function compileCache(options: BuildOptions) { +function compileCache() { const outfile = path.join(options.outputDir, ".build", "cache.cjs"); - const dangerousOptions = options.dangerous; esbuildSync( { external: ["next", "styled-jsx", "react", "@aws-sdk/*"], @@ -648,10 +651,10 @@ function compileCache(options: BuildOptions) { banner: { js: [ `globalThis.disableIncrementalCache = ${ - dangerousOptions?.disableIncrementalCache ?? false + config.dangerous?.disableIncrementalCache ?? false };`, `globalThis.disableDynamoDBCache = ${ - dangerousOptions?.disableTagCache ?? false + config.dangerous?.disableTagCache ?? false };`, ].join(""), }, @@ -661,10 +664,10 @@ function compileCache(options: BuildOptions) { return outfile; } -async function createMiddleware(config: OpenNextConfig) { +async function createMiddleware() { console.info(`Bundling middleware function...`); - const { appBuildOutputPath, outputDir, externalMiddleware } = options; + const { appBuildOutputPath, outputDir } = options; // Get middleware manifest const middlewareManifest = JSON.parse( @@ -688,7 +691,7 @@ async function createMiddleware(config: OpenNextConfig) { appBuildOutputPath, }; - if (externalMiddleware) { + if (config.middleware?.external) { outputPath = path.join(outputDir, "middleware"); fs.mkdirSync(outputPath, { recursive: true }); diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts index fe74196b9..9cb8cc238 100644 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ b/packages/open-next/src/build/copyTracedFiles.ts @@ -11,6 +11,7 @@ import { } from "fs"; import path from "path"; import { NextConfig, PrerenderManifest } from "types/next-types"; +import logger from "../logger"; export async function copyTracedFiles( buildOutputPath: string, @@ -19,7 +20,7 @@ export async function copyTracedFiles( routes: string[], bundledNextServer: boolean, ) { - console.time("copyTracedFiles"); + const tsStart = Date.now(); const dotNextDir = path.join(buildOutputPath, ".next"); const standaloneDir = path.join(dotNextDir, "standalone"); const standaloneNextDir = path.join(standaloneDir, packagePath, ".next"); @@ -232,5 +233,5 @@ export async function copyTracedFiles( }); } - console.timeEnd("copyTracedFiles"); + logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 07f1926d7..5f84d935c 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -29,21 +29,21 @@ const require = topLevelCreateRequire(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); export async function createServerBundle( - options: OpenNextConfig, - buildRuntimeOptions: BuildOptions, + config: OpenNextConfig, + options: BuildOptions, ) { const foundRoutes = new Set(); // Get all functions to build - const defaultFn = options.default; - const functions = Object.entries(options.functions ?? {}); + const defaultFn = config.default; + const functions = Object.entries(config.functions ?? {}); const promises = functions.map(async ([name, fnOptions]) => { const routes = fnOptions.routes; routes.forEach((route) => foundRoutes.add(route)); if (fnOptions.runtime === "edge") { - await generateEdgeBundle(name, buildRuntimeOptions, fnOptions); + await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, buildRuntimeOptions, fnOptions); + await generateBundle(name, config, options, fnOptions); } }); @@ -54,13 +54,13 @@ export async function createServerBundle( const remainingRoutes = new Set(); - const { monorepoRoot, appBuildOutputPath } = buildRuntimeOptions; + const { monorepoRoot, appBuildOutputPath } = options; const packagePath = path.relative(monorepoRoot, appBuildOutputPath); // Find remaining routes const serverPath = path.join( - buildRuntimeOptions.appBuildOutputPath, + appBuildOutputPath, ".next", "standalone", packagePath, @@ -105,7 +105,7 @@ export async function createServerBundle( } // Generate default function - await generateBundle("default", buildRuntimeOptions, { + await generateBundle("default", config, options, { ...defaultFn, routes: Array.from(remainingRoutes), patterns: ["*"], @@ -114,6 +114,7 @@ export async function createServerBundle( async function generateBundle( name: string, + config: OpenNextConfig, options: BuildOptions, fnOptions: SplittedFunctionOptions, ) { @@ -149,7 +150,7 @@ async function generateBundle( // // Copy middleware if ( - !options.externalMiddleware && + !config.middleware?.external && existsSync(path.join(outputDir, ".build", "middleware.mjs")) ) { fs.copyFileSync( @@ -187,7 +188,7 @@ async function generateBundle( const isBefore13413 = compareSemver(options.nextVersion, "13.4.13") <= 0; const isAfter141 = compareSemver(options.nextVersion, "14.0.4") >= 0; - const disableRouting = isBefore13413 || options.externalMiddleware; + const disableRouting = isBefore13413 || config.middleware?.external; const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, diff --git a/packages/open-next/src/build/generateOutput.ts b/packages/open-next/src/build/generateOutput.ts index a1e25cf73..2e7ea5a03 100644 --- a/packages/open-next/src/build/generateOutput.ts +++ b/packages/open-next/src/build/generateOutput.ts @@ -153,23 +153,23 @@ async function extractCommonOverride(override?: OverrideOptions) { export async function generateOutput( outputPath: string, - buildOptions: OpenNextConfig, + config: OpenNextConfig, ) { const edgeFunctions: OpenNextOutput["edgeFunctions"] = {}; - const isExternalMiddleware = buildOptions.middleware?.external ?? false; + const isExternalMiddleware = config.middleware?.external ?? false; if (isExternalMiddleware) { edgeFunctions.middleware = { bundle: ".open-next/middleware", handler: "handler.handler", pathResolver: await extractOverrideName( "pattern-env", - buildOptions.middleware!.originResolver, + config.middleware!.originResolver, ), - ...(await extractOverrideFn(buildOptions.middleware?.override)), + ...(await extractOverrideFn(config.middleware?.override)), }; } // Add edge functions - Object.entries(buildOptions.functions ?? {}).forEach(async ([key, value]) => { + Object.entries(config.functions ?? {}).forEach(async ([key, value]) => { if (value.placement === "global") { edgeFunctions[key] = { bundle: `.open-next/functions/${key}`, @@ -179,7 +179,7 @@ export async function generateOutput( } }); - const defaultOriginCanstream = await canStream(buildOptions.default); + const defaultOriginCanstream = await canStream(config.default); // First add s3 origins and image optimization @@ -194,7 +194,7 @@ export async function generateOutput( cached: true, versionedSubDir: "_next", }, - ...(buildOptions.dangerous?.disableIncrementalCache + ...(config.dangerous?.disableIncrementalCache ? [] : [ { @@ -212,25 +212,25 @@ export async function generateOutput( streaming: false, imageLoader: await extractOverrideName( "s3", - buildOptions.imageOptimization?.loader, + config.imageOptimization?.loader, ), - ...(await extractOverrideFn(buildOptions.imageOptimization?.override)), + ...(await extractOverrideFn(config.imageOptimization?.override)), }, - default: buildOptions.default.override?.generateDockerfile + default: config.default.override?.generateDockerfile ? { type: "ecs", bundle: ".open-next/server-functions/default", dockerfile: ".open-next/server-functions/default/Dockerfile", - ...(await extractOverrideFn(buildOptions.default.override)), - ...(await extractCommonOverride(buildOptions.default.override)), + ...(await extractOverrideFn(config.default.override)), + ...(await extractCommonOverride(config.default.override)), } : { type: "function", handler: "index.handler", bundle: ".open-next/server-functions/default", streaming: defaultOriginCanstream, - ...(await extractOverrideFn(buildOptions.default.override)), - ...(await extractCommonOverride(buildOptions.default.override)), + ...(await extractOverrideFn(config.default.override)), + ...(await extractCommonOverride(config.default.override)), }, }; @@ -239,7 +239,7 @@ export async function generateOutput( // Then add function origins await Promise.all( - Object.entries(buildOptions.functions ?? {}).map(async ([key, value]) => { + Object.entries(config.functions ?? {}).map(async ([key, value]) => { if (!value.placement || value.placement === "regional") { if (value.override?.generateDockerfile) { origins[key] = { @@ -270,7 +270,7 @@ export async function generateOutput( ]; // Then we add the routes - Object.entries(buildOptions.functions ?? {}).forEach(([key, value]) => { + Object.entries(config.functions ?? {}).forEach(([key, value]) => { const patterns = "patterns" in value ? value.patterns : ["*"]; patterns.forEach((pattern) => { behaviors.push({ @@ -319,19 +319,19 @@ export async function generateOutput( origins, behaviors, additionalProps: { - disableIncrementalCache: buildOptions.dangerous?.disableIncrementalCache, - disableTagCache: buildOptions.dangerous?.disableTagCache, + disableIncrementalCache: config.dangerous?.disableIncrementalCache, + disableTagCache: config.dangerous?.disableTagCache, warmer: { handler: "index.handler", bundle: ".open-next/warmer-function", }, - initializationFunction: buildOptions.dangerous?.disableTagCache + initializationFunction: config.dangerous?.disableTagCache ? undefined : { handler: "index.handler", bundle: ".open-next/initialization-function", }, - revalidationFunction: buildOptions.dangerous?.disableIncrementalCache + revalidationFunction: config.dangerous?.disableIncrementalCache ? undefined : { handler: "index.handler", diff --git a/packages/open-next/src/build/helper.ts b/packages/open-next/src/build/helper.ts index 2ece8544e..43d1584ea 100644 --- a/packages/open-next/src/build/helper.ts +++ b/packages/open-next/src/build/helper.ts @@ -17,14 +17,17 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); export type BuildOptions = ReturnType; -export function normalizeOptions(opts: OpenNextConfig, root: string) { - const appPath = path.join(process.cwd(), opts.appPath || "."); - const buildOutputPath = path.join(process.cwd(), opts.buildOutputPath || "."); +export function normalizeOptions(config: OpenNextConfig, root: string) { + const appPath = path.join(process.cwd(), config.appPath || "."); + const buildOutputPath = path.join( + process.cwd(), + config.buildOutputPath || ".", + ); const outputDir = path.join(buildOutputPath, ".open-next"); let nextPackageJsonPath: string; - if (opts.packageJsonPath) { - const _pkgPath = path.join(process.cwd(), opts.packageJsonPath); + if (config.packageJsonPath) { + const _pkgPath = path.join(process.cwd(), config.packageJsonPath); nextPackageJsonPath = _pkgPath.endsWith("package.json") ? _pkgPath : path.join(_pkgPath, "./package.json"); @@ -41,9 +44,6 @@ export function normalizeOptions(opts: OpenNextConfig, root: string) { outputDir, tempDir: path.join(outputDir, ".build"), debug: Boolean(process.env.OPEN_NEXT_DEBUG) ?? false, - buildCommand: opts.buildCommand, - dangerous: opts.dangerous, - externalMiddleware: opts.middleware?.external ?? false, monorepoRoot: root, }; }