From b5479b8be4c4fbe63845fa05c352aa1d8bfff58d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Dec 2020 15:32:17 -0500 Subject: [PATCH] wip: css @import hmr --- packages/vite/src/client/client.ts | 18 +++++-- packages/vite/src/node/plugins/css.ts | 17 +++++-- packages/vite/src/node/plugins/imports.ts | 31 ++++++------ packages/vite/src/node/server/hmr.ts | 5 +- packages/vite/src/node/server/moduleGraph.ts | 49 +++++++++++++++---- .../vite/src/node/server/pluginContainer.ts | 12 ++--- .../vite/src/node/server/transformRequest.ts | 2 +- packages/vite/types/hmrPayload.d.ts | 6 +-- 8 files changed, 95 insertions(+), 45 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index cc547cc5cfa3f5..967454a58914af 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -119,13 +119,13 @@ async function handleMessage(payload: HMRPayload) { location.reload() } break - case 'dispose': + case 'prune': // 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) + const fn = pruneMap.get(path) if (fn) { fn(dataMap.get(path)) } @@ -308,7 +308,12 @@ async function fetchUpdate({ path, changedPath, timestamp }: Update) { for (const { deps, fn } of qualifiedCallbacks) { fn(deps.map((dep) => moduleMap.get(dep))) } - console.log(`[vite]: js module hot updated: `, path) + const updateType = /\.(css|less|sass|scss|styl|stylus|postcss)($|\?)/.test( + path.slice(0, -3) + ) + ? 'css' + : 'js' + console.log(`[vite]: ${updateType} module hot updated: `, path) } } @@ -325,6 +330,7 @@ interface HotCallback { const hotModulesMap = new Map() const disposeMap = new Map void | Promise>() +const pruneMap = new Map void | Promise>() const dataMap = new Map() const customUpdateMap = new Map void)[]>() @@ -384,7 +390,11 @@ export const createHotContext = (ownerPath: string) => { disposeMap.set(ownerPath, cb) }, - // noop, used for static analysis only + prune(cb: (data: any) => void) { + pruneMap.set(ownerPath, cb) + }, + + // TODO decline() {}, invalidate() { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 9b366460a7a48f..e24d9bd3a9a103 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -1,7 +1,7 @@ import { createDebugger } from '../utils' import path from 'path' import fs, { promises as fsp } from 'fs' -import { Plugin, ResolvedConfig } from '..' +import { Plugin, ResolvedConfig, ViteDevServer } from '..' import postcssrc from 'postcss-load-config' import merge from 'merge-source-map' import { SourceMap } from 'rollup' @@ -65,10 +65,17 @@ export function cssPlugin(config: ResolvedConfig, isBuild: boolean): Plugin { // server-only *.css.js proxy module if (!isBuild) { if (deps) { - deps.forEach((file) => { - // TODO record css @import virtual modules for HMR + // record deps in the module graph so edits to @import css can trigger + // main import to hot update + const { moduleGraph } = (this as any).server as ViteDevServer + const thisModule = moduleGraph.getModuleById(id)! + const depModules = new Set( + [...deps].map((file) => moduleGraph.createFileOnlyEntry(file)) + ) + moduleGraph.updateModuleInfo(thisModule, depModules, depModules, true) + for (const file of deps) { this.addWatchFile(file) - }) + } } if (isProxyRequest) { @@ -82,7 +89,7 @@ export function cssPlugin(config: ResolvedConfig, isBuild: boolean): Plugin { `updateStyle(id, css)`, `${modulesCode || `export default css`}`, `import.meta.hot.accept()`, - `import.meta.hot.dispose(() => removeStyle(id))` + `import.meta.hot.prune(() => removeStyle(id))` ].join('\n') } else { debug(`[link] ${chalk.dim(path.relative(config.root, id))}`) diff --git a/packages/vite/src/node/plugins/imports.ts b/packages/vite/src/node/plugins/imports.ts index 6ea8f4876966ed..0c4604cb1cc062 100644 --- a/packages/vite/src/node/plugins/imports.ts +++ b/packages/vite/src/node/plugins/imports.ts @@ -3,16 +3,16 @@ import { Plugin, ResolvedConfig, ViteDevServer } from '..' import chalk from 'chalk' import MagicString from 'magic-string' import { init, parse, ImportSpecifier } from 'es-module-lexer' -import { isCSSRequest } from './css' +import { isCSSProxy, isCSSRequest } from './css' import slash from 'slash' import { createDebugger, prettifyUrl, timeFrom } from '../utils' -import { debugHmr, handleDisposedModules } from '../server/hmr' +import { debugHmr, handlePrunedModules } 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:imports') +const debugRewrite = createDebugger('vite:rewrite') const skipRE = /\.(map|json)$/ const canSkip = (id: string) => skipRE.test(id) || isCSSRequest(id) @@ -177,7 +177,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // its last updated timestamp to force the browser to fetch the most // up-to-date version of this module. try { - const depModule = await moduleGraph.ensureEntry(absoluteUrl) + const depModule = await moduleGraph.ensureEntryFromUrl(absoluteUrl) if (depModule.lastHMRTimestamp > 0) { str().appendLeft( end, @@ -224,16 +224,19 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) } - // update the module graph for HMR analysis - const noLongerImportedDeps = await moduleGraph.updateModuleInfo( - importerModule, - importedUrls, - new Set([...acceptedUrls].map(toAbsoluteUrl)), - isSelfAccepting - ) - - if (hasHMR && noLongerImportedDeps) { - handleDisposedModules(noLongerImportedDeps, (this as any).server) + // update the module graph for HMR analysis. + // node CSS imports does its own graph update in the css plugin so we + // only handle js graph updates here. + if (!isCSSProxy(importer)) { + const prunedImports = await moduleGraph.updateModuleInfo( + importerModule, + importedUrls, + new Set([...acceptedUrls].map(toAbsoluteUrl)), + isSelfAccepting + ) + if (hasHMR && prunedImports) { + handlePrunedModules(prunedImports, (this as any).server) + } } isDebug && diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 82788d0e75fcd0..962d4b59e0766e 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -84,7 +84,7 @@ export function handleHMRUpdate( }) } -export function handleDisposedModules( +export function handlePrunedModules( mods: Set, { ws }: ViteDevServer ) { @@ -94,9 +94,10 @@ export function handleDisposedModules( const t = Date.now() mods.forEach((mod) => { mod.lastHMRTimestamp = t + debugHmr(`[dispose] ${chalk.dim(mod.file)}`) }) ws.send({ - type: 'dispose', + type: 'prune', paths: [...mods].map((m) => m.url) }) } diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 2e722586839cb0..00ca319d07eddd 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -69,8 +69,8 @@ export class ModuleGraph { */ async updateModuleInfo( mod: ModuleNode, - importedUrls: Set, - acceptedUrls: Set, + importedModules: Set, + acceptedModules: Set, isSelfAccepting: boolean ): Promise | undefined> { mod.isSelfAccepting = isSelfAccepting @@ -78,8 +78,11 @@ export class ModuleGraph { const nextImports = (mod.importedModules = new Set()) let noLongerImported: Set | undefined // update import graph - for (const depUrl of importedUrls) { - const dep = await this.ensureEntry(depUrl) + for (const imported of importedModules) { + const dep = + typeof imported === 'string' + ? await this.ensureEntryFromUrl(imported) + : imported dep.importers.add(mod) nextImports.add(dep) } @@ -94,14 +97,18 @@ export class ModuleGraph { } }) // update accepted hmr deps - const newDeps = (mod.acceptedHmrDeps = new Set()) - for (const depUrl of acceptedUrls) { - newDeps.add(await this.ensureEntry(depUrl)) + const deps = (mod.acceptedHmrDeps = new Set()) + for (const accepted of acceptedModules) { + const dep = + typeof accepted === 'string' + ? await this.ensureEntryFromUrl(accepted) + : accepted + deps.add(dep) } return noLongerImported } - async ensureEntry(rawUrl: string) { + async ensureEntryFromUrl(rawUrl: string) { const [url, resolvedId] = await this.resolveUrl(rawUrl) let mod = this.urlToModuleMap.get(url) if (!mod) { @@ -120,6 +127,28 @@ export class ModuleGraph { return mod } + // some deps, like a css file referenced via @import, don't have its own + // url because they are inlined into the main css import. But they still + // need to be represented in the module graph so that they can trigger + // hmr in the importing css file. + createFileOnlyEntry(file: string) { + const url = `/@fs/${file}` + let fileMappedMdoules = this.fileToModulesMap.get(file) + if (!fileMappedMdoules) { + fileMappedMdoules = new Set() + this.fileToModulesMap.set(file, fileMappedMdoules) + } + for (const m of fileMappedMdoules) { + if (m.url === url) { + return m + } + } + const mod = new ModuleNode(url) + mod.file = file + fileMappedMdoules.add(mod) + return mod + } + // for incoming urls, it is important to: // 1. remove the HMR timestamp query (?t=xxxx) // 2. resolve its extension so that urls with or without extension all map to @@ -128,7 +157,9 @@ export class ModuleGraph { url = removeTimestampQuery(url) const resolvedId = (await this.container.resolveId(url)).id if (resolvedId === FAILED_RESOLVE) { - throw Error(`Failed to resolve url: ${url}\nDoes the file exist?`) + throw Error( + `Failed to resolve url: ${unwrapCSSProxy(url)}\nDoes the file exist?` + ) } const ext = extname(cleanUrl(resolvedId)) const [pathname, query] = url.split('?') diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 76bdf3541ecfd0..9ba084b21982bb 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -130,7 +130,6 @@ export async function createPluginContainer( const MODULES = new Map() const files = new Map() const watchFiles = new Set() - const resolveCache = new Map() // get rollup version const rollupPkgPath = resolve(require.resolve('rollup'), '../../package.json') @@ -210,7 +209,10 @@ export async function createPluginContainer( addWatchFile(id: string) { watchFiles.add(id) - watcher.add(id) + // only need to add it if file is out of root. + if (!id.startsWith(root)) { + watcher.add(id) + } } getWatchFiles() { @@ -379,10 +381,6 @@ export async function createPluginContainer( `${rawId}\n${importer}` + (_skip ? _skip.map((p) => p.name).join('\n') : ``) - if (resolveCache.has(key)) { - return resolveCache.get(key)! - } - nestedResolveCall++ const resolveStart = Date.now() @@ -425,10 +423,10 @@ export async function createPluginContainer( if (id) { partial.id = id - resolveCache.set(key, partial as PartialResolvedId) } nestedResolveCall-- + isDebug && !nestedResolveCall && debugResolve( diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 36cdf044b5dbe1..c78ad468747a14 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -74,7 +74,7 @@ export async function transformRequest( } // ensure module in graph after successful load - const mod = await moduleGraph.ensureEntry(url) + const mod = await moduleGraph.ensureEntryFromUrl(url) // file is out of root, add it to the watch list if (mod.file && !mod.file.startsWith(root)) { watcher.add(mod.file) diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hmrPayload.d.ts index 5b441ca0c7852e..eb71e03a62dde9 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hmrPayload.d.ts @@ -5,7 +5,7 @@ export type HMRPayload = | StyleRemovePayload | CustomPayload | ErrorPayload - | DisposePayload + | PrunePayload export interface ConnectedPayload { type: 'connected' @@ -23,8 +23,8 @@ export interface Update { timestamp: number } -export interface DisposePayload { - type: 'dispose' +export interface PrunePayload { + type: 'prune' paths: string[] }