diff --git a/.changeset/fast-spiders-hammer.md b/.changeset/fast-spiders-hammer.md new file mode 100644 index 000000000..b761c124e --- /dev/null +++ b/.changeset/fast-spiders-hammer.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +perf: drop `babel` to reduce the server bundle size diff --git a/packages/cloudflare/src/cli/build/bundle-server.ts b/packages/cloudflare/src/cli/build/bundle-server.ts index 52cf3147d..ae3453ecb 100644 --- a/packages/cloudflare/src/cli/build/bundle-server.ts +++ b/packages/cloudflare/src/cli/build/bundle-server.ts @@ -11,6 +11,7 @@ import { getOpenNextConfig } from "../../api/config.js"; import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js"; import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js"; import * as patches from "./patches/index.js"; +import { patchDropBabel } from "./patches/plugins/babel.js"; import { inlineBuildId } from "./patches/plugins/build-id.js"; import { inlineDynamicRequires } from "./patches/plugins/dynamic-requires.js"; import { inlineEvalManifest } from "./patches/plugins/eval-manifest.js"; @@ -101,6 +102,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise { inlineLoadManifest(updater, buildOpts), inlineBuildId(updater), patchDepdDeprecations(updater), + patchDropBabel(updater), // Apply updater updates, must be the last plugin updater.plugin, ] as Plugin[], diff --git a/packages/cloudflare/src/cli/build/patches/plugins/babel.spec.ts b/packages/cloudflare/src/cli/build/patches/plugins/babel.spec.ts new file mode 100644 index 000000000..fb8788729 --- /dev/null +++ b/packages/cloudflare/src/cli/build/patches/plugins/babel.spec.ts @@ -0,0 +1,193 @@ +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { describe, expect, test } from "vitest"; + +import { createEmptyBodyRule, errorInspectRule } from "./babel.js"; + +describe("babel-drop", () => { + test("Drop body", () => { + const code = ` +class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('invariant: runMiddleware should not be called in minimal mode'); + } + // Middleware is skipped for on-demand revalidate requests + if ((0, _apiutils.checkIsOnDemandRevalidate)(params.request, this.renderOpts.previewProps).isOnDemandRevalidate) { + return { + response: new Response(null, { + headers: { + 'x-middleware-next': '1' + } + }) + }; + } + // ... + } + async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('Middleware is not supported in minimal mode.'); + } + let edgeInfo; + const { query, page, match } = params; + if (!match) await this.ensureEdgeFunction({ + page, + appPaths: params.appPaths, + url: params.req.url + }); + // ... + } + + // ... +}`; + + expect(patchCode(code, createEmptyBodyRule("runMiddleware"))).toMatchInlineSnapshot(` + "class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + throw new Error("runMiddleware should not be called by OpenNext"); + } + async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('Middleware is not supported in minimal mode.'); + } + let edgeInfo; + const { query, page, match } = params; + if (!match) await this.ensureEdgeFunction({ + page, + appPaths: params.appPaths, + url: params.req.url + }); + // ... + } + + // ... + }" + `); + + expect(patchCode(code, createEmptyBodyRule("runEdgeFunction"))).toMatchInlineSnapshot(` + "class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('invariant: runMiddleware should not be called in minimal mode'); + } + // Middleware is skipped for on-demand revalidate requests + if ((0, _apiutils.checkIsOnDemandRevalidate)(params.request, this.renderOpts.previewProps).isOnDemandRevalidate) { + return { + response: new Response(null, { + headers: { + 'x-middleware-next': '1' + } + }) + }; + } + // ... + } + async runEdgeFunction(params) { + throw new Error("runEdgeFunction should not be called by OpenNext"); + } + + // ... + }" + `); + }); + + test("Error Inspect", () => { + const code = ` +// This file should be imported before any others. It sets up the environment +// for later imports to work properly. +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +require("./node-environment-baseline"); +require("./node-environment-extensions/error-inspect"); +require("./node-environment-extensions/random"); +require("./node-environment-extensions/date"); +require("./node-environment-extensions/web-crypto"); +require("./node-environment-extensions/node-crypto"); + +//# sourceMappingURL=node-environment.js.map +}`; + + expect(patchCode(code, errorInspectRule)).toMatchInlineSnapshot(` + "// This file should be imported before any others. It sets up the environment + // for later imports to work properly. + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + require("./node-environment-baseline"); + // Removed by OpenNext + // require("./node-environment-extensions/error-inspect"); + require("./node-environment-extensions/random"); + require("./node-environment-extensions/date"); + require("./node-environment-extensions/web-crypto"); + require("./node-environment-extensions/node-crypto"); + + //# sourceMappingURL=node-environment.js.map + }" + `); + }); +}); diff --git a/packages/cloudflare/src/cli/build/patches/plugins/babel.ts b/packages/cloudflare/src/cli/build/patches/plugins/babel.ts new file mode 100644 index 000000000..e88ad81d6 --- /dev/null +++ b/packages/cloudflare/src/cli/build/patches/plugins/babel.ts @@ -0,0 +1,76 @@ +/** + * Patches to avoid pulling babel (~4MB). + * + * Details: + * - empty `NextServer#runMiddleware` and `NextServer#runEdgeFunction` that are not used + * - drop `next/dist/server/node-environment-extensions/error-inspect.js` + */ + +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater"; +import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; + +/** + * Swaps the body for a throwing implementation + * + * @param methodName The name of the method + * @returns A rule to replace the body with a `throw` + */ +export function createEmptyBodyRule(methodName: string) { + return ` +rule: + pattern: + selector: method_definition + context: "class { async ${methodName}($$$PARAMS) { $$$_ } }" +fix: |- + async ${methodName}($$$PARAMS) { + throw new Error("${methodName} should not be called by OpenNext"); + } +`; +} + +/** + * Drops `require("./node-environment-extensions/error-inspect");` + */ +export const errorInspectRule = ` +rule: + pattern: require("./node-environment-extensions/error-inspect"); +fix: |- + // Removed by OpenNext + // require("./node-environment-extensions/error-inspect"); +`; + +export function patchDropBabel(updater: ContentUpdater): Plugin { + updater.updateContent("drop-babel-next-server", [ + { + field: { + filter: getCrossPlatformPathRegex(String.raw`/next/dist/server/next-server\.js$`, { + escape: false, + }), + contentFilter: /runMiddleware\(/, + callback: ({ contents }) => { + contents = patchCode(contents, createEmptyBodyRule("runMiddleware")); + contents = patchCode(contents, createEmptyBodyRule("runEdgeFunction")); + return contents; + }, + }, + }, + ]); + + updater.updateContent("drop-babel-error-inspect", [ + { + field: { + filter: getCrossPlatformPathRegex(String.raw`next/dist/server/node-environment\.js$`, { + escape: false, + }), + contentFilter: /node-environment-extensions\/error-inspect/, + callback: ({ contents }) => patchCode(contents, errorInspectRule), + }, + }, + ]); + + return { + name: "drop-babel", + setup() {}, + }; +}