@netlify/plugin-nextjs 4.0.0-beta.12 → 4.0.0-beta.13
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 +12 -10
- package/lib/helpers/cache.js +11 -6
- package/lib/helpers/config.js +20 -133
- package/lib/helpers/files.js +19 -3
- package/lib/helpers/redirects.js +130 -0
- package/lib/helpers/requiredServerFilesType.js +8 -0
- package/lib/helpers/utils.js +32 -0
- package/lib/index.js +7 -4
- package/lib/templates/getHandler.js +32 -5
- package/package.json +3 -3
package/lib/constants.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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.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
|
-
|
|
8
|
+
exports.HIDDEN_PATHS = [
|
|
6
9
|
'/cache/*',
|
|
7
10
|
'/server/*',
|
|
8
11
|
'/serverless/*',
|
|
@@ -13,9 +16,8 @@ const HIDDEN_PATHS = [
|
|
|
13
16
|
'/react-loadable-manifest.json',
|
|
14
17
|
'/BUILD_ID',
|
|
15
18
|
];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
|
|
22
|
+
exports.OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
|
|
23
|
+
exports.DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
|
package/lib/helpers/cache.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
exports
|
|
3
|
-
|
|
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.
|
|
12
|
-
|
|
13
|
-
const
|
|
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;
|
package/lib/helpers/config.js
CHANGED
|
@@ -1,136 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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");
|
|
7
11
|
const defaultFailBuild = (message, { error }) => {
|
|
8
12
|
throw new Error(`${message}\n${error && error.stack}`);
|
|
9
13
|
};
|
|
10
|
-
const {
|
|
11
|
-
const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}`;
|
|
12
|
-
const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`;
|
|
13
|
-
const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
|
|
14
|
-
const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
|
|
15
|
-
const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
|
|
16
|
-
const getNetlifyRoutes = (nextRoute) => {
|
|
17
|
-
let netlifyRoutes = [nextRoute];
|
|
18
|
-
// If the route is an optional catch-all route, we need to add a second
|
|
19
|
-
// Netlify route for the base path (when no parameters are present).
|
|
20
|
-
// The file ending must be present!
|
|
21
|
-
if (OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
|
|
22
|
-
let netlifyRoute = nextRoute.replace(OPTIONAL_CATCH_ALL_REGEX, '$2');
|
|
23
|
-
// When optional catch-all route is at top-level, the regex on line 19 will
|
|
24
|
-
// create an empty string, but actually needs to be a forward slash
|
|
25
|
-
if (netlifyRoute === '')
|
|
26
|
-
netlifyRoute = '/';
|
|
27
|
-
// When optional catch-all route is at top-level, the regex on line 19 will
|
|
28
|
-
// create an incorrect route for the data route. For example, it creates
|
|
29
|
-
// /_next/data/%BUILDID%.json, but NextJS looks for
|
|
30
|
-
// /_next/data/%BUILDID%/index.json
|
|
31
|
-
netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json');
|
|
32
|
-
// Add second route to the front of the array
|
|
33
|
-
netlifyRoutes.unshift(netlifyRoute);
|
|
34
|
-
}
|
|
35
|
-
// Replace catch-all, e.g., [...slug]
|
|
36
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(CATCH_ALL_REGEX, '/:$1/*'));
|
|
37
|
-
// Replace optional catch-all, e.g., [[...slug]]
|
|
38
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(OPTIONAL_CATCH_ALL_REGEX, '/*'));
|
|
39
|
-
// Replace dynamic parameters, e.g., [id]
|
|
40
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(DYNAMIC_PARAMETER_REGEX, '/:$1'));
|
|
41
|
-
return netlifyRoutes;
|
|
42
|
-
};
|
|
43
|
-
exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
|
|
44
|
-
const { dynamicRoutes, routes: staticRoutes } = await readJSON(join(netlifyConfig.build.publish, 'prerender-manifest.json'));
|
|
45
|
-
netlifyConfig.redirects.push(...HIDDEN_PATHS.map((path) => ({
|
|
46
|
-
from: `${basePath}${path}`,
|
|
47
|
-
to: '/404.html',
|
|
48
|
-
status: 404,
|
|
49
|
-
force: true,
|
|
50
|
-
})));
|
|
51
|
-
const dataRedirects = [];
|
|
52
|
-
const pageRedirects = [];
|
|
53
|
-
const isrRedirects = [];
|
|
54
|
-
let hasIsr = false;
|
|
55
|
-
const dynamicRouteEntries = Object.entries(dynamicRoutes);
|
|
56
|
-
const staticRouteEntries = Object.entries(staticRoutes);
|
|
57
|
-
staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
|
|
58
|
-
// Only look for revalidate as we need to rewrite these to SSR rather than ODB
|
|
59
|
-
if (initialRevalidateSeconds === false) {
|
|
60
|
-
// These can be ignored, as they're static files handled by the CDN
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if ((i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) && route.startsWith(`/${i18n.defaultLocale}/`)) {
|
|
64
|
-
route = route.slice(i18n.defaultLocale.length + 1);
|
|
65
|
-
}
|
|
66
|
-
hasIsr = true;
|
|
67
|
-
isrRedirects.push(...getNetlifyRoutes(dataRoute), ...getNetlifyRoutes(route));
|
|
68
|
-
});
|
|
69
|
-
dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
|
|
70
|
-
// Add redirects if fallback is "null" (aka blocking) or true/a string
|
|
71
|
-
if (fallback === false) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
pageRedirects.push(...getNetlifyRoutes(route));
|
|
75
|
-
dataRedirects.push(...getNetlifyRoutes(dataRoute));
|
|
76
|
-
});
|
|
77
|
-
if (i18n) {
|
|
78
|
-
netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
|
|
79
|
-
}
|
|
80
|
-
// This is only used in prod, so dev uses `next dev` directly
|
|
81
|
-
netlifyConfig.redirects.push(
|
|
82
|
-
// Static files are in `static`
|
|
83
|
-
{ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
|
|
84
|
-
// API routes always need to be served from the regular function
|
|
85
|
-
{
|
|
86
|
-
from: `${basePath}/api`,
|
|
87
|
-
to: HANDLER_FUNCTION_PATH,
|
|
88
|
-
status: 200,
|
|
89
|
-
}, {
|
|
90
|
-
from: `${basePath}/api/*`,
|
|
91
|
-
to: HANDLER_FUNCTION_PATH,
|
|
92
|
-
status: 200,
|
|
93
|
-
},
|
|
94
|
-
// Preview mode gets forced to the function, to bypess pre-rendered pages
|
|
95
|
-
{
|
|
96
|
-
from: `${basePath}/*`,
|
|
97
|
-
to: HANDLER_FUNCTION_PATH,
|
|
98
|
-
status: 200,
|
|
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 ? ODB_FUNCTION_PATH : 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: ODB_FUNCTION_PATH,
|
|
114
|
-
status: 200,
|
|
115
|
-
})),
|
|
116
|
-
// ...then all the other fallback pages
|
|
117
|
-
...pageRedirects.map((redirect) => ({
|
|
118
|
-
from: `${basePath}${redirect}`,
|
|
119
|
-
to: ODB_FUNCTION_PATH,
|
|
120
|
-
status: 200,
|
|
121
|
-
})),
|
|
122
|
-
// Everything else is handled by the regular function
|
|
123
|
-
{ from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 });
|
|
124
|
-
if (hasIsr) {
|
|
125
|
-
console.log(yellowBright(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.getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
|
|
14
|
+
const getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
|
|
131
15
|
try {
|
|
132
|
-
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'));
|
|
133
17
|
if (!config) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
19
|
+
// @ts-ignore
|
|
134
20
|
return failBuild('Error loading your Next config');
|
|
135
21
|
}
|
|
136
22
|
return { ...config, appDir, ignore };
|
|
@@ -139,26 +25,27 @@ exports.getNextConfig = async function getNextConfig({ publish, failBuild = defa
|
|
|
139
25
|
return failBuild('Error loading your Next config', { error });
|
|
140
26
|
}
|
|
141
27
|
};
|
|
28
|
+
exports.getNextConfig = getNextConfig;
|
|
142
29
|
const resolveModuleRoot = (moduleName) => {
|
|
143
30
|
try {
|
|
144
|
-
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()] })));
|
|
145
32
|
}
|
|
146
33
|
catch (error) {
|
|
147
34
|
return null;
|
|
148
35
|
}
|
|
149
36
|
};
|
|
150
37
|
const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'];
|
|
151
|
-
|
|
38
|
+
const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
|
|
152
39
|
var _a;
|
|
153
40
|
/* eslint-disable no-underscore-dangle */
|
|
154
41
|
(_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
|
|
155
42
|
netlifyConfig.functions._ipx.node_bundler = 'nft';
|
|
156
|
-
[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
|
|
43
|
+
[constants_1.HANDLER_FUNCTION_NAME, constants_1.ODB_FUNCTION_NAME].forEach((functionName) => {
|
|
157
44
|
var _a, _b;
|
|
158
45
|
(_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
|
|
159
46
|
netlifyConfig.functions[functionName].node_bundler = 'nft';
|
|
160
47
|
(_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
|
|
161
|
-
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) => `!${
|
|
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)}`));
|
|
162
49
|
const nextRoot = resolveModuleRoot('next');
|
|
163
50
|
if (nextRoot) {
|
|
164
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`);
|
|
@@ -171,4 +58,4 @@ exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) =>
|
|
|
171
58
|
});
|
|
172
59
|
});
|
|
173
60
|
};
|
|
174
|
-
|
|
61
|
+
exports.configureHandlerFunctions = configureHandlerFunctions;
|
package/lib/helpers/files.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
const { cpus } = require('os');
|
|
3
3
|
const { yellowBright } = require('chalk');
|
|
4
|
-
const { existsSync, readJson, move,
|
|
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');
|
|
@@ -49,6 +49,9 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
|
|
|
49
49
|
console.log('Moving static page files to serve from CDN...');
|
|
50
50
|
const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
|
|
51
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);
|
|
52
55
|
// Load the middleware manifest so we can check if a file matches it before moving
|
|
53
56
|
let middleware;
|
|
54
57
|
const manifestPath = join(outputDir, 'middleware-manifest.json');
|
|
@@ -70,10 +73,14 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
|
|
|
70
73
|
}
|
|
71
74
|
});
|
|
72
75
|
const files = [];
|
|
76
|
+
const filesManifest = {};
|
|
73
77
|
const moveFile = async (file) => {
|
|
78
|
+
const isData = file.endsWith('.json');
|
|
74
79
|
const source = join(root, file);
|
|
80
|
+
const target = isData ? join(dataDir, file) : file;
|
|
75
81
|
files.push(file);
|
|
76
|
-
|
|
82
|
+
filesManifest[file] = target;
|
|
83
|
+
const dest = join(netlifyConfig.build.publish, target);
|
|
77
84
|
try {
|
|
78
85
|
await move(source, dest);
|
|
79
86
|
}
|
|
@@ -169,13 +176,22 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
// Write the manifest for use in the serverless functions
|
|
172
|
-
await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'),
|
|
179
|
+
await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), Object.entries(filesManifest));
|
|
173
180
|
if (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) {
|
|
174
181
|
// Copy the default locale into the root
|
|
175
182
|
const defaultLocaleDir = join(netlifyConfig.build.publish, i18n.defaultLocale);
|
|
176
183
|
if (existsSync(defaultLocaleDir)) {
|
|
177
184
|
await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`);
|
|
178
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
|
+
}
|
|
179
195
|
}
|
|
180
196
|
};
|
|
181
197
|
const patchFile = async ({ file, from, to }) => {
|
|
@@ -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}${trailingSlash ? '/' : ''}`,
|
|
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}/`,
|
|
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,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable eslint-comments/disable-enable-pair, @typescript-eslint/no-empty-interface, no-use-before-define */
|
|
3
|
+
/**
|
|
4
|
+
* This was generated using @see {@link https://quicktype.io/}, and using the
|
|
5
|
+
* demo's {@code demos/default/.next/required-server-files.json} file. I was
|
|
6
|
+
* unable to find any types for this file so instead this was manually generated
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -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;
|
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
|
|
4
|
+
const { getNextConfig, configureHandlerFunctions } = require('./helpers/config');
|
|
5
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,7 +22,10 @@ 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({
|
|
25
|
+
const { appDir, basePath, i18n, images, target, ignore, trailingSlash } = 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 });
|
|
@@ -38,8 +42,7 @@ module.exports = {
|
|
|
38
42
|
await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
|
|
39
43
|
await generateRedirects({
|
|
40
44
|
netlifyConfig,
|
|
41
|
-
basePath,
|
|
42
|
-
i18n,
|
|
45
|
+
nextConfig: { basePath, i18n, trailingSlash },
|
|
43
46
|
});
|
|
44
47
|
},
|
|
45
48
|
async onPostBuild({ netlifyConfig, utils: { cache, functions, failBuild }, constants: { FUNCTIONS_DIST } }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines-per-function */
|
|
1
2
|
const { promises, existsSync } = require('fs');
|
|
2
3
|
const { Server } = require('http');
|
|
3
4
|
const { tmpdir } = require('os');
|
|
@@ -27,11 +28,14 @@ const makeHandler = () =>
|
|
|
27
28
|
// In most cases these are served from the CDN, but for rewrites Next may try to read them
|
|
28
29
|
// from disk. We need to intercept these and load them from the CDN instead
|
|
29
30
|
// Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know.
|
|
30
|
-
const staticFiles = new
|
|
31
|
+
const staticFiles = new Map(staticManifest);
|
|
32
|
+
const downloadPromises = new Map();
|
|
33
|
+
const statsCache = new Map();
|
|
31
34
|
// Yes, you can cache stuff locally in a Lambda
|
|
32
35
|
const cacheDir = path.join(tmpdir(), 'next-static-cache');
|
|
33
36
|
// Grab the real fs.promises.readFile...
|
|
34
37
|
const readfileOrig = promises.readFile;
|
|
38
|
+
const statsOrig = promises.stat;
|
|
35
39
|
// ...then money-patch it to see if it's requesting a CDN file
|
|
36
40
|
promises.readFile = async (file, options) => {
|
|
37
41
|
// We only care about page files
|
|
@@ -42,12 +46,23 @@ const makeHandler = () =>
|
|
|
42
46
|
if (staticFiles.has(filePath) && !existsSync(file)) {
|
|
43
47
|
// This name is safe to use, because it's one that was already created by Next
|
|
44
48
|
const cacheFile = path.join(cacheDir, filePath);
|
|
45
|
-
|
|
49
|
+
const url = `${base}/${staticFiles.get(filePath)}`;
|
|
50
|
+
// If it's already downloading we can wait for it to finish
|
|
51
|
+
if (downloadPromises.has(url)) {
|
|
52
|
+
await downloadPromises.get(url);
|
|
53
|
+
}
|
|
54
|
+
// Have we already cached it? We download every time if running locally to avoid staleness
|
|
46
55
|
if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) {
|
|
47
56
|
await promises.mkdir(path.dirname(cacheFile), { recursive: true });
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
try {
|
|
58
|
+
// Append the path to our host and we can load it like a regular page
|
|
59
|
+
const downloadPromise = downloadFile(url, cacheFile);
|
|
60
|
+
downloadPromises.set(url, downloadPromise);
|
|
61
|
+
await downloadPromise;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
downloadPromises.delete(url);
|
|
65
|
+
}
|
|
51
66
|
}
|
|
52
67
|
// Return the cache file
|
|
53
68
|
return readfileOrig(cacheFile, options);
|
|
@@ -55,6 +70,17 @@ const makeHandler = () =>
|
|
|
55
70
|
}
|
|
56
71
|
return readfileOrig(file, options);
|
|
57
72
|
};
|
|
73
|
+
promises.stat = async (file, options) => {
|
|
74
|
+
// We only care about page files
|
|
75
|
+
if (file.startsWith(pageRoot)) {
|
|
76
|
+
// We only want the part after `pages/`
|
|
77
|
+
const cacheFile = path.join(cacheDir, file.slice(pageRoot.length + 1));
|
|
78
|
+
if (existsSync(cacheFile)) {
|
|
79
|
+
return statsOrig(cacheFile, options);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return statsOrig(file, options);
|
|
83
|
+
};
|
|
58
84
|
}
|
|
59
85
|
let NextServer;
|
|
60
86
|
try {
|
|
@@ -163,3 +189,4 @@ exports.handler = ${isODB
|
|
|
163
189
|
: `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`}
|
|
164
190
|
`;
|
|
165
191
|
module.exports = getHandler;
|
|
192
|
+
/* eslint-enable max-lines-per-function */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/plugin-nextjs",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.13",
|
|
4
4
|
"description": "Run Next.js seamlessly on Netlify",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"@babel/core": "^7.15.8",
|
|
76
76
|
"@babel/preset-env": "^7.15.8",
|
|
77
77
|
"@babel/preset-typescript": "^7.16.0",
|
|
78
|
-
"@netlify/build": "^
|
|
79
|
-
"@netlify/eslint-config-node": "^3.3.
|
|
78
|
+
"@netlify/build": "^20.0.4",
|
|
79
|
+
"@netlify/eslint-config-node": "^3.3.11",
|
|
80
80
|
"@testing-library/cypress": "^8.0.1",
|
|
81
81
|
"@types/fs-extra": "^9.0.13",
|
|
82
82
|
"@types/jest": "^27.0.2",
|