@netlify/plugin-nextjs 4.0.0-beta.2 → 4.0.0-beta.6

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
@@ -14,6 +14,9 @@
14
14
  </a>
15
15
  </p>
16
16
 
17
+ ## What's new in this version
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)
17
20
 
18
21
  ## Installing the beta
19
22
 
@@ -31,7 +34,13 @@ publish = ".next"
31
34
  package = "@netlify/plugin-nextjs"
32
35
  ```
33
36
 
34
- If you previously set `target: "serverless"` in your `next.config.js` this is no longer needed and can be removed.
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.
38
+
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.
40
+
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.
42
+
43
+ If you are using Nx, then you will need to point `publish` to the folder inside `dist`, e.g. `dist/apps/myapp/.next`.
35
44
 
36
45
  ## Beta feedback
37
46
 
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
- const { readJSON } = require('fs-extra');
2
+ const { readJSON, existsSync } = require('fs-extra');
3
3
  const { join, dirname, relative } = require('pathe');
4
+ const slash = require('slash');
4
5
  const defaultFailBuild = (message, { error }) => {
5
6
  throw new Error(`${message}\n${error && error.stack}`);
6
7
  };
@@ -58,12 +59,23 @@ exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
58
59
  if (i18n) {
59
60
  netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 });
60
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
+ }
61
74
  // This is only used in prod, so dev uses `next dev` directly
62
75
  netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 }, {
63
76
  from: `${basePath}/*`,
64
77
  to: HANDLER_FUNCTION_PATH,
65
78
  status: 200,
66
- force: true,
67
79
  conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
68
80
  }, ...redirects.map((redirect) => ({
69
81
  from: `${basePath}${redirect}`,
@@ -91,24 +103,27 @@ const resolveModuleRoot = (moduleName) => {
91
103
  return null;
92
104
  }
93
105
  };
106
+ const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'];
94
107
  exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) => {
95
108
  var _a;
96
109
  /* eslint-disable no-underscore-dangle */
97
110
  (_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
98
- netlifyConfig.functions._ipx.node_bundler = 'esbuild';
111
+ netlifyConfig.functions._ipx.node_bundler = 'nft';
99
112
  [HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
100
113
  var _a, _b;
101
114
  (_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
102
115
  netlifyConfig.functions[functionName].node_bundler = 'nft';
103
116
  (_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
104
- netlifyConfig.functions[functionName].included_files.push(`${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, ...ignore.map((path) => `!${path}`));
117
+ 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)}`));
105
118
  const nextRoot = resolveModuleRoot('next');
106
119
  if (nextRoot) {
107
120
  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`);
108
121
  }
109
- const sharpRoot = resolveModuleRoot('sharp');
110
- if (sharpRoot) {
111
- netlifyConfig.functions[functionName].included_files.push(`!${sharpRoot}/**/*`);
112
- }
122
+ DEFAULT_EXCLUDED_MODULES.forEach((moduleName) => {
123
+ const moduleRoot = resolveModuleRoot(moduleName);
124
+ if (moduleRoot) {
125
+ netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`);
126
+ }
127
+ });
113
128
  });
114
129
  };
@@ -0,0 +1,51 @@
1
+ // @ts-check
2
+ const { cpus } = require('os');
3
+ const { existsSync, readJson, move, cpSync, copy, writeJson } = require('fs-extra');
4
+ const globby = require('globby');
5
+ const pLimit = require('p-limit');
6
+ const { join } = require('pathe');
7
+ const slash = require('slash');
8
+ const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/;
9
+ const isDynamicRoute = (route) => TEST_ROUTE.test(route);
10
+ exports.moveStaticPages = async ({ netlifyConfig, target, i18n, failBuild }) => {
11
+ console.log('Moving static page files to serve from CDN...');
12
+ const root = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless', 'pages');
13
+ const files = [];
14
+ const moveFile = async (file) => {
15
+ const source = join(root, file);
16
+ files.push(file);
17
+ const dest = join(netlifyConfig.build.publish, file);
18
+ await move(source, dest);
19
+ };
20
+ // Move all static files, except error documents and nft manifests
21
+ const pages = await globby(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
22
+ cwd: root,
23
+ dot: true,
24
+ });
25
+ // Limit concurrent file moves to number of cpus or 2 if there is only 1
26
+ const limit = pLimit(Math.max(2, cpus().length));
27
+ const promises = pages.map(async (rawPath) => {
28
+ const filePath = slash(rawPath);
29
+ if (isDynamicRoute(filePath)) {
30
+ return;
31
+ }
32
+ return limit(moveFile, filePath);
33
+ });
34
+ await Promise.all(promises);
35
+ console.log(`Moved ${files.length} 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
+ const defaultLocaleDir = join(netlifyConfig.build.publish, i18n.defaultLocale);
41
+ if (existsSync(defaultLocaleDir)) {
42
+ await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`);
43
+ }
44
+ }
45
+ };
46
+ exports.movePublicFiles = async ({ appDir, publish }) => {
47
+ const publicDir = join(appDir, 'public');
48
+ if (existsSync(publicDir)) {
49
+ await copy(publicDir, `${publish}/`);
50
+ }
51
+ };
@@ -1,5 +1,5 @@
1
- const { join, relative } = require('path');
2
1
  const { copyFile, ensureDir, writeFile, writeJSON } = require('fs-extra');
2
+ const { join, relative } = require('pathe');
3
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');
@@ -1,13 +1,11 @@
1
+ const { existsSync, promises } = require('fs');
1
2
  const path = require('path');
2
- const { yellowBright, greenBright, blueBright } = require('chalk');
3
- const { existsSync } = require('fs-extra');
3
+ const { relative } = require('path');
4
+ const { yellowBright, greenBright, blueBright, redBright } = 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 }) => {
@@ -41,7 +39,41 @@ exports.checkForRootPublish = ({ publish, failBuild }) => {
41
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.`);
42
40
  }
43
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
+ };
44
76
  exports.logBetaMessage = () => console.log(greenBright(outdent `
45
77
  Thank you for trying the Essential Next.js beta plugin.
46
- Please share feedback at ${blueBright `https://ntl.fyi/next-beta-feedback`}
78
+ Please share feedback (both good and bad) at ${blueBright `https://ntl.fyi/next-beta-feedback`}
47
79
  `));
package/lib/index.js CHANGED
@@ -1,29 +1,33 @@
1
1
  // @ts-check
2
2
  const { join, relative } = require('path');
3
- const { copy, existsSync } = require('fs-extra');
3
+ const { ODB_FUNCTION_NAME } = require('./constants');
4
4
  const { restoreCache, saveCache } = require('./helpers/cache');
5
5
  const { getNextConfig, configureHandlerFunctions, generateRedirects } = require('./helpers/config');
6
+ const { moveStaticPages, movePublicFiles } = require('./helpers/files');
6
7
  const { generateFunctions, setupImageFunction, generatePagesResolver } = require('./helpers/functions');
7
- const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, verifyBuildTarget, checkForRootPublish, logBetaMessage, } = require('./helpers/verification');
8
+ const { verifyNetlifyBuildVersion, checkNextSiteHasBuilt, checkForRootPublish, logBetaMessage, checkZipSize, } = require('./helpers/verification');
8
9
  module.exports = {
9
10
  async onPreBuild({ constants, netlifyConfig, utils: { build: { failBuild }, cache, }, }) {
11
+ var _a;
10
12
  logBetaMessage();
11
13
  const { publish } = netlifyConfig.build;
12
14
  checkForRootPublish({ publish, failBuild });
13
15
  verifyNetlifyBuildVersion({ failBuild, ...constants });
14
16
  await restoreCache({ cache, publish });
17
+ (_a = netlifyConfig.build).environment || (_a.environment = {});
18
+ // eslint-disable-next-line unicorn/consistent-destructuring
19
+ netlifyConfig.build.environment.NEXT_PRIVATE_TARGET = 'server';
15
20
  },
16
21
  async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
17
22
  const { publish } = netlifyConfig.build;
18
23
  checkNextSiteHasBuilt({ publish, failBuild });
19
24
  const { appDir, basePath, i18n, images, target, ignore } = await getNextConfig({ publish, failBuild });
20
- verifyBuildTarget(target);
21
25
  configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) });
22
26
  await generateFunctions(constants, appDir);
23
27
  await generatePagesResolver({ netlifyConfig, target, constants });
24
- const publicDir = join(appDir, 'public');
25
- if (existsSync(publicDir)) {
26
- await copy(publicDir, `${publish}/`);
28
+ await movePublicFiles({ appDir, publish });
29
+ if (process.env.EXPERIMENTAL_MOVE_STATIC_PAGES) {
30
+ await moveStaticPages({ target, failBuild, netlifyConfig, i18n });
27
31
  }
28
32
  await setupImageFunction({ constants, imageconfig: images, netlifyConfig, basePath });
29
33
  await generateRedirects({
@@ -32,8 +36,9 @@ module.exports = {
32
36
  i18n,
33
37
  });
34
38
  },
35
- async onPostBuild({ netlifyConfig, utils: { cache } }) {
36
- return saveCache({ cache, publish: netlifyConfig.build.publish });
39
+ async onPostBuild({ netlifyConfig, utils: { cache }, constants: { FUNCTIONS_DIST } }) {
40
+ await saveCache({ cache, publish: netlifyConfig.build.publish });
41
+ await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`));
37
42
  },
38
43
  onEnd() {
39
44
  logBetaMessage();
@@ -1,15 +1,64 @@
1
+ const { promises, createWriteStream, existsSync } = require('fs');
1
2
  const { Server } = require('http');
3
+ const { tmpdir } = require('os');
2
4
  const path = require('path');
5
+ const { promisify } = require('util');
6
+ const streamPipeline = promisify(require('stream').pipeline);
3
7
  const { Bridge } = require('@vercel/node/dist/bridge');
8
+ const fetch = require('node-fetch');
4
9
  const makeHandler = () =>
5
10
  // We return a function and then call `toString()` on it to serialise it as the launcher function
6
- (conf, app) => {
7
- // This is just so nft knows about the page entrypoints
11
+ (conf, app, pageRoot, staticManifest = []) => {
12
+ // This is just so nft knows about the page entrypoints. It's not actually used
8
13
  try {
9
14
  // eslint-disable-next-line node/no-missing-require
10
15
  require.resolve('./pages.js');
11
16
  }
12
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
+ }
13
62
  let NextServer;
14
63
  try {
15
64
  // next >= 11.0.1. Yay breaking changes in patch releases!
@@ -53,10 +102,16 @@ const makeHandler = () =>
53
102
  const bridge = new Bridge(server);
54
103
  bridge.listen();
55
104
  return async (event, context) => {
56
- var _a, _b;
105
+ var _a, _b, _c, _d;
57
106
  // Next expects to be able to parse the query from the URL
58
107
  const query = new URLSearchParams(event.queryStringParameters).toString();
59
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
+ }
60
115
  const { headers, ...result } = await bridge.launcher(event, context);
61
116
  /** @type import("@netlify/functions").HandlerResponse */
62
117
  // Convert all headers to multiValueHeaders
@@ -73,6 +128,10 @@ const makeHandler = () =>
73
128
  delete multiValueHeaders.etag;
74
129
  multiValueHeaders['cache-control'] = ['no-cache'];
75
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
+ }
76
135
  return {
77
136
  ...result,
78
137
  multiValueHeaders,
@@ -82,13 +141,24 @@ const makeHandler = () =>
82
141
  };
83
142
  const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) => `
84
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)
85
148
  // We copy the file here rather than requiring from the node module
86
149
  const { Bridge } = require("./bridge");
150
+ const fetch = require('node-fetch')
151
+
87
152
  const { builder } = require("@netlify/functions");
88
153
  const { config } = require("${publishDir}/required-server-files.json")
154
+ let staticManifest
155
+ try {
156
+ staticManifest = require("${publishDir}/static-manifest.json")
157
+ } catch {}
89
158
  const path = require("path");
159
+ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages"));
90
160
  exports.handler = ${isODB
91
- ? `builder((${makeHandler().toString()})(config, "${appDir}"));`
92
- : `(${makeHandler().toString()})(config, "${appDir}");`}
161
+ ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest));`
162
+ : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest);`}
93
163
  `;
94
164
  module.exports = getHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.0.0-beta.2",
3
+ "version": "4.0.0-beta.6",
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,14 +52,19 @@
52
52
  },
53
53
  "homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
54
54
  "dependencies": {
55
- "@netlify/functions": "^0.7.2",
55
+ "@netlify/functions": "^0.8.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",
62
+ "node-fetch": "^2.6.6",
63
+ "node-stream-zip": "^1.15.0",
61
64
  "outdent": "^0.8.0",
65
+ "p-limit": "^3.1.0",
62
66
  "pathe": "^0.2.0",
67
+ "pretty-bytes": "^5.6.0",
63
68
  "semver": "^7.3.5",
64
69
  "slash": "^3.0.0",
65
70
  "tiny-glob": "^0.2.9"
@@ -69,6 +74,7 @@
69
74
  "@babel/preset-env": "^7.15.8",
70
75
  "@netlify/eslint-config-node": "^3.3.0",
71
76
  "@testing-library/cypress": "^8.0.1",
77
+ "@types/fs-extra": "^9.0.13",
72
78
  "@types/jest": "^27.0.2",
73
79
  "@types/mocha": "^9.0.0",
74
80
  "babel-jest": "^27.2.5",
@@ -78,7 +84,7 @@
78
84
  "husky": "^4.3.0",
79
85
  "jest": "^27.0.0",
80
86
  "netlify-plugin-cypress": "^2.2.0",
81
- "next": "^11.1.2",
87
+ "next": "^12.0.2",
82
88
  "npm-run-all": "^4.1.5",
83
89
  "prettier": "^2.1.2",
84
90
  "react": "^17.0.1",