config.ts 16.7 KB
Newer Older
1
import chalk from 'chalk'
G
find-up  
Guy Bedford 已提交
2
import findUp from 'next/dist/compiled/find-up'
3
import os from 'os'
4
import { basename, extname } from 'path'
5
import * as Log from '../../build/output/log'
T
Tim Neutkens 已提交
6
import { CONFIG_FILE } from '../lib/constants'
7
import { execOnce } from '../lib/utils'
8
import { ImageConfig, imageConfigDefault, VALID_LOADERS } from './image-config'
9

10
const targets = ['server', 'serverless', 'experimental-serverless-trace']
11
const reactModes = ['legacy', 'blocking', 'concurrent']
T
Tim Neutkens 已提交
12

13
const defaultConfig: { [key: string]: any } = {
14
  env: [],
15
  webpack: null,
16
  webpackDevMiddleware: null,
17
  distDir: '.next',
18
  assetPrefix: '',
19
  configOrigin: 'default',
20
  useFileSystemPublicRoutes: true,
21
  generateBuildId: () => null,
22
  generateEtags: true,
23
  pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
J
Joe Haddad 已提交
24
  target: 'server',
T
Tim Neutkens 已提交
25
  poweredByHeader: true,
26
  compress: true,
27
  analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
28
  images: imageConfigDefault,
29 30 31
  devIndicators: {
    buildActivity: true,
  },
32 33
  onDemandEntries: {
    maxInactiveAge: 60 * 1000,
T
Tim Neutkens 已提交
34
    pagesBufferLength: 2,
35
  },
36 37 38
  amp: {
    canonicalBase: '',
  },
39
  basePath: '',
40
  sassOptions: {},
41
  trailingSlash: false,
J
Joe Haddad 已提交
42
  i18n: false,
43
  experimental: {
44 45 46
    cpus: Math.max(
      1,
      (Number(process.env.CIRCLE_NODE_TOTAL) ||
47
        (os.cpus() || { length: 1 }).length) - 1
48
    ),
49
    plugins: false,
J
Joe Haddad 已提交
50
    profiling: false,
J
JJ Kasper 已提交
51
    sprFlushToDisk: true,
52
    reactMode: 'legacy',
53
    workerThreads: false,
54
    pageEnv: false,
55
    productionBrowserSourceMaps: false,
P
Prateek Bhatnagar 已提交
56
    optimizeFonts: false,
57
    optimizeImages: false,
58
    scrollRestoration: false,
59 60 61
  },
  future: {
    excludeDefaultMomentLocales: false,
T
Tim Neutkens 已提交
62
  },
63 64
  serverRuntimeConfig: {},
  publicRuntimeConfig: {},
G
Gerald Monaco 已提交
65
  reactStrictMode: false,
T
Tim Neutkens 已提交
66 67
}

68
const experimentalWarning = execOnce(() => {
T
Tim Neutkens 已提交
69 70
  Log.warn(chalk.bold('You have enabled experimental feature(s).'))
  Log.warn(
J
Joe Haddad 已提交
71 72 73 74
    `Experimental features are not covered by semver, and may cause unexpected or broken application behavior. ` +
      `Use them at your own risk.`
  )
  console.warn()
75 76
})

77
function assignDefaults(userConfig: { [key: string]: any }) {
78 79 80 81 82 83 84 85 86 87 88
  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
  }

89
  const config = Object.keys(userConfig).reduce<{ [key: string]: any }>(
90
    (currentConfig, key) => {
91
      const value = userConfig[key]
92

93
      if (value === undefined || value === null) {
94
        return currentConfig
95 96
      }

97 98
      if (key === 'experimental' && value && value !== defaultConfig[key]) {
        experimentalWarning()
99
      }
100

101 102 103 104 105 106 107
      if (key === 'distDir') {
        if (typeof value !== 'string') {
          throw new Error(
            `Specified distDir is not a string, found type "${typeof value}"`
          )
        }
        const userDistDir = value.trim()
108

109 110 111 112
        // don't allow public as the distDir as this is a reserved folder for
        // public files
        if (userDistDir === 'public') {
          throw new Error(
113
            `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`
114 115 116 117 118 119 120 121 122
          )
        }
        // 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`
          )
        }
123 124
      }

125 126 127 128 129 130
      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.`
          )
        }
131

132
        if (!value.length) {
133
          throw new Error(
134
            `Specified pageExtensions is an empty array. Please update it with the relevant extensions or remove it.`
135 136 137
          )
        }

138 139 140 141 142 143 144
        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.`
            )
          }
        })
145 146
      }

147
      if (!!value && value.constructor === Object) {
148
        currentConfig[key] = {
149 150 151 152 153 154 155 156 157 158
          ...defaultConfig[key],
          ...Object.keys(value).reduce<any>((c, k) => {
            const v = value[k]
            if (v !== undefined && v !== null) {
              c[k] = v
            }
            return c
          }, {}),
        }
      } else {
159
        currentConfig[key] = value
160 161
      }

162
      return currentConfig
163 164 165 166 167
    },
    {}
  )

  const result = { ...defaultConfig, ...config }
168 169 170

  if (typeof result.assetPrefix !== 'string') {
    throw new Error(
171
      `Specified assetPrefix is not a string, found type "${typeof result.assetPrefix}" https://err.sh/vercel/next.js/invalid-assetprefix`
172 173
    )
  }
174 175 176 177 178 179 180 181 182

  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 === '/') {
T
Tim Neutkens 已提交
183
      throw new Error(
184
        `Specified basePath /. basePath has to be either an empty string or a path prefix"`
T
Tim Neutkens 已提交
185 186 187
      )
    }

188 189 190 191 192
    if (!result.basePath.startsWith('/')) {
      throw new Error(
        `Specified basePath has to start with a /, found "${result.basePath}"`
      )
    }
T
Tim Neutkens 已提交
193

194 195
    if (result.basePath !== '/') {
      if (result.basePath.endsWith('/')) {
T
Tim Neutkens 已提交
196
        throw new Error(
197
          `Specified basePath should not end with /, found "${result.basePath}"`
T
Tim Neutkens 已提交
198 199 200
        )
      }

201 202 203
      if (result.assetPrefix === '') {
        result.assetPrefix = result.basePath
      }
204

205 206
      if (result.amp.canonicalBase === '') {
        result.amp.canonicalBase = result.basePath
T
Tim Neutkens 已提交
207 208
      }
    }
J
Joe Haddad 已提交
209
  }
210

S
Steven 已提交
211
  if (result?.images) {
212
    const images: Partial<ImageConfig> = result.images
213

S
Steven 已提交
214 215
    if (typeof images !== 'object') {
      throw new Error(
216
        `Specified images should be an object received ${typeof images}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
S
Steven 已提交
217 218
      )
    }
219

S
Steven 已提交
220 221 222
    if (images.domains) {
      if (!Array.isArray(images.domains)) {
        throw new Error(
223
          `Specified images.domains should be an Array received ${typeof images.domains}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
S
Steven 已提交
224 225
        )
      }
226 227 228

      if (images.domains.length > 50) {
        throw new Error(
229
          `Specified images.domains exceeds length of 50, received length (${images.domains.length}), please reduce the length of the array to continue.\nSee more info here: https://err.sh/next.js/invalid-images-config`
230 231 232
        )
      }

S
Steven 已提交
233 234 235 236 237 238 239
      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(
            ', '
240
          )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
S
Steven 已提交
241 242 243
        )
      }
    }
244 245 246
    if (images.deviceSizes) {
      const { deviceSizes } = images
      if (!Array.isArray(deviceSizes)) {
S
Steven 已提交
247
        throw new Error(
248
          `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
S
Steven 已提交
249 250
        )
      }
251

252
      if (deviceSizes.length > 25) {
253
        throw new Error(
254
          `Specified images.deviceSizes exceeds length of 25, received length (${deviceSizes.length}), please reduce the length of the array to continue.\nSee more info here: https://err.sh/next.js/invalid-images-config`
255 256 257
        )
      }

258
      const invalid = deviceSizes.filter((d: unknown) => {
259 260 261
        return typeof d !== 'number' || d < 1 || d > 10000
      })

S
Steven 已提交
262 263
      if (invalid.length > 0) {
        throw new Error(
264 265
          `Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
            ', '
266
          )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
267 268 269
        )
      }
    }
270 271 272
    if (images.imageSizes) {
      const { imageSizes } = images
      if (!Array.isArray(imageSizes)) {
273
        throw new Error(
274
          `Specified images.imageSizes should be an Array received ${typeof imageSizes}.\nSee more info here: https://err.sh/next.js/invalid-images-config`
275 276 277
        )
      }

278
      if (imageSizes.length > 25) {
279
        throw new Error(
280
          `Specified images.imageSizes exceeds length of 25, received length (${imageSizes.length}), please reduce the length of the array to continue.\nSee more info here: https://err.sh/next.js/invalid-images-config`
281 282 283
        )
      }

284
      const invalid = imageSizes.filter((d: unknown) => {
285 286 287 288 289
        return typeof d !== 'number' || d < 1 || d > 10000
      })

      if (invalid.length > 0) {
        throw new Error(
290
          `Specified images.imageSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
S
Steven 已提交
291
            ', '
292
          )}).\nSee more info here: https://err.sh/next.js/invalid-images-config`
S
Steven 已提交
293 294 295
        )
      }
    }
296

297 298 299 300 301 302 303 304 305 306 307 308 309 310
    if (!images.loader) {
      images.loader = 'default'
    }

    if (!VALID_LOADERS.includes(images.loader)) {
      throw new Error(
        `Specified images.loader should be one of (${VALID_LOADERS.join(
          ', '
        )}), received invalid value (${
          images.loader
        }).\nSee more info here: https://err.sh/next.js/invalid-images-config`
      )
    }

311 312
    // Append trailing slash for non-default loaders
    if (images.path) {
313 314 315 316
      if (
        images.loader !== 'default' &&
        images.path[images.path.length - 1] !== '/'
      ) {
317 318 319
        images.path += '/'
      }
    }
S
Steven 已提交
320 321
  }

J
Joe Haddad 已提交
322 323
  if (result.i18n) {
    const { i18n } = result
324 325 326
    const i18nType = typeof i18n

    if (i18nType !== 'object') {
327
      throw new Error(
328
        `Specified i18n should be an object received ${i18nType}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
329
      )
330 331 332 333
    }

    if (!Array.isArray(i18n.locales)) {
      throw new Error(
334
        `Specified i18n.locales should be an Array received ${typeof i18n.locales}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
335 336 337 338 339 340
      )
    }

    const defaultLocaleType = typeof i18n.defaultLocale

    if (!i18n.defaultLocale || defaultLocaleType !== 'string') {
341
      throw new Error(
342
        `Specified i18n.defaultLocale should be a string.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
343
      )
344 345
    }

346 347
    if (typeof i18n.domains !== 'undefined' && !Array.isArray(i18n.domains)) {
      throw new Error(
348
        `Specified i18n.domains must be an array of domain objects e.g. [ { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] } ] received ${typeof i18n.domains}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
349 350 351 352 353 354 355 356 357
      )
    }

    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

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
        let hasInvalidLocale = false

        if (Array.isArray(item.locales)) {
          for (const locale of item.locales) {
            if (typeof locale !== 'string') hasInvalidLocale = true

            for (const domainItem of i18n.domains) {
              if (domainItem === item) continue
              if (domainItem.locales && domainItem.locales.includes(locale)) {
                console.warn(
                  `Both ${item.domain} and ${domainItem.domain} configured the locale (${locale}) but only one can. Remove it from one i18n.domains config to continue`
                )
                hasInvalidLocale = true
                break
              }
            }
          }
        }

        return hasInvalidLocale
378 379 380 381 382 383 384 385
      })

      if (invalidDomainItems.length > 0) {
        throw new Error(
          `Invalid i18n.domains values:\n${invalidDomainItems
            .map((item: any) => JSON.stringify(item))
            .join(
              '\n'
386
            )}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
387 388 389 390
        )
      }
    }

391 392
    if (!Array.isArray(i18n.locales)) {
      throw new Error(
393
        `Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
394 395 396 397 398 399 400 401 402
      )
    }

    const invalidLocales = i18n.locales.filter(
      (locale: any) => typeof locale !== 'string'
    )

    if (invalidLocales.length > 0) {
      throw new Error(
403 404 405 406 407
        `Specified i18n.locales contains invalid values (${invalidLocales
          .map(String)
          .join(
            ', '
          )}), locales must be valid locale tags provided as strings e.g. "en-US".\n` +
408 409 410 411
          `See here for list of valid language sub-tags: http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry`
      )
    }

412 413
    if (!i18n.locales.includes(i18n.defaultLocale)) {
      throw new Error(
414
        `Specified i18n.defaultLocale should be included in i18n.locales.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
415 416 417
      )
    }

418 419 420 421 422 423
    // make sure default Locale is at the front
    i18n.locales = [
      i18n.defaultLocale,
      ...i18n.locales.filter((locale: string) => locale !== i18n.defaultLocale),
    ]

424
    const localeDetectionType = typeof i18n.localeDetection
425 426 427 428 429 430

    if (
      localeDetectionType !== 'boolean' &&
      localeDetectionType !== 'undefined'
    ) {
      throw new Error(
431
        `Specified i18n.localeDetection should be undefined or a boolean received ${localeDetectionType}.\nSee more info here: https://err.sh/next.js/invalid-i18n-config`
432 433 434 435
      )
    }
  }

J
Joe Haddad 已提交
436
  return result
437 438
}

439
export function normalizeConfig(phase: string, config: any) {
T
Tim Neutkens 已提交
440
  if (typeof config === 'function') {
441
    config = config(phase, { defaultConfig })
T
Tim Neutkens 已提交
442

443 444
    if (typeof config.then === 'function') {
      throw new Error(
445
        '> Promise returned in next config. https://err.sh/vercel/next.js/promise-in-next-config'
446 447 448
      )
    }
  }
T
Tim Neutkens 已提交
449
  return config
450
}
N
Naoyuki Kanezawa 已提交
451

452 453 454
export default function loadConfig(
  phase: string,
  dir: string,
J
Joe Haddad 已提交
455
  customConfig?: object | null
456
) {
457
  if (customConfig) {
458
    return assignDefaults({ configOrigin: 'server', ...customConfig })
459
  }
T
Tim Neutkens 已提交
460
  const path = findUp.sync(CONFIG_FILE, {
T
Tim Neutkens 已提交
461
    cwd: dir,
462
  })
N
Naoyuki Kanezawa 已提交
463

464
  // If config file was found
465
  if (path?.length) {
466
    const userConfigModule = require(path)
467 468 469 470
    const userConfig = normalizeConfig(
      phase,
      userConfigModule.default || userConfigModule
    )
471 472

    if (Object.keys(userConfig).length === 0) {
473 474
      Log.warn(
        'Detected next.config.js, no exported configuration found. https://err.sh/vercel/next.js/empty-configuration'
475 476 477
      )
    }

T
Tim Neutkens 已提交
478
    if (userConfig.target && !targets.includes(userConfig.target)) {
479 480 481 482 483
      throw new Error(
        `Specified target is invalid. Provided: "${
          userConfig.target
        }" should be one of ${targets.join(', ')}`
      )
484
    }
485

486
    if (userConfig.amp?.canonicalBase) {
487
      const { canonicalBase } = userConfig.amp || ({} as any)
488
      userConfig.amp = userConfig.amp || {}
489 490 491 492
      userConfig.amp.canonicalBase =
        (canonicalBase.endsWith('/')
          ? canonicalBase.slice(0, -1)
          : canonicalBase) || ''
493 494
    }

495
    if (
496
      userConfig.experimental?.reactMode &&
497 498 499 500 501 502 503 504 505
      !reactModes.includes(userConfig.experimental.reactMode)
    ) {
      throw new Error(
        `Specified React Mode is invalid. Provided: ${
          userConfig.experimental.reactMode
        } should be one of ${reactModes.join(', ')}`
      )
    }

506 507 508 509 510
    return assignDefaults({
      configOrigin: CONFIG_FILE,
      configFile: path,
      ...userConfig,
    })
511 512 513 514 515 516 517 518 519 520 521
  } else {
    const configBaseName = basename(CONFIG_FILE, extname(CONFIG_FILE))
    const nonJsPath = findUp.sync(
      [
        `${configBaseName}.jsx`,
        `${configBaseName}.ts`,
        `${configBaseName}.tsx`,
        `${configBaseName}.json`,
      ],
      { cwd: dir }
    )
522
    if (nonJsPath?.length) {
523 524 525 526 527 528
      throw new Error(
        `Configuring Next.js via '${basename(
          nonJsPath
        )}' is not supported. Please replace the file with 'next.config.js'.`
      )
    }
529
  }
530

531
  return defaultConfig
N
Naoyuki Kanezawa 已提交
532
}
533 534 535 536 537 538

export function isTargetLikeServerless(target: string) {
  const isServerless = target === 'serverless'
  const isServerlessTrace = target === 'experimental-serverless-trace'
  return isServerless || isServerlessTrace
}