178 lines
6.4 KiB
JavaScript
178 lines
6.4 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
const { spawnSync } = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { test } = require("node:test");
|
|
|
|
const root = path.resolve(__dirname, "../../..");
|
|
const server = path.join(root, "editors", "vscode", "src", "language-server.cjs");
|
|
|
|
function rpc(value) {
|
|
const body = Buffer.from(JSON.stringify(value));
|
|
return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]);
|
|
}
|
|
|
|
function parseResponses(output) {
|
|
const responses = [];
|
|
let position = 0;
|
|
while (position < output.length) {
|
|
const headerEnd = output.indexOf(Buffer.from("\r\n\r\n"), position);
|
|
if (headerEnd < 0) break;
|
|
const header = output.subarray(position, headerEnd).toString();
|
|
const length = Number(/Content-Length:\s*(\d+)/i.exec(header)?.[1] || 0);
|
|
const start = headerEnd + 4;
|
|
const end = start + length;
|
|
if (!length || end > output.length) break;
|
|
responses.push(JSON.parse(output.subarray(start, end).toString()));
|
|
position = end;
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
function run(messages) {
|
|
const result = spawnSync(process.execPath, [server], {
|
|
cwd: root,
|
|
input: Buffer.concat(messages.map(rpc)),
|
|
timeout: 10_000,
|
|
});
|
|
assert.equal(result.status, 0, (result.stderr || Buffer.alloc(0)).toString());
|
|
return parseResponses(result.stdout || Buffer.alloc(0));
|
|
}
|
|
|
|
test("language server starts under Node and keeps official typed components clean", () => {
|
|
const files = [
|
|
path.join(root, "packages", "ui", "components", "AuthForm.wrn"),
|
|
path.join(root, "packages", "ui", "components", "FeatureGrid.wrn"),
|
|
];
|
|
const messages = [
|
|
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } },
|
|
{ jsonrpc: "2.0", method: "initialized", params: {} },
|
|
];
|
|
files.forEach((file, index) => {
|
|
messages.push({
|
|
jsonrpc: "2.0",
|
|
method: "textDocument/didOpen",
|
|
params: {
|
|
textDocument: {
|
|
uri: `file://${file}`,
|
|
languageId: "wrn",
|
|
version: index + 1,
|
|
text: fs.readFileSync(file, "utf8"),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
messages.push({ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} });
|
|
messages.push({ jsonrpc: "2.0", method: "exit", params: {} });
|
|
|
|
const responses = run(messages);
|
|
const initialization = responses.find((item) => item.id === 1)?.result;
|
|
assert.equal(initialization?.serverInfo?.version, "0.8.3");
|
|
assert.deepEqual(initialization?.capabilities?.textDocumentSync, {
|
|
openClose: true,
|
|
change: 1,
|
|
save: true,
|
|
});
|
|
const diagnostics = responses.filter((item) => item.method === "textDocument/publishDiagnostics");
|
|
assert.equal(diagnostics.length, 2);
|
|
assert.deepEqual(
|
|
diagnostics.map((item) => item.params.diagnostics),
|
|
[[], []],
|
|
);
|
|
});
|
|
|
|
test("language server defers expensive type analysis while typing and restores it on save", () => {
|
|
const uri = "file:///tmp/wrnexus-save-analysis.wrn";
|
|
const valid = `component SaveAnalysis {
|
|
outputs { save(payload: { id: string }) }
|
|
functions { client function submit(): void { output.save({ id: "ok" }) } }
|
|
view { <button @click="submit()"></button> }
|
|
}`;
|
|
const invalid = valid.replace('id: "ok"', "id: 123");
|
|
const responses = run([
|
|
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } },
|
|
{ jsonrpc: "2.0", method: "initialized", params: {} },
|
|
{
|
|
jsonrpc: "2.0",
|
|
method: "textDocument/didOpen",
|
|
params: { textDocument: { uri, languageId: "wrn", version: 1, text: valid } },
|
|
},
|
|
{
|
|
jsonrpc: "2.0",
|
|
method: "textDocument/didChange",
|
|
params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: invalid }] },
|
|
},
|
|
{ jsonrpc: "2.0", method: "textDocument/didSave", params: { textDocument: { uri } } },
|
|
{ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} },
|
|
{ jsonrpc: "2.0", method: "exit", params: {} },
|
|
]);
|
|
const diagnostics = responses.filter((item) => item.method === "textDocument/publishDiagnostics");
|
|
assert.equal(diagnostics.length, 2);
|
|
assert.ok(diagnostics.at(-1).params.diagnostics.some((item) => item.code === "WRN-TYPE-2322"));
|
|
});
|
|
|
|
test("language server formatter preserves code samples and compact sibling markup", () => {
|
|
const source = `page Docs {
|
|
view {
|
|
<pre><code><span class="muted">// One project, one language</span></code></pre>
|
|
<div class="strip"><span>Page</span><b>→</b><span>API</span></div>
|
|
}
|
|
}\n`;
|
|
const uri = "file:///tmp/wrnexus-docs.wrn";
|
|
const responses = run([
|
|
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } },
|
|
{ jsonrpc: "2.0", method: "initialized", params: {} },
|
|
{
|
|
jsonrpc: "2.0",
|
|
method: "textDocument/didOpen",
|
|
params: { textDocument: { uri, languageId: "wrn", version: 1, text: source } },
|
|
},
|
|
{
|
|
jsonrpc: "2.0",
|
|
id: 2,
|
|
method: "textDocument/formatting",
|
|
params: { textDocument: { uri }, options: { tabSize: 2, insertSpaces: true } },
|
|
},
|
|
{ jsonrpc: "2.0", id: 3, method: "shutdown", params: {} },
|
|
{ jsonrpc: "2.0", method: "exit", params: {} },
|
|
]);
|
|
const formatted = responses.find((item) => item.id === 2)?.result?.[0]?.newText;
|
|
assert.ok(formatted);
|
|
assert.match(formatted, /\/\/ One project, one language/);
|
|
assert.match(formatted, /<span>Page<\/span><b>→<\/b><span>API<\/span>/);
|
|
});
|
|
|
|
test("language server runs TypeScript diagnostics with workspace standard libraries", () => {
|
|
const uri = "file:///tmp/wrnexus-invalid-state.wrn";
|
|
const source = `component InvalidState {
|
|
outputs { save(payload: { id: string }) }
|
|
functions {
|
|
client function submit(value: string): void {
|
|
output.save({ id: 123 })
|
|
}
|
|
}
|
|
view { <button @click="submit('demo')"></button> }
|
|
}
|
|
`;
|
|
const responses = run([
|
|
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } },
|
|
{ jsonrpc: "2.0", method: "initialized", params: {} },
|
|
{
|
|
jsonrpc: "2.0",
|
|
method: "textDocument/didOpen",
|
|
params: { textDocument: { uri, languageId: "wrn", version: 1, text: source } },
|
|
},
|
|
{ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} },
|
|
{ jsonrpc: "2.0", method: "exit", params: {} },
|
|
]);
|
|
const diagnostics = responses.find((item) => item.method === "textDocument/publishDiagnostics")
|
|
?.params?.diagnostics;
|
|
assert.ok(Array.isArray(diagnostics));
|
|
assert.ok(
|
|
diagnostics.some((diagnostic) => diagnostic.code === "WRN-TYPE-2322"),
|
|
JSON.stringify(diagnostics, null, 2),
|
|
);
|
|
});
|