@netlify/plugin-nextjs 3.9.1 → 4.0.0-beta.3

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.
Files changed (95) hide show
  1. package/README.md +18 -56
  2. package/lib/constants.js +21 -0
  3. package/lib/helpers/cache.js +20 -0
  4. package/lib/helpers/config.js +115 -0
  5. package/lib/helpers/files.js +48 -0
  6. package/lib/helpers/functions.js +64 -0
  7. package/lib/helpers/verification.js +79 -0
  8. package/lib/index.js +47 -0
  9. package/lib/templates/getHandler.js +164 -0
  10. package/lib/templates/getPageResolver.js +24 -0
  11. package/lib/templates/ipx.js +8 -0
  12. package/manifest.yml +1 -1
  13. package/package.json +46 -53
  14. package/LICENSE.md +0 -7
  15. package/helpers/cacheBuild.js +0 -31
  16. package/helpers/checkNxConfig.js +0 -56
  17. package/helpers/copyUnstableIncludedDirs.js +0 -27
  18. package/helpers/doesNotNeedPlugin.js +0 -49
  19. package/helpers/doesSiteUseNextOnNetlify.js +0 -20
  20. package/helpers/getNextConfig.js +0 -46
  21. package/helpers/getNextRoot.js +0 -20
  22. package/helpers/isStaticExportProject.js +0 -20
  23. package/helpers/resolveNextModule.js +0 -30
  24. package/helpers/usesBuildCommand.js +0 -37
  25. package/helpers/validateNextUsage.js +0 -53
  26. package/helpers/verifyBuildTarget.js +0 -69
  27. package/index.js +0 -175
  28. package/src/index.js +0 -94
  29. package/src/lib/config.js +0 -58
  30. package/src/lib/constants/regex.js +0 -9
  31. package/src/lib/helpers/addDefaultLocaleRedirect.js +0 -25
  32. package/src/lib/helpers/addLocaleRedirects.js +0 -19
  33. package/src/lib/helpers/asyncForEach.js +0 -7
  34. package/src/lib/helpers/convertToBasePathRedirects.js +0 -80
  35. package/src/lib/helpers/copyDynamicImportChunks.js +0 -39
  36. package/src/lib/helpers/formatRedirectTarget.js +0 -9
  37. package/src/lib/helpers/getDataRouteForRoute.js +0 -30
  38. package/src/lib/helpers/getFilePathForRoute.js +0 -10
  39. package/src/lib/helpers/getI18n.js +0 -10
  40. package/src/lib/helpers/getNetlifyFunctionName.js +0 -38
  41. package/src/lib/helpers/getNetlifyRoutes.js +0 -45
  42. package/src/lib/helpers/getNextDistDir.js +0 -12
  43. package/src/lib/helpers/getNextSrcDir.js +0 -5
  44. package/src/lib/helpers/getPagesManifest.js +0 -19
  45. package/src/lib/helpers/getPrerenderManifest.js +0 -40
  46. package/src/lib/helpers/getPreviewModeFunctionName.js +0 -5
  47. package/src/lib/helpers/getRoutesManifest.js +0 -12
  48. package/src/lib/helpers/getSortedRedirects.js +0 -37
  49. package/src/lib/helpers/handleFileTracking.js +0 -61
  50. package/src/lib/helpers/isApiRoute.js +0 -4
  51. package/src/lib/helpers/isDynamicRoute.js +0 -6
  52. package/src/lib/helpers/isFrameworkRoute.js +0 -5
  53. package/src/lib/helpers/isHtmlFile.js +0 -4
  54. package/src/lib/helpers/isRootCatchAllRedirect.js +0 -6
  55. package/src/lib/helpers/isRouteInPrerenderManifest.js +0 -23
  56. package/src/lib/helpers/isRouteWithDataRoute.js +0 -13
  57. package/src/lib/helpers/isRouteWithFallback.js +0 -12
  58. package/src/lib/helpers/logger.js +0 -44
  59. package/src/lib/helpers/removeFileExtension.js +0 -4
  60. package/src/lib/helpers/runJobsQueue.js +0 -18
  61. package/src/lib/helpers/setupNetlifyFunctionForPage.js +0 -44
  62. package/src/lib/helpers/setupStaticFileForPage.js +0 -18
  63. package/src/lib/pages/api/pages.js +0 -22
  64. package/src/lib/pages/api/redirects.js +0 -13
  65. package/src/lib/pages/api/setup.js +0 -15
  66. package/src/lib/pages/getInitialProps/pages.js +0 -40
  67. package/src/lib/pages/getInitialProps/redirects.js +0 -26
  68. package/src/lib/pages/getInitialProps/setup.js +0 -15
  69. package/src/lib/pages/getServerSideProps/pages.js +0 -43
  70. package/src/lib/pages/getServerSideProps/redirects.js +0 -44
  71. package/src/lib/pages/getServerSideProps/setup.js +0 -15
  72. package/src/lib/pages/getStaticProps/pages.js +0 -26
  73. package/src/lib/pages/getStaticProps/redirects.js +0 -70
  74. package/src/lib/pages/getStaticProps/setup.js +0 -68
  75. package/src/lib/pages/getStaticPropsWithFallback/pages.js +0 -24
  76. package/src/lib/pages/getStaticPropsWithFallback/redirects.js +0 -51
  77. package/src/lib/pages/getStaticPropsWithFallback/setup.js +0 -29
  78. package/src/lib/pages/getStaticPropsWithRevalidate/pages.js +0 -45
  79. package/src/lib/pages/getStaticPropsWithRevalidate/redirects.js +0 -55
  80. package/src/lib/pages/getStaticPropsWithRevalidate/setup.js +0 -40
  81. package/src/lib/pages/withoutProps/pages.js +0 -27
  82. package/src/lib/pages/withoutProps/redirects.js +0 -51
  83. package/src/lib/pages/withoutProps/setup.js +0 -34
  84. package/src/lib/pages/worker.js +0 -19
  85. package/src/lib/steps/copyNextAssets.js +0 -31
  86. package/src/lib/steps/copyPublicFiles.js +0 -18
  87. package/src/lib/steps/prepareFolders.js +0 -39
  88. package/src/lib/steps/setupHeaders.js +0 -37
  89. package/src/lib/steps/setupImageFunction.js +0 -17
  90. package/src/lib/steps/setupPages.js +0 -25
  91. package/src/lib/steps/setupRedirects.js +0 -105
  92. package/src/lib/templates/getHandlerFunction.js +0 -209
  93. package/src/lib/templates/getTemplate.js +0 -15
  94. package/src/lib/templates/imageFunction.js +0 -135
  95. package/src/next-on-netlify.js +0 -22
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  ![Next.js on Netlify Build Plugin](next-on-netlify.png)
2
2
 
3
- # Essential Next.js Build Plugin
3
+ # Essential Next.js Build Plugin (beta)
4
+
5
+
6
+ :warning: This is the beta version of the Essential Next.js plugin. For the stable version, see [Essential Next.js plugin v3](https://github.com/netlify/netlify-plugin-nextjs/tree/v3#readme) :warning:
4
7
 
5
8
  <p align="center">
6
9
  <a aria-label="npm version" href="https://www.npmjs.com/package/@netlify/plugin-nextjs">
@@ -11,72 +14,31 @@
11
14
  </a>
12
15
  </p>
13
16
 
14
- This build plugin is a utility for supporting Next.js on Netlify. To enable server-side rendering and other framework-specific features in your Next.js application on Netlify, you will need to install this plugin for your app.
15
-
16
- ## Table of Contents
17
-
18
- - [Installation and Configuration](#installation-and-configuration)
19
- - [Docs](#docs)
20
- - [Credits](#credits)
21
-
22
- ## Installation and Configuration
23
-
24
- ### For new Next.js sites
25
-
26
- As of v3.0.0, all new sites deployed to Netlify with Next.js will automatically install this plugin for a seamless experience deploying Next.js on Netlify!
27
-
28
- This means that you don't have to do anything — just build and deploy your site to Netlify as usual and we'll handle the rest.
29
-
30
- You're able to [remove the plugin](https://docs.netlify.com/configure-builds/build-plugins/#remove-a-plugin) at any time by visiting the **Plugins** tab for your site in the Netlify UI.
31
-
32
- ### For existing Next.js sites
33
-
34
- ### UI Installation
35
17
 
36
- If your Next.js project was already deployed to Netlify pre-3.0.0, use the Netlify UI to [install the Essential Next.js Build Plugin](http://app.netlify.com/plugins/@netlify/plugin-nextjs/install) in a few clicks.
18
+ ## Installing the beta
37
19
 
38
- ### Manual installation
39
-
40
- 1\. Create a `netlify.toml` in the root of your project. Your file should include the plugins section below:
41
20
 
21
+ - Install the module:
22
+ ```shell
23
+ npm install -D @netlify/plugin-nextjs@beta
24
+ ```
25
+ - Change the `publish` directory to `.next` and add the plugin to `netlify.toml` if not already installed:
42
26
  ```toml
43
27
  [build]
44
- command = "npm run build"
45
- publish = "out"
28
+ publish = ".next"
46
29
 
47
30
  [[plugins]]
48
- package = "@netlify/plugin-nextjs"
49
- ```
50
-
51
- Note: the plugin does not run for statically exported Next.js sites (aka sites that use `next export`). To use the plugin, you should use the `[build]` config in the .toml snippet above. Be sure to exclude `next export` from your build script.
52
- The plugin will attempt to detect if the site uses static export or Storybook, and will not run for either. If you want to disable the auto-detection, you can set the `NEXT_PLUGIN_FORCE_RUN` environment variable to `true` or `false`.
53
- Setting it to `true` or `1` will mean the plugin always runs, and setting it to `false` or `0` will mean it never runs. If unset, auto-detection will be used. This variable should be set in the Netlify UI or in the `netlify.toml` file.
54
-
55
- 2\. From your project's base directory, use `npm`, `yarn`, or any other Node.js package manager to add this plugin to `dependencies` in `package.json`.
56
-
57
- ```
58
- npm install --save @netlify/plugin-nextjs
31
+ package = "@netlify/plugin-nextjs"
59
32
  ```
60
33
 
61
- or
62
-
63
- ```
64
- yarn add @netlify/plugin-nextjs
65
- ```
34
+ If you previously set `target: "serverless"` or a custom `distDir` in your `next.config.js`, or set `node_bundler` or `external_node_modules` in your `netlify.toml` these are no longer needed and can be removed.
66
35
 
67
- Read more about [file-based plugin installation](https://docs.netlify.com/configure-builds/build-plugins/#file-based-installation) in our docs.
36
+ The `serverless` and `experimental-serverless-trace` targets are deprecated in Next 12, and all builds with this plugin will now use the default `server` target.
68
37
 
69
- ## Docs
38
+ If you are using a monorepo you will need to change `publish` to point to the full path to the built `.next` directory, which may be in a subdirectory. If you have changed your `distDir` then it will need to match that.
70
39
 
71
- - [CLI Usage](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/cli-usage.md)
72
- - [Custom Netlify Functions](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/custom-functions.md)
73
- - [Image Handling](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/image-handling.md)
74
- - [Monorepos and Nx](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/monorepos.md)
75
- - [Custom Netlify Redirects](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/custom-redirects.md)
76
- - [Local Files in Runtime](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/local-files-in-runtime.md)
77
- - [FAQ](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/faq.md)
78
- - [Caveats](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/caveats.md)
40
+ If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
79
41
 
80
- ## Credits
42
+ ## Beta feedback
81
43
 
82
- This package extends the project [next-on-netlify](https://github.com/netlify/next-on-netlify), authored originally by [Finn Woelm](https://github.com/finnwoelm).
44
+ Please share any thoughts, feedback or questions about the beta [in our discussion](https://github.com/netlify/netlify-plugin-nextjs/discussions/706).
@@ -0,0 +1,21 @@
1
+ const HANDLER_FUNCTION_NAME = '___netlify-handler';
2
+ const ODB_FUNCTION_NAME = '___netlify-odb-handler';
3
+ const IMAGE_FUNCTION_NAME = '_ipx';
4
+ // These are paths in .next that shouldn't be publicly accessible
5
+ const HIDDEN_PATHS = [
6
+ '/cache/*',
7
+ '/server/*',
8
+ '/serverless/*',
9
+ '/traces',
10
+ '/routes-manifest.json',
11
+ '/build-manifest.json',
12
+ '/prerender-manifest.json',
13
+ '/react-loadable-manifest.json',
14
+ '/BUILD_ID',
15
+ ];
16
+ module.exports = {
17
+ HIDDEN_PATHS,
18
+ IMAGE_FUNCTION_NAME,
19
+ HANDLER_FUNCTION_NAME,
20
+ ODB_FUNCTION_NAME,
21
+ };
@@ -0,0 +1,20 @@
1
+ const { posix: { join }, } = require('path');
2
+ exports.restoreCache = async ({ cache, publish }) => {
3
+ const cacheDir = join(publish, 'cache');
4
+ if (await cache.restore(cacheDir)) {
5
+ console.log('Next.js cache restored.');
6
+ }
7
+ else {
8
+ console.log('No Next.js cache to restore.');
9
+ }
10
+ };
11
+ exports.saveCache = async ({ cache, publish }) => {
12
+ const cacheDir = join(publish, 'cache');
13
+ const buildManifest = join(publish, 'build-manifest.json');
14
+ if (await cache.save(cacheDir, { digests: [buildManifest] })) {
15
+ console.log('Next.js cache saved.');
16
+ }
17
+ else {
18
+ console.log('No Next.js cache to save.');
19
+ }
20
+ };
@@ -0,0 +1,115 @@
1
+ // @ts-check
2
+ const { readJSON } = require('fs-extra');
3
+ const { join, dirname, relative } = require('pathe');
4
+ const slash = require('slash');
5
+ const defaultFailBuild = (message, { error }) => {
6
+ throw new Error(`${message}\n${error && error.stack}`);
7
+ };
8
+ const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, HIDDEN_PATHS } = require('../constants');
9
+ const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}`;
10
+ const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`;
11
+ const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/;
12
+ const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/;
13
+ const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g;
14
+ const getNetlifyRoutes = (nextRoute) => {
15
+ let netlifyRoutes = [nextRoute];
16
+ // If the route is an optional catch-all route, we need to add a second
17
+ // Netlify route for the base path (when no parameters are present).
18
+ // The file ending must be present!
19
+ if (OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
20
+ let netlifyRoute = nextRoute.replace(OPTIONAL_CATCH_ALL_REGEX, '$2');
21
+ // When optional catch-all route is at top-level, the regex on line 19 will
22
+ // create an empty string, but actually needs to be a forward slash
23
+ if (netlifyRoute === '')
24
+ netlifyRoute = '/';
25
+ // When optional catch-all route is at top-level, the regex on line 19 will
26
+ // create an incorrect route for the data route. For example, it creates
27
+ // /_next/data/%BUILDID%.json, but NextJS looks for
28
+ // /_next/data/%BUILDID%/index.json
29
+ netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json');
30
+ // Add second route to the front of the array
31
+ netlifyRoutes.unshift(netlifyRoute);
32
+ }
33
+ // Replace catch-all, e.g., [...slug]
34
+ netlifyRoutes = netlifyRoutes.map((route) => route.replace(CATCH_ALL_REGEX, '/:$1/*'));
35
+ // Replace optional catch-all, e.g., [[...slug]]
36
+ netlifyRoutes = netlifyRoutes.map((route) => route.replace(OPTIONAL_CATCH_ALL_REGEX, '/*'));
37
+ // Replace dynamic parameters, e.g., [id]
38
+ netlifyRoutes = netlifyRoutes.map((route) => route.replace(DYNAMIC_PARAMETER_REGEX, '/:$1'));
39
+ return netlifyRoutes;
40
+ };
41
+ exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
42
+ const { dynamicRoutes } = await readJSON(join(netlifyConfig.build.publish, 'prerender-manifest.json'));
43
+ const redirects = [];
44
+ netlifyConfig.redirects.push(...HIDDEN_PATHS.map((path) => ({
45
+ from: `${basePath}${path}`,
46
+ to: '/404.html',
47
+ status: 404,
48
+ force: true,
49
+ })));
50
+ const dynamicRouteEntries = Object.entries(dynamicRoutes);
51
+ dynamicRouteEntries.sort((a, b) => a[0].localeCompare(b[0]));
52
+ dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
53
+ // Add redirects if fallback is "null" (aka blocking) or true/a string
54
+ if (fallback === false) {
55
+ return;
56
+ }
57
+ redirects.push(...getNetlifyRoutes(route), ...getNetlifyRoutes(dataRoute));
58
+ });
59
+ if (i18n) {
60
+ netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
61
+ }
62
+ // This is only used in prod, so dev uses `next dev` directly
63
+ netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 }, {
64
+ from: `${basePath}/*`,
65
+ to: HANDLER_FUNCTION_PATH,
66
+ status: 200,
67
+ force: true,
68
+ conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
69
+ }, ...redirects.map((redirect) => ({
70
+ from: `${basePath}${redirect}`,
71
+ to: ODB_FUNCTION_PATH,
72
+ status: 200,
73
+ })), { from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 });
74
+ };
75
+ exports.getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
76
+ try {
77
+ const { config, appDir, ignore } = await readJSON(join(publish, 'required-server-files.json'));
78
+ if (!config) {
79
+ return failBuild('Error loading your Next config');
80
+ }
81
+ return { ...config, appDir, ignore };
82
+ }
83
+ catch (error) {
84
+ return failBuild('Error loading your Next config', { error });
85
+ }
86
+ };
87
+ const resolveModuleRoot = (moduleName) => {
88
+ try {
89
+ return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })));
90
+ }
91
+ catch (error) {
92
+ return null;
93
+ }
94
+ };
95
+ exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
96
+ var _a;
97
+ /* eslint-disable no-underscore-dangle */
98
+ (_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
99
+ netlifyConfig.functions._ipx.node_bundler = 'nft';
100
+ [HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
101
+ var _a, _b;
102
+ (_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
103
+ netlifyConfig.functions[functionName].node_bundler = 'nft';
104
+ (_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
105
+ netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, ...ignore.map((path) => `!${slash(path)}`));
106
+ const nextRoot = resolveModuleRoot('next');
107
+ if (nextRoot) {
108
+ 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`);
109
+ }
110
+ const sharpRoot = resolveModuleRoot('sharp');
111
+ if (sharpRoot) {
112
+ netlifyConfig.functions[functionName].included_files.push(`!${sharpRoot}/**/*`);
113
+ }
114
+ });
115
+ };
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+ const { existsSync, readJson, move, cpSync, copy, writeJson } = require('fs-extra');
3
+ const pLimit = require('p-limit');
4
+ const { join } = require('pathe');
5
+ const TEST_ROUTE = /\/\[[^/]+?](?=\/|$)/;
6
+ const isDynamicRoute = (route) => TEST_ROUTE.test(route);
7
+ exports.moveStaticPages = async ({ netlifyConfig, target, i18n, failBuild }) => {
8
+ const root = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
9
+ const pagesManifestPath = join(root, 'pages-manifest.json');
10
+ if (!existsSync(pagesManifestPath)) {
11
+ failBuild(`Could not find pages manifest at ${pagesManifestPath}`);
12
+ }
13
+ const files = [];
14
+ const moveFile = async (file) => {
15
+ const source = join(root, file);
16
+ // Trim the initial "pages"
17
+ const filePath = file.slice(6);
18
+ files.push(filePath);
19
+ const dest = join(netlifyConfig.build.publish, filePath);
20
+ await move(source, dest);
21
+ };
22
+ const pagesManifest = await readJson(pagesManifestPath);
23
+ // Arbitrary limit of 10 concurrent file moves
24
+ const limit = pLimit(10);
25
+ const promises = Object.entries(pagesManifest).map(async ([route, filePath]) => {
26
+ if (isDynamicRoute(route) ||
27
+ !(filePath.endsWith('.html') || filePath.endsWith('.json')) ||
28
+ filePath.endsWith('/404.html') ||
29
+ filePath.endsWith('/500.html')) {
30
+ return;
31
+ }
32
+ return limit(moveFile, filePath);
33
+ });
34
+ await Promise.all(promises);
35
+ console.log(`Moved ${files.length} page files`);
36
+ // Write the manifest for use in the serverless functions
37
+ await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), files);
38
+ if (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) {
39
+ // Copy the default locale into the root
40
+ await copy(join(netlifyConfig.build.publish, i18n.defaultLocale), `${netlifyConfig.build.publish}/`);
41
+ }
42
+ };
43
+ exports.movePublicFiles = async ({ appDir, publish }) => {
44
+ const publicDir = join(appDir, 'public');
45
+ if (existsSync(publicDir)) {
46
+ await copy(publicDir, `${publish}/`);
47
+ }
48
+ };
@@ -0,0 +1,64 @@
1
+ const { copyFile, ensureDir, writeFile, writeJSON } = require('fs-extra');
2
+ const { join, relative } = require('pathe');
3
+ const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../constants');
4
+ const getHandler = require('../templates/getHandler');
5
+ const { getPageResolver } = require('../templates/getPageResolver');
6
+ const DEFAULT_FUNCTIONS_SRC = 'netlify/functions';
7
+ exports.generateFunctions = async ({ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir) => {
8
+ const functionsDir = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
9
+ const bridgeFile = require.resolve('@vercel/node/dist/bridge');
10
+ const functionDir = join(process.cwd(), functionsDir, HANDLER_FUNCTION_NAME);
11
+ const publishDir = relative(functionDir, join(process.cwd(), PUBLISH_DIR));
12
+ const writeHandler = async (func, isODB) => {
13
+ const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) });
14
+ await ensureDir(join(functionsDir, func));
15
+ await writeFile(join(functionsDir, func, `${func}.js`), handlerSource);
16
+ await copyFile(bridgeFile, join(functionsDir, func, 'bridge.js'));
17
+ };
18
+ await writeHandler(HANDLER_FUNCTION_NAME, false);
19
+ await writeHandler(ODB_FUNCTION_NAME, true);
20
+ };
21
+ /**
22
+ * Writes a file in each function directory that contains references to every page entrypoint.
23
+ * This is just so that the nft bundler knows about them. We'll eventually do this better.
24
+ */
25
+ exports.generatePagesResolver = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC }, netlifyConfig, target, }) => {
26
+ const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
27
+ const jsSource = await getPageResolver({
28
+ netlifyConfig,
29
+ target,
30
+ });
31
+ await writeFile(join(functionsPath, ODB_FUNCTION_NAME, 'pages.js'), jsSource);
32
+ await writeFile(join(functionsPath, HANDLER_FUNCTION_NAME, 'pages.js'), jsSource);
33
+ };
34
+ // Move our next/image function into the correct functions directory
35
+ exports.setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC }, imageconfig = {}, netlifyConfig, basePath, }) => {
36
+ const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
37
+ const functionName = `${IMAGE_FUNCTION_NAME}.js`;
38
+ const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME);
39
+ await ensureDir(functionDirectory);
40
+ await writeJSON(join(functionDirectory, 'imageconfig.json'), {
41
+ ...imageconfig,
42
+ basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'),
43
+ });
44
+ await copyFile(join(__dirname, '..', 'templates', 'ipx.js'), join(functionDirectory, functionName));
45
+ const imagePath = imageconfig.path || '/_next/image';
46
+ netlifyConfig.redirects.push({
47
+ from: `${imagePath}*`,
48
+ query: { url: ':url', w: ':width', q: ':quality' },
49
+ to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
50
+ status: 301,
51
+ }, {
52
+ from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,
53
+ to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`,
54
+ status: 200,
55
+ });
56
+ if (basePath) {
57
+ // next/image generates image static URLs that still point at the site root
58
+ netlifyConfig.redirects.push({
59
+ from: '/_next/static/image/*',
60
+ to: '/static/image/:splat',
61
+ status: 200,
62
+ });
63
+ }
64
+ };
@@ -0,0 +1,79 @@
1
+ const { existsSync, promises } = require('fs');
2
+ const path = require('path');
3
+ const { relative } = require('path');
4
+ const { yellowBright, greenBright, blueBright, redBright } = require('chalk');
5
+ const { async: StreamZip } = require('node-stream-zip');
6
+ const outdent = require('outdent');
7
+ const prettyBytes = require('pretty-bytes');
8
+ const { satisfies } = require('semver');
9
+ // This is when nft support was added
10
+ const REQUIRED_BUILD_VERSION = '>=18.16.0';
11
+ exports.verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuild }) => {
12
+ // We check for build version because that's what's available to us, but prompt about the cli because that's what they can upgrade
13
+ if (IS_LOCAL && !satisfies(NETLIFY_BUILD_VERSION, REQUIRED_BUILD_VERSION, { includePrerelease: true })) {
14
+ return failBuild(outdent `
15
+ This version of the Essential Next.js plugin requires netlify-cli@6.12.4 or higher. Please upgrade and try again.
16
+ You can do this by running: "npm install -g netlify-cli@latest" or "yarn global add netlify-cli@latest"
17
+ `);
18
+ }
19
+ };
20
+ exports.checkNextSiteHasBuilt = ({ publish, failBuild }) => {
21
+ if (!existsSync(path.join(publish, 'BUILD_ID'))) {
22
+ return failBuild(outdent `
23
+ 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.
24
+ 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.
25
+ If you are using "next export" then the Essential Next.js plugin should be removed. See https://ntl.fyi/remove-plugin for details.
26
+ `);
27
+ }
28
+ if (existsSync(path.join(publish, 'export-detail.json'))) {
29
+ failBuild(outdent `
30
+ Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
31
+ This plugin is not needed for "next export" so should be removed, and publish directory set to "out".
32
+ See https://ntl.fyi/remove-plugin for more details on how to remove this plugin.`);
33
+ }
34
+ };
35
+ exports.checkForRootPublish = ({ publish, failBuild }) => {
36
+ if (path.resolve(publish) === path.resolve('.')) {
37
+ failBuild(outdent `
38
+ 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.
39
+ 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.`);
40
+ }
41
+ };
42
+ // 50MB, which is the documented max, though the hard max seems to be higher
43
+ const LAMBDA_MAX_SIZE = 1024 * 1024 * 50;
44
+ exports.checkZipSize = async (file, maxSize = LAMBDA_MAX_SIZE) => {
45
+ if (!existsSync(file)) {
46
+ console.warn(`Could not check zip size because ${file} does not exist`);
47
+ return;
48
+ }
49
+ const size = await promises.stat(file).then(({ size }) => size);
50
+ if (size < maxSize) {
51
+ return;
52
+ }
53
+ // We don't fail the build, because the actual hard max size is larger so it might still succeed
54
+ console.log(redBright(outdent `
55
+
56
+ The function zip ${yellowBright(relative(process.cwd(), file))} size is ${prettyBytes(size)}, which is larger than the maximum supported size of ${prettyBytes(maxSize)}.
57
+ There are a few reasons this could happen. You may have accidentally bundled a large dependency, or you might have a
58
+ large number of pre-rendered pages included.
59
+
60
+ `));
61
+ const zip = new StreamZip({ file });
62
+ console.log(`Contains ${await zip.entriesCount} files`);
63
+ const sortedFiles = Object.values(await zip.entries()).sort((a, b) => b.size - a.size);
64
+ const largest = {};
65
+ for (let i = 0; i < 10 && i < sortedFiles.length; i++) {
66
+ largest[`${i + 1}`] = {
67
+ File: sortedFiles[i].name,
68
+ 'Compressed Size': prettyBytes(sortedFiles[i].compressedSize),
69
+ 'Uncompressed Size': prettyBytes(sortedFiles[i].size),
70
+ };
71
+ }
72
+ console.log(yellowBright `\n\nThese are the largest files in the zip:`);
73
+ console.table(largest);
74
+ console.log(greenBright `\n\nFor more information on fixing this, see ${blueBright `https://ntl.fyi/large-next-functions`}`);
75
+ };
76
+ exports.logBetaMessage = () => console.log(greenBright(outdent `
77
+ Thank you for trying the Essential Next.js beta plugin.
78
+ Please share feedback (both good and bad) at ${blueBright `https://ntl.fyi/next-beta-feedback`}
79
+ `));
package/lib/index.js ADDED
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+ const { join, relative } = require('path');
3
+ const { copy, existsSync } = require('fs-extra');
4
+ const { ODB_FUNCTION_NAME } = require('./constants');
5
+ const { restoreCache, saveCache } = require('./helpers/cache');
6
+ const { getNextConfig, configureHandlerFunctions, generateRedirects } = require('./helpers/config');
7
+ const { moveStaticPages, movePublicFiles } = require('./helpers/files');
8
+ const { generateFunctions, setupImageFunction, generatePagesResolver } = require('./helpers/functions');
9
+ const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, } = require('./helpers/verification');
10
+ module.exports = {
11
+ async onPreBuild({ constants, netlifyConfig, utils: { build: { failBuild }, cache, }, }) {
12
+ var _a;
13
+ logBetaMessage();
14
+ const { publish } = netlifyConfig.build;
15
+ checkForRootPublish({ publish, failBuild });
16
+ verifyNetlifyBuildVersion({ failBuild, ...constants });
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';
21
+ },
22
+ async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
23
+ const { publish } = netlifyConfig.build;
24
+ checkNextSiteHasBuilt({ publish, failBuild });
25
+ const { appDir, basePath, i18n, images, target, ignore } = await getNextConfig({ publish, failBuild });
26
+ configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) });
27
+ await generateFunctions(constants, appDir);
28
+ await generatePagesResolver({ netlifyConfig, target, constants });
29
+ await movePublicFiles({ appDir, publish });
30
+ if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
31
+ await moveStaticPages({ target, failBuild, netlifyConfig, i18n });
32
+ }
33
+ await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
34
+ await generateRedirects({
35
+ netlifyConfig,
36
+ basePath,
37
+ i18n,
38
+ });
39
+ },
40
+ async onPostBuild({ netlifyConfig, utils: { cache }, constants }) {
41
+ await saveCache({ cache, publish: netlifyConfig.build.publish });
42
+ await checkZipSize(join(process.cwd(), constants.FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`));
43
+ },
44
+ onEnd() {
45
+ logBetaMessage();
46
+ },
47
+ };
@@ -0,0 +1,164 @@
1
+ const { promises, createWriteStream, existsSync } = require('fs');
2
+ const { Server } = require('http');
3
+ const { tmpdir } = require('os');
4
+ const path = require('path');
5
+ const { promisify } = require('util');
6
+ const streamPipeline = promisify(require('stream').pipeline);
7
+ const { Bridge } = require('@vercel/node/dist/bridge');
8
+ const fetch = require('node-fetch');
9
+ const makeHandler = () =>
10
+ // We return a function and then call `toString()` on it to serialise it as the launcher function
11
+ (conf, app, pageRoot, staticManifest = []) => {
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
+ // Set during the request as it needs the host header. Hoisted so we can define the function once
19
+ let base;
20
+ // Only do this if we have some static files moved to the CDN
21
+ if (staticManifest.length !== 0) {
22
+ // These are static page files that have been removed from the function bundle
23
+ // In most cases these are served from the CDN, but for rewrites Next may try to read them
24
+ // from disk. We need to intercept these and load them from the CDN instead
25
+ // Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know.
26
+ const staticFiles = new Set(staticManifest);
27
+ // Yes, you can cache stuff locally in a Lambda
28
+ const cacheDir = path.join(tmpdir(), 'next-static-cache');
29
+ // Grab the real fs.promises.readFile...
30
+ const readfileOrig = promises.readFile;
31
+ // ...then money-patch it to see if it's requesting a CDN file
32
+ promises.readFile = async (file, options) => {
33
+ // We only care about page files
34
+ if (file.startsWith(pageRoot)) {
35
+ // We only want the part after `pages/`
36
+ const filePath = file.slice(pageRoot.length + 1);
37
+ // Is it in the CDN and not local?
38
+ if (staticFiles.has(filePath) && !existsSync(file)) {
39
+ // This name is safe to use, because it's one that was already created by Next
40
+ const cacheFile = path.join(cacheDir, filePath);
41
+ // Have we already cached it? We ignore the cache if running locally to avoid staleness
42
+ if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) {
43
+ await promises.mkdir(path.dirname(cacheFile), { recursive: true });
44
+ // Append the path to our host and we can load it like a regular page
45
+ const url = `${base}/${filePath}`;
46
+ console.log(`Downloading ${url} to ${cacheFile}`);
47
+ const response = await fetch(url);
48
+ if (!response.ok) {
49
+ // Next catches this and returns it as a not found file
50
+ throw new Error(`Failed to fetch ${url}`);
51
+ }
52
+ // Stream it to disk
53
+ await streamPipeline(response.body, createWriteStream(cacheFile));
54
+ }
55
+ // Return the cache file
56
+ return readfileOrig(cacheFile, options);
57
+ }
58
+ }
59
+ return readfileOrig(file, options);
60
+ };
61
+ }
62
+ let NextServer;
63
+ try {
64
+ // next >= 11.0.1. Yay breaking changes in patch releases!
65
+ NextServer = require('next/dist/server/next-server').default;
66
+ }
67
+ catch (error) {
68
+ if (!error.message.includes("Cannot find module 'next/dist/server/next-server'")) {
69
+ // A different error, so rethrow it
70
+ throw error;
71
+ }
72
+ // Probably an old version of next
73
+ }
74
+ if (!NextServer) {
75
+ try {
76
+ // next < 11.0.1
77
+ // eslint-disable-next-line node/no-missing-require, import/no-unresolved
78
+ NextServer = require('next/dist/next-server/server/next-server').default;
79
+ }
80
+ catch (error) {
81
+ if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) {
82
+ throw error;
83
+ }
84
+ throw new Error('Could not find Next.js server');
85
+ }
86
+ }
87
+ const nextServer = new NextServer({
88
+ conf,
89
+ dir: path.resolve(__dirname, app),
90
+ customServer: false,
91
+ });
92
+ const requestHandler = nextServer.getRequestHandler();
93
+ const server = new Server(async (req, res) => {
94
+ try {
95
+ await requestHandler(req, res);
96
+ }
97
+ catch (error) {
98
+ console.error(error);
99
+ throw new Error('server function error');
100
+ }
101
+ });
102
+ const bridge = new Bridge(server);
103
+ bridge.listen();
104
+ return async (event, context) => {
105
+ var _a, _b, _c, _d;
106
+ // Next expects to be able to parse the query from the URL
107
+ const query = new URLSearchParams(event.queryStringParameters).toString();
108
+ event.path = query ? `${event.path}?${query}` : event.path;
109
+ // Only needed if we're intercepting static files
110
+ if (staticManifest.length !== 0) {
111
+ const { host } = event.headers;
112
+ const protocol = event.headers['x-forwarded-proto'] || 'http';
113
+ base = `${protocol}://${host}`;
114
+ }
115
+ const { headers, ...result } = await bridge.launcher(event, context);
116
+ /** @type import("@netlify/functions").HandlerResponse */
117
+ // Convert all headers to multiValueHeaders
118
+ const multiValueHeaders = {};
119
+ for (const key of Object.keys(headers)) {
120
+ if (Array.isArray(headers[key])) {
121
+ multiValueHeaders[key] = headers[key];
122
+ }
123
+ else {
124
+ multiValueHeaders[key] = [headers[key]];
125
+ }
126
+ }
127
+ if ((_b = (_a = multiValueHeaders['set-cookie']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.includes('__prerender_bypass')) {
128
+ delete multiValueHeaders.etag;
129
+ multiValueHeaders['cache-control'] = ['no-cache'];
130
+ }
131
+ // Sending SWR headers causes undefined behaviour with the Netlify CDN
132
+ if ((_d = (_c = multiValueHeaders['cache-control']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.includes('stale-while-revalidate')) {
133
+ multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
134
+ }
135
+ return {
136
+ ...result,
137
+ multiValueHeaders,
138
+ isBase64Encoded: result.encoding === 'base64',
139
+ };
140
+ };
141
+ };
142
+ const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) => `
143
+ const { Server } = require("http");
144
+ const { tmpdir } = require('os')
145
+ const { promises, createWriteStream, existsSync } = require("fs");
146
+ const { promisify } = require('util')
147
+ const streamPipeline = promisify(require('stream').pipeline)
148
+ // We copy the file here rather than requiring from the node module
149
+ const { Bridge } = require("./bridge");
150
+ const fetch = require('node-fetch')
151
+
152
+ const { builder } = require("@netlify/functions");
153
+ const { config } = require("${publishDir}/required-server-files.json")
154
+ let staticManifest
155
+ try {
156
+ staticManifest = require("${publishDir}/static-manifest.json")
157
+ } catch {}
158
+ const path = require("path");
159
+ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages"));
160
+ exports.handler = ${isODB
161
+ ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest));`
162
+ : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest);`}
163
+ `;
164
+ module.exports = getHandler;