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 (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/packages/nitro/src/server/dev.ts b/packages/nitro/src/server/dev.ts index 2b856a54eb..ab047a8340 100644 --- a/packages/nitro/src/server/dev.ts +++ b/packages/nitro/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) }) diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index ed8c035d14..d3024fd8c7 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -1,4 +1,6 @@ import * as vite from 'vite' +import { resolve } from 'upath' +import { mkdirp, writeFile } from 'fs-extra' import vitePlugin from '@vitejs/plugin-vue' import { cacheDirPlugin } from './plugins/cache-dir' import { replace } from './plugins/replace' @@ -30,15 +32,25 @@ export async function buildClient (ctx: ViteBuildContext) { await ctx.nuxt.callHook('vite:extendConfig', clientConfig, { isClient: true, isServer: false }) + const clientManifest = { + publicPath: ctx.nuxt.options.build.publicPath, + all: [], + initial: [ctx.nuxt.options.dev && '@vite/client', 'entry.mjs'].filter(Boolean), + async: [], + modules: {} + } + + const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server') + await mkdirp(serverDist) + await writeFile(resolve(serverDist, 'client.manifest.json'), JSON.stringify(clientManifest, null, 2), 'utf8') + await writeFile(resolve(serverDist, 'client.manifest.mjs'), 'export default ' + JSON.stringify(clientManifest, null, 2), 'utf8') + const viteServer = await vite.createServer(clientConfig) await ctx.nuxt.callHook('vite:serverCreated', viteServer) const viteMiddleware = (req, res, next) => { // Workaround: vite devmiddleware modifies req.url const originalURL = req.url - if (req.url === '/_nuxt/client.js') { - return res.end('') - } viteServer.middlewares.handle(req, res, (err) => { req.url = originalURL next(err) diff --git a/packages/vite/src/server.ts b/packages/vite/src/server.ts index 7a4d12476f..fee11919e1 100644 --- a/packages/vite/src/server.ts +++ b/packages/vite/src/server.ts @@ -1,7 +1,6 @@ import { resolve } from 'upath' import * as vite from 'vite' import vuePlugin from '@vitejs/plugin-vue' -import { mkdirp, writeFile } from 'fs-extra' import consola from 'consola' import { ViteBuildContext, ViteOptions } from './vite' import { wpfs } from './utils/wpfs' @@ -52,12 +51,6 @@ export async function buildServer (ctx: ViteBuildContext) { await ctx.nuxt.callHook('vite:extendConfig', serverConfig, { isClient: false, isServer: true }) - const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server') - await mkdirp(serverDist) - - await writeFile(resolve(serverDist, 'client.manifest.json'), 'false', 'utf8') - await writeFile(resolve(serverDist, 'client.manifest.mjs'), 'export default false', 'utf8') - const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs) if (!ctx.nuxt.options.ssr) { diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 37a9ad512c..4a5c9ee136 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -37,8 +37,9 @@ export async function bundle (nuxt: Nuxt) { ...nuxt.options.alias, '#app': nuxt.options.appDir, '#build': nuxt.options.buildDir, - '/__app': nuxt.options.appDir, - '/__build': nuxt.options.buildDir, + '/build': nuxt.options.buildDir, + '/app': nuxt.options.appDir, + '/entry.mjs': resolve(nuxt.options.appDir, 'entry'), '~': nuxt.options.srcDir, '@': nuxt.options.srcDir, 'web-streams-polyfill/ponyfill/es2018': 'unenv/runtime/mock/empty', @@ -46,6 +47,7 @@ export async function bundle (nuxt: Nuxt) { 'abort-controller': 'unenv/runtime/mock/empty' } }, + base: nuxt.options.build.publicPath, vue: {}, css: {}, optimizeDeps: { @@ -67,7 +69,7 @@ export async function bundle (nuxt: Nuxt) { ], server: { fs: { - strict: true, + strict: false, allow: [ nuxt.options.buildDir, nuxt.options.appDir, @@ -85,11 +87,13 @@ export async function bundle (nuxt: Nuxt) { nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => { const start = Date.now() - warmupViteServer(server, ['/__app/entry']).then(() => { + warmupViteServer(server, ['/app/entry.mjs']).then(() => { consola.info(`Vite warmed up in ${Date.now() - start}ms`) }).catch(consola.error) }) await buildClient(ctx) - await buildServer(ctx) + if (ctx.nuxt.options.ssr) { + await buildServer(ctx) + } } diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 1377cebdce..3bda442908 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -85,9 +85,9 @@ class WebpackBundler { this.getWebpackConfig('client') ] - // if (options.build.ssr) { - webpackConfigs.push(this.getWebpackConfig('server')) - // } + if (options.ssr) { + webpackConfigs.push(this.getWebpackConfig('server')) + } await this.nuxt.callHook('webpack:config', webpackConfigs) diff --git a/test/fixtures/compat/nuxt.config.ts b/test/fixtures/compat/nuxt.config.ts index 30230171cd..ec09b10159 100644 --- a/test/fixtures/compat/nuxt.config.ts +++ b/test/fixtures/compat/nuxt.config.ts @@ -7,6 +7,14 @@ export default defineNuxtConfig({ buildModules: [ '@nuxt/nitro/compat' ], + serverMiddleware: [ + { + handle (req, _res, next) { + req.spa = req.url.includes('?spa') + next() + } + } + ], buildDir: process.env.NITRO_BUILD_DIR, nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } diff --git a/yarn.lock b/yarn.lock index 2b9c971530..1573d8e9bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1549,7 +1549,7 @@ __metadata: 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 languageName: unknown linkType: soft @@ -12056,12 +12056,12 @@ fsevents@~2.3.2: languageName: node linkType: hard -"vue-bundle-renderer@npm:^0.2.5": - version: 0.2.5 - resolution: "vue-bundle-renderer@npm:0.2.5" +"vue-bundle-renderer@npm:^0.2.9": + version: 0.2.9 + resolution: "vue-bundle-renderer@npm:0.2.9" dependencies: bundle-runner: ^0.0.1 - checksum: 9848b493aec6dda72296cf885e3f270610e8481bc1773035f4827915d84f560c49c9e819a2c8ecbc89b70499ec188453772720ad9d58977aa92585878f028adf + checksum: 82f1d06f7e839016707159f211c1feca4ca7ee123864545d8c2f81078ba80c84f3199bf52f66e4117bc4355c4ab91e704476cedf5f29979a5872639d7e3ae8e6 languageName: node linkType: hard