Skip to content

Commit

Permalink
wip: css @import hmr
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Dec 14, 2020
1 parent e6958ae commit b5479b8
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 45 deletions.
18 changes: 14 additions & 4 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -325,6 +330,7 @@ interface HotCallback {

const hotModulesMap = new Map<string, HotModule>()
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
const pruneMap = new Map<string, (data: any) => void | Promise<void>>()
const dataMap = new Map<string, any>()
const customUpdateMap = new Map<string, ((customData: any) => void)[]>()

Expand Down Expand Up @@ -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() {
Expand Down
17 changes: 12 additions & 5 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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))}`)
Expand Down
31 changes: 17 additions & 14 deletions packages/vite/src/node/plugins/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 &&
Expand Down
5 changes: 3 additions & 2 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function handleHMRUpdate(
})
}

export function handleDisposedModules(
export function handlePrunedModules(
mods: Set<ModuleNode>,
{ ws }: ViteDevServer
) {
Expand All @@ -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)
})
}
Expand Down
49 changes: 40 additions & 9 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,20 @@ export class ModuleGraph {
*/
async updateModuleInfo(
mod: ModuleNode,
importedUrls: Set<string>,
acceptedUrls: Set<string>,
importedModules: Set<string | ModuleNode>,
acceptedModules: Set<string | ModuleNode>,
isSelfAccepting: boolean
): Promise<Set<ModuleNode> | undefined> {
mod.isSelfAccepting = isSelfAccepting
const prevImports = mod.importedModules
const nextImports = (mod.importedModules = new Set())
let noLongerImported: Set<ModuleNode> | 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)
}
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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('?')
Expand Down
12 changes: 5 additions & 7 deletions packages/vite/src/node/server/pluginContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export async function createPluginContainer(
const MODULES = new Map()
const files = new Map<string, EmittedFile>()
const watchFiles = new Set<string>()
const resolveCache = new Map<string, PartialResolvedId>()

// get rollup version
const rollupPkgPath = resolve(require.resolve('rollup'), '../../package.json')
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -425,10 +423,10 @@ export async function createPluginContainer(

if (id) {
partial.id = id
resolveCache.set(key, partial as PartialResolvedId)
}

nestedResolveCall--

isDebug &&
!nestedResolveCall &&
debugResolve(
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions packages/vite/types/hmrPayload.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type HMRPayload =
| StyleRemovePayload
| CustomPayload
| ErrorPayload
| DisposePayload
| PrunePayload

export interface ConnectedPayload {
type: 'connected'
Expand All @@ -23,8 +23,8 @@ export interface Update {
timestamp: number
}

export interface DisposePayload {
type: 'dispose'
export interface PrunePayload {
type: 'prune'
paths: string[]
}

Expand Down

0 comments on commit b5479b8

Please sign in to comment.