1944 lines
62 KiB
JavaScript
1944 lines
62 KiB
JavaScript
var __defProp = Object.defineProperty;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
function __accessProp(key) {
|
|
return this[key];
|
|
}
|
|
var __toCommonJS = (from) => {
|
|
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
if (entry)
|
|
return entry;
|
|
entry = __defProp({}, "__esModule", { value: true });
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (var key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(entry, key))
|
|
__defProp(entry, key, {
|
|
get: __accessProp.bind(from, key),
|
|
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
});
|
|
}
|
|
__moduleCache.set(from, entry);
|
|
return entry;
|
|
};
|
|
var __moduleCache;
|
|
var __returnValue = (v) => v;
|
|
function __exportSetter(name, newValue) {
|
|
this[name] = __returnValue.bind(null, newValue);
|
|
}
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, {
|
|
get: all[name],
|
|
enumerable: true,
|
|
configurable: true,
|
|
set: __exportSetter.bind(all, name)
|
|
});
|
|
};
|
|
|
|
// ../../packages/compiler/src/index.ts
|
|
var exports_src = {};
|
|
__export(exports_src, {
|
|
parse: () => parse,
|
|
generateNative: () => generateNative,
|
|
generate: () => generate,
|
|
compileWireFile: () => compileWireFile,
|
|
compileNativeWireFile: () => compileNativeWireFile,
|
|
compile: () => compile,
|
|
ParseError: () => ParseError,
|
|
NativeCompileError: () => NativeCompileError,
|
|
Lexer: () => Lexer,
|
|
LexError: () => LexError
|
|
});
|
|
module.exports = __toCommonJS(exports_src);
|
|
|
|
// ../../packages/compiler/src/tokenizer.ts
|
|
class LexError extends Error {
|
|
}
|
|
var isWs = (c) => c === " " || c === "\t" || c === `
|
|
` || c === "\r";
|
|
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
|
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
|
|
|
class Lexer {
|
|
src;
|
|
pos = 0;
|
|
constructor(src) {
|
|
this.src = src;
|
|
}
|
|
skipTrivia() {
|
|
const { src } = this;
|
|
while (this.pos < src.length) {
|
|
const c = src[this.pos];
|
|
if (isWs(c)) {
|
|
this.pos++;
|
|
continue;
|
|
}
|
|
if (c === "/" && src[this.pos + 1] === "/") {
|
|
while (this.pos < src.length && src[this.pos] !== `
|
|
`)
|
|
this.pos++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
next() {
|
|
this.skipTrivia();
|
|
const { src } = this;
|
|
const pos = this.pos;
|
|
if (pos >= src.length)
|
|
return { type: "eof", value: "", pos };
|
|
const c = src[pos];
|
|
switch (c) {
|
|
case "{":
|
|
this.pos++;
|
|
return { type: "lbrace", value: c, pos };
|
|
case "}":
|
|
this.pos++;
|
|
return { type: "rbrace", value: c, pos };
|
|
case "(":
|
|
this.pos++;
|
|
return { type: "lparen", value: c, pos };
|
|
case ")":
|
|
this.pos++;
|
|
return { type: "rparen", value: c, pos };
|
|
case "@":
|
|
this.pos++;
|
|
return { type: "at", value: c, pos };
|
|
case "=":
|
|
this.pos++;
|
|
return { type: "eq", value: c, pos };
|
|
case ",":
|
|
this.pos++;
|
|
return { type: "comma", value: c, pos };
|
|
case '"':
|
|
case "'":
|
|
return this.readString(c, pos);
|
|
}
|
|
if (isIdentStart(c)) {
|
|
let v = "";
|
|
while (this.pos < src.length && isIdentPart(src[this.pos]))
|
|
v += src[this.pos++];
|
|
return { type: "ident", value: v, pos };
|
|
}
|
|
throw new LexError(`Unexpected character '${c}' at offset ${pos} (line ${this.lineAt(pos)})`);
|
|
}
|
|
peek() {
|
|
const save = this.pos;
|
|
const t = this.next();
|
|
this.pos = save;
|
|
return t;
|
|
}
|
|
readString(quote, pos) {
|
|
const { src } = this;
|
|
let v = "";
|
|
this.pos++;
|
|
while (this.pos < src.length) {
|
|
const c = src[this.pos++];
|
|
if (c === "\\") {
|
|
const n = src[this.pos++];
|
|
v += n === "n" ? `
|
|
` : n === "t" ? "\t" : n;
|
|
continue;
|
|
}
|
|
if (c === quote)
|
|
return { type: "string", value: v, pos };
|
|
v += c;
|
|
}
|
|
throw new LexError(`Unterminated string at offset ${pos}`);
|
|
}
|
|
readPath() {
|
|
this.skipTrivia();
|
|
const { src } = this;
|
|
let v = "";
|
|
while (this.pos < src.length && !isWs(src[this.pos]) && src[this.pos] !== "{") {
|
|
v += src[this.pos++];
|
|
}
|
|
if (!v)
|
|
throw new LexError(`Expected a path at offset ${this.pos}`);
|
|
return v;
|
|
}
|
|
readToLineEnd() {
|
|
const { src } = this;
|
|
let v = "";
|
|
while (this.pos < src.length && src[this.pos] !== `
|
|
`)
|
|
v += src[this.pos++];
|
|
return v.trim();
|
|
}
|
|
readBalancedBraces() {
|
|
this.skipTrivia();
|
|
const { src } = this;
|
|
if (src[this.pos] !== "{") {
|
|
throw new LexError(`Expected '{' at offset ${this.pos}`);
|
|
}
|
|
const start = this.pos + 1;
|
|
let depth = 0;
|
|
let i = this.pos;
|
|
let str = null;
|
|
for (;i < src.length; i++) {
|
|
const c = src[i];
|
|
if (str) {
|
|
if (c === "\\") {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (c === str)
|
|
str = null;
|
|
continue;
|
|
}
|
|
if (c === '"' || c === "'" || c === "`") {
|
|
str = c;
|
|
continue;
|
|
}
|
|
if (c === "{")
|
|
depth++;
|
|
else if (c === "}") {
|
|
depth--;
|
|
if (depth === 0) {
|
|
this.pos = i + 1;
|
|
return src.slice(start, i);
|
|
}
|
|
}
|
|
}
|
|
throw new LexError(`Unbalanced braces starting at offset ${this.pos}`);
|
|
}
|
|
lineAt(pos) {
|
|
let line = 1;
|
|
for (let i = 0;i < pos && i < this.src.length; i++) {
|
|
if (this.src[i] === `
|
|
`)
|
|
line++;
|
|
}
|
|
return line;
|
|
}
|
|
}
|
|
|
|
// ../../packages/compiler/src/parser.ts
|
|
var VOID_ELEMENTS = new Set([
|
|
"area",
|
|
"base",
|
|
"br",
|
|
"col",
|
|
"embed",
|
|
"hr",
|
|
"img",
|
|
"input",
|
|
"link",
|
|
"meta",
|
|
"param",
|
|
"source",
|
|
"track",
|
|
"wbr"
|
|
]);
|
|
|
|
class ParseError extends Error {
|
|
}
|
|
function parseSeoBlock(body) {
|
|
const out = {};
|
|
const pair = /([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\n;]+))/g;
|
|
for (const match of body.matchAll(pair)) {
|
|
const key = match[1];
|
|
const rawValue = match[2] ?? match[3] ?? match[4] ?? "";
|
|
out[key] = unescapeSeoValue(rawValue.trim());
|
|
}
|
|
return out;
|
|
}
|
|
function unescapeSeoValue(value) {
|
|
return value.replace(/\\(["'\\nrt])/g, (_match, ch) => {
|
|
if (ch === "n")
|
|
return `
|
|
`;
|
|
if (ch === "r")
|
|
return "\r";
|
|
if (ch === "t")
|
|
return "\t";
|
|
return ch;
|
|
});
|
|
}
|
|
function parse(source) {
|
|
const lx = new Lexer(source);
|
|
const expect = (type) => {
|
|
const t = lx.next();
|
|
if (t.type !== type) {
|
|
throw new ParseError(`Expected ${type} but got '${t.value || t.type}' at offset ${t.pos}`);
|
|
}
|
|
return t;
|
|
};
|
|
const expectKeyword = (kw) => {
|
|
const t = lx.next();
|
|
if (t.type !== "ident" || t.value !== kw) {
|
|
throw new ParseError(`Expected '${kw}' but got '${t.value || t.type}' at offset ${t.pos}`);
|
|
}
|
|
};
|
|
try {
|
|
const opener = lx.next();
|
|
if (opener.type !== "ident" || !["page", "component", "layout"].includes(opener.value)) {
|
|
throw new ParseError(`Expected 'page', 'component', or 'layout' but got '${opener.value || opener.type}' at offset ${opener.pos}`);
|
|
}
|
|
const kind = opener.value;
|
|
const name = expect("ident").value;
|
|
expect("lbrace");
|
|
let layout;
|
|
const props = [];
|
|
const states = [];
|
|
const seo = {};
|
|
const view = [];
|
|
const styles = [];
|
|
const functions = [];
|
|
const dataApis = [];
|
|
const modeFunctions = [];
|
|
const lifecycle = {};
|
|
const watches = [];
|
|
const apis = [];
|
|
const realtimes = [];
|
|
while (lx.peek().type !== "rbrace") {
|
|
const kw = lx.peek();
|
|
if (kw.type === "eof")
|
|
throw new ParseError(`Unexpected end of input inside ${kind}`);
|
|
if (kw.type !== "ident") {
|
|
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
|
|
}
|
|
switch (kw.value) {
|
|
case "layout": {
|
|
lx.next();
|
|
expect("eq");
|
|
layout = expect("string").value;
|
|
break;
|
|
}
|
|
case "props": {
|
|
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 props");
|
|
if (t.type !== "ident") {
|
|
throw new ParseError(`Expected a prop name at offset ${t.pos}`);
|
|
}
|
|
const pName = expect("ident").value;
|
|
expect("eq");
|
|
props.push({ name: pName, default: lx.readToLineEnd() });
|
|
}
|
|
expect("rbrace");
|
|
break;
|
|
}
|
|
case "state": {
|
|
lx.next();
|
|
const sName = expect("ident").value;
|
|
expect("eq");
|
|
states.push({ name: sName, expr: lx.readToLineEnd() });
|
|
break;
|
|
}
|
|
case "view": {
|
|
lx.next();
|
|
expect("lbrace");
|
|
const { nodes, endPos } = parseHtmlView(lx.src, lx.pos);
|
|
view.push(...nodes);
|
|
lx.pos = endPos;
|
|
expect("rbrace");
|
|
break;
|
|
}
|
|
case "seo": {
|
|
lx.next();
|
|
Object.assign(seo, parseSeoBlock(lx.readBalancedBraces()));
|
|
break;
|
|
}
|
|
case "api": {
|
|
lx.next();
|
|
const method = expect("ident").value.toUpperCase();
|
|
const path = lx.readPath();
|
|
const body = lx.readBalancedBraces();
|
|
apis.push({ method, path, body });
|
|
break;
|
|
}
|
|
case "ssr":
|
|
case "client": {
|
|
const mode = kw.value === "ssr" ? "ssr" : "client";
|
|
lx.next();
|
|
expect("lbrace");
|
|
while (lx.peek().type !== "rbrace") {
|
|
const member = lx.peek();
|
|
if (member.type === "eof") {
|
|
throw new ParseError(`Unexpected end of input inside ${mode} block`);
|
|
}
|
|
if (member.type !== "ident") {
|
|
throw new ParseError(`Expected a ${mode} member keyword at offset ${member.pos}`);
|
|
}
|
|
switch (member.value) {
|
|
case "api": {
|
|
lx.next();
|
|
const name2 = expect("ident").value;
|
|
const method = expect("ident").value.toUpperCase();
|
|
const path = lx.readPath();
|
|
const body = lx.readBalancedBraces();
|
|
dataApis.push({ mode, name: name2, method, path, body });
|
|
break;
|
|
}
|
|
case "functions": {
|
|
lx.next();
|
|
modeFunctions.push({ mode, body: lx.readBalancedBraces() });
|
|
break;
|
|
}
|
|
default:
|
|
throw new ParseError(`Unknown ${mode} member '${member.value}' at offset ${member.pos}`);
|
|
}
|
|
}
|
|
expect("rbrace");
|
|
break;
|
|
}
|
|
case "realtime": {
|
|
lx.next();
|
|
const rName = expect("ident").value;
|
|
expect("lbrace");
|
|
const handlers = [];
|
|
while (lx.peek().type !== "rbrace") {
|
|
expectKeyword("on");
|
|
const event = expect("ident").value;
|
|
expect("lparen");
|
|
const args = [];
|
|
while (lx.peek().type !== "rparen") {
|
|
args.push(expect("ident").value);
|
|
if (lx.peek().type === "comma")
|
|
lx.next();
|
|
}
|
|
expect("rparen");
|
|
handlers.push({ event, args, body: lx.readBalancedBraces() });
|
|
}
|
|
expect("rbrace");
|
|
realtimes.push({ name: rName, handlers });
|
|
break;
|
|
}
|
|
case "style": {
|
|
lx.next();
|
|
styles.push(lx.readBalancedBraces());
|
|
break;
|
|
}
|
|
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");
|
|
}
|
|
if (hook.type !== "ident") {
|
|
throw new ParseError(`Expected a lifecycle hook at offset ${hook.pos}`);
|
|
}
|
|
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;
|
|
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": {
|
|
lx.next();
|
|
const stateName = expect("ident").value;
|
|
const body = lx.readBalancedBraces();
|
|
watches.push({
|
|
state: stateName,
|
|
body
|
|
});
|
|
break;
|
|
}
|
|
case "functions": {
|
|
lx.next();
|
|
functions.push(lx.readBalancedBraces());
|
|
break;
|
|
}
|
|
default:
|
|
throw new ParseError(`Unknown page member '${kw.value}' at offset ${kw.pos}`);
|
|
}
|
|
}
|
|
expect("rbrace");
|
|
const declaredStates = new Set(states.map((state) => state.name));
|
|
for (const watcher of watches) {
|
|
if (!declaredStates.has(watcher.state)) {
|
|
throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`);
|
|
}
|
|
}
|
|
return {
|
|
type: "page",
|
|
kind,
|
|
name,
|
|
layout,
|
|
props,
|
|
states,
|
|
seo,
|
|
view,
|
|
styles,
|
|
functions,
|
|
dataApis,
|
|
modeFunctions,
|
|
lifecycle,
|
|
watches,
|
|
apis,
|
|
realtimes
|
|
};
|
|
} catch (err) {
|
|
if (err instanceof LexError)
|
|
throw new ParseError(err.message);
|
|
throw err;
|
|
}
|
|
}
|
|
function parseHtmlView(src, pos) {
|
|
let i = pos;
|
|
const isNameStart = (c) => /[A-Za-z_]/.test(c);
|
|
const isTagNamePart = (c) => /[A-Za-z0-9_$:.-]/.test(c);
|
|
const isWs2 = (c) => c === " " || c === "\t" || c === `
|
|
` || c === "\r";
|
|
const fail = (msg) => {
|
|
throw new ParseError(`${msg} at offset ${i}`);
|
|
};
|
|
const skipWs = () => {
|
|
while (i < src.length && isWs2(src[i]))
|
|
i++;
|
|
};
|
|
const readInterpolation = () => {
|
|
const start = i;
|
|
let depth = 0;
|
|
for (;i < src.length; i++) {
|
|
if (src[i] === "{")
|
|
depth++;
|
|
else if (src[i] === "}" && --depth === 0) {
|
|
i++;
|
|
return src.slice(start, i);
|
|
}
|
|
}
|
|
return fail("Unterminated `{` interpolation in view");
|
|
};
|
|
const readQuoted = () => {
|
|
const quote = src[i];
|
|
if (quote !== '"' && quote !== "'")
|
|
return fail("Expected a quoted attribute value");
|
|
i++;
|
|
const start = i;
|
|
while (i < src.length && src[i] !== quote)
|
|
i++;
|
|
if (i >= src.length)
|
|
return fail("Unterminated attribute value");
|
|
const value = src.slice(start, i);
|
|
i++;
|
|
return value;
|
|
};
|
|
const readTagName = () => {
|
|
if (i >= src.length || !isNameStart(src[i])) {
|
|
return fail("Expected a tag name");
|
|
}
|
|
const start = i++;
|
|
while (i < src.length && isTagNamePart(src[i])) {
|
|
i++;
|
|
}
|
|
return src.slice(start, i);
|
|
};
|
|
const readAttributeName = () => {
|
|
if (i >= src.length) {
|
|
return fail("Expected an attribute name");
|
|
}
|
|
const start = i;
|
|
while (i < src.length) {
|
|
const char = src[i];
|
|
const next = src[i + 1];
|
|
if (char === "=" || char === ">" || char === '"' || char === "'" || char === " " || char === "\t" || char === `
|
|
` || char === "\r" || char === "/" && next === ">") {
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
if (i === start) {
|
|
return fail("Expected an attribute name");
|
|
}
|
|
return src.slice(start, i);
|
|
};
|
|
const parseTag = () => {
|
|
i++;
|
|
const tag = readTagName();
|
|
const attrs = [];
|
|
for (;; ) {
|
|
skipWs();
|
|
const c = src[i];
|
|
if (c === undefined)
|
|
return fail(`Unterminated <${tag}> tag`);
|
|
if (c === ">") {
|
|
i++;
|
|
break;
|
|
}
|
|
if (c === "/" && src[i + 1] === ">") {
|
|
i += 2;
|
|
return { type: "element", tag, attrs, children: [] };
|
|
}
|
|
if (c === "@") {
|
|
i++;
|
|
const name2 = readAttributeName();
|
|
skipWs();
|
|
if (src[i] !== "=")
|
|
return fail(`Expected '=' after @${name2}`);
|
|
i++;
|
|
skipWs();
|
|
attrs.push({ name: name2, value: readQuoted(), event: true });
|
|
continue;
|
|
}
|
|
const name = readAttributeName();
|
|
skipWs();
|
|
if (src[i] === "=") {
|
|
i++;
|
|
skipWs();
|
|
attrs.push({ name, value: readQuoted(), event: false });
|
|
} else {
|
|
attrs.push({ name, value: "", event: false, boolean: true });
|
|
}
|
|
}
|
|
if (VOID_ELEMENTS.has(tag.toLowerCase())) {
|
|
return { type: "element", tag, attrs, children: [] };
|
|
}
|
|
const children = parseNodeList("element");
|
|
if (src[i] !== "<" || src[i + 1] !== "/")
|
|
return fail(`Expected </${tag}>`);
|
|
i += 2;
|
|
skipWs();
|
|
const close = readTagName();
|
|
if (close !== tag)
|
|
return fail(`Mismatched </${close}>, expected </${tag}>`);
|
|
skipWs();
|
|
if (src[i] !== ">")
|
|
return fail(`Expected '>' to close </${tag}>`);
|
|
i++;
|
|
return { type: "element", tag, attrs, children };
|
|
};
|
|
const EACH_HEADER = /^\{#each\s+([\s\S]+?)\s+as\s+([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\}$/;
|
|
function parseEach() {
|
|
const header = readInterpolation();
|
|
const m = EACH_HEADER.exec(header);
|
|
if (!m)
|
|
return fail(`Invalid {#each …} header: ${header}`);
|
|
const list = m[1].trim();
|
|
const item = m[2];
|
|
const index = m[3];
|
|
const body = parseNodeList("each");
|
|
let empty = [];
|
|
if (src.startsWith("{:empty}", i)) {
|
|
i += "{:empty}".length;
|
|
empty = parseNodeList("each");
|
|
}
|
|
if (!src.startsWith("{/each}", i))
|
|
return fail("Expected `{/each}` to close `{#each}`");
|
|
i += "{/each}".length;
|
|
return { type: "each", list, item, index, body, empty };
|
|
}
|
|
function parseIf() {
|
|
const header = readInterpolation();
|
|
const m = /^\{#if\s+([\s\S]+?)\s*\}$/.exec(header);
|
|
if (!m)
|
|
return fail(`Invalid {#if …} header: ${header}`);
|
|
const branches = [
|
|
{ cond: m[1].trim(), body: parseNodeList("if") }
|
|
];
|
|
for (;; ) {
|
|
if (src.startsWith("{:else if", i)) {
|
|
const h = readInterpolation();
|
|
const mm = /^\{:else if\s+([\s\S]+?)\s*\}$/.exec(h);
|
|
if (!mm)
|
|
return fail(`Invalid {:else if …}: ${h}`);
|
|
branches.push({ cond: mm[1].trim(), body: parseNodeList("if") });
|
|
continue;
|
|
}
|
|
if (src.startsWith("{:else}", i)) {
|
|
i += "{:else}".length;
|
|
branches.push({ cond: null, body: parseNodeList("if") });
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (!src.startsWith("{/if}", i))
|
|
return fail("Expected `{/if}` to close `{#if}`");
|
|
i += "{/if}".length;
|
|
return { type: "if", branches };
|
|
}
|
|
function parseNodeList(mode) {
|
|
const nodes2 = [];
|
|
let text = "";
|
|
const flush = () => {
|
|
if (text.length > 0) {
|
|
nodes2.push({ type: "text", value: text });
|
|
text = "";
|
|
}
|
|
};
|
|
for (;; ) {
|
|
if (i >= src.length) {
|
|
return mode === "root" ? fail("Unexpected end of view (missing `}`)") : fail("Unclosed block");
|
|
}
|
|
const c = src[i];
|
|
if (c === "<") {
|
|
const next = src[i + 1];
|
|
if (next === "/") {
|
|
flush();
|
|
break;
|
|
}
|
|
if (src.startsWith("<!--", i)) {
|
|
const end = src.indexOf("-->", i + 4);
|
|
i = end === -1 ? src.length : end + 3;
|
|
continue;
|
|
}
|
|
if (next !== undefined && (isNameStart(next) || next === "!")) {
|
|
flush();
|
|
nodes2.push(parseTag());
|
|
continue;
|
|
}
|
|
text += c;
|
|
i++;
|
|
continue;
|
|
}
|
|
if (c === "{") {
|
|
if (src.startsWith("{#each", i)) {
|
|
flush();
|
|
nodes2.push(parseEach());
|
|
continue;
|
|
}
|
|
if (src.startsWith("{#if", i)) {
|
|
flush();
|
|
nodes2.push(parseIf());
|
|
continue;
|
|
}
|
|
if (mode === "each" && (src.startsWith("{:empty}", i) || src.startsWith("{/each}", i))) {
|
|
flush();
|
|
break;
|
|
}
|
|
if (mode === "if" && (src.startsWith("{:else", i) || src.startsWith("{/if}", i))) {
|
|
flush();
|
|
break;
|
|
}
|
|
text += readInterpolation();
|
|
continue;
|
|
}
|
|
if (c === "}" && mode === "root") {
|
|
flush();
|
|
break;
|
|
}
|
|
text += c;
|
|
i++;
|
|
}
|
|
return nodes2;
|
|
}
|
|
const nodes = parseNodeList("root");
|
|
return { nodes, endPos: i };
|
|
}
|
|
|
|
// ../../packages/compiler/src/codegen.ts
|
|
var import_node_buffer = require("node:buffer");
|
|
function isComponentTag(tag) {
|
|
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
|
}
|
|
function attrEscape(value) {
|
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
function templateEscape(html) {
|
|
return html.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
}
|
|
function styleEscape(css) {
|
|
return css.replace(/<\/style/gi, "<\\/style");
|
|
}
|
|
function attrValue(attrs, name) {
|
|
return attrs.find((attr) => !attr.event && attr.name === name)?.value;
|
|
}
|
|
function renderAttr(attr) {
|
|
if (attr.event)
|
|
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
|
switch (attr.name) {
|
|
case "api":
|
|
case "ssrGet":
|
|
case "ssrText":
|
|
case "csrGet":
|
|
case "csrText":
|
|
return "";
|
|
default:
|
|
return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`;
|
|
}
|
|
}
|
|
function eventAttribute(name) {
|
|
if (name.startsWith("window:")) {
|
|
return `data-on-window-${name.slice("window:".length)}`;
|
|
}
|
|
if (name.startsWith("document:")) {
|
|
return `data-on-document-${name.slice("document:".length)}`;
|
|
}
|
|
if (name.startsWith("browser-")) {
|
|
return `data-on-wrnexus-browser-${name.slice(8)}`;
|
|
}
|
|
if (name.startsWith("mobile-")) {
|
|
return `data-on-wrnexus-mobile-${name.slice(7)}`;
|
|
}
|
|
return `data-on-${name}`;
|
|
}
|
|
function reactiveAttrValue(raw, reactive) {
|
|
let found = false;
|
|
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
|
const expr = inner.trim();
|
|
if (!exprRefsState(expr, reactive.stateNames))
|
|
return whole;
|
|
found = true;
|
|
try {
|
|
const result = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
|
return result == null ? "" : String(result);
|
|
} catch {
|
|
return whole;
|
|
}
|
|
});
|
|
return found ? value : null;
|
|
}
|
|
function renderAttrs(attrs, csrId, reactive = null) {
|
|
let bindIndex = 0;
|
|
const rendered = attrs.map((attr) => {
|
|
const base = renderAttr(attr);
|
|
if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
|
|
return base;
|
|
const initial = reactiveAttrValue(attr.value, reactive);
|
|
if (initial === null)
|
|
return base;
|
|
const marker = JSON.stringify([attr.name, attr.value]);
|
|
return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
|
|
}).join("");
|
|
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
|
|
}
|
|
function substituteTMarkers(text) {
|
|
return text.replace(/\{t:([^{}]+)\}/g, (_m, key) => `<span data-t="${attrEscape(key.trim())}"></span>`);
|
|
}
|
|
function htmlTextEscape(value) {
|
|
return value.replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
}
|
|
function evalStateSeeds(states) {
|
|
const scope = {};
|
|
for (const s of states) {
|
|
try {
|
|
scope[s.name] = new Function("with(this){return (" + s.expr + ");}").call(scope);
|
|
} catch {
|
|
scope[s.name] = undefined;
|
|
}
|
|
}
|
|
return scope;
|
|
}
|
|
function substituteReactiveText(raw, reactive) {
|
|
const text = substituteTMarkers(raw);
|
|
if (!reactive || reactive.stateNames.size === 0)
|
|
return text;
|
|
return text.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
|
const expr = inner.trim();
|
|
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames))
|
|
return whole;
|
|
let value;
|
|
try {
|
|
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
|
} catch {
|
|
return whole;
|
|
}
|
|
const baked = htmlTextEscape(value == null ? "" : String(value));
|
|
return `<span data-text="${attrEscape(expr)}">${baked}</span>`;
|
|
});
|
|
}
|
|
function bakeLoopText(raw) {
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
const re = /\{([^{}]+)\}/g;
|
|
while (m = re.exec(raw)) {
|
|
out += escLit(raw.slice(last, m.index));
|
|
const expr = m[1].trim();
|
|
if (expr.startsWith("t:")) {
|
|
out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
|
|
} else {
|
|
out += "${__wrnexusEscapeHtml(" + expr + ")}";
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(raw.slice(last));
|
|
}
|
|
function bakeLoopAttr(raw) {
|
|
if (!raw.includes("{"))
|
|
return escLit(attrEscape(raw));
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
const re = /\{([^{}]+)\}/g;
|
|
while (m = re.exec(raw)) {
|
|
out += escLit(attrEscape(raw.slice(last, m.index)));
|
|
out += "${__wrnexusEscapeHtml(" + m[1].trim() + ")}";
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(attrEscape(raw.slice(last)));
|
|
}
|
|
function renderLoopBody(node) {
|
|
if (node.type === "text") {
|
|
return bakeLoopText(node.value);
|
|
}
|
|
if (node.type === "each") {
|
|
return compileEachExpr(node);
|
|
}
|
|
if (node.type === "if") {
|
|
return compileIfExpr(node);
|
|
}
|
|
const componentTag = isComponentTag(node.tag);
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
|
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
|
if (attr.boolean) {
|
|
return escLit(` ${name}`);
|
|
}
|
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
|
}).join("");
|
|
const inner = node.children.map(renderLoopBody).join("");
|
|
if (componentTag) {
|
|
return escLit(`<div data-component="${attrEscape(node.tag)}"`) + attrs + escLit(">") + inner + escLit("</div>");
|
|
}
|
|
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
|
}
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
|
}
|
|
function compileEachExpr(node) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const body = node.body.map(renderLoopBody).join("");
|
|
const empty = node.empty.map(renderLoopBody).join("");
|
|
return "${(() => { const __wl = Array.isArray(" + node.list + ") ? (" + node.list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
|
|
}
|
|
function compileIfExpr(node) {
|
|
let expr = "``";
|
|
for (let k = node.branches.length - 1;k >= 0; k--) {
|
|
const b = node.branches[k];
|
|
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
|
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
|
}
|
|
return "${" + expr + "}";
|
|
}
|
|
function collectControlExprs(nodes, out = []) {
|
|
for (const node of nodes) {
|
|
if (node.type === "each") {
|
|
out.push(node.list);
|
|
collectControlExprs(node.body, out);
|
|
collectControlExprs(node.empty, out);
|
|
} else if (node.type === "if") {
|
|
for (const b of node.branches) {
|
|
if (b.cond)
|
|
out.push(b.cond);
|
|
collectControlExprs(b.body, out);
|
|
}
|
|
} else if (node.type === "element") {
|
|
collectControlExprs(node.children, out);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
|
if (node.type === "text")
|
|
return substituteReactiveText(node.value, reactive);
|
|
if (node.type === "each" || node.type === "if") {
|
|
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
|
return `\x00WRNEACH${loops.length - 1}\x00`;
|
|
}
|
|
if (isComponentTag(node.tag)) {
|
|
return renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive);
|
|
}
|
|
const apiName = attrValue(node.attrs, "api");
|
|
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
|
if (apiName && !apiBinding) {
|
|
throw new Error(`Unknown .wrn api binding "${apiName}"`);
|
|
}
|
|
const ssrGet = attrValue(node.attrs, "ssrGet");
|
|
const ssrText = attrValue(node.attrs, "ssrText");
|
|
const csrGet = attrValue(node.attrs, "csrGet");
|
|
const csrText = attrValue(node.attrs, "csrText");
|
|
const csrId = apiBinding?.mode === "client" ? csrMarker(csrBindings, renderBinding(apiBinding)) : csrGet && csrText ? csrMarker(csrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(csrGet),
|
|
body: expressionBody(csrText),
|
|
helpers: ""
|
|
}) : undefined;
|
|
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>`;
|
|
}
|
|
const inner = apiBinding?.mode === "ssr" ? ssrMarker(ssrBindings, renderBinding(apiBinding)) : ssrGet && ssrText ? ssrMarker(ssrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(ssrGet),
|
|
body: expressionBody(ssrText),
|
|
helpers: ""
|
|
}) : node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
|
}
|
|
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => renderPageComponentAttr(attr, loops)).join("");
|
|
const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
|
|
}
|
|
function renderNestedComponentInvocation(node, ctx) {
|
|
let bindIndex = 0;
|
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
|
if (attr.event) {
|
|
return escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`);
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const wholeExpression = wholeAttributeExpression(attr.value);
|
|
const compiledValue = wholeExpression ? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}` : compileAttrValue(attr.value, ctx);
|
|
const rendered = ` ${attr.name}="${compiledValue}"`;
|
|
if (wholeExpression || !attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
|
return rendered;
|
|
}
|
|
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
|
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const loops = loopVarsOf(node);
|
|
const childCtx = loops.length > 0 ? {
|
|
...ctx,
|
|
loopVars: new Set([...ctx.loopVars ?? [], ...loops])
|
|
} : ctx;
|
|
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
|
}
|
|
function ssrMarker(bindings, binding) {
|
|
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
|
bindings.push({ marker, ...binding });
|
|
return marker;
|
|
}
|
|
function csrMarker(bindings, binding) {
|
|
const id = String(bindings.length);
|
|
bindings.push({ id, ...binding });
|
|
return id;
|
|
}
|
|
function renderBinding(binding) {
|
|
return {
|
|
method: binding.method,
|
|
path: binding.path,
|
|
body: binding.body,
|
|
helpers: binding.helpers
|
|
};
|
|
}
|
|
function hasClientBehavior(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text")
|
|
return /\{(?!t:)[^{}]+\}/.test(node.value);
|
|
if (node.type === "each" || node.type === "if")
|
|
return false;
|
|
return node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") || hasClientBehavior(node.children);
|
|
});
|
|
}
|
|
function apiRoutePath(path) {
|
|
const trimmed = path.trim();
|
|
if (!trimmed.startsWith("/")) {
|
|
throw new Error(`.wrn API paths must start with "/": ${path}`);
|
|
}
|
|
if (trimmed.includes("\x00") || trimmed.includes("\\") || /(^|\/)\.\.(\/|$)/.test(trimmed)) {
|
|
throw new Error(`Unsafe .wrn API path: ${path}`);
|
|
}
|
|
if (trimmed === "/api" || trimmed.startsWith("/api/"))
|
|
return trimmed;
|
|
return `/api${trimmed}`;
|
|
}
|
|
function expressionBody(expr) {
|
|
return `return (${expr});`;
|
|
}
|
|
function dataBody(source) {
|
|
const trimmed = source.trim();
|
|
if (!trimmed)
|
|
return "return undefined;";
|
|
return /\breturn\b/.test(trimmed) ? trimmed : expressionBody(trimmed);
|
|
}
|
|
function modeHelpers(ast, mode, sharedHelpers) {
|
|
return [
|
|
sharedHelpers,
|
|
...ast.modeFunctions.filter((block) => block.mode === mode).map((block) => block.body.trim()).filter(Boolean)
|
|
].filter(Boolean).join(`
|
|
|
|
`);
|
|
}
|
|
function apiBindingMap(ast, sharedHelpers) {
|
|
const bindings = new Map;
|
|
for (const block of ast.dataApis) {
|
|
if (bindings.has(block.name)) {
|
|
throw new Error(`Duplicate .wrn api binding "${block.name}"`);
|
|
}
|
|
bindings.set(block.name, {
|
|
mode: block.mode,
|
|
method: block.method,
|
|
path: apiRoutePath(block.path),
|
|
body: dataBody(block.body),
|
|
helpers: modeHelpers(ast, block.mode, sharedHelpers)
|
|
});
|
|
}
|
|
return bindings;
|
|
}
|
|
function ssrRuntimeSource() {
|
|
return `const __wrnexusHtmlEscapes = { "&": "&", "<": "<", ">": ">", "\\"": """, "'": "'" };
|
|
function __wrnexusEscapeHtml(value: unknown): string {
|
|
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
|
}
|
|
|
|
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any): unknown {
|
|
const adapters = {
|
|
cookies: ctx.cookies,
|
|
session: ctx.session,
|
|
localStorage: ctx.localStorage,
|
|
};
|
|
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
|
|
}
|
|
|
|
function __wrnexusPropAttr(
|
|
value: unknown,
|
|
): string {
|
|
const serialized =
|
|
value !== null &&
|
|
typeof value === "object"
|
|
? JSON.stringify(value)
|
|
: String(value == null ? "" : value);
|
|
|
|
return serialized.replace(
|
|
/[&<>"]/g,
|
|
(character) =>
|
|
character === "&"
|
|
? "&"
|
|
: character === "<"
|
|
? "<"
|
|
: character === ">"
|
|
? ">"
|
|
: """,
|
|
);
|
|
}
|
|
|
|
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
|
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
|
return await ctx.__wrnexusCallApi(path, method);
|
|
}
|
|
|
|
const url = new URL(path, ctx.req.url);
|
|
const res = await fetch(new Request(url, { method, headers: ctx.req.headers }));
|
|
if (!res.ok) {
|
|
throw new Error(".wrn data API request failed with status " + res.status);
|
|
}
|
|
|
|
const type = res.headers.get("content-type") || "";
|
|
return type.includes("application/json") ? await res.json() : await res.text();
|
|
}
|
|
|
|
async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<string> {
|
|
for (const binding of __wrnexusSsrBindings) {
|
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
|
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
|
html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
|
|
}
|
|
return html;
|
|
}`;
|
|
}
|
|
function generate(ast) {
|
|
if (ast.kind === "component" || ast.kind === "layout") {
|
|
return generateComponent(ast);
|
|
}
|
|
const out = [];
|
|
const ssrBindings = [];
|
|
const csrBindings = [];
|
|
const helpers = ast.functions.map((body2) => body2.trim()).filter(Boolean).join(`
|
|
|
|
`);
|
|
const apiBindings = apiBindingMap(ast, helpers);
|
|
if (helpers) {
|
|
out.push(`// --- .wrn functions ---
|
|
${helpers}`);
|
|
}
|
|
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)};`);
|
|
const reactive = ast.states.length > 0 ? { stateNames: new Set(ast.states.map((s) => s.name)), scope: evalStateSeeds(ast.states) } : null;
|
|
const loops = [];
|
|
let html = ast.view.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
|
const styles = ast.styles.map((body2) => body2.trim()).filter(Boolean);
|
|
const needsClientRuntime = ast.states.length > 0 || hasClientBehavior(ast.view);
|
|
if (needsClientRuntime) {
|
|
const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
|
|
html = `<div data-scope="${scopePlaceholder}">${html}</div>`;
|
|
}
|
|
if (styles.length > 0) {
|
|
const css = styles.map(styleEscape).join(`
|
|
`);
|
|
html = `<style data-wrnexus-style="${attrEscape(ast.name)}">
|
|
${css}
|
|
</style>${html}`;
|
|
}
|
|
if (csrBindings.length > 0) {
|
|
out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
|
|
}
|
|
let body = templateEscape(html);
|
|
const dynamicStateScope = ast.states.map((state) => `${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`).join(", ");
|
|
loops.forEach((code, idx) => {
|
|
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
|
});
|
|
const loopConsts = [];
|
|
if (loops.length > 0) {
|
|
const lists = collectControlExprs(ast.view);
|
|
for (const [name, binding] of apiBindings) {
|
|
if (binding.mode !== "ssr")
|
|
continue;
|
|
if (!lists.some((expr) => new RegExp(`\\b${name}\\b`).test(expr)))
|
|
continue;
|
|
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
|
}
|
|
}
|
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || ast.states.some((state) => /\bctx\b/.test(state.expr));
|
|
if (needsSsrRuntime) {
|
|
out.push(ssrRuntimeSource());
|
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
|
const decls = loopConsts.length > 0 ? loopConsts.join(`
|
|
`) + `
|
|
` : "";
|
|
out.push(`export default async function ${ast.name}(ctx: any) {
|
|
${decls}
|
|
const __state = { ${dynamicStateScope} };
|
|
|
|
const __scopeValue = Object.entries(__state)
|
|
.map(([key, value]) => {
|
|
const encoded =
|
|
typeof value === "number" || typeof value === "boolean"
|
|
? String(value)
|
|
: JSON.stringify(value == null ? "" : String(value));
|
|
|
|
return key + ": " + encoded;
|
|
})
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
const html = \`${body}\`.replace(
|
|
"__WRNEXUS_DYNAMIC_SCOPE__",
|
|
__scopeValue,
|
|
);
|
|
|
|
return await __wrnexusRenderSsrBindings(html, ctx);
|
|
}`);
|
|
} else {
|
|
out.push(`export default function ${ast.name}(ctx: any) {
|
|
const __state = { ${dynamicStateScope} };
|
|
|
|
const __scopeValue = Object.entries(__state)
|
|
.map(([key, value]) => {
|
|
const encoded =
|
|
typeof value === "number" || typeof value === "boolean"
|
|
? String(value)
|
|
: JSON.stringify(value == null ? "" : String(value));
|
|
|
|
return key + ": " + encoded;
|
|
})
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
return \`${body}\`.replace(
|
|
"__WRNEXUS_DYNAMIC_SCOPE__",
|
|
__scopeValue,
|
|
);
|
|
}`);
|
|
}
|
|
if (ast.apis.length > 0) {
|
|
ast.apis.forEach((api, index) => {
|
|
const name = `__wrnexusApi_${api.method}_${index}`;
|
|
out.push(`// ${api.method} ${apiRoutePath(api.path)}
|
|
const ${name} = async (ctx: any) => {${api.body}};`);
|
|
});
|
|
const entries = ast.apis.map((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
|
out.push(`export const __wrnexusApi = {
|
|
${entries.join(`
|
|
`)}
|
|
};`);
|
|
const exported = new Set;
|
|
ast.apis.forEach((api, index) => {
|
|
if (exported.has(api.method))
|
|
return;
|
|
exported.add(api.method);
|
|
out.push(`export const ${api.method} = __wrnexusApi_${api.method}_${index};`);
|
|
});
|
|
}
|
|
if (ast.realtimes.length > 0) {
|
|
const handlers = ast.realtimes.flatMap((rt) => rt.handlers.map((h) => {
|
|
const params = ["ws", ...h.args].join(", ");
|
|
return ` ${h.event}(${params}: any) {${h.body}},`;
|
|
}));
|
|
out.push(`export const websocket = {
|
|
${handlers.join(`
|
|
`)}
|
|
};`);
|
|
}
|
|
return out.join(`
|
|
|
|
`) + `
|
|
`;
|
|
}
|
|
function parseForExpr(value) {
|
|
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec(value);
|
|
if (!m)
|
|
return null;
|
|
return { item: m[1], index: m[2], list: m[3] };
|
|
}
|
|
function loopVarsOf(node) {
|
|
if (node.type !== "element")
|
|
return [];
|
|
const attr = node.attrs.find((a) => !a.event && a.name === "data-for");
|
|
if (!attr)
|
|
return [];
|
|
const parsed = parseForExpr(attr.value);
|
|
return parsed ? [parsed.item, ...parsed.index ? [parsed.index] : []] : [];
|
|
}
|
|
var JS_RESERVED = new Set([
|
|
"class",
|
|
"for",
|
|
"default",
|
|
"function",
|
|
"return",
|
|
"if",
|
|
"else",
|
|
"new",
|
|
"delete",
|
|
"typeof",
|
|
"in",
|
|
"instanceof",
|
|
"void",
|
|
"do",
|
|
"while",
|
|
"switch",
|
|
"case",
|
|
"break",
|
|
"continue",
|
|
"this",
|
|
"super",
|
|
"import",
|
|
"export",
|
|
"extends",
|
|
"var",
|
|
"let",
|
|
"const",
|
|
"null",
|
|
"true",
|
|
"false",
|
|
"try",
|
|
"catch",
|
|
"finally",
|
|
"throw",
|
|
"yield",
|
|
"await",
|
|
"enum",
|
|
"with",
|
|
"debugger"
|
|
]);
|
|
function safeRef(name) {
|
|
return JS_RESERVED.has(name) ? `__p_${name}` : name;
|
|
}
|
|
function escLit(s) {
|
|
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
}
|
|
function componentBehavior(ast) {
|
|
const functions = ast.functions.map((body) => body.trim()).filter(Boolean).join(`
|
|
|
|
`);
|
|
const lifecycle = {
|
|
...ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {},
|
|
...ast.lifecycle.update?.trim() ? { update: ast.lifecycle.update.trim() } : {},
|
|
...ast.lifecycle.unmount?.trim() ? { unmount: ast.lifecycle.unmount.trim() } : {}
|
|
};
|
|
const watches = ast.watches.map((watch) => ({
|
|
state: watch.state,
|
|
body: watch.body.trim()
|
|
}));
|
|
if (!functions && Object.keys(lifecycle).length === 0 && watches.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
functions,
|
|
lifecycle,
|
|
watches
|
|
};
|
|
}
|
|
function behaviorAttribute(behavior) {
|
|
if (!behavior) {
|
|
return "";
|
|
}
|
|
const encoded = import_node_buffer.Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
|
|
return ` data-wrn-behavior="${encoded}"`;
|
|
}
|
|
var INTERP_RE = /\{([^{}]+)\}/g;
|
|
function exprRefsState(expr, stateNames) {
|
|
for (const name of stateNames) {
|
|
if (new RegExp(`\\b${name}\\b`).test(expr))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
function viewHasEvents(nodes) {
|
|
return nodes.some((node) => {
|
|
if (node.type === "text")
|
|
return false;
|
|
if (node.type === "each") {
|
|
return viewHasEvents(node.body) || viewHasEvents(node.empty);
|
|
}
|
|
if (node.type === "if") {
|
|
return node.branches.some((branch) => viewHasEvents(branch.body));
|
|
}
|
|
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
|
|
});
|
|
}
|
|
function compileText(raw, ctx) {
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
INTERP_RE.lastIndex = 0;
|
|
while (m = INTERP_RE.exec(raw)) {
|
|
out += escLit(raw.slice(last, m.index));
|
|
const expr = m[1].trim();
|
|
if (expr.startsWith("t:")) {
|
|
out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
|
|
} else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
|
out += escLit(`{${expr}}`);
|
|
} else if (exprRefsState(expr, ctx.stateNames)) {
|
|
out += escLit(`<span data-text="${attrEscape(expr)}">`) + `\${__wireHtml(${ctx.resolveExpr(expr)})}` + escLit(`</span>`);
|
|
} else if (expr === "content") {
|
|
out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
|
|
} else {
|
|
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(raw.slice(last));
|
|
}
|
|
function compileAttrValue(raw, ctx) {
|
|
if (!raw.includes("{"))
|
|
return escLit(attrEscape(raw));
|
|
let out = "";
|
|
let last = 0;
|
|
let m;
|
|
INTERP_RE.lastIndex = 0;
|
|
while (m = INTERP_RE.exec(raw)) {
|
|
out += escLit(attrEscape(raw.slice(last, m.index)));
|
|
const expr = m[1].trim();
|
|
if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
|
out += escLit(`{${expr}}`);
|
|
} else {
|
|
out += `\${__wireAttr(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(attrEscape(raw.slice(last)));
|
|
}
|
|
function renderComponentIfNode(node, ctx) {
|
|
let expression = "``";
|
|
for (let index = node.branches.length - 1;index >= 0; index--) {
|
|
const branch = node.branches[index];
|
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
|
const bodyExpression = "`" + body + "`";
|
|
expression = branch.cond === null ? bodyExpression : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
|
}
|
|
return "${" + expression + "}";
|
|
}
|
|
function renderComponentEachNode(node, ctx) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const list = ctx.resolveExpr(node.list);
|
|
const childCtx = {
|
|
...ctx,
|
|
serverLocals: new Set([...ctx.serverLocals ?? [], item, index])
|
|
};
|
|
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
|
|
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
|
|
}
|
|
function renderComponentNode(node, ctx) {
|
|
if (node.type === "text")
|
|
return compileText(node.value, ctx);
|
|
if (node.type === "each") {
|
|
return renderComponentEachNode(node, ctx);
|
|
}
|
|
if (node.type === "if") {
|
|
return renderComponentIfNode(node, ctx);
|
|
}
|
|
if (isComponentTag(node.tag)) {
|
|
return renderNestedComponentInvocation(node, ctx);
|
|
}
|
|
let bindIndex = 0;
|
|
const staticClasses = [];
|
|
const conditionalClasses = [];
|
|
for (const attr of node.attrs) {
|
|
if (!attr.event && attr.name === "class") {
|
|
staticClasses.push(attr.value);
|
|
}
|
|
if (!attr.event && attr.name.startsWith("class:")) {
|
|
conditionalClasses.push({
|
|
className: attr.name.slice("class:".length),
|
|
expression: attr.value
|
|
});
|
|
}
|
|
}
|
|
const attrs = node.attrs.filter((a) => a.name !== "class" && !a.name.startsWith("class:")).map((a) => {
|
|
if (a.event) {
|
|
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
|
|
}
|
|
if (a.boolean) {
|
|
return ` ${a.name}`;
|
|
}
|
|
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`;
|
|
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) {
|
|
return rendered;
|
|
}
|
|
const marker = attrEscape(JSON.stringify([a.name, a.value]));
|
|
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => {
|
|
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
|
|
}).join("");
|
|
const staticClassValue = staticClasses.join(" ");
|
|
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
|
|
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
|
|
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
|
|
const classBindings = conditionalClasses.filter(({ expression }) => {
|
|
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
|
|
}).map(({ className, expression }, index) => {
|
|
const marker = attrEscape(JSON.stringify([className, expression]));
|
|
return ` data-wrn-class-${index}="${escLit(marker)}"`;
|
|
}).join("");
|
|
const loops = loopVarsOf(node);
|
|
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
|
|
const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
|
|
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return `<${node.tag}${allAttrs}>`;
|
|
}
|
|
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
|
|
}
|
|
function generateComponent(ast) {
|
|
const out = [];
|
|
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ? [
|
|
{
|
|
name: "content",
|
|
default: '""'
|
|
},
|
|
...ast.props
|
|
] : ast.props;
|
|
const stateNames = new Set(ast.states.map((s) => s.name));
|
|
const nameRefs = new Map;
|
|
for (const p of effectiveProps) {
|
|
nameRefs.set(p.name, safeRef(p.name));
|
|
}
|
|
for (const s of ast.states)
|
|
nameRefs.set(s.name, safeRef(s.name));
|
|
const resolveExpr = (expr) => {
|
|
let result = expr;
|
|
for (const [name, ref] of nameRefs) {
|
|
if (name !== ref)
|
|
result = result.replace(new RegExp(`\\b${name}\\b`, "g"), ref);
|
|
}
|
|
return result;
|
|
};
|
|
const ctx = { stateNames, resolveExpr };
|
|
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
|
|
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
|
|
const styleTag = styles.length > 0 ? escLit(`<style data-wrnexus-style="${attrEscape(ast.name)}">
|
|
${styles.map(styleEscape).join(`
|
|
`)}
|
|
</style>`) : "";
|
|
const behavior = componentBehavior(ast);
|
|
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view) || behavior !== null;
|
|
const scopeKeys = [
|
|
...effectiveProps.map((prop) => prop.name),
|
|
...ast.states.map((state) => state.name)
|
|
];
|
|
const behaviorAttr = behaviorAttribute(behavior);
|
|
const decls = [];
|
|
for (const prop of effectiveProps) {
|
|
decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`);
|
|
}
|
|
for (const state of ast.states) {
|
|
decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
|
|
}
|
|
const returnExpr = needsScope ? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`" : "`" + styleTag + viewCode + "`";
|
|
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });
|
|
` : needsScope ? ` const __scope = "";
|
|
` : "";
|
|
if (ast.kind === "layout") {
|
|
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
|
|
} else {
|
|
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
|
}
|
|
if (behavior) {
|
|
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
|
|
}
|
|
out.push(`function __coerce(v: any, def: any): any {
|
|
if (v === undefined || v === null) {
|
|
return def;
|
|
}
|
|
|
|
if (typeof def === "number") {
|
|
return Number(v);
|
|
}
|
|
|
|
if (typeof def === "boolean") {
|
|
return v === true || v === "" || v === "true";
|
|
}
|
|
|
|
if (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 {
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
if (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 {
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
return String(v);
|
|
}
|
|
|
|
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 __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);
|
|
}`);
|
|
if (needsScope) {
|
|
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string {
|
|
const lit = (v: any) =>
|
|
typeof v === "number" || typeof v === "boolean"
|
|
? String(v)
|
|
: "'" + String(v).replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "\\\\'").replace(/\\n/g, "\\\\n") + "'";
|
|
return Object.keys(obj)
|
|
.map((k) => k + ": " + lit(obj[k]))
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}`);
|
|
}
|
|
out.push(`export function render(props: Record<string, any> = {}): string {
|
|
` + ` const __p = props || {};
|
|
` + (decls.length > 0 ? decls.join(`
|
|
`) + `
|
|
` : "") + scopeLine + ` return ${returnExpr};
|
|
` + `}`);
|
|
return out.join(`
|
|
|
|
`) + `
|
|
`;
|
|
}
|
|
function wholeAttributeExpression(value) {
|
|
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
|
|
return match?.[1]?.trim() || null;
|
|
}
|
|
function renderPageComponentAttr(attr, dynamicExpressions) {
|
|
if (attr.event) {
|
|
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const expression = wholeAttributeExpression(attr.value);
|
|
if (!expression) {
|
|
return ` ${attr.name}="${attrEscape(attr.value)}"`;
|
|
}
|
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
|
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
|
return ` ${attr.name}="${marker}"`;
|
|
}
|
|
|
|
// ../../packages/compiler/src/native-codegen.ts
|
|
class NativeCompileError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "NativeCompileError";
|
|
}
|
|
}
|
|
var tagMap = {
|
|
div: "View",
|
|
main: "View",
|
|
section: "View",
|
|
article: "View",
|
|
nav: "View",
|
|
header: "View",
|
|
footer: "View",
|
|
aside: "View",
|
|
form: "View",
|
|
ul: "View",
|
|
ol: "View",
|
|
li: "View",
|
|
p: "Text",
|
|
span: "Text",
|
|
strong: "Text",
|
|
em: "Text",
|
|
small: "Text",
|
|
label: "Text",
|
|
h1: "Text",
|
|
h2: "Text",
|
|
h3: "Text",
|
|
h4: "Text",
|
|
h5: "Text",
|
|
h6: "Text",
|
|
button: "Pressable",
|
|
a: "Pressable",
|
|
input: "TextInput",
|
|
textarea: "TextInput",
|
|
img: "Image",
|
|
view: "View",
|
|
text: "Text",
|
|
pressable: "Pressable",
|
|
textinput: "TextInput",
|
|
image: "Image",
|
|
scrollview: "ScrollView",
|
|
safeareaview: "SafeAreaView",
|
|
flatlist: "FlatList",
|
|
activityindicator: "ActivityIndicator"
|
|
};
|
|
var attrMap = {
|
|
class: "style",
|
|
className: "style",
|
|
src: "source",
|
|
alt: "accessibilityLabel",
|
|
placeholder: "placeholder",
|
|
disabled: "disabled",
|
|
value: "value",
|
|
href: "__href",
|
|
"aria-label": "accessibilityLabel"
|
|
};
|
|
function expression(value) {
|
|
const exact = /^\{([\s\S]+)\}$/.exec(value.trim());
|
|
return exact?.[1]?.trim() ?? null;
|
|
}
|
|
function textJsx(value) {
|
|
const pieces = [];
|
|
let last = 0;
|
|
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
if (match.index > last)
|
|
pieces.push(value.slice(last, match.index));
|
|
const expr = match[1].trim();
|
|
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
|
last = match.index + match[0].length;
|
|
}
|
|
pieces.push(value.slice(last));
|
|
return pieces.join("").replace(/([<>])/g, (char) => char === "<" ? "<" : ">");
|
|
}
|
|
function eventBody(value, states) {
|
|
let body = expression(value) ?? value;
|
|
for (const state of states) {
|
|
const cap = state[0].toUpperCase() + state.slice(1);
|
|
body = body.replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`).replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`).replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`);
|
|
}
|
|
return `() => { ${body} }`;
|
|
}
|
|
function renderAttrs2(attrs, states) {
|
|
return attrs.map((attr) => {
|
|
if (attr.event) {
|
|
if (attr.name.startsWith("browser-"))
|
|
return "";
|
|
const eventName = attr.name.startsWith("mobile-") ? attr.name.slice(7) : attr.name;
|
|
const event = eventName === "click" || eventName === "press" ? "onPress" : eventName === "input" || eventName === "change" ? "onChangeText" : `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
|
|
return ` ${event}={${eventBody(attr.value, states)}}`;
|
|
}
|
|
if (attr.name === "data-native-browser" || attr.name.startsWith("data-native-on-browser-"))
|
|
return "";
|
|
if (attr.name === "data-native-options" || attr.name === "data-native-only" || attr.name === "data-native-requires" || attr.name === "data-native-unsupported")
|
|
return "";
|
|
if (attr.name === "data-native-mobile") {
|
|
throw new NativeCompileError(`Declarative native capability "${attr.value}" currently targets browser/Capacitor pages. In Expo output, call the installed Expo package from an @mobile-event handler.`);
|
|
}
|
|
const name = attrMap[attr.name] ?? attr.name;
|
|
if (name === "__href")
|
|
return ` onPress={() => router.push(${JSON.stringify(attr.value)})}`;
|
|
if (name === "source") {
|
|
const expr2 = expression(attr.value);
|
|
return ` source={${expr2 ? `{ uri: ${expr2} }` : `{ uri: ${JSON.stringify(attr.value)} }`}}`;
|
|
}
|
|
if (name === "style" && attr.name !== "style") {
|
|
return ` style={[${attr.value.split(/\s+/).filter(Boolean).map((value) => `styles[${JSON.stringify(value)}]`).join(", ")} ]}`;
|
|
}
|
|
if (name === "style") {
|
|
const inlineExpression = expression(attr.value);
|
|
if (inlineExpression)
|
|
return ` style={${inlineExpression}}`;
|
|
throw new NativeCompileError('Inline CSS strings are not portable to native; use class="name" and a page style block');
|
|
}
|
|
if (attr.boolean)
|
|
return ` ${name}`;
|
|
const expr = expression(attr.value);
|
|
return expr ? ` ${name}={${expr}}` : ` ${name}=${JSON.stringify(attr.value)}`;
|
|
}).join("");
|
|
}
|
|
function renderNode2(node, states, key) {
|
|
if (node.type === "text")
|
|
return textJsx(node.value);
|
|
if (node.type === "each") {
|
|
const params = node.index ? `${node.item}, ${node.index}` : `${node.item}, __index`;
|
|
const body = node.body.map((child, index) => renderNode2(child, states, index === 0 ? node.index ?? "__index" : undefined)).join("");
|
|
const empty = node.empty.map((child) => renderNode2(child, states)).join("");
|
|
return `{(${node.list})?.length ? (${node.list}).map((${params}) => <>${body}</>) : <>${empty}</>}`;
|
|
}
|
|
if (node.type === "if") {
|
|
const result = node.branches.reduceRight((fallback, branch) => branch.cond === null ? `<>${branch.body.map((child) => renderNode2(child, states)).join("")}</>` : `(${branch.cond}) ? <>${branch.body.map((child) => renderNode2(child, states)).join("")}</> : ${fallback}`, "null");
|
|
return `{${result}}`;
|
|
}
|
|
const nativeOnly = node.attrs.find((attr) => !attr.event && attr.name === "data-native-only")?.value;
|
|
if (nativeOnly === "browser" || nativeOnly === "web")
|
|
return "";
|
|
const nativeTag = tagMap[node.tag.toLowerCase()] ?? (/^[A-Z]/.test(node.tag) ? node.tag : undefined);
|
|
if (!nativeTag)
|
|
throw new NativeCompileError(`HTML element <${node.tag}> has no native equivalent`);
|
|
const attrs = renderAttrs2(node.attrs, states) + (key ? ` key={${key}}` : "");
|
|
if (nativeTag === "TextInput" || nativeTag === "Image" || nativeTag === "ActivityIndicator")
|
|
return `<${nativeTag}${attrs} />`;
|
|
const children = node.children.map((child) => {
|
|
if (child.type !== "text")
|
|
return renderNode2(child, states);
|
|
if (!child.value.trim())
|
|
return "";
|
|
const text = textJsx(child.value);
|
|
return nativeTag === "Text" ? text : `<Text>${text}</Text>`;
|
|
}).join("");
|
|
return `<${nativeTag}${attrs}>${children}</${nativeTag}>`;
|
|
}
|
|
function nativeStyles(blocks) {
|
|
const entries = [];
|
|
for (const block of blocks) {
|
|
for (const match of block.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^}]*)\}/g)) {
|
|
const props = [];
|
|
for (const declaration of match[2].split(";")) {
|
|
const colon = declaration.indexOf(":");
|
|
if (colon < 0)
|
|
continue;
|
|
const name = declaration.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
let value = declaration.slice(colon + 1).trim();
|
|
if (/^-?\d+(?:\.\d+)?px$/.test(value))
|
|
value = Number(value.slice(0, -2));
|
|
props.push(`${JSON.stringify(name)}: ${typeof value === "number" ? value : JSON.stringify(value)}`);
|
|
}
|
|
entries.push(`${JSON.stringify(match[1])}: { ${props.join(", ")} }`);
|
|
}
|
|
}
|
|
return `const styles = StyleSheet.create({ ${entries.join(`,
|
|
`)} });`;
|
|
}
|
|
function generateNative(ast) {
|
|
if (ast.kind !== "page")
|
|
throw new NativeCompileError("Native route compilation currently accepts page files only");
|
|
if (ast.dataApis.length)
|
|
throw new NativeCompileError("Data API blocks are not yet portable to native screens; fetch through the generated native backend helper");
|
|
const states = new Set(ast.states.map((state) => state.name));
|
|
const hooks = ast.states.map((state) => {
|
|
const cap = state.name[0].toUpperCase() + state.name.slice(1);
|
|
return ` const [${state.name}, set${cap}] = useState(${state.expr});`;
|
|
}).join(`
|
|
`);
|
|
const body = ast.view.map((node) => renderNode2(node, states)).join("");
|
|
return `// generated from .wrn for Expo/React Native
|
|
import React, { useState } from "react";
|
|
import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
|
|
export default function ${ast.name}() {
|
|
const router = useRouter();
|
|
${hooks}
|
|
return <>${body}</>;
|
|
}
|
|
|
|
${nativeStyles(ast.styles)}
|
|
`;
|
|
}
|
|
|
|
// ../../packages/compiler/src/index.ts
|
|
function compileNativeWireFile(source) {
|
|
return generateNative(parse(source));
|
|
}
|
|
function compileWireFile(source) {
|
|
const ast = parse(source);
|
|
return `// compiled from .wrn
|
|
${generate(ast)}`;
|
|
}
|
|
function compile(source) {
|
|
const diagnostics = [];
|
|
try {
|
|
const ast = parse(source);
|
|
return { code: `// compiled from .wrn
|
|
${generate(ast)}`, ast, diagnostics };
|
|
} catch (err) {
|
|
if (err instanceof ParseError)
|
|
diagnostics.push(err.message);
|
|
throw err;
|
|
}
|
|
}
|