6.6 · difficulty 3/4 · 16 min read
Hands-on: Implementing HMR for Custom File Types
Bring together everything learned in the previous sections — implement full hot update support for a .yaml config file, including server-side handleHotUpdate logic and client-side import.meta.hot acceptance.
Vite 8.1Stable
Goal: Hot Updates for YAML Config
Turn a YAML config file into a hot-updatable module:
ts
import config from "virtual:app-config"
// Use the config during initialization
initApp(config)
if (import.meta.hot) {
// When the config file hot-updates: automatically re-apply the config
import.meta.hot.accept("virtual:app-config", (newConfig) => {
if (newConfig) {
updateApp(newConfig.default)
console.log("[HMR] config updated:", newConfig.default)
}
})
}Modify app.config.yaml, and the browser applies the new config automatically — no page reload, no loss of application state.
Full Implementation
Step 1: YAML loader plugin skeleton
ts
import type { Plugin } from "vite"
import { readFileSync, existsSync } from "node:fs"
import { resolve } from "node:path"
// Simple YAML parser (use a mature yaml package in production)
function parseSimpleYaml(content: string): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const line of content.split("\n")) {
const colonIdx = line.indexOf(":")
if (colonIdx === -1 || line.trim().startsWith("#")) continue
const key = line.slice(0, colonIdx).trim()
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^["']|["']$/g, "")
if (key) result[key] = value
}
return result
}
const VIRTUAL_ID = "virtual:app-config"
const RESOLVED_ID = "\0" + VIRTUAL_ID
export function yamlConfigPlugin(configFile = "app.config.yaml"): Plugin {
const configPath = resolve(configFile)
function loadConfig(): Record<string, unknown> {
if (!existsSync(configPath)) {
console.warn(`[yaml-config] config file not found: ${configPath}`)
return {}
}
try {
const content = readFileSync(configPath, "utf-8")
return parseSimpleYaml(content)
} catch (e) {
console.error(`[yaml-config] parse failed:`, e)
return {}
}
}
return {
name: "vite-plugin-yaml-config",
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED_ID
return null
},
load(id) {
if (id !== RESOLVED_ID) return null
// Watch the config file for changes
this.addWatchFile(configPath)
const config = loadConfig()
return `export default ${JSON.stringify(config)}`
},
handleHotUpdate({ file, server }) {
if (file !== configPath) return
console.log(`[yaml-config] config file updated: ${configFile}`)
// Invalidate the virtual module
const module = server.moduleGraph.getModuleById(RESOLVED_ID)
if (module) {
server.moduleGraph.invalidateModule(module)
return [module]
}
return []
},
}
}Step 2: Use the plugin
ts
import { defineConfig } from "vite"
import { yamlConfigPlugin } from "./src/yaml-config-plugin"
export default defineConfig({
plugins: [yamlConfigPlugin("app.config.yaml")],
})Step 3: Create the config file
yaml
apiUrl: https://api.example.com
theme: dark
language: en
featureFlags: experimentalFeature,newUIStep 4: Type declarations
ts
declare module "virtual:app-config" {
const config: {
apiUrl: string
theme: string
language: string
featureFlags: string
[key: string]: string
}
export default config
}Step 5: Use it in the application
ts
import config from "virtual:app-config"
// Initialize the app
const app = createApp(config)
if (import.meta.hot) {
// Accept updates from virtual:app-config
import.meta.hot.accept("virtual:app-config", (newModule) => {
if (!newModule) return
const newConfig = newModule.default
console.log("[HMR] app config hot updated:", newConfig)
// Re-apply the config
app.updateConfig(newConfig)
// If certain config changes require a full reload, use invalidate
if (newConfig.theme !== config.theme) {
console.log("[HMR] theme changed, may need a reload")
// import.meta.hot!.invalidate() // uncomment if a reload is needed
}
})
}Error Handling: Graceful Degradation on Syntax Errors
ts
// Add error handling inside the load hook
load(id) {
if (id !== RESOLVED_ID) return null
this.addWatchFile(configPath)
let config: Record<string, unknown>
try {
config = loadConfig()
} catch (e) {
// When the config file has a syntax error, return the last successful config
// and show an error overlay in the browser
this.error(`YAML config file parse failed: ${e instanceof Error ? e.message : e}`)
return null // this.error throws, so this line is never reached
}
return `export default ${JSON.stringify(config)}`
},Browser Side: Complete HMR Acceptance Logic
ts
import config from "virtual:app-config"
type Config = typeof config
let currentConfig: Config = config
export function getConfig(): Config {
return currentConfig
}
export function onConfigUpdate(callback: (config: Config) => void) {
if (import.meta.hot) {
import.meta.hot.accept("virtual:app-config", (newModule) => {
if (!newModule) return
currentConfig = newModule.default
callback(currentConfig)
})
}
}Self-check
- Why do you call
this.addWatchFile()inside theloadhook rather than insidehandleHotUpdate? - What happens when
this.error()is called due to a YAML syntax error? What does the browser display? - In the client-side
accept()callback,newModulecan sometimes beundefined. Under what circumstances does that happen? - Why use
accept("virtual:app-config", callback)instead ofaccept(callback)? What is the difference between the two?
ts
// Extend yamlConfigPlugin to:
// 1. Watch all .yaml files under a directory
// e.g. the configs/ directory contains database.yaml / redis.yaml / app.yaml
// 2. Each file maps to a virtual module: virtual:config/database, virtual:config/redis, etc.
// 3. When any file changes, only update the corresponding virtual module — leave others untouched
import type { Plugin } from "vite"
import { readdirSync } from "node:fs"
import { resolve, basename } from "node:path"
export function multiYamlPlugin(configDir: string): Plugin {
const dir = resolve(configDir)
return {
name: "vite-plugin-multi-yaml",
resolveId(id) {
// TODO: handle IDs in the virtual:config/<name> format
},
load(id) {
// TODO: find the corresponding yaml file from the ID and load it
},
handleHotUpdate({ file, server }) {
// TODO: when a yaml file under configDir changes, update only the corresponding virtual module
},
}
}