From 711a6249f1092ddc95b0fb91ff1c080cdda7d62b Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 29 Jul 2023 20:34:09 +0200 Subject: [PATCH] fix(vite-node): correctly resolve hmr filepath (#3834) --- packages/vite-node/src/cli.ts | 3 + packages/vite-node/src/client.ts | 21 ++++-- packages/vite-node/src/hmr/hmr.ts | 104 +++++++++++++----------------- test/vite-node/src/script.js | 7 ++ test/vite-node/test/hmr.test.ts | 17 +++++ test/vite-node/vitest.config.ts | 1 + 6 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 test/vite-node/src/script.js create mode 100644 test/vite-node/test/hmr.test.ts diff --git a/packages/vite-node/src/cli.ts b/packages/vite-node/src/cli.ts index 77e5900db1e6..39794c21dee9 100644 --- a/packages/vite-node/src/cli.ts +++ b/packages/vite-node/src/cli.ts @@ -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(), ], diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index f930bf83f07d..d2814e122f00 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -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 = { @@ -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) => { diff --git a/packages/vite-node/src/hmr/hmr.ts b/packages/vite-node/src/hmr/hmr.ts index 4346b2c35573..e6c77677b3d2 100644 --- a/packages/vite-node/src/hmr/hmr.ts +++ b/packages/vite-node/src/hmr/hmr.ts @@ -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 & { + [Symbol.toStringTag]: 'Module' +} + const debugHmr = createDebug('vite-node:hmr') export type InferCustomEventPayload = @@ -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 { @@ -77,16 +82,16 @@ export async function reload(runner: ViteNodeRunner, files: string[]) { return Promise.all(files.map(file => runner.executeId(file))) } -function notifyListeners( +async function notifyListeners( runner: ViteNodeRunner, event: T, data: InferCustomEventPayload, -): void -function notifyListeners(runner: ViteNodeRunner, event: string, data: any): void { +): Promise +async function notifyListeners(runner: ViteNodeRunner, event: string, data: any): Promise { 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>) { @@ -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) @@ -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() - 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}`) @@ -179,28 +168,27 @@ 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) @@ -208,7 +196,7 @@ export async function handleMessage(runner: ViteNodeRunner, emitter: HMREmitter, }) 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 diff --git a/test/vite-node/src/script.js b/test/vite-node/src/script.js new file mode 100644 index 000000000000..16b5afa83080 --- /dev/null +++ b/test/vite-node/src/script.js @@ -0,0 +1,7 @@ +console.error('Hello!') + +if (import.meta.hot) { + import.meta.hot.accept(() => { + console.error('Accept') + }) +} diff --git a/test/vite-node/test/hmr.test.ts b/test/vite-node/test/hmr.test.ts new file mode 100644 index 000000000000..af427f2bcaa3 --- /dev/null +++ b/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}`) +}) diff --git a/test/vite-node/vitest.config.ts b/test/vite-node/vitest.config.ts index 1515dfeb73a4..3212242e1663 100644 --- a/test/vite-node/vitest.config.ts +++ b/test/vite-node/vitest.config.ts @@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { clearMocks: true, + testTimeout: process.env.CI ? 120_000 : 5_000, }, })