feat: use sigma (#95)

This commit is contained in:
pooya parsa 2021-01-18 12:49:50 +01:00 committed by GitHub
parent f4975dfd09
commit 0091dba181
24 changed files with 26 additions and 2634 deletions

View File

@ -52,6 +52,8 @@ async function build (builder: Builder) {
}
await bundle(builder)
await nuxt.callHook('build:done')
}
function watch (builder: Builder) {

View File

@ -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<any>) {
if (lockRelease) {
if (this._lockRelease) {

View File

@ -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
}

View File

@ -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<any>) {
await this.callHook('close', this)

View File

@ -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<string, any>>
generatedRoutes: Set<string>
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)
}
}

View File

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

View File

@ -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
}
}

View File

@ -1,2 +0,0 @@
export { default as Server } from './server'
export { default as Listener } from './listener'

View File

@ -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<Options> = {}
) {
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<DOMWindow>((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
}

View File

@ -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
}
}

View File

@ -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: '<empty>' }
}
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')
}
}

View File

@ -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 }
}

View File

@ -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<Timer['end']>) {
const desc = time.description ? `;desc="${time.description}"` : ''
return `${time.name};dur=${time.duration}${desc}`
}
}

View File

@ -1,6 +0,0 @@
export default {
build: true,
rollup: {
externals: ['jsdom']
}
}

View File

@ -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<string, string[]>
publicPath: string
initial: Array<string>
async: Array<string>
}
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<typeof import('compression')>('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]
}
}
}

View File

@ -1 +0,0 @@
export { default as VueRenderer } from './renderer'

View File

@ -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<boolean>;
}
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<any>
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]
}
}
}

View File

@ -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<string, any>) => void, opts: Record<string, any>) {
// 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')
}
}

View File

@ -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<string, string>
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<string, string> = {}
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 = /<script[^>]*?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('<script', '<script nomodule')
}
const moduleTag = scriptTag
.replace('<script', '<script type="module"')
.replace(legacyJsFile, modernJsFile)
const noModuleTag = scriptTag.replace('<script', '<script nomodule')
return noModuleTag + moduleTag
})
const safariNoModuleFixScript = `<script>${safariNoModuleFix}</script>`
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 = /<link[^>]*?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)
}
}

View File

@ -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<unknown, unknown>
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<string, any> = 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 <base href=""> meta if router base specified
if (this.options._routerBaseSpecified) {
meta.HEAD += `<base href="${this.options.router.base}">`
}
// 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 `<link rel="${rel}"${cors} href="${publicPath}${file}"${
asType !== '' ? ` as="${asType}"` : ''}${extra}>`
})
.join('')
}
// Prefetch async resources
if (Array.isArray(manifest.async)) {
meta.resourceHints += manifest.async
.map(SPARenderer.normalizeFile)
.filter(({ fileWithoutQuery, asType }) => shouldPrefetch(fileWithoutQuery, asType))
.map(({ file }) => `<link rel="prefetch" href="${publicPath}${file}">`)
.join('')
}
// Add them to HEAD
if (meta.resourceHints) {
meta.HEAD += meta.resourceHints
}
}
// Serialize state (runtime config)
let APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`
APP += `<script>window.${this.serverContext.globals.context}=${devalue({
config: renderContext.runtimeConfig.public,
staticAssetsBase: renderContext.staticAssetsBase
})}</script>`
// 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 ''
}
}
}

View File

@ -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<typeof createBundleRenderer>
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(
/<script/g,
`<script crossorigin="${crossorigin}"`
)
}
getPreloadFiles (renderContext) {
return renderContext.getPreloadFiles()
}
renderResourceHints (resourceHints) {
const { render: { crossorigin } } = this.options
if (!crossorigin) {
return resourceHints
}
return resourceHints.replace(
/rel="preload"/g,
`rel="preload" crossorigin="${crossorigin}"`
)
}
createRenderer () {
// Create bundle renderer for SSR
this.vueRenderer = createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}
useSSRLog () {
if (!this.options.render.ssrLog) {
return
}
const logs = []
const devReporter = {
log (logObj) {
logs.push({
...logObj,
args: logObj.args.map(arg => format(arg))
})
}
}
consola.addReporter(devReporter)
return () => {
consola.removeReporter(devReporter)
return logs
}
}
async render (renderContext) {
// Call ssr:context hook to extend context from modules
await this.serverContext.nuxt.callHook('vue-renderer:ssr:prepareContext', renderContext)
const getSSRLog = this.useSSRLog()
// Call Vue renderer renderToString
const { html: _html, renderResourceHints, renderScripts, renderStyles } = await this.vueRenderer.renderToString(renderContext)
let APP = _html
// Wrap with Nuxt id
APP = `<div id="${this.serverContext.globals.id}">${APP}</div>`
// Call render:done in app
await renderContext.nuxt.hooks.callHook('vue-renderer:done')
if (typeof getSSRLog === 'function') {
renderContext.payload.logs = getSSRLog()
}
// Call ssr:context hook
await this.serverContext.nuxt.callHook('vue-renderer:ssr:context', renderContext)
// TODO: Remove in next major release (#4722)
await this.serverContext.nuxt.callHook('_render:context', renderContext.nuxt)
// Fallback to empty response
if (!renderContext.payload.serverRendered) {
APP = `<div id="${this.serverContext.globals.id}"></div>`
}
// Perf: early returns if server target and redirected
if (renderContext.redirected && renderContext.target === TARGETS.server) {
return {
html: APP,
error: renderContext.payload.error,
redirected: renderContext.redirected
}
}
let HEAD = ''
// Inject head meta
// (this is unset when features.meta is false in server template)
const meta = renderContext.meta && renderContext.meta.inject({
isSSR: renderContext.payload.serverRendered,
ln: this.options.dev
})
if (meta) {
HEAD += meta.title.text() + meta.meta.text()
}
// Add <base href=""> meta if router base specified
if (this.options._routerBaseSpecified) {
HEAD += `<base href="${this.options.router.base}">`
}
if (meta) {
HEAD += meta.link.text() +
meta.style.text() +
meta.script.text() +
meta.noscript.text()
}
// Check if we need to inject scripts and state
const shouldInjectScripts = this.options.render.injectScripts !== false
// Inject resource hints
if (this.options.render.resourceHints && shouldInjectScripts) {
HEAD += this.renderResourceHints(renderResourceHints())
}
// Inject styles
HEAD += renderStyles()
if (meta) {
const prependInjectorOptions = { pbody: true }
const BODY_PREPEND =
meta.meta.text(prependInjectorOptions) +
meta.link.text(prependInjectorOptions) +
meta.style.text(prependInjectorOptions) +
meta.script.text(prependInjectorOptions) +
meta.noscript.text(prependInjectorOptions)
if (BODY_PREPEND) {
APP = `${BODY_PREPEND}${APP}`
}
}
const { csp } = this.options.render
let shouldHashCspScriptSrc = false
if (typeof csp === 'object') {
const { policies, unsafeInlineCompatibility } = csp
shouldHashCspScriptSrc = unsafeInlineCompatibility ||
// Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
!(policies && policies['script-src'] && policies['script-src'].includes('\'unsafe-inline\''))
}
const inlineScripts = []
if (renderContext.staticAssetsBase) {
const preloadScripts = []
renderContext.staticAssets = []
const routerBase = this.options.router.base
const { staticAssetsBase, url, nuxt, staticAssets } = renderContext
const { data, fetch, mutations, ...state } = nuxt
// Initial state
const stateScript = `window.${this.serverContext.globals.context}=${devalue({
staticAssetsBase,
...state
})};`
// Make chunk for initial state > 10 KB
const stateScriptKb = (stateScript.length * 4 /* utf8 */) / 100
if (stateScriptKb > 10) {
const statePath = urlJoin(url, 'state.js')
const stateUrl = urlJoin(routerBase, staticAssetsBase, statePath)
staticAssets.push({ path: statePath, src: stateScript })
APP += `<script defer src="${stateUrl}"></script>`
preloadScripts.push(stateUrl)
} else {
APP += `<script>${stateScript}</script>`
}
// 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 += `<link rel="preload" href="${href}" as="script">`
}
} 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 += `<script>${serializedSession}</script>`
}
}
// 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 += `<meta http-equiv="Content-Security-Policy" content="script-src ${cspScriptSrcHashes.join()}">`
}
}
// 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
}
}
}

View File

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

View File

@ -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()
})
})

View File

@ -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({