diff --git a/src/compat.ts b/src/compat.ts index 90e4e32264..83c4278382 100644 --- a/src/compat.ts +++ b/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/src/context.ts b/src/context.ts index d1da8901fc..66b58b7292 100644 --- a/src/context.ts +++ b/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/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000000..69b9455c84 --- /dev/null +++ b/src/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/src/package.json b/src/package.json index b9ac34de1d..43d0a16147 100644 --- a/src/package.json +++ b/src/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/src/rollup/config.ts b/src/rollup/config.ts index 2e75316587..96bf961de4 100644 --- a/src/rollup/config.ts +++ b/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/src/runtime/app/render.ts b/src/runtime/app/render.ts index dc0b5b0be0..277d953635 100644 --- a/src/runtime/app/render.ts +++ b/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 (importer: () => Promise) { + return cachedResult(() => importer().then(_interopDefault).catch((err) => { + if (err.code === 'ERR_MODULE_NOT_FOUND') { return null } + throw err + })) +} + +function cachedResult (fn: () => Promise): () => Promise { + let res = null + return () => { + if (res === null) { + res = fn().catch((err) => { res = null; throw err }) + } + return res + } +} diff --git a/src/server/dev.ts b/src/server/dev.ts index 2b856a54eb..ab047a8340 100644 --- a/src/server/dev.ts +++ b/src/server/dev.ts @@ -70,6 +70,11 @@ export function createDevServer (nitroContext: NitroContext) { const proxy = createProxy() app.use((req, res) => { if (workerAddress) { + // Workaround to pass legacy req.spa to proxy + // @ts-ignore + if (req.spa) { + req.headers['x-nuxt-no-ssr'] = 'true' + } proxy.web(req, res, { target: workerAddress }, (_err: unknown) => { // console.error('[proxy]', err) })