Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 6 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,9 @@ 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",
"process.env.TURBOPACK": "false",
// This define should be safe to use for Next 14.2+
"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.
// 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