@netlify/plugin-nextjs 5.0.0-beta.0 → 5.0.0-beta.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.
@@ -8,7 +8,7 @@ import {
8
8
  copyNextDependencies,
9
9
  copyNextServerCode,
10
10
  writeTagsManifest
11
- } from "../../esm-chunks/chunk-G2VRYWGL.js";
11
+ } from "../../esm-chunks/chunk-A22224GM.js";
12
12
  import "../../esm-chunks/chunk-AVWFCGVE.js";
13
13
  import "../../esm-chunks/chunk-RSKIKBZH.js";
14
14
  export {
@@ -9,7 +9,7 @@ import {
9
9
  copyStaticContent,
10
10
  publishStaticDir,
11
11
  unpublishStaticDir
12
- } from "../../esm-chunks/chunk-62KDS27E.js";
12
+ } from "../../esm-chunks/chunk-Z7ZMLVTM.js";
13
13
  import "../../esm-chunks/chunk-AVWFCGVE.js";
14
14
  import "../../esm-chunks/chunk-RSKIKBZH.js";
15
15
  export {
@@ -6,8 +6,8 @@
6
6
 
7
7
  import {
8
8
  createEdgeHandlers
9
- } from "../../esm-chunks/chunk-XD5TSLZO.js";
10
- import "../../esm-chunks/chunk-HPGTYMVD.js";
9
+ } from "../../esm-chunks/chunk-UXLNY5XK.js";
10
+ import "../../esm-chunks/chunk-VP3PT3VV.js";
11
11
  import "../../esm-chunks/chunk-RSKIKBZH.js";
12
12
  export {
13
13
  createEdgeHandlers
@@ -6,10 +6,10 @@
6
6
 
7
7
  import {
8
8
  createServerHandler
9
- } from "../../esm-chunks/chunk-PEBUKFKH.js";
10
- import "../../esm-chunks/chunk-G2VRYWGL.js";
9
+ } from "../../esm-chunks/chunk-NOX2JUQZ.js";
10
+ import "../../esm-chunks/chunk-A22224GM.js";
11
11
  import "../../esm-chunks/chunk-AVWFCGVE.js";
12
- import "../../esm-chunks/chunk-HPGTYMVD.js";
12
+ import "../../esm-chunks/chunk-VP3PT3VV.js";
13
13
  import "../../esm-chunks/chunk-RSKIKBZH.js";
14
14
  export {
15
15
  createServerHandler
@@ -8,7 +8,7 @@ import {
8
8
  EDGE_HANDLER_NAME,
9
9
  PluginContext,
10
10
  SERVER_HANDLER_NAME
11
- } from "../esm-chunks/chunk-HPGTYMVD.js";
11
+ } from "../esm-chunks/chunk-VP3PT3VV.js";
12
12
  import "../esm-chunks/chunk-RSKIKBZH.js";
13
13
  export {
14
14
  EDGE_HANDLER_NAME,
@@ -382,6 +382,9 @@ var Store = class _Store {
382
382
  };
383
383
  }
384
384
  static validateKey(key) {
385
+ if (key === "") {
386
+ throw new Error("Blob key must not be empty.");
387
+ }
385
388
  if (key.startsWith("/") || key.startsWith("%2F")) {
386
389
  throw new Error("Blob key must not start with forward slash (/).");
387
390
  }
@@ -532,9 +535,9 @@ var adjustDateHeader = async (headers, request) => {
532
535
  headers.set("x-nextjs-date", headers.get("date") ?? lastModifiedDate.toUTCString());
533
536
  headers.set("date", lastModifiedDate.toUTCString());
534
537
  };
535
- var setCacheControlHeaders = (headers) => {
538
+ var setCacheControlHeaders = (headers, request) => {
536
539
  const cacheControl = headers.get("cache-control");
537
- if (cacheControl !== null && !headers.has("cdn-cache-control") && !headers.has("netlify-cdn-cache-control")) {
540
+ if (cacheControl !== null && ["GET", "HEAD"].includes(request.method) && !headers.has("cdn-cache-control") && !headers.has("netlify-cdn-cache-control")) {
538
541
  const privateCacheControl = omitHeaderValues(cacheControl, [
539
542
  "s-maxage",
540
543
  "stale-while-revalidate"
@@ -25,13 +25,17 @@ var copyNextServerCode = async (ctx) => {
25
25
  });
26
26
  await Promise.all(
27
27
  paths.map(async (path) => {
28
+ const srcPath = join(srcDir, path);
28
29
  const destPath = join(destDir, path);
29
30
  if (path === "server/middleware-manifest.json") {
30
- await mkdir(dirname(destPath), { recursive: true });
31
- await writeFile(destPath, getEmptyMiddlewareManifest());
31
+ try {
32
+ await replaceMiddlewareManifest(srcPath, destPath);
33
+ } catch (error) {
34
+ throw new Error("Could not patch middleware manifest file", { cause: error });
35
+ }
32
36
  return;
33
37
  }
34
- await cp(join(srcDir, path), destPath, { recursive: true });
38
+ await cp(srcPath, destPath, { recursive: true });
35
39
  })
36
40
  );
37
41
  };
@@ -90,14 +94,16 @@ var writeTagsManifest = async (ctx) => {
90
94
  "utf-8"
91
95
  );
92
96
  };
93
- var getEmptyMiddlewareManifest = () => {
94
- const manifest = {
95
- sortedMiddleware: [],
96
- middleware: {},
97
- functions: {},
98
- version: 2
97
+ var replaceMiddlewareManifest = async (sourcePath, destPath) => {
98
+ await mkdir(dirname(destPath), { recursive: true });
99
+ const data = await readFile(sourcePath, "utf8");
100
+ const manifest = JSON.parse(data);
101
+ const newManifest = {
102
+ ...manifest,
103
+ middleware: {}
99
104
  };
100
- return JSON.stringify(manifest);
105
+ const newData = JSON.stringify(newManifest);
106
+ await writeFile(destPath, newData);
101
107
  };
102
108
 
103
109
  export {
@@ -8,13 +8,13 @@ import {
8
8
  copyNextDependencies,
9
9
  copyNextServerCode,
10
10
  writeTagsManifest
11
- } from "./chunk-G2VRYWGL.js";
11
+ } from "./chunk-A22224GM.js";
12
12
  import {
13
13
  require_out
14
14
  } from "./chunk-AVWFCGVE.js";
15
15
  import {
16
16
  SERVER_HANDLER_NAME
17
- } from "./chunk-HPGTYMVD.js";
17
+ } from "./chunk-VP3PT3VV.js";
18
18
  import {
19
19
  __toESM
20
20
  } from "./chunk-RSKIKBZH.js";
@@ -6,7 +6,7 @@
6
6
 
7
7
  import {
8
8
  EDGE_HANDLER_NAME
9
- } from "./chunk-HPGTYMVD.js";
9
+ } from "./chunk-VP3PT3VV.js";
10
10
 
11
11
  // src/build/functions/edge.ts
12
12
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -15,15 +15,16 @@ var writeEdgeManifest = async (ctx, manifest) => {
15
15
  await mkdir(ctx.edgeFunctionsDir, { recursive: true });
16
16
  await writeFile(join(ctx.edgeFunctionsDir, "manifest.json"), JSON.stringify(manifest, null, 2));
17
17
  };
18
- var writeHandlerFile = async (ctx, { name }) => {
18
+ var writeHandlerFile = async (ctx, { matchers, name }) => {
19
19
  const handlerName = getHandlerName({ name });
20
- await cp(
21
- join(ctx.pluginDir, "edge-runtime"),
22
- join(ctx.edgeFunctionsDir, handlerName, "edge-runtime"),
23
- { recursive: true }
24
- );
20
+ const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName);
21
+ const handlerRuntimeDirectory = join(handlerDirectory, "edge-runtime");
22
+ await cp(join(ctx.pluginDir, "edge-runtime"), handlerRuntimeDirectory, {
23
+ recursive: true
24
+ });
25
+ await writeFile(join(handlerRuntimeDirectory, "matchers.json"), JSON.stringify(matchers));
25
26
  await writeFile(
26
- join(ctx.edgeFunctionsDir, handlerName, `${handlerName}.js`),
27
+ join(handlerDirectory, `${handlerName}.js`),
27
28
  `
28
29
  import {handleMiddleware} from './edge-runtime/middleware.ts';
29
30
  import handler from './server/${name}.js';
@@ -87,6 +87,12 @@ var PluginContext = class {
87
87
  await readFile(join(this.publishDir, "server/middleware-manifest.json"), "utf-8")
88
88
  );
89
89
  }
90
+ /**
91
+ * Get Next.js routes manifest from the build output
92
+ */
93
+ async getRoutesManifest() {
94
+ return JSON.parse(await readFile(join(this.publishDir, "routes-manifest.json"), "utf-8"));
95
+ }
90
96
  /**
91
97
  * Write a cache entry to the blob upload directory using
92
98
  * base64 keys to avoid collisions with directories
@@ -0,0 +1,60 @@
1
+
2
+ const require = await (async () => {
3
+ const { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+
8
+ // src/run/systemlog.ts
9
+ var systemLogTag = "__nfSystemLog";
10
+ var serializeError = (error) => {
11
+ const cause = error?.cause instanceof Error ? serializeError(error.cause) : error.cause;
12
+ return {
13
+ error: error.message,
14
+ error_cause: cause,
15
+ error_stack: error.stack
16
+ };
17
+ };
18
+ var StructuredLogger = class _StructuredLogger {
19
+ fields;
20
+ message;
21
+ constructor(message, fields) {
22
+ this.fields = fields ?? {};
23
+ this.message = message ?? "";
24
+ }
25
+ // TODO: add sampling
26
+ doLog(logger2, message) {
27
+ logger2(systemLogTag, JSON.stringify({ msg: message, fields: this.fields }));
28
+ }
29
+ log(message) {
30
+ this.doLog(console.log, message);
31
+ }
32
+ info(message) {
33
+ this.doLog(console.info, message);
34
+ }
35
+ debug(message) {
36
+ this.doLog(console.debug, message);
37
+ }
38
+ warn(message) {
39
+ this.doLog(console.warn, message);
40
+ }
41
+ error(message) {
42
+ this.doLog(console.error, message);
43
+ }
44
+ withError(error) {
45
+ const fields = error instanceof Error ? serializeError(error) : { error };
46
+ return this.withFields(fields);
47
+ }
48
+ withFields(fields) {
49
+ return new _StructuredLogger(this.message, {
50
+ ...this.fields,
51
+ ...fields
52
+ });
53
+ }
54
+ };
55
+ var logger = new StructuredLogger();
56
+
57
+ export {
58
+ StructuredLogger,
59
+ logger
60
+ };
@@ -38,11 +38,12 @@ var copyStaticContent = async (ctx) => {
38
38
  var copyStaticAssets = async (ctx) => {
39
39
  try {
40
40
  await rm(ctx.staticDir, { recursive: true, force: true });
41
+ const { basePath } = await ctx.getRoutesManifest();
41
42
  if (existsSync(ctx.resolve("public"))) {
42
- await cp(ctx.resolve("public"), ctx.staticDir, { recursive: true });
43
+ await cp(ctx.resolve("public"), join(ctx.staticDir, basePath), { recursive: true });
43
44
  }
44
45
  if (existsSync(join(ctx.publishDir, "static"))) {
45
- await cp(join(ctx.publishDir, "static"), join(ctx.staticDir, "_next/static"), {
46
+ await cp(join(ctx.publishDir, "static"), join(ctx.staticDir, basePath, "_next/static"), {
46
47
  recursive: true
47
48
  });
48
49
  }
package/dist/index.js CHANGED
@@ -4,31 +4,31 @@
4
4
  return createRequire(import.meta.url);
5
5
  })();
6
6
 
7
- import {
8
- createServerHandler
9
- } from "./esm-chunks/chunk-PEBUKFKH.js";
10
7
  import {
11
8
  copyFetchContent,
12
9
  copyPrerenderedContent
13
10
  } from "./esm-chunks/chunk-G3GM7JNF.js";
14
- import "./esm-chunks/chunk-G2VRYWGL.js";
15
11
  import {
16
12
  copyStaticAssets,
17
13
  copyStaticContent,
18
14
  publishStaticDir,
19
15
  unpublishStaticDir
20
- } from "./esm-chunks/chunk-62KDS27E.js";
16
+ } from "./esm-chunks/chunk-Z7ZMLVTM.js";
17
+ import {
18
+ createEdgeHandlers
19
+ } from "./esm-chunks/chunk-UXLNY5XK.js";
20
+ import {
21
+ createServerHandler
22
+ } from "./esm-chunks/chunk-NOX2JUQZ.js";
23
+ import "./esm-chunks/chunk-A22224GM.js";
21
24
  import "./esm-chunks/chunk-AVWFCGVE.js";
22
25
  import {
23
26
  restoreBuildCache,
24
27
  saveBuildCache
25
28
  } from "./esm-chunks/chunk-GGHAQM5D.js";
26
- import {
27
- createEdgeHandlers
28
- } from "./esm-chunks/chunk-XD5TSLZO.js";
29
29
  import {
30
30
  PluginContext
31
- } from "./esm-chunks/chunk-HPGTYMVD.js";
31
+ } from "./esm-chunks/chunk-VP3PT3VV.js";
32
32
  import "./esm-chunks/chunk-RSKIKBZH.js";
33
33
 
34
34
  // src/index.ts
@@ -631,6 +631,9 @@ var Store = class _Store {
631
631
  };
632
632
  }
633
633
  static validateKey(key) {
634
+ if (key === "") {
635
+ throw new Error("Blob key must not be empty.");
636
+ }
634
637
  if (key.startsWith("/") || key.startsWith("%2F")) {
635
638
  throw new Error("Blob key must not start with forward slash (/).");
636
639
  }
@@ -13,10 +13,13 @@ import {
13
13
  setCacheControlHeaders,
14
14
  setCacheTagsHeaders,
15
15
  setVaryHeaders
16
- } from "../../esm-chunks/chunk-ZBX3SNQG.js";
16
+ } from "../../esm-chunks/chunk-5N2CWXSJ.js";
17
17
  import {
18
18
  nextResponseProxy
19
19
  } from "../../esm-chunks/chunk-B6QMRLBH.js";
20
+ import {
21
+ logger
22
+ } from "../../esm-chunks/chunk-YZXA5QBC.js";
20
23
  import {
21
24
  __commonJS,
22
25
  __toESM
@@ -3260,9 +3263,10 @@ var server_default = async (request) => {
3260
3263
  setRunConfig(nextConfig);
3261
3264
  tagsManifest = await getTagsManifest();
3262
3265
  const { getMockedRequestHandlers } = await import("../next.cjs");
3266
+ const url = new URL(request.url);
3263
3267
  [nextHandler] = await getMockedRequestHandlers({
3264
- port: 3e3,
3265
- hostname: "localhost",
3268
+ port: Number(url.port) || 443,
3269
+ hostname: url.hostname,
3266
3270
  dir: process.cwd(),
3267
3271
  isDev: false
3268
3272
  });
@@ -3273,15 +3277,19 @@ var server_default = async (request) => {
3273
3277
  try {
3274
3278
  await nextHandler(req, resProxy);
3275
3279
  } catch (error) {
3280
+ logger.withError(error).error("next handler error");
3276
3281
  console.error(error);
3277
3282
  resProxy.statusCode = 500;
3278
3283
  resProxy.end("Internal Server Error");
3279
3284
  }
3280
3285
  const response = await toComputeResponse(resProxy);
3281
3286
  await adjustDateHeader(response.headers, request);
3282
- setCacheControlHeaders(response.headers);
3287
+ setCacheControlHeaders(response.headers, request);
3283
3288
  setCacheTagsHeaders(response.headers, request, tagsManifest);
3284
3289
  setVaryHeaders(response.headers, request, nextConfig);
3290
+ if (response.status > 300 && response.status < 400) {
3291
+ return new Response(null, response);
3292
+ }
3285
3293
  return response;
3286
3294
  };
3287
3295
  export {
@@ -9,7 +9,7 @@ import {
9
9
  setCacheControlHeaders,
10
10
  setCacheTagsHeaders,
11
11
  setVaryHeaders
12
- } from "../esm-chunks/chunk-ZBX3SNQG.js";
12
+ } from "../esm-chunks/chunk-5N2CWXSJ.js";
13
13
  import "../esm-chunks/chunk-RSKIKBZH.js";
14
14
  export {
15
15
  adjustDateHeader,
package/dist/run/next.cjs CHANGED
@@ -933,6 +933,9 @@ var Store = class _Store {
933
933
  };
934
934
  }
935
935
  static validateKey(key) {
936
+ if (key === "") {
937
+ throw new Error("Blob key must not be empty.");
938
+ }
936
939
  if (key.startsWith("/") || key.startsWith("%2F")) {
937
940
  throw new Error("Blob key must not start with forward slash (/).");
938
941
  }
@@ -0,0 +1,15 @@
1
+
2
+ const require = await (async () => {
3
+ const { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ StructuredLogger,
9
+ logger
10
+ } from "../esm-chunks/chunk-YZXA5QBC.js";
11
+ import "../esm-chunks/chunk-RSKIKBZH.js";
12
+ export {
13
+ StructuredLogger,
14
+ logger
15
+ };
@@ -0,0 +1,451 @@
1
+ /**
2
+ * Various router utils ported to Deno from Next.js source
3
+ * Licence: https://github.com/vercel/next.js/blob/7280c3ced186bb9a7ae3d7012613ef93f20b0fa9/license.md
4
+ *
5
+ * Some types have been re-implemented to be more compatible with Deno or avoid chains of dependent files
6
+ */
7
+
8
+ import type { Key } from '../vendor/deno.land/x/path_to_regexp@v6.2.1/index.ts'
9
+
10
+ import { compile, pathToRegexp } from '../vendor/deno.land/x/path_to_regexp@v6.2.1/index.ts'
11
+ import { getCookies } from '../vendor/deno.land/std@0.175.0/http/cookie.ts'
12
+
13
+ /*
14
+ ┌─────────────────────────────────────────────────────────────────────────┐
15
+ │ Inlined/re-implemented types │
16
+ └─────────────────────────────────────────────────────────────────────────┘
17
+ */
18
+ export interface ParsedUrlQuery {
19
+ [key: string]: string | string[]
20
+ }
21
+
22
+ export interface Params {
23
+ [param: string]: any
24
+ }
25
+
26
+ export type RouteHas =
27
+ | {
28
+ type: 'header' | 'query' | 'cookie'
29
+ key: string
30
+ value?: string
31
+ }
32
+ | {
33
+ type: 'host'
34
+ key?: undefined
35
+ value: string
36
+ }
37
+
38
+ export type Rewrite = {
39
+ source: string
40
+ destination: string
41
+ basePath?: false
42
+ locale?: false
43
+ has?: RouteHas[]
44
+ missing?: 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
+ missing?: RouteHas[]
55
+ regex: string
56
+ }
57
+
58
+ export type Redirect = {
59
+ source: string
60
+ destination: string
61
+ basePath?: false
62
+ locale?: false
63
+ has?: RouteHas[]
64
+ missing?: RouteHas[]
65
+ statusCode?: number
66
+ permanent?: boolean
67
+ regex: string
68
+ }
69
+
70
+ export type DynamicRoute = {
71
+ page: string
72
+ regex: string
73
+ namedRegex?: string
74
+ routeKeys?: { [key: string]: string }
75
+ }
76
+
77
+ export type RoutesManifest = {
78
+ basePath: string
79
+ redirects: Redirect[]
80
+ headers: Header[]
81
+ rewrites: {
82
+ beforeFiles: Rewrite[]
83
+ afterFiles: Rewrite[]
84
+ fallback: Rewrite[]
85
+ }
86
+ dynamicRoutes: DynamicRoute[]
87
+ }
88
+
89
+ /*
90
+ ┌─────────────────────────────────────────────────────────────────────────┐
91
+ │ packages/next/src/shared/lib/escape-regexp.ts │
92
+ └─────────────────────────────────────────────────────────────────────────┘
93
+ */
94
+ // regexp is based on https://github.com/sindresorhus/escape-string-regexp
95
+ const reHasRegExp = /[|\\{}()[\]^$+*?.-]/
96
+ const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g
97
+
98
+ export function escapeStringRegexp(str: string) {
99
+ // see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23
100
+ if (reHasRegExp.test(str)) {
101
+ return str.replace(reReplaceRegExp, '\\$&')
102
+ }
103
+
104
+ return str
105
+ }
106
+
107
+ /*
108
+ ┌─────────────────────────────────────────────────────────────────────────┐
109
+ │ packages/next/src/shared/lib/router/utils/querystring.ts │
110
+ └─────────────────────────────────────────────────────────────────────────┘
111
+ */
112
+ export function searchParamsToUrlQuery(searchParams: URLSearchParams): ParsedUrlQuery {
113
+ const query: ParsedUrlQuery = {}
114
+
115
+ searchParams.forEach((value, key) => {
116
+ if (typeof query[key] === 'undefined') {
117
+ query[key] = value
118
+ } else if (Array.isArray(query[key])) {
119
+ ;(query[key] as string[]).push(value)
120
+ } else {
121
+ query[key] = [query[key] as string, value]
122
+ }
123
+ })
124
+
125
+ return query
126
+ }
127
+
128
+ /*
129
+ ┌─────────────────────────────────────────────────────────────────────────┐
130
+ │ packages/next/src/shared/lib/router/utils/parse-url.ts │
131
+ └─────────────────────────────────────────────────────────────────────────┘
132
+ */
133
+ interface ParsedUrl {
134
+ hash: string
135
+ hostname?: string | null
136
+ href: string
137
+ pathname: string
138
+ port?: string | null
139
+ protocol?: string | null
140
+ query: ParsedUrlQuery
141
+ search: string
142
+ }
143
+
144
+ export function parseUrl(url: string): ParsedUrl {
145
+ const parsedURL = url.startsWith('/') ? new URL(url, 'http://n') : new URL(url)
146
+
147
+ return {
148
+ hash: parsedURL.hash,
149
+ hostname: parsedURL.hostname,
150
+ href: parsedURL.href,
151
+ pathname: parsedURL.pathname,
152
+ port: parsedURL.port,
153
+ protocol: parsedURL.protocol,
154
+ query: searchParamsToUrlQuery(parsedURL.searchParams),
155
+ search: parsedURL.search,
156
+ }
157
+ }
158
+
159
+ /*
160
+ ┌─────────────────────────────────────────────────────────────────────────┐
161
+ │ packages/next/src/shared/lib/router/utils/prepare-destination.ts │
162
+ │ — Changed to use WHATWG Fetch `Request` instead of │
163
+ │ `http.IncomingMessage`. │
164
+ └─────────────────────────────────────────────────────────────────────────┘
165
+ */
166
+ export function matchHas(
167
+ req: Pick<Request, 'headers' | 'url'>,
168
+ query: Params,
169
+ has: RouteHas[] = [],
170
+ missing: RouteHas[] = [],
171
+ ): false | Params {
172
+ const params: Params = {}
173
+ const cookies = getCookies(req.headers)
174
+ const url = new URL(req.url)
175
+ const hasMatch = (hasItem: RouteHas) => {
176
+ let value: undefined | string | null
177
+ let key = hasItem.key
178
+
179
+ switch (hasItem.type) {
180
+ case 'header': {
181
+ key = hasItem.key.toLowerCase()
182
+ value = req.headers.get(key)
183
+ break
184
+ }
185
+ case 'cookie': {
186
+ value = cookies[hasItem.key]
187
+ break
188
+ }
189
+ case 'query': {
190
+ value = query[hasItem.key]
191
+ break
192
+ }
193
+ case 'host': {
194
+ value = url.hostname
195
+ break
196
+ }
197
+ default: {
198
+ break
199
+ }
200
+ }
201
+ if (!hasItem.value && value && key) {
202
+ params[getSafeParamName(key)] = value
203
+
204
+ return true
205
+ } else if (value) {
206
+ const matcher = new RegExp(`^${hasItem.value}$`)
207
+ const matches = Array.isArray(value)
208
+ ? value.slice(-1)[0].match(matcher)
209
+ : value.match(matcher)
210
+
211
+ if (matches) {
212
+ if (Array.isArray(matches)) {
213
+ if (matches.groups) {
214
+ Object.keys(matches.groups).forEach((groupKey) => {
215
+ params[groupKey] = matches.groups![groupKey]
216
+ })
217
+ } else if (hasItem.type === 'host' && matches[0]) {
218
+ params.host = matches[0]
219
+ }
220
+ }
221
+ return true
222
+ }
223
+ }
224
+ return false
225
+ }
226
+
227
+ const allMatch = has.every((item) => hasMatch(item)) && !missing.some((item) => hasMatch(item))
228
+
229
+ if (allMatch) {
230
+ return params
231
+ }
232
+ return false
233
+ }
234
+
235
+ export function compileNonPath(value: string, params: Params): string {
236
+ if (!value.includes(':')) {
237
+ return value
238
+ }
239
+
240
+ for (const key of Object.keys(params)) {
241
+ if (value.includes(`:${key}`)) {
242
+ value = value
243
+ .replace(new RegExp(`:${key}\\*`, 'g'), `:${key}--ESCAPED_PARAM_ASTERISKS`)
244
+ .replace(new RegExp(`:${key}\\?`, 'g'), `:${key}--ESCAPED_PARAM_QUESTION`)
245
+ .replace(new RegExp(`:${key}\\+`, 'g'), `:${key}--ESCAPED_PARAM_PLUS`)
246
+ .replace(new RegExp(`:${key}(?!\\w)`, 'g'), `--ESCAPED_PARAM_COLON${key}`)
247
+ }
248
+ }
249
+ value = value
250
+ .replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1')
251
+ .replace(/--ESCAPED_PARAM_PLUS/g, '+')
252
+ .replace(/--ESCAPED_PARAM_COLON/g, ':')
253
+ .replace(/--ESCAPED_PARAM_QUESTION/g, '?')
254
+ .replace(/--ESCAPED_PARAM_ASTERISKS/g, '*')
255
+ // the value needs to start with a forward-slash to be compiled
256
+ // correctly
257
+ return compile(`/${value}`, { validate: false })(params).slice(1)
258
+ }
259
+
260
+ export function prepareDestination(args: {
261
+ appendParamsToQuery: boolean
262
+ destination: string
263
+ params: Params
264
+ query: ParsedUrlQuery
265
+ }) {
266
+ const query = Object.assign({}, args.query)
267
+ delete query.__nextLocale
268
+ delete query.__nextDefaultLocale
269
+ delete query.__nextDataReq
270
+
271
+ let escapedDestination = args.destination
272
+
273
+ for (const param of Object.keys({ ...args.params, ...query })) {
274
+ escapedDestination = escapeSegment(escapedDestination, param)
275
+ }
276
+
277
+ const parsedDestination: ParsedUrl = parseUrl(escapedDestination)
278
+ const destQuery = parsedDestination.query
279
+ const destPath = unescapeSegments(`${parsedDestination.pathname!}${parsedDestination.hash || ''}`)
280
+ const destHostname = unescapeSegments(parsedDestination.hostname || '')
281
+ const destPathParamKeys: Key[] = []
282
+ const destHostnameParamKeys: Key[] = []
283
+ pathToRegexp(destPath, destPathParamKeys)
284
+ pathToRegexp(destHostname, destHostnameParamKeys)
285
+
286
+ const destParams: (string | number)[] = []
287
+
288
+ destPathParamKeys.forEach((key) => destParams.push(key.name))
289
+ destHostnameParamKeys.forEach((key) => destParams.push(key.name))
290
+
291
+ const destPathCompiler = compile(
292
+ destPath,
293
+ // we don't validate while compiling the destination since we should
294
+ // have already validated before we got to this point and validating
295
+ // breaks compiling destinations with named pattern params from the source
296
+ // e.g. /something:hello(.*) -> /another/:hello is broken with validation
297
+ // since compile validation is meant for reversing and not for inserting
298
+ // params from a separate path-regex into another
299
+ { validate: false },
300
+ )
301
+
302
+ const destHostnameCompiler = compile(destHostname, { validate: false })
303
+
304
+ // update any params in query values
305
+ for (const [key, strOrArray] of Object.entries(destQuery)) {
306
+ // the value needs to start with a forward-slash to be compiled
307
+ // correctly
308
+ if (Array.isArray(strOrArray)) {
309
+ destQuery[key] = strOrArray.map((value) =>
310
+ compileNonPath(unescapeSegments(value), args.params),
311
+ )
312
+ } else {
313
+ destQuery[key] = compileNonPath(unescapeSegments(strOrArray), args.params)
314
+ }
315
+ }
316
+
317
+ // add path params to query if it's not a redirect and not
318
+ // already defined in destination query or path
319
+ const paramKeys = Object.keys(args.params).filter((name) => name !== 'nextInternalLocale')
320
+
321
+ if (args.appendParamsToQuery && !paramKeys.some((key) => destParams.includes(key))) {
322
+ for (const key of paramKeys) {
323
+ if (!(key in destQuery)) {
324
+ destQuery[key] = args.params[key]
325
+ }
326
+ }
327
+ }
328
+
329
+ let newUrl
330
+
331
+ try {
332
+ newUrl = destPathCompiler(args.params)
333
+
334
+ const [pathname, hash] = newUrl.split('#')
335
+ parsedDestination.hostname = destHostnameCompiler(args.params)
336
+ parsedDestination.pathname = pathname
337
+ parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
338
+ delete (parsedDestination as any).search
339
+ } catch (err: any) {
340
+ if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
341
+ throw new Error(
342
+ `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`,
343
+ )
344
+ }
345
+ throw err
346
+ }
347
+
348
+ // Query merge order lowest priority to highest
349
+ // 1. initial URL query values
350
+ // 2. path segment values
351
+ // 3. destination specified query values
352
+ parsedDestination.query = {
353
+ ...query,
354
+ ...parsedDestination.query,
355
+ }
356
+
357
+ return {
358
+ newUrl,
359
+ destQuery,
360
+ parsedDestination,
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Ensure only a-zA-Z are used for param names for proper interpolating
366
+ * with path-to-regexp
367
+ */
368
+ function getSafeParamName(paramName: string) {
369
+ let newParamName = ''
370
+
371
+ for (let i = 0; i < paramName.length; i++) {
372
+ const charCode = paramName.charCodeAt(i)
373
+
374
+ if (
375
+ (charCode > 64 && charCode < 91) || // A-Z
376
+ (charCode > 96 && charCode < 123) // a-z
377
+ ) {
378
+ newParamName += paramName[i]
379
+ }
380
+ }
381
+ return newParamName
382
+ }
383
+
384
+ function escapeSegment(str: string, segmentName: string) {
385
+ return str.replace(
386
+ new RegExp(`:${escapeStringRegexp(segmentName)}`, 'g'),
387
+ `__ESC_COLON_${segmentName}`,
388
+ )
389
+ }
390
+
391
+ function unescapeSegments(str: string) {
392
+ return str.replace(/__ESC_COLON_/gi, ':')
393
+ }
394
+
395
+ /*
396
+ ┌─────────────────────────────────────────────────────────────────────────┐
397
+ │ packages/next/src/shared/lib/router/utils/is-dynamic.ts │
398
+ └─────────────────────────────────────────────────────────────────────────┘
399
+ */
400
+ // Identify /[param]/ in route string
401
+ const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/
402
+
403
+ export function isDynamicRoute(route: string): boolean {
404
+ return TEST_ROUTE.test(route)
405
+ }
406
+
407
+ /*
408
+ ┌─────────────────────────────────────────────────────────────────────────┐
409
+ │ packages/next/shared/lib/router/utils/middleware-route-matcher.ts │
410
+ └─────────────────────────────────────────────────────────────────────────┘
411
+ */
412
+ export interface MiddlewareRouteMatch {
413
+ (
414
+ pathname: string | null | undefined,
415
+ request: Pick<Request, 'headers' | 'url'>,
416
+ query: Params,
417
+ ): boolean
418
+ }
419
+
420
+ export interface MiddlewareMatcher {
421
+ regexp: string
422
+ locale?: false
423
+ has?: RouteHas[]
424
+ missing?: RouteHas[]
425
+ }
426
+
427
+ export function getMiddlewareRouteMatcher(matchers: MiddlewareMatcher[]): MiddlewareRouteMatch {
428
+ return (
429
+ pathname: string | null | undefined,
430
+ req: Pick<Request, 'headers' | 'url'>,
431
+ query: Params,
432
+ ) => {
433
+ for (const matcher of matchers) {
434
+ const routeMatch = new RegExp(matcher.regexp).exec(pathname!)
435
+ if (!routeMatch) {
436
+ continue
437
+ }
438
+
439
+ if (matcher.has || matcher.missing) {
440
+ const hasParams = matchHas(req, query, matcher.has, matcher.missing)
441
+ if (!hasParams) {
442
+ continue
443
+ }
444
+ }
445
+
446
+ return true
447
+ }
448
+
449
+ return false
450
+ }
451
+ }
@@ -0,0 +1 @@
1
+ []
@@ -1,11 +1,20 @@
1
1
  import type { Context } from '@netlify/edge-functions'
2
2
 
3
+ import matchers from './matchers.json' assert { type: 'json' }
4
+
3
5
  import { buildNextRequest, RequestData } from './lib/next-request.ts'
4
6
  import { buildResponse } from './lib/response.ts'
5
7
  import { FetchEventResult } from './lib/response.ts'
8
+ import {
9
+ type MiddlewareRouteMatch,
10
+ getMiddlewareRouteMatcher,
11
+ searchParamsToUrlQuery,
12
+ } from './lib/routing.ts'
6
13
 
7
14
  type NextHandler = (params: { request: RequestData }) => Promise<FetchEventResult>
8
15
 
16
+ const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || [])
17
+
9
18
  /**
10
19
  * Runs a Next.js middleware as a Netlify Edge Function. It translates a web
11
20
  * platform Request into a NextRequest instance on the way in, and translates
@@ -20,13 +29,17 @@ export async function handleMiddleware(
20
29
  context: Context,
21
30
  nextHandler: NextHandler,
22
31
  ) {
23
- // Don't run in dev
24
- if (Netlify.env.has('NETLIFY_DEV')) {
32
+ const nextRequest = buildNextRequest(request, context)
33
+ const url = new URL(request.url)
34
+
35
+ // While we have already checked the path when mapping to the edge function,
36
+ // Next.js supports extra rules that we need to check here too, because we
37
+ // might be running an edge function for a path we should not. If we find
38
+ // that's the case, short-circuit the execution.
39
+ if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
25
40
  return
26
41
  }
27
42
 
28
- const nextRequest = buildNextRequest(request, context)
29
-
30
43
  try {
31
44
  const result = await nextHandler({ request: nextRequest })
32
45
  const response = await buildResponse({ result, request: request, context })
@@ -2,6 +2,9 @@
2
2
  // a Webpack bundle. You should not import this file from anywhere in the
3
3
  // application.
4
4
  import { AsyncLocalStorage } from 'node:async_hooks'
5
+ import process from 'node:process'
6
+
7
+ globalThis.process = process
5
8
 
6
9
  globalThis.AsyncLocalStorage = AsyncLocalStorage
7
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "5.0.0-beta.0",
3
+ "version": "5.0.0-beta.1",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",