diff --git a/.changeset/new-wombats-crash.md b/.changeset/new-wombats-crash.md new file mode 100644 index 000000000..e1b560a67 --- /dev/null +++ b/.changeset/new-wombats-crash.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +perf: drop `babel` to reduce the server bundle size diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 3582f5549..303c6b651 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -21,15 +21,7 @@ import { import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; -import { - patchBackgroundRevalidation, - patchEnvVars, - patchFetchCacheForISR, - patchFetchCacheSetMissingWaitUntil, - patchNextServer, - patchUnstableCacheForISR, - patchUseCacheForISR, -} from "./patch/patches/index.js"; +import * as patches from "./patch/patches/index.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -207,13 +199,14 @@ async function generateBundle( const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; await applyCodePatches(options, tracedFiles, manifests, [ - patchFetchCacheSetMissingWaitUntil, - patchFetchCacheForISR, - patchUnstableCacheForISR, - patchNextServer, - patchEnvVars, - patchBackgroundRevalidation, - patchUseCacheForISR, + patches.patchFetchCacheSetMissingWaitUntil, + patches.patchFetchCacheForISR, + patches.patchUnstableCacheForISR, + patches.patchNextServer, + patches.patchEnvVars, + patches.patchBackgroundRevalidation, + patches.patchUseCacheForISR, + patches.patchDropBabel, ...additionalCodePatches, ]); diff --git a/packages/open-next/src/build/patch/patches/dropBabel.ts b/packages/open-next/src/build/patch/patches/dropBabel.ts new file mode 100644 index 000000000..7e2eae519 --- /dev/null +++ b/packages/open-next/src/build/patch/patches/dropBabel.ts @@ -0,0 +1,88 @@ +/** + * 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 { getCrossPlatformPathRegex } from "utils/regex.js"; +import { patchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +export const patchDropBabel: CodePatcher = { + name: "patch-drop-babel", + patches: [ + // Empty the body of `NextServer#runMiddleware` + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`/next/dist/server/next-server\.js$`, + { + escape: false, + }, + ), + contentFilter: /runMiddleware\(/, + patchCode: async ({ code }) => + patchCode(code, createEmptyBodyRule("runMiddleware")), + }, + }, + // Empty the body of `NextServer#runEdgeFunction` + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`/next/dist/server/next-server\.js$`, + { + escape: false, + }, + ), + contentFilter: /runEdgeFunction\(/, + patchCode: async ({ code }) => + patchCode(code, createEmptyBodyRule("runEdgeFunction")), + }, + }, + // Drop `error-inspect` that pulls babel + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`next/dist/server/node-environment\.js$`, + { + escape: false, + }, + ), + contentFilter: /error-inspect/, + patchCode: async ({ code }) => patchCode(code, errorInspectRule), + }, + }, + ], +}; + +/** + * 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 with 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"); +`; diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts index bd46d6532..bd7ec945e 100644 --- a/packages/open-next/src/build/patch/patches/index.ts +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -7,3 +7,4 @@ export { } from "./patchFetchCacheISR.js"; export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js"; export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js"; +export { patchDropBabel } from "./dropBabel.js"; diff --git a/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts b/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts new file mode 100644 index 000000000..63af54789 --- /dev/null +++ b/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts @@ -0,0 +1,194 @@ +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { + createEmptyBodyRule, + errorInspectRule, +} from "@opennextjs/aws/build/patch/patches/dropBabel.js"; +import { describe, expect, test } from "vitest"; + +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 with 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 with 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 + }" + `); + }); +});