Skip to content

Commit

Permalink
feat: vite-node hmr
Browse files Browse the repository at this point in the history
  • Loading branch information
poyoho committed Jun 20, 2022
1 parent 41c20db commit 54ef5b8
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 16 deletions.
23 changes: 10 additions & 13 deletions packages/vite-node/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import cac from 'cac'
import { cyan, dim, red } from 'kolorist'
import { red } from 'kolorist'
import { createServer } from 'vite'
import { version } from '../package.json'
import { ViteNodeServer } from './server'
import { ViteNodeRunner } from './client'
import type { ViteNodeServerOptions } from './types'
import { toArray } from './utils'
import { createHotContext, handleMessage, viteNodeHmrPlugin } from './hmr'

const cli = cac('vite-node')

Expand Down Expand Up @@ -49,6 +50,9 @@ async function run(files: string[], options: CliOptions = {}) {
logLevel: 'error',
configFile: options.config,
root: options.root,
plugins: [
viteNodeHmrPlugin(),
],
})
await server.pluginContainer.buildStart({})

Expand All @@ -63,6 +67,9 @@ async function run(files: string[], options: CliOptions = {}) {
resolveId(id, importer) {
return node.resolveId(id, importer)
},
createHotContext(runner, url) {
return createHotContext(runner, server.emitter, files, url)
},
})

// provide the vite define variable in this context
Expand All @@ -74,18 +81,8 @@ async function run(files: string[], options: CliOptions = {}) {
if (!options.watch)
await server.close()

server.watcher.on('change', async (path) => {
console.log(`${cyan('[vite-node]')} File change detected. ${dim(path)}`)

// invalidate module cache but not node_modules
Array.from(runner.moduleCache.keys())
.forEach((i) => {
if (!i.includes('node_modules'))
runner.moduleCache.delete(i)
})

for (const file of files)
await runner.executeFile(file)
server.emitter.on('message', (payload) => {
handleMessage(runner, server.emitter, files, payload)
})
}

Expand Down
5 changes: 4 additions & 1 deletion packages/vite-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ export class ViteNodeRunner {
__vite_ssr_dynamic_import__: request,
__vite_ssr_exports__: exports,
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
__vite_ssr_import_meta__: { url },
__vite_ssr_import_meta__: {
url,
hot: this.options.createHotContext?.(this, `/@fs/${fsPath}`),
},

__vitest_resolve_id__: resolveId,

Expand Down
42 changes: 42 additions & 0 deletions packages/vite-node/src/hmr/emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { EventEmitter } from 'events'
import type { HMRPayload, Plugin } from 'vite'

export type EventType = string | symbol
export type Handler<T = unknown> = (event: T) => void
export interface Emitter<Events extends Record<EventType, unknown>> {
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void
off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void
}

export type HMREmitter = Emitter<{
'message': HMRPayload
}> & EventEmitter

declare module 'vite' {
interface ViteDevServer {
emitter: HMREmitter
}
}

export function createHmrEmitter(): HMREmitter {
const emitter = new EventEmitter()
return emitter
}

export function viteNodeHmrPlugin(): Plugin {
const emitter = createHmrEmitter()
return {
name: 'vite-node:hmr',

configureServer(server) {
const _send = server.ws.send
server.emitter = emitter
server.ws.send = function (payload: HMRPayload) {
_send(payload)
emitter.emit('message', payload)
}
},
}
}
169 changes: 169 additions & 0 deletions packages/vite-node/src/hmr/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* eslint-disable no-console */
import type { ErrorPayload, FullReloadPayload, HMRPayload, PrunePayload, Update, UpdatePayload } from 'vite/types/hmrPayload'
import { cyan } from 'kolorist'
import type { ViteNodeRunner } from '../client'
import { getCache } from './hotContext'
import type { HMREmitter } from './emitter'

export interface CustomEventMap {
'vite:beforeUpdate': UpdatePayload
'vite:beforePrune': PrunePayload
'vite:beforeFullReload': FullReloadPayload
'vite:error': ErrorPayload
}

export type InferCustomEventPayload<T extends string> =
T extends keyof CustomEventMap ? CustomEventMap[T] : any

export function sendMessageBuffer(runner: ViteNodeRunner, emitter: HMREmitter) {
const maps = getCache(runner)
maps.messageBuffer.forEach(msg => emitter.emit('custom', msg))
maps.messageBuffer.length = 0
}

export async function reload(runner: ViteNodeRunner, files: string[]) {
// invalidate module cache but not node_modules
Array.from(runner.moduleCache.keys())
.forEach((i) => {
if (!i.includes('node_modules'))
runner.moduleCache.delete(i)
})

return Promise.all(files.map(file => runner.executeId(file)))
}

function notifyListeners<T extends string>(
runner: ViteNodeRunner,
event: T,
data: InferCustomEventPayload<T>,
): void
function notifyListeners(runner: ViteNodeRunner, event: string, data: any): void {
const maps = getCache(runner)
const cbs = maps.customListenersMap.get(event)
if (cbs)
cbs.forEach(cb => cb(data))
}

async function queueUpdate(runner: ViteNodeRunner, p: Promise<(() => void) | undefined>) {
const maps = getCache(runner)
maps.queued.push(p)
if (!maps.pending) {
maps.pending = true
await Promise.resolve()
maps.pending = false
const loading = [...maps.queued]
maps.queued = []
;(await Promise.all(loading)).forEach(fn => fn && fn())
}
}

async function fetchUpdate(runner: ViteNodeRunner, { path, acceptedPath }: Update) {
const maps = getCache(runner)
const mod = maps.hotModulesMap.get(path)

if (!mod) {
// In a code-splitting project,
// it is common that the hot-updating module is not loaded yet.
// https://github.com/vitejs/vite/issues/721
return
}

const moduleMap = new Map()
const isSelfUpdate = path === acceptedPath

// make sure we only import each dep once
const modulesToUpdate = new Set<string>()
if (isSelfUpdate) {
// self update - only update self
modulesToUpdate.add(path)
}
else {
// dep update
for (const { deps } of mod.callbacks) {
deps.forEach((dep) => {
if (acceptedPath === dep)
modulesToUpdate.add(dep)
})
}
}

// determine the qualified callbacks before we re-import the modules
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
return deps.some(dep => modulesToUpdate.has(dep))
})

await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const disposer = maps.disposeMap.get(dep)
if (disposer)
await disposer(maps.dataMap.get(dep))
try {
const newMod = await reload(runner, [dep])
moduleMap.set(dep, newMod)
}
catch (e: any) {
warnFailedFetch(e, dep)
}
}),
)

return () => {
for (const { deps, fn } of qualifiedCallbacks)
fn(deps.map(dep => moduleMap.get(dep)))

const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`${cyan('[vite-node]')} hot updated: ${loggedPath}`)
}
}

function warnFailedFetch(err: Error, path: string | string[]) {
if (!err.message.match('fetch'))
console.error(err)

console.error(
`[hmr] Failed to reload ${path}. `
+ 'This could be due to syntax errors or importing non-existent '
+ 'modules. (see errors above)',
)
}

export async function handleMessage(runner: ViteNodeRunner, emitter: HMREmitter, files: string[], payload: HMRPayload) {
const maps = getCache(runner)
switch (payload.type) {
case 'connected':
sendMessageBuffer(runner, emitter)
break
case 'update':
notifyListeners(runner, 'vite:beforeUpdate', payload)
if (maps.isFirstUpdate) {
reload(runner, files)
maps.isFirstUpdate = true
}
payload.updates.forEach((update) => {
if (update.type === 'js-update') {
queueUpdate(runner, fetchUpdate(runner, update))
}
else {
// css-update
console.error(`${cyan('[vite-node]')} no support css hmr.}`)
}
})
break
case 'full-reload':
reload(runner, files)
break
case 'prune':
payload.paths.forEach((path) => {
const fn = maps.pruneMap.get(path)
if (fn)
fn(maps.dataMap.get(path))
})
break
case 'error': {
notifyListeners(runner, 'vite:error', payload)
const err = payload.err
console.error(`${cyan('[vite-node]')} Internal Server Error\n${err.message}\n${err.stack}`)
break
}
}
}

0 comments on commit 54ef5b8

Please sign in to comment.