release: WRNexusJS 0.2.27
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -313,6 +313,14 @@ const MIGRATIONS: Migration[] = [
|
||||
// No project file migration is required for this framework release.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.27",
|
||||
id: "component-improvements-tag",
|
||||
description: "Adds Component renders system allowed 2 ways.",
|
||||
apply() {
|
||||
// No project file migration is required for this framework release.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -36,6 +36,10 @@ interface NamedDataBinding extends RenderBinding {
|
||||
mode: DataMode;
|
||||
}
|
||||
|
||||
function isComponentTag(tag: string): boolean {
|
||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||
}
|
||||
|
||||
/** Escape a value placed inside a double-quoted HTML attribute. */
|
||||
function attrEscape(value: string): string {
|
||||
return value
|
||||
@@ -217,19 +221,49 @@ function bakeLoopAttr(raw: string): string {
|
||||
|
||||
/** Render one loop-body node to template-literal source (nested loops inline). */
|
||||
function renderLoopBody(node: ViewNode): string {
|
||||
if (node.type === "text") return bakeLoopText(node.value);
|
||||
if (node.type === "each") return compileEachExpr(node);
|
||||
if (node.type === "if") return compileIfExpr(node);
|
||||
if (node.type === "text") {
|
||||
return bakeLoopText(node.value);
|
||||
}
|
||||
|
||||
if (node.type === "each") {
|
||||
return compileEachExpr(node);
|
||||
}
|
||||
|
||||
if (node.type === "if") {
|
||||
return compileIfExpr(node);
|
||||
}
|
||||
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
|
||||
const attrs = node.attrs
|
||||
.map((a) => {
|
||||
const name = a.event ? eventAttribute(a.name) : a.name;
|
||||
if (a.boolean) return escLit(` ${name}`);
|
||||
return escLit(` ${name}="`) + bakeLoopAttr(a.value) + escLit(`"`);
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
||||
|
||||
if (attr.boolean) {
|
||||
return escLit(` ${name}`);
|
||||
}
|
||||
|
||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
||||
})
|
||||
.join("");
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase()))
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
||||
|
||||
const inner = node.children.map(renderLoopBody).join("");
|
||||
|
||||
if (componentTag) {
|
||||
return (
|
||||
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
||||
attrs +
|
||||
escLit(">") +
|
||||
inner +
|
||||
escLit("</div>")
|
||||
);
|
||||
}
|
||||
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
||||
}
|
||||
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||
}
|
||||
|
||||
@@ -314,6 +348,17 @@ function renderNode(
|
||||
return `\x00WRNEACH${loops.length - 1}\x00`;
|
||||
}
|
||||
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderPageComponentInvocation(
|
||||
node,
|
||||
ssrBindings,
|
||||
csrBindings,
|
||||
apiBindings,
|
||||
loops,
|
||||
reactive,
|
||||
);
|
||||
}
|
||||
|
||||
const apiName = attrValue(node.attrs, "api");
|
||||
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
||||
if (apiName && !apiBinding) {
|
||||
@@ -361,6 +406,71 @@ function renderNode(
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
||||
}
|
||||
|
||||
function renderPageComponentInvocation(
|
||||
node: Extract<ViewNode, { type: "element" }>,
|
||||
ssrBindings: SsrBinding[],
|
||||
csrBindings: CsrBinding[],
|
||||
apiBindings: Map<string, NamedDataBinding>,
|
||||
loops: string[],
|
||||
reactive: PageReactive | null,
|
||||
): string {
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component");
|
||||
|
||||
const inner = node.children
|
||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||
.join("");
|
||||
|
||||
return `<div data-component="${attrEscape(node.tag)}"${renderAttrs(
|
||||
attrs,
|
||||
undefined,
|
||||
reactive,
|
||||
)}>${inner}</div>`;
|
||||
}
|
||||
|
||||
function renderNestedComponentInvocation(
|
||||
node: Extract<ViewNode, { type: "element" }>,
|
||||
ctx: CompCtx,
|
||||
): string {
|
||||
let bindIndex = 0;
|
||||
|
||||
const attrs = node.attrs
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
if (attr.event) {
|
||||
return ` ${eventAttribute(attr.name)}="${compileAttrValue(attr.value, ctx)}"`;
|
||||
}
|
||||
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
}
|
||||
|
||||
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`;
|
||||
|
||||
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
||||
|
||||
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const loops = loopVarsOf(node);
|
||||
|
||||
const childCtx =
|
||||
loops.length > 0
|
||||
? {
|
||||
...ctx,
|
||||
loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
|
||||
}
|
||||
: ctx;
|
||||
|
||||
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
|
||||
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
||||
}
|
||||
|
||||
function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
|
||||
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
||||
bindings.push({ marker, ...binding });
|
||||
@@ -829,6 +939,9 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
"Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).",
|
||||
);
|
||||
}
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderNestedComponentInvocation(node, ctx);
|
||||
}
|
||||
|
||||
let bindIndex = 0;
|
||||
const staticClasses: string[] = [];
|
||||
|
||||
@@ -386,13 +386,15 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
let i = pos;
|
||||
|
||||
const isNameStart = (c: string): boolean => /[A-Za-z_]/.test(c);
|
||||
const isNamePart = (c: string): boolean => /[A-Za-z0-9_:.[\]%-]/.test(c);
|
||||
|
||||
const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c);
|
||||
|
||||
const isAttributeNamePart = (c: string, next: string | undefined): boolean => {
|
||||
if (c === "/") {
|
||||
return next !== ">";
|
||||
}
|
||||
|
||||
return isNamePart(c);
|
||||
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
|
||||
};
|
||||
const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r";
|
||||
|
||||
@@ -429,18 +431,39 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
return value;
|
||||
};
|
||||
|
||||
const readName = (): string => {
|
||||
if (i >= src.length || !isNameStart(src[i]!)) return fail("Expected a tag or attribute name");
|
||||
const readTagName = (): string => {
|
||||
if (i >= src.length || !isNameStart(src[i]!)) {
|
||||
return fail("Expected a tag name");
|
||||
}
|
||||
|
||||
const start = i++;
|
||||
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
|
||||
|
||||
while (i < src.length && isTagNamePart(src[i]!)) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return src.slice(start, i);
|
||||
};
|
||||
|
||||
const readAttributeName = (): string => {
|
||||
const first = src[i];
|
||||
|
||||
if (i >= src.length || (!isNameStart(first!) && first !== ":" && first !== "$")) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
|
||||
const start = i++;
|
||||
|
||||
while (i < src.length && isAttributeNamePart(src[i]!, src[i + 1])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return src.slice(start, i);
|
||||
};
|
||||
|
||||
const parseTag = (): ViewNode => {
|
||||
i++; // consume '<'
|
||||
const tag = readName();
|
||||
const tag = readTagName();
|
||||
const attrs: Attr[] = [];
|
||||
|
||||
for (;;) {
|
||||
@@ -457,7 +480,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
}
|
||||
if (c === "@") {
|
||||
i++;
|
||||
const name = readName();
|
||||
const name = readAttributeName();
|
||||
skipWs();
|
||||
if (src[i] !== "=") return fail(`Expected '=' after @${name}`);
|
||||
i++;
|
||||
@@ -465,7 +488,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
attrs.push({ name, value: readQuoted(), event: true });
|
||||
continue;
|
||||
}
|
||||
const name = readName();
|
||||
const name = readAttributeName();
|
||||
skipWs();
|
||||
if (src[i] === "=") {
|
||||
i++;
|
||||
@@ -485,7 +508,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
if (src[i] !== "<" || src[i + 1] !== "/") return fail(`Expected </${tag}>`);
|
||||
i += 2;
|
||||
skipWs();
|
||||
const close = readName();
|
||||
const close = readTagName();
|
||||
if (close !== tag) return fail(`Mismatched </${close}>, expected </${tag}>`);
|
||||
skipWs();
|
||||
if (src[i] !== ">") return fail(`Expected '>' to close </${tag}>`);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parse } from "../src/index.ts";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
import { compileWireFile } from "../src/index.ts";
|
||||
|
||||
let seq = 0;
|
||||
@@ -148,3 +148,67 @@ test("named slots pass through to the component output", () => {
|
||||
expect(out).toContain('<slot name="header">');
|
||||
expect(out).toContain("<slot></slot>");
|
||||
});
|
||||
|
||||
test("capitalized self-closing tags compile to component mounts", () => {
|
||||
const source = `
|
||||
page ComponentPage {
|
||||
view {
|
||||
<UserCard
|
||||
name="Ajay"
|
||||
role="Admin"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="UserCard"');
|
||||
|
||||
expect(output).toContain('name="Ajay"');
|
||||
|
||||
expect(output).toContain('role="Admin"');
|
||||
|
||||
expect(output).not.toContain("<UserCard");
|
||||
});
|
||||
|
||||
test("capitalized component tags preserve slot children", () => {
|
||||
const source = `
|
||||
page SlotPage {
|
||||
view {
|
||||
<Card>
|
||||
<h2>Hello</h2>
|
||||
<p>Component content</p>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="Card"');
|
||||
|
||||
expect(output).toContain("<h2>Hello</h2>");
|
||||
|
||||
expect(output).toContain("<p>Component content</p>");
|
||||
});
|
||||
|
||||
test("component tags work inside reusable components", () => {
|
||||
const source = `
|
||||
component ParentCard {
|
||||
props {
|
||||
title = "Hello"
|
||||
}
|
||||
|
||||
view {
|
||||
<ChildCard title="{title}" />
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="ChildCard"');
|
||||
|
||||
expect(output).toContain('title="${__wireAttr(title)}"');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user