mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
feat: use a single transport for fetchModule and HMR support (#18362)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
This commit is contained in:
parent
643928ceeb
commit
78dc4902ff
@ -41,7 +41,7 @@ class DevEnvironment {
|
|||||||
* Communication channel to send and receive messages from the
|
* Communication channel to send and receive messages from the
|
||||||
* associated module runner in the target runtime.
|
* associated module runner in the target runtime.
|
||||||
*/
|
*/
|
||||||
hot: HotChannel | null
|
hot: NormalizedHotChannel
|
||||||
/**
|
/**
|
||||||
* Graph of module nodes, with the imported relationship between
|
* Graph of module nodes, with the imported relationship between
|
||||||
* processed modules and the cached result of the processed code.
|
* processed modules and the cached result of the processed code.
|
||||||
|
@ -29,7 +29,8 @@ function createWorkedEnvironment(
|
|||||||
dev: {
|
dev: {
|
||||||
createEnvironment(name, config) {
|
createEnvironment(name, config) {
|
||||||
return createWorkerdDevEnvironment(name, config, {
|
return createWorkerdDevEnvironment(name, config, {
|
||||||
hot: customHotChannel(),
|
hot: true,
|
||||||
|
transport: customHotChannel(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -82,29 +83,26 @@ A Vite Module Runner allows running any code by processing it with Vite plugins
|
|||||||
One of the goals of this feature is to provide a customizable API to process and run code. Users can create new environment factories using the exposed primitives.
|
One of the goals of this feature is to provide a customizable API to process and run code. Users can create new environment factories using the exposed primitives.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { DevEnvironment, RemoteEnvironmentTransport } from 'vite'
|
import { DevEnvironment, HotChannel } from 'vite'
|
||||||
|
|
||||||
function createWorkerdDevEnvironment(
|
function createWorkerdDevEnvironment(
|
||||||
name: string,
|
name: string,
|
||||||
config: ResolvedConfig,
|
config: ResolvedConfig,
|
||||||
context: DevEnvironmentContext
|
context: DevEnvironmentContext
|
||||||
) {
|
) {
|
||||||
const hot = /* ... */
|
|
||||||
const connection = /* ... */
|
const connection = /* ... */
|
||||||
const transport = new RemoteEnvironmentTransport({
|
const transport: HotChannel = {
|
||||||
|
on: (listener) => { connection.on('message', listener) },
|
||||||
send: (data) => connection.send(data),
|
send: (data) => connection.send(data),
|
||||||
onMessage: (listener) => connection.on('message', listener),
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const workerdDevEnvironment = new DevEnvironment(name, config, {
|
const workerdDevEnvironment = new DevEnvironment(name, config, {
|
||||||
options: {
|
options: {
|
||||||
resolve: { conditions: ['custom'] },
|
resolve: { conditions: ['custom'] },
|
||||||
...context.options,
|
...context.options,
|
||||||
},
|
},
|
||||||
hot,
|
hot: true,
|
||||||
remoteRunner: {
|
transport,
|
||||||
transport,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return workerdDevEnvironment
|
return workerdDevEnvironment
|
||||||
}
|
}
|
||||||
@ -152,13 +150,12 @@ Module runner exposes `import` method. When Vite server triggers `full-reload` H
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner'
|
import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner'
|
||||||
import { root, fetchModule } from './rpc-implementation.js'
|
import { root, transport } from './rpc-implementation.js'
|
||||||
|
|
||||||
const moduleRunner = new ModuleRunner(
|
const moduleRunner = new ModuleRunner(
|
||||||
{
|
{
|
||||||
root,
|
root,
|
||||||
fetchModule,
|
transport,
|
||||||
// you can also provide hmr.connection to support HMR
|
|
||||||
},
|
},
|
||||||
new ESModulesEvaluator(),
|
new ESModulesEvaluator(),
|
||||||
)
|
)
|
||||||
@ -177,7 +174,7 @@ export interface ModuleRunnerOptions {
|
|||||||
/**
|
/**
|
||||||
* A set of methods to communicate with the server.
|
* A set of methods to communicate with the server.
|
||||||
*/
|
*/
|
||||||
transport: RunnerTransport
|
transport: ModuleRunnerTransport
|
||||||
/**
|
/**
|
||||||
* Configure how source maps are resolved.
|
* Configure how source maps are resolved.
|
||||||
* Prefers `node` if `process.setSourceMapsEnabled` is available.
|
* Prefers `node` if `process.setSourceMapsEnabled` is available.
|
||||||
@ -197,10 +194,6 @@ export interface ModuleRunnerOptions {
|
|||||||
hmr?:
|
hmr?:
|
||||||
| false
|
| false
|
||||||
| {
|
| {
|
||||||
/**
|
|
||||||
* Configure how HMR communicates between client and server.
|
|
||||||
*/
|
|
||||||
connection: ModuleRunnerHMRConnection
|
|
||||||
/**
|
/**
|
||||||
* Configure HMR logger.
|
* Configure HMR logger.
|
||||||
*/
|
*/
|
||||||
@ -245,59 +238,91 @@ export interface ModuleEvaluator {
|
|||||||
|
|
||||||
Vite exports `ESModulesEvaluator` that implements this interface by default. It uses `new AsyncFunction` to evaluate code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically by the `ESModulesEvaluator`. Custom evaluators will not add additional lines.
|
Vite exports `ESModulesEvaluator` that implements this interface by default. It uses `new AsyncFunction` to evaluate code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically by the `ESModulesEvaluator`. Custom evaluators will not add additional lines.
|
||||||
|
|
||||||
## RunnerTransport
|
## `ModuleRunnerTransport`
|
||||||
|
|
||||||
**Type Signature:**
|
**Type Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface RunnerTransport {
|
interface ModuleRunnerTransport {
|
||||||
/**
|
connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
|
||||||
* A method to get the information about the module.
|
disconnect?(): Promise<void> | void
|
||||||
*/
|
send?(data: HotPayload): Promise<void> | void
|
||||||
fetchModule: FetchFunction
|
invoke?(
|
||||||
|
data: HotPayload,
|
||||||
|
): Promise<{ /** result */ r: any } | { /** error */ e: any }>
|
||||||
|
timeout?: number
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Transport object that communicates with the environment via an RPC or by directly calling the function. By default, you need to pass an object with `fetchModule` method - it can use any type of RPC inside of it, but Vite also exposes bidirectional transport interface via a `RemoteRunnerTransport` class to make the configuration easier. You need to couple it with the `RemoteEnvironmentTransport` instance on the server like in this example where module runner is created in the worker thread:
|
Transport object that communicates with the environment via an RPC or by directly calling the function. When `invoke` method is not implemented, the `send` method and `connect` method is required to be implemented. Vite will construct the `invoke` internally.
|
||||||
|
|
||||||
|
You need to couple it with the `HotChannel` instance on the server like in this example where module runner is created in the worker thread:
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
|
|
||||||
```ts [worker.js]
|
```js [worker.js]
|
||||||
import { parentPort } from 'node:worker_threads'
|
import { parentPort } from 'node:worker_threads'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import {
|
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
||||||
ESModulesEvaluator,
|
|
||||||
ModuleRunner,
|
/** @type {import('vite/module-runner').ModuleRunnerTransport} */
|
||||||
RemoteRunnerTransport,
|
const transport = {
|
||||||
} from 'vite/module-runner'
|
connect({ onMessage, onDisconnection }) {
|
||||||
|
parentPort.on('message', onMessage)
|
||||||
|
parentPort.on('close', onDisconnection)
|
||||||
|
},
|
||||||
|
send(data) {
|
||||||
|
parentPort.postMessage(data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const runner = new ModuleRunner(
|
const runner = new ModuleRunner(
|
||||||
{
|
{
|
||||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
transport: new RemoteRunnerTransport({
|
transport,
|
||||||
send: (data) => parentPort.postMessage(data),
|
|
||||||
onMessage: (listener) => parentPort.on('message', listener),
|
|
||||||
timeout: 5000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
new ESModulesEvaluator(),
|
new ESModulesEvaluator(),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts [server.js]
|
```js [server.js]
|
||||||
import { BroadcastChannel } from 'node:worker_threads'
|
import { BroadcastChannel } from 'node:worker_threads'
|
||||||
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'
|
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'
|
||||||
|
|
||||||
function createWorkerEnvironment(name, config, context) {
|
function createWorkerEnvironment(name, config, context) {
|
||||||
const worker = new Worker('./worker.js')
|
const worker = new Worker('./worker.js')
|
||||||
return new DevEnvironment(name, config, {
|
const handlerToWorkerListener = new WeakMap()
|
||||||
hot: /* custom hot channel */,
|
|
||||||
remoteRunner: {
|
const workerHotChannel = {
|
||||||
transport: new RemoteEnvironmentTransport({
|
send: (data) => w.postMessage(data),
|
||||||
send: (data) => worker.postMessage(data),
|
on: (event, handler) => {
|
||||||
onMessage: (listener) => worker.on('message', listener),
|
if (event === 'connection') return
|
||||||
}),
|
|
||||||
|
const listener = (value) => {
|
||||||
|
if (value.type === 'custom' && value.event === event) {
|
||||||
|
const client = {
|
||||||
|
send(payload) {
|
||||||
|
w.postMessage(payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler(value.data, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlerToWorkerListener.set(handler, listener)
|
||||||
|
w.on('message', listener)
|
||||||
},
|
},
|
||||||
|
off: (event, handler) => {
|
||||||
|
if (event === 'connection') return
|
||||||
|
const listener = handlerToWorkerListener.get(handler)
|
||||||
|
if (listener) {
|
||||||
|
w.off('message', listener)
|
||||||
|
handlerToWorkerListener.delete(handler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DevEnvironment(name, config, {
|
||||||
|
transport: workerHotChannel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +339,7 @@ await createServer({
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
`RemoteRunnerTransport` and `RemoteEnvironmentTransport` are meant to be used together, but you don't have to use them at all. You can define your own function to communicate between the runner and the server. For example, if you connect to the environment via an HTTP request, you can call `fetch().json()` in `fetchModule` function:
|
A different example using an HTTP request to communicate between the runner and the server:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
||||||
@ -323,10 +348,11 @@ export const runner = new ModuleRunner(
|
|||||||
{
|
{
|
||||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
transport: {
|
transport: {
|
||||||
async fetchModule(id, importer) {
|
async invoke(data) {
|
||||||
const response = await fetch(
|
const response = await fetch(`http://my-vite-server/invoke`, {
|
||||||
`http://my-vite-server/fetch?id=${id}&importer=${importer}`,
|
method: 'POST',
|
||||||
)
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
return response.json()
|
return response.json()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -337,37 +363,22 @@ export const runner = new ModuleRunner(
|
|||||||
await runner.import('/entry.js')
|
await runner.import('/entry.js')
|
||||||
```
|
```
|
||||||
|
|
||||||
## ModuleRunnerHMRConnection
|
In this case, the `handleInvoke` method in the `NormalizedHotChannel` can be used:
|
||||||
|
|
||||||
**Type Signature:**
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export interface ModuleRunnerHMRConnection {
|
const customEnvironment = new DevEnvironment(name, config, context)
|
||||||
/**
|
|
||||||
* Checked before sending messages to the server.
|
server.onRequest((request: Request) => {
|
||||||
*/
|
const url = new URL(request.url)
|
||||||
isReady(): boolean
|
if (url.pathname === '/invoke') {
|
||||||
/**
|
const payload = (await request.json()) as HotPayload
|
||||||
* Send a message to the server.
|
const result = customEnvironment.hot.handleInvoke(payload)
|
||||||
*/
|
return new Response(JSON.stringify(result))
|
||||||
send(payload: HotPayload): void
|
}
|
||||||
/**
|
return Response.error()
|
||||||
* Configure how HMR is handled when this connection triggers an update.
|
})
|
||||||
* This method expects that the connection will start listening for HMR
|
|
||||||
* updates and call this callback when it's received.
|
|
||||||
*/
|
|
||||||
onUpdate(callback: (payload: HotPayload) => void): void
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`).
|
But note that for HMR support, `send` and `connect` methods are required. The `send` method is usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`).
|
||||||
|
|
||||||
`onUpdate` is called only once when the new module runner is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this:
|
Vite exports `createServerHotChannel` from the main entry point to support HMR during Vite SSR.
|
||||||
|
|
||||||
```js
|
|
||||||
function onUpdate(callback) {
|
|
||||||
this.connection.on('hmr', (event) => callback(event.data))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in a module runner will wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules.
|
|
||||||
|
@ -32,6 +32,7 @@ const clientConfig = defineConfig({
|
|||||||
input: path.resolve(__dirname, 'src/client/client.ts'),
|
input: path.resolve(__dirname, 'src/client/client.ts'),
|
||||||
external: ['@vite/env'],
|
external: ['@vite/env'],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
nodeResolve({ preferBuiltins: true }),
|
||||||
esbuild({
|
esbuild({
|
||||||
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
|
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
|
||||||
}),
|
}),
|
||||||
@ -186,7 +187,7 @@ const moduleRunnerConfig = defineConfig({
|
|||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
...createSharedNodePlugins({ esbuildOptions: { minifySyntax: true } }),
|
...createSharedNodePlugins({ esbuildOptions: { minifySyntax: true } }),
|
||||||
bundleSizeLimit(50),
|
bundleSizeLimit(53),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@ import type { ErrorPayload, HotPayload } from 'types/hmrPayload'
|
|||||||
import type { ViteHotContext } from 'types/hot'
|
import type { ViteHotContext } from 'types/hot'
|
||||||
import type { InferCustomEventPayload } from 'types/customEvent'
|
import type { InferCustomEventPayload } from 'types/customEvent'
|
||||||
import { HMRClient, HMRContext } from '../shared/hmr'
|
import { HMRClient, HMRContext } from '../shared/hmr'
|
||||||
|
import {
|
||||||
|
createWebSocketModuleRunnerTransport,
|
||||||
|
normalizeModuleRunnerTransport,
|
||||||
|
} from '../shared/moduleRunnerTransport'
|
||||||
import { ErrorOverlay, overlayId } from './overlay'
|
import { ErrorOverlay, overlayId } from './overlay'
|
||||||
import '@vite/env'
|
import '@vite/env'
|
||||||
|
|
||||||
@ -30,96 +34,75 @@ const socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${
|
|||||||
}${__HMR_BASE__}`
|
}${__HMR_BASE__}`
|
||||||
const directSocketHost = __HMR_DIRECT_TARGET__
|
const directSocketHost = __HMR_DIRECT_TARGET__
|
||||||
const base = __BASE__ || '/'
|
const base = __BASE__ || '/'
|
||||||
|
const hmrTimeout = __HMR_TIMEOUT__
|
||||||
|
|
||||||
let socket: WebSocket
|
const transport = normalizeModuleRunnerTransport(
|
||||||
try {
|
(() => {
|
||||||
let fallback: (() => void) | undefined
|
let wsTransport = createWebSocketModuleRunnerTransport({
|
||||||
// only use fallback when port is inferred to prevent confusion
|
createConnection: () =>
|
||||||
if (!hmrPort) {
|
new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr'),
|
||||||
fallback = () => {
|
pingInterval: hmrTimeout,
|
||||||
// fallback to connecting directly to the hmr server
|
})
|
||||||
// for servers which does not support proxying websocket
|
|
||||||
socket = setupWebSocket(socketProtocol, directSocketHost, () => {
|
return {
|
||||||
const currentScriptHostURL = new URL(import.meta.url)
|
async connect(handlers) {
|
||||||
const currentScriptHost =
|
try {
|
||||||
currentScriptHostURL.host +
|
await wsTransport.connect(handlers)
|
||||||
currentScriptHostURL.pathname.replace(/@vite\/client$/, '')
|
} catch (e) {
|
||||||
console.error(
|
// only use fallback when port is inferred and was not connected before to prevent confusion
|
||||||
'[vite] failed to connect to websocket.\n' +
|
if (!hmrPort) {
|
||||||
'your current setup:\n' +
|
wsTransport = createWebSocketModuleRunnerTransport({
|
||||||
` (browser) ${currentScriptHost} <--[HTTP]--> ${serverHost} (server)\n` +
|
createConnection: () =>
|
||||||
` (browser) ${socketHost} <--[WebSocket (failing)]--> ${directSocketHost} (server)\n` +
|
new WebSocket(
|
||||||
'Check out your Vite / network configuration and https://vite.dev/config/server-options.html#server-hmr .',
|
`${socketProtocol}://${directSocketHost}`,
|
||||||
)
|
'vite-hmr',
|
||||||
})
|
),
|
||||||
socket.addEventListener(
|
pingInterval: hmrTimeout,
|
||||||
'open',
|
})
|
||||||
() => {
|
try {
|
||||||
console.info(
|
await wsTransport.connect(handlers)
|
||||||
'[vite] Direct websocket connection fallback. Check out https://vite.dev/config/server-options.html#server-hmr to remove the previous connection error.',
|
console.info(
|
||||||
)
|
'[vite] Direct websocket connection fallback. Check out https://vite.dev/config/server-options.html#server-hmr to remove the previous connection error.',
|
||||||
},
|
)
|
||||||
{ once: true },
|
} catch (e) {
|
||||||
)
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
e.message.includes('WebSocket closed without opened.')
|
||||||
|
) {
|
||||||
|
const currentScriptHostURL = new URL(import.meta.url)
|
||||||
|
const currentScriptHost =
|
||||||
|
currentScriptHostURL.host +
|
||||||
|
currentScriptHostURL.pathname.replace(/@vite\/client$/, '')
|
||||||
|
console.error(
|
||||||
|
'[vite] failed to connect to websocket.\n' +
|
||||||
|
'your current setup:\n' +
|
||||||
|
` (browser) ${currentScriptHost} <--[HTTP]--> ${serverHost} (server)\n` +
|
||||||
|
` (browser) ${socketHost} <--[WebSocket (failing)]--> ${directSocketHost} (server)\n` +
|
||||||
|
'Check out your Vite / network configuration and https://vite.dev/config/server-options.html#server-hmr .',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error(`[vite] failed to connect to websocket (${e}). `)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async disconnect() {
|
||||||
|
await wsTransport.disconnect()
|
||||||
|
},
|
||||||
|
send(data) {
|
||||||
|
wsTransport.send(data)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
})(),
|
||||||
|
)
|
||||||
|
|
||||||
socket = setupWebSocket(socketProtocol, socketHost, fallback)
|
let willUnload = false
|
||||||
} catch (error) {
|
if (typeof window !== 'undefined') {
|
||||||
console.error(`[vite] failed to connect to websocket (${error}). `)
|
window.addEventListener('beforeunload', () => {
|
||||||
}
|
willUnload = true
|
||||||
|
|
||||||
function setupWebSocket(
|
|
||||||
protocol: string,
|
|
||||||
hostAndPath: string,
|
|
||||||
onCloseWithoutOpen?: () => void,
|
|
||||||
) {
|
|
||||||
const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'vite-hmr')
|
|
||||||
let isOpened = false
|
|
||||||
|
|
||||||
socket.addEventListener(
|
|
||||||
'open',
|
|
||||||
() => {
|
|
||||||
isOpened = true
|
|
||||||
notifyListeners('vite:ws:connect', { webSocket: socket })
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Listen for messages
|
|
||||||
socket.addEventListener('message', async ({ data }) => {
|
|
||||||
handleMessage(JSON.parse(data))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let willUnload = false
|
|
||||||
window.addEventListener(
|
|
||||||
'beforeunload',
|
|
||||||
() => {
|
|
||||||
willUnload = true
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// ping server
|
|
||||||
socket.addEventListener('close', async () => {
|
|
||||||
// ignore close caused by top-level navigation
|
|
||||||
if (willUnload) return
|
|
||||||
|
|
||||||
if (!isOpened && onCloseWithoutOpen) {
|
|
||||||
onCloseWithoutOpen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners('vite:ws:disconnect', { webSocket: socket })
|
|
||||||
|
|
||||||
if (hasDocument) {
|
|
||||||
console.log(`[vite] server connection lost. Polling for restart...`)
|
|
||||||
await waitForSuccessfulPing(protocol, hostAndPath)
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return socket
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanUrl(pathname: string): string {
|
function cleanUrl(pathname: string): string {
|
||||||
@ -150,10 +133,7 @@ const hmrClient = new HMRClient(
|
|||||||
error: (err) => console.error('[vite]', err),
|
error: (err) => console.error('[vite]', err),
|
||||||
debug: (...msg) => console.debug('[vite]', ...msg),
|
debug: (...msg) => console.debug('[vite]', ...msg),
|
||||||
},
|
},
|
||||||
{
|
transport,
|
||||||
isReady: () => socket && socket.readyState === 1,
|
|
||||||
send: (payload) => socket.send(JSON.stringify(payload)),
|
|
||||||
},
|
|
||||||
async function importUpdatedModule({
|
async function importUpdatedModule({
|
||||||
acceptedPath,
|
acceptedPath,
|
||||||
timestamp,
|
timestamp,
|
||||||
@ -181,19 +161,12 @@ const hmrClient = new HMRClient(
|
|||||||
return await importPromise
|
return await importPromise
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
transport.connect!(handleMessage)
|
||||||
|
|
||||||
async function handleMessage(payload: HotPayload) {
|
async function handleMessage(payload: HotPayload) {
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
console.debug(`[vite] connected.`)
|
console.debug(`[vite] connected.`)
|
||||||
hmrClient.messenger.flush()
|
|
||||||
// proxy(nginx, docker) hmr ws maybe caused timeout,
|
|
||||||
// so send ping package let ws keep alive.
|
|
||||||
setInterval(() => {
|
|
||||||
if (socket.readyState === socket.OPEN) {
|
|
||||||
socket.send('{"type":"ping"}')
|
|
||||||
}
|
|
||||||
}, __HMR_TIMEOUT__)
|
|
||||||
break
|
break
|
||||||
case 'update':
|
case 'update':
|
||||||
notifyListeners('vite:beforeUpdate', payload)
|
notifyListeners('vite:beforeUpdate', payload)
|
||||||
@ -264,6 +237,14 @@ async function handleMessage(payload: HotPayload) {
|
|||||||
break
|
break
|
||||||
case 'custom': {
|
case 'custom': {
|
||||||
notifyListeners(payload.event, payload.data)
|
notifyListeners(payload.event, payload.data)
|
||||||
|
if (payload.event === 'vite:ws:disconnect') {
|
||||||
|
if (hasDocument && !willUnload) {
|
||||||
|
console.log(`[vite] server connection lost. Polling for restart...`)
|
||||||
|
const socket = payload.data.webSocket as WebSocket
|
||||||
|
await waitForSuccessfulPing(socket.url)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'full-reload':
|
case 'full-reload':
|
||||||
@ -305,6 +286,8 @@ async function handleMessage(payload: HotPayload) {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'ping': // noop
|
||||||
|
break
|
||||||
default: {
|
default: {
|
||||||
const check: never = payload
|
const check: never = payload
|
||||||
return check
|
return check
|
||||||
@ -336,16 +319,9 @@ function hasErrorOverlay() {
|
|||||||
return document.querySelectorAll(overlayId).length
|
return document.querySelectorAll(overlayId).length
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForSuccessfulPing(
|
async function waitForSuccessfulPing(socketUrl: string, ms = 1000) {
|
||||||
socketProtocol: string,
|
|
||||||
hostAndPath: string,
|
|
||||||
ms = 1000,
|
|
||||||
) {
|
|
||||||
async function ping() {
|
async function ping() {
|
||||||
const socket = new WebSocket(
|
const socket = new WebSocket(socketUrl, 'vite-ping')
|
||||||
`${socketProtocol}://${hostAndPath}`,
|
|
||||||
'vite-ping',
|
|
||||||
)
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
|
@ -19,7 +19,6 @@ export async function handleHotPayload(
|
|||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
hmrClient.logger.debug(`connected.`)
|
hmrClient.logger.debug(`connected.`)
|
||||||
hmrClient.messenger.flush()
|
|
||||||
break
|
break
|
||||||
case 'update':
|
case 'update':
|
||||||
await hmrClient.notifyListeners('vite:beforeUpdate', payload)
|
await hmrClient.notifyListeners('vite:beforeUpdate', payload)
|
||||||
@ -73,6 +72,8 @@ export async function handleHotPayload(
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'ping': // noop
|
||||||
|
break
|
||||||
default: {
|
default: {
|
||||||
const check: never = payload
|
const check: never = payload
|
||||||
return check
|
return check
|
||||||
|
@ -3,19 +3,21 @@
|
|||||||
export { EvaluatedModules, type EvaluatedModuleNode } from './evaluatedModules'
|
export { EvaluatedModules, type EvaluatedModuleNode } from './evaluatedModules'
|
||||||
export { ModuleRunner } from './runner'
|
export { ModuleRunner } from './runner'
|
||||||
export { ESModulesEvaluator } from './esmEvaluator'
|
export { ESModulesEvaluator } from './esmEvaluator'
|
||||||
export { RemoteRunnerTransport } from './runnerTransport'
|
|
||||||
|
|
||||||
export type { RunnerTransport } from './runnerTransport'
|
export { createWebSocketModuleRunnerTransport } from '../shared/moduleRunnerTransport'
|
||||||
export type { HMRLogger, HMRConnection } from '../shared/hmr'
|
|
||||||
|
export type { FetchFunctionOptions, FetchResult } from '../shared/invokeMethods'
|
||||||
|
export type {
|
||||||
|
ModuleRunnerTransportHandlers,
|
||||||
|
ModuleRunnerTransport,
|
||||||
|
} from '../shared/moduleRunnerTransport'
|
||||||
|
export type { HMRLogger } from '../shared/hmr'
|
||||||
export type {
|
export type {
|
||||||
ModuleEvaluator,
|
ModuleEvaluator,
|
||||||
ModuleRunnerContext,
|
ModuleRunnerContext,
|
||||||
FetchResult,
|
|
||||||
FetchFunction,
|
FetchFunction,
|
||||||
FetchFunctionOptions,
|
|
||||||
ResolvedResult,
|
ResolvedResult,
|
||||||
SSRImportMetadata,
|
SSRImportMetadata,
|
||||||
ModuleRunnerHMRConnection,
|
|
||||||
ModuleRunnerImportMeta,
|
ModuleRunnerImportMeta,
|
||||||
ModuleRunnerOptions,
|
ModuleRunnerOptions,
|
||||||
ModuleRunnerHmr,
|
ModuleRunnerHmr,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import type { ViteHotContext } from 'types/hot'
|
import type { ViteHotContext } from 'types/hot'
|
||||||
import { HMRClient, HMRContext } from '../shared/hmr'
|
import { HMRClient, HMRContext, type HMRLogger } from '../shared/hmr'
|
||||||
import { cleanUrl, isPrimitive, isWindows } from '../shared/utils'
|
import { cleanUrl, isPrimitive, isWindows } from '../shared/utils'
|
||||||
import { analyzeImportedModDifference } from '../shared/ssrTransform'
|
import { analyzeImportedModDifference } from '../shared/ssrTransform'
|
||||||
|
import {
|
||||||
|
type NormalizedModuleRunnerTransport,
|
||||||
|
normalizeModuleRunnerTransport,
|
||||||
|
} from '../shared/moduleRunnerTransport'
|
||||||
import type { EvaluatedModuleNode } from './evaluatedModules'
|
import type { EvaluatedModuleNode } from './evaluatedModules'
|
||||||
import { EvaluatedModules } from './evaluatedModules'
|
import { EvaluatedModules } from './evaluatedModules'
|
||||||
import type {
|
import type {
|
||||||
@ -29,7 +33,6 @@ import {
|
|||||||
import { hmrLogger, silentConsole } from './hmrLogger'
|
import { hmrLogger, silentConsole } from './hmrLogger'
|
||||||
import { createHMRHandler } from './hmrHandler'
|
import { createHMRHandler } from './hmrHandler'
|
||||||
import { enableSourceMapSupport } from './sourcemap/index'
|
import { enableSourceMapSupport } from './sourcemap/index'
|
||||||
import type { RunnerTransport } from './runnerTransport'
|
|
||||||
|
|
||||||
interface ModuleRunnerDebugger {
|
interface ModuleRunnerDebugger {
|
||||||
(formatter: unknown, ...args: unknown[]): void
|
(formatter: unknown, ...args: unknown[]): void
|
||||||
@ -46,7 +49,7 @@ export class ModuleRunner {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
private readonly transport: RunnerTransport
|
private readonly transport: NormalizedModuleRunnerTransport
|
||||||
private readonly resetSourceMapSupport?: () => void
|
private readonly resetSourceMapSupport?: () => void
|
||||||
private readonly root: string
|
private readonly root: string
|
||||||
private readonly concurrentModuleNodePromises = new Map<
|
private readonly concurrentModuleNodePromises = new Map<
|
||||||
@ -64,16 +67,27 @@ export class ModuleRunner {
|
|||||||
const root = this.options.root
|
const root = this.options.root
|
||||||
this.root = root[root.length - 1] === '/' ? root : `${root}/`
|
this.root = root[root.length - 1] === '/' ? root : `${root}/`
|
||||||
this.evaluatedModules = options.evaluatedModules ?? new EvaluatedModules()
|
this.evaluatedModules = options.evaluatedModules ?? new EvaluatedModules()
|
||||||
this.transport = options.transport
|
this.transport = normalizeModuleRunnerTransport(options.transport)
|
||||||
if (typeof options.hmr === 'object') {
|
if (options.hmr) {
|
||||||
|
const resolvedHmrLogger: HMRLogger =
|
||||||
|
options.hmr === true || options.hmr.logger === undefined
|
||||||
|
? hmrLogger
|
||||||
|
: options.hmr.logger === false
|
||||||
|
? silentConsole
|
||||||
|
: options.hmr.logger
|
||||||
this.hmrClient = new HMRClient(
|
this.hmrClient = new HMRClient(
|
||||||
options.hmr.logger === false
|
resolvedHmrLogger,
|
||||||
? silentConsole
|
this.transport,
|
||||||
: options.hmr.logger || hmrLogger,
|
|
||||||
options.hmr.connection,
|
|
||||||
({ acceptedPath }) => this.import(acceptedPath),
|
({ acceptedPath }) => this.import(acceptedPath),
|
||||||
)
|
)
|
||||||
options.hmr.connection.onUpdate(createHMRHandler(this))
|
if (!this.transport.connect) {
|
||||||
|
throw new Error(
|
||||||
|
'HMR is not supported by this runner transport, but `hmr` option was set to true',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.transport.connect(createHMRHandler(this))
|
||||||
|
} else {
|
||||||
|
this.transport.connect?.()
|
||||||
}
|
}
|
||||||
if (options.sourcemapInterceptor !== false) {
|
if (options.sourcemapInterceptor !== false) {
|
||||||
this.resetSourceMapSupport = enableSourceMapSupport(this)
|
this.resetSourceMapSupport = enableSourceMapSupport(this)
|
||||||
@ -105,6 +119,7 @@ export class ModuleRunner {
|
|||||||
this.clearCache()
|
this.clearCache()
|
||||||
this.hmrClient = undefined
|
this.hmrClient = undefined
|
||||||
this.closed = true
|
this.closed = true
|
||||||
|
await this.transport.disconnect?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -255,10 +270,14 @@ export class ModuleRunner {
|
|||||||
(
|
(
|
||||||
url.startsWith('data:')
|
url.startsWith('data:')
|
||||||
? { externalize: url, type: 'builtin' }
|
? { externalize: url, type: 'builtin' }
|
||||||
: await this.transport.fetchModule(url, importer, {
|
: await this.transport.invoke('fetchModule', [
|
||||||
cached: isCached,
|
url,
|
||||||
startOffset: this.evaluator.startOffset,
|
importer,
|
||||||
})
|
{
|
||||||
|
cached: isCached,
|
||||||
|
startOffset: this.evaluator.startOffset,
|
||||||
|
},
|
||||||
|
])
|
||||||
) as ResolvedResult
|
) as ResolvedResult
|
||||||
|
|
||||||
if ('cache' in fetchedModule) {
|
if ('cache' in fetchedModule) {
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid/non-secure'
|
|
||||||
import type { FetchFunction, FetchResult } from './types'
|
|
||||||
|
|
||||||
export interface RunnerTransport {
|
|
||||||
fetchModule: FetchFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RemoteRunnerTransport implements RunnerTransport {
|
|
||||||
private rpcPromises = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
resolve: (data: any) => void
|
|
||||||
reject: (data: any) => void
|
|
||||||
timeoutId?: NodeJS.Timeout
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly options: {
|
|
||||||
send: (data: any) => void
|
|
||||||
onMessage: (handler: (data: any) => void) => void
|
|
||||||
timeout?: number
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
this.options.onMessage(async (data) => {
|
|
||||||
if (typeof data !== 'object' || !data || !data.__v) return
|
|
||||||
|
|
||||||
const promise = this.rpcPromises.get(data.i)
|
|
||||||
if (!promise) return
|
|
||||||
|
|
||||||
if (promise.timeoutId) clearTimeout(promise.timeoutId)
|
|
||||||
|
|
||||||
this.rpcPromises.delete(data.i)
|
|
||||||
|
|
||||||
if (data.e) {
|
|
||||||
promise.reject(data.e)
|
|
||||||
} else {
|
|
||||||
promise.resolve(data.r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolve<T>(method: string, ...args: any[]) {
|
|
||||||
const promiseId = nanoid()
|
|
||||||
this.options.send({
|
|
||||||
__v: true,
|
|
||||||
m: method,
|
|
||||||
a: args,
|
|
||||||
i: promiseId,
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
const timeout = this.options.timeout ?? 60000
|
|
||||||
let timeoutId
|
|
||||||
if (timeout > 0) {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
this.rpcPromises.delete(promiseId)
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`${method}(${args.map((arg) => JSON.stringify(arg)).join(', ')}) timed out after ${timeout}ms`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, timeout)
|
|
||||||
timeoutId?.unref?.()
|
|
||||||
}
|
|
||||||
this.rpcPromises.set(promiseId, { resolve, reject, timeoutId })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchModule(id: string, importer?: string): Promise<FetchResult> {
|
|
||||||
return this.resolve<FetchResult>('fetchModule', id, importer)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,16 @@
|
|||||||
import type { ViteHotContext } from 'types/hot'
|
import type { ViteHotContext } from 'types/hot'
|
||||||
import type { HotPayload } from 'types/hmrPayload'
|
import type { HMRLogger } from '../shared/hmr'
|
||||||
import type { HMRConnection, HMRLogger } from '../shared/hmr'
|
|
||||||
import type {
|
import type {
|
||||||
DefineImportMetadata,
|
DefineImportMetadata,
|
||||||
SSRImportMetadata,
|
SSRImportMetadata,
|
||||||
} from '../shared/ssrTransform'
|
} from '../shared/ssrTransform'
|
||||||
|
import type {
|
||||||
|
ExternalFetchResult,
|
||||||
|
FetchFunctionOptions,
|
||||||
|
FetchResult,
|
||||||
|
ViteFetchResult,
|
||||||
|
} from '../shared/invokeMethods'
|
||||||
|
import type { ModuleRunnerTransport } from '../shared/moduleRunnerTransport'
|
||||||
import type { EvaluatedModuleNode, EvaluatedModules } from './evaluatedModules'
|
import type { EvaluatedModuleNode, EvaluatedModules } from './evaluatedModules'
|
||||||
import type {
|
import type {
|
||||||
ssrDynamicImportKey,
|
ssrDynamicImportKey,
|
||||||
@ -14,18 +20,9 @@ import type {
|
|||||||
ssrModuleExportsKey,
|
ssrModuleExportsKey,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import type { InterceptorOptions } from './sourcemap/interceptor'
|
import type { InterceptorOptions } from './sourcemap/interceptor'
|
||||||
import type { RunnerTransport } from './runnerTransport'
|
|
||||||
|
|
||||||
export type { DefineImportMetadata, SSRImportMetadata }
|
export type { DefineImportMetadata, SSRImportMetadata }
|
||||||
|
|
||||||
export interface ModuleRunnerHMRConnection extends HMRConnection {
|
|
||||||
/**
|
|
||||||
* Configure how HMR is handled when this connection triggers an update.
|
|
||||||
* This method expects that connection will start listening for HMR updates and call this callback when it's received.
|
|
||||||
*/
|
|
||||||
onUpdate(callback: (payload: HotPayload) => void): void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModuleRunnerImportMeta extends ImportMeta {
|
export interface ModuleRunnerImportMeta extends ImportMeta {
|
||||||
url: string
|
url: string
|
||||||
env: ImportMetaEnv
|
env: ImportMetaEnv
|
||||||
@ -67,59 +64,6 @@ export interface ModuleEvaluator {
|
|||||||
runExternalModule(file: string): Promise<any>
|
runExternalModule(file: string): Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FetchResult =
|
|
||||||
| CachedFetchResult
|
|
||||||
| ExternalFetchResult
|
|
||||||
| ViteFetchResult
|
|
||||||
|
|
||||||
export interface CachedFetchResult {
|
|
||||||
/**
|
|
||||||
* If module cached in the runner, we can just confirm
|
|
||||||
* it wasn't invalidated on the server side.
|
|
||||||
*/
|
|
||||||
cache: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExternalFetchResult {
|
|
||||||
/**
|
|
||||||
* The path to the externalized module starting with file://,
|
|
||||||
* by default this will be imported via a dynamic "import"
|
|
||||||
* instead of being transformed by vite and loaded with vite runner
|
|
||||||
*/
|
|
||||||
externalize: string
|
|
||||||
/**
|
|
||||||
* Type of the module. Will be used to determine if import statement is correct.
|
|
||||||
* For example, if Vite needs to throw an error if variable is not actually exported
|
|
||||||
*/
|
|
||||||
type: 'module' | 'commonjs' | 'builtin' | 'network'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ViteFetchResult {
|
|
||||||
/**
|
|
||||||
* Code that will be evaluated by vite runner
|
|
||||||
* by default this will be wrapped in an async function
|
|
||||||
*/
|
|
||||||
code: string
|
|
||||||
/**
|
|
||||||
* File path of the module on disk.
|
|
||||||
* This will be resolved as import.meta.url/filename
|
|
||||||
* Will be equal to `null` for virtual modules
|
|
||||||
*/
|
|
||||||
file: string | null
|
|
||||||
/**
|
|
||||||
* Module ID in the server module graph.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* Module URL used in the import.
|
|
||||||
*/
|
|
||||||
url: string
|
|
||||||
/**
|
|
||||||
* Invalidate module on the client side.
|
|
||||||
*/
|
|
||||||
invalidate: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & {
|
export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & {
|
||||||
url: string
|
url: string
|
||||||
id: string
|
id: string
|
||||||
@ -131,16 +75,7 @@ export type FetchFunction = (
|
|||||||
options?: FetchFunctionOptions,
|
options?: FetchFunctionOptions,
|
||||||
) => Promise<FetchResult>
|
) => Promise<FetchResult>
|
||||||
|
|
||||||
export interface FetchFunctionOptions {
|
|
||||||
cached?: boolean
|
|
||||||
startOffset?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModuleRunnerHmr {
|
export interface ModuleRunnerHmr {
|
||||||
/**
|
|
||||||
* Configure how HMR communicates between the client and the server.
|
|
||||||
*/
|
|
||||||
connection: ModuleRunnerHMRConnection
|
|
||||||
/**
|
/**
|
||||||
* Configure HMR logger.
|
* Configure HMR logger.
|
||||||
*/
|
*/
|
||||||
@ -155,7 +90,7 @@ export interface ModuleRunnerOptions {
|
|||||||
/**
|
/**
|
||||||
* A set of methods to communicate with the server.
|
* A set of methods to communicate with the server.
|
||||||
*/
|
*/
|
||||||
transport: RunnerTransport
|
transport: ModuleRunnerTransport
|
||||||
/**
|
/**
|
||||||
* Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available.
|
* Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available.
|
||||||
* Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method.
|
* Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method.
|
||||||
@ -169,7 +104,7 @@ export interface ModuleRunnerOptions {
|
|||||||
/**
|
/**
|
||||||
* Disable HMR or configure HMR options.
|
* Disable HMR or configure HMR options.
|
||||||
*/
|
*/
|
||||||
hmr?: false | ModuleRunnerHmr
|
hmr?: boolean | ModuleRunnerHmr
|
||||||
/**
|
/**
|
||||||
* Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance.
|
* Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance.
|
||||||
*/
|
*/
|
||||||
|
@ -203,21 +203,13 @@ function defaultCreateClientDevEnvironment(
|
|||||||
context: CreateDevEnvironmentContext,
|
context: CreateDevEnvironmentContext,
|
||||||
) {
|
) {
|
||||||
return new DevEnvironment(name, config, {
|
return new DevEnvironment(name, config, {
|
||||||
hot: context.ws,
|
hot: true,
|
||||||
|
transport: context.ws,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultCreateSsrDevEnvironment(
|
|
||||||
name: string,
|
|
||||||
config: ResolvedConfig,
|
|
||||||
): DevEnvironment {
|
|
||||||
return createRunnableDevEnvironment(name, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultCreateDevEnvironment(name: string, config: ResolvedConfig) {
|
function defaultCreateDevEnvironment(name: string, config: ResolvedConfig) {
|
||||||
return new DevEnvironment(name, config, {
|
return createRunnableDevEnvironment(name, config)
|
||||||
hot: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedDevEnvironmentOptions = Required<DevEnvironmentOptions>
|
export type ResolvedDevEnvironmentOptions = Required<DevEnvironmentOptions>
|
||||||
@ -608,9 +600,7 @@ export function resolveDevEnvironmentOptions(
|
|||||||
dev?.createEnvironment ??
|
dev?.createEnvironment ??
|
||||||
(environmentName === 'client'
|
(environmentName === 'client'
|
||||||
? defaultCreateClientDevEnvironment
|
? defaultCreateClientDevEnvironment
|
||||||
: environmentName === 'ssr'
|
: defaultCreateDevEnvironment),
|
||||||
? defaultCreateSsrDevEnvironment
|
|
||||||
: defaultCreateDevEnvironment),
|
|
||||||
recoverable: dev?.recoverable ?? consumer === 'client',
|
recoverable: dev?.recoverable ?? consumer === 'client',
|
||||||
moduleRunnerTransform:
|
moduleRunnerTransform:
|
||||||
dev?.moduleRunnerTransform ??
|
dev?.moduleRunnerTransform ??
|
||||||
|
@ -19,7 +19,6 @@ export { formatPostcssSourceMap, preprocessCSS } from './plugins/css'
|
|||||||
export { transformWithEsbuild } from './plugins/esbuild'
|
export { transformWithEsbuild } from './plugins/esbuild'
|
||||||
export { buildErrorMessage } from './server/middlewares/error'
|
export { buildErrorMessage } from './server/middlewares/error'
|
||||||
|
|
||||||
export { RemoteEnvironmentTransport } from './server/environmentTransport'
|
|
||||||
export {
|
export {
|
||||||
createRunnableDevEnvironment,
|
createRunnableDevEnvironment,
|
||||||
isRunnableDevEnvironment,
|
isRunnableDevEnvironment,
|
||||||
@ -35,7 +34,6 @@ export { BuildEnvironment } from './build'
|
|||||||
export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule'
|
export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule'
|
||||||
export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner'
|
export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner'
|
||||||
export { createServerHotChannel } from './server/hmr'
|
export { createServerHotChannel } from './server/hmr'
|
||||||
export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector'
|
|
||||||
export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform'
|
export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform'
|
||||||
export type { ModuleRunnerTransformOptions } from './ssr/ssrTransform'
|
export type { ModuleRunnerTransformOptions } from './ssr/ssrTransform'
|
||||||
|
|
||||||
@ -165,6 +163,7 @@ export type {
|
|||||||
HMRBroadcasterClient,
|
HMRBroadcasterClient,
|
||||||
ServerHMRChannel,
|
ServerHMRChannel,
|
||||||
HMRChannel,
|
HMRChannel,
|
||||||
|
HotChannelListener,
|
||||||
HotChannel,
|
HotChannel,
|
||||||
ServerHotChannel,
|
ServerHotChannel,
|
||||||
HotChannelClient,
|
HotChannelClient,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { createDebugger, getHash, promiseWithResolvers } from '../utils'
|
import { createDebugger, getHash } from '../utils'
|
||||||
import type { PromiseWithResolvers } from '../utils'
|
import {
|
||||||
|
type PromiseWithResolvers,
|
||||||
|
promiseWithResolvers,
|
||||||
|
} from '../../shared/utils'
|
||||||
import type { DevEnvironment } from '../server/environment'
|
import type { DevEnvironment } from '../server/environment'
|
||||||
import { devToScanEnvironment } from './scan'
|
import { devToScanEnvironment } from './scan'
|
||||||
import {
|
import {
|
||||||
|
@ -226,7 +226,7 @@ async function getDevEnvironment(
|
|||||||
// @ts-expect-error This plugin requires a ViteDevServer instance.
|
// @ts-expect-error This plugin requires a ViteDevServer instance.
|
||||||
config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias'))
|
config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias'))
|
||||||
|
|
||||||
const environment = new DevEnvironment('client', config, { hot: false })
|
const environment = new DevEnvironment('client', config, { hot: true })
|
||||||
await environment.init()
|
await environment.init()
|
||||||
|
|
||||||
return environment
|
return environment
|
||||||
|
@ -10,7 +10,7 @@ import type {
|
|||||||
ResolvedConfig,
|
ResolvedConfig,
|
||||||
ResolvedEnvironmentOptions,
|
ResolvedEnvironmentOptions,
|
||||||
} from '../config'
|
} from '../config'
|
||||||
import { mergeConfig, promiseWithResolvers } from '../utils'
|
import { mergeConfig } from '../utils'
|
||||||
import { fetchModule } from '../ssr/fetchModule'
|
import { fetchModule } from '../ssr/fetchModule'
|
||||||
import type { DepsOptimizer } from '../optimizer'
|
import type { DepsOptimizer } from '../optimizer'
|
||||||
import { isDepOptimizationDisabled } from '../optimizer'
|
import { isDepOptimizationDisabled } from '../optimizer'
|
||||||
@ -20,11 +20,12 @@ import {
|
|||||||
} from '../optimizer/optimizer'
|
} from '../optimizer/optimizer'
|
||||||
import { resolveEnvironmentPlugins } from '../plugin'
|
import { resolveEnvironmentPlugins } from '../plugin'
|
||||||
import { ERR_OUTDATED_OPTIMIZED_DEP } from '../constants'
|
import { ERR_OUTDATED_OPTIMIZED_DEP } from '../constants'
|
||||||
|
import { promiseWithResolvers } from '../../shared/utils'
|
||||||
import type { ViteDevServer } from '../server'
|
import type { ViteDevServer } from '../server'
|
||||||
import { EnvironmentModuleGraph } from './moduleGraph'
|
import { EnvironmentModuleGraph } from './moduleGraph'
|
||||||
import type { EnvironmentModuleNode } from './moduleGraph'
|
import type { EnvironmentModuleNode } from './moduleGraph'
|
||||||
import type { HotChannel } from './hmr'
|
import type { HotChannel, NormalizedHotChannel } from './hmr'
|
||||||
import { createNoopHotChannel, getShortName, updateModules } from './hmr'
|
import { getShortName, normalizeHotChannel, updateModules } from './hmr'
|
||||||
import type { TransformResult } from './transformRequest'
|
import type { TransformResult } from './transformRequest'
|
||||||
import { transformRequest } from './transformRequest'
|
import { transformRequest } from './transformRequest'
|
||||||
import type { EnvironmentPluginContainer } from './pluginContainer'
|
import type { EnvironmentPluginContainer } from './pluginContainer'
|
||||||
@ -32,16 +33,15 @@ import {
|
|||||||
ERR_CLOSED_SERVER,
|
ERR_CLOSED_SERVER,
|
||||||
createEnvironmentPluginContainer,
|
createEnvironmentPluginContainer,
|
||||||
} from './pluginContainer'
|
} from './pluginContainer'
|
||||||
import type { RemoteEnvironmentTransport } from './environmentTransport'
|
import { type WebSocketServer, isWebSocketServer } from './ws'
|
||||||
import { isWebSocketServer } from './ws'
|
|
||||||
import { warmupFiles } from './warmup'
|
import { warmupFiles } from './warmup'
|
||||||
|
|
||||||
export interface DevEnvironmentContext {
|
export interface DevEnvironmentContext {
|
||||||
hot: false | HotChannel
|
hot: boolean
|
||||||
|
transport?: HotChannel | WebSocketServer
|
||||||
options?: EnvironmentOptions
|
options?: EnvironmentOptions
|
||||||
remoteRunner?: {
|
remoteRunner?: {
|
||||||
inlineSourceMap?: boolean
|
inlineSourceMap?: boolean
|
||||||
transport?: RemoteEnvironmentTransport
|
|
||||||
}
|
}
|
||||||
depsOptimizer?: DepsOptimizer
|
depsOptimizer?: DepsOptimizer
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ export class DevEnvironment extends BaseEnvironment {
|
|||||||
* @example
|
* @example
|
||||||
* environment.hot.send({ type: 'full-reload' })
|
* environment.hot.send({ type: 'full-reload' })
|
||||||
*/
|
*/
|
||||||
hot: HotChannel
|
hot: NormalizedHotChannel
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
config: ResolvedConfig,
|
config: ResolvedConfig,
|
||||||
@ -117,12 +117,21 @@ export class DevEnvironment extends BaseEnvironment {
|
|||||||
this.pluginContainer!.resolveId(url, undefined),
|
this.pluginContainer!.resolveId(url, undefined),
|
||||||
)
|
)
|
||||||
|
|
||||||
this.hot = context.hot || createNoopHotChannel()
|
|
||||||
|
|
||||||
this._crawlEndFinder = setupOnCrawlEnd()
|
this._crawlEndFinder = setupOnCrawlEnd()
|
||||||
|
|
||||||
this._remoteRunnerOptions = context.remoteRunner ?? {}
|
this._remoteRunnerOptions = context.remoteRunner ?? {}
|
||||||
context.remoteRunner?.transport?.register(this)
|
|
||||||
|
this.hot = context.transport
|
||||||
|
? isWebSocketServer in context.transport
|
||||||
|
? context.transport
|
||||||
|
: normalizeHotChannel(context.transport, context.hot)
|
||||||
|
: normalizeHotChannel({}, context.hot)
|
||||||
|
|
||||||
|
this.hot.setInvokeHandler({
|
||||||
|
fetchModule: (id, importer, options) => {
|
||||||
|
return this.fetchModule(id, importer, options)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.hot.on('vite:invalidate', async ({ path, message }) => {
|
this.hot.on('vite:invalidate', async ({ path, message }) => {
|
||||||
invalidateModule(this, {
|
invalidateModule(this, {
|
||||||
@ -226,7 +235,7 @@ export class DevEnvironment extends BaseEnvironment {
|
|||||||
this.pluginContainer.close(),
|
this.pluginContainer.close(),
|
||||||
this.depsOptimizer?.close(),
|
this.depsOptimizer?.close(),
|
||||||
// WebSocketServer is independent of HotChannel and should not be closed on environment close
|
// WebSocketServer is independent of HotChannel and should not be closed on environment close
|
||||||
isWebSocketServer in this.hot ? Promise.resolve() : this.hot.close(),
|
isWebSocketServer in this.hot ? Promise.resolve() : this.hot.close?.(),
|
||||||
(async () => {
|
(async () => {
|
||||||
while (this._pendingRequests.size > 0) {
|
while (this._pendingRequests.size > 0) {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import type { DevEnvironment } from './environment'
|
|
||||||
|
|
||||||
export class RemoteEnvironmentTransport {
|
|
||||||
constructor(
|
|
||||||
private readonly options: {
|
|
||||||
send: (data: any) => void
|
|
||||||
onMessage: (handler: (data: any) => void) => void
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
|
|
||||||
register(environment: DevEnvironment): void {
|
|
||||||
this.options.onMessage(async (data) => {
|
|
||||||
if (typeof data !== 'object' || !data || !data.__v) return
|
|
||||||
|
|
||||||
const method = data.m as 'fetchModule'
|
|
||||||
const parameters = data.a as [string, string]
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await environment[method](...parameters)
|
|
||||||
this.options.send({
|
|
||||||
__v: true,
|
|
||||||
r: result,
|
|
||||||
i: data.i,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.options.send({
|
|
||||||
__v: true,
|
|
||||||
e: {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
},
|
|
||||||
i: data.i,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import type { DevEnvironmentContext } from '../environment'
|
|||||||
import { DevEnvironment } from '../environment'
|
import { DevEnvironment } from '../environment'
|
||||||
import type { ServerModuleRunnerOptions } from '../../ssr/runtime/serverModuleRunner'
|
import type { ServerModuleRunnerOptions } from '../../ssr/runtime/serverModuleRunner'
|
||||||
import { createServerModuleRunner } from '../../ssr/runtime/serverModuleRunner'
|
import { createServerModuleRunner } from '../../ssr/runtime/serverModuleRunner'
|
||||||
import type { HotChannel } from '../hmr'
|
|
||||||
import { createServerHotChannel } from '../hmr'
|
import { createServerHotChannel } from '../hmr'
|
||||||
import type { Environment } from '../../environment'
|
import type { Environment } from '../../environment'
|
||||||
|
|
||||||
@ -13,8 +12,11 @@ export function createRunnableDevEnvironment(
|
|||||||
config: ResolvedConfig,
|
config: ResolvedConfig,
|
||||||
context: RunnableDevEnvironmentContext = {},
|
context: RunnableDevEnvironmentContext = {},
|
||||||
): DevEnvironment {
|
): DevEnvironment {
|
||||||
|
if (context.transport == null) {
|
||||||
|
context.transport = createServerHotChannel()
|
||||||
|
}
|
||||||
if (context.hot == null) {
|
if (context.hot == null) {
|
||||||
context.hot = createServerHotChannel()
|
context.hot = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RunnableDevEnvironment(name, config, context)
|
return new RunnableDevEnvironment(name, config, context)
|
||||||
@ -27,7 +29,7 @@ export interface RunnableDevEnvironmentContext
|
|||||||
options?: ServerModuleRunnerOptions,
|
options?: ServerModuleRunnerOptions,
|
||||||
) => ModuleRunner
|
) => ModuleRunner
|
||||||
runnerOptions?: ServerModuleRunnerOptions
|
runnerOptions?: ServerModuleRunnerOptions
|
||||||
hot?: false | HotChannel
|
hot?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRunnableDevEnvironment(
|
export function isRunnableDevEnvironment(
|
||||||
|
@ -4,6 +4,11 @@ import { EventEmitter } from 'node:events'
|
|||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import type { CustomPayload, HotPayload, Update } from 'types/hmrPayload'
|
import type { CustomPayload, HotPayload, Update } from 'types/hmrPayload'
|
||||||
import type { RollupError } from 'rollup'
|
import type { RollupError } from 'rollup'
|
||||||
|
import type {
|
||||||
|
InvokeMethods,
|
||||||
|
InvokeResponseData,
|
||||||
|
InvokeSendData,
|
||||||
|
} from '../../shared/invokeMethods'
|
||||||
import { CLIENT_DIR } from '../constants'
|
import { CLIENT_DIR } from '../constants'
|
||||||
import { createDebugger, normalizePath } from '../utils'
|
import { createDebugger, normalizePath } from '../utils'
|
||||||
import type { InferCustomEventPayload, ViteDevServer } from '..'
|
import type { InferCustomEventPayload, ViteDevServer } from '..'
|
||||||
@ -71,6 +76,51 @@ interface PropagationBoundary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HotChannelClient {
|
export interface HotChannelClient {
|
||||||
|
send(payload: HotPayload): void
|
||||||
|
}
|
||||||
|
/** @deprecated use `HotChannelClient` instead */
|
||||||
|
export type HMRBroadcasterClient = HotChannelClient
|
||||||
|
|
||||||
|
export type HotChannelListener<T extends string = string> = (
|
||||||
|
data: InferCustomEventPayload<T>,
|
||||||
|
client: HotChannelClient,
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export interface HotChannel<Api = any> {
|
||||||
|
/**
|
||||||
|
* Broadcast events to all clients
|
||||||
|
*/
|
||||||
|
send?(payload: HotPayload): void
|
||||||
|
/**
|
||||||
|
* Handle custom event emitted by `import.meta.hot.send`
|
||||||
|
*/
|
||||||
|
on?<T extends string>(event: T, listener: HotChannelListener<T>): void
|
||||||
|
on?(event: 'connection', listener: () => void): void
|
||||||
|
/**
|
||||||
|
* Unregister event listener
|
||||||
|
*/
|
||||||
|
off?(event: string, listener: Function): void
|
||||||
|
/**
|
||||||
|
* Start listening for messages
|
||||||
|
*/
|
||||||
|
listen?(): void
|
||||||
|
/**
|
||||||
|
* Disconnect all clients, called when server is closed or restarted.
|
||||||
|
*/
|
||||||
|
close?(): Promise<unknown> | void
|
||||||
|
|
||||||
|
api?: Api
|
||||||
|
}
|
||||||
|
/** @deprecated use `HotChannel` instead */
|
||||||
|
export type HMRChannel = HotChannel
|
||||||
|
|
||||||
|
export function getShortName(file: string, root: string): string {
|
||||||
|
return file.startsWith(withTrailingSlash(root))
|
||||||
|
? path.posix.relative(root, file)
|
||||||
|
: file
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NormalizedHotChannelClient {
|
||||||
/**
|
/**
|
||||||
* Send event to the client
|
* Send event to the client
|
||||||
*/
|
*/
|
||||||
@ -80,10 +130,8 @@ export interface HotChannelClient {
|
|||||||
*/
|
*/
|
||||||
send(event: string, payload?: CustomPayload['data']): void
|
send(event: string, payload?: CustomPayload['data']): void
|
||||||
}
|
}
|
||||||
/** @deprecated use `HotChannelClient` instead */
|
|
||||||
export type HMRBroadcasterClient = HotChannelClient
|
|
||||||
|
|
||||||
export interface HotChannel {
|
export interface NormalizedHotChannel<Api = any> {
|
||||||
/**
|
/**
|
||||||
* Broadcast events to all clients
|
* Broadcast events to all clients
|
||||||
*/
|
*/
|
||||||
@ -99,8 +147,7 @@ export interface HotChannel {
|
|||||||
event: T,
|
event: T,
|
||||||
listener: (
|
listener: (
|
||||||
data: InferCustomEventPayload<T>,
|
data: InferCustomEventPayload<T>,
|
||||||
client: HotChannelClient,
|
client: NormalizedHotChannelClient,
|
||||||
...args: any[]
|
|
||||||
) => void,
|
) => void,
|
||||||
): void
|
): void
|
||||||
on(event: 'connection', listener: () => void): void
|
on(event: 'connection', listener: () => void): void
|
||||||
@ -108,6 +155,9 @@ export interface HotChannel {
|
|||||||
* Unregister event listener
|
* Unregister event listener
|
||||||
*/
|
*/
|
||||||
off(event: string, listener: Function): void
|
off(event: string, listener: Function): void
|
||||||
|
/** @internal */
|
||||||
|
setInvokeHandler(invokeHandlers: InvokeMethods | undefined): void
|
||||||
|
handleInvoke(payload: HotPayload): Promise<{ r: any } | { e: any }>
|
||||||
/**
|
/**
|
||||||
* Start listening for messages
|
* Start listening for messages
|
||||||
*/
|
*/
|
||||||
@ -116,14 +166,169 @@ export interface HotChannel {
|
|||||||
* Disconnect all clients, called when server is closed or restarted.
|
* Disconnect all clients, called when server is closed or restarted.
|
||||||
*/
|
*/
|
||||||
close(): Promise<unknown> | void
|
close(): Promise<unknown> | void
|
||||||
}
|
|
||||||
/** @deprecated use `HotChannel` instead */
|
|
||||||
export type HMRChannel = HotChannel
|
|
||||||
|
|
||||||
export function getShortName(file: string, root: string): string {
|
api?: Api
|
||||||
return file.startsWith(withTrailingSlash(root))
|
}
|
||||||
? path.posix.relative(root, file)
|
|
||||||
: file
|
export const normalizeHotChannel = (
|
||||||
|
channel: HotChannel,
|
||||||
|
enableHmr: boolean,
|
||||||
|
): NormalizedHotChannel => {
|
||||||
|
const normalizedListenerMap = new WeakMap<
|
||||||
|
(data: any, client: NormalizedHotChannelClient) => void | Promise<void>,
|
||||||
|
(data: any, client: HotChannelClient) => void | Promise<void>
|
||||||
|
>()
|
||||||
|
const listenersForEvents = new Map<
|
||||||
|
string,
|
||||||
|
Set<(data: any, client: HotChannelClient) => void | Promise<void>>
|
||||||
|
>()
|
||||||
|
|
||||||
|
let invokeHandlers: InvokeMethods | undefined
|
||||||
|
let listenerForInvokeHandler:
|
||||||
|
| ((data: InvokeSendData, client: HotChannelClient) => void)
|
||||||
|
| undefined
|
||||||
|
const handleInvoke = async <T extends keyof InvokeMethods>(
|
||||||
|
payload: HotPayload,
|
||||||
|
) => {
|
||||||
|
if (!invokeHandlers) {
|
||||||
|
return {
|
||||||
|
e: {
|
||||||
|
name: 'TransportError',
|
||||||
|
message: 'invokeHandlers is not set',
|
||||||
|
stack: new Error().stack,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: InvokeSendData<T> = (payload as CustomPayload).data
|
||||||
|
const { name, data: args } = data
|
||||||
|
try {
|
||||||
|
const invokeHandler = invokeHandlers[name]
|
||||||
|
// @ts-expect-error `invokeHandler` is `InvokeMethods[T]`, so passing the args is fine
|
||||||
|
const result = await invokeHandler(...args)
|
||||||
|
return { r: result }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
e: {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...channel,
|
||||||
|
on: (
|
||||||
|
event: string,
|
||||||
|
fn: (data: any, client: NormalizedHotChannelClient) => void,
|
||||||
|
) => {
|
||||||
|
if (event === 'connection') {
|
||||||
|
channel.on?.(event, fn as () => void)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerWithNormalizedClient = (
|
||||||
|
data: any,
|
||||||
|
client: HotChannelClient,
|
||||||
|
) => {
|
||||||
|
const normalizedClient: NormalizedHotChannelClient = {
|
||||||
|
send: (...args) => {
|
||||||
|
let payload: HotPayload
|
||||||
|
if (typeof args[0] === 'string') {
|
||||||
|
payload = {
|
||||||
|
type: 'custom',
|
||||||
|
event: args[0],
|
||||||
|
data: args[1],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload = args[0]
|
||||||
|
}
|
||||||
|
client.send(payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fn(data, normalizedClient)
|
||||||
|
}
|
||||||
|
normalizedListenerMap.set(fn, listenerWithNormalizedClient)
|
||||||
|
|
||||||
|
channel.on?.(event, listenerWithNormalizedClient)
|
||||||
|
if (!listenersForEvents.has(event)) {
|
||||||
|
listenersForEvents.set(event, new Set())
|
||||||
|
}
|
||||||
|
listenersForEvents.get(event)!.add(listenerWithNormalizedClient)
|
||||||
|
},
|
||||||
|
off: (event: string, fn: () => void) => {
|
||||||
|
if (event === 'connection') {
|
||||||
|
channel.off?.(event, fn as () => void)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedListener = normalizedListenerMap.get(fn)
|
||||||
|
if (normalizedListener) {
|
||||||
|
channel.off?.(event, normalizedListener)
|
||||||
|
listenersForEvents.get(event)?.delete(normalizedListener)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setInvokeHandler(_invokeHandlers) {
|
||||||
|
invokeHandlers = _invokeHandlers
|
||||||
|
if (!_invokeHandlers) {
|
||||||
|
if (listenerForInvokeHandler) {
|
||||||
|
channel.off?.('vite:invoke', listenerForInvokeHandler)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerForInvokeHandler = async (payload, client) => {
|
||||||
|
const responseInvoke = payload.id.replace('send', 'response') as
|
||||||
|
| 'response'
|
||||||
|
| `response:${string}`
|
||||||
|
client.send({
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:invoke',
|
||||||
|
data: {
|
||||||
|
name: payload.name,
|
||||||
|
id: responseInvoke,
|
||||||
|
data: (await handleInvoke({
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:invoke',
|
||||||
|
data: payload,
|
||||||
|
}))!,
|
||||||
|
} satisfies InvokeResponseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
channel.on?.('vite:invoke', listenerForInvokeHandler)
|
||||||
|
},
|
||||||
|
handleInvoke,
|
||||||
|
send: (...args: any[]) => {
|
||||||
|
let payload: HotPayload
|
||||||
|
if (typeof args[0] === 'string') {
|
||||||
|
payload = {
|
||||||
|
type: 'custom',
|
||||||
|
event: args[0],
|
||||||
|
data: args[1],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
enableHmr ||
|
||||||
|
payload.type === 'connected' ||
|
||||||
|
payload.type === 'ping' ||
|
||||||
|
payload.type === 'custom' ||
|
||||||
|
payload.type === 'error'
|
||||||
|
) {
|
||||||
|
channel.send?.(payload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listen() {
|
||||||
|
return channel.listen?.()
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
return channel.close?.()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSortedPluginsByHotUpdateHook(
|
export function getSortedPluginsByHotUpdateHook(
|
||||||
@ -892,12 +1097,14 @@ async function readModifiedFile(file: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerHotChannel extends HotChannel {
|
export type ServerHotChannelApi = {
|
||||||
api: {
|
innerEmitter: EventEmitter
|
||||||
innerEmitter: EventEmitter
|
outsideEmitter: EventEmitter
|
||||||
outsideEmitter: EventEmitter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ServerHotChannel = HotChannel<ServerHotChannelApi>
|
||||||
|
export type NormalizedServerHotChannel =
|
||||||
|
NormalizedHotChannel<ServerHotChannelApi>
|
||||||
/** @deprecated use `ServerHotChannel` instead */
|
/** @deprecated use `ServerHotChannel` instead */
|
||||||
export type ServerHMRChannel = ServerHotChannel
|
export type ServerHMRChannel = ServerHotChannel
|
||||||
|
|
||||||
@ -906,17 +1113,7 @@ export function createServerHotChannel(): ServerHotChannel {
|
|||||||
const outsideEmitter = new EventEmitter()
|
const outsideEmitter = new EventEmitter()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
send(...args: any[]) {
|
send(payload: HotPayload) {
|
||||||
let payload: HotPayload
|
|
||||||
if (typeof args[0] === 'string') {
|
|
||||||
payload = {
|
|
||||||
type: 'custom',
|
|
||||||
event: args[0],
|
|
||||||
data: args[1],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = args[0]
|
|
||||||
}
|
|
||||||
outsideEmitter.emit('send', payload)
|
outsideEmitter.emit('send', payload)
|
||||||
},
|
},
|
||||||
off(event, listener: () => void) {
|
off(event, listener: () => void) {
|
||||||
@ -939,23 +1136,9 @@ export function createServerHotChannel(): ServerHotChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNoopHotChannel(): HotChannel {
|
|
||||||
function noop() {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
send: noop,
|
|
||||||
on: noop,
|
|
||||||
off: noop,
|
|
||||||
listen: noop,
|
|
||||||
close: noop,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated use `environment.hot` instead */
|
/** @deprecated use `environment.hot` instead */
|
||||||
export interface HotBroadcaster extends HotChannel {
|
export interface HotBroadcaster extends NormalizedHotChannel {
|
||||||
readonly channels: HotChannel[]
|
readonly channels: NormalizedHotChannel[]
|
||||||
/**
|
/**
|
||||||
* A noop.
|
* A noop.
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@ -966,12 +1149,22 @@ export interface HotBroadcaster extends HotChannel {
|
|||||||
/** @deprecated use `environment.hot` instead */
|
/** @deprecated use `environment.hot` instead */
|
||||||
export type HMRBroadcaster = HotBroadcaster
|
export type HMRBroadcaster = HotBroadcaster
|
||||||
|
|
||||||
export function createDeprecatedHotBroadcaster(ws: HotChannel): HotBroadcaster {
|
export function createDeprecatedHotBroadcaster(
|
||||||
|
ws: NormalizedHotChannel,
|
||||||
|
): HotBroadcaster {
|
||||||
const broadcaster: HotBroadcaster = {
|
const broadcaster: HotBroadcaster = {
|
||||||
on: ws.on,
|
on: ws.on,
|
||||||
off: ws.off,
|
off: ws.off,
|
||||||
listen: ws.listen,
|
listen: ws.listen,
|
||||||
send: ws.send,
|
send: ws.send,
|
||||||
|
setInvokeHandler: ws.setInvokeHandler,
|
||||||
|
handleInvoke: async () => ({
|
||||||
|
e: {
|
||||||
|
name: 'TransportError',
|
||||||
|
message: 'handleInvoke not implemented',
|
||||||
|
stack: new Error().stack,
|
||||||
|
},
|
||||||
|
}),
|
||||||
get channels() {
|
get channels() {
|
||||||
return [ws]
|
return [ws]
|
||||||
},
|
},
|
||||||
@ -979,7 +1172,9 @@ export function createDeprecatedHotBroadcaster(ws: HotChannel): HotBroadcaster {
|
|||||||
return broadcaster
|
return broadcaster
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
return Promise.all(broadcaster.channels.map((channel) => channel.close()))
|
return Promise.all(
|
||||||
|
broadcaster.channels.map((channel) => channel.close?.()),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return broadcaster
|
return broadcaster
|
||||||
|
@ -9,11 +9,11 @@ import colors from 'picocolors'
|
|||||||
import type { WebSocket as WebSocketRaw } from 'ws'
|
import type { WebSocket as WebSocketRaw } from 'ws'
|
||||||
import { WebSocketServer as WebSocketServerRaw_ } from 'ws'
|
import { WebSocketServer as WebSocketServerRaw_ } from 'ws'
|
||||||
import type { WebSocket as WebSocketTypes } from 'dep-types/ws'
|
import type { WebSocket as WebSocketTypes } from 'dep-types/ws'
|
||||||
import type { ErrorPayload, HotPayload } from 'types/hmrPayload'
|
import type { ErrorPayload } from 'types/hmrPayload'
|
||||||
import type { InferCustomEventPayload } from 'types/customEvent'
|
import type { InferCustomEventPayload } from 'types/customEvent'
|
||||||
import type { HotChannelClient, ResolvedConfig } from '..'
|
import type { HotChannelClient, ResolvedConfig } from '..'
|
||||||
import { isObject } from '../utils'
|
import { isObject } from '../utils'
|
||||||
import type { HotChannel } from './hmr'
|
import { type NormalizedHotChannel, normalizeHotChannel } from './hmr'
|
||||||
import type { HttpServer } from '.'
|
import type { HttpServer } from '.'
|
||||||
|
|
||||||
/* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version
|
/* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version
|
||||||
@ -29,24 +29,12 @@ export const HMR_HEADER = 'vite-hmr'
|
|||||||
export type WebSocketCustomListener<T> = (
|
export type WebSocketCustomListener<T> = (
|
||||||
data: T,
|
data: T,
|
||||||
client: WebSocketClient,
|
client: WebSocketClient,
|
||||||
|
invoke?: 'send' | `send:${string}`,
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
export const isWebSocketServer = Symbol('isWebSocketServer')
|
export const isWebSocketServer = Symbol('isWebSocketServer')
|
||||||
|
|
||||||
export interface WebSocketServer extends HotChannel {
|
export interface WebSocketServer extends NormalizedHotChannel {
|
||||||
[isWebSocketServer]: true
|
|
||||||
/**
|
|
||||||
* Listen on port and host
|
|
||||||
*/
|
|
||||||
listen(): void
|
|
||||||
/**
|
|
||||||
* Get all connected clients.
|
|
||||||
*/
|
|
||||||
clients: Set<WebSocketClient>
|
|
||||||
/**
|
|
||||||
* Disconnect all clients and terminate the server.
|
|
||||||
*/
|
|
||||||
close(): Promise<void>
|
|
||||||
/**
|
/**
|
||||||
* Handle custom event emitted by `import.meta.hot.send`
|
* Handle custom event emitted by `import.meta.hot.send`
|
||||||
*/
|
*/
|
||||||
@ -62,6 +50,20 @@ export interface WebSocketServer extends HotChannel {
|
|||||||
off: WebSocketTypes.Server['off'] & {
|
off: WebSocketTypes.Server['off'] & {
|
||||||
(event: string, listener: Function): void
|
(event: string, listener: Function): void
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Listen on port and host
|
||||||
|
*/
|
||||||
|
listen(): void
|
||||||
|
/**
|
||||||
|
* Disconnect all clients and terminate the server.
|
||||||
|
*/
|
||||||
|
close(): Promise<void>
|
||||||
|
|
||||||
|
[isWebSocketServer]: true
|
||||||
|
/**
|
||||||
|
* Get all connected clients.
|
||||||
|
*/
|
||||||
|
clients: Set<WebSocketClient>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSocketClient extends HotChannelClient {
|
export interface WebSocketClient extends HotChannelClient {
|
||||||
@ -100,6 +102,14 @@ export function createWebSocketServer(
|
|||||||
},
|
},
|
||||||
on: noop as any as WebSocketServer['on'],
|
on: noop as any as WebSocketServer['on'],
|
||||||
off: noop as any as WebSocketServer['off'],
|
off: noop as any as WebSocketServer['off'],
|
||||||
|
setInvokeHandler: noop,
|
||||||
|
handleInvoke: async () => ({
|
||||||
|
e: {
|
||||||
|
name: 'TransportError',
|
||||||
|
message: 'handleInvoke not implemented',
|
||||||
|
stack: new Error().stack,
|
||||||
|
},
|
||||||
|
}),
|
||||||
listen: noop,
|
listen: noop,
|
||||||
send: noop,
|
send: noop,
|
||||||
}
|
}
|
||||||
@ -209,7 +219,9 @@ export function createWebSocketServer(
|
|||||||
const listeners = customListeners.get(parsed.event)
|
const listeners = customListeners.get(parsed.event)
|
||||||
if (!listeners?.size) return
|
if (!listeners?.size) return
|
||||||
const client = getSocketClient(socket)
|
const client = getSocketClient(socket)
|
||||||
listeners.forEach((listener) => listener(parsed.data, client))
|
listeners.forEach((listener) =>
|
||||||
|
listener(parsed.data, client, parsed.invoke),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
socket.on('error', (err) => {
|
socket.on('error', (err) => {
|
||||||
config.logger.error(`${colors.red(`ws error:`)}\n${err.stack}`, {
|
config.logger.error(`${colors.red(`ws error:`)}\n${err.stack}`, {
|
||||||
@ -243,17 +255,7 @@ export function createWebSocketServer(
|
|||||||
function getSocketClient(socket: WebSocketRaw) {
|
function getSocketClient(socket: WebSocketRaw) {
|
||||||
if (!clientsMap.has(socket)) {
|
if (!clientsMap.has(socket)) {
|
||||||
clientsMap.set(socket, {
|
clientsMap.set(socket, {
|
||||||
send: (...args) => {
|
send: (payload) => {
|
||||||
let payload: HotPayload
|
|
||||||
if (typeof args[0] === 'string') {
|
|
||||||
payload = {
|
|
||||||
type: 'custom',
|
|
||||||
event: args[0],
|
|
||||||
data: args[1],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = args[0]
|
|
||||||
}
|
|
||||||
socket.send(JSON.stringify(payload))
|
socket.send(JSON.stringify(payload))
|
||||||
},
|
},
|
||||||
socket,
|
socket,
|
||||||
@ -268,86 +270,90 @@ export function createWebSocketServer(
|
|||||||
// connected client.
|
// connected client.
|
||||||
let bufferedError: ErrorPayload | null = null
|
let bufferedError: ErrorPayload | null = null
|
||||||
|
|
||||||
return {
|
const normalizedHotChannel = normalizeHotChannel(
|
||||||
[isWebSocketServer]: true,
|
{
|
||||||
listen: () => {
|
send(payload) {
|
||||||
wsHttpServer?.listen(port, host)
|
if (payload.type === 'error' && !wss.clients.size) {
|
||||||
},
|
bufferedError = payload
|
||||||
on: ((event: string, fn: () => void) => {
|
return
|
||||||
if (wsServerEvents.includes(event)) wss.on(event, fn)
|
}
|
||||||
else {
|
|
||||||
|
const stringified = JSON.stringify(payload)
|
||||||
|
wss.clients.forEach((client) => {
|
||||||
|
// readyState 1 means the connection is open
|
||||||
|
if (client.readyState === 1) {
|
||||||
|
client.send(stringified)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
on(event: string, fn: any) {
|
||||||
if (!customListeners.has(event)) {
|
if (!customListeners.has(event)) {
|
||||||
customListeners.set(event, new Set())
|
customListeners.set(event, new Set())
|
||||||
}
|
}
|
||||||
customListeners.get(event)!.add(fn)
|
customListeners.get(event)!.add(fn)
|
||||||
|
},
|
||||||
|
off(event: string, fn: any) {
|
||||||
|
customListeners.get(event)?.delete(fn)
|
||||||
|
},
|
||||||
|
listen() {
|
||||||
|
wsHttpServer?.listen(port, host)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
// should remove listener if hmr.server is set
|
||||||
|
// otherwise the old listener swallows all WebSocket connections
|
||||||
|
if (hmrServerWsListener && wsServer) {
|
||||||
|
wsServer.off('upgrade', hmrServerWsListener)
|
||||||
|
}
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
wss.clients.forEach((client) => {
|
||||||
|
client.terminate()
|
||||||
|
})
|
||||||
|
wss.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
if (wsHttpServer) {
|
||||||
|
wsHttpServer.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config.server.hmr !== false,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...normalizedHotChannel,
|
||||||
|
|
||||||
|
on: ((event: string, fn: any) => {
|
||||||
|
if (wsServerEvents.includes(event)) {
|
||||||
|
wss.on(event, fn)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
normalizedHotChannel.on(event, fn)
|
||||||
}) as WebSocketServer['on'],
|
}) as WebSocketServer['on'],
|
||||||
off: ((event: string, fn: () => void) => {
|
off: ((event: string, fn: any) => {
|
||||||
if (wsServerEvents.includes(event)) {
|
if (wsServerEvents.includes(event)) {
|
||||||
wss.off(event, fn)
|
wss.off(event, fn)
|
||||||
} else {
|
return
|
||||||
customListeners.get(event)?.delete(fn)
|
|
||||||
}
|
}
|
||||||
|
normalizedHotChannel.off(event, fn)
|
||||||
}) as WebSocketServer['off'],
|
}) as WebSocketServer['off'],
|
||||||
|
async close() {
|
||||||
|
await normalizedHotChannel.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
[isWebSocketServer]: true,
|
||||||
get clients() {
|
get clients() {
|
||||||
return new Set(Array.from(wss.clients).map(getSocketClient))
|
return new Set(Array.from(wss.clients).map(getSocketClient))
|
||||||
},
|
},
|
||||||
|
|
||||||
send(...args: any[]) {
|
|
||||||
let payload: HotPayload
|
|
||||||
if (typeof args[0] === 'string') {
|
|
||||||
payload = {
|
|
||||||
type: 'custom',
|
|
||||||
event: args[0],
|
|
||||||
data: args[1],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'error' && !wss.clients.size) {
|
|
||||||
bufferedError = payload
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringified = JSON.stringify(payload)
|
|
||||||
wss.clients.forEach((client) => {
|
|
||||||
// readyState 1 means the connection is open
|
|
||||||
if (client.readyState === 1) {
|
|
||||||
client.send(stringified)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
close() {
|
|
||||||
// should remove listener if hmr.server is set
|
|
||||||
// otherwise the old listener swallows all WebSocket connections
|
|
||||||
if (hmrServerWsListener && wsServer) {
|
|
||||||
wsServer.off('upgrade', hmrServerWsListener)
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
wss.clients.forEach((client) => {
|
|
||||||
client.terminate()
|
|
||||||
})
|
|
||||||
wss.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
if (wsHttpServer) {
|
|
||||||
wsHttpServer.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,30 @@
|
|||||||
|
|
||||||
import { BroadcastChannel, parentPort } from 'node:worker_threads'
|
import { BroadcastChannel, parentPort } from 'node:worker_threads'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { ESModulesEvaluator, ModuleRunner, RemoteRunnerTransport } from 'vite/module-runner'
|
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
||||||
|
|
||||||
if (!parentPort) {
|
if (!parentPort) {
|
||||||
throw new Error('File "worker.js" must be run in a worker thread')
|
throw new Error('File "worker.js" must be run in a worker thread')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('worker_threads').MessagePort} */
|
||||||
|
const pPort = parentPort
|
||||||
|
|
||||||
|
/** @type {import('vite/module-runner').ModuleRunnerTransport} */
|
||||||
|
const messagePortTransport = {
|
||||||
|
connect({ onMessage, onDisconnection }) {
|
||||||
|
pPort.on('message', onMessage)
|
||||||
|
pPort.on('close', onDisconnection)
|
||||||
|
},
|
||||||
|
send(data) {
|
||||||
|
pPort.postMessage(data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const runner = new ModuleRunner(
|
const runner = new ModuleRunner(
|
||||||
{
|
{
|
||||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
transport: new RemoteRunnerTransport({
|
transport: messagePortTransport,
|
||||||
onMessage: listener => {
|
|
||||||
parentPort?.on('message', listener)
|
|
||||||
},
|
|
||||||
send: message => {
|
|
||||||
parentPort?.postMessage(message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
new ESModulesEvaluator(),
|
new ESModulesEvaluator(),
|
||||||
)
|
)
|
||||||
@ -32,4 +39,4 @@ channel.onmessage = async (message) => {
|
|||||||
channel.postMessage({ error: e.stack })
|
channel.postMessage({ error: e.stack })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parentPort.postMessage('ready')
|
parentPort.postMessage('ready')
|
||||||
|
@ -1,8 +1,44 @@
|
|||||||
import { BroadcastChannel, Worker } from 'node:worker_threads'
|
import { BroadcastChannel, Worker } from 'node:worker_threads'
|
||||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||||
import { DevEnvironment, RemoteEnvironmentTransport } from '../../..'
|
import type { HotChannel, HotChannelListener, HotPayload } from 'vite'
|
||||||
|
import { DevEnvironment } from '../../..'
|
||||||
import { createServer } from '../../../server'
|
import { createServer } from '../../../server'
|
||||||
|
|
||||||
|
const createWorkerTransport = (w: Worker): HotChannel => {
|
||||||
|
const handlerToWorkerListener = new WeakMap<
|
||||||
|
HotChannelListener,
|
||||||
|
(value: HotPayload) => void
|
||||||
|
>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
send: (data) => w.postMessage(data),
|
||||||
|
on: (event: string, handler: HotChannelListener) => {
|
||||||
|
if (event === 'connection') return
|
||||||
|
|
||||||
|
const listener = (value: HotPayload) => {
|
||||||
|
if (value.type === 'custom' && value.event === event) {
|
||||||
|
const client = {
|
||||||
|
send(payload: HotPayload) {
|
||||||
|
w.postMessage(payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler(value.data, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlerToWorkerListener.set(handler, listener)
|
||||||
|
w.on('message', listener)
|
||||||
|
},
|
||||||
|
off: (event, handler: HotChannelListener) => {
|
||||||
|
if (event === 'connection') return
|
||||||
|
const listener = handlerToWorkerListener.get(handler)
|
||||||
|
if (listener) {
|
||||||
|
w.off('message', listener)
|
||||||
|
handlerToWorkerListener.delete(handler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('running module runner inside a worker', () => {
|
describe('running module runner inside a worker', () => {
|
||||||
it('correctly runs ssr code', async () => {
|
it('correctly runs ssr code', async () => {
|
||||||
expect.assertions(1)
|
expect.assertions(1)
|
||||||
@ -31,13 +67,8 @@ describe('running module runner inside a worker', () => {
|
|||||||
dev: {
|
dev: {
|
||||||
createEnvironment: (name, config) => {
|
createEnvironment: (name, config) => {
|
||||||
return new DevEnvironment(name, config, {
|
return new DevEnvironment(name, config, {
|
||||||
remoteRunner: {
|
|
||||||
transport: new RemoteEnvironmentTransport({
|
|
||||||
send: (data) => worker.postMessage(data),
|
|
||||||
onMessage: (handler) => worker.on('message', handler),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
hot: false,
|
hot: false,
|
||||||
|
transport: createWorkerTransport(worker),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
import type { CustomPayload, HotPayload } from 'types/hmrPayload'
|
|
||||||
import type { ModuleRunnerHMRConnection } from 'vite/module-runner'
|
|
||||||
import type { HotChannelClient, ServerHotChannel } from '../../server/hmr'
|
|
||||||
|
|
||||||
class ServerHMRBroadcasterClient implements HotChannelClient {
|
|
||||||
constructor(private readonly hotChannel: ServerHotChannel) {}
|
|
||||||
|
|
||||||
send(...args: any[]) {
|
|
||||||
let payload: HotPayload
|
|
||||||
if (typeof args[0] === 'string') {
|
|
||||||
payload = {
|
|
||||||
type: 'custom',
|
|
||||||
event: args[0],
|
|
||||||
data: args[1],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = args[0]
|
|
||||||
}
|
|
||||||
if (payload.type !== 'custom') {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot send non-custom events from the client to the server.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.hotChannel.send(payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The connector class to establish HMR communication between the server and the Vite runtime.
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
export class ServerHMRConnector implements ModuleRunnerHMRConnection {
|
|
||||||
private handlers: ((payload: HotPayload) => void)[] = []
|
|
||||||
private hmrClient: ServerHMRBroadcasterClient
|
|
||||||
|
|
||||||
private connected = false
|
|
||||||
|
|
||||||
constructor(private hotChannel: ServerHotChannel) {
|
|
||||||
this.hmrClient = new ServerHMRBroadcasterClient(hotChannel)
|
|
||||||
hotChannel.api.outsideEmitter.on('send', (payload: HotPayload) => {
|
|
||||||
this.handlers.forEach((listener) => listener(payload))
|
|
||||||
})
|
|
||||||
this.hotChannel = hotChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
isReady(): boolean {
|
|
||||||
return this.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
send(payload_: HotPayload): void {
|
|
||||||
const payload = payload_ as CustomPayload
|
|
||||||
this.hotChannel.api.innerEmitter.emit(
|
|
||||||
payload.event,
|
|
||||||
payload.data,
|
|
||||||
this.hmrClient,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(handler: (payload: HotPayload) => void): void {
|
|
||||||
this.handlers.push(handler)
|
|
||||||
handler({ type: 'connected' })
|
|
||||||
this.connected = true
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,13 +2,16 @@ import { existsSync, readFileSync } from 'node:fs'
|
|||||||
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
||||||
import type {
|
import type {
|
||||||
ModuleEvaluator,
|
ModuleEvaluator,
|
||||||
ModuleRunnerHMRConnection,
|
|
||||||
ModuleRunnerHmr,
|
ModuleRunnerHmr,
|
||||||
ModuleRunnerOptions,
|
ModuleRunnerOptions,
|
||||||
} from 'vite/module-runner'
|
} from 'vite/module-runner'
|
||||||
|
import type { HotPayload } from 'types/hmrPayload'
|
||||||
import type { DevEnvironment } from '../../server/environment'
|
import type { DevEnvironment } from '../../server/environment'
|
||||||
import type { ServerHotChannel } from '../../server/hmr'
|
import type {
|
||||||
import { ServerHMRConnector } from './serverHmrConnector'
|
HotChannelClient,
|
||||||
|
NormalizedServerHotChannel,
|
||||||
|
} from '../../server/hmr'
|
||||||
|
import type { ModuleRunnerTransport } from '../../../shared/moduleRunnerTransport'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
@ -24,7 +27,6 @@ export interface ServerModuleRunnerOptions
|
|||||||
hmr?:
|
hmr?:
|
||||||
| false
|
| false
|
||||||
| {
|
| {
|
||||||
connection?: ModuleRunnerHMRConnection
|
|
||||||
logger?: ModuleRunnerHmr['logger']
|
logger?: ModuleRunnerHmr['logger']
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -40,16 +42,8 @@ function createHMROptions(
|
|||||||
if (environment.config.server.hmr === false || options.hmr === false) {
|
if (environment.config.server.hmr === false || options.hmr === false) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (options.hmr?.connection) {
|
|
||||||
return {
|
|
||||||
connection: options.hmr.connection,
|
|
||||||
logger: options.hmr.logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!('api' in environment.hot)) return false
|
if (!('api' in environment.hot)) return false
|
||||||
const connection = new ServerHMRConnector(environment.hot as ServerHotChannel)
|
|
||||||
return {
|
return {
|
||||||
connection,
|
|
||||||
logger: options.hmr?.logger,
|
logger: options.hmr?.logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,6 +72,48 @@ function resolveSourceMapOptions(options: ServerModuleRunnerOptions) {
|
|||||||
return prepareStackTrace
|
return prepareStackTrace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createServerModuleRunnerTransport = (options: {
|
||||||
|
channel: NormalizedServerHotChannel
|
||||||
|
}): ModuleRunnerTransport => {
|
||||||
|
const hmrClient: HotChannelClient = {
|
||||||
|
send: (payload: HotPayload) => {
|
||||||
|
if (payload.type !== 'custom') {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot send non-custom events from the client to the server.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options.channel.send(payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler: ((data: HotPayload) => void) | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect({ onMessage }) {
|
||||||
|
options.channel.api!.outsideEmitter.on('send', onMessage)
|
||||||
|
onMessage({ type: 'connected' })
|
||||||
|
handler = onMessage
|
||||||
|
},
|
||||||
|
disconnect() {
|
||||||
|
if (handler) {
|
||||||
|
options.channel.api!.outsideEmitter.off('send', handler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send(payload) {
|
||||||
|
if (payload.type !== 'custom') {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot send non-custom events from the server to the client.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options.channel.api!.innerEmitter.emit(
|
||||||
|
payload.event,
|
||||||
|
payload.data,
|
||||||
|
hmrClient,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance of the Vite SSR runtime that support HMR.
|
* Create an instance of the Vite SSR runtime that support HMR.
|
||||||
* @experimental
|
* @experimental
|
||||||
@ -91,10 +127,9 @@ export function createServerModuleRunner(
|
|||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
root: environment.config.root,
|
root: environment.config.root,
|
||||||
transport: {
|
transport: createServerModuleRunnerTransport({
|
||||||
fetchModule: (id, importer, options) =>
|
channel: environment.hot as NormalizedServerHotChannel,
|
||||||
environment.fetchModule(id, importer, options),
|
}),
|
||||||
},
|
|
||||||
hmr,
|
hmr,
|
||||||
sourcemapInterceptor: resolveSourceMapOptions(options),
|
sourcemapInterceptor: resolveSourceMapOptions(options),
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,9 @@ import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
|
|||||||
import type { ViteDevServer } from '../server'
|
import type { ViteDevServer } from '../server'
|
||||||
import { unwrapId } from '../../shared/utils'
|
import { unwrapId } from '../../shared/utils'
|
||||||
import type { DevEnvironment } from '../server/environment'
|
import type { DevEnvironment } from '../server/environment'
|
||||||
|
import type { NormalizedServerHotChannel } from '../server/hmr'
|
||||||
import { ssrFixStacktrace } from './ssrStacktrace'
|
import { ssrFixStacktrace } from './ssrStacktrace'
|
||||||
|
import { createServerModuleRunnerTransport } from './runtime/serverModuleRunner'
|
||||||
|
|
||||||
type SSRModule = Record<string, any>
|
type SSRModule = Record<string, any>
|
||||||
|
|
||||||
@ -62,10 +64,9 @@ class SSRCompatModuleRunner extends ModuleRunner {
|
|||||||
super(
|
super(
|
||||||
{
|
{
|
||||||
root: environment.config.root,
|
root: environment.config.root,
|
||||||
transport: {
|
transport: createServerModuleRunnerTransport({
|
||||||
fetchModule: (id, importer, options) =>
|
channel: environment.hot as NormalizedServerHotChannel,
|
||||||
environment.fetchModule(id, importer, options),
|
}),
|
||||||
},
|
|
||||||
sourcemapInterceptor: false,
|
sourcemapInterceptor: false,
|
||||||
hmr: false,
|
hmr: false,
|
||||||
},
|
},
|
||||||
|
@ -1358,21 +1358,6 @@ export function isDevServer(
|
|||||||
return 'pluginContainer' in server
|
return 'pluginContainer' in server
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromiseWithResolvers<T> {
|
|
||||||
promise: Promise<T>
|
|
||||||
resolve: (value: T | PromiseLike<T>) => void
|
|
||||||
reject: (reason?: any) => void
|
|
||||||
}
|
|
||||||
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
|
|
||||||
let resolve: any
|
|
||||||
let reject: any
|
|
||||||
const promise = new Promise<T>((_resolve, _reject) => {
|
|
||||||
resolve = _resolve
|
|
||||||
reject = _reject
|
|
||||||
})
|
|
||||||
return { promise, resolve, reject }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSerialPromiseQueue<T>(): {
|
export function createSerialPromiseQueue<T>(): {
|
||||||
run(f: () => Promise<T>): Promise<T>
|
run(f: () => Promise<T>): Promise<T>
|
||||||
} {
|
} {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { HotPayload, Update } from 'types/hmrPayload'
|
import type { HotPayload, Update } from 'types/hmrPayload'
|
||||||
import type { ModuleNamespace, ViteHotContext } from 'types/hot'
|
import type { ModuleNamespace, ViteHotContext } from 'types/hot'
|
||||||
import type { InferCustomEventPayload } from 'types/customEvent'
|
import type { InferCustomEventPayload } from 'types/customEvent'
|
||||||
|
import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport'
|
||||||
|
|
||||||
type CustomListenersMap = Map<string, ((data: any) => void)[]>
|
type CustomListenersMap = Map<string, ((data: any) => void)[]>
|
||||||
|
|
||||||
@ -20,17 +21,6 @@ export interface HMRLogger {
|
|||||||
debug(...msg: unknown[]): void
|
debug(...msg: unknown[]): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HMRConnection {
|
|
||||||
/**
|
|
||||||
* Checked before sending messages to the client.
|
|
||||||
*/
|
|
||||||
isReady(): boolean
|
|
||||||
/**
|
|
||||||
* Send message to the client.
|
|
||||||
*/
|
|
||||||
send(messages: HotPayload): void
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HMRContext implements ViteHotContext {
|
export class HMRContext implements ViteHotContext {
|
||||||
private newListeners: CustomListenersMap
|
private newListeners: CustomListenersMap
|
||||||
|
|
||||||
@ -154,7 +144,7 @@ export class HMRContext implements ViteHotContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void {
|
send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void {
|
||||||
this.hmrClient.messenger.send({ type: 'custom', event, data })
|
this.hmrClient.send({ type: 'custom', event, data })
|
||||||
}
|
}
|
||||||
|
|
||||||
private acceptDeps(
|
private acceptDeps(
|
||||||
@ -173,24 +163,6 @@ export class HMRContext implements ViteHotContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HMRMessenger {
|
|
||||||
constructor(private connection: HMRConnection) {}
|
|
||||||
|
|
||||||
private queue: HotPayload[] = []
|
|
||||||
|
|
||||||
public send(payload: HotPayload): void {
|
|
||||||
this.queue.push(payload)
|
|
||||||
this.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
public flush(): void {
|
|
||||||
if (this.connection.isReady()) {
|
|
||||||
this.queue.forEach((msg) => this.connection.send(msg))
|
|
||||||
this.queue = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HMRClient {
|
export class HMRClient {
|
||||||
public hotModulesMap = new Map<string, HotModule>()
|
public hotModulesMap = new Map<string, HotModule>()
|
||||||
public disposeMap = new Map<string, (data: any) => void | Promise<void>>()
|
public disposeMap = new Map<string, (data: any) => void | Promise<void>>()
|
||||||
@ -199,16 +171,12 @@ export class HMRClient {
|
|||||||
public customListenersMap: CustomListenersMap = new Map()
|
public customListenersMap: CustomListenersMap = new Map()
|
||||||
public ctxToListenersMap = new Map<string, CustomListenersMap>()
|
public ctxToListenersMap = new Map<string, CustomListenersMap>()
|
||||||
|
|
||||||
public messenger: HMRMessenger
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public logger: HMRLogger,
|
public logger: HMRLogger,
|
||||||
connection: HMRConnection,
|
private transport: NormalizedModuleRunnerTransport,
|
||||||
// This allows implementing reloading via different methods depending on the environment
|
// This allows implementing reloading via different methods depending on the environment
|
||||||
private importUpdatedModule: (update: Update) => Promise<ModuleNamespace>,
|
private importUpdatedModule: (update: Update) => Promise<ModuleNamespace>,
|
||||||
) {
|
) {}
|
||||||
this.messenger = new HMRMessenger(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async notifyListeners<T extends string>(
|
public async notifyListeners<T extends string>(
|
||||||
event: T,
|
event: T,
|
||||||
@ -221,6 +189,10 @@ export class HMRClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public send(payload: HotPayload): void {
|
||||||
|
this.transport.send(payload)
|
||||||
|
}
|
||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.hotModulesMap.clear()
|
this.hotModulesMap.clear()
|
||||||
this.disposeMap.clear()
|
this.disposeMap.clear()
|
||||||
|
85
packages/vite/src/shared/invokeMethods.ts
Normal file
85
packages/vite/src/shared/invokeMethods.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
export interface FetchFunctionOptions {
|
||||||
|
cached?: boolean
|
||||||
|
startOffset?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FetchResult =
|
||||||
|
| CachedFetchResult
|
||||||
|
| ExternalFetchResult
|
||||||
|
| ViteFetchResult
|
||||||
|
|
||||||
|
export interface CachedFetchResult {
|
||||||
|
/**
|
||||||
|
* If module cached in the runner, we can just confirm
|
||||||
|
* it wasn't invalidated on the server side.
|
||||||
|
*/
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExternalFetchResult {
|
||||||
|
/**
|
||||||
|
* The path to the externalized module starting with file://,
|
||||||
|
* by default this will be imported via a dynamic "import"
|
||||||
|
* instead of being transformed by vite and loaded with vite runner
|
||||||
|
*/
|
||||||
|
externalize: string
|
||||||
|
/**
|
||||||
|
* Type of the module. Will be used to determine if import statement is correct.
|
||||||
|
* For example, if Vite needs to throw an error if variable is not actually exported
|
||||||
|
*/
|
||||||
|
type: 'module' | 'commonjs' | 'builtin' | 'network'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViteFetchResult {
|
||||||
|
/**
|
||||||
|
* Code that will be evaluated by vite runner
|
||||||
|
* by default this will be wrapped in an async function
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
/**
|
||||||
|
* File path of the module on disk.
|
||||||
|
* This will be resolved as import.meta.url/filename
|
||||||
|
* Will be equal to `null` for virtual modules
|
||||||
|
*/
|
||||||
|
file: string | null
|
||||||
|
/**
|
||||||
|
* Module ID in the server module graph.
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* Module URL used in the import.
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Invalidate module on the client side.
|
||||||
|
*/
|
||||||
|
invalidate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InvokeSendData<
|
||||||
|
T extends keyof InvokeMethods = keyof InvokeMethods,
|
||||||
|
> = {
|
||||||
|
name: T
|
||||||
|
/** 'send' is for requests without an id */
|
||||||
|
id: 'send' | `send:${string}`
|
||||||
|
data: Parameters<InvokeMethods[T]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InvokeResponseData<
|
||||||
|
T extends keyof InvokeMethods = keyof InvokeMethods,
|
||||||
|
> = {
|
||||||
|
name: T
|
||||||
|
/** 'response' is for responses without an id */
|
||||||
|
id: 'response' | `response:${string}`
|
||||||
|
data:
|
||||||
|
| { r: Awaited<ReturnType<InvokeMethods[T]>>; e?: undefined }
|
||||||
|
| { r?: undefined; e: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InvokeMethods = {
|
||||||
|
fetchModule: (
|
||||||
|
id: string,
|
||||||
|
importer?: string,
|
||||||
|
options?: FetchFunctionOptions,
|
||||||
|
) => Promise<FetchResult>
|
||||||
|
}
|
315
packages/vite/src/shared/moduleRunnerTransport.ts
Normal file
315
packages/vite/src/shared/moduleRunnerTransport.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import { nanoid } from 'nanoid/non-secure'
|
||||||
|
import type { CustomPayload, HotPayload } from 'types/hmrPayload'
|
||||||
|
import { promiseWithResolvers } from './utils'
|
||||||
|
import type {
|
||||||
|
InvokeMethods,
|
||||||
|
InvokeResponseData,
|
||||||
|
InvokeSendData,
|
||||||
|
} from './invokeMethods'
|
||||||
|
|
||||||
|
export type ModuleRunnerTransportHandlers = {
|
||||||
|
onMessage: (data: HotPayload) => void
|
||||||
|
onDisconnection: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "send and connect" or "invoke" must be implemented
|
||||||
|
*/
|
||||||
|
export interface ModuleRunnerTransport {
|
||||||
|
connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
|
||||||
|
disconnect?(): Promise<void> | void
|
||||||
|
send?(data: HotPayload): Promise<void> | void
|
||||||
|
invoke?(
|
||||||
|
data: HotPayload,
|
||||||
|
): Promise<{ /** result */ r: any } | { /** error */ e: any }>
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvokeableModuleRunnerTransport = Omit<ModuleRunnerTransport, 'invoke'> & {
|
||||||
|
invoke<T extends keyof InvokeMethods>(
|
||||||
|
name: T,
|
||||||
|
data: Parameters<InvokeMethods[T]>,
|
||||||
|
): Promise<ReturnType<Awaited<InvokeMethods[T]>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const createInvokeableTransport = (
|
||||||
|
transport: ModuleRunnerTransport,
|
||||||
|
): InvokeableModuleRunnerTransport => {
|
||||||
|
if (transport.invoke) {
|
||||||
|
return {
|
||||||
|
...transport,
|
||||||
|
async invoke(name, data) {
|
||||||
|
const result = await transport.invoke!({
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:invoke',
|
||||||
|
data: {
|
||||||
|
id: 'send',
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
} satisfies InvokeSendData,
|
||||||
|
} satisfies CustomPayload)
|
||||||
|
if ('e' in result) {
|
||||||
|
throw result.e
|
||||||
|
}
|
||||||
|
return result.r
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transport.send || !transport.connect) {
|
||||||
|
throw new Error(
|
||||||
|
'transport must implement send and connect when invoke is not implemented',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpcPromises = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
resolve: (data: any) => void
|
||||||
|
reject: (data: any) => void
|
||||||
|
name: string
|
||||||
|
timeoutId?: ReturnType<typeof setTimeout>
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...transport,
|
||||||
|
connect({ onMessage, onDisconnection }) {
|
||||||
|
return transport.connect!({
|
||||||
|
onMessage(payload) {
|
||||||
|
if (payload.type === 'custom' && payload.event === 'vite:invoke') {
|
||||||
|
const data = payload.data as InvokeResponseData
|
||||||
|
if (data.id.startsWith('response:')) {
|
||||||
|
const invokeId = data.id.slice('response:'.length)
|
||||||
|
const promise = rpcPromises.get(invokeId)
|
||||||
|
if (!promise) return
|
||||||
|
|
||||||
|
if (promise.timeoutId) clearTimeout(promise.timeoutId)
|
||||||
|
|
||||||
|
rpcPromises.delete(invokeId)
|
||||||
|
|
||||||
|
const { e, r } = data.data
|
||||||
|
if (e) {
|
||||||
|
promise.reject(e)
|
||||||
|
} else {
|
||||||
|
promise.resolve(r)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMessage(payload)
|
||||||
|
},
|
||||||
|
onDisconnection,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disconnect() {
|
||||||
|
rpcPromises.forEach((promise) => {
|
||||||
|
promise.reject(
|
||||||
|
new Error(
|
||||||
|
`transport was disconnected, cannot call ${JSON.stringify(promise.name)}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
rpcPromises.clear()
|
||||||
|
return transport.disconnect?.()
|
||||||
|
},
|
||||||
|
send(data) {
|
||||||
|
return transport.send!(data)
|
||||||
|
},
|
||||||
|
async invoke<T extends keyof InvokeMethods>(
|
||||||
|
name: T,
|
||||||
|
data: Parameters<InvokeMethods[T]>,
|
||||||
|
) {
|
||||||
|
const promiseId = nanoid()
|
||||||
|
const wrappedData: CustomPayload = {
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:invoke',
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
id: `send:${promiseId}`,
|
||||||
|
data,
|
||||||
|
} satisfies InvokeSendData,
|
||||||
|
}
|
||||||
|
const sendPromise = transport.send!(wrappedData)
|
||||||
|
|
||||||
|
const { promise, resolve, reject } =
|
||||||
|
promiseWithResolvers<ReturnType<Awaited<InvokeMethods[T]>>>()
|
||||||
|
const timeout = transport.timeout ?? 60000
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
|
if (timeout > 0) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
rpcPromises.delete(promiseId)
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`transport invoke timed out after ${timeout}ms (data: ${JSON.stringify(wrappedData)})`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}, timeout)
|
||||||
|
timeoutId?.unref?.()
|
||||||
|
}
|
||||||
|
rpcPromises.set(promiseId, { resolve, reject, name, timeoutId })
|
||||||
|
|
||||||
|
if (sendPromise) {
|
||||||
|
sendPromise.catch((err) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
rpcPromises.delete(promiseId)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await promise
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NormalizedModuleRunnerTransport {
|
||||||
|
connect?(onMessage?: (data: HotPayload) => void): Promise<void> | void
|
||||||
|
disconnect?(): Promise<void> | void
|
||||||
|
send(data: HotPayload): void
|
||||||
|
invoke<T extends keyof InvokeMethods>(
|
||||||
|
name: T,
|
||||||
|
data: Parameters<InvokeMethods[T]>,
|
||||||
|
): Promise<ReturnType<Awaited<InvokeMethods[T]>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeModuleRunnerTransport = (
|
||||||
|
transport: ModuleRunnerTransport,
|
||||||
|
): NormalizedModuleRunnerTransport => {
|
||||||
|
const invokeableTransport = createInvokeableTransport(transport)
|
||||||
|
|
||||||
|
let isConnected = !invokeableTransport.connect
|
||||||
|
let connectingPromise: Promise<void> | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(transport as Omit<ModuleRunnerTransport, 'connect'>),
|
||||||
|
...(invokeableTransport.connect
|
||||||
|
? {
|
||||||
|
async connect(onMessage) {
|
||||||
|
if (isConnected) return
|
||||||
|
if (connectingPromise) {
|
||||||
|
await connectingPromise
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybePromise = invokeableTransport.connect!({
|
||||||
|
onMessage: onMessage ?? (() => {}),
|
||||||
|
onDisconnection() {
|
||||||
|
isConnected = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (maybePromise) {
|
||||||
|
connectingPromise = maybePromise
|
||||||
|
await connectingPromise
|
||||||
|
connectingPromise = undefined
|
||||||
|
}
|
||||||
|
isConnected = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(invokeableTransport.disconnect
|
||||||
|
? {
|
||||||
|
async disconnect() {
|
||||||
|
if (!isConnected) return
|
||||||
|
if (connectingPromise) {
|
||||||
|
await connectingPromise
|
||||||
|
}
|
||||||
|
isConnected = false
|
||||||
|
await invokeableTransport.disconnect!()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
async send(data) {
|
||||||
|
if (!invokeableTransport.send) return
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
if (connectingPromise) {
|
||||||
|
await connectingPromise
|
||||||
|
} else {
|
||||||
|
throw new Error('send was called before connect')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await invokeableTransport.send(data)
|
||||||
|
},
|
||||||
|
async invoke(name, data) {
|
||||||
|
if (!isConnected) {
|
||||||
|
if (connectingPromise) {
|
||||||
|
await connectingPromise
|
||||||
|
} else {
|
||||||
|
throw new Error('invoke was called before connect')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return invokeableTransport.invoke(name, data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createWebSocketModuleRunnerTransport = (options: {
|
||||||
|
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
||||||
|
createConnection: () => WebSocket
|
||||||
|
pingInterval?: number
|
||||||
|
}): Required<
|
||||||
|
Pick<ModuleRunnerTransport, 'connect' | 'disconnect' | 'send'>
|
||||||
|
> => {
|
||||||
|
const pingInterval = options.pingInterval ?? 30000
|
||||||
|
|
||||||
|
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
||||||
|
let ws: WebSocket | undefined
|
||||||
|
let pingIntervalId: ReturnType<typeof setInterval> | undefined
|
||||||
|
return {
|
||||||
|
async connect({ onMessage, onDisconnection }) {
|
||||||
|
const socket = options.createConnection()
|
||||||
|
socket.addEventListener('message', async ({ data }) => {
|
||||||
|
onMessage(JSON.parse(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
let isOpened = socket.readyState === socket.OPEN
|
||||||
|
if (!isOpened) {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
socket.addEventListener(
|
||||||
|
'open',
|
||||||
|
() => {
|
||||||
|
isOpened = true
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
socket.addEventListener('close', async () => {
|
||||||
|
if (!isOpened) {
|
||||||
|
reject(new Error('WebSocket closed without opened.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage({
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:ws:disconnect',
|
||||||
|
data: { webSocket: socket },
|
||||||
|
})
|
||||||
|
onDisconnection()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage({
|
||||||
|
type: 'custom',
|
||||||
|
event: 'vite:ws:connect',
|
||||||
|
data: { webSocket: socket },
|
||||||
|
})
|
||||||
|
ws = socket
|
||||||
|
|
||||||
|
// proxy(nginx, docker) hmr ws maybe caused timeout,
|
||||||
|
// so send ping package let ws keep alive.
|
||||||
|
pingIntervalId = setInterval(() => {
|
||||||
|
if (socket.readyState === socket.OPEN) {
|
||||||
|
socket.send(JSON.stringify({ type: 'ping' }))
|
||||||
|
}
|
||||||
|
}, pingInterval)
|
||||||
|
},
|
||||||
|
disconnect() {
|
||||||
|
clearInterval(pingIntervalId)
|
||||||
|
ws?.close()
|
||||||
|
},
|
||||||
|
send(data) {
|
||||||
|
ws!.send(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -67,3 +67,18 @@ export function getAsyncFunctionDeclarationPaddingLineCount(): number {
|
|||||||
}
|
}
|
||||||
return asyncFunctionDeclarationPaddingLineCount
|
return asyncFunctionDeclarationPaddingLineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PromiseWithResolvers<T> {
|
||||||
|
promise: Promise<T>
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void
|
||||||
|
reject: (reason?: any) => void
|
||||||
|
}
|
||||||
|
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
|
||||||
|
let resolve: any
|
||||||
|
let reject: any
|
||||||
|
const promise = new Promise<T>((_resolve, _reject) => {
|
||||||
|
resolve = _resolve
|
||||||
|
reject = _reject
|
||||||
|
})
|
||||||
|
return { promise, resolve, reject }
|
||||||
|
}
|
||||||
|
5
packages/vite/types/hmrPayload.d.ts
vendored
5
packages/vite/types/hmrPayload.d.ts
vendored
@ -2,6 +2,7 @@
|
|||||||
export type HMRPayload = HotPayload
|
export type HMRPayload = HotPayload
|
||||||
export type HotPayload =
|
export type HotPayload =
|
||||||
| ConnectedPayload
|
| ConnectedPayload
|
||||||
|
| PingPayload
|
||||||
| UpdatePayload
|
| UpdatePayload
|
||||||
| FullReloadPayload
|
| FullReloadPayload
|
||||||
| CustomPayload
|
| CustomPayload
|
||||||
@ -12,6 +13,10 @@ export interface ConnectedPayload {
|
|||||||
type: 'connected'
|
type: 'connected'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PingPayload {
|
||||||
|
type: 'ping'
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdatePayload {
|
export interface UpdatePayload {
|
||||||
type: 'update'
|
type: 'update'
|
||||||
updates: Update[]
|
updates: Update[]
|
||||||
|
@ -11,11 +11,7 @@ import {
|
|||||||
vi,
|
vi,
|
||||||
} from 'vitest'
|
} from 'vitest'
|
||||||
import type { InlineConfig, RunnableDevEnvironment, ViteDevServer } from 'vite'
|
import type { InlineConfig, RunnableDevEnvironment, ViteDevServer } from 'vite'
|
||||||
import {
|
import { createRunnableDevEnvironment, createServer } from 'vite'
|
||||||
createRunnableDevEnvironment,
|
|
||||||
createServer,
|
|
||||||
createServerHotChannel,
|
|
||||||
} from 'vite'
|
|
||||||
import type { ModuleRunner } from 'vite/module-runner'
|
import type { ModuleRunner } from 'vite/module-runner'
|
||||||
import {
|
import {
|
||||||
addFile,
|
addFile,
|
||||||
@ -1085,7 +1081,6 @@ async function setupModuleRunner(
|
|||||||
createEnvironment(name, config) {
|
createEnvironment(name, config) {
|
||||||
return createRunnableDevEnvironment(name, config, {
|
return createRunnableDevEnvironment(name, config, {
|
||||||
runnerOptions: { hmr: { logger } },
|
runnerOptions: { hmr: { logger } },
|
||||||
hot: createServerHotChannel(),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -36,6 +36,15 @@ test(`deadlock doesn't happen for dynamic imports`, async () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.runIf(isServe)('html proxy is encoded', async () => {
|
||||||
|
await page.goto(
|
||||||
|
`${url}?%22%3E%3C/script%3E%3Cscript%3Econsole.log(%27html%20proxy%20is%20not%20encoded%27)%3C/script%3E`,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(browserLogs).not.toContain('html proxy is not encoded')
|
||||||
|
})
|
||||||
|
|
||||||
|
// run this at the end to reduce flakiness
|
||||||
test('should restart ssr', async () => {
|
test('should restart ssr', async () => {
|
||||||
editFile('./vite.config.ts', (content) => content)
|
editFile('./vite.config.ts', (content) => content)
|
||||||
await withRetry(async () => {
|
await withRetry(async () => {
|
||||||
@ -47,23 +56,3 @@ test('should restart ssr', async () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.runIf(isServe)('html proxy is encoded', async () => {
|
|
||||||
try {
|
|
||||||
await page.goto(
|
|
||||||
`${url}?%22%3E%3C/script%3E%3Cscript%3Econsole.log(%27html%20proxy%20is%20not%20encoded%27)%3C/script%3E`,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(browserLogs).not.toContain('html proxy is not encoded')
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore net::ERR_ABORTED, which is causing flakiness in this test
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
e.message.includes('net::ERR_ABORTED') ||
|
|
||||||
e.message.includes('interrupted')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
Loading…
Reference in New Issue
Block a user