release: WRNexusJS 0.8.0
This commit is contained in:
@@ -149,3 +149,63 @@ test("extracts v0.6 optional props, declared union options, and outputs", () =>
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts unresolved dynamic expressions for typed component props", () => {
|
||||
const featureCard = parseComponentMetadata(`component FeatureCard {
|
||||
props {
|
||||
external: boolean = false
|
||||
disabled: boolean = false
|
||||
showArrow: boolean = true
|
||||
}
|
||||
view { <article></article> }
|
||||
}`);
|
||||
const source = `component FeatureGrid {
|
||||
props { items: unknown[] = [] }
|
||||
view {
|
||||
{#each items as item}
|
||||
<FeatureCard
|
||||
external='{item.external || false}'
|
||||
disabled='{item.disabled || false}'
|
||||
showArrow='{item.showArrow !== false}'
|
||||
/>
|
||||
{/each}
|
||||
}
|
||||
}`;
|
||||
|
||||
assert.equal(attributeValueType("{item.external || false}"), "unknown");
|
||||
assert.equal(attributeValueType("{item.showArrow !== false}"), "boolean");
|
||||
assert.deepEqual(validateComponentTags(source, new Map([["FeatureCard", featureCard]])), []);
|
||||
});
|
||||
|
||||
test("continues to reject invalid literal boolean props", () => {
|
||||
const target = parseComponentMetadata(`component Toggle {
|
||||
props { enabled: boolean = false }
|
||||
view { <button></button> }
|
||||
}`);
|
||||
const diagnostics = validateComponentTags(
|
||||
`<Toggle enabled="definitely" />`,
|
||||
new Map([["Toggle", target]]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
diagnostics.map(({ code }) => code),
|
||||
["wrn-component-prop-type"],
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts unresolved dynamic expressions for declared string options", () => {
|
||||
const target = parseComponentMetadata(`component FeatureCard {
|
||||
props { target: "_blank" | "_self" = "_self" }
|
||||
view { <article></article> }
|
||||
}`);
|
||||
const dynamic = `<FeatureCard target='{item.target || ""}' />`;
|
||||
assert.deepEqual(validateComponentTags(dynamic, new Map([["FeatureCard", target]])), []);
|
||||
|
||||
const invalidLiteral = validateComponentTags(
|
||||
`<FeatureCard target="popup" />`,
|
||||
new Map([["FeatureCard", target]]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
invalidLiteral.map(({ code }) => code),
|
||||
["wrn-component-prop-option"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -201,3 +201,36 @@ page Test {
|
||||
|
||||
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []);
|
||||
});
|
||||
|
||||
test("does not interpret TypeScript generic types in outputs as HTML tags", () => {
|
||||
const source = `component AuthForm {
|
||||
outputs {
|
||||
change(payload: { values?: Array<string | number | boolean | null | object>; sourceEvent?: Event })
|
||||
}
|
||||
view { <form><strong>Sign in</strong></form> }
|
||||
}`;
|
||||
|
||||
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []);
|
||||
});
|
||||
|
||||
test("allows JavaScript-looking documentation text inside pre and code", () => {
|
||||
const source = `page Docs {
|
||||
view {
|
||||
<pre><code><span class="code-muted">// One project, one language</span>
|
||||
<span class="code-key">page</span> Dashboard {
|
||||
<button>Ship</button>
|
||||
}</code></pre>
|
||||
}
|
||||
}`;
|
||||
|
||||
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []);
|
||||
});
|
||||
|
||||
test("still reports genuinely mismatched view tags", () => {
|
||||
const source = `page Broken {
|
||||
view { <main><strong>Broken</main> }
|
||||
}`;
|
||||
const diagnostics = validateHtmlTags(mockDocument(source), source);
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0].code, "wrn-mismatched-html-tag");
|
||||
});
|
||||
|
||||
@@ -255,3 +255,48 @@ test("formats v0.6 grouped state and outputs blocks", () => {
|
||||
/\n {2}outputs \{\n {4}confirm\(payload: ConfirmPayload\)\n {4}cancel\(\)\n {2}\}/,
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves preformatted WRN code examples", () => {
|
||||
const source = `page Docs {
|
||||
view {
|
||||
<pre><code><span class="code-muted">// One project, one language</span>
|
||||
<span class="code-key">page</span> ProductDashboard {
|
||||
<span class="code-key">state</span> count: number = 0
|
||||
<button @click='count++'>Ship {count}</button>
|
||||
}</code></pre>
|
||||
}
|
||||
}
|
||||
`;
|
||||
const options = { insertSpaces: true, tabSize: 2, printWidth: 100 };
|
||||
const formatted = formatWrn(source, options);
|
||||
assert.match(
|
||||
formatted,
|
||||
/<pre><code><span class="code-muted">\/\/ One project, one language<\/span>\n<span class="code-key">page<\/span>/,
|
||||
);
|
||||
assert.equal(formatWrn(formatted, options), formatted);
|
||||
});
|
||||
|
||||
test("keeps attribute-free opening tags intact when content is long", () => {
|
||||
const source = `page Docs {
|
||||
view {
|
||||
<p>This documentation sentence is deliberately long enough to exceed the formatter print width without splitting the p opening delimiter.</p>
|
||||
}
|
||||
}
|
||||
`;
|
||||
const options = { insertSpaces: true, tabSize: 2, printWidth: 70 };
|
||||
const formatted = formatWrn(source, options);
|
||||
assert.match(formatted, /^ {4}<p>$/m);
|
||||
assert.doesNotMatch(formatted, /^ {4}<p\n {4}>$/m);
|
||||
assert.equal(formatWrn(formatted, options), formatted);
|
||||
});
|
||||
|
||||
test("preserves balanced compact sibling markup without indentation drift", () => {
|
||||
const source = `page Compact {
|
||||
view {
|
||||
<div class="strip"><span>Page</span><b>→</b><span>API</span></div>
|
||||
}
|
||||
}\n`;
|
||||
const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 });
|
||||
assert.match(formatted, /<div class="strip"><span>Page<\/span><b>→<\/b><span>API<\/span><\/div>/);
|
||||
assert.equal(formatWrn(formatted, { tabSize: 2, printWidth: 80 }), formatted);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"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);
|
||||
assert.equal(responses.find((item) => item.id === 1)?.result?.serverInfo?.version, "0.8.3");
|
||||
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 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),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user