index.ts 10.7 KB
Newer Older
1
import chalk from 'chalk'
J
Joe Haddad 已提交
2 3 4 5
import findUp from 'find-up'
import {
  copyFile as copyFileOrig,
  existsSync,
6
  mkdir as mkdirOrig,
J
Joe Haddad 已提交
7 8 9
  readFileSync,
  writeFileSync,
} from 'fs'
10
import Worker from 'jest-worker'
J
Joe Haddad 已提交
11
import { cpus } from 'os'
12
import { dirname, join, resolve, sep } from 'path'
J
Joe Haddad 已提交
13 14 15
import { promisify } from 'util'
import { AmpPageStatus, formatAmpMessages } from '../build/output/index'
import createSpinner from '../build/spinner'
16 17 18
import { API_ROUTE } from '../lib/constants'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveDelete } from '../lib/recursive-delete'
19 20
import {
  BUILD_ID_FILE,
J
Joe Haddad 已提交
21 22 23
  CLIENT_PUBLIC_FILES_PATH,
  CLIENT_STATIC_FILES_PATH,
  CONFIG_FILE,
J
Joe Haddad 已提交
24
  EXPORT_DETAIL,
J
Joe Haddad 已提交
25 26
  PAGES_MANIFEST,
  PHASE_EXPORT,
J
JJ Kasper 已提交
27 28
  PRERENDER_MANIFEST,
  SERVERLESS_DIRECTORY,
J
Joe Haddad 已提交
29
  SERVER_DIRECTORY,
30
} from '../next-server/lib/constants'
J
Joe Haddad 已提交
31 32 33
import loadConfig, {
  isTargetLikeServerless,
} from '../next-server/server/config'
34
import { eventCliSession } from '../telemetry/events'
J
Joe Haddad 已提交
35
import { Telemetry } from '../telemetry/storage'
36
import { normalizePagePath } from '../next-server/server/normalize-page-path'
37

J
JJ Kasper 已提交
38
const copyFile = promisify(copyFileOrig)
39
const mkdir = promisify(mkdirOrig)
40

J
Joe Haddad 已提交
41
const createProgress = (total: number, label = 'Exporting') => {
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
  let curProgress = 0
  let progressSpinner = createSpinner(`${label} (${curProgress}/${total})`, {
    spinner: {
      frames: [
        '[    ]',
        '[=   ]',
        '[==  ]',
        '[=== ]',
        '[ ===]',
        '[  ==]',
        '[   =]',
        '[    ]',
        '[   =]',
        '[  ==]',
        '[ ===]',
        '[====]',
        '[=== ]',
        '[==  ]',
J
Joe Haddad 已提交
60
        '[=   ]',
61
      ],
J
Joe Haddad 已提交
62 63
      interval: 80,
    },
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  })

  return () => {
    curProgress++

    const newText = `${label} (${curProgress}/${total})`
    if (progressSpinner) {
      progressSpinner.text = newText
    } else {
      console.log(newText)
    }

    if (curProgress === total && progressSpinner) {
      progressSpinner.stop()
      console.log(newText)
    }
  }
}

J
Joe Haddad 已提交
83 84 85 86 87 88 89 90 91 92 93 94 95
type ExportPathMap = {
  [page: string]: { page: string; query?: { [key: string]: string } }
}

export default async function(
  dir: string,
  options: any,
  configuration?: any
): Promise<void> {
  function log(message: string) {
    if (options.silent) {
      return
    }
96 97 98
    console.log(message)
  }

99
  dir = resolve(dir)
100
  const nextConfig = configuration || loadConfig(PHASE_EXPORT, dir)
101
  const threads = options.threads || Math.max(cpus().length - 1, 1)
102
  const distDir = join(dir, nextConfig.distDir)
103 104 105 106

  const telemetry = options.buildExport ? null : new Telemetry({ distDir })

  if (telemetry) {
107
    telemetry.record(
108
      eventCliSession(PHASE_EXPORT, distDir, {
109 110 111 112 113 114
        cliCommand: 'export',
        isSrcDir: null,
        hasNowJson: !!(await findUp('now.json', { cwd: dir })),
        isCustomServer: null,
      })
    )
115 116
  }

117
  const subFolders = nextConfig.exportTrailingSlash
J
JJ Kasper 已提交
118
  const isLikeServerless = nextConfig.target !== 'server'
119

120
  log(`> using build directory: ${distDir}`)
121

122
  if (!existsSync(distDir)) {
123 124 125
    throw new Error(
      `Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
    )
126 127
  }

128
  const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
129
  const pagesManifest =
130 131 132 133 134 135
    !options.pages &&
    require(join(
      distDir,
      isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
      PAGES_MANIFEST
    ))
136

J
JJ Kasper 已提交
137 138 139 140 141 142 143 144 145 146 147 148 149
  let prerenderManifest
  try {
    prerenderManifest = require(join(distDir, PRERENDER_MANIFEST))
  } catch (_) {}

  const distPagesDir = join(
    distDir,
    isLikeServerless
      ? SERVERLESS_DIRECTORY
      : join(SERVER_DIRECTORY, 'static', buildId),
    'pages'
  )

J
JJ Kasper 已提交
150
  const pages = options.pages || Object.keys(pagesManifest)
J
Joe Haddad 已提交
151
  const defaultPathMap: ExportPathMap = {}
152 153

  for (const page of pages) {
154 155
    // _document and _app are not real pages
    // _error is exported as 404.html later on
156
    // API Routes are Node.js functions
157 158 159 160
    if (
      page === '/_document' ||
      page === '/_app' ||
      page === '/_error' ||
161
      page.match(API_ROUTE)
162
    ) {
163 164 165
      continue
    }

166 167 168 169
    // iSSG pages that are dynamic should not export templated version by
    // default. In most cases, this would never work. There is no server that
    // could run `getStaticProps`. If users make their page work lazily, they
    // can manually add it to the `exportPathMap`.
170
    if (prerenderManifest?.dynamicRoutes[page]) {
171 172 173
      continue
    }

174 175
    defaultPathMap[page] = { page }
  }
176 177

  // Initialize the output directory
178
  const outDir = options.outdir
179 180 181 182 183 184 185

  if (outDir === join(dir, 'public')) {
    throw new Error(
      `The 'public' directory is reserved in Next.js and can not be used as the export out directory. https://err.sh/zeit/next.js/can-not-output-to-public`
    )
  }

186
  await recursiveDelete(join(outDir))
187
  await mkdir(join(outDir, '_next', buildId), { recursive: true })
188

J
Joe Haddad 已提交
189 190 191 192 193 194 195 196 197 198
  writeFileSync(
    join(distDir, EXPORT_DETAIL),
    JSON.stringify({
      version: 1,
      outDirectory: outDir,
      success: false,
    }),
    'utf8'
  )

199
  // Copy static directory
200
  if (!options.buildExport && existsSync(join(dir, 'static'))) {
201
    log('  copying "static" directory')
202
    await recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
203 204
  }

205
  // Copy .next/static directory
206
  if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
207
    log('  copying "static build" directory')
208
    await recursiveCopy(
209 210
      join(distDir, CLIENT_STATIC_FILES_PATH),
      join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
211 212 213
    )
  }

214
  // Get the exportPathMap from the config file
215
  if (typeof nextConfig.exportPathMap !== 'function') {
216 217 218
    console.log(
      `> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
    )
J
Joe Haddad 已提交
219
    nextConfig.exportPathMap = async (defaultMap: ExportPathMap) => {
220 221
      return defaultMap
    }
222 223
  }

224 225 226 227
  // Start the rendering process
  const renderOpts = {
    dir,
    buildId,
228
    nextExport: true,
229
    assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
230
    distDir,
231 232
    dev: false,
    staticMarkup: false,
233
    hotReloader: null,
234
    canonicalBase: nextConfig.amp?.canonicalBase || '',
J
Joe Haddad 已提交
235
    isModern: nextConfig.experimental.modern,
236
    ampValidatorPath: nextConfig.experimental.amp?.validator || undefined,
237 238
    ampSkipValidation: nextConfig.experimental.amp?.skipValidation || false,
    ampOptimizerConfig: nextConfig.experimental.amp?.optimizer || undefined,
239 240
  }

241
  const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
242

243
  if (Object.keys(publicRuntimeConfig).length > 0) {
J
Joe Haddad 已提交
244
    ;(renderOpts as any).runtimeConfig = publicRuntimeConfig
245 246
  }

247
  // We need this for server rendering the Link component.
J
Joe Haddad 已提交
248 249
  ;(global as any).__NEXT_DATA__ = {
    nextExport: true,
250 251
  }

252
  log(`  launching ${threads} workers`)
253 254 255 256 257
  const exportPathMap = await nextConfig.exportPathMap(defaultPathMap, {
    dev: false,
    dir,
    outDir,
    distDir,
J
Joe Haddad 已提交
258
    buildId,
259
  })
260 261
  if (!exportPathMap['/404'] && !exportPathMap['/404.html']) {
    exportPathMap['/404'] = exportPathMap['/404.html'] = {
J
Joe Haddad 已提交
262
      page: '/_error',
263
    }
264
  }
265
  const exportPaths = Object.keys(exportPathMap)
266 267 268 269 270 271 272 273 274 275 276 277 278 279
  const filteredPaths = exportPaths.filter(
    // Remove API routes
    route => !exportPathMap[route].page.match(API_ROUTE)
  )
  const hasApiRoutes = exportPaths.length !== filteredPaths.length

  // Warn if the user defines a path for an API page
  if (hasApiRoutes) {
    log(
      chalk.yellow(
        '  API pages are not supported by next export. https://err.sh/zeit/next.js/api-routes-static-export'
      )
    )
  }
280

281
  const progress = !options.silent && createProgress(filteredPaths.length)
282
  const pagesDataDir = options.buildExport
J
JJ Kasper 已提交
283 284
    ? outDir
    : join(outDir, '_next/data', buildId)
A
Arunoda Susiripala 已提交
285

J
Joe Haddad 已提交
286
  const ampValidations: AmpPageStatus = {}
J
JJ Kasper 已提交
287 288
  let hadValidationError = false

289 290
  const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
  // Copy public directory
291
  if (!options.buildExport && existsSync(publicDir)) {
292
    log('  copying "public" directory')
293
    await recursiveCopy(publicDir, outDir, {
J
Joe Haddad 已提交
294
      filter(path) {
295
        // Exclude paths used by pages
296
        return !exportPathMap[path]
J
Joe Haddad 已提交
297
      },
298
    })
299
  }
300

J
Joe Haddad 已提交
301 302 303 304 305
  const worker: Worker & { default: Function } = new Worker(
    require.resolve('./worker'),
    {
      maxRetries: 0,
      numWorkers: threads,
306
      enableWorkerThreads: nextConfig.experimental.workerThreads,
J
Joe Haddad 已提交
307 308 309
      exposedMethods: ['default'],
    }
  ) as any
310 311 312 313 314

  worker.getStdout().pipe(process.stdout)
  worker.getStderr().pipe(process.stderr)

  let renderError = false
315

316
  await Promise.all(
317 318 319 320 321 322 323
    filteredPaths.map(async path => {
      const result = await worker.default({
        path,
        pathMap: exportPathMap[path],
        distDir,
        buildId,
        outDir,
324
        pagesDataDir,
325 326 327
        renderOpts,
        serverRuntimeConfig,
        subFolders,
J
JJ Kasper 已提交
328
        buildExport: options.buildExport,
J
Joe Haddad 已提交
329
        serverless: isTargetLikeServerless(nextConfig.target),
330 331 332 333 334
      })

      for (const validation of result.ampValidations || []) {
        const { page, result } = validation
        ampValidations[page] = result
J
Joe Haddad 已提交
335 336
        hadValidationError =
          hadValidationError ||
337
          (Array.isArray(result?.errors) && result.errors.length > 0)
338
      }
J
Joe Haddad 已提交
339
      renderError = renderError || !!result.error
J
JJ Kasper 已提交
340 341 342 343 344 345 346 347

      if (
        options.buildExport &&
        typeof result.fromBuildExportRevalidate !== 'undefined'
      ) {
        configuration.initialPageRevalidationMap[path] =
          result.fromBuildExportRevalidate
      }
348 349
      if (progress) progress()
    })
350
  )
351

352
  worker.end()
J
JJ Kasper 已提交
353

J
JJ Kasper 已提交
354 355 356 357
  // copy prerendered routes to outDir
  if (!options.buildExport && prerenderManifest) {
    await Promise.all(
      Object.keys(prerenderManifest.routes).map(async route => {
358
        route = normalizePagePath(route)
J
JJ Kasper 已提交
359
        const orig = join(distPagesDir, route)
360 361 362 363 364 365
        const htmlDest = join(
          outDir,
          `${route}${
            subFolders && route !== '/index' ? `${sep}index` : ''
          }.html`
        )
366
        const jsonDest = join(pagesDataDir, `${route}.json`)
J
JJ Kasper 已提交
367

368 369
        await mkdir(dirname(htmlDest), { recursive: true })
        await mkdir(dirname(jsonDest), { recursive: true })
J
JJ Kasper 已提交
370 371 372 373 374 375
        await copyFile(`${orig}.html`, htmlDest)
        await copyFile(`${orig}.json`, jsonDest)
      })
    )
  }

J
JJ Kasper 已提交
376 377 378 379
  if (Object.keys(ampValidations).length) {
    console.log(formatAmpMessages(ampValidations))
  }
  if (hadValidationError) {
380 381 382
    throw new Error(
      `AMP Validation caused the export to fail. https://err.sh/zeit/next.js/amp-export-validation`
    )
J
JJ Kasper 已提交
383 384
  }

385 386 387
  if (renderError) {
    throw new Error(`Export encountered errors`)
  }
388 389
  // Add an empty line to the console for the better readability.
  log('')
390

J
Joe Haddad 已提交
391 392 393 394 395 396 397 398 399 400
  writeFileSync(
    join(distDir, EXPORT_DETAIL),
    JSON.stringify({
      version: 1,
      outDirectory: outDir,
      success: true,
    }),
    'utf8'
  )

401 402 403
  if (telemetry) {
    await telemetry.flush()
  }
404
}