UNPKG

vite

Version:

Native-ESM powered web dev build tool

494 lines (455 loc) 13 kB
import fs from 'fs' import path from 'path' import chalk from 'chalk' import { createServer, ViteDevServer } from '..' import { createDebugger, normalizePath } from '../utils' import { ModuleNode } from './moduleGraph' import { Update } from 'types/hmrPayload' import { CLIENT_DIR } from '../constants' import { RollupError } from 'rollup' import { prepareError } from './middlewares/error' import match from 'minimatch' import { Server } from 'http' import { cssLangRE } from '../plugins/css' export const debugHmr = createDebugger('vite:hmr') const normalizedClientDir = normalizePath(CLIENT_DIR) export interface HmrOptions { protocol?: string host?: string port?: number clientPort?: number path?: string timeout?: number overlay?: boolean server?: Server } export interface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer } function getShortName(file: string, root: string) { return file.startsWith(root + '/') ? path.posix.relative(root, file) : file } export async function handleHMRUpdate( file: string, server: ViteDevServer ): Promise<any> { const { ws, config, moduleGraph } = server const shortFile = getShortName(file, config.root) const isConfig = file === config.configFile const isConfigDependency = config.configFileDependencies.some( (name) => file === path.resolve(name) ) const isEnv = config.inlineConfig.envFile !== false && file.endsWith('.env') if (isConfig || isConfigDependency || isEnv) { // auto restart server debugHmr(`[config change] ${chalk.dim(shortFile)}`) config.logger.info( chalk.green( `${path.relative(process.cwd(), file)} changed, restarting server...` ), { clear: true, timestamp: true } ) await restartServer(server) return } debugHmr(`[file change] ${chalk.dim(shortFile)}`) // (dev only) the client itself cannot be hot updated. if (file.startsWith(normalizedClientDir)) { ws.send({ type: 'full-reload', path: '*' }) return } const mods = moduleGraph.getModulesByFile(file) // check if any plugin wants to perform custom HMR handling const timestamp = Date.now() const hmrContext: HmrContext = { file, timestamp, modules: mods ? [...mods] : [], read: () => readModifiedFile(file), server } for (const plugin of config.plugins) { if (plugin.handleHotUpdate) { const filteredModules = await plugin.handleHotUpdate(hmrContext) if (filteredModules) { hmrContext.modules = filteredModules } } } if (!hmrContext.modules.length) { // html file cannot be hot updated if (file.endsWith('.html')) { config.logger.info(chalk.green(`page reload `) + chalk.dim(shortFile), { clear: true, timestamp: true }) ws.send({ type: 'full-reload', path: config.server.middlewareMode ? '*' : '/' + normalizePath(path.relative(config.root, file)) }) } else { // loaded but not in the module graph, probably not js debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`) } return } updateModules(shortFile, hmrContext.modules, timestamp, server) } function updateModules( file: string, modules: ModuleNode[], timestamp: number, { config, ws }: ViteDevServer ) { const updates: Update[] = [] const invalidatedModules = new Set<ModuleNode>() let needFullReload = false for (const mod of modules) { invalidate(mod, timestamp, invalidatedModules) if (needFullReload) { continue } const boundaries = new Set<{ boundary: ModuleNode acceptedVia: ModuleNode }>() const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries) if (hasDeadEnd) { needFullReload = true continue } updates.push( ...[...boundaries].map(({ boundary, acceptedVia }) => ({ type: `${boundary.type}-update` as Update['type'], timestamp, path: boundary.url, acceptedPath: acceptedVia.url })) ) } if (needFullReload) { config.logger.info(chalk.green(`page reload `) + chalk.dim(file), { clear: true, timestamp: true }) ws.send({ type: 'full-reload' }) } else { config.logger.info( updates .map(({ path }) => chalk.green(`hmr update `) + chalk.dim(path)) .join('\n'), { clear: true, timestamp: true } ) ws.send({ type: 'update', updates }) } } export async function handleFileAddUnlink( file: string, server: ViteDevServer, isUnlink = false ): Promise<void> { const modules = [...(server.moduleGraph.getModulesByFile(file) ?? [])] if (isUnlink && file in server._globImporters) { delete server._globImporters[file] } else { for (const i in server._globImporters) { const { module, importGlobs } = server._globImporters[i] for (const { base, pattern } of importGlobs) { if (match(file, pattern) || match(path.relative(base, file), pattern)) { modules.push(module) // We use `onFileChange` to invalidate `module.file` so that subsequent `ssrLoadModule()` // calls get fresh glob import results with(out) the newly added(/removed) `file`. server.moduleGraph.onFileChange(module.file!) break } } } } if (modules.length > 0) { updateModules( getShortName(file, server.config.root), modules, Date.now(), server ) } } function propagateUpdate( node: ModuleNode, timestamp: number, boundaries: Set<{ boundary: ModuleNode acceptedVia: ModuleNode }>, currentChain: ModuleNode[] = [node] ): boolean /* hasDeadEnd */ { if (node.isSelfAccepting) { boundaries.add({ boundary: node, acceptedVia: node }) // additionally check for CSS importers, since a PostCSS plugin like // Tailwind JIT may register any file as a dependency to a CSS file. for (const importer of node.importers) { if (cssLangRE.test(importer.url) && !currentChain.includes(importer)) { propagateUpdate( importer, timestamp, boundaries, currentChain.concat(importer) ) } } return false } if (!node.importers.size) { return true } // #3716, #3913 // For a non-CSS file, if all of its importers are CSS files (registered via // PostCSS plugins) it should be considered a dead end and force full reload. if ( !cssLangRE.test(node.url) && [...node.importers].every((i) => cssLangRE.test(i.url)) ) { return true } for (const importer of node.importers) { const subChain = currentChain.concat(importer) if (importer.acceptedHmrDeps.has(node)) { boundaries.add({ boundary: importer, acceptedVia: node }) continue } if (currentChain.includes(importer)) { // circular deps is considered dead end return true } if (propagateUpdate(importer, timestamp, boundaries, subChain)) { return true } } return false } function invalidate(mod: ModuleNode, timestamp: number, seen: Set<ModuleNode>) { if (seen.has(mod)) { return } seen.add(mod) mod.lastHMRTimestamp = timestamp mod.transformResult = null mod.ssrModule = null mod.ssrTransformResult = null mod.importers.forEach((importer) => { if (!importer.acceptedHmrDeps.has(mod)) { invalidate(importer, timestamp, seen) } }) } export function handlePrunedModules( mods: Set<ModuleNode>, { ws }: ViteDevServer ): void { // update the disposed modules' hmr timestamp // since if it's re-imported, it should re-apply side effects // and without the timestamp the browser will not re-import it! const t = Date.now() mods.forEach((mod) => { mod.lastHMRTimestamp = t debugHmr(`[dispose] ${chalk.dim(mod.file)}`) }) ws.send({ type: 'prune', paths: [...mods].map((m) => m.url) }) } const enum LexerState { inCall, inSingleQuoteString, inDoubleQuoteString, inTemplateString, inArray } /** * Lex import.meta.hot.accept() for accepted deps. * Since hot.accept() can only accept string literals or array of string * literals, we don't really need a heavy @babel/parse call on the entire source. * * @returns selfAccepts */ export function lexAcceptedHmrDeps( code: string, start: number, urls: Set<{ url: string; start: number; end: number }> ): boolean { let state: LexerState = LexerState.inCall // the state can only be 2 levels deep so no need for a stack let prevState: LexerState = LexerState.inCall let currentDep: string = '' function addDep(index: number) { urls.add({ url: currentDep, start: index - currentDep.length - 1, end: index + 1 }) currentDep = '' } for (let i = start; i < code.length; i++) { const char = code.charAt(i) switch (state) { case LexerState.inCall: case LexerState.inArray: if (char === `'`) { prevState = state state = LexerState.inSingleQuoteString } else if (char === `"`) { prevState = state state = LexerState.inDoubleQuoteString } else if (char === '`') { prevState = state state = LexerState.inTemplateString } else if (/\s/.test(char)) { continue } else { if (state === LexerState.inCall) { if (char === `[`) { state = LexerState.inArray } else { // reaching here means the first arg is neither a string literal // nor an Array literal (direct callback) or there is no arg // in both case this indicates a self-accepting module return true // done } } else if (state === LexerState.inArray) { if (char === `]`) { return false // done } else if (char === ',') { continue } else { error(i) } } } break case LexerState.inSingleQuoteString: if (char === `'`) { addDep(i) if (prevState === LexerState.inCall) { // accept('foo', ...) return false } else { state = prevState } } else { currentDep += char } break case LexerState.inDoubleQuoteString: if (char === `"`) { addDep(i) if (prevState === LexerState.inCall) { // accept('foo', ...) return false } else { state = prevState } } else { currentDep += char } break case LexerState.inTemplateString: if (char === '`') { addDep(i) if (prevState === LexerState.inCall) { // accept('foo', ...) return false } else { state = prevState } } else if (char === '$' && code.charAt(i + 1) === '{') { error(i) } else { currentDep += char } break default: throw new Error('unknown import.meta.hot lexer state') } } return false } function error(pos: number) { const err = new Error( `import.meta.accept() can only accept string literals or an ` + `Array of string literals.` ) as RollupError err.pos = pos throw err } // vitejs/vite#610 when hot-reloading Vue files, we read immediately on file // change event and sometimes this can be too early and get an empty buffer. // Poll until the file's modified time has changed before reading again. async function readModifiedFile(file: string): Promise<string> { const content = fs.readFileSync(file, 'utf-8') if (!content) { const mtime = fs.statSync(file).mtimeMs await new Promise((r) => { let n = 0 const poll = async () => { n++ const newMtime = fs.statSync(file).mtimeMs if (newMtime !== mtime || n > 10) { r(0) } else { setTimeout(poll, 10) } } setTimeout(poll, 10) }) return fs.readFileSync(file, 'utf-8') } else { return content } } async function restartServer(server: ViteDevServer) { // @ts-ignore global.__vite_start_time = Date.now() let newServer = null try { newServer = await createServer(server.config.inlineConfig) } catch (err) { server.ws.send({ type: 'error', err: prepareError(err) }) return } await server.close() for (const key in newServer) { if (key !== 'app') { // @ts-ignore server[key] = newServer[key] } } if (!server.config.server.middlewareMode) { await server.listen(undefined, true) } else { server.config.logger.info('server restarted.', { timestamp: true }) } }