Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 57 additions & 82 deletions packages/open-next/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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`,
Expand All @@ -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.");
}

Expand All @@ -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}`)),
Expand All @@ -140,48 +134,23 @@ 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)
? `${packager} run build`
: `${packager} build`);
cp.execSync(command, {
stdio: "inherit",
cwd: path.dirname(appPackageJsonPath),
cwd: path.dirname(options.appPackageJsonPath),
});
}

Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`);

Expand Down Expand Up @@ -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(
Expand Down
8 changes: 3 additions & 5 deletions packages/open-next/src/build/createServerBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
// Get all functions to build
const defaultFn = config.default;
Expand All @@ -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]) => {
Expand Down
23 changes: 12 additions & 11 deletions packages/open-next/src/build/generateOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"
Expand All @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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),
);
}
Loading
Loading