@netlify/plugin-nextjs 5.5.1 → 5.7.0-ipx.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.
@@ -169,7 +169,7 @@ var copyNextDependencies = async (ctx) => {
169
169
  await tracer.withActiveSpan("copyNextDependencies", async () => {
170
170
  const entries = await readdir(ctx.standaloneDir);
171
171
  const promises = entries.map(async (entry) => {
172
- if (entry === "package.json" || entry === ctx.nextDistDir) {
172
+ if (entry === ctx.nextDistDir) {
173
173
  return;
174
174
  }
175
175
  const src = join(ctx.standaloneDir, entry);
@@ -392,6 +392,7 @@ var import_path_to_regexp = __toESM(require_dist(), 1);
392
392
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
393
393
  import { dirname, join } from "node:path";
394
394
  import { EDGE_HANDLER_NAME } from "../plugin-context.js";
395
+ import { createIpxEdgeAcceptHandler } from "./ipx.js";
395
396
  var writeEdgeManifest = async (ctx, manifest) => {
396
397
  await mkdir(ctx.edgeFunctionsDir, { recursive: true });
397
398
  await writeFile(join(ctx.edgeFunctionsDir, "manifest.json"), JSON.stringify(manifest, null, 2));
@@ -431,10 +432,7 @@ var writeHandlerFile = async (ctx, { matchers, name }) => {
431
432
  const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName);
432
433
  const handlerRuntimeDirectory = join(handlerDirectory, "edge-runtime");
433
434
  await copyRuntime(ctx, handlerDirectory);
434
- await writeFile(
435
- join(handlerRuntimeDirectory, "matchers.json"),
436
- JSON.stringify(augmentMatchers(matchers, ctx))
437
- );
435
+ await writeFile(join(handlerRuntimeDirectory, "matchers.json"), JSON.stringify(matchers));
438
436
  const minimalNextConfig = {
439
437
  basePath: nextConfig.basePath,
440
438
  i18n: nextConfig.i18n,
@@ -516,6 +514,9 @@ var createEdgeHandlers = async (ctx) => {
516
514
  version: 1,
517
515
  functions: netlifyDefinitions
518
516
  };
517
+ if (ctx.imageService === "ipx") {
518
+ await createIpxEdgeAcceptHandler(ctx, netlifyManifest);
519
+ }
519
520
  await writeEdgeManifest(ctx, netlifyManifest);
520
521
  };
521
522
  export {
@@ -0,0 +1,83 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import "../../esm-chunks/chunk-OEQOKJGE.js";
8
+
9
+ // src/build/functions/ipx.ts
10
+ import { cp, mkdir, writeFile } from "fs/promises";
11
+ import { join } from "path";
12
+ import { IPX_HANDLER_NAME } from "../plugin-context.js";
13
+ var sanitizeEdgePath = (imagesPath) => new URL(imagesPath, process.env.URL || "http://n").pathname;
14
+ var getAdjustedImageConfig = (ctx) => {
15
+ return {
16
+ ...ctx.buildConfig.images,
17
+ basePath: [ctx.buildConfig.basePath, IPX_HANDLER_NAME].join("/")
18
+ };
19
+ };
20
+ var createIpxHandler = async (ctx) => {
21
+ await mkdir(ctx.ipxHandlerRootDir, { recursive: true });
22
+ await cp(
23
+ join(ctx.pluginDir, "dist/build/templates/ipx.ts"),
24
+ join(ctx.ipxHandlerRootDir, "_ipx.ts")
25
+ );
26
+ await writeFile(
27
+ join(ctx.ipxHandlerRootDir, "imageconfig.json"),
28
+ JSON.stringify(getAdjustedImageConfig(ctx))
29
+ );
30
+ await writeFile(
31
+ join(ctx.ipxHandlerRootDir, "_ipx.json"),
32
+ JSON.stringify({
33
+ version: 1,
34
+ config: {
35
+ name: "next/image handler",
36
+ generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
37
+ timeout: 120
38
+ }
39
+ })
40
+ );
41
+ ctx.netlifyConfig.redirects.push(
42
+ {
43
+ from: ctx.buildConfig.images.path,
44
+ // eslint-disable-next-line id-length
45
+ query: { url: ":url", w: ":width", q: ":quality" },
46
+ to: `${ctx.buildConfig.basePath}/${IPX_HANDLER_NAME}/w_:width,q_:quality/:url`,
47
+ status: 301
48
+ },
49
+ {
50
+ from: `${ctx.buildConfig.basePath}/${IPX_HANDLER_NAME}/*`,
51
+ to: `/.netlify/builders/${IPX_HANDLER_NAME}`,
52
+ status: 200
53
+ }
54
+ );
55
+ };
56
+ var createIpxEdgeAcceptHandler = async (ctx, netlifyManifest) => {
57
+ await mkdir(ctx.ipxEdgeHandlerRootDir, { recursive: true });
58
+ await cp(
59
+ join(ctx.pluginDir, "dist/build/templates/ipx-edge-accept-handler.ts"),
60
+ join(ctx.ipxEdgeHandlerRootDir, "index.ts")
61
+ );
62
+ await writeFile(
63
+ join(ctx.ipxEdgeHandlerRootDir, "imageconfig.json"),
64
+ JSON.stringify(getAdjustedImageConfig(ctx))
65
+ );
66
+ netlifyManifest.functions.push({
67
+ function: IPX_HANDLER_NAME,
68
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
69
+ // @ts-ignore
70
+ name: "next/image handler",
71
+ path: ctx.buildConfig.images.path ? sanitizeEdgePath(ctx.buildConfig.images.path) : "/_next/image",
72
+ generator: `${ctx.pluginName}@${ctx.pluginVersion}`
73
+ });
74
+ netlifyManifest.layers ??= [];
75
+ netlifyManifest.layers.push({
76
+ name: `https://ipx-edge-function-layer.netlify.app/mod.ts`,
77
+ flag: "ipx-edge-function-layer-url"
78
+ });
79
+ };
80
+ export {
81
+ createIpxEdgeAcceptHandler,
82
+ createIpxHandler
83
+ };
@@ -61,10 +61,16 @@ var copyHandlerDependencies = async (ctx) => {
61
61
  )
62
62
  );
63
63
  }
64
+ promises.push(
65
+ writeFile(
66
+ join(ctx.serverHandlerRuntimeModulesDir, "package.json"),
67
+ JSON.stringify({ type: "module" })
68
+ )
69
+ );
64
70
  const fileList = await (0, import_fast_glob.glob)("dist/**/*", { cwd: ctx.pluginDir });
65
71
  for (const filePath of fileList) {
66
72
  promises.push(
67
- cp(join(ctx.pluginDir, filePath), join(ctx.serverHandlerDir, ".netlify", filePath), {
73
+ cp(join(ctx.pluginDir, filePath), join(ctx.serverHandlerRuntimeModulesDir, filePath), {
68
74
  recursive: true,
69
75
  force: true
70
76
  })
@@ -90,12 +96,6 @@ var writeHandlerManifest = async (ctx) => {
90
96
  "utf-8"
91
97
  );
92
98
  };
93
- var writePackageMetadata = async (ctx) => {
94
- await writeFile(
95
- join(ctx.serverHandlerRootDir, "package.json"),
96
- JSON.stringify({ type: "module" })
97
- );
98
- };
99
99
  var applyTemplateVariables = (template, variables) => {
100
100
  return Object.entries(variables).reduce((acc, [key, value]) => {
101
101
  return acc.replaceAll(key, value);
@@ -126,12 +126,11 @@ var clearStaleServerHandlers = async (ctx) => {
126
126
  };
127
127
  var createServerHandler = async (ctx) => {
128
128
  await tracer.withActiveSpan("createServerHandler", async () => {
129
- await mkdir(join(ctx.serverHandlerDir, ".netlify"), { recursive: true });
129
+ await mkdir(join(ctx.serverHandlerRuntimeModulesDir), { recursive: true });
130
130
  await copyNextServerCode(ctx);
131
131
  await copyNextDependencies(ctx);
132
132
  await copyHandlerDependencies(ctx);
133
133
  await writeHandlerManifest(ctx);
134
- await writePackageMetadata(ctx);
135
134
  await writeHandlerFile(ctx);
136
135
  await verifyHandlerDirStructure(ctx);
137
136
  });
@@ -1528,6 +1528,7 @@ var require_picomatch2 = __commonJS({
1528
1528
 
1529
1529
  // src/build/image-cdn.ts
1530
1530
  var import_picomatch = __toESM(require_picomatch2(), 1);
1531
+ import { createIpxHandler } from "./functions/ipx.js";
1531
1532
  function generateRegexFromPattern(pattern) {
1532
1533
  return (0, import_picomatch.makeRe)(pattern).source;
1533
1534
  }
@@ -1538,67 +1539,71 @@ var setImageConfig = async (ctx) => {
1538
1539
  if (imageLoader !== "default") {
1539
1540
  return;
1540
1541
  }
1541
- ctx.netlifyConfig.redirects.push(
1542
- {
1543
- from: imageEndpointPath,
1544
- // w and q are too short to be used as params with id-length rule
1545
- // but we are forced to do so because of the next/image loader decides on their names
1546
- // eslint-disable-next-line id-length
1547
- query: { url: ":url", w: ":width", q: ":quality" },
1548
- to: "/.netlify/images?url=:url&w=:width&q=:quality",
1549
- status: 200
1550
- },
1551
- // when migrating from @netlify/plugin-nextjs@4 image redirect to ipx might be cached in the browser
1552
- {
1553
- from: "/_ipx/*",
1554
- // w and q are too short to be used as params with id-length rule
1555
- // but we are forced to do so because of the next/image loader decides on their names
1556
- // eslint-disable-next-line id-length
1557
- query: { url: ":url", w: ":width", q: ":quality" },
1558
- to: "/.netlify/images?url=:url&w=:width&q=:quality",
1559
- status: 200
1560
- }
1561
- );
1562
- if (remotePatterns?.length !== 0 || domains?.length !== 0) {
1563
- ctx.netlifyConfig.images ||= { remote_images: [] };
1564
- ctx.netlifyConfig.images.remote_images ||= [];
1565
- if (remotePatterns && remotePatterns.length !== 0) {
1566
- for (const remotePattern of remotePatterns) {
1567
- let { protocol, hostname, port, pathname } = remotePattern;
1568
- if (pathname) {
1569
- pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
1570
- }
1571
- const combinedRemotePattern = `${protocol ?? "http?(s)"}://${hostname}${port ? `:${port}` : ""}${pathname ?? "/**"}`;
1572
- try {
1573
- ctx.netlifyConfig.images.remote_images.push(
1574
- generateRegexFromPattern(combinedRemotePattern)
1575
- );
1576
- } catch (error) {
1577
- ctx.failBuild(
1578
- `Failed to generate Image CDN remote image regex from Next.js remote pattern: ${JSON.stringify(
1579
- { remotePattern, combinedRemotePattern },
1580
- null,
1581
- 2
1582
- )}`,
1583
- error
1584
- );
1542
+ if (ctx.imageService === "ipx") {
1543
+ await createIpxHandler(ctx);
1544
+ } else {
1545
+ ctx.netlifyConfig.redirects.push(
1546
+ {
1547
+ from: imageEndpointPath,
1548
+ // w and q are too short to be used as params with id-length rule
1549
+ // but we are forced to do so because of the next/image loader decides on their names
1550
+ // eslint-disable-next-line id-length
1551
+ query: { url: ":url", w: ":width", q: ":quality" },
1552
+ to: "/.netlify/images?url=:url&w=:width&q=:quality",
1553
+ status: 200
1554
+ },
1555
+ // when migrating from @netlify/plugin-nextjs@4 image redirect to ipx might be cached in the browser
1556
+ {
1557
+ from: "/_ipx/*",
1558
+ // w and q are too short to be used as params with id-length rule
1559
+ // but we are forced to do so because of the next/image loader decides on their names
1560
+ // eslint-disable-next-line id-length
1561
+ query: { url: ":url", w: ":width", q: ":quality" },
1562
+ to: "/.netlify/images?url=:url&w=:width&q=:quality",
1563
+ status: 200
1564
+ }
1565
+ );
1566
+ if (remotePatterns?.length !== 0 || domains?.length !== 0) {
1567
+ ctx.netlifyConfig.images ||= { remote_images: [] };
1568
+ ctx.netlifyConfig.images.remote_images ||= [];
1569
+ if (remotePatterns && remotePatterns.length !== 0) {
1570
+ for (const remotePattern of remotePatterns) {
1571
+ let { protocol, hostname, port, pathname } = remotePattern;
1572
+ if (pathname) {
1573
+ pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
1574
+ }
1575
+ const combinedRemotePattern = `${protocol ?? "http?(s)"}://${hostname}${port ? `:${port}` : ""}${pathname ?? "/**"}`;
1576
+ try {
1577
+ ctx.netlifyConfig.images.remote_images.push(
1578
+ generateRegexFromPattern(combinedRemotePattern)
1579
+ );
1580
+ } catch (error) {
1581
+ ctx.failBuild(
1582
+ `Failed to generate Image CDN remote image regex from Next.js remote pattern: ${JSON.stringify(
1583
+ { remotePattern, combinedRemotePattern },
1584
+ null,
1585
+ 2
1586
+ )}`,
1587
+ error
1588
+ );
1589
+ }
1585
1590
  }
1586
1591
  }
1587
- }
1588
- if (domains && domains.length !== 0) {
1589
- for (const domain of domains) {
1590
- const patternFromDomain = `http?(s)://${domain}/**`;
1591
- try {
1592
- ctx.netlifyConfig.images.remote_images.push(generateRegexFromPattern(patternFromDomain));
1593
- } catch (error) {
1594
- ctx.failBuild(
1595
- `Failed to generate Image CDN remote image regex from Next.js domain: ${JSON.stringify(
1596
- { domain, patternFromDomain },
1597
- null,
1598
- 2
1599
- )}`,
1600
- error
1601
- );
1592
+ if (domains && domains.length !== 0) {
1593
+ for (const domain of domains) {
1594
+ const patternFromDomain = `http?(s)://${domain}/**`;
1595
+ try {
1596
+ ctx.netlifyConfig.images.remote_images.push(generateRegexFromPattern(patternFromDomain));
1597
+ } catch (error) {
1598
+ ctx.failBuild(
1599
+ `Failed to generate Image CDN remote image regex from Next.js domain: ${JSON.stringify(
1600
+ { domain, patternFromDomain },
1601
+ null,
1602
+ 2
1603
+ )}`,
1604
+ error
1605
+ );
1606
+ }
1602
1607
  }
1603
1608
  }
1604
1609
  }
@@ -23,6 +23,7 @@ var MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
23
23
  var PLUGIN_DIR = join(MODULE_DIR, "../..");
24
24
  var DEFAULT_PUBLISH_DIR = ".next";
25
25
  var SERVER_HANDLER_NAME = "___netlify-server-handler";
26
+ var IPX_HANDLER_NAME = "_ipx";
26
27
  var EDGE_HANDLER_NAME = "___netlify-edge-handler";
27
28
  var PluginContext = class {
28
29
  featureFlags;
@@ -116,6 +117,9 @@ var PluginContext = class {
116
117
  const REQUIRED_BUILD_VERSION = ">=29.41.5";
117
118
  return (0, import_semver.satisfies)(this.buildVersion, REQUIRED_BUILD_VERSION, { includePrerelease: true });
118
119
  }
120
+ get imageService() {
121
+ return "ipx";
122
+ }
119
123
  /**
120
124
  * Absolute path of the directory containing the files for the serverless lambda function
121
125
  * `.netlify/functions-internal`
@@ -127,12 +131,19 @@ var PluginContext = class {
127
131
  get serverHandlerRootDir() {
128
132
  return join(this.serverFunctionsDir, SERVER_HANDLER_NAME);
129
133
  }
134
+ /** Absolute path of the ipx handler */
135
+ get ipxHandlerRootDir() {
136
+ return join(this.serverFunctionsDir, IPX_HANDLER_NAME);
137
+ }
130
138
  get serverHandlerDir() {
131
139
  if (this.relativeAppDir.length === 0) {
132
140
  return this.serverHandlerRootDir;
133
141
  }
134
142
  return join(this.serverHandlerRootDir, this.distDirParent);
135
143
  }
144
+ get serverHandlerRuntimeModulesDir() {
145
+ return join(this.serverHandlerDir, ".netlify");
146
+ }
136
147
  get nextServerHandler() {
137
148
  if (this.relativeAppDir.length !== 0) {
138
149
  return join(this.lambdaWorkingDirectory, ".netlify/dist/run/handlers/server.js");
@@ -150,6 +161,10 @@ var PluginContext = class {
150
161
  get edgeHandlerDir() {
151
162
  return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME);
152
163
  }
164
+ /** Absolute path of the ipx edge handler */
165
+ get ipxEdgeHandlerRootDir() {
166
+ return join(this.edgeFunctionsDir, IPX_HANDLER_NAME);
167
+ }
153
168
  constructor(options) {
154
169
  this.constants = options.constants;
155
170
  this.featureFlags = options.featureFlags;
@@ -273,6 +288,7 @@ var PluginContext = class {
273
288
  };
274
289
  export {
275
290
  EDGE_HANDLER_NAME,
291
+ IPX_HANDLER_NAME,
276
292
  PluginContext,
277
293
  SERVER_HANDLER_NAME
278
294
  };
@@ -0,0 +1,13 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-ignore
3
+ import { getHandler } from 'https://ipx-edge-function-layer.netlify.app/mod.ts'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
+ // @ts-ignore Injected at build time
7
+ import imageconfig from './imageconfig.json' assert { type: 'json' }
8
+
9
+ export default getHandler({
10
+ formats: imageconfig?.formats,
11
+ basePath: imageconfig?.basePath,
12
+ imageCDNCompat: true,
13
+ })
@@ -0,0 +1,11 @@
1
+ import { createIPXHandler } from '@netlify/ipx'
2
+
3
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
+ // @ts-ignore Injected at build time
5
+ import { basePath, domains, remotePatterns } from './imageconfig.json'
6
+
7
+ export const handler = createIPXHandler({
8
+ basePath,
9
+ domains,
10
+ remotePatterns,
11
+ })
@@ -103,11 +103,10 @@ async function verifyNetlifyFormsWorkaround(ctx) {
103
103
  }
104
104
  }
105
105
  function verifyNetlifyForms(ctx, html) {
106
- if (!verifications.has("netlifyForms") && !verifications.has("netlifyFormsWorkaround") && formDetectionRegex.test(html)) {
107
- console.warn(
106
+ if (process.env.NETLIFY_NEXT_VERIFY_FORMS !== "0" && process.env.NETLIFY_NEXT_VERIFY_FORMS?.toUpperCase() !== "FALSE" && !verifications.has("netlifyFormsWorkaround") && formDetectionRegex.test(html)) {
107
+ ctx.failBuild(
108
108
  "@netlify/plugin-nextjs@5 requires migration steps to support Netlify Forms. Refer to https://ntl.fyi/next-runtime-forms-migration for migration example."
109
109
  );
110
- verifications.add("netlifyForms");
111
110
  }
112
111
  }
113
112
  export {
@@ -8,7 +8,7 @@ import "./chunk-OEQOKJGE.js";
8
8
 
9
9
  // package.json
10
10
  var name = "@netlify/plugin-nextjs";
11
- var version = "5.5.1";
11
+ var version = "5.7.0-ipx.0";
12
12
  var description = "Run Next.js seamlessly on Netlify";
13
13
  var main = "./dist/index.js";
14
14
  var type = "module";
@@ -55,15 +55,18 @@ var bugs = {
55
55
  url: "https://github.com/netlify/next-runtime/issues"
56
56
  };
57
57
  var homepage = "https://github.com/netlify/next-runtime#readme";
58
+ var dependencies = {
59
+ "@netlify/ipx": "^1.4.6"
60
+ };
58
61
  var devDependencies = {
59
62
  "@fastly/http-compute-js": "1.1.4",
60
63
  "@netlify/blobs": "^7.3.0",
61
64
  "@netlify/build": "^29.50.2",
62
65
  "@netlify/edge-bundler": "^12.1.1",
63
- "@netlify/edge-functions": "^2.8.1",
66
+ "@netlify/edge-functions": "^2.10.0",
64
67
  "@netlify/eslint-config-node": "^7.0.1",
65
68
  "@netlify/functions": "^2.8.1",
66
- "@netlify/serverless-functions-api": "^1.19.1",
69
+ "@netlify/serverless-functions-api": "^1.22.0",
67
70
  "@netlify/zip-it-and-ship-it": "^9.37.3",
68
71
  "@opentelemetry/api": "^1.8.0",
69
72
  "@opentelemetry/exporter-trace-otlp-http": "^0.51.0",
@@ -104,7 +107,6 @@ var clean_package = {
104
107
  indent: 2,
105
108
  remove: [
106
109
  "clean-package",
107
- "dependencies",
108
110
  "devDependencies",
109
111
  "scripts"
110
112
  ]
@@ -123,6 +125,7 @@ var package_default = {
123
125
  license,
124
126
  bugs,
125
127
  homepage,
128
+ dependencies,
126
129
  devDependencies,
127
130
  "clean-package": clean_package
128
131
  };
@@ -130,6 +133,7 @@ export {
130
133
  bugs,
131
134
  clean_package as "clean-package",
132
135
  package_default as default,
136
+ dependencies,
133
137
  description,
134
138
  devDependencies,
135
139
  engines,
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ var onBuild = async (options) => {
62
62
  await saveBuildCache(ctx);
63
63
  }
64
64
  if (ctx.buildConfig.output === "export") {
65
- return copyStaticExport(ctx);
65
+ return Promise.all([copyStaticExport(ctx), setImageConfig(ctx)]);
66
66
  }
67
67
  await verifyAdvancedAPIRoutes(ctx);
68
68
  await verifyNetlifyFormsWorkaround(ctx);
@@ -67385,7 +67385,7 @@ var import_semantic_conventions = __toESM(require_src(), 1);
67385
67385
  import { getLogger } from "./request-context.cjs";
67386
67386
  var {
67387
67387
  default: { version, name }
67388
- } = await import("../../esm-chunks/package-SHOGGUO3.js");
67388
+ } = await import("../../esm-chunks/package-O63J727E.js");
67389
67389
  var sdk = new import_sdk_node.NodeSDK({
67390
67390
  resource: new import_resources.Resource({
67391
67391
  [import_semantic_conventions.SEMRESATTRS_SERVICE_NAME]: name,
@@ -2,6 +2,7 @@ import type { Context } from '@netlify/edge-functions'
2
2
 
3
3
  import {
4
4
  addBasePath,
5
+ addLocale,
5
6
  addTrailingSlash,
6
7
  normalizeDataUrl,
7
8
  normalizeLocalePath,
@@ -73,6 +74,33 @@ const normalizeRequestURL = (
73
74
  }
74
75
  }
75
76
 
77
+ export const localizeRequest = (
78
+ url: URL,
79
+ nextConfig?: {
80
+ basePath?: string
81
+ i18n?: I18NConfig | null
82
+ },
83
+ ): { localizedUrl: URL; locale?: string } => {
84
+ const localizedUrl = new URL(url)
85
+ localizedUrl.pathname = removeBasePath(localizedUrl.pathname, nextConfig?.basePath)
86
+
87
+ // Detect the locale from the URL
88
+ const { detectedLocale } = normalizeLocalePath(localizedUrl.pathname, nextConfig?.i18n?.locales)
89
+
90
+ // Add the locale to the URL if not already present
91
+ localizedUrl.pathname = addLocale(
92
+ localizedUrl.pathname,
93
+ detectedLocale ?? nextConfig?.i18n?.defaultLocale,
94
+ )
95
+
96
+ localizedUrl.pathname = addBasePath(localizedUrl.pathname, nextConfig?.basePath)
97
+
98
+ return {
99
+ localizedUrl,
100
+ locale: detectedLocale,
101
+ }
102
+ }
103
+
76
104
  export const buildNextRequest = (
77
105
  request: Request,
78
106
  context: Context,
@@ -29,6 +29,20 @@ export const addBasePath = (path: string, basePath?: string) => {
29
29
  return path
30
30
  }
31
31
 
32
+ // add locale prefix if not present, allowing for locale fallbacks
33
+ export const addLocale = (path: string, locale?: string) => {
34
+ if (
35
+ locale &&
36
+ path.toLowerCase() !== `/${locale.toLowerCase()}` &&
37
+ !path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) &&
38
+ !path.startsWith(`/api/`) &&
39
+ !path.startsWith(`/_next/`)
40
+ ) {
41
+ return `/${locale}${path}`
42
+ }
43
+ return path
44
+ }
45
+
32
46
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts
33
47
 
34
48
  export interface PathLocale {
@@ -5,7 +5,7 @@ import nextConfig from './next.config.json' with { type: 'json' }
5
5
 
6
6
  import { InternalHeaders } from './lib/headers.ts'
7
7
  import { logger, LogLevel } from './lib/logging.ts'
8
- import { buildNextRequest, RequestData } from './lib/next-request.ts'
8
+ import { buildNextRequest, localizeRequest, RequestData } from './lib/next-request.ts'
9
9
  import { buildResponse, FetchEventResult } from './lib/response.ts'
10
10
  import {
11
11
  getMiddlewareRouteMatcher,
@@ -31,8 +31,8 @@ export async function handleMiddleware(
31
31
  context: Context,
32
32
  nextHandler: NextHandler,
33
33
  ) {
34
- const nextRequest = buildNextRequest(request, context, nextConfig)
35
34
  const url = new URL(request.url)
35
+
36
36
  const reqLogger = logger
37
37
  .withLogLevel(
38
38
  request.headers.has(InternalHeaders.NFDebugLogging) ? LogLevel.Debug : LogLevel.Log,
@@ -40,16 +40,20 @@ export async function handleMiddleware(
40
40
  .withFields({ url_path: url.pathname })
41
41
  .withRequestID(request.headers.get(InternalHeaders.NFRequestID))
42
42
 
43
+ const { localizedUrl } = localizeRequest(url, nextConfig)
43
44
  // While we have already checked the path when mapping to the edge function,
44
45
  // Next.js supports extra rules that we need to check here too, because we
45
46
  // might be running an edge function for a path we should not. If we find
46
47
  // that's the case, short-circuit the execution.
47
- if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) {
48
+ if (
49
+ !matchesMiddleware(localizedUrl.pathname, request, searchParamsToUrlQuery(url.searchParams))
50
+ ) {
48
51
  reqLogger.debug('Aborting middleware due to runtime rules')
49
52
 
50
53
  return
51
54
  }
52
55
 
56
+ const nextRequest = buildNextRequest(request, context, nextConfig)
53
57
  try {
54
58
  const result = await nextHandler({ request: nextRequest })
55
59
  const response = await buildResponse({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "5.5.1",
3
+ "version": "5.7.0-ipx.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -26,5 +26,8 @@
26
26
  "bugs": {
27
27
  "url": "https://github.com/netlify/next-runtime/issues"
28
28
  },
29
- "homepage": "https://github.com/netlify/next-runtime#readme"
29
+ "homepage": "https://github.com/netlify/next-runtime#readme",
30
+ "dependencies": {
31
+ "@netlify/ipx": "^1.4.6"
32
+ }
30
33
  }