@netlify/plugin-nextjs 4.30.4-ipx-layer.0 → 4.31.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.
@@ -14,7 +14,7 @@ const config_1 = require("./config");
14
14
  const matchers_1 = require("./matchers");
15
15
  const maybeLoadJson = (path) => {
16
16
  if ((0, fs_1.existsSync)(path)) {
17
- return (0, fs_extra_1.readJson)(path);
17
+ return (0, fs_extra_1.readJSON)(path);
18
18
  }
19
19
  };
20
20
  const isAppDirRoute = (route, appPathRoutesManifest) => Boolean(appPathRoutesManifest) && Object.values(appPathRoutesManifest).includes(route);
@@ -29,53 +29,6 @@ exports.loadPrerenderManifest = loadPrerenderManifest;
29
29
  * Convert the Next middleware name into a valid Edge Function name
30
30
  */
31
31
  const sanitizeName = (name) => `next_${name.replace(/\W/g, '_')}`;
32
- /**
33
- * Initialization added to the top of the edge function bundle
34
- */
35
- const preamble = /* js */ `
36
- import {
37
- decode as _base64Decode,
38
- } from "https://deno.land/std@0.159.0/encoding/base64.ts";
39
- // Deno defines "window", but naughty libraries think this means it's a browser
40
- delete globalThis.window
41
- globalThis.process = { env: {...Deno.env.toObject(), NEXT_RUNTIME: 'edge', 'NEXT_PRIVATE_MINIMAL_MODE': '1' } }
42
- globalThis.EdgeRuntime = "netlify-edge"
43
- let _ENTRIES = {}
44
-
45
- // Next.js uses this extension to the Headers API implemented by Cloudflare workerd
46
- if(!('getAll' in Headers.prototype)) {
47
- Headers.prototype.getAll = function getAll(name) {
48
- name = name.toLowerCase();
49
- if (name !== "set-cookie") {
50
- throw new Error("Headers.getAll is only supported for Set-Cookie");
51
- }
52
- return [...this.entries()]
53
- .filter(([key]) => key === name)
54
- .map(([, value]) => value);
55
- };
56
- }
57
- // Next uses blob: urls to refer to local assets, so we need to intercept these
58
- const _fetch = globalThis.fetch
59
- const fetch = async (url, init) => {
60
- try {
61
- if (typeof url === 'object' && url.href?.startsWith('blob:')) {
62
- const key = url.href.slice(5)
63
- if (key in _ASSETS) {
64
- return new Response(_base64Decode(_ASSETS[key]))
65
- }
66
- }
67
- return await _fetch(url, init)
68
- } catch (error) {
69
- console.error(error)
70
- throw error
71
- }
72
- }
73
-
74
- // Next edge runtime uses "self" as a function-scoped global-like object, but some of the older polyfills expect it to equal globalThis
75
- // See https://nextjs.org/docs/basic-features/supported-browsers-features#polyfills
76
- const self = { ...globalThis, fetch }
77
-
78
- `;
79
32
  // Slightly different spacing in different versions!
80
33
  const IMPORT_UNSUPPORTED = [
81
34
  `Object.defineProperty(globalThis,"__import_unsupported"`,
@@ -86,7 +39,8 @@ const IMPORT_UNSUPPORTED = [
86
39
  */
87
40
  const getMiddlewareBundle = async ({ edgeFunctionDefinition, netlifyConfig, }) => {
88
41
  const { publish } = netlifyConfig.build;
89
- const chunks = [preamble];
42
+ const shims = await fs_1.promises.readFile(getEdgeTemplatePath('shims.js'), 'utf8');
43
+ const chunks = [shims];
90
44
  chunks.push(`export const _DEFINITION = ${JSON.stringify(edgeFunctionDefinition)}`);
91
45
  if ('wasm' in edgeFunctionDefinition) {
92
46
  for (const { name, filePath } of edgeFunctionDefinition.wasm) {
@@ -234,7 +188,6 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
234
188
  var _a;
235
189
  const manifest = {
236
190
  functions: [],
237
- layers: [],
238
191
  version: 1,
239
192
  };
240
193
  const edgeFunctionRoot = (0, path_1.resolve)('.netlify', 'edge-functions');
@@ -246,105 +199,108 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
246
199
  await (0, fs_extra_1.copy)(getEdgeTemplatePath('../edge-shared'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared'));
247
200
  await (0, fs_extra_1.writeJSON)((0, path_1.join)(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig);
248
201
  await (0, fs_extra_1.copy)((0, path_1.join)(publish, 'prerender-manifest.json'), (0, path_1.join)(edgeFunctionRoot, 'edge-shared', 'prerender-manifest.json'));
249
- if (!(0, destr_1.default)(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
250
- !(0, destr_1.default)(process.env.NEXT_DISABLE_NETLIFY_EDGE) &&
251
- !(0, destr_1.default)(process.env.DISABLE_IPX)) {
252
- console.log('Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.');
253
- const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, 'ipx');
254
- await (0, fs_extra_1.ensureDir)(edgeFunctionDir);
255
- await copyEdgeSourceFile({ edgeFunctionDir, file: 'ipx.ts', target: 'index.ts' });
256
- await (0, fs_extra_1.copyFile)((0, path_1.join)('.netlify', 'functions-internal', '_ipx', 'imageconfig.json'), (0, path_1.join)(edgeFunctionDir, 'imageconfig.json'));
257
- manifest.functions.push({
258
- function: 'ipx',
259
- name: 'next/image handler',
260
- path: nextConfig.images.path || '/_next/image',
202
+ // early return if edge is disabled
203
+ if ((0, destr_1.default)(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
204
+ console.log('Environment variable NEXT_DISABLE_NETLIFY_EDGE has been set, skipping Netlify Edge Function creation.');
205
+ return;
206
+ }
207
+ const rscFunctions = await (0, exports.writeRscDataEdgeFunction)({
208
+ prerenderManifest: await (0, exports.loadPrerenderManifest)(netlifyConfig),
209
+ appPathRoutesManifest: await (0, exports.loadAppPathRoutesManifest)(netlifyConfig),
210
+ });
211
+ manifest.functions.push(...rscFunctions);
212
+ const middlewareManifest = await (0, exports.loadMiddlewareManifest)(netlifyConfig);
213
+ if (!middlewareManifest) {
214
+ console.error("Couldn't find the middleware manifest");
215
+ return;
216
+ }
217
+ let usesEdge = false;
218
+ for (const middleware of middlewareManifest.sortedMiddleware) {
219
+ usesEdge = true;
220
+ const edgeFunctionDefinition = middlewareManifest.middleware[middleware];
221
+ const functionName = sanitizeName(edgeFunctionDefinition.name);
222
+ const matchers = generateEdgeFunctionMiddlewareMatchers({
223
+ edgeFunctionDefinition,
224
+ edgeFunctionRoot,
225
+ nextConfig,
261
226
  });
262
- manifest.layers.push({
263
- name: 'https://ipx-edge-function-layer.netlify.app/mod.ts',
264
- flag: 'ipx-edge-function-layer-url',
227
+ await writeEdgeFunction({
228
+ edgeFunctionDefinition,
229
+ edgeFunctionRoot,
230
+ netlifyConfig,
231
+ functionName,
232
+ matchers,
233
+ middleware: true,
265
234
  });
235
+ manifest.functions.push(...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)));
266
236
  }
267
- if (!(0, destr_1.default)(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
268
- const rscFunctions = await (0, exports.writeRscDataEdgeFunction)({
269
- prerenderManifest: await (0, exports.loadPrerenderManifest)(netlifyConfig),
270
- appPathRoutesManifest: await (0, exports.loadAppPathRoutesManifest)(netlifyConfig),
271
- });
272
- manifest.functions.push(...rscFunctions);
273
- const middlewareManifest = await (0, exports.loadMiddlewareManifest)(netlifyConfig);
274
- if (!middlewareManifest) {
275
- console.error("Couldn't find the middleware manifest");
276
- return;
277
- }
278
- let usesEdge = false;
279
- for (const middleware of middlewareManifest.sortedMiddleware) {
237
+ // Functions (i.e. not middleware, but edge SSR and API routes)
238
+ if (typeof middlewareManifest.functions === 'object') {
239
+ // When using the app dir, we also need to check if the EF matches a page
240
+ const appPathRoutesManifest = await (0, exports.loadAppPathRoutesManifest)(netlifyConfig);
241
+ // A map of all route pages to their page regex. This is used for pages dir and appDir.
242
+ const pageRegexMap = new Map([...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
243
+ route.page,
244
+ route.regex,
245
+ ]));
246
+ // Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
247
+ const dataRoutesMap = new Map([...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]));
248
+ for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
280
249
  usesEdge = true;
281
- const edgeFunctionDefinition = middlewareManifest.middleware[middleware];
282
250
  const functionName = sanitizeName(edgeFunctionDefinition.name);
283
- const matchers = generateEdgeFunctionMiddlewareMatchers({
284
- edgeFunctionDefinition,
285
- edgeFunctionRoot,
286
- nextConfig,
287
- });
288
251
  await writeEdgeFunction({
289
252
  edgeFunctionDefinition,
290
253
  edgeFunctionRoot,
291
254
  netlifyConfig,
292
255
  functionName,
293
- matchers,
294
- middleware: true,
295
256
  });
296
- manifest.functions.push(...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)));
297
- }
298
- // Functions (i.e. not middleware, but edge SSR and API routes)
299
- if (typeof middlewareManifest.functions === 'object') {
300
- // When using the app dir, we also need to check if the EF matches a page
301
- const appPathRoutesManifest = await (0, exports.loadAppPathRoutesManifest)(netlifyConfig);
302
- // A map of all route pages to their page regex. This is used for pages dir and appDir.
303
- const pageRegexMap = new Map([...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
304
- route.page,
305
- route.regex,
306
- ]));
307
- // Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
308
- const dataRoutesMap = new Map([...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]));
309
- for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
310
- usesEdge = true;
311
- const functionName = sanitizeName(edgeFunctionDefinition.name);
312
- await writeEdgeFunction({
313
- edgeFunctionDefinition,
314
- edgeFunctionRoot,
315
- netlifyConfig,
316
- functionName,
317
- });
318
- const pattern = (0, exports.getEdgeFunctionPatternForPage)({
319
- edgeFunctionDefinition,
320
- pageRegexMap,
321
- appPathRoutesManifest,
322
- });
257
+ const pattern = (0, exports.getEdgeFunctionPatternForPage)({
258
+ edgeFunctionDefinition,
259
+ pageRegexMap,
260
+ appPathRoutesManifest,
261
+ });
262
+ manifest.functions.push({
263
+ function: functionName,
264
+ name: edgeFunctionDefinition.name,
265
+ pattern,
266
+ // cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
267
+ cache: usesAppDir ? 'manual' : undefined,
268
+ });
269
+ // pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
270
+ const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page);
271
+ if (dataRoute) {
323
272
  manifest.functions.push({
324
273
  function: functionName,
325
274
  name: edgeFunctionDefinition.name,
326
- pattern,
327
- // cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
275
+ pattern: dataRoute,
328
276
  cache: usesAppDir ? 'manual' : undefined,
329
277
  });
330
- // pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
331
- const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page);
332
- if (dataRoute) {
333
- manifest.functions.push({
334
- function: functionName,
335
- name: edgeFunctionDefinition.name,
336
- pattern: dataRoute,
337
- cache: usesAppDir ? 'manual' : undefined,
338
- });
339
- }
340
278
  }
341
279
  }
342
- if (usesEdge) {
343
- console.log((0, outdent_1.outdent) `
344
- ✨ Deploying middleware and functions to ${(0, chalk_1.greenBright) `Netlify Edge Functions`} ✨
345
- This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
346
- `);
347
- }
280
+ }
281
+ if ((0, destr_1.default)(process.env.NEXT_FORCE_EDGE_IMAGES) &&
282
+ !(0, destr_1.default)(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
283
+ !(0, destr_1.default)(process.env.DISABLE_IPX)) {
284
+ usesEdge = true;
285
+ console.log('Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.');
286
+ const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, 'ipx');
287
+ await (0, fs_extra_1.ensureDir)(edgeFunctionDir);
288
+ await copyEdgeSourceFile({ edgeFunctionDir, file: 'ipx.ts', target: 'index.ts' });
289
+ await (0, fs_extra_1.copyFile)((0, path_1.join)('.netlify', 'functions-internal', '_ipx', 'imageconfig.json'), (0, path_1.join)(edgeFunctionDir, 'imageconfig.json'));
290
+ manifest.functions.push({
291
+ function: 'ipx',
292
+ name: 'next/image handler',
293
+ path: '/_next/image*',
294
+ });
295
+ }
296
+ else {
297
+ console.log('You are not using Netlify Edge Functions for image format detection. Set env var "NEXT_FORCE_EDGE_IMAGES=true" to enable.');
298
+ }
299
+ if (usesEdge) {
300
+ console.log((0, outdent_1.outdent) `
301
+ ✨ Deploying middleware and functions to ${(0, chalk_1.greenBright) `Netlify Edge Functions`} ✨
302
+ This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
303
+ `);
348
304
  }
349
305
  await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionRoot, 'manifest.json'), manifest);
350
306
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.30.4-ipx-layer.0",
3
+ "version": "4.31.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -36,12 +36,12 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@delucis/if-env": "^1.1.2",
39
- "@netlify/build": "^29.5.2",
39
+ "@netlify/build": "^29.6.4",
40
40
  "@types/fs-extra": "^9.0.13",
41
41
  "@types/jest": "^27.4.1",
42
42
  "@types/merge-stream": "^1.1.2",
43
43
  "@types/node": "^17.0.25",
44
- "next": "^13.0.7",
44
+ "next": "^13.1.6",
45
45
  "npm-run-all": "^4.1.5",
46
46
  "typescript": "^4.6.3"
47
47
  },
@@ -1,5 +1,73 @@
1
- import { getHandler } from 'https://ipx-edge-function-layer.netlify.app/mod.ts'
2
-
1
+ import { Accepts } from 'https://deno.land/x/accepts@2.1.1/mod.ts'
2
+ import type { Context } from 'https://edge.netlify.com'
3
+ // Available at build time
3
4
  import imageconfig from './imageconfig.json' assert { type: 'json' }
4
5
 
5
- export default getHandler({ formats: imageconfig?.formats })
6
+ const defaultFormat = 'webp'
7
+
8
+ interface ImageConfig extends Record<string, unknown> {
9
+ formats?: string[]
10
+ }
11
+
12
+ // Checks if a URL param is numeric
13
+ const isNumeric = (value: string | null) => Number(value).toString() === value
14
+
15
+ /**
16
+ * Implement content negotiation for images
17
+ */
18
+
19
+ // deno-lint-ignore require-await
20
+ const handler = async (req: Request, context: Context) => {
21
+ const { searchParams } = new URL(req.url)
22
+ const accept = new Accepts(req.headers)
23
+ const { formats = [defaultFormat] } = imageconfig as ImageConfig
24
+ if (formats.length === 0) {
25
+ formats.push(defaultFormat)
26
+ }
27
+ let type = accept.types(formats) || defaultFormat
28
+ if (Array.isArray(type)) {
29
+ type = type[0]
30
+ }
31
+
32
+ const source = searchParams.get('url')
33
+ const width = searchParams.get('w')
34
+ const quality = searchParams.get('q') ?? '75'
35
+
36
+ const errors: Array<string> = []
37
+
38
+ if (!source) {
39
+ errors.push('Missing "url" parameter')
40
+ } else if (!source.startsWith('http') && !source.startsWith('/')) {
41
+ errors.push('The "url" parameter must be a valid URL or path')
42
+ }
43
+
44
+ if (!width) {
45
+ errors.push('Missing "w" parameter')
46
+ } else if (!isNumeric(width)) {
47
+ errors.push('Invalid "w" parameter')
48
+ }
49
+
50
+ if (!isNumeric(quality)) {
51
+ errors.push('Invalid "q" parameter')
52
+ }
53
+
54
+ if (!source || errors.length > 0) {
55
+ return new Response(`Invalid request: \n${errors.join('\n')}`, {
56
+ status: 400,
57
+ })
58
+ }
59
+
60
+ const modifiers = [`w_${width}`, `q_${quality}`]
61
+
62
+ if (type) {
63
+ if (type.includes('/')) {
64
+ // If this is a mimetype, strip "image/"
65
+ type = type.split('/')[1]
66
+ }
67
+ modifiers.push(`f_${type}`)
68
+ }
69
+ const target = `/_ipx/${modifiers.join(',')}/${encodeURIComponent(source)}`
70
+ return context.rewrite(target)
71
+ }
72
+
73
+ export default handler
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+ // deno-lint-ignore-file no-var prefer-const no-unused-vars no-explicit-any
3
+ import { decode as _base64Decode } from 'https://deno.land/std@0.175.0/encoding/base64.ts'
4
+ import { AsyncLocalStorage as ALSCompat } from 'https://deno.land/std@0.175.0/node/async_hooks.ts'
5
+
6
+ /**
7
+ * These are the shims, polyfills and other kludges to make Next.js work in standards-compliant runtime.
8
+ * This file isn't imported, but is instead inlined along with other chunks into the edge bundle.
9
+ */
10
+
11
+ // Deno defines "window", but naughty libraries think this means it's a browser
12
+ // @ts-ignore
13
+ delete globalThis.window
14
+ globalThis.process = {
15
+ env: { ...Deno.env.toObject(), NEXT_RUNTIME: 'edge', NEXT_PRIVATE_MINIMAL_MODE: '1' },
16
+ }
17
+ globalThis.EdgeRuntime = 'netlify-edge'
18
+ let _ENTRIES = {}
19
+
20
+ // Next.js expects this as a global
21
+ globalThis.AsyncLocalStorage = ALSCompat
22
+
23
+ // Next.js uses this extension to the Headers API implemented by Cloudflare workerd
24
+ if (!('getAll' in Headers.prototype)) {
25
+ // @ts-ignore
26
+ Headers.prototype.getAll = function getAll(name) {
27
+ name = name.toLowerCase()
28
+ if (name !== 'set-cookie') {
29
+ throw new Error('Headers.getAll is only supported for Set-Cookie')
30
+ }
31
+ return [...this.entries()].filter(([key]) => key === name).map(([, value]) => value)
32
+ }
33
+ }
34
+ // Next uses blob: urls to refer to local assets, so we need to intercept these
35
+ const _fetch = globalThis.fetch
36
+ const fetch /* type {typeof globalThis.fetch} */ = async (url, init) => {
37
+ try {
38
+ if (url instanceof URL && url.href?.startsWith('blob:')) {
39
+ const key = url.href.slice(5)
40
+ if (key in _ASSETS) {
41
+ return new Response(_base64Decode(_ASSETS[key]))
42
+ }
43
+ }
44
+ return await _fetch(url, init)
45
+ } catch (error) {
46
+ console.error(error)
47
+ throw error
48
+ }
49
+ }
50
+
51
+ // Next edge runtime uses "self" as a function-scoped global-like object, but some of the older polyfills expect it to equal globalThis
52
+ // See https://nextjs.org/docs/basic-features/supported-browsers-features#polyfills
53
+ const self = { ...globalThis, fetch }
@@ -1,3 +0,0 @@
1
- {
2
- "formats": ["webp"]
3
- }