Skip to content

refactor: use the traced file copy from OpenNext #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 27, 2025
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
7 changes: 7 additions & 0 deletions .changeset/quick-timers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@opennextjs/cloudflare": patch
---

fix: @vercel/og failing due to using the node version.

Patches usage of the @vercel/og library to require the edge runtime version, and enables importing of the fallback font.
1 change: 0 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main, experimental]
pull_request:
branches: [main, experimental]

jobs:
checks:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/prereleases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main, experimental]
pull_request:
branches: [main, experimental]

jobs:
release:
Expand Down
65 changes: 65 additions & 0 deletions examples/api/app/og/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ImageResponse } from "next/og";

export const dynamic = "force-dynamic";

export async function GET() {
try {
return new ImageResponse(
(
<div
style={{
backgroundColor: "black",
backgroundSize: "150px 150px",
height: "100%",
width: "100%",
display: "flex",
textAlign: "center",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
flexWrap: "nowrap",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
justifyItems: "center",
}}
>
<img
alt="Vercel"
height={200}
src="data:image/svg+xml,%3Csvg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M57.5 0L115 100H0L57.5 0z' /%3E%3C/svg%3E"
style={{ margin: "0 30px" }}
width={232}
/>
</div>
<div
style={{
fontSize: 60,
fontStyle: "normal",
letterSpacing: "-0.025em",
color: "white",
marginTop: 30,
padding: "0 120px",
lineHeight: 1.4,
whiteSpace: "pre-wrap",
}}
>
'next/og'
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
return new Response("Failed to generate the image", {
status: 500,
});
}
}
19 changes: 19 additions & 0 deletions examples/api/e2e/base.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { test, expect } from "@playwright/test";
import type { BinaryLike } from "node:crypto";
import { createHash } from "node:crypto";

const OG_MD5 = "2f7b724d62d8c7739076da211aa62e7b";

export function validateMd5(data: Buffer, expectedHash: string) {
return (
createHash("md5")
.update(data as BinaryLike)
.digest("hex") === expectedHash
);
}

test("the application's noop index page is visible and it allows navigating to the hello-world api route", async ({
page,
Expand Down Expand Up @@ -42,3 +54,10 @@ test("returns correct information about the request from a route handler", async
const expectedURL = expect.stringMatching(/https?:\/\/localhost:(?!3000)\d+\/api\/request/);
await expect(res.json()).resolves.toEqual({ nextUrl: expectedURL, url: expectedURL });
});

test("generates an og image successfully", async ({ page }) => {
const res = await page.request.get("/og");
expect(res.status()).toEqual(200);
expect(res.headers()["content-type"]).toEqual("image/png");
expect(validateMd5(await res.body(), OG_MD5)).toEqual(true);
});
2 changes: 1 addition & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"dependencies": {
"@ast-grep/napi": "^0.34.1",
"@dotenvx/dotenvx": "catalog:",
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@704",
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@712",
"enquirer": "^2.4.1",
"glob": "catalog:",
"ts-morph": "catalog:",
Expand Down
19 changes: 3 additions & 16 deletions packages/cloudflare/src/cli/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { cpSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { dirname } from "node:path";

import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
Expand All @@ -11,8 +10,7 @@ import * as buildHelper from "@opennextjs/aws/build/helper.js";
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
import logger from "@opennextjs/aws/logger.js";

import type { ProjectOptions } from "../config.js";
import { containsDotNextDir, getConfig } from "../config.js";
import type { ProjectOptions } from "../project-options.js";
import { bundleServer } from "./bundle-server.js";
import { compileEnvFiles } from "./open-next/compile-env-files.js";
import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
Expand Down Expand Up @@ -69,10 +67,6 @@ export async function build(projectOpts: ProjectOptions): Promise<void> {
buildNextjsApp(options);
}

if (!containsDotNextDir(projectOpts.sourceDir)) {
throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
}

// Generate deployable bundle
printHeader("Generating bundle");
buildHelper.initOutputDir(options);
Expand All @@ -95,14 +89,7 @@ export async function build(projectOpts: ProjectOptions): Promise<void> {

await createServerBundle(options);

// TODO: drop this copy.
// Copy the .next directory to the output directory so it can be mutated.
cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true });

const projConfig = getConfig(projectOpts);

// TODO: rely on options only.
await bundleServer(projConfig, options);
await bundleServer(options);

if (!projectOpts.skipWranglerConfigCheck) {
await createWranglerConfigIfNotExistent(projectOpts);
Expand Down
85 changes: 41 additions & 44 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import path from "node:path";
import { fileURLToPath } from "node:url";

import { Lang, parse } from "@ast-grep/napi";
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
import { build, Plugin } from "esbuild";

import { Config } from "../config.js";
import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
import * as patches from "./patches/index.js";
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
Expand All @@ -19,22 +18,26 @@ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "
/**
* Bundle the Open Next server.
*/
export async function bundleServer(config: Config, openNextOptions: BuildOptions): Promise<void> {
patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);

const nextConfigStr =
fs
.readFileSync(path.join(config.paths.output.standaloneApp, "server.js"), "utf8")
?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};
export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
patches.copyPackageCliFiles(packageDistDir, buildOpts);

const { appPath, outputDir, monorepoRoot } = buildOpts;
const serverFiles = path.join(
outputDir,
"server-functions/default",
getPackagePath(buildOpts),
".next/required-server-files.json"
);
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;

console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);

patches.patchWranglerDeps(config);
patches.updateWebpackChunksFile(config);
patches.patchWranglerDeps(buildOpts);
await patches.updateWebpackChunksFile(buildOpts);
patches.patchVercelOgLibrary(buildOpts);

const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
const outputPath = path.join(outputDir, "server-functions", "default");
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
const packagePath = getPackagePath(buildOpts);
const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);

Expand All @@ -45,25 +48,28 @@ export async function bundleServer(config: Config, openNextOptions: BuildOptions
format: "esm",
target: "esnext",
minify: false,
plugins: [createFixRequiresESBuildPlugin(config)],
plugins: [createFixRequiresESBuildPlugin(buildOpts)],
external: ["./middleware/handler.mjs", "caniuse-lite"],
alias: {
// 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.internal.templates, "shims", "empty.js"),
"next/dist/compiled/ws": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
// 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.internal.templates, "shims", "empty.js"),
"next/dist/compiled/edge-runtime": path.join(
buildOpts.outputDir,
"cloudflare-templates/shims/empty.js"
),
// `@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.internal.templates, "shims", "env.js"),
"@next/env": path.join(buildOpts.outputDir, "cloudflare-templates/shims/env.js"),
},
define: {
// config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
"process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(nextConfigStr),
"process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": `${JSON.stringify(nextConfig)}`,
// Next.js tried to access __dirname so we need to define it
__dirname: '""',
// Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
Expand Down Expand Up @@ -117,7 +123,7 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
},
});

await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);
await updateWorkerBundledCode(openNextServerBundle, buildOpts);

const isMonorepo = monorepoRoot !== appPath;
if (isMonorepo) {
Expand All @@ -127,35 +133,26 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
);
}

console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(openNextOptions)}\` 🚀\n\x1b[0m`);
console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
}

/**
* This function applies string replacements on the bundled worker code necessary to get it to run in workerd
*
* Needless to say all the logic in this function is something we should avoid as much as possible!
*
* @param workerOutputFile
* @param config
* This function applies patches required for the code to run on workers.
*/
async function updateWorkerBundledCode(
workerOutputFile: string,
config: Config,
openNextOptions: BuildOptions
): Promise<void> {
async function updateWorkerBundledCode(workerOutputFile: string, buildOpts: BuildOptions): Promise<void> {
const code = await readFile(workerOutputFile, "utf8");

const patchedCode = await patchCodeWithValidations(code, [
["require", patches.patchRequire],
["`buildId` function", (code) => patches.patchBuildId(code, config)],
["`loadManifest` function", (code) => patches.patchLoadManifest(code, config)],
["next's require", (code) => patches.inlineNextRequire(code, config)],
["`findDir` function", (code) => patches.patchFindDir(code, config)],
["`evalManifest` function", (code) => patches.inlineEvalManifest(code, config)],
["cacheHandler", (code) => patches.patchCache(code, openNextOptions)],
["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
[
"'require(this.middlewareManifestPath)'",
(code) => patches.inlineMiddlewareManifestRequire(code, config),
(code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
],
["exception bubbling", patches.patchExceptionBubbling],
["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
Expand All @@ -180,20 +177,20 @@ async function updateWorkerBundledCode(

const bundle = parse(Lang.TypeScript, patchedCode).root();

const edits = patchOptionalDependencies(bundle);
const { edits } = patchOptionalDependencies(bundle);

await writeFile(workerOutputFile, bundle.commitEdits(edits));
}

function createFixRequiresESBuildPlugin(config: Config): Plugin {
function createFixRequiresESBuildPlugin(options: BuildOptions): Plugin {
return {
name: "replaceRelative",
setup(build) {
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
build.onResolve(
{ filter: getCrossPlatformPathRegex(String.raw`^\./require-hook$`, { escape: false }) },
() => ({
path: path.join(config.paths.internal.templates, "shims", "empty.js"),
path: path.join(options.outputDir, "cloudflare-templates/shims/empty.js"),
})
);
},
Expand All @@ -203,9 +200,9 @@ function createFixRequiresESBuildPlugin(config: Config): Plugin {
/**
* Gets the path of the worker.js file generated by the build process
*
* @param openNextOptions the open-next build options
* @param buildOpts the open-next build options
* @returns the path of the worker.js file that the build process generates
*/
export function getOutputWorkerPath(openNextOptions: BuildOptions): string {
return path.join(openNextOptions.outputDir, "worker.js");
export function getOutputWorkerPath(buildOpts: BuildOptions): string {
return path.join(buildOpts.outputDir, "worker.js");
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { extractProjectEnvVars } from "../utils/index.js";
/**
* Compiles the values extracted from the project's env files to the output directory for use in the worker.
*/
export function compileEnvFiles(options: BuildOptions) {
export function compileEnvFiles(buildOpts: BuildOptions) {
["production", "development", "test"].forEach((mode) =>
fs.appendFileSync(
path.join(options.outputDir, `.env.mjs`),
`export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, options))};\n`
path.join(buildOpts.outputDir, `.env.mjs`),
`export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, buildOpts))};\n`
)
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type SgNode } from "@ast-grep/napi";

import { getRuleEdits } from "./util.js";
import { applyRule } from "./util.js";

/**
* Handle optional dependencies.
Expand Down Expand Up @@ -31,5 +31,5 @@ fix: |-
`;

export function patchOptionalDependencies(root: SgNode) {
return getRuleEdits(optionalDepRule, root);
return applyRule(optionalDepRule, root);
}
Loading
Loading