release: WRNexusJS 0.8.0
This commit is contained in:
@@ -1,31 +1,141 @@
|
||||
import console from "node:console";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
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 } from "node:path";
|
||||
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, "editors", "vscode", "src", "extension.js");
|
||||
const destination = join(root, "editors", "vscode", "src", "extension.bundle.cjs");
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const check = process.argv.includes("--check");
|
||||
const temporary = check ? mkdtempSync(join(tmpdir(), "wrnexus-editor-extension-")) : null;
|
||||
const output = temporary ? join(temporary, "extension.bundle.cjs") : destination;
|
||||
const temporary = mkdtempSync(join(tmpdir(), "wrnexus-editor-extension-"));
|
||||
const output = join(temporary, "extension.bundle.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));
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) continue;
|
||||
if (!/\.(?:js|json|cjs)$/.test(path)) continue;
|
||||
if (path === destination || path.endsWith(`${sep}language-server.cjs`)) continue;
|
||||
files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const sourceFiles = [
|
||||
...walk(join(root, "editors", "vscode", "src")),
|
||||
join(root, "editors", "vscode", "package.json"),
|
||||
join(root, "editors", "vscode", "language-configuration.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 stampBundle(bundle) {
|
||||
const withoutOldStamp = normalizeText(bundle)
|
||||
.replace(/^\/\/ WRN editor extension source hash: [a-f0-9]{64}\n/m, "")
|
||||
.replace(/^\/\/ WRN editor extension 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 extension source hash: ${sourceHash}`,
|
||||
`// WRN editor extension generator hash: ${generatorHash}`,
|
||||
);
|
||||
return `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
||||
}
|
||||
|
||||
function verifySyntax(file) {
|
||||
const result = spawnSync(process.execPath, ["--check", file], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`VS Code extension bundle syntax check 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", "--external=vscode", `--outfile=${output}`],
|
||||
{ cwd: root, encoding: "utf8" },
|
||||
);
|
||||
if (built.status !== 0)
|
||||
if (built.status !== 0) {
|
||||
throw new Error(built.stderr || built.stdout || "extension bundle failed");
|
||||
}
|
||||
|
||||
const generated = stampBundle(readFileSync(output, "utf8"));
|
||||
writeFileSync(output, generated, "utf8");
|
||||
verifySyntax(output);
|
||||
|
||||
if (check) {
|
||||
if (!existsSync(destination) || readFileSync(destination).compare(readFileSync(output)) !== 0)
|
||||
const existing = existsSync(destination)
|
||||
? normalizeText(readFileSync(destination, "utf8"))
|
||||
: "";
|
||||
const embeddedSourceHash = existing.match(
|
||||
/^\/\/ WRN editor extension source hash: ([a-f0-9]{64})$/m,
|
||||
)?.[1];
|
||||
const embeddedGeneratorHash = existing.match(
|
||||
/^\/\/ WRN editor extension 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.`,
|
||||
`${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`,
|
||||
);
|
||||
console.log(`Verified ${relative(root, destination)} matches extension sources.`);
|
||||
} else console.log(`Built ${relative(root, destination)}.`);
|
||||
}
|
||||
verifySyntax(destination);
|
||||
process.stdout.write(
|
||||
`Verified ${relative(root, destination)} matches the extension sources and has valid Node syntax.\n`,
|
||||
);
|
||||
} else {
|
||||
writeFileSync(destination, generated, "utf8");
|
||||
process.stdout.write(`Built ${relative(root, destination)}.\n`);
|
||||
}
|
||||
} finally {
|
||||
if (temporary) rmSync(temporary, { recursive: true, force: true });
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,16 +1,124 @@
|
||||
import console from "node:console";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
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 } from "node:path";
|
||||
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 = check ? mkdtempSync(join(tmpdir(), "wrnexus-editor-lsp-")) : null;
|
||||
const output = temporary ? join(temporary, "language-server.cjs") : destination;
|
||||
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",
|
||||
@@ -18,13 +126,39 @@ try {
|
||||
{ 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) {
|
||||
if (!existsSync(destination) || readFileSync(destination).compare(readFileSync(output)) !== 0)
|
||||
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.`,
|
||||
`${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`,
|
||||
);
|
||||
console.log(`Verified ${relative(root, destination)} matches the language server sources.`);
|
||||
} else console.log(`Built ${relative(root, destination)}.`);
|
||||
}
|
||||
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 {
|
||||
if (temporary) rmSync(temporary, { recursive: true, force: true });
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user