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
5 changes: 5 additions & 0 deletions .changeset/famous-deers-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

Fix for `Invariant: renderHTML should not be called in minimal mode`
11 changes: 9 additions & 2 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { patchFetchCacheSetMissingWaitUntil } from "./patches/plugins/fetch-cach
import { inlineFindDir } from "./patches/plugins/find-dir.js";
import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
import { patchNextMinimal } from "./patches/plugins/next-minimal.js";
import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
import { fixRequire } from "./patches/plugins/require.js";
import { shimRequireHook } from "./patches/plugins/require-hook.js";
import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
import { needsExperimentalReact, normalizePath, patchCodeWithValidations } from "./utils/index.js";

/** The dist directory of the Cloudflare adapter package */
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
Expand Down Expand Up @@ -99,6 +100,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
inlineLoadManifest(updater, buildOpts),
inlineBuildId(updater),
patchDepdDeprecations(updater),
patchNextMinimal(updater),
// Apply updater updaters, must be the last plugin
updater.plugin,
],
Expand Down Expand Up @@ -136,7 +138,12 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
// We make sure that environment variables that Next.js expects are properly defined
"process.env.NEXT_RUNTIME": '"nodejs"',
"process.env.NODE_ENV": '"production"',
"process.env.NEXT_MINIMAL": "true",
// The 2 following defines are used to reduce the bundle size by removing unnecessary code
// Next uses different precompiled renderers (i.e. `app-page.runtime.prod.js`) based on if you use `TURBOPACK` or some experimental React features
// Turbopack is not supported for build at the moment, so we disable it
"process.env.TURBOPACK": "false",
// This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
"process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
},
platform: "node",
banner: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from "vitest";

import { patchCode } from "../ast/util";
import { abortControllerRule } from "./next-minimal";

const appPageRuntimeProdJs = `let p = new AbortController;
async function h(e3, t3) {
let { flightRouterState: r3, nextUrl: a2, prefetchKind: i2 } = t3, u2 = { [n2.hY]: "1", [n2.B]: encodeURIComponent(JSON.stringify(r3)) };
i2 === o.ob.AUTO && (u2[n2._V] = "1"), a2 && (u2[n2.kO] = a2);
try {
var c2;
let t4 = i2 ? i2 === o.ob.TEMPORARY ? "high" : "low" : "auto";
"export" === process.env.__NEXT_CONFIG_OUTPUT && ((e3 = new URL(e3)).pathname.endsWith("/") ? e3.pathname += "index.txt" : e3.pathname += ".txt");
let r4 = await m(e3, u2, t4, p.signal), a3 = d(r4.url), h2 = r4.redirected ? a3 : void 0, g = r4.headers.get("content-type") || "", v = !!(null == (c2 = r4.headers.get("vary")) ? void 0 : c2.includes(n2.kO)), b = !!r4.headers.get(n2.jc), S = r4.headers.get(n2.UK), _ = null !== S ? parseInt(S, 10) : -1, w = g.startsWith(n2.al);
if ("export" !== process.env.__NEXT_CONFIG_OUTPUT || w || (w = g.startsWith("text/plain")), !w || !r4.ok || !r4.body)
return e3.hash && (a3.hash = e3.hash), f(a3.toString());
let k = b ? function(e4) {
let t5 = e4.getReader();
return new ReadableStream({ async pull(e5) {
for (; ; ) {
let { done: r5, value: n3 } = await t5.read();
if (!r5) {
e5.enqueue(n3);
continue;
}
return;
}
} });
}(r4.body) : r4.body, E = await y(k);
if ((0, l.X)() !== E.b)
return f(r4.url);
return { flightData: (0, s.aj)(E.f), canonicalUrl: h2, couldBeIntercepted: v, prerendered: E.S, postponed: b, staleTime: _ };
} catch (t4) {
return p.signal.aborted || console.error("Failed to fetch RSC payload for " + e3 + ". Falling back to browser navigation.", t4), { flightData: e3.toString(), canonicalUrl: void 0, couldBeIntercepted: false, prerendered: false, postponed: false, staleTime: -1 };
}
}
`;

describe("Abort controller", () => {
test("minimal", () => {
expect(patchCode(appPageRuntimeProdJs, abortControllerRule)).toBe(
`let p = {signal:{aborted: false}};
async function h(e3, t3) {
let { flightRouterState: r3, nextUrl: a2, prefetchKind: i2 } = t3, u2 = { [n2.hY]: "1", [n2.B]: encodeURIComponent(JSON.stringify(r3)) };
i2 === o.ob.AUTO && (u2[n2._V] = "1"), a2 && (u2[n2.kO] = a2);
try {
var c2;
let t4 = i2 ? i2 === o.ob.TEMPORARY ? "high" : "low" : "auto";
"export" === process.env.__NEXT_CONFIG_OUTPUT && ((e3 = new URL(e3)).pathname.endsWith("/") ? e3.pathname += "index.txt" : e3.pathname += ".txt");
let r4 = await m(e3, u2, t4, p.signal), a3 = d(r4.url), h2 = r4.redirected ? a3 : void 0, g = r4.headers.get("content-type") || "", v = !!(null == (c2 = r4.headers.get("vary")) ? void 0 : c2.includes(n2.kO)), b = !!r4.headers.get(n2.jc), S = r4.headers.get(n2.UK), _ = null !== S ? parseInt(S, 10) : -1, w = g.startsWith(n2.al);
if ("export" !== process.env.__NEXT_CONFIG_OUTPUT || w || (w = g.startsWith("text/plain")), !w || !r4.ok || !r4.body)
return e3.hash && (a3.hash = e3.hash), f(a3.toString());
let k = b ? function(e4) {
let t5 = e4.getReader();
return new ReadableStream({ async pull(e5) {
for (; ; ) {
let { done: r5, value: n3 } = await t5.read();
if (!r5) {
e5.enqueue(n3);
continue;
}
return;
}
} });
}(r4.body) : r4.body, E = await y(k);
if ((0, l.X)() !== E.b)
return f(r4.url);
return { flightData: (0, s.aj)(E.f), canonicalUrl: h2, couldBeIntercepted: v, prerendered: E.S, postponed: b, staleTime: _ };
} catch (t4) {
return p.signal.aborted || console.error("Failed to fetch RSC payload for " + e3 + ". Falling back to browser navigation.", t4), { flightData: e3.toString(), canonicalUrl: void 0, couldBeIntercepted: false, prerendered: false, postponed: false, staleTime: -1 };
}
}
`
);
});
});
92 changes: 92 additions & 0 deletions packages/cloudflare/src/cli/build/patches/plugins/next-minimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { patchCode } from "../ast/util.js";
import { ContentUpdater } from "./content-updater.js";

// We try to be as specific as possible to avoid patching the wrong thing here
// It seems that there is a bug in the worker runtime. When the AbortController is created outside of the request context it throws an error (not sure if it's expected or not) except in this case. https://github.com/cloudflare/workerd/issues/3657
// It fails while requiring the `app-page.runtime.prod.js` file, but instead of throwing an error, it just return an empty object for the `require('app-page.runtime.prod.js')` call which makes every request to an app router page fail.
// If it's a bug in workerd and it's not expected to throw an error, we can remove this patch.
export const abortControllerRule = `
rule:
all:
- kind: lexical_declaration
pattern: let $VAR = new AbortController
- precedes:
kind: function_declaration
stopBy: end
has:
kind: statement_block
has:
kind: try_statement
has:
kind: catch_clause
has:
kind: statement_block
has:
kind: return_statement
all:
- has:
stopBy: end
kind: member_expression
pattern: $VAR.signal.aborted
- has:
stopBy: end
kind: call_expression
regex: console.error\\("Failed to fetch RSC payload for

fix:
'let $VAR = {signal:{aborted: false}};'
`;

// This rule is used instead of defining `process.env.NEXT_MINIMAL` in the `esbuild config.
// Do we want to entirely replace these functions to reduce the bundle size?
// In next `renderHTML` is used as a fallback in case of errors, but in minimal mode it just throws the error and the responsability of handling it is on the infra.
export const nextMinimalRule = `
rule:
kind: member_expression
pattern: process.env.NEXT_MINIMAL
any:
- inside:
kind: parenthesized_expression
stopBy: end
inside:
kind: if_statement
any:
- inside:
kind: statement_block
inside:
kind: method_definition
any:
- has: {kind: property_identifier, field: name, regex: runEdgeFunction}
- has: {kind: property_identifier, field: name, regex: runMiddleware}
- has: {kind: property_identifier, field: name, regex: imageOptimizer}
- has:
kind: statement_block
has:
kind: expression_statement
pattern: res.statusCode = 400;
fix:
'true'
`;

export function patchNextMinimal(updater: ContentUpdater) {
updater.updateContent(
"patch-abortController-next15.2",
{ filter: /app-page(-experimental)?\.runtime\.prod\.js$/, contentFilter: /new AbortController/ },
async ({ contents }) => {
return patchCode(contents, abortControllerRule);
}
);

updater.updateContent(
"patch-next-minimal",
{ filter: /next-server\.(js)$/, contentFilter: /.*/ },
async ({ contents }) => {
return patchCode(contents, nextMinimalRule);
}
);

return {
name: "patch-abortController",
setup() {},
};
}
1 change: 1 addition & 0 deletions packages/cloudflare/src/cli/build/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./apply-patches.js";
export * from "./create-config-files.js";
export * from "./ensure-cf-config.js";
export * from "./extract-project-env-vars.js";
export * from "./needs-experimental-react.js";
export * from "./normalize-path.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextConfig } from "@opennextjs/aws/types/next-types";

// Not sure if this should be upstreamed to aws
// Adding more stuff there make typing incorrect actually, these properties are never undefined as long as it is the right version of next
// Ideally we'd have different `NextConfig` types for different versions of next
interface ExtendedNextConfig extends NextConfig {
experimental: {
ppr?: boolean;
taint?: boolean;
viewTransition?: boolean;
serverActions?: boolean;
};
}

// Copied from https://github.com/vercel/next.js/blob/4518bc91641a0fd938664b781e12ae7c145f3396/packages/next/src/lib/needs-experimental-react.ts#L3-L6
export function needsExperimentalReact(nextConfig: ExtendedNextConfig) {
const { ppr, taint, viewTransition } = nextConfig.experimental || {};
return Boolean(ppr || taint || viewTransition);
}
Loading