diff --git a/packages/app/src/_templates/views/app.template.html b/packages/app/src/_templates/views/app.template.html index 1b0949de13..51d1a37494 100644 --- a/packages/app/src/_templates/views/app.template.html +++ b/packages/app/src/_templates/views/app.template.html @@ -1,11 +1,12 @@ -
- {{ HEAD }} - - - {{ APP }} - <% if (nuxt.options.vite && nuxt.options.dev) { %> - <% } %> - + + + {{ HEAD }} + + + + {{ APP }} + + diff --git a/packages/app/src/entry.ts b/packages/app/src/entry.ts index e70ea24525..78ecb9e583 100644 --- a/packages/app/src/entry.ts +++ b/packages/app/src/entry.ts @@ -33,7 +33,8 @@ if (process.client) { } entry = async function initApp () { - const app = createSSRApp(App) + const isSSR = Boolean(window.__NUXT__?.serverRendered) + const app = isSSR ? createSSRApp(App) : createApp(App) const nuxt = createNuxt({ app }) diff --git a/packages/nitro/index.d.ts b/packages/nitro/index.d.ts new file mode 100644 index 0000000000..69b9455c84 --- /dev/null +++ b/packages/nitro/index.d.ts @@ -0,0 +1,14 @@ +declare module '#build/dist/server/client.manifest.mjs' { + type ClientManifest = any // TODO: export from vue-bundle-renderer + const clientManifest: ClientManifest + export default clientManifest +} + +declare module '#build/dist/server/server.mjs' { + const _default: any + export default _default +} + +declare module '#nitro-renderer' { + export const renderToString: Function +} diff --git a/packages/nitro/package.json b/packages/nitro/package.json index b9ac34de1d..43d0a16147 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -67,7 +67,7 @@ "unstorage": "^0.2.3", "upath": "^2.0.1", "vue": "3.1.5", - "vue-bundle-renderer": "^0.2.5", + "vue-bundle-renderer": "^0.2.9", "vue-server-renderer": "^2.6.14" }, "devDependencies": { diff --git a/packages/nitro/src/compat.ts b/packages/nitro/src/compat.ts index 90e4e32264..83c4278382 100644 --- a/packages/nitro/src/compat.ts +++ b/packages/nitro/src/compat.ts @@ -50,7 +50,9 @@ export default function nuxt2CompatModule () { // Disable server sourceMap, esbuild will generate for it. nuxt.hook('webpack:config', (webpackConfigs) => { const serverConfig = webpackConfigs.find(config => config.name === 'server') - serverConfig.devtool = false + if (serverConfig) { + serverConfig.devtool = false + } }) // Nitro client plugin diff --git a/packages/nitro/src/context.ts b/packages/nitro/src/context.ts index d1da8901fc..66b58b7292 100644 --- a/packages/nitro/src/context.ts +++ b/packages/nitro/src/context.ts @@ -40,6 +40,7 @@ export interface NitroContext { _nuxt: { majorVersion: number dev: boolean + ssr: boolean rootDir: string srcDir: string buildDir: string @@ -99,6 +100,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N _nuxt: { majorVersion: nuxtOptions._majorVersion || 2, dev: nuxtOptions.dev, + ssr: nuxtOptions.ssr, rootDir: nuxtOptions.rootDir, srcDir: nuxtOptions.srcDir, buildDir: nuxtOptions.buildDir, diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index 2e75316587..96bf961de4 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -134,6 +134,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => { 'global.': 'globalThis.', 'process.server': 'true', 'process.client': 'false', + 'process.env.NUXT_NO_SSR': JSON.stringify(!nitroContext._nuxt.ssr), 'process.env.ROUTER_BASE': JSON.stringify(nitroContext._nuxt.routerBase), 'process.env.PUBLIC_PATH': JSON.stringify(nitroContext._nuxt.publicPath), 'process.env.NUXT_STATIC_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base), diff --git a/packages/nitro/src/runtime/app/render.ts b/packages/nitro/src/runtime/app/render.ts index dc0b5b0be0..277d953635 100644 --- a/packages/nitro/src/runtime/app/render.ts +++ b/packages/nitro/src/runtime/app/render.ts @@ -4,27 +4,47 @@ import { runtimeConfig } from './config' // @ts-ignore import htmlTemplate from '#build/views/document.template.mjs' -function _interopDefault (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e } - const STATIC_ASSETS_BASE = process.env.NUXT_STATIC_BASE + '/' + process.env.NUXT_STATIC_VERSION +const NUXT_NO_SSR = process.env.NUXT_NO_SSR const PAYLOAD_JS = '/payload.js' -let _renderer -async function loadRenderer () { - if (_renderer) { - return _renderer - } - // @ts-ignore +const getClientManifest = cachedImport(() => import('#build/dist/server/client.manifest.mjs')) +const getSSRApp = cachedImport(() => import('#build/dist/server/server.mjs')) + +const getSSRRenderer = cachedResult(async () => { + // Load client manifest + const clientManifest = await getClientManifest() + if (!clientManifest) { throw new Error('client.manifest is missing') } + // Load server bundle + const createSSRApp = await getSSRApp() + if (!createSSRApp) { throw new Error('Server bundle is missing') } + // Create renderer const { renderToString } = await import('#nitro-renderer') - // @ts-ignore - const createApp = await import('#build/dist/server/server.mjs') - // @ts-ignore - const clientManifest = await import('#build/dist/server/client.manifest.mjs') - _renderer = createRenderer(_interopDefault(createApp), { - clientManifest: _interopDefault(clientManifest), - renderToString + return createRenderer((createSSRApp), { clientManifest, renderToString }).renderToString +}) + +const getSPARenderer = cachedResult(async () => { + const clientManifest = await getClientManifest() + return (ssrContext) => { + ssrContext.nuxt = {} + return { + html: '', + renderResourceHints: () => '', + renderStyles: () => '', + renderScripts: () => clientManifest.initial.map((s) => { + const isMJS = !s.endsWith('.js') + return `` + }).join('') + } + } +}) + +function renderToString (ssrContext) { + const getRenderer = (NUXT_NO_SSR || ssrContext.noSSR) ? getSPARenderer : getSSRRenderer + return getRenderer().then(renderToString => renderToString(ssrContext)).catch((err) => { + console.warn('Server Side Rendering Error:', err) + return getSPARenderer().then(renderToString => renderToString(ssrContext)) }) - return _renderer } export async function renderMiddleware (req, res) { @@ -37,15 +57,18 @@ export async function renderMiddleware (req, res) { url = url.substr(STATIC_ASSETS_BASE.length, url.length - STATIC_ASSETS_BASE.length - PAYLOAD_JS.length) } + // Initialize ssr context const ssrContext = { url, req, res, runtimeConfig, + noSSR: req.spa || req.headers['x-nuxt-no-ssr'], ...(req.context || {}) } - const renderer = await loadRenderer() - const rendered = await renderer.renderToString(ssrContext) + + // Render app + const rendered = await renderToString(ssrContext) // Handle errors if (ssrContext.error) { @@ -107,3 +130,24 @@ async function renderHTML (payload, rendered, ssrContext) { function renderPayload (payload, url) { return `__NUXT_JSONP__("${url}", ${devalue(payload)})` } + +function _interopDefault (e) { + return e && typeof e === 'object' && 'default' in e ? e.default : e +} + +function cachedImport