Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
17 changes: 16 additions & 1 deletion packages/vite/rolldown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const envConfig = defineConfig({
})

const clientConfig = defineConfig({
input: path.resolve(dirname, 'src/client/client.ts'),
input: path.resolve(dirname, 'src/client/clientEntry.ts'),
platform: 'browser',
transform: {
target: 'es2020',
Expand All @@ -38,6 +38,20 @@ const clientConfig = defineConfig({
},
})

// separate entry so the full-bundle-mode HMR code is never bundled into `client.mjs`
const fbmClientConfig = defineConfig({
input: path.resolve(dirname, 'src/client/fbmClient.ts'),
platform: 'browser',
transform: {
target: 'es2020',
},
external: ['@vite/env'],
output: {
dir: path.resolve(dirname, 'dist'),
entryFileNames: 'client/fbmClient.mjs',
},
})

const sharedNodeOptions = defineConfig({
platform: 'node',
treeshake: {
Expand Down Expand Up @@ -155,6 +169,7 @@ const moduleRunnerConfig = defineConfig({
export default defineConfig([
envConfig,
clientConfig,
fbmClientConfig,
nodeConfig,
moduleRunnerConfig,
])
Expand Down
191 changes: 66 additions & 125 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import { nanoid } from 'nanoid/non-secure'
import type {
DevRuntime as DevRuntimeType,
Messenger,
} from 'rolldown/experimental/runtime-types'
import type { ErrorPayload, HotPayload } from '#types/hmrPayload'
import type { ViteHotContext } from '#types/hot'
import { HMRClient, HMRContext } from '../shared/hmr'
Expand All @@ -12,6 +7,7 @@ import {
} from '../shared/moduleRunnerTransport'
import { createHMRHandler } from '../shared/hmrHandler'
import { setupForwardConsoleHandler } from '../shared/forwardConsole'
import type { FbmHMRClient } from './fbmHmrClient'
import { ErrorOverlay, cspNonce, overlayId } from './overlay'
// @ts-expect-error internal virtual module
import '@vite/env'
Expand All @@ -28,7 +24,6 @@ declare const __HMR_TIMEOUT__: number
declare const __HMR_ENABLE_OVERLAY__: boolean
declare const __WS_TOKEN__: string
declare const __SERVER_FORWARD_CONSOLE__: any
declare const __BUNDLED_DEV__: boolean

console.debug('[vite] connecting...')

Expand All @@ -43,13 +38,12 @@ const socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${
hmrPort || importMetaUrl.port
}${__HMR_BASE__}`
const directSocketHost = __HMR_DIRECT_TARGET__
const base = __BASE__ || '/'
export const base = __BASE__ || '/'
const hmrTimeout = __HMR_TIMEOUT__
const wsToken = __WS_TOKEN__
const isBundleMode = __BUNDLED_DEV__
const forwardConsole = __SERVER_FORWARD_CONSOLE__

const transport = normalizeModuleRunnerTransport(
export const transport = normalizeModuleRunnerTransport(
(() => {
let wsTransport = createWebSocketModuleRunnerTransport({
createConnection: () =>
Expand Down Expand Up @@ -143,87 +137,82 @@ const debounceReload = (time: number) => {
}, time)
}
}
const pageReload = debounceReload(20)
export const pageReload = debounceReload(20)

const hmrClient = new HMRClient(
{
error: (err) => console.error('[vite]', err),
debug: (...msg) => console.debug('[vite]', ...msg),
},
transport,
isBundleMode
? async function importUpdatedModule({
url,
acceptedPath,
isWithinCircularImport,
}) {
const importPromise = import(base + url!).then(() =>
// @ts-expect-error globalThis.__rolldown_runtime__
globalThis.__rolldown_runtime__.loadExports(acceptedPath),
)
if (isWithinCircularImport) {
importPromise.catch(() => {
console.info(
`[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
`To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
)
pageReload()
})
}
return await importPromise
}
: async function importUpdatedModule({
acceptedPath,
timestamp,
explicitImportRequired,
isWithinCircularImport,
}) {
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
const importPromise = import(
/* @vite-ignore */
base +
acceptedPathWithoutQuery.slice(1) +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
query ? `&${query}` : ''
}`
async function importUpdatedModule({
acceptedPath,
timestamp,
explicitImportRequired,
isWithinCircularImport,
}) {
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
const importPromise = import(
/* @vite-ignore */
base +
acceptedPathWithoutQuery.slice(1) +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
query ? `&${query}` : ''
}`
)
if (isWithinCircularImport) {
importPromise.catch(() => {
console.info(
`[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
`To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
)
if (isWithinCircularImport) {
importPromise.catch(() => {
console.info(
`[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
`To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
)
pageReload()
})
}
return await importPromise
},
pageReload()
})
}
return await importPromise
},
)
// set by the full-bundle-mode entry (`fbmClient.ts`); the `import type` above keeps
// `FbmHMRClient` compile-time only, so `client.mjs` bundles no FBM code
let fbmClient: FbmHMRClient | undefined
export function registerFbmClient(client: FbmHMRClient): void {
fbmClient = client
}
transport.connect!(createHMRHandler(handleMessage))

setupForwardConsoleHandler(transport, forwardConsole)

// if this is the first update and there's already an error overlay, it means the
// page opened with existing server compile error and the whole module script failed
// to load (since one of the nested imports is 500). in this case a normal update
// won't work and a full reload is needed.
export function clearOverlayOrReloadOnFirstUpdate(): 'reload' | 'continue' {
if (hasDocument) {
if (isFirstUpdate && hasErrorOverlay()) {
location.reload()
return 'reload'
}
if (enableOverlay) {
clearErrorOverlay()
}
isFirstUpdate = false
}
return 'continue'
}

async function handleMessage(payload: HotPayload) {
const activeHmrClient = fbmClient ?? hmrClient
switch (payload.type) {
case 'connected':
console.debug(`[vite] connected.`)
break
case 'fbm-update':
fbmClient!.handlePush(payload)
break
case 'update':
await hmrClient.notifyListeners('vite:beforeUpdate', payload)
if (hasDocument) {
// if this is the first update and there's already an error overlay, it
// means the page opened with existing server compile error and the whole
// module script failed to load (since one of the nested imports is 500).
// in this case a normal update won't work and a full reload is needed.
if (isFirstUpdate && hasErrorOverlay()) {
location.reload()
return
} else {
if (enableOverlay) {
clearErrorOverlay()
}
isFirstUpdate = false
}
await activeHmrClient.notifyListeners('vite:beforeUpdate', payload)
if (clearOverlayOrReloadOnFirstUpdate() === 'reload') {
return
}
await Promise.all(
payload.updates.map(async (update): Promise<void> => {
Expand Down Expand Up @@ -273,10 +262,10 @@ async function handleMessage(payload: HotPayload) {
})
}),
)
await hmrClient.notifyListeners('vite:afterUpdate', payload)
await activeHmrClient.notifyListeners('vite:afterUpdate', payload)
break
case 'custom': {
await hmrClient.notifyListeners(payload.event, payload.data)
await activeHmrClient.notifyListeners(payload.event, payload.data)
if (payload.event === 'vite:ws:disconnect') {
if (hasDocument && !willUnload) {
console.log(`[vite] server connection lost. Polling for restart...`)
Expand All @@ -290,7 +279,7 @@ async function handleMessage(payload: HotPayload) {
break
}
case 'full-reload':
await hmrClient.notifyListeners('vite:beforeFullReload', payload)
await activeHmrClient.notifyListeners('vite:beforeFullReload', payload)
if (hasDocument) {
if (payload.path && payload.path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
Expand All @@ -311,11 +300,11 @@ async function handleMessage(payload: HotPayload) {
}
break
case 'prune':
await hmrClient.notifyListeners('vite:beforePrune', payload)
await hmrClient.prunePaths(payload.paths)
await activeHmrClient.notifyListeners('vite:beforePrune', payload)
await activeHmrClient.prunePaths(payload.paths)
break
case 'error': {
await hmrClient.notifyListeners('vite:error', payload)
await activeHmrClient.notifyListeners('vite:error', payload)
if (hasDocument) {
const err = payload.err
if (enableOverlay) {
Expand Down Expand Up @@ -623,51 +612,3 @@ export function injectQuery(url: string, queryToInject: string): string {
}

export { ErrorOverlay }

declare const DevRuntime: typeof DevRuntimeType

if (isBundleMode && typeof DevRuntime !== 'undefined') {
class ViteDevRuntime extends DevRuntime {
override createModuleHotContext(moduleId: string) {
const ctx = createHotContext(moduleId)
// @ts-expect-error TODO: support CSS properly
ctx._internal = { updateStyle, removeStyle }
return ctx
}

override applyUpdates(_boundaries: [string, string][]): void {
// noop, handled in the HMR client
}
}

const clientId = nanoid()

// notify client id
transport.send({
type: 'custom',
event: 'vite:module-loaded',
data: { modules: [], clientId },
})

const wrappedSocket: Messenger = {
send(message) {
switch (message.type) {
case 'hmr:module-registered': {
transport.send({
type: 'custom',
event: 'vite:module-loaded',
// clone array as the runtime reuses the array instance
data: { modules: message.modules.slice(), clientId },
})
break
}
default:
throw new Error(`Unknown message type: ${JSON.stringify(message)}`)
}
},
}
;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime(
wrappedSocket,
clientId,
)
}
11 changes: 11 additions & 0 deletions packages/vite/src/client/clientEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// entry for `client.mjs` (middleware mode). `client.ts` exports extra internal
// bindings (`transport`, `registerFbmClient`, ...) so the full-bundle-mode
// entry can reuse them; this wrapper keeps them out of `/@vite/client`'s
// public API.
export {
createHotContext,
injectQuery,
removeStyle,
updateStyle,
ErrorOverlay,
} from './client'
67 changes: 67 additions & 0 deletions packages/vite/src/client/fbmClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { nanoid } from 'nanoid/non-secure'
import type { DevRuntime as DevRuntimeType } from 'rolldown/experimental/runtime-types'
import { FbmHMRClient, FbmHMRContext } from './fbmHmrClient'
import {
base,
clearOverlayOrReloadOnFirstUpdate,
pageReload,
registerFbmClient,
removeStyle,
transport,
updateStyle,
} from './client'

// keep the same public exports as `client.ts`, which this entry replaces when inlined
export {
createHotContext,
injectQuery,
removeStyle,
updateStyle,
ErrorOverlay,
} from './client'

// injected by rolldown's hmr plugin into the bundle prelude, ahead of this client
declare const DevRuntime: typeof DevRuntimeType

if (typeof DevRuntime !== 'undefined') {
class ViteDevRuntime extends DevRuntime {
override createModuleHotContext(moduleId: string) {
const ctx = new FbmHMRContext(fbmHmrClient, moduleId)
// @ts-expect-error TODO: support CSS properly
ctx._internal = { updateStyle, removeStyle }
return ctx
}
}

const clientId = nanoid()

transport.send({
type: 'custom',
event: 'vite:client-connected',
data: { clientId },
})

const runtime = ((globalThis as any).__rolldown_runtime__ ??=
new ViteDevRuntime(clientId))

const fbmHmrClient = new FbmHMRClient(
{
error: (err) => console.error('[vite]', err),
debug: (...msg) => console.debug('[vite]', ...msg),
},
transport,
runtime,
{
base,
beforeApply: clearOverlayOrReloadOnFirstUpdate,
pageReload,
},
)
registerFbmClient(fbmHmrClient)

runtime.hooks = {
createModuleHotContext: (id: string) => runtime.createModuleHotContext(id),
onModuleCacheRemoval: (id: string) =>
fbmHmrClient.handleModuleCacheRemoval(id),
}
}
Loading