generator.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*!
  2. * @nuxt/generator v2.15.8 (c) 2016-2021
  3. * Released under the MIT License
  4. * Repository: https://github.com/nuxt/nuxt.js
  5. * Website: https://nuxtjs.org
  6. */
  7. 'use strict';
  8. Object.defineProperty(exports, '__esModule', { value: true });
  9. const path = require('path');
  10. const chalk = require('chalk');
  11. const consola = require('consola');
  12. const devalue = require('devalue');
  13. const fsExtra = require('fs-extra');
  14. const defu = require('defu');
  15. const htmlMinifier = require('html-minifier');
  16. const nodeHtmlParser = require('node-html-parser');
  17. const ufo = require('ufo');
  18. const utils = require('@nuxt/utils');
  19. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  20. const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
  21. const chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk);
  22. const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
  23. const devalue__default = /*#__PURE__*/_interopDefaultLegacy(devalue);
  24. const fsExtra__default = /*#__PURE__*/_interopDefaultLegacy(fsExtra);
  25. const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
  26. const htmlMinifier__default = /*#__PURE__*/_interopDefaultLegacy(htmlMinifier);
  27. class Generator {
  28. constructor (nuxt, builder) {
  29. this.nuxt = nuxt;
  30. this.options = nuxt.options;
  31. this.builder = builder;
  32. // Set variables
  33. this.isFullStatic = utils.isFullStatic(this.options);
  34. if (this.isFullStatic) {
  35. consola__default['default'].info(`Full static generation activated`);
  36. }
  37. this.staticRoutes = path__default['default'].resolve(this.options.srcDir, this.options.dir.static);
  38. this.srcBuiltPath = path__default['default'].resolve(this.options.buildDir, 'dist', 'client');
  39. this.distPath = this.options.generate.dir;
  40. this.distNuxtPath = path__default['default'].join(
  41. this.distPath,
  42. utils.isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath
  43. );
  44. // Payloads for full static
  45. if (this.isFullStatic) {
  46. const { staticAssets, manifest } = this.options.generate;
  47. this.staticAssetsDir = path__default['default'].resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version);
  48. this.staticAssetsBase = utils.urlJoin(this.options.app.cdnURL, this.options.generate.staticAssets.versionBase);
  49. if (manifest) {
  50. this.manifest = defu__default['default'](manifest, {
  51. routes: []
  52. });
  53. }
  54. }
  55. // Shared payload
  56. this._payload = null;
  57. this.setPayload = (payload) => {
  58. this._payload = defu__default['default'](payload, this._payload);
  59. };
  60. }
  61. async generate ({ build = true, init = true } = {}) {
  62. consola__default['default'].debug('Initializing generator...');
  63. await this.initiate({ build, init });
  64. consola__default['default'].debug('Preparing routes for generate...');
  65. const routes = await this.initRoutes();
  66. consola__default['default'].info('Generating pages' + (this.isFullStatic ? ' with full static mode' : ''));
  67. const errors = await this.generateRoutes(routes);
  68. await this.afterGenerate();
  69. // Save routes manifest for full static
  70. if (this.manifest) {
  71. await this.nuxt.callHook('generate:manifest', this.manifest, this);
  72. const manifestPath = path__default['default'].join(this.staticAssetsDir, 'manifest.js');
  73. await fsExtra__default['default'].ensureDir(this.staticAssetsDir);
  74. await fsExtra__default['default'].writeFile(manifestPath, `__NUXT_JSONP__("manifest.js", ${devalue__default['default'](this.manifest)})`, 'utf-8');
  75. consola__default['default'].success('Static manifest generated');
  76. }
  77. // Done hook
  78. await this.nuxt.callHook('generate:done', this, errors);
  79. await this.nuxt.callHook('export:done', this, { errors });
  80. return { errors }
  81. }
  82. async initiate ({ build = true, init = true } = {}) {
  83. // Wait for nuxt be ready
  84. await this.nuxt.ready();
  85. // Call before hook
  86. await this.nuxt.callHook('generate:before', this, this.options.generate);
  87. await this.nuxt.callHook('export:before', this);
  88. if (build) {
  89. // Add flag to set process.static
  90. this.builder.forGenerate();
  91. // Start build process
  92. await this.builder.build();
  93. } else {
  94. const hasBuilt = await fsExtra__default['default'].exists(path__default['default'].resolve(this.options.buildDir, 'dist', 'server', 'client.manifest.json'));
  95. if (!hasBuilt) {
  96. throw new Error(
  97. `No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build\``
  98. )
  99. }
  100. }
  101. // Initialize dist directory
  102. if (init) {
  103. await this.initDist();
  104. }
  105. }
  106. async initRoutes (...args) {
  107. // Resolve config.generate.routes promises before generating the routes
  108. let generateRoutes = [];
  109. if (this.options.router.mode !== 'hash') {
  110. try {
  111. generateRoutes = await utils.promisifyRoute(
  112. this.options.generate.routes || [],
  113. ...args
  114. );
  115. } catch (e) {
  116. consola__default['default'].error('Could not resolve routes');
  117. throw e // eslint-disable-line no-unreachable
  118. }
  119. }
  120. let routes = [];
  121. // Generate only index.html for router.mode = 'hash' or client-side apps
  122. if (this.options.router.mode === 'hash') {
  123. routes = ['/'];
  124. } else {
  125. try {
  126. routes = utils.flatRoutes(this.getAppRoutes());
  127. } catch (err) {
  128. // Case: where we use custom router.js
  129. // https://github.com/nuxt-community/router-module/issues/83
  130. routes = ['/'];
  131. }
  132. }
  133. routes = routes.filter(route => this.shouldGenerateRoute(route));
  134. routes = this.decorateWithPayloads(routes, generateRoutes);
  135. // extendRoutes hook
  136. await this.nuxt.callHook('generate:extendRoutes', routes);
  137. await this.nuxt.callHook('export:extendRoutes', { routes });
  138. return routes
  139. }
  140. shouldGenerateRoute (route) {
  141. return this.options.generate.exclude.every((regex) => {
  142. if (typeof regex === 'string') {
  143. return regex !== route
  144. }
  145. return !regex.test(route)
  146. })
  147. }
  148. getBuildConfig () {
  149. try {
  150. return utils.requireModule(path__default['default'].join(this.options.buildDir, 'nuxt/config.json'))
  151. } catch (err) {
  152. return null
  153. }
  154. }
  155. getAppRoutes () {
  156. return utils.requireModule(path__default['default'].join(this.options.buildDir, 'routes.json'))
  157. }
  158. async generateRoutes (routes) {
  159. const errors = [];
  160. this.routes = [];
  161. this.generatedRoutes = new Set();
  162. routes.forEach(({ route, ...props }) => {
  163. route = decodeURI(this.normalizeSlash(route));
  164. this.routes.push({ route, ...props });
  165. // Add routes to the tracked generated routes (for crawler)
  166. this.generatedRoutes.add(route);
  167. });
  168. // Start generate process
  169. while (this.routes.length) {
  170. let n = 0;
  171. await Promise.all(
  172. this.routes
  173. .splice(0, this.options.generate.concurrency)
  174. .map(async ({ route, payload }) => {
  175. await utils.waitFor(n++ * this.options.generate.interval);
  176. await this.generateRoute({ route, payload, errors });
  177. })
  178. );
  179. }
  180. // Improve string representation for errors
  181. // TODO: Use consola for more consistency
  182. errors.toString = () => this._formatErrors(errors);
  183. return errors
  184. }
  185. _formatErrors (errors) {
  186. return errors
  187. .map(({ type, route, error }) => {
  188. const isHandled = type === 'handled';
  189. const color = isHandled ? 'yellow' : 'red';
  190. let line = chalk__default['default'][color](` ${route}\n\n`);
  191. if (isHandled) {
  192. line += chalk__default['default'].grey(JSON.stringify(error, undefined, 2) + '\n');
  193. } else {
  194. line += chalk__default['default'].grey(error.stack || error.message || `${error}`);
  195. }
  196. return line
  197. })
  198. .join('\n')
  199. }
  200. async afterGenerate () {
  201. const { fallback } = this.options.generate;
  202. // Disable SPA fallback if value isn't a non-empty string
  203. if (typeof fallback !== 'string' || !fallback) {
  204. return
  205. }
  206. const fallbackPath = path__default['default'].join(this.distPath, fallback);
  207. // Prevent conflicts
  208. if (await fsExtra__default['default'].exists(fallbackPath)) {
  209. consola__default['default'].warn(`SPA fallback was configured, but the configured path (${fallbackPath}) already exists.`);
  210. return
  211. }
  212. // Render and write the SPA template to the fallback path
  213. let { html } = await this.nuxt.server.renderRoute('/', {
  214. spa: true,
  215. staticAssetsBase: this.staticAssetsBase
  216. });
  217. try {
  218. html = this.minifyHtml(html);
  219. } catch (error) {
  220. consola__default['default'].warn('HTML minification failed for SPA fallback');
  221. }
  222. await fsExtra__default['default'].writeFile(fallbackPath, html, 'utf8');
  223. consola__default['default'].success('Client-side fallback created: `' + fallback + '`');
  224. }
  225. async initDist () {
  226. // Clean destination folder
  227. await fsExtra__default['default'].emptyDir(this.distPath);
  228. consola__default['default'].info(`Generating output directory: ${path__default['default'].basename(this.distPath)}/`);
  229. await this.nuxt.callHook('generate:distRemoved', this);
  230. await this.nuxt.callHook('export:distRemoved', this);
  231. // Copy static and built files
  232. if (await fsExtra__default['default'].exists(this.staticRoutes)) {
  233. await fsExtra__default['default'].copy(this.staticRoutes, this.distPath);
  234. }
  235. // Copy .nuxt/dist/client/ to dist/_nuxt/
  236. await fsExtra__default['default'].copy(this.srcBuiltPath, this.distNuxtPath);
  237. if (this.payloadDir) {
  238. await fsExtra__default['default'].ensureDir(this.payloadDir);
  239. }
  240. // Add .nojekyll file to let GitHub Pages add the _nuxt/ folder
  241. // https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/
  242. const nojekyllPath = path__default['default'].resolve(this.distPath, '.nojekyll');
  243. await fsExtra__default['default'].writeFile(nojekyllPath, '');
  244. await this.nuxt.callHook('generate:distCopied', this);
  245. await this.nuxt.callHook('export:distCopied', this);
  246. }
  247. normalizeSlash (route) {
  248. return this.options.router && this.options.router.trailingSlash ? ufo.withTrailingSlash(route) : ufo.withoutTrailingSlash(route)
  249. }
  250. decorateWithPayloads (routes, generateRoutes) {
  251. const routeMap = {};
  252. // Fill routeMap for known routes
  253. routes.forEach((route) => {
  254. routeMap[route] = { route: this.normalizeSlash(route), payload: null };
  255. });
  256. // Fill routeMap with given generate.routes
  257. generateRoutes.forEach((route) => {
  258. // route is either a string or like { route : '/my_route/1', payload: {} }
  259. const path = utils.isString(route) ? route : route.route;
  260. routeMap[path] = {
  261. route: this.normalizeSlash(path),
  262. payload: route.payload || null
  263. };
  264. });
  265. return Object.values(routeMap)
  266. }
  267. async generateRoute ({ route, payload = {}, errors = [] }) {
  268. let html;
  269. const pageErrors = [];
  270. route = this.normalizeSlash(route);
  271. const setPayload = (_payload) => {
  272. payload = defu__default['default'](_payload, payload);
  273. };
  274. // Apply shared payload
  275. if (this._payload) {
  276. payload = defu__default['default'](payload, this._payload);
  277. }
  278. await this.nuxt.callHook('generate:route', { route, setPayload });
  279. await this.nuxt.callHook('export:route', { route, setPayload });
  280. try {
  281. const renderContext = {
  282. payload,
  283. staticAssetsBase: this.staticAssetsBase
  284. };
  285. const res = await this.nuxt.server.renderRoute(route, renderContext);
  286. html = res.html;
  287. // If crawler activated and called from generateRoutes()
  288. if (this.options.generate.crawler && this.options.render.ssr) {
  289. nodeHtmlParser.parse(html).querySelectorAll('a').map((el) => {
  290. const sanitizedHref = (el.getAttribute('href') || '')
  291. .replace(this.options.router.base, '/')
  292. .split('?')[0]
  293. .split('#')[0]
  294. .replace(/\/+$/, '')
  295. .trim();
  296. const foundRoute = decodeURI(this.normalizeSlash(sanitizedHref));
  297. if (foundRoute.startsWith('/') && !foundRoute.startsWith('//') && !path__default['default'].extname(foundRoute) && this.shouldGenerateRoute(foundRoute) && !this.generatedRoutes.has(foundRoute)) {
  298. this.generatedRoutes.add(foundRoute);
  299. this.routes.push({ route: foundRoute });
  300. }
  301. return null
  302. });
  303. }
  304. // Save Static Assets
  305. if (this.staticAssetsDir && renderContext.staticAssets) {
  306. for (const asset of renderContext.staticAssets) {
  307. const assetPath = path__default['default'].join(this.staticAssetsDir, decodeURI(asset.path));
  308. await fsExtra__default['default'].ensureDir(path__default['default'].dirname(assetPath));
  309. await fsExtra__default['default'].writeFile(assetPath, asset.src, 'utf-8');
  310. }
  311. // Add route to manifest (only if no error and redirect)
  312. if (this.manifest && (!res.error && !res.redirected)) {
  313. this.manifest.routes.push(ufo.withoutTrailingSlash(route));
  314. }
  315. }
  316. // SPA fallback
  317. if (res.error) {
  318. pageErrors.push({ type: 'handled', route, error: res.error });
  319. }
  320. } catch (err) {
  321. pageErrors.push({ type: 'unhandled', route, error: err });
  322. errors.push(...pageErrors);
  323. await this.nuxt.callHook('generate:routeFailed', { route, errors: pageErrors });
  324. await this.nuxt.callHook('export:routeFailed', { route, errors: pageErrors });
  325. consola__default['default'].error(this._formatErrors(pageErrors));
  326. return false
  327. }
  328. try {
  329. html = this.minifyHtml(html);
  330. } catch (err) {
  331. const minifyErr = new Error(
  332. `HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`
  333. );
  334. pageErrors.push({ type: 'unhandled', route, error: minifyErr });
  335. }
  336. let fileName;
  337. if (this.options.generate.subFolders) {
  338. fileName = route === '/404'
  339. ? path__default['default'].join(path__default['default'].sep, '404.html') // /404 -> /404.html
  340. : path__default['default'].join(route, path__default['default'].sep, 'index.html'); // /about -> /about/index.html
  341. } else {
  342. const normalizedRoute = route.replace(/\/$/, '');
  343. fileName = route.length > 1 ? path__default['default'].join(path__default['default'].sep, normalizedRoute + '.html') : path__default['default'].join(path__default['default'].sep, 'index.html');
  344. }
  345. // Call hook to let user update the path & html
  346. const page = {
  347. route,
  348. path: fileName,
  349. html,
  350. exclude: false,
  351. errors: pageErrors
  352. };
  353. page.page = page; // Backward compatibility for export:page hook
  354. await this.nuxt.callHook('generate:page', page);
  355. if (page.exclude) {
  356. return false
  357. }
  358. page.path = path__default['default'].join(this.distPath, page.path);
  359. // Make sure the sub folders are created
  360. await fsExtra__default['default'].mkdirp(path__default['default'].dirname(page.path));
  361. await fsExtra__default['default'].writeFile(page.path, page.html, 'utf8');
  362. await this.nuxt.callHook('generate:routeCreated', { route, path: page.path, errors: pageErrors });
  363. await this.nuxt.callHook('export:routeCreated', { route, path: page.path, errors: pageErrors });
  364. if (pageErrors.length) {
  365. consola__default['default'].error(`Error generating route "${route}": ${pageErrors.map(e => e.error.message).join(', ')}`);
  366. errors.push(...pageErrors);
  367. } else {
  368. consola__default['default'].success(`Generated route "${route}"`);
  369. }
  370. return true
  371. }
  372. minifyHtml (html) {
  373. let minificationOptions = this.options.build.html.minify;
  374. // Legacy: Override minification options with generate.minify if present
  375. // TODO: Remove in Nuxt version 3
  376. if (typeof this.options.generate.minify !== 'undefined') {
  377. minificationOptions = this.options.generate.minify;
  378. consola__default['default'].warn('generate.minify has been deprecated and will be removed in the next major version.' +
  379. ' Use build.html.minify instead!');
  380. }
  381. if (!minificationOptions) {
  382. return html
  383. }
  384. return htmlMinifier__default['default'].minify(html, minificationOptions)
  385. }
  386. }
  387. function getGenerator (nuxt) {
  388. return new Generator(nuxt)
  389. }
  390. exports.Generator = Generator;
  391. exports.getGenerator = getGenerator;