import chalk from 'next/dist/compiled/chalk' import findUp from 'next/dist/compiled/find-up' import os from 'os' import { basename, extname } from 'path' import * as Log from '../../build/output/log' import { CONFIG_FILE } from '../lib/constants' import { execOnce } from '../lib/utils' const targets = ['server', 'serverless', 'experimental-serverless-trace'] const reactModes = ['legacy', 'blocking', 'concurrent'] const defaultConfig: { [key: string]: any } = { env: [], webpack: null, webpackDevMiddleware: null, distDir: '.next', assetPrefix: '', configOrigin: 'default', useFileSystemPublicRoutes: true, generateBuildId: () => null, generateEtags: true, pageExtensions: ['tsx', 'ts', 'jsx', 'js'], target: 'server', poweredByHeader: true, compress: true, images: { sizes: [320, 420, 768, 1024, 1200], domains: [], path: '/_next/image', loader: 'default', }, devIndicators: { buildActivity: true, autoPrerender: true, }, onDemandEntries: { maxInactiveAge: 60 * 1000, pagesBufferLength: 2, }, amp: { canonicalBase: '', }, basePath: '', sassOptions: {}, trailingSlash: false, experimental: { cpus: Math.max( 1, (Number(process.env.CIRCLE_NODE_TOTAL) || (os.cpus() || { length: 1 }).length) - 1 ), modern: false, plugins: false, profiling: false, sprFlushToDisk: true, reactMode: 'legacy', workerThreads: false, pageEnv: false, productionBrowserSourceMaps: false, optimizeFonts: false, optimizeImages: false, scrollRestoration: false, i18n: false, analyticsId: process.env.VERCEL_ANALYTICS_ID || '', }, future: { excludeDefaultMomentLocales: false, }, serverRuntimeConfig: {}, publicRuntimeConfig: {}, reactStrictMode: false, } const experimentalWarning = execOnce(() => { Log.warn(chalk.bold('You have enabled experimental feature(s).')) Log.warn( `Experimental features are not covered by semver, and may cause unexpected or broken application behavior. ` + `Use them at your own risk.` ) console.warn() }) function assignDefaults(userConfig: { [key: string]: any }) { if (typeof userConfig.exportTrailingSlash !== 'undefined') { console.warn( chalk.yellow.bold('Warning: ') + 'The "exportTrailingSlash" option has been renamed to "trailingSlash". Please update your next.config.js.' ) if (typeof userConfig.trailingSlash === 'undefined') { userConfig.trailingSlash = userConfig.exportTrailingSlash } delete userConfig.exportTrailingSlash } const config = Object.keys(userConfig).reduce<{ [key: string]: any }>( (currentConfig, key) => { const value = userConfig[key] if (value === undefined || value === null) { return currentConfig } if (key === 'experimental' && value && value !== defaultConfig[key]) { experimentalWarning() } if (key === 'distDir') { if (typeof value !== 'string') { throw new Error( `Specified distDir is not a string, found type "${typeof value}"` ) } const userDistDir = value.trim() // don't allow public as the distDir as this is a reserved folder for // public files if (userDistDir === 'public') { throw new Error( `The 'public' directory is reserved in Next.js and can not be set as the 'distDir'. https://err.sh/vercel/next.js/can-not-output-to-public` ) } // make sure distDir isn't an empty string as it can result in the provided // directory being deleted in development mode if (userDistDir.length === 0) { throw new Error( `Invalid distDir provided, distDir can not be an empty string. Please remove this config or set it to undefined` ) } } if (key === 'pageExtensions') { if (!Array.isArray(value)) { throw new Error( `Specified pageExtensions is not an array of strings, found "${value}". Please update this config or remove it.` ) } if (!value.length) { throw new Error( `Specified pageExtensions is an empty array. Please update it with the relevant extensions or remove it.` ) } value.forEach((ext) => { if (typeof ext !== 'string') { throw new Error( `Specified pageExtensions is not an array of strings, found "${ext}" of type "${typeof ext}". Please update this config or remove it.` ) } }) } if (!!value && value.constructor === Object) { currentConfig[key] = { ...defaultConfig[key], ...Object.keys(value).reduce((c, k) => { const v = value[k] if (v !== undefined && v !== null) { c[k] = v } return c }, {}), } } else { currentConfig[key] = value } return currentConfig }, {} ) const result = { ...defaultConfig, ...config } if (typeof result.assetPrefix !== 'string') { throw new Error( `Specified assetPrefix is not a string, found type "${typeof result.assetPrefix}" https://err.sh/vercel/next.js/invalid-assetprefix` ) } if (typeof result.basePath !== 'string') { throw new Error( `Specified basePath is not a string, found type "${typeof result.basePath}"` ) } if (result.basePath !== '') { if (result.basePath === '/') { throw new Error( `Specified basePath /. basePath has to be either an empty string or a path prefix"` ) } if (!result.basePath.startsWith('/')) { throw new Error( `Specified basePath has to start with a /, found "${result.basePath}"` ) } if (result.basePath !== '/') { if (result.basePath.endsWith('/')) { throw new Error( `Specified basePath should not end with /, found "${result.basePath}"` ) } if (result.assetPrefix === '') { result.assetPrefix = result.basePath } if (result.amp.canonicalBase === '') { result.amp.canonicalBase = result.basePath } } } if (result?.images) { const { images } = result // Normalize defined image host to end in slash if (images?.path) { if (images.path[images.path.length - 1] !== '/') { images.path += '/' } } if (typeof images !== 'object') { throw new Error( `Specified images should be an object received ${typeof images}` ) } if (images.domains) { if (!Array.isArray(images.domains)) { throw new Error( `Specified images.domains should be an Array received ${typeof images.domains}` ) } const invalid = images.domains.filter( (d: unknown) => typeof d !== 'string' ) if (invalid.length > 0) { throw new Error( `Specified images.domains should be an Array of strings received invalid values (${invalid.join( ', ' )})` ) } } if (images.sizes) { if (!Array.isArray(images.sizes)) { throw new Error( `Specified images.sizes should be an Array received ${typeof images.sizes}` ) } const invalid = images.sizes.filter((d: unknown) => typeof d !== 'number') if (invalid.length > 0) { throw new Error( `Specified images.sizes should be an Array of numbers received invalid values (${invalid.join( ', ' )})` ) } } } if (result.experimental?.i18n) { const { i18n } = result.experimental const i18nType = typeof i18n if (i18nType !== 'object') { throw new Error(`Specified i18n should be an object received ${i18nType}`) } if (!Array.isArray(i18n.locales)) { throw new Error( `Specified i18n.locales should be an Array received ${typeof i18n.locales}` ) } const defaultLocaleType = typeof i18n.defaultLocale if (!i18n.defaultLocale || defaultLocaleType !== 'string') { throw new Error(`Specified i18n.defaultLocale should be a string`) } if (typeof i18n.domains !== 'undefined' && !Array.isArray(i18n.domains)) { throw new Error( `Specified i18n.domains must be an array of domain objects e.g. [ { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] } ] received ${typeof i18n.domains}` ) } if (i18n.domains) { const invalidDomainItems = i18n.domains.filter((item: any) => { if (!item || typeof item !== 'object') return true if (!item.defaultLocale) return true if (!item.domain || typeof item.domain !== 'string') return true return false }) if (invalidDomainItems.length > 0) { throw new Error( `Invalid i18n.domains values:\n${invalidDomainItems .map((item: any) => JSON.stringify(item)) .join( '\n' )}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }` ) } } if (!Array.isArray(i18n.locales)) { throw new Error( `Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}` ) } const invalidLocales = i18n.locales.filter( (locale: any) => typeof locale !== 'string' ) if (invalidLocales.length > 0) { throw new Error( `Specified i18n.locales contains invalid values, locales must be valid locale tags provided as strings e.g. "en-US".\n` + `See here for list of valid language sub-tags: http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry` ) } if (!i18n.locales.includes(i18n.defaultLocale)) { throw new Error( `Specified i18n.defaultLocale should be included in i18n.locales` ) } // make sure default Locale is at the front i18n.locales = [ i18n.defaultLocale, ...i18n.locales.filter((locale: string) => locale !== i18n.defaultLocale), ] const localeDetectionType = typeof i18n.locales.localeDetection if ( localeDetectionType !== 'boolean' && localeDetectionType !== 'undefined' ) { throw new Error( `Specified i18n.localeDetection should be undefined or a boolean received ${localeDetectionType}` ) } } return result } export function normalizeConfig(phase: string, config: any) { if (typeof config === 'function') { config = config(phase, { defaultConfig }) if (typeof config.then === 'function') { throw new Error( '> Promise returned in next config. https://err.sh/vercel/next.js/promise-in-next-config' ) } } return config } export default function loadConfig( phase: string, dir: string, customConfig?: object | null ) { if (customConfig) { return assignDefaults({ configOrigin: 'server', ...customConfig }) } const path = findUp.sync(CONFIG_FILE, { cwd: dir, }) // If config file was found if (path?.length) { const userConfigModule = require(path) const userConfig = normalizeConfig( phase, userConfigModule.default || userConfigModule ) if (Object.keys(userConfig).length === 0) { Log.warn( 'Detected next.config.js, no exported configuration found. https://err.sh/vercel/next.js/empty-configuration' ) } if (userConfig.target && !targets.includes(userConfig.target)) { throw new Error( `Specified target is invalid. Provided: "${ userConfig.target }" should be one of ${targets.join(', ')}` ) } if (userConfig.amp?.canonicalBase) { const { canonicalBase } = userConfig.amp || ({} as any) userConfig.amp = userConfig.amp || {} userConfig.amp.canonicalBase = (canonicalBase.endsWith('/') ? canonicalBase.slice(0, -1) : canonicalBase) || '' } if ( userConfig.experimental?.reactMode && !reactModes.includes(userConfig.experimental.reactMode) ) { throw new Error( `Specified React Mode is invalid. Provided: ${ userConfig.experimental.reactMode } should be one of ${reactModes.join(', ')}` ) } return assignDefaults({ configOrigin: CONFIG_FILE, configFile: path, ...userConfig, }) } else { const configBaseName = basename(CONFIG_FILE, extname(CONFIG_FILE)) const nonJsPath = findUp.sync( [ `${configBaseName}.jsx`, `${configBaseName}.ts`, `${configBaseName}.tsx`, `${configBaseName}.json`, ], { cwd: dir } ) if (nonJsPath?.length) { throw new Error( `Configuring Next.js via '${basename( nonJsPath )}' is not supported. Please replace the file with 'next.config.js'.` ) } } return defaultConfig } export function isTargetLikeServerless(target: string) { const isServerless = target === 'serverless' const isServerlessTrace = target === 'experimental-serverless-trace' return isServerless || isServerlessTrace }