Skip to content

Commit 096c273

Browse files
conico974vicb
andauthored
feat: node middleware (#725)
* initial implementation * fix linting * hack for deployed lambda * use standalone for middleware trace * support for external middleware * fix test and linting * fix top level await in the middleware * fix comment * changeset * add function manifest to edge plugins * fix matcher when function manifest exist but no node middleware remove useless function * fix in case it can't find function manifest * remove comment * review fix * Apply suggestions from code review Co-authored-by: Victor Berchet <[email protected]> * review fix --------- Co-authored-by: Victor Berchet <[email protected]>
1 parent 2311e86 commit 096c273

File tree

21 files changed

+349
-63
lines changed

21 files changed

+349
-63
lines changed

.changeset/nasty-geckos-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": minor
3+
---
4+
5+
Add support for the node middleware

packages/open-next/src/adapters/config/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
loadBuildId,
99
loadConfig,
1010
loadConfigHeaders,
11+
loadFunctionsConfigManifest,
1112
loadHtmlPages,
1213
loadMiddlewareManifest,
1314
loadPrerenderManifest,
@@ -35,3 +36,6 @@ export const MiddlewareManifest =
3536
export const AppPathsManifest = /* @__PURE__ */ loadAppPathsManifest(NEXT_DIR);
3637
export const AppPathRoutesManifest =
3738
/* @__PURE__ */ loadAppPathRoutesManifest(NEXT_DIR);
39+
40+
export const FunctionsConfigManifest =
41+
/* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR);

packages/open-next/src/adapters/config/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import type {
4+
FunctionsConfigManifest,
45
MiddlewareManifest,
56
NextConfig,
67
PrerenderManifest,
@@ -123,3 +124,13 @@ export function loadMiddlewareManifest(nextDir: string) {
123124
const json = fs.readFileSync(filePath, "utf-8");
124125
return JSON.parse(json) as MiddlewareManifest;
125126
}
127+
128+
export function loadFunctionsConfigManifest(nextDir: string) {
129+
const filePath = path.join(nextDir, "server/functions-config-manifest.json");
130+
try {
131+
const json = fs.readFileSync(filePath, "utf-8");
132+
return JSON.parse(json) as FunctionsConfigManifest;
133+
} catch (e) {
134+
return { functions: {}, version: 1 };
135+
}
136+
}

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(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//TODO: Move all other manifest path here as well
2+
export const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json";

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import path from "node:path";
1515
import type { NextConfig, PrerenderManifest } from "types/next-types";
1616

1717
import logger from "../logger.js";
18+
import { MIDDLEWARE_TRACE_FILE } from "./constant.js";
1819

1920
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
2021

@@ -24,14 +25,24 @@ function copyPatchFile(outputDir: string) {
2425
copyFileSync(patchFile, outputPatchFile);
2526
}
2627

28+
interface CopyTracedFilesOptions {
29+
buildOutputPath: string;
30+
packagePath: string;
31+
outputDir: string;
32+
routes: string[];
33+
bundledNextServer: boolean;
34+
skipServerFiles?: boolean;
35+
}
36+
2737
// eslint-disable-next-line sonarjs/cognitive-complexity
28-
export async function copyTracedFiles(
29-
buildOutputPath: string,
30-
packagePath: string,
31-
outputDir: string,
32-
routes: string[],
33-
bundledNextServer: boolean,
34-
) {
38+
export async function copyTracedFiles({
39+
buildOutputPath,
40+
packagePath,
41+
outputDir,
42+
routes,
43+
bundledNextServer,
44+
skipServerFiles,
45+
}: CopyTracedFilesOptions) {
3546
const tsStart = Date.now();
3647
const dotNextDir = path.join(buildOutputPath, ".next");
3748
const standaloneDir = path.join(dotNextDir, "standalone");
@@ -58,10 +69,11 @@ export async function copyTracedFiles(
5869
const filesToCopy = new Map<string, string>();
5970

6071
// Files necessary by the server
61-
extractFiles(requiredServerFiles.files).forEach((f) => {
62-
filesToCopy.set(f, f.replace(standaloneDir, outputDir));
63-
});
64-
72+
if (!skipServerFiles) {
73+
extractFiles(requiredServerFiles.files).forEach((f) => {
74+
filesToCopy.set(f, f.replace(standaloneDir, outputDir));
75+
});
76+
}
6577
// create directory for pages
6678
if (existsSync(path.join(standaloneDir, ".next/server/pages"))) {
6779
mkdirSync(path.join(outputNextDir, "server/pages"), {
@@ -141,6 +153,15 @@ File ${fullFilePath} does not exist
141153
}
142154
};
143155

156+
if (existsSync(path.join(dotNextDir, MIDDLEWARE_TRACE_FILE))) {
157+
// We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw
158+
copyFileSync(
159+
path.join(dotNextDir, MIDDLEWARE_TRACE_FILE),
160+
path.join(standaloneNextDir, MIDDLEWARE_TRACE_FILE),
161+
);
162+
computeCopyFilesForPage("middleware");
163+
}
164+
144165
const hasPageDir = routes.some((route) => route.startsWith("pages/"));
145166
const hasAppDir = routes.some((route) => route.startsWith("app/"));
146167

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33

4+
import { loadFunctionsConfigManifest } from "config/util.js";
45
import logger from "../logger.js";
56
import type {
67
MiddlewareInfo,
@@ -9,6 +10,10 @@ import type {
910
import { buildEdgeBundle } from "./edge/createEdgeBundle.js";
1011
import * as buildHelper from "./helper.js";
1112
import { installDependencies } from "./installDeps.js";
13+
import {
14+
buildBundledNodeMiddleware,
15+
buildExternalNodeMiddleware,
16+
} from "./middleware/buildNodeMiddleware.js";
1217

1318
/**
1419
* Compiles the middleware bundle.
@@ -32,10 +37,24 @@ export async function createMiddleware(
3237
),
3338
) as MiddlewareManifest;
3439

35-
const middlewareInfo = middlewareManifest.middleware["/"] as
40+
const edgeMiddlewareInfo = middlewareManifest.middleware["/"] as
3641
| MiddlewareInfo
3742
| undefined;
3843

44+
if (!edgeMiddlewareInfo) {
45+
// If there is no middleware info, it might be a node middleware
46+
const functionsConfigManifest = loadFunctionsConfigManifest(
47+
path.join(appBuildOutputPath, ".next"),
48+
);
49+
50+
if (functionsConfigManifest?.functions["/_middleware"]) {
51+
await (config.middleware?.external
52+
? buildExternalNodeMiddleware(options)
53+
: buildBundledNodeMiddleware(options));
54+
return;
55+
}
56+
}
57+
3958
if (config.middleware?.external) {
4059
const outputPath = path.join(outputDir, "middleware");
4160
fs.mkdirSync(outputPath, { recursive: true });
@@ -55,7 +74,7 @@ export async function createMiddleware(
5574
"middleware.js",
5675
),
5776
outfile: path.join(outputPath, "handler.mjs"),
58-
middlewareInfo,
77+
middlewareInfo: edgeMiddlewareInfo,
5978
options,
6079
overrides: {
6180
...config.middleware.override,
@@ -77,7 +96,7 @@ export async function createMiddleware(
7796
"edgeFunctionHandler.js",
7897
),
7998
outfile: path.join(options.buildDir, "middleware.mjs"),
80-
middlewareInfo,
99+
middlewareInfo: edgeMiddlewareInfo,
81100
options,
82101
onlyBuildOnce: true,
83102
name: "middleware",

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ async function generateBundle(
153153
buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
154154

155155
// Copy all necessary traced files
156-
await copyTracedFiles(
157-
appBuildOutputPath,
156+
await copyTracedFiles({
157+
buildOutputPath: appBuildOutputPath,
158158
packagePath,
159-
outputPath,
160-
fnOptions.routes ?? ["app/page.tsx"],
161-
isBundled,
162-
);
159+
outputDir: outputPath,
160+
routes: fnOptions.routes ?? ["app/page.tsx"],
161+
bundledNextServer: isBundled,
162+
});
163163

164164
// Build Lambda code
165165
// note: bundle in OpenNext package b/c the adapter relies on the

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import type { OriginResolver } from "types/overrides.js";
1717
import logger from "../../logger.js";
1818
import { openNextEdgePlugins } from "../../plugins/edge.js";
19+
import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
1920
import { openNextReplacementPlugin } from "../../plugins/replacement.js";
2021
import { openNextResolvePlugin } from "../../plugins/resolve.js";
2122
import { getCrossPlatformPathRegex } from "../../utils/regex.js";
@@ -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 type {
5+
IncludedOriginResolver,
6+
LazyLoadedOverride,
7+
OverrideOptions,
8+
} from "types/open-next.js";
9+
import type { OriginResolver } from "types/overrides.js";
10+
import { getCrossPlatformPathRegex } from "utils/regex.js";
11+
import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js";
12+
import { openNextReplacementPlugin } from "../../plugins/replacement.js";
13+
import { openNextResolvePlugin } from "../../plugins/resolve.js";
14+
import { copyTracedFiles } from "../copyTracedFiles.js";
15+
import * as buildHelper from "../helper.js";
16+
import { installDependencies } from "../installDeps.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+
buildOutputPath: appBuildOutputPath,
50+
packagePath,
51+
outputDir: outputPath,
52+
routes: [],
53+
bundledNextServer: false,
54+
skipServerFiles: 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+
}

0 commit comments

Comments
 (0)