diff --git a/packages/nuxt3/src/builder/builder.ts b/packages/nuxt3/src/builder/builder.ts index c395929f16..6132c4bc20 100644 --- a/packages/nuxt3/src/builder/builder.ts +++ b/packages/nuxt3/src/builder/builder.ts @@ -52,6 +52,8 @@ async function build (builder: Builder) { } await bundle(builder) + + await nuxt.callHook('build:done') } function watch (builder: Builder) { diff --git a/packages/nuxt3/src/cli/command.ts b/packages/nuxt3/src/cli/command.ts index ad4346a210..5aaac69288 100644 --- a/packages/nuxt3/src/cli/command.ts +++ b/packages/nuxt3/src/cli/command.ts @@ -7,7 +7,6 @@ import Hookable from 'hookable' import { Builder } from 'src/builder' import { CliConfiguration } from 'src/config/options' import { Nuxt } from 'src/core' -import { Generator } from 'src/generator' import { name, version } from '../../package.json' @@ -154,11 +153,6 @@ export default class NuxtCommand extends Hookable { return new Builder(nuxt) } - async getGenerator (nuxt: Nuxt) { - const builder = await this.getBuilder(nuxt) - return new Generator(nuxt, builder) - } - async setLock (lockRelease?: () => Promise) { if (lockRelease) { if (this._lockRelease) { diff --git a/packages/nuxt3/src/config/options.ts b/packages/nuxt3/src/config/options.ts index 1d2522bb3a..4ee7be3602 100644 --- a/packages/nuxt3/src/config/options.ts +++ b/packages/nuxt3/src/config/options.ts @@ -198,7 +198,7 @@ function normalizeConfig (_options: CliConfiguration) { // If app.html is defined, set the template path to the user template if (options.documentPath === undefined) { - options.documentPath = path.resolve(options.buildDir, 'views/document.template.html') + options.documentPath = path.resolve(options.buildDir, 'views/app.template.html') // SIGMA/Nuxt2 compat const userDocumentPath = path.join(options.srcDir, 'document.html') if (fs.existsSync(userDocumentPath)) { options.documentPath = userDocumentPath @@ -440,6 +440,11 @@ function normalizeConfig (_options: CliConfiguration) { options._modules.push('@nuxt/telemetry') } + // Sigma + options.appTemplatePath = path.resolve(options.appDir, '_templates/views/document.template.html') // SIGMA TODO + options._majorVersion = 3 + options._modules.push('@nuxt/sigma/src') + return options } diff --git a/packages/nuxt3/src/core/nuxt.ts b/packages/nuxt3/src/core/nuxt.ts index 53f4ff44f8..b56002b6da 100644 --- a/packages/nuxt3/src/core/nuxt.ts +++ b/packages/nuxt3/src/core/nuxt.ts @@ -4,9 +4,7 @@ import isPlainObject from 'lodash/isPlainObject' import consola from 'consola' import Hookable from 'hookable' -import { defineAlias } from 'src/utils' import { getNuxtConfig, Configuration, NormalizedConfiguration } from 'src/config' -import { Server } from 'src/server' import { version } from '../../package.json' @@ -29,9 +27,9 @@ export default class Nuxt extends Hookable { options: NormalizedConfiguration resolver: Resolver moduleContainer: ModuleContainer - server?: Server - renderer?: Server - render?: Server['app'] + server?: any + renderer?: any + render?: any['app'] showReady?: () => void constructor (options: Configuration = {}) { @@ -44,29 +42,7 @@ export default class Nuxt extends Hookable { this.resolver = new Resolver(this) this.moduleContainer = new ModuleContainer(this) - // Deprecated hooks - this.deprecateHooks({ - // #3294 - 7514db73b25c23b8c14ebdafbb4e129ac282aabd - 'render:context': { - to: '_render:context', - message: '`render:context(nuxt)` is deprecated, Please use `vue-renderer:ssr:context(context)`' - }, - // #3773 - 'render:routeContext': { - to: '_render:context', - message: '`render:routeContext(nuxt)` is deprecated, Please use `vue-renderer:ssr:context(context)`' - }, - showReady: 'webpack:done' - }) - - // Add Legacy aliases - defineAlias(this, this.resolver, ['resolveAlias', 'resolvePath']) - this.showReady = () => { this.callHook('webpack:done') } - - // Init server - if (this.options.server !== false) { - this._initServer() - } + this.server = {} // SIGMA TODO // Call ready if (this.options._ready !== false) { @@ -114,16 +90,6 @@ export default class Nuxt extends Hookable { return this } - _initServer () { - if (this.server) { - return - } - this.server = new Server(this) - this.renderer = this.server - this.render = this.server.app - defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen']) - } - async close (callback?: () => any | Promise) { await this.callHook('close', this) diff --git a/packages/nuxt3/src/generator/generator.ts b/packages/nuxt3/src/generator/generator.ts deleted file mode 100644 index 3decc609c7..0000000000 --- a/packages/nuxt3/src/generator/generator.ts +++ /dev/null @@ -1,427 +0,0 @@ -import path from 'path' -import chalk from 'chalk' -import consola from 'consola' -import fsExtra from 'fs-extra' -import defu from 'defu' -import htmlMinifier from 'html-minifier' -import { parse } from 'node-html-parser' - -import type { Builder } from 'src/builder' -import type { Nuxt } from 'src/core' -import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from 'src/utils' - -export default class Generator { - _payload: null - setPayload: (payload: any) => void - - builder?: Builder - isFullStatic: boolean - nuxt: Nuxt - options: Nuxt['options'] - staticRoutes: string - srcBuiltPath: string - distPath: string - distNuxtPath: string - - staticAssetsDir?: string - staticAssetsBase?: string - - payloadDir?: string - - routes: Array<{ route: string } & Record> - generatedRoutes: Set - - constructor (nuxt: Nuxt, builder?: Builder) { - this.nuxt = nuxt - this.options = nuxt.options - this.builder = builder - this.isFullStatic = false - - // Set variables - 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, - isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath - ) - - // Shared payload - this._payload = null - this.setPayload = (payload) => { - this._payload = defu(payload, this._payload) - } - } - - async generate ({ build = true, init = true } = {}) { - consola.debug('Initializing generator...') - await this.initiate({ build, init }) - - // Payloads for full static - if (this.isFullStatic) { - consola.info('Full static mode activated') - const { staticAssets } = this.options.generate - this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version) - this.staticAssetsBase = this.options.generate.staticAssets.versionBase - } - - consola.debug('Preparing routes for generate...') - const routes = await this.initRoutes() - - consola.info('Generating pages') - const errors = await this.generateRoutes(routes) - - await this.afterGenerate() - - // Done hook - await this.nuxt.callHook('generate:done', this, errors) - await this.nuxt.callHook('export:done', this, { errors }) - - return { errors } - } - - async initiate ({ build = true, init = true } = {}) { - // Wait for nuxt be ready - await this.nuxt.ready() - - // Call before hook - await this.nuxt.callHook('generate:before', this, this.options.generate) - await this.nuxt.callHook('export:before', this) - - if (build) { - // Add flag to set process.static - this.builder.forGenerate() - - // Start build process - await this.builder.build() - this.isFullStatic = isFullStatic(this.options) - } else { - const hasBuilt = await fsExtra.exists(path.resolve(this.options.buildDir, 'dist', 'server', 'client.manifest.json')) - if (!hasBuilt) { - const fullStaticArgs = isFullStatic(this.options) ? ' --target static' : '' - throw new Error( - `No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build${fullStaticArgs}\` before calling \`nuxt export\`` - ) - } - const config = this.getBuildConfig() - if (!config || (config.target !== TARGETS.static && !this.options._legacyGenerate)) { - throw new Error( - 'In order to use `nuxt export`, you need to run `nuxt build --target static`' - ) - } - this.isFullStatic = config.isFullStatic - this.options.render.ssr = config.ssr - } - - // Initialize dist directory - if (init) { - await this.initDist() - } - } - - async initRoutes (...args) { - // Resolve config.generate.routes promises before generating the routes - let generateRoutes = [] - if (this.options.router.mode !== 'hash') { - try { - generateRoutes = await promisifyRoute( - this.options.generate.routes || [], - ...args - ) - } catch (e) { - consola.error('Could not resolve routes') - throw e // eslint-disable-line no-unreachable - } - } - let routes = [] - // Generate only index.html for router.mode = 'hash' or client-side apps - if (this.options.router.mode === 'hash') { - routes = ['/'] - } else { - routes = flatRoutes(this.getAppRoutes()) - } - routes = routes.filter(route => this.shouldGenerateRoute(route)) - routes = this.decorateWithPayloads(routes, generateRoutes) - - // extendRoutes hook - 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 require(path.join(this.options.buildDir, 'nuxt/config.json')) - } catch (err) { - return null - } - } - - getAppRoutes () { - return require(path.join(this.options.buildDir, 'routes.json')) - } - - async generateRoutes (routes) { - const errors = [] - - this.routes = [] - this.generatedRoutes = new Set() - - routes.forEach(({ route, ...props }) => { - route = decodeURI(route) - this.routes.push({ route, ...props }) - // Add routes to the tracked generated routes (for crawler) - this.generatedRoutes.add(route) - }) - - // Start generate process - while (this.routes.length) { - let n = 0 - await Promise.all( - this.routes - .splice(0, this.options.generate.concurrency) - .map(async ({ route, payload }) => { - await waitFor(n++ * this.options.generate.interval) - await this.generateRoute({ route, payload, errors }) - }) - ) - } - - // Improve string representation for errors - // TODO: Use consola for more consistency - 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}\n\n`) - - if (isHandled) { - line += chalk.grey(JSON.stringify(error, undefined, 2) + '\n') - } else { - line += chalk.grey(error.stack || error.message || `${error}`) - } - - return line - }) - .join('\n') - } - - async afterGenerate () { - const { fallback } = this.options.generate - - // Disable SPA fallback if value isn't a non-empty string - if (typeof fallback !== 'string' || !fallback) { - return - } - - const fallbackPath = path.join(this.distPath, fallback) - - // Prevent conflicts - if (await fsExtra.exists(fallbackPath)) { - consola.warn(`SPA fallback was configured, but the configured path (${fallbackPath}) already exists.`) - return - } - - // Render and write the SPA template to the fallback path - let { html } = await this.nuxt.server.renderRoute('/', { - spa: true, - staticAssetsBase: this.staticAssetsBase - }) - - try { - html = 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 () { - // Clean destination folder - 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) - - // Copy static and built files - if (await fsExtra.exists(this.staticRoutes)) { - await fsExtra.copy(this.staticRoutes, this.distPath) - } - // Copy .nuxt/dist/client/ to dist/_nuxt/ - await fsExtra.copy(this.srcBuiltPath, this.distNuxtPath) - - if (this.payloadDir) { - await fsExtra.ensureDir(this.payloadDir) - } - - // Add .nojekyll file to let GitHub Pages add the _nuxt/ folder - // https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/ - const nojekyllPath = path.resolve(this.distPath, '.nojekyll') - fsExtra.writeFile(nojekyllPath, '') - - await this.nuxt.callHook('generate:distCopied', this) - await this.nuxt.callHook('export:distCopied', this) - } - - decorateWithPayloads (routes, generateRoutes) { - const routeMap = {} - // Fill routeMap for known routes - routes.forEach((route) => { - routeMap[route] = { route, payload: null } - }) - // Fill routeMap with given generate.routes - generateRoutes.forEach((route) => { - // route is either a string or like { route : '/my_route/1', payload: {} } - const path = isString(route) ? route : route.route - routeMap[path] = { - route: path, - payload: route.payload || null - } - }) - return Object.values(routeMap) - } - - async generateRoute ({ route, payload = {}, errors = [] }) { - let html - const pageErrors = [] - - const setPayload = (_payload) => { - payload = defu(_payload, payload) - } - - // Apply shared payload - if (this._payload) { - payload = 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, - staticAssets: undefined - } - const res = await this.nuxt.server.renderRoute(route, renderContext) - html = res.html - - // If crawler activated and called from generateRoutes() - if (this.options.generate.crawler && this.options.render.ssr) { - const possibleTrailingSlash = this.options.router.trailingSlash ? '/' : '' - parse(html).querySelectorAll('a').forEach((el) => { - const sanitizedHref = (el.getAttribute('href') || '') - .replace(this.options.router.base, '/') - .replace(/\/+$/, '') - .split('?')[0] - .split('#')[0] - .trim() - - const route = decodeURI(sanitizedHref + possibleTrailingSlash) - - if (route.startsWith('/') && !path.extname(route) && this.shouldGenerateRoute(route) && !this.generatedRoutes.has(route)) { - this.generatedRoutes.add(route) - this.routes.push({ route }) - } - }) - } - - // Save Static Assets - if (this.staticAssetsDir && renderContext.staticAssets) { - for (const asset of renderContext.staticAssets) { - const assetPath = path.join(this.staticAssetsDir, asset.path) - await fsExtra.ensureDir(path.dirname(assetPath)) - await fsExtra.writeFile(assetPath, asset.src, 'utf-8') - } - } - - 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 = this.minifyHtml(html) - } catch (err) { - const minifyErr = new Error( - `HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}` - ) - pageErrors.push({ type: 'unhandled', route, error: minifyErr }) - } - - let fileName: string - - if (this.options.generate.subFolders) { - fileName = path.join(route, path.sep, 'index.html') // /about -> /about/index.html - fileName = fileName === '/404/index.html' ? '/404.html' : fileName // /404 -> /404.html - } else { - const normalizedRoute = route.replace(/\/$/, '') - fileName = route.length > 1 ? path.join(path.sep, normalizedRoute + '.html') : path.join(path.sep, 'index.html') - } - - // Call hook to let user update the path & html - const page = { route, path: fileName, html, exclude: false } - await this.nuxt.callHook('generate:page', page) - await this.nuxt.callHook('export:page', { page, errors: pageErrors }) - - if (page.exclude) { - return false - } - page.path = path.join(this.distPath, page.path) - - // Make sure the sub folders are created - 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: string) { - const minificationOptions = this.options.build.html.minify - - if (!minificationOptions) { - return html - } - - return htmlMinifier.minify(html, minificationOptions) - } -} diff --git a/packages/nuxt3/src/generator/index.ts b/packages/nuxt3/src/generator/index.ts deleted file mode 100644 index ab03cc622b..0000000000 --- a/packages/nuxt3/src/generator/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Nuxt } from 'src/core' - -import Generator from './generator' -export { default as Generator } from './generator' - -export function getGenerator (nuxt: Nuxt) { - return new Generator(nuxt) -} diff --git a/packages/nuxt3/src/server/context.ts b/packages/nuxt3/src/server/context.ts deleted file mode 100644 index fadad4c39a..0000000000 --- a/packages/nuxt3/src/server/context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Server } from 'src/server' - -export default class ServerContext { - nuxt: Server['nuxt'] - globals: Server['globals'] - options: Server['options'] - resources: Server['resources'] - - constructor (server: Server) { - this.nuxt = server.nuxt - this.globals = server.globals - this.options = server.options - this.resources = server.resources - } -} diff --git a/packages/nuxt3/src/server/index.ts b/packages/nuxt3/src/server/index.ts deleted file mode 100644 index 854a01a63b..0000000000 --- a/packages/nuxt3/src/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Server } from './server' -export { default as Listener } from './listener' diff --git a/packages/nuxt3/src/server/jsdom.ts b/packages/nuxt3/src/server/jsdom.ts deleted file mode 100644 index 9885780120..0000000000 --- a/packages/nuxt3/src/server/jsdom.ts +++ /dev/null @@ -1,79 +0,0 @@ -import consola from 'consola' -import { BaseOptions, DOMWindow } from 'jsdom' -import { DeterminedGlobals, timeout } from 'src/utils' - -interface Options { - globals: DeterminedGlobals - loadedCallback: string - loadingTimeout?: number -} - -export default async function renderAndGetWindow ( - url = 'http://localhost:3000', - jsdomOpts = {}, - { - loadedCallback, - loadingTimeout = 2000, - globals - }: Partial = {} -) { - const jsdom = await import('jsdom') - .then(m => m.default || m) - .catch((e) => { - consola.error(` - jsdom is not installed. Please install jsdom with: - $ yarn add --dev jsdom - OR - $ npm install --dev jsdom - `) - throw e - }) - - const options: BaseOptions = Object.assign({ - // Load subresources (https://github.com/tmpvar/jsdom#loading-subresources) - resources: 'usable' as const, - runScripts: 'dangerously' as const, - virtualConsole: undefined, - beforeParse (window: DOMWindow) { - // Mock window.scrollTo - window.scrollTo = () => {} - } - }, jsdomOpts) - - const jsdomErrHandler = (err: any) => { - throw err - } - - if (options.virtualConsole) { - if (options.virtualConsole === undefined) { - options.virtualConsole = new jsdom.VirtualConsole().sendTo(consola as unknown as typeof console) - } - // Throw error when window creation failed - options.virtualConsole.on('jsdomError', jsdomErrHandler) - } - - const { window } = await jsdom.JSDOM.fromURL(url, options) - - // If Nuxt could not be loaded (error from the server-side) - const nuxtExists = window.document.body.innerHTML.includes(`id="${globals.id}"`) - - if (!nuxtExists) { - const error = new Error('Could not load the nuxt app') - ;(error as any).body = window.document.body.innerHTML - window.close() - throw error - } - - // Used by Nuxt.js to say when the components are loaded and the app ready - await timeout(new Promise((resolve) => { - window[loadedCallback] = () => resolve(window) - }), loadingTimeout, `Components loading in renderAndGetWindow was not completed in ${loadingTimeout / 1000}s`) - - if (options.virtualConsole) { - // After window initialized successfully - options.virtualConsole.removeListener('jsdomError', jsdomErrHandler) - } - - // Send back window object - return window -} diff --git a/packages/nuxt3/src/server/listener.ts b/packages/nuxt3/src/server/listener.ts deleted file mode 100644 index 10aa523e10..0000000000 --- a/packages/nuxt3/src/server/listener.ts +++ /dev/null @@ -1,148 +0,0 @@ -import http from 'http' -import https from 'https' -import type { ListenOptions } from 'net' -import enableDestroy from 'server-destroy' -import ip from 'ip' -import consola from 'consola' -import pify from 'pify' - -import type { NormalizedConfiguration } from 'src/config' - -let RANDOM_PORT = '0' - -interface ListenerOptions { - port: number | string - host: string - socket: string - https: NormalizedConfiguration['server']['https'] - app: any - dev: boolean - baseURL: string -} - -export default class Listener { - port: number | string - host: string - socket: string - https: NormalizedConfiguration['server']['https'] - app: any - dev: boolean - baseURL: string - - listening: boolean - _server: null | http.Server - server: null | http.Server - address: null - url: null | string - constructor ({ port, host, socket, https, app, dev, baseURL }: ListenerOptions) { - // Options - this.port = port - this.host = host - this.socket = socket - this.https = https - this.app = app - this.dev = dev - this.baseURL = baseURL - - // After listen - this.listening = false - this._server = null - this.server = null - this.address = null - this.url = null - } - - async close () { - // Destroy server by forcing every connection to be closed - if (this.server && this.server.listening) { - await this.server.destroy() - consola.debug('server closed') - } - - // Delete references - this.listening = false - this._server = null - this.server = null - this.address = null - this.url = null - } - - computeURL () { - const address = this.server.address() - if (typeof address === 'string') { - return address - } - if (!this.socket) { - switch (address.address) { - case '127.0.0.1': this.host = 'localhost'; break - case '0.0.0.0': this.host = ip.address(); break - } - this.port = address.port - this.url = `http${this.https ? 's' : ''}://${this.host}:${this.port}${this.baseURL}` - return - } - this.url = `unix+http://${address}` - } - - async listen () { - // Prevent multi calls - if (this.listening) { - return - } - - // Initialize underlying http(s) server - const protocol = this.https ? https : http - const protocolOpts = this.https ? [this.https] : [] - this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app)) - - // Call server.listen - // Prepare listenArgs - const listenArgs: ListenOptions = this.socket ? { path: this.socket } : { host: this.host, port: Number(this.port) } - listenArgs.exclusive = false - - // Call server.listen - try { - this.server = await new Promise((resolve, reject) => { - this._server.on('error', error => reject(error)) - const s = this._server.listen(listenArgs, () => resolve(s)) - }) - } catch (error) { - return this.serverErrorHandler(error) - } - - // Enable destroy support - enableDestroy(this.server) - pify(this.server.destroy) - - // Compute listen URL - this.computeURL() - - // Set this.listening to true - this.listening = true - } - - async serverErrorHandler (error) { - // Detect if port is not available - const addressInUse = error.code === 'EADDRINUSE' - - // Use better error message - if (addressInUse) { - const address = this.socket || `${this.host}:${this.port}` - error.message = `Address \`${address}\` is already in use.` - - // Listen to a random port on dev as a fallback - if (this.dev && !this.socket && this.port !== RANDOM_PORT) { - consola.warn(error.message) - consola.info('Trying a random port...') - this.port = RANDOM_PORT - await this.close() - await this.listen() - RANDOM_PORT = this.port - return - } - } - - // Throw error - throw error - } -} diff --git a/packages/nuxt3/src/server/middleware/error.ts b/packages/nuxt3/src/server/middleware/error.ts deleted file mode 100644 index fd84e6553d..0000000000 --- a/packages/nuxt3/src/server/middleware/error.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'http' - -import path from 'path' -import fs from 'fs-extra' -import consola from 'consola' -import Youch from '@nuxtjs/youch' - -import type { Nuxt } from 'src/core' - -export default ({ resources, options }) => async function errorMiddleware (_error, req: IncomingMessage, res: ServerResponse) { - // Normalize error - const error = normalizeError(_error, options) - - const sendResponse = (content, type = 'text/html') => { - // Set Headers - res.statusCode = error.statusCode - res.statusMessage = 'RuntimeError' - res.setHeader('Content-Type', type + '; charset=utf-8') - res.setHeader('Content-Length', Buffer.byteLength(content)) - res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate') - - // Error headers - if (error.headers) { - for (const name in error.headers) { - res.setHeader(name, error.headers[name]) - } - } - - // Send Response - res.end(content, 'utf-8') - } - - // Check if request accepts JSON - const hasReqHeader = (header, includes) => { - const headerValue = req.headers[header] - if (typeof headerValue === 'string') { - return headerValue.toLowerCase().includes(includes) - } - } - const isJson = - hasReqHeader('accept', 'application/json') || - hasReqHeader('user-agent', 'curl/') - - // Use basic errors when debug mode is disabled - if (!options.debug) { - // We hide actual errors from end users, so show them on server logs - if (error.statusCode !== 404) { - consola.error(error) - } - - // Json format is compatible with Youch json responses - const json = { - status: error.statusCode, - message: error.message, - name: error.name - } - if (isJson) { - sendResponse(JSON.stringify(json, undefined, 2), 'text/json') - return - } - const html = resources.errorTemplate(json) - sendResponse(html) - return - } - - // Show stack trace - const youch = new Youch( - error, - req, - readSource, - options.router.base, - true - ) - if (isJson) { - const json = await youch.toJSON() - sendResponse(JSON.stringify(json, undefined, 2), 'text/json') - return - } - - const html = await youch.toHTML() - sendResponse(html) -} - -const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : null - -const normalizeError = (_error, { srcDir, rootDir, buildDir }) : Nuxt['error'] => { - if (typeof _error === 'string') { - _error = { message: _error } - } else if (!_error) { - _error = { message: '' } - } - - const error: Nuxt['error'] = new Error() - error.message = _error.message - error.name = _error.name - error.statusCode = _error.statusCode || 500 - error.headers = _error.headers - - const searchPath = [ - srcDir, - rootDir, - path.join(buildDir, 'dist', 'server'), - buildDir, - process.cwd() - ] - - const findInPaths = (fileName) => { - for (const dir of searchPath) { - const fullPath = path.resolve(dir, fileName) - if (fs.existsSync(fullPath)) { - return fullPath - } - } - return fileName - } - - error.stack = (_error.stack || '') - .split('\n') - .map((line) => { - const match = line.match(/\(([^)]+)\)|([^\s]+\.[^\s]+):/) - if (!match) { - return line - } - const src = match[1] || match[2] || '' - return line.replace(src, findInPaths(sanitizeName(src))) - }) - .join('\n') - - return error -} - -async function readSource (frame) { - if (fs.existsSync(frame.fileName)) { - frame.fullPath = frame.fileName // Youch BW compat - frame.contents = await fs.readFile(frame.fileName, 'utf-8') - } -} diff --git a/packages/nuxt3/src/server/middleware/nuxt.ts b/packages/nuxt3/src/server/middleware/nuxt.ts deleted file mode 100644 index b03262490c..0000000000 --- a/packages/nuxt3/src/server/middleware/nuxt.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { RenderContext } from 'src/vue-renderer/renderer' - -import generateETag from 'etag' -import fresh from 'fresh' -import consola from 'consola' - -import { getContext, TARGETS } from 'src/utils' - -export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) { - // Get context - const context: RenderContext = getContext(req, res) - - try { - const url = decodeURI(req.url) - res.statusCode = 200 - const result = await renderRoute(url, context) - - // If result is falsy, call renderLoading - if (!result) { - await nuxt.callHook('server:nuxt:renderLoading', req, res) - return - } - - await nuxt.callHook('render:route', url, result, context) - const { - html, - cspScriptSrcHashes, - error, - redirected, - preloadFiles - } = result - - if (redirected && context.target !== TARGETS.static) { - await nuxt.callHook('render:routeDone', url, result, context) - return html - } - if (error) { - res.statusCode = context.nuxt.error.statusCode || 500 - } - - // Add ETag header - if (!error && options.render.etag) { - const { hash } = options.render.etag - const etag = hash ? hash(html, options.render.etag) : generateETag(html, options.render.etag) - if (fresh(req.headers, { etag })) { - res.statusCode = 304 - await nuxt.callHook('render:beforeResponse', url, result, context) - res.end() - await nuxt.callHook('render:routeDone', url, result, context) - return - } - res.setHeader('ETag', etag) - } - - // HTTP2 push headers for preload assets - if (!error && options.render.http2.push) { - // Parse resourceHints to extract HTTP.2 prefetch/push headers - // https://w3c.github.io/preload/#server-push-http-2 - const { shouldPush, pushAssets } = options.render.http2 - const { publicPath } = resources.clientManifest - - const links = pushAssets - ? pushAssets(req, res, publicPath, preloadFiles) - : defaultPushAssets(preloadFiles, shouldPush, publicPath, options) - - // Pass with single Link header - // https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header - // https://www.w3.org/Protocols/9707-link-header.html - if (links.length > 0) { - res.setHeader('Link', links.join(', ')) - } - } - - if (options.render.csp && cspScriptSrcHashes) { - const { allowedSources, policies } = options.render.csp - const isReportOnly = !!options.render.csp.reportOnly - const cspHeader = isReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' - - res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isDev: options.dev, isReportOnly })) - } - - // Send response - res.setHeader('Content-Type', 'text/html; charset=utf-8') - res.setHeader('Accept-Ranges', 'none') // #3870 - res.setHeader('Content-Length', Buffer.byteLength(html)) - await nuxt.callHook('render:beforeResponse', url, result, context) - res.end(html, 'utf8') - await nuxt.callHook('render:routeDone', url, result, context) - return html - } catch (err) { - if (context && context.redirected) { - consola.error(err) - return err - } - - if (err.name === 'URIError') { - err.statusCode = 400 - } - next(err) - } -} - -const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => { - if (shouldPush && options.dev) { - consola.warn('http2.shouldPush is deprecated. Use http2.pushAssets function') - } - - const links = [] - preloadFiles.forEach(({ file, asType, fileWithoutQuery, modern }) => { - // By default, we only preload scripts or css - if (!shouldPush && asType !== 'script' && asType !== 'style') { - return - } - - // User wants to explicitly control what to preload - if (shouldPush && !shouldPush(fileWithoutQuery, asType)) { - return - } - - const { crossorigin } = options.render - const cors = `${crossorigin ? ` crossorigin=${crossorigin};` : ''}` - // `modulepreload` rel attribute only supports script-like `as` value - // https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload - const rel = modern && asType === 'script' ? 'modulepreload' : 'preload' - - links.push(`<${publicPath}${file}>; rel=${rel};${cors} as=${asType}`) - }) - return links -} - -const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev, isReportOnly }) => { - const joinedHashes = cspScriptSrcHashes.join(' ') - const baseCspStr = `script-src 'self'${isDev ? ' \'unsafe-eval\'' : ''} ${joinedHashes}` - const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies) - - if (Array.isArray(allowedSources) && allowedSources.length) { - return isReportOnly && policyObjectAvailable && !!policies['report-uri'] ? `${baseCspStr} ${allowedSources.join(' ')}; report-uri ${policies['report-uri']};` : `${baseCspStr} ${allowedSources.join(' ')}` - } - - if (policyObjectAvailable) { - const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes) - return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`).join('; ') - } - - return baseCspStr -} - -const transformPolicyObject = (policies, cspScriptSrcHashes) => { - const userHasDefinedScriptSrc = policies['script-src'] && Array.isArray(policies['script-src']) - - const additionalPolicies = userHasDefinedScriptSrc ? policies['script-src'] : [] - - // Self is always needed for inline-scripts, so add it, no matter if the user specified script-src himself. - const hashAndPolicyList = cspScriptSrcHashes.concat('\'self\'', additionalPolicies) - - return { ...policies, 'script-src': hashAndPolicyList } -} diff --git a/packages/nuxt3/src/server/middleware/timing.ts b/packages/nuxt3/src/server/middleware/timing.ts deleted file mode 100644 index cea61e1064..0000000000 --- a/packages/nuxt3/src/server/middleware/timing.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { ServerResponse, IncomingMessage } from 'http' - -import consola from 'consola' -import onHeaders from 'on-headers' -import { Timer } from 'src/utils' - -export default options => (_req: IncomingMessage, res: ServerResponse & { timing?: ServerTiming }, next: (err?: any) => void) => { - if (res.timing) { - consola.warn('server-timing is already registered.') - } - res.timing = new ServerTiming() - - if (options && options.total) { - res.timing.start('total', 'Nuxt Server Time') - } - - onHeaders(res, () => { - res.timing.end('total') - - if (res.timing.headers.length > 0) { - res.setHeader( - 'Server-Timing', - [] - .concat(res.getHeader('Server-Timing') || []) - .concat(res.timing.headers) - .join(', ') - ) - } - res.timing.clear() - }) - - next() -} - -class ServerTiming extends Timer { - headers: string[] - - constructor () { - super() - this.headers = [] - } - - end (name?: string) { - const time = super.end(name) - if (time) { - this.headers.push(this.formatHeader(time)) - } - return time - } - - clear () { - super.clear() - this.headers.length = 0 - } - - formatHeader (time: ReturnType) { - const desc = time.description ? `;desc="${time.description}"` : '' - return `${time.name};dur=${time.duration}${desc}` - } -} diff --git a/packages/nuxt3/src/server/package.ts b/packages/nuxt3/src/server/package.ts deleted file mode 100644 index 2f78f772ee..0000000000 --- a/packages/nuxt3/src/server/package.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default { - build: true, - rollup: { - externals: ['jsdom'] - } -} diff --git a/packages/nuxt3/src/server/server.ts b/packages/nuxt3/src/server/server.ts deleted file mode 100644 index 131466d4b4..0000000000 --- a/packages/nuxt3/src/server/server.ts +++ /dev/null @@ -1,437 +0,0 @@ -import path from 'path' -import { ServerResponse, IncomingMessage } from 'http' -import consola from 'consola' -import launchMiddleware from 'launch-editor-middleware' -import serveStatic from 'serve-static' -import servePlaceholder from 'serve-placeholder' -import express from 'express' -import type { TemplateExecutor } from 'lodash' - -import { Nuxt } from 'src/core' -import { DeterminedGlobals, determineGlobals, isUrl } from 'src/utils' -import { VueRenderer } from 'src/vue-renderer' - -import ServerContext from './context' -import renderAndGetWindow from './jsdom' -import nuxtMiddleware from './middleware/nuxt' -import errorMiddleware from './middleware/error' -import Listener from './listener' -import createTimingMiddleware from './middleware/timing' - -interface Manifest { - assetsMapping: Record - publicPath: string - initial: Array - async: Array -} - -type NuxtMiddleware = express.Handler & { - prefix?: string, - entry?: string, - _middleware?: NuxtMiddleware -} - -export default class Server { - __closed?: boolean - _readyCalled?: boolean - - app: express.Application - devMiddleware: (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => any - listeners: Listener[] - nuxt: Nuxt - globals: DeterminedGlobals - options: Nuxt['options'] - publicPath: string - renderer: VueRenderer - resources: { - clientManifest?: Manifest - loadingHTML?: string - modernManifest?: Manifest - serverManifest?: Manifest - ssrTemplate?: TemplateExecutor - spaTemplate?: TemplateExecutor - errorTemplate?: TemplateExecutor - } - - serverContext: ServerContext - - constructor (nuxt: Nuxt) { - this.nuxt = nuxt - this.options = nuxt.options - - this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals) - - this.publicPath = isUrl(this.options.build.publicPath) - ? this.options.build._publicPath - : this.options.build.publicPath - - // Runtime shared resources - this.resources = {} - - // Will be set after listen - this.listeners = [] - - // Create new express instance - this.app = express() - - // Close hook - this.nuxt.hook('close', () => this.close()) - - // devMiddleware placeholder - if (this.options.dev) { - this.nuxt.hook('server:devMiddleware', (devMiddleware) => { - this.devMiddleware = devMiddleware - }) - } - } - - async ready () { - if (this._readyCalled) { - return this - } - this._readyCalled = true - - await this.nuxt.callHook('render:before', this, this.options.render) - - // Initialize vue-renderer - this.serverContext = new ServerContext(this) - this.renderer = new VueRenderer(this.serverContext) - await this.renderer.ready() - - // Setup nuxt middleware - await this.setupMiddleware() - - // Call done hook - await this.nuxt.callHook('render:done', this) - - return this - } - - async setupMiddleware () { - // Apply setupMiddleware from modules first - await this.nuxt.callHook('render:setupMiddleware', this.app) - - // Compression middleware for production - if (!this.options.dev) { - const { compressor } = this.options.render - if (typeof compressor === 'object') { - // If only setting for `compression` are provided, require the module and insert - const compression = this.nuxt.resolver.requireModule('compression') - this.useMiddleware(compression(compressor)) - } else if (compressor) { - // Else, require own compression middleware if compressor is actually truthy - this.useMiddleware(compressor) - } - } - - if (typeof this.options.server !== 'boolean' && this.options.server.timing) { - this.useMiddleware(createTimingMiddleware(this.options.server.timing)) - } - - // For serving static/ files to / - const staticMiddleware : NuxtMiddleware = serveStatic( - path.resolve(this.options.srcDir, this.options.dir.static), - this.options.render.static - ) - - staticMiddleware.prefix = this.options.render.static.prefix - this.useMiddleware(staticMiddleware) - - // Serve .nuxt/dist/client files only for production - // For dev they will be served with devMiddleware - if (!this.options.dev) { - const distDir = path.resolve(this.options.buildDir, 'dist', 'client') - this.useMiddleware({ - path: this.publicPath, - handler: serveStatic( - distDir, - this.options.render.dist - ) - }) - } - - // Dev middleware - if (this.options.dev) { - this.useMiddleware((req, res, next) => { - if (!this.devMiddleware) { - return next() - } - this.devMiddleware(req, res, next) - }) - - // open in editor for debug mode only - if (this.options.debug) { - this.useMiddleware({ - path: '__open-in-editor', - handler: launchMiddleware(this.options.editor) - }) - } - } - - // Add user provided middleware - for (const m of this.options.serverMiddleware) { - this.useMiddleware(m) - } - - // Graceful 404 error handler - const { fallback } = this.options.render - if (fallback) { - // Dist files - if (fallback.dist) { - this.useMiddleware({ - path: this.publicPath, - handler: servePlaceholder(fallback.dist) - }) - } - - // Other paths - if (fallback.static) { - this.useMiddleware({ - path: '/', - handler: servePlaceholder(fallback.static) - }) - } - } - - // Finally use nuxtMiddleware - this.useMiddleware(nuxtMiddleware({ - options: this.options, - nuxt: this.nuxt, - renderRoute: this.renderRoute.bind(this), - resources: this.resources - })) - - // Apply errorMiddleware from modules first - await this.nuxt.callHook('render:errorMiddleware', this.app) - - // Error middleware for errors that occurred in middleware that declared above - this.useMiddleware(errorMiddleware({ - resources: this.resources, - options: this.options - })) - } - - _normalizeMiddleware (middleware) { - // Normalize plain function - if (typeof middleware === 'function') { - middleware = { handle: middleware } - } - - // If a plain string provided as path to middleware - if (typeof middleware === 'string') { - middleware = this._requireMiddleware(middleware) - } - - // Normalize handler to handle (backward compatibility) - if (middleware.handler && !middleware.handle) { - middleware.handle = middleware.handler - delete middleware.handler - } - - // Normalize path to route (backward compatibility) - if (middleware.path && !middleware.route) { - middleware.route = middleware.path - delete middleware.path - } - - // If handle is a string pointing to path - if (typeof middleware.handle === 'string') { - Object.assign(middleware, this._requireMiddleware(middleware.handle)) - } - - // No handle - if (!middleware.handle) { - middleware.handle = (_req, _res, next) => { - next(new Error('ServerMiddleware should expose a handle: ' + middleware.entry)) - } - } - - // Prefix on handle (proxy-module) - if (middleware.handle.prefix !== undefined && middleware.prefix === undefined) { - middleware.prefix = middleware.handle.prefix - } - - // sub-app (express) - if (typeof middleware.handle.handle === 'function') { - const server = middleware.handle - middleware.handle = server.handle.bind(server) - } - - return middleware - } - - _requireMiddleware (entry) { - // Resolve entry - entry = this.nuxt.resolver.resolvePath(entry) - - // Require middleware - let middleware - try { - middleware = this.nuxt.resolver.requireModule(entry) - } catch (error) { - // Show full error - consola.error('ServerMiddleware Error:', error) - - // Placeholder for error - middleware = (_req, _res, next) => { next(error) } - } - - // Normalize - middleware = this._normalizeMiddleware(middleware) - - // Set entry - middleware.entry = entry - - return middleware - } - - resolveMiddleware (middleware, fallbackRoute = '/') { - // Ensure middleware is normalized - middleware = this._normalizeMiddleware(middleware) - - // Fallback route - if (!middleware.route) { - middleware.route = fallbackRoute - } - - // Resolve final route - middleware.route = ( - (middleware.prefix !== false ? this.options.router.base : '') + - (typeof middleware.route === 'string' ? middleware.route : '') - ).replace(/\/\//g, '/') - - // Strip trailing slash - if (middleware.route.endsWith('/')) { - middleware.route = middleware.route.slice(0, -1) - } - - // Assign _middleware to handle to make accessible from app.stack - middleware.handle._middleware = middleware - - return middleware - } - - useMiddleware (middleware) { - const { route, handle } = this.resolveMiddleware(middleware) - this.app.use(route, handle) - } - - replaceMiddleware (query, middleware) { - let serverStackItem - - if (typeof query === 'string') { - // Search by entry - serverStackItem = this.app.stack.find(({ handle }) => { - const middleware = (handle as NuxtMiddleware)._middleware - return middleware && middleware.entry === query - }) - } else { - // Search by reference - serverStackItem = this.app.stack.find(({ handle }) => handle === query) - } - - // Stop if item not found - if (!serverStackItem) { - return - } - - // unload middleware - this.unloadMiddleware(serverStackItem) - - // Resolve middleware - const { route, handle } = this.resolveMiddleware(middleware, serverStackItem.route) - - // Update serverStackItem - serverStackItem.handle = handle - - // Error State - serverStackItem.route = route - - // Return updated item - return serverStackItem - } - - unloadMiddleware ({ handle }) { - if (handle._middleware && typeof handle._middleware.unload === 'function') { - handle._middleware.unload() - } - } - - serverMiddlewarePaths () { - return this.app.stack.map(({ handle }) => { - const middleware = (handle as NuxtMiddleware)._middleware - return middleware && middleware.entry - }).filter(Boolean) - } - - renderRoute () { - return this.renderer.renderRoute.apply(this.renderer, arguments) - } - - loadResources () { - return this.renderer.loadResources.apply(this.renderer, arguments) - } - - renderAndGetWindow (url, opts = {}, { - loadingTimeout = 2000, - loadedCallback = this.globals.loadedCallback, - globals = this.globals - } = {}) { - return renderAndGetWindow(url, opts, { - loadingTimeout, - loadedCallback, - globals - }) - } - - async listen (port?: string | number, host?: string, socket?: string) { - // Ensure nuxt is ready - await this.nuxt.ready() - - const serviceConfig = typeof this.options.server === 'object' ? this.options.server : {} - - // Create a new listener - const listener = new Listener({ - port: typeof port !== 'number' && isNaN(parseInt(port)) ? serviceConfig.port : port, - host: host || serviceConfig.host, - socket: socket || serviceConfig.socket, - https: serviceConfig.https, - app: this.app, - dev: this.options.dev, - baseURL: this.options.router.base - }) - - // Listen - await listener.listen() - - // Push listener to this.listeners - this.listeners.push(listener) - - await this.nuxt.callHook('listen', listener.server, listener) - - return listener - } - - async close () { - if (this.__closed) { - return - } - this.__closed = true - - await Promise.all(this.listeners.map(l => l.close())) - - this.listeners = [] - - if (typeof this.renderer.close === 'function') { - await this.renderer.close() - } - - this.app.stack.forEach(this.unloadMiddleware) - this.app.removeAllListeners() - this.app = null - - for (const key in this.resources) { - delete this.resources[key] - } - } -} diff --git a/packages/nuxt3/src/vue-renderer/index.ts b/packages/nuxt3/src/vue-renderer/index.ts deleted file mode 100644 index 0567e6025f..0000000000 --- a/packages/nuxt3/src/vue-renderer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as VueRenderer } from './renderer' diff --git a/packages/nuxt3/src/vue-renderer/renderer.ts b/packages/nuxt3/src/vue-renderer/renderer.ts deleted file mode 100644 index eefa440681..0000000000 --- a/packages/nuxt3/src/vue-renderer/renderer.ts +++ /dev/null @@ -1,419 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' -import consola from 'consola' -import template from 'lodash/template' -import { Target, TARGETS, isModernRequest, waitFor } from 'src/utils' - -import ServerContext from 'src/server/context' -import SPARenderer from './renderers/spa' -import SSRRenderer from './renderers/ssr' -import ModernRenderer from './renderers/modern' - -declare module 'fs-extra' { - export function exists(path: string): Promise; -} - -export interface RenderContext { - target?: Target - spa?: boolean - modern?: boolean - nuxt?: ServerContext['nuxt'] - redirected?: boolean - req?: any - res?: any - runtimeConfig?: { - private: ServerContext['options']['privateRuntimeConfig'], - public: ServerContext['options']['publicRuntimeConfig'] - } - url?: string -} - -export default class VueRenderer { - __closed?: boolean - _state?: 'created' | 'loading' | 'ready' | 'error' - _error?: null - _readyPromise?: Promise - distPath: string - options: ServerContext['options'] - serverContext: ServerContext - renderer: { - ssr: any - modern: any - spa: any - } - - constructor (context: ServerContext) { - this.serverContext = context - this.options = this.serverContext.options - - // Will be set by createRenderer - this.renderer = { - ssr: undefined, - modern: undefined, - spa: undefined - } - - // Renderer runtime resources - Object.assign(this.serverContext.resources, { - clientManifest: undefined, - modernManifest: undefined, - serverManifest: undefined, - ssrTemplate: undefined, - spaTemplate: undefined, - errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error') - }) - - // Default status - this._state = 'created' - this._error = null - } - - ready () { - if (!this._readyPromise) { - this._state = 'loading' - this._readyPromise = this._ready() - .then(() => { - this._state = 'ready' - return this - }) - .catch((error) => { - this._state = 'error' - this._error = error - throw error - }) - } - - return this._readyPromise - } - - async _ready () { - // Resolve dist path - this.distPath = path.resolve(this.options.buildDir, 'dist', 'server') - - // -- Development mode -- - if (this.options.dev) { - this.serverContext.nuxt.hook('build:resources', mfs => this.loadResources(mfs)) - return - } - - // -- Production mode -- - - // Try once to load SSR resources from fs - await this.loadResources(fs) - - // Without using `nuxt start` (programmatic, tests and generate) - if (!this.options._start) { - this.serverContext.nuxt.hook('build:resources', () => this.loadResources(fs)) - return - } - - // Verify resources - if (this.options.modern && !this.isModernReady) { - throw new Error( - `No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.` - ) - } else if (!this.isReady) { - throw new Error( - `No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.` - ) - } - } - - async loadResources (_fs: typeof import('fs-extra')) { - const updated = [] - - const readResource = async (fileName: string, encoding: string) => { - try { - const fullPath = path.resolve(this.distPath, fileName) - - if (!await _fs.exists(fullPath)) { - return - } - const contents = await _fs.readFile(fullPath, encoding) - - return contents - } catch (err) { - consola.error('Unable to load resource:', fileName, err) - } - } - - for (const resourceName in this.resourceMap) { - const { fileName, transform, encoding } = this.resourceMap[resourceName] - - // Load resource - let resource = await readResource(fileName, encoding) - - // Skip unavailable resources - if (!resource) { - continue - } - - // Apply transforms - if (typeof transform === 'function') { - resource = await transform(resource, { readResource }) - } - - // Update resource - this.serverContext.resources[resourceName] = resource - updated.push(resourceName) - } - - // Load templates - await this.loadTemplates() - - await this.serverContext.nuxt.callHook('render:resourcesLoaded', this.serverContext.resources) - - // Detect if any resource updated - if (updated.length > 0) { - // Create new renderer - this.createRenderer() - } - } - - async loadTemplates () { - // Reload error template - const errorTemplatePath = path.resolve(this.options.buildDir, 'views/error.html') - - if (await fs.exists(errorTemplatePath)) { - const errorTemplate = await fs.readFile(errorTemplatePath, 'utf8') - this.serverContext.resources.errorTemplate = this.parseTemplate(errorTemplate) - } - - // Reload loading template - const loadingHTMLPath = path.resolve(this.options.buildDir, 'loading.html') - - if (await fs.exists(loadingHTMLPath)) { - this.serverContext.resources.loadingHTML = await fs.readFile(loadingHTMLPath, 'utf8') - this.serverContext.resources.loadingHTML = this.serverContext.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '') - } else { - this.serverContext.resources.loadingHTML = '' - } - } - - // TODO: Remove in Nuxt 3 - get noSSR () { /* Backward compatibility */ - return this.options.render.ssr === false - } - - get SSR () { - return this.options.render.ssr === true - } - - get isReady () { - // SPA - if (!this.serverContext.resources.spaTemplate || !this.renderer.spa) { - return false - } - - // SSR - if (this.SSR && (!this.serverContext.resources.ssrTemplate || !this.renderer.ssr)) { - return false - } - - return true - } - - get isModernReady () { - return this.isReady && this.serverContext.resources.modernManifest - } - - // TODO: Remove in Nuxt 3 - get isResourcesAvailable () { /* Backward compatibility */ - return this.isReady - } - - detectModernBuild () { - const { options, resources } = this.serverContext - if ([false, 'client', 'server'].includes(options.modern)) { - return - } - - const isExplicitStaticModern = options.target === TARGETS.static && options.modern - if (!resources.modernManifest && !isExplicitStaticModern) { - options.modern = false - return - } - - options.modern = options.render.ssr ? 'server' : 'client' - consola.info(`Modern bundles are detected. Modern mode (\`${options.modern}\`) is enabled now.`) - } - - createRenderer () { - // Resource clientManifest is always required - if (!this.serverContext.resources.clientManifest) { - return - } - - this.detectModernBuild() - - // Create SPA renderer - if (this.serverContext.resources.spaTemplate) { - this.renderer.spa = new SPARenderer(this.serverContext) - } - - // Skip the rest if SSR resources are not available - if (this.serverContext.resources.ssrTemplate && this.serverContext.resources.serverManifest) { - // Create bundle renderer for SSR - this.renderer.ssr = new SSRRenderer(this.serverContext) - - if (this.options.modern !== false) { - this.renderer.modern = new ModernRenderer(this.serverContext) - } - } - } - - renderSPA (renderContext) { - return this.renderer.spa.render(renderContext) - } - - renderSSR (renderContext) { - // Call renderToString from the bundleRenderer and generate the HTML (will update the renderContext as well) - const renderer = renderContext.modern ? this.renderer.modern : this.renderer.ssr - return renderer.render(renderContext) - } - - async renderRoute (url, renderContext : RenderContext = {}, _retried = 0) { - /* istanbul ignore if */ - if (!this.isReady) { - // Fall-back to loading-screen if enabled - if (this.options.build.loadingScreen) { - // Tell nuxt middleware to use `server:nuxt:renderLoading hook - return false - } - - // Retry - const retryLimit = this.options.dev ? 60 : 3 - if (_retried < retryLimit && this._state !== 'error') { - await this.ready().then(() => waitFor(1000)) - return this.renderRoute(url, renderContext, _retried + 1) - } - - // Throw Error - switch (this._state) { - case 'created': - throw new Error('Renderer ready() is not called! Please ensure `nuxt.ready()` is called and awaited.') - case 'loading': - throw new Error('Renderer is loading.') - case 'error': - throw this._error - case 'ready': - throw new Error(`Renderer resources are not loaded! Please check possible console errors and ensure dist (${this.distPath}) exists.`) - default: - throw new Error('Renderer is in unknown state!') - } - } - - // Log rendered url - consola.debug(`Rendering url ${url}`) - - // Add url to the renderContext - renderContext.url = url - // Add target to the renderContext - renderContext.target = this.serverContext.nuxt.options.target - - const { req = {}, res = {} } = renderContext - - // renderContext.spa - if (renderContext.spa === undefined) { - // TODO: Remove reading from renderContext.res in Nuxt3 - renderContext.spa = !this.SSR || req.spa || res.spa - } - - // renderContext.modern - if (renderContext.modern === undefined) { - const modernMode = this.options.modern - renderContext.modern = modernMode === 'client' || isModernRequest(req, modernMode) - } - - // Set runtime config on renderContext - renderContext.runtimeConfig = { - private: renderContext.spa ? {} : { ...this.options.privateRuntimeConfig }, - public: { ...this.options.publicRuntimeConfig } - } - - // Call renderContext hook - await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext) - - // Render SPA or SSR - return renderContext.spa - ? this.renderSPA(renderContext) - : this.renderSSR(renderContext) - } - - get resourceMap () { - return { - clientManifest: { - fileName: 'client.manifest.json', - transform: (src: string) => JSON.parse(src) - }, - modernManifest: { - fileName: 'modern.manifest.json', - transform: (src: string) => JSON.parse(src) - }, - serverManifest: { - fileName: 'server.manifest.json', - // BundleRenderer needs resolved contents - transform: async (src: string, { readResource }) => { - const serverManifest = JSON.parse(src) - - const readResources = async (obj) => { - const _obj = {} - await Promise.all(Object.keys(obj).map(async (key) => { - _obj[key] = await readResource(obj[key]) - })) - return _obj - } - - const [files, maps] = await Promise.all([ - readResources(serverManifest.files), - readResources(serverManifest.maps) - ]) - - // Try to parse sourcemaps - for (const map in maps) { - if (maps[map] && maps[map].version) { - continue - } - try { - maps[map] = JSON.parse(maps[map]) - } catch (e) { - maps[map] = { version: 3, sources: [], mappings: '' } - } - } - - return { - ...serverManifest, - files, - maps - } - } - }, - ssrTemplate: { - fileName: 'index.ssr.html', - transform: (src: string) => this.parseTemplate(src) - }, - spaTemplate: { - fileName: 'index.spa.html', - transform: (src: string) => this.parseTemplate(src) - } - } - } - - parseTemplate (templateStr: string) { - return template(templateStr, { - interpolate: /{{([\s\S]+?)}}/g, - evaluate: /{%([\s\S]+?)%}/g - }) - } - - close () { - if (this.__closed) { - return - } - this.__closed = true - - for (const key in this.renderer) { - delete this.renderer[key] - } - } -} diff --git a/packages/nuxt3/src/vue-renderer/renderers/base.ts b/packages/nuxt3/src/vue-renderer/renderers/base.ts deleted file mode 100644 index 5bafdf6f0f..0000000000 --- a/packages/nuxt3/src/vue-renderer/renderers/base.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ServerContext from 'src/server/context' -import { RenderContext } from '../renderer' - -export default class BaseRenderer { - serverContext: ServerContext - options: ServerContext['options'] - - constructor (serverContext: ServerContext) { - this.serverContext = serverContext - this.options = serverContext.options - } - - renderTemplate (templateFn: (options: Record) => void, opts: Record) { - // Fix problem with HTMLPlugin's minify option (#3392) - opts.html_attrs = opts.HTML_ATTRS - opts.head_attrs = opts.HEAD_ATTRS - opts.body_attrs = opts.BODY_ATTRS - - return templateFn(opts) - } - - render (_renderContext: RenderContext) { - throw new Error('`render()` needs to be implemented') - } -} diff --git a/packages/nuxt3/src/vue-renderer/renderers/modern.ts b/packages/nuxt3/src/vue-renderer/renderers/modern.ts deleted file mode 100644 index bec3d59eda..0000000000 --- a/packages/nuxt3/src/vue-renderer/renderers/modern.ts +++ /dev/null @@ -1,128 +0,0 @@ -import ServerContext from 'src/server/context' -import { isUrl, urlJoin, safariNoModuleFix } from 'src/utils' - -import SSRRenderer from './ssr' - -export default class ModernRenderer extends SSRRenderer { - _assetsMapping?: Record - publicPath: string - - constructor (serverContext: ServerContext) { - super(serverContext) - - const { build: { publicPath }, router: { base } } = this.options - this.publicPath = isUrl(publicPath) ? publicPath : urlJoin(base, publicPath) - } - - get assetsMapping () { - if (this._assetsMapping) { - return this._assetsMapping - } - - const { clientManifest, modernManifest } = this.serverContext.resources - const legacyAssets = clientManifest.assetsMapping - const modernAssets = modernManifest.assetsMapping - const mapping: Record = {} - - Object.keys(legacyAssets).forEach((componentHash) => { - const modernComponentAssets = modernAssets[componentHash] || [] - legacyAssets[componentHash].forEach((legacyAssetName, index) => { - mapping[legacyAssetName] = modernComponentAssets[index] - }) - }) - delete clientManifest.assetsMapping - delete modernManifest.assetsMapping - this._assetsMapping = mapping - - return mapping - } - - get isServerMode () { - return this.options.modern === 'server' - } - - get rendererOptions () { - const rendererOptions = super.rendererOptions - if (this.isServerMode) { - rendererOptions.clientManifest = this.serverContext.resources.modernManifest - } - return rendererOptions - } - - renderScripts (renderContext) { - const scripts = super.renderScripts(renderContext) - - if (this.isServerMode) { - return scripts - } - - const scriptPattern = /]*?src="([^"]*?)" defer><\/script>/g - - const modernScripts = scripts.replace(scriptPattern, (scriptTag, jsFile) => { - const legacyJsFile = jsFile.replace(this.publicPath, '') - const modernJsFile = this.assetsMapping[legacyJsFile] - if (!modernJsFile) { - return scriptTag.replace('${safariNoModuleFix}` - - return safariNoModuleFixScript + modernScripts - } - - getModernFiles (legacyFiles = []) { - const modernFiles = [] - - for (const legacyJsFile of legacyFiles) { - const modernFile = { ...legacyJsFile, modern: true } - if (modernFile.asType === 'script') { - const file = this.assetsMapping[legacyJsFile.file] - modernFile.file = file - modernFile.fileWithoutQuery = file.replace(/\?.*/, '') - } - modernFiles.push(modernFile) - } - - return modernFiles - } - - getPreloadFiles (renderContext) { - const preloadFiles = super.getPreloadFiles(renderContext) - // In eligible server modern mode, preloadFiles are modern bundles from modern renderer - return this.isServerMode ? preloadFiles : this.getModernFiles(preloadFiles) - } - - renderResourceHints (renderContext) { - const resourceHints = super.renderResourceHints(renderContext) - if (this.isServerMode) { - return resourceHints - } - - const linkPattern = /]*?href="([^"]*?)"[^>]*?as="script"[^>]*?>/g - - return resourceHints.replace(linkPattern, (linkTag, jsFile) => { - const legacyJsFile = jsFile.replace(this.publicPath, '') - const modernJsFile = this.assetsMapping[legacyJsFile] - if (!modernJsFile) { - return '' - } - return linkTag - .replace('rel="preload"', 'rel="modulepreload"') - .replace(legacyJsFile, modernJsFile) - }) - } - - render (renderContext) { - if (this.isServerMode) { - renderContext.res.setHeader('Vary', 'User-Agent') - } - return super.render(renderContext) - } -} diff --git a/packages/nuxt3/src/vue-renderer/renderers/spa.ts b/packages/nuxt3/src/vue-renderer/renderers/spa.ts deleted file mode 100644 index d3f51903d9..0000000000 --- a/packages/nuxt3/src/vue-renderer/renderers/spa.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { extname } from 'path' -import cloneDeep from 'lodash/cloneDeep' -import VueMeta from 'vue-meta' -import LRU from 'lru-cache' -import devalue from '@nuxt/devalue' -import { TARGETS, isModernRequest } from 'src/utils' -import ServerContext from 'src/server/context' -import BaseRenderer from './base' - -export default class SPARenderer extends BaseRenderer { - cache: LRU - vueMetaConfig: { - ssrAppId: string - keyName: string - attribute: string - ssrAttribute: string - tagIDKeyName: string - } - - constructor (serverContext: ServerContext) { - super(serverContext) - - this.cache = new LRU() - - this.vueMetaConfig = { - ssrAppId: '1', - ...this.options.vueMeta, - keyName: 'head', - attribute: 'data-n-head', - ssrAttribute: 'data-n-head-ssr', - tagIDKeyName: 'hid' - } - } - - async render (renderContext) { - const { url = '/', req = {} } = renderContext - const modernMode = this.options.modern - const modern = (modernMode && this.options.target === TARGETS.static) || isModernRequest(req, modernMode) - const cacheKey = `${modern ? 'modern:' : 'legacy:'}${url}` - let meta : Record = this.cache.get(cacheKey) - - if (meta) { - // Return a copy of the content, so that future - // modifications do not effect the data in cache - return cloneDeep(meta) - } - - meta = { - HTML_ATTRS: '', - HEAD_ATTRS: '', - BODY_ATTRS: '', - HEAD: '', - BODY_SCRIPTS_PREPEND: '', - BODY_SCRIPTS: '' - } - - if (this.options.features.meta) { - // Get vue-meta context - let head - if (typeof this.options.head === 'function') { - head = this.options.head() - } else { - head = cloneDeep(this.options.head) - } - - const m = VueMeta.generate(head || {}, this.vueMetaConfig) - - // HTML_ATTRS - meta.HTML_ATTRS = m.htmlAttrs.text() - - // HEAD_ATTRS - meta.HEAD_ATTRS = m.headAttrs.text() - - // BODY_ATTRS - meta.BODY_ATTRS = m.bodyAttrs.text() - - // HEAD tags - meta.HEAD = - m.title.text() + - m.meta.text() + - m.link.text() + - m.style.text() + - m.script.text() + - m.noscript.text() - - // Add meta if router base specified - if (this.options._routerBaseSpecified) { - meta.HEAD += `` - } - - // BODY_SCRIPTS (PREPEND) - meta.BODY_SCRIPTS_PREPEND = - m.meta.text({ pbody: true }) + - m.link.text({ pbody: true }) + - m.style.text({ pbody: true }) + - m.script.text({ pbody: true }) + - m.noscript.text({ pbody: true }) - - // BODY_SCRIPTS (APPEND) - meta.BODY_SCRIPTS = - m.meta.text({ body: true }) + - m.link.text({ body: true }) + - m.style.text({ body: true }) + - m.script.text({ body: true }) + - m.noscript.text({ body: true }) - } - - // Resources Hints - meta.resourceHints = '' - - const { resources: { modernManifest, clientManifest } } = this.serverContext - const manifest = modern ? modernManifest : clientManifest - - const { shouldPreload, shouldPrefetch } = this.options.render.bundleRenderer - - if (this.options.render.resourceHints && manifest) { - const publicPath = manifest.publicPath || '/_nuxt/' - - // Preload initial resources - if (Array.isArray(manifest.initial)) { - const { crossorigin } = this.options.render - const cors = `${crossorigin ? ` crossorigin="${crossorigin}"` : ''}` - - meta.preloadFiles = manifest.initial - .map(SPARenderer.normalizeFile) - .filter(({ fileWithoutQuery, asType }) => shouldPreload(fileWithoutQuery, asType)) - .map(file => ({ ...file, modern })) - - meta.resourceHints += meta.preloadFiles - .map(({ file, extension, asType, modern }) => { - let extra = '' - if (asType === 'font') { - extra = ` type="font/${extension}"${cors ? '' : ' crossorigin'}` - } - const rel = modern && asType === 'script' ? 'modulepreload' : 'preload' - return `` - }) - .join('') - } - - // Prefetch async resources - if (Array.isArray(manifest.async)) { - meta.resourceHints += manifest.async - .map(SPARenderer.normalizeFile) - .filter(({ fileWithoutQuery, asType }) => shouldPrefetch(fileWithoutQuery, asType)) - .map(({ file }) => ``) - .join('') - } - - // Add them to HEAD - if (meta.resourceHints) { - meta.HEAD += meta.resourceHints - } - } - - // Serialize state (runtime config) - let APP = `${meta.BODY_SCRIPTS_PREPEND}
${this.serverContext.resources.loadingHTML}
${meta.BODY_SCRIPTS}` - - APP += `` - - // Prepare template params - const templateParams = { - ...meta, - APP, - ENV: this.options.env - } - - // Call spa:templateParams hook - await this.serverContext.nuxt.callHook('vue-renderer:spa:templateParams', templateParams) - - // Render with SPA template - const html = this.renderTemplate(this.serverContext.resources.spaTemplate, templateParams) - const content = { - html, - preloadFiles: meta.preloadFiles || [] - } - - // Set meta tags inside cache - this.cache.set(cacheKey, content) - - // Return a copy of the content, so that future - // modifications do not effect the data in cache - return cloneDeep(content) - } - - static normalizeFile (file) { - const withoutQuery = file.replace(/\?.*/, '') - const extension = extname(withoutQuery).slice(1) - return { - file, - extension, - fileWithoutQuery: withoutQuery, - asType: SPARenderer.getPreloadType(extension) - } - } - - static getPreloadType (ext: string) { - if (ext === 'js') { - return 'script' - } else if (ext === 'css') { - return 'style' - } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) { - return 'image' - } else if (/woff2?|ttf|otf|eot/.test(ext)) { - return 'font' - } else { - return '' - } - } -} diff --git a/packages/nuxt3/src/vue-renderer/renderers/ssr.ts b/packages/nuxt3/src/vue-renderer/renderers/ssr.ts deleted file mode 100644 index 8da4ba688c..0000000000 --- a/packages/nuxt3/src/vue-renderer/renderers/ssr.ts +++ /dev/null @@ -1,307 +0,0 @@ -import path from 'path' -import crypto from 'crypto' -import { format } from 'util' -import fs from 'fs-extra' -import consola from 'consola' -import { TARGETS, urlJoin } from 'src/utils' -import devalue from '@nuxt/devalue' -import { createBundleRenderer } from 'vue-bundle-renderer' - -import ServerContext from 'src/server/context' -import BaseRenderer from './base' - -export default class SSRRenderer extends BaseRenderer { - vueRenderer: ReturnType - - constructor (serverContext: ServerContext) { - super(serverContext) - this.createRenderer() - } - - get rendererOptions () { - const hasModules = fs.existsSync(path.resolve(this.options.rootDir, 'node_modules')) - - return { - renderToString: require('@vue/server-renderer').renderToString, - bundleRunner: require('bundle-runner'), - clientManifest: this.serverContext.resources.clientManifest, - // for globally installed nuxt command, search dependencies in global dir - basedir: hasModules ? this.options.rootDir : __dirname, - ...this.options.render.bundleRenderer - } - } - - renderScripts (scripts) { - const { render: { crossorigin } } = this.options - if (!crossorigin) { - return scripts - } - return scripts.replace( - /` - preloadScripts.push(stateUrl) - } else { - APP += `` - } - - // Page level payload.js (async loaded for CSR) - const payloadPath = urlJoin(url, 'payload.js') - const payloadUrl = urlJoin(routerBase, staticAssetsBase, payloadPath) - const routePath = (url.replace(/\/+$/, '') || '/').split('?')[0] // remove trailing slash and query params - const payloadScript = `__NUXT_JSONP__("${routePath}", ${devalue({ data, fetch, mutations })});` - staticAssets.push({ path: payloadPath, src: payloadScript }) - preloadScripts.push(payloadUrl) - - // Preload links - for (const href of preloadScripts) { - HEAD += `` - } - } else { - // Serialize state - let serializedSession - if (shouldInjectScripts || shouldHashCspScriptSrc) { - // Only serialized session if need inject scripts or csp hash - serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.payload)};` - inlineScripts.push(serializedSession) - } - - if (shouldInjectScripts) { - APP += `` - } - } - - // Calculate CSP hashes - const cspScriptSrcHashes = [] - if (typeof csp === 'object') { - if (shouldHashCspScriptSrc) { - for (const script of inlineScripts) { - const hash = crypto.createHash(csp.hashAlgorithm) - hash.update(script) - cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`) - } - } - - // Call ssr:csp hook - await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes) - - // Add csp meta tags - if (csp.addMeta) { - HEAD += `` - } - } - - // Prepend scripts - if (shouldInjectScripts) { - APP += this.renderScripts(renderScripts()) - } - - if (meta) { - const appendInjectorOptions = { body: true } - - // Append body scripts - APP += meta.meta.text(appendInjectorOptions) - APP += meta.link.text(appendInjectorOptions) - APP += meta.style.text(appendInjectorOptions) - APP += meta.script.text(appendInjectorOptions) - APP += meta.noscript.text(appendInjectorOptions) - } - - // Template params - const templateParams = { - HTML_ATTRS: meta ? meta.htmlAttrs.text(renderContext.payload.serverRendered /* addSrrAttribute */) : '', - HEAD_ATTRS: meta ? meta.headAttrs.text() : '', - BODY_ATTRS: meta ? meta.bodyAttrs.text() : '', - HEAD, - APP, - ENV: this.options.env - } - - // Call ssr:templateParams hook - await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams, renderContext) - - // Render with SSR template - const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams) - - let preloadFiles - if (this.options.render.http2.push) { - preloadFiles = this.getPreloadFiles(renderContext) - } - - return { - html, - cspScriptSrcHashes, - preloadFiles, - error: renderContext.nuxt.error, - redirected: renderContext.redirected - } - } -} diff --git a/packages/nuxt3/src/webpack/configs/client.ts b/packages/nuxt3/src/webpack/configs/client.ts index 7ea3e50ab5..5755f36a28 100644 --- a/packages/nuxt3/src/webpack/configs/client.ts +++ b/packages/nuxt3/src/webpack/configs/client.ts @@ -87,23 +87,12 @@ function clientHTML (ctx: WebpackConfigContext) { const { options, config } = ctx // Generate output HTML for SSR - if (options.build.ssr) { - config.plugins.push( - new HTMLPlugin({ - filename: '../server/index.ssr.html', - template: options.documentPath, - minify: options.build.html.minify as any, - inject: false // Resources will be injected using bundleRenderer - }) - ) - } - config.plugins.push( new HTMLPlugin({ - filename: '../server/index.spa.html', + filename: '../server/index.ssr.html', template: options.documentPath, minify: options.build.html.minify as any, - inject: true + inject: false // Resources will be injected using bundleRenderer }) ) } diff --git a/packages/nuxt3/src/webpack/plugins/vue/client.ts b/packages/nuxt3/src/webpack/plugins/vue/client.ts index f9be444332..abf1bbad59 100644 --- a/packages/nuxt3/src/webpack/plugins/vue/client.ts +++ b/packages/nuxt3/src/webpack/plugins/vue/client.ts @@ -3,8 +3,10 @@ * https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/client.js */ +import { dirname } from 'path' import hash from 'hash-sum' import uniq from 'lodash/uniq' +import { writeFile, mkdirp } from 'fs-extra' import { Compilation } from 'webpack' import { isJS, isCSS } from './util' @@ -25,7 +27,7 @@ export default class VueSSRClientPlugin { compilation.hooks.processAssets.tapAsync({ name: 'VueSSRClientPlugin', stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL - }, (assets, cb) => { + }, async (_assets, cb) => { const stats = compilation.getStats().toJson() const allFiles = uniq(stats.assets @@ -110,10 +112,13 @@ export default class VueSSRClientPlugin { const src = JSON.stringify(manifest, null, 2) - assets[this.options.filename] = { - source: () => src, - size: () => src.length - } + await mkdirp(dirname(this.options.filename)) + await writeFile(this.options.filename, src) + // assets[this.options.filename] = { + // source: () => src, + // size: () => src.length + // } + cb() }) }) diff --git a/packages/nuxt3/src/webpack/presets/vue.ts b/packages/nuxt3/src/webpack/presets/vue.ts index e5b5640e36..06bfcc2620 100644 --- a/packages/nuxt3/src/webpack/presets/vue.ts +++ b/packages/nuxt3/src/webpack/presets/vue.ts @@ -1,3 +1,4 @@ +import { resolve } from 'path' import VueLoaderPlugin from 'vue-loader/dist/pluginWebpack5' import { DefinePlugin } from 'webpack' import VueSSRClientPlugin from '../plugins/vue/client' @@ -17,7 +18,7 @@ export function vue (ctx: WebpackConfigContext) { if (ctx.isClient) { config.plugins.push(new VueSSRClientPlugin({ - filename: `../server/${ctx.name}.manifest.json` + filename: resolve(options.buildDir, 'dist/server', `${ctx.name}.manifest.json`) })) } else { config.plugins.push(new VueSSRServerPlugin({