@netlify/plugin-nextjs 4.0.0-beta.5 → 4.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  # Essential Next.js Build Plugin (beta)
4
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:
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">
@@ -16,16 +16,19 @@
16
16
 
17
17
  ## What's new in this version
18
18
 
19
- Version 4 is a complete rewrite of the Essential Next.js plugin. For full details of everything that's new, see [the v4 release notes](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/release-notes/v4.md)
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)
20
21
 
21
22
  ## Installing the beta
22
23
 
23
-
24
24
  - Install the module:
25
+
25
26
  ```shell
26
27
  npm install -D @netlify/plugin-nextjs@beta
27
28
  ```
28
- - Change the `publish` directory to `.next` and add the plugin to `netlify.toml` if not already installed:
29
+
30
+ - Change the `publish` directory to `.next` and add the plugin to `netlify.toml` if not already installed:
31
+
29
32
  ```toml
30
33
  [build]
31
34
  publish = ".next"
@@ -34,14 +37,27 @@ publish = ".next"
34
37
  package = "@netlify/plugin-nextjs"
35
38
  ```
36
39
 
37
- 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.
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.
38
49
 
39
- 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.
50
+ If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
40
51
 
41
- 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.
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.
42
55
 
43
- If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
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.
44
59
 
45
60
  ## Beta feedback
46
61
 
47
- Please share any thoughts, feedback or questions about the beta [in our discussion](https://github.com/netlify/netlify-plugin-nextjs/discussions/706).
62
+ Please share any thoughts, feedback or questions about the beta
63
+ [in our discussion](https://github.com/netlify/netlify-plugin-nextjs/discussions/706).
@@ -59,24 +59,13 @@ exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
59
59
  if (i18n) {
60
60
  netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
61
61
  }
62
- const staticManifest = join(netlifyConfig.build.publish, 'static-manifest.json');
63
- if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES && existsSync(staticManifest)) {
64
- // Static page files need to have a forced redirect for preview mode. Otherwise it's non-forced
65
- const staticFiles = await readJSON(staticManifest);
66
- netlifyConfig.redirects.push(...staticFiles.map((file) => ({
67
- from: `${basePath}/${file}`,
68
- to: HANDLER_FUNCTION_PATH,
69
- status: 200,
70
- force: true,
71
- conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
72
- })));
73
- }
74
62
  // This is only used in prod, so dev uses `next dev` directly
75
63
  netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 }, {
76
64
  from: `${basePath}/*`,
77
65
  to: HANDLER_FUNCTION_PATH,
78
66
  status: 200,
79
67
  conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
68
+ force: true,
80
69
  }, ...redirects.map((redirect) => ({
81
70
  from: `${basePath}${redirect}`,
82
71
  to: ODB_FUNCTION_PATH,
@@ -114,7 +103,7 @@ exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) =>
114
103
  (_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
115
104
  netlifyConfig.functions[functionName].node_bundler = 'nft';
116
105
  (_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
117
- netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, ...ignore.map((path) => `!${slash(path)}`));
106
+ netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, `!${publish}/server/**/*.js.nft.json`, ...ignore.map((path) => `!${slash(path)}`));
118
107
  const nextRoot = resolveModuleRoot('next');
119
108
  if (nextRoot) {
120
109
  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`);
@@ -1,43 +1,99 @@
1
1
  // @ts-check
2
+ const { cpus } = require('os');
3
+ const { yellowBright } = require('chalk');
2
4
  const { existsSync, readJson, move, cpSync, copy, writeJson } = require('fs-extra');
5
+ const globby = require('globby');
6
+ const { outdent } = require('outdent');
3
7
  const pLimit = require('p-limit');
4
8
  const { join } = require('pathe');
5
- const TEST_ROUTE = /\/\[[^/]+?](?=\/|$)/;
9
+ const slash = require('slash');
10
+ const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/;
6
11
  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
+ 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
+ exports.matchMiddleware = matchMiddleware;
22
+ exports.stripLocale = stripLocale;
23
+ exports.isDynamicRoute = isDynamicRoute;
24
+ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
25
+ console.log('Moving static page files to serve from CDN...');
26
+ const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
27
+ const root = join(outputDir, 'pages');
28
+ // Load the middleware manifest so we can check if a file matches it before moving
29
+ let middleware;
30
+ const manifestPath = join(outputDir, 'middleware-manifest.json');
31
+ if (existsSync(manifestPath)) {
32
+ const manifest = await readJson(manifestPath);
33
+ if (manifest === null || manifest === void 0 ? void 0 : manifest.middleware) {
34
+ middleware = Object.keys(manifest.middleware).map((path) => path.slice(1));
35
+ }
12
36
  }
13
37
  const files = [];
14
38
  const moveFile = async (file) => {
15
39
  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);
40
+ files.push(file);
41
+ const dest = join(netlifyConfig.build.publish, file);
20
42
  await move(source, dest);
21
43
  };
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')) {
44
+ // Move all static files, except error documents and nft manifests
45
+ const pages = await globby(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
46
+ cwd: root,
47
+ dot: true,
48
+ });
49
+ const matchingMiddleware = new Set();
50
+ const matchedPages = new Set();
51
+ // Limit concurrent file moves to number of cpus or 2 if there is only 1
52
+ const limit = pLimit(Math.max(2, cpus().length));
53
+ const promises = pages.map(async (rawPath) => {
54
+ const filePath = slash(rawPath);
55
+ if (isDynamicRoute(filePath)) {
56
+ return;
57
+ }
58
+ // Middleware matches against the unlocalised path
59
+ const unlocalizedPath = stripLocale(rawPath, i18n === null || i18n === void 0 ? void 0 : i18n.locales);
60
+ const middlewarePath = matchMiddleware(middleware, unlocalizedPath);
61
+ // 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
62
+ if (middlewarePath) {
63
+ matchingMiddleware.add(middlewarePath);
64
+ matchedPages.add(rawPath);
30
65
  return;
31
66
  }
32
67
  return limit(moveFile, filePath);
33
68
  });
34
69
  await Promise.all(promises);
35
- console.log(`Moved ${files.length} page files`);
70
+ console.log(`Moved ${files.length} files`);
71
+ if (matchedPages.size !== 0) {
72
+ console.log(yellowBright(outdent `
73
+ Skipped moving ${matchedPages.size} ${matchedPages.size === 1 ? 'file because it matches' : 'files because they match'} middleware, so cannot be deployed to the CDN and will be served from the origin instead. This is fine, but we're letting you know because it may not be what you expect.
74
+ `));
75
+ console.log(outdent `
76
+ The following middleware matched statically-rendered pages:
77
+
78
+ ${yellowBright([...matchingMiddleware].map((mid) => `- /${mid}/_middleware`).join('\n'))}
79
+ `);
80
+ // There could potentially be thousands of matching pages, so we don't want to spam the console with this
81
+ if (matchedPages.size < 50) {
82
+ console.log(outdent `
83
+ The following files matched middleware and were not moved to the CDN:
84
+
85
+ ${yellowBright([...matchedPages].map((mid) => `- ${mid}`).join('\n'))}
86
+ `);
87
+ }
88
+ }
36
89
  // Write the manifest for use in the serverless functions
37
90
  await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), files);
38
91
  if (i18n === null || i18n === void 0 ? void 0 : i18n.defaultLocale) {
39
92
  // Copy the default locale into the root
40
- await copy(join(netlifyConfig.build.publish, i18n.defaultLocale), `${netlifyConfig.build.publish}/`);
93
+ const defaultLocaleDir = join(netlifyConfig.build.publish, i18n.defaultLocale);
94
+ if (existsSync(defaultLocaleDir)) {
95
+ await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`);
96
+ }
41
97
  }
42
98
  };
43
99
  exports.movePublicFiles = async ({ appDir, publish }) => {
@@ -1,7 +1,7 @@
1
1
  const { existsSync, promises } = require('fs');
2
2
  const path = require('path');
3
3
  const { relative } = require('path');
4
- const { yellowBright, greenBright, blueBright, redBright } = require('chalk');
4
+ const { yellowBright, greenBright, blueBright, redBright, reset } = require('chalk');
5
5
  const { async: StreamZip } = require('node-stream-zip');
6
6
  const outdent = require('outdent');
7
7
  const prettyBytes = require('pretty-bytes');
@@ -17,26 +17,42 @@ exports.verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuil
17
17
  `);
18
18
  }
19
19
  };
20
+ exports.checkForOldFunctions = async ({ functions }) => {
21
+ const oldFunctions = (await functions.list()).filter(({ name }) => name.startsWith('next_'));
22
+ if (oldFunctions.length !== 0) {
23
+ console.log(yellowBright(outdent `
24
+ 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_".
25
+
26
+ ${reset(oldFunctions.map(({ name }) => `- ${name}`).join('\n'))}
27
+
28
+ 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:
29
+
30
+ ${reset(oldFunctions.map(({ mainFile }) => `- ${path.relative(process.cwd(), path.dirname(mainFile))}`).join('\n'))}
31
+ `));
32
+ }
33
+ };
20
34
  exports.checkNextSiteHasBuilt = ({ publish, failBuild }) => {
21
35
  if (!existsSync(path.join(publish, 'BUILD_ID'))) {
22
36
  return failBuild(outdent `
23
37
  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
38
  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
39
  If you are using "next export" then the Essential Next.js plugin should be removed. See https://ntl.fyi/remove-plugin for details.
26
- `);
40
+ `);
27
41
  }
28
42
  if (existsSync(path.join(publish, 'export-detail.json'))) {
29
43
  failBuild(outdent `
30
44
  Detected that "next export" was run, but site is incorrectly publishing the ".next" directory.
31
45
  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.`);
46
+ See https://ntl.fyi/remove-plugin for more details on how to remove this plugin.
47
+ `);
33
48
  }
34
49
  };
35
50
  exports.checkForRootPublish = ({ publish, failBuild }) => {
36
51
  if (path.resolve(publish) === path.resolve('.')) {
37
52
  failBuild(outdent `
38
53
  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.`);
54
+ 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
+ `);
40
56
  }
41
57
  };
42
58
  // 50MB, which is the documented max, though the hard max seems to be higher
@@ -52,11 +68,9 @@ exports.checkZipSize = async (file, maxSize = LAMBDA_MAX_SIZE) => {
52
68
  }
53
69
  // We don't fail the build, because the actual hard max size is larger so it might still succeed
54
70
  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
-
71
+ The function zip ${yellowBright(relative(process.cwd(), file))} size is ${prettyBytes(size)}, which is larger than the maximum supported size of ${prettyBytes(maxSize)}.
72
+ There are a few reasons this could happen. You may have accidentally bundled a large dependency, or you might have a
73
+ large number of pre-rendered pages included.
60
74
  `));
61
75
  const zip = new StreamZip({ file });
62
76
  console.log(`Contains ${await zip.entriesCount} files`);
package/lib/index.js CHANGED
@@ -1,12 +1,11 @@
1
- // @ts-check
2
1
  const { join, relative } = require('path');
3
- const { copy, existsSync } = require('fs-extra');
4
2
  const { ODB_FUNCTION_NAME } = require('./constants');
5
3
  const { restoreCache, saveCache } = require('./helpers/cache');
6
4
  const { getNextConfig, configureHandlerFunctions, generateRedirects } = require('./helpers/config');
7
5
  const { moveStaticPages, movePublicFiles } = require('./helpers/files');
8
6
  const { generateFunctions, setupImageFunction, generatePagesResolver } = require('./helpers/functions');
9
- const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, } = require('./helpers/verification');
7
+ const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, checkForOldFunctions, } = require('./helpers/verification');
8
+ /** @type import("@netlify/build").NetlifyPlugin */
10
9
  module.exports = {
11
10
  async onPreBuild({ constants, netlifyConfig, utils: { build: { failBuild }, cache, }, }) {
12
11
  var _a;
@@ -28,6 +27,9 @@ module.exports = {
28
27
  await generatePagesResolver({ netlifyConfig, target, constants });
29
28
  await movePublicFiles({ appDir, publish });
30
29
  if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
30
+ console.log("The flag 'EXPERIMENTAL_MOVE_STATIC_PAGES' is no longer required, as it is now the default. To disable this behavior, set the env var 'SERVE_STATIC_FILES_FROM_ORIGIN' to 'true'");
31
+ }
32
+ if (!process.env.SERVE_STATIC_FILES_FROM_ORIGIN) {
31
33
  await moveStaticPages({ target, failBuild, netlifyConfig, i18n });
32
34
  }
33
35
  await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
@@ -37,8 +39,9 @@ module.exports = {
37
39
  i18n,
38
40
  });
39
41
  },
40
- async onPostBuild({ netlifyConfig, utils: { cache }, constants: { FUNCTIONS_DIST } }) {
42
+ async onPostBuild({ netlifyConfig, utils: { cache, functions }, constants: { FUNCTIONS_DIST } }) {
41
43
  await saveCache({ cache, publish: netlifyConfig.build.publish });
44
+ await checkForOldFunctions({ functions });
42
45
  await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`));
43
46
  },
44
47
  onEnd() {
@@ -103,6 +103,8 @@ const makeHandler = () =>
103
103
  bridge.listen();
104
104
  return async (event, context) => {
105
105
  var _a, _b, _c, _d;
106
+ // Ensure that paths are encoded - but don't double-encode them
107
+ event.path = new URL(event.path, event.rawUrl).pathname;
106
108
  // Next expects to be able to parse the query from the URL
107
109
  const query = new URLSearchParams(event.queryStringParameters).toString();
108
110
  event.path = query ? `${event.path}?${query}` : event.path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.0.0-beta.5",
3
+ "version": "4.0.0-beta.9",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "build:demo": "next build demo",
12
12
  "cy:open": "cypress open --config-file cypress/config/all.json",
13
- "cy:run": "cypress run --config-file cypress/config/all.json",
13
+ "cy:run": "cypress run --config-file ../cypress/config/ci.json",
14
14
  "dev:demo": "next dev demo",
15
15
  "format": "run-s format:check-fix:*",
16
16
  "format:ci": "run-s format:check:*",
@@ -52,11 +52,12 @@
52
52
  },
53
53
  "homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
54
54
  "dependencies": {
55
- "@netlify/functions": "^0.8.0",
55
+ "@netlify/functions": "^0.9.0",
56
56
  "@netlify/ipx": "^0.0.7",
57
57
  "@vercel/node": "^1.11.2-canary.4",
58
58
  "chalk": "^4.1.2",
59
59
  "fs-extra": "^10.0.0",
60
+ "globby": "^11.0.4",
60
61
  "moize": "^6.1.0",
61
62
  "node-fetch": "^2.6.6",
62
63
  "node-stream-zip": "^1.15.0",
@@ -71,7 +72,8 @@
71
72
  "devDependencies": {
72
73
  "@babel/core": "^7.15.8",
73
74
  "@babel/preset-env": "^7.15.8",
74
- "@netlify/eslint-config-node": "^3.3.0",
75
+ "@netlify/build": "^18.25.1",
76
+ "@netlify/eslint-config-node": "^3.3.7",
75
77
  "@testing-library/cypress": "^8.0.1",
76
78
  "@types/fs-extra": "^9.0.13",
77
79
  "@types/jest": "^27.0.2",