UNPKG

@nuxt/generator-edge

Version:
347 lines (341 loc) • 13 kB
/*! * @nuxt/generator-edge v2.18.2-28661769.e265ef3 (c) 2016-2024 * Released under the MIT License * Repository: https://github.com/nuxt/nuxt.js * Website: https://nuxtjs.org */ 'use strict'; const path = require('path'); const chalk = require('chalk'); const consola = require('consola'); const devalue = require('devalue'); const fsExtra = require('fs-extra'); const defu = require('defu'); const htmlMinifier = require('html-minifier-terser'); const nodeHtmlParser = require('node-html-parser'); const ufo = require('ufo'); const utilsEdge = require('@nuxt/utils-edge'); class Generator { constructor(nuxt, builder) { this.nuxt = nuxt; this.options = nuxt.options; this.builder = builder; this.isFullStatic = utilsEdge.isFullStatic(this.options); if (this.isFullStatic) { consola.info(`Full static generation activated`); } this.staticRoutes = path.resolve(this.options.srcDir, this.options.dir.static); this.srcBuiltPath = path.resolve(this.options.buildDir, "dist", "client"); this.distPath = this.options.generate.dir; this.distNuxtPath = path.join( this.distPath, utilsEdge.isUrl(this.options.build.publicPath) ? "" : this.options.build.publicPath ); if (this.isFullStatic) { const { staticAssets, manifest } = this.options.generate; this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version); this.staticAssetsBase = utilsEdge.urlJoin(this.options.app.cdnURL, this.options.generate.staticAssets.versionBase); if (manifest) { this.manifest = defu.defu(manifest, { routes: [] }); } } this._payload = null; this.setPayload = (payload) => { this._payload = defu.defu(payload, this._payload); }; } async generate({ build = true, init = true } = {}) { consola.debug("Initializing generator..."); await this.initiate({ build, init }); consola.debug("Preparing routes for generate..."); const routes = await this.initRoutes(); consola.info("Generating pages" + (this.isFullStatic ? " with full static mode" : "")); const errors = await this.generateRoutes(routes); await this.afterGenerate(); if (this.manifest) { await this.nuxt.callHook("generate:manifest", this.manifest, this); const manifestPath = path.join(this.staticAssetsDir, "manifest.js"); await fsExtra.ensureDir(this.staticAssetsDir); await fsExtra.writeFile(manifestPath, `__NUXT_JSONP__("manifest.js", ${devalue(this.manifest)})`, "utf-8"); consola.success("Static manifest generated"); } await this.nuxt.callHook("generate:done", this, errors); await this.nuxt.callHook("export:done", this, { errors }); return { errors }; } async initiate({ build = true, init = true } = {}) { await this.nuxt.ready(); await this.nuxt.callHook("generate:before", this, this.options.generate); await this.nuxt.callHook("export:before", this); if (build) { if (!this.builder) { throw new Error( `Could not generate. Make sure a Builder instance is passed to the constructor of \`Generator\` class or \`getGenerator\` function or disable the build step: \`generate({ build: false })\`` ); } this.builder.forGenerate(); await this.builder.build(); } else { const hasBuilt = await fsExtra.exists(path.resolve(this.options.buildDir, "dist", "server", "client.manifest.json")); if (!hasBuilt) { throw new Error( `No build files found in ${this.srcBuiltPath}. Please run \`nuxt build\`` ); } } if (init) { await this.initDist(); } } async initRoutes(...args) { let generateRoutes = []; if (this.options.router.mode !== "hash") { try { generateRoutes = await utilsEdge.promisifyRoute( this.options.generate.routes || [], ...args ); } catch (e) { consola.error("Could not resolve routes"); throw e; } } let routes = []; if (this.options.router.mode === "hash") { routes = ["/"]; } else { try { routes = utilsEdge.flatRoutes(this.getAppRoutes()); } catch (err) { routes = ["/"]; } } routes = routes.filter((route) => this.shouldGenerateRoute(route)); routes = this.decorateWithPayloads(routes, generateRoutes); await this.nuxt.callHook("generate:extendRoutes", routes); await this.nuxt.callHook("export:extendRoutes", { routes }); return routes; } shouldGenerateRoute(route) { return this.options.generate.exclude.every((regex) => { if (typeof regex === "string") { return regex !== route; } return !regex.test(route); }); } getBuildConfig() { try { return utilsEdge.requireModule(path.join(this.options.buildDir, "nuxt/config.json")); } catch (err) { return null; } } getAppRoutes() { return utilsEdge.requireModule(path.join(this.options.buildDir, "routes.json")); } async generateRoutes(routes) { const errors = []; this.routes = []; this.generatedRoutes = /* @__PURE__ */ new Set(); routes.forEach(({ route, ...props }) => { route = decodeURI(this.normalizeSlash(route)); this.routes.push({ route, ...props }); this.generatedRoutes.add(route); }); while (this.routes.length) { let n = 0; await Promise.all( this.routes.splice(0, this.options.generate.concurrency).map(async ({ route, payload }) => { await utilsEdge.waitFor(n++ * this.options.generate.interval); await this.generateRoute({ route, payload, errors }); }) ); } errors.toString = () => this._formatErrors(errors); return errors; } _formatErrors(errors) { return errors.map(({ type, route, error }) => { const isHandled = type === "handled"; const color = isHandled ? "yellow" : "red"; let line = chalk[color](` ${route} `); if (isHandled) { line += chalk.grey(JSON.stringify(error, void 0, 2) + "\n"); } else { line += chalk.grey(error.stack || error.message || `${error}`); } return line; }).join("\n"); } async afterGenerate() { const { fallback } = this.options.generate; if (typeof fallback !== "string" || !fallback) { return; } const fallbackPath = path.join(this.distPath, fallback); if (await fsExtra.exists(fallbackPath)) { consola.warn(`SPA fallback was configured, but the configured path (${fallbackPath}) already exists.`); return; } let { html } = await this.nuxt.server.renderRoute("/", { spa: true, staticAssetsBase: this.staticAssetsBase }); try { html = await this.minifyHtml(html); } catch (error) { consola.warn("HTML minification failed for SPA fallback"); } await fsExtra.writeFile(fallbackPath, html, "utf8"); consola.success("Client-side fallback created: `" + fallback + "`"); } async initDist() { await fsExtra.emptyDir(this.distPath); consola.info(`Generating output directory: ${path.basename(this.distPath)}/`); await this.nuxt.callHook("generate:distRemoved", this); await this.nuxt.callHook("export:distRemoved", this); if (await fsExtra.exists(this.staticRoutes)) { await fsExtra.copy(this.staticRoutes, this.distPath); } await fsExtra.copy(this.srcBuiltPath, this.distNuxtPath); if (this.payloadDir) { await fsExtra.ensureDir(this.payloadDir); } if (this.options.generate.nojekyll) { const nojekyllPath = path.resolve(this.distPath, ".nojekyll"); await fsExtra.writeFile(nojekyllPath, ""); } await this.nuxt.callHook("generate:distCopied", this); await this.nuxt.callHook("export:distCopied", this); } normalizeSlash(route) { return this.options.router && this.options.router.trailingSlash ? ufo.withTrailingSlash(route) : ufo.withoutTrailingSlash(route); } decorateWithPayloads(routes, generateRoutes) { const routeMap = {}; routes.forEach((route) => { routeMap[route] = { route: this.normalizeSlash(route), payload: null }; }); generateRoutes.forEach((route) => { const path2 = utilsEdge.isString(route) ? route : route.route; routeMap[path2] = { route: this.normalizeSlash(path2), payload: route.payload || null }; }); return Object.values(routeMap); } async generateRoute({ route, payload = {}, errors = [] }) { let html; const pageErrors = []; route = this.normalizeSlash(route); const setPayload = (_payload) => { payload = defu.defu(_payload, payload); }; if (this._payload) { payload = defu.defu(payload, this._payload); } await this.nuxt.callHook("generate:route", { route, setPayload }); await this.nuxt.callHook("export:route", { route, setPayload }); try { const renderContext = { payload, staticAssetsBase: this.staticAssetsBase }; const res = await this.nuxt.server.renderRoute(route, renderContext); html = res.html; if (this.options.generate.crawler && this.options.render.ssr) { nodeHtmlParser.parse(html).querySelectorAll("a").map((el) => { const sanitizedHref = (el.getAttribute("href") || "").replace(this.options.router.base, "/").split("?")[0].split("#")[0].replace(/\/+$/, "").trim(); const foundRoute = decodeURI(this.normalizeSlash(sanitizedHref)); if (foundRoute.startsWith("/") && !foundRoute.startsWith("//") && !path.extname(foundRoute) && this.shouldGenerateRoute(foundRoute) && !this.generatedRoutes.has(foundRoute)) { this.generatedRoutes.add(foundRoute); this.routes.push({ route: foundRoute }); } return null; }); } if (this.staticAssetsDir && renderContext.staticAssets) { for (const asset of renderContext.staticAssets) { const assetPath = path.join(this.staticAssetsDir, ufo.decode(asset.path)); await fsExtra.ensureDir(path.dirname(assetPath)); await fsExtra.writeFile(assetPath, asset.src, "utf-8"); } if (this.manifest && (!res.error && !res.redirected)) { this.manifest.routes.push(ufo.withoutTrailingSlash(route)); } } if (res.error) { pageErrors.push({ type: "handled", route, error: res.error }); } } catch (err) { pageErrors.push({ type: "unhandled", route, error: err }); errors.push(...pageErrors); await this.nuxt.callHook("generate:routeFailed", { route, errors: pageErrors }); await this.nuxt.callHook("export:routeFailed", { route, errors: pageErrors }); consola.error(this._formatErrors(pageErrors)); return false; } try { html = await this.minifyHtml(html); } catch (err) { const minifyErr = new Error( `HTML minification failed. Make sure the route generates valid HTML. Failed HTML: ${html}` ); pageErrors.push({ type: "unhandled", route, error: minifyErr }); } let fileName; if (this.options.generate.subFolders) { fileName = route === "/404" ? path.join(path.sep, "404.html") : path.join(route, path.sep, "index.html"); } else { const normalizedRoute = route.replace(/\/$/, ""); fileName = route.length > 1 ? path.join(path.sep, normalizedRoute + ".html") : path.join(path.sep, "index.html"); } const page = { route, path: fileName, html, exclude: false, errors: pageErrors }; page.page = page; await this.nuxt.callHook("generate:page", page); if (page.exclude) { return false; } page.path = path.join(this.distPath, page.path); await fsExtra.mkdirp(path.dirname(page.path)); await fsExtra.writeFile(page.path, page.html, "utf8"); await this.nuxt.callHook("generate:routeCreated", { route, path: page.path, errors: pageErrors }); await this.nuxt.callHook("export:routeCreated", { route, path: page.path, errors: pageErrors }); if (pageErrors.length) { consola.error(`Error generating route "${route}": ${pageErrors.map((e) => e.error.message).join(", ")}`); errors.push(...pageErrors); } else { consola.success(`Generated route "${route}"`); } return true; } minifyHtml(html) { let minificationOptions = this.options.build.html.minify; if (typeof this.options.generate.minify !== "undefined") { minificationOptions = this.options.generate.minify; consola.warn("generate.minify has been deprecated and will be removed in the next major version. Use build.html.minify instead!"); } if (!minificationOptions) { return html; } return htmlMinifier.minify(html, minificationOptions); } } function getGenerator(nuxt, builder) { return new Generator(nuxt, builder); } exports.Generator = Generator; exports.getGenerator = getGenerator;