momoi-explorer 0.8.0

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/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # momoi-explorer
2
+
3
+ ヘッドレスファイルエクスプローラーライブラリ。フレームワーク非依存のコア + React バインディング + デフォルト UI の3層構成。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install momoi-explorer
9
+ ```
10
+
11
+ ## アーキテクチャ
12
+
13
+ 3つのエントリポイントを持つ段階的なアーキテクチャ:
14
+
15
+ | エントリポイント | 用途 | React必須 |
16
+ |---|---|---|
17
+ | `momoi-explorer` | コアエンジン(フレームワーク非依存) | No |
18
+ | `momoi-explorer/react` | React バインディング(hooks + context) | Yes |
19
+ | `momoi-explorer/ui` | デフォルト UI コンポーネント | Yes |
20
+
21
+ ## クイックスタート
22
+
23
+ ### 1. FileSystemAdapter を実装する
24
+
25
+ 全ての始まりは `FileSystemAdapter` の実装。`readDir` のみ必須で、他のメソッドはオプション(実装すると対応機能が有効になる)。
26
+
27
+ ```ts
28
+ import type { FileSystemAdapter } from 'momoi-explorer'
29
+
30
+ const adapter: FileSystemAdapter = {
31
+ // 必須: ディレクトリの中身を返す
32
+ async readDir(dirPath) {
33
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
34
+ return entries.map(e => ({
35
+ name: e.name,
36
+ path: path.join(dirPath, e.name),
37
+ isDirectory: e.isDirectory(),
38
+ }))
39
+ },
40
+ // オプション: リネーム
41
+ async rename(oldPath, newPath) {
42
+ await fs.rename(oldPath, newPath)
43
+ },
44
+ // オプション: 削除
45
+ async delete(paths) {
46
+ for (const p of paths) await fs.rm(p, { recursive: true })
47
+ },
48
+ // オプション: ファイル作成
49
+ async createFile(parentPath, name) {
50
+ await fs.writeFile(path.join(parentPath, name), '')
51
+ },
52
+ // オプション: フォルダ作成
53
+ async createDir(parentPath, name) {
54
+ await fs.mkdir(path.join(parentPath, name))
55
+ },
56
+ // オプション: ファイル監視(デバウンス・合体はコアが行う)
57
+ watch(dirPath, callback) {
58
+ const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
59
+ callback([{ type: event === 'rename' ? 'create' : 'modify', path: filename, isDirectory: false }])
60
+ })
61
+ return () => watcher.close()
62
+ },
63
+ }
64
+ ```
65
+
66
+ ### 2a. デフォルト UI を使う(最も簡単)
67
+
68
+ ```tsx
69
+ import { FileExplorer } from 'momoi-explorer/ui'
70
+ import 'momoi-explorer/ui/style.css'
71
+
72
+ function App() {
73
+ return (
74
+ <FileExplorer
75
+ adapter={adapter}
76
+ rootPath="/home/user/project"
77
+ onOpen={(path) => openFile(path)}
78
+ onEvent={(e) => console.log('tree event:', e)}
79
+ showFilterBar
80
+ />
81
+ )
82
+ }
83
+ ```
84
+
85
+ ### 2b. React hooks でカスタム UI を構築する
86
+
87
+ ```tsx
88
+ import { TreeProvider, useFileTree, useTreeNode } from 'momoi-explorer/react'
89
+
90
+ function App() {
91
+ return (
92
+ <TreeProvider adapter={adapter} rootPath="/home/user/project">
93
+ <MyCustomTree />
94
+ </TreeProvider>
95
+ )
96
+ }
97
+
98
+ function MyCustomTree() {
99
+ const { flatList, controller } = useFileTree()
100
+
101
+ return (
102
+ <div>
103
+ {flatList.map(({ node, depth }) => (
104
+ <div key={node.path} style={{ paddingLeft: depth * 16 }}>
105
+ <span onClick={() => controller.toggleExpand(node.path)}>
106
+ {node.name}
107
+ </span>
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )
112
+ }
113
+ ```
114
+
115
+ ### 2c. コアのみ使用(フレームワーク非依存)
116
+
117
+ ```ts
118
+ import { createFileTree } from 'momoi-explorer'
119
+
120
+ const tree = createFileTree({
121
+ adapter,
122
+ rootPath: '/home/user/project',
123
+ onEvent: (e) => console.log(e),
124
+ })
125
+
126
+ // 状態購読
127
+ tree.subscribe((state) => {
128
+ console.log('nodes:', state.rootNodes)
129
+ console.log('flatList:', state.flatList)
130
+ })
131
+
132
+ // ツリーを読み込み
133
+ await tree.loadRoot()
134
+
135
+ // 操作
136
+ await tree.expand('/home/user/project/src')
137
+ tree.select('/home/user/project/src/index.ts')
138
+ tree.setSearchQuery('config')
139
+
140
+ // 後始末
141
+ tree.destroy()
142
+ ```
143
+
144
+ ## API リファレンス
145
+
146
+ ### コア層 (`momoi-explorer`)
147
+
148
+ #### `createFileTree(options): FileTreeController`
149
+
150
+ ヘッドレスファイルツリーのメインエントリポイント。
151
+
152
+ **FileTreeOptions:**
153
+ | プロパティ | 型 | 説明 |
154
+ |---|---|---|
155
+ | `adapter` | `FileSystemAdapter` | ファイルシステムアダプタ(必須) |
156
+ | `rootPath` | `string` | ルートディレクトリの絶対パス |
157
+ | `sort` | `(a, b) => number` | カスタムソート関数 |
158
+ | `filter` | `(entry) => boolean` | カスタムフィルタ関数 |
159
+ | `watchOptions` | `WatchOptions` | ファイル監視設定 |
160
+ | `onEvent` | `(event: TreeEvent) => void` | イベントコールバック |
161
+
162
+ **FileTreeController のメソッド:**
163
+
164
+ | メソッド | 説明 |
165
+ |---|---|
166
+ | `getState()` | 現在の TreeState を取得 |
167
+ | `subscribe(listener)` | 状態変更を購読。unsubscribe関数を返す |
168
+ | `loadRoot()` | ルートを読み込み・初期化(最初に必ず呼ぶ) |
169
+ | `expand(path)` | ディレクトリを展開 |
170
+ | `collapse(path)` | ディレクトリを折りたたみ |
171
+ | `toggleExpand(path)` | 展開/折りたたみをトグル |
172
+ | `expandTo(path)` | 指定パスまで祖先をすべて展開 |
173
+ | `select(path, mode?)` | ノードを選択(mode: 'replace' / 'toggle' / 'range') |
174
+ | `selectAll()` | 全ノードを選択 |
175
+ | `clearSelection()` | 選択解除 |
176
+ | `startRename(path)` | リネームモード開始 |
177
+ | `commitRename(newName)` | リネーム確定 |
178
+ | `cancelRename()` | リネームキャンセル |
179
+ | `startCreate(parentPath, isDirectory)` | インライン新規作成モード開始 |
180
+ | `commitCreate(name)` | 新規作成確定 |
181
+ | `cancelCreate()` | 新規作成キャンセル |
182
+ | `createFile(parentPath, name)` | ファイル作成 |
183
+ | `createDir(parentPath, name)` | フォルダ作成 |
184
+ | `deleteSelected()` | 選択中のアイテムを削除 |
185
+ | `refresh(path?)` | ツリーをリフレッシュ(展開状態は保持) |
186
+ | `setSearchQuery(query)` | ファジー検索クエリ設定(nullで解除) |
187
+ | `collectAllFiles()` | 全ファイルを再帰収集(QuickOpen用) |
188
+ | `setFilter(fn)` | フィルタ関数を動的変更 |
189
+ | `setSort(fn)` | ソート関数を動的変更 |
190
+ | `destroy()` | コントローラ破棄(監視停止・購読解除) |
191
+
192
+ #### ユーティリティ関数
193
+
194
+ | 関数 | 説明 |
195
+ |---|---|
196
+ | `flattenTree(nodes, expandedPaths, matchingPaths?)` | ツリーをフラットリストに変換 |
197
+ | `computeSelection(current, anchor, target, mode, flatList)` | 選択状態を計算 |
198
+ | `fuzzyMatch(query, target)` | ファジーマッチ(match + score) |
199
+ | `fuzzyFind(files, query, maxResults?)` | スコア順にファジー検索 |
200
+ | `findMatchingPaths(nodes, query)` | マッチするパスのSetを返す |
201
+ | `coalesceEvents(raw)` | 生イベントを合体処理 |
202
+ | `createEventProcessor(callback, options?)` | デバウンス付きイベントプロセッサ |
203
+ | `defaultSort(a, b)` | デフォルトソート(フォルダ優先・名前昇順) |
204
+ | `defaultFilter(entry)` | デフォルトフィルタ(全表示) |
205
+
206
+ ### React層 (`momoi-explorer/react`)
207
+
208
+ | エクスポート | 種別 | 説明 |
209
+ |---|---|---|
210
+ | `TreeProvider` | コンポーネント | ファイルツリーのコンテキストプロバイダー。内部で `createFileTree` + `loadRoot` を行う |
211
+ | `useFileTree()` | Hook | ツリー全体の状態とコントローラを返す |
212
+ | `useTreeNode(path)` | Hook | 個別ノードの展開/選択/リネーム状態を返す(見つからない場合 null) |
213
+ | `useContextMenu()` | Hook | 右クリックメニューの表示制御(show/hide + 座標管理) |
214
+ | `useTreeContext()` | Hook | TreeContext の生の値を取得(通常は useFileTree を使う) |
215
+
216
+ ### UI層 (`momoi-explorer/ui`)
217
+
218
+ | エクスポート | 説明 |
219
+ |---|---|
220
+ | `FileExplorer` | オールインワンコンポーネント(TreeProvider内包、仮想スクロール、コンテキストメニュー対応) |
221
+ | `TreeNodeRow` | ツリーの1行コンポーネント(アイコン、インデント、選択、リネーム対応) |
222
+ | `ContextMenu` | 右クリックメニュー(外側クリック/Escで閉じる) |
223
+ | `InlineRename` | インライン名前変更input(Enter確定、Escキャンセル) |
224
+ | `TreeFilterBar` | ファジー検索フィルタバー |
225
+ | `QuickOpen` | VSCode風クイックオープンダイアログ(Ctrl+P相当) |
226
+
227
+ **スタイル:**
228
+
229
+ ```ts
230
+ import 'momoi-explorer/ui/style.css'
231
+ ```
232
+
233
+ VSCode風ダークテーマ。CSS変数やクラス名(`.momoi-explorer-*`)でカスタマイズ可能。
234
+
235
+ ### FileExplorer の Props
236
+
237
+ ```tsx
238
+ <FileExplorer
239
+ adapter={adapter} // FileSystemAdapter(必須)
240
+ rootPath="/path/to/dir" // ルートパス(必須)
241
+ sort={(a, b) => ...} // カスタムソート
242
+ filter={(entry) => ...} // カスタムフィルタ
243
+ watchOptions={{ ... }} // ファイル監視設定
244
+ onEvent={(e) => ...} // ツリーイベントコールバック
245
+ onOpen={(path) => ...} // ファイルダブルクリック時
246
+ renderIcon={(node, expanded) => ...} // カスタムアイコン
247
+ renderBadge={(node) => ...} // カスタムバッジ(git status等)
248
+ contextMenuItems={(nodes) => [...]} // コンテキストメニュー項目
249
+ showFilterBar // フィルタバー表示
250
+ onControllerReady={(ctrl) => ...} // コントローラ参照の取得
251
+ className="my-explorer" // CSSクラス
252
+ style={{ height: 400 }} // インラインスタイル
253
+ />
254
+ ```
255
+
256
+ ### QuickOpen の使い方
257
+
258
+ ```tsx
259
+ import { FileExplorer, QuickOpen } from 'momoi-explorer/ui'
260
+
261
+ function App() {
262
+ const [ctrl, setCtrl] = useState<FileTreeController | null>(null)
263
+ const [quickOpen, setQuickOpen] = useState(false)
264
+
265
+ return (
266
+ <>
267
+ <FileExplorer
268
+ adapter={adapter}
269
+ rootPath={rootPath}
270
+ onControllerReady={setCtrl}
271
+ />
272
+ {ctrl && (
273
+ <QuickOpen
274
+ controller={ctrl}
275
+ isOpen={quickOpen}
276
+ onClose={() => setQuickOpen(false)}
277
+ onSelect={(entry) => openFile(entry.path)}
278
+ />
279
+ )}
280
+ </>
281
+ )
282
+ }
283
+ ```
284
+
285
+ ## 主要な型
286
+
287
+ ```ts
288
+ interface FileEntry {
289
+ name: string // ファイル名
290
+ path: string // 絶対パス
291
+ isDirectory: boolean // ディレクトリか
292
+ meta?: Record<string, unknown> // 拡張用メタデータ
293
+ }
294
+
295
+ interface TreeNode extends FileEntry {
296
+ depth: number
297
+ children?: TreeNode[]
298
+ childrenLoaded: boolean
299
+ }
300
+
301
+ interface FlatNode {
302
+ node: TreeNode
303
+ depth: number
304
+ }
305
+
306
+ interface TreeState {
307
+ rootPath: string
308
+ rootNodes: TreeNode[]
309
+ expandedPaths: Set<string>
310
+ selectedPaths: Set<string>
311
+ anchorPath: string | null
312
+ renamingPath: string | null
313
+ creatingState: CreatingState | null
314
+ searchQuery: string | null
315
+ flatList: FlatNode[]
316
+ }
317
+
318
+ type TreeEvent =
319
+ | { type: 'expand'; path: string }
320
+ | { type: 'collapse'; path: string }
321
+ | { type: 'select'; paths: string[] }
322
+ | { type: 'open'; path: string }
323
+ | { type: 'rename'; oldPath: string; newPath: string }
324
+ | { type: 'delete'; paths: string[] }
325
+ | { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
326
+ | { type: 'refresh'; path?: string }
327
+ | { type: 'external-change'; changes: WatchEvent[] }
328
+ ```
329
+
330
+ ## ファイル監視
331
+
332
+ `adapter.watch` を実装すると自動でファイル監視が有効になる。生イベントをそのまま投げるだけでよく、以下の処理はコアが自動で行う:
333
+
334
+ - **デバウンス** (75ms, VSCode準拠)
335
+ - **イベント合体**: rename → delete+create、delete+create(同一パス) → modify、親フォルダ削除時に子イベント除去
336
+ - **スロットリング**: 大量イベント時にチャンク分割(500件/200ms間隔)
337
+
338
+ ```ts
339
+ const tree = createFileTree({
340
+ adapter,
341
+ rootPath: '/project',
342
+ watchOptions: {
343
+ debounceMs: 100, // デフォルト: 75
344
+ coalesce: true, // デフォルト: true
345
+ throttle: {
346
+ maxChunkSize: 1000, // デフォルト: 500
347
+ delayMs: 300, // デフォルト: 200
348
+ },
349
+ },
350
+ })
351
+ ```
352
+
353
+ ## ライセンス
354
+
355
+ MIT