@netlify/plugin-nextjs 4.0.0-rc.2 → 4.1.2

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/README.md CHANGED
@@ -1,9 +1,6 @@
1
- ![Next.js on Netlify Build Plugin](next-on-netlify.png)
1
+ ![Essential Next.js Build Plugin](next-on-netlify.png)
2
2
 
3
- # Essential Next.js Build Plugin (beta)
4
-
5
- :warning: This is the beta version of the Essential Next.js plugin. For the stable version, refer to
6
- [Essential Next.js plugin v3](https://github.com/netlify/netlify-plugin-nextjs/tree/v3#readme) :warning:
3
+ # Essential Next.js Build Plugin
7
4
 
8
5
  <p align="center">
9
6
  <a aria-label="npm version" href="https://www.npmjs.com/package/@netlify/plugin-nextjs">
@@ -19,36 +16,46 @@
19
16
  Version 4 is a complete rewrite of the Essential Next.js plugin. For full details of everything that's new, check out
20
17
  [the v4 release notes](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/release-notes/v4.md)
21
18
 
22
- ## Installing the beta
19
+ ## Installing the plugin
23
20
 
24
- - Install the module:
21
+ The plugin installs automatically for new Next.js sites on Netlify. You can also install it manually like this:
25
22
 
26
23
  ```shell
27
- npm install -D @netlify/plugin-nextjs@beta
24
+ npm install -D @netlify/plugin-nextjs
28
25
  ```
29
26
 
30
- - Change the `publish` directory to `.next` and add the plugin to `netlify.toml` if not already installed:
27
+ ...then add the plugin to your `netlify.toml` file:
31
28
 
32
29
  ```toml
33
- [build]
34
- publish = ".next"
35
-
36
30
  [[plugins]]
37
31
  package = "@netlify/plugin-nextjs"
38
32
  ```
39
33
 
40
- If you previously set a custom `distDir` in your `next.config.js`, or set `node_bundler` or `external_node_modules` in
41
- your `netlify.toml` these are no longer needed and can be removed.
34
+ ## Migrating from an older version of the plugin
35
+
36
+ You can manually upgrade from the previous version of the plugin by running the following command:
37
+
38
+ ```shell
39
+ npm install -D @netlify/plugin-nextjs@latest
40
+ ```
41
+
42
+ Change the `publish` directory to `.next`:
43
+
44
+ ```toml
45
+ [build]
46
+ publish = ".next"
47
+ ```
48
+
49
+ If you previously set these values, they're no longer needed and can be removed:
50
+
51
+ - `distDir` in your `next.config.js`
52
+ - `node_bundler = "esbuild"` in `netlify.toml`
53
+ - `external_node_modules` in `netlify.toml`
42
54
 
43
55
  The `serverless` and `experimental-serverless-trace` targets are deprecated in Next 12, and all builds with this plugin
44
56
  will now use the default `server` target. If you previously set the target in your `next.config.js`, you should remove
45
57
  it.
46
58
 
47
- If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory,
48
- which may be in a subdirectory. If you have changed your `distDir` then it will need to match that.
49
-
50
- If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
51
-
52
59
  If you currently use redirects or rewrites on your site, see
53
60
  [the Rewrites and Redirects guide](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/redirects-rewrites.md)
54
61
  for information on changes to how they are handled in this version.
@@ -57,7 +64,29 @@ If you want to use Next 12's beta Middleware feature, this will mostly work as e
57
64
  [read the docs on some caveats and workarounds](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/middleware.md)
58
65
  that are currently needed.
59
66
 
60
- ## Beta feedback
67
+ ## Monorepos
68
+
69
+ If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory,
70
+ which may be in a subdirectory. If you have changed your `distDir` then it will need to match that.
71
+
72
+ If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
73
+
74
+ ## Incremental Static Regeneration (ISR)
75
+
76
+ The Essential Next.js plugin now fully supports ISR on Netlify. For more details see
77
+ [the ISR docs](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/isr.md).
78
+
79
+ ## Use with `next export`
80
+
81
+ If you are using `next export` to generate a static site, you do not need most of the functionality of this plugin and
82
+ you can remove it. Alternatively you can
83
+ [set the environment variable](https://docs.netlify.com/configure-builds/environment-variables/)
84
+ `NETLIFY_NEXT_PLUGIN_SKIP` to `true` and the plugin will handle caching but won't generate any functions for SSR
85
+ support. See [`demos/next-export`](https://github.com/netlify/netlify-plugin-nextjs/tree/main/demos/next-export) for an
86
+ example.
87
+
88
+ ## Feedback
61
89
 
62
- Please share any thoughts, feedback or questions about the beta
63
- [in our discussion](https://github.com/netlify/netlify-plugin-nextjs/discussions/706).
90
+ If you think you have found a bug in the plugin,
91
+ [please open an issue](https://github.com/netlify/netlify-plugin-nextjs/issues). If you have comments or feature
92
+ requests, [see the dicussion board](https://github.com/netlify/netlify-plugin-nextjs/discussions)
@@ -30,7 +30,7 @@ const resolveModuleRoot = (moduleName) => {
30
30
  try {
31
31
  return pathe_1.dirname(pathe_1.relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })));
32
32
  }
33
- catch (error) {
33
+ catch {
34
34
  return null;
35
35
  }
36
36
  };
@@ -75,7 +75,7 @@ const moveStaticPages = async ({ netlifyConfig, target, i18n, }) => {
75
75
  Object.entries(prerenderManifest.routes).forEach(([route, { initialRevalidateSeconds }]) => {
76
76
  if (initialRevalidateSeconds) {
77
77
  // Find all files used by ISR routes
78
- const trimmedPath = route.slice(1);
78
+ const trimmedPath = route === '/' ? 'index' : route.slice(1);
79
79
  isrFiles.add(`${trimmedPath}.html`);
80
80
  isrFiles.add(`${trimmedPath}.json`);
81
81
  if (initialRevalidateSeconds < constants_1.MINIMUM_REVALIDATE_SECONDS) {
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateRedirects = void 0;
3
+ exports.generateRedirects = exports.generateStaticRedirects = void 0;
7
4
  const fs_extra_1 = require("fs-extra");
8
- const globby_1 = __importDefault(require("globby"));
9
5
  const pathe_1 = require("pathe");
10
6
  const constants_1 = require("../constants");
11
7
  const utils_1 = require("./utils");
@@ -37,8 +33,17 @@ const generateLocaleRedirects = ({ i18n, basePath, trailingSlash, }) => {
37
33
  });
38
34
  return redirects;
39
35
  };
40
- const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, }) => {
41
- const { dynamicRoutes, routes: staticRoutes } = await fs_extra_1.readJSON(pathe_1.join(netlifyConfig.build.publish, 'prerender-manifest.json'));
36
+ const generateStaticRedirects = ({ netlifyConfig, nextConfig: { i18n, basePath }, }) => {
37
+ // Static files are in `static`
38
+ netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 });
39
+ if (i18n) {
40
+ netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
41
+ }
42
+ };
43
+ exports.generateStaticRedirects = generateStaticRedirects;
44
+ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId, }) => {
45
+ const { dynamicRoutes: prerenderedDynamicRoutes, routes: prerenderedStaticRoutes } = await fs_extra_1.readJSON(pathe_1.join(netlifyConfig.build.publish, 'prerender-manifest.json'));
46
+ const { dynamicRoutes, staticRoutes } = await fs_extra_1.readJSON(pathe_1.join(netlifyConfig.build.publish, 'routes-manifest.json'));
42
47
  netlifyConfig.redirects.push(...constants_1.HIDDEN_PATHS.map((path) => ({
43
48
  from: `${basePath}${path}`,
44
49
  to: '/404.html',
@@ -48,83 +53,71 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
48
53
  if (i18n && i18n.localeDetection !== false) {
49
54
  netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }));
50
55
  }
51
- const dataRedirects = [];
52
- const pageRedirects = [];
53
- const isrRedirects = [];
54
- const dynamicRouteEntries = Object.entries(dynamicRoutes);
55
- const staticRouteEntries = Object.entries(staticRoutes);
56
- staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
57
- // Only look for revalidate as we need to rewrite these to SSR rather than ODB
56
+ // This is only used in prod, so dev uses `next dev` directly
57
+ netlifyConfig.redirects.push(
58
+ // API routes always need to be served from the regular function
59
+ ...utils_1.getApiRewrites(basePath),
60
+ // Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
61
+ ...(await utils_1.getPreviewRewrites({ basePath, appDir })));
62
+ const staticRouteEntries = Object.entries(prerenderedStaticRoutes);
63
+ const staticRoutePaths = new Set();
64
+ // First add all static ISR routes
65
+ staticRouteEntries.forEach(([route, { initialRevalidateSeconds }]) => {
66
+ if (utils_1.isApiRoute(route)) {
67
+ return;
68
+ }
69
+ staticRoutePaths.add(route);
58
70
  if (initialRevalidateSeconds === false) {
59
71
  // These can be ignored, as they're static files handled by the CDN
60
72
  return;
61
73
  }
74
+ // The default locale is served from the root, not the localised path
62
75
  if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && route.startsWith(`/${i18n.defaultLocale}/`)) {
63
76
  route = route.slice(i18n.defaultLocale.length + 1);
77
+ staticRoutePaths.add(route);
78
+ netlifyConfig.redirects.push(...utils_1.redirectsForNextRouteWithData({
79
+ route,
80
+ dataRoute: utils_1.routeToDataRoute(route, buildId, i18n.defaultLocale),
81
+ basePath,
82
+ to: constants_1.ODB_FUNCTION_PATH,
83
+ force: true,
84
+ }));
85
+ }
86
+ else {
87
+ // ISR routes use the ODB handler
88
+ netlifyConfig.redirects.push(
89
+ // No i18n, because the route is already localized
90
+ ...utils_1.redirectsForNextRoute({ route, basePath, to: constants_1.ODB_FUNCTION_PATH, force: true, buildId, i18n: null }));
64
91
  }
65
- isrRedirects.push(...utils_1.netlifyRoutesForNextRoute(dataRoute), ...utils_1.netlifyRoutesForNextRoute(route));
66
92
  });
67
- dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
68
- // Add redirects if fallback is "null" (aka blocking) or true/a string
69
- if (fallback === false) {
93
+ // Add rewrites for all static SSR routes
94
+ staticRoutes.forEach((route) => {
95
+ if (staticRoutePaths.has(route.page) || utils_1.isApiRoute(route.page)) {
96
+ // Prerendered static routes are either handled by the CDN or are ISR
70
97
  return;
71
98
  }
72
- pageRedirects.push(...utils_1.netlifyRoutesForNextRoute(route));
73
- dataRedirects.push(...utils_1.netlifyRoutesForNextRoute(dataRoute));
99
+ netlifyConfig.redirects.push(...utils_1.redirectsForNextRoute({ route: route.page, buildId, basePath, to: constants_1.HANDLER_FUNCTION_PATH, i18n }));
74
100
  });
75
- if (i18n) {
76
- netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
77
- }
78
- const publicFiles = await globby_1.default('**/*', { cwd: pathe_1.join(appDir, 'public') });
79
- // This is only used in prod, so dev uses `next dev` directly
80
- netlifyConfig.redirects.push(
81
- // Static files are in `static`
82
- { from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
83
- // API routes always need to be served from the regular function
84
- {
85
- from: `${basePath}/api`,
86
- to: constants_1.HANDLER_FUNCTION_PATH,
87
- status: 200,
88
- }, {
89
- from: `${basePath}/api/*`,
90
- to: constants_1.HANDLER_FUNCTION_PATH,
91
- status: 200,
92
- },
93
- // Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
94
- ...publicFiles.map((file) => ({
95
- from: `${basePath}/${file}`,
96
- // This is a no-op, but we do it to stop it matching the following rule
97
- to: `${basePath}/${file}`,
98
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
99
- status: 200,
100
- })), {
101
+ // Add rewrites for all dynamic routes (both SSR and ISR)
102
+ dynamicRoutes.forEach((route) => {
103
+ if (utils_1.isApiRoute(route.page)) {
104
+ return;
105
+ }
106
+ if (route.page in prerenderedDynamicRoutes) {
107
+ const { fallback } = prerenderedDynamicRoutes[route.page];
108
+ const { to, status } = utils_1.targetForFallback(fallback);
109
+ netlifyConfig.redirects.push(...utils_1.redirectsForNextRoute({ buildId, route: route.page, basePath, to, status, i18n }));
110
+ }
111
+ else {
112
+ // If the route isn't prerendered, it's SSR
113
+ netlifyConfig.redirects.push(...utils_1.redirectsForNextRoute({ route: route.page, buildId, basePath, to: constants_1.HANDLER_FUNCTION_PATH, i18n }));
114
+ }
115
+ });
116
+ // Final fallback
117
+ netlifyConfig.redirects.push({
101
118
  from: `${basePath}/*`,
102
119
  to: constants_1.HANDLER_FUNCTION_PATH,
103
120
  status: 200,
104
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
105
- force: true,
106
- },
107
- // ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
108
- ...isrRedirects.map((redirect) => ({
109
- from: `${basePath}${redirect}`,
110
- to: constants_1.ODB_FUNCTION_PATH,
111
- status: 200,
112
- force: true,
113
- })),
114
- // These are pages with fallback set, which need an ODB
115
- // Data redirects go first, to avoid conflict with splat redirects
116
- ...dataRedirects.map((redirect) => ({
117
- from: `${basePath}${redirect}`,
118
- to: constants_1.ODB_FUNCTION_PATH,
119
- status: 200,
120
- })),
121
- // ...then all the other fallback pages
122
- ...pageRedirects.map((redirect) => ({
123
- from: `${basePath}${redirect}`,
124
- to: constants_1.ODB_FUNCTION_PATH,
125
- status: 200,
126
- })),
127
- // Everything else is handled by the regular function
128
- { from: `${basePath}/*`, to: constants_1.HANDLER_FUNCTION_PATH, status: 200 });
121
+ });
129
122
  };
130
123
  exports.generateRedirects = generateRedirects;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.netlifyRoutesForNextRoute = void 0;
6
+ exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNextRoute = exports.targetForFallback = exports.isApiRoute = exports.routeToDataRoute = exports.netlifyRoutesForNextRouteWithData = exports.toNetlifyRoute = void 0;
7
+ const globby_1 = __importDefault(require("globby"));
8
+ const pathe_1 = require("pathe");
4
9
  const constants_1 = require("../constants");
5
- const netlifyRoutesForNextRoute = (nextRoute) => {
10
+ const toNetlifyRoute = (nextRoute) => {
6
11
  const netlifyRoutes = [nextRoute];
7
12
  // If the route is an optional catch-all route, we need to add a second
8
13
  // Netlify route for the base path (when no parameters are present).
@@ -29,4 +34,95 @@ const netlifyRoutesForNextRoute = (nextRoute) => {
29
34
  // Replace dynamic parameters, e.g., [id]
30
35
  .replace(constants_1.DYNAMIC_PARAMETER_REGEX, '/:$1'));
31
36
  };
32
- exports.netlifyRoutesForNextRoute = netlifyRoutesForNextRoute;
37
+ exports.toNetlifyRoute = toNetlifyRoute;
38
+ const netlifyRoutesForNextRouteWithData = ({ route, dataRoute }) => [
39
+ ...exports.toNetlifyRoute(dataRoute),
40
+ ...exports.toNetlifyRoute(route),
41
+ ];
42
+ exports.netlifyRoutesForNextRouteWithData = netlifyRoutesForNextRouteWithData;
43
+ const routeToDataRoute = (route, buildId, locale) => `/_next/data/${buildId}${locale ? `/${locale}` : ''}${route === '/' ? '/index' : route}.json`;
44
+ exports.routeToDataRoute = routeToDataRoute;
45
+ const netlifyRoutesForNextRoute = (route, buildId, i18n) => {
46
+ var _a;
47
+ if (!((_a = i18n === null || i18n === void 0 ? void 0 : i18n.locales) === null || _a === void 0 ? void 0 : _a.length)) {
48
+ return exports.netlifyRoutesForNextRouteWithData({ route, dataRoute: exports.routeToDataRoute(route, buildId) });
49
+ }
50
+ const { locales, defaultLocale } = i18n;
51
+ const routes = [];
52
+ locales.forEach((locale) => {
53
+ // Data route is always localized
54
+ const dataRoute = exports.routeToDataRoute(route, buildId, locale);
55
+ routes.push(
56
+ // Default locale is served from root, not localized
57
+ ...exports.netlifyRoutesForNextRouteWithData({
58
+ route: locale === defaultLocale ? route : `/${locale}${route}`,
59
+ dataRoute,
60
+ }));
61
+ });
62
+ return routes;
63
+ };
64
+ const isApiRoute = (route) => route.startsWith('/api/') || route === '/api';
65
+ exports.isApiRoute = isApiRoute;
66
+ const targetForFallback = (fallback) => {
67
+ if (fallback === null || fallback === false) {
68
+ // fallback = null mean "blocking", which uses ODB. For fallback=false then anything prerendered should 404.
69
+ // However i18n pages may not have been prerendered, so we still need to hit the origin
70
+ return { to: constants_1.ODB_FUNCTION_PATH, status: 200 };
71
+ }
72
+ // fallback = true is also ODB
73
+ return { to: constants_1.ODB_FUNCTION_PATH, status: 200 };
74
+ };
75
+ exports.targetForFallback = targetForFallback;
76
+ const redirectsForNextRoute = ({ route, buildId, basePath, to, i18n, status = 200, force = false, }) => netlifyRoutesForNextRoute(route, buildId, i18n).map((redirect) => ({
77
+ from: `${basePath}${redirect}`,
78
+ to,
79
+ status,
80
+ force,
81
+ }));
82
+ exports.redirectsForNextRoute = redirectsForNextRoute;
83
+ const redirectsForNextRouteWithData = ({ route, dataRoute, basePath, to, status = 200, force = false, }) => exports.netlifyRoutesForNextRouteWithData({ route, dataRoute }).map((redirect) => ({
84
+ from: `${basePath}${redirect}`,
85
+ to,
86
+ status,
87
+ force,
88
+ }));
89
+ exports.redirectsForNextRouteWithData = redirectsForNextRouteWithData;
90
+ const getApiRewrites = (basePath) => [
91
+ {
92
+ from: `${basePath}/api`,
93
+ to: constants_1.HANDLER_FUNCTION_PATH,
94
+ status: 200,
95
+ },
96
+ {
97
+ from: `${basePath}/api/*`,
98
+ to: constants_1.HANDLER_FUNCTION_PATH,
99
+ status: 200,
100
+ },
101
+ ];
102
+ exports.getApiRewrites = getApiRewrites;
103
+ const getPreviewRewrites = async ({ basePath, appDir }) => {
104
+ const publicFiles = await globby_1.default('**/*', { cwd: pathe_1.join(appDir, 'public') });
105
+ // Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
106
+ return [
107
+ ...publicFiles.map((file) => ({
108
+ from: `${basePath}/${file}`,
109
+ // This is a no-op, but we do it to stop it matching the following rule
110
+ to: `${basePath}/${file}`,
111
+ conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
112
+ status: 200,
113
+ })),
114
+ {
115
+ from: `${basePath}/*`,
116
+ to: constants_1.HANDLER_FUNCTION_PATH,
117
+ status: 200,
118
+ conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
119
+ force: true,
120
+ },
121
+ ];
122
+ };
123
+ exports.getPreviewRewrites = getPreviewRewrites;
124
+ const shouldSkip = () => process.env.NEXT_PLUGIN_FORCE_RUN === 'false' ||
125
+ process.env.NEXT_PLUGIN_FORCE_RUN === '0' ||
126
+ process.env.NETLIFY_NEXT_PLUGIN_SKIP === 'true' ||
127
+ process.env.NETLIFY_NEXT_PLUGIN_SKIP === '1';
128
+ exports.shouldSkip = shouldSkip;
@@ -22,7 +22,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.logBetaMessage = exports.checkZipSize = exports.checkForRootPublish = exports.checkNextSiteHasBuilt = exports.checkForOldFunctions = exports.verifyNetlifyBuildVersion = void 0;
25
+ exports.warnForProblematicUserRewrites = exports.getProblematicUserRewrites = exports.checkZipSize = exports.checkForRootPublish = exports.checkNextSiteHasBuilt = exports.checkForOldFunctions = exports.verifyNetlifyBuildVersion = void 0;
26
+ /* eslint-disable max-lines */
26
27
  const fs_1 = require("fs");
27
28
  const path_1 = __importStar(require("path"));
28
29
  const chalk_1 = require("chalk");
@@ -61,17 +62,19 @@ const checkForOldFunctions = async ({ functions }) => {
61
62
  exports.checkForOldFunctions = checkForOldFunctions;
62
63
  const checkNextSiteHasBuilt = ({ publish, failBuild, }) => {
63
64
  if (!fs_1.existsSync(path_1.default.join(publish, 'BUILD_ID'))) {
65
+ const outWarning = path_1.default.basename(publish) === 'out'
66
+ ? `Your publish directory is set to "out", but in most cases it should be ".next".`
67
+ : `In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config.`;
64
68
  return failBuild(outdent_1.outdent `
65
- The directory "${path_1.default.relative(process.cwd(), publish)}" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory.
66
- In most cases it should be set to the site's ".next" directory path, unless you have chosen a custom "distDir" in your Next config.
67
- If you are using "next export" then the Essential Next.js plugin should be removed. See https://ntl.fyi/remove-plugin for details.
69
+ The directory "${path_1.default.relative(process.cwd(), publish)}" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory.
70
+ ${outWarning}
71
+ If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true".
68
72
  `);
69
73
  }
70
74
  if (fs_1.existsSync(path_1.default.join(publish, 'export-detail.json'))) {
71
75
  failBuild(outdent_1.outdent `
72
76
  Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
73
- This plugin is not needed for "next export" so should be removed, and publish directory set to "out".
74
- See https://ntl.fyi/remove-plugin for more details on how to remove this plugin.
77
+ The publish directory should be set to "out", and you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true".
75
78
  `);
76
79
  }
77
80
  };
@@ -116,8 +119,38 @@ const checkZipSize = async (file, maxSize = constants_1.LAMBDA_MAX_SIZE) => {
116
119
  console.log(chalk_1.greenBright `\n\nFor more information on fixing this, see ${chalk_1.blueBright `https://ntl.fyi/large-next-functions`}`);
117
120
  };
118
121
  exports.checkZipSize = checkZipSize;
119
- const logBetaMessage = () => console.log(chalk_1.greenBright(outdent_1.outdent `
120
- Thank you for trying the Essential Next.js beta plugin.
121
- Please share feedback (both good and bad) at ${chalk_1.blueBright `https://ntl.fyi/next-beta-feedback`}
122
- `));
123
- exports.logBetaMessage = logBetaMessage;
122
+ const getProblematicUserRewrites = ({ redirects, basePath, }) => {
123
+ const userRewrites = [];
124
+ for (const redirect of redirects) {
125
+ // This is the first of the plugin-generated redirects so we can stop checking
126
+ if (redirect.from === `${basePath}/_next/static/*` && redirect.to === `/static/:splat` && redirect.status === 200) {
127
+ break;
128
+ }
129
+ if (
130
+ // Redirects are fine
131
+ (redirect.status === 200 || redirect.status === 404) &&
132
+ // Rewriting to a function is also fine
133
+ !redirect.to.startsWith('/.netlify/') &&
134
+ // ...so is proxying
135
+ !redirect.to.startsWith('http')) {
136
+ userRewrites.push(redirect);
137
+ }
138
+ }
139
+ return userRewrites;
140
+ };
141
+ exports.getProblematicUserRewrites = getProblematicUserRewrites;
142
+ const warnForProblematicUserRewrites = ({ redirects, basePath, }) => {
143
+ const userRewrites = exports.getProblematicUserRewrites({ redirects, basePath });
144
+ if (userRewrites.length === 0) {
145
+ return;
146
+ }
147
+ console.log(chalk_1.yellowBright(outdent_1.outdent `
148
+ You have the following Netlify rewrite${userRewrites.length === 1 ? '' : 's'} that might cause conflicts with the Next.js plugin:
149
+
150
+ ${chalk_1.reset(userRewrites.map(({ from, to, status }) => `- ${from} ${to} ${status}`).join('\n'))}
151
+
152
+ For more information, see https://ntl.fyi/next-rewrites
153
+ `));
154
+ };
155
+ exports.warnForProblematicUserRewrites = warnForProblematicUserRewrites;
156
+ /* eslint-enable max-lines */
package/lib/index.js CHANGED
@@ -1,18 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const path_1 = require("path");
4
+ const fs_extra_1 = require("fs-extra");
4
5
  const constants_1 = require("./constants");
5
6
  const cache_1 = require("./helpers/cache");
6
7
  const config_1 = require("./helpers/config");
7
8
  const files_1 = require("./helpers/files");
8
9
  const functions_1 = require("./helpers/functions");
9
10
  const redirects_1 = require("./helpers/redirects");
11
+ const utils_1 = require("./helpers/utils");
10
12
  const verification_1 = require("./helpers/verification");
11
13
  const plugin = {
12
14
  async onPreBuild({ constants, netlifyConfig, utils: { build: { failBuild }, cache, }, }) {
13
15
  var _a;
14
- verification_1.logBetaMessage();
15
16
  const { publish } = netlifyConfig.build;
17
+ if (utils_1.shouldSkip()) {
18
+ await cache_1.restoreCache({ cache, publish });
19
+ console.log('Not running Essential Next.js plugin');
20
+ if (fs_extra_1.existsSync(path_1.join(constants.INTERNAL_FUNCTIONS_SRC, constants_1.HANDLER_FUNCTION_NAME))) {
21
+ console.log(`Please ensure you remove any generated functions from ${constants.INTERNAL_FUNCTIONS_SRC}`);
22
+ }
23
+ return;
24
+ }
16
25
  verification_1.checkForRootPublish({ publish, failBuild });
17
26
  verification_1.verifyNetlifyBuildVersion({ failBuild, ...constants });
18
27
  await cache_1.restoreCache({ cache, publish });
@@ -21,40 +30,54 @@ const plugin = {
21
30
  netlifyConfig.build.environment.NEXT_PRIVATE_TARGET = 'server';
22
31
  },
23
32
  async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
33
+ if (utils_1.shouldSkip()) {
34
+ return;
35
+ }
24
36
  const { publish } = netlifyConfig.build;
25
37
  verification_1.checkNextSiteHasBuilt({ publish, failBuild });
26
38
  const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir } = await config_1.getNextConfig({
27
39
  publish,
28
40
  failBuild,
29
41
  });
42
+ const buildId = fs_extra_1.readFileSync(path_1.join(publish, 'BUILD_ID'), 'utf8').trim();
30
43
  config_1.configureHandlerFunctions({ netlifyConfig, ignore, publish: path_1.relative(process.cwd(), publish) });
31
44
  await functions_1.generateFunctions(constants, appDir);
32
45
  await functions_1.generatePagesResolver({ target, constants });
33
46
  await files_1.movePublicFiles({ appDir, outdir, publish });
34
- if (process.env.EXPERIMENTAL_ODB_TTL) {
35
- await files_1.patchNextFiles(basePath);
36
- }
47
+ await files_1.patchNextFiles(basePath);
37
48
  if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
38
49
  console.log("The flag 'EXPERIMENTAL_MOVE_STATIC_PAGES' is no longer required, as it is now the default. To disable this behavior, set the env var 'SERVE_STATIC_FILES_FROM_ORIGIN' to 'true'");
39
50
  }
40
51
  if (!process.env.SERVE_STATIC_FILES_FROM_ORIGIN) {
41
52
  await files_1.moveStaticPages({ target, netlifyConfig, i18n });
42
53
  }
54
+ await redirects_1.generateStaticRedirects({
55
+ netlifyConfig,
56
+ nextConfig: { basePath, i18n },
57
+ });
43
58
  await functions_1.setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
44
59
  await redirects_1.generateRedirects({
45
60
  netlifyConfig,
46
61
  nextConfig: { basePath, i18n, trailingSlash, appDir },
62
+ buildId,
47
63
  });
48
64
  },
49
- async onPostBuild({ netlifyConfig, utils: { cache, functions, build: { failBuild }, }, constants: { FUNCTIONS_DIST }, }) {
50
- await cache_1.saveCache({ cache, publish: netlifyConfig.build.publish });
65
+ async onPostBuild({ netlifyConfig: { build: { publish }, redirects, }, utils: { status, cache, functions, build: { failBuild }, }, constants: { FUNCTIONS_DIST }, }) {
66
+ await cache_1.saveCache({ cache, publish });
67
+ if (utils_1.shouldSkip()) {
68
+ status.show({
69
+ title: 'Essential Next.js plugin did not run',
70
+ summary: `Next cache was stored, but all other functions were skipped because ${process.env.NETLIFY_NEXT_PLUGIN_SKIP
71
+ ? `NETLIFY_NEXT_PLUGIN_SKIP is set`
72
+ : `NEXT_PLUGIN_FORCE_RUN is set to ${process.env.NEXT_PLUGIN_FORCE_RUN}`}`,
73
+ });
74
+ return;
75
+ }
51
76
  await verification_1.checkForOldFunctions({ functions });
52
77
  await verification_1.checkZipSize(path_1.join(FUNCTIONS_DIST, `${constants_1.ODB_FUNCTION_NAME}.zip`));
53
- const { basePath } = await config_1.getNextConfig({ publish: netlifyConfig.build.publish, failBuild });
78
+ const { basePath } = await config_1.getNextConfig({ publish, failBuild });
79
+ verification_1.warnForProblematicUserRewrites({ basePath, redirects });
54
80
  await files_1.unpatchNextFiles(basePath);
55
81
  },
56
- onEnd() {
57
- verification_1.logBetaMessage();
58
- },
59
82
  };
60
83
  module.exports = plugin;
@@ -12,17 +12,19 @@ const makeHandler = () =>
12
12
  // We return a function and then call `toString()` on it to serialise it as the launcher function
13
13
  // eslint-disable-next-line max-params
14
14
  (conf, app, pageRoot, staticManifest = [], mode = 'ssr') => {
15
+ var _a;
15
16
  // This is just so nft knows about the page entrypoints. It's not actually used
16
17
  try {
17
18
  // eslint-disable-next-line node/no-missing-require
18
19
  require.resolve('./pages.js');
19
20
  }
20
21
  catch { }
21
- // eslint-disable-next-line no-underscore-dangle
22
- process.env._BYPASS_SSG = 'true';
23
22
  const ONE_YEAR_IN_SECONDS = 31536000;
23
+ (_a = process.env).NODE_ENV || (_a.NODE_ENV = 'production');
24
24
  // We don't want to write ISR files to disk in the lambda environment
25
25
  conf.experimental.isrFlushToDisk = false;
26
+ // eslint-disable-next-line no-underscore-dangle
27
+ process.env._BYPASS_SSG = 'true';
26
28
  // Set during the request as it needs the host header. Hoisted so we can define the function once
27
29
  let base;
28
30
  augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base });
@@ -69,13 +71,12 @@ const makeHandler = () =>
69
71
  const cacheHeader = (_c = multiValueHeaders['cache-control']) === null || _c === void 0 ? void 0 : _c[0];
70
72
  if (cacheHeader === null || cacheHeader === void 0 ? void 0 : cacheHeader.includes('stale-while-revalidate')) {
71
73
  if (requestMode === 'odb') {
72
- requestMode = 'isr';
73
74
  const ttl = getMaxAge(cacheHeader);
74
- // Long-expiry TTL is basically no TTL
75
+ // Long-expiry TTL is basically no TTL, so we'll skip it
75
76
  if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) {
76
77
  result.ttl = ttl;
78
+ requestMode = 'isr';
77
79
  }
78
- multiValueHeaders['x-rendered-at'] = [new Date().toISOString()];
79
80
  }
80
81
  multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
81
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.0.0-rc.2",
3
+ "version": "4.1.2",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -74,8 +74,8 @@
74
74
  "@babel/core": "^7.15.8",
75
75
  "@babel/preset-env": "^7.15.8",
76
76
  "@babel/preset-typescript": "^7.16.0",
77
- "@netlify/build": "^20.2.0",
78
- "@netlify/eslint-config-node": "^4.0.0",
77
+ "@netlify/build": "^26.1.0",
78
+ "@netlify/eslint-config-node": "^4.1.0",
79
79
  "@testing-library/cypress": "^8.0.1",
80
80
  "@types/fs-extra": "^9.0.13",
81
81
  "@types/jest": "^27.0.2",