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: add more robust remote transport to communicate between the runner and server #16449

6 changes: 4 additions & 2 deletions packages/vite/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ function createModuleRunnerConfig(isProduction: boolean) {
isProduction ? false : './dist/node',
),
esbuildMinifyPlugin({ minify: false, minifySyntax: true }),
bundleSizeLimit(47),
bundleSizeLimit(48),
],
})
}
Expand Down Expand Up @@ -335,7 +335,9 @@ const __require = require;
// don't patch runner utils chunk because it should stay lightweight and we know it doesn't use require
if (
chunk.name === 'utils' &&
chunk.moduleIds.some((id) => id.endsWith('/ssr/module-runner/utils.ts'))
chunk.moduleIds.some((id) =>
id.endsWith('/ssr/module-runner/utils.ts'.replace(/\//g, path.sep)),
)
)
return
const match = code.match(/^(?:import[\s\S]*?;\s*)+/)
Expand Down
21 changes: 15 additions & 6 deletions packages/vite/rollup.dts.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { findStaticImports } from 'mlly'
import { defineConfig } from 'rollup'
import type { Plugin, PluginContext, RenderedChunk } from 'rollup'
Expand Down Expand Up @@ -92,11 +93,17 @@ function patchTypes(): Plugin {
}
},
renderChunk(code, chunk) {
if (
chunk.fileName.startsWith('module-runner') ||
chunk.fileName.startsWith('types.d-')
) {
validateRunnerChunk.call(this, chunk)
const directory = (dir: string) => `${path.sep}${dir}${path.sep}`
const isNonMainChunk =
chunk.moduleIds.length &&
chunk.moduleIds.every(
(id) =>
id.includes(directory('module-runner')) ||
id.includes(directory('shared')),
)

if (isNonMainChunk) {
validateNonMainChunk.call(this, chunk)
} else {
validateChunkImports.call(this, chunk)
code = replaceConfusingTypeNames.call(this, code, chunk)
Expand All @@ -111,11 +118,12 @@ function patchTypes(): Plugin {
/**
* Runner chunk should only import local dependencies to stay lightweight
*/
function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) {
function validateNonMainChunk(this: PluginContext, chunk: RenderedChunk) {
for (const id of chunk.imports) {
if (
!id.startsWith('./') &&
!id.startsWith('../') &&
!id.startsWith('remoteTransport.d') &&
!id.startsWith('types.d')
) {
this.warn(`${chunk.fileName} imports "${id}" which is not allowed`)
Expand All @@ -134,6 +142,7 @@ function validateChunkImports(this: PluginContext, chunk: RenderedChunk) {
!id.startsWith('./') &&
!id.startsWith('../') &&
!id.startsWith('node:') &&
!id.startsWith('remoteTransport.d') &&
!id.startsWith('types.d') &&
!id.startsWith('vite/') &&
!deps.includes(id) &&
Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/module-runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
import { hmrLogger, silentConsole } from './hmrLogger'
import { createHMRHandler } from './hmrHandler'
import { enableSourceMapSupport } from './sourcemap/index'
import type { RunnerTransport } from './runnerTransport'
import { RemoteRunnerTransport, type RunnerTransport } from './runnerTransport'

interface ModuleRunnerDebugger {
(formatter: unknown, ...args: unknown[]): void
Expand Down Expand Up @@ -74,6 +74,9 @@ export class ModuleRunner {
) {
this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root)
this.transport = options.transport
if (options.transport instanceof RemoteRunnerTransport) {
options.transport.register(this)
}
if (typeof options.hmr === 'object') {
this.hmrClient = new HMRClient(
options.hmr.logger === false
Expand Down
105 changes: 36 additions & 69 deletions packages/vite/src/module-runner/runnerTransport.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,50 @@
import type {
TransportMethods,
TransportOptions,
} from '../shared/remoteTransport'
import { RemoteTransport } from '../shared/remoteTransport'
import type { FetchFunction, FetchResult } from './types'
import type { ModuleRunner } from './runner'

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)
interface RemoteRunnerTransportOptions<M extends TransportMethods = any>
extends Omit<TransportOptions<M>, 'methods'> {
methods?: M
}

if (data.e) {
promise.reject(data.e)
} else {
promise.resolve(data.r)
}
export class RemoteRunnerTransport<
M extends TransportMethods = {},
E extends TransportMethods = {},
>
extends RemoteTransport<M, E>
implements RunnerTransport
{
private _runner: ModuleRunner | undefined

constructor(options: RemoteRunnerTransportOptions<M>) {
super({
...options,
methods: {
...(options.methods as M),
evaluate: async (url: string) => {
if (!this._runner) {
throw new Error('[vite-transport] Runner is not registered')
}
await this._runner.import(url)
},
},
})
}

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 })
})
async fetchModule(id: string, importer?: string): Promise<FetchResult> {
// fetchModule is a special method that we don't expoe in types
return await (this.invoke as any)('fetchModule', id, importer)
}

fetchModule(id: string, importer?: string): Promise<FetchResult> {
return this.resolve<FetchResult>('fetchModule', id, importer)
register(runner: ModuleRunner): void {
this._runner = runner
}
}

// port from nanoid
// https://github.com/ai/nanoid
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
function nanoid(size = 21) {
let id = ''
let i = size
while (i--) id += urlAlphabet[(Math.random() * 64) | 0]
return id
}
2 changes: 1 addition & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export { transformWithEsbuild } from './plugins/esbuild'
export { buildErrorMessage } from './server/middlewares/error'

export { RemoteEnvironmentTransport } from './server/environmentTransport'
export { createNodeDevEnvironment } from './server/environments/nodeEnvironment'
export { DevEnvironment, type DevEnvironmentSetup } from './server/environment'
export { BuildEnvironment } from './build'
export { createNodeDevEnvironment } from './server/environments/nodeEnvironment'

export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule'
export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner'
Expand Down
31 changes: 18 additions & 13 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ import type { TransformResult } from './transformRequest'
import { ERR_CLOSED_SERVER } from './pluginContainer'
import type { RemoteEnvironmentTransport } from './environmentTransport'

export interface DevEnvironmentRunnerOptions extends FetchModuleOptions {
transport?: RemoteEnvironmentTransport
}

export interface DevEnvironmentSetup {
hot?: false | HMRChannel
options?: EnvironmentOptions
runner?: FetchModuleOptions & {
transport?: RemoteEnvironmentTransport
}
runner?: DevEnvironmentRunnerOptions
depsOptimizer?: DepsOptimizer
}

// Maybe we will rename this to DevEnvironment
Expand All @@ -38,7 +41,7 @@ export class DevEnvironment extends Environment {
/**
* @internal
*/
_ssrRunnerOptions: FetchModuleOptions | undefined
_ssrRunnerOptions: DevEnvironmentRunnerOptions | undefined
/**
* HMR channel for this environment. If not provided or disabled,
* it will be a noop channel that does nothing.
Expand All @@ -50,14 +53,7 @@ export class DevEnvironment extends Environment {
constructor(
server: ViteDevServer,
name: string,
setup?: {
hot?: false | HMRChannel
options?: EnvironmentOptions
runner?: FetchModuleOptions & {
transport?: RemoteEnvironmentTransport
}
depsOptimizer?: DepsOptimizer
},
setup?: DevEnvironmentSetup,
) {
let options =
server.config.environments[name] ??
Expand All @@ -80,7 +76,7 @@ export class DevEnvironment extends Environment {

const ssrRunnerOptions = setup?.runner || {}
this._ssrRunnerOptions = ssrRunnerOptions
setup?.runner?.transport?.register(this)
ssrRunnerOptions.transport?.register(this)

this.hot.on('vite:invalidate', async ({ path, message }) => {
invalidateModule(this, {
Expand Down Expand Up @@ -137,6 +133,15 @@ export class DevEnvironment extends Environment {
})
}

async evaluate(url: string): Promise<void> {
if (!this._ssrRunnerOptions?.transport) {
throw new Error(
`Cannot evaluate module ${url} in ${this.name} environment without a configured remote transport.`,
)
}
await this._ssrRunnerOptions.transport.evaluate(url)
}

async close(): Promise<void> {
await this.depsOptimizer?.close()
}
Expand Down
66 changes: 35 additions & 31 deletions packages/vite/src/node/server/environmentTransport.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import type {
TransportMethods,
TransportOptions,
} from '../../shared/remoteTransport'
import { RemoteTransport } from '../../shared/remoteTransport'
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
interface RemoteEnvironmentTransportOptions<M extends TransportMethods = any>
extends Omit<TransportOptions<M>, 'methods'> {
methods?: M
}

const method = data.m as 'fetchModule'
const parameters = data.a as [string, string]
export class RemoteEnvironmentTransport<
M extends TransportMethods = {},
E extends TransportMethods = {},
> extends RemoteTransport<M, E> {
private _environment: DevEnvironment | undefined

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,
})
}
constructor(options: RemoteEnvironmentTransportOptions<M>) {
super({
...options,
methods: {
...(options.methods as M),
fetchModule: (url: string) => {
if (!this._environment) {
throw new Error('[vite-transport] Environment is not registered')
}
return this._environment.fetchModule(url)
},
},
})
}

async evaluate(url: string): Promise<void> {
// evaluate is a special method that we don't expoe in types
await (this.invoke as any)('evaluate', url)
}

register(environment: DevEnvironment): void {
this._environment = environment
}
}
14 changes: 13 additions & 1 deletion packages/vite/src/node/server/environments/nodeEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ModuleRunner } from 'vite/module-runner'
import type { DevEnvironmentSetup } from '../environment'
import { DevEnvironment } from '../environment'
import type { ViteDevServer } from '../index'
import { asyncFunctionDeclarationPaddingLineCount } from '../../../shared/utils'
import { createServerModuleRunner } from '../../ssr/runtime/serverModuleRunner'

export function createNodeDevEnvironment(
server: ViteDevServer,
name: string,
options?: DevEnvironmentSetup,
): DevEnvironment {
return new DevEnvironment(server, name, {
return new NodeDevEnvironment(server, name, {
...options,
runner: {
processSourceMap(map) {
Expand All @@ -22,3 +24,13 @@ export function createNodeDevEnvironment(
},
})
}

class NodeDevEnvironment extends DevEnvironment {
private runner: ModuleRunner | undefined

override async evaluate(url: string): Promise<void> {
if (this._ssrRunnerOptions?.transport) return super.evaluate(url)
const runner = this.runner || (this.runner = createServerModuleRunner(this))
await runner.import(url)
}
}
Loading
Loading