Skip to content

Commit 83dab7e

Browse files
authored
feat(hmr): experimental.hmrPartialAccept (#7324)
1 parent f0aecba commit 83dab7e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+891
-18
lines changed

packages/vite/LICENSE.md

+57
Original file line numberDiff line numberDiff line change
@@ -1963,6 +1963,35 @@ Repository: git+https://github.com/json5/json5.git
19631963
19641964
---------------------------------------
19651965

1966+
## jsonc-parser
1967+
License: MIT
1968+
By: Microsoft Corporation
1969+
Repository: https://github.com/microsoft/node-jsonc-parser
1970+
1971+
> The MIT License (MIT)
1972+
>
1973+
> Copyright (c) Microsoft
1974+
>
1975+
> Permission is hereby granted, free of charge, to any person obtaining a copy
1976+
> of this software and associated documentation files (the "Software"), to deal
1977+
> in the Software without restriction, including without limitation the rights
1978+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1979+
> copies of the Software, and to permit persons to whom the Software is
1980+
> furnished to do so, subject to the following conditions:
1981+
>
1982+
> The above copyright notice and this permission notice shall be included in all
1983+
> copies or substantial portions of the Software.
1984+
>
1985+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1986+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1987+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1988+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1989+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1990+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1991+
> SOFTWARE.
1992+
1993+
---------------------------------------
1994+
19661995
## launch-editor
19671996
License: MIT
19681997
By: Evan You
@@ -2162,6 +2191,34 @@ Repository: git://github.com/isaacs/minimatch.git
21622191
21632192
---------------------------------------
21642193

2194+
## mlly
2195+
License: MIT
2196+
Repository: unjs/mlly
2197+
2198+
> MIT License
2199+
>
2200+
> Copyright (c) 2022 UnJS
2201+
>
2202+
> Permission is hereby granted, free of charge, to any person obtaining a copy
2203+
> of this software and associated documentation files (the "Software"), to deal
2204+
> in the Software without restriction, including without limitation the rights
2205+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2206+
> copies of the Software, and to permit persons to whom the Software is
2207+
> furnished to do so, subject to the following conditions:
2208+
>
2209+
> The above copyright notice and this permission notice shall be included in all
2210+
> copies or substantial portions of the Software.
2211+
>
2212+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2213+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2214+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2215+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2216+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2217+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2218+
> SOFTWARE.
2219+
2220+
---------------------------------------
2221+
21652222
## mrmime
21662223
License: MIT
21672224
By: Luke Edwards

packages/vite/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"launch-editor-middleware": "^2.4.0",
100100
"magic-string": "^0.26.2",
101101
"micromatch": "^4.0.5",
102+
"mlly": "^0.5.1",
102103
"mrmime": "^1.0.1",
103104
"node-forge": "^1.3.1",
104105
"okie": "^1.0.1",

packages/vite/src/client/client.ts

+6
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,12 @@ export function createHotContext(ownerPath: string): ViteHotContext {
452452
}
453453
},
454454

455+
// export names (first arg) are irrelevant on the client side, they're
456+
// extracted in the server for propagation
457+
acceptExports(_: string | readonly string[], callback?: any) {
458+
acceptDeps([ownerPath], callback && (([mod]) => callback(mod)))
459+
},
460+
455461
dispose(cb) {
456462
disposeMap.set(ownerPath, cb)
457463
},

packages/vite/src/node/config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,14 @@ export interface ExperimentalOptions {
247247
* @default false
248248
*/
249249
importGlobRestoreExtension?: boolean
250+
251+
/**
252+
* Enables support of HMR partial accept via `import.meta.hot.acceptExports`.
253+
*
254+
* @experimental
255+
* @default false
256+
*/
257+
hmrPartialAccept?: boolean
250258
}
251259

252260
export interface LegacyOptions {

packages/vite/src/node/plugins/css.ts

+2
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,11 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
258258
moduleGraph.updateModuleInfo(
259259
thisModule,
260260
depModules,
261+
null,
261262
// The root CSS proxy module is self-accepting and should not
262263
// have an explicit accept list
263264
new Set(),
265+
null,
264266
isSelfAccepting,
265267
ssr
266268
)

packages/vite/src/node/plugins/importAnalysis.ts

+85-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ImportSpecifier } from 'es-module-lexer'
77
import { init, parse as parseImports } from 'es-module-lexer'
88
import { parse as parseJS } from 'acorn'
99
import type { Node } from 'estree'
10+
import { findStaticImports, parseStaticImport } from 'mlly'
1011
import { makeLegalIdentifier } from '@rollup/pluginutils'
1112
import type { ViteDevServer } from '..'
1213
import {
@@ -20,7 +21,8 @@ import {
2021
import {
2122
debugHmr,
2223
handlePrunedModules,
23-
lexAcceptedHmrDeps
24+
lexAcceptedHmrDeps,
25+
lexAcceptedHmrExports
2426
} from '../server/hmr'
2527
import {
2628
cleanUrl,
@@ -84,6 +86,48 @@ function markExplicitImport(url: string) {
8486
return url
8587
}
8688

89+
async function extractImportedBindings(
90+
id: string,
91+
source: string,
92+
importSpec: ImportSpecifier,
93+
importedBindings: Map<string, Set<string>>
94+
) {
95+
let bindings = importedBindings.get(id)
96+
if (!bindings) {
97+
bindings = new Set<string>()
98+
importedBindings.set(id, bindings)
99+
}
100+
101+
const isDynamic = importSpec.d > -1
102+
const isMeta = importSpec.d === -2
103+
if (isDynamic || isMeta) {
104+
// this basically means the module will be impacted by any change in its dep
105+
bindings.add('*')
106+
return
107+
}
108+
109+
const exp = source.slice(importSpec.ss, importSpec.se)
110+
const [match0] = findStaticImports(exp)
111+
if (!match0) {
112+
return
113+
}
114+
const parsed = parseStaticImport(match0)
115+
if (!parsed) {
116+
return
117+
}
118+
if (parsed.namespacedImport) {
119+
bindings.add('*')
120+
}
121+
if (parsed.defaultImport) {
122+
bindings.add('default')
123+
}
124+
if (parsed.namedImports) {
125+
for (const name of Object.keys(parsed.namedImports)) {
126+
bindings.add(name)
127+
}
128+
}
129+
}
130+
87131
/**
88132
* Server-only plugin that lexes, resolves, rewrites and analyzes url imports.
89133
*
@@ -116,6 +160,7 @@ function markExplicitImport(url: string) {
116160
export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
117161
const { root, base } = config
118162
const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH)
163+
const enablePartialAccept = config.experimental?.hmrPartialAccept
119164
let server: ViteDevServer
120165

121166
return {
@@ -143,9 +188,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
143188
const start = performance.now()
144189
await init
145190
let imports: readonly ImportSpecifier[] = []
191+
let exports: readonly string[] = []
146192
source = stripBomTag(source)
147193
try {
148-
imports = parseImports(source)[0]
194+
;[imports, exports] = parseImports(source)
149195
} catch (e: any) {
150196
const isVue = importer.endsWith('.vue')
151197
const maybeJSX = !isVue && isJSRequest(importer)
@@ -204,6 +250,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
204250
start: number
205251
end: number
206252
}>()
253+
let isPartiallySelfAccepting = false
254+
const acceptedExports = new Set<string>()
255+
const importedBindings = enablePartialAccept
256+
? new Map<string, Set<string>>()
257+
: null
207258
const toAbsoluteUrl = (url: string) =>
208259
path.posix.resolve(path.posix.dirname(importerModule.url), url)
209260

@@ -344,7 +395,14 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
344395
hasHMR = true
345396
if (source.slice(end + 4, end + 11) === '.accept') {
346397
// further analyze accepted modules
347-
if (
398+
if (source.slice(end + 4, end + 18) === '.acceptExports') {
399+
lexAcceptedHmrExports(
400+
source,
401+
source.indexOf('(', end + 18) + 1,
402+
acceptedExports
403+
)
404+
isPartiallySelfAccepting = true
405+
} else if (
348406
lexAcceptedHmrDeps(
349407
source,
350408
source.indexOf('(', end + 11) + 1,
@@ -464,6 +522,16 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
464522
// make sure to normalize away base
465523
const urlWithoutBase = url.replace(base, '/')
466524
importedUrls.add(urlWithoutBase)
525+
526+
if (enablePartialAccept && importedBindings) {
527+
extractImportedBindings(
528+
resolvedId,
529+
source,
530+
imports[index],
531+
importedBindings
532+
)
533+
}
534+
467535
if (!isDynamicImport) {
468536
// for pre-transforming
469537
staticImportedUrls.add({ url: urlWithoutBase, id: resolvedId })
@@ -531,6 +599,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
531599
`${
532600
isSelfAccepting
533601
? `[self-accepts]`
602+
: isPartiallySelfAccepting
603+
? `[accepts-exports]`
534604
: acceptedUrls.size
535605
? `[accepts-deps]`
536606
: `[detected api usage]`
@@ -585,10 +655,22 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
585655
if (ssr && importerModule.isSelfAccepting) {
586656
isSelfAccepting = true
587657
}
658+
// a partially accepted module that accepts all its exports
659+
// behaves like a self-accepted module in practice
660+
if (
661+
!isSelfAccepting &&
662+
isPartiallySelfAccepting &&
663+
acceptedExports.size >= exports.length &&
664+
exports.every((name) => acceptedExports.has(name))
665+
) {
666+
isSelfAccepting = true
667+
}
588668
const prunedImports = await moduleGraph.updateModuleInfo(
589669
importerModule,
590670
importedUrls,
671+
importedBindings,
591672
normalizedAcceptedUrls,
673+
isPartiallySelfAccepting ? acceptedExports : null,
592674
isSelfAccepting,
593675
ssr
594676
)

packages/vite/src/node/server/hmr.ts

+58-11
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ export async function handleFileAddUnlink(
201201
}
202202
}
203203

204+
function areAllImportsAccepted(
205+
importedBindings: Set<string>,
206+
acceptedExports: Set<string>
207+
) {
208+
for (const binding of importedBindings) {
209+
if (!acceptedExports.has(binding)) {
210+
return false
211+
}
212+
}
213+
return true
214+
}
215+
204216
function propagateUpdate(
205217
node: ModuleNode,
206218
boundaries: Set<{
@@ -233,18 +245,30 @@ function propagateUpdate(
233245
return false
234246
}
235247

236-
if (!node.importers.size) {
237-
return true
238-
}
248+
// A partially accepted module with no importers is considered self accepting,
249+
// because the deal is "there are parts of myself I can't self accept if they
250+
// are used outside of me".
251+
// Also, the imported module (this one) must be updated before the importers,
252+
// so that they do get the fresh imported module when/if they are reloaded.
253+
if (node.acceptedHmrExports) {
254+
boundaries.add({
255+
boundary: node,
256+
acceptedVia: node
257+
})
258+
} else {
259+
if (!node.importers.size) {
260+
return true
261+
}
239262

240-
// #3716, #3913
241-
// For a non-CSS file, if all of its importers are CSS files (registered via
242-
// PostCSS plugins) it should be considered a dead end and force full reload.
243-
if (
244-
!isCSSRequest(node.url) &&
245-
[...node.importers].every((i) => isCSSRequest(i.url))
246-
) {
247-
return true
263+
// #3716, #3913
264+
// For a non-CSS file, if all of its importers are CSS files (registered via
265+
// PostCSS plugins) it should be considered a dead end and force full reload.
266+
if (
267+
!isCSSRequest(node.url) &&
268+
[...node.importers].every((i) => isCSSRequest(i.url))
269+
) {
270+
return true
271+
}
248272
}
249273

250274
for (const importer of node.importers) {
@@ -257,6 +281,16 @@ function propagateUpdate(
257281
continue
258282
}
259283

284+
if (node.id && node.acceptedHmrExports && importer.importedBindings) {
285+
const importedBindingsFromNode = importer.importedBindings.get(node.id)
286+
if (
287+
importedBindingsFromNode &&
288+
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)
289+
) {
290+
continue
291+
}
292+
}
293+
260294
if (currentChain.includes(importer)) {
261295
// circular deps is considered dead end
262296
return true
@@ -423,6 +457,19 @@ export function lexAcceptedHmrDeps(
423457
return false
424458
}
425459

460+
export function lexAcceptedHmrExports(
461+
code: string,
462+
start: number,
463+
exportNames: Set<string>
464+
): boolean {
465+
const urls = new Set<{ url: string; start: number; end: number }>()
466+
lexAcceptedHmrDeps(code, start, urls)
467+
for (const { url } of urls) {
468+
exportNames.add(url)
469+
}
470+
return urls.size > 0
471+
}
472+
426473
function error(pos: number) {
427474
const err = new Error(
428475
`import.meta.hot.accept() can only accept string literals or an ` +

0 commit comments

Comments
 (0)