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..c7a5f6f3ab 100644 --- a/helpers/getNextConfig.js +++ b/helpers/getNextConfig.js @@ -1,24 +1,32 @@ 'use strict' +const { cwd: getCwd } = require('process') 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 (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}`) 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..c7a121a8c2 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(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) + await saveCache({ cache: utils.cache, distDir: nextConfig.distDir }) copyUnstableIncludedDirs({ nextConfig, functionsDist: FUNCTIONS_DIST }) }, } 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..bc7c3afd0c 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(path.normalize(distPath)).toBe(path.normalize('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(path.normalize(distPath)).toBe(path.normalize('build/cache')) + expect(path.normalize(manifestPath.digests[0])).toBe(path.normalize('build/build-manifest.json')) + }) +})