Skip to content

Commit 509eca8

Browse files
committed
support for external middleware
1 parent d7ee73f commit 509eca8

File tree

8 files changed

+187
-42
lines changed

8 files changed

+187
-42
lines changed

packages/open-next/src/build/compileConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function compileOpenNextConfig(
5050
// We need to check if the config uses the edge runtime at any point
5151
// If it does, we need to compile it with the edge runtime
5252
const usesEdgeRuntime =
53-
config.middleware?.external ||
53+
(config.middleware?.external && config.middleware.runtime !== "node") ||
5454
Object.values(config.functions || {}).some((fn) => fn.runtime === "edge");
5555
if (!usesEdgeRuntime) {
5656
logger.debug(

packages/open-next/src/build/copyTracedFiles.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function copyTracedFiles(
3131
outputDir: string,
3232
routes: string[],
3333
bundledNextServer: boolean,
34+
skipServerFiles = false,
3435
) {
3536
const tsStart = Date.now();
3637
const dotNextDir = path.join(buildOutputPath, ".next");
@@ -58,10 +59,11 @@ export async function copyTracedFiles(
5859
const filesToCopy = new Map<string, string>();
5960

6061
// Files necessary by the server
61-
extractFiles(requiredServerFiles.files).forEach((f) => {
62-
filesToCopy.set(f, f.replace(standaloneDir, outputDir));
63-
});
64-
62+
if (!skipServerFiles) {
63+
extractFiles(requiredServerFiles.files).forEach((f) => {
64+
filesToCopy.set(f, f.replace(standaloneDir, outputDir));
65+
});
66+
}
6567
// create directory for pages
6668
if (existsSync(path.join(standaloneDir, ".next/server/pages"))) {
6769
mkdirSync(path.join(outputNextDir, "server/pages"), {

packages/open-next/src/build/createMiddleware.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import type {
1111
import { buildEdgeBundle } from "./edge/createEdgeBundle.js";
1212
import * as buildHelper from "./helper.js";
1313
import { installDependencies } from "./installDeps.js";
14+
import {
15+
buildBundledNodeMiddleware,
16+
buildExternalNodeMiddleware,
17+
} from "./middleware/buildNodeMiddleware.js";
1418

1519
/**
1620
* Compiles the middleware bundle.
@@ -52,24 +56,15 @@ export async function createMiddleware(
5256

5357
// TODO: Handle external node middleware in the future
5458
if (functionsConfigManifest?.functions["/_middleware"]) {
55-
// If we are here, it means that we are using a node middleware
56-
await buildHelper.esbuildAsync(
57-
{
58-
entryPoints: [
59-
path.join(
60-
options.openNextDistDir,
61-
"core",
62-
"nodeMiddlewareHandler.js",
63-
),
64-
],
65-
external: ["./.next/*"],
66-
outfile: path.join(options.buildDir, "middleware.mjs"),
67-
bundle: true,
68-
platform: "node",
69-
},
70-
options,
71-
);
72-
// We return early to not build the edge middleware
59+
if (!config.middleware?.external) {
60+
// If we are here, it means that we are using a node middleware
61+
await buildBundledNodeMiddleware(options);
62+
// We return early to not build the edge middleware
63+
return;
64+
}
65+
66+
// Here it means that we are using a node external middleware
67+
await buildExternalNodeMiddleware(options);
7368
return;
7469
}
7570
}

packages/open-next/src/build/edge/createEdgeBundle.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { openNextResolvePlugin } from "../../plugins/resolve.js";
2121
import { getCrossPlatformPathRegex } from "../../utils/regex.js";
2222
import { type BuildOptions, isEdgeRuntime } from "../helper.js";
2323
import { copyOpenNextConfig, esbuildAsync } from "../helper.js";
24+
import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
2425

2526
type Override = OverrideOptions & {
2627
originResolver?: LazyLoadedOverride<OriginResolver> | IncludedOriginResolver;
@@ -88,14 +89,12 @@ export async function buildEdgeBundle({
8889
target: getCrossPlatformPathRegex("adapters/middleware.js"),
8990
deletes: includeCache ? [] : ["includeCacheInMiddleware"],
9091
}),
92+
openNextExternalMiddlewarePlugin(
93+
path.join(options.openNextDistDir, "core", "edgeFunctionHandler.js"),
94+
),
9195
openNextEdgePlugins({
9296
middlewareInfo,
9397
nextDir: path.join(options.appBuildOutputPath, ".next"),
94-
edgeFunctionHandlerPath: path.join(
95-
options.openNextDistDir,
96-
"core",
97-
"edgeFunctionHandler.js",
98-
),
9998
isInCloudfare,
10099
}),
101100
],
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
import * as buildHelper from "../helper.js";
5+
import type {
6+
IncludedOriginResolver,
7+
LazyLoadedOverride,
8+
OverrideOptions,
9+
} from "types/open-next.js";
10+
import type { OriginResolver } from "types/overrides.js";
11+
import { openNextResolvePlugin } from "../../plugins/resolve.js";
12+
import { openNextReplacementPlugin } from "../../plugins/replacement.js";
13+
import { getCrossPlatformPathRegex } from "utils/regex.js";
14+
import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
15+
import { installDependencies } from "../installDeps.js";
16+
import { copyTracedFiles } from "../copyTracedFiles.js";
17+
18+
type Override = OverrideOptions & {
19+
originResolver?: LazyLoadedOverride<OriginResolver> | IncludedOriginResolver;
20+
};
21+
22+
export async function buildExternalNodeMiddleware(
23+
options: buildHelper.BuildOptions,
24+
) {
25+
const { appBuildOutputPath, config, outputDir } = options;
26+
if (!config.middleware?.external) {
27+
throw new Error(
28+
"This function should only be called for external middleware",
29+
);
30+
}
31+
const outputPath = path.join(outputDir, "middleware");
32+
fs.mkdirSync(outputPath, { recursive: true });
33+
34+
// Copy open-next.config.mjs
35+
buildHelper.copyOpenNextConfig(
36+
options.buildDir,
37+
outputPath,
38+
await buildHelper.isEdgeRuntime(config.middleware.override),
39+
);
40+
const overrides = {
41+
...config.middleware.override,
42+
originResolver: config.middleware.originResolver,
43+
};
44+
const includeCache = config.dangerous?.enableCacheInterception;
45+
const packagePath = buildHelper.getPackagePath(options);
46+
47+
// TODO: change this so that we don't copy unnecessary files
48+
await copyTracedFiles(
49+
appBuildOutputPath,
50+
packagePath,
51+
outputPath,
52+
[],
53+
false,
54+
true,
55+
);
56+
57+
function override<T extends keyof Override>(target: T) {
58+
return typeof overrides?.[target] === "string"
59+
? overrides[target]
60+
: undefined;
61+
}
62+
63+
// Bundle middleware
64+
await buildHelper.esbuildAsync(
65+
{
66+
entryPoints: [
67+
path.join(options.openNextDistDir, "adapters", "middleware.js"),
68+
],
69+
outfile: path.join(outputPath, "handler.mjs"),
70+
external: ["./.next/*"],
71+
platform: "node",
72+
plugins: [
73+
openNextResolvePlugin({
74+
overrides: {
75+
wrapper: override("wrapper") ?? "aws-lambda",
76+
converter: override("converter") ?? "aws-cloudfront",
77+
...(includeCache
78+
? {
79+
tagCache: override("tagCache") ?? "dynamodb-lite",
80+
incrementalCache: override("incrementalCache") ?? "s3-lite",
81+
queue: override("queue") ?? "sqs-lite",
82+
}
83+
: {}),
84+
originResolver: override("originResolver") ?? "pattern-env",
85+
proxyExternalRequest: override("proxyExternalRequest") ?? "node",
86+
},
87+
fnName: "middleware",
88+
}),
89+
openNextReplacementPlugin({
90+
name: "externalMiddlewareOverrides",
91+
target: getCrossPlatformPathRegex("adapters/middleware.js"),
92+
deletes: includeCache ? [] : ["includeCacheInMiddleware"],
93+
}),
94+
openNextExternalMiddlewarePlugin(
95+
path.join(
96+
options.openNextDistDir,
97+
"core",
98+
"nodeMiddlewareHandler.js",
99+
),
100+
),
101+
],
102+
banner: {
103+
js: [
104+
`globalThis.monorepoPackagePath = "${packagePath}";`,
105+
"import process from 'node:process';",
106+
"import { Buffer } from 'node:buffer';",
107+
"import {AsyncLocalStorage} from 'node:async_hooks';",
108+
"import { createRequire as topLevelCreateRequire } from 'module';",
109+
"const require = topLevelCreateRequire(import.meta.url);",
110+
"import bannerUrl from 'url';",
111+
"const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));",
112+
].join(""),
113+
},
114+
},
115+
options,
116+
);
117+
118+
// Do we need to copy or do something with env file here?
119+
120+
installDependencies(outputPath, config.middleware?.install);
121+
}
122+
123+
export async function buildBundledNodeMiddleware(
124+
options: buildHelper.BuildOptions,
125+
) {
126+
await buildHelper.esbuildAsync(
127+
{
128+
entryPoints: [
129+
path.join(options.openNextDistDir, "core", "nodeMiddlewareHandler.js"),
130+
],
131+
external: ["./.next/*"],
132+
outfile: path.join(options.buildDir, "middleware.mjs"),
133+
bundle: true,
134+
platform: "node",
135+
},
136+
options,
137+
);
138+
}

packages/open-next/src/plugins/edge.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,18 @@ import { getCrossPlatformPathRegex } from "../utils/regex.js";
2222

2323
export interface IPluginSettings {
2424
nextDir: string;
25-
edgeFunctionHandlerPath?: string;
2625
middlewareInfo?: MiddlewareInfo;
2726
isInCloudfare?: boolean;
2827
}
2928

3029
/**
3130
* @param opts.nextDir - The path to the .next directory
32-
* @param opts.edgeFunctionHandlerPath - The path to the edgeFunctionHandler.js file that we'll use to bundle the routing
3331
* @param opts.middlewareInfo - Information about the middleware
3432
* @param opts.isInCloudfare - Whether the code runs on the cloudflare runtime
3533
* @returns
3634
*/
3735
export function openNextEdgePlugins({
3836
nextDir,
39-
edgeFunctionHandlerPath,
4037
middlewareInfo,
4138
isInCloudfare,
4239
}: IPluginSettings): Plugin {
@@ -57,17 +54,6 @@ export function openNextEdgePlugins({
5754
name: "opennext-edge",
5855
setup(build) {
5956
logger.debug(chalk.blue("OpenNext Edge plugin"));
60-
if (edgeFunctionHandlerPath) {
61-
// If we bundle the routing, we need to resolve the middleware
62-
build.onResolve(
63-
{ filter: getCrossPlatformPathRegex("./middleware.mjs") },
64-
() => {
65-
return {
66-
path: edgeFunctionHandlerPath,
67-
};
68-
},
69-
);
70-
}
7157

7258
build.onResolve({ filter: /\.(mjs|wasm)$/g }, () => {
7359
return {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Plugin } from "esbuild";
2+
import { getCrossPlatformPathRegex } from "utils/regex.js";
3+
4+
export function openNextExternalMiddlewarePlugin(functionPath: string): Plugin {
5+
return {
6+
name: "open-next-external-node-middleware",
7+
setup(build) {
8+
// If we bundle the routing, we need to resolve the middleware
9+
build.onResolve(
10+
{ filter: getCrossPlatformPathRegex("./middleware.mjs") },
11+
() => {
12+
return {
13+
path: functionPath,
14+
};
15+
},
16+
);
17+
},
18+
};
19+
}

packages/open-next/src/types/open-next.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ export interface OpenNextConfig {
345345
//We force the middleware to be a function
346346
external: true;
347347

348+
/**
349+
* The runtime used by next for the middleware.
350+
* @default "edge"
351+
*/
352+
runtime?: "node" | "edge";
353+
348354
/**
349355
* The override options for the middleware.
350356
* By default the lite override are used (.i.e. s3-lite, dynamodb-lite, sqs-lite)

0 commit comments

Comments
 (0)