mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 05:54:09 +08:00
fix: retire SDK declaration compiler before semantic rendering
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { deserialize, serialize } from "node:v8";
|
||||
import {
|
||||
resolveRuntimeWorkerArgv,
|
||||
resolveRuntimeWorkerUrl,
|
||||
} from "../../src/infra/runtime-worker-url.js";
|
||||
import { hasUnjoinedWork, runManagedCommand } from "./managed-child-process.mts";
|
||||
import { emitNativeDeclarations, type NativeDeclaration } from "./native-declaration-emitter.mts";
|
||||
import { isRecord } from "./record-shared.mjs";
|
||||
|
||||
type DeclarationRequest = Pick<
|
||||
Parameters<typeof emitNativeDeclarations>[0],
|
||||
"cwd" | "configFile" | "roots" | "compilerOptions" | "diagnostics"
|
||||
>;
|
||||
|
||||
function readRequest(file: string): DeclarationRequest {
|
||||
const value: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
typeof value.cwd !== "string" ||
|
||||
typeof value.configFile !== "string" ||
|
||||
!Array.isArray(value.roots) ||
|
||||
!value.roots.every((root): root is string => typeof root === "string") ||
|
||||
(value.compilerOptions !== undefined && !isRecord(value.compilerOptions)) ||
|
||||
(value.diagnostics !== undefined &&
|
||||
value.diagnostics !== "declarations" &&
|
||||
value.diagnostics !== "all")
|
||||
) {
|
||||
throw new Error("Invalid native declaration subprocess request");
|
||||
}
|
||||
return {
|
||||
cwd: value.cwd,
|
||||
configFile: value.configFile,
|
||||
roots: value.roots,
|
||||
compilerOptions: value.compilerOptions,
|
||||
diagnostics: value.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
function isDeclaration(value: unknown): value is NativeDeclaration {
|
||||
if (!isRecord(value) || typeof value.code !== "string" || !isRecord(value.map)) {
|
||||
return false;
|
||||
}
|
||||
const map = value.map;
|
||||
return (
|
||||
map.version === 3 &&
|
||||
typeof map.file === "string" &&
|
||||
typeof map.mappings === "string" &&
|
||||
Array.isArray(map.sources) &&
|
||||
map.sources.every((source) => typeof source === "string") &&
|
||||
Array.isArray(map.names) &&
|
||||
map.names.every((name) => typeof name === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function readResult(file: string): Awaited<ReturnType<typeof emitNativeDeclarations>> {
|
||||
const value: unknown = deserialize(fs.readFileSync(file));
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!Array.isArray(value.inputs) ||
|
||||
!value.inputs.every((input): input is string => typeof input === "string") ||
|
||||
!(value.declarations instanceof Map)
|
||||
) {
|
||||
throw new Error("Invalid native declaration subprocess result");
|
||||
}
|
||||
const declarations = new Map<string, NativeDeclaration>();
|
||||
for (const [source, declaration] of value.declarations) {
|
||||
if (typeof source !== "string" || !isDeclaration(declaration)) {
|
||||
throw new Error("Invalid native declaration subprocess output");
|
||||
}
|
||||
declarations.set(source, declaration);
|
||||
}
|
||||
return { inputs: value.inputs, declarations };
|
||||
}
|
||||
|
||||
/** Join native emission before synchronous semantic work can prevent its shutdown. */
|
||||
export async function emitNativeDeclarationsInSubprocess(options: DeclarationRequest) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-native-declarations-"));
|
||||
const request = path.join(directory, "request.json");
|
||||
let joined = true;
|
||||
try {
|
||||
fs.writeFileSync(request, JSON.stringify(options));
|
||||
const worker = resolveRuntimeWorkerUrl({
|
||||
currentModuleUrl: import.meta.url,
|
||||
sourceWorkerName: "native-declaration-subprocess",
|
||||
distWorkerPath: "legacy-finalizer/scripts/lib/native-declaration-subprocess.js",
|
||||
});
|
||||
const heapArgs = process.execArgv.flatMap((arg, index, args) =>
|
||||
/^--max[-_]old[-_]space[-_]size=/.test(arg)
|
||||
? [arg]
|
||||
: /^--max[-_]old[-_]space[-_]size$/.test(arg)
|
||||
? [arg, args[index + 1]!]
|
||||
: [],
|
||||
);
|
||||
let errorOutput = "";
|
||||
const code = await runManagedCommand({
|
||||
bin: process.execPath,
|
||||
args: [...heapArgs, ...resolveRuntimeWorkerArgv(worker), request],
|
||||
cwd: options.cwd,
|
||||
shell: false,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
requireProcessTreeExit: process.platform !== "win32",
|
||||
onReady(child) {
|
||||
child.stderr!.setEncoding("utf8");
|
||||
child.stderr!.on("data", (chunk: string) => {
|
||||
errorOutput += chunk.slice(0, Math.max(0, 64 * 1024 - errorOutput.length));
|
||||
});
|
||||
},
|
||||
});
|
||||
if (code !== 0) {
|
||||
throw new Error(
|
||||
errorOutput.trim() || `Native declaration subprocess exited with code ${code}`,
|
||||
);
|
||||
}
|
||||
return readResult(path.join(directory, "result.bin"));
|
||||
} catch (error) {
|
||||
joined = !hasUnjoinedWork(error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (joined) {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const request = process.argv[2];
|
||||
if (!request || process.argv.length !== 3) {
|
||||
throw new Error("Expected one native declaration request path");
|
||||
}
|
||||
const result = await emitNativeDeclarations(readRequest(request));
|
||||
fs.writeFileSync(path.join(path.dirname(request), "result.bin"), serialize(result));
|
||||
}
|
||||
@@ -132,6 +132,7 @@ export const preservedModuleBuildSources = [
|
||||
"scripts/run-additional-boundary-checks.mts",
|
||||
"scripts/run-with-env.mts",
|
||||
"scripts/plugin-sdk-api-diff.mts",
|
||||
"scripts/lib/native-declaration-subprocess.mts",
|
||||
"scripts/test-projects.mts",
|
||||
"scripts/lib/vitest-build-prerequisites.mts",
|
||||
"scripts/lib/vitest-batch-runner.mts",
|
||||
|
||||
@@ -6,8 +6,8 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as ts from "typescript/unstable/ast";
|
||||
import { Program } from "typescript/unstable/async";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as nativeDeclarations from "../../scripts/lib/native-declaration-subprocess.mts";
|
||||
import * as nativeTypeScript from "../../scripts/lib/native-typescript.mts";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { createDeclarationClosureRenderer } from "./api-baseline-declaration-closure.js";
|
||||
@@ -577,6 +577,38 @@ describe("Plugin SDK API baseline", () => {
|
||||
expect(fixtureError).not.toContain("return this.status");
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"joins the native declaration compiler before opening semantic overlays",
|
||||
async () => {
|
||||
const createProject = nativeTypeScript.createNativeTypeScriptProject;
|
||||
let observedOverlay = false;
|
||||
const create = vi
|
||||
.spyOn(nativeTypeScript, "createNativeTypeScriptProject")
|
||||
.mockImplementation((options) => {
|
||||
if (Object.keys(options.files ?? {}).some((file) => file.endsWith("fixture.ts"))) {
|
||||
observedOverlay = true;
|
||||
const processes = spawnSync("ps", ["-axo", "pid=,ppid=,command="], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(processes.status).toBe(0);
|
||||
expect(
|
||||
processes.stdout
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("--api --async") && line.includes(options.cwd)),
|
||||
).toEqual([]);
|
||||
}
|
||||
return createProject(options);
|
||||
});
|
||||
try {
|
||||
const rendered = await renderSourceFixture({ "fixture.ts": "export const value = 1;" });
|
||||
expect(observedOverlay).toBe(true);
|
||||
expect(rendered.modules[0]?.exports[0]?.declaration).toBe("export const value: 1;");
|
||||
} finally {
|
||||
create.mockRestore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(
|
||||
["source project creation", "declaration emission"].flatMap((timing) =>
|
||||
[0, 60_000].map((clockSkewMs) => ({ timing, clockSkewMs })),
|
||||
@@ -644,7 +676,8 @@ describe("Plugin SDK API baseline", () => {
|
||||
const createProject = nativeTypeScript.createNativeTypeScriptProject;
|
||||
const create = vi.spyOn(nativeTypeScript, "createNativeTypeScriptProject");
|
||||
// Declaration errors come from the emit result, the last compiler stage before publication.
|
||||
const emit = vi.spyOn(Program.prototype, "emitToString");
|
||||
const emitDeclarations = nativeDeclarations.emitNativeDeclarationsInSubprocess;
|
||||
const emit = vi.spyOn(nativeDeclarations, "emitNativeDeclarationsInSubprocess");
|
||||
if (timing === "source project creation") {
|
||||
create.mockImplementationOnce(function intercept(options) {
|
||||
const native = createProject(options);
|
||||
@@ -657,14 +690,11 @@ describe("Plugin SDK API baseline", () => {
|
||||
return native;
|
||||
});
|
||||
} else {
|
||||
emit.mockImplementationOnce(async function (this: Program, ...args) {
|
||||
emit.mockImplementationOnce(async function (options) {
|
||||
emit.mockRestore();
|
||||
const result = await this.emitToString(...args);
|
||||
expect(
|
||||
(await this.getSourceFileNames()).some((file) => path.resolve(file) === entry),
|
||||
).toBe(true);
|
||||
expect(result.emitSkipped).toBe(false);
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
const result = await emitDeclarations(options);
|
||||
expect(result.inputs).toContain(entry);
|
||||
expect(result.declarations.get(entry)?.code).toContain('"checked"');
|
||||
changeSource();
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
type Symbol as CompilerSymbol,
|
||||
} from "typescript/unstable/sync";
|
||||
import { CompilerInputSnapshot } from "../../scripts/lib/compiler-input-snapshot.mts";
|
||||
import { emitNativeDeclarations } from "../../scripts/lib/native-declaration-emitter.mts";
|
||||
import { createDeclarationFileSystem } from "../../scripts/lib/native-declaration-filesystem.mts";
|
||||
import { emitNativeDeclarationsInSubprocess } from "../../scripts/lib/native-declaration-subprocess.mts";
|
||||
import {
|
||||
createNativeTypeScriptProject,
|
||||
resolveInstalledNativeTypeScriptCompiler,
|
||||
@@ -146,7 +146,7 @@ async function createCompilerContext(
|
||||
fs: view.filesystem,
|
||||
});
|
||||
view.assertValid();
|
||||
const emitted = await emitNativeDeclarations({
|
||||
const emitted = await emitNativeDeclarationsInSubprocess({
|
||||
cwd: repoRoot,
|
||||
configFile: configPath,
|
||||
roots: fileNames,
|
||||
|
||||
Reference in New Issue
Block a user