release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+219 -55
View File
@@ -24,9 +24,27 @@
import { Lexer, LexError, type Token } from "./tokenizer.ts";
import { validateTypedInitializer } from "./types.ts";
import {
parseComputedDeclarations,
parseOutputs,
parsePersist,
parseRuntimeFunctions,
parseStateDeclarations,
parseStoreLifecycle,
parseStructuredImports,
type OutputDecl,
type PersistDecl,
type RuntimeFunctionDecl,
type StateRuntime,
type StoreKind,
type StoreLifecycleDecl,
type StructuredImportDecl,
} from "./v060.ts";
export interface StateDecl {
name: string;
/** Runtime visibility. Legacy declarations are shared. */
runtime: StateRuntime;
/** Explicit TypeScript-style type annotation, when supplied. */
valueType?: string;
/** Raw JS initializer expression, e.g. `0` or `'x'`. */
@@ -35,6 +53,7 @@ export interface StateDecl {
export interface ComputedDecl {
name: string;
valueType?: string;
expr: string;
}
@@ -166,7 +185,7 @@ export interface PropDecl {
}
export interface EventDecl {
/** Public event name used by consumers as `@name="handler(event)"`. */
/** Legacy public event declaration retained for compatibility. */
name: string;
}
@@ -174,22 +193,27 @@ export interface PageAst {
type: "page";
/** Static ES module imports declared before the WRN root declaration. */
imports: string[];
structuredImports: StructuredImportDecl[];
/**
* `page` is a route, `component` is a reusable fragment,
* and `layout` is a reusable page wrapper.
*/
kind: "page" | "component" | "layout";
kind: "page" | "component" | "layout" | "global-store" | "page-store";
storeKind?: StoreKind;
name: string;
/** Name of the page layout (`app/layouts/<layout>.wrn`), if the page sets one. */
layout?: string;
layoutIsSymbol?: boolean;
/** Execution boundary metadata. Defaults to universal. */
runtime?: "server" | "client" | "universal";
/** Client hydration strategy. Defaults to load when interactivity is present. */
hydrate?: string;
/** Declared component props (empty for pages). */
props: PropDecl[];
/** Public events exposed by a reusable component. */
/** Legacy public events exposed by a reusable component. */
events: EventDecl[];
/** Canonical typed callable outputs. */
outputs: OutputDecl[];
/** Raw declarations from `types { ... }`, emitted as TypeScript. */
types: string[];
states: StateDecl[];
@@ -202,9 +226,12 @@ export interface PageAst {
view: ViewNode[];
styles: string[];
functions: string[];
runtimeFunctions: RuntimeFunctionDecl[];
dataApis: DataApiBlock[];
modeFunctions: ModeFunctionsBlock[];
lifecycle: LifecycleBlock;
storeLifecycle: StoreLifecycleDecl;
persist?: PersistDecl;
watches: WatchBlock[];
apis: ApiBlock[];
realtimes: RealtimeBlock[];
@@ -277,26 +304,49 @@ export function parse(source: string): PageAst {
};
try {
// A file may contain a page, reusable component, or reusable layout.
// A file may contain a page, component, layout, global store, or page store.
const opener = lx.next();
if (opener.type !== "ident" || !["page", "component", "layout"].includes(opener.value)) {
if (
opener.type !== "ident" ||
!["page", "component", "layout", "global"].includes(opener.value)
) {
throw new ParseError(
`Expected 'page', 'component', or 'layout' but got '${
`Expected 'page', 'component', 'layout', 'global store', or 'page store' but got '${
opener.value || opener.type
}' at offset ${opener.pos}`,
);
}
const kind = opener.value as "page" | "component" | "layout";
const name = expect("ident").value;
let kind: PageAst["kind"];
let storeKind: StoreKind | undefined;
let name: string;
if (opener.value === "global") {
expectKeyword("store");
kind = "global-store";
storeKind = "global";
name = expect("ident").value;
} else if (
opener.value === "page" &&
lx.peek().type === "ident" &&
lx.peek().value === "store"
) {
lx.next();
kind = "page-store";
storeKind = "page";
name = expect("ident").value;
} else {
kind = opener.value as "page" | "component" | "layout";
name = expect("ident").value;
}
expect("lbrace");
let layout: string | undefined;
let layoutIsSymbol = false;
let runtime: PageAst["runtime"];
let hydrate: string | undefined;
const props: PropDecl[] = [];
const events: EventDecl[] = [];
const outputs: OutputDecl[] = [];
const types: string[] = [];
const states: StateDecl[] = [];
const computed: ComputedDecl[] = [];
@@ -308,9 +358,12 @@ export function parse(source: string): PageAst {
const view: ViewNode[] = [];
const styles: string[] = [];
const functions: string[] = [];
const runtimeFunctions: RuntimeFunctionDecl[] = [];
const dataApis: DataApiBlock[] = [];
const modeFunctions: ModeFunctionsBlock[] = [];
const lifecycle: LifecycleBlock = {};
let storeLifecycle: StoreLifecycleDecl = {};
let persist: PersistDecl | undefined;
const watches: WatchBlock[] = [];
const apis: ApiBlock[] = [];
const realtimes: RealtimeBlock[] = [];
@@ -326,7 +379,14 @@ export function parse(source: string): PageAst {
// layout = "public" — selects app/layouts/<name>.wrn for this page.
lx.next();
expect("eq");
layout = expect("string").value;
const layoutToken = lx.next();
if (layoutToken.type !== "string" && layoutToken.type !== "ident") {
throw new ParseError(
`Expected a layout string or imported symbol at offset ${layoutToken.pos}`,
);
}
layout = layoutToken.value;
layoutIsSymbol = layoutToken.type === "ident";
break;
}
case "runtime": {
@@ -378,6 +438,7 @@ export function parse(source: string): PageAst {
throw new ParseError(`Expected a prop name at offset ${t.pos}`);
}
const pName = expect("ident").value;
const optional = lx.peek().type === "question" ? (lx.next(), true) : false;
let valueType: string | undefined;
let hasDefault = false;
if (lx.peek().type === "colon") {
@@ -390,43 +451,70 @@ export function parse(source: string): PageAst {
hasDefault = true;
}
const defaultValue = hasDefault ? lx.readPropInitializer() : "undefined";
props.push({ name: pName, valueType, required: !hasDefault, default: defaultValue });
props.push({
name: pName,
valueType,
required: !hasDefault && !optional,
default: defaultValue,
});
}
expect("rbrace");
break;
}
case "state": {
lx.next();
if (lx.peek().type === "lbrace") {
const grouped = parseStateDeclarations(lx.readBalancedBraces(), "shared");
states.push(...grouped);
break;
}
const sName = expect("ident").value;
let valueType: string | undefined;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault) {
if (!annotation.hasDefault)
throw new ParseError(`State '${sName}' requires an initializer`);
}
} else {
expect("eq");
}
// State values may be multiline structured expressions. Use the same
// balanced initializer reader as props so formatted arrays/objects
// remain one declaration instead of exposing their inner braces as
// component members on the next line.
states.push({ name: sName, valueType, expr: lx.readPropInitializer() });
states.push({
name: sName,
valueType,
expr: lx.readPropInitializer(),
runtime: "shared",
});
break;
}
case "computed": {
lx.next();
expect("lbrace");
while (lx.peek().type !== "rbrace") {
const t = lx.peek();
if (t.type === "eof") throw new ParseError("Unexpected end of input inside computed");
const name = expect("ident").value;
expect("eq");
computed.push({ name, expr: lx.readPropInitializer() });
if (lx.peek().type === "lbrace") {
computed.push(...parseComputedDeclarations(lx.readBalancedBraces()));
} else {
const cName = expect("ident").value;
let valueType: string | undefined;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new ParseError(`Computed '${cName}' requires an expression`);
} else expect("eq");
computed.push({ name: cName, valueType, expr: lx.readPropInitializer() });
}
break;
}
case "outputs": {
lx.next();
try {
outputs.push(...parseOutputs(lx.readBalancedBraces()));
} catch (error) {
throw new ParseError(
error instanceof Error ? error.message : String(error),
"WRN-OUTPUT-DECLARATION",
);
}
expect("rbrace");
break;
}
case "effect": {
@@ -496,9 +584,24 @@ export function parse(source: string): PageAst {
break;
}
case "ssr":
case "client": {
const mode: DataMode = kw.value === "ssr" ? "ssr" : "client";
case "client":
case "server": {
const rawMode = kw.value;
const mode: DataMode = rawMode === "client" ? "client" : "ssr";
lx.next();
if (
(rawMode === "client" || rawMode === "server") &&
lx.peek().type === "ident" &&
lx.peek().value === "state"
) {
lx.next();
if (lx.peek().type !== "lbrace")
throw new ParseError(`Expected a grouped ${rawMode} state block`);
states.push(
...parseStateDeclarations(lx.readBalancedBraces(), rawMode as StateRuntime),
);
break;
}
if (mode === "client" && lx.peek().type === "eq") {
lx.next();
hydrate = expect("string").value;
@@ -537,6 +640,14 @@ export function parse(source: string): PageAst {
expect("rbrace");
break;
}
case "shared": {
lx.next();
const member = expect("ident");
if (member.value !== "state")
throw new ParseError(`Expected 'state' after shared at offset ${member.pos}`);
states.push(...parseStateDeclarations(lx.readBalancedBraces(), "shared"));
break;
}
case "realtime": {
lx.next();
const rName = expect("ident").value;
@@ -565,35 +676,51 @@ export function parse(source: string): PageAst {
}
case "lifecycle": {
lx.next();
expect("lbrace");
while (lx.peek().type !== "rbrace") {
const hook = lx.peek();
if (hook.type === "eof") {
throw new ParseError("Unexpected end of input inside lifecycle block");
const body = lx.readBalancedBraces();
if (kind === "global-store" || kind === "page-store") {
storeLifecycle = parseStoreLifecycle(body);
const allowedStoreHooks = new Set(["serverInit", "clientInit", "hydrate", "dispose"]);
const hookLexer = new Lexer(body);
while (hookLexer.peek().type !== "eof") {
const token = hookLexer.next();
if (token.type !== "ident") {
throw new ParseError(`Expected a lifecycle hook at offset ${token.pos}`);
}
if (!allowedStoreHooks.has(token.value)) {
throw new ParseError(`Unknown store lifecycle hook '${token.value}'`);
}
hookLexer.readBalancedBraces();
}
if (hook.type !== "ident") {
throw new ParseError(`Expected a lifecycle hook at offset ${hook.pos}`);
} else {
const allowedComponentHooks = new Set([
"mount",
"update",
"unmount",
"clientInit",
"dispose",
]);
const hookLexer = new Lexer(body);
while (hookLexer.peek().type !== "eof") {
const token = hookLexer.next();
if (token.type !== "ident") {
throw new ParseError(`Expected a lifecycle hook at offset ${token.pos}`);
}
if (!allowedComponentHooks.has(token.value)) {
throw new ParseError(`Unknown lifecycle hook '${token.value}'`);
}
const hookBody = hookLexer.readBalancedBraces();
const hook =
token.value === "clientInit"
? "mount"
: token.value === "dispose"
? "unmount"
: (token.value as LifecycleHookName);
if (lifecycle[hook] !== undefined) {
throw new ParseError(`Duplicate lifecycle hook '${hook}'`);
}
lifecycle[hook] = hookBody;
}
if (hook.value !== "mount" && hook.value !== "update" && hook.value !== "unmount") {
throw new ParseError(`Unknown lifecycle hook '${hook.value}' at offset ${hook.pos}`);
}
const hookName = hook.value as LifecycleHookName;
lx.next();
if (lifecycle[hookName] !== undefined) {
throw new ParseError(`Duplicate lifecycle hook '${hookName}' at offset ${hook.pos}`);
}
lifecycle[hookName] = lx.readBalancedBraces();
}
expect("rbrace");
break;
}
case "watch": {
@@ -611,7 +738,21 @@ export function parse(source: string): PageAst {
}
case "functions": {
lx.next();
functions.push(lx.readBalancedBraces());
const body = lx.readBalancedBraces();
functions.push(body);
try {
runtimeFunctions.push(...parseRuntimeFunctions(body));
} catch (error) {
throw new ParseError(
error instanceof Error ? error.message : String(error),
"WRN-FUNCTION-DECLARATION",
);
}
break;
}
case "persist": {
lx.next();
persist = parsePersist(lx.readBalancedBraces());
break;
}
default:
@@ -649,16 +790,36 @@ export function parse(source: string): PageAst {
symbols.add(declaration.name);
}
const outputNames = new Set<string>();
for (const output of outputs) {
if (outputNames.has(output.name))
throw new ParseError(`Duplicate output '${output.name}'`, "WRN-OUTPUT-DUPLICATE");
outputNames.add(output.name);
}
const functionKeys = new Set<string>();
for (const fn of runtimeFunctions) {
const key = `${fn.runtime}:${fn.name}`;
if (functionKeys.has(key))
throw new ParseError(
`Duplicate ${fn.runtime} function '${fn.name}'`,
"WRN-FUNCTION-DUPLICATE",
);
functionKeys.add(key);
}
return {
type: "page",
imports,
structuredImports: parseStructuredImports(imports),
kind,
storeKind,
name,
layout,
layoutIsSymbol,
runtime,
hydrate,
props,
events,
outputs,
types,
states,
computed,
@@ -670,9 +831,12 @@ export function parse(source: string): PageAst {
view,
styles,
functions,
runtimeFunctions,
dataApis,
modeFunctions,
lifecycle,
storeLifecycle,
persist,
watches,
apis,
realtimes,