release: WRNexusJS 0.8.0
This commit is contained in:
+112
-11
@@ -1,3 +1,5 @@
|
||||
import { WRN_RUNTIME_TARGETS } from "./spec.ts";
|
||||
|
||||
/**
|
||||
* Recursive-descent parser for `.wrn`, producing a small AST.
|
||||
*
|
||||
@@ -63,12 +65,16 @@ export interface EffectBlock {
|
||||
|
||||
export interface LoadBlock {
|
||||
mode: "server" | "client";
|
||||
name?: string;
|
||||
dependsOn?: string[];
|
||||
deferred?: boolean;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ActionBlock {
|
||||
name: string;
|
||||
args: string[];
|
||||
schema?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
@@ -205,9 +211,13 @@ export interface PageAst {
|
||||
layout?: string;
|
||||
layoutIsSymbol?: boolean;
|
||||
/** Execution boundary metadata. Defaults to universal. */
|
||||
runtime?: "server" | "client" | "universal";
|
||||
runtime?: "server" | "client" | "universal" | "edge" | "worker" | "service-worker";
|
||||
/** Explicit rendering policy; `hybrid` is the default SSR + optional hydration behavior. */
|
||||
renderMode?: "static" | "server" | "hybrid" | "client" | "partial-static";
|
||||
/** Client hydration strategy. Defaults to load when interactivity is present. */
|
||||
hydrate?: string;
|
||||
/** Declarative framework cache policy. */
|
||||
cache?: Record<string, string>;
|
||||
/** Declared component props (empty for pages). */
|
||||
props: PropDecl[];
|
||||
/** Legacy public events exposed by a reusable component. */
|
||||
@@ -222,6 +232,7 @@ export interface PageAst {
|
||||
loads: LoadBlock[];
|
||||
actions: ActionBlock[];
|
||||
security: Record<string, string>;
|
||||
navigation: Record<string, string>;
|
||||
seo: SeoBlock;
|
||||
view: ViewNode[];
|
||||
styles: string[];
|
||||
@@ -343,6 +354,7 @@ export function parse(source: string): PageAst {
|
||||
let layout: string | undefined;
|
||||
let layoutIsSymbol = false;
|
||||
let runtime: PageAst["runtime"];
|
||||
let renderMode: PageAst["renderMode"];
|
||||
let hydrate: string | undefined;
|
||||
const props: PropDecl[] = [];
|
||||
const events: EventDecl[] = [];
|
||||
@@ -354,6 +366,8 @@ export function parse(source: string): PageAst {
|
||||
const loads: LoadBlock[] = [];
|
||||
const actions: ActionBlock[] = [];
|
||||
const security: Record<string, string> = {};
|
||||
const cache: Record<string, string> = {};
|
||||
const navigation: Record<string, string> = {};
|
||||
const seo: SeoBlock = {};
|
||||
const view: ViewNode[] = [];
|
||||
const styles: string[] = [];
|
||||
@@ -393,19 +407,32 @@ export function parse(source: string): PageAst {
|
||||
lx.next();
|
||||
expect("eq");
|
||||
const value = expect("string").value;
|
||||
if (value !== "server" && value !== "client" && value !== "universal") {
|
||||
if (!WRN_RUNTIME_TARGETS.includes(value as (typeof WRN_RUNTIME_TARGETS)[number])) {
|
||||
throw new ParseError(
|
||||
`Unknown runtime target '${value}' at offset ${kw.pos}`,
|
||||
"WRN-RUNTIME-TARGET",
|
||||
);
|
||||
}
|
||||
runtime = value;
|
||||
runtime = value as PageAst["runtime"];
|
||||
break;
|
||||
}
|
||||
case "render": {
|
||||
lx.next();
|
||||
expect("eq");
|
||||
const value = expect("string").value;
|
||||
if (!["static", "server", "hybrid", "client", "partial-static"].includes(value))
|
||||
throw new ParseError(
|
||||
`Unknown render mode '${value}' at offset ${kw.pos}`,
|
||||
"WRN-RENDER-MODE",
|
||||
);
|
||||
renderMode = value as PageAst["renderMode"];
|
||||
break;
|
||||
}
|
||||
case "hydrate": {
|
||||
lx.next();
|
||||
expect("eq");
|
||||
hydrate = expect("string").value;
|
||||
const value = expect("string").value;
|
||||
hydrate = value === "never" ? "none" : value;
|
||||
break;
|
||||
}
|
||||
case "props": {
|
||||
@@ -549,15 +576,49 @@ export function parse(source: string): PageAst {
|
||||
Object.assign(security, parseSeoBlock(lx.readBalancedBraces()));
|
||||
break;
|
||||
}
|
||||
case "navigation": {
|
||||
lx.next();
|
||||
Object.assign(navigation, parseSeoBlock(lx.readBalancedBraces()));
|
||||
break;
|
||||
}
|
||||
case "cache": {
|
||||
lx.next();
|
||||
Object.assign(cache, parseSeoBlock(lx.readBalancedBraces()));
|
||||
break;
|
||||
}
|
||||
case "load": {
|
||||
lx.next();
|
||||
const modeToken = expect("ident");
|
||||
if (modeToken.value !== "server" && modeToken.value !== "client") {
|
||||
throw new ParseError(
|
||||
`Expected 'server' or 'client' after load at offset ${modeToken.pos}`,
|
||||
);
|
||||
const first = expect("ident");
|
||||
const mode = first.value === "client" ? "client" : "server";
|
||||
const name =
|
||||
first.value === "server" || first.value === "client"
|
||||
? lx.peek().type === "ident"
|
||||
? expect("ident").value
|
||||
: undefined
|
||||
: first.value;
|
||||
const dependsOn: string[] = [];
|
||||
let deferred = false;
|
||||
while (lx.peek().type === "ident") {
|
||||
if (lx.peek().value === "defer") {
|
||||
lx.next();
|
||||
deferred = true;
|
||||
continue;
|
||||
}
|
||||
if (lx.peek().value !== "after") break;
|
||||
lx.next();
|
||||
dependsOn.push(expect("ident").value);
|
||||
while (lx.peek().type === "comma") {
|
||||
lx.next();
|
||||
dependsOn.push(expect("ident").value);
|
||||
}
|
||||
}
|
||||
loads.push({ mode: modeToken.value, body: lx.readBalancedBraces() });
|
||||
loads.push({
|
||||
mode,
|
||||
name,
|
||||
...(dependsOn.length ? { dependsOn } : {}),
|
||||
...(deferred ? { deferred: true } : {}),
|
||||
body: lx.readBalancedBraces(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "action": {
|
||||
@@ -572,7 +633,12 @@ export function parse(source: string): PageAst {
|
||||
}
|
||||
expect("rparen");
|
||||
}
|
||||
actions.push({ name: actionName, args, body: lx.readBalancedBraces() });
|
||||
let schema: string | undefined;
|
||||
if (lx.peek().type === "ident" && lx.peek().value === "using") {
|
||||
lx.next();
|
||||
schema = expect("ident").value;
|
||||
}
|
||||
actions.push({ name: actionName, args, schema, body: lx.readBalancedBraces() });
|
||||
break;
|
||||
}
|
||||
case "api": {
|
||||
@@ -806,6 +872,38 @@ export function parse(source: string): PageAst {
|
||||
);
|
||||
functionKeys.add(key);
|
||||
}
|
||||
const namedLoads = new Map(loads.filter((load) => load.name).map((load) => [load.name!, load]));
|
||||
for (const load of namedLoads.values()) {
|
||||
for (const dependency of load.dependsOn ?? []) {
|
||||
const dependencyLoad = namedLoads.get(dependency);
|
||||
if (!dependencyLoad)
|
||||
throw new ParseError(
|
||||
`Load '${load.name}' depends on unknown load '${dependency}'`,
|
||||
"WRN-LOAD-DEPENDENCY",
|
||||
);
|
||||
if (
|
||||
load.mode === "server" &&
|
||||
!load.deferred &&
|
||||
(dependencyLoad.mode !== "server" || dependencyLoad.deferred)
|
||||
)
|
||||
throw new ParseError(
|
||||
`Server load '${load.name}' cannot depend on deferred/client load '${dependency}'`,
|
||||
"WRN-LOAD-PHASE",
|
||||
);
|
||||
}
|
||||
}
|
||||
const visiting = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
const visitLoad = (name: string): void => {
|
||||
if (visiting.has(name))
|
||||
throw new ParseError(`Load dependency cycle includes '${name}'`, "WRN-LOAD-CYCLE");
|
||||
if (visited.has(name)) return;
|
||||
visiting.add(name);
|
||||
for (const dependency of namedLoads.get(name)?.dependsOn ?? []) visitLoad(dependency);
|
||||
visiting.delete(name);
|
||||
visited.add(name);
|
||||
};
|
||||
for (const name of namedLoads.keys()) visitLoad(name);
|
||||
return {
|
||||
type: "page",
|
||||
imports,
|
||||
@@ -816,7 +914,9 @@ export function parse(source: string): PageAst {
|
||||
layout,
|
||||
layoutIsSymbol,
|
||||
runtime,
|
||||
renderMode,
|
||||
hydrate,
|
||||
cache,
|
||||
props,
|
||||
events,
|
||||
outputs,
|
||||
@@ -827,6 +927,7 @@ export function parse(source: string): PageAst {
|
||||
loads,
|
||||
actions,
|
||||
security,
|
||||
navigation,
|
||||
seo,
|
||||
view,
|
||||
styles,
|
||||
|
||||
Reference in New Issue
Block a user