release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## 0.3.0
- Added language support for `runtime`, `hydrate`, `computed`, `effect`, `security`,
`load server`, `load client`, and named `action` declarations.
- Added snippets and completion entries for execution boundaries, deferred hydration,
derived state, effects, data loading, actions, and security metadata.
- Updated root-member diagnostics to understand prefixed blocks without reporting
action names, HTTP methods, load targets, or realtime handler names as members.
- Rebuilt the bundled compiler from the shared 0.3 syntax package and added a
reproducible TypeScript-based bundle script.
- Fixed multiline tag formatting so long inline-closing elements remain
idempotent across repeated format-on-save passes.
- Aligned the extension version with the WRNexusJS 0.3.0 framework release.
## 0.2.14
- Added top-level ES module import highlighting, completion, snippets, formatting,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wrnexus",
"version": "0.2.14",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrnexus",
"version": "0.2.14",
"version": "0.3.0",
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"@vscode/vsce": "^3.9.2"
+2 -2
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.15",
"version": "0.3.0",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
@@ -257,7 +257,7 @@
]
},
"scripts": {
"build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs",
"build:compiler": "node ../../scripts/build-editor-compiler.mjs",
"build": "bun run build:compiler",
"test": "node --test test/*.test.js",
"validate": "node test/validate.mjs",
+51 -30
View File
@@ -24,7 +24,6 @@
"}"
]
},
"WRN dynamic page": {
"prefix": ["wrn-dynamic-page", "dynamic-page"],
"description": "Create a WRN page using a route parameter",
@@ -42,7 +41,6 @@
"}"
]
},
"WRN component": {
"prefix": ["wrn-component", "component"],
"description": "Create a reusable WRN component",
@@ -62,7 +60,6 @@
"}"
]
},
"WRN reactive component": {
"prefix": ["wrn-reactive-component", "reactive-component"],
"description": "Create a component with functions, lifecycle hooks, and a watcher",
@@ -96,7 +93,6 @@
"}"
]
},
"WRN layout": {
"prefix": ["wrn-layout", "layout"],
"description": "Create a reusable WRN layout",
@@ -110,31 +106,26 @@
"}"
]
},
"WRN state": {
"prefix": ["wrn-state", "state"],
"description": "Create reactive state",
"body": ["state ${1:name}: ${2:string} = ${3:\"value\"}"]
},
"WRN functions block": {
"prefix": ["wrn-functions", "functions"],
"description": "Create a browser-side functions block",
"body": ["functions {", " function ${1:handler}(${2}) {", " $0", " }", "}"]
},
"WRN function": {
"prefix": ["wrn-function", "function"],
"description": "Create a function inside a functions block",
"body": ["function ${1:name}(${2}) {", " $0", "}"]
},
"WRN async function": {
"prefix": ["wrn-async-function", "async-function"],
"description": "Create an asynchronous component function",
"body": ["async function ${1:name}(${2}) {", " $0", "}"]
},
"WRN lifecycle": {
"prefix": ["wrn-lifecycle", "lifecycle"],
"description": "Create mount, update, and unmount lifecycle hooks",
@@ -154,37 +145,31 @@
"}"
]
},
"WRN mount hook": {
"prefix": ["wrn-mount", "mount"],
"description": "Create a component mount lifecycle hook",
"body": ["mount {", " $0", "}"]
},
"WRN update hook": {
"prefix": ["wrn-update", "update-hook"],
"description": "Create a batched component update lifecycle hook",
"body": ["update {", " $0", "}"]
},
"WRN unmount hook": {
"prefix": ["wrn-unmount", "unmount"],
"description": "Create a component unmount lifecycle hook",
"body": ["unmount {", " $0", "}"]
},
"WRN watcher": {
"prefix": ["wrn-watch", "watch"],
"description": "Watch a declared state value",
"body": ["watch ${1:stateName} {", " console.log(value, previous)", " $0", "}"]
},
"WRN watcher with condition": {
"prefix": ["wrn-watch-if", "watch-if"],
"description": "Watch state and react to a specific value",
"body": ["watch ${1:stateName} {", " if (value === ${2:true}) {", " $0", " }", "}"]
},
"WRN SEO block": {
"prefix": ["wrn-seo", "seo"],
"description": "Create SEO metadata",
@@ -195,7 +180,6 @@
"}"
]
},
"WRN form": {
"prefix": ["wrn-form", "form"],
"description": "Create a POST form",
@@ -212,7 +196,6 @@
"</form>"
]
},
"WRN input": {
"prefix": ["wrn-input", "input"],
"description": "Create a labeled input",
@@ -229,61 +212,51 @@
"</label>"
]
},
"WRN conditional class": {
"prefix": ["wrn-class-if", "class-if"],
"description": "Add a conditional class directive",
"body": ["class:${1:border-indigo-500}=\"${2:condition}\""]
},
"WRN conditional class pair": {
"prefix": ["wrn-class-toggle", "class-toggle"],
"description": "Toggle two classes from one condition",
"body": ["class:${1:opacity-100}=\"${3:visible}\"", "class:${2:opacity-0}=\"!${3:visible}\""]
},
"WRN click event": {
"prefix": ["wrn-click", "click"],
"description": "Add a click event handler",
"body": ["@click=\"${1:handler()}\""]
},
"WRN window scroll event": {
"prefix": ["wrn-window-scroll", "window-scroll"],
"description": "Add a window scroll event handler",
"body": ["@window:scroll=\"${1:handler()}\""]
},
"WRN window resize event": {
"prefix": ["wrn-window-resize", "window-resize"],
"description": "Add a window resize event handler",
"body": ["@window:resize=\"${1:handler()}\""]
},
"WRN document event": {
"prefix": ["wrn-document-event", "document-event"],
"description": "Add a document-level event handler",
"body": ["@document:${1:click}=\"${2:handler()}\""]
},
"WRN show directive": {
"prefix": ["wrn-show", "show"],
"description": "Conditionally show an element",
"body": ["data-show=\"${1:condition}\""]
},
"WRN loop": {
"prefix": ["wrn-for", "for"],
"description": "Create a reactive list loop",
"body": ["<div data-for=\"${1:item} in ${2:items}\">", " <span>{${1:item}}</span>", "</div>"]
},
"WRN dynamic link": {
"prefix": ["wrn-route-link", "route-link"],
"description": "Create a link using route state",
"body": ["<a href=\"/${1:resource}/{${2:id}}\">", " ${3:Open}", "</a>"]
},
"WRN API GET": {
"prefix": ["wrn-api-get", "api-get"],
"description": "Create a GET API route",
@@ -295,7 +268,6 @@
"}"
]
},
"WRN API POST": {
"prefix": ["wrn-api-post", "api-post"],
"description": "Create a POST API route",
@@ -310,7 +282,6 @@
"}"
]
},
"WRN loading state": {
"prefix": ["wrn-loading", "loading-state"],
"description": "Create loading state and a reactive button",
@@ -327,7 +298,6 @@
"</button>"
]
},
"WRN BackToTop component": {
"prefix": ["wrn-back-to-top", "back-to-top"],
"description": "Create an optimized BackToTop component using lifecycle cleanup",
@@ -385,5 +355,56 @@
" }",
"}"
]
},
"WRN runtime target": {
"prefix": ["wrn-runtime", "runtime"],
"description": "Set server, client, or universal execution",
"body": ["runtime = \"${1|universal,server,client|}\""]
},
"WRN hydration strategy": {
"prefix": ["wrn-hydrate", "hydrate"],
"description": "Control when a declaration hydrates in the browser",
"body": ["hydrate = \"${1|load,idle,visible,interaction,none|}\""]
},
"WRN media hydration": {
"prefix": ["wrn-hydrate-media", "hydrate-media"],
"description": "Hydrate only when a media query matches",
"body": ["hydrate = \"media:${1:(min-width: 768px)}\""]
},
"WRN computed block": {
"prefix": ["wrn-computed", "computed"],
"description": "Declare cached reactive derived values",
"body": ["computed {", " ${1:displayName} = ${2:firstName + \" \" + lastName}", "}"]
},
"WRN effect block": {
"prefix": ["wrn-effect", "effect"],
"description": "Run a side effect when reactive dependencies change",
"body": ["effect {", " ${1:console.log(value)}", "}"]
},
"WRN security block": {
"prefix": ["wrn-security", "security"],
"description": "Declare route security policy metadata",
"body": [
"security {",
" auth = \"${1|required,optional,public|}\"",
" csrf = \"${2:true}\"",
" rateLimit = \"${3:standard}\"",
"}"
]
},
"WRN server load": {
"prefix": ["wrn-load-server", "load-server"],
"description": "Load data only on the server",
"body": ["load server {", " ${1:return {}}", "}"]
},
"WRN client load": {
"prefix": ["wrn-load-client", "load-client"],
"description": "Load data in the browser",
"body": ["load client {", " ${1:return {}}", "}"]
},
"WRN action": {
"prefix": ["wrn-action", "action"],
"description": "Declare a named server action",
"body": ["action ${1:save}(${2:input}) {", " $0", "}"]
}
}
File diff suppressed because it is too large Load Diff
+52
View File
@@ -106,6 +106,56 @@ const BLOCK_COMPLETIONS = [
documentation: "Declare reactive state owned by the current component, layout, or page.",
snippet: 'state ${1:name}: ${2:string} = ${3:"value"}',
},
{
label: "runtime",
detail: "WRN execution target",
documentation: "Choose whether this declaration executes on the server, client, or both.",
snippet: 'runtime = "${1|universal,server,client|}"',
},
{
label: "hydrate",
detail: "WRN hydration strategy",
documentation: "Choose when client interactivity is initialized for this declaration.",
snippet: 'hydrate = "${1|load,idle,visible,interaction,none|}"',
},
{
label: "computed",
detail: "Derived reactive values block",
documentation:
"Declare values that are recomputed only when their reactive dependencies change.",
snippet: ["computed {", " ${1:displayName} = ${2:firstName + ' ' + lastName}", "}"].join(
"\n",
),
},
{
label: "effect",
detail: "Reactive side-effect block",
documentation: "Run browser-side code whenever its referenced reactive values change.",
snippet: ["effect {", " ${1:console.log(value)}", "}"].join("\n"),
},
{
label: "security",
detail: "Route security metadata block",
documentation: "Declare authentication, authorization, CSRF, and rate-limit policy metadata.",
snippet: [
"security {",
' auth = "${1|required,optional,public|}"',
' csrf = "${2:true}"',
"}",
].join("\n"),
},
{
label: "load",
detail: "Typed data-loading block",
documentation: "Load data on the server or client with an explicit execution boundary.",
snippet: ["load ${1|server,client|} {", " ${2:return {}}", "}"].join("\n"),
},
{
label: "action",
detail: "Named server action",
documentation: "Declare a callable mutation with an explicit name and arguments.",
snippet: ["action ${1:save}(${2:input}) {", " $0", "}"].join("\n"),
},
{
label: "functions",
detail: "Browser component functions block",
@@ -215,6 +265,8 @@ const CONTEXT_COMPLETIONS = [
["ctx.user", "Authenticated user"],
["ctx.session", "Current session"],
["ctx.locals", "Request-local data"],
["ctx.tenant", "Resolved tenant context"],
["ctx.tracer", "Request tracing interface"],
];
const WATCH_VALUE_COMPLETIONS = [
+100 -33
View File
@@ -11,33 +11,34 @@ const VALID_TOP_LEVEL_KINDS = new Set(["page", "component", "layout"]);
const VALID_LIFECYCLE_HOOKS = new Set(["mount", "update", "unmount"]);
const ROOT_MEMBER_NAMES = [
"layout",
"runtime",
"hydrate",
"client",
"types",
"props",
"state",
"computed",
"effect",
"watch",
"lifecycle",
"view",
"seo",
"security",
"load",
"action",
"api",
"ssr",
"realtime",
"style",
"functions",
];
const VALID_MEMBERS = {
page: new Set([
"layout",
"state",
"types",
"view",
"seo",
"style",
"functions",
"api",
"ssr",
"client",
"realtime",
]),
component: new Set([
"types",
"props",
"state",
"view",
"style",
"functions",
"lifecycle",
"watch",
]),
layout: new Set(["types", "props", "state", "view", "style", "functions", "lifecycle", "watch"]),
page: new Set(ROOT_MEMBER_NAMES),
component: new Set(ROOT_MEMBER_NAMES),
layout: new Set(ROOT_MEMBER_NAMES),
};
function createDiagnostic(
@@ -544,21 +545,87 @@ function findRootMembers(source, bodyStart, bodyEnd) {
end: identifier.end,
});
if (identifier.name === "state" || identifier.name === "layout") {
const assignmentMembers = new Set(["layout", "runtime", "hydrate", "state"]);
if (assignmentMembers.has(identifier.name)) {
skipLine();
continue;
}
if (identifier.name === "watch") {
index = skipWhitespace(source, index, bodyEnd);
if (identifier.name === "client") {
const next = skipWhitespace(source, index, bodyEnd);
const watchedState = readIdentifier(source, index, bodyEnd);
if (watchedState) {
index = watchedState.end;
if (source[next] === "=") {
skipLine();
continue;
}
}
const blockMembers = new Set([
"types",
"props",
"computed",
"effect",
"watch",
"lifecycle",
"view",
"seo",
"security",
"load",
"action",
"api",
"ssr",
"client",
"realtime",
"style",
"functions",
]);
if (blockMembers.has(identifier.name)) {
let cursor = index;
let parenthesisDepth = 0;
let bracketDepth = 0;
let memberQuote = null;
let memberEscaped = false;
while (cursor < bodyEnd) {
const current = source[cursor];
if (memberQuote !== null) {
if (memberEscaped) {
memberEscaped = false;
} else if (current === "\\") {
memberEscaped = true;
} else if (current === memberQuote) {
memberQuote = null;
}
cursor += 1;
continue;
}
if (current === '"' || current === "'") {
memberQuote = current;
cursor += 1;
continue;
}
if (current === "(") parenthesisDepth += 1;
if (current === ")") parenthesisDepth = Math.max(0, parenthesisDepth - 1);
if (current === "[") bracketDepth += 1;
if (current === "]") bracketDepth = Math.max(0, bracketDepth - 1);
if (current === "{" && parenthesisDepth === 0 && bracketDepth === 0) {
index = cursor;
break;
}
cursor += 1;
}
if (cursor >= bodyEnd) index = bodyEnd;
}
continue;
}
+12 -2
View File
@@ -179,11 +179,14 @@ function parseOpeningTag(value) {
const inlineClosing = remainder === `</${tagName}>`;
const closesInRemainder = remainder.startsWith(`</${tagName}>`);
return {
tagName,
attributes,
selfClosing,
inlineClosing,
closesInRemainder,
remainder,
};
}
@@ -202,7 +205,11 @@ function formatOpeningTag(value, unit, depth, printWidth = 100) {
const attributeIndent = unit.repeat(depth + 1);
const normalizedSingleLine = value.replace(/\s+/g, " ").trim();
const normalizedOpening = `<${parsed.tagName}${
parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""
}${parsed.selfClosing ? " /" : ""}>`;
const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`;
const shouldBreak =
parsed.attributes.length > 1 ||
@@ -212,6 +219,7 @@ function formatOpeningTag(value, unit, depth, printWidth = 100) {
const opensElement =
!parsed.selfClosing &&
!parsed.inlineClosing &&
!parsed.closesInRemainder &&
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
if (!shouldBreak) {
@@ -375,7 +383,9 @@ function collectOpeningTag(inputLines, startIndex) {
}
return {
value: collected.join(" "),
// 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,
};
}
+208 -4
View File
@@ -82,6 +82,24 @@
{
"include": "#realtime-block"
},
{
"include": "#execution-decl"
},
{
"include": "#computed-block"
},
{
"include": "#effect-block"
},
{
"include": "#security-block"
},
{
"include": "#load-block"
},
{
"include": "#action-block"
},
{
"include": "#mode-block"
},
@@ -143,14 +161,24 @@
"types-block": {
"begin": "\\b(types)\\b\\s*(\\{)",
"beginCaptures": {
"1": { "name": "keyword.control.wrn" },
"2": { "name": "punctuation.definition.block.begin.wrn" }
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": { "name": "punctuation.definition.block.end.wrn" }
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"patterns": [{ "include": "source.ts" }]
"patterns": [
{
"include": "source.ts"
}
]
},
"props-block": {
"begin": "\\b(props)\\b\\s*(\\{)",
@@ -757,6 +785,182 @@
]
}
]
},
"execution-decl": {
"begin": "\\b(runtime|hydrate|client)\\b\\s*(=)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "keyword.operator.assignment.wrn"
}
},
"end": "$",
"patterns": [
{
"include": "#strings"
}
]
},
"computed-block": {
"begin": "\\b(computed)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"patterns": [
{
"include": "#comments"
},
{
"match": "\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s*(=)",
"captures": {
"1": {
"name": "variable.other.readwrite.declaration.wrn"
},
"2": {
"name": "keyword.operator.assignment.wrn"
}
}
},
{
"include": "source.ts"
}
]
},
"effect-block": {
"begin": "\\b(effect)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"contentName": "meta.embedded.block.ts",
"patterns": [
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
},
"security-block": {
"begin": "\\b(security)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"patterns": [
{
"include": "#comments"
},
{
"match": "\\b([A-Za-z_$][A-Za-z0-9_$-]*)\\s*(=)",
"captures": {
"1": {
"name": "support.type.property-name.wrn"
},
"2": {
"name": "keyword.operator.assignment.wrn"
}
}
},
{
"include": "#strings"
}
]
},
"load-block": {
"begin": "\\b(load)\\b\\s+(server|client)\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "storage.modifier.wrn"
},
"3": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"contentName": "meta.embedded.block.ts",
"patterns": [
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
},
"action-block": {
"begin": "\\b(action)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(\\([^)]*\\))\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "entity.name.function.wrn"
},
"3": {
"name": "variable.parameter.wrn"
},
"4": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"contentName": "meta.embedded.block.ts",
"patterns": [
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
}
}
}
+23
View File
@@ -30,6 +30,7 @@ const {
findTopLevelDeclaration,
maskLeadingTrivia,
validateBalancedCharacters,
validateRootMembers,
} = require("../src/diagnostics");
Module._load = originalLoad;
@@ -92,3 +93,25 @@ component ThemeToggle {
assert.deepEqual(diagnostics, []);
});
test("accepts all WRN 0.3 root members without false unknown-member errors", () => {
const source = `page Dashboard {
runtime = "universal"
hydrate = "visible"
computed { doubled = count * 2 }
effect { console.log(doubled) }
security { auth = "required" }
load server { return {} }
action save(input) { return input }
client { functions { function ready() {} } }
view { <main>{doubled}</main> }
}`;
const declaration = findTopLevelDeclaration({}, source);
const document = {
positionAt(offset) {
return offset;
},
};
assert.deepEqual(validateRootMembers(document, source, declaration.kind, declaration.match), []);
});
+18
View File
@@ -19,3 +19,21 @@ test("preserves nested prop defaults while formatting", () => {
assert.match(formatted, /options = \{ gap: 4, dense: false \}/);
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
});
test("keeps long inline-closing tags idempotent after multiline expansion", () => {
const source = `component Status {
view {
<section class="overflow-hidden rounded-3xl border border-[var(--wire-color-border,#e2e8f0)] bg-[var(--wire-color-surface,#ffffff)]">
<span class="inline-flex items-center gap-2 rounded-full bg-[var(--wire-color-surface-2,#f8fafc)] px-3 py-1.5 text-xs font-semibold">
<span class="h-2 w-2 animate-pulse rounded-full bg-[var(--wire-color-primary,#059669)]"></span>Loading
</span>
</section>
}
}
`;
const options = { insertSpaces: true, tabSize: 2, printWidth: 100 };
const formatted = formatWrn(source, options);
assert.equal(formatWrn(formatted, options), formatted);
assert.match(formatted, /<\/span>Loading/);
});