From eb88cedc4803d3250b7aea9cd00eb2ff8e3e86f0 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Thu, 27 Mar 2025 11:49:41 +0100 Subject: [PATCH 1/9] remove useless call and import --- .../open-next/src/build/createServerBundle.ts | 3 + .../src/build/patch/patchNextServer.ts | 114 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packages/open-next/src/build/patch/patchNextServer.ts diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index b062911b..7fea5c90 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -22,6 +22,7 @@ import { patchUnstableCacheForISR, } from "./patch/patchFetchCacheISR.js"; import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js"; +import { patchEnvVars, patchNextServer } from "./patch/patchNextServer.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -187,6 +188,8 @@ async function generateBundle( patchFetchCacheSetMissingWaitUntil, patchFetchCacheForISR, patchUnstableCacheForISR, + patchNextServer, + patchEnvVars, ...additionalCodePatches, ]); diff --git a/packages/open-next/src/build/patch/patchNextServer.ts b/packages/open-next/src/build/patch/patchNextServer.ts new file mode 100644 index 00000000..17e1b474 --- /dev/null +++ b/packages/open-next/src/build/patch/patchNextServer.ts @@ -0,0 +1,114 @@ +import { createPatchCode } from "./astCodePatcher.js"; +import type { CodePatcher } from "./codePatcher"; + +export const minimalRule = ` +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 const disablePreloadingRule = ` +rule: + kind: statement_block + inside: + kind: if_statement + any: + - has: + kind: member_expression + pattern: this.nextConfig.experimental.preloadEntriesOnStart + stopBy: end + - has: + kind: binary_expression + pattern: appDocumentPreloading === true + stopBy: end +fix: + '{}' +`; + +const envVarRuleCreator = (envVar: string, value: string) => ` +rule: + kind: member_expression + pattern: process.env.${envVar} + inside: + kind: if_statement + stopBy: end +fix: + '${value}' +`; + +export const patchNextServer: CodePatcher = { + name: "patch-next-server", + patches: [ + { + versions: ">=15.0.0", + field: { + pathFilter: /next-server\.(js)$/, + contentFilter: /process\.env\.NEXT_MINIMAL/, + patchCode: createPatchCode(minimalRule), + }, + }, + { + versions: ">=15.0.0", + field: { + pathFilter: /next-server\.(js)$/, + contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/, + patchCode: createPatchCode(disablePreloadingRule), + }, + }, + ], +}; + +export const patchEnvVars: CodePatcher = { + name: "patch-env-vars", + patches: [ + { + versions: ">=15.0.0", + field: { + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.NEXT_RUNTIME/, + patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), + }, + }, + { + versions: ">=15.0.0", + field: { + pathFilter: + /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, + contentFilter: /process\.env\.NODE_ENV/, + patchCode: createPatchCode( + envVarRuleCreator("NODE_ENV", '"production"'), + ), + }, + }, + { + versions: ">=15.0.0", + field: { + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.TURBOPACK/, + patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")), + }, + }, + ], +}; From f2bcf13da623bae36a9d23711ac44b3a951fb965 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 28 Mar 2025 11:46:21 +0100 Subject: [PATCH 2/9] add test for next server --- .../src/build/patch/patchNextServer.ts | 23 +- .../tests/build/patch/patchNextServer.test.ts | 1193 +++++++++++++++++ 2 files changed, 1215 insertions(+), 1 deletion(-) create mode 100644 packages/tests-unit/tests/build/patch/patchNextServer.test.ts diff --git a/packages/open-next/src/build/patch/patchNextServer.ts b/packages/open-next/src/build/patch/patchNextServer.ts index 17e1b474..54d3b257 100644 --- a/packages/open-next/src/build/patch/patchNextServer.ts +++ b/packages/open-next/src/build/patch/patchNextServer.ts @@ -47,7 +47,20 @@ fix: '{}' `; -const envVarRuleCreator = (envVar: string, value: string) => ` +// This rule is mostly for splitted edge functions so that we don't try to match them on the other non edge functions +export const removeMiddlewareManifestRule = ` +rule: + kind: statement_block + inside: + kind: method_definition + has: + kind: property_identifier + regex: getMiddlewareManifest +fix: + '{return null;}' +`; + +export const envVarRuleCreator = (envVar: string, value: string) => ` rule: kind: member_expression pattern: process.env.${envVar} @@ -77,6 +90,14 @@ export const patchNextServer: CodePatcher = { patchCode: createPatchCode(disablePreloadingRule), }, }, + { + versions: ">=15.0.0", + field: { + pathFilter: /next-server\.(js)$/, + contentFilter: /getMiddlewareManifest/, + patchCode: createPatchCode(removeMiddlewareManifestRule), + }, + }, ], }; diff --git a/packages/tests-unit/tests/build/patch/patchNextServer.test.ts b/packages/tests-unit/tests/build/patch/patchNextServer.test.ts new file mode 100644 index 00000000..7b4f7b9f --- /dev/null +++ b/packages/tests-unit/tests/build/patch/patchNextServer.test.ts @@ -0,0 +1,1193 @@ +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { + removeMiddlewareManifestRule, + minimalRule, + disablePreloadingRule, +} from "@opennextjs/aws/build/patch/patchNextServer.js"; +import { it, describe } from "vitest"; + +const nextServerGetMiddlewareManifestCode = ` +class NextNodeServer extends _baseserver.default { +getMiddlewareManifest() { + if (this.minimalMode) { + return null; + } else { + const manifest = require(this.middlewareManifestPath); + return manifest; + } + } +} +`; + +const nextServerMinimalCode = ` +class NextNodeServer extends _baseserver.default { +constructor(options){ + var _options_conf_experimental_sri, _options_conf_experimental; + // Initialize super class + super(options), this.registeredInstrumentation = false, this.cleanupListeners = new _asynccallbackset.AsyncCallbackSet(), this.handleNextImageRequest = async (req, res, parsedUrl)=>{ + if (!parsedUrl.pathname || !parsedUrl.pathname.startsWith('/_next/image')) { + return false; + } + // Ignore if its a middleware request + if ((0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke')) { + return false; + } + if (this.minimalMode || this.nextConfig.output === 'export' || process.env.NEXT_MINIMAL) { + res.statusCode = 400; + res.body('Bad Request').send(); + return true; + // the \`else\` branch is needed for tree-shaking + } else { + const { ImageOptimizerCache } = require('./image-optimizer'); + const imageOptimizerCache = new ImageOptimizerCache({ + distDir: this.distDir, + nextConfig: this.nextConfig + }); + const { sendResponse, ImageError } = require('./image-optimizer'); + if (!this.imageResponseCache) { + throw Object.defineProperty(new Error('invariant image optimizer cache was not initialized'), "__NEXT_ERROR_CODE", { + value: "E160", + enumerable: false, + configurable: true + }); + } + const imagesConfig = this.nextConfig.images; + if (imagesConfig.loader !== 'default' || imagesConfig.unoptimized) { + await this.render404(req, res); + return true; + } + const paramsResult = ImageOptimizerCache.validateParams(req.originalRequest, parsedUrl.query, this.nextConfig, !!this.renderOpts.dev); + if ('errorMessage' in paramsResult) { + res.statusCode = 400; + res.body(paramsResult.errorMessage).send(); + return true; + } + const cacheKey = ImageOptimizerCache.getCacheKey(paramsResult); + try { + var _cacheEntry_value, _cacheEntry_cacheControl; + const { getExtension } = require('./serve-static'); + const cacheEntry = await this.imageResponseCache.get(cacheKey, async ({ previousCacheEntry })=>{ + const { buffer, contentType, maxAge, upstreamEtag, etag } = await this.imageOptimizer(req, res, paramsResult, previousCacheEntry); + return { + value: { + kind: _responsecache.CachedRouteKind.IMAGE, + buffer, + etag, + extension: getExtension(contentType), + upstreamEtag + }, + isFallback: false, + cacheControl: { + revalidate: maxAge, + expire: undefined + } + }; + }, { + routeKind: _routekind.RouteKind.IMAGE, + incrementalCache: imageOptimizerCache, + isFallback: false + }); + if ((cacheEntry == null ? void 0 : (_cacheEntry_value = cacheEntry.value) == null ? void 0 : _cacheEntry_value.kind) !== _responsecache.CachedRouteKind.IMAGE) { + throw Object.defineProperty(new Error('invariant did not get entry from image response cache'), "__NEXT_ERROR_CODE", { + value: "E518", + enumerable: false, + configurable: true + }); + } + sendResponse(req.originalRequest, res.originalResponse, paramsResult.href, cacheEntry.value.extension, cacheEntry.value.buffer, cacheEntry.value.etag, paramsResult.isStatic, cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT', imagesConfig, ((_cacheEntry_cacheControl = cacheEntry.cacheControl) == null ? void 0 : _cacheEntry_cacheControl.revalidate) || 0, Boolean(this.renderOpts.dev)); + return true; + } catch (err) { + if (err instanceof ImageError) { + res.statusCode = err.statusCode; + res.body(err.message).send(); + return true; + } + throw err; + } + } + }, this.handleCatchallRenderRequest = async (req, res, parsedUrl)=>{ + let { pathname, query } = parsedUrl; + if (!pathname) { + throw Object.defineProperty(new Error('Invariant: pathname is undefined'), "__NEXT_ERROR_CODE", { + value: "E409", + enumerable: false, + configurable: true + }); + } + // This is a catch-all route, there should be no fallbacks so mark it as + // such. + (0, _requestmeta.addRequestMeta)(req, 'bubbleNoFallback', true); + try { + var _this_i18nProvider; + // next.js core assumes page path without trailing slash + pathname = (0, _removetrailingslash.removeTrailingSlash)(pathname); + const options = { + i18n: (_this_i18nProvider = this.i18nProvider) == null ? void 0 : _this_i18nProvider.fromRequest(req, pathname) + }; + const match = await this.matchers.match(pathname, options); + // If we don't have a match, try to render it anyways. + if (!match) { + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } + // Add the match to the request so we don't have to re-run the matcher + // for the same request. + (0, _requestmeta.addRequestMeta)(req, 'match', match); + // TODO-APP: move this to a route handler + const edgeFunctionsPages = this.getEdgeFunctionsPages(); + for (const edgeFunctionsPage of edgeFunctionsPages){ + // If the page doesn't match the edge function page, skip it. + if (edgeFunctionsPage !== match.definition.page) continue; + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + delete query[_approuterheaders.NEXT_RSC_UNION_QUERY]; + // If we handled the request, we can return early. + // For api routes edge runtime + try { + const handled = await this.runEdgeFunction({ + req, + res, + query, + params: match.params, + page: match.definition.page, + match, + appPaths: null + }); + if (handled) return true; + } catch (apiError) { + await this.instrumentationOnRequestError(apiError, req, { + routePath: match.definition.page, + routerKind: 'Pages Router', + routeType: 'route', + // Edge runtime does not support ISR + revalidateReason: undefined + }); + throw apiError; + } + } + // If the route was detected as being a Pages API route, then handle + // it. + // TODO: move this behavior into a route handler. + if ((0, _pagesapiroutematch.isPagesAPIRouteMatch)(match)) { + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + const handled = await this.handleApiRequest(req, res, query, match); + if (handled) return true; + } + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } catch (err) { + if (err instanceof _baseserver.NoFallbackError) { + throw err; + } + try { + if (this.renderOpts.dev) { + const { formatServerError } = require('../lib/format-server-error'); + formatServerError(err); + this.logErrorWithOriginalStack(err); + } else { + this.logError(err); + } + res.statusCode = 500; + await this.renderError(err, req, res, pathname, query); + return true; + } catch {} + throw err; + } + }, this.handleCatchallMiddlewareRequest = async (req, res, parsed)=>{ + const isMiddlewareInvoke = (0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke'); + if (!isMiddlewareInvoke) { + return false; + } + const handleFinished = ()=>{ + (0, _requestmeta.addRequestMeta)(req, 'middlewareInvoke', true); + res.body('').send(); + return true; + }; + const middleware = await this.getMiddleware(); + if (!middleware) { + return handleFinished(); + } + const initUrl = (0, _requestmeta.getRequestMeta)(req, 'initURL'); + const parsedUrl = (0, _parseurl.parseUrl)(initUrl); + const pathnameInfo = (0, _getnextpathnameinfo.getNextPathnameInfo)(parsedUrl.pathname, { + nextConfig: this.nextConfig, + i18nProvider: this.i18nProvider + }); + parsedUrl.pathname = pathnameInfo.pathname; + const normalizedPathname = (0, _removetrailingslash.removeTrailingSlash)(parsed.pathname || ''); + if (!middleware.match(normalizedPathname, req, parsedUrl.query)) { + return handleFinished(); + } + let result; + let bubblingResult = false; + try { + await this.ensureMiddleware(req.url); + result = await this.runMiddleware({ + request: req, + response: res, + parsedUrl: parsedUrl, + parsed: parsed + }); + if ('response' in result) { + if (isMiddlewareInvoke) { + bubblingResult = true; + throw Object.defineProperty(new _tracer.BubbledError(true, result), "__NEXT_ERROR_CODE", { + value: "E394", + enumerable: false, + configurable: true + }); + } + for (const [key, value] of Object.entries((0, _utils1.toNodeOutgoingHttpHeaders)(result.response.headers))){ + if (key !== 'content-encoding' && value !== undefined) { + res.setHeader(key, value); + } + } + res.statusCode = result.response.status; + const { originalResponse } = res; + if (result.response.body) { + await (0, _pipereadable.pipeToNodeResponse)(result.response.body, originalResponse); + } else { + originalResponse.end(); + } + return true; + } + } catch (err) { + if (bubblingResult) { + throw err; + } + if ((0, _iserror.default)(err) && err.code === 'ENOENT') { + await this.render404(req, res, parsed); + return true; + } + if (err instanceof _utils.DecodeError) { + res.statusCode = 400; + await this.renderError(err, req, res, parsed.pathname || ''); + return true; + } + const error = (0, _iserror.getProperError)(err); + console.error(error); + res.statusCode = 500; + await this.renderError(error, req, res, parsed.pathname || ''); + return true; + } + return result.finished; + }; + console.time('Next.js server initialization'); + this.isDev = options.dev ?? false; + this.sriEnabled = Boolean((_options_conf_experimental = options.conf.experimental) == null ? void 0 : (_options_conf_experimental_sri = _options_conf_experimental.sri) == null ? void 0 : _options_conf_experimental_sri.algorithm); + /** + * This sets environment variable to be used at the time of SSR by head.tsx. + * Using this from process.env allows targeting SSR by calling + * \`process.env.__NEXT_OPTIMIZE_CSS\`. + */ if (this.renderOpts.optimizeCss) { + process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true); + } + if (this.renderOpts.nextScriptWorkers) { + process.env.__NEXT_SCRIPT_WORKERS = JSON.stringify(true); + } + process.env.NEXT_DEPLOYMENT_ID = this.nextConfig.deploymentId || ''; + if (!this.minimalMode) { + this.imageResponseCache = new _responsecache.default(this.minimalMode); + } + const { appDocumentPreloading } = this.nextConfig.experimental; + const isDefaultEnabled = typeof appDocumentPreloading === 'undefined'; + if (!options.dev && (appDocumentPreloading === true || !(this.minimalMode && isDefaultEnabled))) { + // pre-warm _document and _app as these will be + // needed for most requests + (0, _loadcomponents.loadComponents)({ + distDir: this.distDir, + page: '/_document', + isAppPath: false, + isDev: this.isDev, + sriEnabled: this.sriEnabled + }).catch(()=>{}); + (0, _loadcomponents.loadComponents)({ + distDir: this.distDir, + page: '/_app', + isAppPath: false, + isDev: this.isDev, + sriEnabled: this.sriEnabled + }).catch(()=>{}); + } + if (!options.dev && !this.minimalMode && this.nextConfig.experimental.preloadEntriesOnStart) { + this.unstable_preloadEntries(); + } + if (!options.dev) { + const { dynamicRoutes = [] } = this.getRoutesManifest() ?? {}; + this.dynamicRoutes = dynamicRoutes.map((r)=>{ + // TODO: can we just re-use the regex from the manifest? + const regex = (0, _routeregex.getRouteRegex)(r.page); + const match = (0, _routematcher.getRouteMatcher)(regex); + return { + match, + page: r.page, + re: regex.re + }; + }); + } + // ensure options are set when loadConfig isn't called + (0, _setuphttpagentenv.setHttpClientAndAgentOptions)(this.nextConfig); + // Intercept fetch and other testmode apis. + if (this.serverOptions.experimentalTestProxy) { + process.env.NEXT_PRIVATE_TEST_PROXY = 'true'; + const { interceptTestApis } = require('next/dist/experimental/testmode/server'); + interceptTestApis(); + } + this.middlewareManifestPath = (0, _path.join)(this.serverDistDir, _constants.MIDDLEWARE_MANIFEST); + // This is just optimization to fire prepare as soon as possible. It will be + // properly awaited later. We add the catch here to ensure that it does not + // cause a unhandled promise rejection. The promise rejection will be + // handled later on via the \`await\` when the request handler is called. + if (!options.dev) { + this.prepare().catch((err)=>{ + console.error('Failed to prepare server', err); + }); + } + console.timeEnd('Next.js server initialization'); + } +async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('invariant: runMiddleware should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E276", + enumerable: false, + configurable: true + }); + } + // Middleware is skipped for on-demand revalidate requests + // REST OF THE CODE + } +async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('Middleware is not supported in minimal mode. Please remove the \`NEXT_MINIMAL\` environment variable.'), "__NEXT_ERROR_CODE", { + value: "E58", + enumerable: false, + configurable: true + }); + } +} +async imageOptimizer(req, res, paramsResult, previousCacheEntry) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('invariant: imageOptimizer should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E506", + enumerable: false, + configurable: true + }); + } else { + const { imageOptimizer, fetchExternalImage, fetchInternalImage } = require('./image-optimizer'); + const handleInternalReq = async (newReq, newRes)=>{ + if (newReq.url === req.url) { + throw Object.defineProperty(new Error(\`Invariant attempted to optimize _next/image itself\`), "__NEXT_ERROR_CODE", { + value: "E496", + enumerable: false, + configurable: true + }); + } + if (!this.routerServerHandler) { + throw Object.defineProperty(new Error(\`Invariant missing routerServerHandler\`), "__NEXT_ERROR_CODE", { + value: "E317", + enumerable: false, + configurable: true + }); + } + await this.routerServerHandler(newReq, newRes); + return; + }; + const { isAbsolute, href } = paramsResult; + const imageUpstream = isAbsolute ? await fetchExternalImage(href) : await fetchInternalImage(href, req.originalRequest, res.originalResponse, handleInternalReq); + return imageOptimizer(imageUpstream, paramsResult, this.nextConfig, { + isDev: this.renderOpts.dev, + previousCacheEntry + }); + } + } +} +`; + +describe("patchNextServer", () => { + it("should patch getMiddlewareManifest", async () => { + expect( + patchCode( + nextServerGetMiddlewareManifestCode, + removeMiddlewareManifestRule, + ), + ).toMatchInlineSnapshot(` +"class NextNodeServer extends _baseserver.default { +getMiddlewareManifest() {return null;} +} +" +`); + }); + + it("should patch minimalMode", async () => { + expect( + patchCode(nextServerMinimalCode, minimalRule), + ).toMatchInlineSnapshot(` +"class NextNodeServer extends _baseserver.default { +constructor(options){ + var _options_conf_experimental_sri, _options_conf_experimental; + // Initialize super class + super(options), this.registeredInstrumentation = false, this.cleanupListeners = new _asynccallbackset.AsyncCallbackSet(), this.handleNextImageRequest = async (req, res, parsedUrl)=>{ + if (!parsedUrl.pathname || !parsedUrl.pathname.startsWith('/_next/image')) { + return false; + } + // Ignore if its a middleware request + if ((0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke')) { + return false; + } + if (this.minimalMode || this.nextConfig.output === 'export' || true) { + res.statusCode = 400; + res.body('Bad Request').send(); + return true; + // the \`else\` branch is needed for tree-shaking + } else { + const { ImageOptimizerCache } = require('./image-optimizer'); + const imageOptimizerCache = new ImageOptimizerCache({ + distDir: this.distDir, + nextConfig: this.nextConfig + }); + const { sendResponse, ImageError } = require('./image-optimizer'); + if (!this.imageResponseCache) { + throw Object.defineProperty(new Error('invariant image optimizer cache was not initialized'), "__NEXT_ERROR_CODE", { + value: "E160", + enumerable: false, + configurable: true + }); + } + const imagesConfig = this.nextConfig.images; + if (imagesConfig.loader !== 'default' || imagesConfig.unoptimized) { + await this.render404(req, res); + return true; + } + const paramsResult = ImageOptimizerCache.validateParams(req.originalRequest, parsedUrl.query, this.nextConfig, !!this.renderOpts.dev); + if ('errorMessage' in paramsResult) { + res.statusCode = 400; + res.body(paramsResult.errorMessage).send(); + return true; + } + const cacheKey = ImageOptimizerCache.getCacheKey(paramsResult); + try { + var _cacheEntry_value, _cacheEntry_cacheControl; + const { getExtension } = require('./serve-static'); + const cacheEntry = await this.imageResponseCache.get(cacheKey, async ({ previousCacheEntry })=>{ + const { buffer, contentType, maxAge, upstreamEtag, etag } = await this.imageOptimizer(req, res, paramsResult, previousCacheEntry); + return { + value: { + kind: _responsecache.CachedRouteKind.IMAGE, + buffer, + etag, + extension: getExtension(contentType), + upstreamEtag + }, + isFallback: false, + cacheControl: { + revalidate: maxAge, + expire: undefined + } + }; + }, { + routeKind: _routekind.RouteKind.IMAGE, + incrementalCache: imageOptimizerCache, + isFallback: false + }); + if ((cacheEntry == null ? void 0 : (_cacheEntry_value = cacheEntry.value) == null ? void 0 : _cacheEntry_value.kind) !== _responsecache.CachedRouteKind.IMAGE) { + throw Object.defineProperty(new Error('invariant did not get entry from image response cache'), "__NEXT_ERROR_CODE", { + value: "E518", + enumerable: false, + configurable: true + }); + } + sendResponse(req.originalRequest, res.originalResponse, paramsResult.href, cacheEntry.value.extension, cacheEntry.value.buffer, cacheEntry.value.etag, paramsResult.isStatic, cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT', imagesConfig, ((_cacheEntry_cacheControl = cacheEntry.cacheControl) == null ? void 0 : _cacheEntry_cacheControl.revalidate) || 0, Boolean(this.renderOpts.dev)); + return true; + } catch (err) { + if (err instanceof ImageError) { + res.statusCode = err.statusCode; + res.body(err.message).send(); + return true; + } + throw err; + } + } + }, this.handleCatchallRenderRequest = async (req, res, parsedUrl)=>{ + let { pathname, query } = parsedUrl; + if (!pathname) { + throw Object.defineProperty(new Error('Invariant: pathname is undefined'), "__NEXT_ERROR_CODE", { + value: "E409", + enumerable: false, + configurable: true + }); + } + // This is a catch-all route, there should be no fallbacks so mark it as + // such. + (0, _requestmeta.addRequestMeta)(req, 'bubbleNoFallback', true); + try { + var _this_i18nProvider; + // next.js core assumes page path without trailing slash + pathname = (0, _removetrailingslash.removeTrailingSlash)(pathname); + const options = { + i18n: (_this_i18nProvider = this.i18nProvider) == null ? void 0 : _this_i18nProvider.fromRequest(req, pathname) + }; + const match = await this.matchers.match(pathname, options); + // If we don't have a match, try to render it anyways. + if (!match) { + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } + // Add the match to the request so we don't have to re-run the matcher + // for the same request. + (0, _requestmeta.addRequestMeta)(req, 'match', match); + // TODO-APP: move this to a route handler + const edgeFunctionsPages = this.getEdgeFunctionsPages(); + for (const edgeFunctionsPage of edgeFunctionsPages){ + // If the page doesn't match the edge function page, skip it. + if (edgeFunctionsPage !== match.definition.page) continue; + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + delete query[_approuterheaders.NEXT_RSC_UNION_QUERY]; + // If we handled the request, we can return early. + // For api routes edge runtime + try { + const handled = await this.runEdgeFunction({ + req, + res, + query, + params: match.params, + page: match.definition.page, + match, + appPaths: null + }); + if (handled) return true; + } catch (apiError) { + await this.instrumentationOnRequestError(apiError, req, { + routePath: match.definition.page, + routerKind: 'Pages Router', + routeType: 'route', + // Edge runtime does not support ISR + revalidateReason: undefined + }); + throw apiError; + } + } + // If the route was detected as being a Pages API route, then handle + // it. + // TODO: move this behavior into a route handler. + if ((0, _pagesapiroutematch.isPagesAPIRouteMatch)(match)) { + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + const handled = await this.handleApiRequest(req, res, query, match); + if (handled) return true; + } + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } catch (err) { + if (err instanceof _baseserver.NoFallbackError) { + throw err; + } + try { + if (this.renderOpts.dev) { + const { formatServerError } = require('../lib/format-server-error'); + formatServerError(err); + this.logErrorWithOriginalStack(err); + } else { + this.logError(err); + } + res.statusCode = 500; + await this.renderError(err, req, res, pathname, query); + return true; + } catch {} + throw err; + } + }, this.handleCatchallMiddlewareRequest = async (req, res, parsed)=>{ + const isMiddlewareInvoke = (0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke'); + if (!isMiddlewareInvoke) { + return false; + } + const handleFinished = ()=>{ + (0, _requestmeta.addRequestMeta)(req, 'middlewareInvoke', true); + res.body('').send(); + return true; + }; + const middleware = await this.getMiddleware(); + if (!middleware) { + return handleFinished(); + } + const initUrl = (0, _requestmeta.getRequestMeta)(req, 'initURL'); + const parsedUrl = (0, _parseurl.parseUrl)(initUrl); + const pathnameInfo = (0, _getnextpathnameinfo.getNextPathnameInfo)(parsedUrl.pathname, { + nextConfig: this.nextConfig, + i18nProvider: this.i18nProvider + }); + parsedUrl.pathname = pathnameInfo.pathname; + const normalizedPathname = (0, _removetrailingslash.removeTrailingSlash)(parsed.pathname || ''); + if (!middleware.match(normalizedPathname, req, parsedUrl.query)) { + return handleFinished(); + } + let result; + let bubblingResult = false; + try { + await this.ensureMiddleware(req.url); + result = await this.runMiddleware({ + request: req, + response: res, + parsedUrl: parsedUrl, + parsed: parsed + }); + if ('response' in result) { + if (isMiddlewareInvoke) { + bubblingResult = true; + throw Object.defineProperty(new _tracer.BubbledError(true, result), "__NEXT_ERROR_CODE", { + value: "E394", + enumerable: false, + configurable: true + }); + } + for (const [key, value] of Object.entries((0, _utils1.toNodeOutgoingHttpHeaders)(result.response.headers))){ + if (key !== 'content-encoding' && value !== undefined) { + res.setHeader(key, value); + } + } + res.statusCode = result.response.status; + const { originalResponse } = res; + if (result.response.body) { + await (0, _pipereadable.pipeToNodeResponse)(result.response.body, originalResponse); + } else { + originalResponse.end(); + } + return true; + } + } catch (err) { + if (bubblingResult) { + throw err; + } + if ((0, _iserror.default)(err) && err.code === 'ENOENT') { + await this.render404(req, res, parsed); + return true; + } + if (err instanceof _utils.DecodeError) { + res.statusCode = 400; + await this.renderError(err, req, res, parsed.pathname || ''); + return true; + } + const error = (0, _iserror.getProperError)(err); + console.error(error); + res.statusCode = 500; + await this.renderError(error, req, res, parsed.pathname || ''); + return true; + } + return result.finished; + }; + console.time('Next.js server initialization'); + this.isDev = options.dev ?? false; + this.sriEnabled = Boolean((_options_conf_experimental = options.conf.experimental) == null ? void 0 : (_options_conf_experimental_sri = _options_conf_experimental.sri) == null ? void 0 : _options_conf_experimental_sri.algorithm); + /** + * This sets environment variable to be used at the time of SSR by head.tsx. + * Using this from process.env allows targeting SSR by calling + * \`process.env.__NEXT_OPTIMIZE_CSS\`. + */ if (this.renderOpts.optimizeCss) { + process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true); + } + if (this.renderOpts.nextScriptWorkers) { + process.env.__NEXT_SCRIPT_WORKERS = JSON.stringify(true); + } + process.env.NEXT_DEPLOYMENT_ID = this.nextConfig.deploymentId || ''; + if (!this.minimalMode) { + this.imageResponseCache = new _responsecache.default(this.minimalMode); + } + const { appDocumentPreloading } = this.nextConfig.experimental; + const isDefaultEnabled = typeof appDocumentPreloading === 'undefined'; + if (!options.dev && (appDocumentPreloading === true || !(this.minimalMode && isDefaultEnabled))) { + // pre-warm _document and _app as these will be + // needed for most requests + (0, _loadcomponents.loadComponents)({ + distDir: this.distDir, + page: '/_document', + isAppPath: false, + isDev: this.isDev, + sriEnabled: this.sriEnabled + }).catch(()=>{}); + (0, _loadcomponents.loadComponents)({ + distDir: this.distDir, + page: '/_app', + isAppPath: false, + isDev: this.isDev, + sriEnabled: this.sriEnabled + }).catch(()=>{}); + } + if (!options.dev && !this.minimalMode && this.nextConfig.experimental.preloadEntriesOnStart) { + this.unstable_preloadEntries(); + } + if (!options.dev) { + const { dynamicRoutes = [] } = this.getRoutesManifest() ?? {}; + this.dynamicRoutes = dynamicRoutes.map((r)=>{ + // TODO: can we just re-use the regex from the manifest? + const regex = (0, _routeregex.getRouteRegex)(r.page); + const match = (0, _routematcher.getRouteMatcher)(regex); + return { + match, + page: r.page, + re: regex.re + }; + }); + } + // ensure options are set when loadConfig isn't called + (0, _setuphttpagentenv.setHttpClientAndAgentOptions)(this.nextConfig); + // Intercept fetch and other testmode apis. + if (this.serverOptions.experimentalTestProxy) { + process.env.NEXT_PRIVATE_TEST_PROXY = 'true'; + const { interceptTestApis } = require('next/dist/experimental/testmode/server'); + interceptTestApis(); + } + this.middlewareManifestPath = (0, _path.join)(this.serverDistDir, _constants.MIDDLEWARE_MANIFEST); + // This is just optimization to fire prepare as soon as possible. It will be + // properly awaited later. We add the catch here to ensure that it does not + // cause a unhandled promise rejection. The promise rejection will be + // handled later on via the \`await\` when the request handler is called. + if (!options.dev) { + this.prepare().catch((err)=>{ + console.error('Failed to prepare server', err); + }); + } + console.timeEnd('Next.js server initialization'); + } +async runMiddleware(params) { + if (true) { + throw Object.defineProperty(new Error('invariant: runMiddleware should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E276", + enumerable: false, + configurable: true + }); + } + // Middleware is skipped for on-demand revalidate requests + // REST OF THE CODE + } +async runEdgeFunction(params) { + if (true) { + throw Object.defineProperty(new Error('Middleware is not supported in minimal mode. Please remove the \`NEXT_MINIMAL\` environment variable.'), "__NEXT_ERROR_CODE", { + value: "E58", + enumerable: false, + configurable: true + }); + } +} +async imageOptimizer(req, res, paramsResult, previousCacheEntry) { + if (true) { + throw Object.defineProperty(new Error('invariant: imageOptimizer should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E506", + enumerable: false, + configurable: true + }); + } else { + const { imageOptimizer, fetchExternalImage, fetchInternalImage } = require('./image-optimizer'); + const handleInternalReq = async (newReq, newRes)=>{ + if (newReq.url === req.url) { + throw Object.defineProperty(new Error(\`Invariant attempted to optimize _next/image itself\`), "__NEXT_ERROR_CODE", { + value: "E496", + enumerable: false, + configurable: true + }); + } + if (!this.routerServerHandler) { + throw Object.defineProperty(new Error(\`Invariant missing routerServerHandler\`), "__NEXT_ERROR_CODE", { + value: "E317", + enumerable: false, + configurable: true + }); + } + await this.routerServerHandler(newReq, newRes); + return; + }; + const { isAbsolute, href } = paramsResult; + const imageUpstream = isAbsolute ? await fetchExternalImage(href) : await fetchInternalImage(href, req.originalRequest, res.originalResponse, handleInternalReq); + return imageOptimizer(imageUpstream, paramsResult, this.nextConfig, { + isDev: this.renderOpts.dev, + previousCacheEntry + }); + } + } +} +"`); + }); + + it("should disable preloading", async () => { + expect( + patchCode(nextServerMinimalCode, disablePreloadingRule), + ).toMatchInlineSnapshot(` +"class NextNodeServer extends _baseserver.default { +constructor(options){ + var _options_conf_experimental_sri, _options_conf_experimental; + // Initialize super class + super(options), this.registeredInstrumentation = false, this.cleanupListeners = new _asynccallbackset.AsyncCallbackSet(), this.handleNextImageRequest = async (req, res, parsedUrl)=>{ + if (!parsedUrl.pathname || !parsedUrl.pathname.startsWith('/_next/image')) { + return false; + } + // Ignore if its a middleware request + if ((0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke')) { + return false; + } + if (this.minimalMode || this.nextConfig.output === 'export' || process.env.NEXT_MINIMAL) { + res.statusCode = 400; + res.body('Bad Request').send(); + return true; + // the \`else\` branch is needed for tree-shaking + } else { + const { ImageOptimizerCache } = require('./image-optimizer'); + const imageOptimizerCache = new ImageOptimizerCache({ + distDir: this.distDir, + nextConfig: this.nextConfig + }); + const { sendResponse, ImageError } = require('./image-optimizer'); + if (!this.imageResponseCache) { + throw Object.defineProperty(new Error('invariant image optimizer cache was not initialized'), "__NEXT_ERROR_CODE", { + value: "E160", + enumerable: false, + configurable: true + }); + } + const imagesConfig = this.nextConfig.images; + if (imagesConfig.loader !== 'default' || imagesConfig.unoptimized) { + await this.render404(req, res); + return true; + } + const paramsResult = ImageOptimizerCache.validateParams(req.originalRequest, parsedUrl.query, this.nextConfig, !!this.renderOpts.dev); + if ('errorMessage' in paramsResult) { + res.statusCode = 400; + res.body(paramsResult.errorMessage).send(); + return true; + } + const cacheKey = ImageOptimizerCache.getCacheKey(paramsResult); + try { + var _cacheEntry_value, _cacheEntry_cacheControl; + const { getExtension } = require('./serve-static'); + const cacheEntry = await this.imageResponseCache.get(cacheKey, async ({ previousCacheEntry })=>{ + const { buffer, contentType, maxAge, upstreamEtag, etag } = await this.imageOptimizer(req, res, paramsResult, previousCacheEntry); + return { + value: { + kind: _responsecache.CachedRouteKind.IMAGE, + buffer, + etag, + extension: getExtension(contentType), + upstreamEtag + }, + isFallback: false, + cacheControl: { + revalidate: maxAge, + expire: undefined + } + }; + }, { + routeKind: _routekind.RouteKind.IMAGE, + incrementalCache: imageOptimizerCache, + isFallback: false + }); + if ((cacheEntry == null ? void 0 : (_cacheEntry_value = cacheEntry.value) == null ? void 0 : _cacheEntry_value.kind) !== _responsecache.CachedRouteKind.IMAGE) { + throw Object.defineProperty(new Error('invariant did not get entry from image response cache'), "__NEXT_ERROR_CODE", { + value: "E518", + enumerable: false, + configurable: true + }); + } + sendResponse(req.originalRequest, res.originalResponse, paramsResult.href, cacheEntry.value.extension, cacheEntry.value.buffer, cacheEntry.value.etag, paramsResult.isStatic, cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT', imagesConfig, ((_cacheEntry_cacheControl = cacheEntry.cacheControl) == null ? void 0 : _cacheEntry_cacheControl.revalidate) || 0, Boolean(this.renderOpts.dev)); + return true; + } catch (err) { + if (err instanceof ImageError) { + res.statusCode = err.statusCode; + res.body(err.message).send(); + return true; + } + throw err; + } + } + }, this.handleCatchallRenderRequest = async (req, res, parsedUrl)=>{ + let { pathname, query } = parsedUrl; + if (!pathname) { + throw Object.defineProperty(new Error('Invariant: pathname is undefined'), "__NEXT_ERROR_CODE", { + value: "E409", + enumerable: false, + configurable: true + }); + } + // This is a catch-all route, there should be no fallbacks so mark it as + // such. + (0, _requestmeta.addRequestMeta)(req, 'bubbleNoFallback', true); + try { + var _this_i18nProvider; + // next.js core assumes page path without trailing slash + pathname = (0, _removetrailingslash.removeTrailingSlash)(pathname); + const options = { + i18n: (_this_i18nProvider = this.i18nProvider) == null ? void 0 : _this_i18nProvider.fromRequest(req, pathname) + }; + const match = await this.matchers.match(pathname, options); + // If we don't have a match, try to render it anyways. + if (!match) { + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } + // Add the match to the request so we don't have to re-run the matcher + // for the same request. + (0, _requestmeta.addRequestMeta)(req, 'match', match); + // TODO-APP: move this to a route handler + const edgeFunctionsPages = this.getEdgeFunctionsPages(); + for (const edgeFunctionsPage of edgeFunctionsPages){ + // If the page doesn't match the edge function page, skip it. + if (edgeFunctionsPage !== match.definition.page) continue; + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + delete query[_approuterheaders.NEXT_RSC_UNION_QUERY]; + // If we handled the request, we can return early. + // For api routes edge runtime + try { + const handled = await this.runEdgeFunction({ + req, + res, + query, + params: match.params, + page: match.definition.page, + match, + appPaths: null + }); + if (handled) return true; + } catch (apiError) { + await this.instrumentationOnRequestError(apiError, req, { + routePath: match.definition.page, + routerKind: 'Pages Router', + routeType: 'route', + // Edge runtime does not support ISR + revalidateReason: undefined + }); + throw apiError; + } + } + // If the route was detected as being a Pages API route, then handle + // it. + // TODO: move this behavior into a route handler. + if ((0, _pagesapiroutematch.isPagesAPIRouteMatch)(match)) { + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl); + return true; + } + const handled = await this.handleApiRequest(req, res, query, match); + if (handled) return true; + } + await this.render(req, res, pathname, query, parsedUrl, true); + return true; + } catch (err) { + if (err instanceof _baseserver.NoFallbackError) { + throw err; + } + try { + if (this.renderOpts.dev) { + const { formatServerError } = require('../lib/format-server-error'); + formatServerError(err); + this.logErrorWithOriginalStack(err); + } else { + this.logError(err); + } + res.statusCode = 500; + await this.renderError(err, req, res, pathname, query); + return true; + } catch {} + throw err; + } + }, this.handleCatchallMiddlewareRequest = async (req, res, parsed)=>{ + const isMiddlewareInvoke = (0, _requestmeta.getRequestMeta)(req, 'middlewareInvoke'); + if (!isMiddlewareInvoke) { + return false; + } + const handleFinished = ()=>{ + (0, _requestmeta.addRequestMeta)(req, 'middlewareInvoke', true); + res.body('').send(); + return true; + }; + const middleware = await this.getMiddleware(); + if (!middleware) { + return handleFinished(); + } + const initUrl = (0, _requestmeta.getRequestMeta)(req, 'initURL'); + const parsedUrl = (0, _parseurl.parseUrl)(initUrl); + const pathnameInfo = (0, _getnextpathnameinfo.getNextPathnameInfo)(parsedUrl.pathname, { + nextConfig: this.nextConfig, + i18nProvider: this.i18nProvider + }); + parsedUrl.pathname = pathnameInfo.pathname; + const normalizedPathname = (0, _removetrailingslash.removeTrailingSlash)(parsed.pathname || ''); + if (!middleware.match(normalizedPathname, req, parsedUrl.query)) { + return handleFinished(); + } + let result; + let bubblingResult = false; + try { + await this.ensureMiddleware(req.url); + result = await this.runMiddleware({ + request: req, + response: res, + parsedUrl: parsedUrl, + parsed: parsed + }); + if ('response' in result) { + if (isMiddlewareInvoke) { + bubblingResult = true; + throw Object.defineProperty(new _tracer.BubbledError(true, result), "__NEXT_ERROR_CODE", { + value: "E394", + enumerable: false, + configurable: true + }); + } + for (const [key, value] of Object.entries((0, _utils1.toNodeOutgoingHttpHeaders)(result.response.headers))){ + if (key !== 'content-encoding' && value !== undefined) { + res.setHeader(key, value); + } + } + res.statusCode = result.response.status; + const { originalResponse } = res; + if (result.response.body) { + await (0, _pipereadable.pipeToNodeResponse)(result.response.body, originalResponse); + } else { + originalResponse.end(); + } + return true; + } + } catch (err) { + if (bubblingResult) { + throw err; + } + if ((0, _iserror.default)(err) && err.code === 'ENOENT') { + await this.render404(req, res, parsed); + return true; + } + if (err instanceof _utils.DecodeError) { + res.statusCode = 400; + await this.renderError(err, req, res, parsed.pathname || ''); + return true; + } + const error = (0, _iserror.getProperError)(err); + console.error(error); + res.statusCode = 500; + await this.renderError(error, req, res, parsed.pathname || ''); + return true; + } + return result.finished; + }; + console.time('Next.js server initialization'); + this.isDev = options.dev ?? false; + this.sriEnabled = Boolean((_options_conf_experimental = options.conf.experimental) == null ? void 0 : (_options_conf_experimental_sri = _options_conf_experimental.sri) == null ? void 0 : _options_conf_experimental_sri.algorithm); + /** + * This sets environment variable to be used at the time of SSR by head.tsx. + * Using this from process.env allows targeting SSR by calling + * \`process.env.__NEXT_OPTIMIZE_CSS\`. + */ if (this.renderOpts.optimizeCss) { + process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true); + } + if (this.renderOpts.nextScriptWorkers) { + process.env.__NEXT_SCRIPT_WORKERS = JSON.stringify(true); + } + process.env.NEXT_DEPLOYMENT_ID = this.nextConfig.deploymentId || ''; + if (!this.minimalMode) { + this.imageResponseCache = new _responsecache.default(this.minimalMode); + } + const { appDocumentPreloading } = this.nextConfig.experimental; + const isDefaultEnabled = typeof appDocumentPreloading === 'undefined'; + if (!options.dev && (appDocumentPreloading === true || !(this.minimalMode && isDefaultEnabled))) {} + if (!options.dev && !this.minimalMode && this.nextConfig.experimental.preloadEntriesOnStart) {} + if (!options.dev) { + const { dynamicRoutes = [] } = this.getRoutesManifest() ?? {}; + this.dynamicRoutes = dynamicRoutes.map((r)=>{ + // TODO: can we just re-use the regex from the manifest? + const regex = (0, _routeregex.getRouteRegex)(r.page); + const match = (0, _routematcher.getRouteMatcher)(regex); + return { + match, + page: r.page, + re: regex.re + }; + }); + } + // ensure options are set when loadConfig isn't called + (0, _setuphttpagentenv.setHttpClientAndAgentOptions)(this.nextConfig); + // Intercept fetch and other testmode apis. + if (this.serverOptions.experimentalTestProxy) { + process.env.NEXT_PRIVATE_TEST_PROXY = 'true'; + const { interceptTestApis } = require('next/dist/experimental/testmode/server'); + interceptTestApis(); + } + this.middlewareManifestPath = (0, _path.join)(this.serverDistDir, _constants.MIDDLEWARE_MANIFEST); + // This is just optimization to fire prepare as soon as possible. It will be + // properly awaited later. We add the catch here to ensure that it does not + // cause a unhandled promise rejection. The promise rejection will be + // handled later on via the \`await\` when the request handler is called. + if (!options.dev) { + this.prepare().catch((err)=>{ + console.error('Failed to prepare server', err); + }); + } + console.timeEnd('Next.js server initialization'); + } +async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('invariant: runMiddleware should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E276", + enumerable: false, + configurable: true + }); + } + // Middleware is skipped for on-demand revalidate requests + // REST OF THE CODE + } +async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('Middleware is not supported in minimal mode. Please remove the \`NEXT_MINIMAL\` environment variable.'), "__NEXT_ERROR_CODE", { + value: "E58", + enumerable: false, + configurable: true + }); + } +} +async imageOptimizer(req, res, paramsResult, previousCacheEntry) { + if (process.env.NEXT_MINIMAL) { + throw Object.defineProperty(new Error('invariant: imageOptimizer should not be called in minimal mode'), "__NEXT_ERROR_CODE", { + value: "E506", + enumerable: false, + configurable: true + }); + } else { + const { imageOptimizer, fetchExternalImage, fetchInternalImage } = require('./image-optimizer'); + const handleInternalReq = async (newReq, newRes)=>{ + if (newReq.url === req.url) { + throw Object.defineProperty(new Error(\`Invariant attempted to optimize _next/image itself\`), "__NEXT_ERROR_CODE", { + value: "E496", + enumerable: false, + configurable: true + }); + } + if (!this.routerServerHandler) { + throw Object.defineProperty(new Error(\`Invariant missing routerServerHandler\`), "__NEXT_ERROR_CODE", { + value: "E317", + enumerable: false, + configurable: true + }); + } + await this.routerServerHandler(newReq, newRes); + return; + }; + const { isAbsolute, href } = paramsResult; + const imageUpstream = isAbsolute ? await fetchExternalImage(href) : await fetchInternalImage(href, req.originalRequest, res.originalResponse, handleInternalReq); + return imageOptimizer(imageUpstream, paramsResult, this.nextConfig, { + isDev: this.renderOpts.dev, + previousCacheEntry + }); + } + } +} +" + `); + }); +}); From c50c1339d7a16b23d65d6c12c34605d47bf0e589 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 28 Mar 2025 11:52:16 +0100 Subject: [PATCH 3/9] refactor --- .../open-next/src/build/createServerBundle.ts | 7 +-- .../src/build/patch/patches/index.ts | 4 ++ .../src/build/patch/patches/patchEnvVar.ts | 46 ++++++++++++++++++ .../patch/{ => patches}/patchFetchCacheISR.ts | 4 +- .../{ => patches}/patchFetchCacheWaitUntil.ts | 4 +- .../patch/{ => patches}/patchNextServer.ts | 48 +------------------ .../build/patch/patchFetchCacheISR.test.ts | 2 +- .../patch/patchFetchCacheWaitUntil.test.ts | 2 +- .../tests/build/patch/patchNextServer.test.ts | 2 +- 9 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 packages/open-next/src/build/patch/patches/index.ts create mode 100644 packages/open-next/src/build/patch/patches/patchEnvVar.ts rename packages/open-next/src/build/patch/{ => patches}/patchFetchCacheISR.ts (96%) rename packages/open-next/src/build/patch/{ => patches}/patchFetchCacheWaitUntil.ts (90%) rename packages/open-next/src/build/patch/{ => patches}/patchNextServer.ts (66%) diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 7fea5c90..7babec48 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -20,9 +20,10 @@ import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; import { patchFetchCacheForISR, patchUnstableCacheForISR, -} from "./patch/patchFetchCacheISR.js"; -import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js"; -import { patchEnvVars, patchNextServer } from "./patch/patchNextServer.js"; + patchNextServer, + patchEnvVars, + patchFetchCacheSetMissingWaitUntil, +} from "./patch/patches/index.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts new file mode 100644 index 00000000..dc9b28d2 --- /dev/null +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -0,0 +1,4 @@ +export * from "./patchEnvVar.js"; +export * from "./patchNextServer.js"; +export * from "./patchFetchCacheISR.js"; +export * from "./patchFetchCacheWaitUntil.js"; diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts new file mode 100644 index 00000000..33c126cc --- /dev/null +++ b/packages/open-next/src/build/patch/patches/patchEnvVar.ts @@ -0,0 +1,46 @@ +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher"; + +export const envVarRuleCreator = (envVar: string, value: string) => ` +rule: + kind: member_expression + pattern: process.env.${envVar} + inside: + kind: if_statement + stopBy: end +fix: + '${value}' +`; + +export const patchEnvVars: CodePatcher = { + name: "patch-env-vars", + patches: [ + { + versions: ">=15.0.0", + field: { + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.NEXT_RUNTIME/, + patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), + }, + }, + { + versions: ">=15.0.0", + field: { + pathFilter: + /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, + contentFilter: /process\.env\.NODE_ENV/, + patchCode: createPatchCode( + envVarRuleCreator("NODE_ENV", '"production"'), + ), + }, + }, + { + versions: ">=15.0.0", + field: { + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.TURBOPACK/, + patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")), + }, + }, + ], +}; diff --git a/packages/open-next/src/build/patch/patchFetchCacheISR.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts similarity index 96% rename from packages/open-next/src/build/patch/patchFetchCacheISR.ts rename to packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts index 724969f9..17ccea62 100644 --- a/packages/open-next/src/build/patch/patchFetchCacheISR.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts @@ -1,7 +1,7 @@ import { Lang } from "@ast-grep/napi"; import { getCrossPlatformPathRegex } from "utils/regex.js"; -import { createPatchCode } from "./astCodePatcher.js"; -import type { CodePatcher } from "./codePatcher"; +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; export const fetchRule = ` rule: diff --git a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts similarity index 90% rename from packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts rename to packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts index 0bb113bc..1266308f 100644 --- a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts @@ -1,6 +1,6 @@ import { getCrossPlatformPathRegex } from "utils/regex.js"; -import { createPatchCode } from "./astCodePatcher.js"; -import type { CodePatcher } from "./codePatcher"; +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; export const rule = ` rule: diff --git a/packages/open-next/src/build/patch/patchNextServer.ts b/packages/open-next/src/build/patch/patches/patchNextServer.ts similarity index 66% rename from packages/open-next/src/build/patch/patchNextServer.ts rename to packages/open-next/src/build/patch/patches/patchNextServer.ts index 54d3b257..0fc86cd9 100644 --- a/packages/open-next/src/build/patch/patchNextServer.ts +++ b/packages/open-next/src/build/patch/patches/patchNextServer.ts @@ -1,5 +1,5 @@ -import { createPatchCode } from "./astCodePatcher.js"; -import type { CodePatcher } from "./codePatcher"; +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; export const minimalRule = ` rule: @@ -60,17 +60,6 @@ fix: '{return null;}' `; -export const envVarRuleCreator = (envVar: string, value: string) => ` -rule: - kind: member_expression - pattern: process.env.${envVar} - inside: - kind: if_statement - stopBy: end -fix: - '${value}' -`; - export const patchNextServer: CodePatcher = { name: "patch-next-server", patches: [ @@ -100,36 +89,3 @@ export const patchNextServer: CodePatcher = { }, ], }; - -export const patchEnvVars: CodePatcher = { - name: "patch-env-vars", - patches: [ - { - versions: ">=15.0.0", - field: { - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.NEXT_RUNTIME/, - patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), - }, - }, - { - versions: ">=15.0.0", - field: { - pathFilter: - /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, - contentFilter: /process\.env\.NODE_ENV/, - patchCode: createPatchCode( - envVarRuleCreator("NODE_ENV", '"production"'), - ), - }, - }, - { - versions: ">=15.0.0", - field: { - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.TURBOPACK/, - patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")), - }, - }, - ], -}; diff --git a/packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts b/packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts index 61a09d50..649c5cc1 100644 --- a/packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts +++ b/packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts @@ -2,7 +2,7 @@ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; import { fetchRule, unstable_cacheRule, -} from "@opennextjs/aws/build/patch/patchFetchCacheISR.js"; +} from "@opennextjs/aws/build/patch/patches/patchFetchCacheISR.js"; import { describe } from "vitest"; const unstable_cacheCode = ` diff --git a/packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts b/packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts index 4ae8f239..893fd18d 100644 --- a/packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts +++ b/packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vitest"; import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import { rule } from "@opennextjs/aws/build/patch/patchFetchCacheWaitUntil.js"; +import { rule } from "@opennextjs/aws/build/patch/patches/patchFetchCacheWaitUntil.js"; describe("patchFetchCacheSetMissingWaitUntil", () => { test("on minified code", () => { diff --git a/packages/tests-unit/tests/build/patch/patchNextServer.test.ts b/packages/tests-unit/tests/build/patch/patchNextServer.test.ts index 7b4f7b9f..6f4cfc98 100644 --- a/packages/tests-unit/tests/build/patch/patchNextServer.test.ts +++ b/packages/tests-unit/tests/build/patch/patchNextServer.test.ts @@ -3,7 +3,7 @@ import { removeMiddlewareManifestRule, minimalRule, disablePreloadingRule, -} from "@opennextjs/aws/build/patch/patchNextServer.js"; +} from "@opennextjs/aws/build/patch/patches/patchNextServer.js"; import { it, describe } from "vitest"; const nextServerGetMiddlewareManifestCode = ` From f8281652fa64b77cc748e8aa445fe8d06ae3b4ae Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 28 Mar 2025 12:01:53 +0100 Subject: [PATCH 4/9] add env var test & move test --- .../build/patch/patches/patchEnvVars.test.ts | 154 ++++++++++++++++++ .../{ => patches}/patchFetchCacheISR.test.ts | 0 .../patchFetchCacheWaitUntil.test.ts | 0 .../{ => patches}/patchNextServer.test.ts | 0 4 files changed, 154 insertions(+) create mode 100644 packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts rename packages/tests-unit/tests/build/patch/{ => patches}/patchFetchCacheISR.test.ts (100%) rename packages/tests-unit/tests/build/patch/{ => patches}/patchFetchCacheWaitUntil.test.ts (100%) rename packages/tests-unit/tests/build/patch/{ => patches}/patchNextServer.test.ts (100%) diff --git a/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts b/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts new file mode 100644 index 00000000..4024bc10 --- /dev/null +++ b/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts @@ -0,0 +1,154 @@ +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { envVarRuleCreator } from "@opennextjs/aws/build/patch/patches/patchEnvVar.js"; +import { describe, it } from "vitest"; + +const moduleCompiledCode = ` +"use strict"; +if (process.env.NEXT_RUNTIME === 'edge') { + module.exports = require('next/dist/server/route-modules/app-page/module.js'); +} else { + if (process.env.__NEXT_EXPERIMENTAL_REACT) { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.prod.js'); + } + } else { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.prod.js'); + } + } +} +`; + +const reactJSXRuntimeCode = ` +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-jsx-runtime.production.js'); +} else { + module.exports = require('./cjs/react-jsx-runtime.development.js'); +} +`; + +describe("patch NODE_ENV", () => { + it("should patch NODE_ENV in module.compiled", () => { + expect( + patchCode( + moduleCompiledCode, + envVarRuleCreator("NODE_ENV", '"production"'), + ), + ).toMatchInlineSnapshot(` +""use strict"; +if (process.env.NEXT_RUNTIME === 'edge') { + module.exports = require('next/dist/server/route-modules/app-page/module.js'); +} else { + if (process.env.__NEXT_EXPERIMENTAL_REACT) { + if ("production" === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.prod.js'); + } + } else { + if ("production" === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.prod.js'); + } + } +} +"`); + }); + + it("should patch NODE_ENV in react/jsx-runtime", () => { + expect( + patchCode( + reactJSXRuntimeCode, + envVarRuleCreator("NODE_ENV", '"production"'), + ), + ).toMatchInlineSnapshot(` +"'use strict'; + +if ("production" === 'production') { + module.exports = require('./cjs/react-jsx-runtime.production.js'); +} else { + module.exports = require('./cjs/react-jsx-runtime.development.js'); +} +"`); + }); +}); + +describe("patch NEXT_RUNTIME", () => { + it("should patch NEXT_RUNTIME in module.compiled", () => { + expect( + patchCode( + moduleCompiledCode, + envVarRuleCreator("NEXT_RUNTIME", '"node"'), + ), + ).toMatchInlineSnapshot(` +""use strict"; +if ("node" === 'edge') { + module.exports = require('next/dist/server/route-modules/app-page/module.js'); +} else { + if (process.env.__NEXT_EXPERIMENTAL_REACT) { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.prod.js'); + } + } else { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.dev.js'); + } else if (process.env.TURBOPACK) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.prod.js'); + } + } +} +"`); + }); +}); + +describe("patch TURBOPACK", () => { + it("should patch TURBOPACK in module.compiled", () => { + expect( + patchCode(moduleCompiledCode, envVarRuleCreator("TURBOPACK", "false")), + ).toMatchInlineSnapshot(` +""use strict"; +if (process.env.NEXT_RUNTIME === 'edge') { + module.exports = require('next/dist/server/route-modules/app-page/module.js'); +} else { + if (process.env.__NEXT_EXPERIMENTAL_REACT) { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.dev.js'); + } else if (false) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.prod.js'); + } + } else { + if (process.env.NODE_ENV === 'development') { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.dev.js'); + } else if (false) { + module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.prod.js'); + } else { + module.exports = require('next/dist/compiled/next-server/app-page.runtime.prod.js'); + } + } +} +"`); + }); +}); diff --git a/packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts similarity index 100% rename from packages/tests-unit/tests/build/patch/patchFetchCacheISR.test.ts rename to packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts diff --git a/packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts similarity index 100% rename from packages/tests-unit/tests/build/patch/patchFetchCacheWaitUntil.test.ts rename to packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts diff --git a/packages/tests-unit/tests/build/patch/patchNextServer.test.ts b/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts similarity index 100% rename from packages/tests-unit/tests/build/patch/patchNextServer.test.ts rename to packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts From d40bb77ff45040daabe0284fab21b55163f95f08 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 28 Mar 2025 12:02:54 +0100 Subject: [PATCH 5/9] lint --- packages/open-next/src/build/createServerBundle.ts | 6 +++--- .../tests/build/patch/patches/patchNextServer.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 7babec48..b1859fed 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -18,11 +18,11 @@ import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; import { - patchFetchCacheForISR, - patchUnstableCacheForISR, - patchNextServer, patchEnvVars, + patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, + patchNextServer, + patchUnstableCacheForISR, } from "./patch/patches/index.js"; interface CodeCustomization { diff --git a/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts b/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts index 6f4cfc98..877a063e 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts @@ -1,10 +1,10 @@ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; import { - removeMiddlewareManifestRule, - minimalRule, disablePreloadingRule, + minimalRule, + removeMiddlewareManifestRule, } from "@opennextjs/aws/build/patch/patches/patchNextServer.js"; -import { it, describe } from "vitest"; +import { describe, it } from "vitest"; const nextServerGetMiddlewareManifestCode = ` class NextNodeServer extends _baseserver.default { From 1217fa39eb3ae00caa584638813b635ea17b24ae Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 28 Mar 2025 17:53:22 +0100 Subject: [PATCH 6/9] changeset --- .changeset/warm-pans-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-pans-argue.md diff --git a/.changeset/warm-pans-argue.md b/.changeset/warm-pans-argue.md new file mode 100644 index 00000000..943307bb --- /dev/null +++ b/.changeset/warm-pans-argue.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +patch code to avoid useless import at runtime From 5b419073e9e8e3e76e7f6719eb0d256a07dd1552 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Tue, 1 Apr 2025 14:36:25 +0200 Subject: [PATCH 7/9] handle preloading --- .changeset/warm-pans-argue.md | 5 +- .../open-next/src/core/createMainHandler.ts | 2 + packages/open-next/src/core/requestHandler.ts | 1 + packages/open-next/src/core/util.ts | 47 ++++++++++++++++++- .../wrappers/aws-lambda-streaming.ts | 1 + packages/open-next/src/types/global.ts | 9 ++++ packages/open-next/src/types/open-next.ts | 17 ++++++- 7 files changed, 79 insertions(+), 3 deletions(-) diff --git a/.changeset/warm-pans-argue.md b/.changeset/warm-pans-argue.md index 943307bb..ad4c7688 100644 --- a/.changeset/warm-pans-argue.md +++ b/.changeset/warm-pans-argue.md @@ -2,4 +2,7 @@ "@opennextjs/aws": patch --- -patch code to avoid useless import at runtime +Some perf improvements : +- Eliminate unnecessary runtime imports. +- Refactor route preloading to be either on-demand or using waitUntil or at the start or during warmerEvent. +- Add a global function to preload routes when needed. \ No newline at end of file diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index 6ceed241..6814d15f 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -26,6 +26,8 @@ export async function createMainHandler() { globalThis.serverId = generateUniqueId(); globalThis.openNextConfig = config; + await globalThis.__next_route_preloader("start"); + // Default queue globalThis.queue = await resolveQueue(thisFunction.override?.queue); diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index e3b3833a..ffe95e4e 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -47,6 +47,7 @@ export async function openNextHandler( waitUntil: options?.waitUntil, }, async () => { + await globalThis.__next_route_preloader("waitUntil"); if (initialHeaders["x-forwarded-host"]) { initialHeaders.host = initialHeaders["x-forwarded-host"]; } diff --git a/packages/open-next/src/core/util.ts b/packages/open-next/src/core/util.ts index ab8a8db9..4b13b5be 100644 --- a/packages/open-next/src/core/util.ts +++ b/packages/open-next/src/core/util.ts @@ -6,7 +6,7 @@ import { // @ts-ignore import NextServer from "next/dist/server/next-server.js"; -import { debug } from "../adapters/logger.js"; +import { debug, error } from "../adapters/logger.js"; import { applyOverride as applyNextjsRequireHooksOverride, overrideHooks as overrideNextjsRequireHooks, @@ -59,6 +59,51 @@ const nextServer = new NextServer.default({ dir: __dirname, }); +let alreadyLoaded = false; + +globalThis.__next_route_preloader = async (stage) => { + if (alreadyLoaded) { + return; + } + const thisFunction = globalThis.fnName + ? globalThis.openNextConfig.functions![globalThis.fnName] + : globalThis.openNextConfig.default; + const routePreloadingBehavior = + thisFunction?.routePreloadingBehavior ?? "none"; + if (routePreloadingBehavior === "none") { + alreadyLoaded = true; + return; + } + if (!("unstable_preloadEntries" in nextServer)) { + debug( + "The current version of Next.js does not support route preloading. Skipping route preloading.", + ); + alreadyLoaded = true; + return; + } + if (stage === "waitUntil" && routePreloadingBehavior === "withWaitUntil") { + // We need to access the waitUntil + const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil; + if (!waitUntil) { + error( + "You've tried to use the 'withWaitUntil' route preloading behavior, but the 'waitUntil' function is not available.", + ); + } + debug("Preloading entries with waitUntil"); + waitUntil?.(nextServer.unstable_preloadEntries()); + alreadyLoaded = true; + } else if ( + (stage === "start" && routePreloadingBehavior === "onStart") || + (stage === "warmerEvent" && routePreloadingBehavior === "onWarmerEvent") || + stage === "onDemand" + ) { + const startTimestamp = Date.now(); + debug("Preloading entries"); + await nextServer.unstable_preloadEntries(); + debug("Preloading entries took", Date.now() - startTimestamp, "ms"); + alreadyLoaded = true; + } +}; // `getRequestHandlerWithMetadata` is not available in older versions of Next.js // It is required to for next 15.2 to pass metadata for page router data route export const requestHandler = (metadata: Record) => diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts index e8243d2a..d3e22e98 100644 --- a/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts @@ -35,6 +35,7 @@ const handler: WrapperHandler = async (handler, converter) => if ("type" in event) { const result = await formatWarmerResponse(event); responseStream.end(Buffer.from(JSON.stringify(result)), "utf-8"); + await globalThis.__next_route_preloader("warmerEvent"); return; } diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index 1231b527..d8669a54 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -211,4 +211,13 @@ declare global { * Defined in `createMainHandler` */ var cdnInvalidationHandler: CDNInvalidationHandler; + + /** + * A function to preload the routes. + * This needs to be defined on globalThis because it can be used by custom overrides. + * Only available in main functions. + */ + var __next_route_preloader: ( + stage: "waitUntil" | "start" | "warmerEvent" | "onDemand", + ) => Promise; } diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index f9e435a2..c993da4a 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -118,6 +118,12 @@ export interface ResolvedRoute { type: RouteType; } +export type RoutePreloadingBehavior = + | "none" + | "withWaitUntil" + | "onWarmerEvent" + | "onStart"; + export interface RoutingResult { internalEvent: InternalEvent; // If the request is an external rewrite, if used with an external middleware will be false on every server function @@ -170,7 +176,6 @@ export type IncludedWarmer = "aws-lambda" | "dummy"; export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy"; export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy"; - export interface DefaultOverrideOptions< E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult, @@ -313,6 +318,16 @@ export interface FunctionOptions extends DefaultFunctionOptions { * @deprecated This is not supported in 14.2+ */ experimentalBundledNextServer?: boolean; + + /** + * The route preloading behavior. Only supported in Next 15+. + * - "none" - No preloading of the route at all + * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none" + * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now + * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation and if not used properly, may increase the cold start time by a lot + * @default "none" + */ + routePreloadingBehavior?: RoutePreloadingBehavior; } export type RouteTemplate = From d71afec4a2f00f8a856c51f310b686c31c6eded1 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 7 Apr 2025 16:30:03 +0200 Subject: [PATCH 8/9] review --- .changeset/warm-pans-argue.md | 2 +- .../open-next/src/build/patch/patches/index.ts | 11 +++++++---- .../src/build/patch/patches/patchEnvVar.ts | 14 +++++++++++++- .../src/build/patch/patches/patchNextServer.ts | 5 +++++ packages/open-next/src/core/createMainHandler.ts | 1 + packages/open-next/src/core/util.ts | 14 ++++++++------ packages/open-next/src/types/open-next.ts | 16 ++++++++-------- 7 files changed, 43 insertions(+), 20 deletions(-) diff --git a/.changeset/warm-pans-argue.md b/.changeset/warm-pans-argue.md index ad4c7688..6d361f24 100644 --- a/.changeset/warm-pans-argue.md +++ b/.changeset/warm-pans-argue.md @@ -3,6 +3,6 @@ --- Some perf improvements : -- Eliminate unnecessary runtime imports. +- Eliminate unnecessary runtime imports (i.e. dev react dependencies and next precompiled dev or turbopack dependencies) - Refactor route preloading to be either on-demand or using waitUntil or at the start or during warmerEvent. - Add a global function to preload routes when needed. \ No newline at end of file diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts index dc9b28d2..861918b2 100644 --- a/packages/open-next/src/build/patch/patches/index.ts +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -1,4 +1,7 @@ -export * from "./patchEnvVar.js"; -export * from "./patchNextServer.js"; -export * from "./patchFetchCacheISR.js"; -export * from "./patchFetchCacheWaitUntil.js"; +export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js"; +export { patchNextServer } from "./patchNextServer.js"; +export { + patchFetchCacheForISR, + patchUnstableCacheForISR, +} from "./patchFetchCacheISR.js"; +export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js"; diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts index 33c126cc..1dfce38f 100644 --- a/packages/open-next/src/build/patch/patches/patchEnvVar.ts +++ b/packages/open-next/src/build/patch/patches/patchEnvVar.ts @@ -1,13 +1,22 @@ import { createPatchCode } from "../astCodePatcher.js"; import type { CodePatcher } from "../codePatcher"; +/** + * This function create an ast-grep rule to replace specific env var inside an if. + * This is used to avoid loading unnecessary deps at runtime + * @param envVar The env var that we want to replace + * @param value The value that we want to replace it with + * @returns + */ export const envVarRuleCreator = (envVar: string, value: string) => ` rule: kind: member_expression pattern: process.env.${envVar} inside: - kind: if_statement + kind: parenthesized_expression stopBy: end + inside: + kind: if_statement fix: '${value}' `; @@ -15,6 +24,7 @@ fix: export const patchEnvVars: CodePatcher = { name: "patch-env-vars", patches: [ + // This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime { versions: ">=15.0.0", field: { @@ -23,6 +33,7 @@ export const patchEnvVars: CodePatcher = { patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), }, }, + // This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime { versions: ">=15.0.0", field: { @@ -34,6 +45,7 @@ export const patchEnvVars: CodePatcher = { ), }, }, + // This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime { versions: ">=15.0.0", field: { diff --git a/packages/open-next/src/build/patch/patches/patchNextServer.ts b/packages/open-next/src/build/patch/patches/patchNextServer.ts index 0fc86cd9..f7a87bcf 100644 --- a/packages/open-next/src/build/patch/patches/patchNextServer.ts +++ b/packages/open-next/src/build/patch/patches/patchNextServer.ts @@ -1,6 +1,7 @@ import { createPatchCode } from "../astCodePatcher.js"; import type { CodePatcher } from "../codePatcher.js"; +// This rule will replace the `NEXT_MINIMAL` env variable with true in multiple places to avoid executing unwanted path (i.e. next middleware, edge functions and image optimization) export const minimalRule = ` rule: kind: member_expression @@ -29,6 +30,7 @@ fix: 'true' `; +// This rule will disable the background preloading of route done by NextServer by default during the creation of NextServer export const disablePreloadingRule = ` rule: kind: statement_block @@ -63,6 +65,7 @@ fix: export const patchNextServer: CodePatcher = { name: "patch-next-server", patches: [ + // Skip executing next middleware, edge functions and image optimization inside NextServer { versions: ">=15.0.0", field: { @@ -71,6 +74,7 @@ export const patchNextServer: CodePatcher = { patchCode: createPatchCode(minimalRule), }, }, + // Disable Next background preloading done at creation of `NetxServer` { versions: ">=15.0.0", field: { @@ -79,6 +83,7 @@ export const patchNextServer: CodePatcher = { patchCode: createPatchCode(disablePreloadingRule), }, }, + // Don't match edge functions in `NextServer` { versions: ">=15.0.0", field: { diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index 6814d15f..8fdc618f 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -26,6 +26,7 @@ export async function createMainHandler() { globalThis.serverId = generateUniqueId(); globalThis.openNextConfig = config; + // If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler. await globalThis.__next_route_preloader("start"); // Default queue diff --git a/packages/open-next/src/core/util.ts b/packages/open-next/src/core/util.ts index 4b13b5be..876710d2 100644 --- a/packages/open-next/src/core/util.ts +++ b/packages/open-next/src/core/util.ts @@ -59,10 +59,10 @@ const nextServer = new NextServer.default({ dir: __dirname, }); -let alreadyLoaded = false; +let routesLoaded = false; globalThis.__next_route_preloader = async (stage) => { - if (alreadyLoaded) { + if (routesLoaded) { return; } const thisFunction = globalThis.fnName @@ -71,14 +71,14 @@ globalThis.__next_route_preloader = async (stage) => { const routePreloadingBehavior = thisFunction?.routePreloadingBehavior ?? "none"; if (routePreloadingBehavior === "none") { - alreadyLoaded = true; + routesLoaded = true; return; } if (!("unstable_preloadEntries" in nextServer)) { debug( "The current version of Next.js does not support route preloading. Skipping route preloading.", ); - alreadyLoaded = true; + routesLoaded = true; return; } if (stage === "waitUntil" && routePreloadingBehavior === "withWaitUntil") { @@ -88,10 +88,12 @@ globalThis.__next_route_preloader = async (stage) => { error( "You've tried to use the 'withWaitUntil' route preloading behavior, but the 'waitUntil' function is not available.", ); + routesLoaded = true; + return; } debug("Preloading entries with waitUntil"); waitUntil?.(nextServer.unstable_preloadEntries()); - alreadyLoaded = true; + routesLoaded = true; } else if ( (stage === "start" && routePreloadingBehavior === "onStart") || (stage === "warmerEvent" && routePreloadingBehavior === "onWarmerEvent") || @@ -101,7 +103,7 @@ globalThis.__next_route_preloader = async (stage) => { debug("Preloading entries"); await nextServer.unstable_preloadEntries(); debug("Preloading entries took", Date.now() - startTimestamp, "ms"); - alreadyLoaded = true; + routesLoaded = true; } }; // `getRequestHandlerWithMetadata` is not available in older versions of Next.js diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index c993da4a..b9dcab05 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -118,6 +118,14 @@ export interface ResolvedRoute { type: RouteType; } +/** + * The route preloading behavior. Only supported in Next 15+. + * - "none" - No preloading of the route at all + * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none" + * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now + * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. + * @default "none" + */ export type RoutePreloadingBehavior = | "none" | "withWaitUntil" @@ -319,14 +327,6 @@ export interface FunctionOptions extends DefaultFunctionOptions { */ experimentalBundledNextServer?: boolean; - /** - * The route preloading behavior. Only supported in Next 15+. - * - "none" - No preloading of the route at all - * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none" - * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now - * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation and if not used properly, may increase the cold start time by a lot - * @default "none" - */ routePreloadingBehavior?: RoutePreloadingBehavior; } From 5b6d8d83a0f505ee49791416aab274e1f10292c3 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 7 Apr 2025 22:29:38 +0200 Subject: [PATCH 9/9] added some comment --- packages/open-next/src/build/patch/patches/patchEnvVar.ts | 2 +- packages/open-next/src/types/open-next.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts index 1dfce38f..96ee7fb2 100644 --- a/packages/open-next/src/build/patch/patches/patchEnvVar.ts +++ b/packages/open-next/src/build/patch/patches/patchEnvVar.ts @@ -2,7 +2,7 @@ import { createPatchCode } from "../astCodePatcher.js"; import type { CodePatcher } from "../codePatcher"; /** - * This function create an ast-grep rule to replace specific env var inside an if. + * Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements * This is used to avoid loading unnecessary deps at runtime * @param envVar The env var that we want to replace * @param value The value that we want to replace it with diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index b9dcab05..e307912d 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -120,10 +120,11 @@ export interface ResolvedRoute { /** * The route preloading behavior. Only supported in Next 15+. - * - "none" - No preloading of the route at all - * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none" + * Default behavior of Next is disabled. You should do your own testing to choose which one suits you best + * - "none" - No preloading of the route at all. This is the default + * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none". At the moment only cloudflare wrappers provide a `waitUntil` * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now - * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. + * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. Useful for long running server or in serverless with some careful testing * @default "none" */ export type RoutePreloadingBehavior =