@netlify/plugin-nextjs 4.0.0-beta.9 → 4.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/constants.js CHANGED
@@ -1,8 +1,11 @@
1
- const HANDLER_FUNCTION_NAME = '___netlify-handler';
2
- const ODB_FUNCTION_NAME = '___netlify-odb-handler';
3
- const IMAGE_FUNCTION_NAME = '_ipx';
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DYNAMIC_PARAMETER_REGEX = exports.OPTIONAL_CATCH_ALL_REGEX = exports.CATCH_ALL_REGEX = exports.DEFAULT_FUNCTIONS_SRC = exports.HANDLER_FUNCTION_PATH = exports.ODB_FUNCTION_PATH = exports.HIDDEN_PATHS = exports.IMAGE_FUNCTION_NAME = exports.ODB_FUNCTION_NAME = exports.HANDLER_FUNCTION_NAME = void 0;
4
+ exports.HANDLER_FUNCTION_NAME = '___netlify-handler';
5
+ exports.ODB_FUNCTION_NAME = '___netlify-odb-handler';
6
+ exports.IMAGE_FUNCTION_NAME = '_ipx';
4
7
  // These are paths in .next that shouldn't be publicly accessible
5
- const HIDDEN_PATHS = [
8
+ exports.HIDDEN_PATHS = [
6
9
  '/cache/*',
7
10
  '/server/*',
8
11
  '/serverless/*',
@@ -13,9 +16,9 @@ const HIDDEN_PATHS = [
13
16
  '/react-loadable-manifest.json',
14
17
  '/BUILD_ID',
15
18
  ];
16
- module.exports = {
17
- HIDDEN_PATHS,
18
- IMAGE_FUNCTION_NAME,
19
- HANDLER_FUNCTION_NAME,
20
- ODB_FUNCTION_NAME,
21
- };
19
+ exports.ODB_FUNCTION_PATH = `/.netlify/builders/${exports.ODB_FUNCTION_NAME}`;
20
+ exports.HANDLER_FUNCTION_PATH = `/.netlify/functions/${exports.HANDLER_FUNCTION_NAME}`;
21
+ exports.DEFAULT_FUNCTIONS_SRC = 'netlify/functions';
22
+ exports.CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
23
+ exports.OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
24
+ exports.DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
@@ -1,6 +1,9 @@
1
- const { posix: { join }, } = require('path');
2
- exports.restoreCache = async ({ cache, publish }) => {
3
- const cacheDir = join(publish, 'cache');
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveCache = exports.restoreCache = void 0;
4
+ const path_1 = require("path");
5
+ const restoreCache = async ({ cache, publish }) => {
6
+ const cacheDir = path_1.posix.join(publish, 'cache');
4
7
  if (await cache.restore(cacheDir)) {
5
8
  console.log('Next.js cache restored.');
6
9
  }
@@ -8,9 +11,10 @@ exports.restoreCache = async ({ cache, publish }) => {
8
11
  console.log('No Next.js cache to restore.');
9
12
  }
10
13
  };
11
- exports.saveCache = async ({ cache, publish }) => {
12
- const cacheDir = join(publish, 'cache');
13
- const buildManifest = join(publish, 'build-manifest.json');
14
+ exports.restoreCache = restoreCache;
15
+ const saveCache = async ({ cache, publish }) => {
16
+ const cacheDir = path_1.posix.join(publish, 'cache');
17
+ const buildManifest = path_1.posix.join(publish, 'build-manifest.json');
14
18
  if (await cache.save(cacheDir, { digests: [buildManifest] })) {
15
19
  console.log('Next.js cache saved.');
16
20
  }
@@ -18,3 +22,4 @@ exports.saveCache = async ({ cache, publish }) => {
18
22
  console.log('No Next.js cache to save.');
19
23
  }
20
24
  };
25
+ exports.saveCache = saveCache;
@@ -1,81 +1,22 @@
1
- // @ts-check
2
- const { readJSON, existsSync } = require('fs-extra');
3
- const { join, dirname, relative } = require('pathe');
4
- const slash = require('slash');
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureHandlerFunctions = exports.getNextConfig = void 0;
7
+ const fs_extra_1 = require("fs-extra");
8
+ const pathe_1 = require("pathe");
9
+ const slash_1 = __importDefault(require("slash"));
10
+ const constants_1 = require("../constants");
5
11
  const defaultFailBuild = (message, { error }) => {
6
12
  throw new Error(`${message}\n${error && error.stack}`);
7
13
  };
8
- const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, HIDDEN_PATHS } = require('../constants');
9
- const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}`;
10
- const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`;
11
- const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
12
- const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
13
- const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
14
- const getNetlifyRoutes = (nextRoute) => {
15
- let netlifyRoutes = [nextRoute];
16
- // If the route is an optional catch-all route, we need to add a second
17
- // Netlify route for the base path (when no parameters are present).
18
- // The file ending must be present!
19
- if (OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
20
- let netlifyRoute = nextRoute.replace(OPTIONAL_CATCH_ALL_REGEX, '$2');
21
- // When optional catch-all route is at top-level, the regex on line 19 will
22
- // create an empty string, but actually needs to be a forward slash
23
- if (netlifyRoute === '')
24
- netlifyRoute = '/';
25
- // When optional catch-all route is at top-level, the regex on line 19 will
26
- // create an incorrect route for the data route. For example, it creates
27
- // /_next/data/%BUILDID%.json, but NextJS looks for
28
- // /_next/data/%BUILDID%/index.json
29
- netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json');
30
- // Add second route to the front of the array
31
- netlifyRoutes.unshift(netlifyRoute);
32
- }
33
- // Replace catch-all, e.g., [...slug]
34
- netlifyRoutes = netlifyRoutes.map((route) => route.replace(CATCH_ALL_REGEX, '/:$1/*'));
35
- // Replace optional catch-all, e.g., [[...slug]]
36
- netlifyRoutes = netlifyRoutes.map((route) => route.replace(OPTIONAL_CATCH_ALL_REGEX, '/*'));
37
- // Replace dynamic parameters, e.g., [id]
38
- netlifyRoutes = netlifyRoutes.map((route) => route.replace(DYNAMIC_PARAMETER_REGEX, '/:$1'));
39
- return netlifyRoutes;
40
- };
41
- exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
42
- const { dynamicRoutes } = await readJSON(join(netlifyConfig.build.publish, 'prerender-manifest.json'));
43
- const redirects = [];
44
- netlifyConfig.redirects.push(...HIDDEN_PATHS.map((path) => ({
45
- from: `${basePath}${path}`,
46
- to: '/404.html',
47
- status: 404,
48
- force: true,
49
- })));
50
- const dynamicRouteEntries = Object.entries(dynamicRoutes);
51
- dynamicRouteEntries.sort((a, b) => a[0].localeCompare(b[0]));
52
- dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
53
- // Add redirects if fallback is "null" (aka blocking) or true/a string
54
- if (fallback === false) {
55
- return;
56
- }
57
- redirects.push(...getNetlifyRoutes(route), ...getNetlifyRoutes(dataRoute));
58
- });
59
- if (i18n) {
60
- netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
61
- }
62
- // This is only used in prod, so dev uses `next dev` directly
63
- netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 }, {
64
- from: `${basePath}/*`,
65
- to: HANDLER_FUNCTION_PATH,
66
- status: 200,
67
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
68
- force: true,
69
- }, ...redirects.map((redirect) => ({
70
- from: `${basePath}${redirect}`,
71
- to: ODB_FUNCTION_PATH,
72
- status: 200,
73
- })), { from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 });
74
- };
75
- exports.getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
14
+ const getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild, }) {
76
15
  try {
77
- const { config, appDir, ignore } = await readJSON(join(publish, 'required-server-files.json'));
16
+ const { config, appDir, ignore } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(publish, 'required-server-files.json'));
78
17
  if (!config) {
18
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
19
+ // @ts-ignore
79
20
  return failBuild('Error loading your Next config');
80
21
  }
81
22
  return { ...config, appDir, ignore };
@@ -84,26 +25,27 @@ exports.getNextConfig = async function getNextConfig({ publish, failBuild = defa
84
25
  return failBuild('Error loading your Next config', { error });
85
26
  }
86
27
  };
28
+ exports.getNextConfig = getNextConfig;
87
29
  const resolveModuleRoot = (moduleName) => {
88
30
  try {
89
- return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })));
31
+ return (0, pathe_1.dirname)((0, pathe_1.relative)(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })));
90
32
  }
91
33
  catch (error) {
92
34
  return null;
93
35
  }
94
36
  };
95
37
  const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'];
96
- exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
38
+ const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
97
39
  var _a;
98
40
  /* eslint-disable no-underscore-dangle */
99
41
  (_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
100
42
  netlifyConfig.functions._ipx.node_bundler = 'nft';
101
- [HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
43
+ [constants_1.HANDLER_FUNCTION_NAME, constants_1.ODB_FUNCTION_NAME].forEach((functionName) => {
102
44
  var _a, _b;
103
45
  (_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
104
46
  netlifyConfig.functions[functionName].node_bundler = 'nft';
105
47
  (_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
106
- netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, `!${publish}/server/**/*.js.nft.json`, ...ignore.map((path) => `!${slash(path)}`));
48
+ netlifyConfig.functions[functionName].included_files.push('.env', '.env.local', '.env.production', '.env.production.local', `${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, `!${publish}/server/**/*.js.nft.json`, ...ignore.map((path) => `!${(0, slash_1.default)(path)}`));
107
49
  const nextRoot = resolveModuleRoot('next');
108
50
  if (nextRoot) {
109
51
  netlifyConfig.functions[functionName].included_files.push(`!${nextRoot}/dist/server/lib/squoosh/**/*.wasm`, `!${nextRoot}/dist/next-server/server/lib/squoosh/**/*.wasm`, `!${nextRoot}/dist/compiled/webpack/bundle4.js`, `!${nextRoot}/dist/compiled/webpack/bundle5.js`, `!${nextRoot}/dist/compiled/terser/bundle.min.js`);
@@ -116,3 +58,4 @@ exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) =>
116
58
  });
117
59
  });
118
60
  };
61
+ exports.configureHandlerFunctions = configureHandlerFunctions;
@@ -1,7 +1,7 @@
1
- // @ts-check
1
+ /* eslint-disable max-lines */
2
2
  const { cpus } = require('os');
3
3
  const { yellowBright } = require('chalk');
4
- const { existsSync, readJson, move, cpSync, copy, writeJson } = require('fs-extra');
4
+ const { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir, readFileSync, } = require('fs-extra');
5
5
  const globby = require('globby');
6
6
  const { outdent } = require('outdent');
7
7
  const pLimit = require('p-limit');
@@ -18,13 +18,40 @@ const stripLocale = (rawPath, locales = []) => {
18
18
  };
19
19
  const matchMiddleware = (middleware, filePath) => (middleware === null || middleware === void 0 ? void 0 : middleware.includes('')) ||
20
20
  (middleware === null || middleware === void 0 ? void 0 : middleware.find((middlewarePath) => filePath === middlewarePath || filePath === `${middlewarePath}.html` || filePath.startsWith(`${middlewarePath}/`)));
21
+ const matchesRedirect = (file, redirects) => {
22
+ if (!Array.isArray(redirects)) {
23
+ return false;
24
+ }
25
+ return redirects.some((redirect) => {
26
+ if (!redirect.regex || redirect.internal) {
27
+ return false;
28
+ }
29
+ // Strips the extension from the file path
30
+ return new RegExp(redirect.regex).test(`/${file.slice(0, -5)}`);
31
+ });
32
+ };
33
+ const matchesRewrite = (file, rewrites) => {
34
+ if (Array.isArray(rewrites)) {
35
+ return matchesRedirect(file, rewrites);
36
+ }
37
+ if (!Array.isArray(rewrites === null || rewrites === void 0 ? void 0 : rewrites.beforeFiles)) {
38
+ return false;
39
+ }
40
+ return matchesRedirect(file, rewrites.beforeFiles);
41
+ };
42
+ exports.matchesRedirect = matchesRedirect;
43
+ exports.matchesRewrite = matchesRewrite;
21
44
  exports.matchMiddleware = matchMiddleware;
22
45
  exports.stripLocale = stripLocale;
23
46
  exports.isDynamicRoute = isDynamicRoute;
47
+ // eslint-disable-next-line max-lines-per-function
24
48
  exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
25
49
  console.log('Moving static page files to serve from CDN...');
26
50
  const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
27
51
  const root = join(outputDir, 'pages');
52
+ const buildId = readFileSync(join(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8').trim();
53
+ const dataDir = join('_next', 'data', buildId);
54
+ await ensureDir(dataDir);
28
55
  // Load the middleware manifest so we can check if a file matches it before moving
29
56
  let middleware;
30
57
  const manifestPath = join(outputDir, 'middleware-manifest.json');
@@ -34,12 +61,32 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
34
61
  middleware = Object.keys(manifest.middleware).map((path) => path.slice(1));
35
62
  }
36
63
  }
64
+ const prerenderManifest = await readJson(join(netlifyConfig.build.publish, 'prerender-manifest.json'));
65
+ const { redirects, rewrites } = await readJson(join(netlifyConfig.build.publish, 'routes-manifest.json'));
66
+ const isrFiles = new Set();
67
+ Object.entries(prerenderManifest.routes).forEach(([route, { initialRevalidateSeconds }]) => {
68
+ if (initialRevalidateSeconds) {
69
+ // Find all files used by ISR routes
70
+ const trimmedPath = route.slice(1);
71
+ isrFiles.add(`${trimmedPath}.html`);
72
+ isrFiles.add(`${trimmedPath}.json`);
73
+ }
74
+ });
37
75
  const files = [];
76
+ const filesManifest = {};
38
77
  const moveFile = async (file) => {
78
+ const isData = file.endsWith('.json');
39
79
  const source = join(root, file);
80
+ const target = isData ? join(dataDir, file) : file;
40
81
  files.push(file);
41
- const dest = join(netlifyConfig.build.publish, file);
42
- await move(source, dest);
82
+ filesManifest[file] = target;
83
+ const dest = join(netlifyConfig.build.publish, target);
84
+ try {
85
+ await move(source, dest);
86
+ }
87
+ catch (error) {
88
+ console.warn('Error moving file', source, error);
89
+ }
43
90
  };
44
91
  // Move all static files, except error documents and nft manifests
45
92
  const pages = await globby(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
@@ -48,13 +95,27 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
48
95
  });
49
96
  const matchingMiddleware = new Set();
50
97
  const matchedPages = new Set();
98
+ const matchedRedirects = new Set();
99
+ const matchedRewrites = new Set();
51
100
  // Limit concurrent file moves to number of cpus or 2 if there is only 1
52
101
  const limit = pLimit(Math.max(2, cpus().length));
53
102
  const promises = pages.map(async (rawPath) => {
54
103
  const filePath = slash(rawPath);
104
+ // Don't move ISR files, as they're used for the first request
105
+ if (isrFiles.has(filePath)) {
106
+ return;
107
+ }
55
108
  if (isDynamicRoute(filePath)) {
56
109
  return;
57
110
  }
111
+ if (matchesRedirect(filePath, redirects)) {
112
+ matchedRedirects.add(filePath);
113
+ return;
114
+ }
115
+ if (matchesRewrite(filePath, rewrites)) {
116
+ matchedRewrites.add(filePath);
117
+ return;
118
+ }
58
119
  // Middleware matches against the unlocalised path
59
120
  const unlocalizedPath = stripLocale(rawPath, i18n === null || i18n === void 0 ? void 0 : i18n.locales);
60
121
  const middlewarePath = matchMiddleware(middleware, unlocalizedPath);
@@ -70,12 +131,15 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
70
131
  console.log(`Moved ${files.length} files`);
71
132
  if (matchedPages.size !== 0) {
72
133
  console.log(yellowBright(outdent `
73
- Skipped moving ${matchedPages.size} ${matchedPages.size === 1 ? 'file because it matches' : 'files because they match'} middleware, so cannot be deployed to the CDN and will be served from the origin instead. This is fine, but we're letting you know because it may not be what you expect.
134
+ Skipped moving ${matchedPages.size} ${matchedPages.size === 1 ? 'file because it matches' : 'files because they match'} middleware, so cannot be deployed to the CDN and will be served from the origin instead.
135
+ This is fine, but we're letting you know because it may not be what you expect.
74
136
  `));
75
137
  console.log(outdent `
76
138
  The following middleware matched statically-rendered pages:
77
139
 
78
140
  ${yellowBright([...matchingMiddleware].map((mid) => `- /${mid}/_middleware`).join('\n'))}
141
+
142
+ ────────────────────────────────────────────────────────────────
79
143
  `);
80
144
  // There could potentially be thousands of matching pages, so we don't want to spam the console with this
81
145
  if (matchedPages.size < 50) {
@@ -83,22 +147,112 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
83
147
  The following files matched middleware and were not moved to the CDN:
84
148
 
85
149
  ${yellowBright([...matchedPages].map((mid) => `- ${mid}`).join('\n'))}
150
+
151
+ ────────────────────────────────────────────────────────────────
152
+ `);
153
+ }
154
+ }
155
+ if (matchedRedirects.size !== 0 || matchedRewrites.size !== 0) {
156
+ console.log(yellowBright(outdent `
157
+ Skipped moving ${matchedRedirects.size + matchedRewrites.size} files because they match redirects or beforeFiles rewrites, so cannot be deployed to the CDN and will be served from the origin instead.
158
+ `));
159
+ if (matchedRedirects.size < 50 && matchedRedirects.size !== 0) {
160
+ console.log(outdent `
161
+ The following files matched redirects and were not moved to the CDN:
162
+
163
+ ${yellowBright([...matchedRedirects].map((mid) => `- ${mid}`).join('\n'))}
164
+
165
+ ────────────────────────────────────────────────────────────────
166
+ `);
167
+ }
168
+ if (matchedRewrites.size < 50 && matchedRewrites.size !== 0) {
169
+ console.log(outdent `
170
+ The following files matched beforeFiles rewrites and were not moved to the CDN:
171
+
172
+ ${yellowBright([...matchedRewrites].map((mid) => `- ${mid}`).join('\n'))}
173
+
174
+ ────────────────────────────────────────────────────────────────
86
175
  `);
87
176
  }
88
177
  }
89
178
  // Write the manifest for use in the serverless functions
90
- await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), files);
179
+ await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), Object.entries(filesManifest));
91
180
  if (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) {
92
181
  // Copy the default locale into the root
93
182
  const defaultLocaleDir = join(netlifyConfig.build.publish, i18n.defaultLocale);
94
183
  if (existsSync(defaultLocaleDir)) {
95
184
  await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`);
96
185
  }
186
+ const defaultLocaleIndex = join(netlifyConfig.build.publish, `${i18n.defaultLocale}.html`);
187
+ const indexHtml = join(netlifyConfig.build.publish, 'index.html');
188
+ if (existsSync(defaultLocaleIndex) && !existsSync(indexHtml)) {
189
+ try {
190
+ await copy(defaultLocaleIndex, indexHtml, { overwrite: false });
191
+ await copy(join(netlifyConfig.build.publish, `${i18n.defaultLocale}.json`), join(netlifyConfig.build.publish, 'index.json'), { overwrite: false });
192
+ }
193
+ catch { }
194
+ }
195
+ }
196
+ };
197
+ const patchFile = async ({ file, from, to }) => {
198
+ if (!existsSync(file)) {
199
+ return;
200
+ }
201
+ const content = await readFile(file, 'utf8');
202
+ if (content.includes(to)) {
203
+ return;
204
+ }
205
+ const newContent = content.replace(from, to);
206
+ await writeFile(`${file}.orig`, content);
207
+ await writeFile(file, newContent);
208
+ };
209
+ const getServerFile = (root) => {
210
+ let serverFile;
211
+ try {
212
+ serverFile = require.resolve('next/dist/server/next-server', { paths: [root] });
213
+ }
214
+ catch {
215
+ // Ignore
216
+ }
217
+ if (!serverFile) {
218
+ try {
219
+ // eslint-disable-next-line node/no-missing-require
220
+ serverFile = require.resolve('next/dist/next-server/server/next-server', { paths: [root] });
221
+ }
222
+ catch {
223
+ // Ignore
224
+ }
225
+ }
226
+ return serverFile;
227
+ };
228
+ exports.patchNextFiles = async (root) => {
229
+ const serverFile = getServerFile(root);
230
+ console.log(`Patching ${serverFile}`);
231
+ if (serverFile) {
232
+ await patchFile({
233
+ file: serverFile,
234
+ from: `let ssgCacheKey = `,
235
+ to: `let ssgCacheKey = process.env._BYPASS_SSG || `,
236
+ });
237
+ }
238
+ };
239
+ exports.unpatchNextFiles = async (root) => {
240
+ const serverFile = getServerFile(root);
241
+ const origFile = `${serverFile}.orig`;
242
+ if (existsSync(origFile)) {
243
+ await move(origFile, serverFile, { overwrite: true });
97
244
  }
98
245
  };
99
- exports.movePublicFiles = async ({ appDir, publish }) => {
100
- const publicDir = join(appDir, 'public');
246
+ exports.movePublicFiles = async ({ appDir, outdir, publish }) => {
247
+ // `outdir` is a config property added when using Next.js with Nx. It's typically
248
+ // a relative path outside of the appDir, e.g. '../../dist/apps/<app-name>', and
249
+ // the parent directory of the .next directory.
250
+ // If it exists, copy the files from the public folder there in order to include
251
+ // any files that were generated during the build. Otherwise, copy the public
252
+ // directory from the original app directory.
253
+ const publicDir = outdir ? join(appDir, outdir, 'public') : join(appDir, 'public');
101
254
  if (existsSync(publicDir)) {
102
255
  await copy(publicDir, `${publish}/`);
103
256
  }
104
257
  };
258
+ /* eslint-enable max-lines */
@@ -1,56 +1,61 @@
1
- const { copyFile, ensureDir, writeFile, writeJSON } = require('fs-extra');
2
- const { join, relative } = require('pathe');
3
- const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../constants');
4
- const getHandler = require('../templates/getHandler');
5
- const { getPageResolver } = require('../templates/getPageResolver');
6
- const DEFAULT_FUNCTIONS_SRC = 'netlify/functions';
7
- exports.generateFunctions = async ({ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir) => {
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupImageFunction = exports.generatePagesResolver = exports.generateFunctions = void 0;
4
+ const fs_extra_1 = require("fs-extra");
5
+ const pathe_1 = require("pathe");
6
+ const constants_1 = require("../constants");
7
+ const getHandler_1 = require("../templates/getHandler");
8
+ const getPageResolver_1 = require("../templates/getPageResolver");
9
+ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir) => {
8
10
  const functionsDir = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
9
11
  const bridgeFile = require.resolve('@vercel/node/dist/bridge');
10
- const functionDir = join(process.cwd(), functionsDir, HANDLER_FUNCTION_NAME);
11
- const publishDir = relative(functionDir, join(process.cwd(), PUBLISH_DIR));
12
+ const functionDir = (0, pathe_1.join)(process.cwd(), functionsDir, constants_1.HANDLER_FUNCTION_NAME);
13
+ const publishDir = (0, pathe_1.relative)(functionDir, (0, pathe_1.join)(process.cwd(), PUBLISH_DIR));
12
14
  const writeHandler = async (func, isODB) => {
13
- const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) });
14
- await ensureDir(join(functionsDir, func));
15
- await writeFile(join(functionsDir, func, `${func}.js`), handlerSource);
16
- await copyFile(bridgeFile, join(functionsDir, func, 'bridge.js'));
15
+ const handlerSource = await (0, getHandler_1.getHandler)({ isODB, publishDir, appDir: (0, pathe_1.relative)(functionDir, appDir) });
16
+ await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, func));
17
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, func, `${func}.js`), handlerSource);
18
+ await (0, fs_extra_1.copyFile)(bridgeFile, (0, pathe_1.join)(functionsDir, func, 'bridge.js'));
19
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, func, 'handlerUtils.js'));
17
20
  };
18
- await writeHandler(HANDLER_FUNCTION_NAME, false);
19
- await writeHandler(ODB_FUNCTION_NAME, true);
21
+ await writeHandler(constants_1.HANDLER_FUNCTION_NAME, false);
22
+ await writeHandler(constants_1.ODB_FUNCTION_NAME, true);
20
23
  };
24
+ exports.generateFunctions = generateFunctions;
21
25
  /**
22
26
  * Writes a file in each function directory that contains references to every page entrypoint.
23
27
  * This is just so that the nft bundler knows about them. We'll eventually do this better.
24
28
  */
25
- exports.generatePagesResolver = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC }, netlifyConfig, target, }) => {
29
+ const generatePagesResolver = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC }, netlifyConfig, target, }) => {
26
30
  const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
27
- const jsSource = await getPageResolver({
31
+ const jsSource = await (0, getPageResolver_1.getPageResolver)({
28
32
  netlifyConfig,
29
33
  target,
30
34
  });
31
- await writeFile(join(functionsPath, ODB_FUNCTION_NAME, 'pages.js'), jsSource);
32
- await writeFile(join(functionsPath, HANDLER_FUNCTION_NAME, 'pages.js'), jsSource);
35
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsPath, constants_1.ODB_FUNCTION_NAME, 'pages.js'), jsSource);
36
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsPath, constants_1.HANDLER_FUNCTION_NAME, 'pages.js'), jsSource);
33
37
  };
38
+ exports.generatePagesResolver = generatePagesResolver;
34
39
  // Move our next/image function into the correct functions directory
35
- exports.setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC }, imageconfig = {}, netlifyConfig, basePath, }) => {
40
+ const setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC }, imageconfig = {}, netlifyConfig, basePath, }) => {
36
41
  const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
37
- const functionName = `${IMAGE_FUNCTION_NAME}.js`;
38
- const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME);
39
- await ensureDir(functionDirectory);
40
- await writeJSON(join(functionDirectory, 'imageconfig.json'), {
42
+ const functionName = `${constants_1.IMAGE_FUNCTION_NAME}.js`;
43
+ const functionDirectory = (0, pathe_1.join)(functionsPath, constants_1.IMAGE_FUNCTION_NAME);
44
+ await (0, fs_extra_1.ensureDir)(functionDirectory);
45
+ await (0, fs_extra_1.writeJSON)((0, pathe_1.join)(functionDirectory, 'imageconfig.json'), {
41
46
  ...imageconfig,
42
- basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'),
47
+ basePath: [basePath, constants_1.IMAGE_FUNCTION_NAME].join('/'),
43
48
  });
44
- await copyFile(join(__dirname, '..', 'templates', 'ipx.js'), join(functionDirectory, functionName));
49
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), (0, pathe_1.join)(functionDirectory, functionName));
45
50
  const imagePath = imageconfig.path || '/_next/image';
46
51
  netlifyConfig.redirects.push({
47
52
  from: `${imagePath}*`,
48
53
  query: { url: ':url', w: ':width', q: ':quality' },
49
- to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
54
+ to: `${basePath}/${constants_1.IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
50
55
  status: 301,
51
56
  }, {
52
- from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,
53
- to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`,
57
+ from: `${basePath}/${constants_1.IMAGE_FUNCTION_NAME}/*`,
58
+ to: `/.netlify/builders/${constants_1.IMAGE_FUNCTION_NAME}`,
54
59
  status: 200,
55
60
  });
56
61
  if (basePath) {
@@ -62,3 +67,4 @@ exports.setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCT
62
67
  });
63
68
  }
64
69
  };
70
+ exports.setupImageFunction = setupImageFunction;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRedirects = void 0;
4
+ const chalk_1 = require("chalk");
5
+ const fs_extra_1 = require("fs-extra");
6
+ const outdent_1 = require("outdent");
7
+ const pathe_1 = require("pathe");
8
+ const constants_1 = require("../constants");
9
+ const utils_1 = require("./utils");
10
+ const generateLocaleRedirects = ({ i18n, basePath, trailingSlash, }) => {
11
+ const redirects = [];
12
+ // If the cookie is set, we need to redirect at the origin
13
+ redirects.push({
14
+ from: `${basePath}/`,
15
+ to: constants_1.HANDLER_FUNCTION_PATH,
16
+ status: 200,
17
+ force: true,
18
+ conditions: {
19
+ Cookie: ['NEXT_LOCALE'],
20
+ },
21
+ });
22
+ i18n.locales.forEach((locale) => {
23
+ if (locale === i18n.defaultLocale) {
24
+ return;
25
+ }
26
+ redirects.push({
27
+ from: `${basePath}/`,
28
+ to: `${basePath}/${locale}${trailingSlash ? '/' : ''}`,
29
+ status: 301,
30
+ conditions: {
31
+ Language: [locale],
32
+ },
33
+ force: true,
34
+ });
35
+ });
36
+ return redirects;
37
+ };
38
+ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash }, }) => {
39
+ const { dynamicRoutes, routes: staticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'prerender-manifest.json'));
40
+ netlifyConfig.redirects.push(...constants_1.HIDDEN_PATHS.map((path) => ({
41
+ from: `${basePath}${path}`,
42
+ to: '/404.html',
43
+ status: 404,
44
+ force: true,
45
+ })));
46
+ if (i18n && i18n.localeDetection !== false) {
47
+ netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }));
48
+ }
49
+ const dataRedirects = [];
50
+ const pageRedirects = [];
51
+ const isrRedirects = [];
52
+ let hasIsr = false;
53
+ const dynamicRouteEntries = Object.entries(dynamicRoutes);
54
+ const staticRouteEntries = Object.entries(staticRoutes);
55
+ staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
56
+ // Only look for revalidate as we need to rewrite these to SSR rather than ODB
57
+ if (initialRevalidateSeconds === false) {
58
+ // These can be ignored, as they're static files handled by the CDN
59
+ return;
60
+ }
61
+ if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && route.startsWith(`/${i18n.defaultLocale}/`)) {
62
+ route = route.slice(i18n.defaultLocale.length + 1);
63
+ }
64
+ hasIsr = true;
65
+ isrRedirects.push(...(0, utils_1.netlifyRoutesForNextRoute)(dataRoute), ...(0, utils_1.netlifyRoutesForNextRoute)(route));
66
+ });
67
+ dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
68
+ // Add redirects if fallback is "null" (aka blocking) or true/a string
69
+ if (fallback === false) {
70
+ return;
71
+ }
72
+ pageRedirects.push(...(0, utils_1.netlifyRoutesForNextRoute)(route));
73
+ dataRedirects.push(...(0, utils_1.netlifyRoutesForNextRoute)(dataRoute));
74
+ });
75
+ if (i18n) {
76
+ netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
77
+ }
78
+ // This is only used in prod, so dev uses `next dev` directly
79
+ netlifyConfig.redirects.push(
80
+ // Static files are in `static`
81
+ { from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
82
+ // API routes always need to be served from the regular function
83
+ {
84
+ from: `${basePath}/api`,
85
+ to: constants_1.HANDLER_FUNCTION_PATH,
86
+ status: 200,
87
+ }, {
88
+ from: `${basePath}/api/*`,
89
+ to: constants_1.HANDLER_FUNCTION_PATH,
90
+ status: 200,
91
+ },
92
+ // Preview mode gets forced to the function, to bypess pre-rendered pages
93
+ {
94
+ from: `${basePath}/*`,
95
+ to: constants_1.HANDLER_FUNCTION_PATH,
96
+ status: 200,
97
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
98
+ // @ts-ignore The conditions type is incorrect
99
+ conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
100
+ force: true,
101
+ },
102
+ // ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
103
+ ...isrRedirects.map((redirect) => ({
104
+ from: `${basePath}${redirect}`,
105
+ to: process.env.EXPERIMENTAL_ODB_TTL ? constants_1.ODB_FUNCTION_PATH : constants_1.HANDLER_FUNCTION_PATH,
106
+ status: 200,
107
+ force: true,
108
+ })),
109
+ // These are pages with fallback set, which need an ODB
110
+ // Data redirects go first, to avoid conflict with splat redirects
111
+ ...dataRedirects.map((redirect) => ({
112
+ from: `${basePath}${redirect}`,
113
+ to: constants_1.ODB_FUNCTION_PATH,
114
+ status: 200,
115
+ })),
116
+ // ...then all the other fallback pages
117
+ ...pageRedirects.map((redirect) => ({
118
+ from: `${basePath}${redirect}`,
119
+ to: constants_1.ODB_FUNCTION_PATH,
120
+ status: 200,
121
+ })),
122
+ // Everything else is handled by the regular function
123
+ { from: `${basePath}/*`, to: constants_1.HANDLER_FUNCTION_PATH, status: 200 });
124
+ if (hasIsr) {
125
+ console.log((0, chalk_1.yellowBright)((0, outdent_1.outdent) `
126
+ You have some pages that use ISR (pages that use getStaticProps with revalidate set), which is not currently fully-supported by this plugin. Be aware that results may be unreliable.
127
+ `));
128
+ }
129
+ };
130
+ exports.generateRedirects = generateRedirects;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.netlifyRoutesForNextRoute = void 0;
4
+ const constants_1 = require("../constants");
5
+ const netlifyRoutesForNextRoute = (nextRoute) => {
6
+ const netlifyRoutes = [nextRoute];
7
+ // If the route is an optional catch-all route, we need to add a second
8
+ // Netlify route for the base path (when no parameters are present).
9
+ // The file ending must be present!
10
+ if (constants_1.OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
11
+ let netlifyRoute = nextRoute.replace(constants_1.OPTIONAL_CATCH_ALL_REGEX, '$2');
12
+ // create an empty string, but actually needs to be a forward slash
13
+ if (netlifyRoute === '') {
14
+ netlifyRoute = '/';
15
+ }
16
+ // When optional catch-all route is at top-level, the regex on line 19 will
17
+ // create an incorrect route for the data route. For example, it creates
18
+ // /_next/data/%BUILDID%.json, but NextJS looks for
19
+ // /_next/data/%BUILDID%/index.json
20
+ netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json');
21
+ // Add second route to the front of the array
22
+ netlifyRoutes.unshift(netlifyRoute);
23
+ }
24
+ return netlifyRoutes.map((route) => route
25
+ // Replace catch-all, e.g., [...slug]
26
+ .replace(constants_1.CATCH_ALL_REGEX, '/:$1/*')
27
+ // Replace optional catch-all, e.g., [[...slug]]
28
+ .replace(constants_1.OPTIONAL_CATCH_ALL_REGEX, '/*')
29
+ // Replace dynamic parameters, e.g., [id]
30
+ .replace(constants_1.DYNAMIC_PARAMETER_REGEX, '/:$1'));
31
+ };
32
+ exports.netlifyRoutesForNextRoute = netlifyRoutesForNextRoute;
@@ -18,7 +18,8 @@ exports.verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuil
18
18
  }
19
19
  };
20
20
  exports.checkForOldFunctions = async ({ functions }) => {
21
- const oldFunctions = (await functions.list()).filter(({ name }) => name.startsWith('next_'));
21
+ const allOldFunctions = await functions.list();
22
+ const oldFunctions = allOldFunctions.filter(({ name }) => name.startsWith('next_'));
22
23
  if (oldFunctions.length !== 0) {
23
24
  console.log(yellowBright(outdent `
24
25
  We have found the following functions in your site that seem to be left over from the old Next.js plugin (v3). We have guessed this because the name starts with "next_".
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const { join, relative } = require('path');
2
2
  const { ODB_FUNCTION_NAME } = require('./constants');
3
3
  const { restoreCache, saveCache } = require('./helpers/cache');
4
- const { getNextConfig, configureHandlerFunctions, generateRedirects } = require('./helpers/config');
5
- const { moveStaticPages, movePublicFiles } = require('./helpers/files');
4
+ const { getNextConfig, configureHandlerFunctions } = require('./helpers/config');
5
+ const { moveStaticPages, movePublicFiles, patchNextFiles, unpatchNextFiles } = require('./helpers/files');
6
6
  const { generateFunctions, setupImageFunction, generatePagesResolver } = require('./helpers/functions');
7
+ const { generateRedirects } = require('./helpers/redirects');
7
8
  const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, checkForOldFunctions, } = require('./helpers/verification');
8
9
  /** @type import("@netlify/build").NetlifyPlugin */
9
10
  module.exports = {
@@ -21,11 +22,17 @@ module.exports = {
21
22
  async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
22
23
  const { publish } = netlifyConfig.build;
23
24
  checkNextSiteHasBuilt({ publish, failBuild });
24
- const { appDir, basePath, i18n, images, target, ignore } = await getNextConfig({ publish, failBuild });
25
+ const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir } = await getNextConfig({
26
+ publish,
27
+ failBuild,
28
+ });
25
29
  configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) });
26
30
  await generateFunctions(constants, appDir);
27
31
  await generatePagesResolver({ netlifyConfig, target, constants });
28
- await movePublicFiles({ appDir, publish });
32
+ await movePublicFiles({ appDir, outdir, publish });
33
+ if (process.env.EXPERIMENTAL_ODB_TTL) {
34
+ await patchNextFiles(basePath);
35
+ }
29
36
  if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
30
37
  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'");
31
38
  }
@@ -35,14 +42,15 @@ module.exports = {
35
42
  await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
36
43
  await generateRedirects({
37
44
  netlifyConfig,
38
- basePath,
39
- i18n,
45
+ nextConfig: { basePath, i18n, trailingSlash },
40
46
  });
41
47
  },
42
- async onPostBuild({ netlifyConfig, utils: { cache, functions }, constants: { FUNCTIONS_DIST } }) {
48
+ async onPostBuild({ netlifyConfig, utils: { cache, functions, failBuild }, constants: { FUNCTIONS_DIST } }) {
43
49
  await saveCache({ cache, publish: netlifyConfig.build.publish });
44
50
  await checkForOldFunctions({ functions });
45
51
  await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`));
52
+ const { basePath } = await getNextConfig({ publish: netlifyConfig.build.publish, failBuild });
53
+ await unpatchNextFiles(basePath);
46
54
  },
47
55
  onEnd() {
48
56
  logBetaMessage();
@@ -1,89 +1,32 @@
1
- const { promises, createWriteStream, existsSync } = require('fs');
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getHandler = void 0;
4
+ const { promises } = require('fs');
2
5
  const { Server } = require('http');
3
- const { tmpdir } = require('os');
4
6
  const path = require('path');
5
- const { promisify } = require('util');
6
- const streamPipeline = promisify(require('stream').pipeline);
7
+ // eslint-disable-next-line node/prefer-global/url, node/prefer-global/url-search-params
8
+ const { URLSearchParams, URL } = require('url');
7
9
  const { Bridge } = require('@vercel/node/dist/bridge');
8
- const fetch = require('node-fetch');
10
+ const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils');
9
11
  const makeHandler = () =>
10
12
  // We return a function and then call `toString()` on it to serialise it as the launcher function
11
- (conf, app, pageRoot, staticManifest = []) => {
13
+ // eslint-disable-next-line max-params
14
+ (conf, app, pageRoot, staticManifest = [], mode = 'ssr') => {
12
15
  // This is just so nft knows about the page entrypoints. It's not actually used
13
16
  try {
14
17
  // eslint-disable-next-line node/no-missing-require
15
18
  require.resolve('./pages.js');
16
19
  }
17
20
  catch { }
21
+ // eslint-disable-next-line no-underscore-dangle
22
+ process.env._BYPASS_SSG = 'true';
23
+ const ONE_YEAR_IN_SECONDS = 31536000;
24
+ // We don't want to write ISR files to disk in the lambda environment
25
+ conf.experimental.isrFlushToDisk = false;
18
26
  // Set during the request as it needs the host header. Hoisted so we can define the function once
19
27
  let base;
20
- // Only do this if we have some static files moved to the CDN
21
- if (staticManifest.length !== 0) {
22
- // These are static page files that have been removed from the function bundle
23
- // In most cases these are served from the CDN, but for rewrites Next may try to read them
24
- // from disk. We need to intercept these and load them from the CDN instead
25
- // Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know.
26
- const staticFiles = new Set(staticManifest);
27
- // Yes, you can cache stuff locally in a Lambda
28
- const cacheDir = path.join(tmpdir(), 'next-static-cache');
29
- // Grab the real fs.promises.readFile...
30
- const readfileOrig = promises.readFile;
31
- // ...then money-patch it to see if it's requesting a CDN file
32
- promises.readFile = async (file, options) => {
33
- // We only care about page files
34
- if (file.startsWith(pageRoot)) {
35
- // We only want the part after `pages/`
36
- const filePath = file.slice(pageRoot.length + 1);
37
- // Is it in the CDN and not local?
38
- if (staticFiles.has(filePath) && !existsSync(file)) {
39
- // This name is safe to use, because it's one that was already created by Next
40
- const cacheFile = path.join(cacheDir, filePath);
41
- // Have we already cached it? We ignore the cache if running locally to avoid staleness
42
- if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) {
43
- await promises.mkdir(path.dirname(cacheFile), { recursive: true });
44
- // Append the path to our host and we can load it like a regular page
45
- const url = `${base}/${filePath}`;
46
- console.log(`Downloading ${url} to ${cacheFile}`);
47
- const response = await fetch(url);
48
- if (!response.ok) {
49
- // Next catches this and returns it as a not found file
50
- throw new Error(`Failed to fetch ${url}`);
51
- }
52
- // Stream it to disk
53
- await streamPipeline(response.body, createWriteStream(cacheFile));
54
- }
55
- // Return the cache file
56
- return readfileOrig(cacheFile, options);
57
- }
58
- }
59
- return readfileOrig(file, options);
60
- };
61
- }
62
- let NextServer;
63
- try {
64
- // next >= 11.0.1. Yay breaking changes in patch releases!
65
- NextServer = require('next/dist/server/next-server').default;
66
- }
67
- catch (error) {
68
- if (!error.message.includes("Cannot find module 'next/dist/server/next-server'")) {
69
- // A different error, so rethrow it
70
- throw error;
71
- }
72
- // Probably an old version of next
73
- }
74
- if (!NextServer) {
75
- try {
76
- // next < 11.0.1
77
- // eslint-disable-next-line node/no-missing-require, import/no-unresolved
78
- NextServer = require('next/dist/next-server/server/next-server').default;
79
- }
80
- catch (error) {
81
- if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) {
82
- throw error;
83
- }
84
- throw new Error('Could not find Next.js server');
85
- }
86
- }
28
+ augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base });
29
+ const NextServer = getNextServer();
87
30
  const nextServer = new NextServer({
88
31
  conf,
89
32
  dir: path.resolve(__dirname, app),
@@ -102,7 +45,8 @@ const makeHandler = () =>
102
45
  const bridge = new Bridge(server);
103
46
  bridge.listen();
104
47
  return async (event, context) => {
105
- var _a, _b, _c, _d;
48
+ var _a, _b, _c;
49
+ let requestMode = mode;
106
50
  // Ensure that paths are encoded - but don't double-encode them
107
51
  event.path = new URL(event.path, event.rawUrl).pathname;
108
52
  // Next expects to be able to parse the query from the URL
@@ -115,25 +59,27 @@ const makeHandler = () =>
115
59
  base = `${protocol}://${host}`;
116
60
  }
117
61
  const { headers, ...result } = await bridge.launcher(event, context);
118
- /** @type import("@netlify/functions").HandlerResponse */
119
62
  // Convert all headers to multiValueHeaders
120
- const multiValueHeaders = {};
121
- for (const key of Object.keys(headers)) {
122
- if (Array.isArray(headers[key])) {
123
- multiValueHeaders[key] = headers[key];
124
- }
125
- else {
126
- multiValueHeaders[key] = [headers[key]];
127
- }
128
- }
63
+ const multiValueHeaders = getMultiValueHeaders(headers);
129
64
  if ((_b = (_a = multiValueHeaders['set-cookie']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.includes('__prerender_bypass')) {
130
65
  delete multiValueHeaders.etag;
131
66
  multiValueHeaders['cache-control'] = ['no-cache'];
132
67
  }
133
68
  // Sending SWR headers causes undefined behaviour with the Netlify CDN
134
- if ((_d = (_c = multiValueHeaders['cache-control']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.includes('stale-while-revalidate')) {
69
+ const cacheHeader = (_c = multiValueHeaders['cache-control']) === null || _c === void 0 ? void 0 : _c[0];
70
+ if (cacheHeader === null || cacheHeader === void 0 ? void 0 : cacheHeader.includes('stale-while-revalidate')) {
71
+ if (requestMode === 'odb' && process.env.EXPERIMENTAL_ODB_TTL) {
72
+ requestMode = 'isr';
73
+ const ttl = getMaxAge(cacheHeader);
74
+ // Long-expiry TTL is basically no TTL
75
+ if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) {
76
+ result.ttl = ttl;
77
+ }
78
+ multiValueHeaders['x-rendered-at'] = [new Date().toISOString()];
79
+ }
135
80
  multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
136
81
  }
82
+ multiValueHeaders['x-render-mode'] = [requestMode];
137
83
  return {
138
84
  ...result,
139
85
  multiValueHeaders,
@@ -143,13 +89,10 @@ const makeHandler = () =>
143
89
  };
144
90
  const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) => `
145
91
  const { Server } = require("http");
146
- const { tmpdir } = require('os')
147
- const { promises, createWriteStream, existsSync } = require("fs");
148
- const { promisify } = require('util')
149
- const streamPipeline = promisify(require('stream').pipeline)
92
+ const { promises } = require("fs");
150
93
  // We copy the file here rather than requiring from the node module
151
94
  const { Bridge } = require("./bridge");
152
- const fetch = require('node-fetch')
95
+ const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils')
153
96
 
154
97
  const { builder } = require("@netlify/functions");
155
98
  const { config } = require("${publishDir}/required-server-files.json")
@@ -160,7 +103,8 @@ try {
160
103
  const path = require("path");
161
104
  const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages"));
162
105
  exports.handler = ${isODB
163
- ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest));`
164
- : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest);`}
106
+ ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));`
107
+ : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`}
165
108
  `;
166
- module.exports = getHandler;
109
+ exports.getHandler = getHandler;
110
+ /* eslint-enable @typescript-eslint/no-var-requires */
@@ -1,19 +1,27 @@
1
- const { posix: { resolve, join, relative }, } = require('path');
2
- const { outdent } = require('outdent');
3
- const slash = require('slash');
4
- const glob = require('tiny-glob');
5
- const { HANDLER_FUNCTION_NAME } = require('../constants');
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPageResolver = void 0;
7
+ const path_1 = require("path");
8
+ const outdent_1 = require("outdent");
9
+ const slash_1 = __importDefault(require("slash"));
10
+ const tiny_glob_1 = __importDefault(require("tiny-glob"));
11
+ const constants_1 = require("../constants");
6
12
  // Generate a file full of require.resolve() calls for all the pages in the
7
13
  // build. This is used by the nft bundler to find all the pages.
8
- exports.getPageResolver = async ({ netlifyConfig, target }) => {
9
- const functionDir = resolve(join('.netlify', 'functions', HANDLER_FUNCTION_NAME));
10
- const root = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless', 'pages');
11
- const pages = await glob('**/*.js', {
14
+ const getPageResolver = async ({ netlifyConfig, target }) => {
15
+ const functionDir = path_1.posix.resolve(path_1.posix.join('.netlify', 'functions', constants_1.HANDLER_FUNCTION_NAME));
16
+ const root = path_1.posix.join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless', 'pages');
17
+ const pages = await (0, tiny_glob_1.default)('**/*.js', {
12
18
  cwd: root,
13
19
  dot: true,
14
20
  });
15
- const pageFiles = pages.map((page) => `require.resolve('${relative(functionDir, join(root, slash(page)))}')`).sort();
16
- return outdent `
21
+ const pageFiles = pages
22
+ .map((page) => `require.resolve('${path_1.posix.relative(functionDir, path_1.posix.join(root, (0, slash_1.default)(page)))}')`)
23
+ .sort();
24
+ return (0, outdent_1.outdent) `
17
25
  // This file is purely to allow nft to know about these pages. It should be temporary.
18
26
  exports.resolvePages = () => {
19
27
  try {
@@ -22,3 +30,4 @@ exports.getPageResolver = async ({ netlifyConfig, target }) => {
22
30
  }
23
31
  `;
24
32
  };
33
+ exports.getPageResolver = getPageResolver;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getNextServer = exports.augmentFsModule = exports.getMultiValueHeaders = exports.getMaxAge = exports.downloadFile = void 0;
7
+ const fs_1 = require("fs");
8
+ const http_1 = __importDefault(require("http"));
9
+ const https_1 = __importDefault(require("https"));
10
+ const os_1 = require("os");
11
+ const path_1 = __importDefault(require("path"));
12
+ const stream_1 = require("stream");
13
+ const util_1 = require("util");
14
+ const streamPipeline = (0, util_1.promisify)(stream_1.pipeline);
15
+ const downloadFile = async (url, destination) => {
16
+ console.log(`Downloading ${url} to ${destination}`);
17
+ const httpx = url.startsWith('https') ? https_1.default : http_1.default;
18
+ await new Promise((resolve, reject) => {
19
+ const req = httpx.get(url, { timeout: 10000 }, (response) => {
20
+ if (response.statusCode < 200 || response.statusCode > 299) {
21
+ reject(new Error(`Failed to download ${url}: ${response.statusCode} ${response.statusMessage || ''}`));
22
+ return;
23
+ }
24
+ const fileStream = (0, fs_1.createWriteStream)(destination);
25
+ streamPipeline(response, fileStream)
26
+ .then(resolve)
27
+ .catch((error) => {
28
+ console.log(`Error downloading ${url}`, error);
29
+ reject(error);
30
+ });
31
+ });
32
+ req.on('error', (error) => {
33
+ console.log(`Error downloading ${url}`, error);
34
+ reject(error);
35
+ });
36
+ });
37
+ };
38
+ exports.downloadFile = downloadFile;
39
+ const getMaxAge = (header) => {
40
+ const parts = header.split(',');
41
+ let maxAge;
42
+ for (const part of parts) {
43
+ const [key, value] = part.split('=');
44
+ if ((key === null || key === void 0 ? void 0 : key.trim()) === 's-maxage') {
45
+ maxAge = value === null || value === void 0 ? void 0 : value.trim();
46
+ }
47
+ }
48
+ if (maxAge) {
49
+ const result = Number.parseInt(maxAge);
50
+ return Number.isNaN(result) ? 0 : result;
51
+ }
52
+ return 0;
53
+ };
54
+ exports.getMaxAge = getMaxAge;
55
+ const getMultiValueHeaders = (headers) => {
56
+ const multiValueHeaders = {};
57
+ for (const key of Object.keys(headers)) {
58
+ const header = headers[key];
59
+ if (Array.isArray(header)) {
60
+ multiValueHeaders[key] = header;
61
+ }
62
+ else {
63
+ multiValueHeaders[key] = [header];
64
+ }
65
+ }
66
+ return multiValueHeaders;
67
+ };
68
+ exports.getMultiValueHeaders = getMultiValueHeaders;
69
+ const augmentFsModule = ({ promises, staticManifest, pageRoot, getBase, }) => {
70
+ // Only do this if we have some static files moved to the CDN
71
+ if (staticManifest.length === 0) {
72
+ return;
73
+ }
74
+ // These are static page files that have been removed from the function bundle
75
+ // In most cases these are served from the CDN, but for rewrites Next may try to read them
76
+ // from disk. We need to intercept these and load them from the CDN instead
77
+ // Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know.
78
+ const staticFiles = new Map(staticManifest);
79
+ const downloadPromises = new Map();
80
+ // Yes, you can cache stuff locally in a Lambda
81
+ const cacheDir = path_1.default.join((0, os_1.tmpdir)(), 'next-static-cache');
82
+ // Grab the real fs.promises.readFile...
83
+ const readfileOrig = promises.readFile;
84
+ const statsOrig = promises.stat;
85
+ // ...then money-patch it to see if it's requesting a CDN file
86
+ promises.readFile = (async (file, options) => {
87
+ const base = getBase();
88
+ // We only care about page files
89
+ if (file.startsWith(pageRoot)) {
90
+ // We only want the part after `pages/`
91
+ const filePath = file.slice(pageRoot.length + 1);
92
+ // Is it in the CDN and not local?
93
+ if (staticFiles.has(filePath) && !(0, fs_1.existsSync)(file)) {
94
+ // This name is safe to use, because it's one that was already created by Next
95
+ const cacheFile = path_1.default.join(cacheDir, filePath);
96
+ const url = `${base}/${staticFiles.get(filePath)}`;
97
+ // If it's already downloading we can wait for it to finish
98
+ if (downloadPromises.has(url)) {
99
+ await downloadPromises.get(url);
100
+ }
101
+ // Have we already cached it? We download every time if running locally to avoid staleness
102
+ if ((!(0, fs_1.existsSync)(cacheFile) || process.env.NETLIFY_DEV) && base) {
103
+ await promises.mkdir(path_1.default.dirname(cacheFile), { recursive: true });
104
+ try {
105
+ // Append the path to our host and we can load it like a regular page
106
+ const downloadPromise = (0, exports.downloadFile)(url, cacheFile);
107
+ downloadPromises.set(url, downloadPromise);
108
+ await downloadPromise;
109
+ }
110
+ finally {
111
+ downloadPromises.delete(url);
112
+ }
113
+ }
114
+ // Return the cache file
115
+ return readfileOrig(cacheFile, options);
116
+ }
117
+ }
118
+ return readfileOrig(file, options);
119
+ });
120
+ promises.stat = (async (file, options) => {
121
+ // We only care about page files
122
+ if (file.startsWith(pageRoot)) {
123
+ // We only want the part after `pages/`
124
+ const cacheFile = path_1.default.join(cacheDir, file.slice(pageRoot.length + 1));
125
+ if ((0, fs_1.existsSync)(cacheFile)) {
126
+ return statsOrig(cacheFile, options);
127
+ }
128
+ }
129
+ return statsOrig(file, options);
130
+ });
131
+ };
132
+ exports.augmentFsModule = augmentFsModule;
133
+ const getNextServer = () => {
134
+ let NextServer;
135
+ try {
136
+ // next >= 11.0.1. Yay breaking changes in patch releases!
137
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
138
+ NextServer = require('next/dist/server/next-server').default;
139
+ }
140
+ catch (error) {
141
+ if (!error.message.includes("Cannot find module 'next/dist/server/next-server'")) {
142
+ // A different error, so rethrow it
143
+ throw error;
144
+ }
145
+ // Probably an old version of next
146
+ }
147
+ if (!NextServer) {
148
+ try {
149
+ // next < 11.0.1
150
+ // eslint-disable-next-line node/no-missing-require, import/no-unresolved, @typescript-eslint/no-var-requires
151
+ NextServer = require('next/dist/next-server/server/next-server').default;
152
+ }
153
+ catch (error) {
154
+ if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) {
155
+ throw error;
156
+ }
157
+ throw new Error('Could not find Next.js server');
158
+ }
159
+ }
160
+ return NextServer;
161
+ };
162
+ exports.getNextServer = getNextServer;
@@ -1,8 +1,12 @@
1
- const { createIPXHandler } = require('@netlify/ipx');
2
- // Injected at build time
3
- // eslint-disable-next-line import/no-unresolved, node/no-missing-require
4
- const { basePath, domains } = require('./imageconfig.json');
5
- exports.handler = createIPXHandler({
6
- basePath,
7
- domains,
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = void 0;
4
+ /* eslint-disable node/no-missing-import, import/no-unresolved, @typescript-eslint/ban-ts-comment */
5
+ const ipx_1 = require("@netlify/ipx");
6
+ // @ts-ignore Injected at build time
7
+ const imageconfig_json_1 = require("./imageconfig.json");
8
+ exports.handler = (0, ipx_1.createIPXHandler)({
9
+ basePath: imageconfig_json_1.basePath,
10
+ domains: imageconfig_json_1.domains,
8
11
  });
12
+ /* eslint-enable node/no-missing-import, import/no-unresolved, @typescript-eslint/ban-ts-comment */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.0.0-beta.9",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -8,10 +8,10 @@
8
8
  "manifest.yml"
9
9
  ],
10
10
  "scripts": {
11
- "build:demo": "next build demo",
11
+ "build:demo": "next build demos/default",
12
12
  "cy:open": "cypress open --config-file cypress/config/all.json",
13
- "cy:run": "cypress run --config-file ../cypress/config/ci.json",
14
- "dev:demo": "next dev demo",
13
+ "cy:run": "cypress run --config-file ../../cypress/config/ci.json",
14
+ "dev:demo": "next dev demos/default",
15
15
  "format": "run-s format:check-fix:*",
16
16
  "format:ci": "run-s format:check:*",
17
17
  "format:check-fix:lint": "run-e format:check:lint format:fix:lint",
@@ -26,6 +26,8 @@
26
26
  "publish:test": "npm test",
27
27
  "test": "run-s build build:demo test:jest",
28
28
  "test:jest": "jest",
29
+ "test:jest:update": "jest --updateSnapshot",
30
+ "test:update": "run-s build build:demo test:jest:update",
29
31
  "prepare": "npm run build",
30
32
  "clean": "rimraf lib",
31
33
  "build": "tsc",
@@ -52,7 +54,7 @@
52
54
  },
53
55
  "homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
54
56
  "dependencies": {
55
- "@netlify/functions": "^0.9.0",
57
+ "@netlify/functions": "^0.10.0",
56
58
  "@netlify/ipx": "^0.0.7",
57
59
  "@vercel/node": "^1.11.2-canary.4",
58
60
  "chalk": "^4.1.2",
@@ -72,16 +74,17 @@
72
74
  "devDependencies": {
73
75
  "@babel/core": "^7.15.8",
74
76
  "@babel/preset-env": "^7.15.8",
75
- "@netlify/build": "^18.25.1",
76
- "@netlify/eslint-config-node": "^3.3.7",
77
+ "@babel/preset-typescript": "^7.16.0",
78
+ "@netlify/build": "^20.0.4",
79
+ "@netlify/eslint-config-node": "^3.3.11",
77
80
  "@testing-library/cypress": "^8.0.1",
78
81
  "@types/fs-extra": "^9.0.13",
79
82
  "@types/jest": "^27.0.2",
80
83
  "@types/mocha": "^9.0.0",
81
84
  "babel-jest": "^27.2.5",
82
85
  "cpy": "^8.1.2",
83
- "cypress": "^8.5.0",
84
- "eslint-config-next": "^11.0.0",
86
+ "cypress": "^9.0.0",
87
+ "eslint-config-next": "^12.0.0",
85
88
  "husky": "^4.3.0",
86
89
  "jest": "^27.0.0",
87
90
  "netlify-plugin-cypress": "^2.2.0",