@netlify/plugin-nextjs 4.12.2 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/helpers/files.js +10 -2
- package/lib/helpers/utils.js +1 -1
- package/lib/helpers/verification.js +14 -4
- package/lib/middleware/html-rewriter.js +4 -0
- package/lib/middleware/index.js +19 -0
- package/lib/middleware/request.js +64 -0
- package/lib/middleware/response.js +90 -0
- package/lib/templates/getHandler.js +4 -4
- package/lib/templates/handlerUtils.js +1 -1
- package/lib/templates/ipx.js +1 -1
- package/package.json +17 -5
- package/src/templates/edge/bundle.js +3 -3
- package/src/templates/edge/ipx.ts +25 -30
- package/src/templates/edge/runtime.ts +33 -5
- package/src/templates/edge/utils.ts +80 -3
package/lib/helpers/files.js
CHANGED
|
@@ -270,13 +270,21 @@ const baseServerReplacements = [
|
|
|
270
270
|
];
|
|
271
271
|
const nextServerReplacements = [
|
|
272
272
|
[
|
|
273
|
-
`getMiddlewareManifest() {\n if (
|
|
274
|
-
`getMiddlewareManifest() {\n if (
|
|
273
|
+
`getMiddlewareManifest() {\n if (this.minimalMode) return null;`,
|
|
274
|
+
`getMiddlewareManifest() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return null;`,
|
|
275
|
+
],
|
|
276
|
+
[
|
|
277
|
+
`generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode) return []`,
|
|
278
|
+
`generateCatchAllMiddlewareRoute(devReady) {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return [];`,
|
|
275
279
|
],
|
|
276
280
|
[
|
|
277
281
|
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode) return undefined;`,
|
|
278
282
|
`generateCatchAllMiddlewareRoute() {\n if (this.minimalMode || process.env.NEXT_USE_NETLIFY_EDGE) return undefined;`,
|
|
279
283
|
],
|
|
284
|
+
[
|
|
285
|
+
`getMiddlewareManifest() {\n if (this.minimalMode) {`,
|
|
286
|
+
`getMiddlewareManifest() {\n if (!this.minimalMode && !process.env.NEXT_USE_NETLIFY_EDGE) {`,
|
|
287
|
+
],
|
|
280
288
|
];
|
|
281
289
|
const patchNextFiles = async (root) => {
|
|
282
290
|
const baseServerFile = getServerFile(root);
|
package/lib/helpers/utils.js
CHANGED
|
@@ -136,7 +136,7 @@ const findModuleFromBase = ({ paths, candidates }) => {
|
|
|
136
136
|
exports.findModuleFromBase = findModuleFromBase;
|
|
137
137
|
const isNextAuthInstalled = () => {
|
|
138
138
|
try {
|
|
139
|
-
// eslint-disable-next-line import/no-unassigned-import, import/no-unresolved,
|
|
139
|
+
// eslint-disable-next-line import/no-unassigned-import, import/no-unresolved, n/no-missing-require
|
|
140
140
|
require('next-auth');
|
|
141
141
|
return true;
|
|
142
142
|
}
|
|
@@ -66,9 +66,19 @@ const checkForOldFunctions = async ({ functions }) => {
|
|
|
66
66
|
exports.checkForOldFunctions = checkForOldFunctions;
|
|
67
67
|
const checkNextSiteHasBuilt = ({ publish, failBuild, }) => {
|
|
68
68
|
if (!(0, fs_1.existsSync)(path_1.default.join(publish, 'BUILD_ID'))) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
let outWarning;
|
|
70
|
+
if (path_1.default.basename(publish) === 'out') {
|
|
71
|
+
outWarning = `Your publish directory is set to "out", but in most cases it should be ".next".`;
|
|
72
|
+
}
|
|
73
|
+
else if (path_1.default.basename(publish) !== '.next' && (0, fs_1.existsSync)(path_1.default.join('.next', 'BUILD_ID'))) {
|
|
74
|
+
outWarning = (0, outdent_1.outdent) `
|
|
75
|
+
However, a '.next' directory was found with a production build.
|
|
76
|
+
Consider changing your 'publish' directory to '.next'
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
outWarning = `In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config.`;
|
|
81
|
+
}
|
|
72
82
|
return failBuild((0, outdent_1.outdent) `
|
|
73
83
|
The directory "${publish}" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory.
|
|
74
84
|
${outWarning}
|
|
@@ -76,7 +86,7 @@ const checkNextSiteHasBuilt = ({ publish, failBuild, }) => {
|
|
|
76
86
|
`);
|
|
77
87
|
}
|
|
78
88
|
if ((0, fs_1.existsSync)(path_1.default.join(publish, 'export-detail.json'))) {
|
|
79
|
-
failBuild((0, outdent_1.outdent) `
|
|
89
|
+
return failBuild((0, outdent_1.outdent) `
|
|
80
90
|
Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
|
|
81
91
|
The publish directory should be set to "out", and you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true".
|
|
82
92
|
`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./response"), exports);
|
|
18
|
+
__exportStar(require("./request"), exports);
|
|
19
|
+
__exportStar(require("./html-rewriter"), exports);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MiddlewareRequest = void 0;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
const response_1 = require("./response");
|
|
6
|
+
/**
|
|
7
|
+
* Supercharge your Next middleware with Netlify Edge Functions
|
|
8
|
+
*/
|
|
9
|
+
class MiddlewareRequest extends Request {
|
|
10
|
+
constructor(nextRequest) {
|
|
11
|
+
super(nextRequest);
|
|
12
|
+
this.nextRequest = nextRequest;
|
|
13
|
+
if (!('Deno' in globalThis)) {
|
|
14
|
+
throw new Error('MiddlewareRequest only works in a Netlify Edge Function environment');
|
|
15
|
+
}
|
|
16
|
+
const requestId = nextRequest.headers.get('x-nf-request-id');
|
|
17
|
+
if (!requestId) {
|
|
18
|
+
throw new Error('Missing x-nf-request-id header');
|
|
19
|
+
}
|
|
20
|
+
const requestContext = globalThis.NFRequestContextMap.get(requestId);
|
|
21
|
+
if (!requestContext) {
|
|
22
|
+
throw new Error(`Could not find request context for request id ${requestId}`);
|
|
23
|
+
}
|
|
24
|
+
this.context = requestContext.context;
|
|
25
|
+
this.originalRequest = requestContext.request;
|
|
26
|
+
}
|
|
27
|
+
// Add the headers to the original request, which will be passed to the origin
|
|
28
|
+
applyHeaders() {
|
|
29
|
+
this.headers.forEach((value, name) => {
|
|
30
|
+
this.originalRequest.headers.set(name, value);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async next() {
|
|
34
|
+
this.applyHeaders();
|
|
35
|
+
const response = await this.context.next();
|
|
36
|
+
return new response_1.MiddlewareResponse(response);
|
|
37
|
+
}
|
|
38
|
+
rewrite(destination, init) {
|
|
39
|
+
if (typeof destination === 'string' && destination.startsWith('/')) {
|
|
40
|
+
destination = new URL(destination, this.url);
|
|
41
|
+
}
|
|
42
|
+
this.applyHeaders();
|
|
43
|
+
return server_1.NextResponse.rewrite(destination, init);
|
|
44
|
+
}
|
|
45
|
+
get headers() {
|
|
46
|
+
return this.nextRequest.headers;
|
|
47
|
+
}
|
|
48
|
+
get cookies() {
|
|
49
|
+
return this.nextRequest.cookies;
|
|
50
|
+
}
|
|
51
|
+
get geo() {
|
|
52
|
+
return this.nextRequest.geo;
|
|
53
|
+
}
|
|
54
|
+
get ip() {
|
|
55
|
+
return this.nextRequest.ip;
|
|
56
|
+
}
|
|
57
|
+
get nextUrl() {
|
|
58
|
+
return this.nextRequest.url;
|
|
59
|
+
}
|
|
60
|
+
get url() {
|
|
61
|
+
return this.nextRequest.url.toString();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.MiddlewareRequest = MiddlewareRequest;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MiddlewareResponse = void 0;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
// A NextResponse that wraps the Netlify origin response
|
|
6
|
+
// We can't pass it through directly, because Next disallows returning a response body
|
|
7
|
+
class MiddlewareResponse extends server_1.NextResponse {
|
|
8
|
+
constructor(originResponse) {
|
|
9
|
+
super();
|
|
10
|
+
this.originResponse = originResponse;
|
|
11
|
+
// These are private in Node when compiling, but we access them in Deno at runtime
|
|
12
|
+
Object.defineProperty(this, 'dataTransforms', {
|
|
13
|
+
value: [],
|
|
14
|
+
enumerable: false,
|
|
15
|
+
writable: false,
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, 'elementHandlers', {
|
|
18
|
+
value: [],
|
|
19
|
+
enumerable: false,
|
|
20
|
+
writable: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Transform the page props before they are passed to the client.
|
|
25
|
+
* This works for both HTML pages and JSON data
|
|
26
|
+
*/
|
|
27
|
+
transformData(transform) {
|
|
28
|
+
// The transforms are evaluated after the middleware is returned
|
|
29
|
+
this.dataTransforms.push(transform);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rewrite the response HTML with the given selector and handlers
|
|
33
|
+
*/
|
|
34
|
+
rewriteHTML(selector, handlers) {
|
|
35
|
+
this.elementHandlers.push([selector, handlers]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sets the value of a page prop.
|
|
39
|
+
* @see transformData if you need more control
|
|
40
|
+
*/
|
|
41
|
+
setPageProp(key, value) {
|
|
42
|
+
this.transformData((props) => {
|
|
43
|
+
props.pageProps || (props.pageProps = {});
|
|
44
|
+
props.pageProps[key] = value;
|
|
45
|
+
return props;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Replace the text of the given element. Takes either a string or a function
|
|
50
|
+
* that is passed the original string and returns new new string.
|
|
51
|
+
* @see rewriteHTML for more control
|
|
52
|
+
*/
|
|
53
|
+
replaceText(selector, valueOrReplacer) {
|
|
54
|
+
// If it's a string then our job is simpler, because we don't need to collect the current text
|
|
55
|
+
if (typeof valueOrReplacer === 'string') {
|
|
56
|
+
this.rewriteHTML(selector, {
|
|
57
|
+
text(textChunk) {
|
|
58
|
+
if (textChunk.lastInTextNode) {
|
|
59
|
+
textChunk.replace(valueOrReplacer);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
textChunk.remove();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
let text = '';
|
|
69
|
+
this.rewriteHTML(selector, {
|
|
70
|
+
text(textChunk) {
|
|
71
|
+
text += textChunk.text;
|
|
72
|
+
// We're finished, so we can replace the text
|
|
73
|
+
if (textChunk.lastInTextNode) {
|
|
74
|
+
textChunk.replace(valueOrReplacer(text));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Remove the chunk, because we'll be adding it back later
|
|
78
|
+
textChunk.remove();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get headers() {
|
|
85
|
+
var _a;
|
|
86
|
+
// If we have the origin response, we should use its headers
|
|
87
|
+
return ((_a = this.originResponse) === null || _a === void 0 ? void 0 : _a.headers) || super.headers;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.MiddlewareResponse = MiddlewareResponse;
|
|
@@ -6,7 +6,7 @@ const outdent_1 = require("outdent");
|
|
|
6
6
|
const { promises } = require('fs');
|
|
7
7
|
const { Server } = require('http');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
// eslint-disable-next-line
|
|
9
|
+
// eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params
|
|
10
10
|
const { URLSearchParams, URL } = require('url');
|
|
11
11
|
const { Bridge } = require('@vercel/node-bridge/bridge');
|
|
12
12
|
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils');
|
|
@@ -22,7 +22,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
|
|
|
22
22
|
}
|
|
23
23
|
// This is just so nft knows about the page entrypoints. It's not actually used
|
|
24
24
|
try {
|
|
25
|
-
// eslint-disable-next-line
|
|
25
|
+
// eslint-disable-next-line n/no-missing-require
|
|
26
26
|
require.resolve('./pages.js');
|
|
27
27
|
}
|
|
28
28
|
catch { }
|
|
@@ -102,7 +102,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
|
|
|
102
102
|
multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
|
|
103
103
|
}
|
|
104
104
|
multiValueHeaders['x-render-mode'] = [requestMode];
|
|
105
|
-
console.log(`[${event.httpMethod}] ${event.path} (${requestMode === null || requestMode === void 0 ? void 0 : requestMode.toUpperCase()})`);
|
|
105
|
+
console.log(`[${event.httpMethod}] ${event.path} (${requestMode === null || requestMode === void 0 ? void 0 : requestMode.toUpperCase()}${result.ttl > 0 ? ` ${result.ttl}s` : ''})`);
|
|
106
106
|
return {
|
|
107
107
|
...result,
|
|
108
108
|
multiValueHeaders,
|
|
@@ -121,7 +121,7 @@ const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '..
|
|
|
121
121
|
|
|
122
122
|
const { builder } = require("@netlify/functions");
|
|
123
123
|
const { config } = require("${publishDir}/required-server-files.json")
|
|
124
|
-
let staticManifest
|
|
124
|
+
let staticManifest
|
|
125
125
|
try {
|
|
126
126
|
staticManifest = require("${publishDir}/static-manifest.json")
|
|
127
127
|
} catch {}
|
|
@@ -160,7 +160,7 @@ const getNextServer = () => {
|
|
|
160
160
|
if (!NextServer) {
|
|
161
161
|
try {
|
|
162
162
|
// next < 11.0.1
|
|
163
|
-
// eslint-disable-next-line
|
|
163
|
+
// eslint-disable-next-line n/no-missing-require, import/no-unresolved, @typescript-eslint/no-var-requires
|
|
164
164
|
NextServer = require('next/dist/next-server/server/next-server').default;
|
|
165
165
|
}
|
|
166
166
|
catch (error) {
|
package/lib/templates/ipx.js
CHANGED
|
@@ -9,4 +9,4 @@ exports.handler = (0, ipx_1.createIPXHandler)({
|
|
|
9
9
|
domains: imageconfig_json_1.domains,
|
|
10
10
|
remotePatterns: imageconfig_json_1.remotePatterns,
|
|
11
11
|
});
|
|
12
|
-
/* eslint-enable
|
|
12
|
+
/* eslint-enable n/no-missing-import, import/no-unresolved, @typescript-eslint/ban-ts-comment */
|
package/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/plugin-nextjs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.0",
|
|
4
4
|
"description": "Run Next.js seamlessly on Netlify",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"lib/**/*",
|
|
8
8
|
"src/templates/edge/*",
|
|
9
|
-
"manifest.yml"
|
|
9
|
+
"manifest.yml",
|
|
10
|
+
"middleware.js"
|
|
10
11
|
],
|
|
12
|
+
"typesVersions": {
|
|
13
|
+
"*": {
|
|
14
|
+
"middleware": [
|
|
15
|
+
"dist-types/middleware"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./lib/index.js",
|
|
21
|
+
"./middleware": "./lib/middleware/index.js"
|
|
22
|
+
},
|
|
11
23
|
"dependencies": {
|
|
12
24
|
"@netlify/functions": "^1.0.0",
|
|
13
25
|
"@netlify/ipx": "^1.1.3",
|
|
@@ -28,7 +40,7 @@
|
|
|
28
40
|
},
|
|
29
41
|
"devDependencies": {
|
|
30
42
|
"@delucis/if-env": "^1.1.2",
|
|
31
|
-
"@netlify/build": "^27.
|
|
43
|
+
"@netlify/build": "^27.7.0",
|
|
32
44
|
"@types/fs-extra": "^9.0.13",
|
|
33
45
|
"@types/jest": "^27.4.1",
|
|
34
46
|
"@types/node": "^17.0.25",
|
|
@@ -41,7 +53,7 @@
|
|
|
41
53
|
"publish:pull": "git pull",
|
|
42
54
|
"publish:install": "npm ci",
|
|
43
55
|
"publish:test": "cd .. && npm ci && npm test",
|
|
44
|
-
"clean": "rimraf lib",
|
|
56
|
+
"clean": "rimraf lib dist-types",
|
|
45
57
|
"build": "tsc",
|
|
46
58
|
"watch": "tsc --watch",
|
|
47
59
|
"prepare": "npm run build"
|
|
@@ -62,4 +74,4 @@
|
|
|
62
74
|
"engines": {
|
|
63
75
|
"node": ">=12.0.0"
|
|
64
76
|
}
|
|
65
|
-
}
|
|
77
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This placeholder is replaced with the compiled Next.js bundle at build time
|
|
3
|
-
* @
|
|
4
|
-
* @
|
|
3
|
+
* @param {Object} props
|
|
4
|
+
* @param {import("./runtime.ts").RequestData} props.request
|
|
5
5
|
* @returns {Promise<import("./utils.ts").FetchEventResult>}
|
|
6
6
|
*/
|
|
7
|
-
export default async (
|
|
7
|
+
export default async ({ request }) => {}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { Accepts } from
|
|
2
|
-
import type { Context } from
|
|
1
|
+
import { Accepts } from 'https://deno.land/x/accepts@2.1.1/mod.ts'
|
|
2
|
+
import type { Context } from 'netlify:edge'
|
|
3
3
|
// Available at build time
|
|
4
|
-
import imageconfig from
|
|
5
|
-
type: "json",
|
|
6
|
-
};
|
|
4
|
+
import imageconfig from './imageconfig.json' assert { type: 'json' }
|
|
7
5
|
|
|
8
|
-
const defaultFormat =
|
|
6
|
+
const defaultFormat = 'webp'
|
|
9
7
|
|
|
10
8
|
interface ImageConfig extends Record<string, unknown> {
|
|
11
|
-
formats?: string[]
|
|
9
|
+
formats?: string[]
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -17,41 +15,38 @@ interface ImageConfig extends Record<string, unknown> {
|
|
|
17
15
|
|
|
18
16
|
// deno-lint-ignore require-await
|
|
19
17
|
const handler = async (req: Request, context: Context) => {
|
|
20
|
-
const { searchParams } = new URL(req.url)
|
|
21
|
-
const accept = new Accepts(req.headers)
|
|
22
|
-
const { formats = [defaultFormat] } = imageconfig as ImageConfig
|
|
18
|
+
const { searchParams } = new URL(req.url)
|
|
19
|
+
const accept = new Accepts(req.headers)
|
|
20
|
+
const { formats = [defaultFormat] } = imageconfig as ImageConfig
|
|
23
21
|
if (formats.length === 0) {
|
|
24
|
-
formats.push(defaultFormat)
|
|
22
|
+
formats.push(defaultFormat)
|
|
25
23
|
}
|
|
26
|
-
let type = accept.types(formats) || defaultFormat
|
|
27
|
-
if(Array.isArray(type)) {
|
|
28
|
-
type = type[0]
|
|
24
|
+
let type = accept.types(formats) || defaultFormat
|
|
25
|
+
if (Array.isArray(type)) {
|
|
26
|
+
type = type[0]
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const quality = searchParams.get("q") ?? 75;
|
|
29
|
+
const source = searchParams.get('url')
|
|
30
|
+
const width = searchParams.get('w')
|
|
31
|
+
const quality = searchParams.get('q') ?? 75
|
|
35
32
|
|
|
36
33
|
if (!source || !width) {
|
|
37
|
-
return new Response(
|
|
34
|
+
return new Response('Invalid request', {
|
|
38
35
|
status: 400,
|
|
39
|
-
})
|
|
36
|
+
})
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
const modifiers = [`w_${width}`, `q_${quality}`]
|
|
39
|
+
const modifiers = [`w_${width}`, `q_${quality}`]
|
|
43
40
|
|
|
44
41
|
if (type) {
|
|
45
|
-
if(type.includes('/')) {
|
|
42
|
+
if (type.includes('/')) {
|
|
46
43
|
// If this is a mimetype, strip "image/"
|
|
47
|
-
type = type.split('/')[1]
|
|
44
|
+
type = type.split('/')[1]
|
|
48
45
|
}
|
|
49
|
-
modifiers.push(`f_${type}`)
|
|
46
|
+
modifiers.push(`f_${type}`)
|
|
50
47
|
}
|
|
51
|
-
const target = `/_ipx/${modifiers.join(
|
|
52
|
-
return context.rewrite(
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
};
|
|
48
|
+
const target = `/_ipx/${modifiers.join(',')}/${encodeURIComponent(source)}`
|
|
49
|
+
return context.rewrite(target)
|
|
50
|
+
}
|
|
56
51
|
|
|
57
|
-
export default handler
|
|
52
|
+
export default handler
|
|
@@ -32,19 +32,43 @@ export interface RequestData {
|
|
|
32
32
|
body?: ReadableStream<Uint8Array>
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface RequestContext {
|
|
36
|
+
request: Request
|
|
37
|
+
context: Context
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare global {
|
|
41
|
+
// deno-lint-ignore no-var
|
|
42
|
+
var NFRequestContextMap: Map<string, RequestContext>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
globalThis.NFRequestContextMap ||= new Map()
|
|
46
|
+
|
|
35
47
|
const handler = async (req: Request, context: Context) => {
|
|
36
48
|
const url = new URL(req.url)
|
|
37
49
|
if (url.pathname.startsWith('/_next/static/')) {
|
|
38
50
|
return
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
const geo = {
|
|
54
|
+
country: context.geo.country?.code,
|
|
55
|
+
region: context.geo.subdivision?.code,
|
|
56
|
+
city: context.geo.city,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestId = req.headers.get('x-nf-request-id')
|
|
60
|
+
if (!requestId) {
|
|
61
|
+
console.error('Missing x-nf-request-id header')
|
|
62
|
+
} else {
|
|
63
|
+
globalThis.NFRequestContextMap.set(requestId, {
|
|
64
|
+
request: req,
|
|
65
|
+
context,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
const request: RequestData = {
|
|
42
70
|
headers: Object.fromEntries(req.headers.entries()),
|
|
43
|
-
geo
|
|
44
|
-
country: context.geo.country?.code,
|
|
45
|
-
region: context.geo.subdivision?.code,
|
|
46
|
-
city: context.geo.city,
|
|
47
|
-
},
|
|
71
|
+
geo,
|
|
48
72
|
url: url.toString(),
|
|
49
73
|
method: req.method,
|
|
50
74
|
ip: context.ip,
|
|
@@ -57,6 +81,10 @@ const handler = async (req: Request, context: Context) => {
|
|
|
57
81
|
} catch (error) {
|
|
58
82
|
console.error(error)
|
|
59
83
|
return new Response(error.message, { status: 500 })
|
|
84
|
+
} finally {
|
|
85
|
+
if (requestId) {
|
|
86
|
+
globalThis.NFRequestContextMap.delete(requestId)
|
|
87
|
+
}
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { Context } from 'netlify:edge'
|
|
2
|
+
import { ElementHandlers, HTMLRewriter } from 'https://deno.land/x/html_rewriter@v0.1.0-pre.17/index.ts'
|
|
2
3
|
|
|
3
4
|
export interface FetchEventResult {
|
|
4
5
|
response: Response
|
|
5
6
|
waitUntil: Promise<any>
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
type NextDataTransform = <T>(data: T) => T
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* This is how Next handles rewritten URLs.
|
|
10
13
|
*/
|
|
11
|
-
|
|
14
|
+
export function relativizeURL(url: string | string, base: string | URL) {
|
|
12
15
|
const baseURL = typeof base === 'string' ? new URL(base) : base
|
|
13
16
|
const relative = new URL(url, base)
|
|
14
17
|
const origin = `${baseURL.protocol}//${baseURL.host}`
|
|
@@ -17,7 +20,6 @@ export interface FetchEventResult {
|
|
|
17
20
|
: relative.toString()
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
21
23
|
export const addMiddlewareHeaders = async (
|
|
22
24
|
originResponse: Promise<Response> | Response,
|
|
23
25
|
middlewareResponse: Response,
|
|
@@ -34,6 +36,29 @@ export const addMiddlewareHeaders = async (
|
|
|
34
36
|
})
|
|
35
37
|
return response
|
|
36
38
|
}
|
|
39
|
+
|
|
40
|
+
interface MiddlewareResponse extends Response {
|
|
41
|
+
originResponse: Response
|
|
42
|
+
dataTransforms: NextDataTransform[]
|
|
43
|
+
elementHandlers: Array<[selector: string, handlers: ElementHandlers]>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface MiddlewareRequest {
|
|
47
|
+
request: Request
|
|
48
|
+
context: Context
|
|
49
|
+
originalRequest: Request
|
|
50
|
+
next(): Promise<MiddlewareResponse>
|
|
51
|
+
rewrite(destination: string | URL, init?: ResponseInit): Response
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isMiddlewareRequest(response: Response | MiddlewareRequest): response is MiddlewareRequest {
|
|
55
|
+
return 'originalRequest' in response
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isMiddlewareResponse(response: Response | MiddlewareResponse): response is MiddlewareResponse {
|
|
59
|
+
return 'dataTransforms' in response
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
export const buildResponse = async ({
|
|
38
63
|
result,
|
|
39
64
|
request,
|
|
@@ -43,13 +68,65 @@ export const buildResponse = async ({
|
|
|
43
68
|
request: Request
|
|
44
69
|
context: Context
|
|
45
70
|
}) => {
|
|
71
|
+
// They've returned the MiddlewareRequest directly, so we'll call `next()` for them.
|
|
72
|
+
if (isMiddlewareRequest(result.response)) {
|
|
73
|
+
result.response = await result.response.next()
|
|
74
|
+
}
|
|
75
|
+
if (isMiddlewareResponse(result.response)) {
|
|
76
|
+
const { response } = result
|
|
77
|
+
if (request.method === 'HEAD' || request.method === 'OPTIONS') {
|
|
78
|
+
return response.originResponse
|
|
79
|
+
}
|
|
80
|
+
// If it's JSON we don't need to use the rewriter, we can just parse it
|
|
81
|
+
if (response.originResponse.headers.get('content-type')?.includes('application/json')) {
|
|
82
|
+
const props = await response.originResponse.json()
|
|
83
|
+
const transformed = response.dataTransforms.reduce((prev, transform) => {
|
|
84
|
+
return transform(prev)
|
|
85
|
+
}, props)
|
|
86
|
+
return context.json(transformed)
|
|
87
|
+
}
|
|
88
|
+
// This var will hold the contents of the script tag
|
|
89
|
+
let buffer = ''
|
|
90
|
+
// Create an HTMLRewriter that matches the Next data script tag
|
|
91
|
+
const rewriter = new HTMLRewriter()
|
|
92
|
+
|
|
93
|
+
if (response.dataTransforms.length > 0) {
|
|
94
|
+
rewriter.on('script[id="__NEXT_DATA__"]', {
|
|
95
|
+
text(textChunk) {
|
|
96
|
+
// Grab all the chunks in the Next data script tag
|
|
97
|
+
buffer += textChunk.text
|
|
98
|
+
if (textChunk.lastInTextNode) {
|
|
99
|
+
try {
|
|
100
|
+
// When we have all the data, try to parse it as JSON
|
|
101
|
+
const data = JSON.parse(buffer.trim())
|
|
102
|
+
// Apply all of the transforms to the props
|
|
103
|
+
const props = response.dataTransforms.reduce((prev, transform) => transform(prev), data.props)
|
|
104
|
+
// Replace the data with the transformed props
|
|
105
|
+
textChunk.replace(JSON.stringify({ ...data, props }))
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.log('Could not parse', err)
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Remove the chunk after we've appended it to the buffer
|
|
111
|
+
textChunk.remove()
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (response.elementHandlers.length > 0) {
|
|
118
|
+
response.elementHandlers.forEach(([selector, handlers]) => rewriter.on(selector, handlers))
|
|
119
|
+
}
|
|
120
|
+
return rewriter.transform(response.originResponse)
|
|
121
|
+
}
|
|
46
122
|
const res = new Response(result.response.body, result.response)
|
|
47
123
|
request.headers.set('x-nf-next-middleware', 'skip')
|
|
124
|
+
|
|
48
125
|
const rewrite = res.headers.get('x-middleware-rewrite')
|
|
49
126
|
if (rewrite) {
|
|
50
127
|
const rewriteUrl = new URL(rewrite, request.url)
|
|
51
128
|
const baseUrl = new URL(request.url)
|
|
52
|
-
if(rewriteUrl.hostname !== baseUrl.hostname) {
|
|
129
|
+
if (rewriteUrl.hostname !== baseUrl.hostname) {
|
|
53
130
|
// Netlify Edge Functions don't support proxying to external domains, but Next middleware does
|
|
54
131
|
const proxied = fetch(new Request(rewriteUrl.toString(), request))
|
|
55
132
|
return addMiddlewareHeaders(proxied, res)
|