sandbox-fs 1.0.0 → 1.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 (60) hide show
  1. package/README.md +16 -0
  2. package/dist/FSModule.d.ts.map +1 -1
  3. package/dist/FSModule.js +81 -18
  4. package/dist/FSModule.js.map +1 -1
  5. package/dist/PathMapper.d.ts.map +1 -1
  6. package/dist/PathMapper.js +147 -41
  7. package/dist/PathMapper.js.map +1 -1
  8. package/dist/VirtualFileSystem.d.ts.map +1 -1
  9. package/dist/VirtualFileSystem.js +27 -1
  10. package/dist/VirtualFileSystem.js.map +1 -1
  11. package/dist/operations/newer.d.ts.map +1 -1
  12. package/dist/operations/newer.js +32 -3
  13. package/dist/operations/newer.js.map +1 -1
  14. package/dist/operations/read.d.ts.map +1 -1
  15. package/dist/operations/read.js +100 -14
  16. package/dist/operations/read.js.map +1 -1
  17. package/dist/utils/ErrorFilter.d.ts.map +1 -1
  18. package/dist/utils/ErrorFilter.js +21 -24
  19. package/dist/utils/ErrorFilter.js.map +1 -1
  20. package/dist/wrappers/VirtualDir.d.ts.map +1 -1
  21. package/dist/wrappers/VirtualDir.js +36 -35
  22. package/dist/wrappers/VirtualDir.js.map +1 -1
  23. package/dist/wrappers/VirtualDirent.d.ts.map +1 -1
  24. package/dist/wrappers/VirtualDirent.js +28 -9
  25. package/dist/wrappers/VirtualDirent.js.map +1 -1
  26. package/package.json +8 -4
  27. package/dist/FSModule.d.ts +0 -153
  28. package/dist/PathMapper.d.ts +0 -30
  29. package/dist/PathModule.d.ts +0 -69
  30. package/dist/ResourceTracker.d.ts +0 -74
  31. package/dist/VirtualFileSystem.d.ts +0 -145
  32. package/dist/index.d.ts +0 -9
  33. package/dist/operations/newer.d.ts +0 -36
  34. package/dist/operations/read.d.ts +0 -24
  35. package/dist/operations/symlink.d.ts +0 -8
  36. package/dist/operations/write.d.ts +0 -29
  37. package/dist/utils/ErrorFilter.d.ts +0 -6
  38. package/dist/utils/callbackify.d.ts +0 -9
  39. package/dist/wrappers/VirtualDir.d.ts +0 -34
  40. package/dist/wrappers/VirtualDirent.d.ts +0 -21
  41. package/example.js +0 -95
  42. package/example.ts +0 -32
  43. package/src/FSModule.ts +0 -546
  44. package/src/PathMapper.ts +0 -102
  45. package/src/PathModule.ts +0 -142
  46. package/src/ResourceTracker.ts +0 -162
  47. package/src/VirtualFileSystem.ts +0 -172
  48. package/src/index.ts +0 -9
  49. package/src/operations/newer.ts +0 -223
  50. package/src/operations/read.ts +0 -319
  51. package/src/operations/symlink.ts +0 -31
  52. package/src/operations/write.ts +0 -189
  53. package/src/utils/ErrorFilter.ts +0 -57
  54. package/src/utils/callbackify.ts +0 -54
  55. package/src/wrappers/VirtualDir.ts +0 -84
  56. package/src/wrappers/VirtualDirent.ts +0 -60
  57. package/test-data/example.txt +0 -1
  58. package/test-data/subdir/nested.txt +0 -1
  59. package/tsconfig.example.json +0 -8
  60. package/tsconfig.json +0 -21
package/src/PathModule.ts DELETED
@@ -1,142 +0,0 @@
1
- import * as path from 'path';
2
-
3
- /**
4
- * Creates a path module that operates in virtual path space
5
- * All paths use Unix-style forward slashes
6
- * Fixed virtual CWD of '/'
7
- */
8
- export function createPathModule() {
9
- const virtualCwd = '/';
10
-
11
- const pathModule = {
12
- /**
13
- * Normalize a path, reducing '..' and '.' parts
14
- */
15
- normalize(p: string): string {
16
- const normalized = path.posix.normalize(p);
17
- // Ensure it starts with / if it's absolute-like
18
- return normalized.startsWith('/') ? normalized : normalized;
19
- },
20
-
21
- /**
22
- * Join all path segments together and normalize the result
23
- */
24
- join(...paths: string[]): string {
25
- return path.posix.join(...paths);
26
- },
27
-
28
- /**
29
- * Resolve a sequence of paths to an absolute path
30
- */
31
- resolve(...paths: string[]): string {
32
- if (paths.length === 0) {
33
- return virtualCwd;
34
- }
35
-
36
- let resolved = virtualCwd;
37
-
38
- for (const p of paths) {
39
- if (path.posix.isAbsolute(p)) {
40
- resolved = p;
41
- } else {
42
- resolved = path.posix.join(resolved, p);
43
- }
44
- }
45
-
46
- return path.posix.normalize(resolved);
47
- },
48
-
49
- /**
50
- * Return the relative path from 'from' to 'to'
51
- */
52
- relative(from: string, to: string): string {
53
- return path.posix.relative(from, to);
54
- },
55
-
56
- /**
57
- * Return the directory name of a path
58
- */
59
- dirname(p: string): string {
60
- return path.posix.dirname(p);
61
- },
62
-
63
- /**
64
- * Return the last portion of a path
65
- */
66
- basename(p: string, ext?: string): string {
67
- return path.posix.basename(p, ext);
68
- },
69
-
70
- /**
71
- * Return the extension of the path
72
- */
73
- extname(p: string): string {
74
- return path.posix.extname(p);
75
- },
76
-
77
- /**
78
- * Parse a path into an object
79
- */
80
- parse(p: string): path.ParsedPath {
81
- const parsed = path.posix.parse(p);
82
- // Ensure root is always '/' for virtual paths
83
- if (parsed.root) {
84
- parsed.root = '/';
85
- }
86
- return parsed;
87
- },
88
-
89
- /**
90
- * Format a path object into a string
91
- */
92
- format(pathObject: path.FormatInputPathObject): string {
93
- // Ensure root is '/' for virtual paths
94
- const virtualPathObject = { ...pathObject };
95
- if (virtualPathObject.root) {
96
- virtualPathObject.root = '/';
97
- }
98
- return path.posix.format(virtualPathObject);
99
- },
100
-
101
- /**
102
- * Check if a path is absolute
103
- */
104
- isAbsolute(p: string): boolean {
105
- return p.startsWith('/');
106
- },
107
-
108
- /**
109
- * Convert path to namespaced path (Windows-only, no-op in virtual space)
110
- */
111
- toNamespacedPath(p: string): string {
112
- // In virtual space, just return the path as-is
113
- return p;
114
- },
115
-
116
- /**
117
- * Path segment separator (always / in virtual space)
118
- */
119
- sep: '/',
120
-
121
- /**
122
- * Path delimiter (always : in virtual space)
123
- */
124
- delimiter: ':',
125
-
126
- /**
127
- * POSIX-specific methods (same as main module in virtual space)
128
- */
129
- posix: null as any,
130
-
131
- /**
132
- * Windows-specific methods (same as main module in virtual space)
133
- */
134
- win32: null as any,
135
- };
136
-
137
- // Both posix and win32 reference the same virtual implementation
138
- pathModule.posix = pathModule;
139
- pathModule.win32 = pathModule;
140
-
141
- return pathModule;
142
- }
@@ -1,162 +0,0 @@
1
- import * as fs from 'fs';
2
- import { Readable, Writable } from 'stream';
3
-
4
- /**
5
- * Information about a tracked file descriptor
6
- */
7
- export interface FDInfo {
8
- fd: number;
9
- virtualPath: string;
10
- realPath: string;
11
- flags: string | number;
12
- mode?: number;
13
- isStream: boolean;
14
- }
15
-
16
- /**
17
- * Manages lifecycle of file descriptors and streams to prevent resource leaks
18
- * and ensure proper cleanup when VFS is closed
19
- */
20
- export class ResourceTracker {
21
- private fdMap: Map<number, FDInfo> = new Map();
22
- private streams: Set<Readable | Writable> = new Set();
23
- private abortControllers: Set<AbortController> = new Set();
24
- private _closed: boolean = false;
25
-
26
- /**
27
- * Check if the VFS has been closed
28
- */
29
- get closed(): boolean {
30
- return this._closed;
31
- }
32
-
33
- /**
34
- * Assert that the VFS is not closed, throwing EBADF if it is
35
- */
36
- assertNotClosed(): void {
37
- if (this._closed) {
38
- const error = new Error('bad file descriptor') as NodeJS.ErrnoException;
39
- error.code = 'EBADF';
40
- error.errno = -9;
41
- error.syscall = 'read';
42
- throw error;
43
- }
44
- }
45
-
46
- /**
47
- * Track a file descriptor
48
- */
49
- trackFD(fd: number, info: Omit<FDInfo, 'fd'>): void {
50
- this.fdMap.set(fd, { fd, ...info });
51
- }
52
-
53
- /**
54
- * Get information about a tracked file descriptor
55
- */
56
- getFD(fd: number): FDInfo | undefined {
57
- return this.fdMap.get(fd);
58
- }
59
-
60
- /**
61
- * Check if a file descriptor is tracked
62
- */
63
- isTracked(fd: number): boolean {
64
- return this.fdMap.has(fd);
65
- }
66
-
67
- /**
68
- * Untrack a file descriptor (when closed)
69
- */
70
- untrackFD(fd: number): FDInfo | undefined {
71
- const info = this.fdMap.get(fd);
72
- this.fdMap.delete(fd);
73
- return info;
74
- }
75
-
76
- /**
77
- * Track a stream
78
- */
79
- trackStream(stream: Readable | Writable): void {
80
- this.streams.add(stream);
81
- }
82
-
83
- /**
84
- * Untrack a stream (when closed)
85
- */
86
- untrackStream(stream: Readable | Writable): void {
87
- this.streams.delete(stream);
88
- }
89
-
90
- /**
91
- * Register an AbortSignal listener for cleanup
92
- */
93
- registerAbortListener(signal: AbortSignal | undefined, cleanup: () => void): void {
94
- if (!signal) return;
95
-
96
- const controller = new AbortController();
97
- this.abortControllers.add(controller);
98
-
99
- signal.addEventListener('abort', () => {
100
- cleanup();
101
- this.abortControllers.delete(controller);
102
- }, { once: true });
103
- }
104
-
105
- /**
106
- * Dispose all tracked resources and mark as closed
107
- */
108
- dispose(): void {
109
- if (this._closed) return;
110
-
111
- // Close all tracked file descriptors
112
- for (const [fd, info] of this.fdMap) {
113
- try {
114
- if (!info.isStream) {
115
- fs.closeSync(fd);
116
- }
117
- } catch (err) {
118
- // Ignore errors during cleanup
119
- }
120
- }
121
- this.fdMap.clear();
122
-
123
- // Destroy all tracked streams
124
- for (const stream of this.streams) {
125
- try {
126
- stream.destroy();
127
- } catch (err) {
128
- // Ignore errors during cleanup
129
- }
130
- }
131
- this.streams.clear();
132
-
133
- // Abort all pending operations
134
- for (const controller of this.abortControllers) {
135
- try {
136
- controller.abort();
137
- } catch (err) {
138
- // Ignore errors during cleanup
139
- }
140
- }
141
- this.abortControllers.clear();
142
-
143
- this._closed = true;
144
- }
145
-
146
- /**
147
- * Get all tracked file descriptors (for debugging)
148
- */
149
- getAllFDs(): FDInfo[] {
150
- return Array.from(this.fdMap.values());
151
- }
152
-
153
- /**
154
- * Get count of tracked resources
155
- */
156
- getResourceCount(): { fds: number; streams: number } {
157
- return {
158
- fds: this.fdMap.size,
159
- streams: this.streams.size,
160
- };
161
- }
162
- }
@@ -1,172 +0,0 @@
1
- import { PathMapper } from './PathMapper';
2
- import { ResourceTracker } from './ResourceTracker';
3
- import { createFSModule } from './FSModule';
4
- import { createPathModule } from './PathModule';
5
-
6
- /**
7
- * Options for creating a VirtualFileSystem
8
- */
9
- export interface VFSOptions {
10
- /**
11
- * The real filesystem path to use as the VFS root.
12
- * Can be a Windows path (C:\data) or Unix path (/opt/data).
13
- * All virtual paths will be mapped relative to this root.
14
- */
15
- root: string;
16
- }
17
-
18
- /**
19
- * A read-only virtual file system that maps Unix-style virtual paths
20
- * to a real directory root.
21
- *
22
- * @example
23
- * ```typescript
24
- * const vfs = new VirtualFileSystem({ root: '/opt/data' });
25
- * const fs = vfs.createNodeFSModule();
26
- * const path = vfs.createNodePathModule();
27
- *
28
- * // Read a file (real path: /opt/data/file.txt)
29
- * const content = fs.readFileSync('/file.txt', 'utf8');
30
- *
31
- * // Write operations are denied
32
- * fs.writeFileSync('/file.txt', 'data'); // throws EACCES
33
- *
34
- * // Clean up when done
35
- * vfs.close();
36
- * ```
37
- */
38
- export class VirtualFileSystem {
39
- private pathMapper: PathMapper;
40
- private resourceTracker: ResourceTracker;
41
-
42
- /**
43
- * Create a new VirtualFileSystem instance
44
- *
45
- * @param options - Configuration options
46
- * @throws Error if root path doesn't exist or is not a directory
47
- */
48
- constructor(options: VFSOptions) {
49
- this.pathMapper = new PathMapper(options.root);
50
- this.resourceTracker = new ResourceTracker();
51
- }
52
-
53
- /**
54
- * Convert a virtual path to a real filesystem path
55
- *
56
- * @param virtualPath - Virtual path (Unix-style, starts with /)
57
- * @returns Real filesystem path (platform-native)
58
- * @throws Error if path traversal is detected
59
- *
60
- * @example
61
- * ```typescript
62
- * const vfs = new VirtualFileSystem({ root: 'C:\\data' });
63
- * vfs.toRealPath('/foo/bar.txt'); // Returns: C:\data\foo\bar.txt
64
- * ```
65
- */
66
- toRealPath(virtualPath: string): string {
67
- return this.pathMapper.toRealPath(virtualPath);
68
- }
69
-
70
- /**
71
- * Convert a real filesystem path to a virtual path
72
- *
73
- * @param realPath - Real filesystem path (platform-native)
74
- * @returns Virtual path (Unix-style, starts with /)
75
- * @throws Error if real path is outside VFS root
76
- *
77
- * @example
78
- * ```typescript
79
- * const vfs = new VirtualFileSystem({ root: 'C:\\data' });
80
- * vfs.toVirtualPath('C:\\data\\foo\\bar.txt'); // Returns: /foo/bar.txt
81
- * ```
82
- */
83
- toVirtualPath(realPath: string): string {
84
- return this.pathMapper.toVirtualPath(realPath);
85
- }
86
-
87
- /**
88
- * Create a Node.js fs-compatible module
89
- *
90
- * Returns an object that matches the Node.js fs module API.
91
- * All read operations work normally, write operations return EACCES,
92
- * and symlink operations return EACCES. Error messages have real paths
93
- * filtered out and replaced with virtual paths.
94
- *
95
- * @returns An fs-compatible object with both callback and promise APIs
96
- *
97
- * @example
98
- * ```typescript
99
- * const vfs = new VirtualFileSystem({ root: '/opt/data' });
100
- * const fs = vfs.createNodeFSModule();
101
- *
102
- * // Callback API
103
- * fs.readFile('/file.txt', 'utf8', (err, data) => {
104
- * if (err) throw err;
105
- * console.log(data);
106
- * });
107
- *
108
- * // Promise API
109
- * const data = await fs.promises.readFile('/file.txt', 'utf8');
110
- *
111
- * // Sync API
112
- * const data = fs.readFileSync('/file.txt', 'utf8');
113
- * ```
114
- */
115
- createNodeFSModule(): any {
116
- return createFSModule(this.pathMapper, this.resourceTracker);
117
- }
118
-
119
- /**
120
- * Create a Node.js path-compatible module
121
- *
122
- * Returns an object that matches the Node.js path module API.
123
- * All operations use Unix-style paths with forward slashes.
124
- * The virtual current working directory is always '/'.
125
- *
126
- * @returns A path-compatible object
127
- *
128
- * @example
129
- * ```typescript
130
- * const vfs = new VirtualFileSystem({ root: '/opt/data' });
131
- * const path = vfs.createNodePathModule();
132
- *
133
- * path.resolve('.'); // Returns: /
134
- * path.resolve('foo/bar.txt'); // Returns: /foo/bar.txt
135
- * path.join('/foo', 'bar'); // Returns: /foo/bar
136
- * path.sep; // Returns: /
137
- * ```
138
- */
139
- createNodePathModule(): any {
140
- return createPathModule();
141
- }
142
-
143
- /**
144
- * Close the VFS and release all resources
145
- *
146
- * Closes all tracked file descriptors, destroys all streams,
147
- * and marks the VFS as closed. After calling close(), all
148
- * subsequent fs operations will throw EBADF errors.
149
- *
150
- * @example
151
- * ```typescript
152
- * const vfs = new VirtualFileSystem({ root: '/opt/data' });
153
- * const fs = vfs.createNodeFSModule();
154
- *
155
- * const fd = fs.openSync('/file.txt', 'r');
156
- *
157
- * vfs.close(); // Closes fd and all other resources
158
- *
159
- * fs.readFileSync('/file.txt'); // Throws EBADF
160
- * ```
161
- */
162
- close(): void {
163
- this.resourceTracker.dispose();
164
- }
165
-
166
- /**
167
- * Check if the VFS has been closed
168
- */
169
- get closed(): boolean {
170
- return this.resourceTracker.closed;
171
- }
172
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * sandbox-fs - A read-only virtual file system for Node.js
3
- *
4
- * Maps Unix-style virtual paths to a real directory root while preventing
5
- * write operations and filtering real paths from error messages.
6
- */
7
-
8
- export { VirtualFileSystem, VirtualFileSystem as default } from './VirtualFileSystem';
9
- export type { VFSOptions } from './VirtualFileSystem';
@@ -1,223 +0,0 @@
1
- import * as fs from 'fs';
2
- import type { PathMapper } from '../PathMapper';
3
- import type { ResourceTracker } from '../ResourceTracker';
4
- import { filterError } from '../utils/ErrorFilter';
5
-
6
- /**
7
- * Wrap an async operation with error filtering and closed check
8
- */
9
- async function wrapOperation<T>(
10
- operation: () => Promise<T>,
11
- pathMapper: PathMapper,
12
- resourceTracker: ResourceTracker
13
- ): Promise<T> {
14
- resourceTracker.assertNotClosed();
15
- try {
16
- return await operation();
17
- } catch (error) {
18
- throw filterError(error as Error, pathMapper);
19
- }
20
- }
21
-
22
- /**
23
- * Create access denied error for write operations
24
- */
25
- function createEACCESError(syscall: string, path: string): NodeJS.ErrnoException {
26
- const error = new Error(`${syscall}: operation not permitted, ${syscall} '${path}'`) as NodeJS.ErrnoException;
27
- error.code = 'EACCES';
28
- error.errno = -13;
29
- error.syscall = syscall;
30
- error.path = path;
31
- return error;
32
- }
33
-
34
- /**
35
- * glob - Pattern matching (Node.js 22+)
36
- */
37
- export function createGlobOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
38
- return async function* (pattern: string, options?: any): AsyncIterableIterator<string> {
39
- resourceTracker.assertNotClosed();
40
-
41
- try {
42
- // Check if glob is available (Node.js 22+)
43
- if (typeof (fs.promises as any).glob !== 'function') {
44
- throw new Error('glob is not supported in this Node.js version');
45
- }
46
-
47
- // Convert pattern to real path context
48
- const realRoot = pathMapper.getRoot();
49
-
50
- if (options?.signal) {
51
- resourceTracker.registerAbortListener(options.signal, () => {});
52
- }
53
-
54
- // Execute glob in real path context
55
- const realOptions = { ...options, cwd: realRoot };
56
- const globIterator = (fs.promises as any).glob(pattern, realOptions);
57
-
58
- for await (const realPath of globIterator) {
59
- try {
60
- // Convert real path back to virtual path
61
- const virtualPath = pathMapper.toVirtualPath(realPath);
62
- yield virtualPath;
63
- } catch (error) {
64
- // Skip paths that can't be converted (outside VFS root)
65
- continue;
66
- }
67
- }
68
- } catch (error) {
69
- throw filterError(error as Error, pathMapper);
70
- }
71
- };
72
- }
73
-
74
- /**
75
- * statfs - File system statistics (Node.js 19+)
76
- */
77
- export function createStatfsOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
78
- return async (virtualPath: string, options?: any): Promise<fs.StatsFsBase<any>> => {
79
- return wrapOperation(async () => {
80
- // Check if statfs is available (Node.js 19+)
81
- if (typeof (fs.promises as any).statfs !== 'function') {
82
- throw new Error('statfs is not supported in this Node.js version');
83
- }
84
-
85
- const realPath = pathMapper.toRealPath(virtualPath);
86
-
87
- if (options?.signal) {
88
- resourceTracker.registerAbortListener(options.signal, () => {});
89
- }
90
-
91
- return (fs.promises as any).statfs(realPath, options);
92
- }, pathMapper, resourceTracker);
93
- };
94
- }
95
-
96
- /**
97
- * cp - Copy files/directories (read source only, write dest is denied)
98
- */
99
- export function createCpOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
100
- return async (source: string, destination: string, options?: any): Promise<void> => {
101
- // Always deny - this is a write operation
102
- throw createEACCESError('cp', destination);
103
- };
104
- }
105
-
106
- /**
107
- * openAsBlob - Open file as Blob (Node.js 19+)
108
- */
109
- export function createOpenAsBlobOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
110
- return async (virtualPath: string, options?: any): Promise<Blob> => {
111
- return wrapOperation(async () => {
112
- // Check if openAsBlob is available (Node.js 19+)
113
- if (typeof (fs as any).openAsBlob !== 'function') {
114
- throw new Error('openAsBlob is not supported in this Node.js version');
115
- }
116
-
117
- const realPath = pathMapper.toRealPath(virtualPath);
118
-
119
- if (options?.signal) {
120
- resourceTracker.registerAbortListener(options.signal, () => {});
121
- }
122
-
123
- return (fs as any).openAsBlob(realPath, options);
124
- }, pathMapper, resourceTracker);
125
- };
126
- }
127
-
128
- /**
129
- * readv - Vector read (Node.js 13+)
130
- */
131
- export function createReadvOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
132
- return async (fd: number, buffers: NodeJS.ArrayBufferView[], position?: number): Promise<number> => {
133
- return wrapOperation(async () => {
134
- if (!resourceTracker.isTracked(fd)) {
135
- const error = new Error('bad file descriptor') as NodeJS.ErrnoException;
136
- error.code = 'EBADF';
137
- error.errno = -9;
138
- error.syscall = 'readv';
139
- throw error;
140
- }
141
-
142
- // Check if readv is available
143
- if (typeof (fs.promises as any).readv !== 'function') {
144
- throw new Error('readv is not supported in this Node.js version');
145
- }
146
-
147
- return (fs.promises as any).readv(fd, buffers, position);
148
- }, pathMapper, resourceTracker);
149
- };
150
- }
151
-
152
- /**
153
- * watch - Watch for file changes
154
- */
155
- export function createWatchOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
156
- return (virtualPath: string, options?: any): fs.FSWatcher => {
157
- resourceTracker.assertNotClosed();
158
-
159
- try {
160
- const realPath = pathMapper.toRealPath(virtualPath);
161
- const watcher = fs.watch(realPath, options);
162
-
163
- // Wrap error events to filter paths
164
- const originalOn = watcher.on.bind(watcher);
165
- watcher.on = function(event: string, listener: any) {
166
- if (event === 'error') {
167
- const wrappedListener = (error: Error) => {
168
- listener(filterError(error, pathMapper));
169
- };
170
- return originalOn(event, wrappedListener);
171
- }
172
- return originalOn(event, listener);
173
- };
174
-
175
- return watcher;
176
- } catch (error) {
177
- throw filterError(error as Error, pathMapper);
178
- }
179
- };
180
- }
181
-
182
- /**
183
- * watchFile - Watch a file for changes
184
- */
185
- export function createWatchFileOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
186
- return (virtualPath: string, options: any, listener?: any): void => {
187
- resourceTracker.assertNotClosed();
188
-
189
- try {
190
- const realPath = pathMapper.toRealPath(virtualPath);
191
-
192
- // Handle both signatures: (path, listener) and (path, options, listener)
193
- if (typeof options === 'function') {
194
- fs.watchFile(realPath, options);
195
- } else {
196
- fs.watchFile(realPath, options, listener);
197
- }
198
- } catch (error) {
199
- throw filterError(error as Error, pathMapper);
200
- }
201
- };
202
- }
203
-
204
- /**
205
- * unwatchFile - Stop watching a file
206
- */
207
- export function createUnwatchFileOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
208
- return (virtualPath: string, listener?: any): void => {
209
- resourceTracker.assertNotClosed();
210
-
211
- try {
212
- const realPath = pathMapper.toRealPath(virtualPath);
213
-
214
- if (listener) {
215
- fs.unwatchFile(realPath, listener);
216
- } else {
217
- fs.unwatchFile(realPath);
218
- }
219
- } catch (error) {
220
- throw filterError(error as Error, pathMapper);
221
- }
222
- };
223
- }