From 8cb478cde68b99d7e429ca68202875b60a6b8cea Mon Sep 17 00:00:00 2001 From: Lindsay Levine Date: Mon, 29 Mar 2021 11:07:54 -0400 Subject: [PATCH 1/4] feat: cache .next/cache between builds --- helpers/cacheBuild.js | 32 +++++++++++ helpers/getNextConfig.js | 6 +- helpers/hasCorrectNextConfig.js | 2 +- helpers/isStaticExportProject.js | 2 +- index.js | 8 ++- src/lib/helpers/getI18n.js | 2 +- src/lib/helpers/getNextDistDir.js | 2 +- src/lib/helpers/getPrerenderManifest.js | 2 +- .../dist_dir_next_config/next.config.js | 4 ++ test/index.js | 57 +++++++++++++++++++ 10 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 helpers/cacheBuild.js create mode 100644 test/fixtures/dist_dir_next_config/next.config.js diff --git a/helpers/cacheBuild.js b/helpers/cacheBuild.js new file mode 100644 index 0000000000..17f5b4c742 --- /dev/null +++ b/helpers/cacheBuild.js @@ -0,0 +1,32 @@ +const path = require('path') + +const DEFAULT_DIST_DIR = '.next' + +// Account for possible custom distDir +const getPath = (distDir, source) => { + return path.join(distDir || DEFAULT_DIST_DIR, source) +} + +const restoreCache = async ({ cache, distDir }) => { + const cacheDir = getPath(distDir, 'cache') + if (await cache.restore(cacheDir)) { + console.log('Next.js cache restored.') + } else { + console.log('No Next.js cache to restore.') + } +} + +const saveCache = async ({ cache, distDir }) => { + const cacheDir = getPath(distDir, 'cache') + const buildManifest = getPath(distDir, 'build-manifest.json') + if (await cache.save(cacheDir, { digests: [buildManifest] })) { + console.log('Next.js cache saved.') + } else { + console.log('No Next.js cache to save.') + } +} + +module.exports = { + restoreCache, + saveCache, +} diff --git a/helpers/getNextConfig.js b/helpers/getNextConfig.js index d6b0eb10ab..19e0da9f39 100644 --- a/helpers/getNextConfig.js +++ b/helpers/getNextConfig.js @@ -4,8 +4,9 @@ const { resolve } = require('path') const moize = require('moize') -// Load next.config.js -const getNextConfig = async function (failBuild = defaultFailBuild) { +// We used to cache nextConfig for any cwd. Now we pass process.cwd() to cache +// (or memoize) nextConfig per cwd. +const getNextConfig = async function (cwd, failBuild = defaultFailBuild) { // We cannot load `next` at the top-level because we validate whether the // site is using `next` inside `onPreBuild`. const { PHASE_PRODUCTION_BUILD } = require('next/constants') @@ -24,4 +25,5 @@ const defaultFailBuild = function (message, { error }) { throw new Error(`${message}\n${error.stack}`) } +// module.exports = process.env.NODE_ENV === 'test' ? getNextConfig : moizedGetNextConfig module.exports = moizedGetNextConfig diff --git a/helpers/hasCorrectNextConfig.js b/helpers/hasCorrectNextConfig.js index 36bf0ff2b5..1bc2da3a45 100644 --- a/helpers/hasCorrectNextConfig.js +++ b/helpers/hasCorrectNextConfig.js @@ -5,7 +5,7 @@ const hasCorrectNextConfig = async ({ nextConfigPath, failBuild }) => { // In the plugin's case, no config is valid because we'll make it ourselves if (nextConfigPath === undefined) return true - const { target } = await getNextConfig(failBuild) + const { target } = await getNextConfig(process.cwd(), failBuild) // If the next config exists, log warning if target isnt in acceptableTargets const acceptableTargets = ['serverless', 'experimental-serverless-trace'] diff --git a/helpers/isStaticExportProject.js b/helpers/isStaticExportProject.js index ec22bf3276..f5a1a12d26 100644 --- a/helpers/isStaticExportProject.js +++ b/helpers/isStaticExportProject.js @@ -17,7 +17,7 @@ const isStaticExportProject = ({ build, scripts }) => { if (isStaticExport) { console.log( - `Static HTML export Next.js projects do not require this plugin. Check your project's build command for 'next export'.`, + 'NOTE: Static HTML export Next.js projects (projects that use `next export`) do not require most of this plugin. For these sites, this plugin *only* caches builds.', ) } diff --git a/index.js b/index.js index 275c2c43cc..4c2b7f87f0 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ const validateNextUsage = require('./helpers/validateNextUsage') const doesNotNeedPlugin = require('./helpers/doesNotNeedPlugin') const getNextConfig = require('./helpers/getNextConfig') const copyUnstableIncludedDirs = require('./helpers/copyUnstableIncludedDirs') +const { restoreCache, saveCache } = require('./helpers/cacheBuild') const pWriteFile = util.promisify(fs.writeFile) @@ -27,6 +28,9 @@ module.exports = { return failBuild('Could not find a package.json for this project') } + const nextConfig = await getNextConfig(process.cwd(), utils.failBuild) + await restoreCache({ cache: utils.cache, distDir: nextConfig.distDir }) + if (await doesNotNeedPlugin({ netlifyConfig, packageJson, failBuild })) { return } @@ -60,12 +64,14 @@ module.exports = { await nextOnNetlify({ functionsDir: FUNCTIONS_SRC, publishDir: PUBLISH_DIR }) }, + async onPostBuild({ netlifyConfig, packageJson, constants: { FUNCTIONS_DIST }, utils }) { if (await doesNotNeedPlugin({ netlifyConfig, packageJson, utils })) { return } - const nextConfig = await getNextConfig(utils.failBuild) + const nextConfig = await getNextConfig(process.cwd(), utils.failBuild) + await saveCache({ cache: utils.cache, distDir: nextConfig.distDir }) copyUnstableIncludedDirs({ nextConfig, functionsDist: FUNCTIONS_DIST }) }, } diff --git a/src/lib/helpers/getI18n.js b/src/lib/helpers/getI18n.js index 817446b68e..955fc9551f 100644 --- a/src/lib/helpers/getI18n.js +++ b/src/lib/helpers/getI18n.js @@ -2,7 +2,7 @@ const getNextConfig = require('../../../helpers/getNextConfig') const getI18n = async () => { - const nextConfig = await getNextConfig() + const nextConfig = await getNextConfig(process.cwd()) return nextConfig.i18n || { locales: [] } } diff --git a/src/lib/helpers/getNextDistDir.js b/src/lib/helpers/getNextDistDir.js index 2bcf34e9f9..5d3b8e7dd7 100644 --- a/src/lib/helpers/getNextDistDir.js +++ b/src/lib/helpers/getNextDistDir.js @@ -3,7 +3,7 @@ const { join } = require('path') const getNextConfig = require('../../../helpers/getNextConfig') const getNextDistDir = async () => { - const nextConfig = await getNextConfig() + const nextConfig = await getNextConfig(process.cwd()) return join('.', nextConfig.distDir) } diff --git a/src/lib/helpers/getPrerenderManifest.js b/src/lib/helpers/getPrerenderManifest.js index 0e0aa7555e..e58970f5e6 100644 --- a/src/lib/helpers/getPrerenderManifest.js +++ b/src/lib/helpers/getPrerenderManifest.js @@ -27,7 +27,7 @@ const transformManifestForI18n = async (manifest) => { } const getPrerenderManifest = async () => { - const nextConfig = await getNextConfig() + const nextConfig = await getNextConfig(process.cwd()) const nextDistDir = await getNextDistDir() const manifest = readJSONSync(join(nextDistDir, 'prerender-manifest.json')) if (nextConfig.i18n) return await transformManifestForI18n(manifest) diff --git a/test/fixtures/dist_dir_next_config/next.config.js b/test/fixtures/dist_dir_next_config/next.config.js new file mode 100644 index 0000000000..e78618ce87 --- /dev/null +++ b/test/fixtures/dist_dir_next_config/next.config.js @@ -0,0 +1,4 @@ +module.exports = { + target: 'serverless', + distDir: 'build', +} diff --git a/test/index.js b/test/index.js index a1fb8d5926..16126ff1de 100644 --- a/test/index.js +++ b/test/index.js @@ -18,6 +18,10 @@ const utils = { throw new Error(message) }, }, + cache: { + save() {}, + restore() {}, + }, } // Temporary switch cwd @@ -155,6 +159,29 @@ describe('preBuild()', () => { }), ).rejects.toThrow(`Error loading your next.config.js.`) }) + + test('restores cache with right paths', async () => { + await useFixture('dist_dir_next_config') + + let distPath + const utils_ = { + ...utils, + cache: { + restore: (x) => (distPath = x), + }, + } + const spy = jest.spyOn(utils_.cache, 'restore') + + await plugin.onPreBuild({ + netlifyConfig, + packageJson: DUMMY_PACKAGE_JSON, + utils: utils_, + constants: { FUNCTIONS_SRC: 'out_functions' }, + }) + + expect(spy).toHaveBeenCalled() + expect(distPath).toBe('build/cache') + }) }) describe('onBuild()', () => { @@ -229,3 +256,33 @@ describe('onBuild()', () => { expect(await pathExists(`${resolvedFunctions}/next_api_test/next_api_test.js`)).toBeTruthy() }) }) + +describe('onPostBuild', () => { + test('saves cache with right paths', async () => { + await useFixture('dist_dir_next_config') + + let distPath + let manifestPath + const utils_ = { + ...utils, + cache: { + save: (x, y) => { + distPath = x + manifestPath = y + }, + }, + } + const spy = jest.spyOn(utils_.cache, 'save') + + await plugin.onPostBuild({ + netlifyConfig, + packageJson: DUMMY_PACKAGE_JSON, + utils: utils_, + constants: { FUNCTIONS_SRC: 'out_functions' }, + }) + + expect(spy).toHaveBeenCalled() + expect(distPath).toBe('build/cache') + expect(manifestPath.digests[0]).toBe('build/build-manifest.json') + }) +}) From 25558159a64d00b58c877abc253a21feb0ed37bc Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 20 Apr 2021 16:59:59 +0200 Subject: [PATCH 2/4] fix: `cwd` memoization --- helpers/getNextConfig.js | 14 ++++++++++---- helpers/hasCorrectNextConfig.js | 2 +- index.js | 4 ++-- src/lib/helpers/getI18n.js | 2 +- src/lib/helpers/getNextDistDir.js | 2 +- src/lib/helpers/getPrerenderManifest.js | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/helpers/getNextConfig.js b/helpers/getNextConfig.js index 19e0da9f39..c7a5f6f3ab 100644 --- a/helpers/getNextConfig.js +++ b/helpers/getNextConfig.js @@ -1,29 +1,35 @@ 'use strict' +const { cwd: getCwd } = require('process') const { resolve } = require('path') const moize = require('moize') // We used to cache nextConfig for any cwd. Now we pass process.cwd() to cache // (or memoize) nextConfig per cwd. -const getNextConfig = async function (cwd, failBuild = defaultFailBuild) { +const getNextConfig = async function (failBuild = defaultFailBuild, cwd = getCwd()) { // We cannot load `next` at the top-level because we validate whether the // site is using `next` inside `onPreBuild`. const { PHASE_PRODUCTION_BUILD } = require('next/constants') const loadConfig = require('next/dist/next-server/server/config').default try { - return await loadConfig(PHASE_PRODUCTION_BUILD, resolve('.')) + return await loadConfig(PHASE_PRODUCTION_BUILD, cwd) } catch (error) { return failBuild('Error loading your next.config.js.', { error }) } } -const moizedGetNextConfig = moize(getNextConfig, { maxSize: 1e3, isPromise: true }) +const moizedGetNextConfig = moize(getNextConfig, { + maxSize: 1e3, + isPromise: true, + // Memoization cache key. We need to use `transformArgs` so `process.cwd()` + // default value is assigned + transformArgs: ([, cwd = getCwd()]) => [cwd], +}) const defaultFailBuild = function (message, { error }) { throw new Error(`${message}\n${error.stack}`) } -// module.exports = process.env.NODE_ENV === 'test' ? getNextConfig : moizedGetNextConfig module.exports = moizedGetNextConfig diff --git a/helpers/hasCorrectNextConfig.js b/helpers/hasCorrectNextConfig.js index 1bc2da3a45..36bf0ff2b5 100644 --- a/helpers/hasCorrectNextConfig.js +++ b/helpers/hasCorrectNextConfig.js @@ -5,7 +5,7 @@ const hasCorrectNextConfig = async ({ nextConfigPath, failBuild }) => { // In the plugin's case, no config is valid because we'll make it ourselves if (nextConfigPath === undefined) return true - const { target } = await getNextConfig(process.cwd(), failBuild) + const { target } = await getNextConfig(failBuild) // If the next config exists, log warning if target isnt in acceptableTargets const acceptableTargets = ['serverless', 'experimental-serverless-trace'] diff --git a/index.js b/index.js index 4c2b7f87f0..c7a121a8c2 100644 --- a/index.js +++ b/index.js @@ -28,7 +28,7 @@ module.exports = { return failBuild('Could not find a package.json for this project') } - const nextConfig = await getNextConfig(process.cwd(), utils.failBuild) + const nextConfig = await getNextConfig(utils.failBuild) await restoreCache({ cache: utils.cache, distDir: nextConfig.distDir }) if (await doesNotNeedPlugin({ netlifyConfig, packageJson, failBuild })) { @@ -70,7 +70,7 @@ module.exports = { return } - const nextConfig = await getNextConfig(process.cwd(), utils.failBuild) + const nextConfig = await getNextConfig(utils.failBuild) await saveCache({ cache: utils.cache, distDir: nextConfig.distDir }) copyUnstableIncludedDirs({ nextConfig, functionsDist: FUNCTIONS_DIST }) }, diff --git a/src/lib/helpers/getI18n.js b/src/lib/helpers/getI18n.js index 955fc9551f..817446b68e 100644 --- a/src/lib/helpers/getI18n.js +++ b/src/lib/helpers/getI18n.js @@ -2,7 +2,7 @@ const getNextConfig = require('../../../helpers/getNextConfig') const getI18n = async () => { - const nextConfig = await getNextConfig(process.cwd()) + const nextConfig = await getNextConfig() return nextConfig.i18n || { locales: [] } } diff --git a/src/lib/helpers/getNextDistDir.js b/src/lib/helpers/getNextDistDir.js index 5d3b8e7dd7..2bcf34e9f9 100644 --- a/src/lib/helpers/getNextDistDir.js +++ b/src/lib/helpers/getNextDistDir.js @@ -3,7 +3,7 @@ const { join } = require('path') const getNextConfig = require('../../../helpers/getNextConfig') const getNextDistDir = async () => { - const nextConfig = await getNextConfig(process.cwd()) + const nextConfig = await getNextConfig() return join('.', nextConfig.distDir) } diff --git a/src/lib/helpers/getPrerenderManifest.js b/src/lib/helpers/getPrerenderManifest.js index e58970f5e6..0e0aa7555e 100644 --- a/src/lib/helpers/getPrerenderManifest.js +++ b/src/lib/helpers/getPrerenderManifest.js @@ -27,7 +27,7 @@ const transformManifestForI18n = async (manifest) => { } const getPrerenderManifest = async () => { - const nextConfig = await getNextConfig(process.cwd()) + const nextConfig = await getNextConfig() const nextDistDir = await getNextDistDir() const manifest = readJSONSync(join(nextDistDir, 'prerender-manifest.json')) if (nextConfig.i18n) return await transformManifestForI18n(manifest) From 2c1e114e4d09e94c3a6ea4cb2f658d2317780e2f Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 20 Apr 2021 17:20:18 +0200 Subject: [PATCH 3/4] fix: fix Windows tests --- test/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.js b/test/index.js index 16126ff1de..41b57dc338 100644 --- a/test/index.js +++ b/test/index.js @@ -180,7 +180,7 @@ describe('preBuild()', () => { }) expect(spy).toHaveBeenCalled() - expect(distPath).toBe('build/cache') + expect(path.normalize(distPath)).toBe(path.normalize('build/cache')) }) }) @@ -282,7 +282,7 @@ describe('onPostBuild', () => { }) expect(spy).toHaveBeenCalled() - expect(distPath).toBe('build/cache') + expect(path.normalize(distPath)).toBe(path.normalize('build/cache')) expect(manifestPath.digests[0]).toBe('build/build-manifest.json') }) }) From 3521e2487135d47977974524b67e43960f9754d0 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 20 Apr 2021 17:43:39 +0200 Subject: [PATCH 4/4] fix: fix Windows tests --- test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index 41b57dc338..bc7c3afd0c 100644 --- a/test/index.js +++ b/test/index.js @@ -283,6 +283,6 @@ describe('onPostBuild', () => { expect(spy).toHaveBeenCalled() expect(path.normalize(distPath)).toBe(path.normalize('build/cache')) - expect(manifestPath.digests[0]).toBe('build/build-manifest.json') + expect(path.normalize(manifestPath.digests[0])).toBe(path.normalize('build/build-manifest.json')) }) })