Skip to content

Commit

Permalink
feat: port upstream vite-node dev server implementation (#779)
Browse files Browse the repository at this point in the history
  • Loading branch information
wattanx authored Jun 20, 2023
1 parent ad4135b commit c29f91a
Show file tree
Hide file tree
Showing 18 changed files with 2,039 additions and 2,425 deletions.
1 change: 1 addition & 0 deletions packages/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"unplugin": "^1.3.1",
"untyped": "^1.3.2",
"vite": "~4.3.9",
"vite-node": "^0.31.1",
"vue-bundle-renderer": "^1.0.3"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge/src/vite/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { jsxPlugin } from './plugins/jsx'
import { ViteBuildContext, ViteOptions } from './types'
import { prepareManifests } from './manifest'
import { viteNodePlugin } from './vite-node'

export async function buildClient (ctx: ViteBuildContext) {
const alias = {
Expand Down Expand Up @@ -62,7 +63,8 @@ export async function buildClient (ctx: ViteBuildContext) {
devStyleSSRPlugin({
srcDir: ctx.nuxt.options.srcDir,
buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir)
})
}),
viteNodePlugin(ctx)
],
appType: 'custom',
server: {
Expand Down
44 changes: 42 additions & 2 deletions packages/bridge/src/vite/dev-bundler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { pathToFileURL } from 'url'
import { existsSync } from 'fs'
import { builtinModules } from 'module'
import { isAbsolute, resolve } from 'pathe'
import { isAbsolute, resolve, normalize } from 'pathe'
import { ExternalsOptions, isExternal as _isExternal, ExternalsDefaults } from 'externality'
import { genDynamicImport, genObjectFromRawEntries } from 'knitwork'
import type { ViteDevServer } from 'vite'
import { hashId, uniq } from './utils'
import fse from 'fs-extra'
import { debounce } from 'perfect-debounce'
import { logger, isIgnored } from '@nuxt/kit'
import { hashId, isCSS, uniq } from './utils'
import { ViteBuildContext } from './types'
import { createIsExternal } from './utils/external'
import { generateDevSSRManifest } from './manifest'

export interface TransformChunk {
id: string,
Expand All @@ -23,6 +29,7 @@ export interface SSRTransformResult {

export interface TransformOptions {
viteServer: ViteDevServer
isExternal(id: string): ReturnType<typeof isExternal>
}

function isExternal (opts: TransformOptions, id: string) {
Expand Down Expand Up @@ -221,3 +228,36 @@ async function __instantiateModule__(url, urlStack) {
ids: chunks.map(i => i.id)
}
}

export async function initViteDevBundler (ctx: ViteBuildContext, onBuild: () => Promise<any>) {
const viteServer = ctx.ssrServer!
const options: TransformOptions = {
viteServer,
isExternal: createIsExternal(viteServer, ctx.nuxt.options.rootDir)
}

// Build and watch
const _doBuild = async () => {
const start = Date.now()
const { code, ids } = await bundleRequest(options, '/.nuxt/server.js')
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 generateDevSSRManifest(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 = debounce(_doBuild)

// 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 || isIgnored(file)) { return }
doBuild()
})
// ctx.nuxt.hook('builder:watch', () => doBuild())
ctx.nuxt.hook('app:templatesGenerated', () => doBuild())
}
9 changes: 9 additions & 0 deletions packages/bridge/src/vite/dirs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'pathe'

let _distDir = dirname(fileURLToPath(import.meta.url))
if (_distDir.match(/(chunks|shared)$/)) {
_distDir = dirname(_distDir)
}
export const distDir = _distDir
export const pkgDir = resolve(distDir, '..')
52 changes: 52 additions & 0 deletions packages/bridge/src/vite/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,55 @@ export async function writeClientManifest (clientManifest: any, buildDir: string
await fse.writeFile(resolve(buildDir, 'dist/server/client.manifest.json'), clientManifestJSON, 'utf-8')
await fse.writeFile(resolve(buildDir, 'dist/server/client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')
}

export async function writeManifest (ctx: ViteBuildContext, css: string[] = []) {
// Write client manifest for use in vue-bundle-renderer
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')

const devClientManifest: Manifest = {
'@vite/client': {
isEntry: true,
file: '@vite/client',
css,
module: true,
resourceType: 'script'
},
'entry.mjs': {
isEntry: true,
file: 'entry.mjs',
module: true,
resourceType: 'script'
}
}

const clientManifest = ctx.nuxt.options.dev
? devClientManifest
: await fse.readJSON(resolve(clientDist, 'manifest.json'))

const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir))
const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`)

for (const key in clientManifest) {
if (clientManifest[key].file) {
clientManifest[key].file = clientManifest[key].file.replace(BASE_RE, '')
}
for (const item of ['css', 'assets']) {
if (clientManifest[key][item]) {
clientManifest[key][item] = clientManifest[key][item].map((i: string) => i.replace(BASE_RE, ''))
}
}
}

await fse.mkdirp(serverDist)

const manifest = normalizeViteManifest(clientManifest)
await ctx.nuxt.callHook('build:manifest', manifest)

await fse.writeFile(resolve(serverDist, 'client.manifest.json'), JSON.stringify(manifest, null, 2), 'utf8')
await fse.writeFile(resolve(serverDist, 'client.manifest.mjs'), 'export default ' + JSON.stringify(manifest, null, 2), 'utf8')

if (!ctx.nuxt.options.dev) {
await fse.rm(resolve(clientDist, 'manifest.json'), { force: true })
}
}
4 changes: 3 additions & 1 deletion packages/bridge/src/vite/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ export default defineNuxtModule<ViteOptions>({
name: 'nuxt-bridge:vite',
configKey: 'vite'
},
defaults: {},
defaults: {
},
setup (viteOptions, nuxt) {
nuxt.options.vite = viteOptions
nuxt.options.cli.badgeMessages.push(`⚡ Vite Mode Enabled (v${version})`)

if (viteOptions.experimentWarning !== false && !nuxt.options.test) {
Expand Down
4 changes: 4 additions & 0 deletions packages/bridge/src/vite/runtime/client.manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @ts-check
import { viteNodeFetch } from './vite-node-shared.mjs'

export default () => viteNodeFetch('/manifest')
13 changes: 13 additions & 0 deletions packages/bridge/src/vite/runtime/vite-node-shared.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-check
import { Agent as HTTPSAgent } from 'node:https'
import { $fetch } from 'ofetch'

export const viteNodeOptions = JSON.parse(process.env.NUXT_VITE_NODE_OPTIONS || '{}')

export const viteNodeFetch = $fetch.create({
baseURL: viteNodeOptions.baseURL,
// @ts-expect-error https://github.com/node-fetch/node-fetch#custom-agent
agent: viteNodeOptions.baseURL.startsWith('https://')
? new HTTPSAgent({ rejectUnauthorized: false })
: null
})
110 changes: 110 additions & 0 deletions packages/bridge/src/vite/runtime/vite-node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// @ts-check

import { performance } from 'node:perf_hooks'
import { createError } from 'h3'
import { ViteNodeRunner } from 'vite-node/client'
import { consola } from 'consola'
import { viteNodeFetch, viteNodeOptions } from './vite-node-shared.mjs'

const runner = createRunner()
/** @type {(ssrContext: import('../../../types').NuxtSSRContext) => Promise<any>} */
let render

/** @param ssrContext {import('../../../').NuxtSSRContext} */
export default async (ssrContext) => {
// Workaround for stub mode
// https://github.com/nuxt/framework/pull/3983
process.server = true

// Invalidate cache for files changed since last rendering
const invalidates = await viteNodeFetch('/invalidates')
const updates = runner.moduleCache.invalidateDepTree(invalidates)

// Execute SSR bundle on demand
const start = performance.now()
render = (updates.has(viteNodeOptions.entryPath) || !render) ? (await runner.executeFile(viteNodeOptions.entryPath)).default : render
if (updates.size) {
const time = Math.round((performance.now() - start) * 1000) / 1000
consola.success(`Vite server hmr ${updates.size} files`, time ? `in ${time}ms` : '')
}

const result = await render(ssrContext)
return result
}

function createRunner () {
const _importers = new Map()
return new ViteNodeRunner({
root: viteNodeOptions.root, // Equals to Nuxt `srcDir`
base: viteNodeOptions.base,
resolveId (id, importer) { _importers.set(id, importer) },
async fetchModule (id) {
const importer = _importers.get(id)
_importers.delete(id)
id = id.replace(/\/\//g, '/') // TODO: fix in vite-node
return await viteNodeFetch('/module/' + encodeURI(id)).catch((err) => {
const errorData = err?.data?.data
if (!errorData) {
throw err
}
let _err
try {
const { message, stack } = formatViteError(errorData, id, importer)
_err = createError({
statusMessage: 'Vite Error',
message,
stack
})
} catch (formatError) {
consola.warn('Internal nuxt error while formatting vite-node error. Please report this!', formatError)
const message = `[vite-node] [TransformError] ${errorData?.message || '-'}`
consola.error(message, errorData)
throw createError({
statusMessage: 'Vite Error',
message,
stack: `${message}\nat ${id}\n` + (errorData?.stack || '')
})
}
throw _err
})
}
})
}

/**
* @param errorData {any}
* @param id {string}
* @param importer {string}
*/
function formatViteError (errorData, id, importer) {
const errorCode = errorData.name || errorData.reasonCode || errorData.code
const frame = errorData.frame || errorData.source || errorData.pluginCode

/** @param locObj {{ file?: string, id?: string, url?: string }} */
const getLocId = (locObj = {}) => locObj.file || locObj.id || locObj.url || id || ''
/** @param locObj {{ line?: string, column?: string }} */
const getLocPos = (locObj = {}) => locObj.line ? `${locObj.line}:${locObj.column || 0}` : ''
const locId = getLocId(errorData.loc) || getLocId(errorData.location) || getLocId(errorData.input) || getLocId(errorData)
const locPos = getLocPos(errorData.loc) || getLocPos(errorData.location) || getLocPos(errorData.input) || getLocPos(errorData)
const loc = locId.replace(process.cwd(), '.') + (locPos ? `:${locPos}` : '')

const message = [
'[vite-node]',
errorData.plugin && `[plugin:${errorData.plugin}]`,
errorCode && `[${errorCode}]`,
loc,
errorData.reason && `: ${errorData.reason}`,
frame && `<br><pre>${frame.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre><br>`
].filter(Boolean).join(' ')

const stack = [
message,
`at ${loc} ${importer ? `(imported from ${importer})` : ''}`,
errorData.stack
].filter(Boolean).join('\n')

return {
message,
stack
}
}
35 changes: 6 additions & 29 deletions packages/bridge/src/vite/server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { resolve } from 'pathe'
import createVuePlugin from '@vitejs/plugin-vue2'
import { logger } from '@nuxt/kit'
import fse from 'fs-extra'
import type { InlineConfig } from 'vite'
import { debounce } from 'perfect-debounce'
import { joinURL, withoutLeadingSlash, withTrailingSlash } from 'ufo'
import { mergeConfig, createServer, build } from './stub-vite.cjs'
import { bundleRequest } from './dev-bundler'
import { isCSS } from './utils'
import { wpfs } from './utils/wpfs'
import { ViteBuildContext, ViteOptions } from './types'
import { jsxPlugin } from './plugins/jsx'
import { generateDevSSRManifest } from './manifest'
import { initViteNodeServer } from './vite-node'

export async function buildServer (ctx: ViteBuildContext) {
// Workaround to disable HMR
Expand Down Expand Up @@ -138,29 +134,10 @@ export async function buildServer (ctx: ViteBuildContext) {
// Initialize plugins
await viteServer.pluginContainer.buildStart({})

// Generate manifest files
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/ssr-manifest.json'), JSON.stringify({}, null, 2), 'utf-8')
await generateDevSSRManifest(ctx)

// Build and watch
const _doBuild = async () => {
const start = Date.now()
const { code, ids } = await bundleRequest({ viteServer }, '/.nuxt/server.js')
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 generateDevSSRManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
const time = (Date.now() - start)
logger.info(`Vite server built in ${time}ms`)
await onBuild()
if (ctx.config.devBundler !== 'legacy') {
await initViteNodeServer(ctx)
} else {
logger.info('Vite server using legacy server bundler...')
await import('./dev-bundler').then(r => r.initViteDevBundler(ctx, onBuild))
}
const doBuild = debounce(_doBuild)

// Initial build
await _doBuild()

// Watch
viteServer.watcher.on('all', (_event, file) => {
if (file.indexOf(ctx.nuxt.options.buildDir) === 0) { return }
doBuild()
})
}
7 changes: 6 additions & 1 deletion packages/bridge/src/vite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ export interface Nuxt {
export interface ViteOptions extends InlineConfig {
/**
* Options for @vitejs/plugin-vue2
*
* @see https://github.com/vitejs/vite-plugin-vue2
*/
vue?: VueViteOptions

experimentWarning?: boolean

/**
* Bundler for dev time server-side rendering.
* @default 'vite-node'
*/
devBundler?: 'vite-node' | 'legacy'
}

export interface ViteBuildContext {
Expand Down
24 changes: 24 additions & 0 deletions packages/bridge/src/vite/utils/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ExternalsOptions } from 'externality'
import { ExternalsDefaults, isExternal } from 'externality'
import type { ViteDevServer } from 'vite'

export function createIsExternal (viteServer: ViteDevServer, rootDir: string) {
const externalOpts: ExternalsOptions = {
inline: [
/virtual:/,
/\.ts$/,
...ExternalsDefaults.inline || [],
...Array.isArray(viteServer.config.ssr.noExternal) ? viteServer.config.ssr.noExternal : []
],
external: [
...viteServer.config.ssr.external || [],
/node_modules/
],
resolve: {
type: 'module',
extensions: ['.ts', '.js', '.json', '.vue', '.mjs', '.jsx', '.tsx', '.wasm']
}
}

return (id: string) => isExternal(id, rootDir, externalOpts)
}
Loading

0 comments on commit c29f91a

Please sign in to comment.