コンテンツにスキップ

ランタイム用環境 API

実験的

環境 API は実験的です。エコシステムがこれを試して基盤を構築できるように、Vite 6 では API を安定させます。Vite 7 では、破壊的変更の可能性があるこれらの新しい API を安定させる予定です。

リソース

フィードバックをお寄せください。

環境ファクトリー

環境ファクトリーは、エンドユーザーではなく、Cloudflare などの環境プロバイダーによって実装されることを意図しています。環境ファクトリーは、開発環境とビルド環境の両方でターゲットランタイムを使用する最も一般的なケースの `EnvironmentOptions` を返します。ユーザーが設定する必要がないように、デフォルトの環境オプションを設定することもできます。

ts
function createWorkerdEnvironment(
  userConfig: EnvironmentOptions,
): EnvironmentOptions {
  return mergeConfig(
    {
      resolve: {
        conditions: [
          /*...*/
        ],
      },
      dev: {
        createEnvironment(name, config) {
          return createWorkerdDevEnvironment(name, config, {
            hot: true,
            transport: customHotChannel(),
          })
        },
      },
      build: {
        createEnvironment(name, config) {
          return createWorkerdBuildEnvironment(name, config)
        },
      },
    },
    userConfig,
  )
}

設定ファイルは次のように記述できます

js
import { createWorkerdEnvironment } from 'vite-environment-workerd'

export default {
  environments: {
    ssr: createWorkerdEnvironment({
      build: {
        outDir: '/dist/ssr',
      },
    }),
    rsc: createWorkerdEnvironment({
      build: {
        outDir: '/dist/rsc',
      },
    }),
  },
}

フレームワークは、workerd ランタイムを使用した環境を使用して、次のように SSR を実行できます

js
const ssrEnvironment = server.environments.ssr

新しい環境ファクトリーの作成

Vite 開発サーバーは、デフォルトで 2 つの環境を公開します。`client` 環境と `ssr` 環境です。クライアント環境はデフォルトでブラウザ環境であり、モジュールランナーは仮想モジュール `/@vite/client` をクライアントアプリにインポートすることで実装されます。SSR 環境は、デフォルトで Vite サーバーと同じ Node ランタイムで実行され、アプリケーションサーバーが開発中に完全な HMR サポートを使用してリクエストをレンダリングするために使用できます。

変換されたソースコードはモジュールと呼ばれ、各環境で処理されるモジュール間の関係はモジュールグラフに保持されます。これらのモジュールの変換されたコードは、実行される各環境に関連付けられたランタイムに送信されます。モジュールがランタイムで評価されると、インポートされたモジュールがリクエストされ、モジュールグラフの一部が処理されます。

Vite モジュールランナーを使用すると、最初に Vite プラグインで処理することで任意のコードを実行できます。ランナーの実装がサーバーから分離されているため、`server.ssrLoadModule` とは異なります。これにより、ライブラリとフレームワークの作成者は、Vite サーバーとランナー間の通信レイヤーを実装できます。ブラウザは、サーバー Web Socket と HTTP リクエストを使用して、対応する環境と通信します。Node モジュールランナーは、同じプロセスで実行されているため、モジュールを処理するための関数呼び出しを直接実行できます。他の環境では、workerd などの JS ランタイム、または Vitest が行うようにワーカースレッドに接続してモジュールを実行できます。

この機能の目標の 1 つは、コードを処理および実行するためのカスタマイズ可能な API を提供することです。ユーザーは、公開されているプリミティブを使用して新しい環境ファクトリーを作成できます。

ts
import { DevEnvironment, HotChannel } from 'vite'

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

  const workerdDevEnvironment = new DevEnvironment(name, config, {
    options: {
      resolve: { conditions: ['custom'] },
      ...context.options,
    },
    hot: true,
    transport,
  })
  return workerdDevEnvironment
}

`ModuleRunner`

モジュールランナーは、ターゲットランタイムでインスタンス化されます。特に明記されていない限り、次のセクションのすべての API は `vite/module-runner` からインポートされます。このエクスポートエントリポイントは、モジュールランナーの作成に必要な最小限のものだけをエクスポートするように、できるだけ軽量に保たれています。

型シグネチャ

ts
export class ModuleRunner {
  constructor(
    public options: ModuleRunnerOptions,
    public evaluator: ModuleEvaluator = new ESModulesEvaluator(),
    private debug?: ModuleRunnerDebugger,
  ) {}
  /**
   * URL to execute.
   * Accepts file path, server path, or id relative to the root.
   */
  public async import<T = any>(url: string): Promise<T>
  /**
   * Clear all caches including HMR listeners.
   */
  public clearCache(): void
  /**
   * Clear all caches, remove all HMR listeners, reset sourcemap support.
   * This method doesn't stop the HMR connection.
   */
  public async close(): Promise<void>
  /**
   * Returns `true` if the runner has been closed by calling `close()`.
   */
  public isClosed(): boolean
}

`ModuleRunner` のモジュールエバリュエーターは、コードの実行を担当します。Vite は `ESModulesEvaluator` をそのままエクスポートし、`new AsyncFunction` を使用してコードを評価します。JavaScript ランタイムが安全でない評価をサポートしていない場合は、独自の実装を提供できます。

モジュールランナーは `import` メソッドを公開します。Viteサーバーが `full-reload` HMRイベントをトリガーすると、影響を受けるすべてのモジュールが再実行されます。モジュールランナーは、この発生時に `exports` オブジェクトを更新しないことに注意してください(上書きします)。最新の `exports` オブジェクトに依存している場合は、`import` を実行するか、`evaluatedModules` からモジュールを再度取得する必要があります。

使用例

js
import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner'
import { root, transport } from './rpc-implementation.js'

const moduleRunner = new ModuleRunner(
  {
    root,
    transport,
  },
  new ESModulesEvaluator(),
)

await moduleRunner.import('/src/entry-point.js')

`ModuleRunnerOptions`

ts
interface ModuleRunnerOptions {
  /**
   * Root of the project
   */
  
root
: string
/** * A set of methods to communicate with the server. */
transport
:
ModuleRunnerTransport
/** * 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. * You can provide an object to configure how file contents and * source maps are resolved for files that were not processed by Vite. */
sourcemapInterceptor
?:
| false | 'node' | 'prepareStackTrace' |
InterceptorOptions
/** * Disable HMR or configure HMR options. * * @default true */
hmr
?: boolean |
ModuleRunnerHmr
/** * Custom module cache. If not provided, it creates a separate module * cache for each module runner instance. */
evaluatedModules
?:
EvaluatedModules
}

`ModuleEvaluator`

型シグネチャ

ts
export interface ModuleEvaluator {
  /**
   * Number of prefixed lines in the transformed code.
   */
  
startOffset
?: number
/** * Evaluate code that was transformed by Vite. * @param context Function context * @param code Transformed code * @param id ID that was used to fetch the module */
runInlinedModule
(
context
:
ModuleRunnerContext
,
code
: string,
id
: string,
):
Promise
<any>
/** * evaluate externalized module. * @param file File URL to the external module */
runExternalModule
(
file
: string):
Promise
<any>
}

Vite は、デフォルトでこのインターフェースを実装する `ESModulesEvaluator` をエクスポートします。`new AsyncFunction` を使用してコードを評価するため、コードにインラインソースマップが含まれている場合は、追加された新しい行に対応するために 2 行のオフセット が含まれている必要があります。これは `ESModulesEvaluator` によって自動的に行われます。カスタムエバリュエーターは追加の行を追加しません。

`ModuleRunnerTransport`

型シグネチャ

ts
interface ModuleRunnerTransport {
  
connect
?(
handlers
:
ModuleRunnerTransportHandlers
):
Promise
<void> | void
disconnect
?():
Promise
<void> | void
send
?(
data
:
HotPayload
):
Promise
<void> | void
invoke
?(
data
:
HotPayload
):
Promise
<{
result
: any } | {
error
: any }>
timeout
?: number
}

RPC または関数を直接呼び出すことによって環境と通信するトランスポートオブジェクト。`invoke` メソッドが実装されていない場合、`send` メソッドと `connect` メソッドを実装する必要があります。Vite は `invoke` を内部的に構築します。

モジュールランナーがワーカースレッドで作成されるこの例のように、サーバー上の `HotChannel` インスタンスと組み合わせる必要があります

js
import { parentPort } from 'node:worker_threads'
import { fileURLToPath } from 'node:url'
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 ESModulesEvaluator(),
)
js
import { BroadcastChannel } from 'node:worker_threads'
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'

function createWorkerEnvironment(name, config, context) {
  const worker = new Worker('./worker.js')
  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,
  })
}

await createServer({
  environments: {
    worker: {
      dev: {
        createEnvironment: createWorkerEnvironment,
      },
    },
  },
})

ランナーとサーバー間の通信に HTTP リクエストを使用する別の例

ts
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'

export const runner = new ModuleRunner(
  {
    root: fileURLToPath(new URL('./', import.meta.url)),
    transport: {
      async invoke(data) {
        const response = await fetch(`http://my-vite-server/invoke`, {
          method: 'POST',
          body: JSON.stringify(data),
        })
        return response.json()
      },
    },
    hmr: false, // disable HMR as HMR requires transport.connect
  },
  new ESModulesEvaluator(),
)

await runner.import('/entry.js')

この場合、`NormalizedHotChannel` の `handleInvoke` メソッドを使用できます

ts
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()
})

ただし、HMR をサポートするには、`send` メソッドと `connect` メソッドが必要です。`send` メソッドは通常、カスタムイベントがトリガーされたときに呼び出されます(例: `import.meta.hot.send("my-event")`)。

Vite は、Vite SSR 中の HMR をサポートするために、メインエントリポイントから `createServerHotChannel` をエクスポートします。

MIT ライセンスでリリースされています。(ccee3d7c)