From 5d67f5fc9b23b826bf44282db655a5a425f4a6f6 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 22 Jul 2022 20:17:33 +0100
Subject: [PATCH 01/31] fix: update patch syntax
---
plugin/src/helpers/files.ts | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/plugin/src/helpers/files.ts b/plugin/src/helpers/files.ts
index 73699bdd5f..1e7971abdf 100644
--- a/plugin/src/helpers/files.ts
+++ b/plugin/src/helpers/files.ts
@@ -331,13 +331,21 @@ const baseServerReplacements: Array<[string, string]> = [
const nextServerReplacements: Array<[string, string]> = [
[
- `getMiddlewareManifest() {\n if (!this.minimalMode) {`,
- `getMiddlewareManifest() {\n if (!this.minimalMode && !process.env.NEXT_USE_NETLIFY_EDGE) {`,
+ `getMiddlewareManifest() {\n if (this.minimalMode) return null;`,
+ `getMiddlewareManifest() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return null;`,
+ ],
+ [
+ `generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode) return []`,
+ `generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return [];`,
],
[
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode) return undefined;`,
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return undefined;`,
],
+ [
+ `getMiddlewareManifest() {\n if (this.minimalMode) {`,
+ `getMiddlewareManifest() {\n if (!this.minimalMode && !process.env.NEXT_USE_NETLIFY_EDGE) {`,
+ ],
]
export const patchNextFiles = async (root: string): Promise => {
From efdb03d45549567abb2e925e9a29c8c035da982a Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 22 Jul 2022 20:18:12 +0100
Subject: [PATCH 02/31] feat: add support for rewriting middleware responses
---
.prettierignore | 1 -
demos/middleware/middleware.ts | 60 +-
demos/middleware/netlify.toml | 789 ++++++++++++++++++++++++++-
demos/middleware/next.config.js | 1 +
demos/middleware/pages/index.js | 3 +
demos/middleware/pages/static.js | 11 +
plugin/src/templates/edge/runtime.ts | 20 +-
plugin/src/templates/edge/utils.ts | 57 +-
8 files changed, 921 insertions(+), 21 deletions(-)
create mode 100644 demos/middleware/pages/static.js
diff --git a/.prettierignore b/.prettierignore
index b626363432..bcac728718 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -21,6 +21,5 @@ node_modules
lib
tsconfig.json
demos/nx-next-monorepo-demo
-plugin/src/templates/edge
plugin/CHANGELOG.md
\ No newline at end of file
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 2f53ce9657..765f774e6e 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,12 +1,68 @@
import { NextResponse } from 'next/server'
-import { NextFetchEvent, NextRequest } from 'next/server'
+import type { NextRequest } from 'next/server'
-export function middleware(request: NextRequest, ev: NextFetchEvent) {
+/**
+ * Supercharge your Next middleware with Netlify Edge Functions
+ */
+class NetlifyResponse {
+ static async next(request: NextRequest): Promise {
+ const context = (request.geo as any).__nf_context
+ if (!context) {
+ throw new Error('NetlifyResponse can only be used with Netlify Edge Functions')
+ }
+ const response: Response = await context.next()
+ return new NetlifyNextResponse(response)
+ }
+}
+
+type NextDataTransform = >(props: T) => T
+
+// A NextReponse that will pass through the Netlify origin response
+// We can't pass it through directly, because Next disallows returning a response body
+class NetlifyNextResponse extends NextResponse {
+ private originResponse: Response
+ private transforms: NextDataTransform[]
+ constructor(originResponse: Response) {
+ super()
+ this.originResponse = originResponse
+ Object.defineProperty(this, 'transforms', {
+ value: [],
+ enumerable: false,
+ writable: false,
+ })
+ }
+
+ /**
+ * Transform the page props before they are passed to the client.
+ * This works for both HTML pages and JSON data
+ */
+ transformData(transform: NextDataTransform) {
+ // The transforms are evaluated after the middleware is returned
+ this.transforms.push(transform)
+ }
+ get headers(): Headers {
+ // If we have the origin response, we should use its headers
+ return this.originResponse?.headers || super.headers
+ }
+}
+
+export async function middleware(request: NextRequest) {
let response
const {
nextUrl: { pathname },
} = request
+ if (pathname.startsWith('/static')) {
+ // Unlike NextResponse.next(), this actually sends the request to the origin
+ const res = await NetlifyResponse.next(request)
+ res.transformData((data) => {
+ data.pageProps.message = `This was static but has been transformed in ${request.geo.country}`
+ return data
+ })
+
+ return res
+ }
+
if (pathname.startsWith('/cookies')) {
response = NextResponse.next()
response.cookies.set('netlifyCookie', 'true')
diff --git a/demos/middleware/netlify.toml b/demos/middleware/netlify.toml
index a0ab27257f..03705982e4 100644
--- a/demos/middleware/netlify.toml
+++ b/demos/middleware/netlify.toml
@@ -1,23 +1,792 @@
+[dev]
+framework = "#static"
+
+[functions]
+
+ [functions._ipx]
+ node_bundler = "nft"
+
+ [functions.___netlify-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
+ [functions.___netlify-odb-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
[build]
command = "npm run build"
publish = ".next"
ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../..; fi;"
-[build.environment]
-NEXT_USE_NETLIFY_EDGE = "true"
+ [build.environment]
+ NEXT_USE_NETLIFY_EDGE = "true"
+ NEXT_PRIVATE_TARGET = "server"
[[plugins]]
package = "../plugin-wrapper/"
-# This is a fake plugin, that makes it run npm install
[[plugins]]
package = "@netlify/plugin-local-install-core"
-[functions]
-included_files = [
- "!node_modules/sharp/vendor/8.12.2/darwin-*/**/*",
- "!node_modules/sharp/build/Release/sharp-darwin-*"
-]
+[[redirects]]
+from = "/_next/static/*"
+to = "/static/:splat"
+status = 200
-[dev]
-framework = "#static"
+[[redirects]]
+from = "/_next/image*"
+to = "/_ipx/w_:width,q_:quality/:url"
+status = 301
+
+ [redirects.query]
+ url = ":url"
+ w = ":width"
+ q = ":quality"
+
+[[redirects]]
+from = "/_ipx/*"
+to = "/.netlify/builders/_ipx"
+status = 200
+
+[[redirects]]
+from = "/cache/*"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/server/*"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/serverless/*"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/trace"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/traces"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/routes-manifest.json"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/build-manifest.json"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/prerender-manifest.json"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/react-loadable-manifest.json"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/BUILD_ID"
+to = "/404.html"
+status = 404
+force = true
+
+[[redirects]]
+from = "/api"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/api/*"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/favicon.ico"
+to = "/favicon.ico"
+status = 200
+
+ [redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+[[redirects]]
+from = "/*"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+force = true
+
+ [redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/cookies"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/shows"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/shows/rewrite-absolute"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/shows/rewrite-external"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/shows/rewriteme"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
+to = "/.netlify/builders/___netlify-odb-handler"
+status = 200
+
+[[redirects]]
+from = "/shows/static/:id"
+to = "/.netlify/builders/___netlify-odb-handler"
+status = 200
+
+[[redirects]]
+from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/shows/:id"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[[redirects]]
+from = "/*"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
+
+[context]
+
+ [context.production]
+
+ [context.production.environment]
+ NEXT_PRIVATE_TARGET = "server"
+
+ [context.production.functions]
+
+ [context.production.functions._ipx]
+ node_bundler = "nft"
+
+ [context.production.functions.___netlify-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
+ [context.production.functions.___netlify-odb-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
+ [context.production.build]
+
+ [context.production.build.environment]
+ NEXT_PRIVATE_TARGET = "server"
+
+ [[context.production.redirects]]
+ from = "/_next/static/*"
+ to = "/static/:splat"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/image*"
+ to = "/_ipx/w_:width,q_:quality/:url"
+ status = 301
+
+ [context.production.redirects.query]
+ url = ":url"
+ w = ":width"
+ q = ":quality"
+
+ [[context.production.redirects]]
+ from = "/_ipx/*"
+ to = "/.netlify/builders/_ipx"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/cache/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/server/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/serverless/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/trace"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/traces"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/routes-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/build-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/prerender-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/react-loadable-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/BUILD_ID"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.production.redirects]]
+ from = "/api"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/api/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/favicon.ico"
+ to = "/favicon.ico"
+ status = 200
+
+ [context.production.redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+ [[context.production.redirects]]
+ from = "/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+ force = true
+
+ [context.production.redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/cookies"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows/rewrite-absolute"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows/rewrite-external"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows/rewriteme"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
+ to = "/.netlify/builders/___netlify-odb-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows/static/:id"
+ to = "/.netlify/builders/___netlify-odb-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/shows/:id"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.production.redirects]]
+ from = "/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [context.main]
+
+ [context.main.environment]
+ NEXT_PRIVATE_TARGET = "server"
+
+ [context.main.functions]
+
+ [context.main.functions._ipx]
+ node_bundler = "nft"
+
+ [context.main.functions.___netlify-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
+ [context.main.functions.___netlify-odb-handler]
+ included_files = [
+ ".env",
+ ".env.local",
+ ".env.production",
+ ".env.production.local",
+ "./public/locales/**",
+ "./next-i18next.config.js",
+ ".next/server/**",
+ ".next/serverless/**",
+ ".next/*.json",
+ ".next/BUILD_ID",
+ ".next/static/chunks/webpack-middleware*.js",
+ "!.next/server/**/*.js.nft.json",
+ "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
+ "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
+ "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
+ "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
+ "!../../node_modules/sharp/**/*"
+ ]
+ external_node_modules = []
+ node_bundler = "nft"
+
+ [context.main.build]
+
+ [context.main.build.environment]
+ NEXT_PRIVATE_TARGET = "server"
+
+ [[context.main.redirects]]
+ from = "/_next/static/*"
+ to = "/static/:splat"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/image*"
+ to = "/_ipx/w_:width,q_:quality/:url"
+ status = 301
+
+ [context.main.redirects.query]
+ url = ":url"
+ w = ":width"
+ q = ":quality"
+
+ [[context.main.redirects]]
+ from = "/_ipx/*"
+ to = "/.netlify/builders/_ipx"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/cache/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/server/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/serverless/*"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/trace"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/traces"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/routes-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/build-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/prerender-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/react-loadable-manifest.json"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/BUILD_ID"
+ to = "/404.html"
+ status = 404
+ force = true
+
+ [[context.main.redirects]]
+ from = "/api"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/api/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/favicon.ico"
+ to = "/favicon.ico"
+ status = 200
+
+ [context.main.redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+ [[context.main.redirects]]
+ from = "/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+ force = true
+
+ [context.main.redirects.conditions]
+ Cookie = [
+ "__prerender_bypass",
+ "__next_preview_data"
+ ]
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/cookies"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows/rewrite-absolute"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows/rewrite-external"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows/rewriteme"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
+ to = "/.netlify/builders/___netlify-odb-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows/static/:id"
+ to = "/.netlify/builders/___netlify-odb-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/shows/:id"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
+
+ [[context.main.redirects]]
+ from = "/*"
+ to = "/.netlify/functions/___netlify-handler"
+ status = 200
\ No newline at end of file
diff --git a/demos/middleware/next.config.js b/demos/middleware/next.config.js
index f99fac3940..b47fec533d 100644
--- a/demos/middleware/next.config.js
+++ b/demos/middleware/next.config.js
@@ -6,6 +6,7 @@ const nextConfig = {
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
+ generateBuildId: () => 'build-id',
}
module.exports = nextConfig
diff --git a/demos/middleware/pages/index.js b/demos/middleware/pages/index.js
index 8d0905762d..1182a71180 100644
--- a/demos/middleware/pages/index.js
+++ b/demos/middleware/pages/index.js
@@ -33,6 +33,9 @@ export default function Home() {
Cookie API
+
+ Rewrite static page content
+
)
diff --git a/demos/middleware/pages/static.js b/demos/middleware/pages/static.js
new file mode 100644
index 0000000000..951a7f22a5
--- /dev/null
+++ b/demos/middleware/pages/static.js
@@ -0,0 +1,11 @@
+const Page = ({ message }) => {message}
+
+export async function getStaticProps(context) {
+ return {
+ props: {
+ message: 'This is a static page',
+ },
+ }
+}
+
+export default Page
diff --git a/plugin/src/templates/edge/runtime.ts b/plugin/src/templates/edge/runtime.ts
index def301d78a..db04060710 100644
--- a/plugin/src/templates/edge/runtime.ts
+++ b/plugin/src/templates/edge/runtime.ts
@@ -38,13 +38,23 @@ const handler = async (req: Request, context: Context) => {
return
}
+ const geo = {
+ country: context.geo.country?.code,
+ region: context.geo.subdivision?.code,
+ city: context.geo.city,
+ }
+
+ // The geo object is passed through to the middleware unchanged
+ // so we're smuggling the Netlify context object inside it
+
+ Object.defineProperty(geo, '__nf_context', {
+ value: context,
+ enumerable: false,
+ })
+
const request: RequestData = {
headers: Object.fromEntries(req.headers.entries()),
- geo: {
- country: context.geo.country?.code,
- region: context.geo.subdivision?.code,
- city: context.geo.city,
- },
+ geo,
url: url.toString(),
method: req.method,
ip: context.ip,
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index 57230273f5..f809c6e986 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -1,14 +1,17 @@
import type { Context } from 'netlify:edge'
+import { HTMLRewriter } from 'https://deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
export interface FetchEventResult {
response: Response
waitUntil: Promise
}
+type NextDataTransform = (data: T) => T
+
/**
* This is how Next handles rewritten URLs.
*/
- export function relativizeURL(url: string | string, base: string | URL) {
+export function relativizeURL(url: string | string, base: string | URL) {
const baseURL = typeof base === 'string' ? new URL(base) : base
const relative = new URL(url, base)
const origin = `${baseURL.protocol}//${baseURL.host}`
@@ -17,7 +20,6 @@ export interface FetchEventResult {
: relative.toString()
}
-
export const addMiddlewareHeaders = async (
originResponse: Promise | Response,
middlewareResponse: Response,
@@ -34,6 +36,12 @@ export const addMiddlewareHeaders = async (
})
return response
}
+
+interface NetlifyNextResponse extends Response {
+ originResponse: Response
+ transforms: NextDataTransform[]
+}
+
export const buildResponse = async ({
result,
request,
@@ -43,13 +51,56 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
+ // This means it's a Netlify Next response.
+ if ('transforms' in result.response) {
+ const response = result.response as NetlifyNextResponse
+ // If it's JSON we don't need to use the rewriter, we can just parse it
+ if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
+ const props = await response.originResponse.json()
+ const transformed = response.transforms.reduce((prev, transform) => {
+ return transform(prev)
+ }, props)
+ return context.json(transformed)
+ }
+ // This var will hold the contents of the script tag
+ let buffer = ''
+ // Create an HTMLRewriter that matches the Next data script tag
+ const rewriter = new HTMLRewriter().on('script[id="__NEXT_DATA__"]', {
+ text(textChunk) {
+ // Grab all the chunks in the Next data script tag
+ buffer += textChunk.text
+ if (textChunk.lastInTextNode) {
+ try {
+ // When we have all the data, try to parse it as JSON
+ const data = JSON.parse(buffer.trim())
+ // Apply all of the transforms to the props
+ const props = response.transforms.reduce((prev, transform) => transform(prev), data.props)
+ // Replace the data with the transformed props
+ textChunk.replace(JSON.stringify({ ...data, props }))
+ } catch (err) {
+ console.log('Could not parse', err)
+ }
+ } else {
+ // Remove the chunk after we've appended it to the buffer
+ textChunk.remove()
+ }
+ },
+ })
+ return rewriter.transform(response.originResponse)
+ }
const res = new Response(result.response.body, result.response)
request.headers.set('x-nf-next-middleware', 'skip')
+ res.headers.forEach((value, key) => {
+ if (key.startsWith('x-request-header-')) {
+ request.headers.set(key.slice(17), value)
+ }
+ })
+
const rewrite = res.headers.get('x-middleware-rewrite')
if (rewrite) {
const rewriteUrl = new URL(rewrite, request.url)
const baseUrl = new URL(request.url)
- if(rewriteUrl.hostname !== baseUrl.hostname) {
+ if (rewriteUrl.hostname !== baseUrl.hostname) {
// Netlify Edge Functions don't support proxying to external domains, but Next middleware does
const proxied = fetch(new Request(rewriteUrl.toString(), request))
return addMiddlewareHeaders(proxied, res)
From e1c41e9180759741bec069f028795664672d0655 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 22 Jul 2022 20:19:02 +0100
Subject: [PATCH 03/31] chore: format
---
plugin/src/templates/edge/ipx.ts | 55 +++++++++++++++-----------------
1 file changed, 25 insertions(+), 30 deletions(-)
diff --git a/plugin/src/templates/edge/ipx.ts b/plugin/src/templates/edge/ipx.ts
index 1298c7ac01..f46818495b 100644
--- a/plugin/src/templates/edge/ipx.ts
+++ b/plugin/src/templates/edge/ipx.ts
@@ -1,14 +1,12 @@
-import { Accepts } from "https://deno.land/x/accepts@2.1.1/mod.ts";
-import type { Context } from "netlify:edge";
+import { Accepts } from 'https://deno.land/x/accepts@2.1.1/mod.ts'
+import type { Context } from 'netlify:edge'
// Available at build time
-import imageconfig from "./imageconfig.json" assert {
- type: "json",
-};
+import imageconfig from './imageconfig.json' assert { type: 'json' }
-const defaultFormat = "webp"
+const defaultFormat = 'webp'
interface ImageConfig extends Record {
- formats?: string[];
+ formats?: string[]
}
/**
@@ -17,41 +15,38 @@ interface ImageConfig extends Record {
// deno-lint-ignore require-await
const handler = async (req: Request, context: Context) => {
- const { searchParams } = new URL(req.url);
- const accept = new Accepts(req.headers);
- const { formats = [defaultFormat] } = imageconfig as ImageConfig;
+ const { searchParams } = new URL(req.url)
+ const accept = new Accepts(req.headers)
+ const { formats = [defaultFormat] } = imageconfig as ImageConfig
if (formats.length === 0) {
- formats.push(defaultFormat);
+ formats.push(defaultFormat)
}
- let type = accept.types(formats) || defaultFormat;
- if(Array.isArray(type)) {
- type = type[0];
+ let type = accept.types(formats) || defaultFormat
+ if (Array.isArray(type)) {
+ type = type[0]
}
-
- const source = searchParams.get("url");
- const width = searchParams.get("w");
- const quality = searchParams.get("q") ?? 75;
+ const source = searchParams.get('url')
+ const width = searchParams.get('w')
+ const quality = searchParams.get('q') ?? 75
if (!source || !width) {
- return new Response("Invalid request", {
+ return new Response('Invalid request', {
status: 400,
- });
+ })
}
- const modifiers = [`w_${width}`, `q_${quality}`];
+ const modifiers = [`w_${width}`, `q_${quality}`]
if (type) {
- if(type.includes('/')) {
+ if (type.includes('/')) {
// If this is a mimetype, strip "image/"
- type = type.split('/')[1];
+ type = type.split('/')[1]
}
- modifiers.push(`f_${type}`);
+ modifiers.push(`f_${type}`)
}
- const target = `/_ipx/${modifiers.join(",")}/${encodeURIComponent(source)}`;
- return context.rewrite(
- target,
- );
-};
+ const target = `/_ipx/${modifiers.join(',')}/${encodeURIComponent(source)}`
+ return context.rewrite(target)
+}
-export default handler;
+export default handler
From d9011d018918fb94a82efe9cdeab10348b8c80dc Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sat, 23 Jul 2022 07:19:32 +0100
Subject: [PATCH 04/31] chore: add extra content
---
demos/middleware/middleware.ts | 1 +
demos/middleware/pages/static.js | 8 +++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 765f774e6e..68333d5285 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -57,6 +57,7 @@ export async function middleware(request: NextRequest) {
const res = await NetlifyResponse.next(request)
res.transformData((data) => {
data.pageProps.message = `This was static but has been transformed in ${request.geo.country}`
+ data.pageProps.showAd = true
return data
})
diff --git a/demos/middleware/pages/static.js b/demos/middleware/pages/static.js
index 951a7f22a5..d524baadd7 100644
--- a/demos/middleware/pages/static.js
+++ b/demos/middleware/pages/static.js
@@ -1,9 +1,15 @@
-const Page = ({ message }) => {message}
+const Page = ({ message, showAd }) => (
+
+
{message}
+ {showAd ?
This is an ad that isn't shown by default
:
No ads for me
}
+
+)
export async function getStaticProps(context) {
return {
props: {
message: 'This is a static page',
+ showAd: false,
},
}
}
From a2c88b7b5ce1fd0c70e6a4fbad9f0384c88847c2 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sat, 23 Jul 2022 07:20:51 +0100
Subject: [PATCH 05/31] chore: use city in demo
---
demos/middleware/middleware.ts | 2 +-
demos/middleware/pages/static.js | 9 ++++++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 68333d5285..67c3bbffbf 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -56,7 +56,7 @@ export async function middleware(request: NextRequest) {
// Unlike NextResponse.next(), this actually sends the request to the origin
const res = await NetlifyResponse.next(request)
res.transformData((data) => {
- data.pageProps.message = `This was static but has been transformed in ${request.geo.country}`
+ data.pageProps.message = `This was static but has been transformed in ${request.geo.city}`
data.pageProps.showAd = true
return data
})
diff --git a/demos/middleware/pages/static.js b/demos/middleware/pages/static.js
index d524baadd7..6ecab25fdf 100644
--- a/demos/middleware/pages/static.js
+++ b/demos/middleware/pages/static.js
@@ -1,7 +1,14 @@
const Page = ({ message, showAd }) => (
{message}
- {showAd ?
This is an ad that isn't shown by default
:
No ads for me
}
+ {showAd ? (
+
+
This is an ad that isn't shown by default
+
+
+ ) : (
+
No ads for me
+ )}
)
From ba284d934e50b80bab8800bf307318c5a2886341 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sat, 23 Jul 2022 09:57:13 +0100
Subject: [PATCH 06/31] feat: add html rewriting
---
demos/middleware/html_rewriter.d.ts | 85 +++++++++++++++++++++++++++++
demos/middleware/middleware.ts | 32 +++++++++--
demos/middleware/pages/static.js | 2 +-
plugin/src/templates/edge/bundle.js | 6 +-
plugin/src/templates/edge/utils.ts | 57 +++++++++++--------
5 files changed, 150 insertions(+), 32 deletions(-)
create mode 100644 demos/middleware/html_rewriter.d.ts
diff --git a/demos/middleware/html_rewriter.d.ts b/demos/middleware/html_rewriter.d.ts
new file mode 100644
index 0000000000..e429cf0df8
--- /dev/null
+++ b/demos/middleware/html_rewriter.d.ts
@@ -0,0 +1,85 @@
+export interface ContentTypeOptions {
+ html?: boolean
+}
+
+export class Element {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ getAttribute(name: string): string | null
+ hasAttribute(name: string): boolean
+ setAttribute(name: string, value: string): this
+ removeAttribute(name: string): this
+ prepend(content: string, options?: ContentTypeOptions): this
+ append(content: string, options?: ContentTypeOptions): this
+ setInnerContent(content: string, options?: ContentTypeOptions): this
+ removeAndKeepContent(): this
+ readonly attributes: IterableIterator<[string, string]>
+ readonly namespaceURI: string
+ readonly removed: boolean
+ tagName: string
+ onEndTag(handler: (this: this, endTag: EndTag) => void | Promise): void
+}
+
+export class EndTag {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ name: string
+}
+
+export class Comment {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ readonly removed: boolean
+ text: string
+}
+
+export class TextChunk {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ readonly lastInTextNode: boolean
+ readonly removed: boolean
+ readonly text: string
+}
+
+export class Doctype {
+ readonly name: string | null
+ readonly publicId: string | null
+ readonly systemId: string | null
+}
+
+export class DocumentEnd {
+ append(content: string, options?: ContentTypeOptions): this
+}
+
+export interface ElementHandlers {
+ element?(element: Element): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(text: TextChunk): void | Promise
+}
+
+export interface DocumentHandlers {
+ doctype?(doctype: Doctype): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(text: TextChunk): void | Promise
+ end?(end: DocumentEnd): void | Promise
+}
+
+export interface HTMLRewriterOptions {
+ enableEsiTags?: boolean
+}
+
+export class HTMLRewriter {
+ constructor(outputSink: (chunk: Uint8Array) => void, options?: HTMLRewriterOptions)
+ on(selector: string, handlers: ElementHandlers): this
+ onDocument(handlers: DocumentHandlers): this
+ write(chunk: Uint8Array): Promise
+ end(): Promise
+ free(): void
+}
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 67c3bbffbf..6d88e7b110 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
+import type { ElementHandlers } from './html_rewriter'
/**
* Supercharge your Next middleware with Netlify Edge Functions
@@ -21,11 +22,18 @@ type NextDataTransform = >(props: T) => T
// We can't pass it through directly, because Next disallows returning a response body
class NetlifyNextResponse extends NextResponse {
private originResponse: Response
- private transforms: NextDataTransform[]
+ private dataTransforms: NextDataTransform[]
+
+ private elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
constructor(originResponse: Response) {
super()
this.originResponse = originResponse
- Object.defineProperty(this, 'transforms', {
+ Object.defineProperty(this, 'dataTransforms', {
+ value: [],
+ enumerable: false,
+ writable: false,
+ })
+ Object.defineProperty(this, 'elementHandlers', {
value: [],
enumerable: false,
writable: false,
@@ -38,8 +46,13 @@ class NetlifyNextResponse extends NextResponse {
*/
transformData(transform: NextDataTransform) {
// The transforms are evaluated after the middleware is returned
- this.transforms.push(transform)
+ this.dataTransforms.push(transform)
}
+
+ rewriteHTML(selector: string, handlers: ElementHandlers) {
+ this.elementHandlers.push([selector, handlers])
+ }
+
get headers(): Headers {
// If we have the origin response, we should use its headers
return this.originResponse?.headers || super.headers
@@ -55,11 +68,22 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
const res = await NetlifyResponse.next(request)
+ const message = `This was static but has been transformed in ${request.geo.city}`
+
res.transformData((data) => {
- data.pageProps.message = `This was static but has been transformed in ${request.geo.city}`
+ data.pageProps.message = message
data.pageProps.showAd = true
return data
})
+ res.rewriteHTML('p[id=message]', {
+ text(textChunk) {
+ if (textChunk.lastInTextNode) {
+ textChunk.replace(message)
+ } else {
+ textChunk.remove()
+ }
+ },
+ })
return res
}
diff --git a/demos/middleware/pages/static.js b/demos/middleware/pages/static.js
index 6ecab25fdf..2345a7b975 100644
--- a/demos/middleware/pages/static.js
+++ b/demos/middleware/pages/static.js
@@ -1,6 +1,6 @@
const Page = ({ message, showAd }) => (
-
{message}
+
{message}
{showAd ? (
This is an ad that isn't shown by default
diff --git a/plugin/src/templates/edge/bundle.js b/plugin/src/templates/edge/bundle.js
index 84846886fa..0765289028 100644
--- a/plugin/src/templates/edge/bundle.js
+++ b/plugin/src/templates/edge/bundle.js
@@ -1,7 +1,7 @@
/**
* This placeholder is replaced with the compiled Next.js bundle at build time
- * @args {Object}
- * @args.request {import("./runtime.ts").RequestData}
+ * @param {Object} props
+ * @param {import("./runtime.ts").RequestData} props.request
* @returns {Promise
}
*/
-export default async (props) => {}
+export default async ({ request }) => {}
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index f809c6e986..72f7afbe33 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -1,5 +1,5 @@
import type { Context } from 'netlify:edge'
-import { HTMLRewriter } from 'https://deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
+import { ElementHandlers, HTMLRewriter } from 'https://deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
export interface FetchEventResult {
response: Response
@@ -39,7 +39,8 @@ export const addMiddlewareHeaders = async (
interface NetlifyNextResponse extends Response {
originResponse: Response
- transforms: NextDataTransform[]
+ dataTransforms: NextDataTransform[]
+ elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
}
export const buildResponse = async ({
@@ -52,12 +53,12 @@ export const buildResponse = async ({
context: Context
}) => {
// This means it's a Netlify Next response.
- if ('transforms' in result.response) {
+ if ('dataTransforms' in result.response) {
const response = result.response as NetlifyNextResponse
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
const props = await response.originResponse.json()
- const transformed = response.transforms.reduce((prev, transform) => {
+ const transformed = response.dataTransforms.reduce((prev, transform) => {
return transform(prev)
}, props)
return context.json(transformed)
@@ -65,27 +66,35 @@ export const buildResponse = async ({
// This var will hold the contents of the script tag
let buffer = ''
// Create an HTMLRewriter that matches the Next data script tag
- const rewriter = new HTMLRewriter().on('script[id="__NEXT_DATA__"]', {
- text(textChunk) {
- // Grab all the chunks in the Next data script tag
- buffer += textChunk.text
- if (textChunk.lastInTextNode) {
- try {
- // When we have all the data, try to parse it as JSON
- const data = JSON.parse(buffer.trim())
- // Apply all of the transforms to the props
- const props = response.transforms.reduce((prev, transform) => transform(prev), data.props)
- // Replace the data with the transformed props
- textChunk.replace(JSON.stringify({ ...data, props }))
- } catch (err) {
- console.log('Could not parse', err)
+ const rewriter = new HTMLRewriter()
+
+ if (response.dataTransforms.length > 0) {
+ rewriter.on('script[id="__NEXT_DATA__"]', {
+ text(textChunk) {
+ // Grab all the chunks in the Next data script tag
+ buffer += textChunk.text
+ if (textChunk.lastInTextNode) {
+ try {
+ // When we have all the data, try to parse it as JSON
+ const data = JSON.parse(buffer.trim())
+ // Apply all of the transforms to the props
+ const props = response.dataTransforms.reduce((prev, transform) => transform(prev), data.props)
+ // Replace the data with the transformed props
+ textChunk.replace(JSON.stringify({ ...data, props }))
+ } catch (err) {
+ console.log('Could not parse', err)
+ }
+ } else {
+ // Remove the chunk after we've appended it to the buffer
+ textChunk.remove()
}
- } else {
- // Remove the chunk after we've appended it to the buffer
- textChunk.remove()
- }
- },
- })
+ },
+ })
+ }
+
+ if (response.elementHandlers.length > 0) {
+ response.elementHandlers.forEach(([selector, handlers]) => rewriter.on(selector, handlers))
+ }
return rewriter.transform(response.originResponse)
}
const res = new Response(result.response.body, result.response)
From d66e08e40211f1de2b6a18efdbb453685f9dcce2 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sat, 23 Jul 2022 16:05:47 +0100
Subject: [PATCH 07/31] feat: add request header support
---
demos/middleware/middleware.ts | 12 ++++++++++++
demos/middleware/pages/api/hello.js | 2 +-
plugin/src/templates/edge/runtime.ts | 7 ++++++-
plugin/src/templates/edge/utils.ts | 5 -----
4 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 6d88e7b110..4b1e510ea2 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -8,9 +8,16 @@ import type { ElementHandlers } from './html_rewriter'
class NetlifyResponse {
static async next(request: NextRequest): Promise {
const context = (request.geo as any).__nf_context
+ const originalRequest: Request = (request.geo as any).__nf_request
+
if (!context) {
throw new Error('NetlifyResponse can only be used with Netlify Edge Functions')
}
+
+ request.headers.forEach((value, key) => {
+ originalRequest.headers.set(key, value)
+ })
+
const response: Response = await context.next()
return new NetlifyNextResponse(response)
}
@@ -88,6 +95,11 @@ export async function middleware(request: NextRequest) {
return res
}
+ if (pathname.startsWith('/api/hello')) {
+ request.headers.set('x-hello', 'world')
+ return NetlifyResponse.next(request)
+ }
+
if (pathname.startsWith('/cookies')) {
response = NextResponse.next()
response.cookies.set('netlifyCookie', 'true')
diff --git a/demos/middleware/pages/api/hello.js b/demos/middleware/pages/api/hello.js
index df63de88fa..5670c43cca 100644
--- a/demos/middleware/pages/api/hello.js
+++ b/demos/middleware/pages/api/hello.js
@@ -1,5 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
- res.status(200).json({ name: 'John Doe' })
+ res.status(200).json({ name: 'John Doe', headers: req.headers })
}
diff --git a/plugin/src/templates/edge/runtime.ts b/plugin/src/templates/edge/runtime.ts
index db04060710..32ed52fc1a 100644
--- a/plugin/src/templates/edge/runtime.ts
+++ b/plugin/src/templates/edge/runtime.ts
@@ -45,13 +45,18 @@ const handler = async (req: Request, context: Context) => {
}
// The geo object is passed through to the middleware unchanged
- // so we're smuggling the Netlify context object inside it
+ // so we're smuggling the Request and Netlify context object inside it
Object.defineProperty(geo, '__nf_context', {
value: context,
enumerable: false,
})
+ Object.defineProperty(geo, '__nf_request', {
+ value: req,
+ enumerable: false,
+ })
+
const request: RequestData = {
headers: Object.fromEntries(req.headers.entries()),
geo,
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index 72f7afbe33..d8ff75ca0e 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -99,11 +99,6 @@ export const buildResponse = async ({
}
const res = new Response(result.response.body, result.response)
request.headers.set('x-nf-next-middleware', 'skip')
- res.headers.forEach((value, key) => {
- if (key.startsWith('x-request-header-')) {
- request.headers.set(key.slice(17), value)
- }
- })
const rewrite = res.headers.get('x-middleware-rewrite')
if (rewrite) {
From 55b5282abb0cde4f3ee10edefe0a815e32ab767d Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sat, 23 Jul 2022 16:46:48 +0100
Subject: [PATCH 08/31] feat: add rewriting
---
demos/middleware/middleware.ts | 47 ++++++++++++++++++++++++----------
1 file changed, 33 insertions(+), 14 deletions(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 4b1e510ea2..b83cde0b49 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,26 +1,42 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import type { ElementHandlers } from './html_rewriter'
+import { NextURL } from 'next/dist/server/web/next-url'
+
+type Context = any
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
class NetlifyResponse {
- static async next(request: NextRequest): Promise {
- const context = (request.geo as any).__nf_context
- const originalRequest: Request = (request.geo as any).__nf_request
+ context: Context
+ originalRequest: Request
+
+ constructor(public request: NextRequest) {
+ this.context = (request.geo as any).__nf_context
+ this.originalRequest = (request.geo as any).__nf_request
- if (!context) {
+ if (!this.context) {
throw new Error('NetlifyResponse can only be used with Netlify Edge Functions')
}
-
- request.headers.forEach((value, key) => {
- originalRequest.headers.set(key, value)
+ }
+ async next(): Promise {
+ this.request.headers.forEach((value, key) => {
+ this.originalRequest.headers.set(key, value)
})
-
- const response: Response = await context.next()
+ const response: Response = await this.context.next()
return new NetlifyNextResponse(response)
}
+
+ rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
+ if (typeof destination === 'string' && destination.startsWith('/')) {
+ destination = new URL(destination, this.request.url)
+ }
+ this.request.headers.forEach((value, key) => {
+ this.originalRequest.headers.set(key, value)
+ })
+ return NextResponse.rewrite(destination, init)
+ }
}
type NextDataTransform = >(props: T) => T
@@ -28,11 +44,9 @@ type NextDataTransform = >(props: T) => T
// A NextReponse that will pass through the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
class NetlifyNextResponse extends NextResponse {
- private originResponse: Response
private dataTransforms: NextDataTransform[]
-
private elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
- constructor(originResponse: Response) {
+ constructor(public originResponse: Response) {
super()
this.originResponse = originResponse
Object.defineProperty(this, 'dataTransforms', {
@@ -74,7 +88,7 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
- const res = await NetlifyResponse.next(request)
+ const res = await new NetlifyResponse(request).next()
const message = `This was static but has been transformed in ${request.geo.city}`
res.transformData((data) => {
@@ -97,7 +111,12 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/api/hello')) {
request.headers.set('x-hello', 'world')
- return NetlifyResponse.next(request)
+ return new NetlifyResponse(request).next()
+ }
+
+ if (pathname.startsWith('/headers')) {
+ request.headers.set('x-hello', 'world')
+ return new NetlifyResponse(request).rewrite('/api/hello')
}
if (pathname.startsWith('/cookies')) {
From 426a116cef2e7724c1627d94154c43491535e33d Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sun, 24 Jul 2022 10:26:01 +0100
Subject: [PATCH 09/31] chore: move NetlifyReponse into a subpackage
---
.eslintignore | 4 +-
.gitignore | 1 +
demos/middleware/middleware.ts | 79 +---------------
demos/middleware/package.json | 4 +-
package-lock.json | 58 +++++-------
plugin/package.json | 16 +++-
plugin/src/middleware/html-rewriter.ts | 89 +++++++++++++++++++
plugin/src/middleware/index.ts | 3 +
.../src/middleware/netlify-next-response.ts | 50 +++++++++++
plugin/src/middleware/netlify-response.ts | 61 +++++++++++++
plugin/tsconfig.json | 3 +
tsconfig.json | 10 ++-
12 files changed, 259 insertions(+), 119 deletions(-)
create mode 100644 plugin/src/middleware/html-rewriter.ts
create mode 100644 plugin/src/middleware/index.ts
create mode 100644 plugin/src/middleware/netlify-next-response.ts
create mode 100644 plugin/src/middleware/netlify-response.ts
diff --git a/.eslintignore b/.eslintignore
index 73eeb62509..07891998fb 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,6 @@ node_modules
test
lib
demos
-plugin/src/templates/edge
\ No newline at end of file
+plugin/src/templates/edge
+plugin/lib
+plugin/dist-types
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d606b6b62a..4f18d78b29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -147,6 +147,7 @@ Temporary Items
demos/default/.next
.parcel-cache
plugin/lib
+plugin/dist-types
# Cypress
cypress/screenshots
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index b83cde0b49..9716e2294f 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,84 +1,7 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import type { ElementHandlers } from './html_rewriter'
-import { NextURL } from 'next/dist/server/web/next-url'
-type Context = any
-
-/**
- * Supercharge your Next middleware with Netlify Edge Functions
- */
-class NetlifyResponse {
- context: Context
- originalRequest: Request
-
- constructor(public request: NextRequest) {
- this.context = (request.geo as any).__nf_context
- this.originalRequest = (request.geo as any).__nf_request
-
- if (!this.context) {
- throw new Error('NetlifyResponse can only be used with Netlify Edge Functions')
- }
- }
- async next(): Promise {
- this.request.headers.forEach((value, key) => {
- this.originalRequest.headers.set(key, value)
- })
- const response: Response = await this.context.next()
- return new NetlifyNextResponse(response)
- }
-
- rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
- if (typeof destination === 'string' && destination.startsWith('/')) {
- destination = new URL(destination, this.request.url)
- }
- this.request.headers.forEach((value, key) => {
- this.originalRequest.headers.set(key, value)
- })
- return NextResponse.rewrite(destination, init)
- }
-}
-
-type NextDataTransform = >(props: T) => T
-
-// A NextReponse that will pass through the Netlify origin response
-// We can't pass it through directly, because Next disallows returning a response body
-class NetlifyNextResponse extends NextResponse {
- private dataTransforms: NextDataTransform[]
- private elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
- constructor(public originResponse: Response) {
- super()
- this.originResponse = originResponse
- Object.defineProperty(this, 'dataTransforms', {
- value: [],
- enumerable: false,
- writable: false,
- })
- Object.defineProperty(this, 'elementHandlers', {
- value: [],
- enumerable: false,
- writable: false,
- })
- }
-
- /**
- * Transform the page props before they are passed to the client.
- * This works for both HTML pages and JSON data
- */
- transformData(transform: NextDataTransform) {
- // The transforms are evaluated after the middleware is returned
- this.dataTransforms.push(transform)
- }
-
- rewriteHTML(selector: string, handlers: ElementHandlers) {
- this.elementHandlers.push([selector, handlers])
- }
-
- get headers(): Headers {
- // If we have the origin response, we should use its headers
- return this.originResponse?.headers || super.headers
- }
-}
+import { NetlifyResponse } from '@netlify/plugin-nextjs/middleware'
export async function middleware(request: NextRequest) {
let response
diff --git a/demos/middleware/package.json b/demos/middleware/package.json
index c3c6755472..8cd8b13353 100644
--- a/demos/middleware/package.json
+++ b/demos/middleware/package.json
@@ -9,12 +9,12 @@
"ntl": "ntl-internal"
},
"dependencies": {
+ "@netlify/plugin-nextjs": "*",
"next": "^12.2.0",
"react": "18.0.0",
"react-dom": "18.0.0"
},
"devDependencies": {
- "@netlify/plugin-nextjs": "*",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.25",
@@ -23,4 +23,4 @@
"npm-run-all": "^4.1.5",
"typescript": "^4.6.3"
}
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a6d1ac284f..1c10a9efe3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -186,12 +186,12 @@
"demos/middleware": {
"version": "0.1.0",
"dependencies": {
+ "@netlify/plugin-nextjs": "*",
"next": "^12.2.0",
"react": "18.0.0",
"react-dom": "18.0.0"
},
"devDependencies": {
- "@netlify/plugin-nextjs": "*",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.25",
@@ -4858,13 +4858,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "dev": true
},
"node_modules/@types/react": {
"version": "17.0.47",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz",
"integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4884,7 +4884,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
- "devOptional": true
+ "dev": true
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -8400,7 +8400,7 @@
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
- "devOptional": true
+ "dev": true
},
"node_modules/custom-routes": {
"resolved": "demos/custom-routes",
@@ -12513,7 +12513,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
- "devOptional": true
+ "dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -19563,7 +19563,7 @@
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.1.tgz",
"integrity": "sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -22610,7 +22610,7 @@
},
"plugin": {
"name": "@netlify/plugin-nextjs",
- "version": "4.12.2",
+ "version": "4.13.0",
"license": "ISC",
"dependencies": {
"@netlify/functions": "^1.0.0",
@@ -26000,13 +26000,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "dev": true
},
"@types/react": {
"version": "17.0.47",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz",
"integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==",
- "devOptional": true,
+ "dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -26026,7 +26026,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
- "devOptional": true
+ "dev": true
},
"@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -26333,8 +26333,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"acorn-walk": {
"version": "7.2.0",
@@ -28675,7 +28674,7 @@
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
- "devOptional": true
+ "dev": true
},
"custom-routes": {
"version": "file:demos/custom-routes",
@@ -29842,15 +29841,13 @@
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-config-standard": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz",
"integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-import-resolver-node": {
"version": "0.3.6",
@@ -30305,8 +30302,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz",
"integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-plugin-react": {
"version": "7.29.4",
@@ -30354,8 +30350,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.5.0.tgz",
"integrity": "sha512-8k1gRt7D7h03kd+SAAlzXkQwWK22BnK6GKZG+FJA6BAGy22CFvl8kCIXKpVux0cCxMWDQUPqSok0LKaZ0aOcCw==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-plugin-unicorn": {
"version": "40.1.0",
@@ -31820,7 +31815,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
- "devOptional": true
+ "dev": true
},
"import-fresh": {
"version": "3.3.0",
@@ -32892,8 +32887,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"jest-regex-util": {
"version": "27.5.1",
@@ -37155,7 +37149,7 @@
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.1.tgz",
"integrity": "sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==",
- "devOptional": true,
+ "dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -38094,8 +38088,7 @@
"styled-jsx": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.2.tgz",
- "integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==",
- "requires": {}
+ "integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ=="
},
"supports-color": {
"version": "9.2.2",
@@ -38871,8 +38864,7 @@
"ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
- "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
- "requires": {}
+ "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg=="
}
}
},
@@ -38980,8 +38972,7 @@
"use-sync-external-store": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
- "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
- "requires": {}
+ "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ=="
},
"util-deprecate": {
"version": "1.0.2",
@@ -39386,8 +39377,7 @@
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"xdg-basedir": {
"version": "4.0.0",
diff --git a/plugin/package.json b/plugin/package.json
index 94427fe050..5330c4729c 100644
--- a/plugin/package.json
+++ b/plugin/package.json
@@ -6,8 +6,20 @@
"files": [
"lib/**/*",
"src/templates/edge/*",
- "manifest.yml"
+ "manifest.yml",
+ "middleware.js"
],
+ "typesVersions": {
+ "*": {
+ "middleware": [
+ "dist-types/middleware"
+ ]
+ }
+ },
+ "exports": {
+ ".": "./lib/index.js",
+ "./middleware": "./lib/middleware/index.js"
+ },
"dependencies": {
"@netlify/functions": "^1.0.0",
"@netlify/ipx": "^1.1.3",
@@ -62,4 +74,4 @@
"engines": {
"node": ">=12.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/plugin/src/middleware/html-rewriter.ts b/plugin/src/middleware/html-rewriter.ts
new file mode 100644
index 0000000000..2b21b46a8c
--- /dev/null
+++ b/plugin/src/middleware/html-rewriter.ts
@@ -0,0 +1,89 @@
+/* eslint-disable max-classes-per-file */
+
+// These types are inlined from the HTMLRewriter package, because we don't use the actual package here
+export interface ContentTypeOptions {
+ html?: boolean
+}
+
+export declare class Element {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ getAttribute(name: string): string | null
+ hasAttribute(name: string): boolean
+ setAttribute(name: string, value: string): this
+ removeAttribute(name: string): this
+ prepend(content: string, options?: ContentTypeOptions): this
+ append(content: string, options?: ContentTypeOptions): this
+ setInnerContent(content: string, options?: ContentTypeOptions): this
+ removeAndKeepContent(): this
+ readonly attributes: IterableIterator<[string, string]>
+ readonly namespaceURI: string
+ readonly removed: boolean
+ tagName: string
+ onEndTag(handler: (this: this, endTag: EndTag) => void | Promise): void
+}
+
+export declare class EndTag {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ name: string
+}
+
+export declare class Comment {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ readonly removed: boolean
+ text: string
+}
+
+export declare class TextChunk {
+ before(content: string, options?: ContentTypeOptions): this
+ after(content: string, options?: ContentTypeOptions): this
+ replace(content: string, options?: ContentTypeOptions): this
+ remove(): this
+ readonly lastInTextNode: boolean
+ readonly removed: boolean
+ readonly text: string
+}
+
+export declare class Doctype {
+ readonly name: string | null
+ readonly publicId: string | null
+ readonly systemId: string | null
+}
+
+export declare class DocumentEnd {
+ append(content: string, options?: ContentTypeOptions): this
+}
+
+export interface ElementHandlers {
+ element?(element: Element): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(text: TextChunk): void | Promise
+}
+
+export interface DocumentHandlers {
+ doctype?(doctype: Doctype): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(text: TextChunk): void | Promise
+ end?(end: DocumentEnd): void | Promise
+}
+
+export interface HTMLRewriterOptions {
+ enableEsiTags?: boolean
+}
+
+export declare class HTMLRewriter {
+ constructor(outputSink: (chunk: Uint8Array) => void, options?: HTMLRewriterOptions)
+ on(selector: string, handlers: ElementHandlers): this
+ onDocument(handlers: DocumentHandlers): this
+ write(chunk: Uint8Array): Promise
+ end(): Promise
+ free(): void
+}
+/* eslint-enable max-classes-per-file */
diff --git a/plugin/src/middleware/index.ts b/plugin/src/middleware/index.ts
new file mode 100644
index 0000000000..1c4c9dd364
--- /dev/null
+++ b/plugin/src/middleware/index.ts
@@ -0,0 +1,3 @@
+export * from './netlify-next-response'
+export * from './netlify-response'
+export * from './html-rewriter'
diff --git a/plugin/src/middleware/netlify-next-response.ts b/plugin/src/middleware/netlify-next-response.ts
new file mode 100644
index 0000000000..0dd3ddcf7f
--- /dev/null
+++ b/plugin/src/middleware/netlify-next-response.ts
@@ -0,0 +1,50 @@
+import { NextResponse } from 'next/server'
+
+import type { ElementHandlers } from './html-rewriter'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type NextDataTransform = >(props: T) => T
+
+// A NextReponse that wraps the Netlify origin response
+// We can't pass it through directly, because Next disallows returning a response body
+export class NetlifyNextResponse extends NextResponse {
+ private readonly dataTransforms: NextDataTransform[]
+ private readonly elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
+ constructor(public originResponse: Response) {
+ super()
+ this.originResponse = originResponse
+
+ // These are private in Node when compiling, but we access them in Deno at runtime
+ Object.defineProperty(this, 'dataTransforms', {
+ value: [],
+ enumerable: false,
+ writable: false,
+ })
+ Object.defineProperty(this, 'elementHandlers', {
+ value: [],
+ enumerable: false,
+ writable: false,
+ })
+ }
+
+ /**
+ * Transform the page props before they are passed to the client.
+ * This works for both HTML pages and JSON data
+ */
+ transformData(transform: NextDataTransform) {
+ // The transforms are evaluated after the middleware is returned
+ this.dataTransforms.push(transform)
+ }
+
+ /**
+ * Rewrite the response HTML with the given selector and handlers
+ */
+ rewriteHTML(selector: string, handlers: ElementHandlers) {
+ this.elementHandlers.push([selector, handlers])
+ }
+
+ get headers(): Headers {
+ // If we have the origin response, we should use its headers
+ return this.originResponse?.headers || super.headers
+ }
+}
diff --git a/plugin/src/middleware/netlify-response.ts b/plugin/src/middleware/netlify-response.ts
new file mode 100644
index 0000000000..995a3ae57c
--- /dev/null
+++ b/plugin/src/middleware/netlify-response.ts
@@ -0,0 +1,61 @@
+/* eslint-disable no-underscore-dangle */
+import { NextURL } from 'next/dist/server/web/next-url'
+import { NextResponse } from 'next/server'
+import type { NextRequest } from 'next/server'
+
+import { NetlifyNextResponse } from './netlify-next-response'
+
+// TODO: add Context type
+type Context = {
+ next: () => Promise
+}
+
+// We sneak our own request and context into the middleware using the geo object
+type AugmentedGeo = NextRequest['geo'] & {
+ // eslint-disable-next-line camelcase
+ __nf_context: Context
+ // eslint-disable-next-line camelcase
+ __nf_request: Request
+}
+
+/**
+ * Supercharge your Next middleware with Netlify Edge Functions
+ */
+export class NetlifyResponse {
+ context: Context
+ originalRequest: Request
+
+ constructor(public request: NextRequest) {
+ if (!('Deno' in globalThis)) {
+ throw new Error('NetlifyResponse only works in a Netlify Edge Function environment')
+ }
+ const geo = request.geo as AugmentedGeo
+ if (!geo) {
+ throw new Error('NetlifyResponse must be instantiated with a NextRequest object')
+ }
+ this.context = geo.__nf_context
+ this.originalRequest = geo.__nf_request
+ }
+
+ // Add the headers to the original request, which will be passed to the origin
+ private applyHeaders() {
+ this.request.headers.forEach((value, name) => {
+ this.originalRequest.headers.set(name, value)
+ })
+ }
+
+ async next(): Promise {
+ this.applyHeaders()
+ const response = await this.context.next()
+ return new NetlifyNextResponse(response)
+ }
+
+ rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
+ if (typeof destination === 'string' && destination.startsWith('/')) {
+ destination = new URL(destination, this.request.url)
+ }
+ this.applyHeaders()
+ return NextResponse.rewrite(destination, init)
+ }
+}
+/* eslint-enable no-underscore-dangle */
diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json
index c1d39c8722..56fb7abc7e 100644
--- a/plugin/tsconfig.json
+++ b/plugin/tsconfig.json
@@ -2,6 +2,9 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./lib" /* Redirect output structure to the directory. */,
+ "declaration": true /* Generates corresponding '.d.ts' file. */,
+ "declarationDir": "./dist-types",
+ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
},
"include": [
"src/**/*.ts",
diff --git a/tsconfig.json b/tsconfig.json
index 6c77f481d9..d9e2dc6261 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,10 @@
// "incremental": true, /* Enable incremental compilation */
"target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
- "lib": ["ES2020"] /* Specify library files to be included in the compilation. */,
+ "lib": [
+ "ES2020",
+ "DOM"
+ ] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -43,7 +46,10 @@
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
- "types": ["jest", "node"], /* Type declaration files to be included in compilation. */
+ "types": [
+ "jest",
+ "node"
+ ], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
From 733e352edc4ec16c427ccd69af254f795d47f68c Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sun, 24 Jul 2022 10:26:17 +0100
Subject: [PATCH 10/31] feat: allow returning `NetlifyReponse` directly
---
plugin/src/templates/edge/utils.ts | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index d8ff75ca0e..f388535495 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -43,6 +43,14 @@ interface NetlifyNextResponse extends Response {
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
}
+interface NetlifyResponse {
+ request: Request
+ context: Context
+ originalRequest: Request
+ next(): Promise
+ rewrite(destination: string | URL, init?: ResponseInit): Response
+}
+
export const buildResponse = async ({
result,
request,
@@ -52,6 +60,10 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
+ // They've returned the NetlifyResponse directly, so we'll call `next()` for them.
+ if ('originalRequest' in result.response) {
+ result.response = await (result.response as unknown as NetlifyResponse).next()
+ }
// This means it's a Netlify Next response.
if ('dataTransforms' in result.response) {
const response = result.response as NetlifyNextResponse
From 138c225d4152bf42f43d2ed53632ce6f00a7f5ee Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sun, 24 Jul 2022 10:28:10 +0100
Subject: [PATCH 11/31] chore: remove inlined types from middleware demo
---
demos/middleware/html_rewriter.d.ts | 85 -----------------------------
1 file changed, 85 deletions(-)
delete mode 100644 demos/middleware/html_rewriter.d.ts
diff --git a/demos/middleware/html_rewriter.d.ts b/demos/middleware/html_rewriter.d.ts
deleted file mode 100644
index e429cf0df8..0000000000
--- a/demos/middleware/html_rewriter.d.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-export interface ContentTypeOptions {
- html?: boolean
-}
-
-export class Element {
- before(content: string, options?: ContentTypeOptions): this
- after(content: string, options?: ContentTypeOptions): this
- replace(content: string, options?: ContentTypeOptions): this
- remove(): this
- getAttribute(name: string): string | null
- hasAttribute(name: string): boolean
- setAttribute(name: string, value: string): this
- removeAttribute(name: string): this
- prepend(content: string, options?: ContentTypeOptions): this
- append(content: string, options?: ContentTypeOptions): this
- setInnerContent(content: string, options?: ContentTypeOptions): this
- removeAndKeepContent(): this
- readonly attributes: IterableIterator<[string, string]>
- readonly namespaceURI: string
- readonly removed: boolean
- tagName: string
- onEndTag(handler: (this: this, endTag: EndTag) => void | Promise): void
-}
-
-export class EndTag {
- before(content: string, options?: ContentTypeOptions): this
- after(content: string, options?: ContentTypeOptions): this
- remove(): this
- name: string
-}
-
-export class Comment {
- before(content: string, options?: ContentTypeOptions): this
- after(content: string, options?: ContentTypeOptions): this
- replace(content: string, options?: ContentTypeOptions): this
- remove(): this
- readonly removed: boolean
- text: string
-}
-
-export class TextChunk {
- before(content: string, options?: ContentTypeOptions): this
- after(content: string, options?: ContentTypeOptions): this
- replace(content: string, options?: ContentTypeOptions): this
- remove(): this
- readonly lastInTextNode: boolean
- readonly removed: boolean
- readonly text: string
-}
-
-export class Doctype {
- readonly name: string | null
- readonly publicId: string | null
- readonly systemId: string | null
-}
-
-export class DocumentEnd {
- append(content: string, options?: ContentTypeOptions): this
-}
-
-export interface ElementHandlers {
- element?(element: Element): void | Promise
- comments?(comment: Comment): void | Promise
- text?(text: TextChunk): void | Promise
-}
-
-export interface DocumentHandlers {
- doctype?(doctype: Doctype): void | Promise
- comments?(comment: Comment): void | Promise
- text?(text: TextChunk): void | Promise
- end?(end: DocumentEnd): void | Promise
-}
-
-export interface HTMLRewriterOptions {
- enableEsiTags?: boolean
-}
-
-export class HTMLRewriter {
- constructor(outputSink: (chunk: Uint8Array) => void, options?: HTMLRewriterOptions)
- on(selector: string, handlers: ElementHandlers): this
- onDocument(handlers: DocumentHandlers): this
- write(chunk: Uint8Array): Promise
- end(): Promise
- free(): void
-}
From 0256f25227b255ed7bcf245f1bd91cd2ae9d90e4 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sun, 24 Jul 2022 10:30:13 +0100
Subject: [PATCH 12/31] chore: remove modified toml
---
demos/middleware/netlify.toml | 789 +---------------------------------
1 file changed, 10 insertions(+), 779 deletions(-)
diff --git a/demos/middleware/netlify.toml b/demos/middleware/netlify.toml
index 03705982e4..a0ab27257f 100644
--- a/demos/middleware/netlify.toml
+++ b/demos/middleware/netlify.toml
@@ -1,792 +1,23 @@
-[dev]
-framework = "#static"
-
-[functions]
-
- [functions._ipx]
- node_bundler = "nft"
-
- [functions.___netlify-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
- [functions.___netlify-odb-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
[build]
command = "npm run build"
publish = ".next"
ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../..; fi;"
- [build.environment]
- NEXT_USE_NETLIFY_EDGE = "true"
- NEXT_PRIVATE_TARGET = "server"
+[build.environment]
+NEXT_USE_NETLIFY_EDGE = "true"
[[plugins]]
package = "../plugin-wrapper/"
+# This is a fake plugin, that makes it run npm install
[[plugins]]
package = "@netlify/plugin-local-install-core"
-[[redirects]]
-from = "/_next/static/*"
-to = "/static/:splat"
-status = 200
-
-[[redirects]]
-from = "/_next/image*"
-to = "/_ipx/w_:width,q_:quality/:url"
-status = 301
-
- [redirects.query]
- url = ":url"
- w = ":width"
- q = ":quality"
-
-[[redirects]]
-from = "/_ipx/*"
-to = "/.netlify/builders/_ipx"
-status = 200
-
-[[redirects]]
-from = "/cache/*"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/server/*"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/serverless/*"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/trace"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/traces"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/routes-manifest.json"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/build-manifest.json"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/prerender-manifest.json"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/react-loadable-manifest.json"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/BUILD_ID"
-to = "/404.html"
-status = 404
-force = true
-
-[[redirects]]
-from = "/api"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/api/*"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/favicon.ico"
-to = "/favicon.ico"
-status = 200
-
- [redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
-[[redirects]]
-from = "/*"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-force = true
-
- [redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/cookies"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/shows"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/shows/rewrite-absolute"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/shows/rewrite-external"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/shows/rewriteme"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
-to = "/.netlify/builders/___netlify-odb-handler"
-status = 200
-
-[[redirects]]
-from = "/shows/static/:id"
-to = "/.netlify/builders/___netlify-odb-handler"
-status = 200
-
-[[redirects]]
-from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/shows/:id"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[[redirects]]
-from = "/*"
-to = "/.netlify/functions/___netlify-handler"
-status = 200
-
-[context]
-
- [context.production]
-
- [context.production.environment]
- NEXT_PRIVATE_TARGET = "server"
-
- [context.production.functions]
-
- [context.production.functions._ipx]
- node_bundler = "nft"
-
- [context.production.functions.___netlify-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
- [context.production.functions.___netlify-odb-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
- [context.production.build]
-
- [context.production.build.environment]
- NEXT_PRIVATE_TARGET = "server"
-
- [[context.production.redirects]]
- from = "/_next/static/*"
- to = "/static/:splat"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/image*"
- to = "/_ipx/w_:width,q_:quality/:url"
- status = 301
-
- [context.production.redirects.query]
- url = ":url"
- w = ":width"
- q = ":quality"
-
- [[context.production.redirects]]
- from = "/_ipx/*"
- to = "/.netlify/builders/_ipx"
- status = 200
-
- [[context.production.redirects]]
- from = "/cache/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/server/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/serverless/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/trace"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/traces"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/routes-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/build-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/prerender-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/react-loadable-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/BUILD_ID"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.production.redirects]]
- from = "/api"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/api/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/favicon.ico"
- to = "/favicon.ico"
- status = 200
-
- [context.production.redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
- [[context.production.redirects]]
- from = "/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
- force = true
-
- [context.production.redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/cookies"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows/rewrite-absolute"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows/rewrite-external"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows/rewriteme"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
- to = "/.netlify/builders/___netlify-odb-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows/static/:id"
- to = "/.netlify/builders/___netlify-odb-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/shows/:id"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.production.redirects]]
- from = "/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [context.main]
-
- [context.main.environment]
- NEXT_PRIVATE_TARGET = "server"
-
- [context.main.functions]
-
- [context.main.functions._ipx]
- node_bundler = "nft"
-
- [context.main.functions.___netlify-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
- [context.main.functions.___netlify-odb-handler]
- included_files = [
- ".env",
- ".env.local",
- ".env.production",
- ".env.production.local",
- "./public/locales/**",
- "./next-i18next.config.js",
- ".next/server/**",
- ".next/serverless/**",
- ".next/*.json",
- ".next/BUILD_ID",
- ".next/static/chunks/webpack-middleware*.js",
- "!.next/server/**/*.js.nft.json",
- "!../../node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
- "!../../node_modules/next/dist/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm",
- "!../../node_modules/next/dist/compiled/webpack/bundle4.js",
- "!../../node_modules/next/dist/compiled/webpack/bundle5.js",
- "!../../node_modules/sharp/**/*"
- ]
- external_node_modules = []
- node_bundler = "nft"
-
- [context.main.build]
-
- [context.main.build.environment]
- NEXT_PRIVATE_TARGET = "server"
-
- [[context.main.redirects]]
- from = "/_next/static/*"
- to = "/static/:splat"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/image*"
- to = "/_ipx/w_:width,q_:quality/:url"
- status = 301
-
- [context.main.redirects.query]
- url = ":url"
- w = ":width"
- q = ":quality"
-
- [[context.main.redirects]]
- from = "/_ipx/*"
- to = "/.netlify/builders/_ipx"
- status = 200
-
- [[context.main.redirects]]
- from = "/cache/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/server/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/serverless/*"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/trace"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/traces"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/routes-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/build-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/prerender-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/react-loadable-manifest.json"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/BUILD_ID"
- to = "/404.html"
- status = 404
- force = true
-
- [[context.main.redirects]]
- from = "/api"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/api/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/favicon.ico"
- to = "/favicon.ico"
- status = 200
-
- [context.main.redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
- [[context.main.redirects]]
- from = "/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
- force = true
-
- [context.main.redirects.conditions]
- Cookie = [
- "__prerender_bypass",
- "__next_preview_data"
- ]
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/index.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/cookies.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/cookies"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-absolute.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows/rewrite-absolute"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewrite-external.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows/rewrite-external"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/rewriteme.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows/rewriteme"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/static/:id.json"
- to = "/.netlify/builders/___netlify-odb-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows/static/:id"
- to = "/.netlify/builders/___netlify-odb-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/_next/data/YLcitAOjPzcjTNIdWrYpy/shows/:id.json"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
-
- [[context.main.redirects]]
- from = "/shows/:id"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
+[functions]
+included_files = [
+ "!node_modules/sharp/vendor/8.12.2/darwin-*/**/*",
+ "!node_modules/sharp/build/Release/sharp-darwin-*"
+]
- [[context.main.redirects]]
- from = "/*"
- to = "/.netlify/functions/___netlify-handler"
- status = 200
\ No newline at end of file
+[dev]
+framework = "#static"
From f35a285a23d253bc103dd0e0b2855665aab46043 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Sun, 24 Jul 2022 10:41:57 +0100
Subject: [PATCH 13/31] chore: add demo links
---
demos/middleware/pages/index.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/demos/middleware/pages/index.js b/demos/middleware/pages/index.js
index 1182a71180..28906e2841 100644
--- a/demos/middleware/pages/index.js
+++ b/demos/middleware/pages/index.js
@@ -33,9 +33,15 @@ export default function Home() {
Cookie API
+
+ Adds `x-hello` request header
+
Rewrite static page content
+
+ Adds `x-hello` request header to a rewrite
+
)
From e5df38554c03e2d50fb246f4c2bb5db3e46a1355 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Mon, 25 Jul 2022 06:54:14 +0100
Subject: [PATCH 14/31] chore: add comments to example
---
demos/middleware/middleware.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 9716e2294f..cdad88a31b 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -14,11 +14,14 @@ export async function middleware(request: NextRequest) {
const res = await new NetlifyResponse(request).next()
const message = `This was static but has been transformed in ${request.geo.city}`
+ // Transform the response page data
res.transformData((data) => {
data.pageProps.message = message
data.pageProps.showAd = true
return data
})
+
+ // Transform the response HTML
res.rewriteHTML('p[id=message]', {
text(textChunk) {
if (textChunk.lastInTextNode) {
@@ -33,11 +36,13 @@ export async function middleware(request: NextRequest) {
}
if (pathname.startsWith('/api/hello')) {
+ // Add a header to the request
request.headers.set('x-hello', 'world')
return new NetlifyResponse(request).next()
}
if (pathname.startsWith('/headers')) {
+ // Add a header to the rewritten request
request.headers.set('x-hello', 'world')
return new NetlifyResponse(request).rewrite('/api/hello')
}
From 2e1a4c61b9977756e0e3e1d47b001682dcf07b26 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Mon, 25 Jul 2022 11:27:30 +0100
Subject: [PATCH 15/31] chore: don't lint generated types
---
.prettierignore | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.prettierignore b/.prettierignore
index bcac728718..081d3085c9 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -22,4 +22,6 @@ lib
tsconfig.json
demos/nx-next-monorepo-demo
-plugin/CHANGELOG.md
\ No newline at end of file
+plugin/CHANGELOG.md
+plugin/lib
+plugin/dist-types
\ No newline at end of file
From d2d16df4bc8377ab207d7e924b3b5b35af6bd68f Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Mon, 25 Jul 2022 13:34:27 +0100
Subject: [PATCH 16/31] chore: rename class
---
demos/middleware/middleware.ts | 8 ++++----
plugin/src/middleware/index.ts | 2 +-
.../{netlify-response.ts => netlify-middleware.ts} | 6 +++---
plugin/src/templates/edge/utils.ts | 6 +++---
4 files changed, 11 insertions(+), 11 deletions(-)
rename plugin/src/middleware/{netlify-response.ts => netlify-middleware.ts} (88%)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index cdad88a31b..7f5df3b262 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { NetlifyResponse } from '@netlify/plugin-nextjs/middleware'
+import { NetlifyMiddleware } from '@netlify/plugin-nextjs/middleware'
export async function middleware(request: NextRequest) {
let response
@@ -11,7 +11,7 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
- const res = await new NetlifyResponse(request).next()
+ const res = await new NetlifyMiddleware(request).next()
const message = `This was static but has been transformed in ${request.geo.city}`
// Transform the response page data
@@ -38,13 +38,13 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/api/hello')) {
// Add a header to the request
request.headers.set('x-hello', 'world')
- return new NetlifyResponse(request).next()
+ return new NetlifyMiddleware(request).next()
}
if (pathname.startsWith('/headers')) {
// Add a header to the rewritten request
request.headers.set('x-hello', 'world')
- return new NetlifyResponse(request).rewrite('/api/hello')
+ return new NetlifyMiddleware(request).rewrite('/api/hello')
}
if (pathname.startsWith('/cookies')) {
diff --git a/plugin/src/middleware/index.ts b/plugin/src/middleware/index.ts
index 1c4c9dd364..86e7daa46a 100644
--- a/plugin/src/middleware/index.ts
+++ b/plugin/src/middleware/index.ts
@@ -1,3 +1,3 @@
export * from './netlify-next-response'
-export * from './netlify-response'
+export * from './netlify-middleware'
export * from './html-rewriter'
diff --git a/plugin/src/middleware/netlify-response.ts b/plugin/src/middleware/netlify-middleware.ts
similarity index 88%
rename from plugin/src/middleware/netlify-response.ts
rename to plugin/src/middleware/netlify-middleware.ts
index 995a3ae57c..128cc6c5c4 100644
--- a/plugin/src/middleware/netlify-response.ts
+++ b/plugin/src/middleware/netlify-middleware.ts
@@ -21,17 +21,17 @@ type AugmentedGeo = NextRequest['geo'] & {
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
-export class NetlifyResponse {
+export class NetlifyMiddleware {
context: Context
originalRequest: Request
constructor(public request: NextRequest) {
if (!('Deno' in globalThis)) {
- throw new Error('NetlifyResponse only works in a Netlify Edge Function environment')
+ throw new Error('NetlifyMiddleware only works in a Netlify Edge Function environment')
}
const geo = request.geo as AugmentedGeo
if (!geo) {
- throw new Error('NetlifyResponse must be instantiated with a NextRequest object')
+ throw new Error('NetlifyMiddleware must be instantiated with a NextRequest object')
}
this.context = geo.__nf_context
this.originalRequest = geo.__nf_request
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index f388535495..a17bfb8838 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -43,7 +43,7 @@ interface NetlifyNextResponse extends Response {
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
}
-interface NetlifyResponse {
+interface NetlifyMiddleware {
request: Request
context: Context
originalRequest: Request
@@ -60,9 +60,9 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
- // They've returned the NetlifyResponse directly, so we'll call `next()` for them.
+ // They've returned the NetlifyMiddleware directly, so we'll call `next()` for them.
if ('originalRequest' in result.response) {
- result.response = await (result.response as unknown as NetlifyResponse).next()
+ result.response = await (result.response as unknown as NetlifyMiddleware).next()
}
// This means it's a Netlify Next response.
if ('dataTransforms' in result.response) {
From e0861cb2a4021d0014f4b3dc884b6131d3ec8a9c Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 10:33:29 +0100
Subject: [PATCH 17/31] chore: add comment about source of htmlrewriter types
---
plugin/src/middleware/html-rewriter.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/plugin/src/middleware/html-rewriter.ts b/plugin/src/middleware/html-rewriter.ts
index 2b21b46a8c..c89115d02e 100644
--- a/plugin/src/middleware/html-rewriter.ts
+++ b/plugin/src/middleware/html-rewriter.ts
@@ -1,6 +1,8 @@
/* eslint-disable max-classes-per-file */
// These types are inlined from the HTMLRewriter package, because we don't use the actual package here
+// https://github.com/cloudflare/html-rewriter-wasm/blob/master/src/html_rewriter.d.ts
+// This is Node code, so we can't import the Deno types from the URL.
export interface ContentTypeOptions {
html?: boolean
}
From 9d0f004587648ceac67a4da568a00135e0bf7c7f Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 10:34:01 +0100
Subject: [PATCH 18/31] refactor: use type guards
---
plugin/src/templates/edge/utils.ts | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index a17bfb8838..68c57e4a5d 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -51,6 +51,14 @@ interface NetlifyMiddleware {
rewrite(destination: string | URL, init?: ResponseInit): Response
}
+function isNetlifyMiddleware(response: Response | NetlifyMiddleware): response is NetlifyMiddleware {
+ return 'originalRequest' in response
+}
+
+function isNetlifyNextResponse(response: Response | NetlifyNextResponse): response is NetlifyNextResponse {
+ return 'dataTransforms' in response
+}
+
export const buildResponse = async ({
result,
request,
@@ -61,12 +69,11 @@ export const buildResponse = async ({
context: Context
}) => {
// They've returned the NetlifyMiddleware directly, so we'll call `next()` for them.
- if ('originalRequest' in result.response) {
- result.response = await (result.response as unknown as NetlifyMiddleware).next()
+ if (isNetlifyMiddleware(result.response)) {
+ result.response = await result.response.next()
}
- // This means it's a Netlify Next response.
- if ('dataTransforms' in result.response) {
- const response = result.response as NetlifyNextResponse
+ if (isNetlifyNextResponse(result.response)) {
+ const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
const props = await response.originResponse.json()
From 8deaba46d62ec9c62ada3f356ace2d505755f05e Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 10:45:33 +0100
Subject: [PATCH 19/31] chore: rename classes
---
demos/middleware/middleware.ts | 8 ++++----
...lify-middleware.ts => enhanced-middleware.ts} | 8 ++++----
...ext-response.ts => enhanced-next-response.ts} | 2 +-
plugin/src/middleware/index.ts | 4 ++--
plugin/src/templates/edge/utils.ts | 16 ++++++++--------
5 files changed, 19 insertions(+), 19 deletions(-)
rename plugin/src/middleware/{netlify-middleware.ts => enhanced-middleware.ts} (89%)
rename plugin/src/middleware/{netlify-next-response.ts => enhanced-next-response.ts} (96%)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 7f5df3b262..06fa4129c5 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { NetlifyMiddleware } from '@netlify/plugin-nextjs/middleware'
+import { EnhancedMiddleware } from '@netlify/plugin-nextjs/middleware'
export async function middleware(request: NextRequest) {
let response
@@ -11,7 +11,7 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
- const res = await new NetlifyMiddleware(request).next()
+ const res = await new EnhancedMiddleware(request).next()
const message = `This was static but has been transformed in ${request.geo.city}`
// Transform the response page data
@@ -38,13 +38,13 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/api/hello')) {
// Add a header to the request
request.headers.set('x-hello', 'world')
- return new NetlifyMiddleware(request).next()
+ return new EnhancedMiddleware(request).next()
}
if (pathname.startsWith('/headers')) {
// Add a header to the rewritten request
request.headers.set('x-hello', 'world')
- return new NetlifyMiddleware(request).rewrite('/api/hello')
+ return new EnhancedMiddleware(request).rewrite('/api/hello')
}
if (pathname.startsWith('/cookies')) {
diff --git a/plugin/src/middleware/netlify-middleware.ts b/plugin/src/middleware/enhanced-middleware.ts
similarity index 89%
rename from plugin/src/middleware/netlify-middleware.ts
rename to plugin/src/middleware/enhanced-middleware.ts
index 128cc6c5c4..2bc6432353 100644
--- a/plugin/src/middleware/netlify-middleware.ts
+++ b/plugin/src/middleware/enhanced-middleware.ts
@@ -3,7 +3,7 @@ import { NextURL } from 'next/dist/server/web/next-url'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { NetlifyNextResponse } from './netlify-next-response'
+import { EnhancedNextResponse } from './enhanced-next-response'
// TODO: add Context type
type Context = {
@@ -21,7 +21,7 @@ type AugmentedGeo = NextRequest['geo'] & {
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
-export class NetlifyMiddleware {
+export class EnhancedMiddleware {
context: Context
originalRequest: Request
@@ -44,10 +44,10 @@ export class NetlifyMiddleware {
})
}
- async next(): Promise {
+ async next(): Promise {
this.applyHeaders()
const response = await this.context.next()
- return new NetlifyNextResponse(response)
+ return new EnhancedNextResponse(response)
}
rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
diff --git a/plugin/src/middleware/netlify-next-response.ts b/plugin/src/middleware/enhanced-next-response.ts
similarity index 96%
rename from plugin/src/middleware/netlify-next-response.ts
rename to plugin/src/middleware/enhanced-next-response.ts
index 0dd3ddcf7f..2c4663cb86 100644
--- a/plugin/src/middleware/netlify-next-response.ts
+++ b/plugin/src/middleware/enhanced-next-response.ts
@@ -7,7 +7,7 @@ export type NextDataTransform = >(props: T) => T
// A NextReponse that wraps the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
-export class NetlifyNextResponse extends NextResponse {
+export class EnhancedNextResponse extends NextResponse {
private readonly dataTransforms: NextDataTransform[]
private readonly elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
constructor(public originResponse: Response) {
diff --git a/plugin/src/middleware/index.ts b/plugin/src/middleware/index.ts
index 86e7daa46a..8f9bba339f 100644
--- a/plugin/src/middleware/index.ts
+++ b/plugin/src/middleware/index.ts
@@ -1,3 +1,3 @@
-export * from './netlify-next-response'
-export * from './netlify-middleware'
+export * from './enhanced-next-response'
+export * from './enhanced-middleware'
export * from './html-rewriter'
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index 68c57e4a5d..dda32e33d0 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -37,25 +37,25 @@ export const addMiddlewareHeaders = async (
return response
}
-interface NetlifyNextResponse extends Response {
+interface EnhancedNextResponse extends Response {
originResponse: Response
dataTransforms: NextDataTransform[]
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
}
-interface NetlifyMiddleware {
+interface EnhancedMiddleware {
request: Request
context: Context
originalRequest: Request
- next(): Promise
+ next(): Promise
rewrite(destination: string | URL, init?: ResponseInit): Response
}
-function isNetlifyMiddleware(response: Response | NetlifyMiddleware): response is NetlifyMiddleware {
+function isEnhancedMiddleware(response: Response | EnhancedMiddleware): response is EnhancedMiddleware {
return 'originalRequest' in response
}
-function isNetlifyNextResponse(response: Response | NetlifyNextResponse): response is NetlifyNextResponse {
+function isEnhancedNextResponse(response: Response | EnhancedNextResponse): response is EnhancedNextResponse {
return 'dataTransforms' in response
}
@@ -68,11 +68,11 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
- // They've returned the NetlifyMiddleware directly, so we'll call `next()` for them.
- if (isNetlifyMiddleware(result.response)) {
+ // They've returned the EnhancedMiddleware directly, so we'll call `next()` for them.
+ if (isEnhancedMiddleware(result.response)) {
result.response = await result.response.next()
}
- if (isNetlifyNextResponse(result.response)) {
+ if (isEnhancedNextResponse(result.response)) {
const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
From 52156884dd2e4f9fd47c85bd9ee933c15249dcdd Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 11:17:02 +0100
Subject: [PATCH 20/31] chore: rename again
---
demos/middleware/middleware.ts | 8 ++++----
plugin/src/middleware/index.ts | 4 ++--
...anced-middleware.ts => middleware-request.ts} | 15 +++++++++++----
...-next-response.ts => next-origin-response.ts} | 3 +--
plugin/src/templates/edge/utils.ts | 16 ++++++++--------
5 files changed, 26 insertions(+), 20 deletions(-)
rename plugin/src/middleware/{enhanced-middleware.ts => middleware-request.ts} (78%)
rename plugin/src/middleware/{enhanced-next-response.ts => next-origin-response.ts} (94%)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 06fa4129c5..3aef48200f 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { EnhancedMiddleware } from '@netlify/plugin-nextjs/middleware'
+import { MiddlewareRequest } from '@netlify/plugin-nextjs/middleware'
export async function middleware(request: NextRequest) {
let response
@@ -11,7 +11,7 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
- const res = await new EnhancedMiddleware(request).next()
+ const res = await new MiddlewareRequest(request).next()
const message = `This was static but has been transformed in ${request.geo.city}`
// Transform the response page data
@@ -38,13 +38,13 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/api/hello')) {
// Add a header to the request
request.headers.set('x-hello', 'world')
- return new EnhancedMiddleware(request).next()
+ return new MiddlewareRequest(request).next()
}
if (pathname.startsWith('/headers')) {
// Add a header to the rewritten request
request.headers.set('x-hello', 'world')
- return new EnhancedMiddleware(request).rewrite('/api/hello')
+ return new MiddlewareRequest(request).rewrite('/api/hello')
}
if (pathname.startsWith('/cookies')) {
diff --git a/plugin/src/middleware/index.ts b/plugin/src/middleware/index.ts
index 8f9bba339f..d787e6096b 100644
--- a/plugin/src/middleware/index.ts
+++ b/plugin/src/middleware/index.ts
@@ -1,3 +1,3 @@
-export * from './enhanced-next-response'
-export * from './enhanced-middleware'
+export * from './next-origin-response'
+export * from './middleware-request'
export * from './html-rewriter'
diff --git a/plugin/src/middleware/enhanced-middleware.ts b/plugin/src/middleware/middleware-request.ts
similarity index 78%
rename from plugin/src/middleware/enhanced-middleware.ts
rename to plugin/src/middleware/middleware-request.ts
index 2bc6432353..068e4b78dd 100644
--- a/plugin/src/middleware/enhanced-middleware.ts
+++ b/plugin/src/middleware/middleware-request.ts
@@ -3,7 +3,7 @@ import { NextURL } from 'next/dist/server/web/next-url'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { EnhancedNextResponse } from './enhanced-next-response'
+import { NextOriginResponse } from './next-origin-response'
// TODO: add Context type
type Context = {
@@ -21,7 +21,7 @@ type AugmentedGeo = NextRequest['geo'] & {
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
-export class EnhancedMiddleware {
+export class MiddlewareRequest {
context: Context
originalRequest: Request
@@ -44,10 +44,10 @@ export class EnhancedMiddleware {
})
}
- async next(): Promise {
+ async next(): Promise {
this.applyHeaders()
const response = await this.context.next()
- return new EnhancedNextResponse(response)
+ return new NextOriginResponse(response)
}
rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
@@ -57,5 +57,12 @@ export class EnhancedMiddleware {
this.applyHeaders()
return NextResponse.rewrite(destination, init)
}
+
+ redirect(destination: string | URL | NextURL, init?: number | ResponseInit) {
+ if (typeof destination === 'string' && destination.startsWith('/')) {
+ destination = new URL(destination, this.request.url)
+ }
+ return NextResponse.redirect(destination, init)
+ }
}
/* eslint-enable no-underscore-dangle */
diff --git a/plugin/src/middleware/enhanced-next-response.ts b/plugin/src/middleware/next-origin-response.ts
similarity index 94%
rename from plugin/src/middleware/enhanced-next-response.ts
rename to plugin/src/middleware/next-origin-response.ts
index 2c4663cb86..172636bf46 100644
--- a/plugin/src/middleware/enhanced-next-response.ts
+++ b/plugin/src/middleware/next-origin-response.ts
@@ -7,12 +7,11 @@ export type NextDataTransform = >(props: T) => T
// A NextReponse that wraps the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
-export class EnhancedNextResponse extends NextResponse {
+export class NextOriginResponse extends NextResponse {
private readonly dataTransforms: NextDataTransform[]
private readonly elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
constructor(public originResponse: Response) {
super()
- this.originResponse = originResponse
// These are private in Node when compiling, but we access them in Deno at runtime
Object.defineProperty(this, 'dataTransforms', {
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index dda32e33d0..ba1c9cc3db 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -37,25 +37,25 @@ export const addMiddlewareHeaders = async (
return response
}
-interface EnhancedNextResponse extends Response {
+interface NextOriginResponse extends Response {
originResponse: Response
dataTransforms: NextDataTransform[]
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
}
-interface EnhancedMiddleware {
+interface MiddlewareRequest {
request: Request
context: Context
originalRequest: Request
- next(): Promise
+ next(): Promise
rewrite(destination: string | URL, init?: ResponseInit): Response
}
-function isEnhancedMiddleware(response: Response | EnhancedMiddleware): response is EnhancedMiddleware {
+function isMiddlewareRequest(response: Response | MiddlewareRequest): response is MiddlewareRequest {
return 'originalRequest' in response
}
-function isEnhancedNextResponse(response: Response | EnhancedNextResponse): response is EnhancedNextResponse {
+function isNextOriginResponse(response: Response | NextOriginResponse): response is NextOriginResponse {
return 'dataTransforms' in response
}
@@ -68,11 +68,11 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
- // They've returned the EnhancedMiddleware directly, so we'll call `next()` for them.
- if (isEnhancedMiddleware(result.response)) {
+ // They've returned the MiddlewareRequest directly, so we'll call `next()` for them.
+ if (isMiddlewareRequest(result.response)) {
result.response = await result.response.next()
}
- if (isEnhancedNextResponse(result.response)) {
+ if (isNextOriginResponse(result.response)) {
const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
From 38543706c24d91d7b6878175229800ac62c48b28 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 11:22:54 +0100
Subject: [PATCH 21/31] chore: rename again
---
plugin/package.json | 2 +-
plugin/src/middleware/index.ts | 4 ++--
.../src/middleware/{middleware-request.ts => request.ts} | 6 +++---
.../middleware/{next-origin-response.ts => response.ts} | 2 +-
plugin/src/templates/edge/utils.ts | 8 ++++----
5 files changed, 11 insertions(+), 11 deletions(-)
rename plugin/src/middleware/{middleware-request.ts => request.ts} (93%)
rename plugin/src/middleware/{next-origin-response.ts => response.ts} (96%)
diff --git a/plugin/package.json b/plugin/package.json
index 7c1075c0e8..20dbed05ac 100644
--- a/plugin/package.json
+++ b/plugin/package.json
@@ -53,7 +53,7 @@
"publish:pull": "git pull",
"publish:install": "npm ci",
"publish:test": "cd .. && npm ci && npm test",
- "clean": "rimraf lib",
+ "clean": "rimraf lib dist-types",
"build": "tsc",
"watch": "tsc --watch",
"prepare": "npm run build"
diff --git a/plugin/src/middleware/index.ts b/plugin/src/middleware/index.ts
index d787e6096b..ca57e04a4d 100644
--- a/plugin/src/middleware/index.ts
+++ b/plugin/src/middleware/index.ts
@@ -1,3 +1,3 @@
-export * from './next-origin-response'
-export * from './middleware-request'
+export * from './response'
+export * from './request'
export * from './html-rewriter'
diff --git a/plugin/src/middleware/middleware-request.ts b/plugin/src/middleware/request.ts
similarity index 93%
rename from plugin/src/middleware/middleware-request.ts
rename to plugin/src/middleware/request.ts
index 068e4b78dd..ffab377353 100644
--- a/plugin/src/middleware/middleware-request.ts
+++ b/plugin/src/middleware/request.ts
@@ -3,7 +3,7 @@ import { NextURL } from 'next/dist/server/web/next-url'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
-import { NextOriginResponse } from './next-origin-response'
+import { MiddlewareResponse } from './response'
// TODO: add Context type
type Context = {
@@ -44,10 +44,10 @@ export class MiddlewareRequest {
})
}
- async next(): Promise {
+ async next(): Promise {
this.applyHeaders()
const response = await this.context.next()
- return new NextOriginResponse(response)
+ return new MiddlewareResponse(response)
}
rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
diff --git a/plugin/src/middleware/next-origin-response.ts b/plugin/src/middleware/response.ts
similarity index 96%
rename from plugin/src/middleware/next-origin-response.ts
rename to plugin/src/middleware/response.ts
index 172636bf46..a04126d077 100644
--- a/plugin/src/middleware/next-origin-response.ts
+++ b/plugin/src/middleware/response.ts
@@ -7,7 +7,7 @@ export type NextDataTransform = >(props: T) => T
// A NextReponse that wraps the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
-export class NextOriginResponse extends NextResponse {
+export class MiddlewareResponse extends NextResponse {
private readonly dataTransforms: NextDataTransform[]
private readonly elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
constructor(public originResponse: Response) {
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index ba1c9cc3db..39f07109d6 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -37,7 +37,7 @@ export const addMiddlewareHeaders = async (
return response
}
-interface NextOriginResponse extends Response {
+interface MiddlewareResponse extends Response {
originResponse: Response
dataTransforms: NextDataTransform[]
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
@@ -47,7 +47,7 @@ interface MiddlewareRequest {
request: Request
context: Context
originalRequest: Request
- next(): Promise
+ next(): Promise
rewrite(destination: string | URL, init?: ResponseInit): Response
}
@@ -55,7 +55,7 @@ function isMiddlewareRequest(response: Response | MiddlewareRequest): response i
return 'originalRequest' in response
}
-function isNextOriginResponse(response: Response | NextOriginResponse): response is NextOriginResponse {
+function isMiddlewareResponse(response: Response | MiddlewareResponse): response is MiddlewareResponse {
return 'dataTransforms' in response
}
@@ -72,7 +72,7 @@ export const buildResponse = async ({
if (isMiddlewareRequest(result.response)) {
result.response = await result.response.next()
}
- if (isNextOriginResponse(result.response)) {
+ if (isMiddlewareResponse(result.response)) {
const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
From e54fec6899a05e909d8cf1f1a7d74c118db6b3ee Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 11:28:02 +0100
Subject: [PATCH 22/31] chore: update example
---
demos/middleware/middleware.ts | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 3aef48200f..35d10297a4 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -3,16 +3,18 @@ import type { NextRequest } from 'next/server'
import { MiddlewareRequest } from '@netlify/plugin-nextjs/middleware'
-export async function middleware(request: NextRequest) {
+export async function middleware(req: NextRequest) {
let response
const {
nextUrl: { pathname },
- } = request
+ } = req
+
+ const request = new MiddlewareRequest(req)
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
- const res = await new MiddlewareRequest(request).next()
- const message = `This was static but has been transformed in ${request.geo.city}`
+ const res = await request.next()
+ const message = `This was static but has been transformed in ${req.geo.city}`
// Transform the response page data
res.transformData((data) => {
@@ -37,14 +39,14 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/api/hello')) {
// Add a header to the request
- request.headers.set('x-hello', 'world')
- return new MiddlewareRequest(request).next()
+ req.headers.set('x-hello', 'world')
+ return request.next()
}
if (pathname.startsWith('/headers')) {
// Add a header to the rewritten request
- request.headers.set('x-hello', 'world')
- return new MiddlewareRequest(request).rewrite('/api/hello')
+ req.headers.set('x-hello', 'world')
+ return request.rewrite('/api/hello')
}
if (pathname.startsWith('/cookies')) {
@@ -55,7 +57,7 @@ export async function middleware(request: NextRequest) {
if (pathname.startsWith('/shows')) {
if (pathname.startsWith('/shows/rewrite-absolute')) {
- response = NextResponse.rewrite(new URL('/shows/100', request.url))
+ response = NextResponse.rewrite(new URL('/shows/100', req.url))
response.headers.set('x-modified-in-rewrite', 'true')
}
if (pathname.startsWith('/shows/rewrite-external')) {
@@ -63,7 +65,7 @@ export async function middleware(request: NextRequest) {
response.headers.set('x-modified-in-rewrite', 'true')
}
if (pathname.startsWith('/shows/rewriteme')) {
- const url = request.nextUrl.clone()
+ const url = req.nextUrl.clone()
url.pathname = '/shows/100'
response = NextResponse.rewrite(url)
response.headers.set('x-modified-in-rewrite', 'true')
From 468a0d3c4d915a06536feb231a1d886d0b0db372 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Tue, 26 Jul 2022 13:14:13 +0100
Subject: [PATCH 23/31] chore: make req a subclass of Request
---
plugin/src/middleware/request.ts | 39 ++++++++++++++++++++++++--------
1 file changed, 29 insertions(+), 10 deletions(-)
diff --git a/plugin/src/middleware/request.ts b/plugin/src/middleware/request.ts
index ffab377353..02499c7995 100644
--- a/plugin/src/middleware/request.ts
+++ b/plugin/src/middleware/request.ts
@@ -21,15 +21,16 @@ type AugmentedGeo = NextRequest['geo'] & {
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
-export class MiddlewareRequest {
+export class MiddlewareRequest extends Request {
context: Context
originalRequest: Request
- constructor(public request: NextRequest) {
+ constructor(private nextRequest: NextRequest) {
+ super(nextRequest)
if (!('Deno' in globalThis)) {
throw new Error('NetlifyMiddleware only works in a Netlify Edge Function environment')
}
- const geo = request.geo as AugmentedGeo
+ const geo = nextRequest.geo as AugmentedGeo
if (!geo) {
throw new Error('NetlifyMiddleware must be instantiated with a NextRequest object')
}
@@ -39,7 +40,7 @@ export class MiddlewareRequest {
// Add the headers to the original request, which will be passed to the origin
private applyHeaders() {
- this.request.headers.forEach((value, name) => {
+ this.headers.forEach((value, name) => {
this.originalRequest.headers.set(name, value)
})
}
@@ -52,17 +53,35 @@ export class MiddlewareRequest {
rewrite(destination: string | URL | NextURL, init?: ResponseInit): NextResponse {
if (typeof destination === 'string' && destination.startsWith('/')) {
- destination = new URL(destination, this.request.url)
+ destination = new URL(destination, this.url)
}
this.applyHeaders()
return NextResponse.rewrite(destination, init)
}
- redirect(destination: string | URL | NextURL, init?: number | ResponseInit) {
- if (typeof destination === 'string' && destination.startsWith('/')) {
- destination = new URL(destination, this.request.url)
- }
- return NextResponse.redirect(destination, init)
+ get headers() {
+ return this.nextRequest.headers
+ }
+
+ get cookies() {
+ return this.nextRequest.cookies
+ }
+
+ get geo() {
+ return this.nextRequest.geo
+ }
+
+ get ip() {
+ return this.nextRequest.ip
+ }
+
+ get nextUrl() {
+ return this.nextRequest.url
+ }
+
+ get url() {
+ return this.nextRequest.url.toString()
}
}
+
/* eslint-enable no-underscore-dangle */
From d4c0dc0548e47096deb9145873b4af48c1d6623d Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Wed, 27 Jul 2022 10:18:41 +0100
Subject: [PATCH 24/31] chore: switch from hidden fields to global map
---
plugin/src/middleware/request.ts | 27 ++++++++------------
plugin/src/templates/edge/runtime.ts | 37 +++++++++++++++++++---------
2 files changed, 35 insertions(+), 29 deletions(-)
diff --git a/plugin/src/middleware/request.ts b/plugin/src/middleware/request.ts
index 02499c7995..360eb452c2 100644
--- a/plugin/src/middleware/request.ts
+++ b/plugin/src/middleware/request.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-underscore-dangle */
import { NextURL } from 'next/dist/server/web/next-url'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
@@ -10,14 +9,6 @@ type Context = {
next: () => Promise
}
-// We sneak our own request and context into the middleware using the geo object
-type AugmentedGeo = NextRequest['geo'] & {
- // eslint-disable-next-line camelcase
- __nf_context: Context
- // eslint-disable-next-line camelcase
- __nf_request: Request
-}
-
/**
* Supercharge your Next middleware with Netlify Edge Functions
*/
@@ -28,14 +19,18 @@ export class MiddlewareRequest extends Request {
constructor(private nextRequest: NextRequest) {
super(nextRequest)
if (!('Deno' in globalThis)) {
- throw new Error('NetlifyMiddleware only works in a Netlify Edge Function environment')
+ throw new Error('MiddlewareRequest only works in a Netlify Edge Function environment')
}
- const geo = nextRequest.geo as AugmentedGeo
- if (!geo) {
- throw new Error('NetlifyMiddleware must be instantiated with a NextRequest object')
+ const requestId = nextRequest.headers.get('x-nf-request-id')
+ if (!requestId) {
+ throw new Error('Missing x-nf-request-id header')
}
- this.context = geo.__nf_context
- this.originalRequest = geo.__nf_request
+ const requestContext = globalThis.NFRequestContextMap.get(requestId)
+ if (!requestContext) {
+ throw new Error(`Could not find request context for request id ${requestId}`)
+ }
+ this.context = requestContext.context
+ this.originalRequest = requestContext.request
}
// Add the headers to the original request, which will be passed to the origin
@@ -83,5 +78,3 @@ export class MiddlewareRequest extends Request {
return this.nextRequest.url.toString()
}
}
-
-/* eslint-enable no-underscore-dangle */
diff --git a/plugin/src/templates/edge/runtime.ts b/plugin/src/templates/edge/runtime.ts
index 32ed52fc1a..4f71b61001 100644
--- a/plugin/src/templates/edge/runtime.ts
+++ b/plugin/src/templates/edge/runtime.ts
@@ -32,6 +32,18 @@ export interface RequestData {
body?: ReadableStream
}
+export interface RequestContext {
+ request: Request
+ context: Context
+}
+
+declare global {
+ // deno-lint-ignore no-var
+ var NFRequestContextMap: Map
+}
+
+globalThis.NFRequestContextMap = new Map()
+
const handler = async (req: Request, context: Context) => {
const url = new URL(req.url)
if (url.pathname.startsWith('/_next/static/')) {
@@ -44,18 +56,15 @@ const handler = async (req: Request, context: Context) => {
city: context.geo.city,
}
- // The geo object is passed through to the middleware unchanged
- // so we're smuggling the Request and Netlify context object inside it
-
- Object.defineProperty(geo, '__nf_context', {
- value: context,
- enumerable: false,
- })
-
- Object.defineProperty(geo, '__nf_request', {
- value: req,
- enumerable: false,
- })
+ const requestId = req.headers.get('x-nf-request-id')
+ if (!requestId) {
+ console.error('Missing x-nf-request-id header')
+ } else {
+ globalThis.NFRequestContextMap.set(requestId, {
+ request: req,
+ context,
+ })
+ }
const request: RequestData = {
headers: Object.fromEntries(req.headers.entries()),
@@ -72,6 +81,10 @@ const handler = async (req: Request, context: Context) => {
} catch (error) {
console.error(error)
return new Response(error.message, { status: 500 })
+ } finally {
+ if (requestId) {
+ globalThis.NFRequestContextMap.delete(requestId)
+ }
}
}
From 0fd6c3354830fa0a479499f048a459643d729321 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 10:24:59 +0100
Subject: [PATCH 25/31] ci: add cypress middleware tests
---
.github/workflows/cypress-middleware.yml | 73 +++++++++++++++++++
cypress/config/middleware.json | 5 ++
.../middleware/rewrites-redirects.spec.ts | 15 ++++
package-lock.json | 3 +-
plugin/src/templates/edge/runtime.ts | 2 +-
tsconfig.json | 12 ++-
6 files changed, 105 insertions(+), 5 deletions(-)
create mode 100644 .github/workflows/cypress-middleware.yml
create mode 100644 cypress/config/middleware.json
create mode 100644 cypress/integration/middleware/rewrites-redirects.spec.ts
diff --git a/.github/workflows/cypress-middleware.yml b/.github/workflows/cypress-middleware.yml
new file mode 100644
index 0000000000..742dd600cd
--- /dev/null
+++ b/.github/workflows/cypress-middleware.yml
@@ -0,0 +1,73 @@
+name: Run e2e (middleware demo)
+on:
+ pull_request:
+ types: [opened, labeled, unlabeled, synchronize]
+ push:
+ branches:
+ - main
+ paths:
+ - 'demos/middleware/**/*.{js,jsx,ts,tsx}'
+ - 'cypress/integration/middleware/**/*.{ts,js}'
+ - 'src/**/*.{ts,js}'
+jobs:
+ cypress:
+ name: Cypress
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ containers: [1, 2, 3, 4]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Generate Github token
+ uses: navikt/github-app-token-generator@v1
+ id: get-token
+ with:
+ private-key: ${{ secrets.TOKENS_PRIVATE_KEY }}
+ app-id: ${{ secrets.TOKENS_APP_ID }}
+
+ - name: Checkout @netlify/wait-for-deploy-action
+ uses: actions/checkout@v2
+ with:
+ repository: netlify/wait-for-deploy-action
+ token: ${{ steps.get-token.outputs.token }}
+ path: ./.github/actions/wait-for-netlify-deploy
+
+ - name: Wait for Netlify Deploy
+ id: deploy
+ uses: ./.github/actions/wait-for-netlify-deploy
+ with:
+ site-name: next-plugin-edge-middleware
+ timeout: 300
+
+ - name: Deploy successful
+ if: ${{ steps.deploy.outputs.origin-url }}
+ run: echo ${{ steps.deploy.outputs.origin-url }}
+
+ - name: Node
+ uses: actions/setup-node@v2
+ with:
+ node-version: '16'
+
+ - run: npm install
+
+ - name: Cypress run
+ if: ${{ steps.deploy.outputs.origin-url }}
+ id: cypress
+ uses: cypress-io/github-action@v2
+ with:
+ browser: chrome
+ headless: true
+ record: true
+ parallel: true
+ config-file: cypress/config/middleware.json
+ group: 'Next Plugin - Middleware'
+ spec: cypress/integration/middleware/*
+ env:
+ DEBUG: '@cypress/github-action'
+ CYPRESS_baseUrl: ${{ steps.deploy.outputs.origin-url }}
+ CYPRESS_NETLIFY_CONTEXT: ${{ steps.deploy.outputs.context }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CYPRESS_RECORD_KEY: ${{ secrets.MIDDLEWARE_CYPRESS_RECORD_KEY }}
diff --git a/cypress/config/middleware.json b/cypress/config/middleware.json
new file mode 100644
index 0000000000..2c8667cf46
--- /dev/null
+++ b/cypress/config/middleware.json
@@ -0,0 +1,5 @@
+{
+ "baseUrl": "http://localhost:3000",
+ "integrationFolder": "cypress/integration/middleware",
+ "projectId": "yn8qwi"
+}
diff --git a/cypress/integration/middleware/rewrites-redirects.spec.ts b/cypress/integration/middleware/rewrites-redirects.spec.ts
new file mode 100644
index 0000000000..faefdbf2dc
--- /dev/null
+++ b/cypress/integration/middleware/rewrites-redirects.spec.ts
@@ -0,0 +1,15 @@
+describe('Rewrites and Redirects', () => {
+ it('rewrites to internal page', () => {
+ // preview mode is off by default
+ cy.visit('/shows/rewriteme')
+ cy.get('h1').should('contain', 'Shows #100')
+ cy.findByText('NextJS on Netlify (imported Header component)')
+ cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewriteme`)
+ })
+
+ it('rewrites to external page', () => {
+ cy.visit('/shows/rewrite-external')
+ cy.get('h1').should('contain', 'Example Domain')
+ cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewrite-external`)
+ })
+})
diff --git a/package-lock.json b/package-lock.json
index 218b57bed9..1a72272197 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25091,8 +25091,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz",
"integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"chalk": {
"version": "5.0.1",
diff --git a/plugin/src/templates/edge/runtime.ts b/plugin/src/templates/edge/runtime.ts
index 4f71b61001..6fbb80af89 100644
--- a/plugin/src/templates/edge/runtime.ts
+++ b/plugin/src/templates/edge/runtime.ts
@@ -42,7 +42,7 @@ declare global {
var NFRequestContextMap: Map
}
-globalThis.NFRequestContextMap = new Map()
+globalThis.NFRequestContextMap ||= new Map()
const handler = async (req: Request, context: Context) => {
const url = new URL(req.url)
diff --git a/tsconfig.json b/tsconfig.json
index d9e2dc6261..a68cb8ff2b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -48,7 +48,7 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
"types": [
"jest",
- "node"
+ "node",
], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
@@ -65,5 +65,13 @@
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
- }
+ },
+ // The following hack is to prevent TS using the chai types instead of jest types.
+ // Source: https://github.com/cypress-io/cypress/issues/1087#issuecomment-552951441
+ "include": [
+ "node_modules/cypress",
+ ],
+ "exclude": [
+ "node_modules/cypress"
+ ]
}
\ No newline at end of file
From 328a58459207ed0da917fcf6e0d6062b82fb2246 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 11:32:47 +0100
Subject: [PATCH 26/31] ci: add tests for middleware headers
---
cypress/config/middleware.json | 2 +-
.../integration/middleware/rewrites-redirects.spec.ts | 11 +++++++++--
demos/middleware/netlify.toml | 10 ++++++++++
3 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/cypress/config/middleware.json b/cypress/config/middleware.json
index 2c8667cf46..c81506ae4b 100644
--- a/cypress/config/middleware.json
+++ b/cypress/config/middleware.json
@@ -1,5 +1,5 @@
{
- "baseUrl": "http://localhost:3000",
+ "baseUrl": "http://localhost:8888",
"integrationFolder": "cypress/integration/middleware",
"projectId": "yn8qwi"
}
diff --git a/cypress/integration/middleware/rewrites-redirects.spec.ts b/cypress/integration/middleware/rewrites-redirects.spec.ts
index faefdbf2dc..8174145b57 100644
--- a/cypress/integration/middleware/rewrites-redirects.spec.ts
+++ b/cypress/integration/middleware/rewrites-redirects.spec.ts
@@ -2,8 +2,7 @@ describe('Rewrites and Redirects', () => {
it('rewrites to internal page', () => {
// preview mode is off by default
cy.visit('/shows/rewriteme')
- cy.get('h1').should('contain', 'Shows #100')
- cy.findByText('NextJS on Netlify (imported Header component)')
+ cy.get('h1').should('contain', 'Show #100')
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewriteme`)
})
@@ -12,4 +11,12 @@ describe('Rewrites and Redirects', () => {
cy.get('h1').should('contain', 'Example Domain')
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewrite-external`)
})
+
+ it('adds headers to static pages', () => {
+ cy.request('/shows/static/3').then((response) => {
+ expect(response.headers).to.have.property('x-middleware-date')
+ expect(response.headers).to.have.property('x-is-deno', 'true')
+ expect(response.headers).to.have.property('x-modified-edge', 'true')
+ })
+ })
})
diff --git a/demos/middleware/netlify.toml b/demos/middleware/netlify.toml
index a0ab27257f..1bb1c1f777 100644
--- a/demos/middleware/netlify.toml
+++ b/demos/middleware/netlify.toml
@@ -21,3 +21,13 @@ included_files = [
[dev]
framework = "#static"
+
+[[redirects]]
+from = "/_next/static/*"
+to = "/static/:splat"
+status = 200
+
+[[redirects]]
+from = "/*"
+to = "/.netlify/functions/___netlify-handler"
+status = 200
From 4f6a2aadc3ca288d711ea7aa3bd07752df912f6d Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 12:07:22 +0100
Subject: [PATCH 27/31] ci: add tests for enhanced middleware
---
.../integration/middleware/enhanced.spec.ts | 30 +++++++++++++++++++
...tes-redirects.spec.ts => standard.spec.ts} | 2 +-
2 files changed, 31 insertions(+), 1 deletion(-)
create mode 100644 cypress/integration/middleware/enhanced.spec.ts
rename cypress/integration/middleware/{rewrites-redirects.spec.ts => standard.spec.ts} (94%)
diff --git a/cypress/integration/middleware/enhanced.spec.ts b/cypress/integration/middleware/enhanced.spec.ts
new file mode 100644
index 0000000000..537b6f7431
--- /dev/null
+++ b/cypress/integration/middleware/enhanced.spec.ts
@@ -0,0 +1,30 @@
+describe('Enhanced middleware', () => {
+ it('adds request headers', () => {
+ cy.request('/api/hello').then((response) => {
+ response.body.json().then((body) => {
+ expect(body.headers).to.have.property('x-hello', 'world')
+ })
+ })
+ })
+
+ it('adds request headers to a rewrite', () => {
+ cy.request('/headers').then((response) => {
+ expect(response.body).to.have.nested.property('headers.x-hello', 'world')
+ })
+ })
+
+ it('rewrites the response body', () => {
+ cy.visit('/static')
+ cy.findByText('This was static but has been transformed in')
+ cy.findByText("This is an ad that isn't shown by default")
+ })
+
+ it('modifies the page props', () => {
+ cy.request('/_next/data/build-id/static.json').then((response) => {
+ expect(response.body).to.have.nested.property('pageProps.showAd', true)
+ expect(response.body)
+ .to.have.nested.property('pageProps.message')
+ .that.includes('This was static but has been transformed in')
+ })
+ })
+})
diff --git a/cypress/integration/middleware/rewrites-redirects.spec.ts b/cypress/integration/middleware/standard.spec.ts
similarity index 94%
rename from cypress/integration/middleware/rewrites-redirects.spec.ts
rename to cypress/integration/middleware/standard.spec.ts
index 8174145b57..dd6b38dd97 100644
--- a/cypress/integration/middleware/rewrites-redirects.spec.ts
+++ b/cypress/integration/middleware/standard.spec.ts
@@ -1,4 +1,4 @@
-describe('Rewrites and Redirects', () => {
+describe('Standard middleware', () => {
it('rewrites to internal page', () => {
// preview mode is off by default
cy.visit('/shows/rewriteme')
From 1cf3e1fb58456131b9442e37b495722127f7b9e6 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 12:54:02 +0100
Subject: [PATCH 28/31] chore: fix test
---
cypress/integration/middleware/enhanced.spec.ts | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/cypress/integration/middleware/enhanced.spec.ts b/cypress/integration/middleware/enhanced.spec.ts
index 537b6f7431..624a61e574 100644
--- a/cypress/integration/middleware/enhanced.spec.ts
+++ b/cypress/integration/middleware/enhanced.spec.ts
@@ -1,9 +1,7 @@
describe('Enhanced middleware', () => {
it('adds request headers', () => {
cy.request('/api/hello').then((response) => {
- response.body.json().then((body) => {
- expect(body.headers).to.have.property('x-hello', 'world')
- })
+ expect(response.body).to.have.nested.property('headers.x-hello', 'world')
})
})
@@ -14,6 +12,9 @@ describe('Enhanced middleware', () => {
})
it('rewrites the response body', () => {
+ cy.on('uncaught:exception', (err, runnable) => {
+ console.log(err.message)
+ })
cy.visit('/static')
cy.findByText('This was static but has been transformed in')
cy.findByText("This is an ad that isn't shown by default")
From 33ade902ff769ef002afc7e48131d2a1515327fa Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 13:26:06 +0100
Subject: [PATCH 29/31] fix: handle other HTTP verbs
---
plugin/src/templates/edge/utils.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index 39f07109d6..49a6d3d659 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -73,6 +73,9 @@ export const buildResponse = async ({
result.response = await result.response.next()
}
if (isMiddlewareResponse(result.response)) {
+ if (request.method === 'HEAD' || request.method === 'OPTIONS') {
+ return response.originResponse
+ }
const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
From 9f4044c9133c5e982401485d8bcaaac6a2c520da Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 14:49:13 +0100
Subject: [PATCH 30/31] feat: add helper methods
---
demos/middleware/middleware.ts | 21 +++----------
demos/middleware/pages/static.js | 41 ++++++++++++++++---------
plugin/src/middleware/response.ts | 48 +++++++++++++++++++++++++++++-
plugin/src/templates/edge/utils.ts | 2 +-
4 files changed, 79 insertions(+), 33 deletions(-)
diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts
index 35d10297a4..2d0b5ea1d5 100644
--- a/demos/middleware/middleware.ts
+++ b/demos/middleware/middleware.ts
@@ -16,23 +16,10 @@ export async function middleware(req: NextRequest) {
const res = await request.next()
const message = `This was static but has been transformed in ${req.geo.city}`
- // Transform the response page data
- res.transformData((data) => {
- data.pageProps.message = message
- data.pageProps.showAd = true
- return data
- })
-
- // Transform the response HTML
- res.rewriteHTML('p[id=message]', {
- text(textChunk) {
- if (textChunk.lastInTextNode) {
- textChunk.replace(message)
- } else {
- textChunk.remove()
- }
- },
- })
+ // Transform the response HTML and props
+ res.replaceText('p[id=message]', message)
+ res.setPageProp('message', message)
+ res.setPageProp('showAd', true)
return res
}
diff --git a/demos/middleware/pages/static.js b/demos/middleware/pages/static.js
index 2345a7b975..0ac0fd6121 100644
--- a/demos/middleware/pages/static.js
+++ b/demos/middleware/pages/static.js
@@ -1,18 +1,31 @@
-const Page = ({ message, showAd }) => (
-
-
{message}
- {showAd ? (
-
-
This is an ad that isn't shown by default
-
-
- ) : (
-
No ads for me
- )}
-
-)
+import * as React from 'react'
-export async function getStaticProps(context) {
+const useHydrated = () => {
+ const [hydrated, setHydrated] = React.useState(false)
+ React.useEffect(() => {
+ setHydrated(true)
+ }, [])
+ return hydrated
+}
+
+const Page = ({ message, showAd }) => {
+ const hydrated = useHydrated()
+ return (
+
+
{message}
+ {hydrated && showAd ? (
+
+
This is an ad that isn't shown by default
+
+
+ ) : (
+
No ads for me
+ )}
+
+ )
+}
+
+export async function getStaticProps() {
return {
props: {
message: 'This is a static page',
diff --git a/plugin/src/middleware/response.ts b/plugin/src/middleware/response.ts
index a04126d077..04f21be79b 100644
--- a/plugin/src/middleware/response.ts
+++ b/plugin/src/middleware/response.ts
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
import type { ElementHandlers } from './html-rewriter'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type NextDataTransform = >(props: T) => T
+export type NextDataTransform = }>(props: T) => T
// A NextReponse that wraps the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
@@ -42,6 +42,52 @@ export class MiddlewareResponse extends NextResponse {
this.elementHandlers.push([selector, handlers])
}
+ /**
+ * Sets the value of a page prop.
+ * @see transformData if you need more control
+ */
+ setPageProp(key: string, value: unknown) {
+ this.transformData((props) => {
+ props.pageProps ||= {}
+ props.pageProps[key] = value
+ return props
+ })
+ }
+
+ /**
+ * Replace the text of the given element. Takes either a string or a function
+ * that is passed the original string and returns new new string.
+ * @see rewriteHTML for more control
+ */
+ replaceText(selector: string, valueOrReplacer: string | ((input: string) => string)): void {
+ // If it's a string then our job is simpler, because we don't need to collect the current text
+ if (typeof valueOrReplacer === 'string') {
+ this.rewriteHTML(selector, {
+ text(textChunk) {
+ if (textChunk.lastInTextNode) {
+ textChunk.replace(valueOrReplacer)
+ } else {
+ textChunk.remove()
+ }
+ },
+ })
+ } else {
+ let text = ''
+ this.rewriteHTML(selector, {
+ text(textChunk) {
+ text += textChunk.text
+ // We're finished, so we can replace the text
+ if (textChunk.lastInTextNode) {
+ textChunk.replace(valueOrReplacer(text))
+ } else {
+ // Remove the chunk, because we'll be adding it back later
+ textChunk.remove()
+ }
+ },
+ })
+ }
+ }
+
get headers(): Headers {
// If we have the origin response, we should use its headers
return this.originResponse?.headers || super.headers
diff --git a/plugin/src/templates/edge/utils.ts b/plugin/src/templates/edge/utils.ts
index 49a6d3d659..ffe251e55a 100644
--- a/plugin/src/templates/edge/utils.ts
+++ b/plugin/src/templates/edge/utils.ts
@@ -73,10 +73,10 @@ export const buildResponse = async ({
result.response = await result.response.next()
}
if (isMiddlewareResponse(result.response)) {
+ const { response } = result
if (request.method === 'HEAD' || request.method === 'OPTIONS') {
return response.originResponse
}
- const { response } = result
// If it's JSON we don't need to use the rewriter, we can just parse it
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
const props = await response.originResponse.json()
From 78c8225e6c26fb29d4f90b52d56ddd174d424e52 Mon Sep 17 00:00:00 2001
From: Matt Kane
Date: Fri, 29 Jul 2022 15:16:14 +0100
Subject: [PATCH 31/31] fix: less flaky test
---
cypress/integration/middleware/enhanced.spec.ts | 7 ++-----
plugin/src/middleware/response.ts | 2 +-
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/cypress/integration/middleware/enhanced.spec.ts b/cypress/integration/middleware/enhanced.spec.ts
index 624a61e574..d70d4bacd8 100644
--- a/cypress/integration/middleware/enhanced.spec.ts
+++ b/cypress/integration/middleware/enhanced.spec.ts
@@ -12,12 +12,9 @@ describe('Enhanced middleware', () => {
})
it('rewrites the response body', () => {
- cy.on('uncaught:exception', (err, runnable) => {
- console.log(err.message)
- })
cy.visit('/static')
- cy.findByText('This was static but has been transformed in')
- cy.findByText("This is an ad that isn't shown by default")
+ cy.get('#message').contains('This was static but has been transformed in')
+ cy.contains("This is an ad that isn't shown by default")
})
it('modifies the page props', () => {
diff --git a/plugin/src/middleware/response.ts b/plugin/src/middleware/response.ts
index 04f21be79b..ad01e75609 100644
--- a/plugin/src/middleware/response.ts
+++ b/plugin/src/middleware/response.ts
@@ -5,7 +5,7 @@ import type { ElementHandlers } from './html-rewriter'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type NextDataTransform = }>(props: T) => T
-// A NextReponse that wraps the Netlify origin response
+// A NextResponse that wraps the Netlify origin response
// We can't pass it through directly, because Next disallows returning a response body
export class MiddlewareResponse extends NextResponse {
private readonly dataTransforms: NextDataTransform[]