feat(wrn): support native structured prop expressions

This commit is contained in:
2026-07-29 16:46:18 +05:30
parent 6afe32f63f
commit 8fc6f15402
68 changed files with 210 additions and 118 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.5.0",
"version": "0.5.1",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
@@ -36,7 +36,7 @@
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck && bun run test"
"check": "bun run typecheck \u0026\u0026 bun run test"
},
"dependencies": {
"@wrnexus/authz": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.5.0",
"version": "0.5.1",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
@@ -38,7 +38,7 @@
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck && bun run test"
"check": "bun run typecheck \u0026\u0026 bun run test"
},
"dependencies": {
"@wrnexus/core": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+9
View File
@@ -1094,6 +1094,15 @@ const MIGRATIONS: Migration[] = [
// Package, component, and development-runtime improvements apply automatically.
},
},
{
version: "0.5.1",
id: "native-wrn-json-expressions",
description:
"Adds native array and object state initializers plus JSX-style expression props in WRN component markup.",
apply() {
// Syntax and compiler support are package-managed; existing applications remain compatible.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+2 -2
View File
@@ -164,8 +164,8 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
- `style { <raw css> }` — inlined page/component stylesheet (repeatable).
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+22
View File
@@ -464,6 +464,28 @@ component ParentCard {
expect(output).toContain("function __wireProp");
});
test("native array and object props serialize through component mounts", async () => {
const mod = await compileAndImport(`
component NativeProps {
state items = [{"label":"Home","href":"/"}]
view {
<Navbar items={items} options={{"dense":true}} />
<Sidebar items={[{"label":"Cases","href":"/cases"}]} />
}
}
`);
const render = mod.render as (props?: Record<string, unknown>) => string;
const html = render();
expect(html).toContain(
'items="[{&quot;label&quot;:&quot;Home&quot;,&quot;href&quot;:&quot;/&quot;}]"',
);
expect(html).toContain('options="{&quot;dense&quot;:true}"');
expect(html).toContain(
'items="[{&quot;label&quot;:&quot;Cases&quot;,&quot;href&quot;:&quot;/cases&quot;}]"',
);
});
test("parses a layout block", () => {
const ast = parse(`
layout AppLayout {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"sideEffects": false,
@@ -16,7 +16,7 @@
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck && bun run test"
"check": "bun run typecheck \u0026\u0026 bun run test"
},
"devDependencies": {
"@types/bun": "latest",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"main": "src/index.ts",
"exports": {
+27 -4
View File
@@ -712,13 +712,30 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
while (i < src.length && isWs(src[i]!)) i++;
};
/** Read a `{...}` interpolation (brace-balanced), braces included. */
/** Read a `{...}` interpolation (brace-balanced and quote-aware), braces included. */
const readInterpolation = (): string => {
const start = i;
let depth = 0;
let quote: string | null = null;
for (; i < src.length; i++) {
if (src[i] === "{") depth++;
else if (src[i] === "}" && --depth === 0) {
const char = src[i]!;
if (quote) {
if (char === "\\" && i + 1 < src.length) {
i++;
continue;
}
if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{") depth++;
else if (char === "}" && --depth === 0) {
i++;
return src.slice(start, i);
}
@@ -819,7 +836,13 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
if (src[i] === "=") {
i++;
skipWs();
attrs.push({ name, value: readQuoted(), event: false });
const value =
src[i] === '"' || src[i] === "'"
? readQuoted()
: src[i] === "{"
? readInterpolation()
: fail(`Expected a quoted value or {...} expression after '${name}='`);
attrs.push({ name, value, event: false });
} else {
attrs.push({ name, value: "", event: false, boolean: true });
}
+29
View File
@@ -1,6 +1,35 @@
import { expect, test } from "bun:test";
import { diagnose, formatDiagnostic, parse } from "../src/index.ts";
test("parses native array and object literals in state and component props", () => {
const ast = parse(`
component Navigation {
state items = [{"label":"Home","href":"/"}]
state options = {"dense":true,"nested":{"label":"A {brace}"}}
view {
<Navbar items={items} />
<Sidebar items={[{"label":"Cases","href":"/cases"}]} options={{"dense":true}} />
}
}
`);
expect(ast.states[0]?.expr).toBe('[{"label":"Home","href":"/"}]');
expect(ast.states[1]?.expr).toBe('{"dense":true,"nested":{"label":"A {brace}"}}');
const elements = ast.view.filter((node) => node.type === "element");
const navbar = elements[0];
const sidebar = elements[1];
expect(navbar?.type).toBe("element");
expect(sidebar?.type).toBe("element");
if (navbar?.type !== "element" || sidebar?.type !== "element") return;
expect(navbar.attrs.find((attr) => attr.name === "items")?.value).toBe("{items}");
expect(sidebar.attrs.find((attr) => attr.name === "items")?.value).toBe(
'{[{"label":"Cases","href":"/cases"}]}',
);
expect(sidebar.attrs.find((attr) => attr.name === "options")?.value).toBe('{{"dense":true}}');
});
test("parses WRN 0.3 execution, data, security, and reactivity blocks", () => {
const ast = parse(`page Dashboard {
runtime = "universal"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"main": "src/index.ts",