@mikrojs/native 0.0.7

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 (109) hide show
  1. package/CMakeLists.txt +198 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/cmake/mikrojs_bytecode.cmake +146 -0
  5. package/cmake.js +22 -0
  6. package/dist/index.d.ts +52 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +132 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/types.d.ts +43 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/types.js.map +1 -0
  14. package/include/byteorder_apple.h +11 -0
  15. package/include/byteorder_windows.h +12 -0
  16. package/include/mikrojs/cbor_helpers.h +24 -0
  17. package/include/mikrojs/cutils_wrap.h +59 -0
  18. package/include/mikrojs/errors.h +144 -0
  19. package/include/mikrojs/mem.h +11 -0
  20. package/include/mikrojs/mik_color.h +32 -0
  21. package/include/mikrojs/mikrojs.h +331 -0
  22. package/include/mikrojs/platform.h +82 -0
  23. package/include/mikrojs/private.h +281 -0
  24. package/include/mikrojs/utils.h +125 -0
  25. package/package.json +100 -0
  26. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  27. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  28. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  29. package/runtime/ble/ble.ts +231 -0
  30. package/runtime/ble/types.ts +194 -0
  31. package/runtime/ble/uuid.ts +89 -0
  32. package/runtime/ble/validators.ts +61 -0
  33. package/runtime/cbor/cbor.ts +1 -0
  34. package/runtime/cbor/types.ts +8 -0
  35. package/runtime/console/types.ts +50 -0
  36. package/runtime/env/env.ts +17 -0
  37. package/runtime/env/types.ts +12 -0
  38. package/runtime/format/types.ts +4 -0
  39. package/runtime/fs/fs.ts +93 -0
  40. package/runtime/fs/types.ts +92 -0
  41. package/runtime/globals.d.ts +87 -0
  42. package/runtime/http/helpers.ts +222 -0
  43. package/runtime/http/native.ts +151 -0
  44. package/runtime/http/request.ts +25 -0
  45. package/runtime/i2c/i2c.ts +35 -0
  46. package/runtime/i2c/types.ts +55 -0
  47. package/runtime/inspect/types.ts +10 -0
  48. package/runtime/internal.d.ts +456 -0
  49. package/runtime/kv/nvs.ts +17 -0
  50. package/runtime/kv/rtc.ts +17 -0
  51. package/runtime/kv/shared.ts +107 -0
  52. package/runtime/kv/types.ts +150 -0
  53. package/runtime/neopixel/neopixel.ts +38 -0
  54. package/runtime/neopixel/types.ts +27 -0
  55. package/runtime/pin/pin.ts +51 -0
  56. package/runtime/pin/types.ts +49 -0
  57. package/runtime/pwm/pwm.ts +32 -0
  58. package/runtime/pwm/types.ts +29 -0
  59. package/runtime/reader/reader.ts +167 -0
  60. package/runtime/reader/types.ts +34 -0
  61. package/runtime/result/native-result.node-shim.ts +44 -0
  62. package/runtime/result/result.ts +26 -0
  63. package/runtime/result/types.ts +60 -0
  64. package/runtime/schema/schema.ts +321 -0
  65. package/runtime/schema/types.ts +152 -0
  66. package/runtime/sleep/sleep.ts +14 -0
  67. package/runtime/sleep/types.ts +44 -0
  68. package/runtime/sntp/sntp.ts +54 -0
  69. package/runtime/sntp/types.ts +38 -0
  70. package/runtime/spi/spi.ts +31 -0
  71. package/runtime/spi/types.ts +42 -0
  72. package/runtime/stdio/stdio.ts +44 -0
  73. package/runtime/stdio/types.ts +22 -0
  74. package/runtime/stream/stream.ts +150 -0
  75. package/runtime/stream/types.ts +47 -0
  76. package/runtime/sys/sys.ts +90 -0
  77. package/runtime/sys/types.ts +131 -0
  78. package/runtime/test/test.ts +595 -0
  79. package/runtime/test/types.ts +97 -0
  80. package/runtime/uart/types.ts +75 -0
  81. package/runtime/uart/uart.ts +51 -0
  82. package/runtime/wifi/types.ts +156 -0
  83. package/runtime/wifi/wifi.ts +208 -0
  84. package/scripts/bundle-runtime.js +149 -0
  85. package/scripts/compare-minifiers.js +189 -0
  86. package/scripts/compile-bytecode.sh +38 -0
  87. package/scripts/copy-prebuild.js +20 -0
  88. package/scripts/generate-symbol-map.js +146 -0
  89. package/src/builtins.cpp +82 -0
  90. package/src/cutils_compat.c +38 -0
  91. package/src/eval_bytecode.cpp +42 -0
  92. package/src/fs.cpp +878 -0
  93. package/src/mem.cpp +63 -0
  94. package/src/mik_abort.cpp +160 -0
  95. package/src/mik_app_config.cpp +358 -0
  96. package/src/mik_cbor.cpp +334 -0
  97. package/src/mik_color.cpp +46 -0
  98. package/src/mik_console.cpp +422 -0
  99. package/src/mik_inspect.cpp +850 -0
  100. package/src/mik_repl.cpp +1122 -0
  101. package/src/mik_result.cpp +344 -0
  102. package/src/mik_stdio.cpp +147 -0
  103. package/src/mik_sys.cpp +239 -0
  104. package/src/mik_text_encoding.cpp +443 -0
  105. package/src/mikrojs.cpp +942 -0
  106. package/src/modules.cpp +944 -0
  107. package/src/platform_posix.cpp +134 -0
  108. package/src/timers.cpp +208 -0
  109. package/src/utils.cpp +173 -0
package/src/fs.cpp ADDED
@@ -0,0 +1,878 @@
1
+ #include <cerrno>
2
+ #include <dirent.h>
3
+ #include <quickjs.h>
4
+ #include <stdio.h>
5
+ #include <string.h>
6
+ #include <sys/stat.h>
7
+ #include <unistd.h>
8
+
9
+ #include "mikrojs/errors.h"
10
+ #include "mikrojs/platform.h"
11
+ #include "mikrojs/private.h"
12
+ #include "mikrojs/utils.h"
13
+
14
+ static JSClassID mik_file_class_id;
15
+
16
+ typedef struct {
17
+ FILE* file;
18
+ JSValue path;
19
+ } MIKFileHandle;
20
+
21
+ /*
22
+ * Resolve a user-provided path against the runtime's fs_base_path.
23
+ * If a base path is configured, user paths are joined onto it (e.g. "/" -> "/appfs").
24
+ * If no base path is set, the path is used as-is.
25
+ * Writes the resolved path into `out` (max `out_size` bytes including NUL).
26
+ */
27
+ /*
28
+ * Normalize an absolute path in-place: resolve "." and ".." components,
29
+ * collapse repeated slashes, and clamp ".." at the root (prevent traversal
30
+ * above "/"). The input must start with "/".
31
+ *
32
+ * Examples:
33
+ * "/a/b/../c" → "/a/c"
34
+ * "/a/../../x" → "/x" (clamped at root)
35
+ * "/./a/./b" → "/a/b"
36
+ * "/../app" → "/app" (then blocked by /app check below)
37
+ */
38
+ static void mik__normalize_path(char* path) {
39
+ /* Stack of component start offsets within `path` */
40
+ int offsets[64];
41
+ int depth = 0;
42
+
43
+ char* dst = path;
44
+ const char* src = path;
45
+
46
+ *dst++ = '/'; /* always starts with / */
47
+ src++; /* skip leading / */
48
+
49
+ while (*src) {
50
+ /* Skip duplicate slashes */
51
+ if (*src == '/') {
52
+ src++;
53
+ continue;
54
+ }
55
+
56
+ /* "." component — skip */
57
+ if (src[0] == '.' && (src[1] == '/' || src[1] == '\0')) {
58
+ src += (src[1] == '/') ? 2 : 1;
59
+ continue;
60
+ }
61
+
62
+ /* ".." component — pop one level (clamped at root) */
63
+ if (src[0] == '.' && src[1] == '.' && (src[2] == '/' || src[2] == '\0')) {
64
+ if (depth > 0) {
65
+ depth--;
66
+ dst = path + offsets[depth];
67
+ } else {
68
+ dst = path + 1; /* clamp at root */
69
+ }
70
+ src += (src[2] == '/') ? 3 : 2;
71
+ continue;
72
+ }
73
+
74
+ /* Regular component — record its start offset and copy */
75
+ if (depth < 64) {
76
+ offsets[depth++] = (int)(dst - path);
77
+ }
78
+ if (dst != path + 1) {
79
+ *dst++ = '/';
80
+ }
81
+ while (*src && *src != '/') {
82
+ *dst++ = *src++;
83
+ }
84
+ if (*src == '/') src++;
85
+ }
86
+
87
+ /* Ensure at least "/" */
88
+ if (dst == path + 1 && path[0] == '/') {
89
+ /* path is just "/" */
90
+ }
91
+ *dst = '\0';
92
+ }
93
+
94
+ int mik__resolve_fs_root(JSContext* ctx, const char* path, char* out, size_t out_size) {
95
+ /* Normalize the path to prevent traversal attacks (../) */
96
+ char normalized[PATH_MAX];
97
+ if (path[0] == '/') {
98
+ snprintf(normalized, sizeof(normalized), "%s", path);
99
+ } else {
100
+ snprintf(normalized, sizeof(normalized), "/%s", path);
101
+ }
102
+ mik__normalize_path(normalized);
103
+
104
+ MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
105
+ /* Use fs_root if set, otherwise fall back to fs_base_path */
106
+ const char* root = mik_rt ? (mik_rt->fs_root ? mik_rt->fs_root : mik_rt->fs_base_path) : NULL;
107
+ if (!root) {
108
+ snprintf(out, out_size, "%s", normalized);
109
+ return 0;
110
+ }
111
+ if (normalized[0] == '/' && normalized[1] == '\0') {
112
+ snprintf(out, out_size, "%s", root);
113
+ } else {
114
+ snprintf(out, out_size, "%s%s", root, normalized);
115
+ }
116
+ return 0;
117
+ }
118
+
119
+ /* Returns true if the normalized virtual path is under /app (read-only zone) */
120
+ static bool mik__is_readonly_path(const char* path) {
121
+ char normalized[PATH_MAX];
122
+ if (path[0] == '/') {
123
+ snprintf(normalized, sizeof(normalized), "%s", path);
124
+ } else {
125
+ snprintf(normalized, sizeof(normalized), "/%s", path);
126
+ }
127
+ mik__normalize_path(normalized);
128
+ return strncmp(normalized, "/app", 4) == 0 && (normalized[4] == '/' || normalized[4] == '\0');
129
+ }
130
+
131
+ /* Create directories recursively (like mkdir -p) */
132
+ static int mik__mkdir_recursive(const char* path, mode_t mode) {
133
+ char tmp[PATH_MAX];
134
+ snprintf(tmp, sizeof(tmp), "%s", path);
135
+ size_t len = strlen(tmp);
136
+
137
+ /* Strip trailing slash */
138
+ if (len > 0 && tmp[len - 1] == '/') {
139
+ tmp[--len] = '\0';
140
+ }
141
+
142
+ for (char* p = tmp + 1; *p; p++) {
143
+ if (*p == '/') {
144
+ *p = '\0';
145
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
146
+ return -1;
147
+ }
148
+ *p = '/';
149
+ }
150
+ }
151
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
152
+ return -1;
153
+ }
154
+ return 0;
155
+ }
156
+
157
+ /* Calculate total bytes used by regular files under a directory (recursive) */
158
+ size_t mik__fs_dir_usage(const char* dir_path) {
159
+ DIR* dir = opendir(dir_path);
160
+ if (!dir) return 0;
161
+ size_t total = 0;
162
+ struct dirent* entry;
163
+ while ((entry = readdir(dir)) != NULL) {
164
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
165
+ char child[PATH_MAX];
166
+ snprintf(child, sizeof(child), "%s/%s", dir_path, entry->d_name);
167
+ struct stat st;
168
+ if (stat(child, &st) == 0) {
169
+ if (S_ISREG(st.st_mode)) {
170
+ total += st.st_size;
171
+ } else if (S_ISDIR(st.st_mode)) {
172
+ total += mik__fs_dir_usage(child);
173
+ }
174
+ }
175
+ }
176
+ closedir(dir);
177
+ return total;
178
+ }
179
+
180
+ /* Check if a write of `bytes` would exceed the fs_limit. Returns 0 if OK,
181
+ * -1 if over limit. Uses a cached usage counter maintained by
182
+ * mik__fs_note_delta/mik__fs_invalidate_usage; walks the tree only on
183
+ * first call (or after an invalidation). */
184
+ static int mik__fs_check_limit(JSContext* ctx, size_t bytes) {
185
+ MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
186
+ if (!mik_rt || mik_rt->fs_limit == 0) return 0;
187
+ const char* root = mik_rt->fs_root ? mik_rt->fs_root : mik_rt->fs_base_path;
188
+ if (!root) return 0;
189
+ if (!mik_rt->fs_used_known) {
190
+ mik_rt->fs_used = mik__fs_dir_usage(root);
191
+ mik_rt->fs_used_known = true;
192
+ }
193
+ if (mik_rt->fs_used + bytes > mik_rt->fs_limit) return -1;
194
+ return 0;
195
+ }
196
+
197
+ /* Apply a signed delta to the cached fs_used counter. No-op if the cache
198
+ * isn't populated yet (the next check will walk). Clamps at zero. */
199
+ static void mik__fs_note_delta(JSContext* ctx, ssize_t delta) {
200
+ MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
201
+ if (!mik_rt || !mik_rt->fs_used_known) return;
202
+ if (delta < 0) {
203
+ size_t dec = (size_t)(-delta);
204
+ mik_rt->fs_used = (mik_rt->fs_used > dec) ? (mik_rt->fs_used - dec) : 0;
205
+ } else {
206
+ mik_rt->fs_used += (size_t)delta;
207
+ }
208
+ }
209
+
210
+ /* Mark the cache stale — next check will re-walk. Used when we can't
211
+ * cheaply compute the delta (FileHandle truncation, rmdir). */
212
+ static void mik__fs_invalidate_usage(JSContext* ctx) {
213
+ MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
214
+ if (mik_rt) mik_rt->fs_used_known = false;
215
+ }
216
+
217
+ /* Return the current size of a file at `resolved_path`, or 0 if absent. */
218
+ static size_t mik__fs_file_size(const char* resolved_path) {
219
+ struct stat st;
220
+ if (stat(resolved_path, &st) != 0) return 0;
221
+ if (!S_ISREG(st.st_mode)) return 0;
222
+ return (size_t)st.st_size;
223
+ }
224
+
225
+ static void mik__file_finalizer(JSRuntime* rt, JSValue val) {
226
+ MIKFileHandle* fh = static_cast<MIKFileHandle*>(JS_GetOpaque(val, mik_file_class_id));
227
+ if (fh) {
228
+ if (fh->file) {
229
+ /* Handle was GC'd before close() — likely a leak (forgotten
230
+ * try/finally, or an exception that bypassed close). Log at
231
+ * debug level so it's visible when chasing fd exhaustion but
232
+ * silent in production. Pulling the path string here is unsafe
233
+ * (the runtime may be shutting down), so the message stays
234
+ * generic — the fd count is what matters in practice. */
235
+ const MIKPlatform* platform = MIK_GetPlatform();
236
+ if (platform && platform->log) {
237
+ platform->log(MIK_LOG_DEBUG, "mikrojs/fs",
238
+ "FileHandle GC'd while still open — forgot close()?");
239
+ }
240
+ fflush(fh->file);
241
+ fclose(fh->file);
242
+ fh->file = NULL;
243
+ }
244
+ JS_FreeValueRT(rt, fh->path);
245
+ js_free_rt(rt, fh);
246
+ }
247
+ }
248
+
249
+ static JSClassDef mik_file_class = {
250
+ .class_name = "FileHandle",
251
+ .finalizer = mik__file_finalizer,
252
+ .gc_mark = nullptr,
253
+ .call = nullptr,
254
+ .exotic = nullptr,
255
+ };
256
+
257
+ static MIKFileHandle* mik__file_get(JSContext* ctx, JSValue obj) {
258
+ return static_cast<MIKFileHandle*>(JS_GetOpaque2(ctx, obj, mik_file_class_id));
259
+ }
260
+
261
+ /* Forward declarations — defined in the public-API section below. */
262
+ static JSValue mik__fs_err_result(JSContext* ctx, int err, const char* path);
263
+ static JSValue mik__fs_stat_result(JSContext* ctx, const struct stat* st);
264
+
265
+ /* FileHandle.prototype.read(size) → Result<Uint8Array | undefined, FSError>
266
+ * (ok(undefined) at EOF). */
267
+ static JSValue mik__file_read(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
268
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
269
+ if (!fh || !fh->file) {
270
+ return mik__fs_err_result(ctx, EBADF, nullptr);
271
+ }
272
+
273
+ uint32_t size;
274
+ if (JS_ToUint32(ctx, &size, argv[0])) {
275
+ return JS_EXCEPTION;
276
+ }
277
+
278
+ uint8_t* buf = static_cast<uint8_t*>(js_malloc(ctx, size));
279
+ if (!buf) {
280
+ return JS_EXCEPTION;
281
+ }
282
+
283
+ size_t n = fread(buf, 1, size, fh->file);
284
+ if (n == 0) {
285
+ js_free(ctx, buf);
286
+ if (feof(fh->file)) {
287
+ return mik__result_ok(ctx, JS_UNDEFINED);
288
+ }
289
+ return mik__fs_err_result(ctx, errno, nullptr);
290
+ }
291
+
292
+ /* Transfer ownership of buf to the Uint8Array */
293
+ return mik__result_ok(ctx, MIK_NewUint8Array(ctx, buf, n));
294
+ }
295
+
296
+ /* FileHandle.prototype.write(data) — string or Uint8Array.
297
+ * Returns Result<number, FSError> where value is bytes written. */
298
+ static JSValue mik__file_write(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
299
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
300
+ if (!fh || !fh->file) {
301
+ return mik__fs_err_result(ctx, EBADF, nullptr);
302
+ }
303
+
304
+ if (JS_GetTypedArrayType(argv[0]) == JS_TYPED_ARRAY_UINT8) {
305
+ size_t size;
306
+ const uint8_t* buf = JS_GetUint8Array(ctx, &size, argv[0]);
307
+ if (!buf) {
308
+ return JS_EXCEPTION;
309
+ }
310
+ if (mik__fs_check_limit(ctx, size) < 0) {
311
+ return mik__fs_err_result(ctx, ENOSPC, nullptr);
312
+ }
313
+ size_t written = fwrite(buf, 1, size, fh->file);
314
+ mik__fs_note_delta(ctx, (ssize_t)written);
315
+ return mik__result_ok(ctx, JS_NewUint32(ctx, written));
316
+ }
317
+
318
+ const char* str = JS_ToCString(ctx, argv[0]);
319
+ if (!str) {
320
+ return JS_EXCEPTION;
321
+ }
322
+ size_t len = strlen(str);
323
+ if (mik__fs_check_limit(ctx, len) < 0) {
324
+ JS_FreeCString(ctx, str);
325
+ return mik__fs_err_result(ctx, ENOSPC, nullptr);
326
+ }
327
+ size_t written = fwrite(str, 1, len, fh->file);
328
+ JS_FreeCString(ctx, str);
329
+ mik__fs_note_delta(ctx, (ssize_t)written);
330
+ return mik__result_ok(ctx, JS_NewUint32(ctx, written));
331
+ }
332
+
333
+ /* FileHandle.prototype.seek(offset [, whence]) → Result<number, FSError>.
334
+ * Value is the new absolute position. whence: 'start' (default) | 'current' | 'end'. */
335
+ static JSValue mik__file_seek(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
336
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
337
+ if (!fh || !fh->file) {
338
+ return mik__fs_err_result(ctx, EBADF, nullptr);
339
+ }
340
+
341
+ int64_t offset;
342
+ if (JS_ToInt64(ctx, &offset, argv[0])) {
343
+ return JS_EXCEPTION;
344
+ }
345
+
346
+ int whence = SEEK_SET;
347
+ if (argc > 1 && JS_IsString(argv[1])) {
348
+ const char* w = JS_ToCString(ctx, argv[1]);
349
+ if (!w) return JS_EXCEPTION;
350
+ if (strcmp(w, "current") == 0) whence = SEEK_CUR;
351
+ else if (strcmp(w, "end") == 0) whence = SEEK_END;
352
+ else if (strcmp(w, "start") != 0) {
353
+ JS_FreeCString(ctx, w);
354
+ return mik__fs_err_result(ctx, EINVAL, nullptr);
355
+ }
356
+ JS_FreeCString(ctx, w);
357
+ }
358
+
359
+ if (fseek(fh->file, (long)offset, whence) != 0) {
360
+ return mik__fs_err_result(ctx, errno, nullptr);
361
+ }
362
+ long pos = ftell(fh->file);
363
+ if (pos < 0) return mik__fs_err_result(ctx, errno, nullptr);
364
+ return mik__result_ok(ctx, JS_NewInt64(ctx, pos));
365
+ }
366
+
367
+ /* FileHandle.prototype.close() → Result<void, FSError>. Idempotent. */
368
+ static JSValue mik__file_close(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
369
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
370
+ if (!fh || !fh->file) {
371
+ return mik__result_ok_void(ctx);
372
+ }
373
+ fflush(fh->file);
374
+ fclose(fh->file);
375
+ fh->file = NULL;
376
+ return mik__result_ok_void(ctx);
377
+ }
378
+
379
+ /* FileHandle.prototype.stat() → Result<StatResult, FSError> */
380
+ static JSValue mik__file_stat(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
381
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
382
+ if (!fh || !fh->file) {
383
+ return mik__fs_err_result(ctx, EBADF, nullptr);
384
+ }
385
+
386
+ struct stat st;
387
+ if (fstat(fileno(fh->file), &st) != 0) {
388
+ return mik__fs_err_result(ctx, errno, nullptr);
389
+ }
390
+
391
+ return mik__result_ok(ctx, mik__fs_stat_result(ctx, &st));
392
+ }
393
+
394
+ /* FileHandle path getter */
395
+ static JSValue mik__file_path_get(JSContext* ctx, JSValue this_val) {
396
+ MIKFileHandle* fh = mik__file_get(ctx, this_val);
397
+ if (!fh) {
398
+ return JS_EXCEPTION;
399
+ }
400
+ return JS_DupValue(ctx, fh->path);
401
+ }
402
+
403
+ static const JSCFunctionListEntry mik_file_proto_funcs[] = {
404
+ MIK_CFUNC_DEF("read", 1, mik__file_read),
405
+ MIK_CFUNC_DEF("write", 1, mik__file_write),
406
+ MIK_CFUNC_DEF("seek", 2, mik__file_seek),
407
+ MIK_CFUNC_DEF("close", 0, mik__file_close),
408
+ MIK_CFUNC_DEF("stat", 0, mik__file_stat),
409
+ MIK_CGETSET_DEF("path", mik__file_path_get, NULL),
410
+ };
411
+
412
+ /* ---- Module-level functions ---- */
413
+
414
+ static JSValue mik__fs_new_file(JSContext* ctx, FILE* file, const char* path) {
415
+ JSValue obj = JS_NewObjectClass(ctx, mik_file_class_id);
416
+ if (JS_IsException(obj)) {
417
+ return obj;
418
+ }
419
+
420
+ MIKFileHandle* fh = static_cast<MIKFileHandle*>(js_mallocz(ctx, sizeof(*fh)));
421
+ if (!fh) {
422
+ JS_FreeValue(ctx, obj);
423
+ return JS_EXCEPTION;
424
+ }
425
+
426
+ fh->file = file;
427
+ fh->path = JS_NewString(ctx, path);
428
+ JS_SetOpaque(obj, fh);
429
+ return obj;
430
+ }
431
+
432
+ /* ── mikrojs/fs: Result-returning public API ──────────────────────── */
433
+
434
+ struct MIKFsErrInfo {
435
+ const char* name;
436
+ bool is_unknown;
437
+ bool path_relevant;
438
+ };
439
+
440
+ /* Map a POSIX errno to an FSError variant descriptor. `path_relevant` is
441
+ * false for variants like BadFileDescriptor where the filesystem path
442
+ * doesn't meaningfully identify the error; `is_unknown` triggers the
443
+ * errno+message debug attachment on the emitted error object. */
444
+ static MIKFsErrInfo mik__fs_errno_info(int err) {
445
+ switch (err) {
446
+ case ENOENT: return {"NotFound", false, true};
447
+ case EEXIST: return {"AlreadyExists", false, true};
448
+ case EACCES:
449
+ case EROFS: return {"AccessDenied", false, true};
450
+ case ENOSPC: return {"NoSpace", false, true};
451
+ case EFBIG: return {"TooLarge", false, true};
452
+ case EISDIR: return {"IsDirectory", false, true};
453
+ case ENOTDIR: return {"NotDirectory", false, true};
454
+ case EBADF: return {"BadFileDescriptor", false, false};
455
+ default: return {"Unknown", true, false};
456
+ }
457
+ }
458
+
459
+ /* Build a Result-shaped error value for an fs op. Emits the final FSError
460
+ * variant object directly — `path` is included for all path-carrying
461
+ * variants, `errno` + `message` are attached for `Unknown` so debuggers
462
+ * still have something actionable. The JS wrapper is a pass-through. */
463
+ static JSValue mik__fs_err_result(JSContext* ctx, int err, const char* path) {
464
+ MIKFsErrInfo info = mik__fs_errno_info(err);
465
+ const char* name = info.name;
466
+ bool is_unknown = info.is_unknown;
467
+ bool has_path = (path != nullptr && info.path_relevant);
468
+
469
+ JSValue error = JS_NewObject(ctx);
470
+ JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, name), JS_PROP_C_W_E);
471
+ if (has_path) {
472
+ JS_DefinePropertyValueStr(ctx, error, "path", JS_NewString(ctx, path), JS_PROP_C_W_E);
473
+ }
474
+ if (is_unknown) {
475
+ JS_DefinePropertyValueStr(ctx, error, "code", JS_NewInt32(ctx, MIK_ERR_FS_UNKNOWN),
476
+ JS_PROP_C_W_E);
477
+ JS_DefinePropertyValueStr(ctx, error, "errno", JS_NewInt32(ctx, err), JS_PROP_C_W_E);
478
+ JS_DefinePropertyValueStr(ctx, error, "message", JS_NewString(ctx, strerror(err)),
479
+ JS_PROP_C_W_E);
480
+ }
481
+ return mik__result_err_obj(ctx, error);
482
+ }
483
+
484
+ /* Resolve a JS-string path argument into absolute filesystem form.
485
+ * On success returns 0 with `resolved`/`virt` filled.
486
+ * On failure returns -1 with `*err_out` holding the Result-shaped error
487
+ * value the caller should return directly. The intermediate JS C-string
488
+ * is freed before return, so callers don't track its lifetime. */
489
+ static int mik__fs_resolve(JSContext* ctx, JSValueConst arg, bool write, char* resolved,
490
+ size_t resolved_size, char* virt, size_t virt_size, JSValue* err_out) {
491
+ const char* cpath = JS_ToCString(ctx, arg);
492
+ if (!cpath) {
493
+ *err_out = JS_EXCEPTION;
494
+ return -1;
495
+ }
496
+ snprintf(virt, virt_size, "%s", cpath);
497
+ int rc = mik__resolve_fs_root(ctx, cpath, resolved, resolved_size);
498
+ bool readonly = write && mik__is_readonly_path(cpath);
499
+ JS_FreeCString(ctx, cpath);
500
+ if (rc < 0 || readonly) {
501
+ *err_out = mik__fs_err_result(ctx, EACCES, virt);
502
+ return -1;
503
+ }
504
+ return 0;
505
+ }
506
+
507
+ /* fs.readFile(path [, encoding]) → Result<Uint8Array | string, FSError> */
508
+ static JSValue mik__pub_fs_read_file(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
509
+ char resolved[PATH_MAX], virt[PATH_MAX];
510
+ JSValue err;
511
+ if (mik__fs_resolve(ctx, argv[0], false, resolved, sizeof(resolved), virt,
512
+ sizeof(virt), &err) < 0) {
513
+ return err;
514
+ }
515
+
516
+ FILE* f = fopen(resolved, "r");
517
+ if (!f) return mik__fs_err_result(ctx, errno, virt);
518
+
519
+ fseek(f, 0, SEEK_END);
520
+ long size = ftell(f);
521
+ fseek(f, 0, SEEK_SET);
522
+ if (size < 0) {
523
+ int e = errno;
524
+ fclose(f);
525
+ return mik__fs_err_result(ctx, e, virt);
526
+ }
527
+
528
+ /* Cap the single-shot read so a large file can't OOM the heap.
529
+ * Callers needing bigger reads should switch to readStream(). */
530
+ MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
531
+ if (mik_rt && mik_rt->fs_read_max > 0 && (size_t)size > mik_rt->fs_read_max) {
532
+ fclose(f);
533
+ return mik__fs_err_result(ctx, EFBIG, virt);
534
+ }
535
+
536
+ bool as_string = argc >= 2 && JS_IsString(argv[1]);
537
+
538
+ /* Empty file: skip js_malloc(0) which asserts in js_malloc_rt. */
539
+ if (size == 0) {
540
+ fclose(f);
541
+ JSValue val = as_string ? JS_NewStringLen(ctx, "", 0) : MIK_NewUint8Array(ctx, nullptr, 0);
542
+ return mik__result_ok(ctx, val);
543
+ }
544
+
545
+ uint8_t* buf = static_cast<uint8_t*>(js_malloc(ctx, size));
546
+ if (!buf) {
547
+ fclose(f);
548
+ return JS_EXCEPTION;
549
+ }
550
+
551
+ size_t n = fread(buf, 1, size, f);
552
+ fclose(f);
553
+
554
+ if (as_string) {
555
+ JSValue str = JS_NewStringLen(ctx, reinterpret_cast<char*>(buf), n);
556
+ js_free(ctx, buf);
557
+ return mik__result_ok(ctx, str);
558
+ }
559
+
560
+ return mik__result_ok(ctx, MIK_NewUint8Array(ctx, buf, n));
561
+ }
562
+
563
+ /* fs.writeFile(path, data [, options]) → Result<void, FSError> */
564
+ static JSValue mik__pub_fs_write_file(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
565
+ char resolved[PATH_MAX], virt[PATH_MAX];
566
+ JSValue err;
567
+ if (mik__fs_resolve(ctx, argv[0], true, resolved, sizeof(resolved), virt,
568
+ sizeof(virt), &err) < 0) {
569
+ return err;
570
+ }
571
+
572
+ /* options.create (default true), options.append (default false) */
573
+ bool create = true;
574
+ bool append = false;
575
+ if (argc >= 3 && JS_IsObject(argv[2])) {
576
+ JSValue create_val = JS_GetPropertyStr(ctx, argv[2], "create");
577
+ if (!JS_IsUndefined(create_val)) create = JS_ToBool(ctx, create_val);
578
+ JS_FreeValue(ctx, create_val);
579
+ JSValue append_val = JS_GetPropertyStr(ctx, argv[2], "append");
580
+ if (!JS_IsUndefined(append_val)) append = JS_ToBool(ctx, append_val);
581
+ JS_FreeValue(ctx, append_val);
582
+ }
583
+
584
+ if (!create) {
585
+ struct stat st;
586
+ if (stat(resolved, &st) != 0) return mik__fs_err_result(ctx, errno, virt);
587
+ }
588
+
589
+ /* Determine write size for limit check */
590
+ size_t write_size = 0;
591
+ if (JS_GetTypedArrayType(argv[1]) == JS_TYPED_ARRAY_UINT8) {
592
+ const uint8_t* buf = JS_GetUint8Array(ctx, &write_size, argv[1]);
593
+ if (!buf) return JS_EXCEPTION;
594
+ } else {
595
+ const char* str = JS_ToCStringLen(ctx, &write_size, argv[1]);
596
+ if (!str) return JS_EXCEPTION;
597
+ JS_FreeCString(ctx, str);
598
+ }
599
+
600
+ /* Quota accounting needs the existing size so the delta is accurate.
601
+ * Append keeps old bytes (net = write_size); truncate replaces them
602
+ * (net = max(write_size - old_size, 0)). */
603
+ size_t old_size = mik__fs_file_size(resolved);
604
+ size_t net = append ? write_size
605
+ : (write_size > old_size ? write_size - old_size : 0);
606
+ if (mik__fs_check_limit(ctx, net) < 0) {
607
+ return mik__fs_err_result(ctx, ENOSPC, virt);
608
+ }
609
+
610
+ FILE* f = fopen(resolved, append ? "a" : "w");
611
+ if (!f) return mik__fs_err_result(ctx, errno, virt);
612
+
613
+ /* Write string or Uint8Array */
614
+ if (JS_GetTypedArrayType(argv[1]) == JS_TYPED_ARRAY_UINT8) {
615
+ size_t size;
616
+ const uint8_t* buf = JS_GetUint8Array(ctx, &size, argv[1]);
617
+ if (!buf) {
618
+ fclose(f);
619
+ return JS_EXCEPTION;
620
+ }
621
+ fwrite(buf, 1, size, f);
622
+ } else {
623
+ size_t len;
624
+ const char* str = JS_ToCStringLen(ctx, &len, argv[1]);
625
+ if (!str) {
626
+ fclose(f);
627
+ return JS_EXCEPTION;
628
+ }
629
+ fwrite(str, 1, len, f);
630
+ JS_FreeCString(ctx, str);
631
+ }
632
+
633
+ fclose(f);
634
+ ssize_t delta = append ? (ssize_t)write_size
635
+ : (ssize_t)write_size - (ssize_t)old_size;
636
+ mik__fs_note_delta(ctx, delta);
637
+ return mik__result_ok_void(ctx);
638
+ }
639
+
640
+ /* Build a StatResult object from a stat struct. mtime is ms since epoch;
641
+ * the property is omitted when the platform doesn't track modification
642
+ * time (LittleFS without CONFIG_LITTLEFS_USE_MTIME reports 0). */
643
+ static JSValue mik__fs_stat_result(JSContext* ctx, const struct stat* st) {
644
+ JSValue obj = JS_NewObject(ctx);
645
+ JS_SetPropertyStr(ctx, obj, "size", JS_NewInt64(ctx, st->st_size));
646
+ JS_SetPropertyStr(ctx, obj, "isDirectory", JS_NewBool(ctx, S_ISDIR(st->st_mode)));
647
+ JS_SetPropertyStr(ctx, obj, "isFile", JS_NewBool(ctx, S_ISREG(st->st_mode)));
648
+ if (st->st_mtime > 0) {
649
+ JS_SetPropertyStr(ctx, obj, "mtime", JS_NewInt64(ctx, (int64_t)st->st_mtime * 1000));
650
+ }
651
+ return obj;
652
+ }
653
+
654
+ /* fs.stat(path) → Result<StatResult, FSError> */
655
+ static JSValue mik__pub_fs_stat(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
656
+ char resolved[PATH_MAX], virt[PATH_MAX];
657
+ JSValue err;
658
+ if (mik__fs_resolve(ctx, argv[0], false, resolved, sizeof(resolved), virt,
659
+ sizeof(virt), &err) < 0) {
660
+ return err;
661
+ }
662
+
663
+ struct stat st;
664
+ if (stat(resolved, &st) != 0) return mik__fs_err_result(ctx, errno, virt);
665
+
666
+ return mik__result_ok(ctx, mik__fs_stat_result(ctx, &st));
667
+ }
668
+
669
+ /* fs.readDir(path) → Result<Array<{name,isFile,isDirectory}>, FSError>.
670
+ * Uses dirent.d_type when available; falls back to stat on DT_UNKNOWN. */
671
+ static JSValue mik__pub_fs_read_dir(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
672
+ char resolved[PATH_MAX], virt[PATH_MAX];
673
+ JSValue err;
674
+ if (mik__fs_resolve(ctx, argv[0], false, resolved, sizeof(resolved), virt,
675
+ sizeof(virt), &err) < 0) {
676
+ return err;
677
+ }
678
+
679
+ DIR* dir = opendir(resolved);
680
+ if (!dir) return mik__fs_err_result(ctx, errno, virt);
681
+
682
+ JSValue arr = JS_NewArray(ctx);
683
+ uint32_t idx = 0;
684
+ struct dirent* entry;
685
+ while ((entry = readdir(dir)) != NULL) {
686
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
687
+
688
+ bool is_dir = false, is_file = false;
689
+ #ifdef DT_DIR
690
+ if (entry->d_type == DT_DIR) {
691
+ is_dir = true;
692
+ } else if (entry->d_type == DT_REG) {
693
+ is_file = true;
694
+ } else
695
+ #endif
696
+ {
697
+ /* d_type unknown or not supported — stat the child to classify. */
698
+ char child[PATH_MAX];
699
+ snprintf(child, sizeof(child), "%s/%s", resolved, entry->d_name);
700
+ struct stat st;
701
+ if (stat(child, &st) == 0) {
702
+ is_dir = S_ISDIR(st.st_mode);
703
+ is_file = S_ISREG(st.st_mode);
704
+ }
705
+ }
706
+
707
+ JSValue e = JS_NewObject(ctx);
708
+ JS_SetPropertyStr(ctx, e, "name", JS_NewString(ctx, entry->d_name));
709
+ JS_SetPropertyStr(ctx, e, "isDirectory", JS_NewBool(ctx, is_dir));
710
+ JS_SetPropertyStr(ctx, e, "isFile", JS_NewBool(ctx, is_file));
711
+ JS_SetPropertyUint32(ctx, arr, idx++, e);
712
+ }
713
+ closedir(dir);
714
+ return mik__result_ok(ctx, arr);
715
+ }
716
+
717
+ /* fs.unlink(path) → Result<void, FSError> */
718
+ static JSValue mik__pub_fs_unlink(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
719
+ char resolved[PATH_MAX], virt[PATH_MAX];
720
+ JSValue err;
721
+ if (mik__fs_resolve(ctx, argv[0], true, resolved, sizeof(resolved), virt,
722
+ sizeof(virt), &err) < 0) {
723
+ return err;
724
+ }
725
+ size_t old_size = mik__fs_file_size(resolved);
726
+ if (unlink(resolved) != 0) return mik__fs_err_result(ctx, errno, virt);
727
+ mik__fs_note_delta(ctx, -(ssize_t)old_size);
728
+ return mik__result_ok_void(ctx);
729
+ }
730
+
731
+ /* fs.rename(from, to) → Result<void, FSError> */
732
+ static JSValue mik__pub_fs_rename(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
733
+ char resolved_from[PATH_MAX], virt_from[PATH_MAX];
734
+ char resolved_to[PATH_MAX], virt_to[PATH_MAX];
735
+ JSValue err;
736
+ if (mik__fs_resolve(ctx, argv[0], true, resolved_from, sizeof(resolved_from),
737
+ virt_from, sizeof(virt_from), &err) < 0) {
738
+ return err;
739
+ }
740
+ if (mik__fs_resolve(ctx, argv[1], true, resolved_to, sizeof(resolved_to), virt_to,
741
+ sizeof(virt_to), &err) < 0) {
742
+ return err;
743
+ }
744
+ if (rename(resolved_from, resolved_to) != 0) {
745
+ return mik__fs_err_result(ctx, errno, virt_from);
746
+ }
747
+ return mik__result_ok_void(ctx);
748
+ }
749
+
750
+ /* fs.mkdir(path) → Result<void, FSError> */
751
+ static JSValue mik__pub_fs_mkdir(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
752
+ char resolved[PATH_MAX], virt[PATH_MAX];
753
+ JSValue err;
754
+ if (mik__fs_resolve(ctx, argv[0], true, resolved, sizeof(resolved), virt,
755
+ sizeof(virt), &err) < 0) {
756
+ return err;
757
+ }
758
+
759
+ bool recursive = false;
760
+ if (argc > 1 && JS_IsObject(argv[1])) {
761
+ JSValue r = JS_GetPropertyStr(ctx, argv[1], "recursive");
762
+ recursive = JS_ToBool(ctx, r);
763
+ JS_FreeValue(ctx, r);
764
+ }
765
+
766
+ int rc = recursive ? mik__mkdir_recursive(resolved, 0755) : mkdir(resolved, 0755);
767
+ if (rc != 0) return mik__fs_err_result(ctx, errno, virt);
768
+ return mik__result_ok_void(ctx);
769
+ }
770
+
771
+ /* fs.rmdir(path) → Result<void, FSError> */
772
+ static JSValue mik__pub_fs_rmdir(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
773
+ char resolved[PATH_MAX], virt[PATH_MAX];
774
+ JSValue err;
775
+ if (mik__fs_resolve(ctx, argv[0], true, resolved, sizeof(resolved), virt,
776
+ sizeof(virt), &err) < 0) {
777
+ return err;
778
+ }
779
+ if (rmdir(resolved) != 0) return mik__fs_err_result(ctx, errno, virt);
780
+ /* rmdir only succeeds on empty dirs. The directory entry itself is
781
+ * tiny and LittleFS-specific; invalidate rather than tracking it. */
782
+ mik__fs_invalidate_usage(ctx);
783
+ return mik__result_ok_void(ctx);
784
+ }
785
+
786
+ /* fs.open(path [, mode]) → Result<FileHandle, FSError>.
787
+ * Modes follow fopen(): 'r' | 'w' | 'a' | 'r+' | 'w+' | 'a+'. Default 'r'. */
788
+ static JSValue mik__pub_fs_open(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
789
+ const char* mode = "r";
790
+ bool mode_owned = argc > 1 && JS_IsString(argv[1]);
791
+ bool write_mode = false;
792
+ if (mode_owned) {
793
+ mode = JS_ToCString(ctx, argv[1]);
794
+ if (!mode) return JS_EXCEPTION;
795
+ write_mode = mode[0] != 'r' || mode[1] == '+';
796
+ }
797
+
798
+ char resolved[PATH_MAX], virt[PATH_MAX];
799
+ JSValue err;
800
+ if (mik__fs_resolve(ctx, argv[0], write_mode, resolved, sizeof(resolved), virt,
801
+ sizeof(virt), &err) < 0) {
802
+ if (mode_owned) JS_FreeCString(ctx, mode);
803
+ return err;
804
+ }
805
+
806
+ /* Modes 'w' and 'w+' truncate any existing file on open — the cached
807
+ * fs_used counter would be stale for that path, so invalidate. */
808
+ bool truncates = mode[0] == 'w';
809
+
810
+ FILE* f = fopen(resolved, mode);
811
+ if (mode_owned) JS_FreeCString(ctx, mode);
812
+ if (!f) return mik__fs_err_result(ctx, errno, virt);
813
+
814
+ if (truncates) mik__fs_invalidate_usage(ctx);
815
+ return mik__result_ok(ctx, mik__fs_new_file(ctx, f, virt));
816
+ }
817
+
818
+ /* fs.exists(path) → boolean; never fails.
819
+ * Kept as a plain bool (not Result) because there is no meaningful error —
820
+ * resolution failure just means "not reachable", which is a superset of
821
+ * "does not exist". */
822
+ static JSValue mik__pub_fs_exists(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
823
+ const char* cpath = JS_ToCString(ctx, argv[0]);
824
+ if (!cpath) return JS_EXCEPTION;
825
+ char resolved[PATH_MAX];
826
+ int rc = mik__resolve_fs_root(ctx, cpath, resolved, sizeof(resolved));
827
+ JS_FreeCString(ctx, cpath);
828
+ if (rc < 0) return JS_NewBool(ctx, false);
829
+ struct stat st;
830
+ return JS_NewBool(ctx, stat(resolved, &st) == 0);
831
+ }
832
+
833
+ /* Module exports for mikrojs/fs */
834
+ static const char* const pub_fs_exports[] = {"open", "readFile", "writeFile", "stat",
835
+ "readDir", "unlink", "rename", "mkdir",
836
+ "rmdir", "exists"};
837
+
838
+ static int mik__pub_fs_module_init(JSContext* ctx, JSModuleDef* m) {
839
+ JSValue ns = JS_NewObjectProto(ctx, JS_NULL);
840
+
841
+ JS_SetPropertyStr(ctx, ns, "open", JS_NewCFunction(ctx, mik__pub_fs_open, "open", 2));
842
+ JS_SetPropertyStr(ctx, ns, "readFile",
843
+ JS_NewCFunction(ctx, mik__pub_fs_read_file, "readFile", 1));
844
+ JS_SetPropertyStr(ctx, ns, "writeFile",
845
+ JS_NewCFunction(ctx, mik__pub_fs_write_file, "writeFile", 2));
846
+ JS_SetPropertyStr(ctx, ns, "stat", JS_NewCFunction(ctx, mik__pub_fs_stat, "stat", 1));
847
+ JS_SetPropertyStr(ctx, ns, "readDir",
848
+ JS_NewCFunction(ctx, mik__pub_fs_read_dir, "readDir", 1));
849
+ JS_SetPropertyStr(ctx, ns, "unlink", JS_NewCFunction(ctx, mik__pub_fs_unlink, "unlink", 1));
850
+ JS_SetPropertyStr(ctx, ns, "rename", JS_NewCFunction(ctx, mik__pub_fs_rename, "rename", 2));
851
+ JS_SetPropertyStr(ctx, ns, "mkdir", JS_NewCFunction(ctx, mik__pub_fs_mkdir, "mkdir", 1));
852
+ JS_SetPropertyStr(ctx, ns, "rmdir", JS_NewCFunction(ctx, mik__pub_fs_rmdir, "rmdir", 1));
853
+ JS_SetPropertyStr(ctx, ns, "exists", JS_NewCFunction(ctx, mik__pub_fs_exists, "exists", 1));
854
+
855
+ for (size_t i = 0; i < countof(pub_fs_exports); i++) {
856
+ JS_SetModuleExport(ctx, m, pub_fs_exports[i], JS_GetPropertyStr(ctx, ns, pub_fs_exports[i]));
857
+ }
858
+ JS_FreeValue(ctx, ns);
859
+ return 0;
860
+ }
861
+
862
+ void mik__pub_fs_register(JSContext* ctx) {
863
+ JSRuntime* rt = JS_GetRuntime(ctx);
864
+
865
+ /* FileHandle class (used by open()) */
866
+ JS_NewClassID(rt, &mik_file_class_id);
867
+ JS_NewClass(rt, mik_file_class_id, &mik_file_class);
868
+ JSValue proto = JS_NewObject(ctx);
869
+ JS_SetPropertyFunctionList(ctx, proto, mik_file_proto_funcs, countof(mik_file_proto_funcs));
870
+ JS_SetClassProto(ctx, mik_file_class_id, proto);
871
+
872
+ JSModuleDef* m = JS_NewCModule(ctx, "native:fs", mik__pub_fs_module_init);
873
+ if (m) {
874
+ for (size_t i = 0; i < countof(pub_fs_exports); i++) {
875
+ JS_AddModuleExport(ctx, m, pub_fs_exports[i]);
876
+ }
877
+ }
878
+ }