mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
fix(fs): support static async realpath
- add fs/promises.realpath with Node-shaped rejections - resolve Windows links to canonical paths - cover behavior and refresh compatibility evidence Fixes #371 Co-authored-by: Hagege Ruben <20857346+HagegeR@users.noreply.github.com>
This commit is contained in:
co-authored by
Hagege Ruben
parent
c9cad7a0ba
commit
1ecfdcdc04
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.15.0",
|
||||
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
|
||||
"artifactVersion": "80b3d2a5ad0d17bd8d8f",
|
||||
"artifactVersion": "85585dd4e87de9edca6c",
|
||||
"rowCount": 3662
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -1225,6 +1225,7 @@ declare module "fs/promises" {
|
||||
export function readdir(path: string): Promise<string[]>;
|
||||
export function rm(path: string): Promise<void>;
|
||||
export function stat(path: string): Promise<import("node:fs").Stats>;
|
||||
export function realpath(path: string): Promise<string>;
|
||||
export function unlink(path: string): Promise<void>;
|
||||
export function chmod(path: string, mode: number): Promise<void>;
|
||||
export function rename(oldPath: string, newPath: string): Promise<void>;
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -397,6 +397,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -696,6 +696,7 @@ export const BUILTIN_MODULE_FNS: Record<string, Record<string, BuiltinModuleFn |
|
||||
readdir: { fn: "fsp.readdir", params: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } },
|
||||
rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } },
|
||||
stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } },
|
||||
realpath: { fn: "fsp.realpath", params: [STRING], result: { kind: "promise", inner: STRING } },
|
||||
unlink: { fn: "fsp.unlink", params: [STRING], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING) },
|
||||
chmod: { fn: "fsp.chmod", params: [STRING, F64], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING, F64) },
|
||||
rename: { fn: "fsp.rename", params: [STRING, STRING], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING, STRING) },
|
||||
|
||||
@@ -3308,6 +3308,7 @@ export type IrLibFn =
|
||||
| "fsp.readdir"
|
||||
| "fsp.rm"
|
||||
| "fsp.stat"
|
||||
| "fsp.realpath"
|
||||
/** fs/promises.open and the statically represented FileHandle surface.
|
||||
* Every operation returns an already-settled promise; syscall failures
|
||||
* become rejections rather than escaping synchronously. read/write
|
||||
|
||||
@@ -874,6 +874,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } },
|
||||
"fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
|
||||
"fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } },
|
||||
"fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } },
|
||||
"fsp.open": { argTypes: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T } },
|
||||
"fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 },
|
||||
"fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } },
|
||||
|
||||
@@ -966,6 +966,12 @@
|
||||
"name": "fs/promises.readdir",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.promises.realpath",
|
||||
"kind": "node-builtin",
|
||||
"name": "fs/promises.realpath",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.promises.rename",
|
||||
"kind": "node-builtin",
|
||||
|
||||
@@ -1657,6 +1657,10 @@ ScrPromise *scr_fsp_stat(ScrStr *path) {
|
||||
return scr_promise_settled_ref(st, &scr_stats_retain_v, &scr_stats_release_v, NULL);
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_realpath(ScrStr *path) {
|
||||
return scr_promise_settled_str(scr_fs_realpath_promise(path));
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath) {
|
||||
scr_fs_rename(oldpath, newpath);
|
||||
return scr_promise_settled_void();
|
||||
|
||||
@@ -1852,34 +1852,94 @@ ScrStr *scr_fs_read_file(ScrStr *path) {
|
||||
return s;
|
||||
}
|
||||
|
||||
ScrStr *scr_fs_realpath(ScrStr *path) {
|
||||
#ifdef _WIN32
|
||||
/* _fullpath resolves . / .. and drive-relative forms (symlink-free —
|
||||
* the honest Windows approximation); a missing path throws Node's
|
||||
* lstat-spelled ENOENT like the POSIX arm. */
|
||||
char buf[PATH_MAX];
|
||||
if (_fullpath(buf, path->data, 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
Reference in New Issue
Block a user