Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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