From e6958ae81c731b688d9770a94e6116a51428015a Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Dec 2020 14:00:59 -0500 Subject: [PATCH] wip: dispose no longer imported modules --- packages/vite/src/client/client.ts | 14 +++++++- packages/vite/src/node/plugins/css.ts | 10 ++++-- .../plugins/{rewriteImports.ts => imports.ts} | 24 +++++++------ packages/vite/src/node/plugins/index.ts | 6 ++-- packages/vite/src/node/server/hmr.ts | 35 ++++++++++++++----- packages/vite/src/node/server/moduleGraph.ts | 33 +++++++++++------ packages/vite/types/hmrPayload.d.ts | 6 ++++ 7 files changed, 92 insertions(+), 36 deletions(-) rename packages/vite/src/node/plugins/{rewriteImports.ts => imports.ts} (94%) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index a2b1228db0a7a9..cc547cc5cfa3f5 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -119,6 +119,18 @@ async function handleMessage(payload: HMRPayload) { location.reload() } break + case 'dispose': + // After an HMR update, some modules are no longer imported on the page + // but they may have left behind side effects that need to be cleaned up + // (.e.g style injections) + // TODO Trigger their dispose callbacks. + payload.paths.forEach((path) => { + const fn = disposeMap.get(path) + if (fn) { + fn(dataMap.get(path)) + } + }) + break case 'error': const err = payload.err if (enableOverlay) { @@ -230,7 +242,7 @@ export function updateStyle(id: string, content: string) { sheetsMap.set(id, style) } -function removeStyle(id: string) { +export function removeStyle(id: string) { let style = sheetsMap.get(id) if (style) { if (style instanceof CSSStyleSheet) { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 56e297d74dfde2..9b366460a7a48f 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -74,11 +74,15 @@ export function cssPlugin(config: ResolvedConfig, isBuild: boolean): Plugin { if (isProxyRequest) { debug(`[import] ${chalk.dim(path.relative(config.root, id))}`) return [ - `import { updateStyle } from ${JSON.stringify(CLIENT_PUBLIC_PATH)}`, + `import { updateStyle, removeStyle } from ${JSON.stringify( + CLIENT_PUBLIC_PATH + )}`, + `const id = ${JSON.stringify(id)}`, `const css = ${JSON.stringify(css)}`, - `updateStyle(${JSON.stringify(id)}, css)`, + `updateStyle(id, css)`, `${modulesCode || `export default css`}`, - `import.meta.hot.accept()` + `import.meta.hot.accept()`, + `import.meta.hot.dispose(() => removeStyle(id))` ].join('\n') } else { debug(`[link] ${chalk.dim(path.relative(config.root, id))}`) diff --git a/packages/vite/src/node/plugins/rewriteImports.ts b/packages/vite/src/node/plugins/imports.ts similarity index 94% rename from packages/vite/src/node/plugins/rewriteImports.ts rename to packages/vite/src/node/plugins/imports.ts index e3a54cf768c93c..6ea8f4876966ed 100644 --- a/packages/vite/src/node/plugins/rewriteImports.ts +++ b/packages/vite/src/node/plugins/imports.ts @@ -6,20 +6,23 @@ import { init, parse, ImportSpecifier } from 'es-module-lexer' import { isCSSRequest } from './css' import slash from 'slash' import { createDebugger, prettifyUrl, timeFrom } from '../utils' -import { debugHmr } from '../server/hmr' +import { debugHmr, handleDisposedModules } from '../server/hmr' import { FILE_PREFIX, CLIENT_PUBLIC_PATH } from '../constants' import { RollupError } from 'rollup' import { FAILED_RESOLVE } from './resolve' const isDebug = !!process.env.DEBUG -const debugRewrite = createDebugger('vite:rewrite') +const debugRewrite = createDebugger('vite:imports') const skipRE = /\.(map|json)$/ const canSkip = (id: string) => skipRE.test(id) || isCSSRequest(id) /** - * Server-only plugin that rewrites url imports (bare modules, css/asset imports) - * so that they can be properly handled by the server. + * Server-only plugin that lexes, resolves, rewrites and analyzes url imports. + * + * - Imports are resolved to ensure they exist on disk + * + * - Lexes HMR accept calls and updates import relationships in the module graph * * - Bare module imports are resolved (by @rollup-plugin/node-resolve) to * absolute file paths, e.g. @@ -43,7 +46,7 @@ const canSkip = (id: string) => skipRE.test(id) || isCSSRequest(id) * import './style.css.js' * ``` */ -export function rewritePlugin(config: ResolvedConfig): Plugin { +export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:imports', async transform(source, importer) { @@ -89,11 +92,6 @@ export function rewritePlugin(config: ResolvedConfig): Plugin { // since we are already in the transform phase of the importer, it must // have been loaded so its entry is guaranteed in the module graph. const importerModule = moduleGraph.getModuleById(importer)! - - if (!importerModule) { - debugger - } - const importedUrls = new Set() const acceptedUrls = new Set() const toAbsoluteUrl = (url: string) => @@ -227,13 +225,17 @@ export function rewritePlugin(config: ResolvedConfig): Plugin { } // update the module graph for HMR analysis - await moduleGraph.updateModuleInfo( + const noLongerImportedDeps = await moduleGraph.updateModuleInfo( importerModule, importedUrls, new Set([...acceptedUrls].map(toAbsoluteUrl)), isSelfAccepting ) + if (hasHMR && noLongerImportedDeps) { + handleDisposedModules(noLongerImportedDeps, (this as any).server) + } + isDebug && debugRewrite( `${timeFrom(rewriteStart, timeSpentResolving)} ${prettyImporter}` diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 06d2ff904b179c..1c015020e37ead 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -4,7 +4,7 @@ import jsonPlugin from '@rollup/plugin-json' import { nodeResolve } from '@rollup/plugin-node-resolve' import { resolvePlugin, supportedExts } from './resolve' import { esbuildPlugin } from './esbuild' -import { rewritePlugin } from './rewriteImports' +import { importAnalysisPlugin } from './imports' import { cssPlugin } from './css' import { assetPlugin } from './asset' import { clientInjectionsPlugin } from './clientInjections' @@ -33,6 +33,8 @@ export function resolvePlugins( assetPlugin(config, isBuild), ...postPlugins, // internal server-only plugins are always applied after everything else - ...(isBuild ? [] : [clientInjectionsPlugin(config), rewritePlugin(config)]) + ...(isBuild + ? [] + : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]) ].filter(Boolean) as Plugin[] } diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 3227af89450868..82788d0e75fcd0 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -18,10 +18,13 @@ export interface HmrOptions { overlay?: boolean } -export function handleHMRUpdate(file: string, context: ViteDevServer): any { +export function handleHMRUpdate( + file: string, + { ws, config, moduleGraph }: ViteDevServer +): any { debugHmr(`[file change] ${chalk.dim(file)}`) - if (file === context.config.configPath) { + if (file === config.configPath) { // TODO auto restart server return } @@ -33,14 +36,14 @@ export function handleHMRUpdate(file: string, context: ViteDevServer): any { // html files and the client itself cannot be hot updated. if (file.endsWith('.html') || file.startsWith(CLIENT_DIR)) { - context.ws.send({ + ws.send({ type: 'full-reload', - path: '/' + slash(path.relative(context.config.root, file)) + path: '/' + slash(path.relative(config.root, file)) }) return } - const mods = context.moduleGraph.getModulesByFile(file) + const mods = moduleGraph.getModulesByFile(file) if (!mods) { // loaded but not in the module graph, probably not js debugHmr(`[no module entry] ${chalk.dim(file)}`) @@ -55,7 +58,7 @@ export function handleHMRUpdate(file: string, context: ViteDevServer): any { const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries) if (hasDeadEnd) { debugHmr(`[full reload] ${chalk.dim(file)}`) - context.ws.send({ + ws.send({ type: 'full-reload' }) return @@ -75,19 +78,35 @@ export function handleHMRUpdate(file: string, context: ViteDevServer): any { ) } - context.ws.send({ + ws.send({ type: 'update', updates }) } +export function handleDisposedModules( + mods: Set, + { ws }: ViteDevServer +) { + // 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 + }) + ws.send({ + type: 'dispose', + paths: [...mods].map((m) => m.url) + }) +} + function propagateUpdate( node: ModuleNode, timestamp: number, boundaries: Set, currentChain: ModuleNode[] = [node] ): boolean /* hasDeadEnd */ { - debugger if (node.isSelfAccepting) { boundaries.add(node) // mark current propagation chain dirty. diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 7ccc9b2c93ad35..2e722586839cb0 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -17,6 +17,7 @@ export class ModuleNode { file: string | null = null type: 'js' | 'css' importers = new Set() + importedModules = new Set() acceptedHmrDeps = new Set() isSelfAccepting = false transformResult: TransformResult | null = null @@ -61,33 +62,43 @@ export class ModuleGraph { } } + /** + * Update the module graph based on a module's updated imports information + * If there are dependencies that no longer have any importers, they are + * returned as a Set. + */ async updateModuleInfo( mod: ModuleNode, importedUrls: Set, acceptedUrls: Set, isSelfAccepting: boolean - ) { + ): Promise | undefined> { mod.isSelfAccepting = isSelfAccepting - const prevDeps = mod.acceptedHmrDeps - const newDeps = (mod.acceptedHmrDeps = new Set()) + const prevImports = mod.importedModules + const nextImports = (mod.importedModules = new Set()) + let noLongerImported: Set | undefined + // update import graph for (const depUrl of importedUrls) { const dep = await this.ensureEntry(depUrl) dep.importers.add(mod) - } - for (const depUrl of acceptedUrls) { - const dep = await this.ensureEntry(depUrl) - dep.importers.add(mod) - newDeps.add(dep) + nextImports.add(dep) } // remove the importer from deps that were imported but no longer are. - prevDeps.forEach((dep) => { - if (!newDeps.has(dep)) { + prevImports.forEach((dep) => { + if (!nextImports.has(dep)) { dep.importers.delete(mod) if (!dep.importers.size) { - console.log(`module no longer has importers: ${mod.url}`) + // dependency no longer imported + ;(noLongerImported || (noLongerImported = new Set())).add(dep) } } }) + // update accepted hmr deps + const newDeps = (mod.acceptedHmrDeps = new Set()) + for (const depUrl of acceptedUrls) { + newDeps.add(await this.ensureEntry(depUrl)) + } + return noLongerImported } async ensureEntry(rawUrl: string) { diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hmrPayload.d.ts index 1840c2824142ce..5b441ca0c7852e 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hmrPayload.d.ts @@ -5,6 +5,7 @@ export type HMRPayload = | StyleRemovePayload | CustomPayload | ErrorPayload + | DisposePayload export interface ConnectedPayload { type: 'connected' @@ -22,6 +23,11 @@ export interface Update { timestamp: number } +export interface DisposePayload { + type: 'dispose' + paths: string[] +} + export interface StyleRemovePayload { type: 'css-remove' path: string