release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-04 12:19:09 +05:30
parent 4cebacadfe
commit 72e4d3eceb
108 changed files with 2276 additions and 532 deletions
+15
View File
@@ -5,6 +5,21 @@
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
- Added compiler awareness for pruned and bundled browser hydration imports.
- Aligned the extension version with the WRNexusJS 0.8.3 framework release.
- Fixed repeated language-server crashes by producing a Node-executable bundle and containing
diagnostics failures without disposing the language-client connection.
- Fixed false missing/mismatched HTML tags from TypeScript generics, object payload types, comments,
and code samples outside or inside WRN `view` blocks.
- Fixed formatting of `<pre><code>` examples, compact sibling markup, long text elements, and
multiline tags so repeated formatting is idempotent.
- Fixed false boolean/string and literal-option diagnostics for dynamic component props such as
`external='{item.external || false}'`.
- Kept component prop/event intelligence active while the shared language server is enabled.
- Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation
diagnostics in packaged extension environments.
- Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers such as
`output[type](payload)` by preserving JavaScript semantics for omitted parameter types.
- Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run
consistently in the repository and extension development environment.
## 0.8.0
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wrnexus",
"version": "0.8.3",
"version": "0.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrnexus",
"version": "0.8.3",
"version": "0.8.4",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"vscode-languageclient": "^10.1.0"
+9 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
"version": "0.8.3",
"version": "0.8.4",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
@@ -126,6 +126,14 @@
"default": true,
"scope": "resource",
"description": "Place opening-tag attributes on separate lines, with the closing delimiter on its own line."
},
"wrnexus.formatting.printWidth": {
"type": "number",
"default": 100,
"minimum": 60,
"maximum": 240,
"scope": "resource",
"description": "Preferred WRNexus formatter line width before long tags are expanded."
}
}
},
+139 -7
View File
@@ -1,6 +1,6 @@
"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: b9e9a9ed0492c387198e368436696480ef2eca5a850ed94852c78c73c70d2959
// WRN editor compiler source hash: 1d2e0d1e1c38a4513b0fce3631bae003f7c28069920c5386ecc68cbe728bf57b
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
// Generated with TypeScript: 5.9.3
const __nodeRequire = require;
@@ -4845,6 +4845,18 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
opensElement,
};
}
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent)
lines.push(`${childIndent}${parsed.inlineContent}`);
lines.push(`${baseIndent}</${parsed.tagName}>`);
}
else if (parsed.remainder) {
lines.push(`${childIndent}${parsed.remainder}`);
}
return { lines, opensElement };
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
@@ -4886,6 +4898,73 @@ function isMultilineOpeningTagStart(value) {
function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
/**
* Preserve compact, already-balanced HTML fragments as one line.
*
* A line such as `<span>Page</span><b>→</b><span>API</span>` is valid and
* intentionally compact. Expanding only its first opening tag makes later
* formatter passes treat the remaining siblings as children, causing runaway
* indentation and false closing-tag diagnostics. Balanced fragments are kept
* intact while normal multiline opening tags continue through the formatter.
*/
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
}
else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return (tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText));
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
@@ -4991,6 +5070,41 @@ function collectOpeningTag(inputLines, startIndex) {
endIndex: index,
};
}
function isPreservedRawBlockStart(value) {
return (/<pre(?:\s|>)/i.test(value) &&
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i))));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
/**
* Put WRN template control markers on their own lines before indentation.
*
@@ -5000,7 +5114,7 @@ function collectOpeningTag(inputLines, startIndex) {
*/
function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
if (!marker.test(line))
return [line];
marker.lastIndex = 0;
@@ -5013,7 +5127,7 @@ function expandInlineControlBlocks(lines) {
});
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match)
return [line];
@@ -5068,6 +5182,14 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
if (isPreservedRawBlockStart(value)) {
const collected = collectPreservedRawBlock(inputLines, index);
const depth = codeDepth + htmlDepth + controlDepth;
output.push(`${unit.repeat(depth)}${collected.lines[0].trimStart()}`);
output.push(...collected.lines.slice(1));
index = collected.endIndex + 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
value = collected.value;
@@ -5090,7 +5212,11 @@ function formatWrnPass(source, options = {}) {
lineHtmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (value.startsWith("<") &&
const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
}
else if (value.startsWith("<") &&
!value.startsWith("</") &&
!value.startsWith("<!--") &&
!value.startsWith("<!") &&
@@ -6639,11 +6765,17 @@ function runtimeTypeOf(annotation) {
if (!annotation)
return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))))
return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 &&
concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part))))
return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part))))
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint";
+59 -9
View File
@@ -39,9 +39,24 @@ function runtimeType(type) {
const value = String(type || "")
.trim()
.replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "boolean";
const unionParts = value.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (
/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
)
return "string";
if (
/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
(concreteParts.length > 0 &&
concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
)
return "number";
if (
/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
)
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "bigint";
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array";
if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object";
@@ -138,15 +153,47 @@ function unwrapAttributeValue(value) {
return trimmed;
}
function isWrappedExpression(value) {
const trimmed = value.trim();
return trimmed.startsWith("{") && trimmed.endsWith("}");
}
function expressionLiteralType(value) {
const expression = value.trim();
if (/^(?:true|false)$/.test(expression)) return "boolean";
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(expression)) return "number";
if (/^(?:"[\s\S]*"|'[\s\S]*'|`[\s\S]*`)$/.test(expression)) return "string";
if (expression.startsWith("[")) return "array";
if (expression.startsWith("{")) return "object";
if (expression === "null") return "null";
return null;
}
function attributeValueType(value, symbols = new Map()) {
const expression = isWrappedExpression(value);
const unwrapped = unwrapAttributeValue(value);
if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
return symbols.get(unwrapped);
}
if (/^(?:true|false)$/.test(unwrapped)) return "boolean";
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return "number";
if (unwrapped.startsWith("[")) return "array";
if (unwrapped.startsWith("{")) return "object";
const literal = expressionLiteralType(unwrapped);
if (literal) return literal;
if (expression) {
if (
/^!\s*/.test(unwrapped) ||
/(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)
) {
return "boolean";
}
// Dynamic member access, calls, logical expressions and ternaries cannot be
// proven from component metadata alone. Treat them as unknown instead of as
// quoted strings so valid expressions such as `{item.external || false}` do
// not produce false boolean/string diagnostics.
return "unknown";
}
return "string";
}
@@ -154,6 +201,7 @@ function isTypeCompatible(prop, value, symbols = new Map()) {
const expected = runtimeType(prop.type);
if (expected === "unknown" || expected === "null") return true;
const actual = runtimeType(attributeValueType(value, symbols));
if (actual === "unknown") return true;
if (expected === actual) return true;
if (expected === "number" && actual === "string") return Number.isFinite(Number(value));
if (expected === "boolean" && actual === "string")
@@ -255,8 +303,10 @@ function validateComponentTags(source, components) {
end: attribute.nameEnd,
});
}
const literal = stringLiteral(attribute.value) ?? attribute.value;
if (prop.options.length > 0 && !prop.options.includes(literal)) {
const optionValue = isWrappedExpression(attribute.value)
? stringLiteral(unwrapAttributeValue(attribute.value))
: (stringLiteral(attribute.value) ?? attribute.value);
if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
diagnostics.push({
severity: "warning",
code: "wrn-component-prop-option",
+152 -119
View File
@@ -94,65 +94,6 @@ function lineDiagnostic(
return diagnostic;
}
function stripComments(source) {
const masked = [...source];
let quote = null;
let escaped = false;
const maskRange = (start, end) => {
for (let index = start; index < end; index += 1) {
if (source[index] !== "\n" && source[index] !== "\r") {
masked[index] = " ";
}
}
};
for (let index = 0; index < source.length; index += 1) {
const character = source[index];
if (quote !== null) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
if (source.startsWith("//", index)) {
const lineEnd = source.indexOf("\n", index + 2);
const end = lineEnd === -1 ? source.length : lineEnd;
maskRange(index, end);
index = end - 1;
continue;
}
if (source.startsWith("/*", index)) {
const commentEnd = source.indexOf("*/", index + 2);
const end = commentEnd === -1 ? source.length : commentEnd + 2;
maskRange(index, end);
index = end - 1;
continue;
}
if (source.startsWith("<!--", index)) {
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
maskRange(index, end);
index = end - 1;
}
}
return masked.join("");
}
function maskLeadingTrivia(source) {
const masked = [...source];
let offset = 0;
@@ -350,10 +291,96 @@ function validateBalancedCharacters(document, source) {
return diagnostics;
}
function maskHtmlComments(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (!source.startsWith("<!--", index)) {
index += 1;
continue;
}
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
for (let cursor = index; cursor < end; cursor += 1) {
if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
}
index = end;
}
return masked.join("");
}
function maskTemplateExpressions(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (source[index] !== "{") {
index += 1;
continue;
}
const start = index;
let depth = 0;
let quote = null;
let escaped = false;
while (index < source.length) {
const character = source[index];
if (quote !== null) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === quote) quote = null;
index += 1;
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
index += 1;
continue;
}
if (character === "{") depth += 1;
else if (character === "}") {
depth -= 1;
index += 1;
if (depth === 0) break;
continue;
}
index += 1;
}
for (let cursor = start; cursor < index; cursor += 1) {
if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
}
}
return masked.join("");
}
function findViewRanges(source) {
const ranges = [];
const pattern = /\bview\s*\{/g;
let match;
while ((match = pattern.exec(source)) !== null) {
const openingBrace = source.indexOf("{", match.index);
const closingBrace = findMatchingBrace(source, openingBrace);
if (closingBrace === -1) break;
ranges.push({ start: openingBrace + 1, end: closingBrace });
pattern.lastIndex = closingBrace + 1;
}
return ranges;
}
function validateHtmlTags(document, source) {
const diagnostics = [];
const stack = [];
const voidElements = new Set([
"area",
"base",
@@ -371,76 +398,82 @@ function validateHtmlTags(document, source) {
"wbr",
]);
const cleaned = stripComments(source);
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
for (const range of findViewRanges(source)) {
const fragment = source.slice(range.start, range.end);
// JavaScript-style `//` is valid rendered text inside <pre>/<code>. Only
// HTML comments and WRN expressions are masked before tag validation.
const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
const stack = [];
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
let match;
let match;
while ((match = tagPattern.exec(cleaned)) !== null) {
const completeTag = match[0];
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const lowerTag = tagName.toLowerCase();
const absoluteStart = range.start + match.index;
const isClosing = completeTag.startsWith("</");
const isSelfClosing = /\/\s*>$/.test(completeTag);
const isVoid = voidElements.has(lowerTag);
while ((match = tagPattern.exec(cleaned)) !== null) {
const completeTag = match[0];
const tagName = match[1];
const lowerTag = tagName.toLowerCase();
const isClosing = completeTag.startsWith("</");
const isSelfClosing = completeTag.endsWith("/>");
const isVoid = voidElements.has(lowerTag);
if (isClosing) {
const last = stack.pop();
if (!last) {
diagnostics.push(
createDiagnostic(
document,
match.index,
match.index + completeTag.length,
`Unexpected closing tag </${tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-unexpected-html-close",
),
);
if (isClosing) {
const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
if (matchingIndex === -1) {
diagnostics.push(
createDiagnostic(
document,
absoluteStart,
absoluteStart + completeTag.length,
`Unexpected closing tag </${tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-unexpected-html-close",
),
);
continue;
}
const last = stack.at(-1);
if (last.normalizedName !== normalizedName) {
diagnostics.push(
createDiagnostic(
document,
absoluteStart,
absoluteStart + completeTag.length,
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-mismatched-html-tag",
),
);
}
stack.splice(matchingIndex);
continue;
}
if (last.tagName !== tagName) {
diagnostics.push(
createDiagnostic(
document,
match.index,
match.index + completeTag.length,
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-mismatched-html-tag",
),
);
if (!isSelfClosing && !isVoid) {
stack.push({
tagName,
normalizedName,
offset: absoluteStart,
length: completeTag.length,
});
}
continue;
}
if (!isSelfClosing && !isVoid) {
stack.push({
tagName,
offset: match.index,
length: completeTag.length,
});
for (const tag of stack) {
diagnostics.push(
createDiagnostic(
document,
tag.offset,
tag.offset + tag.length,
`Missing closing tag </${tag.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-missing-html-close",
),
);
}
}
for (const tag of stack) {
diagnostics.push(
createDiagnostic(
document,
tag.offset,
tag.offset + tag.length,
`Missing closing tag </${tag.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-missing-html-close",
),
);
}
return diagnostics;
}
+279 -99
View File
@@ -1,3 +1,5 @@
// WRN editor extension source hash: 825c406dea4b196976b1b6e53b3c71c3263bd63f403acdaf2fe7e5ad174275da
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -27187,6 +27189,17 @@ ${serverFunctions}
opensElement
};
}
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines2 = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent)
lines2.push(`${childIndent}${parsed.inlineContent}`);
lines2.push(`${baseIndent}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines2.push(`${childIndent}${parsed.remainder}`);
}
return { lines: lines2, opensElement };
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -27224,6 +27237,63 @@ ${serverFunctions}
function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
} else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText);
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
@@ -27319,9 +27389,43 @@ ${serverFunctions}
endIndex: index
};
}
function isPreservedRawBlockStart(value) {
return /<pre(?:\s|>)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
if (!marker.test(line))
return [line];
marker.lastIndex = 0;
@@ -27331,7 +27435,7 @@ ${serverFunctions}
});
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match)
return [line];
@@ -27387,6 +27491,14 @@ ${serverFunctions}
index += 1;
continue;
}
if (isPreservedRawBlockStart(value)) {
const collected = collectPreservedRawBlock(inputLines, index);
const depth2 = codeDepth + htmlDepth + controlDepth;
output.push(`${unit.repeat(depth2)}${collected.lines[0].trimStart()}`);
output.push(...collected.lines.slice(1));
index = collected.endIndex + 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
value = collected.value;
@@ -27409,7 +27521,10 @@ ${serverFunctions}
lineHtmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
} else if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
output.push(...formattedTag.lines);
if (formattedTag.opensElement) {
@@ -28895,11 +29010,13 @@ ${serverFunctions}
if (!annotation)
return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint";
@@ -30196,11 +30313,13 @@ var require_component_metadata = __commonJS((exports2, module2) => {
}
function runtimeType(type) {
const value = String(type || "").trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
const unionParts = value.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value))
return "bigint";
@@ -30289,19 +30408,41 @@ var require_component_metadata = __commonJS((exports2, module2) => {
return trimmed.slice(1, -1).trim();
return trimmed;
}
function isWrappedExpression(value) {
const trimmed = value.trim();
return trimmed.startsWith("{") && trimmed.endsWith("}");
}
function expressionLiteralType(value) {
const expression = value.trim();
if (/^(?:true|false)$/.test(expression))
return "boolean";
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(expression))
return "number";
if (/^(?:"[\s\S]*"|'[\s\S]*'|`[\s\S]*`)$/.test(expression))
return "string";
if (expression.startsWith("["))
return "array";
if (expression.startsWith("{"))
return "object";
if (expression === "null")
return "null";
return null;
}
function attributeValueType(value, symbols = new Map) {
const expression = isWrappedExpression(value);
const unwrapped = unwrapAttributeValue(value);
if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
return symbols.get(unwrapped);
}
if (/^(?:true|false)$/.test(unwrapped))
return "boolean";
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped))
return "number";
if (unwrapped.startsWith("["))
return "array";
if (unwrapped.startsWith("{"))
return "object";
const literal = expressionLiteralType(unwrapped);
if (literal)
return literal;
if (expression) {
if (/^!\s*/.test(unwrapped) || /(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)) {
return "boolean";
}
return "unknown";
}
return "string";
}
function isTypeCompatible(prop, value, symbols = new Map) {
@@ -30309,6 +30450,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
if (expected === "unknown" || expected === "null")
return true;
const actual = runtimeType(attributeValueType(value, symbols));
if (actual === "unknown")
return true;
if (expected === actual)
return true;
if (expected === "number" && actual === "string")
@@ -30409,8 +30552,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
end: attribute.nameEnd
});
}
const literal = stringLiteral(attribute.value) ?? attribute.value;
if (prop.options.length > 0 && !prop.options.includes(literal)) {
const optionValue = isWrappedExpression(attribute.value) ? stringLiteral(unwrapAttributeValue(attribute.value)) : stringLiteral(attribute.value) ?? attribute.value;
if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
diagnostics.push({
severity: "warning",
code: "wrn-component-prop-option",
@@ -30727,58 +30870,6 @@ var require_diagnostics = __commonJS((exports2, module2) => {
}
return diagnostic;
}
function stripComments(source) {
const masked = [...source];
let quote = null;
let escaped = false;
const maskRange = (start, end) => {
for (let index = start;index < end; index += 1) {
if (source[index] !== `
` && source[index] !== "\r") {
masked[index] = " ";
}
}
};
for (let index = 0;index < source.length; index += 1) {
const character = source[index];
if (quote !== null) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
if (source.startsWith("//", index)) {
const lineEnd = source.indexOf(`
`, index + 2);
const end = lineEnd === -1 ? source.length : lineEnd;
maskRange(index, end);
index = end - 1;
continue;
}
if (source.startsWith("/*", index)) {
const commentEnd = source.indexOf("*/", index + 2);
const end = commentEnd === -1 ? source.length : commentEnd + 2;
maskRange(index, end);
index = end - 1;
continue;
}
if (source.startsWith("<!--", index)) {
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
maskRange(index, end);
index = end - 1;
}
}
return masked.join("");
}
function maskLeadingTrivia(source) {
const masked = [...source];
let offset = 0;
@@ -30897,9 +30988,89 @@ var require_diagnostics = __commonJS((exports2, module2) => {
}
return diagnostics;
}
function maskHtmlComments(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (!source.startsWith("<!--", index)) {
index += 1;
continue;
}
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
for (let cursor = index;cursor < end; cursor += 1) {
if (source[cursor] !== `
` && source[cursor] !== "\r")
masked[cursor] = " ";
}
index = end;
}
return masked.join("");
}
function maskTemplateExpressions(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (source[index] !== "{") {
index += 1;
continue;
}
const start = index;
let depth = 0;
let quote = null;
let escaped = false;
while (index < source.length) {
const character = source[index];
if (quote !== null) {
if (escaped)
escaped = false;
else if (character === "\\")
escaped = true;
else if (character === quote)
quote = null;
index += 1;
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
index += 1;
continue;
}
if (character === "{")
depth += 1;
else if (character === "}") {
depth -= 1;
index += 1;
if (depth === 0)
break;
continue;
}
index += 1;
}
for (let cursor = start;cursor < index; cursor += 1) {
if (source[cursor] !== `
` && source[cursor] !== "\r")
masked[cursor] = " ";
}
}
return masked.join("");
}
function findViewRanges(source) {
const ranges = [];
const pattern = /\bview\s*\{/g;
let match;
while ((match = pattern.exec(source)) !== null) {
const openingBrace = source.indexOf("{", match.index);
const closingBrace = findMatchingBrace(source, openingBrace);
if (closingBrace === -1)
break;
ranges.push({ start: openingBrace + 1, end: closingBrace });
pattern.lastIndex = closingBrace + 1;
}
return ranges;
}
function validateHtmlTags(document, source) {
const diagnostics = [];
const stack = [];
const voidElements = new Set([
"area",
"base",
@@ -30916,38 +31087,47 @@ var require_diagnostics = __commonJS((exports2, module2) => {
"track",
"wbr"
]);
const cleaned = stripComments(source);
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
let match;
while ((match = tagPattern.exec(cleaned)) !== null) {
const completeTag = match[0];
const tagName = match[1];
const lowerTag = tagName.toLowerCase();
const isClosing = completeTag.startsWith("</");
const isSelfClosing = completeTag.endsWith("/>");
const isVoid = voidElements.has(lowerTag);
if (isClosing) {
const last = stack.pop();
if (!last) {
diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Unexpected closing tag </${tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-unexpected-html-close"));
for (const range of findViewRanges(source)) {
const fragment = source.slice(range.start, range.end);
const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
const stack = [];
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
let match;
while ((match = tagPattern.exec(cleaned)) !== null) {
const completeTag = match[0];
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const lowerTag = tagName.toLowerCase();
const absoluteStart = range.start + match.index;
const isClosing = completeTag.startsWith("</");
const isSelfClosing = /\/\s*>$/.test(completeTag);
const isVoid = voidElements.has(lowerTag);
if (isClosing) {
const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
if (matchingIndex === -1) {
diagnostics.push(createDiagnostic(document, absoluteStart, absoluteStart + completeTag.length, `Unexpected closing tag </${tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-unexpected-html-close"));
continue;
}
const last = stack.at(-1);
if (last.normalizedName !== normalizedName) {
diagnostics.push(createDiagnostic(document, absoluteStart, absoluteStart + completeTag.length, `Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag"));
}
stack.splice(matchingIndex);
continue;
}
if (last.tagName !== tagName) {
diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag"));
if (!isSelfClosing && !isVoid) {
stack.push({
tagName,
normalizedName,
offset: absoluteStart,
length: completeTag.length
});
}
continue;
}
if (!isSelfClosing && !isVoid) {
stack.push({
tagName,
offset: match.index,
length: completeTag.length
});
for (const tag of stack) {
diagnostics.push(createDiagnostic(document, tag.offset, tag.offset + tag.length, `Missing closing tag </${tag.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
}
}
for (const tag of stack) {
diagnostics.push(createDiagnostic(document, tag.offset, tag.offset + tag.length, `Missing closing tag </${tag.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
}
return diagnostics;
}
function getRootBodyRange(source, rootMatch) {
@@ -31923,9 +32103,9 @@ async function activate(context) {
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(context);
}
registerComponentIntelligence(context);
registerSemanticTokens(context);
registerV060LanguageFeatures(context);
}
+4 -1
View File
@@ -376,9 +376,12 @@ async function activate(context) {
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(context);
}
// Package/local component metadata is not yet part of the LSP workspace
// catalog, so keep prop/event completion, hover, and validation available in
// both LSP and built-in-provider modes.
registerComponentIntelligence(context);
registerSemanticTokens(context);
registerV060LanguageFeatures(context);
}
+274 -37
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env bun
#!/usr/bin/env node
// WRN editor language server source hash: 5397c7912894dd5ab330f6fafc0f4093adbb94d95c23366ba7883605a724a7ae
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
@@ -169438,6 +169440,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
opensElement
};
}
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines2 = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent)
lines2.push(`${childIndent}${parsed.inlineContent}`);
lines2.push(`${baseIndent}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines2.push(`${childIndent}${parsed.remainder}`);
}
return { lines: lines2, opensElement };
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -169475,6 +169488,63 @@ function isMultilineOpeningTagStart(value) {
function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
} else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText);
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
@@ -169570,9 +169640,43 @@ function collectOpeningTag(inputLines, startIndex) {
endIndex: index
};
}
function isPreservedRawBlockStart(value) {
return /<pre(?:\s|>)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
if (!marker.test(line))
return [line];
marker.lastIndex = 0;
@@ -169582,7 +169686,7 @@ function expandInlineControlBlocks(lines) {
});
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match)
return [line];
@@ -169638,6 +169742,14 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
if (isPreservedRawBlockStart(value)) {
const collected = collectPreservedRawBlock(inputLines, index);
const depth2 = codeDepth + htmlDepth + controlDepth;
output.push(`${unit.repeat(depth2)}${collected.lines[0].trimStart()}`);
output.push(...collected.lines.slice(1));
index = collected.endIndex + 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
value = collected.value;
@@ -169660,7 +169772,10 @@ function formatWrnPass(source, options = {}) {
lineHtmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
} else if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
output.push(...formattedTag.lines);
if (formattedTag.opensElement) {
@@ -169745,11 +169860,13 @@ function runtimeTypeOf(annotation) {
if (!annotation)
return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type))
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint";
@@ -171689,7 +171806,7 @@ function runtimeNamespace(runtime) {
return `__wrn_${runtime}`;
}
function functionDeclaration(fn) {
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
}
function retainedImports(ast) {
@@ -171702,6 +171819,51 @@ function retainedImports(ast) {
}).map((entry) => entry.raw).join(`
`);
}
var RESERVED_BINDING_NAMES = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"let",
"new",
"null",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield"
]);
function safeBindingName(name) {
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
}
function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
const ast = parse(source);
const chunks = [];
@@ -171728,6 +171890,8 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
`), ast.types[0]?.trim());
for (const prop of ast.props) {
if (!safeBindingName(prop.name))
continue;
append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name);
}
for (const state of ast.states) {
@@ -171739,7 +171903,7 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; ");
const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server");
const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`).join("; ");
append(`declare const output: { ${outputType} };`);
append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
for (const output of ast.outputs)
append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`);
for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g))
@@ -171923,7 +172087,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
continue;
const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value));
const expected = runtimeTypeOf(prop.type);
if (expected !== "unknown" && received !== "unknown" && expected !== received) {
const compatibleLiteral = expected === "boolean" && /^(?:true|false|1|0|yes|no|on|off)?$/i.test(attr.value) || expected === "number" && attr.value.trim() !== "" && Number.isFinite(Number(attr.value));
if (expected !== "unknown" && received !== "unknown" && expected !== received && !compatibleLiteral) {
diagnostics.push({
code: "WRN-COMPONENT-PROP-TYPE",
category: "error",
@@ -171956,8 +172121,12 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
function category(value) {
return value === import_typescript.default.DiagnosticCategory.Error ? "error" : value === import_typescript.default.DiagnosticCategory.Warning ? "warning" : "info";
}
function hostWithVirtualFiles(files, options) {
function hostWithVirtualFiles(files, options, standardLibraryDirectory) {
const host = import_typescript.default.createCompilerHost(options, true);
if (standardLibraryDirectory) {
host.getDefaultLibFileName = (compilerOptions) => import_node_path2.join(standardLibraryDirectory, import_typescript.default.getDefaultLibFileName(compilerOptions));
host.getDefaultLibLocation = () => standardLibraryDirectory;
}
const originalGet = host.getSourceFile.bind(host);
host.fileExists = (fileName) => files.has(import_node_path2.normalize(fileName)) || import_typescript.default.sys.fileExists(fileName);
host.readFile = (fileName) => files.get(import_node_path2.normalize(fileName)) ?? import_typescript.default.sys.readFile(fileName);
@@ -171967,10 +172136,23 @@ function hostWithVirtualFiles(files, options) {
};
return host;
}
function resolveStandardLibraryDirectory(appRoot, options) {
const candidates = [
import_node_path2.dirname(import_typescript.default.getDefaultLibFilePath(options)),
import_node_path2.join(appRoot, "node_modules", "typescript", "lib"),
import_node_path2.join(process.cwd(), "node_modules", "typescript", "lib")
];
for (const candidate of candidates) {
if ((options.lib ?? []).every((name) => import_typescript.default.sys.fileExists(import_node_path2.join(candidate, name)))) {
return candidate;
}
}
return null;
}
function mappedPosition(virtual, line, column) {
const mapping = virtual.mappings.find((entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine);
if (!mapping)
return { line: 1, column: 1 };
return null;
const offset = line - mapping.virtualStartLine;
return {
line: mapping.sourceStartLine + offset,
@@ -172099,25 +172281,37 @@ function checkWrnSource(source, options = {}) {
lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"]
};
const rootNames = [virtual.fileName, ...appTypes.files.keys()];
const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions));
const tsDiagnostics = import_typescript.default.getPreEmitDiagnostics(program).map((diagnostic) => {
const file = diagnostic.file;
const start = diagnostic.start ?? 0;
const virtualPosition = file?.getLineAndCharacterOfPosition(start) ?? { line: 0, character: 0 };
const isVirtual = import_node_path2.normalize(file?.fileName ?? "") === import_node_path2.normalize(virtual.fileName);
const sourcePosition = isVirtual ? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1) : { line: virtualPosition.line + 1, column: virtualPosition.character + 1 };
return {
code: `WRN-TYPE-${diagnostic.code}`,
category: category(diagnostic.category),
message: import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, `
const standardLibraryDirectory = resolveStandardLibraryDirectory(appRoot, compilerOptions);
const tsDiagnostics = [];
if (standardLibraryDirectory) {
const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions, standardLibraryDirectory));
for (const diagnostic of import_typescript.default.getPreEmitDiagnostics(program)) {
const file = diagnostic.file;
if (!file)
continue;
const normalizedFile = import_node_path2.normalize(file.fileName);
const isVirtual = normalizedFile === import_node_path2.normalize(virtual.fileName);
const isApplicationType = files.has(normalizedFile);
if (!isVirtual && !isApplicationType)
continue;
const start = diagnostic.start ?? 0;
const virtualPosition = file.getLineAndCharacterOfPosition(start);
const sourcePosition = isVirtual ? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1) : { line: virtualPosition.line + 1, column: virtualPosition.character + 1 };
if (!sourcePosition)
continue;
tsDiagnostics.push({
code: `WRN-TYPE-${diagnostic.code}`,
category: category(diagnostic.category),
message: import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, `
`),
file: isVirtual ? filePath : file?.fileName ?? filePath,
line: sourcePosition.line,
column: sourcePosition.column,
length: diagnostic.length ?? 1,
hint: "Fix the TypeScript contract or expression in the related .wrn declaration."
};
});
file: isVirtual ? filePath : file.fileName,
line: sourcePosition.line,
column: sourcePosition.column,
length: diagnostic.length ?? 1,
hint: "Fix the TypeScript contract or expression in the related .wrn declaration."
});
}
}
return [
...options.checkRuntimeBoundaries === false ? [] : runtimeDiagnostics(source, virtual.ast, filePath),
...componentUsageDiagnostics(source, virtual.ast, filePath, appRoot),
@@ -172527,11 +172721,30 @@ function send(value) {
function result(id, value) {
send({ jsonrpc: "2.0", id, result: value });
}
function internalDiagnostic(document, error) {
const message = error instanceof Error ? error.message : String(error);
return {
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
severity: 1,
code: "WRN-LSP-INTERNAL",
source: "wrnexus",
message: `WRNexus language analysis failed safely: ${message}`
};
}
function safeDocumentDiagnostics(document) {
try {
return documentDiagnostics(document);
} catch (error) {
process.stderr.write(`[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? error.stack ?? error.message : String(error)}
`);
return [internalDiagnostic(document, error)];
}
}
function publish(document) {
send({
jsonrpc: "2.0",
method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: documentDiagnostics(document) }
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) }
});
}
function clearDiagnosticTimer(uri) {
@@ -172567,7 +172780,7 @@ async function handle(message) {
case "initialize":
workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
result(message.id, {
serverInfo: { name: "WRNexus Language Server", version: "0.8.0" },
serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
capabilities: {
textDocumentSync: 1,
documentFormattingProvider: true,
@@ -172676,7 +172889,7 @@ async function handle(message) {
result(message.id, []);
break;
}
const actions = documentDiagnostics(document).filter((item) => item.code === "WRNA11Y001").map((item) => ({
const actions = safeDocumentDiagnostics(document).filter((item) => item.code === "WRNA11Y001").map((item) => ({
title: "Add empty alt attribute",
kind: "quickfix",
diagnostics: [item],
@@ -172740,12 +172953,36 @@ function consume() {
return;
const body = buffer.subarray(bodyStart, bodyStart + length).toString();
buffer = buffer.subarray(bodyStart + length);
handle(JSON.parse(body));
let message;
try {
message = JSON.parse(body);
} catch (error) {
process.stderr.write(`[wrnexus-lsp] invalid JSON-RPC payload: ${error instanceof Error ? error.message : String(error)}
`);
continue;
}
handle(message).catch((error) => {
const detail = error instanceof Error ? error.stack ?? error.message : String(error);
process.stderr.write(`[wrnexus-lsp] request failed: ${detail}
`);
if (message.id !== undefined) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32603, message: "WRNexus language server request failed", data: detail }
});
}
});
}
}
process.stdin.on("data", (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
consume();
try {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
consume();
} catch (error) {
process.stderr.write(`[wrnexus-lsp] input processing failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}
`);
}
});
process.stdin.resume();
})
})(exports, require, module, __filename, __dirname);
+23 -4
View File
@@ -195,8 +195,12 @@
"patterns": [
{
"include": "source.ts"
},
{
"include": "#ts-braces"
}
]
],
"applyEndPatternLast": true
},
"props-block": {
"begin": "\\b(props)\\b\\s*(\\{)",
@@ -254,8 +258,15 @@
},
{
"include": "#strings"
},
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
],
"applyEndPatternLast": true
},
"seo-block": {
"begin": "\\b(seo)\\b\\s*(\\{)",
@@ -651,7 +662,8 @@
{
"include": "text.html.basic"
}
]
],
"applyEndPatternLast": true
},
"wire-event": {
"match": "(@)(?:(window|document)(:))?([A-Za-z][A-Za-z0-9_-]*)(\\s*=)",
@@ -1067,8 +1079,15 @@
"name": "punctuation.definition.parameters.end.wrn"
}
}
},
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
],
"applyEndPatternLast": true
},
"grouped-state-block": {
"begin": "\\b(?:(client|server|shared)\\s+)?(state)\\b\\s*(\\{)",
@@ -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"],
);
});
+33
View File
@@ -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 {
&lt;button&gt;Ship&lt;/button&gt;
}</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");
});
+45
View File
@@ -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
&lt;button @click='count++'&gt;Ship {count}&lt;/button&gt;
}</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);
});
+141
View File
@@ -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),
);
});