@netlify/plugin-nextjs 4.30.0 → 4.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/helpers/edge.js +99 -35
- package/lib/helpers/files.js +6 -3
- package/lib/helpers/matchers.js +4 -1
- package/lib/helpers/redirects.js +34 -9
- package/lib/helpers/utils.js +19 -18
- package/lib/templates/getApiHandler.js +1 -1
- package/lib/templates/getHandler.js +4 -5
- package/lib/templates/handlerUtils.js +16 -1
- package/package.json +2 -2
- package/src/templates/edge/function-runtime.ts +23 -0
- package/src/templates/edge/{runtime.ts → middleware-runtime.ts} +3 -56
- package/src/templates/edge/next-dev.js +1 -0
- package/src/templates/edge/rsc-data.ts +5 -0
- package/src/templates/edge-shared/nextConfig.json +3 -0
- package/src/templates/edge-shared/prerender-manifest.json +7 -0
- package/src/templates/edge-shared/rsc-data.test.ts +0 -0
- package/src/templates/edge-shared/rsc-data.ts +74 -0
- package/src/templates/edge-shared/utils.test.ts +51 -1
- package/src/templates/edge-shared/utils.ts +75 -0
package/lib/helpers/edge.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.writeEdgeFunctions = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadAppPathRoutesManifest = exports.loadMiddlewareManifest = void 0;
|
|
6
|
+
exports.writeEdgeFunctions = exports.writeRscDataEdgeFunction = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadPrerenderManifest = exports.loadAppPathRoutesManifest = exports.loadMiddlewareManifest = exports.isAppDirRoute = void 0;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const chalk_1 = require("chalk");
|
|
@@ -17,10 +17,14 @@ const maybeLoadJson = (path) => {
|
|
|
17
17
|
return (0, fs_extra_1.readJson)(path);
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
+
const isAppDirRoute = (route, appPathRoutesManifest) => Boolean(appPathRoutesManifest) && Object.values(appPathRoutesManifest).includes(route);
|
|
21
|
+
exports.isAppDirRoute = isAppDirRoute;
|
|
20
22
|
const loadMiddlewareManifest = (netlifyConfig) => maybeLoadJson((0, path_1.resolve)(netlifyConfig.build.publish, 'server', 'middleware-manifest.json'));
|
|
21
23
|
exports.loadMiddlewareManifest = loadMiddlewareManifest;
|
|
22
24
|
const loadAppPathRoutesManifest = (netlifyConfig) => maybeLoadJson((0, path_1.resolve)(netlifyConfig.build.publish, 'app-path-routes-manifest.json'));
|
|
23
25
|
exports.loadAppPathRoutesManifest = loadAppPathRoutesManifest;
|
|
26
|
+
const loadPrerenderManifest = (netlifyConfig) => (0, fs_extra_1.readJSON)((0, path_1.resolve)(netlifyConfig.build.publish, 'prerender-manifest.json'));
|
|
27
|
+
exports.loadPrerenderManifest = loadPrerenderManifest;
|
|
24
28
|
/**
|
|
25
29
|
* Convert the Next middleware name into a valid Edge Function name
|
|
26
30
|
*/
|
|
@@ -110,9 +114,8 @@ const getMiddlewareBundle = async ({ edgeFunctionDefinition, netlifyConfig, }) =
|
|
|
110
114
|
};
|
|
111
115
|
const getEdgeTemplatePath = (file) => (0, path_1.join)(__dirname, '..', '..', 'src', 'templates', 'edge', file);
|
|
112
116
|
const copyEdgeSourceFile = ({ file, target, edgeFunctionDir, }) => fs_1.promises.copyFile(getEdgeTemplatePath(file), (0, path_1.join)(edgeFunctionDir, target !== null && target !== void 0 ? target : file));
|
|
113
|
-
const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, netlifyConfig,
|
|
114
|
-
const
|
|
115
|
-
const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, name);
|
|
117
|
+
const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, netlifyConfig, functionName, matchers = [], middleware = false, }) => {
|
|
118
|
+
const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, functionName);
|
|
116
119
|
const bundle = await getMiddlewareBundle({
|
|
117
120
|
edgeFunctionDefinition,
|
|
118
121
|
netlifyConfig,
|
|
@@ -121,37 +124,30 @@ const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, net
|
|
|
121
124
|
await fs_1.promises.writeFile((0, path_1.join)(edgeFunctionDir, 'bundle.js'), bundle);
|
|
122
125
|
await copyEdgeSourceFile({
|
|
123
126
|
edgeFunctionDir,
|
|
124
|
-
file: 'runtime.ts',
|
|
127
|
+
file: middleware ? 'middleware-runtime.ts' : 'function-runtime.ts',
|
|
125
128
|
target: 'index.ts',
|
|
126
129
|
});
|
|
127
|
-
|
|
130
|
+
if (middleware) {
|
|
131
|
+
// Functions don't have complex matchers, so we can rely on the Netlify matcher
|
|
132
|
+
await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionDir, 'matchers.json'), matchers);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const generateEdgeFunctionMiddlewareMatchers = ({ edgeFunctionDefinition, nextConfig, }) => {
|
|
128
136
|
// The v1 middleware manifest has a single regexp, but the v2 has an array of matchers
|
|
129
137
|
if ('regexp' in edgeFunctionDefinition) {
|
|
130
|
-
|
|
138
|
+
return [{ regexp: edgeFunctionDefinition.regexp }];
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
if (nextConfig.i18n) {
|
|
141
|
+
return edgeFunctionDefinition.matchers.map((matcher) => ({
|
|
134
142
|
...matcher,
|
|
135
143
|
regexp: (0, matchers_1.makeLocaleOptional)(matcher.regexp),
|
|
136
|
-
}))
|
|
144
|
+
}));
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (pageRegexMap && edgeFunctionDefinition.page in appPathRoutesManifest) {
|
|
144
|
-
const regexp = pageRegexMap.get(appPathRoutesManifest[edgeFunctionDefinition.page]);
|
|
145
|
-
if (regexp) {
|
|
146
|
-
matchers.push({ regexp });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionDir, 'matchers.json'), matchers);
|
|
150
|
-
// We add a defintion for each matching path
|
|
151
|
-
return matchers.map((matcher) => {
|
|
152
|
-
const pattern = (0, matchers_1.stripLookahead)(matcher.regexp);
|
|
153
|
-
return { function: name, pattern, name: edgeFunctionDefinition.name, cache };
|
|
154
|
-
});
|
|
146
|
+
return edgeFunctionDefinition.matchers;
|
|
147
|
+
};
|
|
148
|
+
const middlewareMatcherToEdgeFunctionDefinition = (matcher, name, cache) => {
|
|
149
|
+
const pattern = (0, matchers_1.transformCaptureGroups)((0, matchers_1.stripLookahead)(matcher.regexp));
|
|
150
|
+
return { function: name, pattern, name, cache };
|
|
155
151
|
};
|
|
156
152
|
const cleanupEdgeFunctions = ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }) => (0, fs_extra_1.emptyDir)(INTERNAL_EDGE_FUNCTIONS_SRC);
|
|
157
153
|
exports.cleanupEdgeFunctions = cleanupEdgeFunctions;
|
|
@@ -175,9 +171,57 @@ const writeDevEdgeFunction = async ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/ed
|
|
|
175
171
|
await copyEdgeSourceFile({ edgeFunctionDir, file: 'next-dev.js', target: 'index.js' });
|
|
176
172
|
};
|
|
177
173
|
exports.writeDevEdgeFunction = writeDevEdgeFunction;
|
|
174
|
+
/**
|
|
175
|
+
* Writes an edge function that routes RSC data requests to the `.rsc` route
|
|
176
|
+
*/
|
|
177
|
+
const writeRscDataEdgeFunction = async ({ prerenderManifest, appPathRoutesManifest, }) => {
|
|
178
|
+
if (!prerenderManifest || !appPathRoutesManifest) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const staticAppdirRoutes = [];
|
|
182
|
+
for (const [path, route] of Object.entries(prerenderManifest.routes)) {
|
|
183
|
+
if ((0, exports.isAppDirRoute)(route.srcRoute, appPathRoutesManifest)) {
|
|
184
|
+
staticAppdirRoutes.push(path, route.dataRoute);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const dynamicAppDirRoutes = [];
|
|
188
|
+
for (const [path, route] of Object.entries(prerenderManifest.dynamicRoutes)) {
|
|
189
|
+
if ((0, exports.isAppDirRoute)(path, appPathRoutesManifest)) {
|
|
190
|
+
dynamicAppDirRoutes.push(route.routeRegex, route.dataRouteRegex);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (staticAppdirRoutes.length === 0 && dynamicAppDirRoutes.length === 0) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const edgeFunctionDir = (0, path_1.resolve)('.netlify', 'edge-functions', 'rsc-data');
|
|
197
|
+
await (0, fs_extra_1.ensureDir)(edgeFunctionDir);
|
|
198
|
+
await copyEdgeSourceFile({ edgeFunctionDir, file: 'rsc-data.ts' });
|
|
199
|
+
return [
|
|
200
|
+
...staticAppdirRoutes.map((path) => ({
|
|
201
|
+
function: 'rsc-data',
|
|
202
|
+
name: 'RSC data routing',
|
|
203
|
+
path,
|
|
204
|
+
})),
|
|
205
|
+
...dynamicAppDirRoutes.map((pattern) => ({
|
|
206
|
+
function: 'rsc-data',
|
|
207
|
+
name: 'RSC data routing',
|
|
208
|
+
pattern,
|
|
209
|
+
})),
|
|
210
|
+
];
|
|
211
|
+
};
|
|
212
|
+
exports.writeRscDataEdgeFunction = writeRscDataEdgeFunction;
|
|
213
|
+
const getEdgeFunctionPatternForPage = ({ edgeFunctionDefinition, pageRegexMap, appPathRoutesManifest, }) => {
|
|
214
|
+
// We don't just use the matcher from the edge function definition, because it doesn't handle trailing slashes
|
|
215
|
+
var _a;
|
|
216
|
+
// appDir functions have a name that _isn't_ the route name, but rather the route with `/page` appended
|
|
217
|
+
const regexp = pageRegexMap.get((_a = appPathRoutesManifest === null || appPathRoutesManifest === void 0 ? void 0 : appPathRoutesManifest[edgeFunctionDefinition.page]) !== null && _a !== void 0 ? _a : edgeFunctionDefinition.page);
|
|
218
|
+
// If we need to fall back to the matcher, we need to add an optional trailing slash
|
|
219
|
+
return regexp !== null && regexp !== void 0 ? regexp : edgeFunctionDefinition.matchers[0].regexp.replace(/([^/])\$$/, '$1/?$');
|
|
220
|
+
};
|
|
178
221
|
/**
|
|
179
222
|
* Writes Edge Functions for the Next middleware
|
|
180
223
|
*/
|
|
224
|
+
// eslint-disable-next-line max-lines-per-function
|
|
181
225
|
const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
|
|
182
226
|
var _a;
|
|
183
227
|
const manifest = {
|
|
@@ -192,6 +236,7 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
|
|
|
192
236
|
const usesAppDir = (_a = nextConfig.experimental) === null || _a === void 0 ? void 0 : _a.appDir;
|
|
193
237
|
await (0, fs_extra_1.copy)(getEdgeTemplatePath('../edge-shared'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared'));
|
|
194
238
|
await (0, fs_extra_1.writeJSON)((0, path_1.join)(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig);
|
|
239
|
+
await (0, fs_extra_1.copy)((0, path_1.join)(publish, 'prerender-manifest.json'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared', 'prerender-manifest.json'));
|
|
195
240
|
if (!(0, destr_1.default)(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
|
|
196
241
|
!(0, destr_1.default)(process.env.NEXT_DISABLE_NETLIFY_EDGE) &&
|
|
197
242
|
!(0, destr_1.default)(process.env.DISABLE_IPX)) {
|
|
@@ -207,6 +252,11 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
|
|
|
207
252
|
});
|
|
208
253
|
}
|
|
209
254
|
if (!(0, destr_1.default)(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
|
|
255
|
+
const rscFunctions = await (0, exports.writeRscDataEdgeFunction)({
|
|
256
|
+
prerenderManifest: await (0, exports.loadPrerenderManifest)(netlifyConfig),
|
|
257
|
+
appPathRoutesManifest: await (0, exports.loadAppPathRoutesManifest)(netlifyConfig),
|
|
258
|
+
});
|
|
259
|
+
manifest.functions.push(...rscFunctions);
|
|
210
260
|
const middlewareManifest = await (0, exports.loadMiddlewareManifest)(netlifyConfig);
|
|
211
261
|
if (!middlewareManifest) {
|
|
212
262
|
console.error("Couldn't find the middleware manifest");
|
|
@@ -216,16 +266,22 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
|
|
|
216
266
|
for (const middleware of middlewareManifest.sortedMiddleware) {
|
|
217
267
|
usesEdge = true;
|
|
218
268
|
const edgeFunctionDefinition = middlewareManifest.middleware[middleware];
|
|
219
|
-
const
|
|
269
|
+
const functionName = sanitizeName(edgeFunctionDefinition.name);
|
|
270
|
+
const matchers = generateEdgeFunctionMiddlewareMatchers({
|
|
220
271
|
edgeFunctionDefinition,
|
|
221
272
|
edgeFunctionRoot,
|
|
222
|
-
netlifyConfig,
|
|
223
273
|
nextConfig,
|
|
224
274
|
});
|
|
225
|
-
|
|
275
|
+
await writeEdgeFunction({
|
|
276
|
+
edgeFunctionDefinition,
|
|
277
|
+
edgeFunctionRoot,
|
|
278
|
+
netlifyConfig,
|
|
279
|
+
functionName,
|
|
280
|
+
matchers,
|
|
281
|
+
middleware: true,
|
|
282
|
+
});
|
|
283
|
+
manifest.functions.push(...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)));
|
|
226
284
|
}
|
|
227
|
-
// Older versions of the manifest format don't have the functions field
|
|
228
|
-
// No, the version field was not incremented
|
|
229
285
|
if (typeof middlewareManifest.functions === 'object') {
|
|
230
286
|
// When using the app dir, we also need to check if the EF matches a page
|
|
231
287
|
const appPathRoutesManifest = await (0, exports.loadAppPathRoutesManifest)(netlifyConfig);
|
|
@@ -235,17 +291,25 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
|
|
|
235
291
|
]));
|
|
236
292
|
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
|
|
237
293
|
usesEdge = true;
|
|
238
|
-
const
|
|
294
|
+
const functionName = sanitizeName(edgeFunctionDefinition.name);
|
|
295
|
+
await writeEdgeFunction({
|
|
239
296
|
edgeFunctionDefinition,
|
|
240
297
|
edgeFunctionRoot,
|
|
241
298
|
netlifyConfig,
|
|
299
|
+
functionName,
|
|
300
|
+
});
|
|
301
|
+
const pattern = getEdgeFunctionPatternForPage({
|
|
302
|
+
edgeFunctionDefinition,
|
|
242
303
|
pageRegexMap,
|
|
243
304
|
appPathRoutesManifest,
|
|
244
|
-
|
|
305
|
+
});
|
|
306
|
+
manifest.functions.push({
|
|
307
|
+
function: functionName,
|
|
308
|
+
name: edgeFunctionDefinition.name,
|
|
309
|
+
pattern,
|
|
245
310
|
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
|
|
246
311
|
cache: usesAppDir ? 'manual' : undefined,
|
|
247
312
|
});
|
|
248
|
-
manifest.functions.push(...functionDefinitions);
|
|
249
313
|
}
|
|
250
314
|
}
|
|
251
315
|
if (usesEdge) {
|
package/lib/helpers/files.js
CHANGED
|
@@ -13,6 +13,7 @@ const p_limit_1 = __importDefault(require("p-limit"));
|
|
|
13
13
|
const pathe_1 = require("pathe");
|
|
14
14
|
const slash_1 = __importDefault(require("slash"));
|
|
15
15
|
const constants_1 = require("../constants");
|
|
16
|
+
const edge_1 = require("./edge");
|
|
16
17
|
const utils_1 = require("./utils");
|
|
17
18
|
const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/;
|
|
18
19
|
const SOURCE_FILE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'];
|
|
@@ -75,16 +76,18 @@ const moveStaticPages = async ({ netlifyConfig, target, i18n, basePath, }) => {
|
|
|
75
76
|
// Load the middleware manifest so we can check if a file matches it before moving
|
|
76
77
|
const middlewarePaths = await (0, exports.getMiddleware)(netlifyConfig.build.publish);
|
|
77
78
|
const middleware = middlewarePaths.map((path) => path.slice(1));
|
|
78
|
-
const prerenderManifest = await (0,
|
|
79
|
+
const prerenderManifest = await (0, edge_1.loadPrerenderManifest)(netlifyConfig);
|
|
79
80
|
const { redirects, rewrites } = await (0, fs_extra_1.readJson)((0, pathe_1.join)(netlifyConfig.build.publish, 'routes-manifest.json'));
|
|
80
81
|
const isrFiles = new Set();
|
|
81
82
|
const shortRevalidateRoutes = [];
|
|
82
|
-
Object.entries(prerenderManifest.routes).forEach(([route,
|
|
83
|
+
Object.entries(prerenderManifest.routes).forEach(([route, ssgRoute]) => {
|
|
84
|
+
const { initialRevalidateSeconds } = ssgRoute;
|
|
85
|
+
const trimmedPath = route === '/' ? 'index' : route.slice(1);
|
|
83
86
|
if (initialRevalidateSeconds) {
|
|
84
87
|
// Find all files used by ISR routes
|
|
85
|
-
const trimmedPath = route === '/' ? 'index' : route.slice(1);
|
|
86
88
|
isrFiles.add(`${trimmedPath}.html`);
|
|
87
89
|
isrFiles.add(`${trimmedPath}.json`);
|
|
90
|
+
isrFiles.add(`${trimmedPath}.rsc`);
|
|
88
91
|
if (initialRevalidateSeconds < constants_1.MINIMUM_REVALIDATE_SECONDS) {
|
|
89
92
|
shortRevalidateRoutes.push({ Route: route, Revalidate: initialRevalidateSeconds });
|
|
90
93
|
}
|
package/lib/helpers/matchers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.makeLocaleOptional = exports.stripLookahead = void 0;
|
|
3
|
+
exports.makeLocaleOptional = exports.transformCaptureGroups = exports.stripLookahead = void 0;
|
|
4
4
|
const regexp_tree_1 = require("regexp-tree");
|
|
5
5
|
// The Go regexp lib doesn't support lookaheads, so we need to remove them
|
|
6
6
|
const stripLookahead = (regex) => {
|
|
@@ -27,6 +27,9 @@ const stripLookahead = (regex) => {
|
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
exports.stripLookahead = stripLookahead;
|
|
30
|
+
// The Go regexp lib has alternative syntax for named capture groups
|
|
31
|
+
const transformCaptureGroups = (regex) => regex.replace(/\(\?<\w+>/, '(');
|
|
32
|
+
exports.transformCaptureGroups = transformCaptureGroups;
|
|
30
33
|
const LOCALIZED_REGEX_PREFIX = '(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))';
|
|
31
34
|
const OPTIONAL_REGEX_PREFIX = '(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))?';
|
|
32
35
|
// Make the locale section of the matcher regex optional
|
package/lib/helpers/redirects.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs_extra_1 = require("fs-extra");
|
|
|
6
6
|
const outdent_1 = require("outdent");
|
|
7
7
|
const pathe_1 = require("pathe");
|
|
8
8
|
const constants_1 = require("../constants");
|
|
9
|
+
const edge_1 = require("./edge");
|
|
9
10
|
const files_1 = require("./files");
|
|
10
11
|
const utils_1 = require("./utils");
|
|
11
12
|
const matchesMiddleware = (middleware, route) => middleware.some((middlewarePath) => route.startsWith(middlewarePath));
|
|
@@ -52,7 +53,7 @@ const generateStaticRedirects = ({ netlifyConfig, nextConfig: { i18n, basePath }
|
|
|
52
53
|
};
|
|
53
54
|
exports.generateStaticRedirects = generateStaticRedirects;
|
|
54
55
|
/**
|
|
55
|
-
* Routes that match middleware need to always use the SSR function
|
|
56
|
+
* Routes that match origin middleware need to always use the SSR function
|
|
56
57
|
* This generates a rewrite for every middleware in every locale, both with and without a splat
|
|
57
58
|
*/
|
|
58
59
|
const generateMiddlewareRewrites = ({ basePath, middleware, i18n, buildId }) => {
|
|
@@ -79,11 +80,11 @@ const generateMiddlewareRewrites = ({ basePath, middleware, i18n, buildId }) =>
|
|
|
79
80
|
// Flatten the array of arrays. Can't use flatMap as it might be 2 levels deep
|
|
80
81
|
.flat(2));
|
|
81
82
|
};
|
|
82
|
-
const generateStaticIsrRewrites = ({ staticRouteEntries, basePath, i18n, buildId, middleware, }) => {
|
|
83
|
+
const generateStaticIsrRewrites = ({ staticRouteEntries, basePath, i18n, buildId, middleware, appPathRoutes, }) => {
|
|
83
84
|
const staticIsrRoutesThatMatchMiddleware = [];
|
|
84
85
|
const staticRoutePaths = new Set();
|
|
85
86
|
const staticIsrRewrites = [];
|
|
86
|
-
staticRouteEntries.forEach(([route, { initialRevalidateSeconds }]) => {
|
|
87
|
+
staticRouteEntries.forEach(([route, { initialRevalidateSeconds, dataRoute, srcRoute }]) => {
|
|
87
88
|
if ((0, utils_1.isApiRoute)(route) || (0, utils_1.is404Route)(route, i18n)) {
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
@@ -92,30 +93,40 @@ const generateStaticIsrRewrites = ({ staticRouteEntries, basePath, i18n, buildId
|
|
|
92
93
|
// These can be ignored, as they're static files handled by the CDN
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
96
|
+
// appDir routes are a different format, so we need to handle them differently
|
|
97
|
+
const isAppDir = (0, edge_1.isAppDirRoute)(srcRoute, appPathRoutes);
|
|
95
98
|
// The default locale is served from the root, not the localised path
|
|
96
|
-
if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && route.startsWith(`/${i18n.defaultLocale}/`)) {
|
|
97
|
-
route = route.slice(i18n.defaultLocale.length + 1);
|
|
99
|
+
if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && (route.startsWith(`/${i18n.defaultLocale}/`) || route === `/${i18n.defaultLocale}`)) {
|
|
100
|
+
route = route.slice(i18n.defaultLocale.length + 1) || '/';
|
|
98
101
|
staticRoutePaths.add(route);
|
|
99
102
|
if (matchesMiddleware(middleware, route)) {
|
|
100
103
|
staticIsrRoutesThatMatchMiddleware.push(route);
|
|
101
104
|
}
|
|
102
105
|
staticIsrRewrites.push(...(0, utils_1.redirectsForNextRouteWithData)({
|
|
103
106
|
route,
|
|
104
|
-
dataRoute: (0, utils_1.routeToDataRoute)(route, buildId, i18n.defaultLocale),
|
|
107
|
+
dataRoute: isAppDir ? dataRoute : (0, utils_1.routeToDataRoute)(route, buildId, i18n.defaultLocale),
|
|
105
108
|
basePath,
|
|
106
109
|
to: constants_1.ODB_FUNCTION_PATH,
|
|
107
110
|
force: true,
|
|
108
111
|
}));
|
|
109
112
|
}
|
|
110
113
|
else if (matchesMiddleware(middleware, route)) {
|
|
111
|
-
// Routes that match middleware can't use the ODB
|
|
114
|
+
// Routes that match origin middleware can't use the ODB. Edge middleware will always return false
|
|
112
115
|
staticIsrRoutesThatMatchMiddleware.push(route);
|
|
113
116
|
}
|
|
114
117
|
else {
|
|
115
118
|
// ISR routes use the ODB handler
|
|
116
119
|
staticIsrRewrites.push(
|
|
117
120
|
// No i18n, because the route is already localized
|
|
118
|
-
...(0, utils_1.redirectsForNextRoute)({
|
|
121
|
+
...(0, utils_1.redirectsForNextRoute)({
|
|
122
|
+
route,
|
|
123
|
+
basePath,
|
|
124
|
+
to: constants_1.ODB_FUNCTION_PATH,
|
|
125
|
+
force: true,
|
|
126
|
+
buildId,
|
|
127
|
+
dataRoute: isAppDir ? dataRoute : null,
|
|
128
|
+
i18n: null,
|
|
129
|
+
}));
|
|
119
130
|
}
|
|
120
131
|
});
|
|
121
132
|
return {
|
|
@@ -127,7 +138,7 @@ const generateStaticIsrRewrites = ({ staticRouteEntries, basePath, i18n, buildId
|
|
|
127
138
|
/**
|
|
128
139
|
* Generate rewrites for all dynamic routes
|
|
129
140
|
*/
|
|
130
|
-
const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, middleware, basePath, buildId, i18n, is404Isr, }) => {
|
|
141
|
+
const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, middleware, basePath, buildId, i18n, is404Isr, appPathRoutes, }) => {
|
|
131
142
|
const dynamicRewrites = [];
|
|
132
143
|
const dynamicRoutesThatMatchMiddleware = [];
|
|
133
144
|
dynamicRoutes.forEach((route) => {
|
|
@@ -138,6 +149,17 @@ const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, midd
|
|
|
138
149
|
if (matchesMiddleware(middleware, route.page)) {
|
|
139
150
|
dynamicRoutesThatMatchMiddleware.push(route.page);
|
|
140
151
|
}
|
|
152
|
+
else if ((0, edge_1.isAppDirRoute)(route.page, appPathRoutes)) {
|
|
153
|
+
dynamicRewrites.push(...(0, utils_1.redirectsForNextRoute)({
|
|
154
|
+
route: route.page,
|
|
155
|
+
buildId,
|
|
156
|
+
basePath,
|
|
157
|
+
to: constants_1.ODB_FUNCTION_PATH,
|
|
158
|
+
i18n,
|
|
159
|
+
dataRoute: prerenderedDynamicRoutes[route.page].dataRoute,
|
|
160
|
+
withData: true,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
141
163
|
else if (prerenderedDynamicRoutes[route.page].fallback === false && !is404Isr) {
|
|
142
164
|
dynamicRewrites.push(...(0, utils_1.redirectsForNext404Route)({ route: route.page, buildId, basePath, i18n }));
|
|
143
165
|
}
|
|
@@ -170,6 +192,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
|
|
|
170
192
|
...(await (0, utils_1.getPreviewRewrites)({ basePath, appDir })));
|
|
171
193
|
const middleware = await (0, files_1.getMiddleware)(netlifyConfig.build.publish);
|
|
172
194
|
netlifyConfig.redirects.push(...generateMiddlewareRewrites({ basePath, i18n, middleware, buildId }));
|
|
195
|
+
const appPathRoutes = await (0, edge_1.loadAppPathRoutesManifest)(netlifyConfig);
|
|
173
196
|
const staticRouteEntries = Object.entries(prerenderedStaticRoutes);
|
|
174
197
|
const is404Isr = staticRouteEntries.some(([route, { initialRevalidateSeconds }]) => (0, utils_1.is404Route)(route, i18n) && initialRevalidateSeconds !== false);
|
|
175
198
|
const routesThatMatchMiddleware = [];
|
|
@@ -179,6 +202,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
|
|
|
179
202
|
i18n,
|
|
180
203
|
buildId,
|
|
181
204
|
middleware,
|
|
205
|
+
appPathRoutes,
|
|
182
206
|
});
|
|
183
207
|
routesThatMatchMiddleware.push(...staticIsrRoutesThatMatchMiddleware);
|
|
184
208
|
netlifyConfig.redirects.push(...staticIsrRewrites);
|
|
@@ -199,6 +223,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
|
|
|
199
223
|
buildId,
|
|
200
224
|
i18n,
|
|
201
225
|
is404Isr,
|
|
226
|
+
appPathRoutes,
|
|
202
227
|
});
|
|
203
228
|
netlifyConfig.redirects.push(...dynamicRewrites);
|
|
204
229
|
routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware);
|
package/lib/helpers/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getRemotePatterns = exports.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNext404Route = exports.redirectsForNextRoute = exports.is404Route = exports.isApiRoute = exports.routeToDataRoute = exports.
|
|
6
|
+
exports.getRemotePatterns = exports.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNext404Route = exports.redirectsForNextRoute = exports.is404Route = exports.isApiRoute = exports.localizeRoute = exports.routeToDataRoute = exports.generateNetlifyRoutes = exports.toNetlifyRoute = exports.getFunctionNameForPage = void 0;
|
|
7
7
|
const globby_1 = __importDefault(require("globby"));
|
|
8
8
|
const pathe_1 = require("pathe");
|
|
9
9
|
const constants_1 = require("../constants");
|
|
@@ -47,17 +47,17 @@ const toNetlifyRoute = (nextRoute) => {
|
|
|
47
47
|
.replace(constants_1.DYNAMIC_PARAMETER_REGEX, '/:$1'));
|
|
48
48
|
};
|
|
49
49
|
exports.toNetlifyRoute = toNetlifyRoute;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
...(0, exports.toNetlifyRoute)(route),
|
|
53
|
-
];
|
|
54
|
-
exports.netlifyRoutesForNextRouteWithData = netlifyRoutesForNextRouteWithData;
|
|
50
|
+
const generateNetlifyRoutes = ({ route, dataRoute, withData = true, }) => [...(withData ? (0, exports.toNetlifyRoute)(dataRoute) : []), ...(0, exports.toNetlifyRoute)(route)];
|
|
51
|
+
exports.generateNetlifyRoutes = generateNetlifyRoutes;
|
|
55
52
|
const routeToDataRoute = (route, buildId, locale) => `/_next/data/${buildId}${locale ? `/${locale}` : ''}${route === '/' ? '/index' : route}.json`;
|
|
56
53
|
exports.routeToDataRoute = routeToDataRoute;
|
|
57
|
-
|
|
54
|
+
// Default locale is served from root, not localized
|
|
55
|
+
const localizeRoute = (route, locale, defaultLocale) => locale === defaultLocale ? route : `/${locale}${route}`;
|
|
56
|
+
exports.localizeRoute = localizeRoute;
|
|
57
|
+
const netlifyRoutesForNextRoute = ({ route, buildId, i18n, withData = true, dataRoute, }) => {
|
|
58
58
|
var _a;
|
|
59
59
|
if (!((_a = i18n === null || i18n === void 0 ? void 0 : i18n.locales) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
60
|
-
return (0, exports.
|
|
60
|
+
return (0, exports.generateNetlifyRoutes)({ route, dataRoute: dataRoute || (0, exports.routeToDataRoute)(route, buildId), withData }).map((redirect) => ({
|
|
61
61
|
redirect,
|
|
62
62
|
locale: false,
|
|
63
63
|
}));
|
|
@@ -65,13 +65,14 @@ const netlifyRoutesForNextRoute = (route, buildId, i18n) => {
|
|
|
65
65
|
const { locales, defaultLocale } = i18n;
|
|
66
66
|
const routes = [];
|
|
67
67
|
locales.forEach((locale) => {
|
|
68
|
-
// Data route is always localized
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...(0, exports.
|
|
73
|
-
route:
|
|
74
|
-
dataRoute,
|
|
68
|
+
// Data route is always localized, except for appDir
|
|
69
|
+
const localizedDataRoute = dataRoute
|
|
70
|
+
? (0, exports.localizeRoute)(dataRoute, locale, defaultLocale)
|
|
71
|
+
: (0, exports.routeToDataRoute)(route, buildId, locale);
|
|
72
|
+
routes.push(...(0, exports.generateNetlifyRoutes)({
|
|
73
|
+
route: (0, exports.localizeRoute)(route, locale, defaultLocale),
|
|
74
|
+
dataRoute: localizedDataRoute,
|
|
75
|
+
withData,
|
|
75
76
|
}).map((redirect) => ({
|
|
76
77
|
redirect,
|
|
77
78
|
locale,
|
|
@@ -83,21 +84,21 @@ const isApiRoute = (route) => route.startsWith('/api/') || route === '/api';
|
|
|
83
84
|
exports.isApiRoute = isApiRoute;
|
|
84
85
|
const is404Route = (route, i18n) => i18n ? i18n.locales.some((locale) => route === `/${locale}/404`) : route === '/404';
|
|
85
86
|
exports.is404Route = is404Route;
|
|
86
|
-
const redirectsForNextRoute = ({ route, buildId, basePath, to, i18n, status = 200, force = false, }) => netlifyRoutesForNextRoute(route, buildId, i18n).map(({ redirect }) => ({
|
|
87
|
+
const redirectsForNextRoute = ({ route, buildId, basePath, to, i18n, status = 200, force = false, withData = true, dataRoute, }) => netlifyRoutesForNextRoute({ route, buildId, i18n, withData, dataRoute }).map(({ redirect }) => ({
|
|
87
88
|
from: `${basePath}${redirect}`,
|
|
88
89
|
to,
|
|
89
90
|
status,
|
|
90
91
|
force,
|
|
91
92
|
}));
|
|
92
93
|
exports.redirectsForNextRoute = redirectsForNextRoute;
|
|
93
|
-
const redirectsForNext404Route = ({ route, buildId, basePath, i18n, force = false, }) => netlifyRoutesForNextRoute(route, buildId, i18n).map(({ redirect, locale }) => ({
|
|
94
|
+
const redirectsForNext404Route = ({ route, buildId, basePath, i18n, force = false, }) => netlifyRoutesForNextRoute({ route, buildId, i18n }).map(({ redirect, locale }) => ({
|
|
94
95
|
from: `${basePath}${redirect}`,
|
|
95
96
|
to: locale ? `${basePath}/server/pages/${locale}/404.html` : `${basePath}/server/pages/404.html`,
|
|
96
97
|
status: 404,
|
|
97
98
|
force,
|
|
98
99
|
}));
|
|
99
100
|
exports.redirectsForNext404Route = redirectsForNext404Route;
|
|
100
|
-
const redirectsForNextRouteWithData = ({ route, dataRoute, basePath, to, status = 200, force = false, }) => (0, exports.
|
|
101
|
+
const redirectsForNextRouteWithData = ({ route, dataRoute, basePath, to, status = 200, force = false, }) => (0, exports.generateNetlifyRoutes)({ route, dataRoute, withData: true }).map((redirect) => ({
|
|
101
102
|
from: `${basePath}${redirect}`,
|
|
102
103
|
to,
|
|
103
104
|
status,
|
|
@@ -97,7 +97,7 @@ const getApiHandler = ({ page, config, publishDir = '../../../.next', appDir = '
|
|
|
97
97
|
const { Server } = require("http");
|
|
98
98
|
// We copy the file here rather than requiring from the node module
|
|
99
99
|
const { Bridge } = require("./bridge");
|
|
100
|
-
const {
|
|
100
|
+
const { getMultiValueHeaders, getNextServer } = require('./handlerUtils')
|
|
101
101
|
|
|
102
102
|
${config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */ ? `const { schedule } = require("@netlify/functions")` : ''}
|
|
103
103
|
|
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
// eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params
|
|
11
11
|
const { URLSearchParams, URL } = require('url');
|
|
12
12
|
const { Bridge } = require('@vercel/node-bridge/bridge');
|
|
13
|
-
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, } = require('./handlerUtils');
|
|
13
|
+
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, normalizePath, } = require('./handlerUtils');
|
|
14
14
|
// We return a function and then call `toString()` on it to serialise it as the launcher function
|
|
15
15
|
// eslint-disable-next-line max-params
|
|
16
16
|
const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') => {
|
|
@@ -79,8 +79,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
|
|
|
79
79
|
if (prefetchResponse) {
|
|
80
80
|
return prefetchResponse;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
event.path = new URL(event.rawUrl).pathname;
|
|
82
|
+
event.path = normalizePath(event);
|
|
84
83
|
// Next expects to be able to parse the query from the URL
|
|
85
84
|
const query = new URLSearchParams(event.queryStringParameters).toString();
|
|
86
85
|
event.path = query ? `${event.path}?${query}` : event.path;
|
|
@@ -99,7 +98,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
|
|
|
99
98
|
headers: multiValueHeaders,
|
|
100
99
|
statusCode: result.statusCode,
|
|
101
100
|
};
|
|
102
|
-
console.log('
|
|
101
|
+
console.log('Next server response:', JSON.stringify(response, null, 2));
|
|
103
102
|
}
|
|
104
103
|
if ((_b = (_a = multiValueHeaders['set-cookie']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.includes('__prerender_bypass')) {
|
|
105
104
|
delete multiValueHeaders.etag;
|
|
@@ -142,7 +141,7 @@ const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '..
|
|
|
142
141
|
const { promises } = require("fs");
|
|
143
142
|
// We copy the file here rather than requiring from the node module
|
|
144
143
|
const { Bridge } = require("./bridge");
|
|
145
|
-
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer } = require('./handlerUtils')
|
|
144
|
+
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, normalizePath } = require('./handlerUtils')
|
|
146
145
|
|
|
147
146
|
${isODB ? `const { builder } = require("@netlify/functions")` : ''}
|
|
148
147
|
const { config } = require("${publishDir}/required-server-files.json")
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getPrefetchResponse = exports.getNextServer = exports.augmentFsModule = exports.getMultiValueHeaders = exports.getMaxAge = exports.downloadFile = void 0;
|
|
6
|
+
exports.normalizePath = exports.getPrefetchResponse = exports.getNextServer = exports.augmentFsModule = exports.getMultiValueHeaders = exports.getMaxAge = exports.downloadFile = void 0;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const os_1 = require("os");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -191,3 +191,18 @@ const getPrefetchResponse = (event, mode) => {
|
|
|
191
191
|
return false;
|
|
192
192
|
};
|
|
193
193
|
exports.getPrefetchResponse = getPrefetchResponse;
|
|
194
|
+
const normalizePath = (event) => {
|
|
195
|
+
var _a;
|
|
196
|
+
if ((_a = event.headers) === null || _a === void 0 ? void 0 : _a.rsc) {
|
|
197
|
+
const originalPath = event.headers['x-rsc-route'];
|
|
198
|
+
if (originalPath) {
|
|
199
|
+
if (event.headers['x-next-debug-logging']) {
|
|
200
|
+
console.log('Original path:', originalPath);
|
|
201
|
+
}
|
|
202
|
+
return originalPath;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Ensure that paths are encoded - but don't double-encode them
|
|
206
|
+
return new URL(event.rawUrl).pathname;
|
|
207
|
+
};
|
|
208
|
+
exports.normalizePath = normalizePath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/plugin-nextjs",
|
|
3
|
-
"version": "4.30.
|
|
3
|
+
"version": "4.30.1",
|
|
4
4
|
"description": "Run Next.js seamlessly on Netlify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@delucis/if-env": "^1.1.2",
|
|
39
|
-
"@netlify/build": "^29.
|
|
39
|
+
"@netlify/build": "^29.5.1",
|
|
40
40
|
"@types/fs-extra": "^9.0.13",
|
|
41
41
|
"@types/jest": "^27.4.1",
|
|
42
42
|
"@types/merge-stream": "^1.1.2",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Context } from 'https://edge.netlify.com'
|
|
2
|
+
// Available at build time
|
|
3
|
+
import edgeFunction from './bundle.js'
|
|
4
|
+
import { buildNextRequest, buildResponse, redirectTrailingSlash } from '../edge-shared/utils.ts'
|
|
5
|
+
import nextConfig from '../edge-shared/nextConfig.json' assert { type: 'json' }
|
|
6
|
+
|
|
7
|
+
const handler = async (req: Request, context: Context) => {
|
|
8
|
+
const url = new URL(req.url)
|
|
9
|
+
const redirect = redirectTrailingSlash(url, nextConfig.trailingSlash)
|
|
10
|
+
if (redirect) {
|
|
11
|
+
return redirect
|
|
12
|
+
}
|
|
13
|
+
const request = buildNextRequest(req, context, nextConfig)
|
|
14
|
+
try {
|
|
15
|
+
const result = await edgeFunction({ request })
|
|
16
|
+
return buildResponse({ result, request: req, context })
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(error)
|
|
19
|
+
return new Response(error.message, { status: 500 })
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default handler
|
|
@@ -1,49 +1,13 @@
|
|
|
1
1
|
import type { Context } from 'https://edge.netlify.com'
|
|
2
2
|
// Available at build time
|
|
3
3
|
import matchers from './matchers.json' assert { type: 'json' }
|
|
4
|
-
import nextConfig from '../edge-shared/nextConfig.json' assert { type: 'json' }
|
|
5
4
|
import edgeFunction from './bundle.js'
|
|
6
|
-
import { buildResponse } from '../edge-shared/utils.ts'
|
|
5
|
+
import { buildNextRequest, buildResponse } from '../edge-shared/utils.ts'
|
|
7
6
|
import { getMiddlewareRouteMatcher, MiddlewareRouteMatch, searchParamsToUrlQuery } from '../edge-shared/next-utils.ts'
|
|
7
|
+
import nextConfig from '../edge-shared/nextConfig.json' assert { type: 'json' }
|
|
8
8
|
|
|
9
9
|
const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
|
|
10
10
|
|
|
11
|
-
export interface FetchEventResult {
|
|
12
|
-
response: Response
|
|
13
|
-
waitUntil: Promise<any>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface I18NConfig {
|
|
17
|
-
defaultLocale: string
|
|
18
|
-
localeDetection?: false
|
|
19
|
-
locales: string[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RequestData {
|
|
23
|
-
geo?: {
|
|
24
|
-
city?: string
|
|
25
|
-
country?: string
|
|
26
|
-
region?: string
|
|
27
|
-
latitude?: string
|
|
28
|
-
longitude?: string
|
|
29
|
-
timezone?: string
|
|
30
|
-
}
|
|
31
|
-
headers: Record<string, string>
|
|
32
|
-
ip?: string
|
|
33
|
-
method: string
|
|
34
|
-
nextConfig?: {
|
|
35
|
-
basePath?: string
|
|
36
|
-
i18n?: I18NConfig | null
|
|
37
|
-
trailingSlash?: boolean
|
|
38
|
-
}
|
|
39
|
-
page?: {
|
|
40
|
-
name?: string
|
|
41
|
-
params?: { [key: string]: string }
|
|
42
|
-
}
|
|
43
|
-
url: string
|
|
44
|
-
body?: ReadableStream<Uint8Array>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
11
|
export interface RequestContext {
|
|
48
12
|
request: Request
|
|
49
13
|
context: Context
|
|
@@ -70,15 +34,6 @@ const handler = async (req: Request, context: Context) => {
|
|
|
70
34
|
return
|
|
71
35
|
}
|
|
72
36
|
|
|
73
|
-
const geo: RequestData['geo'] = {
|
|
74
|
-
country: context.geo.country?.code,
|
|
75
|
-
region: context.geo.subdivision?.code,
|
|
76
|
-
city: context.geo.city,
|
|
77
|
-
latitude: context.geo.latitude?.toString(),
|
|
78
|
-
longitude: context.geo.longitude?.toString(),
|
|
79
|
-
timezone: context.geo.timezone,
|
|
80
|
-
}
|
|
81
|
-
|
|
82
37
|
const requestId = req.headers.get('x-nf-request-id')
|
|
83
38
|
if (!requestId) {
|
|
84
39
|
console.error('Missing x-nf-request-id header')
|
|
@@ -89,15 +44,7 @@ const handler = async (req: Request, context: Context) => {
|
|
|
89
44
|
})
|
|
90
45
|
}
|
|
91
46
|
|
|
92
|
-
const request
|
|
93
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
94
|
-
geo,
|
|
95
|
-
url: req.url,
|
|
96
|
-
method: req.method,
|
|
97
|
-
ip: context.ip,
|
|
98
|
-
body: req.body ?? undefined,
|
|
99
|
-
nextConfig,
|
|
100
|
-
}
|
|
47
|
+
const request = buildNextRequest(req, context, nextConfig)
|
|
101
48
|
|
|
102
49
|
try {
|
|
103
50
|
const result = await edgeFunction({ request })
|
|
@@ -5,6 +5,7 @@ import { buildResponse } from '../edge-shared/utils.ts'
|
|
|
5
5
|
|
|
6
6
|
globalThis.NFRequestContextMap ||= new Map()
|
|
7
7
|
globalThis.__dirname = fromFileUrl(new URL('./', import.meta.url)).slice(0, -1)
|
|
8
|
+
globalThis.process ||= { env: Deno.env.toObject() }
|
|
8
9
|
|
|
9
10
|
// Next.js uses this extension to the Headers API implemented by Cloudflare workerd
|
|
10
11
|
if (!('getAll' in Headers.prototype)) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import prerenderManifest from '../edge-shared/prerender-manifest.json' assert { type: 'json' }
|
|
2
|
+
import { getRscDataRouter, PrerenderManifest } from '../edge-shared/rsc-data.ts'
|
|
3
|
+
|
|
4
|
+
const handler = getRscDataRouter(prerenderManifest as PrerenderManifest)
|
|
5
|
+
export default handler
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { EdgeFunction } from 'https://edge.netlify.com'
|
|
2
|
+
|
|
3
|
+
export declare type SsgRoute = {
|
|
4
|
+
initialRevalidateSeconds: number | false
|
|
5
|
+
srcRoute: string | null
|
|
6
|
+
dataRoute: string
|
|
7
|
+
}
|
|
8
|
+
export declare type DynamicSsgRoute = {
|
|
9
|
+
routeRegex: string
|
|
10
|
+
fallback: string | null | false
|
|
11
|
+
dataRoute: string
|
|
12
|
+
dataRouteRegex: string
|
|
13
|
+
}
|
|
14
|
+
export declare type PrerenderManifest = {
|
|
15
|
+
version: 3
|
|
16
|
+
routes: {
|
|
17
|
+
[route: string]: SsgRoute
|
|
18
|
+
}
|
|
19
|
+
dynamicRoutes: {
|
|
20
|
+
[route: string]: DynamicSsgRoute
|
|
21
|
+
}
|
|
22
|
+
notFoundRoutes: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const noop = () => {}
|
|
26
|
+
|
|
27
|
+
// Ensure that routes with and without a trailing slash map to different ODB paths
|
|
28
|
+
const rscifyPath = (route: string) => {
|
|
29
|
+
if (route.endsWith('/')) {
|
|
30
|
+
return route.slice(0, -1) + '.rsc/'
|
|
31
|
+
}
|
|
32
|
+
return route + '.rsc'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const getRscDataRouter = ({ routes: staticRoutes, dynamicRoutes }: PrerenderManifest): EdgeFunction => {
|
|
36
|
+
const staticRouteSet = new Set(
|
|
37
|
+
Object.entries(staticRoutes)
|
|
38
|
+
.filter(([, { dataRoute }]) => dataRoute.endsWith('.rsc'))
|
|
39
|
+
.map(([route]) => route),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const dynamicRouteMatcher = Object.values(dynamicRoutes)
|
|
43
|
+
.filter(({ dataRoute }) => dataRoute.endsWith('.rsc'))
|
|
44
|
+
.map(({ routeRegex }) => new RegExp(routeRegex))
|
|
45
|
+
|
|
46
|
+
const matchesDynamicRscDataRoute = (pathname: string) => {
|
|
47
|
+
return dynamicRouteMatcher.some((matcher) => matcher.test(pathname))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const matchesStaticRscDataRoute = (pathname: string) => {
|
|
51
|
+
const key = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
|
|
52
|
+
return staticRouteSet.has(key)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const matchesRscRoute = (pathname: string) => {
|
|
56
|
+
return matchesStaticRscDataRoute(pathname) || matchesDynamicRscDataRoute(pathname)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (request, context) => {
|
|
60
|
+
const debug = request.headers.has('x-next-debug-logging')
|
|
61
|
+
const log = debug ? (...args: unknown[]) => console.log(...args) : noop
|
|
62
|
+
const url = new URL(request.url)
|
|
63
|
+
// If this is a static RSC request, rewrite to the data route
|
|
64
|
+
if (request.headers.get('rsc') === '1') {
|
|
65
|
+
log('Is rsc request')
|
|
66
|
+
if (matchesRscRoute(url.pathname)) {
|
|
67
|
+
request.headers.set('x-rsc-route', url.pathname)
|
|
68
|
+
const target = rscifyPath(url.pathname)
|
|
69
|
+
log('Rewriting to', target)
|
|
70
|
+
return context.rewrite(target)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertEquals } from 'https://deno.land/std@0.167.0/testing/asserts.ts'
|
|
2
2
|
import { beforeEach, describe, it } from 'https://deno.land/std@0.167.0/testing/bdd.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { redirectTrailingSlash, updateModifiedHeaders } from './utils.ts'
|
|
4
4
|
|
|
5
5
|
describe('updateModifiedHeaders', () => {
|
|
6
6
|
it("does not modify the headers if 'x-middleware-override-headers' is not found", () => {
|
|
@@ -62,3 +62,53 @@ describe('updateModifiedHeaders', () => {
|
|
|
62
62
|
})
|
|
63
63
|
})
|
|
64
64
|
})
|
|
65
|
+
|
|
66
|
+
describe('trailing slash redirects', () => {
|
|
67
|
+
it('adds a trailing slash to the pathn if trailingSlash is enabled', () => {
|
|
68
|
+
const url = new URL('https://example.com/foo')
|
|
69
|
+
const result = redirectTrailingSlash(url, true)
|
|
70
|
+
assertEquals(result?.status, 308)
|
|
71
|
+
assertEquals(result?.headers.get('location'), 'https://example.com/foo/')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("doesn't add a trailing slash if trailingSlash is false", () => {
|
|
75
|
+
const url = new URL('https://example.com/foo')
|
|
76
|
+
const result = redirectTrailingSlash(url, false)
|
|
77
|
+
assertEquals(result, undefined)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("doesn't add a trailing slash if the path is a file", () => {
|
|
81
|
+
const url = new URL('https://example.com/foo.txt')
|
|
82
|
+
const result = redirectTrailingSlash(url, true)
|
|
83
|
+
assertEquals(result, undefined)
|
|
84
|
+
})
|
|
85
|
+
it('adds a trailing slash if there is a dot in the path', () => {
|
|
86
|
+
const url = new URL('https://example.com/foo.bar/baz')
|
|
87
|
+
const result = redirectTrailingSlash(url, true)
|
|
88
|
+
assertEquals(result?.status, 308)
|
|
89
|
+
assertEquals(result?.headers.get('location'), 'https://example.com/foo.bar/baz/')
|
|
90
|
+
})
|
|
91
|
+
it("doesn't add a trailing slash if the path is /", () => {
|
|
92
|
+
const url = new URL('https://example.com/')
|
|
93
|
+
const result = redirectTrailingSlash(url, true)
|
|
94
|
+
assertEquals(result, undefined)
|
|
95
|
+
})
|
|
96
|
+
it('removes a trailing slash from the path if trailingSlash is false', () => {
|
|
97
|
+
const url = new URL('https://example.com/foo/')
|
|
98
|
+
const result = redirectTrailingSlash(url, false)
|
|
99
|
+
assertEquals(result?.status, 308)
|
|
100
|
+
assertEquals(result?.headers.get('location'), 'https://example.com/foo')
|
|
101
|
+
})
|
|
102
|
+
it("doesn't remove a trailing slash if trailingSlash is true", () => {
|
|
103
|
+
const url = new URL('https://example.com/foo/')
|
|
104
|
+
const result = redirectTrailingSlash(url, true)
|
|
105
|
+
assertEquals(result, undefined)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('removes a trailing slash from the path if the path is a file', () => {
|
|
109
|
+
const url = new URL('https://example.com/foo.txt/')
|
|
110
|
+
const result = redirectTrailingSlash(url, false)
|
|
111
|
+
assertEquals(result?.status, 308)
|
|
112
|
+
assertEquals(result?.headers.get('location'), 'https://example.com/foo.txt')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -56,6 +56,37 @@ interface MiddlewareRequest {
|
|
|
56
56
|
rewrite(destination: string | URL, init?: ResponseInit): Response
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface I18NConfig {
|
|
60
|
+
defaultLocale: string
|
|
61
|
+
localeDetection?: false
|
|
62
|
+
locales: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RequestData {
|
|
66
|
+
geo?: {
|
|
67
|
+
city?: string
|
|
68
|
+
country?: string
|
|
69
|
+
region?: string
|
|
70
|
+
latitude?: string
|
|
71
|
+
longitude?: string
|
|
72
|
+
timezone?: string
|
|
73
|
+
}
|
|
74
|
+
headers: Record<string, string>
|
|
75
|
+
ip?: string
|
|
76
|
+
method: string
|
|
77
|
+
nextConfig?: {
|
|
78
|
+
basePath?: string
|
|
79
|
+
i18n?: I18NConfig | null
|
|
80
|
+
trailingSlash?: boolean
|
|
81
|
+
}
|
|
82
|
+
page?: {
|
|
83
|
+
name?: string
|
|
84
|
+
params?: { [key: string]: string }
|
|
85
|
+
}
|
|
86
|
+
url: string
|
|
87
|
+
body?: ReadableStream<Uint8Array>
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
function isMiddlewareRequest(response: Response | MiddlewareRequest): response is MiddlewareRequest {
|
|
60
91
|
return 'originalRequest' in response
|
|
61
92
|
}
|
|
@@ -90,6 +121,34 @@ export function updateModifiedHeaders(requestHeaders: Headers, responseHeaders:
|
|
|
90
121
|
responseHeaders.delete('x-middleware-override-headers')
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
export const buildNextRequest = (
|
|
125
|
+
request: Request,
|
|
126
|
+
context: Context,
|
|
127
|
+
nextConfig?: RequestData['nextConfig'],
|
|
128
|
+
): RequestData => {
|
|
129
|
+
const { url, method, body, headers } = request
|
|
130
|
+
|
|
131
|
+
const { country, subdivision, city, latitude, longitude, timezone } = context.geo
|
|
132
|
+
|
|
133
|
+
const geo: RequestData['geo'] = {
|
|
134
|
+
country: country?.code,
|
|
135
|
+
region: subdivision?.code,
|
|
136
|
+
city,
|
|
137
|
+
latitude: latitude?.toString(),
|
|
138
|
+
longitude: longitude?.toString(),
|
|
139
|
+
timezone,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
headers: Object.fromEntries(headers.entries()),
|
|
144
|
+
geo,
|
|
145
|
+
url,
|
|
146
|
+
method,
|
|
147
|
+
ip: context.ip,
|
|
148
|
+
body: body ?? undefined,
|
|
149
|
+
nextConfig,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
93
152
|
export const buildResponse = async ({
|
|
94
153
|
result,
|
|
95
154
|
request,
|
|
@@ -196,3 +255,19 @@ export const buildResponse = async ({
|
|
|
196
255
|
}
|
|
197
256
|
return res
|
|
198
257
|
}
|
|
258
|
+
|
|
259
|
+
export const redirectTrailingSlash = (url: URL, trailingSlash: boolean): Response | undefined => {
|
|
260
|
+
const { pathname } = url
|
|
261
|
+
if (pathname === '/') {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (!trailingSlash && pathname.endsWith('/')) {
|
|
265
|
+
url.pathname = pathname.slice(0, -1)
|
|
266
|
+
return Response.redirect(url, 308)
|
|
267
|
+
}
|
|
268
|
+
// Add a slash, unless there's a file extension
|
|
269
|
+
if (trailingSlash && !pathname.endsWith('/') && !pathname.split('/').pop()?.includes('.')) {
|
|
270
|
+
url.pathname = `${pathname}/`
|
|
271
|
+
return Response.redirect(url, 308)
|
|
272
|
+
}
|
|
273
|
+
}
|