Files
WRNexusJS/scripts/build-editor-language-server.mjs
Clintchiz 72e4d3eceb
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-04 12:19:09 +05:30

165 lines
5.5 KiB
JavaScript

import { Buffer } from "node:buffer";
import { createHash } from "node:crypto";
import {
existsSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, relative, resolve, sep } from "node:path";
import { spawnSync } from "node:child_process";
import process from "node:process";
import { fileURLToPath } from "node:url";
const root = resolve(import.meta.dirname, "..");
const source = join(root, "packages", "language-server", "src", "server.ts");
const destination = join(root, "editors", "vscode", "src", "language-server.cjs");
const scriptPath = fileURLToPath(import.meta.url);
const check = process.argv.includes("--check");
const temporary = mkdtempSync(join(tmpdir(), "wrnexus-editor-lsp-"));
const output = join(temporary, "language-server.cjs");
function normalizeText(value) {
return value.replace(/\r\n?/g, "\n");
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
function walk(directory) {
const files = [];
for (const entry of readdirSync(directory)) {
const path = join(directory, entry);
const stat = statSync(path);
if (stat.isDirectory()) files.push(...walk(path));
else if (stat.isFile() && path.endsWith(".ts") && !path.endsWith(".test.ts")) files.push(path);
}
return files;
}
const sourceFiles = [
...walk(join(root, "packages", "language-server", "src")),
...walk(join(root, "packages", "syntax", "src")),
...walk(join(root, "packages", "typecheck", "src")),
join(root, "packages", "language-server", "package.json"),
join(root, "packages", "syntax", "package.json"),
join(root, "packages", "typecheck", "package.json"),
].sort();
function fingerprint(files) {
const hash = createHash("sha256");
for (const file of files) {
hash.update(relative(root, file).split(sep).join("/"));
hash.update("\0");
hash.update(normalizeText(readFileSync(file, "utf8")));
hash.update("\0");
}
return hash.digest("hex");
}
const sourceHash = fingerprint(sourceFiles);
const generatorHash = sha256(normalizeText(readFileSync(scriptPath, "utf8")));
function makeNodeExecutable(bundle) {
let value = normalizeText(bundle).replace(/^#!\/usr\/bin\/env bun\n/, "#!/usr/bin/env node\n");
const bunWrapper = "(function(exports, require, module, __filename, __dirname) {";
const invocation = "})(exports, require, module, __filename, __dirname);";
if (
value.includes("// @bun @bun-cjs") &&
value.includes(bunWrapper) &&
!value.trimEnd().endsWith(invocation)
) {
value = value.replace(/\}\)\s*$/, `${invocation}\n`);
}
return value;
}
function stampBundle(bundle) {
const withoutOldStamp = bundle
.replace(/^\/\/ WRN editor language server source hash: [a-f0-9]{64}\n/m, "")
.replace(/^\/\/ WRN editor language server generator hash: [a-f0-9]{64}\n/m, "");
const lines = withoutOldStamp.split("\n");
const insertion = lines[0]?.startsWith("#!") ? 1 : 0;
lines.splice(
insertion,
0,
`// WRN editor language server source hash: ${sourceHash}`,
`// WRN editor language server generator hash: ${generatorHash}`,
);
return `${lines.join("\n").replace(/\n+$/, "")}\n`;
}
function rpc(value) {
const body = JSON.stringify(value);
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
}
function verifyStarts(file) {
const input = [
rpc({ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } }),
rpc({ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }),
rpc({ jsonrpc: "2.0", method: "exit", params: {} }),
].join("");
const result = spawnSync(process.execPath, [file], {
cwd: root,
input,
encoding: "utf8",
timeout: 10_000,
});
if (result.error) throw result.error;
if (result.status !== 0 || !result.stdout.includes("WRNexus Language Server")) {
throw new Error(
`VS Code language server smoke test failed.\n${result.stderr || result.stdout || `exit ${result.status}`}`,
);
}
}
try {
const built = spawnSync(
process.env.WRNEXUS_BUN_BINARY || "bun",
["build", source, "--target=node", "--format=cjs", `--outfile=${output}`],
{ cwd: root, encoding: "utf8" },
);
if (built.status !== 0) throw new Error(built.stderr || built.stdout || "LSP bundle failed");
const generated = stampBundle(makeNodeExecutable(readFileSync(output, "utf8")));
writeFileSync(output, generated, "utf8");
verifyStarts(output);
if (check) {
const existing = existsSync(destination)
? normalizeText(readFileSync(destination, "utf8"))
: "";
const embeddedSourceHash = existing.match(
/^\/\/ WRN editor language server source hash: ([a-f0-9]{64})$/m,
)?.[1];
const embeddedGeneratorHash = existing.match(
/^\/\/ WRN editor language server generator hash: ([a-f0-9]{64})$/m,
)?.[1];
const exact = existing === generated;
const sourceCompatible =
embeddedSourceHash === sourceHash && embeddedGeneratorHash === generatorHash;
if (!exact && !sourceCompatible) {
throw new Error(
`${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`,
);
}
verifyStarts(destination);
process.stdout.write(
`Verified ${relative(root, destination)} matches the language server sources and starts under Node.\n`,
);
} else {
writeFileSync(destination, generated, "utf8");
process.stdout.write(
`Built ${relative(root, destination)} and verified its Node entrypoint.\n`,
);
}
} finally {
rmSync(temporary, { recursive: true, force: true });
}