feat: support ssr: false (#351)

Co-Authored-By: Daniel Roe <daniel@roe.dev>
This commit is contained in:
pooya parsa 2021-07-21 22:05:22 +02:00 committed by GitHub
parent 9c7085da58
commit be255772b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 139 additions and 52 deletions

View File

@ -1,11 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html {{ HTML_ATTRS }}> <html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }} {{ HEAD }}
</head> </head>
<body {{ BODY_ATTRS }}>
<body {{ BODY_ATTRS }}>
{{ APP }} {{ APP }}
<% if (nuxt.options.vite && nuxt.options.dev) { %><script type="module" src="/@vite/client"></script> </body>
<script type="module" src="/__app/entry"></script><% } %>
</body>
</html> </html>

View File

@ -33,7 +33,8 @@ if (process.client) {
} }
entry = async function initApp () { 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 }) const nuxt = createNuxt({ app })

14
packages/nitro/index.d.ts vendored Normal file
View File

@ -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
}

View File

@ -67,7 +67,7 @@
"unstorage": "^0.2.3", "unstorage": "^0.2.3",
"upath": "^2.0.1", "upath": "^2.0.1",
"vue": "3.1.5", "vue": "3.1.5",
"vue-bundle-renderer": "^0.2.5", "vue-bundle-renderer": "^0.2.9",
"vue-server-renderer": "^2.6.14" "vue-server-renderer": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {

View File

@ -50,7 +50,9 @@ export default function nuxt2CompatModule () {
// Disable server sourceMap, esbuild will generate for it. // Disable server sourceMap, esbuild will generate for it.
nuxt.hook('webpack:config', (webpackConfigs) => { nuxt.hook('webpack:config', (webpackConfigs) => {
const serverConfig = webpackConfigs.find(config => config.name === 'server') const serverConfig = webpackConfigs.find(config => config.name === 'server')
if (serverConfig) {
serverConfig.devtool = false serverConfig.devtool = false
}
}) })
// Nitro client plugin // Nitro client plugin

View File

@ -40,6 +40,7 @@ export interface NitroContext {
_nuxt: { _nuxt: {
majorVersion: number majorVersion: number
dev: boolean dev: boolean
ssr: boolean
rootDir: string rootDir: string
srcDir: string srcDir: string
buildDir: string buildDir: string
@ -99,6 +100,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
_nuxt: { _nuxt: {
majorVersion: nuxtOptions._majorVersion || 2, majorVersion: nuxtOptions._majorVersion || 2,
dev: nuxtOptions.dev, dev: nuxtOptions.dev,
ssr: nuxtOptions.ssr,
rootDir: nuxtOptions.rootDir, rootDir: nuxtOptions.rootDir,
srcDir: nuxtOptions.srcDir, srcDir: nuxtOptions.srcDir,
buildDir: nuxtOptions.buildDir, buildDir: nuxtOptions.buildDir,

View File

@ -134,6 +134,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'global.': 'globalThis.', 'global.': 'globalThis.',
'process.server': 'true', 'process.server': 'true',
'process.client': 'false', 'process.client': 'false',
'process.env.NUXT_NO_SSR': JSON.stringify(!nitroContext._nuxt.ssr),
'process.env.ROUTER_BASE': JSON.stringify(nitroContext._nuxt.routerBase), 'process.env.ROUTER_BASE': JSON.stringify(nitroContext._nuxt.routerBase),
'process.env.PUBLIC_PATH': JSON.stringify(nitroContext._nuxt.publicPath), 'process.env.PUBLIC_PATH': JSON.stringify(nitroContext._nuxt.publicPath),
'process.env.NUXT_STATIC_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base), 'process.env.NUXT_STATIC_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base),

View File

@ -4,27 +4,47 @@ import { runtimeConfig } from './config'
// @ts-ignore // @ts-ignore
import htmlTemplate from '#build/views/document.template.mjs' 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 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' const PAYLOAD_JS = '/payload.js'
let _renderer const getClientManifest = cachedImport(() => import('#build/dist/server/client.manifest.mjs'))
async function loadRenderer () { const getSSRApp = cachedImport(() => import('#build/dist/server/server.mjs'))
if (_renderer) {
return _renderer const getSSRRenderer = cachedResult(async () => {
} // Load client manifest
// @ts-ignore 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') const { renderToString } = await import('#nitro-renderer')
// @ts-ignore return createRenderer((createSSRApp), { clientManifest, renderToString }).renderToString
const createApp = await import('#build/dist/server/server.mjs') })
// @ts-ignore
const clientManifest = await import('#build/dist/server/client.manifest.mjs') const getSPARenderer = cachedResult(async () => {
_renderer = createRenderer(_interopDefault(createApp), { const clientManifest = await getClientManifest()
clientManifest: _interopDefault(clientManifest), return (ssrContext) => {
renderToString ssrContext.nuxt = {}
return {
html: '<div id="__nuxt"></div>',
renderResourceHints: () => '',
renderStyles: () => '',
renderScripts: () => clientManifest.initial.map((s) => {
const isMJS = !s.endsWith('.js')
return `<script ${isMJS ? 'type="module"' : ''} src="${clientManifest.publicPath}${s}"></script>`
}).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) { 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) url = url.substr(STATIC_ASSETS_BASE.length, url.length - STATIC_ASSETS_BASE.length - PAYLOAD_JS.length)
} }
// Initialize ssr context
const ssrContext = { const ssrContext = {
url, url,
req, req,
res, res,
runtimeConfig, runtimeConfig,
noSSR: req.spa || req.headers['x-nuxt-no-ssr'],
...(req.context || {}) ...(req.context || {})
} }
const renderer = await loadRenderer()
const rendered = await renderer.renderToString(ssrContext) // Render app
const rendered = await renderToString(ssrContext)
// Handle errors // Handle errors
if (ssrContext.error) { if (ssrContext.error) {
@ -107,3 +130,24 @@ async function renderHTML (payload, rendered, ssrContext) {
function renderPayload (payload, url) { function renderPayload (payload, url) {
return `__NUXT_JSONP__("${url}", ${devalue(payload)})` return `__NUXT_JSONP__("${url}", ${devalue(payload)})`
} }
function _interopDefault (e) {
return e && typeof e === 'object' && 'default' in e ? e.default : e
}
function cachedImport <M> (importer: () => Promise<M>) {
return cachedResult(() => importer().then(_interopDefault).catch((err) => {
if (err.code === 'ERR_MODULE_NOT_FOUND') { return null }
throw err
}))
}
function cachedResult <T> (fn: () => Promise<T>): () => Promise<T> {
let res = null
return () => {
if (res === null) {
res = fn().catch((err) => { res = null; throw err })
}
return res
}
}

View File

@ -70,6 +70,11 @@ export function createDevServer (nitroContext: NitroContext) {
const proxy = createProxy() const proxy = createProxy()
app.use((req, res) => { app.use((req, res) => {
if (workerAddress) { 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) => { proxy.web(req, res, { target: workerAddress }, (_err: unknown) => {
// console.error('[proxy]', err) // console.error('[proxy]', err)
}) })

View File

@ -1,4 +1,6 @@
import * as vite from 'vite' import * as vite from 'vite'
import { resolve } from 'upath'
import { mkdirp, writeFile } from 'fs-extra'
import vitePlugin from '@vitejs/plugin-vue' import vitePlugin from '@vitejs/plugin-vue'
import { cacheDirPlugin } from './plugins/cache-dir' import { cacheDirPlugin } from './plugins/cache-dir'
import { replace } from './plugins/replace' 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 }) 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) const viteServer = await vite.createServer(clientConfig)
await ctx.nuxt.callHook('vite:serverCreated', viteServer) await ctx.nuxt.callHook('vite:serverCreated', viteServer)
const viteMiddleware = (req, res, next) => { const viteMiddleware = (req, res, next) => {
// Workaround: vite devmiddleware modifies req.url // Workaround: vite devmiddleware modifies req.url
const originalURL = req.url const originalURL = req.url
if (req.url === '/_nuxt/client.js') {
return res.end('')
}
viteServer.middlewares.handle(req, res, (err) => { viteServer.middlewares.handle(req, res, (err) => {
req.url = originalURL req.url = originalURL
next(err) next(err)

View File

@ -1,7 +1,6 @@
import { resolve } from 'upath' import { resolve } from 'upath'
import * as vite from 'vite' import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue' import vuePlugin from '@vitejs/plugin-vue'
import { mkdirp, writeFile } from 'fs-extra'
import consola from 'consola' import consola from 'consola'
import { ViteBuildContext, ViteOptions } from './vite' import { ViteBuildContext, ViteOptions } from './vite'
import { wpfs } from './utils/wpfs' 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 }) 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) const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
if (!ctx.nuxt.options.ssr) { if (!ctx.nuxt.options.ssr) {

View File

@ -37,8 +37,9 @@ export async function bundle (nuxt: Nuxt) {
...nuxt.options.alias, ...nuxt.options.alias,
'#app': nuxt.options.appDir, '#app': nuxt.options.appDir,
'#build': nuxt.options.buildDir, '#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,
'@': nuxt.options.srcDir, '@': nuxt.options.srcDir,
'web-streams-polyfill/ponyfill/es2018': 'unenv/runtime/mock/empty', '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' 'abort-controller': 'unenv/runtime/mock/empty'
} }
}, },
base: nuxt.options.build.publicPath,
vue: {}, vue: {},
css: {}, css: {},
optimizeDeps: { optimizeDeps: {
@ -67,7 +69,7 @@ export async function bundle (nuxt: Nuxt) {
], ],
server: { server: {
fs: { fs: {
strict: true, strict: false,
allow: [ allow: [
nuxt.options.buildDir, nuxt.options.buildDir,
nuxt.options.appDir, nuxt.options.appDir,
@ -85,11 +87,13 @@ export async function bundle (nuxt: Nuxt) {
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => { nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => {
const start = Date.now() 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`) consola.info(`Vite warmed up in ${Date.now() - start}ms`)
}).catch(consola.error) }).catch(consola.error)
}) })
await buildClient(ctx) await buildClient(ctx)
if (ctx.nuxt.options.ssr) {
await buildServer(ctx) await buildServer(ctx)
}
} }

View File

@ -85,9 +85,9 @@ class WebpackBundler {
this.getWebpackConfig('client') this.getWebpackConfig('client')
] ]
// if (options.build.ssr) { if (options.ssr) {
webpackConfigs.push(this.getWebpackConfig('server')) webpackConfigs.push(this.getWebpackConfig('server'))
// } }
await this.nuxt.callHook('webpack:config', webpackConfigs) await this.nuxt.callHook('webpack:config', webpackConfigs)

View File

@ -7,6 +7,14 @@ export default defineNuxtConfig({
buildModules: [ buildModules: [
'@nuxt/nitro/compat' '@nuxt/nitro/compat'
], ],
serverMiddleware: [
{
handle (req, _res, next) {
req.spa = req.url.includes('?spa')
next()
}
}
],
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,
nitro: { nitro: {
output: { dir: process.env.NITRO_OUTPUT_DIR } output: { dir: process.env.NITRO_OUTPUT_DIR }

View File

@ -1549,7 +1549,7 @@ __metadata:
unstorage: ^0.2.3 unstorage: ^0.2.3
upath: ^2.0.1 upath: ^2.0.1
vue: 3.1.5 vue: 3.1.5
vue-bundle-renderer: ^0.2.5 vue-bundle-renderer: ^0.2.9
vue-server-renderer: ^2.6.14 vue-server-renderer: ^2.6.14
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -12056,12 +12056,12 @@ fsevents@~2.3.2:
languageName: node languageName: node
linkType: hard linkType: hard
"vue-bundle-renderer@npm:^0.2.5": "vue-bundle-renderer@npm:^0.2.9":
version: 0.2.5 version: 0.2.9
resolution: "vue-bundle-renderer@npm:0.2.5" resolution: "vue-bundle-renderer@npm:0.2.9"
dependencies: dependencies:
bundle-runner: ^0.0.1 bundle-runner: ^0.0.1
checksum: 9848b493aec6dda72296cf885e3f270610e8481bc1773035f4827915d84f560c49c9e819a2c8ecbc89b70499ec188453772720ad9d58977aa92585878f028adf checksum: 82f1d06f7e839016707159f211c1feca4ca7ee123864545d8c2f81078ba80c84f3199bf52f66e4117bc4355c4ab91e704476cedf5f29979a5872639d7e3ae8e6
languageName: node languageName: node
linkType: hard linkType: hard