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 all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/vite/LICENSE.md
Expand Up @@ -1963,6 +1963,35 @@ Repository: git+https://github.com/json5/json5.git

---------------------------------------

## jsonc-parser
License: MIT
By: Microsoft Corporation
Repository: https://github.com/microsoft/node-jsonc-parser

> The MIT License (MIT)
>
> Copyright (c) Microsoft
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.

---------------------------------------

## launch-editor
License: MIT
By: Evan You
Expand Down Expand Up @@ -2162,6 +2191,34 @@ Repository: git://github.com/isaacs/minimatch.git

---------------------------------------

## mlly
License: MIT
Repository: unjs/mlly

> MIT License
>
> Copyright (c) 2022 UnJS
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.

---------------------------------------

## mrmime
License: MIT
By: Luke Edwards
Expand Down
1 change: 1 addition & 0 deletions packages/vite/package.json
Expand Up @@ -99,6 +99,7 @@
"launch-editor-middleware": "^2.4.0",
"magic-string": "^0.26.2",
"micromatch": "^4.0.5",
"mlly": "^0.5.1",
"mrmime": "^1.0.1",
"node-forge": "^1.3.1",
"okie": "^1.0.1",
Expand Down
6 changes: 6 additions & 0 deletions packages/vite/src/client/client.ts
Expand Up @@ -452,6 +452,12 @@ export function createHotContext(ownerPath: string): ViteHotContext {
}
},

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

dispose(cb) {
disposeMap.set(ownerPath, cb)
},
Expand Down
8 changes: 8 additions & 0 deletions packages/vite/src/node/config.ts
Expand Up @@ -247,6 +247,14 @@ export interface ExperimentalOptions {
* @default false
*/
importGlobRestoreExtension?: boolean

/**
* Enables support of HMR partial accept via `import.meta.hot.acceptExports`.
*
* @experimental
* @default false
*/
hmrPartialAccept?: boolean
}

export interface LegacyOptions {
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -258,9 +258,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
88 changes: 85 additions & 3 deletions packages/vite/src/node/plugins/importAnalysis.ts
Expand Up @@ -7,6 +7,7 @@ import type { ImportSpecifier } from 'es-module-lexer'
import { init, parse as parseImports } from 'es-module-lexer'
import { parse as parseJS } from 'acorn'
import type { Node } from 'estree'
import { findStaticImports, parseStaticImport } from 'mlly'
import { makeLegalIdentifier } from '@rollup/pluginutils'
import type { ViteDevServer } from '..'
import {
Expand All @@ -20,7 +21,8 @@ import {
import {
debugHmr,
handlePrunedModules,
lexAcceptedHmrDeps
lexAcceptedHmrDeps,
lexAcceptedHmrExports
} from '../server/hmr'
import {
cleanUrl,
Expand Down Expand Up @@ -84,6 +86,48 @@ 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 isDynamic = importSpec.d > -1
const isMeta = importSpec.d === -2
if (isDynamic || isMeta) {
// this basically means the module will be impacted by any change in its dep
bindings.add('*')
return
}

const exp = source.slice(importSpec.ss, importSpec.se)
const [match0] = findStaticImports(exp)
if (!match0) {
return
}
const parsed = parseStaticImport(match0)
if (!parsed) {
return
}
if (parsed.namespacedImport) {
bindings.add('*')
}
if (parsed.defaultImport) {
bindings.add('default')
}
if (parsed.namedImports) {
for (const name of Object.keys(parsed.namedImports)) {
bindings.add(name)
}
}
}

/**
* Server-only plugin that lexes, resolves, rewrites and analyzes url imports.
*
Expand Down Expand Up @@ -116,6 +160,7 @@ function markExplicitImport(url: string) {
export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const { root, base } = config
const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH)
const enablePartialAccept = config.experimental?.hmrPartialAccept
let server: ViteDevServer

return {
Expand Down Expand Up @@ -143,9 +188,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const start = performance.now()
await init
let imports: readonly ImportSpecifier[] = []
let exports: readonly string[] = []
source = stripBomTag(source)
try {
imports = parseImports(source)[0]
;[imports, exports] = parseImports(source)
} catch (e: any) {
const isVue = importer.endsWith('.vue')
const maybeJSX = !isVue && isJSRequest(importer)
Expand Down Expand Up @@ -204,6 +250,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 @@ -344,7 +395,14 @@ 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') {
lexAcceptedHmrExports(
source,
source.indexOf('(', end + 18) + 1,
acceptedExports
)
isPartiallySelfAccepting = true
} else if (
lexAcceptedHmrDeps(
source,
source.indexOf('(', end + 11) + 1,
Expand Down Expand Up @@ -464,6 +522,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({ url: urlWithoutBase, id: resolvedId })
Expand Down Expand Up @@ -531,6 +599,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
`${
isSelfAccepting
? `[self-accepts]`
: isPartiallySelfAccepting
? `[accepts-exports]`
: acceptedUrls.size
? `[accepts-deps]`
: `[detected api usage]`
Expand Down Expand Up @@ -585,10 +655,22 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
if (ssr && importerModule.isSelfAccepting) {
isSelfAccepting = true
}
// a partially accepted module that accepts all its exports
// behaves like a self-accepted module in practice
if (
!isSelfAccepting &&
isPartiallySelfAccepting &&
acceptedExports.size >= exports.length &&
exports.every((name) => acceptedExports.has(name))
) {
isSelfAccepting = true
}
const prunedImports = await moduleGraph.updateModuleInfo(
importerModule,
importedUrls,
importedBindings,
normalizedAcceptedUrls,
isPartiallySelfAccepting ? acceptedExports : null,
isSelfAccepting,
ssr
)
Expand Down
69 changes: 58 additions & 11 deletions packages/vite/src/node/server/hmr.ts
Expand Up @@ -201,6 +201,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 Down Expand Up @@ -233,18 +245,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 @@ -257,6 +281,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 @@ -423,6 +457,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.hot.accept() can only accept string literals or an ` +
Expand Down