diff --git a/packages/open-next/src/adapter.ts b/packages/open-next/src/adapter.ts new file mode 100644 index 000000000..e1ec0e748 --- /dev/null +++ b/packages/open-next/src/adapter.ts @@ -0,0 +1,146 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import type { NextConfig } from "types/next-types"; +import { compileCache } from "./build/compileCache.js"; +import { compileOpenNextConfig } from "./build/compileConfig.js"; +import { compileTagCacheProvider } from "./build/compileTagCacheProvider.js"; +import { createCacheAssets, createStaticAssets } from "./build/createAssets.js"; +import { createImageOptimizationBundle } from "./build/createImageOptimizationBundle.js"; +import { createMiddleware } from "./build/createMiddleware.js"; +import { createRevalidationBundle } from "./build/createRevalidationBundle.js"; +import { createServerBundle } from "./build/createServerBundle.js"; +import { createWarmerBundle } from "./build/createWarmerBundle.js"; +import { generateOutput } from "./build/generateOutput.js"; +import * as buildHelper from "./build/helper.js"; +import { addDebugFile } from "./debug.js"; +import type { ContentUpdater } from "./plugins/content-updater.js"; +import { + externalChunksPlugin, + inlineRouteHandler, +} from "./plugins/inlineRouteHandlers.js"; + +export type NextAdapterOutputs = { + pages: any[]; + pagesApi: any[]; + appPages: any[]; + appRoutes: any[]; +}; + +type NextAdapter = { + name: string; + modifyConfig: ( + config: NextConfig, + { phase }: { phase: string }, + ) => Promise; + onBuildComplete: (props: { + routes: any; + outputs: NextAdapterOutputs; + projectDir: string; + repoRoot: string; + distDir: string; + config: NextConfig; + nextVersion: string; + }) => Promise; +}; //TODO: use the one provided by Next + +let buildOpts: buildHelper.BuildOptions; + +export default { + name: "OpenNext", + async modifyConfig(nextConfig, { phase }) { + // We have to precompile the cache here, probably compile OpenNext config as well + const { config, buildDir } = await compileOpenNextConfig( + "open-next.config.ts", + { nodeExternals: undefined }, + ); + + const require = createRequire(import.meta.url); + const openNextDistDir = path.dirname( + require.resolve("@opennextjs/aws/index.js"), + ); + + buildOpts = buildHelper.normalizeOptions(config, openNextDistDir, buildDir); + + buildHelper.initOutputDir(buildOpts); + + const cache = compileCache(buildOpts); + + // We then have to copy the cache files to the .next dir so that they are available at runtime + //TODO: use a better path, this one is temporary just to make it work + const tempCachePath = `${buildOpts.outputDir}/server-functions/default/.open-next/.build`; + fs.mkdirSync(tempCachePath, { recursive: true }); + fs.copyFileSync(cache.cache, path.join(tempCachePath, "cache.cjs")); + fs.copyFileSync( + cache.composableCache, + path.join(tempCachePath, "composable-cache.cjs"), + ); + + //TODO: We should check the version of Next here, below 16 we'd throw or show a warning + return { + ...nextConfig, + cacheHandler: cache.cache, //TODO: compute that here, + cacheMaxMemorySize: 0, + experimental: { + ...nextConfig.experimental, + trustHostHeader: true, + cacheHandlers: { + default: cache.composableCache, + }, + }, + }; + }, + async onBuildComplete(outputs) { + console.log("OpenNext build will start now"); + + // TODO(vicb): save outputs + addDebugFile(buildOpts, "outputs.json", outputs); + + // Compile middleware + await createMiddleware(buildOpts); + console.log("Middleware created"); + + createStaticAssets(buildOpts); + console.log("Static assets created"); + + if (buildOpts.config.dangerous?.disableIncrementalCache !== true) { + const { useTagCache } = createCacheAssets(buildOpts); + console.log("Cache assets created"); + if (useTagCache) { + await compileTagCacheProvider(buildOpts); + console.log("Tag cache provider compiled"); + } + } + + await createServerBundle( + buildOpts, + { + additionalPlugins: getAdditionalPluginsFactory( + buildOpts, + outputs.outputs, + ), + }, + outputs.outputs, + ); + + console.log("Server bundle created"); + await createRevalidationBundle(buildOpts); + console.log("Revalidation bundle created"); + await createImageOptimizationBundle(buildOpts); + console.log("Image optimization bundle created"); + await createWarmerBundle(buildOpts); + console.log("Warmer bundle created"); + await generateOutput(buildOpts); + console.log("Output generated"); + }, +} satisfies NextAdapter; + +function getAdditionalPluginsFactory( + buildOpts: buildHelper.BuildOptions, + outputs: NextAdapterOutputs, +) { + return (updater: ContentUpdater) => [ + inlineRouteHandler(updater, outputs), + externalChunksPlugin(outputs), + ]; +} diff --git a/packages/open-next/src/build/compileConfig.ts b/packages/open-next/src/build/compileConfig.ts index 9b5647ff5..3feb7ccb7 100644 --- a/packages/open-next/src/build/compileConfig.ts +++ b/packages/open-next/src/build/compileConfig.ts @@ -23,6 +23,7 @@ export async function compileOpenNextConfig( { nodeExternals = "", compileEdge = false } = {}, ) { const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-next-tmp")); + let configPath = compileOpenNextConfigNode( openNextConfigPath, buildDir, diff --git a/packages/open-next/src/build/copyAdapterFiles.ts b/packages/open-next/src/build/copyAdapterFiles.ts new file mode 100644 index 000000000..aec0b0172 --- /dev/null +++ b/packages/open-next/src/build/copyAdapterFiles.ts @@ -0,0 +1,76 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { NextAdapterOutputs } from "../adapter"; +import { addDebugFile } from "../debug.js"; +import type * as buildHelper from "./helper.js"; + +export async function copyAdapterFiles( + options: buildHelper.BuildOptions, + fnName: string, + outputs: NextAdapterOutputs, +) { + const filesToCopy = new Map(); + + // Copying the files from outputs to the output dir + for (const [key, value] of Object.entries(outputs)) { + if (["pages", "pagesApi", "appPages", "appRoutes"].includes(key)) { + for (const route of value as any[]) { + const assets = route.assets; + // We need to copy the filepaths to the output dir + const relativeFilePath = path.relative(options.appPath, route.filePath); + // console.log( + // "route.filePath", + // route.filePath, + // "relativeFilePath", + // relativeFilePath, + // ); + filesToCopy.set( + route.filePath, + `${options.outputDir}/server-functions/${fnName}/${relativeFilePath}`, + ); + + for (const [relative, from] of Object.entries(assets || {})) { + // console.log("route.assets", from, relative); + filesToCopy.set( + from as string, + `${options.outputDir}/server-functions/${fnName}/${relative}`, + ); + } + // copyFileSync(from, `${options.outputDir}/${relative}`); + } + } + } + + console.log("\n### Copying adapter files"); + const debugCopiedFiles: Record = {}; + for (const [from, to] of filesToCopy) { + debugCopiedFiles[from] = to; + + //make sure the directory exists first + fs.mkdirSync(path.dirname(to), { recursive: true }); + // For pnpm symlink we need to do that + // see https://github.com/vercel/next.js/blob/498f342b3552d6fc6f1566a1cc5acea324ce0dec/packages/next/src/build/utils.ts#L1932 + let symlink = ""; + try { + symlink = fs.readlinkSync(from); + } catch (e) { + //Ignore + } + if (symlink) { + try { + fs.symlinkSync(symlink, to); + } catch (e: any) { + if (e.code !== "EEXIST") { + throw e; + } + } + } else { + fs.copyFileSync(from, to); + } + } + + // TODO(vicb): debug + addDebugFile(options, "copied_files.json", debugCopiedFiles); + + return Array.from(filesToCopy.values()); +} diff --git a/packages/open-next/src/build/createAssets.ts b/packages/open-next/src/build/createAssets.ts index 027b8f8a7..3f3c57d08 100644 --- a/packages/open-next/src/build/createAssets.ts +++ b/packages/open-next/src/build/createAssets.ts @@ -89,7 +89,7 @@ export function createCacheAssets(options: buildHelper.BuildOptions) { const dotNextPath = path.join( appBuildOutputPath, - ".next/standalone", + options.config.dangerous?.useAdapterOutputs ? "" : ".next/standalone", packagePath, ); diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 743383c29..ae495dbdf 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -3,8 +3,10 @@ import path from "node:path"; import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next"; +import { createRequire } from "node:module"; import { loadMiddlewareManifest } from "config/util.js"; import type { Plugin } from "esbuild"; +import type { NextAdapterOutputs } from "../adapter.js"; import logger from "../logger.js"; import { minifyAll } from "../minimize-js.js"; import { ContentUpdater } from "../plugins/content-updater.js"; @@ -13,6 +15,7 @@ import { openNextResolvePlugin } from "../plugins/resolve.js"; import { getCrossPlatformPathRegex } from "../utils/regex.js"; import { bundleNextServer } from "./bundleNextServer.js"; import { compileCache } from "./compileCache.js"; +import { copyAdapterFiles } from "./copyAdapterFiles.js"; import { copyTracedFiles } from "./copyTracedFiles.js"; import { copyMiddlewareResources, @@ -22,6 +25,7 @@ import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; import * as patches from "./patch/patches/index.js"; +const require = createRequire(import.meta.url); interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -34,6 +38,7 @@ interface CodeCustomization { export async function createServerBundle( options: buildHelper.BuildOptions, codeCustomization?: CodeCustomization, + nextOutputs?: NextAdapterOutputs, ) { const { config } = options; const foundRoutes = new Set(); @@ -55,7 +60,13 @@ export async function createServerBundle( if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, options, fnOptions, codeCustomization); + await generateBundle( + name, + options, + fnOptions, + codeCustomization, + nextOutputs, + ); } }); @@ -108,12 +119,18 @@ export async function createServerBundle( } // Generate default function - await generateBundle("default", options, { - ...defaultFn, - // @ts-expect-error - Those string are RouteTemplate - routes: Array.from(remainingRoutes), - patterns: ["*"], - }); + await generateBundle( + "default", + options, + { + ...defaultFn, + // @ts-expect-error - Those string are RouteTemplate + routes: Array.from(remainingRoutes), + patterns: ["*"], + }, + codeCustomization, + nextOutputs, + ); } async function generateBundle( @@ -121,6 +138,7 @@ async function generateBundle( options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, codeCustomization?: CodeCustomization, + nextOutputs?: NextAdapterOutputs, ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; @@ -187,14 +205,25 @@ async function generateBundle( // Copy env files buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); + let tracedFiles: string[] = []; + let manifests: any = {}; + // Copy all necessary traced files - const { tracedFiles, manifests } = await copyTracedFiles({ - buildOutputPath: appBuildOutputPath, - packagePath, - outputDir: outputPath, - routes: fnOptions.routes ?? ["app/page.tsx"], - bundledNextServer: isBundled, - }); + if (config.dangerous?.useAdapterOutputs) { + tracedFiles = await copyAdapterFiles(options, name, nextOutputs!); + //TODO: we should load manifests here + } else { + const oldTracedFileOutput = await copyTracedFiles({ + buildOutputPath: appBuildOutputPath, + packagePath, + outputDir: outputPath, + routes: fnOptions.routes ?? ["app/page.tsx"], + bundledNextServer: isBundled, + skipServerFiles: options.config.dangerous?.useAdapterOutputs === true, + }); + tracedFiles = oldTracedFileOutput.tracedFiles; + manifests = oldTracedFileOutput.manifests; + } const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; @@ -250,6 +279,8 @@ async function generateBundle( "15.4.0", ); + const useAdapterHandler = config.dangerous?.useAdapterOutputs === true; + const disableRouting = isBefore13413 || config.middleware?.external; const updater = new ContentUpdater(options); @@ -268,6 +299,7 @@ async function generateBundle( ...(isAfter142 ? ["patchAsyncStorage"] : []), ...(isAfter141 ? ["appendPrefetch"] : []), ...(isAfter154 ? [] : ["setInitialURL"]), + ...(useAdapterHandler ? ["useRequestHandler"] : ["useAdapterHandler"]), ], }), openNextReplacementPlugin({ @@ -281,6 +313,8 @@ async function generateBundle( : ["stableIncrementalCache"]), ...(isAfter152 ? [] : ["composableCache"]), ], + replacements: [require.resolve("../core/util.adapter.js")], + entireFile: useAdapterHandler, }), openNextResolvePlugin({ @@ -309,6 +343,7 @@ async function generateBundle( "const require = topLevelCreateRequire(import.meta.url);", "import bannerUrl from 'url';", "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + "const __filename = bannerUrl.fileURLToPath(import.meta.url);", name === "default" ? "" : `globalThis.fnName = "${name}";`, ].join(""), }, diff --git a/packages/open-next/src/build/helper.ts b/packages/open-next/src/build/helper.ts index 030b309cd..342d84190 100644 --- a/packages/open-next/src/build/helper.ts +++ b/packages/open-next/src/build/helper.ts @@ -148,7 +148,8 @@ export async function esbuildAsync( format: "esm", platform: "node", bundle: true, - minify, + // TODO(vicb): revert to `minify,` + minify: false, metafile, mainFields: ["module", "main"], sourcemap: debug ? "inline" : false, diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index b66eda577..b3857b0b9 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -14,6 +14,7 @@ import { NextConfig } from "config/index"; import type { OpenNextHandlerOptions } from "types/overrides"; import { debug, error } from "../adapters/logger"; import { patchAsyncStorage } from "./patchAsyncStorage"; +import { adapterHandler } from "./routing/adapterHandler"; import { constructNextUrl, convertRes, @@ -208,7 +209,15 @@ export async function openNextHandler( options?.streamCreator, ); + //#override useAdapterHandler + await adapterHandler(req, res, routingResult, { + waitUntil: options?.waitUntil, + }); + //#endOverride + + //#override useRequestHandler await processRequest(req, res, routingResult); + //#endOverride const { statusCode, @@ -254,6 +263,7 @@ async function processRequest( } else if (routingResult.internalEvent.rawPath === "/404") { invokeStatus = 404; } + const requestMetadata = { isNextDataReq: routingResult.internalEvent.query.__nextDataReq === "1", initURL: routingResult.initialURL, @@ -311,11 +321,12 @@ async function handleNoFallbackError( return; } try { - await requestHandler({ - ...routingResult, - invokeOutput: routingResult.resolvedRoutes[index].route, - ...metadata, - })(req, res); + // await requestHandler({ + // ...routingResult, + // invokeOutput: routingResult.resolvedRoutes[index].route, + // ...metadata, + // })(req, res); + //TODO: find a way to do that without breaking current main } catch (e: any) { if (e.constructor.name === "NoFallbackError") { await handleNoFallbackError(req, res, routingResult, metadata, index + 1); @@ -346,7 +357,7 @@ async function tryRenderError( invokeStatus: type === "404" ? 404 : 500, middlewareInvoke: false, }; - await requestHandler(requestMetadata)(_req, res); + // await requestHandler(requestMetadata)(_req, res); } catch (e) { error("NextJS request failed.", e); res.statusCode = 500; diff --git a/packages/open-next/src/core/routing/adapterHandler.ts b/packages/open-next/src/core/routing/adapterHandler.ts new file mode 100644 index 000000000..c6c0125aa --- /dev/null +++ b/packages/open-next/src/core/routing/adapterHandler.ts @@ -0,0 +1,55 @@ +import type { IncomingMessage } from "node:http"; +import { finished } from "node:stream/promises"; +import type { OpenNextNodeResponse } from "http/index"; +import type { ResolvedRoute, RoutingResult, WaitUntil } from "types/open-next"; + +/** + * This function loads the necessary routes, and invoke the expected handler. + * @param routingResult The result of the routing process, containing information about the matched route and any parameters. + */ +export async function adapterHandler( + req: IncomingMessage, + res: OpenNextNodeResponse, + routingResult: RoutingResult, + options: { + waitUntil?: WaitUntil; + } = {}, +) { + let resolved = false; + + //TODO: replace this at runtime with a version precompiled for the cloudflare adapter. + for (const route of routingResult.resolvedRoutes) { + const module = getHandler(route); + if (!module || resolved) { + return; + } + + try { + console.log("## adapterHandler trying route", route, req.url); + const result = await module.handler(req, res, { + waitUntil: options.waitUntil, + }); + await finished(res); // Not sure this one is necessary. + console.log("## adapterHandler route succeeded", route); + resolved = true; + return result; + //If it doesn't throw, we are done + } catch (e) { + console.log("## adapterHandler route failed", route, e); + // I'll have to run some more tests, but in theory, we should not have anything special to do here, and we should return the 500 page here. + } + } +} + +// Body replaced at build time +function getHandler(route: ResolvedRoute): + | undefined + | { + handler: ( + req: IncomingMessage, + res: OpenNextNodeResponse, + options: { waitUntil?: (promise: Promise) => void }, + ) => Promise; + } { + return undefined; +} diff --git a/packages/open-next/src/core/util.adapter.ts b/packages/open-next/src/core/util.adapter.ts new file mode 100644 index 000000000..238072e6b --- /dev/null +++ b/packages/open-next/src/core/util.adapter.ts @@ -0,0 +1,11 @@ +//This file is the one used instead of util.ts when using the adapter API from Next.js +import { adapterHandler } from "./routing/adapterHandler"; + +globalThis.__next_route_preloader = async (stage: string) => { + // TODO: Implement route preloading logic here +}; + +export const requestHandler = adapterHandler; + +// NOOP for adapter +export function setNextjsPrebundledReact(rawPath: string) {} diff --git a/packages/open-next/src/debug.ts b/packages/open-next/src/debug.ts new file mode 100644 index 000000000..267ffa3ab --- /dev/null +++ b/packages/open-next/src/debug.ts @@ -0,0 +1,19 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { BuildOptions } from "./build/helper"; + +let init = false; + +export function addDebugFile( + options: BuildOptions, + name: string, + content: unknown, +) { + if (!init) { + fs.mkdirSync(path.join(options.outputDir, ".debug"), { recursive: true }); + init = true; + } + const strContent = + typeof content === "string" ? content : JSON.stringify(content, null, 2); + fs.writeFileSync(path.join(options.outputDir, ".debug", name), strContent); +} diff --git a/packages/open-next/src/plugins/inlineRouteHandlers.ts b/packages/open-next/src/plugins/inlineRouteHandlers.ts new file mode 100644 index 000000000..ddd42bbb3 --- /dev/null +++ b/packages/open-next/src/plugins/inlineRouteHandlers.ts @@ -0,0 +1,127 @@ +import { getCrossPlatformPathRegex } from "utils/regex.js"; +import type { NextAdapterOutputs } from "../adapter.js"; +import { patchCode } from "../build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "./content-updater.js"; + +export function inlineRouteHandler( + updater: ContentUpdater, + outputs: NextAdapterOutputs, +): Plugin { + console.log("## inlineRouteHandler"); + return updater.updateContent("inlineRouteHandler", [ + // This one will inline the route handlers into the adapterHandler's getHandler function. + { + filter: getCrossPlatformPathRegex( + String.raw`core/routing/adapterHandler\.js$`, + { + escape: false, + }, + ), + contentFilter: /getHandler/, + callback: ({ contents }) => patchCode(contents, inlineRule(outputs)), + }, + // For turbopack, we need to also patch the `[turbopack]_runtime.js` file. + { + filter: getCrossPlatformPathRegex( + String.raw`\[turbopack\]_runtime\.js$`, + { + escape: false, + }, + ), + contentFilter: /loadRuntimeChunkPath/, + callback: ({ contents }) => { + const result = patchCode(contents, inlineChunksRule); + //TODO: Maybe find another way to do that. + return `${result}\n${inlineChunksFn(outputs)}`; + }, + }, + ]); +} + +function inlineRule(outputs: NextAdapterOutputs) { + const routeToHandlerPath: Record = {}; + + for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { + for (const { pathname, filePath } of outputs[type]) { + routeToHandlerPath[pathname] = filePath; + } + } + + return ` +rule: + pattern: "function getHandler($ROUTE) { $$$BODY }" +fix: |- + function getHandler($ROUTE) { + switch($ROUTE.route) { +${Object.entries(routeToHandlerPath) + .map(([route, file]) => ` case "${route}": return require("${file}");`) + .join("\n")} + default: + throw new Error(\`Not found \${$ROUTE.route}\`); + } + + }`; +} + +//TODO: Make this one more resilient to code changes +const inlineChunksRule = ` +rule: + kind: call_expression + pattern: require(resolved) +fix: + requireChunk(chunkPath) +`; + +function getInlinableChunks(outputs: NextAdapterOutputs, prefix?: string) { + const chunks = new Set(); + for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { + for (const { assets } of outputs[type]) { + for (const asset of Object.keys(assets)) { + if ( + asset.includes(".next/server/chunks/") && + !asset.includes("[turbopack]_runtime.js") + ) { + chunks.add(prefix ? `${prefix}${asset}` : asset); + } + } + } + } + return chunks; +} + +function inlineChunksFn(outputs: NextAdapterOutputs) { + // From the outputs, we extract every chunks + const chunks = getInlinableChunks(outputs); + return ` + function requireChunk(chunk) { + const chunkPath = ".next/" + chunk; + switch(chunkPath) { +${Array.from(chunks) + .map((chunk) => ` case "${chunk}": return require("./${chunk}");`) + .join("\n")} + default: + throw new Error(\`Not found \${chunkPath}\`); + } + } +`; +} + +/** + * Esbuild plugin to mark all chunks that we inline as external. + */ +export function externalChunksPlugin(outputs: NextAdapterOutputs): Plugin { + const chunks = getInlinableChunks(outputs, "./"); + return { + name: "external-chunks", + setup(build) { + build.onResolve({ filter: /\/chunks\// }, (args) => { + if (chunks.has(args.path)) { + return { + path: args.path, + external: true, + }; + } + }); + }, + }; +} diff --git a/packages/open-next/src/plugins/replacement.ts b/packages/open-next/src/plugins/replacement.ts index 7e6a771e9..55fdfa272 100644 --- a/packages/open-next/src/plugins/replacement.ts +++ b/packages/open-next/src/plugins/replacement.ts @@ -10,6 +10,7 @@ export interface IPluginSettings { replacements?: string[]; deletes?: string[]; name?: string; + entireFile?: boolean; } const overridePattern = /\/\/#override (\w+)\n([\s\S]*?)\n\/\/#endOverride/gm; @@ -47,6 +48,8 @@ const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm; * @param opts.replacements - list of files used to replace the imports/overrides in the target * - the path is absolute * @param opts.deletes - list of ids to delete from the target + * @param opts.entireFile - whether to replace the entire file or just specific blocks. + * @param opts.name - name of the plugin * @returns */ export function openNextReplacementPlugin({ @@ -54,11 +57,22 @@ export function openNextReplacementPlugin({ replacements, deletes, name, + entireFile, }: IPluginSettings): Plugin { return { name: name ?? "opennext", setup(build) { build.onLoad({ filter: target }, async (args) => { + if (entireFile) { + if (replacements?.length !== 1) { + throw new Error( + "When using entireFile option, exactly one replacement file must be provided", + ); + } + const contents = await readFile(replacements[0], "utf-8"); + return { contents }; + } + let contents = await readFile(args.path, "utf-8"); await Promise.all([ diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index aa531e31e..9241cd50c 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -109,6 +109,14 @@ export interface DangerousOptions { * @default false */ middlewareHeadersOverrideNextConfigHeaders?: boolean; + + /** + * Whether to use the outputs from Next.js adapter API. + * This is a very experimental feature as it may not be stabilized in Next.js yet. + * This will be the default in the future once Next.js adapter API is stabilized. + * @default false + */ + useAdapterOutputs?: boolean; } export type BaseOverride = {