mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-04 10:10:01 +00:00
231 lines
7.5 KiB
TypeScript
231 lines
7.5 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import type { FileSystem } from "typescript/unstable/fs";
|
|
import { createDeclarationInputBoundary } from "./local-check-runtime.mts";
|
|
|
|
export type DeclarationLookup =
|
|
| { kind: "readFile" | "fileExists" | "directoryExists"; path: string; result: boolean }
|
|
| {
|
|
kind: "getAccessibleEntries";
|
|
path: string;
|
|
result: { files: string[]; directories: string[] };
|
|
}
|
|
| { kind: "realpath"; path: string; result: string };
|
|
|
|
/** Record compiler reads and apply the caller's checkout policy when supplied. */
|
|
export function createDeclarationFileSystem(
|
|
cwd: string,
|
|
admit: ((file: string) => string) | undefined,
|
|
virtualFiles: ReadonlyMap<string, string>,
|
|
readText: (file: string) => string = (file) => fs.readFileSync(file, "utf8"),
|
|
) {
|
|
const boundary = admit ? createDeclarationInputBoundary(cwd) : undefined;
|
|
const resolve = (file: string) => boundary?.resolve(file) ?? path.resolve(cwd, file);
|
|
const inputs = new Set<string>();
|
|
const lookups = new Map<string, DeclarationLookup>();
|
|
let failure: Error | undefined;
|
|
const reject = (error: unknown) => {
|
|
failure ??= error instanceof Error ? error : new Error(String(error));
|
|
};
|
|
const assertValid = () => {
|
|
if (failure) {
|
|
throw failure;
|
|
}
|
|
};
|
|
const observe = (lookup: DeclarationLookup) => {
|
|
if (!boundary) {
|
|
return;
|
|
}
|
|
const absolute = resolve(lookup.path);
|
|
const relative = path.relative(boundary.root, absolute);
|
|
// Virtual configs derive from sealed options; outside candidates are always denied.
|
|
if (
|
|
virtualFiles.has(absolute) ||
|
|
relative === ".." ||
|
|
relative.startsWith(`..${path.sep}`) ||
|
|
path.isAbsolute(relative)
|
|
) {
|
|
return;
|
|
}
|
|
const file = relative.split(path.sep).join("/") || ".";
|
|
const recorded: DeclarationLookup =
|
|
lookup.kind === "realpath"
|
|
? {
|
|
...lookup,
|
|
path: file,
|
|
result: path.relative(boundary.root, lookup.result).split(path.sep).join("/") || ".",
|
|
}
|
|
: { ...lookup, path: file };
|
|
const key = `${recorded.kind}\0${recorded.path}`;
|
|
const previous = lookups.get(key);
|
|
if (previous && JSON.stringify(previous) !== JSON.stringify(recorded)) {
|
|
reject(new Error(`Native compiler lookup changed during compilation: ${recorded.path}`));
|
|
}
|
|
lookups.set(key, recorded);
|
|
};
|
|
const local = (file: string) => {
|
|
const absolute = resolve(file);
|
|
if (!boundary || !admit) {
|
|
return absolute;
|
|
}
|
|
const relative = path.relative(boundary.root, absolute);
|
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
return undefined;
|
|
}
|
|
// A local alias must not expose an external installation. Outside lexical
|
|
// candidates are absent even if a symlink would lead back into the checkout.
|
|
try {
|
|
return admit(boundary.assert(absolute));
|
|
} catch (error) {
|
|
// A thrown RPC callback can strand the native request. Deny the lookup
|
|
// and report its original boundary error after that request settles.
|
|
reject(error);
|
|
return undefined;
|
|
}
|
|
};
|
|
const missing = (error: unknown) =>
|
|
error instanceof Error &&
|
|
"code" in error &&
|
|
(error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
const stat = (file: string) => {
|
|
try {
|
|
return fs.statSync(file, { throwIfNoEntry: false });
|
|
} catch (error) {
|
|
if (missing(error)) {
|
|
return undefined;
|
|
}
|
|
reject(error);
|
|
return undefined;
|
|
}
|
|
};
|
|
const physical = {
|
|
readFile(file) {
|
|
const accepted = local(file);
|
|
if (accepted === undefined) {
|
|
return null;
|
|
}
|
|
const virtual = virtualFiles.get(accepted);
|
|
if (virtual !== undefined) {
|
|
return virtual;
|
|
}
|
|
try {
|
|
const text = readText(accepted);
|
|
inputs.add(accepted);
|
|
return text;
|
|
} catch (error) {
|
|
if (missing(error)) {
|
|
return null;
|
|
}
|
|
reject(error);
|
|
return null;
|
|
}
|
|
},
|
|
fileExists(file) {
|
|
const accepted = local(file);
|
|
return (
|
|
accepted !== undefined && (virtualFiles.has(accepted) || stat(accepted)?.isFile() === true)
|
|
);
|
|
},
|
|
directoryExists(directory) {
|
|
const accepted = local(directory);
|
|
return accepted !== undefined && stat(accepted)?.isDirectory() === true;
|
|
},
|
|
getAccessibleEntries(directory) {
|
|
const accepted = local(directory);
|
|
const entries = { files: [] as string[], directories: [] as string[] };
|
|
if (accepted === undefined) {
|
|
return entries;
|
|
}
|
|
try {
|
|
for (const entry of fs.readdirSync(accepted, { withFileTypes: true })) {
|
|
const file = entry.isSymbolicLink() ? local(path.join(accepted, entry.name)) : undefined;
|
|
if (entry.isSymbolicLink() && file === undefined) {
|
|
continue;
|
|
}
|
|
const isDirectory = file === undefined ? entry.isDirectory() : stat(file)?.isDirectory();
|
|
(isDirectory ? entries.directories : entries.files).push(entry.name);
|
|
}
|
|
return entries;
|
|
} catch (error) {
|
|
if (missing(error)) {
|
|
return entries;
|
|
}
|
|
reject(error);
|
|
return entries;
|
|
}
|
|
},
|
|
realpath(file) {
|
|
const accepted = local(file);
|
|
// Returning undefined delegates to the host filesystem, including outside
|
|
// paths. Missing paths retain their spelling instead of escaping the view.
|
|
if (accepted === undefined || virtualFiles.has(accepted)) {
|
|
return resolve(file);
|
|
}
|
|
try {
|
|
const real = fs.realpathSync.native(accepted);
|
|
return boundary?.assert(real) ?? real;
|
|
} catch (error) {
|
|
if (!missing(error)) {
|
|
reject(error);
|
|
}
|
|
return accepted;
|
|
}
|
|
},
|
|
writeFile(this: void) {
|
|
reject(new Error("Native declarations must be emitted in memory"));
|
|
},
|
|
} satisfies FileSystem;
|
|
const filesystem = {
|
|
readFile(file: string) {
|
|
const result = physical.readFile(file);
|
|
observe({ kind: "readFile", path: file, result: result !== null });
|
|
return result;
|
|
},
|
|
fileExists(file: string) {
|
|
const result = physical.fileExists(file);
|
|
observe({ kind: "fileExists", path: file, result });
|
|
return result;
|
|
},
|
|
directoryExists(directory: string) {
|
|
const result = physical.directoryExists(directory);
|
|
observe({ kind: "directoryExists", path: directory, result });
|
|
return result;
|
|
},
|
|
getAccessibleEntries(directory: string) {
|
|
const result = physical.getAccessibleEntries(directory);
|
|
observe({ kind: "getAccessibleEntries", path: directory, result });
|
|
return result;
|
|
},
|
|
realpath(file: string) {
|
|
const result = physical.realpath(file);
|
|
observe({ kind: "realpath", path: file, result });
|
|
return result;
|
|
},
|
|
writeFile: physical.writeFile,
|
|
} satisfies FileSystem;
|
|
return {
|
|
filesystem,
|
|
inputs,
|
|
getLookups() {
|
|
assertValid();
|
|
return [...lookups]
|
|
.toSorted(([left], [right]) => (left < right ? -1 : 1))
|
|
.map(([, lookup]) => lookup);
|
|
},
|
|
assertValid,
|
|
};
|
|
}
|
|
|
|
/** Replay the same bounded operations, including negative resolution candidates. */
|
|
export function replayDeclarationLookups(
|
|
cwd: string,
|
|
admit: (file: string) => string,
|
|
lookups: readonly DeclarationLookup[],
|
|
readText: (file: string) => string,
|
|
) {
|
|
const view = createDeclarationFileSystem(cwd, admit, new Map(), readText);
|
|
for (const lookup of lookups) {
|
|
view.filesystem[lookup.kind](lookup.path);
|
|
}
|
|
return view.getLookups();
|
|
}
|