diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 9b1f1d5d7..e9b7b833b 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "80b3d2a5ad0d17bd8d8f", + "artifactVersion": "85585dd4e87de9edca6c", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 3fdbd17da..35bc4ee50 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -458,9 +458,9 @@ "apiEntries": 276, "static": { "supported": 0, - "partial": 39, + "partial": 40, "refused": 0, - "not-implemented": 237, + "not-implemented": 236, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -36385,9 +36385,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 3edadc3d7..3f21cf0fb 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 46, - "verify-gap": 2711, - "audit-partial": 455, + "verify-gap": 2710, + "audit-partial": 456, "classify": 2, "implement": 81 }, @@ -35463,12 +35463,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:fs.fsPromises.realpath", + "tests": [ + "tests/corpus/2940-fsp-realpath.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 4c47229a8..7d51dd7b9 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -507,9 +507,9 @@ "apiEntries": 276, "static": { "supported": 0, - "partial": 39, + "partial": 40, "refused": 0, - "not-implemented": 237, + "not-implemented": 236, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -50572,7 +50572,8 @@ "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2844-node-disposable-resources.ts", "tests/corpus/2919-fs-function-values.ts", - "tests/corpus/2920-fs-promises-function-values.ts" + "tests/corpus/2920-fs-promises-function-values.ts", + "tests/corpus/2940-fsp-realpath.ts" ] }, "dynamic": { @@ -52042,8 +52043,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-feature:fs.fsPromises.realpath", + "tests": [ + "tests/corpus/2940-fsp-realpath.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index c10d05910..5ca49ab78 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -97,6 +97,7 @@ { "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] }, { "chapter": "child_process", "signatures": ["`stdin` Type: {stream.Writable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", "tests/corpus/2896-child-stdin-write-after-finish.ts", "tests/corpus/2897-child-stdin-destroy.ts", "tests/corpus/2898-child-stdin-spawn-failure.ts"] }, { "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, + { "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] }, { "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 1a0e724ee..ad4a6ed44 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -1225,6 +1225,7 @@ declare module "fs/promises" { export function readdir(path: string): Promise; export function rm(path: string): Promise; export function stat(path: string): Promise; + export function realpath(path: string): Promise; export function unlink(path: string): Promise; export function chmod(path: string, mode: number): Promise; export function rename(oldPath: string, newPath: string): Promise; diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index c0e13f567..b08b23b86 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -4667,6 +4667,8 @@ function emitFilesystemLibCall(state: LibCallState): Temp { return finish(`scr_fsp_rm(${arg(0)})`); case "fsp.stat": return finish(`scr_fsp_stat(${arg(0)})`); + case "fsp.realpath": + return finish(`scr_fsp_realpath(${arg(0)})`); case "fsp.open": return finish(`scr_fsp_open(${arg(0)}, ${arg(1)}, ${arg(2)})`); case "fileHandle.fd": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index a1a232b64..116657d95 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -397,6 +397,7 @@ export const LIB_FN_SYMS: Record = { "fsp.readdir": "scr_fsp_readdir", "fsp.rm": "scr_fsp_rm", "fsp.stat": "scr_fsp_stat", + "fsp.realpath": "scr_fsp_realpath", "fsp.open": "scr_fsp_open", "fileHandle.fd": "scr_file_handle_fd", "fileHandle.close": "scr_file_handle_close_promise", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index b179ac5c7..c8977a61b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -696,6 +696,7 @@ export const BUILTIN_MODULE_FNS: Recorddata, sizeof buf) == NULL) { - scr_fs_throw(errno ? errno : ENOENT, "lstat", path); +static WCHAR *scr_fs_win_wide(const ScrStr *path); +static int scr_fs_win_errno(DWORD error); +#endif + +static ScrStr *scr_fs_realpath_common(ScrStr *path, const char *op) { +#ifdef _WIN32 + WCHAR *wide = scr_fs_win_wide(path); + if (!wide) { + scr_fs_throw(scr_fs_win_errno(GetLastError()), op, path); return NULL; } - if (GetFileAttributesA(buf) == INVALID_FILE_ATTRIBUTES) { - scr_fs_throw(ENOENT, "lstat", path); + HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + free(wide); + if (handle == INVALID_HANDLE_VALUE) { + scr_fs_throw(scr_fs_win_errno(GetLastError()), op, path); return NULL; } - return scr_str_new(buf, strlen(buf)); + DWORD needed = GetFinalPathNameByHandleW(handle, NULL, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (needed == 0) { + DWORD error = GetLastError(); + CloseHandle(handle); + scr_fs_throw(scr_fs_win_errno(error), op, path); + return NULL; + } + WCHAR *resolved = malloc(((size_t)needed + 1) * sizeof *resolved); + if (!resolved) { + CloseHandle(handle); + scr_trap("scriptc: out of memory\n"); + } + DWORD length = GetFinalPathNameByHandleW(handle, resolved, needed + 1, + FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + DWORD error = length == 0 ? GetLastError() : length > needed ? ERROR_INSUFFICIENT_BUFFER : ERROR_SUCCESS; + CloseHandle(handle); + if (error != ERROR_SUCCESS) { + free(resolved); + scr_fs_throw(scr_fs_win_errno(error), op, path); + return NULL; + } + size_t prefix = 0; + bool unc = false; + if (length >= 8 && wcsncmp(resolved, L"\\\\?\\UNC\\", 8) == 0) { + prefix = 8; + unc = true; + } else if (length >= 4 && wcsncmp(resolved, L"\\\\?\\", 4) == 0) { + prefix = 4; + } + int utf8_len = WideCharToMultiByte(CP_UTF8, 0, resolved + prefix, + (int)(length - prefix), NULL, 0, NULL, NULL); + if (utf8_len <= 0) { + error = GetLastError(); + free(resolved); + scr_fs_throw(scr_fs_win_errno(error), op, path); + return NULL; + } + size_t extra = unc ? 2 : 0; + char *utf8 = malloc(extra + (size_t)utf8_len); + if (!utf8) { + free(resolved); + scr_trap("scriptc: out of memory\n"); + } + if (unc) { utf8[0] = '\\'; utf8[1] = '\\'; } + (void)WideCharToMultiByte(CP_UTF8, 0, resolved + prefix, (int)(length - prefix), + utf8 + extra, utf8_len, NULL, NULL); + ScrStr *result = scr_str_new(utf8, extra + (size_t)utf8_len); + free(utf8); + free(resolved); + return result; #else - /* realpath(3); Node's realpathSync reports failures with the "lstat" - * syscall in the message ("ENOENT: no such file or directory, lstat - * 'x'") — its own resolution walks lstat by component. */ char buf[PATH_MAX]; if (realpath(path->data, buf) == NULL) { - scr_fs_throw(errno, "lstat", path); + scr_fs_throw(errno, op, path); return NULL; } return scr_str_new(buf, strlen(buf)); #endif } +ScrStr *scr_fs_realpath(ScrStr *path) { + return scr_fs_realpath_common(path, "lstat"); +} + +ScrStr *scr_fs_realpath_promise(ScrStr *path) { + return scr_fs_realpath_common(path, "realpath"); +} + static void scr_fs_write_common(ScrStr *path, ScrStr *data, const char *mode) { FILE *f = fopen(path->data, mode); if (!f) { diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 3204cb659..cbdb22d7f 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -2312,6 +2312,7 @@ ScrStr *scr_fs_read_file(ScrStr *path); /* realpath(3) (+1 fresh); failures throw with Node's realpathSync * spelling (syscall "lstat"). */ ScrStr *scr_fs_realpath(ScrStr *path); +ScrStr *scr_fs_realpath_promise(ScrStr *path); void scr_fs_write_file(ScrStr *path, ScrStr *data); void scr_fs_append_file(ScrStr *path, ScrStr *data); bool scr_fs_exists(ScrStr *path); @@ -2428,6 +2429,7 @@ ScrPromise *scr_fsp_chmod(ScrStr *path, double mode); ScrPromise *scr_fsp_readdir(ScrStr *path); ScrPromise *scr_fsp_rm(ScrStr *path); ScrPromise *scr_fsp_stat(ScrStr *path); +ScrPromise *scr_fsp_realpath(ScrStr *path); ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath); ScrPromise *scr_fsp_open(ScrStr *path, ScrStr *flags, double mode); ScrPromise *scr_file_handle_close_promise(ScrFileHandle *h); diff --git a/tests/corpus/2940-fsp-realpath.ts b/tests/corpus/2940-fsp-realpath.ts new file mode 100644 index 000000000..affa2111d --- /dev/null +++ b/tests/corpus/2940-fsp-realpath.ts @@ -0,0 +1,41 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import * as fs from "node:fs"; +import { realpath } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-realpath-")); +const target = join(dir, "target"); +const link = join(dir, "link"); +mkdirSync(target); +writeFileSync(join(target, "file.txt"), "content"); +execFileSync( + "node", + [ + "-e", + "require('node:fs').symlinkSync(process.argv[1],process.argv[2],process.platform==='win32'?'junction':'dir')", + target, + link, + ], + { stdio: "pipe" }, +); + +async function main(): Promise { + try { + console.log("cwd", (await realpath(".")) === realpathSync(".")); + console.log("link", (await realpath(join(link, "file.txt"))) === realpathSync(join(target, "file.txt"))); + console.log("namespace", (await fs.promises.realpath(link)) === realpathSync(target)); + try { + await realpath(join(dir, "missing")); + console.log("missing unexpectedly resolved"); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log("missing", error instanceof Error, error.code, error.message.includes("realpath"), error.message.includes("missing")); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +main();