@aiquants/duckdb-helper 1.5.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +164 -5
- package/dist/index.d.ts +164 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/docs/specs/2026.04.11 [AI] 00-overview.md +32 -11
- package/docs/specs/2026.04.11 [AI] 02-service.md +114 -240
- package/docs/specs/2026.04.11 [AI] 03-hooks.md +10 -21
- package/docs/specs/2026.04.11 [AI] 05-logger.md +3 -5
- package/docs/specs/2026.04.11 [AI] 06-build-config.md +22 -4
- package/docs/specs/2026.07.05 [AI] 07-testing.md +184 -0
- package/package.json +6 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# DuckDBService 詳細仕様書
|
|
2
2
|
|
|
3
3
|
> ソースファイル: `src/services/duckdb.ts`
|
|
4
|
+
> 最終更新: 2026-07-05(v1.6.0 — 初期化ライフサイクル / トランザクション / 型推定の堅牢化)
|
|
4
5
|
|
|
5
6
|
## 1. 概要
|
|
6
7
|
|
|
@@ -11,47 +12,36 @@ Web Worker の初期化、コネクション管理、SQL クエリの実行、
|
|
|
11
12
|
|
|
12
13
|
### 2.1 デザインパターン
|
|
13
14
|
|
|
14
|
-
- **シングルトンパターン**: `getInstance()`
|
|
15
|
-
- **オブザーバーパターン**: `addStatusListener` / `removeStatusListener`
|
|
15
|
+
- **シングルトンパターン**: `getInstance()` で唯一のインスタンスを取得する。アプリ全体で DuckDB Worker を1つだけ保持し、メモリとリソースを節約する。
|
|
16
|
+
- **オブザーバーパターン**: `addStatusListener` / `removeStatusListener` でステータス変更を購読できる(push 型)。React Hook (`useDuckDB`) がこれを利用する。**状態遷移のたびに `notifyListeners()` が必ず呼ばれる**ため、ポーリングは不要。
|
|
17
|
+
- **操作直列化キュー**: 単一の共有コネクション上で、`executeQuery` / `getTableInfo` / `listTables` と `createTableFromData` の DDL/DML が相互に割り込まないよう、内部の `operationQueue` で直列化する。
|
|
16
18
|
|
|
17
19
|
### 2.2 プライベートプロパティ
|
|
18
20
|
|
|
19
21
|
| プロパティ名 | 型 | 初期値 | 説明 |
|
|
20
22
|
|-------------|------|--------|------|
|
|
21
23
|
| `instance` | `DuckDBService \| null` | `null` | シングルトンインスタンス(static) |
|
|
22
|
-
| `operationFlags` | `Map<string,
|
|
23
|
-
| `workerInstance` | `AsyncDuckDB \| null` | `null` | DuckDB Worker
|
|
24
|
+
| `operationFlags` | `Map<string, Promise<void>>` | 空の Map | **進行中の**テーブル作成 Promise(重複呼び出しは同じ Promise を await する) |
|
|
25
|
+
| `workerInstance` | `AsyncDuckDB \| null` | `null` | DuckDB Worker インスタンス(**instantiate 成功後にのみ設定**) |
|
|
24
26
|
| `connectionInstance` | `AsyncDuckDBConnection \| null` | `null` | DB コネクション(再利用される) |
|
|
27
|
+
| `connectionPromise` | `Promise<AsyncDuckDBConnection> \| null` | `null` | コネクション生成中の Promise(同時 connect の二重実行防止) |
|
|
25
28
|
| `isInitializing` | `boolean` | `false` | 初期化中フラグ |
|
|
26
29
|
| `initPromise` | `Promise<AsyncDuckDB> \| null` | `null` | 初期化 Promise(重複呼び出し防止用) |
|
|
27
30
|
| `hasError` | `boolean` | `false` | エラー発生フラグ |
|
|
31
|
+
| `pendingWorker` | `Worker \| null` | `null` | 初期化中の Worker(タイムアウト時などに terminate するため保持) |
|
|
32
|
+
| `pendingWorkerUrl` | `string \| null` | `null` | 初期化中の Blob URL(同上、revoke するため保持) |
|
|
33
|
+
| `operationQueue` | `Promise<unknown>` | `Promise.resolve()` | 操作直列化キュー |
|
|
28
34
|
| `listeners` | `Set<() => void>` | 空の Set | ステータス変更リスナー群 |
|
|
29
35
|
|
|
30
36
|
## 3. メソッド仕様
|
|
31
37
|
|
|
32
38
|
### 3.1 `getInstance(): DuckDBService` (static)
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
public static getInstance(): DuckDBService
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
| 項目 | 内容 |
|
|
39
|
-
|------|------|
|
|
40
|
-
| 戻り値 | `DuckDBService` のシングルトンインスタンス |
|
|
41
|
-
| 副作用 | インスタンスが未作成の場合、新規作成する |
|
|
42
|
-
| スレッド安全性 | JavaScript はシングルスレッドなので問題なし |
|
|
43
|
-
|
|
44
|
-
**処理フロー**:
|
|
45
|
-
1. `instance` が `null` → 新しい `DuckDBService` を作成して `instance` に格納
|
|
46
|
-
2. `instance` を返す
|
|
40
|
+
インスタンスが未作成なら生成し、シングルトンを返す。JavaScript はシングルスレッドのため排他制御は不要。
|
|
47
41
|
|
|
48
42
|
---
|
|
49
43
|
|
|
50
|
-
### 3.2 `getStatus():
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
public getStatus(): "not-initialized" | "initializing" | "ready" | "error"
|
|
54
|
-
```
|
|
44
|
+
### 3.2 `getStatus(): "not-initialized" | "initializing" | "ready" | "error"`
|
|
55
45
|
|
|
56
46
|
| ステータス | 条件 | 意味 |
|
|
57
47
|
|-----------|------|------|
|
|
@@ -60,21 +50,17 @@ public getStatus(): "not-initialized" | "initializing" | "ready" | "error"
|
|
|
60
50
|
| `"ready"` | `workerInstance !== null` | 利用可能 |
|
|
61
51
|
| `"not-initialized"` | 上記いずれでもない | まだ初期化されていない |
|
|
62
52
|
|
|
63
|
-
>
|
|
53
|
+
> **v1.6.0 での整合性改善**: 初期化に失敗すると `workerInstance` は必ず `null` にリセットされる。したがって `isReady()`(`workerInstance !== null`)と `getStatus()`(`"error"`)が矛盾しなくなった(旧実装では失敗後に `isReady() === true` かつ `getStatus() === "error"` という不整合が起きていた)。
|
|
64
54
|
|
|
65
55
|
---
|
|
66
56
|
|
|
67
57
|
### 3.3 `initialize(): Promise<AsyncDuckDB>`
|
|
68
58
|
|
|
69
|
-
```typescript
|
|
70
|
-
public async initialize(): Promise<duckdb.AsyncDuckDB>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
59
|
| 項目 | 内容 |
|
|
74
60
|
|------|------|
|
|
75
61
|
| 戻り値 | 初期化された `AsyncDuckDB` インスタンス |
|
|
76
62
|
| 例外 | 初期化失敗時(タイムアウト含む)に throw |
|
|
77
|
-
| タイムアウト | 120秒(120,000ms
|
|
63
|
+
| タイムアウト | 120秒(120,000ms、`INIT_TIMEOUT_MS`) |
|
|
78
64
|
|
|
79
65
|
**処理フロー**:
|
|
80
66
|
|
|
@@ -84,304 +70,192 @@ flowchart TD
|
|
|
84
70
|
B -- Yes --> C["そのまま返す(高速パス)"]
|
|
85
71
|
B -- No --> D{既に初期化中?}
|
|
86
72
|
D -- Yes --> E["initPromise を返す(重複防止)"]
|
|
87
|
-
D -- No --> F["isInitializing = true, hasError = false\nリスナーに通知(
|
|
88
|
-
F --> G["Promise.race(\n internalInitializeWorker(),\n
|
|
89
|
-
G -- 成功 --> H["
|
|
90
|
-
G --
|
|
73
|
+
D -- No --> F["isInitializing = true, hasError = false\nリスナーに通知(initializing)\nアボートシグナル + タイムアウトタイマーを用意"]
|
|
74
|
+
F --> G["Promise.race(\n internalInitializeWorker(signal),\n 120秒タイムアウト\n)"]
|
|
75
|
+
G -- 成功 --> H["clearTimeout\nisInitializing = false\nリスナー通知(ready)\nAsyncDuckDB を返す"]
|
|
76
|
+
G -- 失敗/タイムアウト --> I["clearTimeout\nsignal.aborted = true\nisInitializing = false, hasError = true\nworkerInstance / connectionInstance / connectionPromise / initPromise を null\nabortPendingWorker()(Worker terminate + Blob URL revoke)\nリスナー通知(error)\nエラーを throw"]
|
|
91
77
|
```
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
複数の箇所から同時に `initialize()` が呼ばれても、2回目以降は `initPromise` を返すため、Worker は1回だけ作成される。
|
|
79
|
+
**v1.6.0 での重要な修正**:
|
|
95
80
|
|
|
96
|
-
|
|
81
|
+
1. **失敗後の完全リセットと再試行可能性**: 失敗/タイムアウト時に `workerInstance` を含む全状態を初期化する。旧実装では失敗しても `workerInstance` が「生成済み・未 instantiate」のまま残り、次回 `initialize()` が高速パスで壊れたインスタンスを返し、`hasError` も解除されず、**セッション全体で永久に復旧不能**になっていた。現在は `initialize()` を再度呼べばクリーンに再初期化される。
|
|
82
|
+
2. **タイマーの確実な解除**: タイムアウト用 `setTimeout` を成功/失敗どちらの経路でも `clearTimeout` する(旧実装は解除漏れで最大120秒間タイマーが残留)。
|
|
83
|
+
3. **タイムアウト後の遅延結果を無視**: アボートシグナルにより、タイムアウト後に遅れて `instantiate()` が解決しても、壊れた/整合しないインスタンスを公開しない。
|
|
97
84
|
|
|
98
|
-
|
|
85
|
+
**重複呼び出し防止**: 同時に `initialize()` が複数回呼ばれても、2回目以降は `initPromise` を返すため Worker は1回だけ作成される。
|
|
99
86
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 3.4 `internalInitializeWorker(signal): Promise<AsyncDuckDB>` (private)
|
|
103
90
|
|
|
104
|
-
Worker
|
|
91
|
+
Worker の実際の初期化処理。**`workerInstance` は全ステップ成功後にのみ `this` に公開する**(途中で失敗/アボートした場合はローカルの Worker / Blob URL を破棄し、壊れたインスタンスを絶対に残さない)。
|
|
105
92
|
|
|
106
93
|
| ステップ | 処理内容 | 補足 |
|
|
107
94
|
|---------|---------|------|
|
|
108
|
-
| 1 | `duckdb.getJsDelivrBundles()`
|
|
109
|
-
| 2 | `duckdb.selectBundle()`
|
|
110
|
-
| 3 | Blob URL
|
|
111
|
-
| 4 | `new duckdb.AsyncDuckDB(logger, worker)`
|
|
112
|
-
| 5 | `
|
|
113
|
-
| 6 | `splink_udfs`
|
|
114
|
-
| 7 | Blob URL を
|
|
115
|
-
|
|
116
|
-
**splink_udfs について**:
|
|
117
|
-
- 名寄せ(レコードリンケージ)用の UDF(ユーザー定義関数)
|
|
118
|
-
- `INSTALL splink_udfs FROM community;` → `LOAD splink_udfs;`
|
|
119
|
-
- インストール/ロードに失敗しても `warn` ログを出すだけで、初期化全体は失敗しない
|
|
95
|
+
| 1 | `duckdb.getJsDelivrBundles()` | jsDelivr CDN のバンドル一覧 |
|
|
96
|
+
| 2 | `duckdb.selectBundle()` → **アボート確認** | ブラウザ互換バンドルを選択 |
|
|
97
|
+
| 3 | Blob URL を作成し Web Worker を生成、`pendingWorker`/`pendingWorkerUrl` に退避 | 失敗時の破棄用 |
|
|
98
|
+
| 4 | `new duckdb.AsyncDuckDB(logger, worker)`(ローカル変数 `db`) | まだ `this` に代入しない |
|
|
99
|
+
| 5 | `db.instantiate()` → **アボート確認** | WASM モジュール読み込み |
|
|
100
|
+
| 6 | `splink_udfs` を一時コネクションで INSTALL/LOAD(失敗しても続行、`finally` で close)→ **アボート確認** | 名寄せ用 UDF |
|
|
101
|
+
| 7 | **成功**: `this.workerInstance = db`、`pendingWorker`/`pendingWorkerUrl` をクリア、Blob URL を revoke | ここで初めて公開 |
|
|
102
|
+
| 失敗時 | ローカル `worker.terminate()` と `URL.revokeObjectURL()` を**必ず**実行(各々 try/catch で握りつぶす)、エラーを rethrow | Worker/Blob URL リーク防止 |
|
|
120
103
|
|
|
121
104
|
---
|
|
122
105
|
|
|
123
106
|
### 3.5 `getConnection(): Promise<AsyncDuckDBConnection>`
|
|
124
107
|
|
|
125
|
-
```typescript
|
|
126
|
-
public async getConnection(): Promise<duckdb.AsyncDuckDBConnection>
|
|
127
|
-
```
|
|
128
|
-
|
|
129
108
|
| 項目 | 内容 |
|
|
130
109
|
|------|------|
|
|
131
|
-
| 戻り値 | DuckDB
|
|
110
|
+
| 戻り値 | DuckDB コネクション(1つを保持・再利用) |
|
|
132
111
|
| 動作 | コネクションが無ければ `initialize()` → `connect()` で作成。あれば再利用 |
|
|
133
112
|
|
|
134
|
-
|
|
113
|
+
**v1.6.0 での修正**: 生成中の `connectionPromise` を共有することで、**同時呼び出し時の二重 `connect()` を防止**する。`connect()` が失敗した場合は `connectionPromise` を `null` に戻し、次回リトライ可能にする。
|
|
135
114
|
|
|
136
115
|
---
|
|
137
116
|
|
|
138
117
|
### 3.6 `executeQuery(sql: string): Promise<unknown>`
|
|
139
118
|
|
|
140
|
-
```typescript
|
|
141
|
-
public async executeQuery(sql: string): Promise<unknown>
|
|
142
|
-
```
|
|
143
|
-
|
|
144
119
|
| 項目 | 内容 |
|
|
145
120
|
|------|------|
|
|
146
|
-
| 引数 | `sql` - 実行する SQL 文字列 |
|
|
147
121
|
| 戻り値 | クエリ結果(Apache Arrow Table)。型は `unknown` |
|
|
148
|
-
| タイムアウト | 60秒(60,000ms
|
|
149
|
-
|
|
|
122
|
+
| タイムアウト | 60秒(60,000ms、`QUERY_TIMEOUT_MS`) |
|
|
123
|
+
| 直列化 | `operationQueue` で他操作と直列化(テーブル構築中の割り込み read を防止) |
|
|
150
124
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
3. 60秒のタイムアウトと `Promise.race` で競合
|
|
155
|
-
4. 結果を返す(またはエラーを throw)
|
|
125
|
+
**v1.6.0 での修正**:
|
|
126
|
+
- タイムアウト用 `setTimeout` を成功/失敗どちらの経路でも `clearTimeout` する(旧実装はクエリ1回ごとに最大60秒残留するタイマーを生成していた)。
|
|
127
|
+
- **タイムアウト時にコネクションを破棄する**: タイムアウトしたクエリはワーカー側で走り続けるため、使用中コネクションを `connectionInstance`/`connectionPromise` から外して close(best-effort)する。これにより後続クエリが詰まったコネクションで待たされず、次回は新しいコネクションで実行される(内部の `runQueryWithTimeout` が担当)。コネクション取得(`getConnection`)自体がハングした場合もタイムアウトが働き、破棄対象が無ければ何もしない。
|
|
156
128
|
|
|
157
129
|
---
|
|
158
130
|
|
|
159
131
|
### 3.7 `executeQueries(queries: string[]): Promise<unknown[]>`
|
|
160
132
|
|
|
161
|
-
|
|
162
|
-
public async executeQueries(queries: string[]): Promise<unknown[]>
|
|
163
|
-
```
|
|
133
|
+
配列の順番通りに1つずつ**逐次実行**し、結果を配列で返す。CREATE → INSERT → SELECT のような依存クエリ列に適する。
|
|
164
134
|
|
|
165
|
-
|
|
166
|
-
|------|------|
|
|
167
|
-
| 引数 | `queries` - SQL 文字列の配列 |
|
|
168
|
-
| 戻り値 | 各クエリの結果を順番に格納した配列 |
|
|
169
|
-
| 実行方法 | **逐次実行**(並列ではない) |
|
|
170
|
-
|
|
171
|
-
> **重要**: クエリは配列の順番通りに1つずつ実行される。CREATE TABLE → INSERT → SELECT のように依存関係があるクエリ列に適している。
|
|
135
|
+
**v1.6.0 での修正**: バッチ全体を**1つのキュー単位**(単一 `enqueue`)として実行するため、他操作がクエリ列の途中に割り込まない(`executeQuery` を個別に enqueue する旧実装では、Q1 と Q2 の間に別操作が挟まり得た)。各クエリには従来どおり 60 秒タイムアウトが適用される。なお DB レベルのトランザクション(原子性)ではないため、原子性が必要な場合は `createTableFromData` か明示的な BEGIN/COMMIT を用いる。
|
|
172
136
|
|
|
173
137
|
---
|
|
174
138
|
|
|
175
139
|
### 3.8 `createTableFromData<T>(tableName, data, options): Promise<void>`
|
|
176
140
|
|
|
177
|
-
```typescript
|
|
178
|
-
public async createTableFromData<T extends Record<string, unknown>>(
|
|
179
|
-
tableName: string,
|
|
180
|
-
data: T[],
|
|
181
|
-
options: {
|
|
182
|
-
dropIfExists?: boolean
|
|
183
|
-
primaryKey?: string
|
|
184
|
-
verbose?: boolean
|
|
185
|
-
} = {},
|
|
186
|
-
): Promise<void>
|
|
187
|
-
```
|
|
188
|
-
|
|
189
141
|
JavaScript の配列データから DuckDB テーブルを作成する高レベルメソッド。
|
|
190
142
|
|
|
191
143
|
#### 引数
|
|
192
144
|
|
|
193
145
|
| 引数名 | 型 | 必須 | 説明 |
|
|
194
146
|
|--------|------|------|------|
|
|
195
|
-
| `tableName` | `string` | Yes |
|
|
196
|
-
| `data` | `T[]` | Yes |
|
|
197
|
-
| `options.dropIfExists` | `boolean` | No | `true`
|
|
198
|
-
| `options.primaryKey` | `string` | No |
|
|
199
|
-
| `options.verbose` | `boolean` | No |
|
|
147
|
+
| `tableName` | `string` | Yes | 作成するテーブル名(識別子として安全にクォートされる) |
|
|
148
|
+
| `data` | `T[]` | Yes | 投入データ配列(1行以上必須) |
|
|
149
|
+
| `options.dropIfExists` | `boolean` | No | `true` で既存テーブルを削除してから作成 |
|
|
150
|
+
| `options.primaryKey` | `string` | No | 主キーに設定するカラム名 |
|
|
151
|
+
| `options.verbose` | `boolean` | No | **ログ出力の有無のみ**を制御(デフォルト `true`)。DB への追加クエリは発行しない |
|
|
200
152
|
|
|
201
153
|
#### 処理フロー
|
|
202
154
|
|
|
203
155
|
```mermaid
|
|
204
156
|
flowchart TD
|
|
205
|
-
A["createTableFromData(
|
|
206
|
-
B -- Yes --> C["Error を throw"]
|
|
207
|
-
B -- No --> D{operationFlags
|
|
208
|
-
D -- Yes --> E["
|
|
209
|
-
D -- No --> F["
|
|
210
|
-
F --> G
|
|
211
|
-
G -- Yes --> H["DROP TABLE IF EXISTS を実行"]
|
|
212
|
-
G -- No --> I1["SHOW TABLES(作成前の確認ログ)"]
|
|
213
|
-
H --> I1
|
|
214
|
-
I1 --> I2["型推定(inferColumnTypes)\n先頭100行→関数内部で先頭10行を使用"]
|
|
215
|
-
I2 --> J["CREATE TABLE 文を生成・実行"]
|
|
216
|
-
J --> J1["SHOW TABLES + information_schema\n(作成後の検証ログ)"]
|
|
217
|
-
J1 --> K["データを1000行ずつバッチ INSERT\nformatValueForSQL で値を変換"]
|
|
218
|
-
K --> L["operationFlags からフラグを削除(finally)"]
|
|
157
|
+
A["createTableFromData(...)"] --> B{data が空 (null / 長さ0)?}
|
|
158
|
+
B -- Yes --> C["Error: 'Data array is empty' を throw"]
|
|
159
|
+
B -- No --> D{operationFlags に同名の進行中 Promise?}
|
|
160
|
+
D -- Yes --> E["**その Promise を await して返す**\n(早期 void 解決しない)"]
|
|
161
|
+
D -- No --> F["buildTable を operationQueue に積み、\noperationFlags に Promise を登録"]
|
|
162
|
+
F --> G["await 完了 → finally で operationFlags を削除"]
|
|
219
163
|
```
|
|
220
164
|
|
|
221
|
-
|
|
165
|
+
**v1.6.0 での重複排除の修正**: 同名テーブルへの同時呼び出しは、進行中の**同一 Promise を await** する。旧実装は2回目の呼び出しが「テーブル未完成のまま即 `void` 解決」し、呼び出し側が存在しない/部分的なテーブルを read してしまう危険があった。
|
|
166
|
+
|
|
167
|
+
#### `buildTable`(private)— トランザクションによる原子的構築
|
|
222
168
|
|
|
223
|
-
|
|
169
|
+
```mermaid
|
|
170
|
+
flowchart TD
|
|
171
|
+
A["列名 = 全行の和集合(collectColumnNames)"] --> B["型推定(先頭 100 行、inferColumnTypes)"]
|
|
172
|
+
B --> C["BEGIN TRANSACTION"]
|
|
173
|
+
C --> D{dropIfExists?}
|
|
174
|
+
D -- Yes --> E["DROP TABLE IF EXISTS"]
|
|
175
|
+
D -- No --> F["CREATE TABLE"]
|
|
176
|
+
E --> F
|
|
177
|
+
F --> G["1000 行ずつ INSERT(formatValueForSQL)"]
|
|
178
|
+
G --> H["COMMIT"]
|
|
179
|
+
H -.->|途中で失敗| I["ROLLBACK(失敗しても warn のみ)→ 元エラーを rethrow"]
|
|
180
|
+
```
|
|
224
181
|
|
|
225
|
-
|
|
182
|
+
- **原子性**: DROP/CREATE/INSERT を1トランザクションにまとめ、途中失敗時は `ROLLBACK` する。**半端に作成されたテーブルを残さない**(旧実装は失敗時にデータ無し/部分ロードのテーブルが残った)。
|
|
183
|
+
- **列名は全行の和集合**(`collectColumnNames`)から決定する。旧実装は先頭行のキーのみを使い、後続行だけが持つ列を**無警告で欠落**させていた。CREATE と INSERT で同じ列集合を使う。
|
|
184
|
+
- **`null`/`undefined` 行**はすべて `NULL` として挿入する(不揃いデータへの耐性)。
|
|
185
|
+
|
|
186
|
+
#### 型推定ルール(`inferColumnTypes`、サンプル = 先頭 100 行)
|
|
226
187
|
|
|
227
188
|
| JavaScript の値 | 推定される DuckDB 型 | 条件 |
|
|
228
189
|
|----------------|---------------------|------|
|
|
229
|
-
| `number
|
|
230
|
-
| `number
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
190
|
+
| `number`(有限・整数) | `BIGINT` | `Number.isFinite` かつ `Number.isInteger`(桁あふれ回避のため一律 BIGINT) |
|
|
191
|
+
| `number`(有限・小数) | `DOUBLE` | 有限だが整数でない |
|
|
192
|
+
| `bigint` | `BIGINT` | `typeof === "bigint"` |
|
|
193
|
+
| `number`(非有限) | (数値扱いしない) | `NaN` / `Infinity` は文字列カウント(挿入時は NULL) |
|
|
194
|
+
| `boolean` | `BOOLEAN` | |
|
|
233
195
|
| `Date` オブジェクト | `TIMESTAMP` | `instanceof Date` |
|
|
234
|
-
| `string
|
|
235
|
-
|
|
|
236
|
-
|
|
196
|
+
| `string`(**厳密**日付) | `TIMESTAMP` | `TIMESTAMP_PATTERN`(`YYYY-MM-DD[ T HH:MM...]`)に一致し、かつ `new Date` が有効 |
|
|
197
|
+
| その他 / 混在 | `VARCHAR` | 上記以外、値がすべて null、型が混在する場合のフォールバック |
|
|
198
|
+
|
|
199
|
+
> **v1.6.0 での修正**:
|
|
200
|
+
> - 整数は `INTEGER` ではなく **`BIGINT`** を採用し、int32 を超える値でのオーバーフローを解消。
|
|
201
|
+
> - TIMESTAMP 推定を厳密化。旧実装の「`new Date()` が有効 かつ `"-"` を含む」は緩すぎ、`ABC-123` のような文字列まで日付扱いして後続行のキャスト失敗を招いた。現在は厳密な日付/日時パターンに一致する場合のみ TIMESTAMP。
|
|
202
|
+
> - サンプルは実際に先頭 100 行を使用(旧実装は「100 行」と称しつつ内部で 10 行に絞る二重スライスのバグがあった)。
|
|
237
203
|
|
|
238
204
|
#### SQL 値フォーマットルール(`formatValueForSQL`)
|
|
239
205
|
|
|
240
206
|
| JavaScript の値 | SQL 出力 | 補足 |
|
|
241
207
|
|----------------|---------|------|
|
|
242
208
|
| `null` / `undefined` | `NULL` | |
|
|
243
|
-
| `string` | `'
|
|
244
|
-
| `Date` | `'2024-01-01T00:00:00.000Z'` | ISO 8601
|
|
209
|
+
| `string` | `'エスケープ済み'` | シングルクォートを `''` にエスケープ |
|
|
210
|
+
| `Date` | `'2024-01-01T00:00:00.000Z'` | ISO 8601 |
|
|
245
211
|
| `boolean` | `TRUE` / `FALSE` | |
|
|
246
|
-
| `
|
|
247
|
-
|
|
|
212
|
+
| `bigint` | `123` | `toString()` |
|
|
213
|
+
| `number`(有限) | `123` / `45.67` | |
|
|
214
|
+
| `number`(非有限: NaN/Infinity) | `NULL` | 有効な SQL 数値リテラルでないため NULL 化 |
|
|
215
|
+
| オブジェクト / 配列 | `'{"x":1}'` / `'["a","b"]'` | `JSON.stringify` してクォート・エスケープ。stringify 不能(循環参照・`undefined`)は `NULL` |
|
|
248
216
|
|
|
249
|
-
|
|
217
|
+
> **v1.6.0 での修正**: 旧実装はオブジェクト/配列を `String(value)` で `[object Object]` や `a,b`(列数ずれ)に、非有限数を裸の `NaN`/`Infinity`(無効 SQL)に変換し、INSERT バッチ全体を失敗させ得た。
|
|
250
218
|
|
|
251
|
-
|
|
219
|
+
#### 識別子のクォート(`quoteIdentifier`)
|
|
252
220
|
|
|
253
|
-
|
|
254
|
-
public async getTableInfo(tableName: string): Promise<unknown>
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
| 項目 | 内容 |
|
|
258
|
-
|------|------|
|
|
259
|
-
| 実行 SQL | `DESCRIBE {tableName}` |
|
|
260
|
-
| 戻り値 | テーブルのカラム情報(名前、型、NULL 可否など) |
|
|
221
|
+
`tableName` / カラム名 / `primaryKey` はすべて `"..."` でクォートし、内部のダブルクォートを `""` にエスケープする。空文字列や文字列以外の識別子は `Error("Invalid SQL identifier")` を投げる。旧実装は `tableName` を無クォートで DDL/DML に、`information_schema` の `WHERE table_name = '...'` に**文字列リテラルとして生挿入**しており、スペース/予約語/クォートを含む名前で破綻し、リテラル注入点も存在した(診断クエリ自体も v1.6.0 で撤去)。
|
|
261
222
|
|
|
262
223
|
---
|
|
263
224
|
|
|
264
|
-
### 3.
|
|
225
|
+
### 3.9 `getTableInfo(tableName: string): Promise<unknown>`
|
|
265
226
|
|
|
266
|
-
|
|
267
|
-
public async listTables(): Promise<unknown>
|
|
268
|
-
```
|
|
227
|
+
`DESCRIBE "<tableName>"`(クォート済み)を `operationQueue` 経由で実行し、カラム情報を返す。
|
|
269
228
|
|
|
270
|
-
|
|
271
|
-
|------|------|
|
|
272
|
-
| 実行 SQL | `SHOW TABLES` |
|
|
273
|
-
| 戻り値 | 現在のデータベースに存在するテーブル一覧 |
|
|
229
|
+
### 3.10 `listTables(): Promise<unknown>`
|
|
274
230
|
|
|
275
|
-
|
|
231
|
+
`SHOW TABLES` を `operationQueue` 経由で実行し、テーブル一覧を返す。
|
|
276
232
|
|
|
277
233
|
### 3.11 `isReady(): boolean`
|
|
278
234
|
|
|
279
|
-
|
|
280
|
-
public isReady(): boolean
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
| 項目 | 内容 |
|
|
284
|
-
|------|------|
|
|
285
|
-
| 戻り値 | `workerInstance !== null` の場合 `true` |
|
|
286
|
-
| 用途 | 同期的に初期化完了を確認したい場合 |
|
|
287
|
-
|
|
288
|
-
---
|
|
235
|
+
`workerInstance !== null` を返す。§3.2 のとおり失敗時は `null` にリセットされるため `getStatus()` と整合する。
|
|
289
236
|
|
|
290
237
|
### 3.12 `cleanup(): Promise<void>`
|
|
291
238
|
|
|
292
|
-
```typescript
|
|
293
|
-
public async cleanup(): Promise<void>
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
リソースの完全な解放を行う。
|
|
297
|
-
|
|
298
239
|
| ステップ | 処理 |
|
|
299
240
|
|---------|------|
|
|
300
241
|
| 1 | コネクションを close |
|
|
301
242
|
| 2 | Worker を terminate |
|
|
302
|
-
| 3 |
|
|
303
|
-
| 4 |
|
|
304
|
-
|
|
305
|
-
> **注意**: cleanup 後に再度 `initialize()` を呼べば再初期化できる。
|
|
243
|
+
| 3 | `pendingWorker`/`pendingWorkerUrl` を破棄(`abortPendingWorker`) |
|
|
244
|
+
| 4 | 全内部状態(`isInitializing`/`hasError`/`initPromise`/`connectionPromise`/`operationQueue`/`operationFlags`)をリセット |
|
|
245
|
+
| 5 | リスナーに通知してから `listeners` をクリア |
|
|
306
246
|
|
|
307
|
-
|
|
247
|
+
teardown 中に例外が出ても `Logger.error` でログするのみで throw しない。cleanup 後に `initialize()` を再度呼べば再初期化できる。
|
|
308
248
|
|
|
309
249
|
### 3.13 `resetInstance(): void` (static)
|
|
310
250
|
|
|
311
|
-
|
|
312
|
-
public static resetInstance(): void
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
| 項目 | 内容 |
|
|
316
|
-
|------|------|
|
|
317
|
-
| 用途 | テスト時にシングルトンをリセットする |
|
|
318
|
-
| 動作 | `cleanup()` を呼び出し(**await しない**)、直後に `instance = null` にする |
|
|
319
|
-
|
|
320
|
-
> **注意**: `cleanup()` は `async` メソッドだが、`resetInstance()` は同期メソッド(`void`)であり `cleanup()` を await しない。コネクションの close や Worker の terminate が完了する前にインスタンスが `null` になる。テストで確実にリソースを解放したい場合は、`resetInstance()` の前に `await DuckDBService.getInstance().cleanup()` を明示的に呼ぶこと。
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
### 3.14 ステータスリスナー
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
public addStatusListener(listener: () => void): void
|
|
328
|
-
public removeStatusListener(listener: () => void): void
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
| メソッド | 説明 |
|
|
332
|
-
|---------|------|
|
|
333
|
-
| `addStatusListener` | ステータス変更時に呼ばれるコールバックを登録 |
|
|
334
|
-
| `removeStatusListener` | 登録済みコールバックを削除 |
|
|
335
|
-
|
|
336
|
-
リスナーが呼ばれるタイミング:
|
|
337
|
-
- `initialize()` 開始時(→ `"initializing"`)
|
|
338
|
-
- `initialize()` 成功時(→ `"ready"`)
|
|
339
|
-
- `initialize()` 失敗時(→ `"error"`)
|
|
340
|
-
|
|
341
|
-
## 4. エクスポート
|
|
342
|
-
|
|
343
|
-
`services/duckdb.ts` のエクスポート:
|
|
344
|
-
|
|
345
|
-
```typescript
|
|
346
|
-
// 名前付きエクスポート(クラス本体)
|
|
347
|
-
export { DuckDBService }
|
|
348
|
-
|
|
349
|
-
// デフォルトエクスポート(シングルトンインスタンス)— services/duckdb.ts 内でのみ default
|
|
350
|
-
export default DuckDBService.getInstance()
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
`index.ts` での re-export:
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
export { DuckDBService, default as duckDBService } from "./services/duckdb"
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
> **注意**: `services/duckdb.ts` はデフォルトエクスポートを持つが、`index.ts` はそれを `duckDBService` という**名前付きエクスポート**として re-export する。パッケージ自体にデフォルトエクスポートは存在しない。
|
|
360
|
-
|
|
361
|
-
| エクスポート | インポート方法 | 用途 |
|
|
362
|
-
|------------|--------------|------|
|
|
363
|
-
| `DuckDBService`(クラス) | `import { DuckDBService } from '...'` | `getInstance()` や `resetInstance()` を呼びたい場合 |
|
|
364
|
-
| `duckDBService`(インスタンス) | `import { duckDBService } from '...'` | 通常利用。シングルトンインスタンスを直接使う |
|
|
365
|
-
|
|
366
|
-
## 5. ステータス遷移図
|
|
367
|
-
|
|
368
|
-
```mermaid
|
|
369
|
-
stateDiagram-v2
|
|
370
|
-
state "not-initialized" as not_init
|
|
371
|
-
[*] --> not_init
|
|
372
|
-
not_init --> initializing : initialize()
|
|
373
|
-
initializing --> ready : 成功
|
|
374
|
-
initializing --> error : 失敗
|
|
375
|
-
ready --> not_init : cleanup()
|
|
376
|
-
error --> initializing : initialize()(リトライ)
|
|
377
|
-
error --> not_init : cleanup()
|
|
378
|
-
```
|
|
251
|
+
`cleanup()` を呼び出し(**await しない**)、直後に `instance = null` にする。主にテスト用。`instance` が既に `null` の場合は何もしない。
|
|
379
252
|
|
|
380
|
-
##
|
|
253
|
+
## 4. 内部定数
|
|
381
254
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
255
|
+
| 定数 | 値 | 用途 |
|
|
256
|
+
|------|------|------|
|
|
257
|
+
| `INIT_TIMEOUT_MS` | `120000` | 初期化タイムアウト |
|
|
258
|
+
| `QUERY_TIMEOUT_MS` | `60000` | クエリタイムアウト |
|
|
259
|
+
| `BATCH_SIZE` | `1000` | バッチ INSERT の行数 |
|
|
260
|
+
| `TYPE_INFERENCE_SAMPLE_SIZE` | `100` | 型推定のサンプル行数 |
|
|
261
|
+
| `TIMESTAMP_PATTERN` | 正規表現 | 文字列を TIMESTAMP とみなす厳密パターン |
|
|
@@ -63,36 +63,21 @@ export interface UseDuckDBResult {
|
|
|
63
63
|
|
|
64
64
|
### 2.4 内部動作の詳細
|
|
65
65
|
|
|
66
|
-
####
|
|
66
|
+
#### ステータス同期の仕組み(v1.6.0 で push 型に一本化)
|
|
67
67
|
|
|
68
|
-
`useDuckDB`
|
|
68
|
+
`useDuckDB` は**ステータスリスナー(push 型)のみ**で `DuckDBService` のステータスを React の状態に同期する。
|
|
69
69
|
|
|
70
|
-
**方法1: ステータスリスナー**
|
|
71
70
|
```
|
|
72
71
|
DuckDBService
|
|
73
|
-
├── addStatusListener(updateStatus) ←
|
|
72
|
+
├── addStatusListener(updateStatus) ← マウント時に登録し、updateStatus() を1回即時実行して初期値を取得
|
|
74
73
|
└── removeStatusListener(updateStatus) ← アンマウント時に解除
|
|
75
74
|
```
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
setInterval(pollStatus, 1000) ← 1秒ごとにステータスをチェック
|
|
80
|
-
```
|
|
76
|
+
`DuckDBService` は状態遷移(initializing / ready / error)と `cleanup()` のたびに必ず `notifyListeners()` を呼ぶため、リスナーだけで全遷移を漏れなく受け取れる。
|
|
81
77
|
|
|
82
|
-
>
|
|
78
|
+
> **v1.6.0 での変更(1秒ポーリングの撤去)**: 旧実装は上記リスナーに加えて `setInterval(pollStatus, 1000)` を各フックインスタンスに常設していた。`useDuckDBQuery` は内部で `useDuckDB()` を呼ぶため、`root.tsx` のアプリ全体マウントと合わせて **消費者ごとに永久 1 秒タイマー** が積み上がり、しかも push リスナーと完全に重複していた。リスナーで全遷移が届く以上ポーリングは不要と判断し、effect ごと削除した。
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
if (currentStatus !== status && currentStatus !== "initializing") {
|
|
87
|
-
setStatus(currentStatus)
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
1. `currentStatus !== status` — サービスの現在のステータスが React state と異なる場合のみ更新
|
|
91
|
-
2. `currentStatus !== "initializing"` — サービスが `"initializing"` を返す場合は更新しない
|
|
92
|
-
|
|
93
|
-
両方を満たした場合のみ React state が更新される。`"initializing"` への遷移はリスナー経由でのみ通知される。
|
|
94
|
-
|
|
95
|
-
**SSR 環境での動作**: `typeof window === "undefined"` の場合(SSR / Node.js 環境)、`setInterval` は設定されず、`pollStatus` を1回だけ同期的に実行してから effect を終了する。ブラウザ環境でも `setInterval` 設定直後に `pollStatus()` が1回即時実行される。
|
|
80
|
+
**SSR 環境での動作**: リスナー登録は `useEffect` 内で行われ、SSR(サーバー)では effect が実行されないため副作用は発生しない。初期レンダリングの `status` は `"not-initialized"` となる。
|
|
96
81
|
|
|
97
82
|
#### 自動初期化フロー
|
|
98
83
|
|
|
@@ -224,6 +209,10 @@ flowchart TD
|
|
|
224
209
|
3. `isReady` が `false` → `true` に変わった(初期化完了)
|
|
225
210
|
4. `refetch()` を手動で呼んだ
|
|
226
211
|
|
|
212
|
+
> **`data` は生の Arrow Table**: `data` は `executeQuery()` の戻り値そのもの(Apache Arrow Table)であり、**行配列ではない**。`.length` や `.map` は持たないため、行として扱うときは必ず `duckdbTableToArray<Row>(data)` で変換すること(型パラメータ `T` には行の型ではなく Arrow Table 相当の型を想定する)。単一クエリの型付き行配列がほしい場合は `useDuckDB().executeQuery()` + `duckdbTableToArray()` の組み合わせが明快。
|
|
213
|
+
|
|
214
|
+
> **`dependencies` の扱い(v1.6.0)**: 内部では可変長の `dependencies` を effect の依存配列へ直接スプレッドせず、`Object.is` 比較で変化を検知する**固定サイズのバージョン番号**(`useDependencyVersion`)に変換して `[refetch, version]` を依存にする。これにより、レンダー間で `dependencies` の**長さが変わっても** React の「依存配列サイズ変更」エラーを起こさず、要素の変化(React と同じ参照比較)で再フェッチされる。
|
|
215
|
+
|
|
227
216
|
### 3.6 使用パターン
|
|
228
217
|
|
|
229
218
|
#### パターン1: 基本的なデータ取得
|
|
@@ -139,9 +139,7 @@ private formatMessage(message: unknown): unknown[] {
|
|
|
139
139
|
|
|
140
140
|
### 開発中(すべてのログを表示)
|
|
141
141
|
|
|
142
|
-
>
|
|
143
|
-
|
|
144
|
-
パッケージ側でエクスポートが追加された場合の使用例:
|
|
142
|
+
> **v1.6.0 で公開**: `Logger` / `LogLevel` / `ILogger` は `index.ts` から公開エクスポートされるようになった。利用側から直接インポートしてログレベルを制御できる(旧バージョンではエクスポートされておらず、初期化ログを有効化する手段が無かった)。
|
|
145
143
|
|
|
146
144
|
```typescript
|
|
147
145
|
import { Logger, LogLevel } from '@aiquants/duckdb-helper'
|
|
@@ -176,8 +174,8 @@ Logger.setImplementation(customLogger)
|
|
|
176
174
|
|
|
177
175
|
## 7. 注意事項
|
|
178
176
|
|
|
179
|
-
1. **Logger / LogLevel は `index.ts`
|
|
180
|
-
2. **デフォルトレベルは `WARN`**。つまり `DuckDBService` 内の `Logger.info(...)`
|
|
177
|
+
1. **Logger / LogLevel / ILogger は `index.ts` から公開エクスポートされている(v1.6.0〜)**。`import { Logger, LogLevel } from '@aiquants/duckdb-helper'` で利用でき、`Logger.setLevel()` などでログレベルや実装を制御できる。
|
|
178
|
+
2. **デフォルトレベルは `WARN`**。つまり `DuckDBService` 内の `Logger.info(...)` による初期化ログは通常表示されない。ただし初期化の**失敗**(`Logger.error`)は WARN でも表示される。詳細なステップを見たい場合は `Logger.setLevel(LogLevel.DEBUG)` を設定する。
|
|
181
179
|
3. **リスナーでの例外はキャッチされる**。`DuckDBService.notifyListeners()` 内でリスナーが例外を投げた場合、`Logger.error` でログに出力されるが、他のリスナーへの通知は続行される。
|
|
182
180
|
|
|
183
181
|
## 8. 新人向けポイント
|
|
@@ -121,7 +121,6 @@ export default defineConfig({
|
|
|
121
121
|
| `build:watch` | `tsup --watch` | ファイル変更時に自動リビルド |
|
|
122
122
|
| `dev` | `tsup --watch` | 開発用(build:watch と同一) |
|
|
123
123
|
| `watch` | `tsup --watch` | ウォッチモード |
|
|
124
|
-
| `type-check` | `tsc --noEmit` | 型チェックのみ実行 |
|
|
125
124
|
| `clean` | `rimraf dist` | ビルド成果物を削除 |
|
|
126
125
|
| `prepare` | `pnpm run build` | パッケージインストール後に自動ビルド |
|
|
127
126
|
| `prepublishOnly` | clean → typecheck → test(`--if-present`) → build | 公開前の品質チェック |
|
|
@@ -131,9 +130,10 @@ export default defineConfig({
|
|
|
131
130
|
| `format:fix` | `biome format --write src/` | Biome によるフォーマット自動修正 |
|
|
132
131
|
| `check` | `biome check src/` | Lint + フォーマットの同時チェック |
|
|
133
132
|
| `check:fix` | `biome check --write src/` | Lint + フォーマットの同時自動修正 |
|
|
134
|
-
| `typecheck` | `
|
|
133
|
+
| `typecheck` | `tsc --noEmit` | 本体(`src`)の型チェック |
|
|
134
|
+
| `typecheck:test` | `tsc --noEmit -p tsconfig.test.json` | テストを含む型チェック(v1.6.0〜) |
|
|
135
135
|
| `test` | `vitest run` | テスト実行 |
|
|
136
|
-
| `test:coverage` | `vitest run --coverage` |
|
|
136
|
+
| `test:coverage` | `vitest run --coverage` | カバレッジ付きテスト(閾値 100%) |
|
|
137
137
|
| `license-check` | `pnpm dlx license-checker ...` | 許可ライセンスのチェック |
|
|
138
138
|
| `license-check:json` | `pnpm dlx license-checker ... --json` | ライセンスチェック(JSON 出力) |
|
|
139
139
|
| `publish:patch` | version patch → publish | パッチバージョン公開 |
|
|
@@ -148,6 +148,24 @@ export default defineConfig({
|
|
|
148
148
|
|
|
149
149
|
npm に公開されるファイル。`src/` はパッケージに含まれない。
|
|
150
150
|
|
|
151
|
+
## 4.5 テスト設定(v1.6.0〜)
|
|
152
|
+
|
|
153
|
+
| ファイル | 役割 |
|
|
154
|
+
|---------|------|
|
|
155
|
+
| `vitest.config.ts` | Vitest 設定。`environment: "jsdom"`、`globals: true`、カバレッジは `v8` プロバイダで `src/**`(`src/types` 除外)を対象に閾値 100%(lines / functions / branches / statements) |
|
|
156
|
+
| `tsconfig.test.json` | `tsconfig.json` を継承し `tests/**` を含めた型チェック用設定 |
|
|
157
|
+
|
|
158
|
+
### テスト関連 devDependencies
|
|
159
|
+
|
|
160
|
+
| パッケージ | 用途 |
|
|
161
|
+
|-----------|------|
|
|
162
|
+
| `vitest` | テストランナー |
|
|
163
|
+
| `@vitest/coverage-v8` | V8 カバレッジプロバイダ |
|
|
164
|
+
| `jsdom` | ブラウザ相当の DOM 環境(`window` / `URL` / React フックのレンダリング) |
|
|
165
|
+
| `@testing-library/react`, `@testing-library/dom` | React フックのテスト(`renderHook` 等) |
|
|
166
|
+
|
|
167
|
+
> テストでは `@duckdb/duckdb-wasm` を `vi.mock` で制御可能なフェイクに差し替え、`Worker` / `URL.createObjectURL` / `Blob` をスタブすることで、実際の WASM ダウンロードや Worker 生成なしに全経路を検証する。詳細は[テスト仕様書](./2026.07.05%20%5BAI%5D%2007-testing.md)を参照。
|
|
168
|
+
|
|
151
169
|
## 5. ビルド成果物の構成
|
|
152
170
|
|
|
153
171
|
```
|
|
@@ -164,7 +182,7 @@ dist/
|
|
|
164
182
|
|
|
165
183
|
1. **`pnpm run build` でビルド**。`dist/` にファイルが生成される。
|
|
166
184
|
2. **`pnpm run dev` で開発中はウォッチモード**を使うと、ソース変更が自動で反映される。
|
|
167
|
-
3. **`pnpm run
|
|
185
|
+
3. **`pnpm run typecheck` で型エラーだけ確認**できる。ビルドせずに素早くチェック。
|
|
168
186
|
4. **`external` に指定されたパッケージはバンドルに含まれない**。利用側でインストールが必要。
|
|
169
187
|
5. **ビルドは tsup(esbuild ベース)が行い、tsc は型チェック専用**。この分業により高速ビルドと厳密な型チェックを両立。
|
|
170
188
|
6. **`src/` は npm パッケージに含まれない**。利用者が見るのは `dist/` のビルド済みコードのみ。
|