@netlify/plugin-nextjs 4.21.2 → 4.23.0
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 +23 -16
- package/lib/helpers/files.js +7 -1
- package/lib/helpers/redirects.js +10 -9
- package/lib/helpers/utils.js +16 -1
- package/lib/index.js +1 -5
- package/lib/templates/getHandler.js +1 -1
- package/package.json +6 -5
- package/src/templates/edge/matchers.json +1 -0
- package/src/templates/edge/next-dev.js +1 -1
- package/src/templates/edge/runtime.ts +13 -2
- package/src/templates/edge-shared/next-utils.ts +396 -0
- package/src/templates/{edge → edge-shared}/utils.ts +1 -1
package/lib/helpers/edge.js
CHANGED
|
@@ -44,9 +44,8 @@ const getMiddlewareBundle = async ({ edgeFunctionDefinition, netlifyConfig, }) =
|
|
|
44
44
|
chunks.push(exports);
|
|
45
45
|
return chunks.join('\n');
|
|
46
46
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const stripLookahead = (regex) => regex.replace('^/(?!_next)', '^/');
|
|
47
|
+
const getEdgeTemplatePath = (file) => (0, path_1.join)(__dirname, '..', '..', 'src', 'templates', 'edge', file);
|
|
48
|
+
const copyEdgeSourceFile = ({ file, target, edgeFunctionDir, }) => fs_1.promises.copyFile(getEdgeTemplatePath(file), (0, path_1.join)(edgeFunctionDir, target !== null && target !== void 0 ? target : file));
|
|
50
49
|
const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, netlifyConfig, }) => {
|
|
51
50
|
const name = sanitizeName(edgeFunctionDefinition.name);
|
|
52
51
|
const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, name);
|
|
@@ -61,15 +60,22 @@ const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, net
|
|
|
61
60
|
file: 'runtime.ts',
|
|
62
61
|
target: 'index.ts',
|
|
63
62
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const matchers = [];
|
|
64
|
+
// The v1 middleware manifest has a single regexp, but the v2 has an array of matchers
|
|
65
|
+
if ('regexp' in edgeFunctionDefinition) {
|
|
66
|
+
matchers.push({ regexp: edgeFunctionDefinition.regexp });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
matchers.push(...edgeFunctionDefinition.matchers);
|
|
70
|
+
}
|
|
71
|
+
await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionDir, 'matchers.json'), matchers);
|
|
72
|
+
// We add a defintion for each matching path
|
|
73
|
+
return matchers.map((matcher) => {
|
|
74
|
+
const pattern = matcher.regexp;
|
|
75
|
+
return { function: name, pattern };
|
|
76
|
+
});
|
|
72
77
|
};
|
|
78
|
+
const cleanupEdgeFunctions = ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }) => (0, fs_extra_1.emptyDir)(INTERNAL_EDGE_FUNCTIONS_SRC);
|
|
73
79
|
exports.cleanupEdgeFunctions = cleanupEdgeFunctions;
|
|
74
80
|
const writeDevEdgeFunction = async ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }) => {
|
|
75
81
|
const manifest = {
|
|
@@ -84,10 +90,10 @@ const writeDevEdgeFunction = async ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/ed
|
|
|
84
90
|
const edgeFunctionRoot = (0, path_1.resolve)(INTERNAL_EDGE_FUNCTIONS_SRC);
|
|
85
91
|
await (0, fs_extra_1.emptyDir)(edgeFunctionRoot);
|
|
86
92
|
await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionRoot, 'manifest.json'), manifest);
|
|
93
|
+
await (0, fs_extra_1.copy)(getEdgeTemplatePath('../edge-shared'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared'));
|
|
87
94
|
const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, 'next-dev');
|
|
88
95
|
await (0, fs_extra_1.ensureDir)(edgeFunctionDir);
|
|
89
96
|
await copyEdgeSourceFile({ edgeFunctionDir, file: 'next-dev.js', target: 'index.js' });
|
|
90
|
-
await copyEdgeSourceFile({ edgeFunctionDir, file: 'utils.ts' });
|
|
91
97
|
};
|
|
92
98
|
exports.writeDevEdgeFunction = writeDevEdgeFunction;
|
|
93
99
|
/**
|
|
@@ -100,6 +106,7 @@ const writeEdgeFunctions = async (netlifyConfig) => {
|
|
|
100
106
|
};
|
|
101
107
|
const edgeFunctionRoot = (0, path_1.resolve)('.netlify', 'edge-functions');
|
|
102
108
|
await (0, fs_extra_1.emptyDir)(edgeFunctionRoot);
|
|
109
|
+
await (0, fs_extra_1.copy)(getEdgeTemplatePath('../edge-shared'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared'));
|
|
103
110
|
if (!process.env.NEXT_DISABLE_EDGE_IMAGES) {
|
|
104
111
|
console.log('Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.');
|
|
105
112
|
const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, 'ipx');
|
|
@@ -119,23 +126,23 @@ const writeEdgeFunctions = async (netlifyConfig) => {
|
|
|
119
126
|
}
|
|
120
127
|
for (const middleware of middlewareManifest.sortedMiddleware) {
|
|
121
128
|
const edgeFunctionDefinition = middlewareManifest.middleware[middleware];
|
|
122
|
-
const
|
|
129
|
+
const functionDefinitions = await writeEdgeFunction({
|
|
123
130
|
edgeFunctionDefinition,
|
|
124
131
|
edgeFunctionRoot,
|
|
125
132
|
netlifyConfig,
|
|
126
133
|
});
|
|
127
|
-
manifest.functions.push(
|
|
134
|
+
manifest.functions.push(...functionDefinitions);
|
|
128
135
|
}
|
|
129
136
|
// Older versions of the manifest format don't have the functions field
|
|
130
137
|
// No, the version field was not incremented
|
|
131
138
|
if (typeof middlewareManifest.functions === 'object') {
|
|
132
139
|
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
|
|
133
|
-
const
|
|
140
|
+
const functionDefinitions = await writeEdgeFunction({
|
|
134
141
|
edgeFunctionDefinition,
|
|
135
142
|
edgeFunctionRoot,
|
|
136
143
|
netlifyConfig,
|
|
137
144
|
});
|
|
138
|
-
manifest.functions.push(
|
|
145
|
+
manifest.functions.push(...functionDefinitions);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
}
|
package/lib/helpers/files.js
CHANGED
|
@@ -271,7 +271,13 @@ const getServerFile = (root, includeBase = true) => {
|
|
|
271
271
|
return (0, utils_1.findModuleFromBase)({ candidates, paths: [root] });
|
|
272
272
|
};
|
|
273
273
|
const baseServerReplacements = [
|
|
274
|
-
|
|
274
|
+
// force manual revalidate during cache fetches
|
|
275
|
+
[
|
|
276
|
+
`checkIsManualRevalidate(req, this.renderOpts.previewProps)`,
|
|
277
|
+
`checkIsManualRevalidate(process.env._REVALIDATE_SSG ? { headers: { 'x-prerender-revalidate': this.renderOpts.previewProps.previewModeId } } : req, this.renderOpts.previewProps)`,
|
|
278
|
+
],
|
|
279
|
+
// ensure ISR 404 pages send the correct SWR cache headers
|
|
280
|
+
[`private: isPreviewMode || is404Page && cachedData`, `private: isPreviewMode && cachedData`],
|
|
275
281
|
];
|
|
276
282
|
const nextServerReplacements = [
|
|
277
283
|
[
|
package/lib/helpers/redirects.js
CHANGED
|
@@ -9,6 +9,12 @@ const constants_1 = require("../constants");
|
|
|
9
9
|
const files_1 = require("./files");
|
|
10
10
|
const utils_1 = require("./utils");
|
|
11
11
|
const matchesMiddleware = (middleware, route) => middleware.some((middlewarePath) => route.startsWith(middlewarePath));
|
|
12
|
+
const generateHiddenPathRedirects = ({ basePath }) => constants_1.HIDDEN_PATHS.map((path) => ({
|
|
13
|
+
from: `${basePath}${path}`,
|
|
14
|
+
to: '/404.html',
|
|
15
|
+
status: 404,
|
|
16
|
+
force: true,
|
|
17
|
+
}));
|
|
12
18
|
const generateLocaleRedirects = ({ i18n, basePath, trailingSlash, }) => {
|
|
13
19
|
const redirects = [];
|
|
14
20
|
// If the cookie is set, we need to redirect at the origin
|
|
@@ -78,7 +84,7 @@ const generateStaticIsrRewrites = ({ staticRouteEntries, basePath, i18n, buildId
|
|
|
78
84
|
const staticRoutePaths = new Set();
|
|
79
85
|
const staticIsrRewrites = [];
|
|
80
86
|
staticRouteEntries.forEach(([route, { initialRevalidateSeconds }]) => {
|
|
81
|
-
if ((0, utils_1.isApiRoute)(route)) {
|
|
87
|
+
if ((0, utils_1.isApiRoute)(route) || (0, utils_1.is404Route)(route, i18n)) {
|
|
82
88
|
return;
|
|
83
89
|
}
|
|
84
90
|
staticRoutePaths.add(route);
|
|
@@ -125,7 +131,7 @@ const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, midd
|
|
|
125
131
|
const dynamicRewrites = [];
|
|
126
132
|
const dynamicRoutesThatMatchMiddleware = [];
|
|
127
133
|
dynamicRoutes.forEach((route) => {
|
|
128
|
-
if ((0, utils_1.isApiRoute)(route.page)) {
|
|
134
|
+
if ((0, utils_1.isApiRoute)(route.page) || (0, utils_1.is404Route)(route.page, i18n)) {
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
137
|
if (route.page in prerenderedDynamicRoutes) {
|
|
@@ -149,12 +155,7 @@ const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, midd
|
|
|
149
155
|
const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId, }) => {
|
|
150
156
|
const { dynamicRoutes: prerenderedDynamicRoutes, routes: prerenderedStaticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'prerender-manifest.json'));
|
|
151
157
|
const { dynamicRoutes, staticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'routes-manifest.json'));
|
|
152
|
-
netlifyConfig.redirects.push(...
|
|
153
|
-
from: `${basePath}${path}`,
|
|
154
|
-
to: '/404.html',
|
|
155
|
-
status: 404,
|
|
156
|
-
force: true,
|
|
157
|
-
})));
|
|
158
|
+
netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath }));
|
|
158
159
|
if (i18n && i18n.localeDetection !== false) {
|
|
159
160
|
netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }));
|
|
160
161
|
}
|
|
@@ -179,7 +180,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
|
|
|
179
180
|
netlifyConfig.redirects.push(...staticIsrRewrites);
|
|
180
181
|
// Add rewrites for all static SSR routes. This is Next 12+
|
|
181
182
|
staticRoutes === null || staticRoutes === void 0 ? void 0 : staticRoutes.forEach((route) => {
|
|
182
|
-
if (staticRoutePaths.has(route.page) || (0, utils_1.isApiRoute)(route.page)) {
|
|
183
|
+
if (staticRoutePaths.has(route.page) || (0, utils_1.isApiRoute)(route.page) || (0, utils_1.is404Route)(route.page)) {
|
|
183
184
|
// Prerendered static routes are either handled by the CDN or are ISR
|
|
184
185
|
return;
|
|
185
186
|
}
|
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.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNextRoute = exports.isApiRoute = exports.routeToDataRoute = exports.netlifyRoutesForNextRouteWithData = exports.toNetlifyRoute = void 0;
|
|
6
|
+
exports.getRemotePatterns = exports.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNextRoute = exports.is404Route = exports.isApiRoute = exports.routeToDataRoute = exports.netlifyRoutesForNextRouteWithData = exports.toNetlifyRoute = void 0;
|
|
7
7
|
const globby_1 = __importDefault(require("globby"));
|
|
8
8
|
const pathe_1 = require("pathe");
|
|
9
9
|
const constants_1 = require("../constants");
|
|
@@ -63,6 +63,8 @@ const netlifyRoutesForNextRoute = (route, buildId, i18n) => {
|
|
|
63
63
|
};
|
|
64
64
|
const isApiRoute = (route) => route.startsWith('/api/') || route === '/api';
|
|
65
65
|
exports.isApiRoute = isApiRoute;
|
|
66
|
+
const is404Route = (route, i18n) => i18n ? i18n.locales.some((locale) => route === `/${locale}/404`) : route === '/404';
|
|
67
|
+
exports.is404Route = is404Route;
|
|
66
68
|
const redirectsForNextRoute = ({ route, buildId, basePath, to, i18n, status = 200, force = false, }) => netlifyRoutesForNextRoute(route, buildId, i18n).map((redirect) => ({
|
|
67
69
|
from: `${basePath}${redirect}`,
|
|
68
70
|
to,
|
|
@@ -156,4 +158,17 @@ const getCustomImageResponseHeaders = (headers) => {
|
|
|
156
158
|
exports.getCustomImageResponseHeaders = getCustomImageResponseHeaders;
|
|
157
159
|
const isBundleSizeCheckDisabled = () => process.env.DISABLE_BUNDLE_ZIP_SIZE_CHECK === '1' || process.env.DISABLE_BUNDLE_ZIP_SIZE_CHECK === 'true';
|
|
158
160
|
exports.isBundleSizeCheckDisabled = isBundleSizeCheckDisabled;
|
|
161
|
+
const getRemotePatterns = (experimental, images) => {
|
|
162
|
+
var _a;
|
|
163
|
+
// Where remote patterns is configured pre-v12.2.5
|
|
164
|
+
if ((_a = experimental.images) === null || _a === void 0 ? void 0 : _a.remotePatterns) {
|
|
165
|
+
return experimental.images.remotePatterns;
|
|
166
|
+
}
|
|
167
|
+
// Where remote patterns is configured after v12.2.5
|
|
168
|
+
if (images.remotePatterns) {
|
|
169
|
+
return images.remotePatterns || [];
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
};
|
|
173
|
+
exports.getRemotePatterns = getRemotePatterns;
|
|
159
174
|
/* eslint-enable max-lines */
|
package/lib/index.js
CHANGED
|
@@ -40,7 +40,6 @@ const plugin = {
|
|
|
40
40
|
}
|
|
41
41
|
const { publish } = netlifyConfig.build;
|
|
42
42
|
(0, verification_1.checkNextSiteHasBuilt)({ publish, failBuild });
|
|
43
|
-
let experimentalRemotePatterns = [];
|
|
44
43
|
const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental } = await (0, config_1.getNextConfig)({
|
|
45
44
|
publish,
|
|
46
45
|
failBuild,
|
|
@@ -68,9 +67,6 @@ const plugin = {
|
|
|
68
67
|
`));
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
|
-
if (experimental.images) {
|
|
72
|
-
experimentalRemotePatterns = experimental.images.remotePatterns || [];
|
|
73
|
-
}
|
|
74
70
|
if ((0, utils_1.isNextAuthInstalled)()) {
|
|
75
71
|
const config = await (0, config_1.getRequiredServerFiles)(publish);
|
|
76
72
|
const userDefinedNextAuthUrl = config.config.env.NEXTAUTH_URL;
|
|
@@ -102,7 +98,7 @@ const plugin = {
|
|
|
102
98
|
imageconfig: images,
|
|
103
99
|
netlifyConfig,
|
|
104
100
|
basePath,
|
|
105
|
-
remotePatterns:
|
|
101
|
+
remotePatterns: (0, utils_1.getRemotePatterns)(experimental, images),
|
|
106
102
|
responseHeaders: (0, utils_1.getCustomImageResponseHeaders)(netlifyConfig.headers),
|
|
107
103
|
});
|
|
108
104
|
await (0, redirects_1.generateRedirects)({
|
|
@@ -32,7 +32,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
|
|
|
32
32
|
conf.experimental.isrFlushToDisk = false;
|
|
33
33
|
// This is our flag that we use when patching the source
|
|
34
34
|
// eslint-disable-next-line no-underscore-dangle
|
|
35
|
-
process.env.
|
|
35
|
+
process.env._REVALIDATE_SSG = 'true';
|
|
36
36
|
for (const [key, value] of Object.entries(conf.env)) {
|
|
37
37
|
process.env[key] = String(value);
|
|
38
38
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/plugin-nextjs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.23.0",
|
|
4
4
|
"description": "Run Next.js seamlessly on Netlify",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"lib/**/*",
|
|
8
8
|
"src/templates/edge/*",
|
|
9
|
+
"src/templates/edge-shared/*",
|
|
9
10
|
"manifest.yml"
|
|
10
11
|
],
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@netlify/esbuild": "0.14.25",
|
|
13
14
|
"@netlify/functions": "^1.2.0",
|
|
14
|
-
"@netlify/ipx": "^1.2.
|
|
15
|
+
"@netlify/ipx": "^1.2.5",
|
|
15
16
|
"@vercel/node-bridge": "^2.1.0",
|
|
16
17
|
"chalk": "^4.1.2",
|
|
17
18
|
"execa": "^5.1.1",
|
|
@@ -31,12 +32,12 @@
|
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@delucis/if-env": "^1.1.2",
|
|
34
|
-
"@netlify/build": "^27.
|
|
35
|
+
"@netlify/build": "^27.17.2",
|
|
35
36
|
"@types/fs-extra": "^9.0.13",
|
|
36
37
|
"@types/jest": "^27.4.1",
|
|
37
38
|
"@types/merge-stream": "^1.1.2",
|
|
38
39
|
"@types/node": "^17.0.25",
|
|
39
|
-
"next": "^12.
|
|
40
|
+
"next": "^12.3.0",
|
|
40
41
|
"npm-run-all": "^4.1.5",
|
|
41
42
|
"typescript": "^4.6.3"
|
|
42
43
|
},
|
|
@@ -62,4 +63,4 @@
|
|
|
62
63
|
"engines": {
|
|
63
64
|
"node": ">=12.0.0"
|
|
64
65
|
}
|
|
65
|
-
}
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest } from 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/request.js'
|
|
2
2
|
import { NextResponse } from 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/response.js'
|
|
3
3
|
import { fromFileUrl } from 'https://deno.land/std@0.151.0/path/mod.ts'
|
|
4
|
-
import { buildResponse } from '
|
|
4
|
+
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)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Context } from 'https://edge.netlify.com'
|
|
2
|
-
|
|
2
|
+
// Available at build time
|
|
3
|
+
import matchers from './matchers.json' assert { type: 'json' }
|
|
3
4
|
import edgeFunction from './bundle.js'
|
|
4
|
-
import { buildResponse } from '
|
|
5
|
+
import { buildResponse } from '../edge-shared/utils.ts'
|
|
6
|
+
import { getMiddlewareRouteMatcher, MiddlewareRouteMatch, searchParamsToUrlQuery } from '../edge-shared/next-utils.ts'
|
|
7
|
+
|
|
8
|
+
const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
|
|
5
9
|
|
|
6
10
|
export interface FetchEventResult {
|
|
7
11
|
response: Response
|
|
@@ -49,8 +53,15 @@ const handler = async (req: Request, context: Context) => {
|
|
|
49
53
|
// Don't run in dev
|
|
50
54
|
return
|
|
51
55
|
}
|
|
56
|
+
|
|
52
57
|
const url = new URL(req.url)
|
|
53
58
|
|
|
59
|
+
// While we have already checked the path when mapping to the edge function,
|
|
60
|
+
// Next.js supports extra rules that we need to check here too.
|
|
61
|
+
if (!matchesMiddleware(url.pathname, req, searchParamsToUrlQuery(url.searchParams))) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
const geo = {
|
|
55
66
|
country: context.geo.country?.code,
|
|
56
67
|
region: context.geo.subdivision?.code,
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Various router utils ported to Deno from Next.js source
|
|
3
|
+
* https://github.com/vercel/next.js/blob/7280c3ced186bb9a7ae3d7012613ef93f20b0fa9/packages/next/shared/lib/router/utils/
|
|
4
|
+
* Licence: https://github.com/vercel/next.js/blob/7280c3ced186bb9a7ae3d7012613ef93f20b0fa9/license.md
|
|
5
|
+
*
|
|
6
|
+
* Some types have been re-implemented to be more compatible with Deno or avoid chains of dependent files
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Deno imports
|
|
10
|
+
import type { Key } from 'https://deno.land/x/path_to_regexp@v6.2.1/index.ts'
|
|
11
|
+
|
|
12
|
+
import { compile, pathToRegexp } from 'https://deno.land/x/path_to_regexp@v6.2.1/index.ts'
|
|
13
|
+
import { getCookies } from 'https://deno.land/std@0.148.0/http/cookie.ts'
|
|
14
|
+
|
|
15
|
+
// Inlined/re-implemented types
|
|
16
|
+
|
|
17
|
+
export interface ParsedUrlQuery {
|
|
18
|
+
[key: string]: string | string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Params {
|
|
22
|
+
// Yeah, best we get
|
|
23
|
+
// deno-lint-ignore no-explicit-any
|
|
24
|
+
[param: string]: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RouteHas =
|
|
28
|
+
| {
|
|
29
|
+
type: 'header' | 'query' | 'cookie'
|
|
30
|
+
key: string
|
|
31
|
+
value?: string
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: 'host'
|
|
35
|
+
key?: undefined
|
|
36
|
+
value: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Rewrite = {
|
|
40
|
+
source: string
|
|
41
|
+
destination: string
|
|
42
|
+
basePath?: false
|
|
43
|
+
locale?: false
|
|
44
|
+
has?: RouteHas[]
|
|
45
|
+
regex: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type Header = {
|
|
49
|
+
source: string
|
|
50
|
+
basePath?: false
|
|
51
|
+
locale?: false
|
|
52
|
+
headers: Array<{ key: string; value: string }>
|
|
53
|
+
has?: RouteHas[]
|
|
54
|
+
regex: string
|
|
55
|
+
}
|
|
56
|
+
export type Redirect = {
|
|
57
|
+
source: string
|
|
58
|
+
destination: string
|
|
59
|
+
basePath?: false
|
|
60
|
+
locale?: false
|
|
61
|
+
has?: RouteHas[]
|
|
62
|
+
statusCode?: number
|
|
63
|
+
permanent?: boolean
|
|
64
|
+
regex: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type DynamicRoute = {
|
|
68
|
+
page: string
|
|
69
|
+
regex: string
|
|
70
|
+
namedRegex?: string
|
|
71
|
+
routeKeys?: { [key: string]: string }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type RoutesManifest = {
|
|
75
|
+
basePath: string
|
|
76
|
+
redirects: Redirect[]
|
|
77
|
+
headers: Header[]
|
|
78
|
+
rewrites: {
|
|
79
|
+
beforeFiles: Rewrite[]
|
|
80
|
+
afterFiles: Rewrite[]
|
|
81
|
+
fallback: Rewrite[]
|
|
82
|
+
}
|
|
83
|
+
dynamicRoutes: DynamicRoute[]
|
|
84
|
+
}
|
|
85
|
+
// escape-regexp.ts
|
|
86
|
+
// regexp is based on https://github.com/sindresorhus/escape-string-regexp
|
|
87
|
+
const reHasRegExp = /[|\\{}()[\]^$+*?.-]/
|
|
88
|
+
const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g
|
|
89
|
+
|
|
90
|
+
export function escapeStringRegexp(str: string) {
|
|
91
|
+
// see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23
|
|
92
|
+
if (reHasRegExp.test(str)) {
|
|
93
|
+
return str.replace(reReplaceRegExp, '\\$&')
|
|
94
|
+
}
|
|
95
|
+
return str
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// querystring.ts
|
|
99
|
+
export function searchParamsToUrlQuery(searchParams: URLSearchParams): ParsedUrlQuery {
|
|
100
|
+
const query: ParsedUrlQuery = {}
|
|
101
|
+
searchParams.forEach((value, key) => {
|
|
102
|
+
if (typeof query[key] === 'undefined') {
|
|
103
|
+
query[key] = value
|
|
104
|
+
} else if (Array.isArray(query[key])) {
|
|
105
|
+
;(query[key] as string[]).push(value)
|
|
106
|
+
} else {
|
|
107
|
+
query[key] = [query[key] as string, value]
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
return query
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// parse-url.ts
|
|
114
|
+
interface ParsedUrl {
|
|
115
|
+
hash: string
|
|
116
|
+
hostname?: string | null
|
|
117
|
+
href: string
|
|
118
|
+
pathname: string
|
|
119
|
+
port?: string | null
|
|
120
|
+
protocol?: string | null
|
|
121
|
+
query: ParsedUrlQuery
|
|
122
|
+
search: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseUrl(url: string): ParsedUrl {
|
|
126
|
+
const parsedURL = url.startsWith('/') ? new URL(url, 'http://n') : new URL(url)
|
|
127
|
+
return {
|
|
128
|
+
hash: parsedURL.hash,
|
|
129
|
+
hostname: parsedURL.hostname,
|
|
130
|
+
href: parsedURL.href,
|
|
131
|
+
pathname: parsedURL.pathname,
|
|
132
|
+
port: parsedURL.port,
|
|
133
|
+
protocol: parsedURL.protocol,
|
|
134
|
+
query: searchParamsToUrlQuery(parsedURL.searchParams),
|
|
135
|
+
search: parsedURL.search,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// prepare-destination.ts
|
|
140
|
+
// Changed to use WHATWG Fetch Request instead of IncomingMessage
|
|
141
|
+
export function matchHas(req: Pick<Request, 'headers' | 'url'>, has: RouteHas[], query: Params): false | Params {
|
|
142
|
+
const params: Params = {}
|
|
143
|
+
const cookies = getCookies(req.headers)
|
|
144
|
+
const url = new URL(req.url)
|
|
145
|
+
const allMatch = has.every((hasItem) => {
|
|
146
|
+
let value: undefined | string | null
|
|
147
|
+
let key = hasItem.key
|
|
148
|
+
|
|
149
|
+
switch (hasItem.type) {
|
|
150
|
+
case 'header': {
|
|
151
|
+
key = hasItem.key.toLowerCase()
|
|
152
|
+
value = req.headers.get(key)
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
case 'cookie': {
|
|
156
|
+
value = cookies[hasItem.key]
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
case 'query': {
|
|
160
|
+
value = query[hasItem.key]
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
case 'host': {
|
|
164
|
+
value = url.hostname
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
default: {
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!hasItem.value && value && key) {
|
|
172
|
+
params[getSafeParamName(key)] = value
|
|
173
|
+
return true
|
|
174
|
+
} else if (value) {
|
|
175
|
+
const matcher = new RegExp(`^${hasItem.value}$`)
|
|
176
|
+
const matches = Array.isArray(value) ? value.slice(-1)[0].match(matcher) : value.match(matcher)
|
|
177
|
+
|
|
178
|
+
if (matches) {
|
|
179
|
+
if (Array.isArray(matches)) {
|
|
180
|
+
if (matches.groups) {
|
|
181
|
+
Object.keys(matches.groups).forEach((groupKey) => {
|
|
182
|
+
params[groupKey] = matches.groups![groupKey]
|
|
183
|
+
})
|
|
184
|
+
} else if (hasItem.type === 'host' && matches[0]) {
|
|
185
|
+
params.host = matches[0]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (allMatch) {
|
|
195
|
+
return params
|
|
196
|
+
}
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function compileNonPath(value: string, params: Params): string {
|
|
201
|
+
if (!value.includes(':')) {
|
|
202
|
+
return value
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const key of Object.keys(params)) {
|
|
206
|
+
if (value.includes(`:${key}`)) {
|
|
207
|
+
value = value
|
|
208
|
+
.replace(new RegExp(`:${key}\\*`, 'g'), `:${key}--ESCAPED_PARAM_ASTERISKS`)
|
|
209
|
+
.replace(new RegExp(`:${key}\\?`, 'g'), `:${key}--ESCAPED_PARAM_QUESTION`)
|
|
210
|
+
.replace(new RegExp(`:${key}\\+`, 'g'), `:${key}--ESCAPED_PARAM_PLUS`)
|
|
211
|
+
.replace(new RegExp(`:${key}(?!\\w)`, 'g'), `--ESCAPED_PARAM_COLON${key}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
value = value
|
|
215
|
+
.replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1')
|
|
216
|
+
.replace(/--ESCAPED_PARAM_PLUS/g, '+')
|
|
217
|
+
.replace(/--ESCAPED_PARAM_COLON/g, ':')
|
|
218
|
+
.replace(/--ESCAPED_PARAM_QUESTION/g, '?')
|
|
219
|
+
.replace(/--ESCAPED_PARAM_ASTERISKS/g, '*')
|
|
220
|
+
// the value needs to start with a forward-slash to be compiled
|
|
221
|
+
// correctly
|
|
222
|
+
return compile(`/${value}`, { validate: false })(params).slice(1)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function prepareDestination(args: {
|
|
226
|
+
appendParamsToQuery: boolean
|
|
227
|
+
destination: string
|
|
228
|
+
params: Params
|
|
229
|
+
query: ParsedUrlQuery
|
|
230
|
+
}) {
|
|
231
|
+
const query = Object.assign({}, args.query)
|
|
232
|
+
delete query.__nextLocale
|
|
233
|
+
delete query.__nextDefaultLocale
|
|
234
|
+
delete query.__nextDataReq
|
|
235
|
+
|
|
236
|
+
let escapedDestination = args.destination
|
|
237
|
+
|
|
238
|
+
for (const param of Object.keys({ ...args.params, ...query })) {
|
|
239
|
+
escapedDestination = escapeSegment(escapedDestination, param)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const parsedDestination: ParsedUrl = parseUrl(escapedDestination)
|
|
243
|
+
const destQuery = parsedDestination.query
|
|
244
|
+
const destPath = unescapeSegments(`${parsedDestination.pathname!}${parsedDestination.hash || ''}`)
|
|
245
|
+
const destHostname = unescapeSegments(parsedDestination.hostname || '')
|
|
246
|
+
const destPathParamKeys: Key[] = []
|
|
247
|
+
const destHostnameParamKeys: Key[] = []
|
|
248
|
+
pathToRegexp(destPath, destPathParamKeys)
|
|
249
|
+
pathToRegexp(destHostname, destHostnameParamKeys)
|
|
250
|
+
|
|
251
|
+
const destParams: (string | number)[] = []
|
|
252
|
+
|
|
253
|
+
destPathParamKeys.forEach((key) => destParams.push(key.name))
|
|
254
|
+
destHostnameParamKeys.forEach((key) => destParams.push(key.name))
|
|
255
|
+
|
|
256
|
+
const destPathCompiler = compile(
|
|
257
|
+
destPath,
|
|
258
|
+
// we don't validate while compiling the destination since we should
|
|
259
|
+
// have already validated before we got to this point and validating
|
|
260
|
+
// breaks compiling destinations with named pattern params from the source
|
|
261
|
+
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
|
|
262
|
+
// since compile validation is meant for reversing and not for inserting
|
|
263
|
+
// params from a separate path-regex into another
|
|
264
|
+
{ validate: false },
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const destHostnameCompiler = compile(destHostname, { validate: false })
|
|
268
|
+
|
|
269
|
+
// update any params in query values
|
|
270
|
+
for (const [key, strOrArray] of Object.entries(destQuery)) {
|
|
271
|
+
// the value needs to start with a forward-slash to be compiled
|
|
272
|
+
// correctly
|
|
273
|
+
if (Array.isArray(strOrArray)) {
|
|
274
|
+
destQuery[key] = strOrArray.map((value) => compileNonPath(unescapeSegments(value), args.params))
|
|
275
|
+
} else {
|
|
276
|
+
destQuery[key] = compileNonPath(unescapeSegments(strOrArray), args.params)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// add path params to query if it's not a redirect and not
|
|
281
|
+
// already defined in destination query or path
|
|
282
|
+
const paramKeys = Object.keys(args.params).filter((name) => name !== 'nextInternalLocale')
|
|
283
|
+
|
|
284
|
+
if (args.appendParamsToQuery && !paramKeys.some((key) => destParams.includes(key))) {
|
|
285
|
+
for (const key of paramKeys) {
|
|
286
|
+
if (!(key in destQuery)) {
|
|
287
|
+
destQuery[key] = args.params[key]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let newUrl
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
newUrl = destPathCompiler(args.params)
|
|
296
|
+
|
|
297
|
+
const [pathname, hash] = newUrl.split('#')
|
|
298
|
+
parsedDestination.hostname = destHostnameCompiler(args.params)
|
|
299
|
+
parsedDestination.pathname = pathname
|
|
300
|
+
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
|
|
301
|
+
delete (parsedDestination as any).search
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://nextjs.org/docs/messages/invalid-multi-match`,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
throw err
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Query merge order lowest priority to highest
|
|
312
|
+
// 1. initial URL query values
|
|
313
|
+
// 2. path segment values
|
|
314
|
+
// 3. destination specified query values
|
|
315
|
+
parsedDestination.query = {
|
|
316
|
+
...query,
|
|
317
|
+
...parsedDestination.query,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
newUrl,
|
|
322
|
+
destQuery,
|
|
323
|
+
parsedDestination,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Ensure only a-zA-Z are used for param names for proper interpolating
|
|
329
|
+
* with path-to-regexp
|
|
330
|
+
*/
|
|
331
|
+
function getSafeParamName(paramName: string) {
|
|
332
|
+
let newParamName = ''
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < paramName.length; i++) {
|
|
335
|
+
const charCode = paramName.charCodeAt(i)
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
(charCode > 64 && charCode < 91) || // A-Z
|
|
339
|
+
(charCode > 96 && charCode < 123) // a-z
|
|
340
|
+
) {
|
|
341
|
+
newParamName += paramName[i]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return newParamName
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function escapeSegment(str: string, segmentName: string) {
|
|
348
|
+
return str.replace(new RegExp(`:${escapeStringRegexp(segmentName)}`, 'g'), `__ESC_COLON_${segmentName}`)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function unescapeSegments(str: string) {
|
|
352
|
+
return str.replace(/__ESC_COLON_/gi, ':')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// is-dynamic.ts
|
|
356
|
+
// Identify /[param]/ in route string
|
|
357
|
+
const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/
|
|
358
|
+
|
|
359
|
+
export function isDynamicRoute(route: string): boolean {
|
|
360
|
+
return TEST_ROUTE.test(route)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// packages/next/shared/lib/router/utils/middleware-route-matcher.ts
|
|
364
|
+
// 12.3 middleware route matcher
|
|
365
|
+
|
|
366
|
+
export interface MiddlewareRouteMatch {
|
|
367
|
+
(pathname: string | null | undefined, request: Pick<Request, 'headers' | 'url'>, query: Params): boolean
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface MiddlewareMatcher {
|
|
371
|
+
regexp: string
|
|
372
|
+
locale?: false
|
|
373
|
+
has?: RouteHas[]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function getMiddlewareRouteMatcher(matchers: MiddlewareMatcher[]): MiddlewareRouteMatch {
|
|
377
|
+
return (pathname: string | null | undefined, req: Pick<Request, 'headers' | 'url'>, query: Params) => {
|
|
378
|
+
for (const matcher of matchers) {
|
|
379
|
+
const routeMatch = new RegExp(matcher.regexp).exec(pathname!)
|
|
380
|
+
if (!routeMatch) {
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (matcher.has) {
|
|
385
|
+
const hasParams = matchHas(req, matcher.has, query)
|
|
386
|
+
if (!hasParams) {
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return true
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -83,7 +83,7 @@ export const buildResponse = async ({
|
|
|
83
83
|
const transformed = response.dataTransforms.reduce((prev, transform) => {
|
|
84
84
|
return transform(prev)
|
|
85
85
|
}, props)
|
|
86
|
-
return
|
|
86
|
+
return new Response(JSON.stringify(transformed), response)
|
|
87
87
|
}
|
|
88
88
|
// This var will hold the contents of the script tag
|
|
89
89
|
let buffer = ''
|