diff --git a/.vscode/settings.json b/.vscode/settings.json index 928c70e5cd..032d621103 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,5 @@ "demos/middleware/.netlify/edge-functions", "demos/server-components/.netlify/edge-functions", ], - "deno.unstable": true, - "deno.importMap": "demos/server-components/.netlify/edge-functions-import-map.json" + "deno.unstable": true } \ No newline at end of file diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts index 0556e1645f..1344f60949 100644 --- a/demos/middleware/middleware.ts +++ b/demos/middleware/middleware.ts @@ -5,16 +5,13 @@ import { MiddlewareRequest } from '@netlify/next' export async function middleware(req: NextRequest) { let response - const { - nextUrl: { pathname }, - } = req + const { pathname } = req.nextUrl const request = new MiddlewareRequest(req) - if (pathname.startsWith('/static')) { // Unlike NextResponse.next(), this actually sends the request to the origin const res = await request.next() - const message = `This was static but has been transformed in ${req.geo.city}` + const message = `This was static but has been transformed in ${req.geo?.city}` // Transform the response HTML and props res.replaceText('p[id=message]', message) @@ -58,6 +55,16 @@ export async function middleware(req: NextRequest) { response.headers.set('x-modified-in-rewrite', 'true') } + if (pathname.startsWith('/shows/redirectme')) { + const url = req.nextUrl.clone() + url.pathname = '/shows/100' + response = NextResponse.redirect(url) + } + + if (pathname.startsWith('/shows/redirectexternal')) { + response = NextResponse.redirect('http://example.com/') + } + if (!response) { response = NextResponse.next() } diff --git a/demos/middleware/netlify.toml b/demos/middleware/netlify.toml index 51f8d6ad06..ea420c6e94 100644 --- a/demos/middleware/netlify.toml +++ b/demos/middleware/netlify.toml @@ -4,6 +4,8 @@ publish = ".next" ignore = "if [ $CACHED_COMMIT_REF == $COMMIT_REF ]; then (exit 1); else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../..; fi;" [[plugins]] +# Switch these when testing `ntl dev` +# package = "@netlify/plugin-nextjs" package = "../plugin-wrapper/" # This is a fake plugin, that makes it run npm install @@ -15,16 +17,14 @@ included_files = [ "!node_modules/sharp/vendor/8.12.2/darwin-*/**/*", "!node_modules/sharp/build/Release/sharp-darwin-*" ] - -[dev] -framework = "#static" - -[[redirects]] -from = "/_next/static/*" -to = "/static/:splat" -status = 200 - -[[redirects]] -from = "/*" -to = "/.netlify/functions/___netlify-handler" -status = 200 +# Uncomment this if testing the built files rather than dev +# [dev] +# framework = "#static" +# [[redirects]] +# from = "/_next/static/*" +# to = "/static/:splat" +# status = 200 +# [[redirects]] +# from = "/*" +# to = "/.netlify/functions/___netlify-handler" +# status = 200 diff --git a/demos/middleware/package.json b/demos/middleware/package.json index e34818bf73..bef88c4795 100644 --- a/demos/middleware/package.json +++ b/demos/middleware/package.json @@ -9,8 +9,8 @@ "ntl": "ntl-internal" }, "dependencies": { - "@netlify/plugin-nextjs": "*", "@netlify/next": "*", + "@netlify/plugin-nextjs": "*", "next": "^12.2.0", "react": "18.0.0", "react-dom": "18.0.0" @@ -24,4 +24,4 @@ "npm-run-all": "^4.1.5", "typescript": "^4.6.3" } -} \ No newline at end of file +} diff --git a/demos/middleware/pages/index.js b/demos/middleware/pages/index.js index 28906e2841..288eb25ffe 100644 --- a/demos/middleware/pages/index.js +++ b/demos/middleware/pages/index.js @@ -25,6 +25,12 @@ export default function Home() {

Rewrite to external URL

+

+ Redirect URL +

+

+ Redirect to external URL +

Add header to static page

diff --git a/package-lock.json b/package-lock.json index 797d641c58..be7fb5d840 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3671,7 +3671,6 @@ "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild/-/esbuild-0.14.25.tgz", "integrity": "sha512-ko0cMTbYpajNr0Sy6kvSqR+JDvgU/vjJhO061K1h8+Zs4MlF5AUhaITkpSOrP3g45zp++IEwN1Brxr+/BIez+g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -3709,7 +3708,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -3725,7 +3723,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -3741,7 +3738,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -3757,7 +3753,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -3773,7 +3768,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -3789,7 +3783,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -3805,7 +3798,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3821,7 +3813,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3837,7 +3828,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3853,7 +3843,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3869,7 +3858,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3885,7 +3873,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3901,7 +3888,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3917,7 +3903,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3933,7 +3918,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -3949,7 +3933,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -3965,7 +3948,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -3981,7 +3963,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -3997,7 +3978,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -4013,7 +3993,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -22935,10 +22914,12 @@ "version": "4.17.0", "license": "MIT", "dependencies": { + "@netlify/esbuild": "0.14.25", "@netlify/functions": "^1.2.0", "@netlify/ipx": "^1.2.2", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "execa": "^5.1.1", "fs-extra": "^10.0.0", "globby": "^11.0.4", "moize": "^6.1.0", @@ -22969,6 +22950,80 @@ "next": "*" } }, + "packages/runtime/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/runtime/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "packages/runtime/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/runtime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "packages/runtime/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/runtime/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/runtime/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -22982,6 +23037,54 @@ "engines": { "node": ">=10" } + }, + "packages/runtime/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "plugin": { + "name": "@netlify/plugin-nextjs", + "version": "4.14.1", + "extraneous": true, + "license": "ISC", + "dependencies": { + "@netlify/functions": "^1.0.0", + "@netlify/ipx": "^1.2.0", + "@vercel/node-bridge": "^2.1.0", + "chalk": "^4.1.2", + "fs-extra": "^10.0.0", + "globby": "^11.0.4", + "moize": "^6.1.0", + "node-fetch": "^2.6.6", + "node-stream-zip": "^1.15.0", + "outdent": "^0.8.0", + "p-limit": "^3.1.0", + "pathe": "^0.2.0", + "pretty-bytes": "^5.6.0", + "semver": "^7.3.5", + "slash": "^3.0.0", + "tiny-glob": "^0.2.9" + }, + "devDependencies": { + "@delucis/if-env": "^1.1.2", + "@netlify/build": "^27.11.2", + "@types/fs-extra": "^9.0.13", + "@types/jest": "^27.4.1", + "@types/node": "^17.0.25", + "next": "^12.2.0", + "npm-run-all": "^4.1.5", + "typescript": "^4.6.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "next": "*" + } } }, "dependencies": { @@ -25465,7 +25568,6 @@ "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild/-/esbuild-0.14.25.tgz", "integrity": "sha512-ko0cMTbYpajNr0Sy6kvSqR+JDvgU/vjJhO061K1h8+Zs4MlF5AUhaITkpSOrP3g45zp++IEwN1Brxr+/BIez+g==", - "dev": true, "requires": { "@netlify/esbuild-android-64": "0.14.25", "@netlify/esbuild-android-arm64": "0.14.25", @@ -25493,140 +25595,120 @@ "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz", "integrity": "sha512-z8vtc3jPgQxEcW9ldN5XwEPW0BHsaNFFZ4eIYSh0D2kxTCk1K2k6PY6+9+4wsCgyY0J5fnykCEjPj9AQBzCRpg==", - "dev": true, "optional": true }, "@netlify/esbuild-android-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz", "integrity": "sha512-M0MHkLvOsGPano1Lpbwbik09/Dku0Pl9YJKtVZimo55/pd6kUFpktUbO+VSF9gA3ihdisEkL8/Y+gc4wxLbJkg==", - "dev": true, "optional": true }, "@netlify/esbuild-darwin-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz", "integrity": "sha512-V1GAIfYLsCIcGfGfyAQ+VhbJ/GrzrEkMamAZd5jO1I2T1XHyPMe4vYV7W7AZzcwcYzpdlj8MXIESCODlCDXnCQ==", - "dev": true, "optional": true }, "@netlify/esbuild-darwin-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz", "integrity": "sha512-jfX7SY2ZD4NzSCDHZiAJfHKoqINxymToWv5LUml5/FJa6602o+x+ghg8vFezVaap1XTr+ULdFbHOEiqKpeFl+A==", - "dev": true, "optional": true }, "@netlify/esbuild-freebsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz", "integrity": "sha512-rsK6mW/zaFZSPVa+7CthO3bPeW6qBE9VtwHAm5tdXCP3+Qpl+9rQnbs1CEqqWGrNUv+ExlTVqrAUKkdrGq8IPg==", - "dev": true, "optional": true }, "@netlify/esbuild-freebsd-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz", "integrity": "sha512-ym2Tf0dsKWJbVu3keFSs1FZezk1PXmxckuFTr0+hJMUazeNwFqJJQrY3SiN0JM7jh+VunND2RePjfsSZpcK54g==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-32": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz", "integrity": "sha512-BGRAge/+6m8/lCejgLzCdq+GpN9ah3/XBp88YGgufb4h3c2CAxrq9fIlizHyZA4THHh2T/ka3rYdBOC5ciEwEw==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz", "integrity": "sha512-yD579mskxDXrDR2vC7Dw/mEFTEuQoNYBcoKsIq+ctLiyQcKI1WCgAapJ+MCNpIDkmZp4O1uVuqIiMSyoMlv1QQ==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-arm": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz", "integrity": "sha512-NtnVECEKNr53v11E4wJzQtf7oM3HSPShDZEcwadjuK85AIJpISZcc7Hi6k/g4PsSyGjp73hH8Jly2hh+o+ruvQ==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz", "integrity": "sha512-t1BDP9Fb94jut9m+PE4AVaTQE40JaCJEVpszvvP/6aByR5NMQ5BrNaU8e6XZ6MS7bulYsJCEcJ8I/pPraXycqg==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-mips64le": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz", "integrity": "sha512-Fo5sBkAVxxy+lEmKNo1bJD1lrVI9lpdwSzXW/I8k6ly9J8Vf2JNDYgvld4GSkNVTij5jA/zuN7aSQDEoIgx4mA==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-ppc64le": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz", "integrity": "sha512-EDInkVpAqfyfmZtYI9g9E78ohPLtyZinR19/8PGtL4zZcRUP2AnEzQRtv4NkAKAlPGa8plv3SiGsg4qKeeYRFA==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-riscv64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz", "integrity": "sha512-MACKlmgawjSkNBH34AQUNoC4CX+KD4kk5KfneiBzQeV5oUW89yBf2Q/GaqiTB58Jz93juBOkWwiV0z25AmJzvg==", - "dev": true, "optional": true }, "@netlify/esbuild-linux-s390x": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz", "integrity": "sha512-Mti6NSFGQ6GT+C9LTn15k2JttvtMcy+c1Xxqj8GYkiOqbM7Oh6NcMlXQiHxnCCsxw5Jx0WSWjdrn/dKhdiC13A==", - "dev": true, "optional": true }, "@netlify/esbuild-netbsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz", "integrity": "sha512-aNDKGpy926VcnA//hqw+d4k1q1ekpmhDdy0cuEib6ZS7Qb/5xGVRH6mjG8pf0TtonY9x+wiYNuQn4Dn/DwP9Kw==", - "dev": true, "optional": true }, "@netlify/esbuild-openbsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz", "integrity": "sha512-70W5TnRX5MroXVN0munWpF5q/AAWlamoy+PUL6cnDgc7cfnRiHHrndY++ZpWczNif8t4fQKVtC4jdUemnyb8Ag==", - "dev": true, "optional": true }, "@netlify/esbuild-sunos-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz", "integrity": "sha512-UImichNlQInjErof7tuoG/8VVbrn8Y5EVVMI4M+RoCafWh9NSl4a57hohcgwbeGwl5NcGJtHg+l/WqzlHQFFsQ==", - "dev": true, "optional": true }, "@netlify/esbuild-windows-32": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz", "integrity": "sha512-OFisPQBbuIH8wMRm//fs7wQ7d6t1PuLylIUsUSgignjEV3BOts4+pjtq0J8Aq9kkKoVp8HGSJjaxpc6v2ER/KA==", - "dev": true, "optional": true }, "@netlify/esbuild-windows-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz", "integrity": "sha512-BgIxcEcqr4pfRc9fXStIXQVpjIkBUc3XHFEjH2t2R9pcEDU4BpMsdBgj0UA2x3Z0KtwVLLCOZDvSiaL+WkiTqA==", - "dev": true, "optional": true }, "@netlify/esbuild-windows-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/@netlify/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz", "integrity": "sha512-B5Neu8aXqucUthCvAwVX7IvKbNSD/n3VFiQQcH0YQ+mtbzEIRIFaEAIanGdkmLx0shVBOlY9JxIeRThGPt2/2A==", - "dev": true, "optional": true }, "@netlify/eslint-config-node": { @@ -25792,6 +25874,7 @@ "requires": { "@delucis/if-env": "^1.1.2", "@netlify/build": "^27.14.0", + "@netlify/esbuild": "0.14.25", "@netlify/functions": "^1.2.0", "@netlify/ipx": "^1.2.2", "@types/fs-extra": "^9.0.13", @@ -25799,6 +25882,7 @@ "@types/node": "^17.0.25", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "execa": "^5.1.1", "fs-extra": "^10.0.0", "globby": "^11.0.4", "moize": "^6.1.0", @@ -25816,6 +25900,53 @@ "typescript": "^4.6.3" }, "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -25823,6 +25954,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" } } }, diff --git a/packages/next/src/middleware/request.ts b/packages/next/src/middleware/request.ts index 3b15ff039e..c323d607cd 100644 --- a/packages/next/src/middleware/request.ts +++ b/packages/next/src/middleware/request.ts @@ -4,9 +4,18 @@ import type { NextRequest } from 'next/server' import { MiddlewareResponse } from './response' +export interface NextOptions { + /** + * Include conditional request headers in the request to the origin. + * If you do this, you must ensure you check the response for a 304 Not Modified response + * and handle it and the missing bode accordingly. + */ + sendConditionalRequest?: boolean +} + // TODO: add Context type type Context = { - next: () => Promise + next: (options?: NextOptions) => Promise } /** @@ -40,9 +49,12 @@ export class MiddlewareRequest extends Request { }) } - async next(): Promise { + /** + * Passes the request to the origin, allowing you to access the response + */ + async next(options?: NextOptions): Promise { this.applyHeaders() - const response = await this.context.next() + const response = await this.context.next(options) return new MiddlewareResponse(response) } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index f5ef17a6ce..4b6cd40e44 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -9,10 +9,12 @@ "manifest.yml" ], "dependencies": { + "@netlify/esbuild": "0.14.25", "@netlify/functions": "^1.2.0", "@netlify/ipx": "^1.2.2", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "execa": "^5.1.1", "fs-extra": "^10.0.0", "globby": "^11.0.4", "moize": "^6.1.0", @@ -22,8 +24,8 @@ "p-limit": "^3.1.0", "pathe": "^0.2.0", "pretty-bytes": "^5.6.0", - "slash": "^3.0.0", "semver": "^7.3.5", + "slash": "^3.0.0", "tiny-glob": "^0.2.9" }, "devDependencies": { diff --git a/packages/runtime/src/helpers/dev.ts b/packages/runtime/src/helpers/dev.ts new file mode 100644 index 0000000000..c029b0a0e9 --- /dev/null +++ b/packages/runtime/src/helpers/dev.ts @@ -0,0 +1,44 @@ +import { resolve } from 'path' + +import { OnPreBuild } from '@netlify/build' +import execa from 'execa' +import { unlink, existsSync } from 'fs-extra' + +import { writeDevEdgeFunction } from './edge' +import { patchNextFiles } from './files' + +// The types haven't been updated yet +export const onPreDev: OnPreBuild = async ({ constants, netlifyConfig }) => { + // Need to patch the files, because build might not have been run + await patchNextFiles(resolve(netlifyConfig.build.publish, '..')) + + // Clean up old functions + await unlink(resolve('.netlify', 'middleware.js')).catch(() => { + // Ignore if it doesn't exist + }) + await writeDevEdgeFunction(constants) + if ( + !existsSync(resolve(netlifyConfig.build.base, 'middleware.ts')) && + !existsSync(resolve(netlifyConfig.build.base, 'middleware.js')) + ) { + console.log( + "No middleware found. Create a 'middleware.ts' or 'middleware.js' file in your project root to add custom middleware.", + ) + } else { + console.log('Watching for changes in Next.js middleware...') + } + // Eventually we might want to do this via esbuild's API, but for now the CLI works fine + const childProcess = execa(`esbuild`, [ + `--bundle`, + `--outdir=${resolve('.netlify')}`, + `--format=esm`, + '--watch', + // Watch for both, because it can have either ts or js + resolve(netlifyConfig.build.base, 'middleware.ts'), + resolve(netlifyConfig.build.base, 'middleware.js'), + ]) + + childProcess.stdout.pipe(process.stdout) + childProcess.stderr.pipe(process.stderr) + // Don't return the promise because we don't want to wait for the child process to finish +} diff --git a/packages/runtime/src/helpers/edge.ts b/packages/runtime/src/helpers/edge.ts index 4c88b4614c..a0e23aeef5 100644 --- a/packages/runtime/src/helpers/edge.ts +++ b/packages/runtime/src/helpers/edge.ts @@ -2,7 +2,7 @@ import { promises as fs, existsSync } from 'fs' import { resolve, join } from 'path' -import type { NetlifyConfig } from '@netlify/build' +import type { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build' import { copyFile, emptyDir, ensureDir, readJSON, readJson, writeJSON, writeJson } from 'fs-extra' import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin' @@ -20,6 +20,7 @@ export interface FunctionManifest { pattern: string } > + import_map?: string } export const loadMiddlewareManifest = (netlifyConfig: NetlifyConfig): Promise => { @@ -122,6 +123,31 @@ const writeEdgeFunction = async ({ } } +export const writeDevEdgeFunction = async ({ + INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', +}: NetlifyPluginConstants & { + // The constants type needs an update + INTERNAL_EDGE_FUNCTIONS_SRC?: string +}) => { + const manifest: FunctionManifest = { + functions: [ + { + function: 'next-dev', + path: '/*', + }, + ], + version: 1, + } + const edgeFunctionRoot = resolve(INTERNAL_EDGE_FUNCTIONS_SRC) + await emptyDir(edgeFunctionRoot) + await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest) + + const edgeFunctionDir = join(edgeFunctionRoot, 'next-dev') + await ensureDir(edgeFunctionDir) + await copyEdgeSourceFile({ edgeFunctionDir, file: 'next-dev.ts', target: 'index.ts' }) + await copyEdgeSourceFile({ edgeFunctionDir, file: 'utils.ts' }) +} + /** * Writes Edge Functions for the Next middleware */ @@ -185,8 +211,6 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => { export const enableEdgeInNextConfig = async (publish: string) => { const configFile = join(publish, 'required-server-files.json') const config = await readJSON(configFile) - // This is for runtime in Next.js, rather than a build plugin setting - config.config.env.NEXT_USE_NETLIFY_EDGE = 'true' await writeJSON(configFile, config) } /* eslint-enable max-lines */ diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 8c9a84d847..f1761fdcee 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -340,19 +340,19 @@ const baseServerReplacements: Array<[string, string]> = [ const nextServerReplacements: Array<[string, string]> = [ [ `getMiddlewareManifest() {\n if (this.minimalMode) return null;`, - `getMiddlewareManifest() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return null;`, + `getMiddlewareManifest() {\n if (this.minimalMode || !process.env.NEXT_DISABLE_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(devReady) {\n if (this.minimalMode || !process.env.NEXT_DISABLE_NETLIFY_EDGE) return [];`, ], [ `generateCatchAllMiddlewareRoute() {\n if (this.minimalMode) return undefined;`, - `generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return undefined;`, + `generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || !process.env.NEXT_DISABLE_NETLIFY_EDGE) return undefined;`, ], [ `getMiddlewareManifest() {\n if (this.minimalMode) {`, - `getMiddlewareManifest() {\n if (!this.minimalMode && !process.env.NEXT_USE_NETLIFY_EDGE) {`, + `getMiddlewareManifest() {\n if (!this.minimalMode && process.env.NEXT_DISABLE_NETLIFY_EDGE) {`, ], ] diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 75c26d8848..b09960271b 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -15,6 +15,7 @@ import { configureHandlerFunctions, generateCustomHeaders, } from './helpers/config' +import { onPreDev } from './helpers/dev' import { enableEdgeInNextConfig, writeEdgeFunctions, loadMiddlewareManifest } from './helpers/edge' import { moveStaticPages, movePublicFiles, patchNextFiles } from './helpers/files' import { generateFunctions, setupImageFunction, generatePagesResolver } from './helpers/functions' @@ -218,5 +219,29 @@ const plugin: NetlifyPlugin = { warnForRootRedirects({ appDir }) }, } -module.exports = plugin +// The types haven't been updated yet +const nextRuntime = ( + _inputs, + meta: { events?: Set } = {}, +): NetlifyPlugin & { onPreDev?: NetlifyPlugin['onPreBuild'] } => { + if (!meta?.events?.has('onPreDev')) { + return { + ...plugin, + onEnd: ({ utils }) => { + utils.status.show({ + title: 'Please upgrade to the latest version of the Netlify CLI', + summary: + 'To support for the latest Next.js features, please upgrade to the latest version of the Netlify CLI', + }) + }, + } + } + return { + ...plugin, + onPreDev, + } +} + +module.exports = nextRuntime + /* eslint-enable max-lines */ diff --git a/packages/runtime/src/templates/edge/next-dev.ts b/packages/runtime/src/templates/edge/next-dev.ts new file mode 100644 index 0000000000..b640054c6e --- /dev/null +++ b/packages/runtime/src/templates/edge/next-dev.ts @@ -0,0 +1,155 @@ +import type { Context } from 'https://edge.netlify.com' +import { NextRequest, NextResponse } from 'https://esm.sh/next/server' +import { fromFileUrl } from 'https://deno.land/std/path/mod.ts' +import { buildResponse } from './utils.ts' + +export interface FetchEventResult { + response: Response + waitUntil: Promise +} + +interface I18NConfig { + defaultLocale: string + domains?: DomainLocale[] + localeDetection?: false + locales: string[] +} + +interface DomainLocale { + defaultLocale: string + domain: string + http?: true + locales?: string[] +} +export interface NextRequestInit extends RequestInit { + geo?: { + city?: string + country?: string + region?: string + } + ip?: string + nextConfig?: { + basePath?: string + i18n?: I18NConfig | null + trailingSlash?: boolean + } +} + +export interface RequestData { + geo?: { + city?: string + country?: string + region?: string + latitude?: string + longitude?: string + } + headers: Record + ip?: string + method: string + nextConfig?: { + basePath?: string + i18n?: Record + trailingSlash?: boolean + } + page?: { + name?: string + params?: { [key: string]: string } + } + url: string + body?: ReadableStream +} + +export interface RequestContext { + request: Request + context: Context +} + +declare global { + // deno-lint-ignore no-var + var NFRequestContextMap: Map + // deno-lint-ignore no-var + var __dirname: string +} + +globalThis.NFRequestContextMap ||= new Map() +globalThis.__dirname = fromFileUrl(new URL('./', import.meta.url)).slice(0, -1) + +// Check if a file exists, given a relative path +const exists = async (relativePath: string) => { + const path = fromFileUrl(new URL(relativePath, import.meta.url)) + try { + await Deno.stat(path) + return true + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return false + } + throw error + } +} + +const handler = async (req: Request, context: Context) => { + // Uncomment when CLI update lands + // if (!Deno.env.get('NETLIFY_DEV')) { + // // Only run in dev + // return + // } + + let middleware + // Dynamic imports and FS operations aren't allowed when deployed, + // but that's fine because this is only ever used locally. + // We don't want to just try importing and use that to test, + // because that would also throw if there's an error in the middleware, + // which we would want to surface not ignore. + if (await exists('../../middleware.js')) { + // These will be user code + const nextMiddleware = await import('../../middleware.js') + middleware = nextMiddleware.middleware + } else { + // No middleware, so we silently return + return + } + + // This is the format expected by Next.js + const geo: NextRequestInit['geo'] = { + country: context.geo.country?.code, + region: context.geo.subdivision?.code, + city: context.geo.city, + } + + // A default request id is fine locally + const requestId = req.headers.get('x-nf-request-id') || 'request-id' + + globalThis.NFRequestContextMap.set(requestId, { + request: req, + context, + }) + + const request: NextRequestInit = { + headers: Object.fromEntries(req.headers.entries()), + geo, + method: req.method, + ip: context.ip, + body: req.body || undefined, + } + + const nextRequest: NextRequest = new NextRequest(req, request) + + try { + const response = await middleware(nextRequest) + return buildResponse({ + result: { response: response || NextResponse.next(), waitUntil: Promise.resolve() }, + request: req, + context, + }) + } catch (error) { + console.error(error) + return new Response(error.message, { status: 500 }) + } finally { + if (requestId) { + globalThis.NFRequestContextMap.delete(requestId) + } + } +} + +export default handler diff --git a/packages/runtime/src/templates/edge/runtime.ts b/packages/runtime/src/templates/edge/runtime.ts index da543acd14..7f33da2954 100644 --- a/packages/runtime/src/templates/edge/runtime.ts +++ b/packages/runtime/src/templates/edge/runtime.ts @@ -45,6 +45,10 @@ declare global { globalThis.NFRequestContextMap ||= new Map() const handler = async (req: Request, context: Context) => { + if (Deno.env.get('NETLIFY_DEV')) { + // Don't run in dev + return + } const url = new URL(req.url) const geo = { diff --git a/packages/runtime/src/templates/edge/utils.ts b/packages/runtime/src/templates/edge/utils.ts index 582eb50ae7..9897c77c03 100644 --- a/packages/runtime/src/templates/edge/utils.ts +++ b/packages/runtime/src/templates/edge/utils.ts @@ -123,17 +123,38 @@ export const buildResponse = async ({ request.headers.set('x-nf-next-middleware', 'skip') const rewrite = res.headers.get('x-middleware-rewrite') + + // Data requests (i.e. requests for /_next/data ) need special handling + const isDataReq = request.headers.get('x-nextjs-data') + if (rewrite) { const rewriteUrl = new URL(rewrite, request.url) const baseUrl = new URL(request.url) + const relativeUrl = relativizeURL(rewrite, request.url) + + // Data requests might be rewritten to an external URL + // This header tells the client router the redirect target, and if it's external then it will do a full navigation + if (isDataReq) { + res.headers.set('x-nextjs-rewrite', relativeUrl) + } 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) } - res.headers.set('x-middleware-rewrite', relativizeURL(rewrite, request.url)) + res.headers.set('x-middleware-rewrite', relativeUrl) + return addMiddlewareHeaders(context.rewrite(rewrite), res) } + + const redirect = res.headers.get('Location') + + // Data requests shouldn;t automatically redirect in the browser (they might be HTML pages): they're handled by the router + if (redirect && isDataReq) { + res.headers.delete('location') + res.headers.set('x-nextjs-redirect', relativizeURL(redirect, request.url)) + } + if (res.headers.get('x-middleware-next') === '1') { return addMiddlewareHeaders(context.next(), res) } diff --git a/test/index.js b/test/index.js index e071c23b10..b2542e2e06 100644 --- a/test/index.js +++ b/test/index.js @@ -14,7 +14,8 @@ const cpy = require('cpy') const { dir: getTmpDir } = require('tmp-promise') const { downloadFile } = require('../packages/runtime/src/templates/handlerUtils') -const nextRuntime = require('../packages/runtime/src') +const nextRuntimeFactory = require('../packages/runtime/src') +const nextRuntime = nextRuntimeFactory({}) const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../packages/runtime/src/constants') const { join } = require('pathe') @@ -33,8 +34,6 @@ const { } = require('../packages/runtime/src/helpers/config') const { dirname } = require('path') const { getProblematicUserRewrites } = require('../packages/runtime/src/helpers/verification') -const { onPostBuild } = require('../packages/runtime/lib') -const { basePath } = require('../demos/next-i18next/next.config') const chance = new Chance() const FIXTURES_DIR = `${__dirname}/fixtures`