diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 6d43ed89..38ddf6e0 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -322,12 +322,33 @@ export default defineConfig({ link: '/guide/api-javascript', }, { - text: 'Environment API', + text: '設定リファレンス', + link: '/config/', + }, + ], + }, + { + text: 'Environment API', + items: [ + { + text: 'はじめに', link: '/guide/api-environment', }, { - text: '設定リファレンス', - link: '/config/', + text: '環境インスタンス', + link: '/guide/api-environment-instances', + }, + { + text: 'プラグイン', + link: '/guide/api-environment-plugins', + }, + { + text: 'フレームワーク', + link: '/guide/api-environment-frameworks', + }, + { + text: 'ランタイム', + link: '/guide/api-environment-runtimes', }, ], }, diff --git a/guide/api-environment-frameworks.md b/guide/api-environment-frameworks.md new file mode 100644 index 00000000..62f69a5b --- /dev/null +++ b/guide/api-environment-frameworks.md @@ -0,0 +1,286 @@ +# フレームワーク向けの Environment API + +:::warning 実験的機能 +この API の初期研究は、Vite 5.1 で「Vite ランタイム API」という名前で導入されました。このガイドでは、Environment API と改名された改訂版 API について説明します。この API は Vite 6 で実験的機能としてリリースされる予定です。すでに最新の `vite@6.0.0-beta.x` バージョンでテストできます。 + +リソース: + +- 新しい API に関するフィードバックを収集する [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) +- 新しい API が実装され、レビューされる [Environment API PR](https://github.com/vitejs/vite/pull/16471) + +この提案をテストする際には、ぜひフィードバックをお寄せください。 +::: + +## 環境とフレームワーク {#environments-and-frameworks} + +暗黙的な `ssr` 環境とその他の非クライアント環境では、開発中にデフォルトで `RunnableDevEnvironment` が使用されます。これには、Vite サーバーが実行しているのと同じランタイムが必要ですが、`ssrLoadModule` と同様に動作し、フレームワークが SSR 開発ストーリーの HMR を移行して有効にできるようにします。`isRunnableDevEnvironment` 関数を使用して、実行可能な環境をすべて保護できます。 + +```ts +export class RunnableDevEnvironment extends DevEnvironment { + public readonly runner: ModuleRunner +} + +class ModuleRunner { + /** + * 実行するURL。ルートからの相対的なファイルパス、サーバーパス、ID を受け付けます。 + * インスタンス化されたモジュールを返します (ssrLoadModule と同じ) + */ + public async import(url: string): Promise> + /** + * その他の ModuleRunner メソッド... + */ +} + +if (isRunnableDevEnvironment(server.environments.ssr)) { + await server.environments.ssr.runner.import('/entry-point.js') +} +``` + +:::warning +`runner` は、初めてアクセスされたときに即座に評価されます。Vite は、`process.setSourceMapsEnabled` を呼び出して `runner` が作成されたとき、またはそれが利用できない場合は `Error.prepareStackTrace` をオーバーライドすることによって、ソースマップのサポートを有効にすることに注意してください。 +::: + +## Default `RunnableDevEnvironment` + +[SSR セットアップガイド](/guide/ssr#setting-up-the-dev-server)で説明されているように、ミドルウェアモードに設定された Vite サーバーがあるとして、Environment API を使って SSR ミドルウェアを実装してみましょう。エラー処理は省略します。 + +```js +import { createServer } from 'vite' + +const server = await createServer({ + server: { middlewareMode: true }, + appType: 'custom', + environments: { + server: { + // デフォルトでは、開発中はモジュールは vite 開発サーバーと同じプロセスで実行されます + }, + }, +}) + +// TypeScript では、これを RunnableDevEnvironment にキャストするか、ランナーへのアクセスを +// 保護するために "isRunnableDevEnvironment" 関数を使用する必要があるかもしれません +const environment = server.environments.node + +app.use('*', async (req, res, next) => { + const url = req.originalUrl + + // 1. index.html を読み込む + let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') + + // 2. Vite HTML 変換を適用します。これにより、Vite HMR クライアントが挿入され、 + // Vite プラグインからの HTML 変換も適用されます。 + // 例: global preambles from @vitejs/plugin-react + template = await server.transformIndexHtml(url, template) + + // 3. サーバーエントリをロードします。import(url) は、 + // ESM ソースコードを Node.js で使用できるように自動的に変換します。 + // バンドルは不要で、完全な HMR サポートを提供します。 + const { render } = await environment.runner.import('/src/entry-server.js') + + // 4. アプリの HTML をレンダリングします。これは、entry-server.js のエクスポートされた + // `render` 関数が適切なフレームワーク SSR API を呼び出すことを前提としています。 + // 例: ReactDOMServer.renderToString() + const appHtml = await render(url) + + // 5. アプリでレンダリングされた HTML をテンプレートに挿入します。 + const html = template.replace(``, appHtml) + + // 6. レンダリングされた HTML を送信します。 + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) +}) +``` + +## ランタイムに依存しない SSR {#runtime-agnostic-ssr} + +`RunnableDevEnvironment` は Vite サーバーと同じランタイムでコードを実行する目的のみで使用できるため、Vite サーバーを実行できるランタイム(Node.js と互換性のあるランタイム)が必要です。つまり、ランタイムに依存しないようにするには、生の `DevEnvironment` を使用する必要があります。 + +:::info `FetchableDevEnvironment` プロポーザル + +当初の提案では、`DevEnvironment` クラスに `run` メソッドがあり、利用者は `transport` オプションを使用してランナー側でインポートを呼び出すことができました。テスト中に、この API は推奨するには汎用性が足りないことがわかりました。現在、[`FetchableDevEnvironment` プロポーザル](https://github.com/vitejs/vite/discussions/18191)に関するフィードバックを募集しています。 + +::: + +`RunnableDevEnvironment` には、モジュールの値を返す `runner.import` 関数があります。ただし、この関数は生の `DevEnvironment` では使用できず、Vite の API を使用するコードとユーザーモジュールを分離する必要があります。 + +たとえば、次の例では、Vite の API を使用するコードからユーザーモジュールの値を使用しています: + +```ts +// Vite の API を使用するコード +import { createServer } from 'vite' + +const server = createServer() +const ssrEnvironment = server.environment.ssr +const input = {} + +const { createHandler } = await ssrEnvironment.runner.import('./entrypoint.js') +const handler = createHandler(input) +const response = handler(new Request('/')) + +// ------------------------------------- +// ./entrypoint.js +export function createHandler(input) { + return function handler(req) { + return new Response('hello') + } +} +``` + +ユーザーモジュールと同じランタイムでコードを実行できる場合(つまり、Node.js 固有の API に依存しない場合)、仮想モジュールを使用できます。このアプローチにより、Vite の API を使用してコードから値にアクセスする必要がなくなります。 + +```ts +// Vite の API を使用するコード +import { createServer } from 'vite' + +const server = createServer({ + plugins: [ + // `virtual:entrypoint` を処理するプラグイン + { + name: 'virtual-module', + /* プラグインの実装 */ + }, + ], +}) +const ssrEnvironment = server.environment.ssr +const input = {} + +// コードを実行する各環境ファクトリーによって公開されている関数を使用します +// 各環境ファクトリーについて、それらが提供するものをチェックします +if (ssrEnvironment instanceof RunnableDevEnvironment) { + ssrEnvironment.runner.import('virtual:entrypoint') +} else if (ssrEnvironment instanceof CustomDevEnvironment) { + ssrEnvironment.runEntrypoint('virtual:entrypoint') +} else { + throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) +} + +// ------------------------------------- +// virtual:entrypoint +const { createHandler } = await import('./entrypoint.js') +const handler = createHandler(input) +const response = handler(new Request('/')) + +// ------------------------------------- +// ./entrypoint.js +export function createHandler(input) { + return function handler(req) { + return new Response('hello') + } +} +``` + +たとえば、ユーザーモジュールで `transformIndexHtml` を呼び出すには、次のプラグインを使用できます: + +```ts {13-21} +function vitePluginVirtualIndexHtml(): Plugin { + let server: ViteDevServer | undefined + return { + name: vitePluginVirtualIndexHtml.name, + configureServer(server_) { + server = server_ + }, + resolveId(source) { + return source === 'virtual:index-html' ? '\0' + source : undefined + }, + async load(id) { + if (id === '\0' + 'virtual:index-html') { + let html: string + if (server) { + this.addWatchFile('index.html') + html = await fs.promises.readFile('index.html', 'utf-8') + html = await server.transformIndexHtml('/', html) + } else { + html = await fs.promises.readFile('dist/client/index.html', 'utf-8') + } + return `export default ${JSON.stringify(html)}` + } + return + }, + } +} +``` + +コードに Node.js API が必要な場合は、`hot.send` を使用して、ユーザーモジュールから Vite の API を使用するコードと通信できます。ただし、このアプローチはビルドプロセス後に同じように機能しない可能性があることに注意してください。 + +```ts +// Vite の API を使用するコード +import { createServer } from 'vite' + +const server = createServer({ + plugins: [ + // `virtual:entrypoint` を処理するプラグイン + { + name: 'virtual-module', + /* プラグインの実装 */ + }, + ], +}) +const ssrEnvironment = server.environment.ssr +const input = {} + +// コードを実行する各環境ファクトリーによって公開されている関数を使用します +// 各環境ファクトリーについて、それらが提供するものをチェックします +if (ssrEnvironment instanceof RunnableDevEnvironment) { + ssrEnvironment.runner.import('virtual:entrypoint') +} else if (ssrEnvironment instanceof CustomDevEnvironment) { + ssrEnvironment.runEntrypoint('virtual:entrypoint') +} else { + throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) +} + +const req = new Request('/') + +const uniqueId = 'a-unique-id' +ssrEnvironment.send('request', serialize({ req, uniqueId })) +const response = await new Promise((resolve) => { + ssrEnvironment.on('response', (data) => { + data = deserialize(data) + if (data.uniqueId === uniqueId) { + resolve(data.res) + } + }) +}) + +// ------------------------------------- +// virtual:entrypoint +const { createHandler } = await import('./entrypoint.js') +const handler = createHandler(input) + +import.meta.hot.on('request', (data) => { + const { req, uniqueId } = deserialize(data) + const res = handler(req) + import.meta.hot.send('response', serialize({ res: res, uniqueId })) +}) + +const response = handler(new Request('/')) + +// ------------------------------------- +// ./entrypoint.js +export function createHandler(input) { + return function handler(req) { + return new Response('hello') + } +} +``` + +## ビルド中の環境 {#environments-during-build} + +CLI において、`vite build` と `vite build --ssr` を呼び出すと、後方互換性のためにクライアントのみの環境と ssr のみの環境がビルドされます。 + +`builder.entireApp` が `true` の場合(または `vite build --app` を呼び出した場合)、`vite build` はアプリ全体のビルドを行います。これは将来のメジャーバージョンではデフォルトになる予定です。`ViteBuilder` インスタンス(ビルド時の `ViteDevServer` に相当)が作成され、プロダクション環境用に設定されたすべての環境がビルドされます。デフォルトでは、環境のビルドは `environments` レコードの順番に従って直列に実行されます。フレームワークやユーザーは環境を構築する方法を設定できます: + +```js +export default { + builder: { + buildApp: async (builder) => { + const environments = Object.values(builder.environments) + return Promise.all( + environments.map((environment) => builder.build(environment)), + ) + }, + }, +} +``` + +## 環境に依存しないコード {#environment-agnostic-code} + +ほとんどの場合、現在の `environment` インスタンスは実行中のコードのコンテキストの一部として利用できるため、`server.environments` を介してアクセスする必要はほとんどありません。たとえば、プラグインフック内では、環境は `PluginContext` の一部として公開されるため、`this.environment` を使用してアクセスできます。環境対応プラグインの構築方法については、[プラグイン向けの Environment API](./api-environment-plugins.md) を参照してください。 diff --git a/guide/api-environment-instances.md b/guide/api-environment-instances.md new file mode 100644 index 00000000..f58c5676 --- /dev/null +++ b/guide/api-environment-instances.md @@ -0,0 +1,188 @@ +# `Environment` インスタンスの使用 + +:::warning 実験的機能 +この API の初期研究は、Vite 5.1 で「Vite ランタイム API」という名前で導入されました。このガイドでは、Environment API と改名された改訂版 API について説明します。この API は Vite 6 で実験的機能としてリリースされる予定です。すでに最新の `vite@6.0.0-beta.x` バージョンでテストできます。 + +リソース: + +- 新しい API に関するフィードバックを収集する [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) +- 新しい API が実装され、レビューされる [Environment API PR](https://github.com/vitejs/vite/pull/16471) + +この提案をテストする際には、ぜひフィードバックをお寄せください。 +::: + +## 環境へのアクセス {#accessing-the-environments} + +開発中は、`server.environments` を使用して開発サーバー内の利用可能な環境にアクセスできます: + +```js +// サーバーを作成するか、configureServer フックから取得する +const server = await createServer(/* オプション */) + +const environment = server.environments.client +environment.transformRequest(url) +console.log(server.environments.ssr.moduleGraph) +``` + +プラグインから現在の環境にアクセスすることもできます。詳細については、[プラグイン向けの Environment API](./api-environment-plugins.md#accessing-the-current-environment-in-hooks) を参照してください。 + +## `DevEnvironment` クラス {#devenvironment-class} + +開発中、各環境は `DevEnvironment` クラスのインスタンスです: + +```ts +class DevEnvironment { + /** + * Vite サーバー内の環境の一意な識別子。 + * デフォルトでは、Vite は 'client' と 'ssr' 環境を公開します。 + */ + name: string + /** + * ターゲットランタイム内の関連モジュールランナーから + * メッセージを送受信するための通信チャネル。 + */ + hot: HotChannel | null + /** + * 処理されたモジュールと処理されたコードのキャッシュ結果との間の + * インポートされた関係を示すモジュールノードのグラフ。 + */ + moduleGraph: EnvironmentModuleGraph + /** + * この環境の解決済みプラグイン。 + * 環境ごとの `create` フックを使って作成されたものも含む。 + */ + plugins: Plugin[] + /** + * 環境プラグインパイプラインを通じて、 + * コードの解決、ロード、変換を可能にする + */ + pluginContainer: EnvironmentPluginContainer + /** + * この環境の解決された設定オプション。 + * サーバーのグローバルスコープのオプションはすべての環境のデフォルトとして扱われ、 + * オーバーライドすることができます (resolve conditions、external、optimizedDeps)。 + */ + config: ResolvedConfig & ResolvedDevEnvironmentOptions + + constructor(name, config, { hot, options }: DevEnvironmentSetup) + + /** + * URL を id に解決してロードし、プラグインパイプラインを使ってコードを処理する。 + * モジュールグラフも更新されます。 + */ + async transformRequest(url: string): TransformResult + + /** + * 低い優先度で処理されるリクエストを登録します。ウォーターフォールを回避するのに + * 役立ちます。Vite サーバーは他のリクエストによってインポートされたモジュールに関する + * 情報を持っているため、モジュールがリクエストされたときにすでに処理されているよう、 + * モジュールグラフをウォームアップできます。 + */ + async warmupRequest(url: string): void +} +``` + +`TransformResult` は次のようになります: + +```ts +interface TransformResult { + code: string + map: SourceMap | { mappings: '' } | null + etag?: string + deps?: string[] + dynamicDeps?: string[] +} +``` + +Vite サーバーの環境インスタンスでは、`environment.transformRequest(url)` メソッドを使用して URL を処理できます。この関数はプラグインパイプラインを使用して `url` をモジュール `id` に解決し、(ファイルシステムからファイルを読み込むか、仮想モジュールを実装するプラグインを介して)モジュールをロードし、コードを変換します。モジュールを変換している間、インポートやその他のメタデータは、対応するモジュールノードを作成または更新することで、環境モジュールグラフに記録されます。処理が完了すると、変換結果もモジュールに保存されます。 + +:::info transformRequest の命名 +この提案の現在のバージョンでは `transformRequest(url)` と `warmupRequest(url)` を使っているので、Vite の現在の API に慣れているユーザーにとっては議論しやすく、理解しやすいと思います。リリースする前に、これらの名前を見直す機会を設ける可能性があります。例えば、プラグインフックで Rollup の `context.load(id)` からページを取得する `environment.processModule(url)` や `environment.loadModule(url)` という名前にすることもできます。今のところは現在の名前のままで、この議論を遅らせる方が良いと考えています。 +::: + +## 独立したモジュールグラフ {#separate-module-graphs} + +各環境は独立したモジュールグラフを持ちます。すべてのモジュールグラフは同じシグネチャーを持つので、環境に依存せずにグラフをクロールしたりクエリしたりする汎用的なアルゴリズムを実装できます。`hotUpdate` が良い例です。ファイルが変更されると、各環境のモジュールグラフを使用して、影響を受けるモジュールを検出し、各環境に対して個別に HMR を実行します。 + +::: info +Vite v5 ではクライアントと SSR のモジュールグラフが混在していました。未処理のノードや無効化されたノードがあった場合、それがクライアントに対応するのか、SSR に対応するのか、あるいは両方の環境に対応するのかを知ることはできません。モジュールノードには、`clientImportedModules` や `ssrImportedModules` (および両者の和を返す `importedModules`) のようなプレフィックス付きのプロパティがあります。`importers` には、各モジュールノードのクライアントと SSR 環境のすべてのインポーターが含まれます。モジュールノードには `transformResult` と `ssrTransformResult` もあります。後方互換性レイヤーはエコシステムが非推奨の `server.moduleGraph` から移行できます。 +::: + +各モジュールは `EnvironmentModuleNode` インスタンスで表現されます。モジュールはまだ処理されていなくてもグラフに登録できます(その場合 `transformResult` は `null` となります)。モジュールが処理されると `importers` と `importedModules` も更新されます。 + +```ts +class EnvironmentModuleNode { + environment: string + + url: string + id: string | null = null + file: string | null = null + + type: 'js' | 'css' + + importers = new Set() + importedModules = new Set() + importedBindings: Map> | null = null + + info?: ModuleInfo + meta?: Record + transformResult: TransformResult | null = null + + acceptedHmrDeps = new Set() + acceptedHmrExports: Set | null = null + isSelfAccepting?: boolean + lastHMRTimestamp = 0 + lastInvalidationTimestamp = 0 +} +``` + +`environment.moduleGraph` は `EnvironmentModuleGraph` のインスタンスです: + +```ts +export class EnvironmentModuleGraph { + environment: string + + urlToModuleMap = new Map() + idToModuleMap = new Map() + etagToModuleMap = new Map() + fileToModulesMap = new Map>() + + constructor( + environment: string, + resolveId: (url: string) => Promise, + ) + + async getModuleByUrl( + rawUrl: string, + ): Promise + + getModulesByFile(file: string): Set | undefined + + onFileChange(file: string): void + + invalidateModule( + mod: EnvironmentModuleNode, + seen: Set = new Set(), + timestamp: number = Date.now(), + isHmr: boolean = false, + ): void + + invalidateAll(): void + + async ensureEntryFromUrl( + rawUrl: string, + setIsSelfAccepting = true, + ): Promise + + createFileOnlyEntry(file: string): EnvironmentModuleNode + + async resolveUrl(url: string): Promise + + updateModuleTransformResult( + mod: EnvironmentModuleNode, + result: TransformResult | null, + ): void + + getModuleByEtag(etag: string): EnvironmentModuleNode | undefined +} +``` diff --git a/guide/api-environment-plugins.md b/guide/api-environment-plugins.md new file mode 100644 index 00000000..99c97586 --- /dev/null +++ b/guide/api-environment-plugins.md @@ -0,0 +1,190 @@ +# プラグイン向けの Environment API + +:::warning 実験的機能 +この API の初期研究は、Vite 5.1 で「Vite ランタイム API」という名前で導入されました。このガイドでは、Environment API と改名された改訂版 API について説明します。この API は Vite 6 で実験的機能としてリリースされる予定です。すでに最新の `vite@6.0.0-beta.x` バージョンでテストできます。 + +リソース: + +- 新しい API に関するフィードバックを収集する [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) +- 新しい API が実装され、レビューされる [Environment API PR](https://github.com/vitejs/vite/pull/16471) + +この提案をテストする際には、ぜひフィードバックをお寄せください。 +::: + +## フック内で現在の環境にアクセスする {} + +Vite 6 までは環境が 2 つ (`client` と `ssr`) しかなかったため、Vite API の現在の環境を識別するのは `ssr` ブール値で十分でした。プラグインフックは最後のオプションパラメーターで `ssr` ブール値を受け取り、いくつかの API はモジュールを正しい環境に適切に関連付けるためにオプションの最後の `ssr` パラメーターを必要としていました (たとえば、`server.moduleGraph.getModuleByUrl(url, { ssr })`)。 + +設定可能な環境の登場により、プラグイン内のオプションやインスタンスにアクセスするための統一された方法が用意されました。プラグインフックはコンテキスト内で `this.environment` を公開するようになり、以前は `ssr` ブール値を期待していた API は適切な環境にスコープされるようになりました (たとえば `environment.moduleGraph.getModuleByUrl(url)`)。 + +Vite サーバーには共有プラグインパイプラインがありますが、モジュールが処理されるときは常に特定の環境のコンテキストで実行されます。`environment` インスタンスはプラグインコンテキストで使用できます。 + +プラグインは、`environment` インスタンスを使用して、環境の設定(`environment.config` を使用してアクセス可能)に応じてモジュールの処理方法を変更できます。 + +```ts + transform(code, id) { + console.log(this.environment.config.resolve.conditions) + } +``` + +## フックを使用して新しい環境を登録する {#registering-new-environments-using-hooks} + +プラグインは、`config` フックに新しい環境を追加できます(たとえば、[RSC](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components) 用の個別のモジュールグラフを作成する場合など): + +```ts + config(config: UserConfig) { + config.environments.rsc ??= {} + } +``` + +環境を登録するには空のオブジェクトで十分で、デフォルト値はルートレベルの環境設定から取得されます。 + +## フックを使用した環境の設定 {#configuring-environment-using-hooks} + +`config` フックが実行されている間、環境の完全なリストはまだ分かっておらず、環境はルートレベルの環境設定からのデフォルト値、または `config.environments` レコードを通して明示的に影響を受ける可能性があります。 +プラグインは `config` フックを使ってデフォルト値を設定してください。各環境を設定するには、新しい `configEnvironment` フックを使用します。このフックは、最終的なデフォルト値の解決を含む、部分的に解決された設定を持つ各環境に対して呼び出されます。 + +```ts + configEnvironment(name: string, options: EnvironmentOptions) { + if (name === 'rsc') { + options.resolve.conditions = // ... +``` + +## `hotUpdate` フック {#the-hotupdate-hook} + +- **型:** `(this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array | void | Promise | void>` +- **参照:** [HMR API](./api-hmr) + +`hotUpdate` フックを使用すると、プラグインが指定された環境に対してカスタム HMR 更新処理を実行できるようになります。ファイルが変更されると、HMR アルゴリズムは `server.environments` の順番に従って各環境で順に実行されるので、`hotUpdate` フックは複数回呼び出されることになります。このフックは以下のシグネチャを持つコンテキストオブジェクトを受け取ります: + +```ts +interface HotUpdateContext { + type: 'create' | 'update' | 'delete' + file: string + timestamp: number + modules: Array + read: () => string | Promise + server: ViteDevServer +} +``` + +- `this.environment` は現在ファイルの更新が処理されているモジュール実行環境です。 + +- `modules` は、変更されたファイルの影響を受ける、この環境のモジュールの配列です。1 つのファイルが複数のモジュール(Vue SFC など)にマッピングされる可能性があるため、配列になっています。 + +- `read` はファイルの内容を返す非同期の読み込み関数です。システムによっては、エディターがファイルの更新を終了する前にファイル変更コールバックが高速に実行され、`fs.readFile` が空の内容を返すことがあるためです。渡された読み込み関数はこの動作を正常化します。 + +フックは以下を選択できます: + +- HMR がより正確になるように、影響を受けるモジュールリストをフィルタリングして絞り込む。 + +- 空の配列を返し、フルリロードを実行する: + + ```js + hotUpdate({ modules, timestamp }) { + if (this.environment.name !== 'client') + return + + // モジュールを手動で無効化 + const invalidatedModules = new Set() + for (const mod of modules) { + this.environment.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true + ) + } + this.environment.hot.send({ type: 'full-reload' }) + return [] + } + ``` + +- 空の配列を返し、カスタムイベントをクライアントに送信することで、完全なカスタム HMR 処理を行う: + + ```js + hotUpdate() { + if (this.environment.name !== 'client') + return + + this.environment.hot.send({ + type: 'custom', + event: 'special-update', + data: {} + }) + return [] + } + ``` + + クライアントコードは [HMR API](./api-hmr) を使って対応するハンドラーを登録する必要があります(これは同じプラグインの `transform` フックによって注入できます): + + ```js + if (import.meta.hot) { + import.meta.hot.on('special-update', (data) => { + // カスタム更新を実行する + }) + } + ``` + +## 環境ごとのプラグイン {#per-environment-plugins} + +プラグインは `applyToEnvironment` 関数で、適用する環境を定義できます。 + +```js +const UnoCssPlugin = () => { + // 共有グローバル状態 + return { + buildStart() { + // WeakMap, this.environment を使って環境ごとの状態を初期化 + }, + configureServer() { + // グローバルフックを通常どおり使用 + }, + applyToEnvironment(environment) { + // このプラグインがこの環境でアクティブになる必要がある場合は true を返します + // この関数が提供されていない場合、プラグインはすべての環境でアクティブになります + }, + resolveId(id, importer) { + // このプラグインが適用される環境に対してのみ呼び出されます + }, + } +} +``` + +## ビルドフックの環境 {#environment-in-build-hooks} + +開発時と同じように、プラグインフックもビルド時に環境インスタンスを受け取り、`ssr` ブール値を置き換えます。 +これは `renderChunk` や `generateBundle` などのビルド専用のフックでも動作します。 + +## ビルド時の共有プラグイン {#shared-plugins-during-build} + +Vite 6 以前は、プラグインパイプラインは開発時とビルド時に異なる方法で動作していました: + +- **開発時:** プラグインは共有されます +- **ビルド時:** プラグインは環境ごとに分離されます(`vite build` と `vite build --ssr` という別々のプロセスで分離されます)。 + +このため、フレームワークはファイルシステムに書き込まれたマニフェストファイルを通して `client` ビルドと `ssr` ビルドの間で状態を共有することを余儀なくされていました。Vite 6 では、すべての環境を単一のプロセスでビルドするようになったので、プラグインのパイプラインと環境間通信の方法を開発時と合わせることができるようになりました。 + +将来のメジャー(Vite 7 または 8)では、完全な整合性を実現することを目指しています: + +- **開発時とビルド時:** プラグインは[環境ごとのフィルタリング](#per-environment-plugins)で共有されます + +また、ビルド時に共有される `ResolvedConfig` インスタンスは 1 つになり、開発時に `WeakMap` を使っていたのと同じように、アプリのビルドプロセスレベル全体でキャッシュが可能になります。 + +Vite 6 では、後方互換性を保つために小さなステップを行う必要があります。エコシステムのプラグインは現在、設定へアクセスするために `environment.config.build` ではなく `config.build` を使用しているため、デフォルトでは環境ごとに新しい `ResolvedConfig` を作成する必要があります。プロジェクトは `builder.sharedConfigBuild` を `true` に設定することで、完全な設定とプラグインパイプラインを共有できます。 + +このオプションは、最初のうちは小さなプロジェクトのサブセットでしか機能しないため、プラグインの作者は `sharedDuringBuild` フラグを `true` に設定することで、特定のプラグインを共有するように選択できます。これにより、通常のプラグインでも簡単に状態を共有できるようになります: + +```js +function myPlugin() { + // 開発環境とビルド環境のすべての環境で状態を共有する + const sharedState = ... + return { + name: 'shared-plugin', + transform(code, id) { ... }, + + // すべての環境で単一のインスタンスにオプトインする + sharedDuringBuild: true, + } +} +``` diff --git a/guide/api-environment-runtimes.md b/guide/api-environment-runtimes.md new file mode 100644 index 00000000..230aa606 --- /dev/null +++ b/guide/api-environment-runtimes.md @@ -0,0 +1,363 @@ +# ランタイム向けの Environment API + +:::warning 実験的機能 +この API の初期研究は、Vite 5.1 で「Vite ランタイム API」という名前で導入されました。このガイドでは、Environment API と改名された改訂版 API について説明します。この API は Vite 6 で実験的機能としてリリースされる予定です。すでに最新の `vite@6.0.0-beta.x` バージョンでテストできます。 + +リソース: + +- 新しい API に関するフィードバックを収集する [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) +- 新しい API が実装され、レビューされる [Environment API PR](https://github.com/vitejs/vite/pull/16471) + +この提案をテストする際には、ぜひフィードバックをお寄せください。 +::: + +## 環境ファクトリー {#environment-factories} + +環境ファクトリーは、エンドユーザーではなく、Cloudflare などの環境プロバイダーによって実装されることを目的としています。環境ファクトリーは、開発環境とビルド環境の両方でターゲットランタイムを使用する最も一般的なケースで、`EnvironmentOptions` を返します。デフォルトの環境オプションも設定できるため、ユーザーが設定する必要はありません。 + +```ts +function createWorkedEnvironment( + userConfig: EnvironmentOptions, +): EnvironmentOptions { + return mergeConfig( + { + resolve: { + conditions: [ + /*...*/ + ], + }, + dev: { + createEnvironment(name, config) { + return createWorkerdDevEnvironment(name, config, { + hot: 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 +``` + +## 新しい環境ファクトリーの作成 {#creating-a-new-environment-factory} + +Vite 開発サーバーは、デフォルトで `client` 環境と `ssr` 環境の 2 つの環境を公開します。クライアント環境はデフォルトではブラウザー環境であり、モジュールランナーは仮想モジュール `/@vite/client` をクライアントアプリにインポートすることによって実装されます。SSR 環境は、デフォルトでは Vite サーバーと同じ Node ランタイムで実行され、開発時は完全な HMR サポートによって、アプリケーションサーバーを使用してリクエストをレンダリングできます。 + +変換されたソースコードはモジュールと呼ばれ、各環境で処理されるモジュール間の関係はモジュールグラフに保持されます。これらのモジュールの変換されたコードは、実行される各環境に関連付けられたランタイムに送信されます。ランタイムでモジュールが評価されると、そのモジュールにインポートされたモジュールがリクエストされ、モジュールグラフのセクションの処理がトリガーされます。 + +Vite モジュールランナーは、最初に Vite プラグインで処理することで、任意のコードを実行できます。ランナーの実装がサーバーから分離されている点が `server.ssrLoadModule` とは異なります。これによりライブラリーおよびフレームワークの作者は、Vite サーバーとランナー間の通信レイヤーを実装できます。ブラウザーは、サーバーの Web ソケットと HTTP リクエストを使用して、対応する環境と通信します。Node モジュールランナーは、同じプロセスで実行されているため、モジュールを処理するために関数呼び出しを直接実行できます。他の環境では、workerd などの JS ランタイムに接続するモジュール、または Vitest のようなワーカースレッドを実行するモジュールを実行できます。 + +この機能の目的の 1 つは、コードを処理および実行するためのカスタマイズ可能な API を提供することです。ユーザーは、公開されたプリミティブを使用して新しい環境ファクトリーを作成できます。 + +```ts +import { DevEnvironment, RemoteEnvironmentTransport } from 'vite' + +function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, context: DevEnvironmentContext) { + const hot = /* ... */ + const connection = /* ... */ + const transport = new RemoteEnvironmentTransport({ + 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, + }, + }) + return workerdDevEnvironment +} +``` + +## `ModuleRunner` + +モジュールランナーはターゲットランタイムでインスタンス化されます。次のセクションの全ての API は、特に断りのない限り `vite/module-runner` からインポートされます。このエクスポート・エントリーポイントは可能な限り軽量に保たれており、モジュールランナーを作成するために必要な最小限のものだけがエクスポートされます。 + +**型シグネチャー:** + +```ts +export class ModuleRunner { + constructor( + public options: ModuleRunnerOptions, + public evaluator: ModuleEvaluator, + private debug?: ModuleRunnerDebugger, + ) {} + /** + * 実行するURL。ルートからの相対的なファイルパス、サーバーパス、ID を受け付けます。 + */ + public async import(url: string): Promise + /** + * HMR リスナーを含むすべてのキャッシュをクリアします。 + */ + public clearCache(): void + /** + * すべてのキャッシュをクリアし、すべての HMR リスナーを削除し、ソースマップのサポートをリセットします。 + * このメソッドは HMR 接続を停止しません。 + */ + public async close(): Promise + /** + * `close()` メソッドを呼び出してランナーを終了した場合は `true` を返します。 + */ + 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, fetchModule } from './rpc-implementation.js' + +const moduleRunner = new ModuleRunner( + { + root, + fetchModule, + // HMR をサポートするために hmr.connection を提供することもできます + }, + new ESModulesEvaluator(), +) + +await moduleRunner.import('/src/entry-point.js') +``` + +## `ModuleRunnerOptions` + +```ts +export interface ModuleRunnerOptions { + /** + * プロジェクトのルート + */ + root: string + /** + * サーバーと通信するための一連のメソッド。 + */ + transport: RunnerTransport + /** + * ソースマップの解決方法を設定します。`process.setSourceMapsEnabled` が使用可能な場合は `node` を優先します。 + * それ以外の場合は、デフォルトで `prepareStackTrace` を使用し、`Error.prepareStackTrace` メソッドをオーバーライドします。 + * Vite によって処理されなかったファイルのファイル内容とソースマップの解決方法を設定するオブジェクトを提供できます。 + */ + sourcemapInterceptor?: + | false + | 'node' + | 'prepareStackTrace' + | InterceptorOptions + /** + * HMR を無効にするか、HMR オプションを設定します。 + */ + hmr?: + | false + | { + /** + * HMR がクライアントとサーバー間で通信する方法を設定します。 + */ + connection: ModuleRunnerHMRConnection + /** + * HMR ロガーを設定します。 + */ + logger?: false | HMRLogger + } + /** + * カスタムモジュールキャッシュ。指定されていない場合は、モジュールランナーインスタンスごとに個別のモジュールキャッシュが作成されます。 + */ + evaluatedModules?: EvaluatedModules +} +``` + +## `ModuleEvaluator` + +**型シグネチャー:** + +```ts +export interface ModuleEvaluator { + /** + * 変換後のコードに含まれるプレフィックスの行数。 + */ + startOffset?: number + /** + * Vite によって変換されたコードを評価します。 + * @param context 関数コンテキスト + * @param code 変換されたコード + * @param id モジュールを取得するために使用された ID + */ + runInlinedModule( + context: ModuleRunnerContext, + code: string, + id: string, + ): Promise + /** + * 外部化されたモジュールを評価します。 + * @param file 外部モジュールへのファイル URL + */ + runExternalModule(file: string): Promise +} +``` + +Vite はデフォルトでこのインターフェイスを実装した `ESModulesEvaluator` をエクスポートします。コードの評価には `new AsyncFunction` を使用するので、インライン化されたソースマップがある場合は、新しい行が追加されたことを考慮して [2 行分のオフセット](https://tc39.es/ecma262/#sec-createdynamicfunction)を追加する必要があります。これは `ESModulesEvaluator` によって自動的に実行されます。カスタムの Evaluator は行を追加しません。 + +## RunnerTransport + +**型シグネチャー:** + +```ts +interface RunnerTransport { + /** + * モジュールに関する情報を取得するメソッド。 + */ + fetchModule: FetchFunction +} +``` + +RPC 経由または関数を直接呼び出して環境と通信するトランスポートオブジェクト。デフォルトでは、`fetchModule` メソッドでオブジェクトを渡す必要があります。このメソッド内ではどのようなタイプの RPC も使用できますが、Vite では設定を簡単にするために `RemoteRunnerTransport` クラスを使用して双方向のトランスポートインターフェースを公開しています。モジュールランナーがワーカースレッドで作成される次の例のように、サーバー上の `RemoteEnvironmentTransport` インスタンスと合わせる必要があります: + +::: code-group + +```ts [worker.js] +import { parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { + ESModulesEvaluator, + ModuleRunner, + RemoteRunnerTransport, +} from 'vite/module-runner' + +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, + }), + }, + new ESModulesEvaluator(), +) +``` + +```ts [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), + }), + }, + }) +} + +await createServer({ + environments: { + worker: { + dev: { + createEnvironment: createWorkerEnvironment, + }, + }, + }, +}) +``` + +::: + +`RemoteRunnerTransport` と `RemoteEnvironmentTransport` は一緒に使うことを想定していますが、必ずしも使う必要はありません。独自の関数を定義して、ランナーとサーバー間の通信を行えます。例えば、HTTP リクエストで環境に接続する場合、`fetchModule` 関数で `fetch().json()` を呼び出せます: + +```ts +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +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}`, + ) + return response.json() + }, + }, + }, + new ESModulesEvaluator(), +) + +await runner.import('/entry.js') +``` + +## ModuleRunnerHMRConnection + +**型シグネチャー:** + +```ts +export interface ModuleRunnerHMRConnection { + /** + * サーバーにメッセージを送信する前にチェックされます。 + */ + isReady(): boolean + /** + * サーバーにメッセージを送信します。 + */ + send(payload: HotPayload): void + /** + * この接続が更新をトリガーしたときに HMR がどのように処理されるかを設定します。 + * このメソッドは、接続が HMR 更新のリッスンを開始し、受信時にこのコールバックを呼び出すことを想定しています。 + */ + onUpdate(callback: (payload: HotPayload) => void): void +} +``` + +このインターフェイスは HMR 通信の確立方法を定義します。Vite の SSR 中に HMR をサポートするために、Vite は `ServerHMRConnector` をメインエントリーからエクスポートします。`isReady` と `send` メソッドは通常、カスタムイベントがトリガーされたときに呼び出されます(`import.meta.hot.send("my-event")` のように)。 + +`onUpdate` は、新しいモジュールランナーが初期化されたときに一度だけ呼ばれます。接続が HMR イベントをトリガーしたときに呼び出されるメソッドを渡します。実装は接続の種類(例として、`WebSocket`/`EventEmitter`/`MessageChannel`)に依存しますが、通常は以下のようになります: + +```js +function onUpdate(callback) { + this.connection.on('hmr', (event) => callback(event.data)) +} +``` + +コールバックはキューに入れられ、次の更新を処理する前に現在の更新が解決されるのを待ちます。ブラウザーの実装とは異なり、モジュールランナーにおける HMR の更新は、モジュールを更新する前に、すべてのリスナー(`vite:beforeUpdate`/`vite:beforeFullReload` など)が終了するまで待機します。 diff --git a/guide/api-environment.md b/guide/api-environment.md index c0fa38f5..e6afded3 100644 --- a/guide/api-environment.md +++ b/guide/api-environment.md @@ -1,6 +1,6 @@ # Environment API -:::warning 低レベル API +:::warning 実験的機能 この API の初期研究は、Vite 5.1 で「Vite ランタイム API」という名前で導入されました。このガイドでは、Environment API と改名された改訂版 API について説明します。この API は Vite 6 で実験的機能としてリリースされる予定です。すでに最新の `vite@6.0.0-beta.x` バージョンでテストできます。 リソース: @@ -11,330 +11,20 @@ この提案をテストする際には、ぜひフィードバックをお寄せください。 ::: -Vite 6 は環境の概念が形式化され、環境を作成・設定するための新しい API を導入するとともに、一貫した API でオプションやコンテキストユーティリティにアクセスできるようになりました。Vite 2 以降、2 つの暗黙的な環境(`client` と `ssr`)が存在していました。プラグインフックは、最後のオプションパラメーターで `ssr` という真偽値を受け取り、処理される各モジュールのターゲット環境を識別しました。いくつかの API はモジュールを正しい環境に適切に関連付けるために、オプションの最後の `ssr` パラメーターを受け付けていました(例えば `server.moduleGraph.getModuleByUrl(url, { ssr })`)。`ssr` 環境は、クライアント環境に存在するオプションの一部が設定された `config.ssr` を使って設定されました。開発時には、`client` 環境と `ssr` 環境の両方が単一の共有プラグインパイプラインで同時に実行されていました。ビルド時には、各ビルドは新しいプラグインのセットを含む新しい解決済みの設定インスタンスを取得していました。 +## 環境の形式化 {#formalizing-environments} -新しい Environment API はこれら 2 つのデフォルト環境を明示的にするだけでなく、ユーザーが必要な数だけ名前付き環境を作成できるようにします。環境を設定するための統一された方法(`config.environment` を使用)があり、処理中のモジュールに関連する環境オプションとコンテキストユーティリティーは、`this.environment` を使用してプラグインフックでアクセスできます。以前は `ssr` という真偽値を受け取っていた API は適切な環境にスコープされるようになりました(例えば `environment.moduleGraph.getModuleByUrl(url)`)。開発中は、以前と同じようにすべての環境が同時に実行されます。ビルド時には、後方互換性のために、各ビルドは独自の解決された設定インスタンスを取得します。ただし、プラグインやユーザーは共有ビルドパイプラインにオプトインできます。 +Vite 6 では、環境の概念が正式化されました。Vite 5 までは、暗黙的な環境が 2 つ(`client` と `ssr`)が存在していました。新しい Environment API を使用すると、ユーザーは必要な数の環境を作成して、アプリが本番環境でどのように動作するかをマッピングできます。この新しい機能には大規模な内部リファクタリングが必要でしたが、後方互換性にも多大な努力が払われました。Vite 6 の当初の目標は、エコシステムをできるだけスムーズに新しいメジャーに移行することであり、十分な数のユーザーが移行し、フレームワークとプラグインの作成者が新しい設計を検証するまで、これらの新しい実験的な API の採用を遅らせます。 -内部的に大きな変更があり、新しいオプトイン API があったとしても、Vite 5 からの破壊的変更はありません。Vite 6 の最初の目標は、エコシステムをできるだけスムーズに新メジャーに移行させ、プラグインの新バージョンを利用できる十分なユーザーが揃うまで、プラグインの新 API の採用促進を遅らせることです。 +## ビルドと開発中のギャップを埋める {#closing-the-gap-between-build-and-dev} -## Vite サーバーでの環境の使用 {#using-environments-in-the-vite-server} +シンプルな SPA の場合、環境は 1 つだけです。アプリはユーザーのブラウザーで実行されます。開発中は、Vite が最新のブラウザーを必要とする場合を除き、環境はプロダクションのランタイムとほぼ一致します。Vite 6 では、ユーザーが環境について知らなくても Vite を使用できます。この場合、通常の vite 構成はデフォルトのクライアント環境で機能します。 -単一の Vite 開発サーバーを使用して、異なるモジュール実行環境と同時にやり取りできます。ここでは環境という言葉は、ID を解決し、ソースコードをロードし、処理でき、コードが実行されるランタイムに接続されている、構成された Vite 処理パイプラインを指します。変換されたソースコードはモジュールと呼ばれ、各環境で処理されるモジュール間の関係はモジュールグラフに保持されます。これらのモジュールのコードは、実行される各環境に関連付けられたランタイムに送信されます。モジュールが評価されると、ランタイムはインポートされたモジュールを要求し、モジュールグラフのセクションの処理をトリガーします。典型的な Vite アプリでは、環境はクライアントに提供される ES モジュールと SSR を行うサーバープログラムに使用されます。アプリは Node サーバーだけでなく、[Cloudflare の workerd](https://github.com/cloudflare/workerd) のような他の JS ランタイムでも SSR を行うことができます。つまり、ブラウザー環境、Node 環境、workerd 環境など、さまざまなタイプの環境を同じ Vite サーバー上に持つことができるのです。 +典型的なサーバーサイドレンダリングの Vite アプリには、2 つの環境があります。クライアント環境はブラウザーでアプリを実行し、Node 環境では SSR を行なうサーバーを実行します。Vite を開発モードで実行すると、サーバーのコードは Vite 開発サーバーと同じ Node プロセスで実行され、プロダクション環境に近い状態になります。ただし、アプリは [Cloudflare の workerd](https://github.com/cloudflare/workerd) のような他の JS ランタイムでサーバーを実行することもできます。また、最新のアプリでは 2 つ以上の環境を持つことも一般的です(たとえば、アプリはブラウザー、Node サーバー、エッジサーバーで実行される可能性があります)。Vite 5 では、これらのケースを適切に表現できませんでした。 -Vite モジュールランナーは、最初に Vite プラグインで処理することで任意のコードを実行できます。`server.ssrLoadModule` とは異なり、ランナーの実装はサーバーから切り離されています。これにより、ライブラリやフレームワークの作者は Vite サーバーとランナー間の通信レイヤーを実装できます。ブラウザーは、サーバーの Web ソケットや HTTP リクエストを使って対応する環境と通信します。Node モジュールランナーは、同じプロセスで実行されているため、モジュールを処理するための関数呼び出しを直接行うことができます。他の環境では、workerd のような JS ランタイムに接続するモジュールや Vitest のようにワーカースレッドに接続するモジュールを実行できます。 - -これらの環境はすべて Vite の HTTP サーバー、ミドルウェア、Web ソケットを共有しています。解決された設定とプラグインのパイプラインも共有されますが、プラグインは `apply` を使うことができるので、フックは特定の環境でのみ呼び出されます。環境はフック内部でアクセスすることもでき、きめ細かい制御が可能です。 +Vite 6 では、ビルドと開発中にアプリの設定を行ない、すべての環境をマッピングできます。開発中は単一の Vite 開発サーバーを使用して、複数の異なる環境で同時にコードを実行できるようになりました。アプリのソースコードは、引き続き Vite 開発サーバーによって変換されます。共有 HTTP サーバー、ミドルウェア、解決された設定、プラグインパイプラインに加えて、Vite サーバーには独立した開発環境のセットが用意されています。各環境は、プロダクションにできるだけ近い形で構成され、コードが実行される開発ランタイムに接続されています(workerd の場合、サーバーコードはローカルで miniflare で実行できるようになりました)。クライアントでは、ブラウザーがコードをインポートして実行します。他の環境では、モジュールランナーが変換されたコードを取得して評価します。 ![Vite Environments](../images/vite-environments.svg) -Vite 開発サーバーはデフォルトで `client` 環境と `ssr` 環境の 2 つの環境を公開します。クライアント環境はデフォルトではブラウザー環境であり、モジュールランナーは `/@vite/client` という仮想モジュールをクライアントアプリにインポートすることで実装されています。SSR 環境はデフォルトで Vite サーバーと同じ Node ランタイムで実行され、HMR を完全にサポートした開発中のリクエストのレンダリングにアプリケーションサーバーを使用できます。フレームワークやユーザーがデフォルトのクライアントと SSR 環境の環境タイプを変更したり、新しい環境を登録したりする方法については後で説明します(例えば [RSC](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components) 用の独立したモジュールグラフを持つなど)。 - -利用可能な環境は `server.environments` を使ってアクセスできます: - -```js -const environment = server.environments.client - -environment.transformRequest(url) - -console.log(server.environments.ssr.moduleGraph) -``` - -ほとんどの場合、現在の `environment` インスタンスは実行中のコードのコンテキストの一部として利用できるので、 `server.environments` を使ってアクセスする必要はほとんどないはずです。例えば、プラグインフックの内部では、環境は `PluginContext` の一部として公開されるので、`this.environment` を使ってアクセスできます。 - -開発環境は `DevEnvironment` クラスのインスタンスです: - -```ts -class DevEnvironment { - /** - * Vite サーバー内の環境の一意な識別子。 - * デフォルトでは、Vite は 'client' と 'ssr' 環境を公開します。 - */ - name: string - /** - * ターゲットランタイム内の関連モジュールランナーから - * メッセージを送受信するための通信チャネル。 - */ - hot: HotChannel | null - /** - * 処理されたモジュールと処理されたコードのキャッシュ結果との間の - * インポートされた関係を示すモジュールノードのグラフ。 - */ - moduleGraph: EnvironmentModuleGraph - /** - * この環境の解決済みプラグイン。 - * 環境ごとの `create` フックを使って作成されたものも含む。 - */ - plugins: Plugin[] - /** - * 環境プラグインパイプラインを通じて、 - * コードの解決、ロード、変換を可能にする - */ - pluginContainer: EnvironmentPluginContainer - /** - * この環境の解決された設定オプション。 - * サーバーのグローバルスコープのオプションはすべての環境のデフォルトとして扱われ、 - * オーバーライドすることができます (resolve conditions、external、optimizedDeps)。 - */ - config: ResolvedConfig & ResolvedDevEnvironmentOptions - - constructor(name, config, { hot, options }: DevEnvironmentSetup) - - /** - * URL を id に解決してロードし、プラグインパイプラインを使ってコードを処理する。 - * モジュールグラフも更新されます。 - */ - async transformRequest(url: string): TransformResult - - /** - * 低い優先度で処理されるリクエストを登録します。ウォーターフォールを回避するのに - * 役立ちます。Vite サーバーは他のリクエストによってインポートされたモジュールに関する - * 情報を持っているため、モジュールがリクエストされたときにすでに処理されているよう、 - * モジュールグラフをウォームアップできます。 - */ - async warmupRequest(url: string): void -} -``` - -`TransformResult` は次のようになります: - -```ts -interface TransformResult { - code: string - map: SourceMap | { mappings: '' } | null - etag?: string - deps?: string[] - dynamicDeps?: string[] -} -``` - -Vite は `ModuleRunner` インスタンスを公開する `DevEnvironment` を拡張した `RunnableDevEnvironment` もサポートしています。`isRunnableDevEnvironment` 関数を使用すると、実行可能な環境をガードできます。 - -:::warning -`runner` は初めてアクセスされたときに、優先的に評価されます。Vite は、`process.setSourceMapsEnabled` を呼び出すことによって `runner` が作成されたとき、または、利用できない場合は `Error.prepareStackTrace` をオーバーライドすることによって、ソースマップのサポートを有効にすることに注意してください。 -::: - -```ts -export class RunnableDevEnvironment extends DevEnvironment { - public readonly runner: ModuleRunnner -} - -if (isRunnableDevEnvironment(server.environments.ssr)) { - await server.environments.ssr.runner.import('/entry-point.js') -} -``` - -Vite サーバーの環境インスタンスでは、`environment.transformRequest(url)` メソッドを使用して URL を処理できます。この関数はプラグインパイプラインを使用して `url` をモジュール `id` に解決し、(ファイルシステムからファイルを読み込むか、仮想モジュールを実装するプラグインを介して)モジュールをロードし、コードを変換します。モジュールを変換している間、インポートやその他のメタデータは、対応するモジュールノードを作成または更新することで、環境モジュールグラフに記録されます。処理が完了すると、変換結果もモジュールに保存されます。 - -しかし、モジュールが実行されるランタイムが Vite サーバーが実行されているランタイムと異なる可能性があるため、環境インスタンスはコード自体を実行することはできません。これはブラウザー環境の場合です。HTML がブラウザーに読み込まれると、そのスクリプトが実行され、静的モジュールグラフ全体の評価が開始されます。インポートされた各 URL は、モジュールコードを取得するために Vite サーバーへのリクエストを生成します。このリクエストは、`server.environment.client.transformRequest(url)` を呼び出すことによって、変換ミドルウェアによって処理されます。サーバーの環境インスタンスとブラウザーのモジュールランナー間の接続は、この場合 HTTP を通して行われます。 - - -:::info transformRequest の命名 -この提案の現在のバージョンでは `transformRequest(url)` と `warmupRequest(url)` を使っているので、Vite の現在の API に慣れているユーザーにとっては議論しやすく、理解しやすいと思います。リリースする前に、これらの名前を見直す機会を設ける可能性があります。例えば、プラグインフックで Rollup の `context.load(id)` からページを取得する `environment.processModule(url)` や `environment.loadModule(url)` という名前にすることもできます。今のところは現在の名前のままで、この議論を遅らせる方が良いと考えています。 - -:::info モジュールの実行 -最初の提案では、コンシューマが `transport` オプションを使うことでランナー側でインポートを呼び出すことができる `run` メソッドがありました。テスト中に、この API を推奨するほど汎用的なものではないことがわかりました。私たちはフレームワークからのフィードバックに基づいて、リモート SSR 実装のための組み込みレイヤーを実装する予定です。それまでの間、Vite はランナー RPC の複雑さを隠すために [`RunnerTransport` API](#runnertransport) を公開しています。 -::: - -開発モードでは、デフォルトの `ssr` 環境は、開発サーバーと同じ JS ランタイムで実行される `new AsyncFunction` で評価したものを実装するモジュールランナーを備えた `RunnableDevEnvironment` です。このランナーは `ModuleRunner` のインスタンスで、次のように公開します: - -```ts -class ModuleRunner { - /** - * 実行するURL。ルートからの相対的なファイルパス、サーバーパス、ID を受け付けます。 - * インスタンス化されたモジュールを返します (ssrLoadModule と同じ) - */ - public async import(url: string): Promise> - /** - * その他の ModuleRunner メソッド... - */ -``` - -:::info -v5.1 のランタイム API では `executeUrl` メソッドと `executeEntryPoint` メソッドがありましたが、現在は単一の `import` メソッドに統合されています。HMR のサポートを停止したい場合は、`hmr: false` フラグを付けてランナーを作成します。 -::: - -[SSR セットアップガイド](/guide/ssr#setting-up-the-dev-server)で説明されているように、ミドルウェアモードに設定された Vite サーバーがあるとして、Environment API を使って SSR ミドルウェアを実装してみましょう。エラー処理は省略します。 - -```js -import { createServer, createRunnableDevEnvironment } from 'vite' - -const server = await createServer({ - server: { middlewareMode: true }, - appType: 'custom', - environments: { - node: { - dev: { - // デフォルトの Vite SSR 環境はコンフィグで上書きできるので、 - // リクエストを受け取る前に Node 環境があることを確認してください。 - createEnvironment(name, config) { - return createRunnableDevEnvironment(name, config) - }, - }, - }, - }, -}) - -// TypeScript では、これを RunnableDevEnvironment にキャストするか、ランナーへのアクセスを -// 保護するために "isRunnableDevEnvironment" 関数を使用する必要があるかもしれません -const environment = server.environments.node - -app.use('*', async (req, res, next) => { - const url = req.originalUrl - - // 1. index.html を読み込む - let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') - - // 2. Vite HTML 変換を適用します。これにより、Vite HMR クライアントが挿入され、 - // Vite プラグインからの HTML 変換も適用されます。 - // 例: global preambles from @vitejs/plugin-react - template = await server.transformIndexHtml(url, template) - - // 3. サーバーエントリをロードします。import(url) は、 - // ESM ソースコードを Node.js で使用できるように自動的に変換します。 - // バンドルは不要で、完全な HMR サポートを提供します。 - const { render } = await environment.runner.import('/src/entry-server.js') - - // 4. アプリの HTML をレンダリングします。これは、entry-server.js のエクスポートされた - // `render` 関数が適切なフレームワーク SSR API を呼び出すことを前提としています。 - // 例: ReactDOMServer.renderToString() - const appHtml = await render(url) - - // 5. アプリでレンダリングされた HTML をテンプレートに挿入します。 - const html = template.replace(``, appHtml) - - // 6. レンダリングされた HTML を送信します。 - res.status(200).set({ 'Content-Type': 'text/html' }).end(html) -}) -``` - -## 環境に依存しない SSR {#environment-agnostic-ssr} - -::: info -最も一般的な SSR のユースケースをカバーするために Vite がどのような API を提供すべきかはまだ明確ではありません。私たちはエコシステムがまず共通のパターンを探索できるように、環境に依存しない SSR を行うための公式な方法なしに Environment API をリリースすることを考えています。 -::: - -## 独立したモジュールグラフ {#separate-module-graphs} - -各環境は独立したモジュールグラフを持ちます。すべてのモジュールグラフは同じシグネチャーを持つので、環境に依存せずにグラフをクロールしたりクエリしたりする汎用的なアルゴリズムを実装できます。`hotUpdate` が良い例です。ファイルが変更されると、各環境のモジュールグラフを使用して、影響を受けるモジュールを検出し、各環境に対して個別に HMR を実行します。 - -::: info -Vite v5 ではクライアントと SSR のモジュールグラフが混在していました。未処理のノードや無効化されたノードがあった場合、それがクライアントに対応するのか、SSR に対応するのか、あるいは両方の環境に対応するのかを知ることはできません。モジュールノードには、`clientImportedModules` や `ssrImportedModules` (および両者の和を返す `importedModules`) のようなプレフィックス付きのプロパティがあります。`importers` には、各モジュールノードのクライアントと SSR 環境のすべてのインポーターが含まれます。モジュールノードには `transformResult` と `ssrTransformResult` もあります。後方互換性レイヤーはエコシステムが非推奨の `server.moduleGraph` から移行できます。 -::: - -各モジュールは `EnvironmentModuleNode` インスタンスで表現されます。モジュールはまだ処理されていなくてもグラフに登録できます(その場合 `transformResult` は `null` となります)。モジュールが処理されると `importers` と `importedModules` も更新されます。 - -```ts -class EnvironmentModuleNode { - environment: string - - url: string - id: string | null = null - file: string | null = null - - type: 'js' | 'css' - - importers = new Set() - importedModules = new Set() - importedBindings: Map> | null = null - - info?: ModuleInfo - meta?: Record - transformResult: TransformResult | null = null - - acceptedHmrDeps = new Set() - acceptedHmrExports: Set | null = null - isSelfAccepting?: boolean - lastHMRTimestamp = 0 - lastInvalidationTimestamp = 0 -} -``` - -`environment.moduleGraph` は `EnvironmentModuleGraph` のインスタンスです: - -```ts -export class EnvironmentModuleGraph { - environment: string - - urlToModuleMap = new Map() - idToModuleMap = new Map() - etagToModuleMap = new Map() - fileToModulesMap = new Map>() - - constructor( - environment: string, - resolveId: (url: string) => Promise, - ) - - async getModuleByUrl( - rawUrl: string, - ): Promise - - getModulesByFile(file: string): Set | undefined - - onFileChange(file: string): void - - invalidateModule( - mod: EnvironmentModuleNode, - seen: Set = new Set(), - timestamp: number = Date.now(), - isHmr: boolean = false, - ): void - - invalidateAll(): void - - async ensureEntryFromUrl( - rawUrl: string, - setIsSelfAccepting = true, - ): Promise - - createFileOnlyEntry(file: string): EnvironmentModuleNode - - async resolveUrl(url: string): Promise - - updateModuleTransformResult( - mod: EnvironmentModuleNode, - result: TransformResult | null, - ): void - - getModuleByEtag(etag: string): EnvironmentModuleNode | undefined -} -``` - -## 新しい環境の作成 {#creating-new-environments} - -この機能の目的のひとつは、コードを処理し実行するためのカスタマイズ可能な API を提供することです。ユーザーは、公開されているプリミティブを使って新しい環境タイプを作成できます。 - -```ts -import { DevEnvironment, RemoteEnvironmentTransport } from 'vite' - -function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, context: DevEnvironmentContext) { - const hot = /* ... */ - const connection = /* ... */ - const transport = new RemoteEnvironmentTransport({ - 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, - }, - }) - return workerdDevEnvironment -} -``` - -そして、ユーザーは以下を使用して SSR を実行するための workerd 環境を作成できます: - -```js -const ssrEnvironment = createWorkerdEnvironment('ssr', config) -``` - ## 環境設定 {#environment-configuration} 環境は `environments` 設定オプションで明示的に設定します。 @@ -397,618 +87,27 @@ interface UserConfig extends EnvironmentOptions { ## カスタム環境インスタンス {#custom-environment-instances} -カスタムの開発環境やビルド環境のインスタンスを作成するには、`dev.createEnvironment` または `build.createEnvironment` 関数を使用します。 - -```js -export default { - environments: { - rsc: { - dev: { - createEnvironment(name, config, { watcher }) { - // 開発時に 'rsc' と解決されたコンフィグで呼び出される - return createRunnableDevEnvironment(name, config, { - hot: customHotChannel(), - watcher - }) - } - }, - build: { - createEnvironment(name, config) { - // ビルド時に 'rsc' と解決されたコンフィグで呼び出される - return createNodeBuildEnvironment(name, config) - } - outDir: '/dist/rsc', - }, - }, - }, -} -``` - -この環境は `server.environments` を通してミドルウェアやプラグインフックでアクセスできます。プラグインフックでは、環境インスタンスがオプションで渡されるので、設定方法に応じて条件を実行できます。 - -Workerd のような環境プロバイダーは、開発環境とビルド環境の両方に同じランタイムを使うという最も一般的なケースのために、環境プロバイダーを公開できます。デフォルトの環境オプションも設定できるので、ユーザーは設定する必要がありません。 +低レベルの設定 API が利用できるので、ランタイムプロバイダーはそれぞれのランタイム用の環境を提供できます。 ```js -function createWorkedEnvironment(userConfig) { - return mergeConfig( - { - resolve: { - conditions: [ - /*...*/ - ], - }, - dev: { - createEnvironment(name, config, { watcher }) { - return createWorkerdDevEnvironment(name, config, { - hot: customHotChannel(), - watcher, - }) - }, - }, - build: { - createEnvironment(name, config) { - return createWorkerdBuildEnvironment(name, config) - }, - }, - }, - userConfig, - ) -} -``` - -設定ファイルは次のように記述できます - -```js -import { createWorkerdEnvironment } from 'vite-environment-workerd' +import { createCustomEnvironment } from 'vite-environment-provider' export default { environments: { - ssr: createWorkerdEnvironment({ + client: { build: { - outDir: '/dist/ssr', + outDir: '/dist/client', }, - }), - rsc: createWorkerdEnvironment({ + } + ssr: createCustomEnvironment({ build: { - outDir: '/dist/rsc', + outDir: '/dist/ssr', }, }), - ], -} -``` - -この例では、`ssr` 環境がランタイムとして workerd を使用するようにどのように設定できるかが分かります。さらに、workerd ランタイムの別のインスタンスによってサポートされる新しいカスタム RSC 環境も定義されます。 - -## プラグインと環境 {#plugins-and-environments} - -### フックで現在の環境にアクセスする {#accessing-the-current-environment-in-hooks} - -Vite サーバーには共有プラグインパイプラインがありますが、モジュールが処理されるときは常に指定された環境のコンテキストで行われます。`environment` インスタンスは `resolveId`、`load`、`transform` のプラグインコンテキストで利用できます。 - -プラグインは `environment` インスタンスを次のように使用できます: - -- 特定の環境に対してのみロジックを適用する。 -- `environment.config` を使用してアクセスできる環境の設定に応じて、動作方法を変更する。例えば vite core resolve プラグインは、`environment.config.resolve.conditions` に基づいて id を解決する方法を変更します。 - -```ts - transform(code, id) { - console.log(this.environment.config.resolve.conditions) - } -``` - -### フックを使用して新しい環境を登録する {#registering-new-environments-using-hooks} - -プラグインは `config` フックで新しい環境を追加できます: - -```ts - config(config: UserConfig) { - config.environments.rsc ??= {} - } -``` - -環境を登録するには空のオブジェクトで十分で、デフォルト値はルートレベルの環境設定から取得されます。 - -### フックを使用した環境の設定 {#configuring-environment-using-hooks} - -`config` フックが実行されている間、環境の完全なリストはまだ分かっておらず、環境はルートレベルの環境設定からのデフォルト値、または `config.environments` レコードを通して明示的に影響を受ける可能性があります。 -プラグインは `config` フックを使ってデフォルト値を設定してください。各環境を設定するには、新しい `configEnvironment` フックを使用します。このフックは、最終的なデフォルト値の解決を含む、部分的に解決された設定を持つ各環境に対して呼び出されます。 - -```ts - configEnvironment(name: string, options: EnvironmentOptions) { - if (name === 'rsc') { - options.resolve.conditions = // ... -``` - -### `hotUpdate` フック {#the-hotupdate-hook} - -- **型:** `(this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array | void | Promise | void>` -- **参照:** [HMR API](./api-hmr) - -`hotUpdate` フックを使用すると、プラグインが指定された環境に対してカスタム HMR 更新処理を実行できるようになります。ファイルが変更されると、HMR アルゴリズムは `server.environments` の順番に従って各環境で順に実行されるので、`hotUpdate` フックは複数回呼び出されることになります。このフックは以下のシグネチャを持つコンテキストオブジェクトを受け取ります: - -```ts -interface HotUpdateContext { - type: 'create' | 'update' | 'delete' - file: string - timestamp: number - modules: Array - read: () => string | Promise - server: ViteDevServer -} -``` - -- `this.environment` は現在ファイルの更新が処理されているモジュール実行環境です。 - -- `modules` は、変更されたファイルの影響を受ける、この環境のモジュールの配列です。1 つのファイルが複数のモジュール(Vue SFC など)にマッピングされる可能性があるため、配列になっています。 - -- `read` はファイルの内容を返す非同期の読み込み関数です。システムによっては、エディターがファイルの更新を終了する前にファイル変更コールバックが高速に実行され、`fs.readFile` が空の内容を返すことがあるためです。渡された読み込み関数はこの動作を正常化します。 - -フックは以下を選択できます: - -- HMR がより正確になるように、影響を受けるモジュールリストをフィルタリングして絞り込む。 - -- 空の配列を返し、フルリロードを実行する: - - ```js - hotUpdate({ modules, timestamp }) { - if (this.environment.name !== 'client') - return - - // モジュールを手動で無効化 - const invalidatedModules = new Set() - for (const mod of modules) { - this.environment.moduleGraph.invalidateModule( - mod, - invalidatedModules, - timestamp, - true - ) - } - this.environment.hot.send({ type: 'full-reload' }) - return [] - } - ``` - -- 空の配列を返し、カスタムイベントをクライアントに送信することで、完全なカスタム HMR 処理を行う: - - ```js - hotUpdate() { - if (this.environment.name !== 'client') - return - - this.environment.hot.send({ - type: 'custom', - event: 'special-update', - data: {} - }) - return [] - } - ``` - - クライアントコードは [HMR API](./api-hmr) を使って対応するハンドラーを登録する必要があります(これは同じプラグインの `transform` フックによって注入できます): - - ```js - if (import.meta.hot) { - import.meta.hot.on('special-update', (data) => { - // カスタム更新を実行する - }) - } - ``` - -### 環境ごとのプラグイン {#per-environment-plugins} - -プラグインは `applyToEnvironment` 関数で、適用する環境を定義できます。 - -```js -const UnoCssPlugin = () => { - // 共有グローバル状態 - return { - buildStart() { - // WeakMap, this.environment を使って環境ごとの状態を初期化 - }, - configureServer() { - // グローバルフックを通常どおり使用 - }, - applyToEnvironment(environment) { - // このプラグインがこの環境でアクティブになる必要がある場合は true を返します - // この関数が提供されていない場合、プラグインはすべての環境でアクティブになります - }, - resolveId(id, importer) { - // このプラグインが適用される環境に対してのみ呼び出されます - }, - } -} -``` - -## `ModuleRunner` - -モジュールランナーはターゲットランタイムでインスタンス化されます。次のセクションの全ての API は、特に断りのない限り `vite/module-runner` からインポートされます。このエクスポート・エントリーポイントは可能な限り軽量に保たれており、モジュールランナーを作成するために必要な最小限のものだけがエクスポートされます。 - -**型シグネチャー:** - -```ts -export class ModuleRunner { - constructor( - public options: ModuleRunnerOptions, - public evaluator: ModuleEvaluator, - private debug?: ModuleRunnerDebugger, - ) {} - /** - * 実行するURL。ルートからの相対的なファイルパス、サーバーパス、ID を受け付けます。 - */ - public async import(url: string): Promise - /** - * HMR リスナーを含むすべてのキャッシュをクリアします。 - */ - public clearCache(): void - /** - * すべてのキャッシュをクリアし、すべての HMR リスナーを削除し、ソースマップのサポートをリセットします。 - * このメソッドは HMR 接続を停止しません。 - */ - public async close(): Promise - /** - * `close()` メソッドを呼び出してランナーを終了した場合は `true` を返します。 - */ - 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, fetchModule } from './rpc-implementation.js' - -const moduleRunner = new ModuleRunner( - { - root, - fetchModule, - // HMR をサポートするために hmr.connection を提供することもできます - }, - new ESModulesEvaluator(), -) - -await moduleRunner.import('/src/entry-point.js') -``` - -## `ModuleRunnerOptions` - -```ts -export interface ModuleRunnerOptions { - /** - * プロジェクトのルート - */ - root: string - /** - * サーバーと通信するための一連のメソッド。 - */ - transport: RunnerTransport - /** - * ソースマップの解決方法を設定します。`process.setSourceMapsEnabled` が使用可能な場合は `node` を優先します。 - * それ以外の場合は、デフォルトで `prepareStackTrace` を使用し、`Error.prepareStackTrace` メソッドをオーバーライドします。 - * Vite によって処理されなかったファイルのファイル内容とソースマップの解決方法を設定するオブジェクトを提供できます。 - */ - sourcemapInterceptor?: - | false - | 'node' - | 'prepareStackTrace' - | InterceptorOptions - /** - * HMR を無効にするか、HMR オプションを設定します。 - */ - hmr?: - | false - | { - /** - * HMR がクライアントとサーバー間で通信する方法を設定します。 - */ - connection: ModuleRunnerHMRConnection - /** - * HMR ロガーを設定します。 - */ - logger?: false | HMRLogger - } - /** - * カスタムモジュールキャッシュ。指定されていない場合は、モジュールランナーインスタンスごとに個別のモジュールキャッシュが作成されます。 - */ - evaluatedModules?: EvaluatedModules -} -``` - -## `ModuleEvaluator` - -**型シグネチャー:** - -```ts -export interface ModuleEvaluator { - /** - * 変換後のコードに含まれるプレフィックスの行数。 - */ - startOffset?: number - /** - * Vite によって変換されたコードを評価します。 - * @param context 関数コンテキスト - * @param code 変換されたコード - * @param id モジュールを取得するために使用された ID - */ - runInlinedModule( - context: ModuleRunnerContext, - code: string, - id: string, - ): Promise - /** - * 外部化されたモジュールを評価します。 - * @param file 外部モジュールへのファイル URL - */ - runExternalModule(file: string): Promise -} -``` - -Vite はデフォルトでこのインターフェイスを実装した `ESModulesEvaluator` をエクスポートします。コードの評価には `new AsyncFunction` を使用するので、インライン化されたソースマップがある場合は、新しい行が追加されたことを考慮して [2 行分のオフセット](https://tc39.es/ecma262/#sec-createdynamicfunction)を追加する必要があります。これは `ESModulesEvaluator` によって自動的に実行されます。カスタムの Evaluator は行を追加しません。 - -## RunnerTransport - -**型シグネチャー:** - -```ts -interface RunnerTransport { - /** - * モジュールに関する情報を取得するメソッド。 - */ - fetchModule: FetchFunction -} -``` - -RPC 経由または関数を直接呼び出して環境と通信するトランスポートオブジェクト。デフォルトでは、`fetchModule` メソッドでオブジェクトを渡す必要があります。このメソッド内ではどのようなタイプの RPC も使用できますが、Vite では設定を簡単にするために `RemoteRunnerTransport` クラスを使用して双方向のトランスポートインターフェースを公開しています。モジュールランナーがワーカースレッドで作成される次の例のように、サーバー上の `RemoteEnvironmentTransport` インスタンスと合わせる必要があります: - -::: code-group - -```ts [worker.js] -import { parentPort } from 'node:worker_threads' -import { fileURLToPath } from 'node:url' -import { - ESModulesEvaluator, - ModuleRunner, - RemoteRunnerTransport, -} from 'vite/module-runner' - -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, - }), - }, - new ESModulesEvaluator(), -) -``` - -```ts [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), - }), - }, - }) -} - -await createServer({ - environments: { - worker: { - dev: { - createEnvironment: createWorkerEnvironment, - }, - }, - }, -}) -``` - -::: - -`RemoteRunnerTransport` と `RemoteEnvironmentTransport` は一緒に使うことを想定していますが、必ずしも使う必要はありません。独自の関数を定義して、ランナーとサーバー間の通信を行えます。例えば、HTTP リクエストで環境に接続する場合、`fetchModule` 関数で `fetch().json()` を呼び出せます: - -```ts -import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' - -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}`, - ) - return response.json() - }, - }, - }, - new ESModulesEvaluator(), -) - -await runner.import('/entry.js') -``` - -::: warning サーバー上のモジュールへのアクセス -私たちはサーバーとランナー間の通信を推奨するつもりはありません。`vite.ssrLoadModule` で明らかになった問題の 1 つは、処理されたモジュールの内部でサーバーの状態に依存しすぎていることです。これにより、ユーザー環境がサーバー API にアクセスできない可能性があるため、ランタイムに依存しない SSR を実装することを難しくします。例えば、次のコードは Vite サーバーとユーザーコードが同じコンテキストで実行できることを想定しています: - -```ts -const vite = createServer() -const routes = collectRoutes() - -const { processRoutes } = await vite.ssrLoadModule('internal:routes-processor') -processRoutes(routes) -``` - -これでは、サーバーの状態とユーザーの状態が連動しているため、ユーザーコードをプロダクション(たとえばエッジなど)と同じように実行できません。そのため、代わりに仮想モジュールを使って状態をインポートし、ユーザーモジュール内で処理することを推奨します: - -```ts -// このコードは別のマシンまたは別のスレッドで実行されます - -import { runner } from './ssr-module-runner.js' -import { processRoutes } from './routes-processor.js' - -const { routes } = await runner.import('virtual:ssr-routes') -processRoutes(routes) -``` - -[SSR ガイド](/guide/ssr)にあるようなシンプルなセットアップで、プロダクション環境でサーバーが別のプロセスで実行されることが想定されていない場合なら、`server.transformIndexHtml` を直接使用できます。しかし、サーバーがエッジ環境や別のプロセスで実行される場合は、HTML をロードする仮想モジュールを作成することをお勧めします: - -```ts {13-21} -function vitePluginVirtualIndexHtml(): Plugin { - let server: ViteDevServer | undefined - return { - name: vitePluginVirtualIndexHtml.name, - configureServer(server_) { - server = server_ - }, - resolveId(source) { - return source === 'virtual:index-html' ? '\0' + source : undefined - }, - async load(id) { - if (id === '\0' + 'virtual:index-html') { - let html: string - if (server) { - this.addWatchFile('index.html') - html = await fs.promises.readFile('index.html', 'utf-8') - html = await server.transformIndexHtml('/', html) - } else { - html = await fs.promises.readFile('dist/client/index.html', 'utf-8') - } - return `export default ${JSON.stringify(html)}` - } - return - }, - } -} -``` - -そして SSR のエントリーポイントで `import('virtual:index-html')` を呼び出すと、処理された HTML を取り出すことができます: - -```ts -import { render } from 'framework' - -// この例では、cloudflare 構文を使用します -export default { - async fetch() { - // 開発時は、変換された HTML を返します - // ビルド時は、基本的な index.html を文字列にバンドルします - const { default: html } = await import('virtual:index-html') - return new Response(render(html), { - headers: { 'content-type': 'text/html' }, - }) - }, -} -``` - -これにより、HTML 処理はサーバーに依存しなくなります。 - -::: - -## ModuleRunnerHMRConnection - -**型シグネチャー:** - -```ts -export interface ModuleRunnerHMRConnection { - /** - * サーバーにメッセージを送信する前にチェックされます。 - */ - isReady(): boolean - /** - * サーバーにメッセージを送信します。 - */ - send(payload: HotPayload): void - /** - * この接続が更新をトリガーしたときに HMR がどのように処理されるかを設定します。 - * このメソッドは、接続が HMR 更新のリッスンを開始し、受信時にこのコールバックを呼び出すことを想定しています。 - */ - onUpdate(callback: (payload: HotPayload) => void): void -} -``` - -このインターフェイスは HMR 通信の確立方法を定義します。Vite の SSR 中に HMR をサポートするために、Vite は `ServerHMRConnector` をメインエントリーからエクスポートします。`isReady` と `send` メソッドは通常、カスタムイベントがトリガーされたときに呼び出されます(`import.meta.hot.send("my-event")` のように)。 - -`onUpdate` は、新しいモジュールランナーが初期化されたときに一度だけ呼ばれます。接続が HMR イベントをトリガーしたときに呼び出されるメソッドを渡します。実装は接続の種類(例として、`WebSocket`/`EventEmitter`/`MessageChannel`)に依存しますが、通常は以下のようになります: - -```js -function onUpdate(callback) { - this.connection.on('hmr', (event) => callback(event.data)) -} -``` - -コールバックはキューに入れられ、次の更新を処理する前に現在の更新が解決されるのを待ちます。ブラウザーの実装とは異なり、モジュールランナーにおける HMR の更新は、モジュールを更新する前に、すべてのリスナー(`vite:beforeUpdate`/`vite:beforeFullReload` など)が終了するまで待機します。 - -## ビルド中の環境 {#environments-during-build} - -CLI において、`vite build` と `vite build --ssr` を呼び出すと、後方互換性のためにクライアントのみの環境と ssr のみの環境がビルドされます。 - -`builder.entireApp` が `true` の場合(または `vite build --app` を呼び出した場合)、`vite build` はアプリ全体のビルドを行います。これは将来のメジャーバージョンではデフォルトになる予定です。`ViteBuilder` インスタンス(ビルド時の `ViteDevServer` に相当)が作成され、プロダクション環境用に設定されたすべての環境がビルドされます。デフォルトでは、環境のビルドは `environments` レコードの順番に従って直列に実行されます。フレームワークやユーザーは環境を構築する方法を設定できます: - -```js -export default { - builder: { - buildApp: async (builder) => { - const environments = Object.values(builder.environments) - return Promise.all( - environments.map((environment) => builder.build(environment)), - ) - }, }, } ``` -### ビルドフックの環境 {#environment-in-build-hooks} - -開発時と同じように、プラグインフックもビルド時に環境インスタンスを受け取り、`ssr` ブール値を置き換えます。 -これは `renderChunk` や `generateBundle` などのビルド専用のフックでも動作します。 - -### ビルド時の共有プラグイン {#shared-plugins-during-build} - -Vite 6 以前は、プラグインパイプラインは開発時とビルド時に異なる方法で動作していました: - -- **開発時:** プラグインは共有されます -- **ビルド時:** プラグインは環境ごとに分離されます(`vite build` と `vite build --ssr` という別々のプロセスで分離されます)。 - -このため、フレームワークはファイルシステムに書き込まれたマニフェストファイルを通して `client` ビルドと `ssr` ビルドの間で状態を共有することを余儀なくされていました。Vite 6 では、すべての環境を単一のプロセスでビルドするようになったので、プラグインのパイプラインと環境間通信の方法を開発時と合わせることができるようになりました。 - -将来のメジャー(Vite 7 または 8)では、完全な整合性を実現することを目指しています: - -- **開発時とビルド時:** プラグインは[環境ごとのフィルタリング](#per-environment-plugins)で共有されます - -また、ビルド時に共有される `ResolvedConfig` インスタンスは 1 つになり、開発時に `WeakMap` を使っていたのと同じように、アプリのビルドプロセスレベル全体でキャッシュが可能になります。 - -Vite 6 では、後方互換性を保つために小さなステップを行う必要があります。エコシステムのプラグインは現在、設定へアクセスするために `environment.config.build` ではなく `config.build` を使用しているため、デフォルトでは環境ごとに新しい `ResolvedConfig` を作成する必要があります。プロジェクトは `builder.sharedConfigBuild` を `true` に設定することで、完全な設定とプラグインパイプラインを共有できます。 - -このオプションは、最初のうちは小さなプロジェクトのサブセットでしか機能しないため、プラグインの作者は `sharedDuringBuild` フラグを `true` に設定することで、特定のプラグインを共有するように選択できます。これにより、通常のプラグインでも簡単に状態を共有できるようになります: - -```js -function myPlugin() { - // 開発環境とビルド環境のすべての環境で状態を共有する - const sharedState = ... - return { - name: 'shared-plugin', - transform(code, id) { ... }, - - // すべての環境で単一のインスタンスにオプトインする - sharedDuringBuild: true, - } -} -``` - ## 後方互換性 {#backward-compatibility} 現在の Vite サーバーAPI はまだ非推奨ではなく、Vite 5 との後方互換性があります。新しい Environment API は実験的なものです。 @@ -1022,3 +121,13 @@ function myPlugin() { - [環境ごとの API への移行](/changes/per-environment-apis) - [`ModuleRunner` API を使った SSR](/changes/ssr-using-modulerunner) - [ビルド時の共有プラグイン](/changes/shared-plugins-during-build) + +## 対象ユーザー {#target-users} + +このガイドでは、エンドユーザー向けの環境に関する基本的な概念を説明します。 + +プラグイン作者は、現在の環境構成とやり取りするために、より一貫性のある API を利用できます。Vite をベースに構築している場合は、[Environment API プラグインガイド](./api-environment-plugins.md)で、拡張プラグイン API を使用して複数のカスタム環境をサポートする方法について説明します。 + +フレームワークは、さまざまなレベルで環境を公開することを決定できます。フレームワーク作者の場合は、[Environment API フレームワークガイド](./api-environment-frameworks)を読み進め、Environment API のプログラム的な側面について学習してください。 + +ランタイムプロバイダーの場合、[Environment API ランタイムガイド](./api-environment-runtimes.md)で、フレームワークとユーザーが使用するカスタム環境を提供する方法について説明します。