@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.1

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/src/stats.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `Stats` and `Dirent`, built over the plain objects the native module returns.
3
+ *
4
+ * The module crosses the bridge with numbers rather than methods — a generated
5
+ * TurboModule result cannot carry predicates — so the classes live here and
6
+ * reproduce the shape Node's callers test for. The `kind` field is the coarse
7
+ * classification the native side can make without a platform syscall; `mode` is
8
+ * there for the finer predicates, and is zero on Windows where there are no
9
+ * Unix mode bits at all.
10
+ */
11
+
12
+ import { constants } from './constants';
13
+
14
+ /** The native `FileKind` numbers; must match `module::kind` in Rust. */
15
+ export const FILE_KIND = {
16
+ file: 0,
17
+ directory: 1,
18
+ symlink: 2,
19
+ other: 3,
20
+ } as const;
21
+
22
+ /** The shape `stat`, `lstat` and `fstat` return across the bridge. */
23
+ export interface StatResult {
24
+ size: number;
25
+ mode: number;
26
+ kind: number;
27
+ atimeMs: number;
28
+ mtimeMs: number;
29
+ ctimeMs: number;
30
+ birthtimeMs: number;
31
+ dev: number;
32
+ ino: number;
33
+ nlink: number;
34
+ uid: number;
35
+ gid: number;
36
+ }
37
+
38
+ /** The shape `readdir` returns across the bridge. */
39
+ export interface DirEntryResult {
40
+ name: string;
41
+ kind: number;
42
+ parentPath: string;
43
+ }
44
+
45
+ /** The type bits of a mode, or 0 when the platform does not report them. */
46
+ function fileTypeBits(mode: number): number {
47
+ return mode & constants.S_IFMT;
48
+ }
49
+
50
+ /**
51
+ * Node's `fs.Stats`.
52
+ *
53
+ * The fields are plain properties rather than getters: they are copied once from
54
+ * the native result, and a caller destructuring the object sees the same values
55
+ * Node's does.
56
+ */
57
+ export class Stats {
58
+ readonly dev: number;
59
+ readonly ino: number;
60
+ readonly mode: number;
61
+ readonly nlink: number;
62
+ readonly uid: number;
63
+ readonly gid: number;
64
+ readonly rdev: number;
65
+ readonly size: number;
66
+ readonly blksize: number;
67
+ readonly blocks: number;
68
+
69
+ readonly atimeMs: number;
70
+ readonly mtimeMs: number;
71
+ readonly ctimeMs: number;
72
+ readonly birthtimeMs: number;
73
+
74
+ readonly atime: Date;
75
+ readonly mtime: Date;
76
+ readonly ctime: Date;
77
+ readonly birthtime: Date;
78
+
79
+ /** The native classification, kept because it answers predicates the mode
80
+ * cannot on platforms that report no type bits. */
81
+ private readonly kind: number;
82
+
83
+ constructor(result: StatResult) {
84
+ this.dev = result.dev;
85
+ this.ino = result.ino;
86
+ this.mode = result.mode;
87
+ this.nlink = result.nlink;
88
+ this.uid = result.uid;
89
+ this.gid = result.gid;
90
+ // The native result has no `rdev`, block size or block count; Node reports
91
+ // zero for them where the platform cannot say, so zero is the honest value.
92
+ this.rdev = 0;
93
+ this.size = result.size;
94
+ this.blksize = 0;
95
+ this.blocks = 0;
96
+
97
+ this.atimeMs = result.atimeMs;
98
+ this.mtimeMs = result.mtimeMs;
99
+ this.ctimeMs = result.ctimeMs;
100
+ this.birthtimeMs = result.birthtimeMs;
101
+
102
+ this.atime = new Date(result.atimeMs);
103
+ this.mtime = new Date(result.mtimeMs);
104
+ this.ctime = new Date(result.ctimeMs);
105
+ this.birthtime = new Date(result.birthtimeMs);
106
+
107
+ this.kind = result.kind;
108
+ }
109
+
110
+ isFile(): boolean {
111
+ const bits = fileTypeBits(this.mode);
112
+ return bits === 0
113
+ ? this.kind === FILE_KIND.file
114
+ : bits === constants.S_IFREG;
115
+ }
116
+
117
+ isDirectory(): boolean {
118
+ const bits = fileTypeBits(this.mode);
119
+ return bits === 0
120
+ ? this.kind === FILE_KIND.directory
121
+ : bits === constants.S_IFDIR;
122
+ }
123
+
124
+ isSymbolicLink(): boolean {
125
+ const bits = fileTypeBits(this.mode);
126
+ return bits === 0
127
+ ? this.kind === FILE_KIND.symlink
128
+ : bits === constants.S_IFLNK;
129
+ }
130
+
131
+ isBlockDevice(): boolean {
132
+ return fileTypeBits(this.mode) === constants.S_IFBLK;
133
+ }
134
+
135
+ isCharacterDevice(): boolean {
136
+ return fileTypeBits(this.mode) === constants.S_IFCHR;
137
+ }
138
+
139
+ isFIFO(): boolean {
140
+ return fileTypeBits(this.mode) === constants.S_IFIFO;
141
+ }
142
+
143
+ isSocket(): boolean {
144
+ return fileTypeBits(this.mode) === constants.S_IFSOCK;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Node's `fs.Dirent`.
150
+ *
151
+ * `parentPath` is `''` when the caller already held the parent, matching what
152
+ * `readdir` with `{withFileTypes: true}` produces there.
153
+ */
154
+ export class Dirent {
155
+ readonly name: string;
156
+ readonly parentPath: string;
157
+ readonly path: string;
158
+
159
+ private readonly kind: number;
160
+
161
+ constructor(result: DirEntryResult) {
162
+ this.name = result.name;
163
+ this.parentPath = result.parentPath;
164
+ // `path` is the older name for the same field, and libraries still read it.
165
+ this.path = result.parentPath;
166
+ this.kind = result.kind;
167
+ }
168
+
169
+ isFile(): boolean {
170
+ return this.kind === FILE_KIND.file;
171
+ }
172
+
173
+ isDirectory(): boolean {
174
+ return this.kind === FILE_KIND.directory;
175
+ }
176
+
177
+ isSymbolicLink(): boolean {
178
+ return this.kind === FILE_KIND.symlink;
179
+ }
180
+
181
+ isBlockDevice(): boolean {
182
+ return false;
183
+ }
184
+
185
+ isCharacterDevice(): boolean {
186
+ return false;
187
+ }
188
+
189
+ isFIFO(): boolean {
190
+ return false;
191
+ }
192
+
193
+ isSocket(): boolean {
194
+ return false;
195
+ }
196
+ }
package/src/upload.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `upload`: a file, or a range of one, streamed as an HTTP request body.
3
+ *
4
+ * The native side reads the file in small blocks while the connection takes
5
+ * them, so nothing larger than a block is ever held in memory.
6
+ */
7
+
8
+ import NativeFs from './NativeFs';
9
+ import { nodeError, toPath, withContext, type NodeError } from './shared';
10
+
11
+ export interface UploadOptions {
12
+ /** `PUT` unless set. */
13
+ method?: string;
14
+ headers?: Record<string, string>;
15
+ /** Where in the file the body starts. Zero unless set. */
16
+ offset?: number;
17
+ /** How many bytes to send. Everything from `offset` unless set. */
18
+ length?: number;
19
+ /** Called while the body is sent. */
20
+ onProgress?: (sent: number, total: number) => void;
21
+ /** Aborting rejects with an `AbortError`. */
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ export interface UploadResult {
26
+ status: number;
27
+ /** Response headers, lower-cased; repeated headers are joined with `, `. */
28
+ headers: Record<string, string>;
29
+ /** The first part of the response body. */
30
+ body: string;
31
+ }
32
+
33
+ let nextId = 1;
34
+
35
+ function abortError(): NodeError {
36
+ const error = nodeError('ABORT_ERR', 'The upload was aborted');
37
+ error.name = 'AbortError';
38
+ return error;
39
+ }
40
+
41
+ function toRecord(list: Array<{ name: string; value: string }>) {
42
+ const record: Record<string, string> = {};
43
+ for (const { name, value } of list) {
44
+ record[name] = value;
45
+ }
46
+ return record;
47
+ }
48
+
49
+ export async function upload(
50
+ url: string,
51
+ path: string,
52
+ options: UploadOptions = {},
53
+ ): Promise<UploadResult> {
54
+ const { signal, onProgress } = options;
55
+ if (signal?.aborted) {
56
+ throw abortError();
57
+ }
58
+ const source = toPath(path);
59
+ const id = nextId++;
60
+ const subscription = onProgress
61
+ ? NativeFs.onUploadProgress(event => {
62
+ if (event.id === id) {
63
+ onProgress(event.sent, event.total);
64
+ }
65
+ })
66
+ : null;
67
+ const onAbort = () => {
68
+ NativeFs.cancelUpload(id);
69
+ };
70
+ signal?.addEventListener('abort', onAbort);
71
+
72
+ try {
73
+ const result = await NativeFs.upload(url, source, {
74
+ id,
75
+ method: options.method ?? 'PUT',
76
+ headers: Object.entries(options.headers ?? {}).map(([name, value]) => ({
77
+ name,
78
+ value,
79
+ })),
80
+ offset: options.offset ?? 0,
81
+ length: options.length ?? -1,
82
+ });
83
+ return {
84
+ status: result.status,
85
+ headers: toRecord(result.headers),
86
+ body: result.body,
87
+ };
88
+ } catch (error) {
89
+ if (signal?.aborted || (error as NodeError)?.code === 'ABORT_ERR') {
90
+ throw abortError();
91
+ }
92
+ throw withContext(error, 'upload', source);
93
+ } finally {
94
+ signal?.removeEventListener('abort', onAbort);
95
+ subscription?.remove();
96
+ }
97
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `watch`: the JS half of the typed watcher event.
3
+ *
4
+ * The native module returns a watcher id; every event it produces carries that
5
+ * id, so one shared emitter serves every open watcher and each listener keeps
6
+ * only its own. The subscription is taken through the generated typed emitter on
7
+ * the module (`NativeFs.onWatchEvent`), which is the same mechanism the fixture's
8
+ * `onProgress` uses in `RustTurboModuleFixture.tsx`, rather than through
9
+ * `NativeEventEmitter`: the spec declares `onWatchEvent` as a typed
10
+ * `CodegenTypes.EventEmitter`, and the bridge installs a listener for exactly
11
+ * that; `NativeEventEmitter` would subscribe on the global device-emitter channel
12
+ * that only `sendEvent` writes to, so it would never fire.
13
+ */
14
+
15
+ import NativeFs, { type WatchEvent } from './NativeFs';
16
+ import { toPath, withContext, type NodeError } from './shared';
17
+
18
+ /** The event names Node uses for the two things a watcher reports. */
19
+ export type WatchEventType = 'rename' | 'change';
20
+
21
+ export interface WatchOptions {
22
+ recursive?: boolean;
23
+ encoding?: string;
24
+ }
25
+
26
+ export interface FSWatcher {
27
+ close(): Promise<void>;
28
+ }
29
+
30
+ export type WatchListener = (
31
+ eventType: WatchEventType,
32
+ filename: string | null,
33
+ ) => void;
34
+
35
+ /**
36
+ * Which of Node's two event types a native kind is.
37
+ *
38
+ * Node folds everything it sees into `rename` (the entry appeared, vanished or
39
+ * moved) and `change` (its contents or metadata changed). The caller's callback
40
+ * signature is Node's, so the mapping happens here while the native kind stays
41
+ * available to anyone who subscribes to the module directly.
42
+ */
43
+ function eventTypeOf(kind: string): WatchEventType {
44
+ return kind === 'modify' ? 'change' : 'rename';
45
+ }
46
+
47
+ /** The file a path names, relative to the watched root where it is below it. */
48
+ /**
49
+ * The filename a listener is given.
50
+ *
51
+ * The native side already reported it relative to the watch root, and its own
52
+ * name for a single-file watch: it canonicalises the root, which is where macOS
53
+ * decides between `/var` and `/private/var`, and it strips whichever spelling the
54
+ * event actually carries. Stripping a prefix here as well would be a second,
55
+ * weaker guess at the same question — a relative root never matches, and Windows
56
+ * separates with `\` — so the event path is passed through unchanged.
57
+ */
58
+ function fileNameOf(eventPath: string): string {
59
+ return eventPath;
60
+ }
61
+
62
+ /**
63
+ * Starts a watcher.
64
+ *
65
+ * The id arrives asynchronously, but the caller expects a handle it can close
66
+ * immediately, so the handle is created first and closing it before the id is
67
+ * known sets a flag the resolution honours. That is the one ordering hazard in
68
+ * this API, and it is invisible from the outside.
69
+ *
70
+ * Node reports a failure to start through an `error` event on the watcher. This
71
+ * handle has no event surface, so the failure is held and rejected from
72
+ * `close()` instead: it can then never become an unhandled rejection, and a
73
+ * caller that closes what it opened is guaranteed to see it.
74
+ */
75
+ export function watch(path: string, listener: WatchListener): FSWatcher;
76
+ export function watch(
77
+ path: string,
78
+ options: WatchOptions | null,
79
+ listener: WatchListener,
80
+ ): FSWatcher;
81
+ export function watch(
82
+ path: string,
83
+ optionsOrListener: WatchOptions | null | WatchListener,
84
+ maybeListener?: WatchListener,
85
+ ): FSWatcher {
86
+ const options =
87
+ typeof optionsOrListener === 'function' ? {} : (optionsOrListener ?? {});
88
+ const listener =
89
+ typeof optionsOrListener === 'function' ? optionsOrListener : maybeListener;
90
+ if (typeof listener !== 'function') {
91
+ throw new TypeError('watch requires a listener function');
92
+ }
93
+
94
+ const target = toPath(path);
95
+ let id: number | null = null;
96
+ let closed = false;
97
+ let failure: NodeError | null = null;
98
+ let subscription: { remove(): void } | null = null;
99
+
100
+ const ready = NativeFs.watch(target, options.recursive ?? false)
101
+ .then(watcherId => {
102
+ id = watcherId;
103
+ if (closed) {
104
+ // `close()` ran while the id was in flight; stop the native watcher now
105
+ // that there is something to stop.
106
+ NativeFs.unwatch(watcherId);
107
+ return;
108
+ }
109
+ subscription = NativeFs.onWatchEvent((event: WatchEvent) => {
110
+ if (event.id !== id) {
111
+ return;
112
+ }
113
+ // The encoding option only affects how a non-ASCII name is decoded, and
114
+ // every name already arrives as a JS string.
115
+ listener(eventTypeOf(event.kind), fileNameOf(event.path));
116
+ });
117
+ })
118
+ .catch((error: unknown) => {
119
+ failure = withContext(error, 'watch', target);
120
+ });
121
+
122
+ return {
123
+ async close(): Promise<void> {
124
+ closed = true;
125
+ subscription?.remove();
126
+ subscription = null;
127
+ if (id !== null) {
128
+ NativeFs.unwatch(id);
129
+ id = null;
130
+ }
131
+ // Awaiting the start makes `close()` immediately after `watch()` still stop
132
+ // the native watcher rather than racing it.
133
+ await ready;
134
+ if (failure !== null) {
135
+ const error = failure;
136
+ failure = null;
137
+ throw error;
138
+ }
139
+ },
140
+ };
141
+ }