Skip to content

Commit

Permalink
fix(vite-node): correctly resolve hmr filepath (#3834)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jul 29, 2023
1 parent 4552185 commit 711a624
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 64 deletions.
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,
},
})

0 comments on commit 711a624

Please sign in to comment.