xbintsc 0.3.12 → 0.3.14

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 (76) hide show
  1. package/README.md +21 -3
  2. package/README.zh-CN.md +18 -3
  3. package/dist/src/codegen/generator/calls/access.js +13 -0
  4. package/dist/src/codegen/generator/calls/access.js.map +1 -1
  5. package/dist/src/codegen/generator/calls/invocation.js +8 -2
  6. package/dist/src/codegen/generator/calls/invocation.js.map +1 -1
  7. package/dist/src/codegen/generator/context.d.ts +23 -0
  8. package/dist/src/codegen/generator/context.js +91 -17
  9. package/dist/src/codegen/generator/context.js.map +1 -1
  10. package/dist/src/codegen/generator/expressions/primary.js +5 -0
  11. package/dist/src/codegen/generator/expressions/primary.js.map +1 -1
  12. package/dist/src/driver/compiler.d.ts +7 -0
  13. package/dist/src/driver/compiler.js +41 -5
  14. package/dist/src/driver/compiler.js.map +1 -1
  15. package/dist/src/driver/modules.d.ts +1 -1
  16. package/dist/src/driver/modules.js +235 -25
  17. package/dist/src/driver/modules.js.map +1 -1
  18. package/dist/src/extensions/node/fs/fs-ops.d.ts +5 -2
  19. package/dist/src/extensions/node/fs/fs-ops.js +45 -6
  20. package/dist/src/extensions/node/fs/fs-ops.js.map +1 -1
  21. package/dist/src/extensions/node/fs/index.d.ts +6 -2
  22. package/dist/src/extensions/node/fs/index.js +22 -3
  23. package/dist/src/extensions/node/fs/index.js.map +1 -1
  24. package/dist/src/extensions/node/fs-promises/index.d.ts +4 -3
  25. package/dist/src/extensions/node/fs-promises/index.js +31 -3
  26. package/dist/src/extensions/node/fs-promises/index.js.map +1 -1
  27. package/dist/tests/cli/main.test.js +17 -0
  28. package/dist/tests/cli/main.test.js.map +1 -1
  29. package/dist/tests/driver/compiler.test.js +93 -2
  30. package/dist/tests/driver/compiler.test.js.map +1 -1
  31. package/dist/tests/driver/modules.test.js +91 -4
  32. package/dist/tests/driver/modules.test.js.map +1 -1
  33. package/dist/tests/e2e/compile-node.test.js +240 -0
  34. package/dist/tests/e2e/compile-node.test.js.map +1 -1
  35. package/dist/tests/e2e/compile.test.js +12 -0
  36. package/dist/tests/e2e/compile.test.js.map +1 -1
  37. package/dist/tests/e2e/harness.js +11 -8
  38. package/dist/tests/e2e/harness.js.map +1 -1
  39. package/dist/tests/e2e/node-modules.test.js +1 -1
  40. package/dist/tests/e2e/node-modules.test.js.map +1 -1
  41. package/dist/tests/e2e/npm-modules.test.d.ts +9 -0
  42. package/dist/tests/e2e/npm-modules.test.js +55 -0
  43. package/dist/tests/e2e/npm-modules.test.js.map +1 -0
  44. package/dist/tests/e2e/runtime-coverage.test.d.ts +11 -0
  45. package/dist/tests/e2e/runtime-coverage.test.js +646 -0
  46. package/dist/tests/e2e/runtime-coverage.test.js.map +1 -0
  47. package/package.json +3 -2
  48. package/runtime/ext_node/buffer/parts/prototype.inc +7 -0
  49. package/runtime/ext_node/buffer/parts/search.inc +8 -4
  50. package/runtime/ext_node/child_process/child_process.c +5 -0
  51. package/runtime/ext_node/fs/constants.c +113 -0
  52. package/runtime/ext_node/fs/copy_ops.c +145 -0
  53. package/runtime/ext_node/fs/dir.c +101 -0
  54. package/runtime/ext_node/fs/fd_ops.c +405 -0
  55. package/runtime/ext_node/fs/fs_common.h +388 -5
  56. package/runtime/ext_node/fs/fs_ops.c +100 -139
  57. package/runtime/ext_node/fs/glob.c +232 -0
  58. package/runtime/ext_node/fs/link_ops.c +91 -0
  59. package/runtime/ext_node/fs/meta_ops.c +403 -0
  60. package/runtime/ext_node/fs/promises.c +275 -60
  61. package/runtime/ext_node/fs/read_file.c +8 -4
  62. package/runtime/ext_node/fs/watch.c +52 -0
  63. package/runtime/ext_node/fs/write_file.c +20 -11
  64. package/runtime/ext_node/stream/stream.c +19 -2
  65. package/runtime/rt.h +9 -0
  66. package/runtime/xt_io.c +21 -0
  67. package/scripts/coverage-runtime.ts +338 -0
  68. package/src/codegen/generator/calls/access.ts +15 -0
  69. package/src/codegen/generator/calls/invocation.ts +8 -2
  70. package/src/codegen/generator/context.ts +112 -21
  71. package/src/codegen/generator/expressions/primary.ts +5 -0
  72. package/src/driver/compiler.ts +42 -5
  73. package/src/driver/modules.ts +251 -26
  74. package/src/extensions/node/fs/fs-ops.ts +48 -6
  75. package/src/extensions/node/fs/index.ts +25 -3
  76. package/src/extensions/node/fs-promises/index.ts +33 -4
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { existsSync, readFileSync, statSync } from "node:fs";
16
- import { dirname, isAbsolute, resolve } from "node:path";
16
+ import { dirname, isAbsolute, join, resolve } from "node:path";
17
17
  import {
18
18
  ModifierKind,
19
19
  SyntaxKind,
@@ -55,27 +55,50 @@ export interface BundleResult {
55
55
  readonly moduleCount: number;
56
56
  }
57
57
 
58
- const RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".mts", ".cts", "/index.ts", "/index.tsx"];
59
-
60
58
  /**
61
- * Bare module specifiers (`fs`, `node:fs`, `@scope/pkg`) are not files to
62
- * bundle: they are resolved at code generation time against the registered
63
- * extension modules.
59
+ * Source extensions probed for an extension-less specifier. TypeScript suffixes
60
+ * are tried before their JavaScript counterparts so a project that ships both
61
+ * `foo.ts` and a compiled `foo.js` prefers the source it is compiled from.
64
62
  */
65
- function isExternalSpecifier(specifier: string): boolean {
66
- return !specifier.startsWith(".") && !isAbsolute(specifier);
63
+ const RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
64
+ /** File extensions a directory `index` entry may use. */
65
+ const INDEX_SUFFIXES = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
66
+ /** Conditions consulted, in order, when reading a package `exports` map. */
67
+ const EXPORT_CONDITIONS = ["import", "module", "default", "node", "require"];
68
+
69
+ /** A specifier is a file path (`./x`, `../x`, `/x`) rather than a package. */
70
+ function isRelativeSpecifier(specifier: string): boolean {
71
+ return specifier.startsWith(".") || isAbsolute(specifier);
67
72
  }
68
73
 
69
- function resolveModule(fromDir: string, specifier: string): string | undefined {
70
- const base = resolve(fromDir, specifier);
71
- // TypeScript sources are imported using their emitted `.js` extension
72
- // (`import ... from "./foo.js"`), so map the extension back to the source
73
- // file before probing the usual suffixes.
74
+ /**
75
+ * Resolve a path on disk, tolerating TypeScript's `.js` import convention
76
+ * (`import "./foo.js"` pointing at `foo.ts`) and Node's directory/`index`
77
+ * conventions.
78
+ */
79
+ function resolvePath(base: string): string | undefined {
80
+ // A JavaScript specifier may refer to a TypeScript source that will emit it
81
+ // (`import "./foo.js"` pointing at `foo.ts`). Probe the matching TypeScript
82
+ // extensions after the literal path: `.js`/`.jsx` -> `.ts`/`.tsx`,
83
+ // `.mjs` -> `.mts`, `.cjs` -> `.cts`.
74
84
  const candidates = [base];
75
- const jsExtension = /\.(?:m|c)?jsx?$/.exec(base);
85
+ const jsExtension = /\.((?:m|c)?jsx?)$/.exec(base);
76
86
  if (jsExtension) {
77
87
  const stem = base.slice(0, jsExtension.index);
78
- candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`, `${stem}.cts`);
88
+ switch (jsExtension[1]) {
89
+ case "jsx":
90
+ candidates.push(`${stem}.tsx`, `${stem}.ts`);
91
+ break;
92
+ case "mjs":
93
+ candidates.push(`${stem}.mts`);
94
+ break;
95
+ case "cjs":
96
+ candidates.push(`${stem}.cts`);
97
+ break;
98
+ default:
99
+ candidates.push(`${stem}.ts`, `${stem}.tsx`);
100
+ break;
101
+ }
79
102
  }
80
103
  for (const candidate of candidates) {
81
104
  if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
@@ -84,9 +107,197 @@ function resolveModule(fromDir: string, specifier: string): string | undefined {
84
107
  const candidate = base + suffix;
85
108
  if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
86
109
  }
110
+ if (existsSync(base) && statSync(base).isDirectory()) return resolveDirectory(base);
111
+ return undefined;
112
+ }
113
+
114
+ /** Resolve a directory to its package entry or an `index` file. */
115
+ function resolveDirectory(directory: string): string | undefined {
116
+ const pkg = readPackageJson(directory);
117
+ if (pkg && typeof pkg.main === "string") {
118
+ const main = resolvePath(resolve(directory, pkg.main));
119
+ if (main) return main;
120
+ }
121
+ for (const suffix of INDEX_SUFFIXES) {
122
+ const candidate = join(directory, `index${suffix}`);
123
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ function resolveModule(fromDir: string, specifier: string): string | undefined {
129
+ return resolvePath(resolve(fromDir, specifier));
130
+ }
131
+
132
+ interface PackageJson {
133
+ readonly main?: string;
134
+ readonly module?: string;
135
+ readonly exports?: unknown;
136
+ }
137
+
138
+ function readPackageJson(directory: string): PackageJson | undefined {
139
+ const file = join(directory, "package.json");
140
+ if (!existsSync(file) || !statSync(file).isFile()) return undefined;
141
+ try {
142
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
143
+ return parsed && typeof parsed === "object" ? (parsed as PackageJson) : undefined;
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ /** Split `pkg/sub/path` or `@scope/pkg/sub/path` into package name and subpath. */
150
+ function splitPackageSpecifier(specifier: string): { packageName: string; subpath: string } {
151
+ const parts = specifier.split("/");
152
+ if (specifier.startsWith("@")) {
153
+ return { packageName: parts.slice(0, 2).join("/"), subpath: parts.slice(2).join("/") };
154
+ }
155
+ return { packageName: parts[0] ?? specifier, subpath: parts.slice(1).join("/") };
156
+ }
157
+
158
+ /**
159
+ * Resolve a bare specifier against the `node_modules` directories above
160
+ * `fromDir`, following a package's `exports` map and falling back to
161
+ * `module`/`main`/`index`. Packages that only ship CommonJS resolve here too;
162
+ * their `require` calls are then rejected during code generation.
163
+ */
164
+ function resolveNodePackage(fromDir: string, specifier: string): string | undefined {
165
+ const { packageName, subpath } = splitPackageSpecifier(specifier);
166
+ if (!packageName || packageName.startsWith(".")) return undefined;
167
+ let directory = fromDir;
168
+ for (;;) {
169
+ const packageDir = join(directory, "node_modules", packageName);
170
+ if (existsSync(packageDir) && statSync(packageDir).isDirectory()) {
171
+ const resolved = subpath
172
+ ? resolvePackageSubpath(packageDir, subpath)
173
+ : resolvePackageEntry(packageDir);
174
+ if (resolved) return resolved;
175
+ }
176
+ const parent = dirname(directory);
177
+ if (parent === directory) return undefined;
178
+ directory = parent;
179
+ }
180
+ }
181
+
182
+ function resolvePackageEntry(packageDir: string): string | undefined {
183
+ const pkg = readPackageJson(packageDir);
184
+ if (pkg) {
185
+ const exported = resolveExportTarget(subpathValue(pkg.exports, "."));
186
+ if (exported) {
187
+ const resolved = resolvePath(resolve(packageDir, exported));
188
+ if (resolved) return resolved;
189
+ }
190
+ if (typeof pkg.module === "string") {
191
+ const resolved = resolvePath(resolve(packageDir, pkg.module));
192
+ if (resolved) return resolved;
193
+ }
194
+ if (typeof pkg.main === "string") {
195
+ const resolved = resolvePath(resolve(packageDir, pkg.main));
196
+ if (resolved) return resolved;
197
+ }
198
+ }
199
+ return resolveDirectory(packageDir);
200
+ }
201
+
202
+ function resolvePackageSubpath(packageDir: string, subpath: string): string | undefined {
203
+ const pkg = readPackageJson(packageDir);
204
+ const exported = resolveExportTarget(subpathValue(pkg?.exports, `./${subpath}`));
205
+ if (exported) {
206
+ const resolved = resolvePath(resolve(packageDir, exported));
207
+ if (resolved) return resolved;
208
+ }
209
+ return resolvePath(join(packageDir, subpath));
210
+ }
211
+
212
+ /** Pick the export target for a subpath (`"."` or `"./sub"`) from an exports map. */
213
+ function subpathValue(exportsField: unknown, subpath: string): unknown {
214
+ if (exportsField === undefined) return undefined;
215
+ if (typeof exportsField === "string" || Array.isArray(exportsField)) {
216
+ return subpath === "." ? exportsField : undefined;
217
+ }
218
+ if (!exportsField || typeof exportsField !== "object") return undefined;
219
+ const record = exportsField as Record<string, unknown>;
220
+ const keys = Object.keys(record);
221
+ // A map without `.`-prefixed keys is a condition map for the root export.
222
+ if (!keys.some((key) => key.startsWith("."))) {
223
+ return subpath === "." ? exportsField : undefined;
224
+ }
225
+ if (subpath in record) return record[subpath];
226
+ // Wildcard patterns such as `"./*": "./dist/*.js"`.
227
+ for (const key of keys) {
228
+ const star = key.indexOf("*");
229
+ if (star === -1) continue;
230
+ const prefix = key.slice(0, star);
231
+ const suffix = key.slice(star + 1);
232
+ if (subpath.startsWith(prefix) && subpath.endsWith(suffix)) {
233
+ const matched = subpath.slice(prefix.length, subpath.length - suffix.length);
234
+ return substituteStar(record[key], matched);
235
+ }
236
+ }
237
+ return undefined;
238
+ }
239
+
240
+ function substituteStar(target: unknown, matched: string): unknown {
241
+ if (typeof target === "string") return target.split("*").join(matched);
242
+ if (Array.isArray(target)) return target.map((entry) => substituteStar(entry, matched));
243
+ if (target && typeof target === "object") {
244
+ const record: Record<string, unknown> = {};
245
+ for (const [key, value] of Object.entries(target)) record[key] = substituteStar(value, matched);
246
+ return record;
247
+ }
248
+ return target;
249
+ }
250
+
251
+ /** Pick a runtime string from a (possibly conditional) exports target. */
252
+ function resolveExportTarget(target: unknown): string | undefined {
253
+ if (typeof target === "string") return target;
254
+ if (Array.isArray(target)) {
255
+ for (const entry of target) {
256
+ const resolved = resolveExportTarget(entry);
257
+ if (resolved) return resolved;
258
+ }
259
+ return undefined;
260
+ }
261
+ if (target && typeof target === "object") {
262
+ const record = target as Record<string, unknown>;
263
+ for (const condition of EXPORT_CONDITIONS) {
264
+ if (condition in record) {
265
+ const resolved = resolveExportTarget(record[condition]);
266
+ if (resolved) return resolved;
267
+ }
268
+ }
269
+ }
87
270
  return undefined;
88
271
  }
89
272
 
273
+ type DependencyResolution =
274
+ | { readonly kind: "external" }
275
+ | { readonly kind: "file"; readonly path: string }
276
+ | { readonly kind: "missing" };
277
+
278
+ /**
279
+ * Classify an import specifier. Known platform modules (extension modules and
280
+ * `node:` builtins) are left for code generation; relative paths and
281
+ * `node_modules` packages are resolved to a source file to bundle; a relative
282
+ * path that does not exist is an error.
283
+ */
284
+ function classifyDependency(
285
+ fromDir: string,
286
+ specifier: string,
287
+ externalSpecifiers: ReadonlySet<string>,
288
+ ): DependencyResolution {
289
+ if (specifier.startsWith("node:") || externalSpecifiers.has(specifier)) {
290
+ return { kind: "external" };
291
+ }
292
+ const resolved = isRelativeSpecifier(specifier)
293
+ ? resolveModule(fromDir, specifier)
294
+ : resolveNodePackage(fromDir, specifier);
295
+ if (resolved) return { kind: "file", path: resolved };
296
+ // A bare specifier that is not a file may still be a platform module the
297
+ // generator knows about (e.g. when the caller passes no extension registry).
298
+ return isRelativeSpecifier(specifier) ? { kind: "missing" } : { kind: "external" };
299
+ }
300
+
90
301
  function hasModifier(node: Node, kind: ModifierKind): boolean {
91
302
  const modifiers = (node as { modifiers?: { modifierKind: ModifierKind }[] }).modifiers;
92
303
  return !!modifiers && modifiers.some((modifier) => modifier.modifierKind === kind);
@@ -111,7 +322,11 @@ function declarationName(node: Node): Identifier | undefined {
111
322
  }
112
323
 
113
324
  /** Load, parse and bind the entry module and every module it imports. */
114
- function loadGraph(entryPath: string, diagnostics: DiagnosticBag): ModuleRecord[] | undefined {
325
+ function loadGraph(
326
+ entryPath: string,
327
+ diagnostics: DiagnosticBag,
328
+ externalSpecifiers: ReadonlySet<string>,
329
+ ): ModuleRecord[] | undefined {
115
330
  const records = new Map<string, ModuleRecord>();
116
331
  const order: ModuleRecord[] = [];
117
332
  const visiting = new Set<string>();
@@ -152,9 +367,9 @@ function loadGraph(entryPath: string, diagnostics: DiagnosticBag): ModuleRecord[
152
367
  for (const statement of sourceFile.statements) {
153
368
  const specifier = moduleSpecifierOf(statement);
154
369
  if (!specifier) continue;
155
- if (isExternalSpecifier(specifier)) continue;
156
- const dependency = resolveModule(dirname(path), specifier);
157
- if (!dependency) {
370
+ const dependency = classifyDependency(dirname(path), specifier, externalSpecifiers);
371
+ if (dependency.kind === "external") continue;
372
+ if (dependency.kind === "missing") {
158
373
  diagnostics.error(
159
374
  DiagnosticCode.CodegenError,
160
375
  `Cannot resolve module '${specifier}' from '${path}'`,
@@ -164,7 +379,7 @@ function loadGraph(entryPath: string, diagnostics: DiagnosticBag): ModuleRecord[
164
379
  failed = true;
165
380
  continue;
166
381
  }
167
- load(dependency);
382
+ load(dependency.path);
168
383
  }
169
384
  visiting.delete(path);
170
385
  order.push(record);
@@ -215,14 +430,18 @@ function topLevelSymbols(record: ModuleRecord): SymbolInfo[] {
215
430
  * Merge every module's statements into a single file. Dependencies come first
216
431
  * (post-order over the import graph), the entry module last.
217
432
  */
218
- export function bundleModules(entryPath: string, diagnostics: DiagnosticBag): BundleResult | undefined {
219
- const records = loadGraph(entryPath, diagnostics);
433
+ export function bundleModules(
434
+ entryPath: string,
435
+ diagnostics: DiagnosticBag,
436
+ externalSpecifiers: ReadonlySet<string> = new Set(),
437
+ ): BundleResult | undefined {
438
+ const records = loadGraph(entryPath, diagnostics, externalSpecifiers);
220
439
  if (!records) return undefined;
221
440
 
222
441
  const byPath = new Map(records.map((record) => [record.path, record]));
223
442
  const dependencyOf = (record: ModuleRecord, specifier: string): ModuleRecord | undefined => {
224
- const resolved = resolveModule(dirname(record.path), specifier);
225
- return resolved ? byPath.get(resolved) : undefined;
443
+ const resolution = classifyDependency(dirname(record.path), specifier, externalSpecifiers);
444
+ return resolution.kind === "file" ? byPath.get(resolution.path) : undefined;
226
445
  };
227
446
 
228
447
  // Phase 1: record every top-level binding and pick a unique final name.
@@ -328,8 +547,14 @@ export function bundleModules(entryPath: string, diagnostics: DiagnosticBag): Bu
328
547
  for (const record of records) {
329
548
  for (const statement of record.sourceFile.statements) {
330
549
  if (statement.kind === SyntaxKind.ImportDeclaration) {
331
- // External (extension) imports stay in place for code generation.
332
- if (isExternalSpecifier((statement as ImportDeclaration).moduleSpecifier.value)) {
550
+ // External (extension / built-in) imports stay in place for codegen.
551
+ const declaration = statement as ImportDeclaration;
552
+ const resolution = classifyDependency(
553
+ dirname(record.path),
554
+ declaration.moduleSpecifier.value,
555
+ externalSpecifiers,
556
+ );
557
+ if (resolution.kind === "external") {
333
558
  merged.push(statement);
334
559
  } else {
335
560
  const synthetic = namespaceStatements.get(statement);
@@ -1,20 +1,62 @@
1
1
  /**
2
- * Builtins for the filesystem operations in Node's `fs` module
3
- * (`existsSync`, directory handling, rename/copy, `statSync`, ...).
2
+ * Builtins for the filesystem operations in Node's `fs` module.
3
+ *
4
+ * Every runtime symbol uses the uniform `xt_value fn(int32_t, xt_value *)`
5
+ * ABI. xbintsc has no event loop, so only the synchronous API is exposed
6
+ * (`*Sync`), plus the watcher factories, whose objects never fire events.
4
7
  */
5
8
 
6
9
  import type { BuiltinFunction } from "../../registry.js";
7
10
 
8
11
  export const fsOpsBuiltins: Readonly<Record<string, BuiltinFunction>> = {
12
+ // Path / metadata -------------------------------------------------------
9
13
  existsSync: { symbol: "xt_node_exists" },
14
+ accessSync: { symbol: "xt_node_access" },
15
+ statSync: { symbol: "xt_node_stat" },
16
+ lstatSync: { symbol: "xt_node_lstat" },
17
+ statfsSync: { symbol: "xt_node_statfs" },
18
+ realpathSync: { symbol: "xt_node_realpath" },
19
+ chmodSync: { symbol: "xt_node_chmod" },
20
+ lchmodSync: { symbol: "xt_node_lchmod" },
21
+ chownSync: { symbol: "xt_node_chown" },
22
+ lchownSync: { symbol: "xt_node_lchown" },
23
+ truncateSync: { symbol: "xt_node_truncate" },
24
+ utimesSync: { symbol: "xt_node_utimes" },
25
+ lutimesSync: { symbol: "xt_node_lutimes" },
26
+
27
+ // Directories / entries -------------------------------------------------
10
28
  readdirSync: { symbol: "xt_node_read_dir" },
11
29
  mkdirSync: { symbol: "xt_node_mkdir" },
30
+ mkdtempSync: { symbol: "xt_node_mkdtemp" },
31
+ rmdirSync: { symbol: "xt_node_rmdir" },
12
32
  rmSync: { symbol: "xt_node_rm" },
13
33
  unlinkSync: { symbol: "xt_node_unlink" },
14
- rmdirSync: { symbol: "xt_node_rmdir" },
15
34
  renameSync: { symbol: "xt_node_rename" },
16
35
  copyFileSync: { symbol: "xt_node_copy_file" },
17
- realpathSync: { symbol: "xt_node_realpath" },
18
- statSync: { symbol: "xt_node_stat" },
19
- lstatSync: { symbol: "xt_node_lstat" },
36
+ cpSync: { symbol: "xt_node_cp" },
37
+ linkSync: { symbol: "xt_node_link" },
38
+ symlinkSync: { symbol: "xt_node_symlink" },
39
+ readlinkSync: { symbol: "xt_node_readlink" },
40
+ opendirSync: { symbol: "xt_node_opendir" },
41
+ globSync: { symbol: "xt_node_glob" },
42
+
43
+ // File descriptors ------------------------------------------------------
44
+ openSync: { symbol: "xt_node_open" },
45
+ closeSync: { symbol: "xt_node_close" },
46
+ readSync: { symbol: "xt_node_read" },
47
+ writeSync: { symbol: "xt_node_write" },
48
+ readvSync: { symbol: "xt_node_readv" },
49
+ writevSync: { symbol: "xt_node_writev" },
50
+ fstatSync: { symbol: "xt_node_fstat" },
51
+ fsyncSync: { symbol: "xt_node_fsync" },
52
+ fdatasyncSync: { symbol: "xt_node_fdatasync" },
53
+ ftruncateSync: { symbol: "xt_node_ftruncate" },
54
+ fchmodSync: { symbol: "xt_node_fchmod" },
55
+ fchownSync: { symbol: "xt_node_fchown" },
56
+ futimesSync: { symbol: "xt_node_futimes" },
57
+
58
+ // Watchers (API-shaped, no events) --------------------------------------
59
+ watch: { symbol: "xt_node_watch" },
60
+ watchFile: { symbol: "xt_node_watch_file" },
61
+ unwatchFile: { symbol: "xt_node_unwatch_file" },
20
62
  };
@@ -1,24 +1,46 @@
1
1
  /**
2
2
  * Node's `fs` module: synchronous file and directory operations backed by
3
3
  * `runtime/ext_node/fs`. Asynchronous callbacks are not supported (xbintsc has
4
- * no event loop), so only the `*Sync` API is exposed. Import the functions, e.g.
5
- * `import { readFileSync } from "fs"`.
4
+ * no event loop), so the API is the `*Sync` surface plus `watch`/`watchFile`
5
+ * (which return API-shaped watchers that never emit).
6
+ *
7
+ * `fs.constants` and `fs.promises` are exposed as value getters, so both
8
+ * `import { constants } from "fs"` and the `fs.constants.F_OK` property form
9
+ * work.
6
10
  */
7
11
 
8
12
  import type { NodeModule } from "../module.js";
9
13
  import { resolveFrom } from "../module.js";
14
+ import type { ModuleExports } from "../../registry.js";
10
15
  import { readFileBuiltins } from "./read-file.js";
11
16
  import { writeFileBuiltins } from "./write-file.js";
12
17
  import { fsOpsBuiltins } from "./fs-ops.js";
13
18
  import { fsStreamBuiltins } from "./streams.js";
14
19
 
20
+ const builtins = { ...readFileBuiltins, ...writeFileBuiltins, ...fsOpsBuiltins, ...fsStreamBuiltins };
21
+
22
+ const exports: ModuleExports = {
23
+ ...builtins,
24
+ constants: { valueSymbol: "xt_fs_constants" },
25
+ promises: { valueSymbol: "xt_fs_promises" },
26
+ };
27
+
15
28
  export const fsModule: NodeModule = {
16
29
  name: "fs",
17
30
  runtimeSources: () => [
18
31
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/read_file.c"),
19
32
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/write_file.c"),
20
33
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/fs_ops.c"),
34
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/fd_ops.c"),
35
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/meta_ops.c"),
36
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/link_ops.c"),
37
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/copy_ops.c"),
38
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/dir.c"),
39
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/glob.c"),
40
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/watch.c"),
41
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/constants.c"),
21
42
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/streams.c"),
22
43
  ],
23
- builtins: () => ({ ...readFileBuiltins, ...writeFileBuiltins, ...fsOpsBuiltins, ...fsStreamBuiltins }),
44
+ builtins: () => builtins,
45
+ exports: () => exports,
24
46
  };
@@ -2,14 +2,15 @@
2
2
  * Node's `fs/promises` module.
3
3
  *
4
4
  * There is no asynchronous I/O scheduler, so each function wraps the matching
5
- * synchronous `fs` implementation in an already-resolved promise. `readFile`,
6
- * `writeFile`, `mkdir`, `readdir`, `stat`, `rm`, ... are therefore importable
7
- * from `fs/promises`, returning promises.
5
+ * synchronous `fs` implementation in an already-settled promise (rejected on
6
+ * failure). `readFile`, `writeFile`, `mkdir`, `readdir`, `stat`, `rm`,
7
+ * `open` (a `FileHandle`), ... are therefore importable from `fs/promises`.
8
+ * `constants` is re-exported as a value getter.
8
9
  */
9
10
 
10
11
  import type { NodeModule } from "../module.js";
11
12
  import { resolveFrom } from "../module.js";
12
- import type { BuiltinFunction } from "../../registry.js";
13
+ import type { BuiltinFunction, ModuleExports } from "../../registry.js";
13
14
 
14
15
  const builtins: Readonly<Record<string, BuiltinFunction>> = {
15
16
  readFile: { symbol: "xt_node_p_read_file" },
@@ -22,10 +23,32 @@ const builtins: Readonly<Record<string, BuiltinFunction>> = {
22
23
  rmdir: { symbol: "xt_node_p_rmdir" },
23
24
  rename: { symbol: "xt_node_p_rename" },
24
25
  copyFile: { symbol: "xt_node_p_copy_file" },
26
+ cp: { symbol: "xt_node_p_cp" },
25
27
  realpath: { symbol: "xt_node_p_realpath" },
26
28
  stat: { symbol: "xt_node_p_stat" },
27
29
  lstat: { symbol: "xt_node_p_lstat" },
30
+ statfs: { symbol: "xt_node_p_statfs" },
28
31
  access: { symbol: "xt_node_p_access" },
32
+ open: { symbol: "xt_node_p_open" },
33
+ chmod: { symbol: "xt_node_p_chmod" },
34
+ lchmod: { symbol: "xt_node_p_lchmod" },
35
+ chown: { symbol: "xt_node_p_chown" },
36
+ lchown: { symbol: "xt_node_p_lchown" },
37
+ truncate: { symbol: "xt_node_p_truncate" },
38
+ utimes: { symbol: "xt_node_p_utimes" },
39
+ lutimes: { symbol: "xt_node_p_lutimes" },
40
+ link: { symbol: "xt_node_p_link" },
41
+ symlink: { symbol: "xt_node_p_symlink" },
42
+ readlink: { symbol: "xt_node_p_readlink" },
43
+ mkdtemp: { symbol: "xt_node_p_mkdtemp" },
44
+ opendir: { symbol: "xt_node_p_opendir" },
45
+ glob: { symbol: "xt_node_p_glob" },
46
+ watch: { symbol: "xt_node_p_watch" },
47
+ };
48
+
49
+ const exports: ModuleExports = {
50
+ ...builtins,
51
+ constants: { valueSymbol: "xt_fs_constants" },
29
52
  };
30
53
 
31
54
  export const fsPromisesModule: NodeModule = {
@@ -35,7 +58,13 @@ export const fsPromisesModule: NodeModule = {
35
58
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/read_file.c"),
36
59
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/write_file.c"),
37
60
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/fs_ops.c"),
61
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/fd_ops.c"),
62
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/meta_ops.c"),
63
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/link_ops.c"),
64
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/copy_ops.c"),
65
+ resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/constants.c"),
38
66
  resolveFrom(import.meta.url, "../../../../runtime/ext_node/fs/promises.c"),
39
67
  ],
40
68
  builtins: () => builtins,
69
+ exports: () => exports,
41
70
  };