release: WRNexusJS 0.6.0
This commit is contained in:
@@ -19,6 +19,8 @@ import { Buffer } from "node:buffer";
|
||||
|
||||
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
|
||||
import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
|
||||
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
|
||||
import { generateStoreModule } from "./store-codegen.ts";
|
||||
|
||||
interface RenderBinding {
|
||||
method: string;
|
||||
@@ -765,6 +767,38 @@ function localStyleExport(ast: PageAst, styles: string[]): string | null {
|
||||
)};`;
|
||||
}
|
||||
|
||||
function isStoreImportSource(source: string): boolean {
|
||||
return /(?:^|\/)stores?\//.test(source) || source.startsWith("@wrnexus/store");
|
||||
}
|
||||
|
||||
function importedStoreBindings(ast: PageAst): Array<{ local: string; internal: string }> {
|
||||
return ast.structuredImports
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.defaultImport && entry.source.endsWith(".wrn") && isStoreImportSource(entry.source),
|
||||
)
|
||||
.map((entry) => ({
|
||||
local: entry.defaultImport!,
|
||||
internal: `__wrnexusStoreDefinition_${entry.defaultImport}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function generatedImports(ast: PageAst): string[] {
|
||||
const stores = new Map(importedStoreBindings(ast).map((entry) => [entry.local, entry.internal]));
|
||||
return ast.structuredImports.map((entry) => {
|
||||
if (!entry.defaultImport) return entry.raw;
|
||||
const internal = stores.get(entry.defaultImport);
|
||||
return internal
|
||||
? entry.raw.replace(
|
||||
new RegExp(
|
||||
`^(\\s*import\\s+(?:type\\s+)?)(?:${entry.defaultImport.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")})(\\s+from\\s+)`,
|
||||
),
|
||||
`$1${internal}$2`,
|
||||
)
|
||||
: entry.raw;
|
||||
});
|
||||
}
|
||||
|
||||
function isSafeGeneratedIdentifier(name: string): boolean {
|
||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
||||
}
|
||||
@@ -781,22 +815,47 @@ function generateSsrStateAliases(stateNames: string[]): string {
|
||||
|
||||
function hydrationAttribute(ast: PageAst): string {
|
||||
const strategy = ast.hydrate ?? "load";
|
||||
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"`;
|
||||
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
|
||||
["legacy", "client", "shared"].includes(fn.runtime),
|
||||
);
|
||||
const moduleAttribute = hasBrowserModule
|
||||
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
|
||||
: "";
|
||||
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
|
||||
}
|
||||
|
||||
function targetFunctions(ast: PageAst, target: "browser" | "server"): string {
|
||||
const runtimes =
|
||||
target === "browser"
|
||||
? (["legacy", "client", "shared"] as const)
|
||||
: (["legacy", "server", "shared"] as const);
|
||||
return ast.functions
|
||||
.map((body) => stripRuntimeFunctionModifiers(body, [...runtimes]))
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function publicOutputNames(ast: PageAst): string[] {
|
||||
return [
|
||||
...new Set([
|
||||
...ast.outputs.map((output) => output.name),
|
||||
...ast.events.map((event) => event.name),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export function generate(ast: PageAst): string {
|
||||
if (ast.kind === "global-store" || ast.kind === "page-store") return generateStoreModule(ast);
|
||||
if (ast.kind === "component" || ast.kind === "layout") {
|
||||
return generateComponent(ast);
|
||||
}
|
||||
|
||||
const out: string[] = [];
|
||||
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
|
||||
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
|
||||
const ssrBindings: SsrBinding[] = [];
|
||||
const csrBindings: CsrBinding[] = [];
|
||||
const helpers = ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
const helpers = targetFunctions(ast, "server");
|
||||
const apiBindings = apiBindingMap(ast, helpers);
|
||||
|
||||
const typeSource = ast.types
|
||||
@@ -811,7 +870,10 @@ export function generate(ast: PageAst): string {
|
||||
|
||||
// --- Page metadata / SEO ---
|
||||
out.push(`export const meta = ${JSON.stringify({ title: ast.name, ...ast.seo }, null, 2)};`);
|
||||
if (ast.layout) out.push(`export const layout = ${JSON.stringify(ast.layout)};`);
|
||||
if (ast.layout)
|
||||
out.push(
|
||||
`export const layout = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`,
|
||||
);
|
||||
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
|
||||
out.push(`export const __wrnexusHydrate = ${JSON.stringify(ast.hydrate ?? "load")};`);
|
||||
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
|
||||
@@ -820,6 +882,7 @@ export function generate(ast: PageAst): string {
|
||||
}
|
||||
|
||||
// --- View -> default page component ---
|
||||
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
||||
const seedScope = evalStateSeeds(ast.states);
|
||||
for (const entry of ast.computed) {
|
||||
try {
|
||||
@@ -831,7 +894,7 @@ export function generate(ast: PageAst): string {
|
||||
}
|
||||
}
|
||||
const reactiveNames = [
|
||||
...ast.states.map((entry) => entry.name),
|
||||
...browserStates.map((entry) => entry.name),
|
||||
...ast.computed.map((entry) => entry.name),
|
||||
];
|
||||
const runtimeStateNames = new Set(
|
||||
@@ -849,7 +912,7 @@ export function generate(ast: PageAst): string {
|
||||
const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
|
||||
const needsClientRuntime =
|
||||
ast.runtime !== "server" &&
|
||||
(ast.states.length > 0 ||
|
||||
(browserStates.length > 0 ||
|
||||
ast.computed.length > 0 ||
|
||||
hasClientBehavior(ast.view) ||
|
||||
pageBehavior !== null);
|
||||
@@ -887,9 +950,17 @@ export function generate(ast: PageAst): string {
|
||||
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
|
||||
.join("; ")} }`
|
||||
: "Record<string, never>";
|
||||
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
||||
|
||||
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
||||
|
||||
const storeBindings = importedStoreBindings(ast);
|
||||
const storeDeclarations = storeBindings
|
||||
.map(
|
||||
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
loops.forEach((code, idx) => {
|
||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
||||
});
|
||||
@@ -918,10 +989,12 @@ export function generate(ast: PageAst): string {
|
||||
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
||||
out.push(
|
||||
`export default async function ${ast.name}(ctx: any) {
|
||||
${storeDeclarations}
|
||||
${decls}
|
||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
||||
${ssrStateAliases}
|
||||
const __scopeValue = Object.entries(__state)
|
||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||
const __scopeValue = Object.entries(__hydrationState)
|
||||
.map(([key, value]) => {
|
||||
const encoded =
|
||||
typeof value === "number" || typeof value === "boolean"
|
||||
@@ -946,10 +1019,12 @@ export function generate(ast: PageAst): string {
|
||||
);
|
||||
} else {
|
||||
out.push(
|
||||
`export default function ${ast.name}(ctx: any) {
|
||||
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
||||
${storeDeclarations}
|
||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
||||
${ssrStateAliases}
|
||||
const __scopeValue = Object.entries(__state)
|
||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||
const __scopeValue = Object.entries(__hydrationState)
|
||||
.map(([key, value]) => {
|
||||
const encoded =
|
||||
typeof value === "number" || typeof value === "boolean"
|
||||
@@ -1062,6 +1137,10 @@ interface CompCtx {
|
||||
|
||||
interface ComponentBehavior {
|
||||
functions: string;
|
||||
outputs: Array<{
|
||||
name: string;
|
||||
payload?: { name: string; valueType: string; optional: boolean };
|
||||
}>;
|
||||
computed: Array<{ name: string; expr: string }>;
|
||||
effects: string[];
|
||||
lifecycle: {
|
||||
@@ -1150,12 +1229,7 @@ function escLit(s: string): string {
|
||||
}
|
||||
|
||||
function componentBehavior(ast: PageAst): ComponentBehavior | null {
|
||||
const functions = eraseFunctionTypes(
|
||||
ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
);
|
||||
const functions = eraseFunctionTypes(targetFunctions(ast, "browser"));
|
||||
|
||||
const computed = ast.computed.map((entry) => ({ name: entry.name, expr: entry.expr.trim() }));
|
||||
const effects = ast.effects.map((entry) => entry.body.trim()).filter(Boolean);
|
||||
@@ -1175,6 +1249,7 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
|
||||
|
||||
if (
|
||||
!functions &&
|
||||
ast.outputs.length === 0 &&
|
||||
computed.length === 0 &&
|
||||
effects.length === 0 &&
|
||||
Object.keys(lifecycle).length === 0 &&
|
||||
@@ -1185,6 +1260,7 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
|
||||
|
||||
return {
|
||||
functions,
|
||||
outputs: ast.outputs,
|
||||
computed,
|
||||
effects,
|
||||
lifecycle,
|
||||
@@ -1613,7 +1689,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
|
||||
function generateComponent(ast: PageAst): string {
|
||||
const out: string[] = [];
|
||||
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
|
||||
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
|
||||
const hasServerEach = viewHasServerEach(ast.view);
|
||||
|
||||
const effectiveProps =
|
||||
@@ -1629,8 +1705,9 @@ function generateComponent(ast: PageAst): string {
|
||||
]
|
||||
: ast.props;
|
||||
|
||||
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
||||
const stateNames = new Set([
|
||||
...ast.states.map((entry) => entry.name),
|
||||
...browserStates.map((entry) => entry.name),
|
||||
...ast.computed.map((entry) => entry.name),
|
||||
]);
|
||||
const nameRefs = new Map<string, string>();
|
||||
@@ -1652,21 +1729,13 @@ function generateComponent(ast: PageAst): string {
|
||||
const ctx: CompCtx = {
|
||||
stateNames,
|
||||
functionNames: new Set(
|
||||
ast.functions.flatMap((body) => {
|
||||
return Array.from(
|
||||
body.matchAll(/(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g),
|
||||
(match) => match[1]!,
|
||||
);
|
||||
}),
|
||||
ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map((fn) => fn.name),
|
||||
),
|
||||
resolveExpr,
|
||||
eventNames: ast.events.map((event) => event.name),
|
||||
eventNames: publicOutputNames(ast),
|
||||
};
|
||||
|
||||
const serverFunctions = ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
const serverFunctions = targetFunctions(ast, "server");
|
||||
|
||||
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
|
||||
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
|
||||
@@ -1694,7 +1763,7 @@ function generateComponent(ast: PageAst): string {
|
||||
|
||||
const needsScope =
|
||||
ast.runtime !== "server" &&
|
||||
(ast.states.length > 0 ||
|
||||
(browserStates.length > 0 ||
|
||||
ast.computed.length > 0 ||
|
||||
viewHasEvents(ast.view) ||
|
||||
behavior !== null);
|
||||
@@ -1705,7 +1774,7 @@ function generateComponent(ast: PageAst): string {
|
||||
|
||||
const scopeKeys = [
|
||||
...effectiveProps.map((prop) => prop.name),
|
||||
...ast.states.map((state) => state.name),
|
||||
...browserStates.map((state) => state.name),
|
||||
];
|
||||
|
||||
const behaviorAttr = behaviorAttribute(behavior);
|
||||
@@ -1791,6 +1860,17 @@ function generateComponent(ast: PageAst): string {
|
||||
);
|
||||
}
|
||||
|
||||
if (ast.outputs.length > 0) {
|
||||
out.push(
|
||||
`export interface ${ast.name}Outputs {\n${ast.outputs
|
||||
.map(
|
||||
(output) =>
|
||||
` ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`,
|
||||
)
|
||||
.join("\n")}\n}`,
|
||||
);
|
||||
}
|
||||
|
||||
out.push(`function __coerce(v: any, def: any, declared: string = "unknown"): any {
|
||||
if (v === undefined || v === null) {
|
||||
return def;
|
||||
@@ -2023,6 +2103,9 @@ function __wireRaw(v: any): string {
|
||||
`}`,
|
||||
);
|
||||
|
||||
out.push(
|
||||
`export default { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.kind)}, render };`,
|
||||
);
|
||||
return out.join("\n\n") + "\n";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user