fix(vscode): repair WRN language diagnostics and activation

This commit is contained in:
2026-07-19 14:02:17 +05:30
parent df4c1d3e7d
commit 646d16f83d
16 changed files with 425 additions and 171 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
// Treat .wrn files as the WrNexus language (redundant with the extension, but
// makes highlighting work the moment the repo is opened).
"files.associations": {
"*.wrn": "wire"
"*.wrn": "wrn"
},
// Distinct colors for WrNexus's own attributes, so they're easy to spot inside
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## 0.2.11
- Fixed `.wrn` files falling back to Plain Text when a legacy `wire` file
association is present.
- Fixed false root-declaration errors when files begin with `//` comments.
- Fixed false unclosed-string errors when `//` comments contain apostrophes or
bracket characters.
- Added explicit default and first-line WRN language detection.
- Removed duplicate semantic-token providers and repaired diagnostics toggling.
- Enabled completions and definition navigation for untitled and remote WRN
documents.
- Improved comment and folding language configuration and package validation.
## 0.2.2
- Added conditional `class:*` support.
+16
View File
@@ -735,6 +735,22 @@ Recommended VS Code configuration:
}
```
## Troubleshooting language detection
Files ending in `.wrn` should show `WRNexus` in the VS Code status bar. If an
older workspace setting maps `*.wrn` to the legacy `wire` language ID, replace
it with:
```json
"files.associations": {
"*.wrn": "wrn"
}
```
Then run **Developer: Reload Window**. The extension also repairs open `.wrn`
documents that VS Code classified as Plain Text or as the legacy `wire`
language.
## Privacy
The extension does not collect telemetry.
+2 -1
View File
@@ -1,5 +1,6 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["<!--", "-->"]
},
"brackets": [
@@ -46,7 +47,7 @@
"folding": {
"offSide": false,
"markers": {
"start": "^\\s*(?:page|component|layout|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$",
"start": "^\\s*(?:page|component|layout|api|realtime|view|seo|props|style|functions|lifecycle|watch|ssr|client)\\b.*\\{\\s*$",
"end": "^\\s*\\}\\s*$"
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wrnexus",
"version": "0.2.1",
"version": "0.2.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrnexus",
"version": "0.2.1",
"version": "0.2.11",
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"@vscode/vsce": "^3.9.2"
+10 -3
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.",
"version": "0.2.10",
"version": "0.2.11",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
@@ -52,7 +52,9 @@
],
"main": "./src/extension.js",
"activationEvents": [
"onLanguage:wrn"
"onLanguage:wrn",
"workspaceContains:**/*.wrn",
"onStartupFinished"
],
"contributes": {
"languages": [
@@ -66,6 +68,7 @@
"extensions": [
".wrn"
],
"firstLine": "^\\s*(?:page|component|layout)\\s+[A-Za-z_$][A-Za-z0-9_$]*\\s*\\{",
"configuration": "./language-configuration.json",
"icon": {
"light": "./icons/wrn.png",
@@ -125,6 +128,9 @@
}
},
"configurationDefaults": {
"files.associations": {
"*.wrn": "wrn"
},
"[wrn]": {
"editor.defaultFormatter": "wrnexus.wrnexus",
"editor.formatOnSave": false,
@@ -253,8 +259,9 @@
"scripts": {
"build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs",
"build": "bun run build:compiler",
"test": "node --test test/*.test.js",
"validate": "node test/validate.mjs",
"check": "bun run build && bun run validate",
"check": "bun run build && bun run test && bun run validate",
"vscode:prepublish": "bun run check",
"package": "vsce package --no-dependencies",
"publish": "vsce publish --no-dependencies",
+119 -24
View File
@@ -1023,8 +1023,12 @@ function hasClientBehavior(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return /\{(?!t:)[^{}]+\}/.test(node.value);
if (node.type === "each" || node.type === "if")
return false;
if (node.type === "each") {
return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => hasClientBehavior(branch.body));
}
return node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") || hasClientBehavior(node.children);
});
}
@@ -1391,6 +1395,18 @@ function viewHasEvents(nodes) {
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
});
}
function viewHasServerEach(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each")
return true;
if (node.type === "if") {
return node.branches.some((branch) => viewHasServerEach(branch.body));
}
return viewHasServerEach(node.children);
});
}
function compileText(raw, ctx) {
let out = "";
let last = 0;
@@ -1455,6 +1471,14 @@ function renderComponentEachNode(node, ctx) {
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
}
function serverLoopLocalsAttribute(ctx) {
const locals = [...ctx.serverLocals ?? []];
if (locals.length === 0) {
return "";
}
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}
function renderComponentNode(node, ctx) {
if (node.type === "text")
return compileText(node.value, ctx);
@@ -1467,6 +1491,11 @@ function renderComponentNode(node, ctx) {
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
const loopVariables = loopVarsOf(node);
const elementContext = loopVariables.length > 0 ? {
...ctx,
loopVars: new Set([...ctx.loopVars ?? [], ...loopVariables])
} : ctx;
let bindIndex = 0;
const staticClasses = [];
const conditionalClasses = [];
@@ -1488,37 +1517,48 @@ function renderComponentNode(node, ctx) {
if (a.boolean) {
return ` ${a.name}`;
}
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`;
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) {
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(a.value, elementContext.loopVars) : false;
const referencesServerLocal = ctx.serverLocals ? exprRefsState(a.value, ctx.serverLocals) : false;
if (!a.value.includes("{") || !referencesState && !referencesLoopVariable && !referencesServerLocal) {
return rendered;
}
const marker = attrEscape(JSON.stringify([a.name, a.value]));
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
}).join("");
const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => {
const referencesLoopVariable = elementContext.loopVars ? exprRefsState(expression, elementContext.loopVars) : false;
if (referencesLoopVariable) {
return "";
}
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
}).join("");
const staticClassValue = staticClasses.join(" ");
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
const classReferencesState = exprRefsState(staticClassValue, ctx.stateNames);
const classReferencesLoopVariable = elementContext.loopVars ? exprRefsState(staticClassValue, elementContext.loopVars) : false;
const classReferencesServerLocal = ctx.serverLocals ? exprRefsState(staticClassValue, ctx.serverLocals) : false;
const classHasReactiveExpression = staticClassValue.includes("{") && (classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"` : "";
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
const classBindings = conditionalClasses.filter(({ expression }) => {
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
}).map(({ className, expression }, index) => {
const classBindings = conditionalClasses.map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression]));
return ` data-wrn-class-${index}="${escLit(marker)}"`;
}).join("");
const loops = loopVarsOf(node);
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const allAttrs = `${loopLocalsAttribute}` + `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${allAttrs}>`;
}
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
}
function generateComponent(ast) {
const out = [];
const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) {
out.push(`import { Buffer } from "node:buffer";`);
}
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ? [
{
name: "content",
@@ -1542,6 +1582,9 @@ function generateComponent(ast) {
return result;
};
const ctx = { stateNames, resolveExpr };
const serverFunctions = ast.functions.map((body) => body.trim()).filter(Boolean).join(`
`);
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const styleTag = styles.length > 0 ? escLit(`<style data-wrnexus-style="${attrEscape(ast.name)}">
@@ -1560,10 +1603,10 @@ ${styles.map(styleEscape).join(`
decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`);
}
for (const state of ast.states) {
decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
decls.push(` let ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
}
const returnExpr = needsScope ? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`" : "`" + styleTag + viewCode + "`";
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`).join(", ")} });
` : needsScope ? ` const __scope = "";
` : "";
if (ast.kind === "layout") {
@@ -1673,14 +1716,56 @@ function __wireProp(v: any): string {
function __wireRaw(v: any): string {
return String(v == null ? "" : v);
}`);
if (hasServerEach) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
}
if (needsScope) {
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string {
const lit = (v: any) =>
typeof v === "number" || typeof v === "boolean"
? String(v)
: "'" + String(v).replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "\\\\'").replace(/\\n/g, "\\\\n") + "'";
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "number") {
return Number.isFinite(value)
? String(value)
: "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
const serialized = JSON.stringify(value);
return serialized === undefined
? "undefined"
: serialized;
} catch {
return "null";
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
return Object.keys(obj)
.map((k) => k + ": " + lit(obj[k]))
.map(
(key) =>
key +
": " +
__wrnexusSerializeScopeValue(
obj[key],
),
)
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
@@ -1688,11 +1773,13 @@ function __wireRaw(v: any): string {
.replace(/>/g, "&gt;");
}`);
}
const serverFunctionSource = serverFunctions ? `${serverFunctions}
` : "";
out.push(`export function render(props: Record<string, any> = {}): string {
` + ` const __p = props || {};
` + (decls.length > 0 ? decls.join(`
`) + `
` : "") + scopeLine + ` return ${returnExpr};
` : "") + serverFunctionSource + scopeLine + ` return ${returnExpr};
` + `}`);
return out.join(`
@@ -1924,8 +2011,16 @@ ${nativeStyles(ast.styles)}
function compileNativeWireFile(source) {
return generateNative(parse(source));
}
function compileWireFile(source) {
const ast = parse(source);
function compileWireFile(source, filePath = "<inline .wrn>") {
let ast;
try {
ast = parse(source);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to parse ${filePath}: ${message}`, {
cause: error
});
}
return `// compiled from .wrn
${generate(ast)}`;
}
-1
View File
@@ -517,7 +517,6 @@ function registerCompletionProvider(context) {
const provider = vscode.languages.registerCompletionItemProvider(
{
language: "wrn",
scheme: "file",
},
{
provideCompletionItems,
-1
View File
@@ -99,7 +99,6 @@ function registerDefinitionProvider(context) {
const disposable = vscode.languages.registerDefinitionProvider(
{
language: "wrn",
scheme: "file",
},
{
provideDefinition,
+53 -2
View File
@@ -76,11 +76,35 @@ function stripComments(source) {
return source.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
}
function maskLeadingTrivia(source) {
const masked = [...source];
let offset = 0;
while (offset < source.length) {
if (/\s/u.test(source[offset])) {
offset += 1;
continue;
}
if (!source.startsWith("//", offset)) break;
while (offset < source.length && source[offset] !== "\n") {
masked[offset] = " ";
offset += 1;
}
}
return masked.join("");
}
function findTopLevelDeclaration(document, source) {
const match = TOP_LEVEL_PATTERN.exec(source);
const sourceWithoutLeadingTrivia = maskLeadingTrivia(source);
const match = TOP_LEVEL_PATTERN.exec(sourceWithoutLeadingTrivia);
if (!match) {
const firstMeaningfulLine = source.split(/\r?\n/).findIndex((line) => line.trim().length > 0);
const firstMeaningfulLine = sourceWithoutLeadingTrivia
.split(/\r?\n/)
.findIndex((line) => line.trim().length > 0);
return {
diagnostic: lineDiagnostic(
@@ -134,6 +158,13 @@ function validateBalancedCharacters(document, source) {
continue;
}
if (source.startsWith("//", index)) {
const lineEnd = source.indexOf("\n", index + 2);
index = lineEnd === -1 ? source.length : lineEnd;
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
@@ -1063,6 +1094,14 @@ function registerDiagnostics(context) {
if (previousTimer) {
clearTimeout(previousTimer);
timers.delete(key);
}
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
if (!configuration.get("diagnostics.enable", true)) {
collection.delete(document.uri);
return;
}
const timer = setTimeout(() => {
@@ -1089,6 +1128,16 @@ function registerDiagnostics(context) {
vscode.workspace.onDidSaveTextDocument(update),
vscode.workspace.onDidChangeConfiguration((event) => {
if (!event.affectsConfiguration("wrnexus.diagnostics.enable")) {
return;
}
for (const document of vscode.workspace.textDocuments) {
update(document);
}
}),
vscode.workspace.onDidCloseTextDocument((document) => {
const key = document.uri.toString();
const timer = timers.get(key);
@@ -1114,6 +1163,8 @@ function registerDiagnostics(context) {
}
module.exports = {
findTopLevelDeclaration,
maskLeadingTrivia,
registerDiagnostics,
validateBalancedCharacters,
validateDocument,
+36 -93
View File
@@ -284,95 +284,37 @@ function registerCompilerDiagnostics(context) {
* @param {vscode.ExtensionContext} context
*/
function registerSemanticTokens(context) {
const semanticTokenLegend = new vscode.SemanticTokensLegend(
["variable"],
["declaration", "modification"],
);
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const collectStateVariables = (text) => {
const states = new Map();
const pattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
let match;
while ((match = pattern.exec(text)) !== null) {
const name = match[1];
if (!name) {
continue;
}
const offset = match.index + match[0].lastIndexOf(name);
const declarations = states.get(name) ?? new Set();
declarations.add(offset);
states.set(name, declarations);
}
return states;
};
const isModification = (text, offset, length) => {
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
if (after) {
return true;
}
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
return Boolean(before);
};
const provider = {
provideDocumentSemanticTokens(document, token) {
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
const text = document.getText();
const states = collectStateVariables(text);
for (const [name, declarationOffsets] of states) {
if (token.isCancellationRequested) {
return builder.build();
}
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
let match;
while ((match = pattern.exec(text)) !== null) {
const offset = match.index;
const position = document.positionAt(offset);
let modifiers = [];
if (declarationOffsets.has(offset)) {
modifiers = ["declaration"];
} else if (isModification(text, offset, name.length)) {
modifiers = ["modification"];
}
builder.push(position.line, position.character, name.length, "variable", modifiers);
}
}
return builder.build();
},
};
const registration = vscode.languages.registerDocumentSemanticTokensProvider(
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: WRN_LANGUAGE_ID,
},
provider,
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
context.subscriptions.push(registration);
/**
* Recover files affected by older workspace settings that associated `*.wrn`
* with the obsolete `wire` language id. Normal language contribution matching
* happens before activation; this fallback is intentionally limited to Plain
* Text and the legacy id so explicit third-party associations are respected.
*
* @param {vscode.TextDocument} document
*/
async function recoverWrnLanguage(document) {
if (!document.fileName.toLowerCase().endsWith(".wrn")) return;
if (!["plaintext", "wire"].includes(document.languageId)) return;
try {
await vscode.languages.setTextDocumentLanguage(document, WRN_LANGUAGE_ID);
} catch (error) {
console.warn(
"[wrnexus] unable to recover .wrn language association:",
error instanceof Error ? error.message : String(error),
);
}
}
/**
@@ -381,22 +323,22 @@ function registerSemanticTokens(context) {
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
for (const document of vscode.workspace.textDocuments) {
void recoverWrnLanguage(document);
}
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
void recoverWrnLanguage(document);
}),
);
registerDiagnostics(context);
registerCompilerDiagnostics(context);
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerFormatter(context);
registerSemanticTokens(context);
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: "wrn",
},
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
function deactivate() {}
@@ -614,5 +556,6 @@ module.exports = {
registerCompilerDiagnostics,
registerFormatter,
registerSemanticTokens,
recoverWrnLanguage,
toCompilerDiagnostic,
};
+13 -13
View File
@@ -582,6 +582,19 @@
"name": "punctuation.definition.string.begin.wrn"
}
},
"end": "\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.wrn"
}
},
"contentName": "meta.embedded.expression.wrn",
"patterns": [
{
"include": "source.ts"
}
]
},
"conditional-class-single": {
"name": "meta.attribute.class-directive.wrn",
"begin": "(?<=\\s)(class)(:)((?:\\[[^\\]\\r\\n]*\\]|[^\\s=])+)(\\s*=\\s*)(')",
@@ -615,19 +628,6 @@
}
]
},
"end": "\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.wrn"
}
},
"contentName": "meta.embedded.expression.wrn",
"patterns": [
{
"include": "source.ts"
}
]
},
"interpolation": {
"patterns": [
{
+11
View File
@@ -1,7 +1,18 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const Module = require("node:module");
// These extraction helpers are pure, but their module also registers VS Code
// providers at runtime. Supply a minimal host shim for unit tests.
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return {};
return originalLoad.call(this, request, parent, isMain);
};
const { extractRouteParams, extractStates } = require("../src/completion");
Module._load = originalLoad;
test("extracts dynamic route params from filename", () => {
const document = {
+81
View File
@@ -0,0 +1,81 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const Module = require("node:module");
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") {
return {
Diagnostic: class Diagnostic {
constructor(range, message, severity) {
this.range = range;
this.message = message;
this.severity = severity;
}
},
DiagnosticSeverity: { Error: 0, Warning: 1 },
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const {
findTopLevelDeclaration,
maskLeadingTrivia,
validateBalancedCharacters,
} = require("../src/diagnostics");
Module._load = originalLoad;
function mockDocument(source) {
return {
positionAt(offset) {
return offset;
},
};
}
test("recognizes a WRN declaration after leading line comments", () => {
const source = `// Component summary
// More details
component ThemeToggle {
view { <button>Toggle</button> }
}`;
const declaration = findTopLevelDeclaration({}, source);
assert.equal(declaration.kind, "component");
assert.equal(declaration.name, "ThemeToggle");
assert.equal(declaration.diagnostic, undefined);
});
test("masks only leading trivia and preserves source offsets", () => {
const source = "\uFEFF// Summary\r\npage Home {\n // member comment\n}";
const masked = maskLeadingTrivia(source);
assert.equal(masked.length, source.length);
assert.equal(masked.indexOf("page Home"), source.indexOf("page Home"));
assert.match(masked, /\/\/ member comment/);
});
test("ignores apostrophes and brackets in line comments when checking balance", () => {
const source = `// The framework's theme runtime binds the click }
component ThemeToggle {
props {
label = "Toggle theme"
class = ""
}
view { <button class="{class}" data-wire-theme-toggle><slot>{label}</slot></button> }
}`;
const diagnostics = validateBalancedCharacters(mockDocument(source), source);
assert.deepEqual(diagnostics, []);
});
+38
View File
@@ -37,10 +37,48 @@ for (const rel of [
const grammar = JSON.parse(readFileSync(join(root, "syntaxes/wrn.tmLanguage.json"), "utf8"));
grammar.scopeName === "source.wrn" ? ok("grammar scopeName") : bad("grammar scopeName");
const localIncludes = [];
const visitGrammarNode = (value) => {
if (Array.isArray(value)) {
value.forEach(visitGrammarNode);
return;
}
if (!value || typeof value !== "object") return;
if (typeof value.include === "string" && value.include.startsWith("#")) {
localIncludes.push(value.include.slice(1));
}
Object.values(value).forEach(visitGrammarNode);
};
visitGrammarNode(grammar.patterns);
visitGrammarNode(grammar.repository);
const missingIncludes = [...new Set(localIncludes)].filter((name) => !grammar.repository?.[name]);
missingIncludes.length === 0
? ok("all local grammar includes resolve")
: bad("all local grammar includes resolve", missingIncludes.join(", "));
grammar.repository?.["conditional-class-single"]
? ok("single-quoted class directives are registered")
: bad("single-quoted class directives are registered");
// 3. Marketplace metadata and the gallery icon meet vsce requirements.
try {
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
manifest.publisher === "wrnexus" ? ok("publisher id") : bad("publisher id");
const language = manifest.contributes?.languages?.find((entry) => entry.id === "wrn");
language?.extensions?.includes(".wrn")
? ok(".wrn extension maps to the wrn language")
: bad(".wrn extension maps to the wrn language");
manifest.contributes?.configurationDefaults?.["files.associations"]?.["*.wrn"] === "wrn"
? ok("default file association uses the wrn language id")
: bad("default file association uses the wrn language id");
manifest.activationEvents?.includes("onLanguage:wrn")
? ok("extension activates for the wrn language")
: bad("extension activates for the wrn language");
manifest.activationEvents?.includes("onStartupFinished")
? ok("extension can repair legacy associations at startup")
: bad("extension can repair legacy associations at startup");
language?.firstLine
? ok("first-line language detection is configured")
: bad("first-line language detection is configured");
manifest.icon?.endsWith(".png") ? ok("Marketplace icon is PNG") : bad("Marketplace icon is PNG");
const icon = readFileSync(join(root, manifest.icon));
const isPng = icon.subarray(1, 4).toString("ascii") === "PNG";
Binary file not shown.