feat: support WRN imports and dynamic public shell

This commit is contained in:
2026-07-21 11:15:06 +05:30
parent 0013c0771d
commit 2ca2d02b22
82 changed files with 959 additions and 174 deletions
+26 -13
View File
@@ -8,6 +8,19 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation
```bash
@@ -76,19 +89,19 @@ class Lexer {
Exported type-only symbols describing the parsed tree:
| Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
| Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.2.73",
"version": "0.2.74",
"type": "module",
"main": "src/index.ts",
"exports": {
+2
View File
@@ -660,6 +660,7 @@ export function generate(ast: PageAst): string {
}
const out: string[] = [];
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
const ssrBindings: SsrBinding[] = [];
const csrBindings: CsrBinding[] = [];
const helpers = ast.functions
@@ -1292,6 +1293,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"));
const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) {
+1 -1
View File
@@ -233,5 +233,5 @@ export function generateNative(ast: PageAst): string {
.map((block) => block.trim())
.filter(Boolean)
.join("\n\n");
return `// generated from .wrn for Expo/React Native\nimport React, { useState } from "react";\nimport { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";\nimport { useRouter } from "expo-router";\n\n${typeSource}\n\nexport default function ${ast.name}() {\n const router = useRouter();\n${hooks}\n return <>${body}</>;\n}\n\n${nativeStyles(ast.styles)}\n`;
return `// generated from .wrn for Expo/React Native\nimport React, { useState } from "react";\nimport { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";\nimport { useRouter } from "expo-router";\n${ast.imports.join("\n")}\n\n${typeSource}\n\nexport default function ${ast.name}() {\n const router = useRouter();\n${hooks}\n return <>${body}</>;\n}\n\n${nativeStyles(ast.styles)}\n`;
}
+18
View File
@@ -146,6 +146,8 @@ export interface PropDecl {
export interface PageAst {
type: "page";
/** Static ES module imports declared before the WRN root declaration. */
imports: string[];
/**
* `page` is a route, `component` is a reusable fragment,
* and `layout` is a reusable page wrapper.
@@ -197,6 +199,21 @@ function unescapeSeoValue(value: string): string {
export function parse(source: string): PageAst {
const lx = new Lexer(source);
const imports: string[] = [];
const importPattern = /import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'][^"'\r\n]+["']\s*;?/y;
while (true) {
while (/\s/u.test(source[lx.pos] ?? "")) lx.pos++;
if (source.startsWith("//", lx.pos)) {
while (lx.pos < source.length && source[lx.pos] !== "\n") lx.pos++;
continue;
}
importPattern.lastIndex = lx.pos;
const statement = importPattern.exec(source);
if (!statement) break;
imports.push(statement[0].trim());
lx.pos = importPattern.lastIndex;
}
const expect = (type: Token["type"]): Token => {
const t = lx.next();
if (t.type !== type) {
@@ -474,6 +491,7 @@ export function parse(source: string): PageAst {
return {
type: "page",
imports,
kind,
name,
layout,
+14
View File
@@ -57,6 +57,20 @@ test("parses a page with a layout member", () => {
expect(ast.layout).toBe("public");
});
test("top-level imports are preserved and available to SSR view expressions", async () => {
const source = `import { posix } from "node:path";
page Home {
view { <PublicHeader signInHref="{posix.join('/sso', '/sign-in')}" /> }
}`;
const ast = parse(source);
expect(ast.imports).toEqual(['import { posix } from "node:path";']);
const mod = await compileAndImport(source);
const render = mod.default as (ctx: unknown) => string;
expect(String(await render({}))).toContain('signInHref="/sso/sign-in"');
});
test("parses a component with props and state", () => {
const ast = parse(
`component C {\n props {\n n = 0\n }\n state count = n\n view { <b>{count}</b> }\n}`,