Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"ignorePaths": ["**/CHANGELOG.md"],
"words": [
//
"kamado"
"kamado",
"unstub"
]
}
60 changes: 57 additions & 3 deletions packages/kamado/ARCHITECTURE.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export interface Context<M extends MetaData> extends Config<M> {

- **`cli.ts`**: CLI のエントリポイント。`@d-zero/roar` を使用してコマンドを処理します。
- **`builder/`**: 静的ビルド(`kamado build`)の実行ロジック。
- **`server/`**: 開発サーバー(`kamado server`)のロジック。Hono を使用。
- **`server/`**: 開発サーバー(`kamado server`)のロジック。Hono を使用。プロキシ転送(`proxy.ts`)、レスポンス変換(`transform.ts`)、ルートハンドリング(`route.ts`)を含む。
- **`compiler/`**: コンパイラ・プラグインのインターフェースと、機能マップの管理。
- **`config/`**: 設定ファイルのロードとマージ、デフォルト値の定義、`defineConfig()`ヘルパーの提供。
- **`data/`**: コンパイル対象ファイルのリストアップ、アセットグループの管理。
Expand Down Expand Up @@ -198,9 +198,15 @@ graph TD
A[CLI: server] --> B[config のロード]
B --> B2[Context の作成 mode='serve']
B2 --> C[compilableFileMap & コンパイラの作成]
C --> C2[Hono サーバーの起動]
C --> C1{proxy が設定<br>されているか?}
C1 -- Yes --> C1a[プロキシルートを登録]
C1a --> C2[Hono サーバーの起動]
C1 -- No --> C2
C2 --> D[ブラウザからのリクエスト受領]
D --> E[URL からローカルパスを計算]
D --> D1{プロキシのパス<br>プレフィックスに一致?}
D1 -- Yes --> D2[ターゲットサーバーへ転送]
D2 --> D3[プロキシレスポンスを返却]
D1 -- No --> E[URL からローカルパスを計算]
E --> F{compilableFileMap に<br>存在するか?}
F -- Yes --> H[オンメモリでコンパイル実行]
H --> I[レスポンス変換を適用]
Expand Down Expand Up @@ -497,6 +503,54 @@ export interface TransformContext<M extends MetaData> {

**注意**: このAPIは意図的に開発専用です。本番用の変換には、page compilerのTransform Pipeline(`transforms`オプションに`manipulateDOM()`、`characterEntities()`、`prettier()`などのtransform factoryを設定)またはビルド時処理を使用してください。

### プロキシAPI

プロキシAPIは、設定されたパスプレフィックスに一致するリクエストを外部サーバーへ転送します。`src/server/proxy.ts`に実装され、`src/server/app.ts`でHonoアプリに統合されています。

#### アーキテクチャ

```typescript
// プロキシルール設定
export interface ProxyRule {
readonly target: string; // プロキシ先のターゲットURL
readonly pathRewrite?: (path: string) => string | Promise<string>; // プロキシ前にパスを書き換え
readonly changeOrigin?: boolean; // Origin/Hostヘッダーを変更(デフォルト: false)
}

// 設定: Record<pathPrefix, ProxyRule | string>
// 例: { '/api': 'https://backend.example.com' }
```

#### 実行フロー

1. **ルート登録**: `setProxyRoutes()`は`app.ts`内で`setRoute()`**より前に**呼ばれるため、プロキシルートがファイルサーブルートよりも優先される
2. **パスソート**: エントリはパスプレフィックスの長さでソートされ(長い順)、特定のルートが一般的なルートよりも先にマッチするようになっている
3. **ルール正規化**: 文字列省略形の値は`normalizeRule()`により`ProxyRule`オブジェクトに正規化される
4. **リクエスト転送**: ネイティブ`fetch()`を使用し、ヘッダーを手動管理。リクエストヘッダーは転送され、`changeOrigin: true`の場合は`Host`/`Origin`がオプションで書き換えられる
5. **ボディ処理**: リクエストボディはボディを持つメソッド(POST、PUT、PATCH、DELETE)でストリーミングされる。GETとHEADリクエストにはボディがない
6. **エラーハンドリング**: プロキシ失敗時は`502 Bad Gateway`レスポンスが返され、エラーはコンソールにログ出力される

#### 実装の詳細

**場所**: `src/server/proxy.ts`

主要な関数:

- `setProxyRoutes(app, proxyConfig)`: Honoアプリにプロキシルートを登録
- `normalizeRule(rule)`: 文字列省略形を`ProxyRule`オブジェクトに変換
- `hasBody(method)`: HTTPメソッドがリクエストボディを持つかどうかを判定

**統合**: `src/server/app.ts`

プロキシルートは条件付きで登録される — `context.devServer.proxy`が定義されている場合のみ。`${pathPrefix}/*`と`${pathPrefix}`の両パターンが登録され、ネストされたリクエストと完全一致リクエストの両方を処理する。

#### 設計上の判断

- **ネイティブ`fetch()`**: HTTPプロキシライブラリではなくランタイム組み込みの`fetch()`を使用し、依存関係を最小限に抑えている
- **`redirect: 'manual'`**: ターゲットサーバーからのリダイレクトレスポンスを自動追従せず、そのまま保持する
- **`duplex: 'half'`**: Node.jsの`fetch()`実装でストリーミングリクエストボディを有効にする
- **レスポンス変換なし**: プロキシレスポンスはレスポンス変換パイプラインを通さず、そのまま返却される

---

## 主要な依存ライブラリ
Expand Down
60 changes: 57 additions & 3 deletions packages/kamado/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Key directories under `packages/kamado/src` and their roles:

- **`cli.ts`**: CLI entry point. Processes commands using `@d-zero/roar`.
- **`builder/`**: Execution logic for static builds (`kamado build`).
- **`server/`**: Logic for the development server (`kamado server`) using Hono.
- **`server/`**: Logic for the development server (`kamado server`) using Hono. Includes proxy forwarding (`proxy.ts`), response transforms (`transform.ts`), and route handling (`route.ts`).
- **`compiler/`**: Management of compiler plugin interfaces and the function map.
- **`config/`**: Loading and merging configuration files, defining default values, and providing the `defineConfig()` helper.
- **`data/`**: Listing files for compilation and managing asset groups.
Expand Down Expand Up @@ -198,9 +198,15 @@ graph TD
A[CLI: server] --> B[Load config]
B --> B2[Create Context with mode='serve']
B2 --> C[Create compilableFileMap & compiler]
C --> C2[Start Hono server]
C --> C1{proxy configured?}
C1 -- Yes --> C1a[Register proxy routes]
C1a --> C2[Start Hono server]
C1 -- No --> C2
C2 --> D[Receive browser request]
D --> E[Calculate local path from URL]
D --> D1{Matches proxy<br>path prefix?}
D1 -- Yes --> D2[Forward to target server]
D2 --> D3[Return proxy response]
D1 -- No --> E[Calculate local path from URL]
E --> F{Exists in<br>compilableFileMap?}
F -- Yes --> H[Perform in-memory compilation]
H --> I[Apply Response Transforms]
Expand Down Expand Up @@ -497,6 +503,54 @@ A helper function `respondWithTransform()` consolidates the transform applicatio

**Note**: This API is intentionally development-only. For production transformations, use the page compiler's Transform Pipeline (configure `transforms` option with transform factories like `manipulateDOM()`, `characterEntities()`, `prettier()`, etc.) or build-time processing.

### Proxy API

The Proxy API forwards requests matching configured path prefixes to external servers during development. It is implemented in `src/server/proxy.ts` and integrated into the Hono app in `src/server/app.ts`.

#### Architecture

```typescript
// Proxy rule configuration
export interface ProxyRule {
readonly target: string; // Target URL to proxy to
readonly pathRewrite?: (path: string) => string | Promise<string>; // Rewrite path before proxying
readonly changeOrigin?: boolean; // Change Origin/Host headers (default: false)
}

// Configuration: Record<pathPrefix, ProxyRule | string>
// e.g., { '/api': 'https://backend.example.com' }
```

#### Execution Flow

1. **Route Registration**: `setProxyRoutes()` is called **before** `setRoute()` in `app.ts`, so proxy routes take priority over file-serving routes
2. **Path Sorting**: Entries are sorted by path prefix length (longest first) to ensure specific routes match before general ones
3. **Rule Normalization**: String shorthand values are normalized into `ProxyRule` objects via `normalizeRule()`
4. **Request Forwarding**: Uses native `fetch()` with manual header management. Request headers are forwarded; `Host`/`Origin` are optionally rewritten when `changeOrigin: true`
5. **Body Handling**: Request bodies are streamed for methods that carry a body (POST, PUT, PATCH, DELETE). GET and HEAD requests have no body
6. **Error Handling**: On proxy failure, a `502 Bad Gateway` response is returned and the error is logged to the console

#### Implementation Details

**Location**: `src/server/proxy.ts`

Key functions:

- `setProxyRoutes(app, proxyConfig)`: Registers proxy routes on the Hono app
- `normalizeRule(rule)`: Converts string shorthand to `ProxyRule` object
- `hasBody(method)`: Determines if an HTTP method carries a request body

**Integration**: `src/server/app.ts`

Proxy routes are registered conditionally — only when `context.devServer.proxy` is defined. Both `${pathPrefix}/*` and `${pathPrefix}` patterns are registered to handle nested and exact-match requests.

#### Design Decisions

- **Native `fetch()`**: Uses the runtime's built-in `fetch()` rather than an HTTP proxy library, keeping the dependency footprint minimal
- **`redirect: 'manual'`**: Preserves redirect responses from the target server instead of following them automatically
- **`duplex: 'half'`**: Enables streaming request bodies in Node.js `fetch()` implementation
- **No response transforms**: Proxy responses are returned as-is without passing through the Response Transform pipeline

---

## Main Dependencies
Expand Down
68 changes: 68 additions & 0 deletions packages/kamado/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default defineConfig({
- `devServer.open`: 起動時にブラウザを自動で開くか(デフォルト: `false`)
- `devServer.startPath`: サーバー起動時にブラウザで開くカスタムパス(オプション、例: `'__tmpl/'`)
- `devServer.transforms`: 開発時にレスポンスを変換する関数の配列(オプション、[レスポンス変換API](#レスポンス変換api)を参照)
- `devServer.proxy`: 開発時に外部サーバーへリクエストを転送するプロキシルール(オプション、[プロキシAPI](#プロキシapi)を参照)

#### コンパイラ設定

Expand Down Expand Up @@ -426,6 +427,73 @@ interface TransformContext<M extends MetaData> {
- 変換は配列の順序で実行されます
- 開発サーバーモード(`kamado server`)でのみ適用され、ビルド時には適用されません

#### プロキシAPI

プロキシAPIを使用すると、開発サーバーモード時にリクエストを外部サーバーに転送できます。静的サイトから別ドメインのAPIにAJAXリクエストを送る場合に、ローカル開発時のCORS問題を回避できます。

**主な特徴:**

- **開発時のみ**: プロキシは`serve`モードでのみ適用され、ビルド時には適用されません
- **全HTTPメソッド対応**: GET、POST、PUT、DELETE、PATCHなどすべてのメソッドをサポート
- **ストリーミング**: レスポンスはバッファリングせずにストリーミングされます
- **パスリライト**: リクエストパスを転送前に書き換え可能
- **簡易・詳細形式**: シンプルな場合は文字列、詳細な制御が必要な場合はオブジェクトを使用

**設定例:**

```typescript
import { defineConfig } from 'kamado/config';

export default defineConfig({
devServer: {
port: 3000,
proxy: {
// 簡易形式: 文字列でターゲットURLを指定 — /api/* をターゲットに転送
'/api': 'https://backend.example.com',

// 詳細形式: パスリライト付きのオブジェクト形式
'/api/v2': {
target: 'https://api-v2.example.com',
// /api/v2/users → /users にリライト
pathRewrite: (path) => path.replace(/^\/api\/v2/, ''),
changeOrigin: true,
},
},
},
});
```

上記の設定では:

- `GET /api/data` → `GET https://backend.example.com/api/data`
- `POST /api/v2/users` → `POST https://api-v2.example.com/users`(パスがリライトされます)

**ProxyRuleインターフェース:**

```typescript
interface ProxyRule {
target: string; // プロキシ先のターゲットURL
pathRewrite?: (path: string) => string | Promise<string>; // プロキシ前にパスを書き換える関数
changeOrigin?: boolean; // Origin/Hostヘッダーをターゲットに合わせて変更するか(デフォルト: false)
}
```

**プロキシ設定:**

`proxy`オプションはレコード型で、以下の形式です:

- **キー**: マッチするパスプレフィックス(例: `'/api'`)
- **値**: ターゲットURL文字列(簡易形式)または`ProxyRule`オブジェクト

**重要な注意事項:**

- プロキシルートはファイルサーブルートよりも先にマッチするため、プロキシパスはローカルファイルよりも優先されます
- 長いパスプレフィックスが先にマッチします(例: `/api/v2` は `/api` よりも優先)
- クエリ文字列は保持されターゲットに転送されます
- リクエストヘッダーは転送されます。ターゲットサーバーが `Host` ヘッダーを検証している場合は `changeOrigin: true` を設定すると `Host` と `Origin` ヘッダーがターゲットに合わせて書き換えられます
- プロキシ失敗時は`502 Bad Gateway`レスポンスが返されます
- 開発サーバーモード(`kamado server`)でのみ適用され、ビルド時には適用されません

### CLIコマンド

#### サイト全体のビルド
Expand Down
68 changes: 68 additions & 0 deletions packages/kamado/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default defineConfig({
- `devServer.open`: Whether to automatically open the browser on startup (default: `false`)
- `devServer.startPath`: Custom path to open in the browser when starting the server (optional, e.g., `'__tmpl/'`)
- `devServer.transforms`: Array of response transformation functions that modify responses during development (optional, see [Response Transform API](#response-transform-api))
- `devServer.proxy`: Proxy rules for forwarding requests to external servers during development (optional, see [Proxy API](#proxy-api))

#### Compiler Settings

Expand Down Expand Up @@ -426,6 +427,73 @@ interface TransformContext<M extends MetaData> {
- Transforms are executed in array order
- Only applied in development server mode (`kamado server`), not during builds

#### Proxy API

The Proxy API allows you to forward requests to external servers during development. This is useful when your static site makes AJAX requests to APIs on different domains, avoiding CORS issues during local development.

**Key Features:**

- **Development-only**: Proxy only applies in `serve` mode, not during builds
- **All HTTP methods**: Supports GET, POST, PUT, DELETE, PATCH, and other methods
- **Streaming**: Responses are streamed without buffering
- **Path rewriting**: Optionally rewrite request paths before forwarding
- **Simple and advanced forms**: Use a string shorthand for simple cases or an object for full control

**Configuration:**

```typescript
import { defineConfig } from 'kamado/config';

export default defineConfig({
devServer: {
port: 3000,
proxy: {
// Simple: string shorthand — forward /api/* to the target
'/api': 'https://backend.example.com',

// Advanced: object form with path rewriting
'/api/v2': {
target: 'https://api-v2.example.com',
// Rewrite /api/v2/users → /users
pathRewrite: (path) => path.replace(/^\/api\/v2/, ''),
changeOrigin: true,
},
},
},
});
```

With the configuration above:

- `GET /api/data` → `GET https://backend.example.com/api/data`
- `POST /api/v2/users` → `POST https://api-v2.example.com/users` (path rewritten)

**ProxyRule Interface:**

```typescript
interface ProxyRule {
target: string; // Target URL to proxy to
pathRewrite?: (path: string) => string | Promise<string>; // Rewrite path before proxying
changeOrigin?: boolean; // Change Origin/Host headers to match target (default: false)
}
```

**Proxy Configuration:**

The `proxy` option is a record where:

- **Key**: Path prefix to match (e.g., `'/api'`)
- **Value**: A target URL string (shorthand) or a `ProxyRule` object

**Important Notes:**

- Proxy routes are matched before file-serving routes, so proxy paths take priority over local files
- Longer path prefixes are matched first (e.g., `/api/v2` takes priority over `/api`)
- Query strings are preserved and forwarded to the target
- Request headers are forwarded. Set `changeOrigin: true` to rewrite `Host` and `Origin` headers to match the target (useful when the target server validates the `Host` header)
- On proxy failure, a `502 Bad Gateway` response is returned
- Only applied in development server mode (`kamado server`), not during builds

### CLI Commands

#### Build Entire Site
Expand Down
1 change: 1 addition & 0 deletions packages/kamado/src/config/merge-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function mergeConfig<M extends MetaData>(
host: 'localhost',
startPath: undefined,
transforms: [],
proxy: undefined,
...config.devServer,
},
pageList: config.pageList,
Expand Down
Loading