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

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -236,7 +236,11 @@ function registerComponentIntelligence(context) {
diagnostics.set(document.uri, results);
};
const watcher = vscode.workspace.createFileSystemWatcher("**/*.wrn");
// Watch only application sources. A workspace-wide **/*.wrn watcher can
// recursively subscribe to dependency and generated trees in large repos.
const watchers = (vscode.workspace.workspaceFolders || []).map((folder) =>
vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, "app/**/*.wrn")),
);
const refreshOpenDocuments = () => {
invalidateCatalog();
for (const document of vscode.workspace.textDocuments) void update(document);
@@ -244,10 +248,12 @@ function registerComponentIntelligence(context) {
context.subscriptions.push(
diagnostics,
watcher,
watcher.onDidCreate(refreshOpenDocuments),
watcher.onDidChange(refreshOpenDocuments),
watcher.onDidDelete(refreshOpenDocuments),
...watchers,
...watchers.flatMap((watcher) => [
watcher.onDidCreate(refreshOpenDocuments),
watcher.onDidChange(refreshOpenDocuments),
watcher.onDidDelete(refreshOpenDocuments),
]),
vscode.workspace.onDidOpenTextDocument(update),
vscode.workspace.onDidChangeTextDocument((event) => void update(event.document)),
vscode.workspace.onDidSaveTextDocument((document) => {
+10
View File
@@ -254,6 +254,16 @@ function validateBalancedCharacters(document, source) {
continue;
}
// Apostrophes in rendered copy (for example, "person's name") are text,
// not the beginning of a WRN/HTML quoted value.
if (
character === "'" &&
/[\p{L}\p{N}]/u.test(source[index - 1] ?? "") &&
/[\p{L}\p{N}]/u.test(source[index + 1] ?? "")
) {
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
File diff suppressed because it is too large Load Diff
+40 -7
View File
@@ -2,6 +2,8 @@
"use strict";
const vscode = require("vscode");
const path = require("node:path");
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
const { formatWrn } = require("./formatter");
@@ -329,7 +331,7 @@ async function recoverWrnLanguage(document) {
*
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
async function activate(context) {
for (const document of vscode.workspace.textDocuments) {
void recoverWrnLanguage(document);
}
@@ -340,12 +342,43 @@ function activate(context) {
}),
);
registerDiagnostics(context);
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(context);
const useLanguageServer = vscode.workspace
.getConfiguration("wrnexus")
.get("languageServer.enable", true);
let languageServerStarted = false;
if (useLanguageServer) {
const module = path.join(context.extensionPath, "src", "language-server.cjs");
const serverOptions = {
run: { module, transport: TransportKind.stdio },
debug: { module, transport: TransportKind.stdio, options: { execArgv: ["--nolazy"] } },
};
const client = new LanguageClient(
"wrnexusLanguageServer",
"WRNexus Language Server",
serverOptions,
{
documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }],
},
);
try {
await client.start();
languageServerStarted = true;
context.subscriptions.push({ dispose: () => void client.stop() });
} catch (error) {
console.warn(
"[wrnexus] language server failed to start; using built-in providers.",
error instanceof Error ? error.message : String(error),
);
}
}
if (!languageServerStarted) {
registerDiagnostics(context);
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(context);
}
registerSemanticTokens(context);
registerV060LanguageFeatures(context);
}
+4 -774
View File
@@ -1,777 +1,7 @@
"use strict";
const VOID_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
// Compatibility facade. The formatter implementation is owned by
// @wrnexus/syntax and bundled into compiler.cjs for the extension runtime.
const { formatWrn } = require("./compiler.cjs");
function splitPropDeclarations(value) {
const declarations = [];
let start = 0;
let index = 0;
let quote = null;
let escaped = false;
let square = 0;
let brace = 0;
let paren = 0;
let segmentHasColon = false;
let segmentHasEquals = false;
const isIdentifierStart = (character) => /[A-Za-z_]/.test(character || "");
const isIdentifierPart = (character) => /[A-Za-z0-9_]/.test(character || "");
const beginsDeclaration = (position) => {
let cursor = position;
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
if (value.slice(cursor).startsWith("@event")) {
cursor += "@event".length;
if (!/\s/.test(value[cursor] || "")) return false;
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
if (!isIdentifierStart(value[cursor])) return false;
cursor += 1;
while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1;
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
return value[cursor] === "=" ? "=" : null;
}
if (!isIdentifierStart(value[cursor])) return false;
cursor += 1;
while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1;
if (value[cursor] === "?") cursor += 1;
while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1;
return value[cursor] === "=" || value[cursor] === ":" ? value[cursor] : null;
};
while (index < value.length) {
const character = value[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 === "[") square += 1;
else if (character === "]" && square > 0) square -= 1;
else if (character === "{") brace += 1;
else if (character === "}" && brace > 0) brace -= 1;
else if (character === "(") paren += 1;
else if (character === ")" && paren > 0) paren -= 1;
const topLevel = square === 0 && brace === 0 && paren === 0;
if (topLevel && character === ":") segmentHasColon = true;
if (topLevel && character === "=") segmentHasEquals = true;
const candidateDelimiter = topLevel && /\s/.test(character) ? beginsDeclaration(index) : null;
const beginsNext =
candidateDelimiter === ":" ||
(candidateDelimiter === "=" && (segmentHasEquals || !segmentHasColon));
if (
topLevel &&
/\s/.test(character) &&
value.slice(start, index).trim() !== "@event" &&
beginsNext
) {
const declaration = value.slice(start, index).trim();
if (declaration) declarations.push(declaration);
while (index < value.length && /\s/.test(value[index])) index += 1;
start = index;
segmentHasColon = false;
segmentHasEquals = false;
continue;
}
index += 1;
}
const declaration = value.slice(start).trim();
if (declaration) declarations.push(declaration);
return declarations;
}
function splitOutputDeclarations(value) {
const declarations = [];
let start = 0;
let paren = 0;
let angle = 0;
let square = 0;
let quote = null;
let escaped = false;
const startsOutput = (position) => {
let cursor = position;
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
if (!/[A-Za-z_$]/.test(value[cursor] || "")) return false;
cursor += 1;
while (cursor < value.length && /[A-Za-z0-9_$]/.test(value[cursor] || "")) cursor += 1;
while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1;
return value[cursor] === "(";
};
for (let index = 0; index < value.length; index += 1) {
const character = value[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 (character === "(") paren += 1;
else if (character === ")" && paren > 0) paren -= 1;
else if (character === "[") square += 1;
else if (character === "]" && square > 0) square -= 1;
else if (character === "<") angle += 1;
else if (character === ">" && angle > 0) angle -= 1;
if (paren === 0 && square === 0 && angle === 0 && /\s/.test(character) && startsOutput(index)) {
const declaration = value.slice(start, index).trim();
if (declaration) declarations.push(declaration);
while (index < value.length && /\s/.test(value[index])) index += 1;
start = index;
index -= 1;
}
}
const finalDeclaration = value.slice(start).trim();
if (finalDeclaration) declarations.push(finalDeclaration);
return declarations;
}
function formatInlineDeclarationBlock(value, unit, depth) {
const match = /^(props|state|computed|outputs)\s*\{([\s\S]*)\}$/.exec(value.trim());
if (!match) return null;
const declarations =
match[1] === "outputs"
? splitOutputDeclarations(match[2].trim())
: splitPropDeclarations(match[2].trim());
return [
`${unit.repeat(depth)}${match[1]} {`,
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
`${unit.repeat(depth)}}`,
];
}
function formatInlinePropsBlock(value, unit, depth) {
const match = /^props\s*\{([\s\S]*)\}$/.exec(value.trim());
if (!match) return null;
const declarations = splitPropDeclarations(match[1].trim());
if (declarations.length === 0) {
return [`${unit.repeat(depth)}props {`, `${unit.repeat(depth)}}`];
}
return [
`${unit.repeat(depth)}props {`,
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
`${unit.repeat(depth)}}`,
];
}
function findOpeningTagEnd(value) {
let quote = null;
let escaped = false;
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (quote !== null) {
if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
}
if (character === ">") {
return index;
}
}
return -1;
}
function parseAttributes(value) {
const attributes = [];
let index = 0;
while (index < value.length) {
while (index < value.length && /\s/.test(value[index])) index += 1;
if (index >= value.length) break;
const start = index;
while (index < value.length && !/[\s=]/.test(value[index])) index += 1;
while (index < value.length && /\s/.test(value[index])) index += 1;
if (value[index] === "=") {
index += 1;
while (index < value.length && /\s/.test(value[index])) index += 1;
const quote = value[index];
if (quote === '"' || quote === "'") {
index += 1;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === quote) break;
}
} else if (value[index] === "{") {
let depth = 0;
let expressionQuote = null;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (expressionQuote !== null) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === expressionQuote) expressionQuote = null;
continue;
}
if (character === '"' || character === "'" || character === "`") {
expressionQuote = character;
} else if (character === "{") {
depth += 1;
} else if (character === "}" && --depth === 0) {
break;
}
}
} else {
while (index < value.length && !/\s/.test(value[index])) index += 1;
}
}
const attribute = value.slice(start, index).trim();
if (attribute) attributes.push(attribute);
}
return attributes;
}
function parseStructuredAttribute(attribute) {
const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute);
if (!match) return null;
const expression = match[2].trim();
if (!expression.startsWith("[") && !expression.startsWith("{")) return null;
try {
return {
name: match[1],
value: JSON.parse(expression),
};
} catch {
return null;
}
}
function formatAttribute(attribute, indentation, unit) {
const structured = parseStructuredAttribute(attribute);
if (!structured) return [`${indentation}${attribute}`];
const jsonLines = JSON.stringify(structured.value, null, unit).split("\n");
if (jsonLines.length === 1) {
return [`${indentation}${structured.name}={${jsonLines[0]}}`];
}
return [
`${indentation}${structured.name}={${jsonLines[0]}`,
...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`),
`${indentation}${jsonLines.at(-1)}}`,
];
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function parseOpeningTag(value) {
const endIndex = findOpeningTagEnd(value);
if (endIndex === -1) {
return null;
}
const openingPart = value.slice(0, endIndex + 1);
const remainder = value.slice(endIndex + 1).trim();
const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart);
if (!match) {
return null;
}
const tagName = match[1];
const attributes = parseAttributes(match[2].trim());
const selfClosing = match[3] === "/";
const escapedTagName = escapeRegExp(tagName);
const immediateClosing = new RegExp(`^<\\/${escapedTagName}\\s*>`, "i").test(remainder);
const trailingClosingMatch = new RegExp(`^([\\s\\S]*?)<\\/${escapedTagName}\\s*>$`, "i").exec(
remainder,
);
const trailingClosing = trailingClosingMatch !== null;
const inlineContent = trailingClosing ? trailingClosingMatch[1].trim() : "";
const inlineClosing = trailingClosing && inlineContent.length === 0;
const closesInRemainder = immediateClosing || trailingClosing;
return {
tagName,
attributes,
selfClosing,
inlineClosing,
immediateClosing,
trailingClosing,
inlineContent,
closesInRemainder,
remainder,
};
}
function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttributes = true) {
const parsed = parseOpeningTag(value);
if (!parsed) {
return {
lines: [`${unit.repeat(depth)}${value.trim()}`],
opensElement: false,
};
}
const baseIndent = unit.repeat(depth);
const childIndent = unit.repeat(depth + 1);
const normalizedOpening =
`<${parsed.tagName}` +
`${parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""}` +
`${parsed.selfClosing ? " /" : ""}>`;
const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`;
const shouldBreak =
value.includes("\n") ||
(multilineAttributes && parsed.attributes.length > 0) ||
baseIndent.length + normalizedSingleLine.length > printWidth;
const opensElement =
!parsed.selfClosing &&
!parsed.closesInRemainder &&
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
if (!shouldBreak) {
return {
lines: [`${baseIndent}${normalizedSingleLine}`],
opensElement,
};
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
];
if (parsed.selfClosing) {
lines.push(`${baseIndent}/>`);
return {
lines,
opensElement,
};
}
lines.push(`${baseIndent}>`);
if (parsed.trailingClosing) {
if (parsed.inlineContent) {
lines.push(`${childIndent}${parsed.inlineContent}`);
}
lines.push(`${baseIndent}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines.push(`${parsed.immediateClosing ? baseIndent : childIndent}${parsed.remainder}`);
}
return {
lines,
opensElement,
};
}
function isMultilineOpeningTagStart(value) {
if (!value.startsWith("<")) {
return false;
}
if (
value.startsWith("</") ||
value.startsWith("<!--") ||
value.startsWith("<!") ||
value.startsWith("<?")
) {
return false;
}
return findOpeningTagEnd(value) === -1;
}
function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
function isControlBlockMiddle(value) {
return /^\{:(?:else(?:\s+if\b[\s\S]*)?|empty)\}$/.test(value);
}
function isControlBlockClose(value) {
return /^\{\/(?:if|each)\}$/.test(value);
}
function countLeadingClosingBraces(value) {
let index = 0;
let count = 0;
while (index < value.length) {
while (index < value.length && /\s/.test(value[index])) {
index += 1;
}
if (value[index] !== "}" && value[index] !== "]") {
break;
}
count += 1;
index += 1;
}
return count;
}
/**
* Count braces outside strings and HTML comments.
*
* This supports WRN blocks, function bodies, lifecycle hooks,
* watcher bodies and multiline JavaScript object literals.
*/
function countStructuralBraces(value) {
let openings = 0;
let closings = 0;
let quote = null;
let escaped = false;
let htmlComment = false;
for (let index = 0; index < value.length; index += 1) {
if (!quote && !htmlComment && value.startsWith("<!--", index)) {
htmlComment = true;
index += 3;
continue;
}
if (htmlComment && value.startsWith("-->", index)) {
htmlComment = false;
index += 2;
continue;
}
if (htmlComment) {
continue;
}
const character = value[index];
if (quote !== null) {
if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
if (character === "{") {
openings += 1;
} else if (character === "}") {
closings += 1;
} else if (character === "[") {
openings += 1;
} else if (character === "]") {
closings += 1;
}
}
return {
openings,
closings,
};
}
function collectOpeningTag(inputLines, startIndex) {
const collected = [inputLines[startIndex].trim()];
let index = startIndex;
while (index + 1 < inputLines.length) {
const joined = collected.join(" ");
if (findOpeningTagEnd(joined) !== -1) {
break;
}
index += 1;
collected.push(inputLines[index].trim());
}
return {
// Preserve the fact that the opening tag was already multiline so a
// second formatter pass cannot collapse it back to one line.
value: collected.join("\n"),
endIndex: index,
};
}
/**
* Put WRN template control markers on their own lines before indentation.
*
* Authors commonly write compact fragments such as
* `{#if loading}<span>…</span>{/if}`. Treating that as one line prevents the
* normal HTML and control-block formatters from seeing its structure.
*/
function expandInlineControlBlocks(lines) {
const marker =
/(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return lines.flatMap((line) => {
if (!marker.test(line)) return [line];
marker.lastIndex = 0;
const indentation = line.match(/^\s*/)?.[0] ?? "";
const segments = line
.split(marker)
.map((segment) => segment.trim())
.filter(Boolean);
return segments.map((segment) => `${indentation}${segment}`);
});
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) return [line];
try {
const parsed = JSON.parse(match[2].trim());
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
if (jsonLines.length === 1) return [`${match[1]}${jsonLines[0]}`];
const leading = match[1].match(/^\s*/)?.[0] ?? "";
return [
`${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`),
];
} catch {
return [line];
}
});
}
function formatWrn(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
const multilineAttributes = options.multilineAttributes !== false;
let codeDepth = 0;
let htmlDepth = 0;
let controlDepth = 0;
let index = 0;
const sourceLines = source.replace(/\r\n/g, "\n").split("\n");
const inputLines = expandInlineControlBlocks(
expandStructuredStateDeclarations(sourceLines, unit),
);
const output = [];
let previousWasBlank = false;
while (index < inputLines.length) {
const originalLine = inputLines[index];
let value = originalLine.trim();
if (value === "") {
if (!previousWasBlank && output.length > 0) {
output.push("");
}
previousWasBlank = true;
index += 1;
continue;
}
previousWasBlank = false;
if (/^import\b/.test(value)) {
const importLines = [value];
while (
!/(?:\bfrom\s+)?["'][^"']+["']\s*;?$/.test(importLines[importLines.length - 1]) &&
index + 1 < inputLines.length
) {
index += 1;
importLines.push(inputLines[index].trim());
}
output.push(importLines[0], ...importLines.slice(1).map((line) => `${unit}${line}`));
index += 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
value = collected.value;
index = collected.endIndex;
}
const inlineDeclaration = formatInlineDeclarationBlock(value, unit, codeDepth + htmlDepth);
const inlineProps =
inlineDeclaration ?? formatInlinePropsBlock(value, unit, codeDepth + htmlDepth);
if (inlineProps) {
output.push(...inlineProps);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
const lineControlDepth =
closesControlBlock || continuesControlBlock ? Math.max(0, controlDepth - 1) : controlDepth;
const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces);
let lineHtmlDepth = htmlDepth;
if (isClosingTag(value)) {
lineHtmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
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) {
htmlDepth += 1;
}
} else {
output.push(`${unit.repeat(depth)}${value}`);
}
if (isClosingTag(value)) {
htmlDepth = lineHtmlDepth;
}
const braces = countStructuralBraces(value);
codeDepth = Math.max(
0,
lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces),
);
if (isControlBlockOpen(value) || continuesControlBlock) {
controlDepth = lineControlDepth + 1;
} else if (closesControlBlock) {
controlDepth = lineControlDepth;
}
index += 1;
}
while (output.length > 0 && output[output.length - 1] === "") {
output.pop();
}
return `${output.join("\n")}\n`;
}
module.exports = {
countStructuralBraces,
formatOpeningTag,
formatAttribute,
formatWrn,
parseAttributes,
parseOpeningTag,
splitOutputDeclarations,
splitPropDeclarations,
};
module.exports = { formatWrn };
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -63,7 +63,7 @@ function declarationRanges(document, name) {
}
async function wrnDocuments() {
const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB);
const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB, 1_000);
const open = new Map(
vscode.workspace.textDocuments
.filter((item) => item.languageId === "wrn")