import { ModuleNode, HmrContext } from 'vite'; import { Code, CompileData } from './utils/compile'; import { log, logCompilerWarnings } from './utils/log'; import { SvelteRequest } from './utils/id'; import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache'; import { ResolvedOptions } from './utils/options'; import { toRollupError } from './utils/error'; /** * Vite-specific HMR handling */ export async function handleHotUpdate( compileSvelte: Function, ctx: HmrContext, svelteRequest: SvelteRequest, cache: VitePluginSvelteCache, options: ResolvedOptions ): Promise { if (!cache.has(svelteRequest)) { // file hasn't been requested yet (e.g. async component) log.debug(`handleHotUpdate called before initial transform for ${svelteRequest.id}`); return; } const { read, server, modules } = ctx; const cachedJS = cache.getJS(svelteRequest); const cachedCss = cache.getCSS(svelteRequest); const content = await read(); let compileData: CompileData; try { compileData = await compileSvelte(svelteRequest, content, options); cache.update(compileData); } catch (e) { cache.setError(svelteRequest, e); throw toRollupError(e, options); } const affectedModules = [...modules]; const cssIdx = modules.findIndex((m) => m.id === svelteRequest.cssId); if (cssIdx > -1) { const cssUpdated = cssChanged(cachedCss, compileData.compiled.css); if (!cssUpdated) { log.debug(`skipping unchanged css for ${svelteRequest.cssId}`); affectedModules.splice(cssIdx, 1); } } const jsIdx = modules.findIndex((m) => m.id === svelteRequest.id); if (jsIdx > -1) { const jsUpdated = jsChanged(cachedJS, compileData.compiled.js, svelteRequest.filename); if (!jsUpdated) { log.debug(`skipping unchanged js for ${svelteRequest.id}`); affectedModules.splice(jsIdx, 1); // transform won't be called, log warnings here logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); } } // TODO is this enough? see also: https://github.com/vitejs/vite/issues/2274 const ssrModulesToInvalidate = affectedModules.filter((m) => !!m.ssrTransformResult); if (ssrModulesToInvalidate.length > 0) { log.debug(`invalidating modules ${ssrModulesToInvalidate.map((m) => m.id).join(', ')}`); ssrModulesToInvalidate.forEach((moduleNode) => server.moduleGraph.invalidateModule(moduleNode)); } if (affectedModules.length > 0) { log.debug( `handleHotUpdate for ${svelteRequest.id} result: ${affectedModules .map((m) => m.id) .join(', ')}` ); } return affectedModules; } function cssChanged(prev?: Code, next?: Code): boolean { return !isCodeEqual(prev?.code, next?.code); } function jsChanged(prev?: Code, next?: Code, filename?: string): boolean { const prevJs = prev?.code; const nextJs = next?.code; const isStrictEqual = isCodeEqual(prevJs, nextJs); if (isStrictEqual) { return false; } const isLooseEqual = isCodeEqual(normalizeJsCode(prevJs), normalizeJsCode(nextJs)); if (!isStrictEqual && isLooseEqual) { log.warn( `ignoring compiler output js change for ${filename} as it is equal to previous output after normalization` ); } return !isLooseEqual; } function isCodeEqual(prev?: string, next?: string): boolean { if (!prev && !next) { return true; } if ((!prev && next) || (prev && !next)) { return false; } return prev === next; } /** * remove code that only changes metadata and does not require a js update for the component to keep working * * 1) add_location() calls. These add location metadata to elements, only used by some dev tools * 2) ... maybe more (or less) in the future * @param code */ function normalizeJsCode(code?: string): string | undefined { if (!code) { return code; } return code.replace(/\s*\badd_location\s*\([^)]*\)\s*;?/g, ''); }