mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 17:35:57 +00:00
feat: use sigma (#95)
This commit is contained in:
parent
f4975dfd09
commit
0091dba181
@ -52,6 +52,8 @@ async function build (builder: Builder) {
|
||||
}
|
||||
|
||||
await bundle(builder)
|
||||
|
||||
await nuxt.callHook('build:done')
|
||||
}
|
||||
|
||||
function watch (builder: Builder) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export { default as Server } from './server'
|
||||
export { default as Listener } from './listener'
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
@ -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}`
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
build: true,
|
||||
rollup: {
|
||||
externals: ['jsdom']
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { default as VueRenderer } from './renderer'
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 ''
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user