feat(vite): experimental `vite-node` support (#2795)

Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
Anthony Fu 2022-03-11 16:41:27 +08:00 committed by GitHub
parent 5d58ef48af
commit ac40c9746c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 218 additions and 39 deletions

View File

@ -30,6 +30,7 @@
"jiti": "^1.13.0",
"nitropack-dev": "link:../nitropack",
"nuxt3": "workspace:./packages/nuxt3",
"vite": "^2.8.4",
"unbuild": "^0.7.0"
},
"devDependencies": {

View File

@ -15,12 +15,12 @@ const getClientManifest = cachedImport(() => import('#build/dist/server/client.m
const getSSRApp = !process.env.NUXT_NO_SSR && 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 not available') }
// Load server bundle
const createSSRApp = await getSSRApp()
if (!createSSRApp) { throw new Error('Server bundle is not available') }
// Load client manifest
const clientManifest = await getClientManifest()
if (!clientManifest) { throw new Error('client.manifest is not available') }
// Create renderer
const { renderToString } = await import('#nitro-renderer')
return createRenderer((createSSRApp), { clientManifest, renderToString, publicPath: buildAssetsURL() }).renderToString

View File

@ -3,5 +3,11 @@ export default {
* Set to true to generate an async entrypoint for the Vue bundle (for module federation support).
* @version 3
*/
asyncEntry: false
asyncEntry: false,
/**
* Use vite-node for on-demand server chunk loading
* @version 3
*/
viteNode: process.env.EXPERIMENTAL_VITE_NODE ? true : false
}

View File

@ -3,7 +3,8 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
declaration: true,
entries: [
'src/index'
'src/index',
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
],
dependencies: [
'vue'

View File

@ -43,7 +43,8 @@
"rollup-plugin-visualizer": "^5.6.0",
"ufo": "^0.7.11",
"unplugin": "^0.4.0",
"vite": "^2.8.6"
"vite": "^2.8.6",
"vite-node": "^0.6.0"
},
"peerDependencies": {
"vue": "3.2.31"

View File

@ -12,6 +12,7 @@ import type { ViteBuildContext, ViteOptions } from './vite'
import { writeManifest } from './manifest'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { DynamicBasePlugin, RelativeAssetPlugin } from './plugins/dynamic-base'
import { viteNodePlugin } from './vite-node'
export async function buildClient (ctx: ViteBuildContext) {
const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, {
@ -44,7 +45,8 @@ export async function buildClient (ctx: ViteBuildContext) {
devStyleSSRPlugin({
rootDir: ctx.nuxt.options.rootDir,
buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir)
})
}),
viteNodePlugin(ctx)
],
server: {
middlewareMode: true
@ -59,6 +61,7 @@ export async function buildClient (ctx: ViteBuildContext) {
await ctx.nuxt.callHook('vite:extendConfig', clientConfig, { isClient: true, isServer: false })
const viteServer = await vite.createServer(clientConfig)
ctx.clientServer = viteServer
await ctx.nuxt.callHook('vite:serverCreated', viteServer)
const viteMiddleware: Connect.NextHandleFunction = (req, res, next) => {

View File

@ -0,0 +1,54 @@
import { promises as fs } from 'fs'
import { fileURLToPath } from 'url'
import { ViteNodeRunner } from 'vite-node/client'
import { dirname, join } from 'pathe'
const entry = '__NUXT_SERVER_ENTRY__'
const url = '__NUXT_SERVER_FETCH_URL__'
const base = '__NUXT_SERVER_BASE__'
const runner = new ViteNodeRunner({
root: process.cwd(),
base,
async fetchModule (id) {
return await $fetch(url, {
method: 'POST',
body: { id }
})
}
})
const IS_CSS_RE = /\.(css|postcss|sass|scss|less|stylus|styl)(\?[^.]+)?$/
function isCSS (file) {
return IS_CSS_RE.test(file)
}
async function writeManifest (extraEntries) {
const dir = dirname(fileURLToPath(import.meta.url))
const entries = [
'@vite/client',
'entry.mjs',
...extraEntries
]
const clientManifest = {
publicPath: '',
all: entries,
initial: entries,
async: [],
modules: {}
}
await fs.writeFile(join(dir, 'client.manifest.json'), JSON.stringify(clientManifest, null, 2), 'utf8')
await fs.writeFile(join(dir, 'client.manifest.mjs'), 'export default ' + JSON.stringify(clientManifest, null, 2), 'utf8')
}
export default (async () => {
const { default: render } = await runner.executeFile(entry)
const result = await render()
const modules = Array.from(runner.moduleCache.keys())
// Write CSS modules intro manifest to prevent FOUC
await writeManifest(modules.filter(i => isCSS(i)).map(i => i.slice(1)))
return result
})()

View File

@ -1,4 +1,4 @@
import { join, resolve, normalize } from 'pathe'
import { resolve, join, normalize } from 'pathe'
import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
@ -9,10 +9,11 @@ import { withoutTrailingSlash } from 'ufo'
import { ViteBuildContext, ViteOptions } from './vite'
import { wpfs } from './utils/wpfs'
import { cacheDirPlugin } from './plugins/cache-dir'
import { prepareDevServerEntry } from './vite-node'
import { DynamicBasePlugin } from './plugins/dynamic-base'
import { isCSS, isDirectory, readDirRecursively } from './utils'
import { bundleRequest } from './dev-bundler'
import { writeManifest } from './manifest'
import { isCSS, isDirectory, readDirRecursively } from './utils'
export async function buildServer (ctx: ViteBuildContext) {
const _resolve = id => resolveModule(id, { paths: ctx.nuxt.options.modulesDir })
@ -85,7 +86,7 @@ export async function buildServer (ctx: ViteBuildContext) {
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
// Remove public files that have been duplicated into buildAssetsDir
// TODO: Add option to configure this behaviour in vite
// TODO: Add option to configure this behavior in vite
const publicDir = join(ctx.nuxt.options.srcDir, ctx.nuxt.options.dir.public)
let publicFiles: string[] = []
if (await isDirectory(publicDir)) {
@ -128,6 +129,8 @@ export async function buildServer (ctx: ViteBuildContext) {
// Start development server
const viteServer = await vite.createServer(serverConfig)
ctx.ssrServer = viteServer
await ctx.nuxt.callHook('vite:serverCreated', viteServer)
// Close server on exit
@ -136,28 +139,33 @@ export async function buildServer (ctx: ViteBuildContext) {
// Initialize plugins
await viteServer.pluginContainer.buildStart({})
// Build and watch
const _doBuild = async () => {
const start = Date.now()
const { code, ids } = await bundleRequest({ viteServer }, resolve(ctx.nuxt.options.appDir, 'entry'))
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8')
// Have CSS in the manifest to prevent FOUC on dev SSR
await writeManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
const time = (Date.now() - start)
logger.success(`Vite server built in ${time}ms`)
await onBuild()
if (ctx.nuxt.options.experimental.viteNode) {
logger.info('Using experimental vite-node server...')
await prepareDevServerEntry(ctx)
} else {
// Build and watch
const _doBuild = async () => {
const start = Date.now()
const { code, ids } = await bundleRequest({ viteServer }, resolve(ctx.nuxt.options.appDir, 'entry'))
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8')
// Have CSS in the manifest to prevent FOUC on dev SSR
await writeManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
const time = (Date.now() - start)
logger.success(`Vite server built in ${time}ms`)
await onBuild()
}
const doBuild = pDebounce(_doBuild, 100)
// Initial build
await _doBuild()
// Watch
viteServer.watcher.on('all', (_event, file) => {
file = normalize(file) // Fix windows paths
if (file.indexOf(ctx.nuxt.options.buildDir) === 0) { return }
doBuild()
})
// ctx.nuxt.hook('builder:watch', () => doBuild())
ctx.nuxt.hook('app:templatesGenerated', () => doBuild())
}
const doBuild = pDebounce(_doBuild, 100)
// Initial build
await _doBuild()
// Watch
viteServer.watcher.on('all', (_event, file) => {
file = normalize(file) // Fix windows paths
if (file.indexOf(ctx.nuxt.options.buildDir) === 0) { return }
doBuild()
})
// ctx.nuxt.hook('builder:watch', () => doBuild())
ctx.nuxt.hook('app:templatesGenerated', () => doBuild())
}

View File

@ -0,0 +1,87 @@
import { IncomingMessage } from 'http'
import { ViteNodeServer } from 'vite-node/server'
import fse from 'fs-extra'
import { resolve } from 'pathe'
import { addServerMiddleware } from '@nuxt/kit'
import type { Connect, Plugin as VitePlugin } from 'vite'
import { distDir } from './dirs'
import type { ViteBuildContext } from './vite'
// TODO: Remove this in favor of registerViteNodeMiddleware
// after Nitropack or h3 fixed for adding middlewares after setup
export function viteNodePlugin (ctx: ViteBuildContext): VitePlugin {
return {
name: 'nuxt:vite-node-server',
enforce: 'pre',
configureServer (server) {
server.middlewares.use('/__nuxt_vite_node__', createViteNodeMiddleware(ctx))
}
}
}
export function registerViteNodeMiddleware (ctx: ViteBuildContext) {
addServerMiddleware({
route: '/__nuxt_vite_node__/',
handle: createViteNodeMiddleware(ctx)
})
}
function createViteNodeMiddleware (ctx: ViteBuildContext): Connect.NextHandleFunction {
let node: ViteNodeServer | undefined
return async (req, res, next) => {
if (!node && ctx.ssrServer) {
node = new ViteNodeServer(ctx.ssrServer, {
deps: {
inline: [
...ctx.nuxt.options.build.transpile as string[]
]
}
})
}
if (!node) {
return next()
}
const body = await getBodyJson(req) || {}
const { id } = body
if (!id) {
res.statusCode = 400
res.end()
} else {
res.write(JSON.stringify(await node.fetchModule(id)))
res.end()
}
}
}
export async function prepareDevServerEntry (ctx: ViteBuildContext) {
const entryPath = resolve(ctx.nuxt.options.appDir, 'entry.async')
const raw = await fse.readFile(resolve(distDir, 'runtime/server.mjs'), 'utf-8')
const host = ctx.nuxt.options.server.host || 'localhost'
const port = ctx.nuxt.options.server.port || '3000'
const protocol = ctx.nuxt.options.server.https ? 'https' : 'http'
const code = raw
.replace('__NUXT_SERVER_FETCH_URL__', `${protocol}://${host}:${port}/__nuxt_vite_node__/`)
.replace('__NUXT_SERVER_ENTRY__', entryPath)
.replace('__NUXT_SERVER_BASE__', ctx.ssrServer.config.base || '/_nuxt/')
await fse.writeFile(
resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'),
code,
'utf-8'
)
}
function getBodyJson (req: IncomingMessage) {
return new Promise<any>((resolve, reject) => {
let body = ''
req.on('data', (chunk) => { body += chunk })
req.on('error', reject)
req.on('end', () => {
try {
resolve(JSON.parse(body) || {})
} catch (e) {
reject(e)
}
})
})
}

View File

@ -20,6 +20,8 @@ export interface ViteOptions extends InlineConfig {
export interface ViteBuildContext {
nuxt: Nuxt
config: ViteOptions
clientServer?: vite.ViteDevServer
ssrServer?: vite.ViteDevServer
}
export async function bundle (nuxt: Nuxt) {

View File

@ -1,6 +1,6 @@
<template>
<div class="app">
<img src="~/assets/logo.svg" class="h-20 mb-4">
<img src="./assets/logo.svg" class="h-20 mb-4">
<h1 class="greeting">
{{ hello }}, <br>Nuxt 3!
</h1>

View File

@ -3387,6 +3387,7 @@ __metadata:
unbuild: latest
unplugin: ^0.4.0
vite: ^2.8.6
vite-node: ^0.6.0
vue: 3.2.31
peerDependencies:
vue: 3.2.31
@ -13386,7 +13387,7 @@ __metadata:
languageName: node
linkType: hard
"kolorist@npm:^1.5.0":
"kolorist@npm:^1.5.0, kolorist@npm:^1.5.1":
version: 1.5.1
resolution: "kolorist@npm:1.5.1"
checksum: c113be08834fc03a24699612141c79879fceba9ff9765ad500507fb594ee4fa3465a3453ea90bbc9b0dd82f7ba5dbd79814da28e9ebaf8da27266a0088ba2714
@ -21677,6 +21678,21 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:^0.6.0":
version: 0.6.0
resolution: "vite-node@npm:0.6.0"
dependencies:
kolorist: ^1.5.1
minimist: ^1.2.5
mlly: ^0.4.3
pathe: ^0.2.0
vite: ^2.8.4
bin:
vite-node: vite-node.mjs
checksum: 0440416b788c083af43a128242a1e5bb28ffde00867707a4251902aae223cb730c8ec6d4fc1901a00aef551d3c8f5676f4705a361584c0f5876eae952389b4c2
languageName: node
linkType: hard
"vite-plugin-vue2@npm:^1.9.3":
version: 1.9.3
resolution: "vite-plugin-vue2@npm:1.9.3"
@ -21708,9 +21724,9 @@ __metadata:
languageName: node
linkType: hard
"vite@npm:^2.7.10, vite@npm:^2.8.2, vite@npm:^2.8.6":
version: 2.8.6
resolution: "vite@npm:2.8.6"
"vite@npm:^2.8.4":
version: 2.8.4
resolution: "vite@npm:2.8.4"
dependencies:
esbuild: ^0.14.14
fsevents: ~2.3.2
@ -21733,7 +21749,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 4b02d133892c98362c10214b7ad518d74b59745889197a2ba0b63260ed083fcef75a447e8fb58dbd2af8747386274b36017983d93031254df6ead38701950dcc
checksum: 0531ea17d354c35026c87e732d28c777492cc5165c4abdaa507c4894535ecbbfcf447fa3f270bbb160cd7cba8ad319cc86a221be18b2ccd40d8be139f9d7381d
languageName: node
linkType: hard