index.ts 9.4 KB
Newer Older
1
import chalk from 'chalk'
J
Joe Haddad 已提交
2
import { copyFile as copyFileOrig, existsSync, readFileSync } from 'fs'
3
import Worker from 'jest-worker'
4
import mkdirpModule from 'mkdirp'
J
Joe Haddad 已提交
5 6 7 8 9 10
import { cpus } from 'os'
import { dirname, join, resolve } from 'path'
import { promisify } from 'util'

import { AmpPageStatus, formatAmpMessages } from '../build/output/index'
import createSpinner from '../build/spinner'
11 12 13
import { API_ROUTE } from '../lib/constants'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveDelete } from '../lib/recursive-delete'
14 15
import {
  BUILD_ID_FILE,
J
Joe Haddad 已提交
16 17 18 19 20
  CLIENT_PUBLIC_FILES_PATH,
  CLIENT_STATIC_FILES_PATH,
  CONFIG_FILE,
  PAGES_MANIFEST,
  PHASE_EXPORT,
J
JJ Kasper 已提交
21
  PRERENDER_MANIFEST,
J
Joe Haddad 已提交
22
  SERVER_DIRECTORY,
J
JJ Kasper 已提交
23
  SERVERLESS_DIRECTORY,
24
} from '../next-server/lib/constants'
J
Joe Haddad 已提交
25 26 27
import loadConfig, {
  isTargetLikeServerless,
} from '../next-server/server/config'
J
Joe Haddad 已提交
28 29
import { eventVersion } from '../telemetry/events'
import { Telemetry } from '../telemetry/storage'
30 31

const mkdirp = promisify(mkdirpModule)
J
JJ Kasper 已提交
32
const copyFile = promisify(copyFileOrig)
33

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

  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 已提交
76 77 78 79 80 81 82 83 84 85 86 87 88
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
    }
89 90 91
    console.log(message)
  }

92
  dir = resolve(dir)
93
  const nextConfig = configuration || loadConfig(PHASE_EXPORT, dir)
94
  const threads = options.threads || Math.max(cpus().length - 1, 1)
95
  const distDir = join(dir, nextConfig.distDir)
96
  if (!options.buildExport) {
J
Joe Haddad 已提交
97
    const telemetry = new Telemetry({ distDir })
98
    telemetry.record(eventVersion({ cliCommand: 'export', isSrcDir: null }))
99 100
  }

101
  const subFolders = nextConfig.exportTrailingSlash
J
JJ Kasper 已提交
102
  const isLikeServerless = nextConfig.target !== 'server'
103

J
JJ Kasper 已提交
104
  if (!options.buildExport && isLikeServerless) {
105 106 107 108
    throw new Error(
      'Cannot export when target is not server. https://err.sh/zeit/next.js/next-export-serverless'
    )
  }
109

110
  log(`> using build directory: ${distDir}`)
111

112
  if (!existsSync(distDir)) {
113 114 115
    throw new Error(
      `Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
    )
116 117
  }

118
  const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
119 120
  const pagesManifest =
    !options.pages && require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST))
121

J
JJ Kasper 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134
  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 已提交
135
  const pages = options.pages || Object.keys(pagesManifest)
J
Joe Haddad 已提交
136
  const defaultPathMap: ExportPathMap = {}
137 138

  for (const page of pages) {
139 140
    // _document and _app are not real pages
    // _error is exported as 404.html later on
141
    // API Routes are Node.js functions
142 143 144 145
    if (
      page === '/_document' ||
      page === '/_app' ||
      page === '/_error' ||
146
      page.match(API_ROUTE)
147
    ) {
148 149 150
      continue
    }

151 152
    defaultPathMap[page] = { page }
  }
153 154

  // Initialize the output directory
155
  const outDir = options.outdir
156 157 158 159 160 161 162

  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`
    )
  }

163
  await recursiveDelete(join(outDir))
164 165
  await mkdirp(join(outDir, '_next', buildId))

166
  // Copy static directory
167
  if (!options.buildExport && existsSync(join(dir, 'static'))) {
168
    log('  copying "static" directory')
169
    await recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
170 171
  }

172
  // Copy .next/static directory
173
  if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
174
    log('  copying "static build" directory')
175
    await recursiveCopy(
176 177
      join(distDir, CLIENT_STATIC_FILES_PATH),
      join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
178 179 180
    )
  }

181
  // Get the exportPathMap from the config file
182
  if (typeof nextConfig.exportPathMap !== 'function') {
183 184 185
    console.log(
      `> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
    )
J
Joe Haddad 已提交
186
    nextConfig.exportPathMap = async (defaultMap: ExportPathMap) => {
187 188
      return defaultMap
    }
189 190
  }

191 192 193 194
  // Start the rendering process
  const renderOpts = {
    dir,
    buildId,
195
    nextExport: true,
196
    assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
197
    distDir,
198 199
    dev: false,
    staticMarkup: false,
200
    hotReloader: null,
201
    canonicalBase: (nextConfig.amp && nextConfig.amp.canonicalBase) || '',
J
Joe Haddad 已提交
202
    isModern: nextConfig.experimental.modern,
203 204
  }

205
  const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
206

207
  if (Object.keys(publicRuntimeConfig).length > 0) {
J
Joe Haddad 已提交
208
    ;(renderOpts as any).runtimeConfig = publicRuntimeConfig
209 210
  }

211
  // We need this for server rendering the Link component.
J
Joe Haddad 已提交
212 213
  ;(global as any).__NEXT_DATA__ = {
    nextExport: true,
214 215
  }

216
  log(`  launching ${threads} workers`)
217 218 219 220 221
  const exportPathMap = await nextConfig.exportPathMap(defaultPathMap, {
    dev: false,
    dir,
    outDir,
    distDir,
J
Joe Haddad 已提交
222
    buildId,
223
  })
224
  if (!exportPathMap['/404']) {
225
    exportPathMap['/404.html'] = exportPathMap['/404.html'] || {
J
Joe Haddad 已提交
226
      page: '/_error',
227
    }
228
  }
229
  const exportPaths = Object.keys(exportPathMap)
230 231 232 233 234 235 236 237 238 239 240 241 242 243
  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'
      )
    )
  }
244

245
  const progress = !options.silent && createProgress(filteredPaths.length)
J
JJ Kasper 已提交
246 247 248
  const sprDataDir = options.buildExport
    ? outDir
    : join(outDir, '_next/data', buildId)
A
Arunoda Susiripala 已提交
249

J
Joe Haddad 已提交
250
  const ampValidations: AmpPageStatus = {}
J
JJ Kasper 已提交
251 252
  let hadValidationError = false

253 254
  const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
  // Copy public directory
255
  if (!options.buildExport && existsSync(publicDir)) {
256
    log('  copying "public" directory')
257
    await recursiveCopy(publicDir, outDir, {
J
Joe Haddad 已提交
258
      filter(path) {
259
        // Exclude paths used by pages
260
        return !exportPathMap[path]
J
Joe Haddad 已提交
261
      },
262
    })
263
  }
264

J
Joe Haddad 已提交
265 266 267 268 269
  const worker: Worker & { default: Function } = new Worker(
    require.resolve('./worker'),
    {
      maxRetries: 0,
      numWorkers: threads,
270
      enableWorkerThreads: nextConfig.experimental.workerThreads,
J
Joe Haddad 已提交
271 272 273
      exposedMethods: ['default'],
    }
  ) as any
274 275 276 277 278

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

  let renderError = false
279

280
  await Promise.all(
281 282 283 284 285 286 287
    filteredPaths.map(async path => {
      const result = await worker.default({
        path,
        pathMap: exportPathMap[path],
        distDir,
        buildId,
        outDir,
J
JJ Kasper 已提交
288
        sprDataDir,
289 290 291
        renderOpts,
        serverRuntimeConfig,
        subFolders,
J
JJ Kasper 已提交
292
        buildExport: options.buildExport,
J
Joe Haddad 已提交
293
        serverless: isTargetLikeServerless(nextConfig.target),
294 295 296 297 298
      })

      for (const validation of result.ampValidations || []) {
        const { page, result } = validation
        ampValidations[page] = result
J
Joe Haddad 已提交
299 300 301
        hadValidationError =
          hadValidationError ||
          (Array.isArray(result && result.errors) && result.errors.length > 0)
302
      }
J
Joe Haddad 已提交
303
      renderError = renderError || !!result.error
J
JJ Kasper 已提交
304 305 306 307 308 309 310 311

      if (
        options.buildExport &&
        typeof result.fromBuildExportRevalidate !== 'undefined'
      ) {
        configuration.initialPageRevalidationMap[path] =
          result.fromBuildExportRevalidate
      }
312 313
      if (progress) progress()
    })
314
  )
315

316
  worker.end()
J
JJ Kasper 已提交
317

J
JJ Kasper 已提交
318 319 320 321
  // copy prerendered routes to outDir
  if (!options.buildExport && prerenderManifest) {
    await Promise.all(
      Object.keys(prerenderManifest.routes).map(async route => {
J
JJ Kasper 已提交
322
        route = route === '/' ? '/index' : route
J
JJ Kasper 已提交
323 324 325 326 327 328 329 330 331 332 333 334
        const orig = join(distPagesDir, route)
        const htmlDest = join(outDir, `${route}.html`)
        const jsonDest = join(sprDataDir, `${route}.json`)

        await mkdirp(dirname(htmlDest))
        await mkdirp(dirname(jsonDest))
        await copyFile(`${orig}.html`, htmlDest)
        await copyFile(`${orig}.json`, jsonDest)
      })
    )
  }

J
JJ Kasper 已提交
335 336 337 338
  if (Object.keys(ampValidations).length) {
    console.log(formatAmpMessages(ampValidations))
  }
  if (hadValidationError) {
339 340 341
    throw new Error(
      `AMP Validation caused the export to fail. https://err.sh/zeit/next.js/amp-export-validation`
    )
J
JJ Kasper 已提交
342 343
  }

344 345 346
  if (renderError) {
    throw new Error(`Export encountered errors`)
  }
347 348
  // Add an empty line to the console for the better readability.
  log('')
349
}