@k8ordo/server

実行と配信

vite build は、リクエストハンドラと、ブラウザに配るファイルを書きます。serve() はそれを Node.js で動かし、ほかのホストはハンドラを直接呼べます。このページは、出力の形、serve() の答え方、ほかのホストでの動かし方を説明します。

ビルドの出力

dist/rsc/index.js がリクエストハンドラ、dist/ssr/ はハンドラがペイロードを HTML にするのに使う部分、dist/client/ がブラウザに配るファイルです。このモードではページを前もって描かないので、dist/client/ にページの HTML はありません。

dist/
  rsc/
    index.js
  ssr/
  client/
    assets/

ハンドラは、アプリの依存を実行時に node_modules から import します。サーバーを動かす場所には、アプリの依存をインストールしておきます。vite はビルドにしか使わないので、pnpm install --prod のように開発時の依存を省いたインストールで動きます。

serve()

serve() はビルドを Node.js の HTTP サーバーで動かします。待ち受けを始めてから、どこで待ち受けているかと止め方を返し、k8ordo: serving <dist> on <url> をログに出します。オプションの型は ServeOptions、返り値の型は Server です。

// serve.js
import { serve } from '@k8ordo/server/runtime';

await serve({ port: 3000, host: '0.0.0.0' });
オプション既定値意味
dist'dist'ビルドの出力ディレクトリ(rsc/client/ を持つもの)。現在の作業ディレクトリから解決します。
port3000待ち受けるポート。0 ならシステムが空いているポートを選び、返り値の port がどれかを教えます。
host'localhost'待ち受けるホスト。既定では同じマシンからしか届かないので、コンテナの中などで外から届かせるには 0.0.0.0 を渡します。
返り値(Server意味
port実際に待ち受けているポート
urlhttp://<host>:<port>
close()サーバーを止め、止まったら解決する Promise を返す

port: 0close() があるので、テストはビルドに対して自分のサーバーを立て、終わったら止められます。

// scripts/smoke.js
import { serve } from '@k8ordo/server/runtime';

const server = await serve({ dist: 'dist', port: 0 });
const response = await fetch(`${server.url}/products/1`);
console.log(response.status);
await server.close();

serve() の答え方

GET と HEAD のうち、dist/client/ の中のファイルを指すものには、そのファイルをそのまま返します。/assets/ の下はファイル名に内容のハッシュが入っているので cache-control: public, max-age=31536000, immutable、それ以外は no-cache です。

それ以外はすべてハンドラに渡ります。POST は、ファイルと同じパスでもかならずハンドラが答えます。ハンドラのステータスとヘッダーはそのまま返り、複数の Set-Cookie も 1 つにまとめられません。ハンドラが例外を投げたときは 500 と本文 internal error だけを返し、中身は訪問者ではなくサーバーのログに出します。

リクエストの pathname は、どう綴られていても dist/client/ の中のファイルしか指せません。..%2e%2e%2f を使った綴りは中に留まり、復号できないエスケープや NUL バイトはファイル名として扱われません。外へ出ることは、リクエストごとに判定する場合ではなく、パスの解決が作り出せない結果です。

ハンドラが返すステータスの一覧は、リンク先にあります。 エラーとリダイレクト

ほかのホストで動かす

ビルドされたハンドラは、dist/rsc/index.js が default export する (request: Request) => Promise<Response> という、ただの関数です。Request を渡して Response を受け取れる環境なら、どこでも動かせます。@k8ordo/static がビルド時に作って呼ぶのも、同じハンドラをそのモード向けにコンパイルしたものです。

// render.js
import handler from './dist/rsc/index.js';

const response = await handler(
  new Request('https://example.com/products/1'),
);
console.log(response.status, response.headers.get('content-type'));

ハンドラはファイルを配りません。ほかのホストでは dist/client/ をホストの側で配信し、それ以外のリクエストをハンドラに渡します。

Request は、訪問者が求めた URL で作ります。ハンドラは、POST の Origin ヘッダーがあり、そのホストがその URL のホストと一致するときだけ受け付け、それ以外の POST には 403 で答えるからです。プロキシの後ろでは、公開されているホストをそのまま渡します。serve() はリクエスト自身の Host ヘッダーから URL を作り、転送用のヘッダーは読まないので、前に置くプロキシは元の Host を書き換えずに渡します。

routesDir

framework() が受け取るオプションは routesDir だけです(型は ServerOptions)。ルートのディレクトリをプロジェクトのルートからの相対パスで指定し、既定は src/routes です。生成されるファイルは変わらず .k8ordo/ に書かれ、問題の行も routes/ から始まります。

// vite.config.ts
import { framework } from '@k8ordo/server';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [framework({ routesDir: 'app/routes' })],
});