Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hmr): experimental.hmrPartialAccept #7324

Merged
merged 18 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
wip: HMR partial accept
  • Loading branch information
rixo committed Mar 15, 2022
commit e5b3da5cf9d8938563403a0c6666acfb8814cceb
1 change: 1 addition & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"node-forge": "^1.2.1",
"okie": "^1.0.1",
"open": "^8.4.0",
"parse-static-imports": "^1.1.0",
"periscopic": "^2.0.3",
"picocolors": "^1.0.0",
"postcss-import": "^14.0.2",
Expand Down
6 changes: 6 additions & 0 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ export const createHotContext = (ownerPath: string) => {
}
},

// export names (first arg) are irrelevant on the client side, they're
// extracted in the server for propagation
acceptExports(_: string, callback?: any) {
acceptDeps([ownerPath], callback)
},

acceptDeps() {
throw new Error(
`hot.acceptDeps() is deprecated. ` +
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,11 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
moduleGraph.updateModuleInfo(
thisModule,
depModules,
null,
// The root CSS proxy module is self-accepting and should not
// have an explicit accept list
new Set(),
null,
isSelfAccepting,
ssr
)
Expand Down
74 changes: 72 additions & 2 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import fs from 'fs'
import path from 'path'
import { isObject } from '../utils'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import colors from 'picocolors'
import MagicString from 'magic-string'
import type { ImportSpecifier } from 'es-module-lexer'
import { init, parse as parseImports } from 'es-module-lexer'
import parseStaticImports from 'parse-static-imports'
import { isCSSRequest, isDirectCSSRequest } from './css'
import {
isBuiltin,
Expand All @@ -27,7 +29,8 @@ import {
import {
debugHmr,
handlePrunedModules,
lexAcceptedHmrDeps
lexAcceptedHmrDeps,
lexAcceptedHmrExports
} from '../server/hmr'
import {
FS_PREFIX,
Expand Down Expand Up @@ -74,6 +77,42 @@ function markExplicitImport(url: string) {
return url
}

async function extractImportedBindings(
id: string,
source: string,
importSpec: ImportSpecifier,
importedBindings: Map<string, Set<string>>
) {
let bindings = importedBindings.get(id)
if (!bindings) {
bindings = new Set<string>()
importedBindings.set(id, bindings)
}

const exp = source.slice(importSpec.ss, importSpec.se)
const [parsed] = parseStaticImports(exp)
if (!parsed) {
return
}
if (parsed.sideEffectOnly) {
return
}
const isDynamic = importSpec.d > -1
const isMeta = importSpec.d === -2
if (isDynamic || isMeta || parsed.starImport) {
// this basically means the module will be impacted by any change in its dep
bindings.add('*')
}
if (parsed.defaultImport) {
bindings.add('default')
}
if (parsed.namedImports) {
for (const { name } of parsed.namedImports) {
bindings.add(name)
}
}
}

/**
* Server-only plugin that lexes, resolves, rewrites and analyzes url imports.
*
Expand Down Expand Up @@ -111,6 +150,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
tryIndex: false,
extensions: []
})
const enablePartialAccept =
isObject(config.server.hmr) && config.server.hmr.partialAccept
let server: ViteDevServer
let isOptimizedDepUrl: (url: string) => boolean

Expand Down Expand Up @@ -186,6 +227,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
start: number
end: number
}>()
let isPartiallySelfAccepting = false
const acceptedExports = new Set<string>()
const importedBindings = enablePartialAccept
? new Map<string, Set<string>>()
: null
const toAbsoluteUrl = (url: string) =>
path.posix.resolve(path.posix.dirname(importerModule.url), url)

Expand Down Expand Up @@ -327,7 +373,17 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
hasHMR = true
if (source.slice(end + 4, end + 11) === '.accept') {
// further analyze accepted modules
if (
if (source.slice(end + 4, end + 18) === '.acceptExports') {
if (
lexAcceptedHmrExports(
source,
source.indexOf('(', end + 18) + 1,
acceptedExports
)
) {
isPartiallySelfAccepting = true
}
} else if (
lexAcceptedHmrDeps(
source,
source.indexOf('(', end + 11) + 1,
Expand Down Expand Up @@ -487,6 +543,16 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
// make sure to normalize away base
const urlWithoutBase = url.replace(base, '/')
importedUrls.add(urlWithoutBase)

if (enablePartialAccept && importedBindings) {
extractImportedBindings(
resolvedId,
source,
imports[index],
importedBindings
)
}

if (!isDynamicImport) {
// for pre-transforming
staticImportedUrls.add(urlWithoutBase)
Expand Down Expand Up @@ -546,6 +612,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
`${
isSelfAccepting
? `[self-accepts]`
: isPartiallySelfAccepting
? `[accepts-exports]`
: acceptedUrls.size
? `[accepts-deps]`
: `[detected api usage]`
Expand Down Expand Up @@ -601,7 +669,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const prunedImports = await moduleGraph.updateModuleInfo(
importerModule,
importedUrls,
importedBindings,
normalizedAcceptedUrls,
isPartiallySelfAccepting ? acceptedExports : null,
isSelfAccepting,
ssr
)
Expand Down
70 changes: 59 additions & 11 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface HmrOptions {
timeout?: number
overlay?: boolean
server?: Server
partialAccept?: boolean
}

export interface HmrContext {
Expand Down Expand Up @@ -217,6 +218,18 @@ export async function handleFileAddUnlink(
}
}

function areAllImportsAccepted(
importedBindings: Set<string>,
acceptedExports: Set<string>
) {
for (const binding of importedBindings) {
if (!acceptedExports.has(binding)) {
return false
}
}
return true
}

function propagateUpdate(
node: ModuleNode,
boundaries: Set<{
Expand All @@ -242,18 +255,30 @@ function propagateUpdate(
return false
}

if (!node.importers.size) {
return true
}
// A partially accepted module with no importers is considered self accepting,
// because the deal is "there are parts of myself I can't self accept if they
// are used outside of me".
// Also, the imported module (this one) must be updated before the importers,
// so that they do get the fresh imported module when/if they are reloaded.
if (node.acceptedHmrExports) {
boundaries.add({
boundary: node,
acceptedVia: node
})
} else {
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 (
!isCSSRequest(node.url) &&
[...node.importers].every((i) => isCSSRequest(i.url))
) {
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 (
!isCSSRequest(node.url) &&
[...node.importers].every((i) => isCSSRequest(i.url))
) {
return true
}
}

for (const importer of node.importers) {
Expand All @@ -266,6 +291,16 @@ function propagateUpdate(
continue
}

if (node.id && node.acceptedHmrExports && importer.importedBindings) {
const importedBindingsFromNode = importer.importedBindings.get(node.id)
if (
importedBindingsFromNode &&
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)
) {
continue
}
}

if (currentChain.includes(importer)) {
// circular deps is considered dead end
return true
Expand Down Expand Up @@ -431,6 +466,19 @@ export function lexAcceptedHmrDeps(
return false
}

export function lexAcceptedHmrExports(
code: string,
start: number,
exportNames: Set<string>
): boolean {
const urls = new Set<{ url: string; start: number; end: number }>()
lexAcceptedHmrDeps(code, start, urls)
for (const { url } of urls) {
exportNames.add(url)
}
return urls.size > 0
}

function error(pos: number) {
const err = new Error(
`import.meta.accept() can only accept string literals or an ` +
Expand Down
7 changes: 7 additions & 0 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export class ModuleNode {
importers = new Set<ModuleNode>()
importedModules = new Set<ModuleNode>()
acceptedHmrDeps = new Set<ModuleNode>()
acceptedHmrExports: Set<string> | null = null
importedBindings: Map<string, Set<string>> | null = null
isSelfAccepting = false
transformResult: TransformResult | null = null
ssrTransformResult: TransformResult | null = null
Expand Down Expand Up @@ -126,7 +128,9 @@ export class ModuleGraph {
async updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>,
importedBindings: Map<string, Set<string>> | null,
acceptedModules: Set<string | ModuleNode>,
acceptedExports: Set<string> | null,
isSelfAccepting: boolean,
ssr?: boolean
): Promise<Set<ModuleNode> | undefined> {
Expand Down Expand Up @@ -162,6 +166,9 @@ export class ModuleGraph {
: accepted
deps.add(dep)
}
// update accepted hmr exports
mod.acceptedHmrExports = acceptedExports
mod.importedBindings = importedBindings
return noLongerImported
}

Expand Down
Loading