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
34 changes: 15 additions & 19 deletions packages/cloudflare/src/cli/build/build-worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Plugin, build } from "esbuild";
import { cp, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { existsSync, readFileSync } from "node:fs";
import { Config } from "../config";
import { copyPackageCliFiles } from "./patches/investigated/copy-package-cli-files";
Expand All @@ -14,11 +15,10 @@ import { patchFindDir } from "./patches/to-investigate/patch-find-dir";
import { patchReadFile } from "./patches/to-investigate/patch-read-file";
import { patchRequire } from "./patches/investigated/patch-require";
import { patchWranglerDeps } from "./patches/to-investigate/wrangler-deps";
import path from "node:path";
import { updateWebpackChunksFile } from "./patches/investigated/update-webpack-chunks-file";

/** The dist directory of the Cloudflare adapter package */
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const packageDistDir = join(dirname(fileURLToPath(import.meta.url)), "..");

/**
* Using the Next.js build output in the `.next` directory builds a workerd compatible output
Expand All @@ -30,18 +30,14 @@ export async function buildWorker(config: Config): Promise<void> {
console.log(`\x1b[35m⚙️ Copying files...\n\x1b[0m`);

// Copy over client-side generated files
await cp(
path.join(config.paths.dotNext, "static"),
path.join(config.paths.outputDir, "assets", "_next", "static"),
{
recursive: true,
}
);
await cp(join(config.paths.dotNext, "static"), join(config.paths.outputDir, "assets", "_next", "static"), {
recursive: true,
});

// Copy over any static files (e.g. images) from the source project
const publicDir = path.join(config.paths.sourceDir, "public");
const publicDir = join(config.paths.sourceDir, "public");
if (existsSync(publicDir)) {
await cp(publicDir, path.join(config.paths.outputDir, "assets"), {
await cp(publicDir, join(config.paths.outputDir, "assets"), {
recursive: true,
});
}
Expand All @@ -51,11 +47,11 @@ export async function buildWorker(config: Config): Promise<void> {

copyPackageCliFiles(packageDistDir, config);

const workerEntrypoint = path.join(config.paths.internalTemplates, "worker.ts");
const workerOutputFile = path.join(config.paths.outputDir, "index.mjs");
const workerEntrypoint = join(config.paths.internalTemplates, "worker.ts");
const workerOutputFile = join(config.paths.outputDir, "index.mjs");

const nextConfigStr =
readFileSync(path.join(config.paths.standaloneApp, "/server.js"), "utf8")?.match(
readFileSync(join(config.paths.standaloneApp, "/server.js"), "utf8")?.match(
/const nextConfig = ({.+?})\n/
)?.[1] ?? {};

Expand All @@ -76,15 +72,15 @@ export async function buildWorker(config: Config): Promise<void> {
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
// eval("require")("bufferutil");
// eval("require")("utf-8-validate");
"next/dist/compiled/ws": path.join(config.paths.internalTemplates, "shims", "empty.ts"),
"next/dist/compiled/ws": join(config.paths.internalTemplates, "shims", "empty.ts"),
// Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
// eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
// which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
// QUESTION: Why did I encountered this but mhart didn't?
"next/dist/compiled/edge-runtime": path.join(config.paths.internalTemplates, "shims", "empty.ts"),
"next/dist/compiled/edge-runtime": join(config.paths.internalTemplates, "shims", "empty.ts"),
// `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
// source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
"@next/env": path.join(config.paths.internalTemplates, "shims", "env.ts"),
"@next/env": join(config.paths.internalTemplates, "shims", "env.ts"),
},
define: {
// config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
Expand Down Expand Up @@ -178,10 +174,10 @@ function createFixRequiresESBuildPlugin(config: Config): Plugin {
setup(build) {
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({
path: path.join(config.paths.internalTemplates, "shims", "empty.ts"),
path: join(config.paths.internalTemplates, "shims", "empty.ts"),
}));
build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({
path: path.join(config.paths.internalTemplates, "shims", "empty.ts"),
path: join(config.paths.internalTemplates, "shims", "empty.ts"),
}));
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Config } from "../../../config";
import { cpSync } from "node:fs";
import path from "node:path";
import { join } from "node:path";

/**
* Copies the template files present in the cloudflare adapter package into the standalone node_modules folder
*/
export function copyPackageCliFiles(packageDistDir: string, config: Config) {
console.log("# copyPackageTemplateFiles");
const sourceDir = path.join(packageDistDir, "cli");
const destinationDir = path.join(config.paths.internalPackage, "cli");
const sourceDir = join(packageDistDir, "cli");
const destinationDir = join(config.paths.internalPackage, "cli");

cpSync(sourceDir, destinationDir, { recursive: true });
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { Config } from "../../../../config";
import { getUpdatedWebpackChunksFileContent } from "./get-updated-webpack-chunks-file-content";
import path from "node:path";
import { join } from "node:path";

/**
* Fixes the webpack-runtime.js file by removing its webpack dynamic requires.
Expand All @@ -11,11 +11,11 @@ import path from "node:path";
*/
export async function updateWebpackChunksFile(config: Config) {
console.log("# updateWebpackChunksFile");
const webpackRuntimeFile = path.join(config.paths.standaloneAppServer, "webpack-runtime.js");
const webpackRuntimeFile = join(config.paths.standaloneAppServer, "webpack-runtime.js");

const fileContent = readFileSync(webpackRuntimeFile, "utf-8");

const chunks = readdirSync(path.join(config.paths.standaloneAppServer, "chunks"))
const chunks = readdirSync(join(config.paths.standaloneAppServer, "chunks"))
.filter((chunk) => /^\d+\.js$/.test(chunk))
.map((chunk) => {
console.log(` - chunk ${chunk}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join, posix } from "node:path";
import { Config } from "../../../config";
import { globSync } from "glob";
import path from "node:path";
import { normalizePath } from "../../utils";

/**
* `evalManifest` relies on readFileSync so we need to patch the function so that it instead returns the content of the manifest files
Expand All @@ -12,22 +13,16 @@ import path from "node:path";
export function inlineEvalManifest(code: string, config: Config): string {
console.log("# inlineEvalManifest");
const manifestJss = globSync(
path
.join(config.paths.standaloneAppDotNext, "**", "*_client-reference-manifest.js")
.replaceAll(path.sep, path.posix.sep)
).map((file) =>
file
.replaceAll(path.sep, path.posix.sep)
.replace(config.paths.standaloneApp.replaceAll(path.sep, path.posix.sep) + path.posix.sep, "")
);
normalizePath(join(config.paths.standaloneAppDotNext, "**", "*_client-reference-manifest.js"))
).map((file) => normalizePath(file).replace(normalizePath(config.paths.standaloneApp) + posix.sep, ""));
return code.replace(
/function evalManifest\((.+?), .+?\) {/,
`$&
${manifestJss
.map(
(manifestJs) => `
if ($1.endsWith("${manifestJs}")) {
require(${JSON.stringify(path.join(config.paths.standaloneApp, manifestJs))});
require(${JSON.stringify(join(config.paths.standaloneApp, manifestJs))});
return {
__RSC_MANIFEST: {
"${manifestJs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import { Config } from "../../../config";
import path from "node:path";
import { join } from "node:path";

/**
* Inlines the middleware manifest from the build output to prevent a dynamic require statement
Expand All @@ -9,7 +9,7 @@ import path from "node:path";
export function inlineMiddlewareManifestRequire(code: string, config: Config) {
console.log("# inlineMiddlewareManifestRequire");

const middlewareManifestPath = path.join(config.paths.standaloneAppServer, "middleware-manifest.json");
const middlewareManifestPath = join(config.paths.standaloneAppServer, "middleware-manifest.json");

const middlewareManifest = existsSync(middlewareManifestPath)
? JSON.parse(readFileSync(middlewareManifestPath, "utf-8"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { existsSync, readFileSync } from "node:fs";
import { Config } from "../../../config";
import path from "node:path";
import { join } from "node:path";

/**
* The following avoid various Next.js specific files `require`d at runtime since we can just read
* and inline their content during build time
*/
export function inlineNextRequire(code: string, config: Config) {
console.log("# inlineNextRequire");
const pagesManifestFile = path.join(config.paths.standaloneAppServer, "pages-manifest.json");
const appPathsManifestFile = path.join(config.paths.standaloneAppServer, "app-paths-manifest.json");
const pagesManifestFile = join(config.paths.standaloneAppServer, "pages-manifest.json");
const appPathsManifestFile = join(config.paths.standaloneAppServer, "app-paths-manifest.json");

const pagesManifestFiles = existsSync(pagesManifestFile)
? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8"))).map(
Expand All @@ -33,7 +33,7 @@ export function inlineNextRequire(code: string, config: Config) {
.map(
(htmlPage) => `
if (pagePath.endsWith("${htmlPage}")) {
return ${JSON.stringify(readFileSync(path.join(config.paths.standaloneApp, htmlPage), "utf-8"))};
return ${JSON.stringify(readFileSync(join(config.paths.standaloneApp, htmlPage), "utf-8"))};
}
`
)
Expand All @@ -42,7 +42,7 @@ export function inlineNextRequire(code: string, config: Config) {
.map(
(module) => `
if (pagePath.endsWith("${module}")) {
return require(${JSON.stringify(path.join(config.paths.standaloneApp, module))});
return require(${JSON.stringify(join(config.paths.standaloneApp, module))});
}
`
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Config } from "../../../config";
import { existsSync } from "node:fs";
import path from "node:path";
import { join } from "node:path";

/**
* Here we patch `findDir` so that the next server can detect whether the `app` or `pages` directory exists
Expand All @@ -15,10 +15,10 @@ export function patchFindDir(code: string, config: Config): string {
`function findDir(dir, name) {
if (dir.endsWith(".next/server")) {
if (name === "app") {
return ${existsSync(`${path.join(config.paths.standaloneAppServer, "app")}`)};
return ${existsSync(`${join(config.paths.standaloneAppServer, "app")}`)};
}
if (name === "pages") {
return ${existsSync(`${path.join(config.paths.standaloneAppServer, "pages")}`)};
return ${existsSync(`${join(config.paths.standaloneAppServer, "pages")}`)};
}
}
throw new Error("Unknown findDir call: " + dir + " " + name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join, posix } from "node:path";
import { Config } from "../../../config";
import { globSync } from "glob";
import path from "node:path";
import { normalizePath } from "../../utils";
import { readFileSync } from "node:fs";

export function patchReadFile(code: string, config: Config): string {
Expand All @@ -12,28 +13,24 @@ export function patchReadFile(code: string, config: Config): string {
code = code.replace(
"getBuildId() {",
`getBuildId() {
return ${JSON.stringify(readFileSync(path.join(config.paths.standaloneAppDotNext, "BUILD_ID"), "utf-8"))};
return ${JSON.stringify(readFileSync(join(config.paths.standaloneAppDotNext, "BUILD_ID"), "utf-8"))};
`
);

// Same as above, the next-server code loads the manifests with `readFileSync` and we want to avoid that
// (source: https://github.com/vercel/next.js/blob/15aeb92e/packages/next/src/server/load-manifest.ts#L34-L56)
// Note: we could/should probably just patch readFileSync here or something!
const manifestJsons = globSync(
path.join(config.paths.standaloneAppDotNext, "**", "*-manifest.json").replaceAll(path.sep, path.posix.sep)
).map((file) =>
file
.replaceAll(path.sep, path.posix.sep)
.replace(config.paths.standaloneApp.replaceAll(path.sep, path.posix.sep) + path.posix.sep, "")
);
normalizePath(join(config.paths.standaloneAppDotNext, "**", "*-manifest.json"))
).map((file) => normalizePath(file).replace(normalizePath(config.paths.standaloneApp) + posix.sep, ""));
code = code.replace(
/function loadManifest\((.+?), .+?\) {/,
`$&
${manifestJsons
.map(
(manifestJson) => `
if ($1.endsWith("${manifestJson}")) {
return ${readFileSync(path.join(config.paths.standaloneApp, manifestJson), "utf-8")};
return ${readFileSync(join(config.paths.standaloneApp, manifestJson), "utf-8")};
}
`
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync, statSync, writeFileSync } from "node:fs";
import { Config } from "../../../config";
import path from "node:path";
import { join } from "node:path";

export function patchWranglerDeps(config: Config) {
console.log("# patchWranglerDeps");
Expand All @@ -13,7 +13,7 @@ export function patchWranglerDeps(config: Config) {
// [alias]
// # critters is `require`d from `pages.runtime.prod.js` when running wrangler dev, so we need to stub it out
// "critters" = "./.next/standalone/node_modules/cf/templates/shims/empty.ts"
const pagesRuntimeFile = path.join(distPath, "compiled", "next-server", "pages.runtime.prod.js");
const pagesRuntimeFile = join(distPath, "compiled", "next-server", "pages.runtime.prod.js");

const patchedPagesRuntime = readFileSync(pagesRuntimeFile, "utf-8").replace(
`e.exports=require("critters")`,
Expand All @@ -32,7 +32,7 @@ export function patchWranglerDeps(config: Config) {
// # try block here: https://github.com/vercel/next.js/blob/9e8266a7/packages/next/src/server/lib/trace/tracer.ts#L27-L31
// # causing the code to require the 'next/dist/compiled/@opentelemetry/api' module instead (which properly works)
// #"@opentelemetry/api" = "./.next/standalone/node_modules/cf/templates/shims/throw.ts"
const tracerFile = path.join(distPath, "server", "lib", "trace", "tracer.js");
const tracerFile = join(distPath, "server", "lib", "trace", "tracer.js");

const patchedTracer = readFileSync(tracerFile, "utf-8").replaceAll(
/\w+\s*=\s*require\([^/]*opentelemetry.*\)/g,
Expand All @@ -57,7 +57,7 @@ export function patchWranglerDeps(config: Config) {
function getDistPath(config: Config): string {
for (const root of [config.paths.standaloneApp, config.paths.standaloneRoot]) {
try {
const distPath = path.join(root, "node_modules", "next", "dist");
const distPath = join(root, "node_modules", "next", "dist");
if (statSync(distPath).isDirectory()) return distPath;
} catch {
/* empty */
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/src/cli/build/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./ts-parse-file";
export * from "./copy-prerendered-routes";
export * from "./normalize-path";
5 changes: 5 additions & 0 deletions packages/cloudflare/src/cli/build/utils/normalize-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { posix, sep } from "node:path";

export function normalizePath(path: string) {
return path.replaceAll(sep, posix.sep);
}
28 changes: 14 additions & 14 deletions packages/cloudflare/src/cli/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import path, { relative } from "node:path";
import { join, relative } from "node:path";
import { readdirSync, statSync } from "node:fs";

const PACKAGE_NAME = "@opennextjs/cloudflare";
Expand Down Expand Up @@ -49,16 +49,16 @@ export type Config = {
* @returns The configuration, see `Config`
*/
export function getConfig(projectOpts: ProjectOptions): Config {
const dotNext = path.join(projectOpts.outputDir, ".next");
const dotNext = join(projectOpts.outputDir, ".next");
const appPath = getNextjsApplicationPath(dotNext).replace(/\/$/, "");
const standaloneRoot = path.join(dotNext, "standalone");
const standaloneApp = path.join(standaloneRoot, appPath);
const standaloneAppDotNext = path.join(standaloneApp, ".next");
const standaloneAppServer = path.join(standaloneAppDotNext, "server");
const standaloneRoot = join(dotNext, "standalone");
const standaloneApp = join(standaloneRoot, appPath);
const standaloneAppDotNext = join(standaloneApp, ".next");
const standaloneAppServer = join(standaloneAppDotNext, "server");

const nodeModules = path.join(standaloneApp, "node_modules");
const internalPackage = path.join(nodeModules, ...PACKAGE_NAME.split("/"));
const internalTemplates = path.join(internalPackage, "cli", "templates");
const nodeModules = join(standaloneApp, "node_modules");
const internalPackage = join(nodeModules, ...PACKAGE_NAME.split("/"));
const internalTemplates = join(internalPackage, "cli", "templates");

process.env.__OPENNEXT_KV_BINDING_NAME ??= "NEXT_CACHE_WORKERS_KV";

Expand Down Expand Up @@ -91,7 +91,7 @@ export function getConfig(projectOpts: ProjectOptions): Config {

export function containsDotNextDir(folder: string): boolean {
try {
return statSync(path.join(folder, ".next")).isDirectory();
return statSync(join(folder, ".next")).isDirectory();
} catch {
return false;
}
Expand Down Expand Up @@ -124,12 +124,12 @@ function getNextjsApplicationPath(dotNextDir: string): string {
throw new Error(`Unexpected Error: no \`.next/server\` folder could be found in \`${serverPath}\``);
}

return relative(path.join(dotNextDir, "standalone"), serverPath);
return relative(join(dotNextDir, "standalone"), serverPath);
}

function findServerParentPath(parentPath: string): string | undefined {
try {
if (statSync(path.join(parentPath, ".next", "server")).isDirectory()) {
if (statSync(join(parentPath, ".next", "server")).isDirectory()) {
return parentPath;
}
} catch {
Expand All @@ -139,8 +139,8 @@ function findServerParentPath(parentPath: string): string | undefined {
const folders = readdirSync(parentPath);

for (const folder of folders) {
const subFolder = path.join(parentPath, folder);
if (statSync(path.join(parentPath, folder)).isDirectory()) {
const subFolder = join(parentPath, folder);
if (statSync(join(parentPath, folder)).isDirectory()) {
const dirServerPath = findServerParentPath(subFolder);
if (dirServerPath) {
return dirServerPath;
Expand Down