@netlify/plugin-nextjs 5.0.0-beta.2 → 5.0.0-beta.3

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/README.md CHANGED
@@ -17,8 +17,11 @@ How to add new integration test scenarios to the application:
17
17
  4. Add your test
18
18
 
19
19
  > Currently the tests require a built version of the `dist/run/handlers/cache.cjs` so you need to
20
- > run `npm run build` before executing the integration tests. In addition, the integration tests
21
- > need to be prepared before first use. You can do this by running `npm run pretest`.
20
+ > run `npm run build` before executing the integration tests.
21
+
22
+ In addition, the integration tests need to be prepared before first use. You can do this by running
23
+ `npm run pretest`. To speed up this process and build only the fixtures whose name starts with a
24
+ given prefix, run `npm run pretest -- <prefix>`.
22
25
 
23
26
  ### E2E testing
24
27
 
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  restoreBuildCache,
9
9
  saveBuildCache
10
- } from "../esm-chunks/chunk-GGHAQM5D.js";
10
+ } from "../esm-chunks/chunk-XQ65S4R2.js";
11
11
  import "../esm-chunks/chunk-RSKIKBZH.js";
12
12
  export {
13
13
  restoreBuildCache,
@@ -6,7 +6,8 @@
6
6
 
7
7
  import {
8
8
  createEdgeHandlers
9
- } from "../../esm-chunks/chunk-3PTPU5GO.js";
9
+ } from "../../esm-chunks/chunk-FFCTA32Q.js";
10
+ import "../../esm-chunks/chunk-AVWFCGVE.js";
10
11
  import "../../esm-chunks/chunk-TJKO3X6O.js";
11
12
  import "../../esm-chunks/chunk-RSKIKBZH.js";
12
13
  export {
@@ -4,25 +4,52 @@
4
4
  return createRequire(import.meta.url);
5
5
  })();
6
6
 
7
+ import {
8
+ require_out
9
+ } from "./chunk-AVWFCGVE.js";
7
10
  import {
8
11
  EDGE_HANDLER_NAME
9
12
  } from "./chunk-TJKO3X6O.js";
13
+ import {
14
+ __toESM
15
+ } from "./chunk-RSKIKBZH.js";
10
16
 
11
17
  // src/build/functions/edge.ts
18
+ var import_fast_glob = __toESM(require_out(), 1);
12
19
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
13
20
  import { dirname, join } from "node:path";
14
21
  var writeEdgeManifest = async (ctx, manifest) => {
15
22
  await mkdir(ctx.edgeFunctionsDir, { recursive: true });
16
23
  await writeFile(join(ctx.edgeFunctionsDir, "manifest.json"), JSON.stringify(manifest, null, 2));
17
24
  };
25
+ var copyRuntime = async (ctx, handlerDirectory) => {
26
+ const files = await (0, import_fast_glob.glob)("edge-runtime/**/*", {
27
+ cwd: ctx.pluginDir,
28
+ ignore: ["**/*.test.ts"],
29
+ dot: true
30
+ });
31
+ await Promise.all(
32
+ files.map(
33
+ (path) => cp(join(ctx.pluginDir, path), join(handlerDirectory, path), { recursive: true })
34
+ )
35
+ );
36
+ };
18
37
  var writeHandlerFile = async (ctx, { matchers, name }) => {
38
+ const nextConfig = await ctx.getBuildConfig();
19
39
  const handlerName = getHandlerName({ name });
20
40
  const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName);
21
41
  const handlerRuntimeDirectory = join(handlerDirectory, "edge-runtime");
22
- await cp(join(ctx.pluginDir, "edge-runtime"), handlerRuntimeDirectory, {
23
- recursive: true
24
- });
42
+ await copyRuntime(ctx, handlerDirectory);
25
43
  await writeFile(join(handlerRuntimeDirectory, "matchers.json"), JSON.stringify(matchers));
44
+ const minimalNextConfig = {
45
+ basePath: nextConfig.basePath,
46
+ i18n: nextConfig.i18n,
47
+ trailingSlash: nextConfig.trailingSlash
48
+ };
49
+ await writeFile(
50
+ join(handlerRuntimeDirectory, "next.config.json"),
51
+ JSON.stringify(minimalNextConfig)
52
+ );
26
53
  await writeFile(
27
54
  join(handlerDirectory, `${handlerName}.js`),
28
55
  `
@@ -0,0 +1,105 @@
1
+
2
+ const require = await (async () => {
3
+ const { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ __commonJS,
9
+ __require,
10
+ __toESM
11
+ } from "./chunk-RSKIKBZH.js";
12
+
13
+ // node_modules/@netlify/functions/dist/lib/system_logger.js
14
+ var require_system_logger = __commonJS({
15
+ "node_modules/@netlify/functions/dist/lib/system_logger.js"(exports) {
16
+ "use strict";
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.systemLogger = exports.LogLevel = void 0;
19
+ var process_1 = __require("process");
20
+ var systemLogTag = "__nfSystemLog";
21
+ var serializeError = (error) => {
22
+ const cause = error?.cause instanceof Error ? serializeError(error.cause) : error.cause;
23
+ return {
24
+ error: error.message,
25
+ error_cause: cause,
26
+ error_stack: error.stack
27
+ };
28
+ };
29
+ var LogLevel2;
30
+ (function(LogLevel3) {
31
+ LogLevel3[LogLevel3["Debug"] = 1] = "Debug";
32
+ LogLevel3[LogLevel3["Log"] = 2] = "Log";
33
+ LogLevel3[LogLevel3["Error"] = 3] = "Error";
34
+ })(LogLevel2 = exports.LogLevel || (exports.LogLevel = {}));
35
+ var SystemLogger = class _SystemLogger {
36
+ fields;
37
+ logLevel;
38
+ constructor(fields = {}, logLevel = LogLevel2.Log) {
39
+ this.fields = fields;
40
+ this.logLevel = logLevel;
41
+ }
42
+ doLog(logger, message) {
43
+ if (process_1.env.NETLIFY_DEV && !process_1.env.NETLIFY_ENABLE_SYSTEM_LOGGING) {
44
+ return;
45
+ }
46
+ logger(systemLogTag, JSON.stringify({ msg: message, fields: this.fields }));
47
+ }
48
+ log(message) {
49
+ if (this.logLevel > LogLevel2.Log) {
50
+ return;
51
+ }
52
+ this.doLog(console.log, message);
53
+ }
54
+ debug(message) {
55
+ if (this.logLevel > LogLevel2.Debug) {
56
+ return;
57
+ }
58
+ this.doLog(console.debug, message);
59
+ }
60
+ error(message) {
61
+ if (this.logLevel > LogLevel2.Error) {
62
+ return;
63
+ }
64
+ this.doLog(console.error, message);
65
+ }
66
+ withLogLevel(level) {
67
+ return new _SystemLogger(this.fields, level);
68
+ }
69
+ withFields(fields) {
70
+ return new _SystemLogger({
71
+ ...this.fields,
72
+ ...fields
73
+ }, this.logLevel);
74
+ }
75
+ withError(error) {
76
+ const fields = error instanceof Error ? serializeError(error) : { error };
77
+ return this.withFields(fields);
78
+ }
79
+ };
80
+ exports.systemLogger = new SystemLogger();
81
+ }
82
+ });
83
+
84
+ // node_modules/@netlify/functions/dist/internal.js
85
+ var require_internal = __commonJS({
86
+ "node_modules/@netlify/functions/dist/internal.js"(exports) {
87
+ "use strict";
88
+ Object.defineProperty(exports, "__esModule", { value: true });
89
+ exports.LogLevel = exports.systemLogger = void 0;
90
+ var system_logger_js_1 = require_system_logger();
91
+ Object.defineProperty(exports, "systemLogger", { enumerable: true, get: function() {
92
+ return system_logger_js_1.systemLogger;
93
+ } });
94
+ Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function() {
95
+ return system_logger_js_1.LogLevel;
96
+ } });
97
+ }
98
+ });
99
+
100
+ // src/run/systemlog.ts
101
+ var import_internal = __toESM(require_internal(), 1);
102
+
103
+ export {
104
+ import_internal
105
+ };
@@ -22,7 +22,7 @@ var setRunConfig = (config) => {
22
22
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
23
23
  };
24
24
  var getTagsManifest = async () => {
25
- return JSON.parse(await readFile(resolve(".netlify/tags-manifest.json"), "utf-8"));
25
+ return JSON.parse(await readFile(resolve(PLUGIN_DIR, ".netlify/tags-manifest.json"), "utf-8"));
26
26
  };
27
27
 
28
28
  export {
@@ -6,6 +6,7 @@
6
6
 
7
7
 
8
8
  // src/build/cache.ts
9
+ import { existsSync } from "node:fs";
9
10
  import { join } from "node:path";
10
11
  var saveBuildCache = async (ctx) => {
11
12
  if (await ctx.utils.cache.save(join(ctx.publishDir, "cache"))) {
@@ -15,7 +16,10 @@ var saveBuildCache = async (ctx) => {
15
16
  }
16
17
  };
17
18
  var restoreBuildCache = async (ctx) => {
18
- if (await ctx.utils.cache.restore(join(ctx.publishDir, "cache"))) {
19
+ const { cache } = ctx.utils;
20
+ if (existsSync(join(ctx.publishDir, "cache"))) {
21
+ console.log("Next.js cache found.");
22
+ } else if (await cache.restore(join(ctx.publishDir, "cache"))) {
19
23
  console.log("Next.js cache restored.");
20
24
  } else {
21
25
  console.log("No Next.js cache to restore.");
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "./esm-chunks/chunk-Z7ZMLVTM.js";
17
17
  import {
18
18
  createEdgeHandlers
19
- } from "./esm-chunks/chunk-3PTPU5GO.js";
19
+ } from "./esm-chunks/chunk-FFCTA32Q.js";
20
20
  import {
21
21
  createServerHandler
22
22
  } from "./esm-chunks/chunk-ALO2SSMH.js";
@@ -25,7 +25,7 @@ import "./esm-chunks/chunk-AVWFCGVE.js";
25
25
  import {
26
26
  restoreBuildCache,
27
27
  saveBuildCache
28
- } from "./esm-chunks/chunk-GGHAQM5D.js";
28
+ } from "./esm-chunks/chunk-XQ65S4R2.js";
29
29
  import {
30
30
  setImageConfig
31
31
  } from "./esm-chunks/chunk-H46DW7YI.js";
@@ -8,7 +8,7 @@ import {
8
8
  getRunConfig,
9
9
  getTagsManifest,
10
10
  setRunConfig
11
- } from "../esm-chunks/chunk-R4NHZWGU.js";
11
+ } from "../esm-chunks/chunk-OIL5MDCV.js";
12
12
  import "../esm-chunks/chunk-4AJYXTWN.js";
13
13
  import "../esm-chunks/chunk-RSKIKBZH.js";
14
14
  export {
@@ -5,11 +5,11 @@
5
5
  })();
6
6
 
7
7
  import {
8
- logger
9
- } from "../../esm-chunks/chunk-YZXA5QBC.js";
8
+ import_internal
9
+ } from "../../esm-chunks/chunk-GHDGGK6V.js";
10
10
  import {
11
11
  getTagsManifest
12
- } from "../../esm-chunks/chunk-R4NHZWGU.js";
12
+ } from "../../esm-chunks/chunk-OIL5MDCV.js";
13
13
  import "../../esm-chunks/chunk-4AJYXTWN.js";
14
14
  import {
15
15
  adjustDateHeader,
@@ -3278,7 +3278,7 @@ var server_default = async (request) => {
3278
3278
  try {
3279
3279
  await nextHandler(req, resProxy);
3280
3280
  } catch (error) {
3281
- logger.withError(error).error("next handler error");
3281
+ import_internal.systemLogger.withError(error).error("next handler error");
3282
3282
  console.error(error);
3283
3283
  resProxy.statusCode = 500;
3284
3284
  resProxy.end("Internal Server Error");
@@ -5,11 +5,12 @@
5
5
  })();
6
6
 
7
7
  import {
8
- StructuredLogger,
9
- logger
10
- } from "../esm-chunks/chunk-YZXA5QBC.js";
8
+ import_internal
9
+ } from "../esm-chunks/chunk-GHDGGK6V.js";
11
10
  import "../esm-chunks/chunk-RSKIKBZH.js";
11
+ var export_LogLevel = import_internal.LogLevel;
12
+ var export_logger = import_internal.systemLogger;
12
13
  export {
13
- StructuredLogger,
14
- logger
14
+ export_LogLevel as LogLevel,
15
+ export_logger as logger
15
16
  };
@@ -1,3 +1,8 @@
1
+ export const InternalHeaders = {
2
+ NFDebugLogging: 'x-nf-debug-logging',
3
+ NFRequestID: 'x-nf-request-id',
4
+ }
5
+
1
6
  // Next 13 supports request header mutations and has the side effect of prepending header values with 'x-middleware-request'
2
7
  // as part of invoking NextResponse.next() in the middleware. We need to remove that before sending the response the user
3
8
  // as the code that removes it in Next isn't run based on how we handle the middleware
@@ -0,0 +1,5 @@
1
+ export {
2
+ logger,
3
+ LogLevel,
4
+ StructuredLogger,
5
+ } from '../vendor/v1-7-0--edge-utils.netlify.app/logger/mod.ts'
@@ -1,5 +1,7 @@
1
1
  import type { Context } from '@netlify/edge-functions'
2
2
 
3
+ import { normalizeDataUrl, removeBasePath, normalizeLocalePath } from './util.ts'
4
+
3
5
  interface I18NConfig {
4
6
  defaultLocale: string
5
7
  localeDetection?: false
@@ -29,6 +31,43 @@ export interface RequestData {
29
31
  }
30
32
  url: string
31
33
  body?: ReadableStream<Uint8Array>
34
+ detectedLocale?: string
35
+ }
36
+
37
+ const normalizeRequestURL = (
38
+ originalURL: string,
39
+ nextConfig?: RequestData['nextConfig'],
40
+ ): { url: string; detectedLocale?: string } => {
41
+ const url = new URL(originalURL)
42
+
43
+ url.pathname = removeBasePath(url.pathname, nextConfig?.basePath)
44
+
45
+ let detectedLocale: string | undefined
46
+
47
+ if (nextConfig?.i18n) {
48
+ const { pathname, detectedLocale: detected } = normalizeLocalePath(
49
+ url.pathname,
50
+ nextConfig?.i18n?.locales,
51
+ )
52
+ url.pathname = pathname
53
+ detectedLocale = detected
54
+ }
55
+
56
+ // We want to run middleware for data requests and expose the URL of the
57
+ // corresponding pages, so we have to normalize the URLs before running
58
+ // the handler.
59
+ url.pathname = normalizeDataUrl(url.pathname)
60
+
61
+ // Normalizing the trailing slash based on the `trailingSlash` configuration
62
+ // property from the Next.js config.
63
+ if (nextConfig?.trailingSlash && url.pathname !== '/' && !url.pathname.endsWith('/')) {
64
+ url.pathname = `${url.pathname}/`
65
+ }
66
+
67
+ return {
68
+ url: url.toString(),
69
+ detectedLocale,
70
+ }
32
71
  }
33
72
 
34
73
  export const buildNextRequest = (
@@ -47,13 +86,16 @@ export const buildNextRequest = (
47
86
  timezone,
48
87
  }
49
88
 
89
+ const { detectedLocale, url: normalizedUrl } = normalizeRequestURL(url, nextConfig)
90
+
50
91
  return {
51
92
  headers: Object.fromEntries(headers.entries()),
52
93
  geo,
53
- url,
94
+ url: normalizedUrl,
54
95
  method,
55
96
  ip: context.ip,
56
97
  body: body ?? undefined,
57
98
  nextConfig,
99
+ detectedLocale,
58
100
  }
59
101
  }
@@ -2,23 +2,37 @@ import type { Context } from '@netlify/edge-functions'
2
2
  import { HTMLRewriter } from '../vendor/deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
3
3
 
4
4
  import { updateModifiedHeaders } from './headers.ts'
5
- import { normalizeDataUrl, relativizeURL } from './util.ts'
5
+ import type { StructuredLogger } from './logging.ts'
6
+ import { normalizeDataUrl, normalizeLocalePath, relativizeURL, rewriteDataPath } from './util.ts'
6
7
  import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts'
8
+ import { RequestData } from './next-request.ts'
7
9
 
8
10
  export interface FetchEventResult {
9
11
  response: Response
10
12
  waitUntil: Promise<any>
11
13
  }
12
14
 
15
+ interface BuildResponseOptions {
16
+ context: Context
17
+ logger: StructuredLogger
18
+ request: Request
19
+ result: FetchEventResult
20
+ nextConfig?: RequestData['nextConfig']
21
+ requestLocale?: string
22
+ }
23
+
13
24
  export const buildResponse = async ({
14
- result,
15
- request,
16
25
  context,
17
- }: {
18
- result: FetchEventResult
19
- request: Request
20
- context: Context
21
- }) => {
26
+ logger,
27
+ request,
28
+ result,
29
+ nextConfig,
30
+ requestLocale,
31
+ }: BuildResponseOptions): Promise<Response | void> => {
32
+ logger
33
+ .withFields({ is_nextresponse_next: result.response.headers.has('x-middleware-next') })
34
+ .debug('Building Next.js response')
35
+
22
36
  updateModifiedHeaders(request.headers, result.response.headers)
23
37
 
24
38
  // They've returned the MiddlewareRequest directly, so we'll call `next()` for them.
@@ -99,32 +113,79 @@ export const buildResponse = async ({
99
113
  const rewrite = res.headers.get('x-middleware-rewrite')
100
114
 
101
115
  // Data requests (i.e. requests for /_next/data ) need special handling
102
- const isDataReq = request.headers.get('x-nextjs-data')
116
+ const isDataReq = request.headers.has('x-nextjs-data')
103
117
 
104
118
  if (rewrite) {
119
+ logger.withFields({ rewrite_url: rewrite }).debug('Found middleware rewrite')
120
+
105
121
  const rewriteUrl = new URL(rewrite, request.url)
106
122
  const baseUrl = new URL(request.url)
123
+ if (rewriteUrl.toString() === baseUrl.toString()) {
124
+ logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
125
+ return
126
+ }
127
+
107
128
  const relativeUrl = relativizeURL(rewrite, request.url)
129
+ const originalPath = new URL(request.url, `http://n`).pathname
108
130
 
109
- // Data requests might be rewritten to an external URL
110
- // This header tells the client router the redirect target, and if it's external then it will do a full navigation
111
131
  if (isDataReq) {
132
+ // Data requests might be rewritten to an external URL
133
+ // This header tells the client router the redirect target, and if it's external then it will do a full navigation
134
+
112
135
  res.headers.set('x-nextjs-rewrite', relativeUrl)
113
136
  }
137
+
114
138
  if (rewriteUrl.origin !== baseUrl.origin) {
115
- // Netlify Edge Functions don't support proxying to external domains, but Next middleware does
116
- const proxied = fetch(new Request(rewriteUrl.toString(), request))
117
- return addMiddlewareHeaders(proxied, res)
139
+ logger.withFields({ rewrite_url: rewrite }).debug('Rewriting to external url')
140
+ let proxyRequest: Request
141
+
142
+ // Remove Netlify internal headers
143
+ const headers = new Headers(
144
+ [...request.headers.entries()].filter(([key]) => !key.startsWith('x-nf-')),
145
+ )
146
+ if (request.body && !request.bodyUsed) {
147
+ // This is not ideal, but streaming to an external URL doesn't work
148
+ const body = await request.arrayBuffer()
149
+ proxyRequest = new Request(rewriteUrl, {
150
+ headers,
151
+ method: request.method,
152
+ body,
153
+ })
154
+ } else {
155
+ proxyRequest = new Request(rewriteUrl, {
156
+ headers,
157
+ method: request.method,
158
+ })
159
+ }
160
+ return addMiddlewareHeaders(fetch(proxyRequest), res)
161
+ } else if (isDataReq) {
162
+ rewriteUrl.pathname = rewriteDataPath({
163
+ dataUrl: originalPath,
164
+ newRoute: rewriteUrl.pathname,
165
+ basePath: nextConfig?.basePath,
166
+ })
118
167
  }
119
168
  res.headers.set('x-middleware-rewrite', relativeUrl)
120
-
121
- request.headers.set('x-original-path', new URL(request.url, `http://n`).pathname)
122
169
  request.headers.set('x-middleware-rewrite', rewrite)
123
-
124
- return addMiddlewareHeaders(context.rewrite(rewrite), res)
170
+ return addMiddlewareHeaders(fetch(new Request(rewriteUrl, request)), res)
125
171
  }
126
172
 
127
- const redirect = res.headers.get('Location')
173
+ let redirect = res.headers.get('location')
174
+
175
+ // If we are redirecting a request that had a locale in the URL, we need to add it back in
176
+ if (redirect && requestLocale) {
177
+ const redirectUrl = new URL(redirect, request.url)
178
+
179
+ const normalizedRedirect = normalizeLocalePath(redirectUrl.pathname, nextConfig?.i18n?.locales)
180
+
181
+ const locale = normalizedRedirect.detectedLocale ?? requestLocale
182
+ // Pages router API routes don't have a locale in the URL
183
+ if (locale && !redirectUrl.pathname.startsWith(`/api/`)) {
184
+ redirectUrl.pathname = `/${locale}${normalizedRedirect.pathname}`
185
+ redirect = redirectUrl.toString()
186
+ res.headers.set('location', redirect)
187
+ }
188
+ }
128
189
 
129
190
  // Data requests shouldn't automatically redirect in the browser (they might be HTML pages): they're handled by the router
130
191
  if (redirect && isDataReq) {
@@ -142,5 +203,6 @@ export const buildResponse = async ({
142
203
  res.headers.delete('x-middleware-next')
143
204
  return addMiddlewareHeaders(context.next(), res)
144
205
  }
206
+
145
207
  return res
146
208
  }
@@ -0,0 +1,39 @@
1
+ import { assertEquals } from 'https://deno.land/std@0.175.0/testing/asserts.ts'
2
+ import { rewriteDataPath } from './util.ts'
3
+
4
+ Deno.test('rewriteDataPath', async (t) => {
5
+ await t.step('should rewrite a data url', async () => {
6
+ const dataUrl = '/_next/data/build-id/rewrite-me.json'
7
+ const newRoute = '/target'
8
+ const result = rewriteDataPath({ dataUrl, newRoute })
9
+ assertEquals(result, '/_next/data/build-id/target.json')
10
+ })
11
+
12
+ await t.step('should rewrite a data url with a base path', async () => {
13
+ const dataUrl = '/baseDir/_next/data/build-id/rewrite-me.json'
14
+ const newRoute = '/target'
15
+ const result = rewriteDataPath({ dataUrl, newRoute, basePath: '/baseDir' })
16
+ assertEquals(result, '/baseDir/_next/data/build-id/target.json')
17
+ })
18
+
19
+ await t.step('should rewrite from an index data url', async () => {
20
+ const dataUrl = '/_next/data/build-id/index.json'
21
+ const newRoute = '/target'
22
+ const result = rewriteDataPath({ dataUrl, newRoute })
23
+ assertEquals(result, '/_next/data/build-id/target.json')
24
+ })
25
+
26
+ await t.step('should rewrite to an index data url', async () => {
27
+ const dataUrl = '/_next/data/build-id/rewrite-me.json'
28
+ const newRoute = '/'
29
+ const result = rewriteDataPath({ dataUrl, newRoute })
30
+ assertEquals(result, '/_next/data/build-id/index.json')
31
+ })
32
+
33
+ await t.step('should rewrite to a route with a trailing slash', async () => {
34
+ const dataUrl = '/_next/data/build-id/rewrite-me.json'
35
+ const newRoute = '/target/'
36
+ const result = rewriteDataPath({ dataUrl, newRoute })
37
+ assertEquals(result, '/_next/data/build-id/target.json')
38
+ })
39
+ })
@@ -1,16 +1,63 @@
1
- // If the redirect is a data URL, we need to normalize it.
2
- // https://github.com/vercel/next.js/blob/25e0988e7c9033cb1503cbe0c62ba5de2e97849c/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L69-L76
3
- export function normalizeDataUrl(redirect: string) {
4
- if (redirect.startsWith('/_next/data/') && redirect.includes('.json')) {
5
- const paths = redirect
1
+ import type { RequestData } from './next-request.ts'
2
+
3
+ /**
4
+ * Normalize a data URL into a route path.
5
+ * @see https://github.com/vercel/next.js/blob/25e0988e7c9033cb1503cbe0c62ba5de2e97849c/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L69-L76
6
+ */
7
+ export function normalizeDataUrl(urlPath: string) {
8
+ if (urlPath.startsWith('/_next/data/') && urlPath.includes('.json')) {
9
+ const paths = urlPath
6
10
  .replace(/^\/_next\/data\//, '')
7
11
  .replace(/\.json/, '')
8
12
  .split('/')
9
13
 
10
- redirect = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/'
14
+ urlPath = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/'
11
15
  }
12
16
 
13
- return redirect
17
+ return urlPath
18
+ }
19
+
20
+ export const removeBasePath = (path: string, basePath?: string) => {
21
+ if (basePath && path.startsWith(basePath)) {
22
+ return path.replace(basePath, '')
23
+ }
24
+ return path
25
+ }
26
+
27
+ // https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts
28
+
29
+ export interface PathLocale {
30
+ detectedLocale?: string
31
+ pathname: string
32
+ }
33
+
34
+ /**
35
+ * For a pathname that may include a locale from a list of locales, it
36
+ * removes the locale from the pathname returning it alongside with the
37
+ * detected locale.
38
+ *
39
+ * @param pathname A pathname that may include a locale.
40
+ * @param locales A list of locales.
41
+ * @returns The detected locale and pathname without locale
42
+ */
43
+ export function normalizeLocalePath(pathname: string, locales?: string[]): PathLocale {
44
+ let detectedLocale: string | undefined
45
+ // first item will be empty string from splitting at first char
46
+ const pathnameParts = pathname.split('/')
47
+
48
+ ;(locales || []).some((locale) => {
49
+ if (pathnameParts[1] && pathnameParts[1].toLowerCase() === locale.toLowerCase()) {
50
+ detectedLocale = locale
51
+ pathnameParts.splice(1, 1)
52
+ pathname = pathnameParts.join('/') || '/'
53
+ return true
54
+ }
55
+ return false
56
+ })
57
+ return {
58
+ pathname,
59
+ detectedLocale,
60
+ }
14
61
  }
15
62
 
16
63
  /**
@@ -24,3 +71,28 @@ export function relativizeURL(url: string | string, base: string | URL) {
24
71
  ? relative.toString().replace(origin, '')
25
72
  : relative.toString()
26
73
  }
74
+
75
+ export const normalizeIndex = (path: string) => (path === '/' ? '/index' : path)
76
+
77
+ const stripTrailingSlash = (path: string) =>
78
+ path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path
79
+
80
+ /**
81
+ * Modify a data url to point to a new page route.
82
+ */
83
+ export function rewriteDataPath({
84
+ dataUrl,
85
+ newRoute,
86
+ basePath,
87
+ }: {
88
+ dataUrl: string
89
+ newRoute: string
90
+ basePath?: string
91
+ }) {
92
+ const normalizedDataUrl = normalizeDataUrl(removeBasePath(dataUrl, basePath))
93
+
94
+ return dataUrl.replace(
95
+ normalizeIndex(normalizedDataUrl),
96
+ stripTrailingSlash(normalizeIndex(newRoute)),
97
+ )
98
+ }
@@ -1,7 +1,10 @@
1
1
  import type { Context } from '@netlify/edge-functions'
2
2
 
3
3
  import matchers from './matchers.json' assert { type: 'json' }
4
+ import nextConfig from './next.config.json' assert { type: 'json' }
4
5
 
6
+ import { InternalHeaders } from './lib/headers.ts'
7
+ import { logger, LogLevel } from './lib/logging.ts'
5
8
  import { buildNextRequest, RequestData } from './lib/next-request.ts'
6
9
  import { buildResponse } from './lib/response.ts'
7
10
  import { FetchEventResult } from './lib/response.ts'
@@ -29,20 +32,34 @@ export async function handleMiddleware(
29
32
  context: Context,
30
33
  nextHandler: NextHandler,
31
34
  ) {
32
- const nextRequest = buildNextRequest(request, context)
35
+ const nextRequest = buildNextRequest(request, context, nextConfig)
33
36
  const url = new URL(request.url)
37
+ const reqLogger = logger
38
+ .withLogLevel(
39
+ request.headers.has(InternalHeaders.NFDebugLogging) ? LogLevel.Debug : LogLevel.Log,
40
+ )
41
+ .withFields({ url_path: url.pathname })
42
+ .withRequestID(request.headers.get(InternalHeaders.NFRequestID))
34
43
 
35
44
  // While we have already checked the path when mapping to the edge function,
36
45
  // Next.js supports extra rules that we need to check here too, because we
37
46
  // might be running an edge function for a path we should not. If we find
38
47
  // that's the case, short-circuit the execution.
39
48
  if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
49
+ reqLogger.debug('Aborting middleware due to runtime rules')
50
+
40
51
  return
41
52
  }
42
53
 
43
54
  try {
44
55
  const result = await nextHandler({ request: nextRequest })
45
- const response = await buildResponse({ result, request: request, context })
56
+ const response = await buildResponse({
57
+ context,
58
+ logger: reqLogger,
59
+ request,
60
+ result,
61
+ requestLocale: nextRequest.detectedLocale,
62
+ })
46
63
 
47
64
  return response
48
65
  } catch (error) {
@@ -0,0 +1 @@
1
+ {}
@@ -3,7 +3,8 @@
3
3
  "https://ghuc.cc/worker-tools/resolvable-promise/index.ts": "./raw.githubusercontent.com/worker-tools/resolvable-promise/master/index.ts",
4
4
  "https://deno.land/": "./deno.land/",
5
5
  "https://esm.sh/": "./esm.sh/",
6
- "https://raw.githubusercontent.com/": "./raw.githubusercontent.com/"
6
+ "https://raw.githubusercontent.com/": "./raw.githubusercontent.com/",
7
+ "https://v1-7-0--edge-utils.netlify.app/": "./v1-7-0--edge-utils.netlify.app/"
7
8
  },
8
9
  "scopes": {
9
10
  "./esm.sh/": {
@@ -0,0 +1,164 @@
1
+ type Logger = (...data: unknown[]) => void;
2
+
3
+ export enum LogLevel {
4
+ Debug = 1,
5
+ Log,
6
+ Error,
7
+ }
8
+
9
+ const serializeError = (error: Error): Record<string, unknown> => {
10
+ const cause = error?.cause instanceof Error
11
+ ? serializeError(error.cause)
12
+ : error.cause;
13
+
14
+ return {
15
+ error: error.message,
16
+ error_cause: cause,
17
+ error_stack: error.stack,
18
+ };
19
+ };
20
+
21
+ export class StructuredLogger {
22
+ private fields: Record<string, unknown>;
23
+ private logLevel: LogLevel;
24
+ private message: string;
25
+ private rawLogger?: Logger;
26
+ private requestID?: string;
27
+ __netlifyStructuredLogger: number;
28
+
29
+ constructor(
30
+ message?: string,
31
+ fields?: Record<string, unknown>,
32
+ requestID?: string,
33
+ rawLogger?: Logger,
34
+ logLevel?: LogLevel,
35
+ ) {
36
+ this.fields = fields ?? {};
37
+ this.logLevel = logLevel ?? LogLevel.Log;
38
+ this.message = message ?? "";
39
+ this.rawLogger = rawLogger;
40
+ this.requestID = requestID;
41
+
42
+ // Value should be equal to the major version
43
+ this.__netlifyStructuredLogger = 1;
44
+ }
45
+
46
+ debug(message: string) {
47
+ if (this.logLevel > LogLevel.Debug) {
48
+ return;
49
+ }
50
+
51
+ const logger = this.rawLogger ?? globalThis.console.log;
52
+
53
+ logger(
54
+ new StructuredLogger(
55
+ message,
56
+ this.fields,
57
+ this.requestID,
58
+ this.rawLogger,
59
+ this.logLevel,
60
+ ),
61
+ );
62
+ }
63
+
64
+ error(message: string) {
65
+ if (this.logLevel > LogLevel.Error) {
66
+ return;
67
+ }
68
+
69
+ const logger = this.rawLogger ?? globalThis.console.log;
70
+
71
+ logger(
72
+ new StructuredLogger(
73
+ message,
74
+ this.fields,
75
+ this.requestID,
76
+ this.rawLogger,
77
+ this.logLevel,
78
+ ),
79
+ );
80
+ }
81
+
82
+ log(message: string) {
83
+ if (this.logLevel > LogLevel.Log) {
84
+ return;
85
+ }
86
+
87
+ const logger = this.rawLogger ?? globalThis.console.log;
88
+
89
+ logger(
90
+ new StructuredLogger(
91
+ message,
92
+ this.fields,
93
+ this.requestID,
94
+ this.rawLogger,
95
+ this.logLevel,
96
+ ),
97
+ );
98
+ }
99
+
100
+ serialize() {
101
+ const log = {
102
+ fields: this.fields,
103
+ message: this.message,
104
+ requestID: this.requestID,
105
+ };
106
+
107
+ return log;
108
+ }
109
+
110
+ withError(error: unknown) {
111
+ const fields = error instanceof Error ? serializeError(error) : { error };
112
+
113
+ return this.withFields(fields);
114
+ }
115
+
116
+ withFields(fields: Record<string, unknown>) {
117
+ return new StructuredLogger(
118
+ this.message,
119
+ {
120
+ ...this.fields,
121
+ ...fields,
122
+ },
123
+ this.requestID,
124
+ this.rawLogger,
125
+ this.logLevel,
126
+ );
127
+ }
128
+
129
+ withLogLevel(logLevel: LogLevel) {
130
+ return new StructuredLogger(
131
+ this.message,
132
+ this.fields,
133
+ this.requestID,
134
+ this.rawLogger,
135
+ logLevel,
136
+ );
137
+ }
138
+
139
+ withRawLogger(logger: Logger) {
140
+ return new StructuredLogger(
141
+ this.message,
142
+ this.fields,
143
+ this.requestID,
144
+ logger,
145
+ this.logLevel,
146
+ );
147
+ }
148
+
149
+ withRequestID(requestID: string | null) {
150
+ if (requestID === null) {
151
+ return this;
152
+ }
153
+
154
+ return new StructuredLogger(
155
+ this.message,
156
+ this.fields,
157
+ requestID,
158
+ this.rawLogger,
159
+ this.logLevel,
160
+ );
161
+ }
162
+ }
163
+
164
+ export const logger = new StructuredLogger();
@@ -0,0 +1 @@
1
+ export * from "./logger.ts";
@@ -16,3 +16,5 @@ import 'https://deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
16
16
 
17
17
  import 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/request.js'
18
18
  import 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/response.js'
19
+
20
+ import 'https://v1-7-0--edge-utils.netlify.app/logger/mod.ts'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "5.0.0-beta.2",
3
+ "version": "5.0.0-beta.3",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -1,60 +0,0 @@
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
- };