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 }); }