release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`compiler output remains snapshot-compatible for the canonical component contract 1`] = `
|
||||
{
|
||||
"code":
|
||||
"// compiled from .wrn
|
||||
import Button from "@wrnexus/ui/components/Button.wrn";
|
||||
|
||||
import { Buffer as __WrnexusBuffer } from "node:buffer";
|
||||
|
||||
export const __wrnexusComponent = "Counter";
|
||||
|
||||
export const __wrnexusRuntime = "universal";
|
||||
|
||||
export const __wrnexusRender = "hybrid";
|
||||
|
||||
export const __wrnexusHydrate = "load";
|
||||
|
||||
export const __wrnexusHydrationId = "Counter:1skggk6";
|
||||
|
||||
export const __wrnexusBehavior = {
|
||||
"functions": "function increment(){\\n count = count + 1\\n output.change(count)\\n }",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "change",
|
||||
"payload": {
|
||||
"name": "value",
|
||||
"valueType": "number",
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"computed": [],
|
||||
"effects": [],
|
||||
"lifecycle": {},
|
||||
"watches": []
|
||||
};
|
||||
|
||||
export interface CounterProps {
|
||||
[attribute: string]: unknown;
|
||||
"label"?: string;
|
||||
}
|
||||
|
||||
export interface CounterOutputs {
|
||||
"change"(value: number): void;
|
||||
}
|
||||
|
||||
function __coerce(v: any, def: any, declared: string = "unknown"): any {
|
||||
if (v === undefined || v === null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (declared === "number" || typeof def === "number") {
|
||||
const parsed = Number(v);
|
||||
if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (declared === "boolean" || typeof def === "boolean") {
|
||||
if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
|
||||
if (v === false || v === "false" || v === 0 || v === "0") return false;
|
||||
throw new TypeError("Expected a boolean prop");
|
||||
}
|
||||
|
||||
if (declared === "array" || Array.isArray(def)) {
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
return Array.isArray(parsed) ? parsed : def;
|
||||
} catch {
|
||||
if (declared === "array") throw new TypeError("Expected an array prop");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
if (declared === "object" || (def !== null && typeof def === "object")) {
|
||||
if (
|
||||
v !== null &&
|
||||
typeof v === "object" &&
|
||||
!Array.isArray(v)
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
|
||||
return (
|
||||
parsed !== null &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed)
|
||||
)
|
||||
? parsed
|
||||
: def;
|
||||
} catch {
|
||||
if (declared === "object") throw new TypeError("Expected an object prop");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
if (declared === "bigint") return BigInt(v);
|
||||
if (declared === "function" && typeof v !== "function") {
|
||||
throw new TypeError("Expected a function prop");
|
||||
}
|
||||
return declared === "unknown" && def === undefined ? v : String(v);
|
||||
}
|
||||
|
||||
function __restProps(
|
||||
props: Record<string, any>,
|
||||
declared: Set<string>,
|
||||
): Record<string, any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||
);
|
||||
}
|
||||
|
||||
function __wireHtml(v: any): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: ">",
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: c === ">"
|
||||
? ">"
|
||||
: """,
|
||||
);
|
||||
}
|
||||
|
||||
function __wireBooleanAttr(name: string, value: any): string {
|
||||
return value === true ||
|
||||
value === "true" ||
|
||||
value === "" ||
|
||||
value === 1 ||
|
||||
value === "1" ||
|
||||
value === name
|
||||
? " " + name
|
||||
: "";
|
||||
}
|
||||
|
||||
function __wireSpreadAttrs(value: any): string {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
|
||||
const booleanAttributes = new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);
|
||||
const attributes: string[] = [];
|
||||
|
||||
for (const [name, raw] of Object.entries(value)) {
|
||||
const lowerName = name.toLowerCase();
|
||||
if (
|
||||
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
|
||||
lowerName.startsWith("on") ||
|
||||
lowerName === "style" ||
|
||||
lowerName === "slot" ||
|
||||
lowerName === "data-component" ||
|
||||
lowerName.startsWith("data-wrn")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (booleanAttributes.has(lowerName)) {
|
||||
attributes.push(__wireBooleanAttr(name, raw));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw === false || raw === null || raw === undefined) continue;
|
||||
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
||||
}
|
||||
|
||||
return attributes.join("");
|
||||
}
|
||||
|
||||
function __wireProp(v: any): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
? JSON.stringify(v)
|
||||
: String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
}
|
||||
|
||||
function __wireRaw(v: any): string {
|
||||
return String(v == null ? "" : v);
|
||||
}
|
||||
|
||||
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(
|
||||
(key) =>
|
||||
key +
|
||||
": " +
|
||||
__wrnexusSerializeScopeValue(
|
||||
obj[key],
|
||||
),
|
||||
)
|
||||
.join(", ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export function render(props: CounterProps = {} as CounterProps): string {
|
||||
const __p = props || {};
|
||||
const label: string = __coerce(__p["label"], ("Count"), "string");
|
||||
const __attrs = __restProps(__p, new Set(["label"]));
|
||||
let count = (0);
|
||||
const __scopeState = { "label": label, "count": count };
|
||||
const __scope = __wrnexusScopeDecl(__scopeState);
|
||||
const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");
|
||||
return \`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}" data-wrn-behavior="eyJmdW5jdGlvbnMiOiJmdW5jdGlvbiBpbmNyZW1lbnQoKXtcbiAgICAgIGNvdW50ID0gY291bnQgKyAxXG4gICAgICBvdXRwdXQuY2hhbmdlKGNvdW50KVxuICAgIH0iLCJvdXRwdXRzIjpbeyJuYW1lIjoiY2hhbmdlIiwicGF5bG9hZCI6eyJuYW1lIjoidmFsdWUiLCJ2YWx1ZVR5cGUiOiJudW1iZXIiLCJvcHRpb25hbCI6ZmFsc2V9fV0sImNvbXB1dGVkIjpbXSwiZWZmZWN0cyI6W10sImxpZmVjeWNsZSI6e30sIndhdGNoZXMiOltdfQ==" data-wrn-hydration="Counter:1skggk6" data-wrn-hydrate="load" data-wrn-runtime="universal" data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__">
|
||||
<div data-component="Button"\${__wireSpreadAttrs(__attrs)} on:click="\${__wireProp(increment)}">\${__wireHtml(label)}: <span data-text="count">\${__wireHtml(count)}</span></div>
|
||||
</div>\`;
|
||||
}
|
||||
|
||||
export default { name: "Counter", kind: "component", render };
|
||||
"
|
||||
,
|
||||
"diagnostics": [],
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { compile } from "../src/index.ts";
|
||||
|
||||
test("compiles schema-backed actions and progressively enhanced forms", () => {
|
||||
const output = compile(
|
||||
`import { CreateUserSchema } from "./schema";
|
||||
page Users {
|
||||
action createUser using CreateUserSchema { invalidate("users"); return { id: input.name } }
|
||||
view { <form @submit='createUser'><input name='name' /></form> }
|
||||
}`,
|
||||
"Users.wrn",
|
||||
).code;
|
||||
expect(output).toContain('data-wrn-action="createUser"');
|
||||
expect(output).toContain('name="_wrnexus_action" value="createUser"');
|
||||
expect(output).toContain("schema: CreateUserSchema");
|
||||
expect(output).toContain("__wrnexusInvalidatedTags");
|
||||
expect(output).toContain(
|
||||
"createActionClient<InferSchema<typeof CreateUserSchema>, Awaited<ReturnType<typeof createUser>>>",
|
||||
);
|
||||
expect(output).not.toContain("data-on-submit");
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
|
||||
test("Async syntax compiles loading, success and error branches into inert templates", () => {
|
||||
const code = generate(
|
||||
parse(`page Users {
|
||||
load client users { return [{ name: "Ada" }] }
|
||||
view {
|
||||
<Async source="users" retries="3">
|
||||
<Loading><p>Loading users</p></Loading>
|
||||
<Success data="users"><p>{users.name}</p></Success>
|
||||
<Error error="error"><p>{error.message}</p></Error>
|
||||
</Async>
|
||||
}
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain('data-wrn-async="users"');
|
||||
expect(code).toContain('data-wrn-async-retries="3"');
|
||||
expect(code).toContain("data-wrn-async-loading");
|
||||
expect(code).toContain("data-wrn-async-success");
|
||||
expect(code).toContain("data-wrn-async-error");
|
||||
expect(code).toContain("__wrnexusClientLoad");
|
||||
});
|
||||
|
||||
test("server named loads render Async success content during SSR", () => {
|
||||
const code = generate(
|
||||
parse(`page Users {
|
||||
load server users { return { name: "Ada" } }
|
||||
view {
|
||||
<Async source="users">
|
||||
<Loading><p>Loading</p></Loading>
|
||||
<Success><p>{users.name}</p></Success>
|
||||
<Error><p>Failed</p></Error>
|
||||
</Async>
|
||||
}
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain('const users = ctx["users"]');
|
||||
expect(code).toContain('data-wrn-async-resolved="true"');
|
||||
expect(code).toContain('ctx["users"] !== undefined');
|
||||
});
|
||||
|
||||
test("named loads support memoized dependencies and deferred execution", () => {
|
||||
const code = generate(
|
||||
parse(`page Data {
|
||||
load server account { return { id: 7 } }
|
||||
load server projects after account { return [account.id] }
|
||||
load server audit after projects defer { return { project: projects[0] } }
|
||||
view { <Async source="audit"><Loading>Wait</Loading><Success>Ready</Success></Async> }
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain("const account = await __load_account()");
|
||||
expect(code).toContain("const projects = await __load_projects()");
|
||||
expect(code).toContain("__promise_projects ??=");
|
||||
expect(code).toContain("export async function __wrnexusClientLoad");
|
||||
expect(code).toContain('return { "audit": __values[0] }');
|
||||
});
|
||||
|
||||
test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
|
||||
expect(() =>
|
||||
parse(
|
||||
`page Cycle { load server first after second { return 1 } load server second after first { return 2 } view { <p>x</p> } }`,
|
||||
),
|
||||
).toThrow("cycle");
|
||||
expect(() =>
|
||||
parse(
|
||||
`page Phase { load client browser { return 1 } load server invalid after browser { return 2 } view { <p>x</p> } }`,
|
||||
),
|
||||
).toThrow("cannot depend");
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
|
||||
test("client-rendered pages emit an inert template and browser mount anchor", () => {
|
||||
const code = generate(
|
||||
parse('page ClientOnly { render = "client" view { <main><h1>Browser only</h1></main> } }'),
|
||||
);
|
||||
expect(code).toContain('data-wrn-client-root="');
|
||||
expect(code).toContain('data-wrn-client-template="');
|
||||
expect(code.indexOf("Browser only")).toBeGreaterThan(code.indexOf("<template"));
|
||||
});
|
||||
@@ -7,6 +7,27 @@ import { generate, parse } from "../src/index.ts";
|
||||
import { compileWireFile } from "../src/index.ts";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
|
||||
test("explicit static rendering disables hydration metadata", () => {
|
||||
const output = compileWireFile(`page StaticPage {
|
||||
render = "static"
|
||||
state count = 0
|
||||
view { <button @click="count++">{count}</button> }
|
||||
}`);
|
||||
expect(output).toContain('export const __wrnexusRender = "static"');
|
||||
expect(output).toContain('data-wrn-hydrate="none"');
|
||||
expect(output).toContain('export const __wrnexusHydrate = "none"');
|
||||
});
|
||||
|
||||
test("named data loads compile as parallel typed data entries", () => {
|
||||
const output = compileWireFile(`page Users {
|
||||
load users { return ["Ada"] }
|
||||
load server teams { return ["Core"] }
|
||||
view { <p>Users</p> }
|
||||
}`);
|
||||
expect(output).toContain("await Promise.all");
|
||||
expect(output).toContain('return { "users": __values[0], "teams": __values[1] }');
|
||||
});
|
||||
|
||||
let seq = 0;
|
||||
/** Compile a `.wrn` source and import the resulting module. */
|
||||
async function compileAndImport(src: string): Promise<Record<string, unknown>> {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { compile } from "../src/index.ts";
|
||||
|
||||
test("compiles declarative portals, transitions and dynamic component cases", () => {
|
||||
const result = compile(`page Ui {
|
||||
view {
|
||||
<Portal to="#modal"><p>Modal</p></Portal>
|
||||
<Transition name="fade"><p>Animated</p></Transition>
|
||||
<Component is="Admin"><section data-component-case="Admin">Admin</section><section data-component-case="Guest">Guest</section></Component>
|
||||
}
|
||||
}`);
|
||||
expect(result.code).toContain('data-wrn-portal="#modal"');
|
||||
expect(result.code).toContain('data-wrn-transition="fade"');
|
||||
expect(result.code).toContain('data-wrn-dynamic-component="Admin"');
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
|
||||
test("KeepAlive compiles to a keyed live-instance preservation boundary", () => {
|
||||
const output = generate(
|
||||
parse(
|
||||
`page Dashboard { navigation { preserve = ["component"] } view { <KeepAlive key="filters"><DashboardFilters /></KeepAlive> } }`,
|
||||
),
|
||||
);
|
||||
expect(output).toContain('data-wrn-keepalive="filters"');
|
||||
expect(output).not.toContain('data-component="KeepAlive"');
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { analyzeOptimizations, generate, optimizeAst, parse } from "../src/index.ts";
|
||||
|
||||
test("compiler folds literal branches and reports optimization opportunities", () => {
|
||||
const ast = parse(`component Optimized {
|
||||
props { title: string = "Hello" }
|
||||
state count = 0
|
||||
state unused = 1
|
||||
functions {
|
||||
client function increment(): void { count++ }
|
||||
client function orphan(): void { unused++ }
|
||||
}
|
||||
style { .used { color: red } .unused-css { color: blue } }
|
||||
view { <section class="used"><h1>{title}</h1>{#if false}<p>dead</p>{:else}<button @click="increment">{count}</button>{/if}</section> }
|
||||
}`);
|
||||
const report = analyzeOptimizations(ast);
|
||||
expect(report.eliminatedBranches).toBeGreaterThan(0);
|
||||
expect(report.unusedHandlers).toContain("orphan");
|
||||
expect(report.unusedLocalCssClasses).toContain("unused-css");
|
||||
expect(report.constantProps).toContain("title");
|
||||
expect(optimizeAst(ast).ast.view).not.toEqual(ast.view);
|
||||
expect(generate(ast)).not.toContain("dead");
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { analyzeRuntimeRequirements, generate, parse } from "../src/index.ts";
|
||||
|
||||
test("partial-static pages compile transparent static and streamed dynamic boundaries", () => {
|
||||
const ast = parse(
|
||||
`page Dashboard { render = "partial-static" view { <Static><header>Docs</header></Static><Dynamic><p>User</p></Dynamic> } }`,
|
||||
);
|
||||
expect(ast.renderMode).toBe("partial-static");
|
||||
expect(analyzeRuntimeRequirements(ast).kind).toBe("streaming-ssr");
|
||||
const output = generate(ast);
|
||||
expect(output).toContain("wrn-dynamic-region");
|
||||
expect(output).toContain("__wrnexusBuildStaticShell");
|
||||
expect(output).toContain('<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
|
||||
expect(output).not.toContain('data-component="Static"');
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { compile, diagnose } from "../src/index.ts";
|
||||
|
||||
test("compiler output remains snapshot-compatible for the canonical component contract", () => {
|
||||
const source = `import Button from "@wrnexus/ui/components/Button.wrn";
|
||||
|
||||
component Counter {
|
||||
props {
|
||||
label: string = "Count"
|
||||
}
|
||||
state {
|
||||
count = 0
|
||||
}
|
||||
outputs {
|
||||
change(value: number)
|
||||
}
|
||||
functions {
|
||||
client function increment(): void {
|
||||
count = count + 1
|
||||
output.change(count)
|
||||
}
|
||||
}
|
||||
view {
|
||||
<Button on:click={increment}>{label}: {count}</Button>
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = compile(source, "Counter.wrn");
|
||||
expect({ code: result.code, diagnostics: result.richDiagnostics }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("diagnostics tolerate deterministic malformed-source fuzz cases", () => {
|
||||
let state = 0x8f3a21;
|
||||
const alphabet = "{}[]()<>=:/@#$'\"` abcdefghijklmnopqrstuvwxyz0123456789\n\t";
|
||||
for (let sample = 0; sample < 500; sample++) {
|
||||
let source = "";
|
||||
const length = 1 + (state % 180);
|
||||
for (let index = 0; index < length; index++) {
|
||||
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
||||
source += alphabet[state % alphabet.length];
|
||||
}
|
||||
expect(() => diagnose(source, { file: `fuzz-${sample}.wrn` })).not.toThrow();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { analyzeRuntimeImports, runtimeCapabilities } from "../src/index.ts";
|
||||
|
||||
test("edge and workers reject Node capabilities with stable diagnostics", () => {
|
||||
const source = `import fs from "node:fs";\nimport { connect } from "node:net";`;
|
||||
expect(analyzeRuntimeImports(source, "edge").map((item) => item.capability)).toEqual([
|
||||
"filesystem",
|
||||
"tcp",
|
||||
]);
|
||||
expect(analyzeRuntimeImports(source, "bun")).toEqual([]);
|
||||
expect(runtimeCapabilities("service-worker").has("filesystem")).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user