@netlify/plugin-nextjs 4.0.0-beta.1 → 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/README.md +34 -5
- package/lib/constants.js +12 -13
- package/lib/helpers/cache.js +11 -6
- package/lib/helpers/config.js +43 -77
- package/lib/helpers/files.js +252 -0
- package/lib/helpers/functions.js +4 -3
- package/lib/helpers/redirects.js +130 -0
- package/lib/helpers/requiredServerFilesType.js +8 -0
- package/lib/helpers/utils.js +32 -0
- package/lib/helpers/verification.js +61 -10
- package/lib/index.js +35 -14
- package/lib/templates/getHandler.js +130 -23
- package/lib/templates/handlerUtils.js +66 -0
- package/package.json +20 -10
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# Essential Next.js Build Plugin (beta)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
:warning: This is the beta version of the Essential Next.js plugin. For the stable version, refer to
|
|
6
|
+
[Essential Next.js plugin v3](https://github.com/netlify/netlify-plugin-nextjs/tree/v3#readme) :warning:
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a aria-label="npm version" href="https://www.npmjs.com/package/@netlify/plugin-nextjs">
|
|
@@ -14,15 +14,21 @@
|
|
|
14
14
|
</a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
+
## What's new in this version
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Version 4 is a complete rewrite of the Essential Next.js plugin. For full details of everything that's new, check out
|
|
20
|
+
[the v4 release notes](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/release-notes/v4.md)
|
|
19
21
|
|
|
22
|
+
## Installing the beta
|
|
20
23
|
|
|
21
24
|
- Install the module:
|
|
25
|
+
|
|
22
26
|
```shell
|
|
23
27
|
npm install -D @netlify/plugin-nextjs@beta
|
|
24
28
|
```
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
- Change the `publish` directory to `.next` and add the plugin to `netlify.toml` if not already installed:
|
|
31
|
+
|
|
26
32
|
```toml
|
|
27
33
|
[build]
|
|
28
34
|
publish = ".next"
|
|
@@ -31,4 +37,27 @@ publish = ".next"
|
|
|
31
37
|
package = "@netlify/plugin-nextjs"
|
|
32
38
|
```
|
|
33
39
|
|
|
34
|
-
If you previously set
|
|
40
|
+
If you previously set a custom `distDir` in your `next.config.js`, or set `node_bundler` or `external_node_modules` in
|
|
41
|
+
your `netlify.toml` these are no longer needed and can be removed.
|
|
42
|
+
|
|
43
|
+
The `serverless` and `experimental-serverless-trace` targets are deprecated in Next 12, and all builds with this plugin
|
|
44
|
+
will now use the default `server` target. If you previously set the target in your `next.config.js`, you should remove
|
|
45
|
+
it.
|
|
46
|
+
|
|
47
|
+
If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory,
|
|
48
|
+
which may be in a subdirectory. If you have changed your `distDir` then it will need to match that.
|
|
49
|
+
|
|
50
|
+
If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
|
|
51
|
+
|
|
52
|
+
If you currently use redirects or rewrites on your site, see
|
|
53
|
+
[the Rewrites and Redirects guide](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/redirects-rewrites.md)
|
|
54
|
+
for information on changes to how they are handled in this version.
|
|
55
|
+
|
|
56
|
+
If you want to use Next 12's beta Middleware feature, this will mostly work as expected but please
|
|
57
|
+
[read the docs on some caveats and workarounds](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/middleware.md)
|
|
58
|
+
that are currently needed.
|
|
59
|
+
|
|
60
|
+
## Beta feedback
|
|
61
|
+
|
|
62
|
+
Please share any thoughts, feedback or questions about the beta
|
|
63
|
+
[in our discussion](https://github.com/netlify/netlify-plugin-nextjs/discussions/706).
|
package/lib/constants.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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';
|
|
6
7
|
// These are paths in .next that shouldn't be publicly accessible
|
|
7
|
-
|
|
8
|
+
exports.HIDDEN_PATHS = [
|
|
8
9
|
'/cache/*',
|
|
9
10
|
'/server/*',
|
|
10
11
|
'/serverless/*',
|
|
@@ -15,10 +16,8 @@ const HIDDEN_PATHS = [
|
|
|
15
16
|
'/react-loadable-manifest.json',
|
|
16
17
|
'/BUILD_ID',
|
|
17
18
|
];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ODB_PATH,
|
|
24
|
-
};
|
|
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,95 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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");
|
|
4
11
|
const defaultFailBuild = (message, { error }) => {
|
|
5
12
|
throw new Error(`${message}\n${error && error.stack}`);
|
|
6
13
|
};
|
|
7
|
-
const {
|
|
8
|
-
const ODB_FUNCTION_PATH = `/.netlify/${ODB_PATH}/${ODB_FUNCTION_NAME}`;
|
|
9
|
-
const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`;
|
|
10
|
-
const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
|
|
11
|
-
const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
|
|
12
|
-
const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
|
|
13
|
-
const getNetlifyRoutes = (nextRoute) => {
|
|
14
|
-
let netlifyRoutes = [nextRoute];
|
|
15
|
-
// If the route is an optional catch-all route, we need to add a second
|
|
16
|
-
// Netlify route for the base path (when no parameters are present).
|
|
17
|
-
// The file ending must be present!
|
|
18
|
-
if (OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
|
|
19
|
-
let netlifyRoute = nextRoute.replace(OPTIONAL_CATCH_ALL_REGEX, '$2');
|
|
20
|
-
// When optional catch-all route is at top-level, the regex on line 19 will
|
|
21
|
-
// create an empty string, but actually needs to be a forward slash
|
|
22
|
-
if (netlifyRoute === '')
|
|
23
|
-
netlifyRoute = '/';
|
|
24
|
-
// When optional catch-all route is at top-level, the regex on line 19 will
|
|
25
|
-
// create an incorrect route for the data route. For example, it creates
|
|
26
|
-
// /_next/data/%BUILDID%.json, but NextJS looks for
|
|
27
|
-
// /_next/data/%BUILDID%/index.json
|
|
28
|
-
netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json');
|
|
29
|
-
// Add second route to the front of the array
|
|
30
|
-
netlifyRoutes.unshift(netlifyRoute);
|
|
31
|
-
}
|
|
32
|
-
// Replace catch-all, e.g., [...slug]
|
|
33
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(CATCH_ALL_REGEX, '/:$1/*'));
|
|
34
|
-
// Replace optional catch-all, e.g., [[...slug]]
|
|
35
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(OPTIONAL_CATCH_ALL_REGEX, '/*'));
|
|
36
|
-
// Replace dynamic parameters, e.g., [id]
|
|
37
|
-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(DYNAMIC_PARAMETER_REGEX, '/:$1'));
|
|
38
|
-
return netlifyRoutes;
|
|
39
|
-
};
|
|
40
|
-
exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
|
|
41
|
-
const { dynamicRoutes } = await readJSON(join(netlifyConfig.build.publish, 'prerender-manifest.json'));
|
|
42
|
-
const redirects = [];
|
|
43
|
-
netlifyConfig.redirects.push(...HIDDEN_PATHS.map((path) => ({
|
|
44
|
-
from: `${basePath}${path}`,
|
|
45
|
-
to: '/404.html',
|
|
46
|
-
status: 404,
|
|
47
|
-
force: true,
|
|
48
|
-
})));
|
|
49
|
-
const dynamicRouteEntries = Object.entries(dynamicRoutes);
|
|
50
|
-
dynamicRouteEntries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
51
|
-
dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
|
|
52
|
-
// Add redirects if fallback is "null" (aka blocking) or true/a string
|
|
53
|
-
if (fallback === false) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
redirects.push(...getNetlifyRoutes(route), ...getNetlifyRoutes(dataRoute));
|
|
57
|
-
});
|
|
58
|
-
if (i18n) {
|
|
59
|
-
netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
|
|
60
|
-
}
|
|
61
|
-
// This is only used in prod, so dev uses `next dev` directly
|
|
62
|
-
netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 }, {
|
|
63
|
-
from: `${basePath}/*`,
|
|
64
|
-
to: HANDLER_FUNCTION_PATH,
|
|
65
|
-
status: 200,
|
|
66
|
-
force: true,
|
|
67
|
-
conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
|
|
68
|
-
}, ...redirects.map((redirect) => ({
|
|
69
|
-
from: `${basePath}${redirect}`,
|
|
70
|
-
to: ODB_FUNCTION_PATH,
|
|
71
|
-
status: 200,
|
|
72
|
-
})), { from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 });
|
|
73
|
-
};
|
|
74
|
-
exports.getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
|
|
14
|
+
const getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
|
|
75
15
|
try {
|
|
76
|
-
const { config, appDir } = 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'));
|
|
77
17
|
if (!config) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
19
|
+
// @ts-ignore
|
|
78
20
|
return failBuild('Error loading your Next config');
|
|
79
21
|
}
|
|
80
|
-
return { ...config, appDir };
|
|
22
|
+
return { ...config, appDir, ignore };
|
|
81
23
|
}
|
|
82
24
|
catch (error) {
|
|
83
25
|
return failBuild('Error loading your Next config', { error });
|
|
84
26
|
}
|
|
85
27
|
};
|
|
86
|
-
exports.
|
|
87
|
-
|
|
88
|
-
|
|
28
|
+
exports.getNextConfig = getNextConfig;
|
|
29
|
+
const resolveModuleRoot = (moduleName) => {
|
|
30
|
+
try {
|
|
31
|
+
return (0, pathe_1.dirname)((0, pathe_1.relative)(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'];
|
|
38
|
+
const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
|
|
39
|
+
var _a;
|
|
40
|
+
/* eslint-disable no-underscore-dangle */
|
|
41
|
+
(_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
|
|
42
|
+
netlifyConfig.functions._ipx.node_bundler = 'nft';
|
|
43
|
+
[constants_1.HANDLER_FUNCTION_NAME, constants_1.ODB_FUNCTION_NAME].forEach((functionName) => {
|
|
89
44
|
var _a, _b;
|
|
90
45
|
(_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
|
|
91
46
|
netlifyConfig.functions[functionName].node_bundler = 'nft';
|
|
92
47
|
(_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
|
|
93
|
-
netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`,
|
|
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)}`));
|
|
49
|
+
const nextRoot = resolveModuleRoot('next');
|
|
50
|
+
if (nextRoot) {
|
|
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`);
|
|
52
|
+
}
|
|
53
|
+
DEFAULT_EXCLUDED_MODULES.forEach((moduleName) => {
|
|
54
|
+
const moduleRoot = resolveModuleRoot(moduleName);
|
|
55
|
+
if (moduleRoot) {
|
|
56
|
+
netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
94
59
|
});
|
|
95
60
|
};
|
|
61
|
+
exports.configureHandlerFunctions = configureHandlerFunctions;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
const { cpus } = require('os');
|
|
3
|
+
const { yellowBright } = require('chalk');
|
|
4
|
+
const { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir, readFileSync, } = require('fs-extra');
|
|
5
|
+
const globby = require('globby');
|
|
6
|
+
const { outdent } = require('outdent');
|
|
7
|
+
const pLimit = require('p-limit');
|
|
8
|
+
const { join } = require('pathe');
|
|
9
|
+
const slash = require('slash');
|
|
10
|
+
const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/;
|
|
11
|
+
const isDynamicRoute = (route) => TEST_ROUTE.test(route);
|
|
12
|
+
const stripLocale = (rawPath, locales = []) => {
|
|
13
|
+
const [locale, ...segments] = rawPath.split('/');
|
|
14
|
+
if (locales.includes(locale)) {
|
|
15
|
+
return segments.join('/');
|
|
16
|
+
}
|
|
17
|
+
return rawPath;
|
|
18
|
+
};
|
|
19
|
+
const matchMiddleware = (middleware, filePath) => (middleware === null || middleware === void 0 ? void 0 : middleware.includes('')) ||
|
|
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;
|
|
44
|
+
exports.matchMiddleware = matchMiddleware;
|
|
45
|
+
exports.stripLocale = stripLocale;
|
|
46
|
+
exports.isDynamicRoute = isDynamicRoute;
|
|
47
|
+
// eslint-disable-next-line max-lines-per-function
|
|
48
|
+
exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
|
|
49
|
+
console.log('Moving static page files to serve from CDN...');
|
|
50
|
+
const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
|
|
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);
|
|
55
|
+
// Load the middleware manifest so we can check if a file matches it before moving
|
|
56
|
+
let middleware;
|
|
57
|
+
const manifestPath = join(outputDir, 'middleware-manifest.json');
|
|
58
|
+
if (existsSync(manifestPath)) {
|
|
59
|
+
const manifest = await readJson(manifestPath);
|
|
60
|
+
if (manifest === null || manifest === void 0 ? void 0 : manifest.middleware) {
|
|
61
|
+
middleware = Object.keys(manifest.middleware).map((path) => path.slice(1));
|
|
62
|
+
}
|
|
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
|
+
});
|
|
75
|
+
const files = [];
|
|
76
|
+
const filesManifest = {};
|
|
77
|
+
const moveFile = async (file) => {
|
|
78
|
+
const isData = file.endsWith('.json');
|
|
79
|
+
const source = join(root, file);
|
|
80
|
+
const target = isData ? join(dataDir, file) : file;
|
|
81
|
+
files.push(file);
|
|
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
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// Move all static files, except error documents and nft manifests
|
|
92
|
+
const pages = await globby(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
|
|
93
|
+
cwd: root,
|
|
94
|
+
dot: true,
|
|
95
|
+
});
|
|
96
|
+
const matchingMiddleware = new Set();
|
|
97
|
+
const matchedPages = new Set();
|
|
98
|
+
const matchedRedirects = new Set();
|
|
99
|
+
const matchedRewrites = new Set();
|
|
100
|
+
// Limit concurrent file moves to number of cpus or 2 if there is only 1
|
|
101
|
+
const limit = pLimit(Math.max(2, cpus().length));
|
|
102
|
+
const promises = pages.map(async (rawPath) => {
|
|
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
|
+
}
|
|
108
|
+
if (isDynamicRoute(filePath)) {
|
|
109
|
+
return;
|
|
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
|
+
}
|
|
119
|
+
// Middleware matches against the unlocalised path
|
|
120
|
+
const unlocalizedPath = stripLocale(rawPath, i18n === null || i18n === void 0 ? void 0 : i18n.locales);
|
|
121
|
+
const middlewarePath = matchMiddleware(middleware, unlocalizedPath);
|
|
122
|
+
// If a file matches middleware it can't be offloaded to the CDN, and needs to stay at the origin to be served by next/server
|
|
123
|
+
if (middlewarePath) {
|
|
124
|
+
matchingMiddleware.add(middlewarePath);
|
|
125
|
+
matchedPages.add(rawPath);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
return limit(moveFile, filePath);
|
|
129
|
+
});
|
|
130
|
+
await Promise.all(promises);
|
|
131
|
+
console.log(`Moved ${files.length} files`);
|
|
132
|
+
if (matchedPages.size !== 0) {
|
|
133
|
+
console.log(yellowBright(outdent `
|
|
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.
|
|
136
|
+
`));
|
|
137
|
+
console.log(outdent `
|
|
138
|
+
The following middleware matched statically-rendered pages:
|
|
139
|
+
|
|
140
|
+
${yellowBright([...matchingMiddleware].map((mid) => `- /${mid}/_middleware`).join('\n'))}
|
|
141
|
+
|
|
142
|
+
────────────────────────────────────────────────────────────────
|
|
143
|
+
`);
|
|
144
|
+
// There could potentially be thousands of matching pages, so we don't want to spam the console with this
|
|
145
|
+
if (matchedPages.size < 50) {
|
|
146
|
+
console.log(outdent `
|
|
147
|
+
The following files matched middleware and were not moved to the CDN:
|
|
148
|
+
|
|
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
|
+
────────────────────────────────────────────────────────────────
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Write the manifest for use in the serverless functions
|
|
179
|
+
await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), Object.entries(filesManifest));
|
|
180
|
+
if (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) {
|
|
181
|
+
// Copy the default locale into the root
|
|
182
|
+
const defaultLocaleDir = join(netlifyConfig.build.publish, i18n.defaultLocale);
|
|
183
|
+
if (existsSync(defaultLocaleDir)) {
|
|
184
|
+
await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`);
|
|
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 });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
exports.movePublicFiles = async ({ appDir, publish }) => {
|
|
247
|
+
const publicDir = join(appDir, 'public');
|
|
248
|
+
if (existsSync(publicDir)) {
|
|
249
|
+
await copy(publicDir, `${publish}/`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
/* eslint-enable max-lines */
|
package/lib/helpers/functions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const { join, relative } = require('path');
|
|
2
1
|
const { copyFile, ensureDir, writeFile, writeJSON } = require('fs-extra');
|
|
3
|
-
const {
|
|
2
|
+
const { join, relative } = require('pathe');
|
|
3
|
+
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../constants');
|
|
4
4
|
const getHandler = require('../templates/getHandler');
|
|
5
5
|
const { getPageResolver } = require('../templates/getPageResolver');
|
|
6
6
|
const DEFAULT_FUNCTIONS_SRC = 'netlify/functions';
|
|
@@ -14,6 +14,7 @@ exports.generateFunctions = async ({ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTE
|
|
|
14
14
|
await ensureDir(join(functionsDir, func));
|
|
15
15
|
await writeFile(join(functionsDir, func, `${func}.js`), handlerSource);
|
|
16
16
|
await copyFile(bridgeFile, join(functionsDir, func, 'bridge.js'));
|
|
17
|
+
await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), join(functionsDir, func, 'handlerUtils.js'));
|
|
17
18
|
};
|
|
18
19
|
await writeHandler(HANDLER_FUNCTION_NAME, false);
|
|
19
20
|
await writeHandler(ODB_FUNCTION_NAME, true);
|
|
@@ -50,7 +51,7 @@ exports.setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCT
|
|
|
50
51
|
status: 301,
|
|
51
52
|
}, {
|
|
52
53
|
from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,
|
|
53
|
-
to: `/.netlify/${
|
|
54
|
+
to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`,
|
|
54
55
|
status: 200,
|
|
55
56
|
});
|
|
56
57
|
if (basePath) {
|
|
@@ -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;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
const { existsSync, promises } = require('fs');
|
|
1
2
|
const path = require('path');
|
|
2
|
-
const {
|
|
3
|
-
const {
|
|
3
|
+
const { relative } = require('path');
|
|
4
|
+
const { yellowBright, greenBright, blueBright, redBright, reset } = require('chalk');
|
|
5
|
+
const { async: StreamZip } = require('node-stream-zip');
|
|
4
6
|
const outdent = require('outdent');
|
|
7
|
+
const prettyBytes = require('pretty-bytes');
|
|
5
8
|
const { satisfies } = require('semver');
|
|
6
|
-
exports.verifyBuildTarget = (target) => {
|
|
7
|
-
if (target !== 'server') {
|
|
8
|
-
console.log(yellowBright `Setting target to ${target} is no longer required. You should check if target=server works for you.`);
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
9
|
// This is when nft support was added
|
|
12
10
|
const REQUIRED_BUILD_VERSION = '>=18.16.0';
|
|
13
11
|
exports.verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuild }) => {
|
|
@@ -19,25 +17,78 @@ exports.verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuil
|
|
|
19
17
|
`);
|
|
20
18
|
}
|
|
21
19
|
};
|
|
20
|
+
exports.checkForOldFunctions = async ({ functions }) => {
|
|
21
|
+
const allOldFunctions = await functions.list();
|
|
22
|
+
const oldFunctions = allOldFunctions.filter(({ name }) => name.startsWith('next_'));
|
|
23
|
+
if (oldFunctions.length !== 0) {
|
|
24
|
+
console.log(yellowBright(outdent `
|
|
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_".
|
|
26
|
+
|
|
27
|
+
${reset(oldFunctions.map(({ name }) => `- ${name}`).join('\n'))}
|
|
28
|
+
|
|
29
|
+
If they were created by the old plugin, these functions are likely to cause errors so should be removed. You can do this by deleting the following directories:
|
|
30
|
+
|
|
31
|
+
${reset(oldFunctions.map(({ mainFile }) => `- ${path.relative(process.cwd(), path.dirname(mainFile))}`).join('\n'))}
|
|
32
|
+
`));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
22
35
|
exports.checkNextSiteHasBuilt = ({ publish, failBuild }) => {
|
|
23
36
|
if (!existsSync(path.join(publish, 'BUILD_ID'))) {
|
|
24
37
|
return failBuild(outdent `
|
|
25
38
|
The directory "${path.relative(process.cwd(), publish)}" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory.
|
|
26
39
|
In most cases it should be set to the site's ".next" directory path, unless you have chosen a custom "distDir" in your Next config.
|
|
27
40
|
If you are using "next export" then the Essential Next.js plugin should be removed. See https://ntl.fyi/remove-plugin for details.
|
|
28
|
-
|
|
41
|
+
`);
|
|
29
42
|
}
|
|
30
43
|
if (existsSync(path.join(publish, 'export-detail.json'))) {
|
|
31
44
|
failBuild(outdent `
|
|
32
45
|
Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
|
|
33
46
|
This plugin is not needed for "next export" so should be removed, and publish directory set to "out".
|
|
34
|
-
See https://ntl.fyi/remove-plugin for more details on how to remove this plugin
|
|
47
|
+
See https://ntl.fyi/remove-plugin for more details on how to remove this plugin.
|
|
48
|
+
`);
|
|
35
49
|
}
|
|
36
50
|
};
|
|
37
51
|
exports.checkForRootPublish = ({ publish, failBuild }) => {
|
|
38
52
|
if (path.resolve(publish) === path.resolve('.')) {
|
|
39
53
|
failBuild(outdent `
|
|
40
54
|
Your publish directory is pointing to the base directory of your site. This is not supported for Next.js sites, and is probably a mistake.
|
|
41
|
-
In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config, or the Next site is in a subdirectory
|
|
55
|
+
In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config, or the Next site is in a subdirectory.
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// 50MB, which is the documented max, though the hard max seems to be higher
|
|
60
|
+
const LAMBDA_MAX_SIZE = 1024 * 1024 * 50;
|
|
61
|
+
exports.checkZipSize = async (file, maxSize = LAMBDA_MAX_SIZE) => {
|
|
62
|
+
if (!existsSync(file)) {
|
|
63
|
+
console.warn(`Could not check zip size because ${file} does not exist`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const size = await promises.stat(file).then(({ size }) => size);
|
|
67
|
+
if (size < maxSize) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// We don't fail the build, because the actual hard max size is larger so it might still succeed
|
|
71
|
+
console.log(redBright(outdent `
|
|
72
|
+
The function zip ${yellowBright(relative(process.cwd(), file))} size is ${prettyBytes(size)}, which is larger than the maximum supported size of ${prettyBytes(maxSize)}.
|
|
73
|
+
There are a few reasons this could happen. You may have accidentally bundled a large dependency, or you might have a
|
|
74
|
+
large number of pre-rendered pages included.
|
|
75
|
+
`));
|
|
76
|
+
const zip = new StreamZip({ file });
|
|
77
|
+
console.log(`Contains ${await zip.entriesCount} files`);
|
|
78
|
+
const sortedFiles = Object.values(await zip.entries()).sort((a, b) => b.size - a.size);
|
|
79
|
+
const largest = {};
|
|
80
|
+
for (let i = 0; i < 10 && i < sortedFiles.length; i++) {
|
|
81
|
+
largest[`${i + 1}`] = {
|
|
82
|
+
File: sortedFiles[i].name,
|
|
83
|
+
'Compressed Size': prettyBytes(sortedFiles[i].compressedSize),
|
|
84
|
+
'Uncompressed Size': prettyBytes(sortedFiles[i].size),
|
|
85
|
+
};
|
|
42
86
|
}
|
|
87
|
+
console.log(yellowBright `\n\nThese are the largest files in the zip:`);
|
|
88
|
+
console.table(largest);
|
|
89
|
+
console.log(greenBright `\n\nFor more information on fixing this, see ${blueBright `https://ntl.fyi/large-next-functions`}`);
|
|
43
90
|
};
|
|
91
|
+
exports.logBetaMessage = () => console.log(greenBright(outdent `
|
|
92
|
+
Thank you for trying the Essential Next.js beta plugin.
|
|
93
|
+
Please share feedback (both good and bad) at ${blueBright `https://ntl.fyi/next-beta-feedback`}
|
|
94
|
+
`));
|
package/lib/index.js
CHANGED
|
@@ -1,37 +1,58 @@
|
|
|
1
|
-
// @ts-check
|
|
2
1
|
const { join, relative } = require('path');
|
|
3
|
-
const {
|
|
2
|
+
const { ODB_FUNCTION_NAME } = require('./constants');
|
|
4
3
|
const { restoreCache, saveCache } = require('./helpers/cache');
|
|
5
|
-
const { getNextConfig, configureHandlerFunctions
|
|
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 {
|
|
7
|
+
const { generateRedirects } = require('./helpers/redirects');
|
|
8
|
+
const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, checkForOldFunctions, } = require('./helpers/verification');
|
|
9
|
+
/** @type import("@netlify/build").NetlifyPlugin */
|
|
8
10
|
module.exports = {
|
|
9
11
|
async onPreBuild({ constants, netlifyConfig, utils: { build: { failBuild }, cache, }, }) {
|
|
12
|
+
var _a;
|
|
13
|
+
logBetaMessage();
|
|
10
14
|
const { publish } = netlifyConfig.build;
|
|
11
15
|
checkForRootPublish({ publish, failBuild });
|
|
12
16
|
verifyNetlifyBuildVersion({ failBuild, ...constants });
|
|
13
17
|
await restoreCache({ cache, publish });
|
|
18
|
+
(_a = netlifyConfig.build).environment || (_a.environment = {});
|
|
19
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
20
|
+
netlifyConfig.build.environment.NEXT_PRIVATE_TARGET = 'server';
|
|
14
21
|
},
|
|
15
22
|
async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
|
|
16
23
|
const { publish } = netlifyConfig.build;
|
|
17
24
|
checkNextSiteHasBuilt({ publish, failBuild });
|
|
18
|
-
const { appDir, basePath, i18n, images, target } = await getNextConfig({
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
const { appDir, basePath, i18n, images, target, ignore, trailingSlash } = await getNextConfig({
|
|
26
|
+
publish,
|
|
27
|
+
failBuild,
|
|
28
|
+
});
|
|
29
|
+
configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) });
|
|
21
30
|
await generateFunctions(constants, appDir);
|
|
22
31
|
await generatePagesResolver({ netlifyConfig, target, constants });
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
await
|
|
32
|
+
await movePublicFiles({ appDir, publish });
|
|
33
|
+
if (process.env.EXPERIMENTAL_ODB_TTL) {
|
|
34
|
+
await patchNextFiles(basePath);
|
|
35
|
+
}
|
|
36
|
+
if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
|
|
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'");
|
|
38
|
+
}
|
|
39
|
+
if (!process.env.SERVE_STATIC_FILES_FROM_ORIGIN) {
|
|
40
|
+
await moveStaticPages({ target, failBuild, netlifyConfig, i18n });
|
|
26
41
|
}
|
|
27
42
|
await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
|
|
28
43
|
await generateRedirects({
|
|
29
44
|
netlifyConfig,
|
|
30
|
-
basePath,
|
|
31
|
-
i18n,
|
|
45
|
+
nextConfig: { basePath, i18n, trailingSlash },
|
|
32
46
|
});
|
|
33
47
|
},
|
|
34
|
-
async onPostBuild({ netlifyConfig, utils: { cache } }) {
|
|
35
|
-
|
|
48
|
+
async onPostBuild({ netlifyConfig, utils: { cache, functions, failBuild }, constants: { FUNCTIONS_DIST } }) {
|
|
49
|
+
await saveCache({ cache, publish: netlifyConfig.build.publish });
|
|
50
|
+
await checkForOldFunctions({ functions });
|
|
51
|
+
await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`));
|
|
52
|
+
const { basePath } = await getNextConfig({ publish: netlifyConfig.build.publish, failBuild });
|
|
53
|
+
await unpatchNextFiles(basePath);
|
|
54
|
+
},
|
|
55
|
+
onEnd() {
|
|
56
|
+
logBetaMessage();
|
|
36
57
|
},
|
|
37
58
|
};
|
|
@@ -1,30 +1,109 @@
|
|
|
1
|
+
/* eslint-disable max-lines-per-function */
|
|
2
|
+
const { promises, existsSync } = require('fs');
|
|
1
3
|
const { Server } = require('http');
|
|
4
|
+
const { tmpdir } = require('os');
|
|
2
5
|
const path = require('path');
|
|
3
6
|
const { Bridge } = require('@vercel/node/dist/bridge');
|
|
7
|
+
const { downloadFile, getMaxAge, getMultiValueHeaders } = require('./handlerUtils');
|
|
4
8
|
const makeHandler = () =>
|
|
5
9
|
// We return a function and then call `toString()` on it to serialise it as the launcher function
|
|
6
|
-
|
|
10
|
+
// eslint-disable-next-line max-params
|
|
11
|
+
(conf, app, pageRoot, staticManifest = [], mode = 'ssr') => {
|
|
12
|
+
// This is just so nft knows about the page entrypoints. It's not actually used
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line node/no-missing-require
|
|
15
|
+
require.resolve('./pages.js');
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
19
|
+
process.env._BYPASS_SSG = 'true';
|
|
20
|
+
const ONE_YEAR_IN_SECONDS = 31536000;
|
|
21
|
+
// We don't want to write ISR files to disk in the lambda environment
|
|
22
|
+
conf.experimental.isrFlushToDisk = false;
|
|
23
|
+
// Set during the request as it needs the host header. Hoisted so we can define the function once
|
|
24
|
+
let base;
|
|
25
|
+
// Only do this if we have some static files moved to the CDN
|
|
26
|
+
if (staticManifest.length !== 0) {
|
|
27
|
+
// These are static page files that have been removed from the function bundle
|
|
28
|
+
// In most cases these are served from the CDN, but for rewrites Next may try to read them
|
|
29
|
+
// from disk. We need to intercept these and load them from the CDN instead
|
|
30
|
+
// Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know.
|
|
31
|
+
const staticFiles = new Map(staticManifest);
|
|
32
|
+
const downloadPromises = new Map();
|
|
33
|
+
const statsCache = new Map();
|
|
34
|
+
// Yes, you can cache stuff locally in a Lambda
|
|
35
|
+
const cacheDir = path.join(tmpdir(), 'next-static-cache');
|
|
36
|
+
// Grab the real fs.promises.readFile...
|
|
37
|
+
const readfileOrig = promises.readFile;
|
|
38
|
+
const statsOrig = promises.stat;
|
|
39
|
+
// ...then money-patch it to see if it's requesting a CDN file
|
|
40
|
+
promises.readFile = async (file, options) => {
|
|
41
|
+
// We only care about page files
|
|
42
|
+
if (file.startsWith(pageRoot)) {
|
|
43
|
+
// We only want the part after `pages/`
|
|
44
|
+
const filePath = file.slice(pageRoot.length + 1);
|
|
45
|
+
// Is it in the CDN and not local?
|
|
46
|
+
if (staticFiles.has(filePath) && !existsSync(file)) {
|
|
47
|
+
// This name is safe to use, because it's one that was already created by Next
|
|
48
|
+
const cacheFile = path.join(cacheDir, filePath);
|
|
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
|
|
55
|
+
if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) {
|
|
56
|
+
await promises.mkdir(path.dirname(cacheFile), { recursive: true });
|
|
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
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Return the cache file
|
|
68
|
+
return readfileOrig(cacheFile, options);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return readfileOrig(file, options);
|
|
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
|
+
};
|
|
84
|
+
}
|
|
7
85
|
let NextServer;
|
|
8
86
|
try {
|
|
9
87
|
// next >= 11.0.1. Yay breaking changes in patch releases!
|
|
10
88
|
NextServer = require('next/dist/server/next-server').default;
|
|
11
89
|
}
|
|
12
|
-
catch {
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!error.message.includes("Cannot find module 'next/dist/server/next-server'")) {
|
|
92
|
+
// A different error, so rethrow it
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
13
95
|
// Probably an old version of next
|
|
14
96
|
}
|
|
15
|
-
// This is just so nft knows about the page entrypoints
|
|
16
|
-
try {
|
|
17
|
-
// eslint-disable-next-line node/no-missing-require
|
|
18
|
-
require.resolve('./pages.js');
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
21
97
|
if (!NextServer) {
|
|
22
98
|
try {
|
|
23
99
|
// next < 11.0.1
|
|
24
100
|
// eslint-disable-next-line node/no-missing-require, import/no-unresolved
|
|
25
101
|
NextServer = require('next/dist/next-server/server/next-server').default;
|
|
26
102
|
}
|
|
27
|
-
catch {
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
28
107
|
throw new Error('Could not find Next.js server');
|
|
29
108
|
}
|
|
30
109
|
}
|
|
@@ -46,24 +125,42 @@ const makeHandler = () =>
|
|
|
46
125
|
const bridge = new Bridge(server);
|
|
47
126
|
bridge.listen();
|
|
48
127
|
return async (event, context) => {
|
|
128
|
+
var _a, _b, _c;
|
|
129
|
+
let requestMode = mode;
|
|
130
|
+
// Ensure that paths are encoded - but don't double-encode them
|
|
131
|
+
event.path = new URL(event.path, event.rawUrl).pathname;
|
|
132
|
+
// Next expects to be able to parse the query from the URL
|
|
133
|
+
const query = new URLSearchParams(event.queryStringParameters).toString();
|
|
134
|
+
event.path = query ? `${event.path}?${query}` : event.path;
|
|
135
|
+
// Only needed if we're intercepting static files
|
|
136
|
+
if (staticManifest.length !== 0) {
|
|
137
|
+
const { host } = event.headers;
|
|
138
|
+
const protocol = event.headers['x-forwarded-proto'] || 'http';
|
|
139
|
+
base = `${protocol}://${host}`;
|
|
140
|
+
}
|
|
49
141
|
const { headers, ...result } = await bridge.launcher(event, context);
|
|
50
142
|
/** @type import("@netlify/functions").HandlerResponse */
|
|
51
143
|
// Convert all headers to multiValueHeaders
|
|
52
|
-
const multiValueHeaders =
|
|
53
|
-
|
|
54
|
-
if (Array.isArray(headers[key])) {
|
|
55
|
-
multiValueHeaders[key] = headers[key];
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
multiValueHeaders[key] = [headers[key]];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (multiValueHeaders['set-cookie'] &&
|
|
62
|
-
multiValueHeaders['set-cookie'][0] &&
|
|
63
|
-
multiValueHeaders['set-cookie'][0].includes('__prerender_bypass')) {
|
|
144
|
+
const multiValueHeaders = getMultiValueHeaders(headers);
|
|
145
|
+
if ((_b = (_a = multiValueHeaders['set-cookie']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.includes('__prerender_bypass')) {
|
|
64
146
|
delete multiValueHeaders.etag;
|
|
65
147
|
multiValueHeaders['cache-control'] = ['no-cache'];
|
|
66
148
|
}
|
|
149
|
+
// Sending SWR headers causes undefined behaviour with the Netlify CDN
|
|
150
|
+
const cacheHeader = (_c = multiValueHeaders['cache-control']) === null || _c === void 0 ? void 0 : _c[0];
|
|
151
|
+
if (cacheHeader === null || cacheHeader === void 0 ? void 0 : cacheHeader.includes('stale-while-revalidate')) {
|
|
152
|
+
if (requestMode === 'odb' && process.env.EXPERIMENTAL_ODB_TTL) {
|
|
153
|
+
requestMode = 'isr';
|
|
154
|
+
const ttl = getMaxAge(cacheHeader);
|
|
155
|
+
// Long-expiry TTL is basically no TTL
|
|
156
|
+
if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) {
|
|
157
|
+
result.ttl = ttl;
|
|
158
|
+
}
|
|
159
|
+
multiValueHeaders['x-rendered-at'] = [new Date().toISOString()];
|
|
160
|
+
}
|
|
161
|
+
multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
|
|
162
|
+
}
|
|
163
|
+
multiValueHeaders['x-render-mode'] = [requestMode];
|
|
67
164
|
return {
|
|
68
165
|
...result,
|
|
69
166
|
multiValueHeaders,
|
|
@@ -73,13 +170,23 @@ const makeHandler = () =>
|
|
|
73
170
|
};
|
|
74
171
|
const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) => `
|
|
75
172
|
const { Server } = require("http");
|
|
173
|
+
const { tmpdir } = require('os')
|
|
174
|
+
const { promises, existsSync } = require("fs");
|
|
76
175
|
// We copy the file here rather than requiring from the node module
|
|
77
176
|
const { Bridge } = require("./bridge");
|
|
177
|
+
const { downloadFile, getMaxAge, getMultiValueHeaders } = require('./handlerUtils')
|
|
178
|
+
|
|
78
179
|
const { builder } = require("@netlify/functions");
|
|
79
180
|
const { config } = require("${publishDir}/required-server-files.json")
|
|
181
|
+
let staticManifest
|
|
182
|
+
try {
|
|
183
|
+
staticManifest = require("${publishDir}/static-manifest.json")
|
|
184
|
+
} catch {}
|
|
80
185
|
const path = require("path");
|
|
186
|
+
const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages"));
|
|
81
187
|
exports.handler = ${isODB
|
|
82
|
-
? `builder((${makeHandler().toString()})(config, "${appDir}"));`
|
|
83
|
-
: `(${makeHandler().toString()})(config, "${appDir}");`}
|
|
188
|
+
? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));`
|
|
189
|
+
: `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`}
|
|
84
190
|
`;
|
|
85
191
|
module.exports = getHandler;
|
|
192
|
+
/* eslint-enable max-lines-per-function */
|
|
@@ -0,0 +1,66 @@
|
|
|
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.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 stream_1 = require("stream");
|
|
11
|
+
const util_1 = require("util");
|
|
12
|
+
const streamPipeline = (0, util_1.promisify)(stream_1.pipeline);
|
|
13
|
+
const downloadFile = async (url, destination) => {
|
|
14
|
+
console.log(`Downloading ${url} to ${destination}`);
|
|
15
|
+
const httpx = url.startsWith('https') ? https_1.default : http_1.default;
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
const req = httpx.get(url, { timeout: 10000 }, (response) => {
|
|
18
|
+
if (response.statusCode < 200 || response.statusCode > 299) {
|
|
19
|
+
reject(new Error(`Failed to download ${url}: ${response.statusCode} ${response.statusMessage || ''}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const fileStream = (0, fs_1.createWriteStream)(destination);
|
|
23
|
+
streamPipeline(response, fileStream)
|
|
24
|
+
.then(resolve)
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
console.log(`Error downloading ${url}`, error);
|
|
27
|
+
reject(error);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on('error', (error) => {
|
|
31
|
+
console.log(`Error downloading ${url}`, error);
|
|
32
|
+
reject(error);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
exports.downloadFile = downloadFile;
|
|
37
|
+
const getMaxAge = (header) => {
|
|
38
|
+
const parts = header.split(',');
|
|
39
|
+
let maxAge;
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
const [key, value] = part.split('=');
|
|
42
|
+
if ((key === null || key === void 0 ? void 0 : key.trim()) === 's-maxage') {
|
|
43
|
+
maxAge = value === null || value === void 0 ? void 0 : value.trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (maxAge) {
|
|
47
|
+
const result = Number.parseInt(maxAge);
|
|
48
|
+
return Number.isNaN(result) ? 0 : result;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
};
|
|
52
|
+
exports.getMaxAge = getMaxAge;
|
|
53
|
+
const getMultiValueHeaders = (headers) => {
|
|
54
|
+
const multiValueHeaders = {};
|
|
55
|
+
for (const key of Object.keys(headers)) {
|
|
56
|
+
const header = headers[key];
|
|
57
|
+
if (Array.isArray(header)) {
|
|
58
|
+
multiValueHeaders[key] = header;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
multiValueHeaders[key] = [header];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return multiValueHeaders;
|
|
65
|
+
};
|
|
66
|
+
exports.getMultiValueHeaders = getMultiValueHeaders;
|
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": [
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"manifest.yml"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build:demo": "next build
|
|
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/
|
|
14
|
-
"dev:demo": "next dev
|
|
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,14 +54,19 @@
|
|
|
52
54
|
},
|
|
53
55
|
"homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
|
|
54
56
|
"dependencies": {
|
|
55
|
-
"@netlify/functions": "^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",
|
|
59
|
-
"destr": "^1.1.0",
|
|
60
61
|
"fs-extra": "^10.0.0",
|
|
62
|
+
"globby": "^11.0.4",
|
|
61
63
|
"moize": "^6.1.0",
|
|
64
|
+
"node-fetch": "^2.6.6",
|
|
65
|
+
"node-stream-zip": "^1.15.0",
|
|
62
66
|
"outdent": "^0.8.0",
|
|
67
|
+
"p-limit": "^3.1.0",
|
|
68
|
+
"pathe": "^0.2.0",
|
|
69
|
+
"pretty-bytes": "^5.6.0",
|
|
63
70
|
"semver": "^7.3.5",
|
|
64
71
|
"slash": "^3.0.0",
|
|
65
72
|
"tiny-glob": "^0.2.9"
|
|
@@ -67,18 +74,21 @@
|
|
|
67
74
|
"devDependencies": {
|
|
68
75
|
"@babel/core": "^7.15.8",
|
|
69
76
|
"@babel/preset-env": "^7.15.8",
|
|
70
|
-
"@
|
|
77
|
+
"@babel/preset-typescript": "^7.16.0",
|
|
78
|
+
"@netlify/build": "^20.0.4",
|
|
79
|
+
"@netlify/eslint-config-node": "^3.3.11",
|
|
71
80
|
"@testing-library/cypress": "^8.0.1",
|
|
81
|
+
"@types/fs-extra": "^9.0.13",
|
|
72
82
|
"@types/jest": "^27.0.2",
|
|
73
83
|
"@types/mocha": "^9.0.0",
|
|
74
84
|
"babel-jest": "^27.2.5",
|
|
75
85
|
"cpy": "^8.1.2",
|
|
76
|
-
"cypress": "^
|
|
77
|
-
"eslint-config-next": "^
|
|
86
|
+
"cypress": "^9.0.0",
|
|
87
|
+
"eslint-config-next": "^12.0.0",
|
|
78
88
|
"husky": "^4.3.0",
|
|
79
89
|
"jest": "^27.0.0",
|
|
80
90
|
"netlify-plugin-cypress": "^2.2.0",
|
|
81
|
-
"next": "^
|
|
91
|
+
"next": "^12.0.2",
|
|
82
92
|
"npm-run-all": "^4.1.5",
|
|
83
93
|
"prettier": "^2.1.2",
|
|
84
94
|
"react": "^17.0.1",
|