release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
completionItems,
|
||||
definitionLocation,
|
||||
documentDiagnostics,
|
||||
documentSymbols,
|
||||
formatDocument,
|
||||
positionAt,
|
||||
symbolLocations,
|
||||
virtualTypeScriptDocument,
|
||||
workspaceCompletionItems,
|
||||
clearWorkspaceIndexCache,
|
||||
extractComponentRefactor,
|
||||
htmlToWrn,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const document = {
|
||||
uri: "file:///Counter.wrn",
|
||||
text: `component Counter {\nstate count = 0\nview { <button @click='count++'>{count}</button> }\n}`,
|
||||
};
|
||||
|
||||
test("provides editor-neutral language features", () => {
|
||||
expect(documentDiagnostics(document)).toEqual([]);
|
||||
expect(completionItems().some((item) => item.label === "state")).toBe(true);
|
||||
expect(documentSymbols(document).map((item) => item.name)).toContain("count");
|
||||
const reference = positionAt(document.text, document.text.lastIndexOf("count"));
|
||||
expect(definitionLocation(document, reference)).not.toBeNull();
|
||||
expect(symbolLocations(document, reference)).toHaveLength(3);
|
||||
expect(formatDocument(document)).toBeArray();
|
||||
});
|
||||
|
||||
test("indexes workspace components, contracts, routes, translations, schemas and classes", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-workspace-"));
|
||||
try {
|
||||
for (const directory of ["components", "pages", "locales", "db"])
|
||||
mkdirSync(join(root, "app", directory), { recursive: true });
|
||||
mkdirSync(join(root, "app", "pages", "users"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "app", "components", "Card.wrn"),
|
||||
`component Card { props { title: string } outputs { close() } view { <slot name="body" /> } }`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app", "pages", "users", "[id].wrn"),
|
||||
`page User { view { <p class="text-red-500">User</p> } }`,
|
||||
{ flag: "w" },
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app", "locales", "en.json"),
|
||||
JSON.stringify({ user: { title: "User" } }),
|
||||
);
|
||||
writeFileSync(join(root, "app", "db", "schema.ts"), `export const UserSchema = {};`);
|
||||
const labels = workspaceCompletionItems(root).map((item) => item.label);
|
||||
for (const label of [
|
||||
"Card",
|
||||
"title",
|
||||
"@close",
|
||||
"slot:body",
|
||||
"/users/:id",
|
||||
"user.title",
|
||||
"UserSchema",
|
||||
"text-red-500",
|
||||
])
|
||||
expect(labels).toContain(label);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace indexing is cached and skips dependency and generated trees", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-bounded-"));
|
||||
try {
|
||||
mkdirSync(join(root, "app", "pages"), { recursive: true });
|
||||
mkdirSync(join(root, "app", "node_modules", "large-package"), { recursive: true });
|
||||
mkdirSync(join(root, "app", ".wrnexus"), { recursive: true });
|
||||
writeFileSync(join(root, "app", "pages", "index.wrn"), `page Home { view { <p>Home</p> } }`);
|
||||
writeFileSync(
|
||||
join(root, "app", "node_modules", "large-package", "secret.ts"),
|
||||
`export const DependencySchema = {};`,
|
||||
);
|
||||
writeFileSync(join(root, "app", ".wrnexus", "generated.ts"), `export const CacheSchema = {};`);
|
||||
|
||||
const first = workspaceCompletionItems(root);
|
||||
const second = workspaceCompletionItems(root);
|
||||
expect(second).toBe(first);
|
||||
expect(first.map((item) => item.label)).not.toContain("DependencySchema");
|
||||
expect(first.map((item) => item.label)).not.toContain("CacheSchema");
|
||||
|
||||
writeFileSync(join(root, "app", "pages", "later.wrn"), `page Later { view { <p>Later</p> } }`);
|
||||
expect(workspaceCompletionItems(root).map((item) => item.label)).not.toContain("/later");
|
||||
clearWorkspaceIndexCache(root);
|
||||
expect(workspaceCompletionItems(root).map((item) => item.label)).toContain("/later");
|
||||
} finally {
|
||||
clearWorkspaceIndexCache(root);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("provides safe component extraction and HTML conversion refactors", () => {
|
||||
const doc = {
|
||||
uri: "file:///project/app/pages/index.wrn",
|
||||
version: 2,
|
||||
text: "<section>Hello</section>",
|
||||
};
|
||||
const range = { start: { line: 0, character: 0 }, end: { line: 0, character: doc.text.length } };
|
||||
const edit = extractComponentRefactor(doc, range, "Greeting");
|
||||
expect(edit.documentChanges).toHaveLength(3);
|
||||
expect(JSON.stringify(edit)).toContain("<Greeting />");
|
||||
expect(htmlToWrn(`<button onclick="save()">Save</button>`, "Imported")).toContain(
|
||||
`@click="save()"`,
|
||||
);
|
||||
});
|
||||
|
||||
test("provides a mapped TypeScript virtual document and expression type diagnostics", () => {
|
||||
const typed = {
|
||||
uri: "file:///Typed.wrn",
|
||||
text: `component Typed {\nprops { count: number }\nstate label: string = count\nview { <p>{label}</p> }\n}`,
|
||||
};
|
||||
const virtual = virtualTypeScriptDocument(typed);
|
||||
expect(virtual.languageId).toBe("typescript");
|
||||
expect(virtual.uri).toEndWith(".wrn.ts");
|
||||
expect(virtual.text).toContain("declare const count: Readonly<number>");
|
||||
expect(virtual.mappings.length).toBeGreaterThan(0);
|
||||
expect(documentDiagnostics(typed).some((item) => String(item.code).startsWith("WRN-TYPE-"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("reports compiler and accessibility diagnostics", () => {
|
||||
const diagnostics = documentDiagnostics({
|
||||
uri: "file:///bad.wrn",
|
||||
text: `page Bad {\n view {\n <img src='/x.png' />\n }\n}`,
|
||||
});
|
||||
expect(diagnostics.some((item) => String(item.code).startsWith("WRN-A11Y"))).toBe(true);
|
||||
});
|
||||
|
||||
test("skips compiler graph creation for oversized WRN documents", () => {
|
||||
const diagnostics = documentDiagnostics({
|
||||
uri: "file:///large.wrn",
|
||||
text: `page Large { view { <p>${"x".repeat(1_048_576)}</p> } }`,
|
||||
});
|
||||
expect(diagnostics).toHaveLength(1);
|
||||
expect(diagnostics[0]?.code).toBe("WRN-LSP-FILE-SIZE");
|
||||
});
|
||||
|
||||
function packet(value: unknown): Uint8Array {
|
||||
const body = JSON.stringify(value);
|
||||
return new TextEncoder().encode(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
||||
}
|
||||
|
||||
test("serves initialize over standard LSP stdio framing", async () => {
|
||||
const process = Bun.spawn(
|
||||
["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))],
|
||||
{
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
);
|
||||
process.stdin.write(packet({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }));
|
||||
await process.stdin.flush();
|
||||
const reader = process.stdout.getReader();
|
||||
let output = "";
|
||||
while (!output.includes('"id":1')) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
output += new TextDecoder().decode(chunk.value);
|
||||
}
|
||||
expect(output).toContain("WRNexus Language Server");
|
||||
expect(output).toContain("documentFormattingProvider");
|
||||
expect(output).toContain("wrnexusVirtualTypeScript");
|
||||
process.kill();
|
||||
await process.exited;
|
||||
});
|
||||
|
||||
test("coalesces rapid document changes into one pending diagnostic analysis", async () => {
|
||||
const process = Bun.spawn(
|
||||
["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))],
|
||||
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
|
||||
);
|
||||
const uri = "file:///rapid.wrn";
|
||||
process.stdin.write(
|
||||
packet({
|
||||
jsonrpc: "2.0",
|
||||
method: "textDocument/didOpen",
|
||||
params: { textDocument: { uri, version: 1, text: `page Rapid { view { <p>1</p> } }` } },
|
||||
}),
|
||||
);
|
||||
for (let version = 2; version <= 50; version++) {
|
||||
process.stdin.write(
|
||||
packet({
|
||||
jsonrpc: "2.0",
|
||||
method: "textDocument/didChange",
|
||||
params: {
|
||||
textDocument: { uri, version },
|
||||
contentChanges: [{ text: `page Rapid { view { <p>${version}</p> } }` }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
process.stdin.write(
|
||||
packet({ jsonrpc: "2.0", id: 99, method: "wrnexus/serverStatus", params: {} }),
|
||||
);
|
||||
await process.stdin.flush();
|
||||
const reader = process.stdout.getReader();
|
||||
let output = "";
|
||||
while (!output.includes('"id":99')) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
output += new TextDecoder().decode(chunk.value);
|
||||
}
|
||||
expect(output).toContain('"pendingDiagnostics":1');
|
||||
expect(output).toContain('"openDocuments":1');
|
||||
process.kill();
|
||||
await process.exited;
|
||||
});
|
||||
Reference in New Issue
Block a user