openclaw/scripts/lib/native-typescript.mts
Vincent Koc 4456a96af6
chore(ci): trace database worker ratchet phases (#186279)
* chore(ci): trace database worker ratchet phases

* fix(ci): preserve optional database worker observer arguments

* fix(ci): preserve database observer contextual types
2026-10-28 08:47:57 +08:00

298 lines
9.5 KiB
TypeScript

import { createRequire } from "node:module";
import path from "node:path";
import type { SourceFile } from "typescript/ast";
import { createFileSystem, createFileSystemLayer, type FileSystemCallbacks } from "typescript/fs";
import {
API,
type CreateSnapshotParams,
type Diagnostic,
type Project,
type Snapshot,
} from "typescript/sync";
export type NativeTypeScriptSource = { fileName: string; text: string };
export type NativeTypeScriptParser = ReturnType<typeof createNativeTypeScriptParser>;
export type NativeTypeScriptProject = ReturnType<typeof createNativeTypeScriptProject>;
export type NativeTypeScriptObserver = (
phase: string,
facts?: Readonly<Record<string, string | number>>,
) => void;
/** Diagnostics must never replace an analysis or disposal outcome. */
export function reportNativeTypeScriptPhase(
observer: NativeTypeScriptObserver | undefined,
phase: string,
facts?: Readonly<Record<string, string | number>>,
) {
try {
observer?.(phase, facts);
} catch {
// The analysis remains authoritative when its diagnostic sink fails.
}
}
function nativePhaseReporter(getApi: () => API | undefined, observer: NativeTypeScriptObserver) {
let totals: Readonly<Record<string, number>> | undefined;
return (phase: string, sample = true) => {
const api = getApi();
if (!api) {
return;
}
let fresh = false;
if (sample) {
try {
const timing = api.getTimingInfo().totals;
totals = Object.fromEntries(
(
[
"requestCount",
"roundTripMs",
"serverTimeMs",
"bytesSent",
"bytesReceived",
"sourceFilesFetched",
"nodesFetched",
"nodesMaterialized",
] as const
).map((metric) => [metric, timing[metric]]),
);
fresh = true;
} catch {
// Timing is an extra RPC; retain the last successful snapshot if it fails.
}
}
reportNativeTypeScriptPhase(observer, phase, {
...totals,
timingSample: fresh ? "current" : totals ? "last-successful" : "unavailable",
});
};
}
type ProjectOptions = {
cwd: string;
configFileName: string;
files?: Readonly<Record<string, string>>;
fs?: FileSystemCallbacks;
};
/** Select the same installed compiler as the native API, including in relocated tooling. */
export function resolveInstalledNativeTypeScriptCompiler() {
const require = createRequire(import.meta.url);
const packageJson = require.resolve("typescript/package.json");
const getExePath: { default: () => string } = require(
path.join(path.dirname(packageJson), "lib/getExePath.js"),
);
return { executable: getExePath.default(), packageJson };
}
function compilerFileName(cwd: string, file: string) {
return path.resolve(cwd, file).split(path.sep).join("/");
}
/** Own one native compiler process and its immutable semantic snapshot. */
export function createNativeTypeScriptProject(
options: ProjectOptions & { observer?: NativeTypeScriptObserver },
) {
const cwd = path.resolve(options.cwd);
const configFileName = compilerFileName(cwd, options.configFileName);
const api = new API({
cwd,
fs: options.fs,
collectTiming: options.observer ? true : undefined,
});
const checkpoint = options.observer && nativePhaseReporter(() => api, options.observer);
let snapshot: Snapshot;
let project: Project;
try {
snapshot = api.createSnapshot({
openProjects: [configFileName],
ensurePrograms: true,
fileSystem: options.files
? createFileSystemLayer(
Object.entries(options.files).map(([file, text]) => [
compilerFileName(cwd, file),
text,
]),
)
: undefined,
});
const opened = snapshot.getConfiguredProject(configFileName);
if (!opened) {
throw new Error(`Native TypeScript did not open ${configFileName}`);
}
project = opened;
checkpoint?.("project");
} catch (error) {
api.close();
checkpoint?.("close", false);
throw error;
}
let closed = false;
const close = () => {
if (closed) {
return;
}
closed = true;
api.close();
// Error unwinding must not issue another RPC to a possibly failed compiler.
checkpoint?.("close", false);
};
return { api, snapshot, project, checkpoint, close, [Symbol.dispose]: close };
}
/** The async transport keeps its child referenced while close ends the server's input. */
export async function createNativeTypeScriptProjectAsync(options: ProjectOptions) {
const { API: AsyncAPI } = await import("typescript/async");
const cwd = path.resolve(options.cwd);
const configFileName = compilerFileName(cwd, options.configFileName);
const api = new AsyncAPI({
cwd,
fs: options.fs,
});
try {
const snapshot = await api.createSnapshot({
openProjects: [configFileName],
ensurePrograms: true,
fileSystem: options.files
? createFileSystemLayer(
Object.entries(options.files).map(([file, text]) => [
compilerFileName(cwd, file),
text,
]),
)
: undefined,
});
const project = snapshot.getConfiguredProject(configFileName);
if (!project) {
throw new Error(`Native TypeScript did not open ${configFileName}`);
}
let closePromise: Promise<void> | undefined;
const close = () => (closePromise ??= api.close());
return { api, snapshot, project, close, [Symbol.asyncDispose]: close };
} catch (error) {
await api.close();
throw error;
}
}
/** Batch syntax scans share one explicit process; source trees remain local after disposal. */
export function createNativeTypeScriptParser({
cwd = process.cwd(),
tsserverPath,
observer,
}: { cwd?: string; tsserverPath?: string; observer?: NativeTypeScriptObserver } = {}) {
const root = path.resolve(cwd);
const configFileName = compilerFileName(root, ".openclaw-native-parser.tsconfig.json");
let api: API | undefined;
const checkpoint = observer && nativePhaseReporter(() => api, observer);
let snapshot: Snapshot | undefined;
let project: Project | undefined;
let sourceNames: string[] = [];
let closed = false;
const assertOpen = () => {
if (closed) {
throw new Error("Native TypeScript parser is closed");
}
};
const parseSourceFiles = (sources: readonly NativeTypeScriptSource[]): readonly SourceFile[] => {
assertOpen();
if (sources.length === 0) {
snapshot?.dispose();
snapshot = undefined;
project = undefined;
return [];
}
const previous = snapshot;
const names = sources.map((source) => compilerFileName(root, source.fileName));
if (new Set(names).size !== names.length) {
throw new Error("Native TypeScript parser received duplicate source paths");
}
if (names.includes(configFileName)) {
throw new Error(`Native TypeScript parser source conflicts with ${configFileName}`);
}
const files = Object.fromEntries(
sources.map((source) => [compilerFileName(root, source.fileName), source.text]),
);
files[configFileName] = JSON.stringify({
compilerOptions: {
allowJs: true,
experimentalDecorators: true,
jsx: "preserve",
noLib: true,
noResolve: true,
target: "esnext",
types: [],
},
files: names,
});
api ??= new API({
cwd: root,
tsserverPath,
collectTiming: observer ? true : undefined,
});
const previousNames = new Set(sourceNames);
const currentNames = new Set(names);
const changes: CreateSnapshotParams = {
...(previous ? {} : { openProjects: [configFileName] }),
fileSystem: createFileSystem(Object.entries(files)),
// A config-file change reloads its root list; invalidateAll retains the old roots.
fileNotifications: {
changed: [configFileName, ...names.filter((name) => previousNames.has(name))],
created: names.filter((name) => !previousNames.has(name)),
deleted: sourceNames.filter((name) => !currentNames.has(name)),
},
ensurePrograms: true,
};
snapshot = previous ? previous.update(changes) : api.createSnapshot(changes);
sourceNames = names;
previous?.dispose();
project = snapshot.getConfiguredProject(configFileName);
if (!project) {
throw new Error("Native TypeScript did not open the syntax scan project");
}
const program = project.program;
checkpoint?.("project");
const parsed = names.map((fileName) => {
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error(`Native TypeScript did not parse ${fileName}`);
}
return sourceFile;
});
checkpoint?.("sources");
return parsed;
};
const close = () => {
if (closed) {
return;
}
closed = true;
api?.close();
checkpoint?.("close", false);
api = undefined;
project = undefined;
snapshot = undefined;
sourceNames = [];
};
return {
checkpoint,
parseSourceFiles,
parseSourceFile(fileName: string, text: string): SourceFile {
const sourceFile = parseSourceFiles([{ fileName, text }])[0];
if (!sourceFile) {
throw new Error(`Native TypeScript did not parse ${fileName}`);
}
return sourceFile;
},
getSyntacticDiagnostics(fileName?: string): readonly Diagnostic[] {
assertOpen();
return (
project?.program.getSyntacticDiagnostics(
fileName === undefined ? undefined : compilerFileName(root, fileName),
) ?? []
);
},
close,
[Symbol.dispose]: close,
};
}