Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/vite/src/client/bundledDevClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
transport,
} from './client'

declare const __HMR_PARTIAL_ACCEPT__: boolean

// keep the same public exports as `client.ts`, which this entry replaces when inlined
export {
createHotContext,
Expand Down Expand Up @@ -55,6 +57,7 @@ if (typeof DevRuntime !== 'undefined') {
runtime,
{
base,
partialAccept: __HMR_PARTIAL_ACCEPT__,
beforeApply: clearOverlayOrReloadOnFirstUpdate,
},
)
Expand Down
55 changes: 47 additions & 8 deletions packages/vite/src/client/bundledDevHmrClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { NormalizedModuleRunnerTransport } from '../shared/moduleRunnerTran
/** the subset of `__rolldown_runtime__` the HMR client uses */
export interface RolldownRuntimeLike {
getImporters(id: string): string[]
getImportedBindings(importer: string, id: string): string[] | undefined
isExecuted(id: string): boolean
hasFactory(id: string): boolean
removeModuleCache(id: string): void
Expand All @@ -33,6 +34,7 @@ type HmrUpdate =

export interface BundledDevHMRClientOptions {
base: string
partialAccept: boolean
/** returning `'reload'` aborts the apply — the hook reloads the page itself */
beforeApply: () => 'reload' | 'continue'
}
Expand All @@ -56,17 +58,43 @@ export class BundledDevHMRClient extends HMRClient {
}

isSelfAccepted(id: string): boolean {
return (
this.hotModulesMap.get(id)?.callbacks.some((c) => c.deps.includes(id)) ??
false
)
return this.acceptsDep(id, id)
}

acceptsDep(parent: string, id: string): boolean {
return (
this.hotModulesMap
.get(parent)
?.callbacks.some((c) => c.deps.includes(id)) ?? false
?.callbacks.some((c) => !c.exports && c.deps.includes(id)) ?? false
)
}

private getAcceptedExports(id: string): Set<string> | undefined {
let accepted: Set<string> | undefined
for (const c of this.hotModulesMap.get(id)?.callbacks ?? []) {
if (!c.exports) continue
accepted ??= new Set()
for (const name of c.exports) accepted.add(name)
}
return accepted
}

private acceptsAllExports(id: string, accepted: Set<string>): boolean {
const exports = this.runtime.loadExports(id)
if (typeof exports !== 'object' || exports == null) return false
return Object.keys(exports).every((name) => accepted.has(name))
}

private importsOnlyAccepted(
parent: string,
id: string,
accepted: Set<string>,
): boolean {
if (!this.options.partialAccept) return false
return (
this.runtime
.getImportedBindings(parent, id)
?.every((name) => accepted.has(name)) ?? false
)
}

Expand Down Expand Up @@ -113,18 +141,23 @@ export class BundledDevHMRClient extends HMRClient {
reason: `update propagated back to ${firstInvalidatedBy}, which already called \`import.meta.hot.invalidate()\``,
}
}
if (this.isSelfAccepted(id)) {
const acceptedExports = this.getAcceptedExports(id)
const selfAccepted =
this.isSelfAccepted(id) ||
(acceptedExports !== undefined &&
this.acceptsAllExports(id, acceptedExports))
if (selfAccepted || acceptedExports) {
boundaries.push({
boundary: id,
acceptedVia: id,
isWithinCircularImport: this.isNodeWithinCircularImports(id, stack),
})
return
if (selfAccepted) return
}
const parents = this.runtime
.getImporters(id)
.filter((p) => this.runtime.isExecuted(p))
if (!parents.length) {
if (!acceptedExports && !parents.length) {
return {
type: 'full-reload',
reason: `no hmr boundary found for module \`${id}\``,
Expand All @@ -143,6 +176,12 @@ export class BundledDevHMRClient extends HMRClient {
})
continue
}
if (
acceptedExports &&
this.importsOnlyAccepted(parent, id, acceptedExports)
) {
continue
}
if (!stack.includes(parent)) {
const fullReload = this.bubble(
parent,
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/node/plugins/clientInjections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ async function createClientConfigValueReplacer(
const hmrTimeoutReplacement = escapeReplacement(timeout)
const hmrEnableOverlayReplacement = escapeReplacement(overlay)
const hmrConfigNameReplacement = escapeReplacement(hmrConfigName)
const hmrPartialAcceptReplacement = escapeReplacement(
config.experimental.hmrPartialAccept,
)
const wsTokenReplacement = escapeReplacement(config.webSocketToken)
const serverForwardConsoleReplacement = escapeReplacement(
config.server.forwardConsole as any,
Expand All @@ -144,6 +147,7 @@ async function createClientConfigValueReplacer(
.replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement)
.replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement)
.replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement)
.replace(`__HMR_PARTIAL_ACCEPT__`, hmrPartialAcceptReplacement)
.replace(`__WS_TOKEN__`, wsTokenReplacement)
.replace(`__SERVER_FORWARD_CONSOLE__`, serverForwardConsoleReplacement)
}
Expand Down
13 changes: 9 additions & 4 deletions packages/vite/src/shared/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface HotCallback {
// the dependencies must be fetchable paths
deps: string[]
fn: (modules: Array<ModuleNamespace | undefined>) => void
exports?: readonly string[]
}

export interface HMRLogger {
Expand Down Expand Up @@ -75,13 +76,15 @@ export class HMRContext implements ViteHotContext {
}
}

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

dispose(cb: (data: any) => void): void {
Expand Down Expand Up @@ -154,6 +157,7 @@ export class HMRContext implements ViteHotContext {
private acceptDeps(
deps: string[],
callback: HotCallback['fn'] = () => {},
exports?: readonly string[],
): void {
const mod: HotModule = this.hmrClient.hotModulesMap.get(this.ownerPath) || {
id: this.ownerPath,
Expand All @@ -162,6 +166,7 @@ export class HMRContext implements ViteHotContext {
mod.callbacks.push({
deps,
fn: callback,
exports,
})
this.hmrClient.hotModulesMap.set(this.ownerPath, mod)
}
Expand Down
44 changes: 21 additions & 23 deletions playground/hmr/__tests__/hmr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
editFile,
getBg,
getColor,
gotoLatestBuild,
hotUpdatedLog,
isBuild,
isBundledDev,
page,
Expand Down Expand Up @@ -513,26 +515,25 @@ if (!isBuild) {
)
})

// bundled dev: partial accept (`import.meta.hot.acceptExports`) is not
// supported (rolldown#10061)
describe.skipIf(isBundledDev)('acceptExports', () => {
describe('acceptExports', () => {
const HOT_UPDATED = /hot updated/
const CONNECTED = /connected/

const baseDir = 'accept-exports'
const openPage = (testDir: string) =>
gotoLatestBuild(`${viteTestUrl}/${testDir}/`)

describe('when all used exports are accepted', () => {
const testDir = baseDir + '/main-accepted'

const fileName = 'target.ts'
const file = `${testDir}/${fileName}`
const url = '/' + file

let dep = 'dep0'

beforeAll(async () => {
await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, />>>>>>/],
(logs) => {
expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`)
Expand All @@ -543,7 +544,6 @@ if (!isBuild) {

it('the callback is called with the new version the module', async () => {
const callbackFile = `${testDir}/callback.ts`
const callbackUrl = '/' + callbackFile

await untilBrowserLogAfter(
() => {
Expand All @@ -557,7 +557,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
'reloaded >>> Y',
`[vite] hot updated: ${callbackUrl}`,
hotUpdatedLog(callbackFile),
])
},
)
Expand All @@ -572,7 +572,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
'reloaded (2) >>> Z',
`[vite] hot updated: ${callbackUrl}`,
hotUpdatedLog(callbackFile),
])
},
)
Expand All @@ -593,7 +593,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
`<<<<<< A0 B0 D0 ; ${dep}`,
`[vite] hot updated: ${url}`,
hotUpdatedLog(file),
])
},
)
Expand All @@ -608,7 +608,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
`<<<<<< A1 B1 D1 ; ${dep}`,
`[vite] hot updated: ${url}`,
hotUpdatedLog(file),
])
},
)
Expand All @@ -632,7 +632,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
`<<<<<< A2 B2 D2 ; ${dep}`,
`[vite] hot updated: ${url}`,
hotUpdatedLog(file),
])
},
)
Expand Down Expand Up @@ -668,7 +668,7 @@ if (!isBuild) {

beforeAll(async () => {
await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, />>>>>>/],
(logs) => {
expect(logs).toContain(`<<< named: ${a} ; ${dep}`)
Expand Down Expand Up @@ -731,7 +731,7 @@ if (!isBuild) {
const file = 'side-effects.ts'

await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, />>>/],
(logs) => {
expect(logs).toContain('>>> side FX')
Expand All @@ -748,7 +748,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
'>>> side FX !!',
`[vite] hot updated: /${testDir}/${file}`,
hotUpdatedLog(`${testDir}/${file}`),
])
},
)
Expand All @@ -760,10 +760,9 @@ if (!isBuild) {
test('accepts itself if no exports are imported', async () => {
const fileName = 'unused.ts'
const file = `${testDir}/${fileName}`
const url = '/' + file

await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, '-- unused --'],
(logs) => {
expect(logs).toContain('-- unused --')
Expand All @@ -779,7 +778,7 @@ if (!isBuild) {
},
HOT_UPDATED,
(logs) => {
expect(logs).toEqual(['-> unused <-', `[vite] hot updated: ${url}`])
expect(logs).toEqual(['-> unused <-', hotUpdatedLog(file)])
},
)
})
Expand All @@ -789,7 +788,7 @@ if (!isBuild) {
const file = `${testDir}/${fileName}`

await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, '-- used --'],
(logs) => {
expect(logs).toContain('-- used --')
Expand Down Expand Up @@ -824,10 +823,9 @@ if (!isBuild) {
it('accepts itself if all its exports are accepted', async () => {
const fileName = 'deps-all-accepted.ts'
const file = `${testDir}/${fileName}`
const url = '/' + file

await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, '>>> ready <<<'],
(logs) => {
expect(logs).toContain('loaded:all:a0b0c0default0')
Expand All @@ -843,7 +841,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
'all >>>>>> a1, b1, c1',
`[vite] hot updated: ${url}`,
hotUpdatedLog(file),
])
},
)
Expand All @@ -856,7 +854,7 @@ if (!isBuild) {
(logs) => {
expect(logs).toEqual([
'all >>>>>> a2, b2, c2',
`[vite] hot updated: ${url}`,
hotUpdatedLog(file),
])
},
)
Expand All @@ -867,7 +865,7 @@ if (!isBuild) {
const file = `${testDir}/${fileName}`

await untilBrowserLogAfter(
() => page.goto(`${viteTestUrl}/${testDir}/`),
() => openPage(testDir),
[CONNECTED, '>>> ready <<<'],
(logs) => {
expect(logs).toContain('loaded:some:a0b0c0default0')
Expand Down
Loading
Loading