@lova/mem-vfs 0.1.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.
Files changed (58) hide show
  1. package/README.md +222 -0
  2. package/dist/core/vfs-directory.d.ts +35 -0
  3. package/dist/core/vfs-directory.d.ts.map +1 -0
  4. package/dist/core/vfs-directory.js +78 -0
  5. package/dist/core/vfs-directory.js.map +1 -0
  6. package/dist/core/vfs-file.d.ts +25 -0
  7. package/dist/core/vfs-file.d.ts.map +1 -0
  8. package/dist/core/vfs-file.js +60 -0
  9. package/dist/core/vfs-file.js.map +1 -0
  10. package/dist/core/vfs-node.d.ts +42 -0
  11. package/dist/core/vfs-node.d.ts.map +1 -0
  12. package/dist/core/vfs-node.js +69 -0
  13. package/dist/core/vfs-node.js.map +1 -0
  14. package/dist/core/vfs-symlink.d.ts +21 -0
  15. package/dist/core/vfs-symlink.d.ts.map +1 -0
  16. package/dist/core/vfs-symlink.js +41 -0
  17. package/dist/core/vfs-symlink.js.map +1 -0
  18. package/dist/core/vfs.d.ts +107 -0
  19. package/dist/core/vfs.d.ts.map +1 -0
  20. package/dist/core/vfs.js +775 -0
  21. package/dist/core/vfs.js.map +1 -0
  22. package/dist/errors/file-system-errors.d.ts +79 -0
  23. package/dist/errors/file-system-errors.d.ts.map +1 -0
  24. package/dist/errors/file-system-errors.js +127 -0
  25. package/dist/errors/file-system-errors.js.map +1 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +23 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/path/path-normalizer.d.ts +23 -0
  31. package/dist/path/path-normalizer.d.ts.map +1 -0
  32. package/dist/path/path-normalizer.js +159 -0
  33. package/dist/path/path-normalizer.js.map +1 -0
  34. package/dist/path/path-resolver.d.ts +31 -0
  35. package/dist/path/path-resolver.d.ts.map +1 -0
  36. package/dist/path/path-resolver.js +68 -0
  37. package/dist/path/path-resolver.js.map +1 -0
  38. package/dist/path/path-validator.d.ts +12 -0
  39. package/dist/path/path-validator.d.ts.map +1 -0
  40. package/dist/path/path-validator.js +87 -0
  41. package/dist/path/path-validator.js.map +1 -0
  42. package/dist/types/index.d.ts +171 -0
  43. package/dist/types/index.d.ts.map +1 -0
  44. package/dist/types/index.js +29 -0
  45. package/dist/types/index.js.map +1 -0
  46. package/dist/watcher/debouncer.d.ts +31 -0
  47. package/dist/watcher/debouncer.d.ts.map +1 -0
  48. package/dist/watcher/debouncer.js +66 -0
  49. package/dist/watcher/debouncer.js.map +1 -0
  50. package/dist/watcher/watcher-events.d.ts +30 -0
  51. package/dist/watcher/watcher-events.d.ts.map +1 -0
  52. package/dist/watcher/watcher-events.js +52 -0
  53. package/dist/watcher/watcher-events.js.map +1 -0
  54. package/dist/watcher/watcher.d.ts +57 -0
  55. package/dist/watcher/watcher.d.ts.map +1 -0
  56. package/dist/watcher/watcher.js +194 -0
  57. package/dist/watcher/watcher.js.map +1 -0
  58. package/package.json +53 -0
@@ -0,0 +1,775 @@
1
+ /**
2
+ * VirtualFileSystem 主類別
3
+ * 記憶體虛擬檔案系統實作
4
+ */
5
+ import { DiffType } from '../types/index.js';
6
+ import { VFSFile } from './vfs-file.js';
7
+ import { VFSDirectory } from './vfs-directory.js';
8
+ import { VFSSymlink } from './vfs-symlink.js';
9
+ import { normalizePath, dirname, basename, join, resolvePath, } from '../path/path-resolver.js';
10
+ import { VFSWatcher } from '../watcher/watcher.js';
11
+ import { FileNotFoundError, DirectoryNotFoundError, DirectoryNotEmptyError, NotAFileError, NotADirectoryError, NotASymlinkError, SymlinkLoopError, FileAlreadyExistsError, } from '../errors/file-system-errors.js';
12
+ /** 預設選項 */
13
+ const DEFAULT_OPTIONS = {
14
+ caseSensitive: true,
15
+ defaultFileMode: 0o644,
16
+ defaultDirectoryMode: 0o755,
17
+ maxSymlinkDepth: 40,
18
+ };
19
+ /** VirtualFileSystem 類別 */
20
+ export class VirtualFileSystem {
21
+ /** 根目錄 */
22
+ root;
23
+ /** 選項 */
24
+ options;
25
+ /** 快照儲存 */
26
+ snapshots = new Map();
27
+ /** 快照計數器 */
28
+ snapshotCounter = 0;
29
+ /** 監聽器列表 */
30
+ watchers = new Set();
31
+ constructor(options) {
32
+ this.options = { ...DEFAULT_OPTIONS, ...options };
33
+ this.root = new VFSDirectory('', this.options.defaultDirectoryMode);
34
+ }
35
+ // ============================================================
36
+ // 檔案操作
37
+ // ============================================================
38
+ /** 讀取檔案 */
39
+ async readFile(filePath, encoding) {
40
+ const node = this.resolveNode(filePath, true);
41
+ if (!node) {
42
+ throw new FileNotFoundError(filePath);
43
+ }
44
+ if (node.isDirectory) {
45
+ throw new NotAFileError(filePath);
46
+ }
47
+ if (node.isFile) {
48
+ return node.read(encoding);
49
+ }
50
+ throw new FileNotFoundError(filePath);
51
+ }
52
+ /** 寫入檔案 */
53
+ async writeFile(filePath, content, _options) {
54
+ const { parentPath, name } = resolvePath(filePath);
55
+ // 確保父目錄存在
56
+ await this.createDirectory(parentPath, true);
57
+ const parent = this.getDirectory(parentPath);
58
+ const existing = parent.getChild(name);
59
+ if (existing) {
60
+ if (existing.isDirectory) {
61
+ throw new NotAFileError(filePath);
62
+ }
63
+ // 更新現有檔案
64
+ if (existing.isFile) {
65
+ existing.write(content);
66
+ this.notifyWatchers(resolvePath(filePath).fullPath, 'change');
67
+ return;
68
+ }
69
+ // 如果是符號連結,跟隨連結
70
+ const resolved = this.resolveNode(filePath, true);
71
+ if (resolved?.isFile) {
72
+ resolved.write(content);
73
+ this.notifyWatchers(resolvePath(filePath).fullPath, 'change');
74
+ return;
75
+ }
76
+ }
77
+ // 建立新檔案
78
+ const file = new VFSFile(name, content, this.options.defaultFileMode);
79
+ parent.addChild(file);
80
+ // 通知 watcher
81
+ this.notifyWatchers(resolvePath(filePath).fullPath, 'add');
82
+ }
83
+ /** 追加檔案內容 */
84
+ async appendFile(filePath, content) {
85
+ const node = this.resolveNode(filePath, true);
86
+ if (!node) {
87
+ // 如果檔案不存在,建立新檔案
88
+ await this.writeFile(filePath, content);
89
+ return;
90
+ }
91
+ if (!node.isFile) {
92
+ throw new NotAFileError(filePath);
93
+ }
94
+ node.append(content);
95
+ }
96
+ /** 刪除檔案 */
97
+ async deleteFile(filePath) {
98
+ const { parentPath, name, fullPath } = resolvePath(filePath);
99
+ const parent = this.getDirectoryOrNull(parentPath);
100
+ if (!parent) {
101
+ throw new FileNotFoundError(fullPath);
102
+ }
103
+ const node = parent.getChild(name);
104
+ if (!node) {
105
+ throw new FileNotFoundError(fullPath);
106
+ }
107
+ if (node.isDirectory) {
108
+ throw new NotAFileError(fullPath);
109
+ }
110
+ parent.removeChild(name);
111
+ this.notifyWatchers(fullPath, 'unlink');
112
+ }
113
+ // ============================================================
114
+ // 目錄操作
115
+ // ============================================================
116
+ /** 建立目錄 */
117
+ async createDirectory(dirPath, recursive = false) {
118
+ const { fullPath, segments } = resolvePath(dirPath);
119
+ if (fullPath === '/') {
120
+ return; // 根目錄已存在
121
+ }
122
+ let current = this.root;
123
+ let currentPath = '';
124
+ for (let i = 0; i < segments.length; i++) {
125
+ const segment = segments[i];
126
+ currentPath = currentPath + '/' + segment;
127
+ const child = current.getChild(segment);
128
+ if (child) {
129
+ if (child.isDirectory) {
130
+ current = child;
131
+ }
132
+ else if (child.isSymlink) {
133
+ // 跟隨符號連結
134
+ const resolved = this.resolveNode(currentPath, true);
135
+ if (resolved?.isDirectory) {
136
+ current = resolved;
137
+ }
138
+ else {
139
+ throw new NotADirectoryError(currentPath);
140
+ }
141
+ }
142
+ else {
143
+ throw new NotADirectoryError(currentPath);
144
+ }
145
+ }
146
+ else {
147
+ // 目錄不存在
148
+ if (!recursive && i < segments.length - 1) {
149
+ throw new DirectoryNotFoundError(currentPath);
150
+ }
151
+ const newDir = new VFSDirectory(segment, this.options.defaultDirectoryMode);
152
+ current.addChild(newDir);
153
+ this.notifyWatchers(currentPath, 'addDir');
154
+ current = newDir;
155
+ }
156
+ }
157
+ }
158
+ /** 讀取目錄內容 */
159
+ async readDirectory(dirPath) {
160
+ const dir = this.getDirectory(dirPath);
161
+ const normalized = normalizePath(dirPath);
162
+ const entries = [];
163
+ for (const node of dir.getChildren()) {
164
+ const entryPath = normalized === '/' ? `/${node.name}` : `${normalized}/${node.name}`;
165
+ entries.push({
166
+ name: node.name,
167
+ path: entryPath,
168
+ isFile: node.isFile,
169
+ isDirectory: node.isDirectory,
170
+ isSymlink: node.isSymlink,
171
+ size: node.size,
172
+ modifiedTime: node.modifiedTime,
173
+ });
174
+ }
175
+ return entries;
176
+ }
177
+ /** 刪除目錄 */
178
+ async deleteDirectory(dirPath, recursive = false) {
179
+ const { parentPath, name, fullPath, isRoot } = resolvePath(dirPath);
180
+ if (isRoot) {
181
+ if (!recursive) {
182
+ throw new DirectoryNotEmptyError('/');
183
+ }
184
+ // 清空根目錄
185
+ for (const childName of this.root.getChildNames()) {
186
+ this.root.removeChild(childName);
187
+ }
188
+ return;
189
+ }
190
+ const parent = this.getDirectory(parentPath);
191
+ const node = parent.getChild(name);
192
+ if (!node) {
193
+ throw new DirectoryNotFoundError(fullPath);
194
+ }
195
+ if (!node.isDirectory) {
196
+ throw new NotADirectoryError(fullPath);
197
+ }
198
+ const dir = node;
199
+ if (!recursive && !dir.isEmpty) {
200
+ throw new DirectoryNotEmptyError(fullPath);
201
+ }
202
+ parent.removeChild(name);
203
+ this.notifyWatchers(fullPath, 'unlinkDir');
204
+ }
205
+ // ============================================================
206
+ // 狀態查詢
207
+ // ============================================================
208
+ /** 檢查路徑是否存在 */
209
+ async exists(targetPath) {
210
+ try {
211
+ const node = this.resolveNode(targetPath, false);
212
+ return node !== null;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
218
+ /** 取得檔案統計 */
219
+ async getStats(targetPath) {
220
+ const node = this.resolveNode(targetPath, true);
221
+ if (!node) {
222
+ throw new FileNotFoundError(targetPath);
223
+ }
224
+ return node.getStats();
225
+ }
226
+ /** 取得符號連結統計(不跟隨連結) */
227
+ async getLinkStats(targetPath) {
228
+ const node = this.resolveNode(targetPath, false);
229
+ if (!node) {
230
+ throw new FileNotFoundError(targetPath);
231
+ }
232
+ return node.getStats();
233
+ }
234
+ /** 檢查是否為檔案 */
235
+ async isFile(targetPath) {
236
+ try {
237
+ const node = this.resolveNode(targetPath, true);
238
+ return node?.isFile ?? false;
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }
244
+ /** 檢查是否為目錄 */
245
+ async isDirectory(targetPath) {
246
+ try {
247
+ const node = this.resolveNode(targetPath, true);
248
+ return node?.isDirectory ?? false;
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ }
254
+ /** 檢查是否為符號連結 */
255
+ async isSymlink(targetPath) {
256
+ try {
257
+ const node = this.resolveNode(targetPath, false);
258
+ return node?.isSymlink ?? false;
259
+ }
260
+ catch {
261
+ return false;
262
+ }
263
+ }
264
+ // ============================================================
265
+ // 複製與移動
266
+ // ============================================================
267
+ /** 複製檔案 */
268
+ async copyFile(srcPath, destPath) {
269
+ const content = await this.readFile(srcPath);
270
+ await this.writeFile(destPath, content);
271
+ }
272
+ /** 移動檔案 */
273
+ async moveFile(srcPath, destPath) {
274
+ await this.copyFile(srcPath, destPath);
275
+ await this.deleteFile(srcPath);
276
+ }
277
+ // ============================================================
278
+ // 符號連結
279
+ // ============================================================
280
+ /** 建立符號連結 */
281
+ async createSymlink(target, linkPath) {
282
+ const { parentPath, name } = resolvePath(linkPath);
283
+ // 確保父目錄存在
284
+ await this.createDirectory(parentPath, true);
285
+ const parent = this.getDirectory(parentPath);
286
+ if (parent.hasChild(name)) {
287
+ throw new FileAlreadyExistsError(linkPath);
288
+ }
289
+ const symlink = new VFSSymlink(name, target);
290
+ parent.addChild(symlink);
291
+ }
292
+ /** 讀取符號連結目標 */
293
+ async readSymlink(linkPath) {
294
+ const node = this.resolveNode(linkPath, false);
295
+ if (!node) {
296
+ throw new FileNotFoundError(linkPath);
297
+ }
298
+ if (!node.isSymlink) {
299
+ throw new NotASymlinkError(linkPath);
300
+ }
301
+ return node.target;
302
+ }
303
+ // ============================================================
304
+ // Glob 搜尋
305
+ // ============================================================
306
+ /** Glob 搜尋 */
307
+ async glob(pattern, options) {
308
+ const cwd = options?.cwd ? normalizePath(options.cwd) : '/';
309
+ const maxDepth = options?.maxDepth ?? Infinity;
310
+ const onlyFiles = options?.onlyFiles ?? false;
311
+ const onlyDirectories = options?.onlyDirectories ?? false;
312
+ const followSymlinks = options?.followSymlinks ?? true;
313
+ const dot = options?.dot ?? false;
314
+ const absolute = options?.absolute ?? true;
315
+ const ignore = options?.ignore ?? [];
316
+ const results = [];
317
+ const regex = this.patternToRegex(pattern);
318
+ const traverse = (dir, currentPath, depth) => {
319
+ if (depth > maxDepth) {
320
+ return;
321
+ }
322
+ for (const node of dir.getChildren()) {
323
+ const nodePath = currentPath === '/' ? `/${node.name}` : `${currentPath}/${node.name}`;
324
+ const relativePath = nodePath.slice(cwd.length + 1) || node.name;
325
+ // 檢查是否為隱藏檔案
326
+ if (!dot && node.name.startsWith('.')) {
327
+ continue;
328
+ }
329
+ // 檢查忽略規則
330
+ if (this.matchesIgnore(nodePath, ignore)) {
331
+ continue;
332
+ }
333
+ // 處理符號連結
334
+ let effectiveNode = node;
335
+ if (node.isSymlink && followSymlinks) {
336
+ const resolved = this.resolveNode(nodePath, true);
337
+ if (resolved) {
338
+ effectiveNode = resolved;
339
+ }
340
+ else {
341
+ continue; // 斷開的符號連結
342
+ }
343
+ }
344
+ // 檢查是否符合 pattern
345
+ if (regex.test(relativePath)) {
346
+ const shouldInclude = (!onlyFiles && !onlyDirectories)
347
+ || (onlyFiles && effectiveNode.isFile)
348
+ || (onlyDirectories && effectiveNode.isDirectory);
349
+ if (shouldInclude) {
350
+ results.push(absolute ? nodePath : relativePath);
351
+ }
352
+ }
353
+ // 遞迴處理目錄
354
+ if (effectiveNode.isDirectory) {
355
+ traverse(effectiveNode, nodePath, depth + 1);
356
+ }
357
+ }
358
+ };
359
+ const startDir = this.getDirectoryOrNull(cwd);
360
+ if (startDir) {
361
+ traverse(startDir, cwd, 0);
362
+ }
363
+ return results.sort();
364
+ }
365
+ // ============================================================
366
+ // 快照與回滾
367
+ // ============================================================
368
+ /** 建立快照 */
369
+ createSnapshot(name) {
370
+ const id = `snapshot-${++this.snapshotCounter}`;
371
+ const clonedRoot = this.root.clone();
372
+ const { fileCount, directoryCount, totalSize } = this.countNodes(this.root);
373
+ const info = {
374
+ id,
375
+ name,
376
+ createdAt: new Date(),
377
+ fileCount,
378
+ directoryCount,
379
+ totalSize,
380
+ };
381
+ this.snapshots.set(id, { root: clonedRoot, info });
382
+ return id;
383
+ }
384
+ /** 還原快照 */
385
+ restoreSnapshot(id) {
386
+ const snapshot = this.snapshots.get(id);
387
+ if (!snapshot) {
388
+ throw new Error(`Snapshot not found: ${id}`);
389
+ }
390
+ // 清空當前根目錄
391
+ for (const name of this.root.getChildNames()) {
392
+ this.root.removeChild(name);
393
+ }
394
+ // 複製快照內容
395
+ const cloned = snapshot.root.clone();
396
+ for (const [, node] of cloned.entries()) {
397
+ this.root.addChild(node);
398
+ }
399
+ }
400
+ /** 取得快照資訊 */
401
+ getSnapshotInfo(id) {
402
+ return this.snapshots.get(id)?.info;
403
+ }
404
+ /** 列出所有快照 */
405
+ listSnapshots() {
406
+ return Array.from(this.snapshots.values()).map(s => s.info);
407
+ }
408
+ /** 刪除快照 */
409
+ deleteSnapshot(id) {
410
+ return this.snapshots.delete(id);
411
+ }
412
+ /** 計算兩個快照之間的差異 */
413
+ diff(fromId, toId) {
414
+ const fromRoot = fromId ? this.snapshots.get(fromId)?.root : undefined;
415
+ const toRoot = toId ? this.snapshots.get(toId)?.root : this.root;
416
+ if (fromId && !fromRoot) {
417
+ throw new Error(`Snapshot not found: ${fromId}`);
418
+ }
419
+ if (toId && !toRoot) {
420
+ throw new Error(`Snapshot not found: ${toId}`);
421
+ }
422
+ const diffs = [];
423
+ this.computeDiff(fromRoot ?? new VFSDirectory(''), toRoot, '', diffs);
424
+ return diffs;
425
+ }
426
+ // ============================================================
427
+ // 工具方法
428
+ // ============================================================
429
+ /** 從 JSON 結構載入 */
430
+ async fromJSON(structure, basePath = '/') {
431
+ // 偵測格式:如果任何 key 以 / 開頭或包含 / 且 value 是字串,視為平面路徑格式
432
+ const isFlatFormat = Object.entries(structure).some(([key, value]) => (key.startsWith('/') || key.includes('/')) &&
433
+ (typeof value === 'string' || Buffer.isBuffer(value) || value === null));
434
+ if (isFlatFormat) {
435
+ // 平面路徑格式:{ '/path/to/file.ts': 'content' }
436
+ await this.fromFlatJSON(structure);
437
+ }
438
+ else {
439
+ // 嵌套結構格式:{ 'dir': { 'file.ts': 'content' } }
440
+ await this.fromNestedJSON(structure, basePath);
441
+ }
442
+ }
443
+ /** 從平面路徑 JSON 結構載入 */
444
+ async fromFlatJSON(structure) {
445
+ for (const [path, value] of Object.entries(structure)) {
446
+ if (value === null || value === undefined) {
447
+ // null/undefined 表示目錄
448
+ await this.createDirectory(path, true);
449
+ }
450
+ else if (typeof value === 'string' || Buffer.isBuffer(value)) {
451
+ // 字串或 Buffer 表示檔案
452
+ await this.writeFile(path, value);
453
+ }
454
+ // 忽略物件(平面格式不應有嵌套物件)
455
+ }
456
+ }
457
+ /** 從嵌套結構 JSON 載入 */
458
+ async fromNestedJSON(structure, basePath) {
459
+ for (const [key, value] of Object.entries(structure)) {
460
+ const fullPath = join(basePath, key);
461
+ if (value === null) {
462
+ // null 表示目錄
463
+ await this.createDirectory(fullPath, true);
464
+ }
465
+ else if (typeof value === 'string' || Buffer.isBuffer(value)) {
466
+ // 字串或 Buffer 表示檔案
467
+ await this.writeFile(fullPath, value);
468
+ }
469
+ else if (typeof value === 'object') {
470
+ // 巢狀物件表示子目錄
471
+ await this.createDirectory(fullPath, true);
472
+ await this.fromNestedJSON(value, fullPath);
473
+ }
474
+ }
475
+ }
476
+ /** 輸出為 JSON 結構 */
477
+ toJSON(basePath = '/', options) {
478
+ if (options?.flatten) {
479
+ return this.toFlatJSON(basePath);
480
+ }
481
+ return this.toNestedJSON(basePath);
482
+ }
483
+ /** 輸出為平面路徑 JSON 結構 */
484
+ toFlatJSON(basePath) {
485
+ const result = {};
486
+ const traverse = (dir, currentPath) => {
487
+ for (const node of dir.getChildren()) {
488
+ const nodePath = currentPath === '/' ? `/${node.name}` : `${currentPath}/${node.name}`;
489
+ if (node.isFile) {
490
+ const content = node.read();
491
+ result[nodePath] = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
492
+ }
493
+ else if (node.isDirectory) {
494
+ traverse(node, nodePath);
495
+ }
496
+ else if (node.isSymlink) {
497
+ result[nodePath] = `symlink:${node.target}`;
498
+ }
499
+ }
500
+ };
501
+ const startDir = this.getDirectoryOrNull(basePath);
502
+ if (startDir) {
503
+ traverse(startDir, basePath === '/' ? '' : basePath);
504
+ }
505
+ return result;
506
+ }
507
+ /** 輸出為嵌套結構 JSON */
508
+ toNestedJSON(basePath) {
509
+ const result = {};
510
+ const dir = this.getDirectoryOrNull(basePath);
511
+ if (!dir) {
512
+ return result;
513
+ }
514
+ for (const node of dir.getChildren()) {
515
+ if (node.isFile) {
516
+ const content = node.read();
517
+ result[node.name] = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
518
+ }
519
+ else if (node.isDirectory) {
520
+ const subPath = basePath === '/' ? `/${node.name}` : `${basePath}/${node.name}`;
521
+ const subContent = this.toNestedJSON(subPath);
522
+ result[node.name] = Object.keys(subContent).length > 0 ? subContent : null;
523
+ }
524
+ else if (node.isSymlink) {
525
+ result[node.name] = `symlink:${node.target}`;
526
+ }
527
+ }
528
+ return result;
529
+ }
530
+ /** 重置檔案系統 */
531
+ reset() {
532
+ for (const name of this.root.getChildNames()) {
533
+ this.root.removeChild(name);
534
+ }
535
+ this.snapshots.clear();
536
+ this.snapshotCounter = 0;
537
+ }
538
+ /** 監聽檔案變更 */
539
+ watch(watchPath, options) {
540
+ const watcher = new VFSWatcher(watchPath, options);
541
+ this.watchers.add(watcher);
542
+ // 初始化已知路徑
543
+ if (!options?.ignoreInitial) {
544
+ this.initializeWatcher(watcher, watchPath);
545
+ }
546
+ // 當 watcher 關閉時從列表移除
547
+ const originalClose = watcher.close.bind(watcher);
548
+ watcher.close = () => {
549
+ this.watchers.delete(watcher);
550
+ originalClose();
551
+ };
552
+ // 發送 ready 事件
553
+ setTimeout(() => watcher.emitReady(), 0);
554
+ return watcher;
555
+ }
556
+ /** 初始化 watcher 的已知路徑 */
557
+ async initializeWatcher(watcher, basePath) {
558
+ const traverse = async (dirPath) => {
559
+ try {
560
+ const entries = await this.readDirectory(dirPath);
561
+ for (const entry of entries) {
562
+ watcher.registerPath(entry.path);
563
+ if (entry.isDirectory) {
564
+ await traverse(entry.path);
565
+ }
566
+ }
567
+ }
568
+ catch {
569
+ // 忽略錯誤
570
+ }
571
+ };
572
+ watcher.registerPath(basePath);
573
+ await traverse(basePath);
574
+ }
575
+ /** 通知所有 watcher 檔案變更 */
576
+ notifyWatchers(path, type) {
577
+ const stats = type !== 'unlink' && type !== 'unlinkDir'
578
+ ? this.resolveNode(path, true)?.getStats()
579
+ : undefined;
580
+ for (const watcher of this.watchers) {
581
+ switch (type) {
582
+ case 'change':
583
+ case 'add':
584
+ watcher.notifyChange(path, stats);
585
+ break;
586
+ case 'unlink':
587
+ watcher.notifyUnlink(path, false);
588
+ break;
589
+ case 'addDir':
590
+ watcher.notifyAddDir(path, stats);
591
+ break;
592
+ case 'unlinkDir':
593
+ watcher.notifyUnlink(path, true);
594
+ break;
595
+ }
596
+ }
597
+ }
598
+ // ============================================================
599
+ // 私有方法
600
+ // ============================================================
601
+ /** 解析節點 */
602
+ resolveNode(inputPath, followSymlinks, depth = 0) {
603
+ if (depth > this.options.maxSymlinkDepth) {
604
+ throw new SymlinkLoopError(inputPath);
605
+ }
606
+ const { segments, isRoot } = resolvePath(inputPath);
607
+ if (isRoot) {
608
+ return this.root;
609
+ }
610
+ let current = this.root;
611
+ for (let i = 0; i < segments.length; i++) {
612
+ const segment = segments[i];
613
+ if (!current.isDirectory) {
614
+ return null;
615
+ }
616
+ const child = current.getChild(segment);
617
+ if (!child) {
618
+ return null;
619
+ }
620
+ // 處理符號連結
621
+ if (child.isSymlink) {
622
+ if (followSymlinks || i < segments.length - 1) {
623
+ // 需要跟隨符號連結
624
+ const symlink = child;
625
+ const targetPath = symlink.target.startsWith('/')
626
+ ? symlink.target
627
+ : join(dirname('/' + segments.slice(0, i).join('/')), symlink.target);
628
+ const resolved = this.resolveNode(targetPath, true, depth + 1);
629
+ if (!resolved) {
630
+ return null;
631
+ }
632
+ if (i === segments.length - 1) {
633
+ return followSymlinks ? resolved : child;
634
+ }
635
+ current = resolved;
636
+ }
637
+ else {
638
+ return child;
639
+ }
640
+ }
641
+ else {
642
+ current = child;
643
+ }
644
+ }
645
+ return current;
646
+ }
647
+ /** 取得目錄節點 */
648
+ getDirectory(dirPath) {
649
+ const node = this.resolveNode(dirPath, true);
650
+ if (!node) {
651
+ throw new DirectoryNotFoundError(dirPath);
652
+ }
653
+ if (!node.isDirectory) {
654
+ throw new NotADirectoryError(dirPath);
655
+ }
656
+ return node;
657
+ }
658
+ /** 取得目錄節點(可能為 null) */
659
+ getDirectoryOrNull(dirPath) {
660
+ try {
661
+ return this.getDirectory(dirPath);
662
+ }
663
+ catch {
664
+ return null;
665
+ }
666
+ }
667
+ /** 將 glob pattern 轉換為正規表示式 */
668
+ patternToRegex(pattern) {
669
+ // 簡單的 glob 到 regex 轉換
670
+ let regex = pattern
671
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // 跳脫特殊字元
672
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>') // 暫時替換 **
673
+ .replace(/\*/g, '[^/]*') // * 匹配任意字元(不含路徑分隔符)
674
+ .replace(/\?/g, '[^/]') // ? 匹配單一字元
675
+ .replace(/<<<GLOBSTAR>>>/g, '.*'); // ** 匹配任意路徑
676
+ return new RegExp(`^${regex}$`);
677
+ }
678
+ /** 檢查是否符合忽略規則 */
679
+ matchesIgnore(path, ignorePatterns) {
680
+ for (const pattern of ignorePatterns) {
681
+ const regex = this.patternToRegex(pattern);
682
+ if (regex.test(path) || regex.test(basename(path))) {
683
+ return true;
684
+ }
685
+ }
686
+ return false;
687
+ }
688
+ /** 計算節點數量 */
689
+ countNodes(dir) {
690
+ let fileCount = 0;
691
+ let directoryCount = 0;
692
+ let totalSize = 0;
693
+ const traverse = (node) => {
694
+ if (node.isFile) {
695
+ fileCount++;
696
+ totalSize += node.size;
697
+ }
698
+ else if (node.isDirectory) {
699
+ directoryCount++;
700
+ for (const child of node.getChildren()) {
701
+ traverse(child);
702
+ }
703
+ }
704
+ };
705
+ traverse(dir);
706
+ return { fileCount, directoryCount, totalSize };
707
+ }
708
+ /** 計算差異 */
709
+ computeDiff(from, to, basePath, diffs) {
710
+ const fromChildren = new Map();
711
+ const toChildren = new Map();
712
+ if (from) {
713
+ for (const node of from.getChildren()) {
714
+ fromChildren.set(node.name, node);
715
+ }
716
+ }
717
+ for (const node of to.getChildren()) {
718
+ toChildren.set(node.name, node);
719
+ }
720
+ // 檢查新增和修改
721
+ for (const [name, toNode] of toChildren) {
722
+ const path = basePath ? `${basePath}/${name}` : `/${name}`;
723
+ const fromNode = fromChildren.get(name);
724
+ if (!fromNode) {
725
+ // 新增
726
+ if (toNode.isFile) {
727
+ diffs.push({
728
+ type: DiffType.Added,
729
+ path,
730
+ newContent: toNode.read(),
731
+ newStats: toNode.getStats(),
732
+ });
733
+ }
734
+ }
735
+ else if (toNode.isFile && fromNode.isFile) {
736
+ // 檢查修改
737
+ const fromContent = fromNode.read();
738
+ const toContent = toNode.read();
739
+ if (!fromContent.equals(toContent)) {
740
+ diffs.push({
741
+ type: DiffType.Modified,
742
+ path,
743
+ oldContent: fromContent,
744
+ newContent: toContent,
745
+ oldStats: fromNode.getStats(),
746
+ newStats: toNode.getStats(),
747
+ });
748
+ }
749
+ }
750
+ // 遞迴處理子目錄
751
+ if (toNode.isDirectory) {
752
+ this.computeDiff(fromNode?.isDirectory ? fromNode : undefined, toNode, path, diffs);
753
+ }
754
+ }
755
+ // 檢查刪除
756
+ for (const [name, fromNode] of fromChildren) {
757
+ if (!toChildren.has(name)) {
758
+ const path = basePath ? `${basePath}/${name}` : `/${name}`;
759
+ if (fromNode.isFile) {
760
+ diffs.push({
761
+ type: DiffType.Deleted,
762
+ path,
763
+ oldContent: fromNode.read(),
764
+ oldStats: fromNode.getStats(),
765
+ });
766
+ }
767
+ }
768
+ }
769
+ }
770
+ }
771
+ /** 建立 VirtualFileSystem 實例 */
772
+ export function createVFS(options) {
773
+ return new VirtualFileSystem(options);
774
+ }
775
+ //# sourceMappingURL=vfs.js.map