@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.
@@ -44,9 +44,8 @@ const getMiddlewareBundle = async ({ edgeFunctionDefinition, netlifyConfig, }) =
44
44
  chunks.push(exports);
45
45
  return chunks.join('\n');
46
46
  };
47
- const copyEdgeSourceFile = ({ file, target, edgeFunctionDir, }) => fs_1.promises.copyFile((0, path_1.join)(__dirname, '..', '..', 'src', 'templates', 'edge', file), (0, path_1.join)(edgeFunctionDir, target !== null && target !== void 0 ? target : file));
48
- // Edge functions don't support lookahead expressions
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
- await copyEdgeSourceFile({ edgeFunctionDir, file: 'utils.ts' });
65
- return {
66
- function: name,
67
- pattern: stripLookahead(edgeFunctionDefinition.regexp),
68
- };
69
- };
70
- const cleanupEdgeFunctions = async ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }) => {
71
- await (0, fs_extra_1.emptyDir)(INTERNAL_EDGE_FUNCTIONS_SRC);
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 functionDefinition = await writeEdgeFunction({
129
+ const functionDefinitions = await writeEdgeFunction({
123
130
  edgeFunctionDefinition,
124
131
  edgeFunctionRoot,
125
132
  netlifyConfig,
126
133
  });
127
- manifest.functions.push(functionDefinition);
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 functionDefinition = await writeEdgeFunction({
140
+ const functionDefinitions = await writeEdgeFunction({
134
141
  edgeFunctionDefinition,
135
142
  edgeFunctionRoot,
136
143
  netlifyConfig,
137
144
  });
138
- manifest.functions.push(functionDefinition);
145
+ manifest.functions.push(...functionDefinitions);
139
146
  }
140
147
  }
141
148
  }
@@ -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
- [`let ssgCacheKey = `, `let ssgCacheKey = process.env._BYPASS_SSG || `],
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
  [
@@ -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(...constants_1.HIDDEN_PATHS.map((path) => ({
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
  }
@@ -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: experimentalRemotePatterns,
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._BYPASS_SSG = 'true';
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.21.2",
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.4",
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.16.1",
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.2.0",
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 './utils.ts'
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 './utils.ts'
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 context.json(transformed)
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 = ''