Files
vercel/packages/now-next/src/index.ts
Nathan Rajlich fadc3f2588 [next][node][static-build] Execute "vercel-build" script if defined (#4863)
* [build-utils] Make `runPackageJsonScript()` run the `vercel-` or `now-` if defined in `package.json`

* [build-utils] Export `getScriptName()`

* [next] Use `getScriptName()` and `remove()`

* [node] Update for `vercel-`

* [static-build] Update for `vercel-`

* Remove debug

* Add `getScriptName()` unit tests

* Test for `vercel-build` in e2e

* Make platform name behavior be opt-in

So that it's not a breaking behavior change.

* Check for only "vercel-build" or "now-build", but not "build"

* Simplify `getScriptName()` to return the first existing script in a possible set

* Revert change

* Fix test

Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-07-21 20:04:24 -07:00

1792 lines
56 KiB
TypeScript

import buildUtils from './build-utils';
import url from 'url';
const {
createLambda,
debug,
download,
getLambdaOptionsFromFunction,
getNodeVersion,
getSpawnOptions,
getScriptName,
glob,
runNpmInstall,
runPackageJsonScript,
execCommand,
getNodeBinPath,
} = buildUtils;
import {
BuildOptions,
Config,
FileBlob,
FileFsRef,
Files,
Lambda,
NowBuildError,
PackageJson,
PrepareCacheOptions,
Prerender,
} from '@vercel/build-utils';
import { Handler, Route } from '@vercel/routing-utils';
import {
convertHeaders,
convertRedirects,
convertRewrites,
} from '@vercel/routing-utils/dist/superstatic';
import { nodeFileTrace, NodeFileTraceReasons } from '@zeit/node-file-trace';
import { ChildProcess, fork } from 'child_process';
import escapeStringRegexp from 'escape-string-regexp';
import { lstat, pathExists, readFile, remove, writeFile } from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import createServerlessConfig from './create-serverless-config';
import nextLegacyVersions from './legacy-versions';
import {
createLambdaFromPseudoLayers,
createPseudoLayer,
EnvConfig,
excludeFiles,
ExperimentalTraceVersion,
getDynamicRoutes,
getExportIntent,
getExportStatus,
getNextConfig,
getPathsInside,
getPrerenderManifest,
getRoutes,
getRoutesManifest,
getSourceFilePathFromPage,
isDynamicRoute,
normalizePackageJson,
normalizePage,
PseudoLayer,
stringMap,
syncEnvVars,
validateEntrypoint,
} from './utils';
import findUp from 'find-up';
import { Sema } from 'async-sema';
interface BuildParamsMeta {
isDev: boolean | undefined;
env?: EnvConfig;
buildEnv?: EnvConfig;
}
interface BuildParamsType extends BuildOptions {
files: Files;
entrypoint: string;
workPath: string;
meta: BuildParamsMeta;
}
export const version = 2;
const htmlContentType = 'text/html; charset=utf-8';
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
process.once(signal as NodeJS.Signals, () => {
for (const child of nowDevChildProcesses) {
debug(
`Got ${signal}, killing dev server child process (pid=${child.pid})`
);
process.kill(child.pid, signal);
}
process.exit(0);
});
});
const MAX_AGE_ONE_YEAR = 31536000;
/**
* Read package.json from files
*/
async function readPackageJson(entryPath: string): Promise<PackageJson> {
const packagePath = path.join(entryPath, 'package.json');
try {
return JSON.parse(await readFile(packagePath, 'utf8'));
} catch (err) {
debug('package.json not found in entry');
return {};
}
}
/**
* Write package.json
*/
async function writePackageJson(workPath: string, packageJson: PackageJson) {
await writeFile(
path.join(workPath, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
}
/**
* Write .npmrc with npm auth token
*/
async function writeNpmRc(workPath: string, token: string) {
await writeFile(
path.join(workPath, '.npmrc'),
`//registry.npmjs.org/:_authToken=${token}`
);
}
/**
* Get the installed Next version.
*/
function getRealNextVersion(entryPath: string): string | false {
try {
// First try to resolve the `next` dependency and get the real version from its
// package.json. This allows the builder to be used with frameworks like Blitz that
// bundle Next but where Next isn't in the project root's package.json
const nextVersion: string = require(resolveFrom(
entryPath,
'next/package.json'
)).version;
debug(`Detected Next.js version: ${nextVersion}`);
return nextVersion;
} catch (_ignored) {
debug(
`Could not identify real Next.js version, ensure it is defined as a project dependency.`
);
return false;
}
}
/**
* Get the package.json Next version.
*/
async function getNextVersionRange(entryPath: string): Promise<string | false> {
let nextVersion: string | false = false;
const pkg = await readPackageJson(entryPath);
if (pkg.dependencies && pkg.dependencies.next) {
nextVersion = pkg.dependencies.next;
} else if (pkg.devDependencies && pkg.devDependencies.next) {
nextVersion = pkg.devDependencies.next;
}
return nextVersion;
}
function isLegacyNext(nextVersion: string) {
// If version is using the dist-tag instead of a version range
if (nextVersion === 'canary' || nextVersion === 'latest') {
return false;
}
// If the version is an exact match with the legacy versions
if (nextLegacyVersions.indexOf(nextVersion) !== -1) {
return true;
}
const maxSatisfying = semver.maxSatisfying(nextLegacyVersions, nextVersion);
// When the version can't be matched with legacy versions, so it must be a newer version
if (maxSatisfying === null) {
return false;
}
return true;
}
const name = '[@vercel/next]';
const urls: stringMap = {};
function startDevServer(entryPath: string, runtimeEnv: EnvConfig) {
// `env` is omitted since that makes it default to `process.env`
const forked = fork(path.join(__dirname, 'dev-server.js'), [], {
cwd: entryPath,
execArgv: [],
});
const getUrl = () =>
new Promise<string>((resolve, reject) => {
forked.once('message', resolve);
forked.once('error', reject);
});
forked.send({ dir: entryPath, runtimeEnv });
return { forked, getUrl };
}
export const build = async ({
files,
workPath,
entrypoint,
config = {} as Config,
meta = {} as BuildParamsMeta,
}: BuildParamsType): Promise<{
routes: Route[];
output: Files;
watch?: string[];
childProcesses: ChildProcess[];
}> => {
validateEntrypoint(entrypoint);
// Limit for max size each lambda can be, 50 MB if no custom limit
const lambdaCompressedByteLimit = config.maxLambdaSize || 50 * 1000 * 1000;
let entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
const outputDirectory = config.outputDirectory || '.next';
const dotNextStatic = path.join(entryPath, outputDirectory, 'static');
await download(files, workPath, meta);
const pkg = await readPackageJson(entryPath);
const nextVersionRange = await getNextVersionRange(entryPath);
const nodeVersion = await getNodeVersion(entryPath, undefined, config, meta);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
const nowJsonPath = await findUp(['now.json', 'vercel.json'], {
cwd: path.join(workPath, path.dirname(entrypoint)),
});
let hasLegacyRoutes = false;
const hasFunctionsConfig = !!config.functions;
if (nowJsonPath) {
const nowJsonData = JSON.parse(await readFile(nowJsonPath, 'utf8'));
if (Array.isArray(nowJsonData.routes) && nowJsonData.routes.length > 0) {
hasLegacyRoutes = true;
console.warn(
`WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes in ${path.basename(
nowJsonPath
)}. http://err.sh/vercel/vercel/next-legacy-routes-optimized-lambdas`
);
}
}
if (hasFunctionsConfig) {
console.warn(
`WARNING: Your application is being opted out of "@vercel/next" optimized lambdas mode due to \`functions\` config.\nMore info: http://err.sh/vercel/vercel/next-functions-config-optimized-lambdas`
);
}
// default to true but still allow opting out with the config
const isSharedLambdas =
!hasLegacyRoutes &&
!hasFunctionsConfig &&
typeof config.sharedLambdas === 'undefined'
? true
: !!config.sharedLambdas;
if (meta.isDev) {
let childProcess: ChildProcess | undefined;
// If this is the initial build, we want to start the server
if (!urls[entrypoint]) {
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'development';
}
// The runtime env vars consist of the base `process.env` vars, but with the
// build env vars removed, and the runtime env vars mixed in afterwards
const runtimeEnv: EnvConfig = Object.assign({}, process.env);
syncEnvVars(runtimeEnv, meta.buildEnv || {}, meta.env || {});
const { forked, getUrl } = startDevServer(entryPath, runtimeEnv);
urls[entrypoint] = await getUrl();
childProcess = forked;
nowDevChildProcesses.add(forked);
debug(
`${name} Development server for ${entrypoint} running at ${urls[entrypoint]}`
);
}
const pathsInside = getPathsInside(entryDirectory, files);
return {
output: {},
routes: await getRoutes(
entryPath,
entryDirectory,
pathsInside,
files,
urls[entrypoint]
),
watch: pathsInside,
childProcesses: childProcess ? [childProcess] : [],
};
}
if (await pathExists(dotNextStatic)) {
console.warn('WARNING: You should not upload the `.next` directory.');
}
const isLegacy = nextVersionRange && isLegacyNext(nextVersionRange);
debug(`MODE: ${isLegacy ? 'legacy' : 'serverless'}`);
if (isLegacy) {
console.warn(
"WARNING: your application is being deployed in @vercel/next's legacy mode. http://err.sh/vercel/vercel/now-next-legacy-mode"
);
await Promise.all([
remove(path.join(entryPath, 'yarn.lock')),
remove(path.join(entryPath, 'package-lock.json')),
]);
debug('Normalizing package.json');
const packageJson = normalizePackageJson(pkg);
debug('Normalized package.json result: ', packageJson);
await writePackageJson(entryPath, packageJson);
}
const buildScriptName = getScriptName(pkg, [
'vercel-build',
'now-build',
'build',
]);
let { buildCommand } = config;
if (!buildScriptName && !buildCommand) {
console.log(
'Your application is being built using `next build`. ' +
'If you need to define a different build step, please create a `vercel-build` script in your `package.json` ' +
'(e.g. `{ "scripts": { "vercel-build": "npm run prepare && next build" } }`).'
);
buildCommand = 'next build';
}
if (process.env.NPM_AUTH_TOKEN) {
debug('Found NPM_AUTH_TOKEN in environment, creating .npmrc');
await writeNpmRc(entryPath, process.env.NPM_AUTH_TOKEN);
}
console.log('Installing dependencies...');
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts, meta);
// Refetch Next version now that dependencies are installed.
// This will now resolve the actual installed Next version,
// even if Next isn't in the project package.json
const nextVersion = getRealNextVersion(entryPath);
if (!nextVersion) {
throw new NowBuildError({
code: 'NEXT_NO_VERSION',
message:
'No Next.js version could be detected in your project. Make sure `"next"` is installed in "dependencies" or "devDependencies"',
});
}
if (!isLegacy) {
await createServerlessConfig(workPath, entryPath, nextVersion);
}
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
const env: { [key: string]: string | undefined } = { ...spawnOpts.env };
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
if (buildCommand) {
// Add `node_modules/.bin` to PATH
const nodeBinPath = await getNodeBinPath({ cwd: entryPath });
env.PATH = `${nodeBinPath}${path.delimiter}${env.PATH}`;
debug(
`Added "${nodeBinPath}" to PATH env because a build command was used.`
);
console.log(`Running "${buildCommand}"`);
await execCommand(buildCommand, {
...spawnOpts,
cwd: entryPath,
env,
});
} else if (buildScriptName) {
await runPackageJsonScript(entryPath, buildScriptName, {
...spawnOpts,
env,
});
}
const appMountPrefixNoTrailingSlash = path.posix
.join('/', entryDirectory)
.replace(/\/+$/, '');
const routesManifest = await getRoutesManifest(
entryPath,
outputDirectory,
nextVersion
);
const prerenderManifest = await getPrerenderManifest(entryPath);
const headers: Route[] = [];
const rewrites: Route[] = [];
const redirects: Route[] = [];
const dataRoutes: Route[] = [];
let dynamicRoutes: Route[] = [];
// whether they have enabled pages/404.js as the custom 404 page
let hasPages404 = false;
if (routesManifest) {
switch (routesManifest.version) {
case 1:
case 2:
case 3: {
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
if (routesManifest.headers) {
headers.push(...convertHeaders(routesManifest.headers));
}
if (routesManifest.dataRoutes) {
// Load the /_next/data routes for both dynamic SSG and SSP pages.
// These must be combined and sorted to prevent conflicts
for (const dataRoute of routesManifest.dataRoutes) {
const ssgDataRoute =
prerenderManifest.fallbackRoutes[dataRoute.page] ||
prerenderManifest.legacyBlockingRoutes[dataRoute.page];
// we don't need to add routes for non-lazy SSG routes since
// they have outputs which would override the routes anyways
if (
prerenderManifest.staticRoutes[dataRoute.page] ||
prerenderManifest.omittedRoutes.includes(dataRoute.page)
) {
continue;
}
dataRoutes.push({
src: (
dataRoute.namedDataRouteRegex || dataRoute.dataRouteRegex
).replace(/^\^/, `^${appMountPrefixNoTrailingSlash}`),
dest: path.join(
'/',
entryDirectory,
// make sure to route SSG data route to the data prerender
// output, we don't do this for SSP routes since they don't
// have a separate data output
`${(ssgDataRoute && ssgDataRoute.dataRoute) || dataRoute.page}${
dataRoute.routeKeys
? `?${Object.keys(dataRoute.routeKeys)
.map(key => `${dataRoute.routeKeys![key]}=$${key}`)
.join('&')}`
: ''
}`
),
check: true,
});
}
}
if (routesManifest.pages404) {
hasPages404 = true;
}
if (routesManifest.basePath && routesManifest.basePath !== '/') {
const nextBasePath = routesManifest.basePath;
if (!nextBasePath.startsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_STARTING_SLASH',
message:
'basePath must start with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
if (nextBasePath.endsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_TRAILING_SLASH',
message:
'basePath must not end with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
entryDirectory = path.join(entryDirectory, nextBasePath);
}
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
throw new NowBuildError({
code: 'NEXT_VERSION_OUTDATED',
message:
'This version of `@vercel/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
}
}
const userExport = await getExportStatus(entryPath);
if (userExport) {
const exportIntent = await getExportIntent(entryPath);
const { trailingSlash = false } = exportIntent || {};
const resultingExport = await getExportStatus(entryPath);
if (!resultingExport) {
throw new NowBuildError({
code: 'NEXT_EXPORT_FAILED',
message:
'Exporting Next.js app failed. Please check your build logs and contact us if this continues.',
});
}
if (resultingExport.success !== true) {
throw new NowBuildError({
code: 'NEXT_EXPORT_FAILED',
message: 'Export of Next.js app failed. Please check your build logs.',
});
}
const outDirectory = resultingExport.outDirectory;
debug(`next export should use trailing slash: ${trailingSlash}`);
// This handles pages, `public/`, and `static/`.
const filesAfterBuild = await glob('**', outDirectory);
const output: Files = { ...filesAfterBuild };
// Strip `.html` extensions from build output
Object.entries(output)
.filter(([name]) => name.endsWith('.html'))
.forEach(([name, value]) => {
const cleanName = name.slice(0, -5);
delete output[name];
output[cleanName] = value;
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
value.contentType = value.contentType || 'text/html; charset=utf-8';
}
});
return {
output,
routes: [
// User headers
...headers,
// User redirects
...redirects,
// Make sure to 404 for the /404 path itself
{
src: path.join('/', entryDirectory, '404'),
status: 404,
continue: true,
},
// Next.js pages, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
// These need to come before handle: miss or else they are grouped
// with that routing section
...rewrites,
// We need to make sure to 404 for /_next after handle: miss since
// handle: miss is called before rewrites and to prevent rewriting
// /_next
{ handle: 'miss' },
{
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
),
status: 404,
check: true,
dest: '$0',
},
// Dynamic routes
// TODO: do we want to do this?: ...dynamicRoutes,
// (if so make sure to add any dynamic routes after handle: 'rewrite' )
// routes to call after a file has been matched
{ handle: 'hit' },
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: {
'cache-control': `public,max-age=${MAX_AGE_ONE_YEAR},immutable`,
},
continue: true,
},
// error handling
...(output[path.join('./', entryDirectory, '404')]
? [
{ handle: 'error' } as Handler,
{
status: 404,
src: path.join(entryDirectory, '.*'),
dest: path.join('/', entryDirectory, '404'),
},
]
: []),
],
watch: [],
childProcesses: [],
};
}
if (isLegacy) {
debug('Running npm install --production...');
await runNpmInstall(
entryPath,
['--prefer-offline', '--production'],
spawnOpts,
meta
);
}
if (process.env.NPM_AUTH_TOKEN) {
await remove(path.join(entryPath, '.npmrc'));
}
const pageLambdaRoutes: Route[] = [];
const dynamicPageLambdaRoutes: Route[] = [];
const dynamicPageLambdaRoutesMap: { [page: string]: Route } = {};
const pageLambdaMap: { [page: string]: string } = {};
const lambdas: { [key: string]: Lambda } = {};
const prerenders: { [key: string]: Prerender | FileFsRef } = {};
const staticPages: { [key: string]: FileFsRef } = {};
const dynamicPages: string[] = [];
let static404Page: string | undefined;
let buildId = '';
let page404Path = '';
let escapedBuildId = '';
if (isLegacy || isSharedLambdas) {
try {
buildId = await readFile(
path.join(entryPath, outputDirectory, 'BUILD_ID'),
'utf8'
);
escapedBuildId = escapeStringRegexp(buildId);
} catch (err) {
console.error(
'BUILD_ID not found in ".next". The "package.json" "build" script did not run "next build"'
);
throw new NowBuildError({
code: 'NOW_NEXT_NO_BUILD_ID',
message: 'Missing BUILD_ID',
});
}
}
if (isLegacy) {
const filesAfterBuild = await glob('**', entryPath);
debug('Preparing serverless function files...');
const dotNextRootFiles = await glob(`${outputDirectory}/*`, entryPath);
const dotNextServerRootFiles = await glob(
`${outputDirectory}/server/*`,
entryPath
);
const nodeModules = excludeFiles(
await glob('node_modules/**', entryPath),
file => file.startsWith('node_modules/.cache')
);
const launcherFiles = {
'now__bridge.js': new FileFsRef({
fsPath: path.join(__dirname, 'now__bridge.js'),
}),
};
const nextFiles: { [key: string]: FileFsRef } = {
...nodeModules,
...dotNextRootFiles,
...dotNextServerRootFiles,
...launcherFiles,
};
if (filesAfterBuild['next.config.js']) {
nextFiles['next.config.js'] = filesAfterBuild['next.config.js'];
}
const pagesDir = path.join(
entryPath,
outputDirectory,
'server',
'static',
buildId,
'pages'
);
const pages = await glob('**/*.js', pagesDir);
const launcherPath = path.join(__dirname, 'legacy-launcher.js');
const launcherData = await readFile(launcherPath, 'utf8');
await Promise.all(
Object.keys(pages).map(async page => {
// These default pages don't have to be handled as they'd always 404
if (['_app.js', '_error.js', '_document.js'].includes(page)) {
return;
}
const pathname = page.replace(/\.js$/, '');
const launcher = launcherData.replace(
'PATHNAME_PLACEHOLDER',
`/${pathname.replace(/(^|\/)index$/, '')}`
);
const pageFiles = {
[`${outputDirectory}/server/static/${buildId}/pages/_document.js`]: filesAfterBuild[
`${outputDirectory}/server/static/${buildId}/pages/_document.js`
],
[`${outputDirectory}/server/static/${buildId}/pages/_app.js`]: filesAfterBuild[
`${outputDirectory}/server/static/${buildId}/pages/_app.js`
],
[`${outputDirectory}/server/static/${buildId}/pages/_error.js`]: filesAfterBuild[
`${outputDirectory}/server/static/${buildId}/pages/_error.js`
],
[`${outputDirectory}/server/static/${buildId}/pages/${page}`]: filesAfterBuild[
`${outputDirectory}/server/static/${buildId}/pages/${page}`
],
};
const lambdaOptions = await getLambdaOptionsFromFunction({
sourceFile: await getSourceFilePathFromPage({ workPath, page }),
config,
});
debug(`Creating serverless function for page: "${page}"...`);
lambdas[path.join(entryDirectory, pathname)] = await createLambda({
files: {
...nextFiles,
...pageFiles,
'now__launcher.js': new FileBlob({ data: launcher }),
},
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
...lambdaOptions,
});
debug(`Created serverless function for page: "${page}"`);
})
);
} else {
debug('Preparing serverless function files...');
const pagesDir = path.join(
entryPath,
outputDirectory,
'serverless',
'pages'
);
const pages = await glob('**/*.js', pagesDir);
const staticPageFiles = await glob('**/*.html', pagesDir);
Object.keys(staticPageFiles).forEach((page: string) => {
const pathname = page.replace(/\.html$/, '');
const routeName = normalizePage(pathname);
// Prerendered routes emit a `.html` file but should not be treated as a
// static page.
// Lazily prerendered routes have a fallback `.html` file on newer
// Next.js versions so we need to also not treat it as a static page here.
if (
prerenderManifest.staticRoutes[routeName] ||
prerenderManifest.fallbackRoutes[routeName]
) {
return;
}
const staticRoute = path.join(entryDirectory, pathname);
staticPages[staticRoute] = staticPageFiles[page];
staticPages[staticRoute].contentType = htmlContentType;
if (isDynamicRoute(pathname)) {
dynamicPages.push(routeName);
return;
}
});
// this can be either 404.html in latest versions
// or _errors/404.html versions while this was experimental
static404Page =
staticPages[path.join(entryDirectory, '404')] && hasPages404
? path.join(entryDirectory, '404')
: staticPages[path.join(entryDirectory, '_errors/404')]
? path.join(entryDirectory, '_errors/404')
: undefined;
// > 1 because _error is a lambda but isn't used if a static 404 is available
const pageKeys = Object.keys(pages);
const hasLambdas = !static404Page || pageKeys.length > 1;
if (pageKeys.length === 0) {
const nextConfig = await getNextConfig(workPath, entryPath);
if (nextConfig != null) {
console.info('Found next.config.js:');
console.info(nextConfig);
console.info();
}
throw new NowBuildError({
code: 'NEXT_NO_SERVERLESS_PAGES',
message: 'No serverless pages were built',
link: 'https://err.sh/vercel/vercel/now-next-no-serverless-pages-built',
});
}
// Assume tracing to be safe, bail if we know we don't need it.
let requiresTracing = hasLambdas;
try {
if (nextVersion && semver.lt(nextVersion, ExperimentalTraceVersion)) {
debug(
'Next.js version is too old for us to trace the required dependencies.\n' +
'Assuming Next.js has handled it!'
);
requiresTracing = false;
}
} catch (err) {
console.log(
'Failed to check Next.js version for tracing compatibility: ' + err
);
}
let assets:
| undefined
| {
[filePath: string]: FileFsRef;
};
let canUsePreviewMode = false;
let pseudoLayerBytes = 0;
let apiPseudoLayerBytes = 0;
const pseudoLayers: PseudoLayer[] = [];
const apiPseudoLayers: PseudoLayer[] = [];
const isApiPage = (page: string) =>
page.replace(/\\/g, '/').match(/serverless\/pages\/api/);
const tracedFiles: {
[filePath: string]: FileFsRef;
} = {};
const apiTracedFiles: {
[filePath: string]: FileFsRef;
} = {};
if (requiresTracing) {
const tracingLabel =
'Traced Next.js serverless functions for external files in';
console.time(tracingLabel);
const apiPages: string[] = [];
const nonApiPages: string[] = [];
const allPagePaths = Object.keys(pages).map(page => pages[page].fsPath);
for (const page of allPagePaths) {
if (isApiPage(page)) {
apiPages.push(page);
canUsePreviewMode = true;
} else {
nonApiPages.push(page);
}
}
const {
fileList: apiFileList,
reasons: apiReasons,
} = await nodeFileTrace(apiPages, { base: workPath });
const {
fileList,
reasons: nonApiReasons,
} = await nodeFileTrace(nonApiPages, { base: workPath });
debug(`node-file-trace result for pages: ${fileList}`);
const lstatSema = new Sema(25, {
capacity: fileList.length + apiFileList.length,
});
const lstatResults: { [key: string]: ReturnType<typeof lstat> } = {};
const collectTracedFiles = (
reasons: NodeFileTraceReasons,
files: { [filePath: string]: FileFsRef }
) => async (file: string) => {
const reason = reasons[file];
if (reason && reason.type === 'initial') {
// Initial files are manually added to the lambda later
return;
}
const filePath = path.join(workPath, file);
if (!lstatResults[filePath]) {
lstatResults[filePath] = lstatSema
.acquire()
.then(() => lstat(filePath))
.finally(() => lstatSema.release());
}
const { mode } = await lstatResults[filePath];
files[file] = new FileFsRef({
fsPath: path.join(workPath, file),
mode,
});
};
await Promise.all(
fileList.map(collectTracedFiles(nonApiReasons, tracedFiles))
);
await Promise.all(
apiFileList.map(collectTracedFiles(apiReasons, apiTracedFiles))
);
console.timeEnd(tracingLabel);
const zippingLabel = 'Compressed shared serverless function files';
console.time(zippingLabel);
let pseudoLayer;
let apiPseudoLayer;
({ pseudoLayer, pseudoLayerBytes } = await createPseudoLayer(
tracedFiles
));
({
pseudoLayer: apiPseudoLayer,
pseudoLayerBytes: apiPseudoLayerBytes,
} = await createPseudoLayer(apiTracedFiles));
pseudoLayers.push(pseudoLayer);
apiPseudoLayers.push(apiPseudoLayer);
console.timeEnd(zippingLabel);
} else {
// An optional assets folder that is placed alongside every page
// entrypoint.
// This is a legacy feature that was needed before we began tracing
// lambdas.
assets = await glob(
'assets/**',
path.join(entryPath, outputDirectory, 'serverless')
);
const assetKeys = Object.keys(assets);
if (assetKeys.length > 0) {
debug(
'detected (legacy) assets to be bundled with serverless function:'
);
assetKeys.forEach(assetFile => debug(`\t${assetFile}`));
debug(
'\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.'
);
}
}
const launcherPath = path.join(__dirname, 'templated-launcher.js');
const launcherData = await readFile(launcherPath, 'utf8');
const allLambdasLabel = `All serverless functions created in`;
if (hasLambdas) {
console.time(allLambdasLabel);
}
type LambdaGroup = {
pages: {
[outputName: string]: {
pageName: string;
pageFileName: string;
pageLayer: PseudoLayer;
};
};
isApiLambda: boolean;
lambdaIdentifier: string;
lambdaCombinedBytes: number;
};
const apiLambdaGroups: Array<LambdaGroup> = [];
const pageLambdaGroups: Array<LambdaGroup> = [];
if (isSharedLambdas) {
// Do initial check to make sure the traced files don't already
// exceed the lambda size limit as we won't be able to continue
// if they do
if (
pseudoLayerBytes >= lambdaCompressedByteLimit ||
apiPseudoLayerBytes >= lambdaCompressedByteLimit
) {
throw new Error(
`Required lambda files exceed max lambda size of ${lambdaCompressedByteLimit} bytes`
);
}
for (const page of pageKeys) {
// These default pages don't have to be handled as they'd always 404
if (['_app.js', '_document.js'].includes(page)) {
continue;
}
// Don't add _error to lambda if we have a static 404 page or
// pages404 is enabled and 404.js is present
if (
page === '_error.js' &&
((static404Page && staticPages[static404Page]) ||
(hasPages404 && pages['404.js']))
) {
continue;
}
const pageFileName = path.normalize(
path.relative(workPath, pages[page].fsPath)
);
const pathname = page.replace(/\.js$/, '');
const routeIsApi = isApiPage(pageFileName);
const routeIsDynamic = isDynamicRoute(pathname);
if (routeIsDynamic) {
dynamicPages.push(normalizePage(pathname));
}
const outputName = path.join('/', entryDirectory, pathname);
const lambdaGroups = routeIsApi ? apiLambdaGroups : pageLambdaGroups;
let lambdaGroupIndex = lambdaGroups.length - 1;
const lastLambdaGroup = lambdaGroups[lambdaGroupIndex];
let currentLambdaGroup = lastLambdaGroup;
if (
!currentLambdaGroup ||
currentLambdaGroup.lambdaCombinedBytes >= lambdaCompressedByteLimit
) {
lambdaGroupIndex++;
currentLambdaGroup = {
pages: {},
isApiLambda: !!routeIsApi,
lambdaCombinedBytes: !requiresTracing
? 0
: routeIsApi
? apiPseudoLayerBytes
: pseudoLayerBytes,
lambdaIdentifier: path.join(
entryDirectory,
`__NEXT_${routeIsApi ? 'API' : 'PAGE'}_LAMBDA_${lambdaGroupIndex}`
),
};
}
const pageLambdaRoute: Route = {
src: `^${escapeStringRegexp(outputName).replace(
/\/index$/,
'(/|/index|)'
)}$`,
dest: `${path.join('/', currentLambdaGroup.lambdaIdentifier)}`,
headers: {
'x-nextjs-page': outputName,
},
check: true,
};
// we only need to add the additional routes if shared lambdas
// is enabled
if (routeIsDynamic) {
dynamicPageLambdaRoutes.push(pageLambdaRoute);
dynamicPageLambdaRoutesMap[outputName] = pageLambdaRoute;
} else {
pageLambdaRoutes.push(pageLambdaRoute);
}
if (page === '_error.js' || (hasPages404 && page === '404.js')) {
page404Path = path.join('/', entryDirectory, pathname);
}
// we create the page as it's own layer so we can track how much
// it increased the lambda size on it's own and know when we
// need to create a new lambda
const {
pseudoLayer: pageLayer,
pseudoLayerBytes: pageLayerBytes,
} = await createPseudoLayer({ [pageFileName]: pages[page] });
currentLambdaGroup.pages[outputName] = {
pageLayer,
pageFileName,
pageName: page,
};
currentLambdaGroup.lambdaCombinedBytes += pageLayerBytes;
lambdaGroups[lambdaGroupIndex] = currentLambdaGroup;
}
} else {
await Promise.all(
pageKeys.map(async page => {
// These default pages don't have to be handled as they'd always 404
if (['_app.js', '_document.js'].includes(page)) {
return;
}
// Don't create _error lambda if we have a static 404 page or
// pages404 is enabled and 404.js is present
if (
page === '_error.js' &&
((static404Page && staticPages[static404Page]) ||
(hasPages404 && pages['404.js']))
) {
return;
}
const pathname = page.replace(/\.js$/, '');
if (isDynamicRoute(pathname)) {
dynamicPages.push(normalizePage(pathname));
}
const pageFileName = path.normalize(
path.relative(workPath, pages[page].fsPath)
);
const launcher = launcherData.replace(
/__LAUNCHER_PAGE_PATH__/g,
JSON.stringify(requiresTracing ? `./${pageFileName}` : './page')
);
const launcherFiles: { [name: string]: FileFsRef | FileBlob } = {
'now__bridge.js': new FileFsRef({
fsPath: path.join(__dirname, 'now__bridge.js'),
}),
'now__launcher.js': new FileBlob({ data: launcher }),
};
const lambdaOptions = await getLambdaOptionsFromFunction({
sourceFile: await getSourceFilePathFromPage({ workPath, page }),
config,
});
const outputName = path.join(entryDirectory, pathname);
if (requiresTracing) {
lambdas[outputName] = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
[requiresTracing ? pageFileName : 'page.js']: pages[page],
},
layers: isApiPage(pageFileName) ? apiPseudoLayers : pseudoLayers,
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
...lambdaOptions,
});
} else {
lambdas[outputName] = await createLambda({
files: {
...launcherFiles,
...assets,
...tracedFiles,
[requiresTracing ? pageFileName : 'page.js']: pages[page],
},
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
...lambdaOptions,
});
}
})
);
}
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
dynamicRoutes = await getDynamicRoutes(
entryPath,
entryDirectory,
dynamicPages,
false,
routesManifest,
new Set(prerenderManifest.omittedRoutes)
).then(arr =>
arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route;
})
);
if (isSharedLambdas) {
const launcherPath = path.join(__dirname, 'templated-launcher-shared.js');
const launcherData = await readFile(launcherPath, 'utf8');
// we need to include the prerenderManifest.omittedRoutes here
// for the page to be able to be matched in the lambda for preview mode
const completeDynamicRoutes = await getDynamicRoutes(
entryPath,
entryDirectory,
dynamicPages,
false,
routesManifest
).then(arr =>
arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route;
})
);
await Promise.all(
[...apiLambdaGroups, ...pageLambdaGroups].map(
async function buildLambdaGroup(group: LambdaGroup) {
const groupPageKeys = Object.keys(group.pages);
const launcher = launcherData.replace(
/\/\/ __LAUNCHER_PAGE_HANDLER__/g,
`
const url = require('url');
page = function(req, res) {
try {
const pages = {
${groupPageKeys
.map(
page =>
`'${page}': require('./${path.join(
'./',
group.pages[page].pageFileName
)}')`
)
.join(',\n')}
${
'' /*
creates a mapping of the page and the page's module e.g.
'/about': require('./.next/serverless/pages/about.js')
*/
}
}
let toRender = req.headers['x-nextjs-page']
if (!toRender) {
try {
const { pathname } = url.parse(req.url)
toRender = pathname
} catch (_) {
// handle failing to parse url
res.statusCode = 400
return res.end('Bad Request')
}
}
let currentPage = pages[toRender]
if (
toRender &&
!currentPage
) {
if (toRender.includes('/_next/data')) {
toRender = toRender
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
.replace(/\\.json$/, '')
currentPage = pages[toRender]
}
if (!currentPage) {
// for prerendered dynamic routes (/blog/post-1) we need to
// find the match since it won't match the page directly
const dynamicRoutes = ${JSON.stringify(
completeDynamicRoutes.map(route => ({
src: route.src,
dest: route.dest,
}))
)}
for (const route of dynamicRoutes) {
const matcher = new RegExp(route.src)
if (matcher.test(toRender)) {
toRender = url.parse(route.dest).pathname
currentPage = pages[toRender]
break
}
}
}
}
if (!currentPage) {
console.error(
"Failed to find matching page for", toRender, "in lambda"
)
res.statusCode = 500
return res.end('internal server error')
}
const method = currentPage.render || currentPage.default || currentPage
return method(req, res)
} catch (err) {
console.error('Unhandled error during request:', err)
throw err
}
}
`
);
const launcherFiles: { [name: string]: FileFsRef | FileBlob } = {
'now__bridge.js': new FileFsRef({
fsPath: path.join(__dirname, 'now__bridge.js'),
}),
'now__launcher.js': new FileBlob({ data: launcher }),
};
const pageLayers: PseudoLayer[] = [];
for (const page of groupPageKeys) {
const { pageLayer } = group.pages[page];
pageLambdaMap[page] = group.lambdaIdentifier;
pageLayers.push(pageLayer);
}
if (requiresTracing) {
lambdas[
group.lambdaIdentifier
] = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
},
layers: [
...(group.isApiLambda ? apiPseudoLayers : pseudoLayers),
...pageLayers,
],
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
});
} else {
lambdas[
group.lambdaIdentifier
] = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
...assets,
},
layers: pageLayers,
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
});
}
}
)
);
}
if (hasLambdas) {
console.timeEnd(allLambdasLabel);
}
let prerenderGroup = 1;
const onPrerenderRoute = (
routeKey: string,
{ isBlocking, isFallback }: { isBlocking: boolean; isFallback: boolean }
) => {
if (isBlocking && isFallback) {
throw new NowBuildError({
code: 'NEXT_ISBLOCKING_ISFALLBACK',
message: 'invariant: isBlocking and isFallback cannot both be true',
});
}
// Get the route file as it'd be mounted in the builder output
const routeFileNoExt = routeKey === '/' ? '/index' : routeKey;
const htmlFsRef = isBlocking
? // Blocking pages do not have an HTML fallback
null
: new FileFsRef({
fsPath: path.join(
pagesDir,
isFallback
? // Fallback pages have a special file.
prerenderManifest.fallbackRoutes[routeKey].fallback
: // Otherwise, the route itself should exist as a static HTML
// file.
`${routeFileNoExt}.html`
),
});
const jsonFsRef =
// JSON data does not exist for fallback or blocking pages
isFallback || isBlocking
? null
: new FileFsRef({
fsPath: path.join(pagesDir, `${routeFileNoExt}.json`),
});
let initialRevalidate: false | number;
let srcRoute: string | null;
let dataRoute: string;
if (isFallback || isBlocking) {
const pr = isFallback
? prerenderManifest.fallbackRoutes[routeKey]
: prerenderManifest.legacyBlockingRoutes[routeKey];
initialRevalidate = 1; // TODO: should Next.js provide this default?
// @ts-ignore
if (initialRevalidate === false) {
// Lazy routes cannot be "snapshotted" in time.
throw new NowBuildError({
code: 'NEXT_ISLAZY_INITIALREVALIDATE',
message: 'invariant isLazy: initialRevalidate !== false',
});
}
srcRoute = null;
dataRoute = pr.dataRoute;
} else {
const pr = prerenderManifest.staticRoutes[routeKey];
({ initialRevalidate, srcRoute, dataRoute } = pr);
}
const outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
let lambda: undefined | Lambda;
const outputPathData = path.posix.join(entryDirectory, dataRoute);
if (isSharedLambdas) {
const outputSrcPathPage = path.join(
'/',
srcRoute == null
? outputPathPage
: path.join(entryDirectory, srcRoute === '/' ? '/index' : srcRoute)
);
const lambdaId = pageLambdaMap[outputSrcPathPage];
lambda = lambdas[lambdaId];
} else {
const outputSrcPathPage =
srcRoute == null
? outputPathPage
: path.posix.join(
entryDirectory,
srcRoute === '/' ? '/index' : srcRoute
);
lambda = lambdas[outputSrcPathPage];
}
if (lambda == null) {
throw new NowBuildError({
code: 'NEXT_MISSING_LAMBDA',
message: `Unable to find lambda for route: ${routeFileNoExt}`,
});
}
if (initialRevalidate === false) {
if (htmlFsRef == null || jsonFsRef == null) {
throw new NowBuildError({
code: 'NEXT_HTMLFSREF_JSONFSREF',
message: 'invariant: htmlFsRef != null && jsonFsRef != null',
});
}
if (!canUsePreviewMode) {
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
}
}
if (prerenders[outputPathPage] == null) {
prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate,
lambda,
fallback: htmlFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
});
prerenders[outputPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
fallback: jsonFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
});
++prerenderGroup;
}
};
Object.keys(prerenderManifest.staticRoutes).forEach(route =>
onPrerenderRoute(route, { isBlocking: false, isFallback: false })
);
Object.keys(prerenderManifest.fallbackRoutes).forEach(route =>
onPrerenderRoute(route, { isBlocking: false, isFallback: true })
);
Object.keys(prerenderManifest.legacyBlockingRoutes).forEach(route =>
onPrerenderRoute(route, { isBlocking: true, isFallback: false })
);
// We still need to use lazyRoutes if the dataRoutes field
// isn't available for backwards compatibility
if (!(routesManifest && routesManifest.dataRoutes)) {
// Dynamic pages for lazy routes should be handled by the lambda flow.
[
...Object.entries(prerenderManifest.fallbackRoutes),
...Object.entries(prerenderManifest.legacyBlockingRoutes),
].forEach(([, { dataRouteRegex, dataRoute }]) => {
dataRoutes.push({
// Next.js provided data route regex
src: dataRouteRegex.replace(
/^\^/,
`^${appMountPrefixNoTrailingSlash}`
),
// Location of lambda in builder output
dest: path.posix.join(entryDirectory, dataRoute),
check: true,
});
});
}
}
const nextStaticFiles = await glob(
'**',
path.join(entryPath, outputDirectory, 'static')
);
const staticFolderFiles = await glob('**', path.join(entryPath, 'static'));
const publicFolderFiles = await glob('**', path.join(entryPath, 'public'));
const staticFiles = Object.keys(nextStaticFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[path.join(entryDirectory, `_next/static/${file}`)]: nextStaticFiles[
file
],
}),
{}
);
const staticDirectoryFiles = Object.keys(staticFolderFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[path.join(entryDirectory, 'static', file)]: staticFolderFiles[file],
}),
{}
);
const publicDirectoryFiles = Object.keys(publicFolderFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[path.join(
entryDirectory,
file.replace(/^public[/\\]+/, '')
)]: publicFolderFiles[file],
}),
{}
);
if (!isSharedLambdas) {
// We need to delete lambdas from output instead of omitting them from the
// start since we rely on them for powering Preview Mode (read above in
// onPrerenderRoute).
prerenderManifest.omittedRoutes.forEach(routeKey => {
// Get the route file as it'd be mounted in the builder output
const routeFileNoExt = path.posix.join(
entryDirectory,
routeKey === '/' ? '/index' : routeKey
);
if (typeof lambdas[routeFileNoExt] === undefined) {
throw new NowBuildError({
code: 'NEXT__UNKNOWN_ROUTE_KEY',
message: `invariant: unknown lambda ${routeKey} (lookup: ${routeFileNoExt}) | please report this immediately`,
});
}
delete lambdas[routeFileNoExt];
});
}
const mergedDataRoutesLambdaRoutes = [];
const mergedDynamicRoutesLambdaRoutes = [];
if (isSharedLambdas) {
// we need to define the page lambda route immediately after
// the dynamic route in handle: 'rewrite' so that a matching
// dynamic route doesn't catch it before the page lambda route
// e.g. /teams/[team]/[inviteCode] -> page lambda
// but we also have /[teamSlug]/[project]/[id] which could match it first
for (let i = 0; i < dynamicRoutes.length; i++) {
const route = dynamicRoutes[i];
mergedDynamicRoutesLambdaRoutes.push(route);
const { pathname } = url.parse(route.dest!);
if (pathname && pageLambdaMap[pathname]) {
mergedDynamicRoutesLambdaRoutes.push(
dynamicPageLambdaRoutesMap[pathname]
);
}
}
for (let i = 0; i < dataRoutes.length; i++) {
const route = dataRoutes[i];
mergedDataRoutesLambdaRoutes.push(route);
const { pathname } = url.parse(route.dest!);
if (
pathname &&
pageLambdaMap[pathname] &&
dynamicPageLambdaRoutesMap[pathname]
) {
mergedDataRoutesLambdaRoutes.push(dynamicPageLambdaRoutesMap[pathname]);
}
}
}
return {
output: {
...publicDirectoryFiles,
...lambdas,
// Prerenders may override Lambdas -- this is an intentional behavior.
...prerenders,
...staticPages,
...staticFiles,
...staticDirectoryFiles,
},
/*
Desired routes order
- Runtime headers
- User headers and redirects
- Runtime redirects
- Runtime routes
- Check filesystem, if nothing found continue
- User rewrites
- Builder rewrites
*/
routes: [
// headers
...headers,
// redirects
...redirects,
// Make sure to 404 for the /404 path itself
{
src: path.join('/', entryDirectory, '404'),
status: 404,
continue: true,
},
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
// map pages to their lambda
...pageLambdaRoutes,
// map /blog/[post] to correct lambda for iSSG
...dynamicPageLambdaRoutes,
// These need to come before handle: miss or else they are grouped
// with that routing section
...rewrites,
// We need to make sure to 404 for /_next after handle: miss since
// handle: miss is called before rewrites and to prevent rewriting /_next
{ handle: 'miss' },
{
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
),
status: 404,
check: true,
dest: '$0',
},
// routes that are called after each rewrite or after routes
// if there no rewrites
{ handle: 'rewrite' },
// /_next/data routes for getServerProps/getStaticProps pages
...(isSharedLambdas ? mergedDataRoutesLambdaRoutes : dataRoutes),
// re-check page routes to map them to the lambda
...pageLambdaRoutes,
// Dynamic routes (must come after dataRoutes as dataRoutes are more
// specific)
...(isSharedLambdas ? mergedDynamicRoutesLambdaRoutes : dynamicRoutes),
// routes to call after a file has been matched
{ handle: 'hit' },
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: {
'cache-control': `public,max-age=${MAX_AGE_ONE_YEAR},immutable`,
},
continue: true,
},
// error handling
...(isLegacy
? []
: [
// Custom Next.js 404 page
{ handle: 'error' } as Handler,
isSharedLambdas
? {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: path.join(
'/',
(static404Page
? static404Page
: pageLambdaMap[page404Path]) as string
),
status: 404,
headers: {
'x-nextjs-page': page404Path,
},
}
: {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: static404Page
? path.join('/', static404Page)
: path.join(
'/',
entryDirectory,
hasPages404 &&
lambdas[path.join('./', entryDirectory, '404')]
? '404'
: '_error'
),
status: 404,
},
]),
],
watch: [],
childProcesses: [],
};
};
export const prepareCache = async ({
workPath,
entrypoint,
config = {},
}: PrepareCacheOptions): Promise<Files> => {
debug('Preparing cache...');
const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
const outputDirectory = config.outputDirectory || '.next';
const nextVersionRange = await getNextVersionRange(entryPath);
const isLegacy = nextVersionRange && isLegacyNext(nextVersionRange);
if (isLegacy) {
// skip caching legacy mode (swapping deps between all and production can get bug-prone)
return {};
}
debug('Producing cache file manifest...');
const cacheEntrypoint = path.relative(workPath, entryPath);
const cache = {
...(await glob(path.join(cacheEntrypoint, 'node_modules/**'), workPath)),
...(await glob(
path.join(cacheEntrypoint, outputDirectory, 'cache/**'),
workPath
)),
};
debug('Cache file manifest produced');
return cache;
};