Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use a single transport for fetchModule and HMR support #18362

Merged
merged 51 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
94963e0
feat: use a single transport for fetchModule and HMR support
sapphi-red Oct 12, 2024
9e9f36d
chore: fix typecheck
sapphi-red Oct 16, 2024
5c2c060
test: run restart test later to reduce flaky fails
sapphi-red Oct 16, 2024
44b0179
chore: merge main
sapphi-red Oct 17, 2024
5e01ae2
refactor: normalize internally
sapphi-red Oct 17, 2024
69b58b5
refactor: replace invokeId internally
sapphi-red Oct 17, 2024
30a7109
chore: change exported types / functions
sapphi-red Oct 17, 2024
00e2098
chore: bump bundle limit
sapphi-red Oct 17, 2024
313fac8
refactor: move onDisconnect handler in connect method parameter
sapphi-red Oct 18, 2024
affa13e
chore: merge main
sapphi-red Oct 18, 2024
d37b86e
refactor: minor change in runnerTransport.ts
sapphi-red Oct 18, 2024
3e5807a
fix: reject on disconnection
sapphi-red Oct 18, 2024
eaafc17
refactor: rename RunnerTransport to ModuleRunnerTransport
sapphi-red Oct 18, 2024
b39895f
fix: make transport optional
sapphi-red Oct 18, 2024
6dbf384
refactor: minor change in runnableEnvironment.ts
sapphi-red Oct 18, 2024
38b50bf
fix: WS compat
sapphi-red Oct 18, 2024
4543a67
feat: send ping as PingPayload
sapphi-red Oct 18, 2024
daceec6
feat: make createWebSocketModuleRunnerTransport more generic
sapphi-red Oct 18, 2024
473b465
refactor: remove not used types
sapphi-red Oct 18, 2024
c481c19
test: update some typings in server-worker-runner.spec.ts
sapphi-red Oct 18, 2024
0b69b63
docs: update content
sapphi-red Oct 18, 2024
54944e4
docs: update title to ModuleRunnerTransport
sapphi-red Oct 20, 2024
f08a2df
refactor: make ModuleRunnerOptions.hmr `boolean | ModuleRunnerHmr` an…
sapphi-red Oct 22, 2024
878849c
test: transport is not required
sapphi-red Oct 22, 2024
f892a8d
fix: error if vite:fetchModule was sent instead of invoked
sapphi-red Oct 22, 2024
fddfd64
fix: call connect anyway
sapphi-red Oct 22, 2024
7536dfb
feat: add `environment.getInvokeHandlers`
sapphi-red Oct 22, 2024
05413c6
refactor: improve types
sapphi-red Oct 22, 2024
f068cbc
docs: update
sapphi-red Oct 24, 2024
7d8ee2f
docs: don't call invokeHandlers directly
sapphi-red Oct 24, 2024
3897427
feat: print name when disconnected
sapphi-red Oct 24, 2024
1668bd8
docs: use /invoke
sapphi-red Oct 25, 2024
f98a467
refactor: change `invoke` type
sapphi-red Oct 25, 2024
d499c9f
docs: change it to HotPayload for now
sapphi-red Oct 25, 2024
34ac5f7
refactor: improve invoke types
sapphi-red Oct 25, 2024
0d2b390
chore: merge main
sapphi-red Oct 25, 2024
d98fd84
fix: client ping target
sapphi-red Oct 25, 2024
aaddea2
wip: use HotChannel even for HTTP requests
sapphi-red Oct 25, 2024
4b349f7
fix: handle floating promises
sapphi-red Oct 28, 2024
665ee30
feat: call custom event listeners when send is called on
sapphi-red Oct 28, 2024
140de68
docs: remove `null` from environment::hot
sapphi-red Oct 28, 2024
33ccd64
refactor: remove `ctx.hot`
sapphi-red Oct 28, 2024
d55aec8
refactor: remove `send` constructed from `invoke`
sapphi-red Oct 28, 2024
27a6e0c
docs: remove unneeded argument
sapphi-red Oct 28, 2024
7f8534c
docs: rename hot channel name for the example using `setInvokeHandler`
sapphi-red Oct 28, 2024
0d92ce1
fix: add back `context.hot` to disable hmr only in single env
sapphi-red Oct 28, 2024
e40e17a
chore: merge main
sapphi-red Oct 28, 2024
f060ccb
chore: merge main
sapphi-red Nov 1, 2024
83d4a24
chore: merge main
sapphi-red Nov 5, 2024
7abf3ac
feat: replace `HotChannel::setInvokeHandler` with `NormalizedHotChann…
sapphi-red Nov 5, 2024
65a23d2
docs: dont extend DevEnvironment
sapphi-red Nov 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/api-environment-instances.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class DevEnvironment {
* Communication channel to send and receive messages from the
* associated module runner in the target runtime.
*/
hot: HotChannel | null
hot: NormalizedHotChannel
/**
* Graph of module nodes, with the imported relationship between
* processed modules and the cached result of the processed code.
Expand Down
167 changes: 89 additions & 78 deletions docs/guide/api-environment-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ function createWorkedEnvironment(
dev: {
createEnvironment(name, config) {
return createWorkerdDevEnvironment(name, config, {
hot: customHotChannel(),
hot: true,
transport: customHotChannel(),
})
},
},
Expand Down Expand Up @@ -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.

```ts
import { DevEnvironment, RemoteEnvironmentTransport } from 'vite'
import { DevEnvironment, HotChannel } from 'vite'

function createWorkerdDevEnvironment(
name: string,
config: ResolvedConfig,
context: DevEnvironmentContext
) {
const hot = /* ... */
const connection = /* ... */
const transport = new RemoteEnvironmentTransport({
const transport: HotChannel = {
on: (listener) => { connection.on('message', listener) },
send: (data) => connection.send(data),
onMessage: (listener) => connection.on('message', listener),
})
}

const workerdDevEnvironment = new DevEnvironment(name, config, {
options: {
resolve: { conditions: ['custom'] },
...context.options,
},
hot,
remoteRunner: {
transport,
},
hot: true,
transport,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be channel or hotChannel? transport is also used on the module runner side (ModuleRunnerTransport).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, transport: HotChannel typing feels confusing. I liked the symmetry of DevEnvironmentContext.transport and ModuleRunnerOptions.transport though, so if we change this on environment side, then we might want to align on runner side.

Copy link
Collaborator

@hi-ogawa hi-ogawa Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think naming stuff is a blocker and we can follow up later if needed.

})
return workerdDevEnvironment
}
Expand Down Expand Up @@ -152,13 +150,12 @@ Module runner exposes `import` method. When Vite server triggers `full-reload` H

```js
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(
{
root,
fetchModule,
// you can also provide hmr.connection to support HMR
transport,
},
new ESModulesEvaluator(),
)
Expand All @@ -177,7 +174,7 @@ export interface ModuleRunnerOptions {
/**
* 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.
Expand All @@ -197,10 +194,6 @@ export interface ModuleRunnerOptions {
hmr?:
| false
| {
/**
* Configure how HMR communicates between client and server.
*/
connection: ModuleRunnerHMRConnection
/**
* Configure HMR logger.
*/
Expand Down Expand Up @@ -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.

## RunnerTransport
## `ModuleRunnerTransport`

**Type Signature:**

```ts
interface RunnerTransport {
/**
* A method to get the information about the module.
*/
fetchModule: FetchFunction
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
}
```

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

```ts [worker.js]
```js [worker.js]
import { parentPort } from 'node:worker_threads'
import { fileURLToPath } from 'node:url'
import {
ESModulesEvaluator,
ModuleRunner,
RemoteRunnerTransport,
} from 'vite/module-runner'
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'

/** @type {import('vite/module-runner').ModuleRunnerTransport} */
const transport = {
connect({ onMessage, onDisconnection }) {
parentPort.on('message', onMessage)
parentPort.on('close', onDisconnection)
},
send(data) {
parentPort.postMessage(data)
},
}

const runner = new ModuleRunner(
{
root: fileURLToPath(new URL('./', import.meta.url)),
transport: new RemoteRunnerTransport({
send: (data) => parentPort.postMessage(data),
onMessage: (listener) => parentPort.on('message', listener),
timeout: 5000,
}),
transport,
},
new ESModulesEvaluator(),
)
```

```ts [server.js]
```js [server.js]
import { BroadcastChannel } from 'node:worker_threads'
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'

function createWorkerEnvironment(name, config, context) {
const worker = new Worker('./worker.js')
return new DevEnvironment(name, config, {
hot: /* custom hot channel */,
remoteRunner: {
transport: new RemoteEnvironmentTransport({
send: (data) => worker.postMessage(data),
onMessage: (listener) => worker.on('message', listener),
}),
const handlerToWorkerListener = new WeakMap()

const workerHotChannel = {
send: (data) => w.postMessage(data),
on: (event, handler) => {
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,
})
}

Expand All @@ -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
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
Expand All @@ -323,10 +348,11 @@ export const runner = new ModuleRunner(
{
root: fileURLToPath(new URL('./', import.meta.url)),
transport: {
async fetchModule(id, importer) {
const response = await fetch(
`http://my-vite-server/fetch?id=${id}&importer=${importer}`,
)
async invoke(data) {
const response = await fetch(`http://my-vite-server/invoke`, {
method: 'POST',
body: JSON.stringify(data),
})
return response.json()
},
},
Expand All @@ -337,37 +363,22 @@ export const runner = new ModuleRunner(
await runner.import('/entry.js')
```

## ModuleRunnerHMRConnection

**Type Signature:**
In this case, the `handleInvoke` method in the `NormalizedHotChannel` can be used:

```ts
export interface ModuleRunnerHMRConnection {
/**
* Checked before sending messages to the server.
*/
isReady(): boolean
/**
* Send a message to the server.
*/
send(payload: HotPayload): void
/**
* 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
}
const customEnvironment = new DevEnvironment(name, config, context)

server.onRequest((request: Request) => {
const url = new URL(request.url)
if (url.pathname === '/invoke') {
const payload = (await request.json()) as HotPayload
const result = customEnvironment.hot.handleInvoke(payload)
return new Response(JSON.stringify(result))
}
return Response.error()
})
```

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")`).

`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:

```js
function onUpdate(callback) {
this.connection.on('hmr', (event) => callback(event.data))
}
```
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")`).

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.
Vite exports `createServerHotChannel` from the main entry point to support HMR during Vite SSR.
3 changes: 2 additions & 1 deletion packages/vite/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const clientConfig = defineConfig({
input: path.resolve(__dirname, 'src/client/client.ts'),
external: ['@vite/env'],
plugins: [
nodeResolve({ preferBuiltins: true }),
esbuild({
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
}),
Expand Down Expand Up @@ -186,7 +187,7 @@ const moduleRunnerConfig = defineConfig({
],
plugins: [
...createSharedNodePlugins({ esbuildOptions: { minifySyntax: true } }),
bundleSizeLimit(50),
bundleSizeLimit(53),
],
})

Expand Down
Loading