Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vite-node): correctly resolve hmr filepath #3834

Merged
merged 13 commits into from Jul 29, 2023
3 changes: 3 additions & 0 deletions packages/vite-node/src/cli.ts
Expand Up @@ -84,6 +84,9 @@ async function run(files: string[], options: CliOptions = {}) {
configFile: options.config,
root: options.root,
mode: options.mode,
server: {
hmr: !!options.watch,
},
plugins: [
options.watch && viteNodeHmrPlugin(),
],
Expand Down
21 changes: 15 additions & 6 deletions packages/vite-node/src/client.ts
Expand Up @@ -32,17 +32,26 @@ const clientStub = {
if (typeof document === 'undefined')
return

const element = document.getElementById(id)
if (element)
element.remove()
const element = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (element) {
element.textContent = css
return
}

const head = document.querySelector('head')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.id = id
style.innerHTML = css
style.setAttribute('data-vite-dev-id', id)
style.textContent = css
head?.appendChild(style)
},
removeStyle(id: string) {
if (typeof document === 'undefined')
return
const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (sheet)
document.head.removeChild(sheet)
},
}

export const DEFAULT_REQUEST_STUBS: Record<string, unknown> = {
Expand Down Expand Up @@ -372,7 +381,7 @@ export class ViteNodeRunner {
Object.defineProperty(meta, 'hot', {
enumerable: true,
get: () => {
hotContext ||= this.options.createHotContext?.(this, `/@fs/${fsPath}`)
hotContext ||= this.options.createHotContext?.(this, moduleId)
return hotContext
},
set: (value) => {
Expand Down
104 changes: 46 additions & 58 deletions packages/vite-node/src/hmr/hmr.ts
Expand Up @@ -6,8 +6,13 @@ import c from 'picocolors'
import createDebug from 'debug'
import type { ViteNodeRunner } from '../client'
import type { HotContext } from '../types'
import { normalizeRequestId } from '../utils'
import type { HMREmitter } from './emitter'

export type ModuleNamespace = Record<string, any> & {
[Symbol.toStringTag]: 'Module'
}

const debugHmr = createDebug('vite-node:hmr')

export type InferCustomEventPayload<T extends string> =
Expand All @@ -21,7 +26,7 @@ export interface HotModule {
export interface HotCallback {
// the dependencies must be fetchable paths
deps: string[]
fn: (modules: object[]) => void
fn: (modules: (ModuleNamespace | undefined)[]) => void
}

interface CacheData {
Expand Down Expand Up @@ -77,16 +82,16 @@ export async function reload(runner: ViteNodeRunner, files: string[]) {
return Promise.all(files.map(file => runner.executeId(file)))
}

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

async function queueUpdate(runner: ViteNodeRunner, p: Promise<(() => void) | undefined>) {
Expand All @@ -103,6 +108,9 @@ async function queueUpdate(runner: ViteNodeRunner, p: Promise<(() => void) | und
}

async function fetchUpdate(runner: ViteNodeRunner, { path, acceptedPath }: Update) {
path = normalizeRequestId(path)
acceptedPath = normalizeRequestId(acceptedPath)

const maps = getCache(runner)
const mod = maps.hotModulesMap.get(path)

Expand All @@ -113,48 +121,29 @@ async function fetchUpdate(runner: ViteNodeRunner, { path, acceptedPath }: Updat
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)
})
}
}
let fetchedModule: ModuleNamespace | undefined

// 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)
}
}),
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
deps.includes(acceptedPath),
)

if (isSelfUpdate || qualifiedCallbacks.length > 0) {
const disposer = maps.disposeMap.get(acceptedPath)
if (disposer)
await disposer(maps.dataMap.get(acceptedPath))
try {
[fetchedModule] = await reload(runner, [acceptedPath])
}
catch (e: any) {
warnFailedFetch(e, acceptedPath)
}
}

return () => {
for (const { deps, fn } of qualifiedCallbacks)
fn(deps.map(dep => moduleMap.get(dep)))
fn(deps.map(dep => (dep === acceptedPath ? fetchedModule : undefined)))

const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`${c.cyan('[vite-node]')} hot updated: ${loggedPath}`)
Expand All @@ -179,36 +168,35 @@ export async function handleMessage(runner: ViteNodeRunner, emitter: HMREmitter,
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(`${c.cyan('[vite-node]')} no support css hmr.}`)
}
})
await notifyListeners(runner, 'vite:beforeUpdate', payload)
await Promise.all(payload.updates.map((update) => {
if (update.type === 'js-update')
return queueUpdate(runner, fetchUpdate(runner, update))

// css-update
console.error(`${c.cyan('[vite-node]')} no support css hmr.}`)
return null
}))
await notifyListeners(runner, 'vite:afterUpdate', payload)
break
case 'full-reload':
notifyListeners(runner, 'vite:beforeFullReload', payload)
await notifyListeners(runner, 'vite:beforeFullReload', payload)
maps.customListenersMap.delete('vite:beforeFullReload')
reload(runner, files)
await reload(runner, files)
break
case 'custom':
await notifyListeners(runner, payload.event, payload.data)
break
case 'prune':
notifyListeners(runner, 'vite:beforePrune', payload)
await notifyListeners(runner, 'vite:beforePrune', payload)
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)
await notifyListeners(runner, 'vite:error', payload)
const err = payload.err
console.error(`${c.cyan('[vite-node]')} Internal Server Error\n${err.message}\n${err.stack}`)
break
Expand Down
7 changes: 7 additions & 0 deletions test/vite-node/src/script.js
@@ -0,0 +1,7 @@
console.error('Hello!')

if (import.meta.hot) {
import.meta.hot.accept(() => {
console.error('Accept')
})
}
17 changes: 17 additions & 0 deletions test/vite-node/test/hmr.test.ts
@@ -0,0 +1,17 @@
import { test } from 'vitest'
import { resolve } from 'pathe'
import { editFile, runViteNodeCli } from '../../test-utils'

test('hmr.accept works correctly', async () => {
const scriptFile = resolve(__dirname, '../src/script.js')

const viteNode = await runViteNodeCli('--watch', scriptFile)

await viteNode.waitForStderr('Hello!')

editFile(scriptFile, content => content.replace('Hello!', 'Hello world!'))

await viteNode.waitForStderr('Hello world!')
await viteNode.waitForStderr('Accept')
await viteNode.waitForStdout(`[vite-node] hot updated: ${scriptFile}`)
})
1 change: 1 addition & 0 deletions test/vite-node/vitest.config.ts
Expand Up @@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: true,
testTimeout: process.env.CI ? 120_000 : 5_000,
},
})