diff --git a/packages/server/src/middleware/modern.js b/packages/server/src/middleware/modern.js
index ddc4a32cf0..e087ebd81d 100644
--- a/packages/server/src/middleware/modern.js
+++ b/packages/server/src/middleware/modern.js
@@ -1,5 +1,3 @@
-import chalk from 'chalk'
-import consola from 'consola'
import UAParser from 'ua-parser-js'
import semver from 'semver'
@@ -23,22 +21,6 @@ const isModernBrowser = (ua) => {
return Boolean(modernBrowsers[browser.name] && semver.gte(browserVersion, modernBrowsers[browser.name]))
}
-const distinctModernModeOptions = [false, 'client', 'server']
-
-const detectModernBuild = ({ options, resources }) => {
- if (distinctModernModeOptions.includes(options.modern)) {
- return
- }
-
- if (!resources.modernManifest) {
- options.modern = false
- return
- }
-
- options.modern = options.render.ssr ? 'server' : 'client'
- consola.info(`Modern bundles are detected. Modern mode (${chalk.green.bold(options.modern)}) is enabled now.`)
-}
-
const detectModernBrowser = ({ socket = {}, headers }) => {
if (socket.isModernBrowser === undefined) {
const ua = headers && headers['user-agent']
@@ -48,15 +30,10 @@ const detectModernBrowser = ({ socket = {}, headers }) => {
return socket.isModernBrowser
}
-export default ({ context }) => {
- let detected = false
+export default ({ serverContext }) => {
return (req, res, next) => {
- if (!detected) {
- detectModernBuild(context)
- detected = true
- }
- if (context.options.modern !== false) {
- req.modernMode = detectModernBrowser(req)
+ if (serverContext.options.modern !== false) {
+ req._modern = detectModernBrowser(req)
}
next()
}
diff --git a/packages/server/src/middleware/nuxt.js b/packages/server/src/middleware/nuxt.js
index 8c3dbfc365..501366af1a 100644
--- a/packages/server/src/middleware/nuxt.js
+++ b/packages/server/src/middleware/nuxt.js
@@ -25,7 +25,7 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
cspScriptSrcHashes,
error,
redirected,
- getPreloadFiles
+ preloadFiles
} = result
if (redirected) {
@@ -52,8 +52,6 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
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 preloadFiles = getPreloadFiles()
-
const { shouldPush, pushAssets } = options.render.http2
const { publicPath } = resources.clientManifest
diff --git a/packages/server/src/server.js b/packages/server/src/server.js
index 483aa091fc..3fb4f20af3 100644
--- a/packages/server/src/server.js
+++ b/packages/server/src/server.js
@@ -56,8 +56,8 @@ export default class Server {
// Initialize vue-renderer
const { VueRenderer } = await import('@nuxt/vue-renderer')
- const context = new ServerContext(this)
- this.renderer = new VueRenderer(context)
+ this.serverContext = new ServerContext(this)
+ this.renderer = new VueRenderer(this.serverContext)
await this.renderer.ready()
// Setup nuxt middleware
@@ -112,7 +112,7 @@ export default class Server {
}
this.useMiddleware(createModernMiddleware({
- context: this.renderer.context
+ serverContext: this.serverContext
}))
// Dev middleware
diff --git a/packages/server/test/middleware/modern.test.js b/packages/server/test/middleware/modern.test.js
index ecde8fd619..7d55df1b5a 100644
--- a/packages/server/test/middleware/modern.test.js
+++ b/packages/server/test/middleware/modern.test.js
@@ -1,19 +1,17 @@
-import consola from 'consola'
-
jest.mock('chalk', () => ({
green: {
bold: modern => `greenBold(${modern})`
}
}))
-const createContext = () => ({
+const createServerContext = () => ({
resources: {},
options: {
render: {}
}
})
-const createServerContext = () => ({
+const createRenderContext = () => ({
req: { headers: {} },
next: jest.fn()
})
@@ -34,107 +32,74 @@ describe('server: modernMiddleware', () => {
})
test('should not detect modern build if modern mode is specified', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
+ const serverContext = createServerContext()
+ const modernMiddleware = createModernMiddleware({ serverContext })
+ const ctx = createRenderContext()
- context.options.modern = false
+ serverContext.options.modern = false
modernMiddleware(ctx.req, ctx.res, ctx.next)
- context.options.modern = 'client'
+ serverContext.options.modern = 'client'
modernMiddleware(ctx.req, ctx.res, ctx.next)
- context.options.modern = 'server'
+ serverContext.options.modern = 'server'
modernMiddleware(ctx.req, ctx.res, ctx.next)
- expect(ctx.req.modernMode).toEqual(false)
- })
-
- test('should detect client modern build and display message', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
-
- context.resources.modernManifest = {}
- modernMiddleware(ctx.req, ctx.res, ctx.next)
- expect(context.options.modern).toEqual('client')
- expect(consola.info).toBeCalledWith('Modern bundles are detected. Modern mode (greenBold(client)) is enabled now.')
- })
-
- test('should detect server modern build and display message', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
-
- context.options.render.ssr = true
- context.resources.modernManifest = {}
- modernMiddleware(ctx.req, ctx.res, ctx.next)
- expect(context.options.modern).toEqual('server')
- expect(consola.info).toBeCalledWith('Modern bundles are detected. Modern mode (greenBold(server)) is enabled now.')
- })
-
- test('should not detect modern browser if modern build is not found', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
-
- modernMiddleware(ctx.req, ctx.res, ctx.next)
-
- expect(ctx.req.modernMode).toBeUndefined()
+ expect(ctx.req._modern).toEqual(false)
})
test('should not detect modern browser if connect has been detected', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
+ const serverContext = createServerContext()
+ const modernMiddleware = createModernMiddleware({ serverContext })
+ const ctx = createRenderContext()
ctx.req.socket = { isModernBrowser: true }
- context.options.dev = true
- context.options.modern = 'server'
+ serverContext.options.dev = true
+ serverContext.options.modern = 'server'
modernMiddleware(ctx.req, ctx.res, ctx.next)
- expect(ctx.req.modernMode).toEqual(true)
+ expect(ctx.req._modern).toEqual(true)
})
test('should detect modern browser based on user-agent', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
+ const serverContext = createServerContext()
+ const modernMiddleware = createModernMiddleware({ serverContext })
+ const ctx = createRenderContext()
const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
ctx.req.headers['user-agent'] = ua
ctx.req.socket = {}
- context.options.dev = true
- context.options.modern = 'server'
+ serverContext.options.dev = true
+ serverContext.options.modern = 'server'
modernMiddleware(ctx.req, ctx.res, ctx.next)
expect(ctx.req.socket.isModernBrowser).toEqual(true)
- expect(ctx.req.modernMode).toEqual(true)
+ expect(ctx.req._modern).toEqual(true)
})
test('should detect legacy browser based on user-agent', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
+ const serverContext = createServerContext()
+ const modernMiddleware = createModernMiddleware({ serverContext })
+ const ctx = createRenderContext()
const ua = 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))'
ctx.req.headers['user-agent'] = ua
ctx.req.socket = {}
- context.options.dev = true
- context.options.modern = 'client'
+ serverContext.options.dev = true
+ serverContext.options.modern = 'client'
modernMiddleware(ctx.req, ctx.res, ctx.next)
expect(ctx.req.socket.isModernBrowser).toEqual(false)
})
test('should ignore illegal user-agent', () => {
- const context = createContext()
- const modernMiddleware = createModernMiddleware({ context })
- const ctx = createServerContext()
+ const serverContext = createServerContext()
+ const modernMiddleware = createModernMiddleware({ serverContext })
+ const ctx = createRenderContext()
const ua = 'illegal user agent'
ctx.req.headers['user-agent'] = ua
ctx.req.socket = {}
- context.options.dev = true
- context.options.modern = 'client'
+ serverContext.options.dev = true
+ serverContext.options.modern = 'client'
modernMiddleware(ctx.req, ctx.res, ctx.next)
expect(ctx.req.socket.isModernBrowser).toEqual(false)
diff --git a/packages/server/test/middleware/nuxt.test.js b/packages/server/test/middleware/nuxt.test.js
index 9bf104b0ac..74d6d0ccf9 100644
--- a/packages/server/test/middleware/nuxt.test.js
+++ b/packages/server/test/middleware/nuxt.test.js
@@ -137,7 +137,7 @@ describe('server: nuxtMiddleware', () => {
const context = createContext()
const result = {
html: 'rendered html',
- getPreloadFiles: jest.fn(() => ['/nuxt/preload1.js', '/nuxt/preload2.js'])
+ preloadFiles: ['/nuxt/preload1.js', '/nuxt/preload2.js']
}
context.renderRoute.mockReturnValue(result)
const pushAssets = jest.fn((req, res, publicPath, preloadFiles) => preloadFiles)
@@ -158,12 +158,12 @@ describe('server: nuxtMiddleware', () => {
const context = createContext()
const result = {
html: 'rendered html',
- getPreloadFiles: jest.fn(() => [
+ preloadFiles: [
{ file: '/nuxt/preload1.js', asType: 'script' },
{ file: '/nuxt/preload2.js', asType: 'script' },
{ file: '/nuxt/style.css', asType: 'style' },
{ file: '/nuxt/font.woff', asType: 'font' }
- ])
+ ]
}
context.renderRoute.mockReturnValue(result)
context.options.render.http2 = { push: true }
@@ -182,12 +182,12 @@ describe('server: nuxtMiddleware', () => {
const context = createContext()
const result = {
html: 'rendered html',
- getPreloadFiles: jest.fn(() => [
+ preloadFiles: [
{ file: '/nuxt/preload1.js', asType: 'script' },
{ file: '/nuxt/preload2.js', asType: 'script', modern: true },
{ file: '/nuxt/style.css', asType: 'style' },
{ file: '/nuxt/font.woff', asType: 'font' }
- ])
+ ]
}
context.renderRoute.mockReturnValue(result)
context.options.dev = true
diff --git a/packages/server/test/server.test.js b/packages/server/test/server.test.js
index ae0727c4bc..82b3dc6e98 100644
--- a/packages/server/test/server.test.js
+++ b/packages/server/test/server.test.js
@@ -170,9 +170,7 @@ describe('server: server', () => {
const nuxt = createNuxt()
const server = new Server(nuxt)
server.useMiddleware = jest.fn()
- server.renderer = {
- context: { id: 'test-server-context' }
- }
+ server.serverContext = { id: 'test-server-context' }
await server.setupMiddleware()
@@ -198,8 +196,9 @@ describe('server: server', () => {
})
const modernMiddleware = {
- context: server.renderer.context
+ serverContext: server.serverContext
}
+
expect(createModernMiddleware).toBeCalledTimes(1)
expect(createModernMiddleware).toBeCalledWith(modernMiddleware)
expect(server.useMiddleware).nthCalledWith(3, {
diff --git a/packages/vue-renderer/src/renderer.js b/packages/vue-renderer/src/renderer.js
index 898c5a7f2d..4b8c3577fe 100644
--- a/packages/vue-renderer/src/renderer.js
+++ b/packages/vue-renderer/src/renderer.js
@@ -1,21 +1,17 @@
import path from 'path'
-import crypto from 'crypto'
import fs from 'fs-extra'
+import chalk from 'chalk'
import consola from 'consola'
-import devalue from '@nuxt/devalue'
-import invert from 'lodash/invert'
import template from 'lodash/template'
-import { isUrl, urlJoin } from '@nuxt/utils'
-import { createBundleRenderer } from 'vue-server-renderer'
-import SPAMetaRenderer from './spa-meta'
+import SPARenderer from './renderers/spa'
+import SSRRenderer from './renderers/ssr'
+import ModernRenderer from './renderers/modern'
export default class VueRenderer {
constructor(context) {
- this.context = context
-
- const { build: { publicPath }, router: { base } } = this.context.options
- this.publicPath = isUrl(publicPath) ? publicPath : urlJoin(base, publicPath)
+ this.serverContext = context
+ this.options = this.serverContext.options
// Will be set by createRenderer
this.renderer = {
@@ -25,7 +21,7 @@ export default class VueRenderer {
}
// Renderer runtime resources
- Object.assign(this.context.resources, {
+ Object.assign(this.serverContext.resources, {
clientManifest: undefined,
modernManifest: undefined,
serverManifest: undefined,
@@ -39,91 +35,6 @@ export default class VueRenderer {
this._error = null
}
- get assetsMapping() {
- if (this._assetsMapping) {
- return this._assetsMapping
- }
-
- const legacyAssets = this.context.resources.clientManifest.assetsMapping
- const modernAssets = invert(this.context.resources.modernManifest.assetsMapping)
- const mapping = {}
-
- for (const legacyJsFile in legacyAssets) {
- const chunkNamesHash = legacyAssets[legacyJsFile]
- mapping[legacyJsFile] = modernAssets[chunkNamesHash]
- }
- delete this.context.resources.clientManifest.assetsMapping
- delete this.context.resources.modernManifest.assetsMapping
- this._assetsMapping = mapping
-
- return mapping
- }
-
- renderScripts(context) {
- if (this.context.options.modern === 'client') {
- const scriptPattern = /`
-
- // Calculate CSP hashes
- const { csp } = this.context.options.render
- const cspScriptSrcHashes = []
- if (csp) {
- // Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
- const containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes(`'unsafe-inline'`)
- if (!containsUnsafeInlineScriptSrc) {
- const hash = crypto.createHash(csp.hashAlgorithm)
- hash.update(serializedSession)
- cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`)
- }
-
- // Call ssr:csp hook
- await this.context.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes)
-
- // Add csp meta tags
- if (csp.addMeta) {
- HEAD += ``
+ if (this.options.modern !== false) {
+ this.renderer.modern = new ModernRenderer(this.serverContext)
}
}
-
- // Prepend scripts
- APP += this.renderScripts(context)
- APP += m.script.text({ body: true })
- APP += m.noscript.text({ body: true })
-
- // Template params
- const templateParams = {
- HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
- HEAD_ATTRS: m.headAttrs.text(),
- BODY_ATTRS: m.bodyAttrs.text(),
- HEAD,
- APP,
- ENV: this.context.options.env
- }
-
- // Call ssr:templateParams hook
- await this.context.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams)
-
- // Render with SSR template
- const html = this.renderTemplate(true, templateParams)
-
- return {
- html,
- cspScriptSrcHashes,
- getPreloadFiles: this.getSsrPreloadFiles.bind(this, context),
- error: context.nuxt.error,
- redirected: context.redirected
- }
}
- async renderRoute(url, context = {}, _retried) {
+ 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 = {}, _retried) {
/* istanbul ignore if */
if (!this.isReady) {
// Production
- if (!this.context.options.dev) {
+ if (!this.options.dev) {
if (!_retried && ['loading', 'created'].includes(this._state)) {
await this.ready()
- return this.renderRoute(url, context, true)
+ return this.renderRoute(url, renderContext, true)
}
switch (this._state) {
case 'created':
@@ -486,29 +267,29 @@ export default class VueRenderer {
// Log rendered url
consola.debug(`Rendering url ${url}`)
- // Add url to the context
- context.url = url
+ // Add url to the renderContext
+ renderContext.url = url
- const { req = {} } = context
+ const { req = {} } = renderContext
- // context.spa
- if (context.spa === undefined) {
- // TODO: Remove reading from context.res in Nuxt3
- context.spa = !this.SSR || req.spa || (context.res && context.res.spa)
+ // renderContext.spa
+ if (renderContext.spa === undefined) {
+ // TODO: Remove reading from renderContext.res in Nuxt3
+ renderContext.spa = !this.SSR || req.spa || (renderContext.res && renderContext.res.spa)
}
- // context.modern
- if (context.modern === undefined) {
- context.modern = req.modernMode && this.context.options.modern === 'server'
+ // renderContext.modern
+ if (renderContext.modern === undefined) {
+ renderContext.modern = req._modern || this.options.modern === 'client'
}
- // Call context hook
- await this.context.nuxt.callHook('vue-renderer:context', context)
+ // Call renderContext hook
+ await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext)
// Render SPA or SSR
- return context.spa
- ? this.renderSPA(context)
- : this.renderSSR(context)
+ return renderContext.spa
+ ? this.renderSPA(renderContext)
+ : this.renderSSR(renderContext)
}
get resourceMap() {
diff --git a/packages/vue-renderer/src/renderers/base.js b/packages/vue-renderer/src/renderers/base.js
new file mode 100644
index 0000000000..c8f4e039d0
--- /dev/null
+++ b/packages/vue-renderer/src/renderers/base.js
@@ -0,0 +1,25 @@
+export default class BaseRenderer {
+ constructor(serverContext) {
+ this.serverContext = serverContext
+ this.options = serverContext.options
+
+ this.vueRenderer = this.createRenderer()
+ }
+
+ createRenderer() {
+ throw new Error('`createRenderer()` needs to be implemented')
+ }
+
+ renderTemplate(templateFn, opts) {
+ // 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() {
+ throw new Error('`render()` needs to be implemented')
+ }
+}
diff --git a/packages/vue-renderer/src/renderers/modern.js b/packages/vue-renderer/src/renderers/modern.js
new file mode 100644
index 0000000000..d4938b4852
--- /dev/null
+++ b/packages/vue-renderer/src/renderers/modern.js
@@ -0,0 +1,112 @@
+import invert from 'lodash/invert'
+import { isUrl, urlJoin } from '@nuxt/utils'
+import SSRRenderer from './ssr'
+
+export default class ModernRenderer extends SSRRenderer {
+ constructor(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 = invert(modernManifest.assetsMapping)
+ const mapping = {}
+
+ for (const legacyJsFile in legacyAssets) {
+ const chunkNamesHash = legacyAssets[legacyJsFile]
+ mapping[legacyJsFile] = modernAssets[chunkNamesHash]
+ }
+ 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 = /`
+
+ // Calculate CSP hashes
+ const { csp } = this.options.render
+ const cspScriptSrcHashes = []
+ if (csp) {
+ // Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
+ const containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes(`'unsafe-inline'`)
+ if (!containsUnsafeInlineScriptSrc) {
+ const hash = crypto.createHash(csp.hashAlgorithm)
+ hash.update(serializedSession)
+ cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`)
+ }
+
+ // Call ssr:csp hook
+ await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes)
+
+ // Add csp meta tags
+ if (csp.addMeta) {
+ HEAD += ``
+ }
+ }
+
+ // Prepend scripts
+ APP += this.renderScripts(renderContext)
+ APP += m.script.text({ body: true })
+ APP += m.noscript.text({ body: true })
+
+ // Template params
+ const templateParams = {
+ HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
+ HEAD_ATTRS: m.headAttrs.text(),
+ BODY_ATTRS: m.bodyAttrs.text(),
+ HEAD,
+ APP,
+ ENV: this.options.env
+ }
+
+ // Call ssr:templateParams hook
+ await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams)
+
+ // Render with SSR template
+ const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams)
+
+ let preloadFiles
+ if (this.options.render.http2.push) {
+ preloadFiles = this.getPreloadFiles(renderContext)
+ }
+
+ return {
+ html,
+ cspScriptSrcHashes,
+ preloadFiles,
+ error: renderContext.nuxt.error,
+ redirected: renderContext.redirected
+ }
+ }
+}
diff --git a/packages/webpack/src/builder.js b/packages/webpack/src/builder.js
index c0fe63e476..b04e6c4653 100644
--- a/packages/webpack/src/builder.js
+++ b/packages/webpack/src/builder.js
@@ -204,7 +204,7 @@ export class WebpackBundler {
}
async middleware(req, res, next) {
- const name = req.modernMode ? 'modern' : 'client'
+ const name = req._modern ? 'modern' : 'client'
if (this.devMiddleware && this.devMiddleware[name]) {
await this.devMiddleware[name](req, res)