@netlify/plugin-nextjs 4.1.0 → 4.2.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/README.md CHANGED
@@ -85,6 +85,14 @@ you can remove it. Alternatively you can
85
85
  support. See [`demos/next-export`](https://github.com/netlify/netlify-plugin-nextjs/tree/main/demos/next-export) for an
86
86
  example.
87
87
 
88
+ ## Generated functions
89
+
90
+ This plugin works by generating three Netlify functions that handle requests that haven't been pre-rendered. These are
91
+ `___netlify-handler` (for SSR and API routes), `___netlify-odb-handler` (for ISR and fallback routes), and `_ipx` (for
92
+ images). You can see the requests for these in [the function logs](https://docs.netlify.com/functions/logs/). For ISR
93
+ and fallback routes you will not see any requests that are served from the edge cache, just actual rendering requests.
94
+ These are all internal functions, so you won't find them in your site's own functions directory.
95
+
88
96
  ## Feedback
89
97
 
90
98
  If you think you have found a bug in the plugin,
@@ -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
  };
@@ -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
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");
@@ -45,8 +41,9 @@ const generateStaticRedirects = ({ netlifyConfig, nextConfig: { i18n, basePath }
45
41
  }
46
42
  };
47
43
  exports.generateStaticRedirects = generateStaticRedirects;
48
- const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, }) => {
49
- const { dynamicRoutes, routes: staticRoutes } = await fs_extra_1.readJSON(pathe_1.join(netlifyConfig.build.publish, 'prerender-manifest.json'));
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'));
50
47
  netlifyConfig.redirects.push(...constants_1.HIDDEN_PATHS.map((path) => ({
51
48
  from: `${basePath}${path}`,
52
49
  to: '/404.html',
@@ -56,78 +53,71 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
56
53
  if (i18n && i18n.localeDetection !== false) {
57
54
  netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }));
58
55
  }
59
- const dataRedirects = [];
60
- const pageRedirects = [];
61
- const isrRedirects = [];
62
- const dynamicRouteEntries = Object.entries(dynamicRoutes);
63
- const staticRouteEntries = Object.entries(staticRoutes);
64
- staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
65
- // 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);
66
70
  if (initialRevalidateSeconds === false) {
67
71
  // These can be ignored, as they're static files handled by the CDN
68
72
  return;
69
73
  }
74
+ // The default locale is served from the root, not the localised path
70
75
  if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && route.startsWith(`/${i18n.defaultLocale}/`)) {
71
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 }));
72
91
  }
73
- isrRedirects.push(...utils_1.netlifyRoutesForNextRoute(dataRoute), ...utils_1.netlifyRoutesForNextRoute(route));
74
92
  });
75
- dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
76
- // Add redirects if fallback is "null" (aka blocking) or true/a string
77
- if (fallback === false) {
93
+ // Add rewrites for all static SSR routes. This is Next 12+
94
+ staticRoutes === null || staticRoutes === void 0 ? void 0 : 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
78
97
  return;
79
98
  }
80
- pageRedirects.push(...utils_1.netlifyRoutesForNextRoute(route));
81
- 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 }));
82
100
  });
83
- const publicFiles = await globby_1.default('**/*', { cwd: pathe_1.join(appDir, 'public') });
84
- // This is only used in prod, so dev uses `next dev` directly
85
- netlifyConfig.redirects.push(
86
- // API routes always need to be served from the regular function
87
- {
88
- from: `${basePath}/api`,
89
- to: constants_1.HANDLER_FUNCTION_PATH,
90
- status: 200,
91
- }, {
92
- from: `${basePath}/api/*`,
93
- to: constants_1.HANDLER_FUNCTION_PATH,
94
- status: 200,
95
- },
96
- // Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
97
- ...publicFiles.map((file) => ({
98
- from: `${basePath}/${file}`,
99
- // This is a no-op, but we do it to stop it matching the following rule
100
- to: `${basePath}/${file}`,
101
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
102
- status: 200,
103
- })), {
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({
104
118
  from: `${basePath}/*`,
105
119
  to: constants_1.HANDLER_FUNCTION_PATH,
106
120
  status: 200,
107
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
108
- force: true,
109
- },
110
- // ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
111
- ...isrRedirects.map((redirect) => ({
112
- from: `${basePath}${redirect}`,
113
- to: constants_1.ODB_FUNCTION_PATH,
114
- status: 200,
115
- force: true,
116
- })),
117
- // These are pages with fallback set, which need an ODB
118
- // Data redirects go first, to avoid conflict with splat redirects
119
- ...dataRedirects.map((redirect) => ({
120
- from: `${basePath}${redirect}`,
121
- to: constants_1.ODB_FUNCTION_PATH,
122
- status: 200,
123
- })),
124
- // ...then all the other fallback pages
125
- ...pageRedirects.map((redirect) => ({
126
- from: `${basePath}${redirect}`,
127
- to: constants_1.ODB_FUNCTION_PATH,
128
- status: 200,
129
- })),
130
- // Everything else is handled by the regular function
131
- { from: `${basePath}/*`, to: constants_1.HANDLER_FUNCTION_PATH, status: 200 });
121
+ });
132
122
  };
133
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.shouldSkip = 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,7 +34,93 @@ 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;
33
124
  const shouldSkip = () => process.env.NEXT_PLUGIN_FORCE_RUN === 'false' ||
34
125
  process.env.NEXT_PLUGIN_FORCE_RUN === '0' ||
35
126
  process.env.NETLIFY_NEXT_PLUGIN_SKIP === 'true' ||
package/lib/index.js CHANGED
@@ -39,13 +39,12 @@ const plugin = {
39
39
  publish,
40
40
  failBuild,
41
41
  });
42
+ const buildId = fs_extra_1.readFileSync(path_1.join(publish, 'BUILD_ID'), 'utf8').trim();
42
43
  config_1.configureHandlerFunctions({ netlifyConfig, ignore, publish: path_1.relative(process.cwd(), publish) });
43
44
  await functions_1.generateFunctions(constants, appDir);
44
45
  await functions_1.generatePagesResolver({ target, constants });
45
46
  await files_1.movePublicFiles({ appDir, outdir, publish });
46
- if (process.env.EXPERIMENTAL_ODB_TTL) {
47
- await files_1.patchNextFiles(basePath);
48
- }
47
+ await files_1.patchNextFiles(basePath);
49
48
  if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
50
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'");
51
50
  }
@@ -60,6 +59,7 @@ const plugin = {
60
59
  await redirects_1.generateRedirects({
61
60
  netlifyConfig,
62
61
  nextConfig: { basePath, i18n, trailingSlash, appDir },
62
+ buildId,
63
63
  });
64
64
  },
65
65
  async onPostBuild({ netlifyConfig: { build: { publish }, redirects, }, utils: { status, cache, functions, build: { failBuild }, }, constants: { FUNCTIONS_DIST }, }) {
@@ -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,17 +71,17 @@ 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
  }
82
83
  multiValueHeaders['x-render-mode'] = [requestMode];
84
+ console.log(`[${event.httpMethod}] ${event.path} (${requestMode === null || requestMode === void 0 ? void 0 : requestMode.toUpperCase()})`);
83
85
  return {
84
86
  ...result,
85
87
  multiValueHeaders,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
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": "^25.0.1",
78
- "@netlify/eslint-config-node": "^4.0.1",
77
+ "@netlify/build": "^26.1.3",
78
+ "@netlify/eslint-config-node": "^4.1.3",
79
79
  "@testing-library/cypress": "^8.0.1",
80
80
  "@types/fs-extra": "^9.0.13",
81
81
  "@types/jest": "^27.0.2",