Files
WRNexusJS/editors/vscode/src/compiler.cjs
T
Clintchiz 7658c5d499
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
fix(editor): type nested attribute expressions
2026-08-25 15:16:38 +05:30

8722 lines
355 KiB
JavaScript

"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
// WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
// Generated with TypeScript: 6.0.3
const __nodeRequire = require;
const __path = __nodeRequire("node:path");
const __modules = {
"packages/compiler/src/analysis.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.optimizeAst = optimizeAst;
exports.analyzeOptimizations = analyzeOptimizations;
exports.routeNeedsIslands = routeNeedsIslands;
exports.analyzeRuntimeRequirements = analyzeRuntimeRequirements;
function identifiers(value) {
return new Set(value.match(/[A-Za-z_$][\w$]*/g) ?? []);
}
function literalBoolean(expression) {
if (expression === null)
return true;
const value = expression.trim();
if (value === "true")
return true;
if (value === "false" ||
value === "null" ||
value === "undefined" ||
value === "0" ||
value === "''" ||
value === '""')
return false;
if (/^-?(?:[1-9]\d*|0?\.\d+)$/.test(value) || /^(['"]).+\1$/.test(value))
return true;
return undefined;
}
function optimizeNodes(nodes, report) {
const output = [];
for (const node of nodes) {
if (node.type === "element")
output.push({
...node,
attrs: node.attrs.map((attribute) => ({ ...attribute })),
children: optimizeNodes(node.children, report),
});
else if (node.type === "each")
output.push({
...node,
body: optimizeNodes(node.body, report),
empty: optimizeNodes(node.empty, report),
});
else if (node.type === "if") {
let selected;
let dynamic = false;
for (const branch of node.branches) {
const value = literalBoolean(branch.cond);
if (value === undefined) {
dynamic = true;
break;
}
report.eliminated++;
if (value) {
selected = branch.body;
break;
}
}
if (dynamic)
output.push({
...node,
branches: node.branches.map((branch) => ({
...branch,
body: optimizeNodes(branch.body, report),
})),
});
else if (selected)
output.push(...optimizeNodes(selected, report));
}
else
output.push({ ...node });
}
return output;
}
/** Safe compile-time folding for literal conditional branches. */
function optimizeAst(ast) {
const report = { eliminated: 0 };
return {
ast: { ...ast, view: optimizeNodes(ast.view, report) },
eliminatedBranches: report.eliminated,
};
}
function analyzeOptimizations(ast) {
const used = new Set();
let staticNodes = 0;
let reactiveRegions = 0;
const componentNames = new Set();
const staticClasses = new Set();
const visit = (nodes) => {
for (const node of nodes) {
if (node.type === "text") {
const refs = identifiers(node.value);
refs.forEach((name) => used.add(name));
if (node.value.includes("{"))
reactiveRegions++;
else
staticNodes++;
}
else if (node.type === "element") {
if (/^[A-Z]/.test(node.tag))
componentNames.add(node.tag);
let reactive = false;
for (const attribute of node.attrs) {
identifiers(attribute.value).forEach((name) => used.add(name));
reactive ||= attribute.event || attribute.value.includes("{");
if (attribute.name === "class" && !attribute.value.includes("{"))
for (const name of attribute.value.split(/\s+/))
if (name)
staticClasses.add(name);
}
if (reactive)
reactiveRegions++;
else
staticNodes++;
visit(node.children);
}
else if (node.type === "each") {
identifiers(`${node.list} ${node.key ?? ""}`).forEach((name) => used.add(name));
reactiveRegions++;
visit(node.body);
visit(node.empty);
}
else {
for (const branch of node.branches) {
identifiers(branch.cond ?? "").forEach((name) => used.add(name));
visit(branch.body);
}
reactiveRegions++;
}
}
};
visit(ast.view);
const handlerReferences = new Set(used);
const executable = [
...ast.runtimeFunctions.map((fn) => fn.body),
...ast.functions,
...ast.effects.map((effect) => effect.body),
...ast.watches.map((watch) => watch.body),
...ast.actions.map((action) => action.body),
].join("\n");
identifiers(executable).forEach((name) => used.add(name));
const localCss = new Set(ast.styles.flatMap((style) => [...style.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map((match) => match[1])));
const optimized = optimizeAst(ast);
const assignmentCounts = ast.runtimeFunctions.map((fn) => ast.states.filter((state) => new RegExp(`\\b${state.name}\\s*(?:[+*/-]?=|\\+\\+|--)`).test(fn.body)).length);
return {
staticNodes,
reactiveRegions,
eliminatedBranches: optimized.eliminatedBranches,
unusedState: ast.states.filter((state) => !used.has(state.name)).map((state) => state.name),
unusedHandlers: ast.runtimeFunctions
.filter((fn) => fn.runtime !== "server" && !handlerReferences.has(fn.name))
.map((fn) => fn.name),
constantProps: ast.props
.filter((prop) => /^(?:-?\d+(?:\.\d+)?|true|false|null|(['"]).*\1)$/.test(prop.default.trim()))
.map((prop) => prop.name),
unusedLocalCssClasses: [...localCss].filter((name) => !staticClasses.has(name)).sort(),
batchableStateUpdates: assignmentCounts
.filter((count) => count > 1)
.reduce((sum, count) => sum + count - 1, 0),
memoizableComponents: [...componentNames].sort(),
preloadDependencies: ast.structuredImports
.filter((entry) => !entry.typeOnly && !entry.source.startsWith("node:"))
.map((entry) => entry.source),
serverOnlyModules: ast.structuredImports
.filter((entry) => entry.source.startsWith("node:") || ast.runtime === "server")
.map((entry) => entry.source),
};
}
function hasEvent(nodes) {
for (const node of nodes) {
if (node.type === "element") {
if (node.attrs.some((attribute) => attribute.event))
return true;
if (hasEvent(node.children))
return true;
}
else if (node.type === "each") {
if (hasEvent(node.body) || hasEvent(node.empty))
return true;
}
else if (node.type === "if") {
if (node.branches.some((branch) => hasEvent(branch.body)))
return true;
}
}
return false;
}
/**
* A route containing a React island ships JavaScript and can no longer be
* classified as zero-JS static, so island presence must reach the classifier.
*/
function routeNeedsIslands(imports) {
return imports.some((entry) => entry.kind === "island");
}
function analyzeRuntimeRequirements(ast, options = {}) {
const hasIslands = options.hasIslands ?? false;
const reasons = [];
if (hasIslands)
reasons.push("react island");
const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server");
const clientState = ast.states.some((state) => state.runtime !== "server");
const interactive = clientFunctions ||
clientState ||
ast.effects.length > 0 ||
ast.watches.length > 0 ||
hasEvent(ast.view);
if (interactive)
reasons.push("client interactivity");
const requestData = ast.loads.length > 0 ||
ast.actions.length > 0 ||
ast.dataApis.length > 0 ||
ast.apis.length > 0 ||
ast.realtimes.length > 0 ||
ast.runtimeFunctions.some((fn) => fn.runtime === "server") ||
ast.states.some((state) => state.runtime === "server");
if (requestData)
reasons.push("server/request data");
const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? "");
if (authenticated)
reasons.push("authentication required");
const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? "");
if (streaming)
reasons.push("streaming enabled");
let kind;
if (streaming)
kind = "streaming-ssr";
else if (authenticated)
kind = "authenticated-ssr";
else if (requestData && interactive)
kind = "dynamic";
else if (requestData)
kind = "request-ssr";
else if (interactive)
kind = "static-interactive";
else
kind = "static";
if (ast.renderMode === "static") {
kind = "static";
reasons.push("explicit static rendering");
}
else if (ast.renderMode === "server") {
kind = requestData ? "request-ssr" : "static";
reasons.push("explicit server rendering");
}
else if (ast.renderMode === "client") {
kind = "static-interactive";
reasons.push("explicit client rendering");
}
else if (ast.renderMode === "partial-static") {
kind = "streaming-ssr";
reasons.push("partial-static shell with streamed dynamic regions");
}
// An island ships JavaScript, so a would-be zero-JS static route must be
// reported as static-interactive. Explicit render modes still win above.
if (hasIslands && kind === "static")
kind = "static-interactive";
const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server";
const serverDisabled = ast.renderMode === "client";
return {
kind,
canPrerender: kind === "static" || kind === "static-interactive",
needsIslandRuntime: hasIslands,
needsClientRuntime: !clientDisabled &&
(interactive || ast.renderMode === "client") &&
ast.hydrate !== "none" &&
ast.runtime !== "server",
needsServerRuntime: !serverDisabled &&
(requestData ||
authenticated ||
streaming ||
ast.renderMode === "server" ||
["server", "edge", "worker", "service-worker"].includes(ast.runtime ?? "")),
hydrationStrategy: clientDisabled ? null : interactive ? (ast.hydrate ?? "load") : null,
reasons,
optimization: analyzeOptimizations(ast),
cachePolicy: { ...(ast.cache ?? {}) },
requiredPermission: ast.security.permission ?? null,
};
}
},
"packages/compiler/src/browser-transpile.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/**
* Strip TypeScript from a generated browser module.
*
* A client function's body is emitted verbatim, so anything TypeScript-only
* inside one -- an annotated local, an `as` cast, a local interface -- reaches
* the browser module as TypeScript source. Codegen removes the types from the
* function's *signature*, which is what made this easy to miss: the emitted
* module looked transpiled, and only bodies carried types through.
*
* The artifact is written as `.mjs` and read back as plain JavaScript, so the
* failure surfaced as a syntax error pointing at generated code rather than at
* the `.wrn` line responsible.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripBrowserTypes = stripBrowserTypes;
let transpiler = null;
function stripBrowserTypes(code) {
const bun = globalThis.Bun;
if (!bun?.Transpiler) {
throw new Error("WRN-CLIENT-TS: emitting a browser module needs the Bun transpiler to remove TypeScript from client function bodies.");
}
transpiler ??= new bun.Transpiler({ loader: "ts", target: "browser" });
return transpiler.transformSync(code);
}
},
"packages/compiler/src/cache.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = void 0;
exports.compilationKey = compilationKey;
exports.createCompilationCache = createCompilationCache;
const node_crypto_1 = require("node:crypto");
const syntax_1 = require("@wrnexus/syntax");
const codegen_ts_1 = require("./codegen.js");
function compileSource(source, filePath) {
const richDiagnostics = (0, syntax_1.diagnose)(source, { file: filePath, accessibility: true });
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
if (errors.length > 0) {
throw new syntax_1.ParseError(errors.map((diagnostic) => diagnostic.message).join("\n"), errors[0].code);
}
const ast = (0, syntax_1.parse)(source);
return {
code: `// compiled from .wrn\n${(0, codegen_ts_1.generate)(ast)}`,
ast,
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
richDiagnostics,
};
}
function compilationKey(source, file = "<inline .wrn>", salt = "") {
return (0, node_crypto_1.createHash)("sha256")
.update(file)
.update("\0")
.update(salt)
.update("\0")
.update(source)
.digest("hex");
}
function createCompilationCache(options = {}) {
const maxEntries = options.maxEntries ?? 500;
if (!Number.isInteger(maxEntries) || maxEntries < 1)
throw new RangeError("maxEntries must be positive");
const now = options.now ?? Date.now;
const entries = new Map();
let hits = 0;
let misses = 0;
function touch(key, value) {
entries.delete(key);
entries.set(key, value);
while (entries.size > maxEntries)
entries.delete(entries.keys().next().value);
}
return {
compile(source, file = "<inline .wrn>", salt = "") {
const key = compilationKey(source, file, salt);
const existing = entries.get(key);
if (existing) {
hits++;
touch(key, existing);
return existing;
}
misses++;
const result = compileSource(source, file);
const entry = {
...result,
key,
file,
sourceHash: (0, node_crypto_1.createHash)("sha256").update(source).digest("hex"),
createdAt: now(),
};
touch(key, entry);
return entry;
},
get(key) {
const entry = entries.get(key);
if (entry)
touch(key, entry);
return entry;
},
invalidate(file) {
let removed = 0;
for (const [key, entry] of entries) {
if (!file || entry.file === file) {
entries.delete(key);
removed++;
}
}
return removed;
},
clear() {
entries.clear();
},
size: () => entries.size,
stats: () => ({ hits, misses, entries: entries.size }),
};
}
class DependencyGraph {
#dependencies = new Map();
#dependents = new Map();
set(file, dependencies) {
this.remove(file);
const values = new Set(dependencies);
this.#dependencies.set(file, values);
for (const dependency of values) {
const set = this.#dependents.get(dependency) ?? new Set();
set.add(file);
this.#dependents.set(dependency, set);
}
}
remove(file) {
for (const dependency of this.#dependencies.get(file) ?? []) {
const set = this.#dependents.get(dependency);
set?.delete(file);
if (set?.size === 0)
this.#dependents.delete(dependency);
}
this.#dependencies.delete(file);
}
dependencies(file) {
return [...(this.#dependencies.get(file) ?? [])].sort();
}
dependents(file) {
return [...(this.#dependents.get(file) ?? [])].sort();
}
affected(file) {
const found = new Set();
const queue = [file];
while (queue.length) {
const current = queue.shift();
for (const dependent of this.#dependents.get(current) ?? []) {
if (found.has(dependent))
continue;
found.add(dependent);
queue.push(dependent);
}
}
return [...found].sort();
}
}
exports.DependencyGraph = DependencyGraph;
},
"packages/compiler/src/client-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.browserModuleRequired = browserModuleRequired;
exports.generateBrowserModule = generateBrowserModule;
const syntax_1 = require("@wrnexus/syntax");
const RESERVED_BINDINGS = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
const RUNTIME_BINDINGS = new Set([
"context",
"state",
"output",
"server",
"props",
"refs",
"api",
"event",
"payload",
]);
function safeIdentifier(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function identifierReferenced(source, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
}
function viewReferenceSource(nodes) {
return nodes.flatMap((node) => {
if (node.type === "text")
return [node.value];
if (node.type === "each") {
return [
node.list,
node.key ?? "",
...viewReferenceSource(node.body),
...viewReferenceSource(node.empty),
];
}
if (node.type === "if") {
return node.branches.flatMap((branch) => [
branch.cond ?? "",
...viewReferenceSource(branch.body),
]);
}
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
});
}
function browserReferenceSource(ast, functions) {
return [
...functions.flatMap((fn) => [fn.source, fn.body]),
...ast.computed.map((entry) => entry.expr),
...ast.effects,
ast.lifecycle.mount ?? "",
ast.lifecycle.update ?? "",
ast.lifecycle.unmount ?? "",
...viewReferenceSource(ast.view),
].join("\n");
}
function renderSelectedImport(entry, source) {
if (entry.typeOnly)
return null;
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
if (entry.source.endsWith(".wrn") && !isStore)
return null;
const defaultImport = entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
? entry.defaultImport
: undefined;
const namespaceImport = entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
? entry.namespaceImport
: undefined;
const namedImports = entry.namedImports.filter((item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)));
const bindings = [
...(defaultImport ? [defaultImport] : []),
...(namespaceImport ? [namespaceImport] : []),
...namedImports.map((item) => item.local),
].filter(safeIdentifier);
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
const sideEffectOnly = !entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
if (!hasBindings && !sideEffectOnly)
return null;
if (sideEffectOnly)
return { code: entry.raw, bindings: [] };
const clauses = [];
if (defaultImport)
clauses.push(defaultImport);
if (namespaceImport)
clauses.push(`* as ${namespaceImport}`);
if (namedImports.length) {
clauses.push(`{ ${namedImports
.map((item) => item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`)
.join(", ")} }`);
}
return {
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
bindings,
};
}
function selectedBrowserImports(ast, functions) {
const referenceSource = browserReferenceSource(ast, functions);
return ast.structuredImports
.map((entry) => renderSelectedImport(entry, referenceSource))
.filter((entry) => entry !== null);
}
function browserModuleRequired(ast) {
const functions = ast.runtimeFunctions.filter((fn) => ["client", "shared"].includes(fn.runtime));
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
}
function _functionEntry(ast, fn, availableFunctions) {
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
/*
* Names the function body declares for itself.
*
* Props and state are destructured into the SAME scope as the body, so a
* body that declares `var size` when `size` is also a prop produced
* "Identifier 'size' has already been declared" and the entire module
* failed to parse -- taking every function in the component down with it,
* with nothing to point at the one line responsible. Skipping the alias for
* a shadowed name is also what plain JavaScript does: inside that function
* the local wins.
*/
const declaredLocals = new Set();
for (const match of fn.body.matchAll(/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)|\bfunction\s+([A-Za-z_$][\w$]*)/g)) {
const name = match[1] ?? match[2];
if (name)
declaredLocals.add(name);
}
const stateNames = ast.states
.filter((state) => state.runtime !== "server" &&
safeIdentifier(state.name) &&
!RUNTIME_BINDINGS.has(state.name) &&
!parameterNames.has(state.name) &&
!declaredLocals.has(state.name))
.map((state) => state.name);
const stateSet = new Set(stateNames);
const propNames = ast.props
.filter((prop) => safeIdentifier(prop.name) &&
!RUNTIME_BINDINGS.has(prop.name) &&
!parameterNames.has(prop.name) &&
!stateSet.has(prop.name) &&
!declaredLocals.has(prop.name))
.map((prop) => prop.name);
const functionAliases = availableFunctions.filter((name) => safeIdentifier(name) &&
!RUNTIME_BINDINGS.has(name) &&
!parameterNames.has(name) &&
!stateSet.has(name) &&
!propNames.includes(name) &&
!declaredLocals.has(name));
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
const initialStateSnapshot = stateNames.length
? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };`
: "";
const stateAliases = stateNames.length ? `let { ${stateNames.join(", ")} } = context.state;` : "";
const propAliases = propNames.length ? `const { ${propNames.join(", ")} } = context.props;` : "";
const syncStateToContext = stateNames.map((name) => `context.state.${name} = ${name};`).join(" ");
const syncStateFromContext = stateNames
.map((name) => `${name} = context.state.${name};`)
.join(" ");
const peerAliases = !stateNames.length
? functionAliases
.map((name) => `const ${name} = (...__wrnexusPeerArgs) => context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs);`)
.join("\n")
: `const __wrnexusFlush = () => { ${syncStateToContext} };
const __wrnexusRestore = () => { ${syncStateFromContext} };
const __wrnexusPeer = (name, args) => {
__wrnexusFlush();
let result;
try {
result = context.functions[name](...args);
} catch (error) {
__wrnexusRestore();
throw error;
}
if (result && typeof result.then === "function") {
return Promise.resolve(result).finally(__wrnexusRestore);
}
__wrnexusRestore();
return result;
};
${functionAliases.map((name) => `const ${name} = (...__wrnexusPeerArgs) => __wrnexusPeer(${JSON.stringify(name)}, __wrnexusPeerArgs);`).join("\n")}`;
const copyBack = stateNames
.map((name) => `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`)
.join("\n");
const commitBinding = stateNames.length
? `const __wrnexusCommit = () => { ${copyBack} };
${!parameterNames.has("commit") && !declaredLocals.has("commit") && !functionAliases.includes("commit") ? "const commit = __wrnexusCommit;" : ""}
${!parameterNames.has("setTimeout") &&
!declaredLocals.has("setTimeout") &&
!functionAliases.includes("setTimeout")
? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
try { return callback(...args); } finally { __wrnexusCommit(); }
}, delay);`
: ""}`
: "";
const body = (0, syntax_1.eraseFunctionTypes)(fn.body);
const runtimeBindings = [
!parameterNames.has("output") ? "const output = context.output;" : "",
!parameterNames.has("server") ? "const server = context.server;" : "",
!parameterNames.has("props") ? "const props = context.props;" : "",
!parameterNames.has("refs") ? "const refs = context.refs;" : "",
]
.filter(Boolean)
.join("\n ");
return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(context${parameters ? `, ${parameters}` : ""}) {
${initialStateSnapshot}
${stateAliases}
${propAliases}
${commitBinding}
${peerAliases}
${runtimeBindings}
try {
${body}
} finally {
${stateNames.length ? "__wrnexusCommit();" : ""}
}
}`;
}
/**
* Blank out string/template literals and comments in a raw JS body, preserving
* length and newlines, so a scanner walking the result never mistakes text
* inside a string or comment for real code. Reuses the tokenizer's
* comment/string-skipping rules (`skipLiteralOrComment`) instead of
* reimplementing them a second hand-rolled scanner is how apostrophes in
* prose used to swallow braces elsewhere in this codebase.
*/
function maskStringsAndComments(src) {
let out = "";
let i = 0;
let atLineStart = true;
while (i < src.length) {
const c = src[i];
if (c === "\n") {
out += c;
atLineStart = true;
i++;
continue;
}
const skipped = (0, syntax_1.skipLiteralOrComment)(src, i, atLineStart);
if (skipped !== null) {
out += src.slice(i, skipped).replace(/[^\n]/g, " ");
i = skipped;
atLineStart = false;
continue;
}
if (c !== " " && c !== "\t" && c !== "\r")
atLineStart = false;
out += c;
i++;
}
return out;
}
/**
* Block names the page's client functions actually call.
*
* A block's response and error bodies are page code. Emitting one the browser
* never calls would ship a server-only transform to every visitor and grow the
* bundle for nothing.
*/
function clientCalledApiNames(ast) {
const called = new Set();
for (const fn of ast.runtimeFunctions.filter((fn) => ["client", "shared"].includes(fn.runtime))) {
const masked = maskStringsAndComments(fn.body);
for (const match of masked.matchAll(/\bapi\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
called.add(match[1]);
}
}
return called;
}
/**
* Refuse to compile a client/shared function whose body references `api` in
* any form other than `api.<identifier>` — e.g. `api["searchUsers"]()`, or
* passing `api` to a helper. Usage-driven emission (see `clientCalledApiNames`
* above) can only see `api.<identifier>` calls; a dynamic or indirect
* reference is invisible to it, so the referenced block would be silently
* dropped from the browser bundle and the call would fail at runtime with
* "api.<name> is not a function". That failure direction is worse than a
* loud compile error, so it is caught here instead.
*
* Strings and comments are masked out first so `api` appearing in prose or in
* a quoted value never trips this check, and every `api.<identifier>` access
* is stripped before the standalone-word scan so a real, well-formed call
* never does either.
*/
function assertNoDynamicApiAccess(ast) {
// Only pages with a sectioned api block have anything at stake here: those
// blocks are emitted solely because usage detection saw `api.<name>`, so a
// dynamic reference this scan can't see is the one that silently drops a
// block from the bundle. A page with no api blocks at all may still declare
// an ordinary `state api` (see the B5 regression test) where a bare "api"
// identifier is just that state, not a missed block reference.
if (!ast.dataApis.some((block) => block.sections))
return;
for (const fn of ast.runtimeFunctions.filter((fn) => ["client", "shared"].includes(fn.runtime))) {
const masked = maskStringsAndComments(fn.body);
const withoutCalls = masked.replace(/\bapi\s*\.\s*[A-Za-z_$][A-Za-z0-9_$]*/g, (match) => match.replace(/[^\n]/g, " "));
if (/\bapi\b/.test(withoutCalls)) {
throw new Error(`.wrn ${fn.runtime} function "${fn.name}" in page "${ast.name}" references "api" in a form other than "api.<name>(...)". ` +
`API blocks must be called as api.name(...) so the compiler can tell which ones the browser needs to receive; ` +
`dynamic or indirect access (e.g. api["name"](), or passing api to a helper) cannot be detected and would silently drop the block from the browser bundle.`);
}
}
}
/**
* A block is emitted into the browser module when it declares typed sections
* and a client function actually calls it. `hasClientApi` below must use this
* exact predicate so the `api` reserved-binding exclusion and the emitted
* object can never disagree.
*/
function isClientEmittedApiBlock(block, called) {
return Boolean(block.sections) && called.has(block.name);
}
/**
* Client-mode and client-called any-mode api blocks become members of an
* `api` object in client scope.
*
* Only the response and error bodies are emitted; the declared field types are
* type-only and are consumed by the types generator instead. Anything
* TypeScript reaching this module would be a syntax error in the .mjs artifact.
*/
function apiBindings(ast) {
const called = clientCalledApiNames(ast);
const members = ast.dataApis
.filter((block) => isClientEmittedApiBlock(block, called))
.map((block) => {
const sections = block.sections;
const response = (0, syntax_1.eraseFunctionTypes)(sections.response).trim() || "return data;";
const error = (0, syntax_1.eraseFunctionTypes)(sections.error).trim();
const failure = error
? `(error) => { const status = error.status; const message = error.message; const data = error.data; ${error} }`
: `(error) => { throw error; }`;
return ` ${JSON.stringify(block.name)}: async (input) => context.callApi(${JSON.stringify(block.path)}, ${JSON.stringify(block.method)}, input).then((data) => { ${response} }, ${failure})`;
});
return members.length ? `const api = {\n${members.join(",\n")}\n };` : "";
}
function generateBrowserModule(ast) {
assertNoDynamicApiAccess(ast);
const functions = ast.runtimeFunctions.filter((fn) => ["client", "shared"].includes(fn.runtime));
const functionNames = functions.map((fn) => fn.name);
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
const selectedImports = selectedBrowserImports(ast, functions);
const imports = selectedImports.map((entry) => entry.code).join("\n");
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
// `api` is only defined as a client-scope binding when the page actually has
// client-mode api blocks (see apiBindings below). A page that declares
// `state api` without any client api blocks must keep reading/writing that
// state as before, so only exclude the "api" name from destructuring when
// there is a real `api` binding to shadow it.
const hasClientApi = ast.dataApis.some((block) => isClientEmittedApiBlock(block, clientCalledApiNames(ast)));
const localRuntimeBindings = hasClientApi
? RUNTIME_BINDINGS
: new Set([...RUNTIME_BINDINGS].filter((name) => name !== "api"));
const sharedState = state.filter((name) => safeIdentifier(name) && !localRuntimeBindings.has(name));
const sharedProps = ast.props
.map((entry) => entry.name)
.filter((name) => safeIdentifier(name) && !localRuntimeBindings.has(name) && !sharedState.includes(name));
const callableAliases = functionNames.filter((name) => safeIdentifier(name) &&
!RUNTIME_BINDINGS.has(name) &&
!sharedState.includes(name) &&
!sharedProps.includes(name));
const sharedCommit = sharedState.map((name) => `context.state.${name} = ${name};`).join(" ");
const sharedRestore = [
...sharedState.map((name) => `${name} = context.state.${name};`),
...sharedProps.map((name) => `${name} = context.props.${name};`),
].join(" ");
const hasAuthoredCommit = callableAliases.includes("commit");
const hasAuthoredSetTimeout = callableAliases.includes("setTimeout");
const implementations = functions
.map((fn) => {
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(${parameters}) {
try { ${(0, syntax_1.eraseFunctionTypes)(fn.body)} } finally { __wrnexusCommit(); }
}`;
})
.join(",\n");
return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {
${functions
.map((fn) => ` ${JSON.stringify(fn.name)}: (context, ...args) => __wrnexusBindings(context)[${JSON.stringify(fn.name)}](...args)`)
.join(",\n")}
};
export const __wrnexusClientState = ${JSON.stringify(state)};
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
function __wrnexusCreateClientFunctions(context) {
${sharedState.length ? `let { ${sharedState.join(", ")} } = context.state;` : ""}
${sharedProps.length ? `let { ${sharedProps.join(", ")} } = context.props;` : ""}
const output = context.output;
const server = context.server;
const props = context.props;
const refs = context.refs;
${apiBindings(ast)}
const __wrnexusCommit = () => { ${sharedCommit} };
const __wrnexusRestore = () => { ${sharedRestore} };
${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
${!hasAuthoredSetTimeout
? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
try { return callback(...args); } finally { __wrnexusCommit(); }
}, delay);`
: ""}
const implementations = {
${implementations}
};
${callableAliases.map((name) => `const ${name} = (...args) => implementations[${JSON.stringify(name)}](...args);`).join("\n ")}
const functions = {};
for (const name of Object.keys(implementations)) {
functions[name] = (...args) => {
__wrnexusRestore();
return implementations[name](...args);
};
}
return functions;
}
function __wrnexusBindings(context) {
return context.__wrnexusBoundFunctions ||
(context.__wrnexusBoundFunctions = __wrnexusCreateClientFunctions(context));
}
export function bindClientScope(context) {
return __wrnexusBindings(context);
}
`;
}
},
"packages/compiler/src/codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/**
* Code generation: lower a `.wrn` AST to TypeScript that targets the framework's
* existing primitives.
*
* state -> a `data-scope` declaration consumed by the runtime
* view -> an HTML string returned by a page component
* @event="..." -> data-on-<event>="..."
* "...{expr}..." -> text kept verbatim ({expr} is mustache for runtime)
* api="<name>" -> SSR/client data binding declared in a mode block
* ssrGet/ssrText -> legacy server-side API fetch + render
* csrGet/csrText -> legacy browser-side API fetch + render
* style -> tagged local stylesheet metadata promoted by SSR
* functions -> server-only helpers for API/realtime code
* api M /p {b} -> export const M = async (ctx) => { b }
* realtime {..} -> export const websocket = { evt(ws, ...args) { b } }
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.generate = generate;
exports.parseForExpr = parseForExpr;
const node_buffer_1 = require("node:buffer");
const syntax_1 = require("@wrnexus/syntax");
const store_codegen_ts_1 = require("./store-codegen.js");
const analysis_ts_1 = require("./analysis.js");
const island_codegen_ts_1 = require("./island-codegen.js");
const client_codegen_ts_1 = require("./client-codegen.js");
/**
* Island names for the file currently being generated.
*
* Codegen is a synchronous single pass, so a module-scoped set avoids threading
* an extra parameter through every render function. Always reset in generate().
*/
let currentIslands = new Set();
/**
* Builds the island placeholder for a component tag that was imported from a
* .tsx file. Returns null for ordinary .wrn components.
*/
function islandMarkerFor(node) {
if (!currentIslands.has(node.tag))
return null;
const directives = node.attrs
.map((attr) => attr.name)
.filter((name) => name.startsWith("client:"));
const props = {};
for (const attr of node.attrs) {
if (attr.name.startsWith("client:"))
continue;
const parsed = (0, island_codegen_ts_1.islandPropValue)(attr.value);
if ("dynamic" in parsed) {
throw new Error(`WRN-ISLAND-PROPS: Island '${node.tag}' received a runtime expression for prop '${attr.name}'. ` +
`Island props are serialized at build time, so they must be literal values ` +
`(for example start={3} or title="Revenue"), not ${attr.value}.`);
}
props[attr.name] = parsed.value;
}
const serialized = (0, island_codegen_ts_1.serializeIslandProps)(node.tag, props);
if ("diagnostic" in serialized)
throw new Error(serialized.diagnostic.message);
return (0, island_codegen_ts_1.renderIslandMarker)({
name: node.tag,
strategy: (0, island_codegen_ts_1.parseIslandStrategy)(directives),
propsJson: serialized.json,
});
}
function isComponentTag(tag) {
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
}
const HTML_BOOLEAN_ATTRIBUTES = new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
]);
function isHtmlBooleanAttribute(name) {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
}
/*
* Constraint attributes that must be absent rather than empty. `pattern` is the
* dangerous one: an empty pattern compiles to a regex matching only the empty
* string, so every typed value becomes invalid and the form silently refuses to
* submit. A component declaring `pattern: string = ""` and rendering
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
* pattern. The rest are inert when empty, but carry no meaning either.
*/
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
"pattern",
"minlength",
"maxlength",
"min",
"max",
"step",
"inputmode",
"accept",
]);
function isOmitWhenEmptyAttribute(name) {
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
}
const URL_ATTRIBUTES = new Set([
"href",
"src",
"action",
"formaction",
"poster",
"cite",
"background",
"xlink:href",
]);
function stripAsciiControlAndSpace(value) {
let result = "";
for (const character of value) {
if (character.charCodeAt(0) > 0x20)
result += character;
}
return result;
}
function sanitizeUrlAttribute(value) {
const compact = stripAsciiControlAndSpace(value.trim());
const lower = compact.toLowerCase();
if (/^(?:javascript|vbscript|file):/.test(lower))
return "about:blank";
if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower))
return "about:blank";
return value;
}
function safeAttributeValue(name, value) {
if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{"))
return value;
return sanitizeUrlAttribute(value);
}
/** Escape a value placed inside a double-quoted HTML attribute. */
function attrEscape(value) {
return value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
/** Make HTML safe to embed inside a JS template literal. */
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:
if (attr.boolean)
return ` ${attr.name}`;
// An empty constraint attribute is worse than absent -- see
// OMIT_WHEN_EMPTY_ATTRIBUTES.
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name))
return "";
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, 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}`;
}
/**
* Event attribute for a handler written on a *component tag*
* (`<Modal @confirm="save()">`).
*
* These need their own attribute name. The mount's attributes are forwarded
* into the component and land on its view root, i.e. inside the component's
* own `data-scope` -- but the statement (`save()`) belongs to the parent that
* wrote the tag. Emitting `data-on-confirm` makes the child's runtime bind it
* against the child's scope, where the parent's functions and state do not
* exist, so the handler silently does nothing. `data-wrn-out-*` is ignored by
* the child and claimed by the mounting scope instead.
*
* `window:`/`document:` (and the browser/mobile bridges) keep the plain
* `data-on-*` form: those bind to a global target rather than to the element,
* and the runtime has no component-output path for them.
*/
function componentEventAttribute(name) {
if (name.startsWith("window:") ||
name.startsWith("document:") ||
name.startsWith("browser-") ||
name.startsWith("mobile-")) {
return eventAttribute(name);
}
return `data-wrn-out-${name}`;
}
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
function expandBindings(attrs) {
const output = [];
for (const attr of attrs) {
if (attr.event || !attr.name.startsWith("bind:")) {
output.push(attr);
continue;
}
const target = attr.name.slice("bind:".length);
if (target !== "value" && target !== "checked") {
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
}
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
}
output.push({ name: target, value: `{${expression}}`, event: false });
output.push({
name: target === "checked" ? "change" : "input",
value: `${expression} = payload.${target}`,
event: true,
});
}
return output;
}
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, dynamicExpressions) {
attrs = expandBindings(attrs);
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 expression = wholeAttributeExpression(attr.value);
if (expression &&
exprRefsState(expression, reactive.runtimeStateNames) &&
dynamicExpressions) {
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
const marker = JSON.stringify([attr.name, attr.value]);
return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
}
const initial = reactiveAttrValue(attr.value, reactive);
if (initial === null)
return base;
const marker = JSON.stringify([attr.name, attr.value]);
return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
})
.join("");
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
}
/**
* Replace i18n text sugar `{t:key}` with a `<span data-t="key">` marker the
* runtime resolves server-side. Other `{expr}` mustaches are left untouched.
*/
function substituteTMarkers(text) {
return text.replace(/\{t:([^{}]+)\}/g, (_m, key) => `<span data-t="${attrEscape(key.trim())}"></span>`);
}
/** Escape a value for safe embedding in HTML text. */
function htmlTextEscape(value) {
return value.replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
}
/**
* Evaluate a page's `state` seed expressions at compile time to obtain the
* initial SSR values used to bake `data-text` spans. Seeds may reference
* earlier ones; anything that can't be evaluated becomes `undefined`.
*/
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;
}
/**
* Page text compilation: resolve `{t:key}` i18n markers, then bake state
* interpolations (`{count}`, `{count * 2}`) into `data-text` spans carrying the
* evaluated initial value — so no-JS clients see real content and the reactive
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
* left as literal client mustaches.
*/
function substituteReactiveText(raw, reactive, dynamicExpressions) {
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;
if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
}
let value;
try {
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
}
catch {
return whole; // can't evaluate → keep as a client-only mustache
}
const baked = htmlTextEscape(value == null ? "" : String(value));
return `<span data-text="${attrEscape(expr)}">${baked}</span>`;
});
}
/**
* Bake a loop-body text run into template-literal source: static text is escaped
* for the literal, `{expr}` becomes `${__wrnexusEscapeHtml(expr)}` (server-rendered,
* escaped), and `{t:key}` becomes a `data-t` marker resolved later by translateHtml.
*/
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));
}
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
function bakeLoopAttr(raw, typed = false) {
const wholeExpression = wholeAttributeExpression(raw);
if (typed && wholeExpression)
return "${__wrnexusPropAttr(" + wholeExpression + ")}";
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)));
}
/** Render one loop-body node to template-literal source (nested loops inline). */
function renderLoopBody(node, locals = []) {
if (node.type === "text") {
return bakeLoopText(node.value);
}
if (node.type === "each") {
return compileEachExpr(node, locals);
}
if (node.type === "if") {
return compileIfExpr(node, locals);
}
const componentTag = isComponentTag(node.tag);
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => {
const name = attr.event
? componentTag
? componentEventAttribute(attr.name)
: eventAttribute(attr.name)
: attr.name;
if (attr.boolean) {
return escLit(` ${name}`);
}
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
})
.join("");
// A handler expression is emitted as text and evaluated at event time, so any
// `{#each}` variable it names has to travel with the element. The runtime
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
// actually bind an event need it -- marking every node would bloat the HTML.
const localsAttr = locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
const openAttrs = attrs + localsAttr;
if (node.tag === "Static")
return inner;
if (node.tag === "Dynamic")
return (escLit('<wrn-dynamic-region data-wrn-dynamic="true">') +
inner +
escLit("</wrn-dynamic-region>"));
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
return (escLit('<div data-wrn-keepalive="') +
bakeLoopAttr(key) +
escLit(`">`) +
inner +
escLit("</div>"));
}
if (node.tag === "Portal") {
const target = node.attrs.find((attribute) => attribute.name === "to")?.value ?? "body";
return (escLit('<div data-wrn-portal="') +
bakeLoopAttr(target) +
escLit('">') +
inner +
escLit("</div>"));
}
if (node.tag === "Transition") {
const name = node.attrs.find((attribute) => attribute.name === "name")?.value ?? "wrn-transition";
return (escLit('<div data-wrn-transition="') +
bakeLoopAttr(name) +
escLit('">') +
inner +
escLit("</div>"));
}
if (node.tag === "Component") {
const selected = node.attrs.find((attribute) => attribute.name === "is")?.value ?? "";
return (escLit('<div data-wrn-dynamic-component="') +
bakeLoopAttr(selected) +
escLit('">') +
inner +
escLit("</div>"));
}
if (componentTag) {
const island = islandMarkerFor(node);
if (island)
return escLit(island);
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
openAttrs +
escLit(">") +
inner +
escLit("</div>"));
}
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return escLit(`<${node.tag}`) + openAttrs + escLit(">");
}
return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
}
/**
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
* body. Deliberately not passed through escLit: the `${...}` must stay live so
* the values are encoded at render time.
*/
function loopLocalsAttr(locals) {
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}
/**
* Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
*/
function compileEachExpr(node, outerLocals = []) {
const item = node.item;
const index = node.index ?? "__wi";
// A nested loop can reference the outer loop's variables too.
const locals = [...outerLocals, item, index];
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
return ("${(() => { const __wl = Array.isArray(" +
node.list +
") ? (" +
node.list +
") : []; return __wl.length ? __wl.map((" +
item +
", " +
index +
") => `" +
body +
'`).join("") : `' +
empty +
"`; })()}");
}
/**
* Compile a `{#if}` block to a `${…}` template-literal interpolation: a nested ternary
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
* Conditions are JS expressions evaluated in the surrounding server scope.
*/
function compileIfExpr(node, locals = []) {
let expr = "``"; // no matching branch → empty string
for (let k = node.branches.length - 1; k >= 0; k--) {
const b = node.branches[k];
const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
}
return "${" + expr + "}";
}
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
if (node.type === "text")
return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking
// Server control block (loop / conditional) → a sentinel that survives
// templateEscape, swapped for its real `${…}` code after escaping.
if (node.type === "each" || node.type === "if") {
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
const definition = node.type === "each"
? {
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
}
: node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
}));
const attribute = node.type === "each" ? "data-wrn-each" : "data-wrn-if";
return `<template ${attribute}="${encodeClientControl(definition)}"></template>\x00WRNEACH${loops.length - 1}\x00<template data-wrn-control-end></template>`;
}
if (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
const attribute = node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback = node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const original = node.attrs.find((item) => item.name === source);
const rendered = original
? renderAttrs([{ ...original, name: attribute }], undefined, reactive, loops)
: ` ${attribute}="${attrEscape(fallback)}"`;
return `<div${rendered}>${inner}</div>`;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
const retries = attrValue(node.attrs, "retries") ?? "2";
const tags = attrValue(node.attrs, "tags") ?? source;
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
const branchElement = (name) => node.children.find((child) => child.type === "element" && child.tag === name);
const branch = (name) => {
const element = branchElement(name);
return (element?.children ?? [])
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
};
const loading = branch("Loading");
const success = branch("Success");
const error = branch("Error");
const successElement = branchElement("Success");
const errorElement = branchElement("Error");
const identifier = (value, fallback) => value && isSafeGeneratedIdentifier(value) ? value : fallback;
const successAlias = identifier(successElement ? attrValue(successElement.attrs, "data") : undefined, identifier(source, "data"));
const errorAlias = identifier(errorElement ? attrValue(errorElement.attrs, "error") : undefined, "error");
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const scoped = (value, alias, expression) => {
const index = loops.push(`\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`) - 1;
return `\x00WRNEACH${index}\x00`;
};
const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
let initial = loading;
if (serverResolved) {
const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const serverSuccess = success.replace(new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"), (_whole, expression) => `\${__wrnexusEscapeHtml(${expression})}`);
const index = loops.push(`\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`) - 1;
initial = `\x00WRNEACH${index}\x00`;
}
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
}
if (node.tag === "KeepAlive") {
const key = attrValue(node.attrs, "key") ?? "default";
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return `<div data-wrn-keepalive="${attrEscape(key)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive);
}
const apiAttr = attrValue(node.attrs, "api");
const parsedApi = apiAttr ? parseApiBinding(apiAttr) : null;
if (apiAttr && !parsedApi) {
throw new Error(`Invalid .wrn api binding "${apiAttr}"`);
}
const apiBinding = parsedApi ? apiBindings.get(parsedApi.name) : undefined;
if (parsedApi && !apiBinding) {
throw new Error(`Unknown .wrn api binding "${parsedApi.name}"`);
}
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 = csrGet && csrText
? csrMarker(csrBindings, {
method: "GET",
path: apiRoutePath(csrGet),
body: expressionBody(csrText),
helpers: "",
})
: undefined;
// Void elements (<br>, <img>, …) have no closing tag and no children.
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
}
const inner = apiBinding
? apiCallMarker(loops, parsedApi.name, parsedApi.args)
: 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, loops)}>${inner}</${node.tag}>`;
}
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
const island = islandMarkerFor(node);
if (island)
return island;
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
.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) {
// Islands work inside .wrn components too, not just pages.
const island = islandMarkerFor(node);
if (island)
return island;
let bindIndex = 0;
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
if (spread) {
return `\${__wrnSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
}
if (attr.event) {
return (escLit(` ${componentEventAttribute(attr.name)}="`) +
escLit(attrEscape(attr.value)) +
escLit(`"`));
}
if (attr.boolean) {
return ` ${attr.name}`;
}
const wholeExpression = wholeAttributeExpression(attr.value);
const compiledValue = wholeExpression
? `\${__wrnProp(${ctx.resolveExpr(wholeExpression)})}`
: compileAttrValue(attr.value, ctx);
const rendered = ` ${attr.name}="${compiledValue}"`;
if (!attr.value.includes("{") ||
(!exprRefsState(attr.value, ctx.stateNames) && !exprRefsState(attr.value, ctx.propNames))) {
return rendered;
}
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
return rendered + ` data-wrn-prop-bind-${bindIndex++}="${escLit(marker)}"`;
})
.join("");
const loops = loopVarsOf(node);
const childCtx = loops.length > 0
? {
...ctx,
forwardRestAttrs: false,
loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
}
: { ...ctx, forwardRestAttrs: false };
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
return (`<div data-component="${attrEscape(node.tag)}"` +
`${ctx.forwardRestAttrs ? "${__wrnSpreadAttrs(__attrs)}" : ""}` +
`${attrs}>${inner}</div>`);
}
/**
* Parses the three `api="…"` render-binding forms: a bare name, an empty call,
* or a call carrying an argument expression. Mirrors the shape of `@click="fn()"`,
* so no new escaping or attribute-naming rules are introduced. The argument
* capture is greedy up to the outer parens, so nested braces/parens/quotes in
* the argument expression (object literals, arrays, strings) are carried
* through untouched rather than truncated at the first `)`.
*/
function parseApiBinding(value) {
const match = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\(([\s\S]*)\))?\s*$/.exec(value);
if (!match)
return null;
return { name: match[1], args: (match[2] ?? "").trim() };
}
/**
* Emit a render-time call into the server `api` object (Task 4's generated
* transport) for an `apis {}` (mode "any") binding, and return the sentinel
* that the loop/expression-splicing mechanism swaps for the real `${…}` code.
* This calls the same server `api.<name>()` member a `load`/action block would
* call -- it does not reimplement fetch/response handling -- so a block that is
* both render-bound and called from code runs its own call each time (no
* dedup is attempted; see apis-render-binding.test.ts).
*/
function apiCallMarker(loops, name, args) {
loops.push(`\${__wrnexusEscapeHtml(await api.${name}(${args}))}`);
return `\x00WRNEACH${loops.length - 1}\x00`;
}
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 hasClientBehavior(nodes) {
return nodes.some((node) => {
// `{t:key}` is i18n sugar resolved server-side — not client reactivity.
if (node.type === "text")
return /\{(?!t:)[^{}]+\}/.test(node.value);
// Server control blocks render on the server; they don't add client reactivity.
if (node.type === "each") {
return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => hasClientBehavior(branch.body));
}
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("\0") || 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 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}"`);
}
const sectioned = block.sections;
const errorSection = sectioned?.error.trim();
bindings.set(block.name, {
mode: block.mode,
method: block.method,
path: apiRoutePath(block.path),
// A sectioned block binds the payload to `data`; the legacy form keeps
// the `with ($data)` injection, which cannot be typed.
body: sectioned
? `const data = $data; ${sectioned.response.trim() || "return data;"}`
: dataBody(block.body),
// Only a sectioned block with a non-empty `error {}` gets a fallback —
// legacy blocks and sectioned blocks without `error` keep failures
// propagating exactly as before.
...(errorSection ? { errorBody: errorSection } : {}),
helpers: sharedHelpers,
});
}
return bindings;
}
/**
* Server-side `api` object.
*
* The transport dispatches in-process, so a call from a load block or an action
* costs a function call rather than a network round trip. The request context
* comes from AsyncLocalStorage because `ctx` is not in scope everywhere server
* code runs.
*/
function serverApiBindings(ast) {
const members = ast.dataApis
.filter((block) => block.mode === "any")
.map((block) => {
const sections = block.sections;
const response = sections.response.trim() || "return data;";
const error = sections.error.trim();
const failure = error
? `const status = (err as { status?: unknown } | null | undefined)?.status; const message = err instanceof Error ? err.message : String(err); const data = (err as { data?: unknown } | null | undefined)?.data; ${error}`
: `throw err;`;
return ` ${JSON.stringify(block.name)}: async (input?: unknown) => {
const ctx = __wrnexusRequireRequestContext(${JSON.stringify(`api.${block.name}`)}) as __WrnexusContext;
let data: any;
try {
data = await __wrnexusCallApi(${JSON.stringify(apiRoutePath(block.path))}, ${JSON.stringify(block.method)}, ctx, input);
} catch (err) {
${failure}
}
${response}
}`;
});
return members.length ? `const api = {\n${members.join(",\n")}\n};` : "";
}
function ssrRuntimeSource() {
return `const __wrnexusHtmlEscapes: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\\"": "&quot;", "'": "&#39;" };
function __wrnexusEscapeHtml(value: unknown): string {
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
}
type __WrnexusContext = import("@wrnexus/core").Context & {
__wrnexusCallApi?: (path: string, method: string, input?: unknown) => Promise<unknown>;
localStorage?: unknown;
};
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: __WrnexusContext): 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 __wrnexusEvalError(err: unknown, body: string, helpers = "", ctx: __WrnexusContext): unknown {
const adapters = {
cookies: ctx.cookies,
session: ctx.session,
localStorage: ctx.localStorage,
};
const status = (err as { status?: unknown } | null | undefined)?.status;
const data = (err as { data?: unknown } | null | undefined)?.data;
const message = err instanceof Error ? err.message : String(err);
return new Function("$status", "$message", "$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nconst status = $status;\\nconst message = $message;\\nconst data = $data;\\n" + helpers + "\\n" + body)(status, message, 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 === "&"
? "&amp;"
: character === "<"
? "&lt;"
: character === ">"
? "&gt;"
: "&quot;",
);
}
async function __wrnexusCallApi(
path: string,
method: string,
ctx: __WrnexusContext,
input?: unknown,
): Promise<unknown> {
if (typeof ctx.__wrnexusCallApi === "function") {
return await ctx.__wrnexusCallApi(path, method, input);
}
const built = __wrnexusBuildApiRequest(path, method, input as Record<string, unknown> | undefined);
const url = new URL(built.url, ctx.req.url);
const headers = new Headers(ctx.req.headers);
if (built.contentType) headers.set("content-type", built.contentType);
const res = await fetch(
new Request(url, {
method,
headers,
...(built.body === undefined ? {} : { body: built.body }),
}),
);
const type = res.headers.get("content-type") || "";
if (!res.ok) {
const data = type.includes("application/json")
? await res.json().catch(() => undefined)
: await res.text().catch(() => undefined);
throw Object.assign(new Error(".wrn data API request failed with status " + res.status), {
status: res.status,
data,
});
}
return type.includes("application/json") ? await res.json() : await res.text();
}
type __WrnexusApiCall = {
path: string;
method: string;
body: string;
helpers: string;
errorBody?: string;
};
type __WrnexusSsrBinding = __WrnexusApiCall & { marker: string };
// Shared by every ssr api-binding consumption site (marker replacement,
// #each loop consts, ...) so the narrow try/catch -- only active when the
// block declared an error section -- cannot drift between call sites.
async function __wrnexusResolveApiBinding(
binding: __WrnexusApiCall,
ctx: __WrnexusContext,
): Promise<unknown> {
if (binding.errorBody) {
let data: unknown;
try {
data = await __wrnexusCallApi(binding.path, binding.method, ctx);
} catch (err) {
return __wrnexusEvalError(err, binding.errorBody, binding.helpers, ctx);
}
return __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
}
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
return __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
}
async function __wrnexusRenderSsrBindings(html: string, ctx: __WrnexusContext): Promise<string> {
for (const binding of __wrnexusSsrBindings) {
const value = await __wrnexusResolveApiBinding(binding, ctx);
html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
}
return html;
}`;
}
function stableHash(value) {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(36);
}
function hydrationId(ast) {
const shape = JSON.stringify({
kind: ast.kind,
name: ast.name,
props: ast.props.map((entry) => entry.name),
events: ast.events.map((entry) => entry.name),
states: ast.states.map((entry) => entry.name),
computed: ast.computed.map((entry) => entry.name),
view: ast.view,
});
return `${ast.name}:${stableHash(shape)}`;
}
function localStyleId(ast) {
return `wrn-${ast.kind}-${stableHash(`${ast.kind}:${ast.name}`)}`;
}
function localStyleTag(ast, styles) {
if (!styles.length)
return "";
const id = localStyleId(ast);
const css = styles.map(styleEscape).join("\n");
return `<style data-wrnexus-style="${attrEscape(ast.name)}" data-wrnexus-style-id="${attrEscape(id)}" data-wrnexus-style-owner="${attrEscape(ast.name)}" data-wrnexus-style-kind="${attrEscape(ast.kind)}">\n${css}\n</style>`;
}
function localStyleExport(ast, styles) {
if (!styles.length)
return null;
return `export const __wrnexusStyles = ${JSON.stringify([
{
id: localStyleId(ast),
owner: ast.name,
kind: ast.kind,
css: styles.join("\n"),
},
], null, 2)};`;
}
function isStoreImportSource(source) {
return /(?:^|\/)stores?\//.test(source) || source.startsWith("@wrnexus/store");
}
function importedStoreBindings(ast) {
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) {
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) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
function generateSsrStateAliases(stateNames) {
const names = [...new Set(stateNames)].filter(isSafeGeneratedIdentifier);
if (!names.length) {
return "";
}
return `const { ${names.join(", ")} } = __state;\n`;
}
function orderPageStates(entries) {
if (entries.length < 2)
return entries;
const byName = new Map(entries.map((entry) => [entry.name, entry]));
const visiting = new Set();
const visited = new Set();
const ordered = [];
const visit = (name) => {
if (visited.has(name))
return;
if (visiting.has(name)) {
throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
}
const entry = byName.get(name);
if (!entry)
return;
visiting.add(name);
for (const dependency of byName.keys()) {
if (dependency !== name &&
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
visit(dependency);
}
}
visiting.delete(name);
visited.add(name);
ordered.push(entry);
};
for (const entry of entries)
visit(entry.name);
return ordered;
}
function generateSsrStateInitializer(entries) {
const ordered = orderPageStates(entries);
const declarations = ordered
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
.map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`)
.join(" ");
const values = entries
.map((entry) => {
if (isSafeGeneratedIdentifier(entry.name)) {
return `${JSON.stringify(entry.name)}: ${entry.name}`;
}
return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
})
.join(", ");
return `(() => { ${declarations} return { ${values} }; })()`;
}
function orderPageComputed(entries) {
if (entries.length < 2)
return entries;
const byName = new Map(entries.map((entry) => [entry.name, entry]));
const visiting = new Set();
const visited = new Set();
const ordered = [];
const visit = (name) => {
if (visited.has(name))
return;
if (visiting.has(name)) {
throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
}
const entry = byName.get(name);
if (!entry)
return;
visiting.add(name);
for (const dependency of byName.keys()) {
if (dependency !== name &&
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
visit(dependency);
}
}
visiting.delete(name);
visited.add(name);
ordered.push(entry);
};
for (const entry of entries)
visit(entry.name);
return ordered;
}
function generateSsrComputedAliases(entries) {
return orderPageComputed(entries)
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
.map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`)
.join("\n");
}
function runtimeComputedNames(states, computed, runtimeRoots = []) {
const entries = [...states, ...computed];
const runtime = new Set([
...runtimeRoots,
...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
]);
let changed = true;
while (changed) {
changed = false;
for (const entry of entries) {
if (runtime.has(entry.name))
continue;
if ([...runtime].some((name) => new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr))) {
runtime.add(entry.name);
changed = true;
}
}
}
return runtime;
}
function hydrationAttribute(ast) {
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
? "none"
: (ast.hydrate ?? "load");
const hasBrowserModule = (0, client_codegen_ts_1.browserModuleRequired)(ast);
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, target) {
const runtimes = target === "browser" ? ["client", "shared"] : ["server", "shared"];
return ast.functions
.map((body) => (0, syntax_1.stripRuntimeFunctionModifiers)(body, [...runtimes]))
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
}
function publicOutputNames(ast) {
return [
...new Set([
...ast.outputs.map((output) => output.name),
...ast.events.map((event) => event.name),
]),
];
}
function prepareActionForms(nodes, actions) {
for (const node of nodes) {
if (node.type === "text")
continue;
if (node.type === "each") {
prepareActionForms(node.body, actions);
prepareActionForms(node.empty, actions);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => prepareActionForms(branch.body, actions));
continue;
}
prepareActionForms(node.children, actions);
if (node.tag.toLowerCase() !== "form")
continue;
const submit = node.attrs.find((attr) => attr.event && attr.name === "submit");
if (!submit || !actions.has(submit.value.trim()))
continue;
const name = submit.value.trim();
node.attrs = node.attrs.filter((attr) => attr !== submit);
if (!node.attrs.some((attr) => !attr.event && attr.name === "method")) {
node.attrs.push({ name: "method", value: "post", event: false });
}
node.attrs.push({ name: "data-wrn-action", value: name, event: false });
node.children.unshift({
type: "element",
tag: "input",
attrs: [
{ name: "type", value: "hidden", event: false },
{ name: "name", value: "_wrnexus_action", event: false },
{ name: "value", value: name, event: false },
],
children: [],
});
}
}
function markServerAsyncBoundaries(nodes, serverLoads) {
for (const node of nodes) {
if (node.type === "text")
continue;
if (node.type === "each") {
markServerAsyncBoundaries(node.body, serverLoads);
markServerAsyncBoundaries(node.empty, serverLoads);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => markServerAsyncBoundaries(branch.body, serverLoads));
continue;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
if (serverLoads.has(source) &&
!node.attrs.some((attribute) => attribute.name === "data-wrn-async-server")) {
node.attrs.push({ name: "data-wrn-async-server", value: "true", event: false });
}
}
markServerAsyncBoundaries(node.children, serverLoads);
}
}
function generate(ast, options = {}) {
currentIslands = options.islands ?? new Set();
try {
return generateInner(ast);
}
finally {
currentIslands = new Set();
}
}
function generateInner(ast) {
ast = (0, analysis_ts_1.optimizeAst)(ast).ast;
if (ast.kind === "global-store" || ast.kind === "page-store")
return (0, store_codegen_ts_1.generateStoreModule)(ast);
if (ast.kind === "component" || ast.kind === "layout") {
return generateComponent(ast);
}
const out = [];
prepareActionForms(ast.view, new Set(ast.actions.map((action) => action.name)));
markServerAsyncBoundaries(ast.view, new Set(ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => load.name)));
if (ast.actions.length > 0) {
out.push(`import { createActionClient } from "@wrnexus/csr";\nimport type { InferSchema } from "@wrnexus/validation";`);
}
if (ast.imports.length > 0)
out.push(generatedImports(ast).join("\n"));
const hasServerApis = ast.dataApis.some((block) => block.mode === "any");
if (hasServerApis) {
out.push(`import { requireRequestContext as __wrnexusRequireRequestContext } from "@wrnexus/core";`);
}
const ssrBindings = [];
const csrBindings = [];
const helpers = targetFunctions(ast, "server");
const apiBindings = apiBindingMap(ast, helpers);
const typeSource = ast.types
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
if (typeSource)
out.push(typeSource);
if (helpers) {
out.push(`// --- .wrn functions ---\n${helpers}`);
}
// --- 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 = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`);
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
// --- View -> default page component ---
const orderedStates = orderPageStates(ast.states);
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const seedScope = evalStateSeeds(orderedStates);
const orderedComputed = orderPageComputed(ast.computed);
for (const entry of orderedComputed) {
try {
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(seedScope);
}
catch {
seedScope[entry.name] = undefined;
}
}
const reactiveNames = [
...browserStates.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
];
const storeBindings = importedStoreBindings(ast);
const runtimeRoots = [
...storeBindings.map((entry) => entry.local),
...ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => load.name),
];
const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
const reactive = reactiveNames.length > 0
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
: null;
const loops = [];
let html = ast.view
.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
const needsClientRuntime = ast.runtime !== "server" &&
(browserStates.length > 0 ||
ast.computed.length > 0 ||
hasClientBehavior(ast.view) ||
pageBehavior !== null);
if (needsClientRuntime) {
const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
html = `<div data-scope="${scopePlaceholder}"${behaviorAttribute(pageBehavior)}${hydrationAttribute(ast)}>${html}</div>`;
}
const pageStyleTag = localStyleTag(ast, styles);
if (pageStyleTag) {
html = `${pageStyleTag}${html}`;
}
if (ast.renderMode === "client") {
const clientRoot = hydrationId(ast);
html = `<div data-wrn-client-root="${clientRoot}" aria-busy="true"></div><template data-wrn-client-template="${clientRoot}">${html}</template>`;
}
const pageStyleExport = localStyleExport(ast, styles);
if (pageStyleExport)
out.push(pageStyleExport);
if (csrBindings.length > 0) {
out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
}
if (pageBehavior) {
out.push(`export const __wrnexusBehavior = ${JSON.stringify(pageBehavior, null, 2)};`);
}
// Escape the static HTML for the template literal, then swap loop sentinels for
// their real `${…}` code (which must NOT be escaped).
let body = templateEscape(html);
let staticShellBody;
if (ast.renderMode === "partial-static") {
const shellHtml = html.replace(/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi, '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
staticShellBody = templateEscape(shellHtml);
}
const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
const stateType = ast.states.length > 0
? `{ ${ast.states
.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 ssrComputedAliases = generateSsrComputedAliases(orderedComputed);
const storeDeclarations = storeBindings
.map((entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`)
.join("\n");
const serverLoadAliases = ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
.join("\n");
// Inner dynamic expressions are registered before the wrapper that scopes
// them (for example an Async branch alias). Resolve from the outside in so a
// wrapper sentinel is expanded before its nested sentinels are visited.
for (let idx = loops.length - 1; idx >= 0; idx--) {
const code = loops[idx];
body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
}
}
const loopConsts = [];
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
const needsRuntimeHelpers = needsSsrRuntime || hasServerApis;
if (needsRuntimeHelpers) {
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
out.push(ssrRuntimeSource());
// Loop bodies that bind an event carry their {#each} locals in an encoded
// attribute. Emitted only when the view actually produced one, so a page
// without handlers in a loop keeps the smaller prelude -- but it MUST be
// emitted whenever the marker is, or the render throws on an undefined
// function instead of the handler throwing on an undefined variable.
if (body.includes("__wrnexusEncodeLoopLocals(")) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
const json = JSON.stringify(value);
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
}`);
}
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
}
if (hasServerApis) {
out.push(serverApiBindings(ast));
}
if (needsSsrRuntime) {
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
out.push(`export default async function ${ast.name}(ctx: __WrnexusContext) {
${storeDeclarations}
${serverLoadAliases}
${decls}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const html = \`${body}\`.replace(
"__WRNEXUS_DYNAMIC_SCOPE__",
__scopeValue,
);
return await __wrnexusRenderSsrBindings(html, ctx);
}`);
}
else {
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: import("@wrnexus/core").Context) {
${storeDeclarations}
${serverLoadAliases}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return \`${body}\`.replace(
"__WRNEXUS_DYNAMIC_SCOPE__",
__scopeValue,
);
}`);
}
if (staticShellBody !== undefined) {
out.push(`export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
${storeDeclarations}
${serverLoadAliases}
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
try {
encoded = value === undefined ? "undefined" : JSON.stringify(value);
} catch {
encoded = JSON.stringify(String(value));
}
return key + ": " + encoded;
})
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return \`${staticShellBody}\`.replace("__WRNEXUS_DYNAMIC_SCOPE__", __scopeValue);
}`);
}
if (ast.loads.length > 0) {
const serverLoads = ast.loads.filter((entry) => entry.mode === "server" && !entry.deferred);
const publicClientLoads = ast.loads.filter((entry) => entry.mode === "client" || entry.deferred);
const namedByName = new Map(ast.loads.filter((entry) => entry.name).map((entry) => [entry.name, entry]));
const clientNames = new Set(publicClientLoads.flatMap((entry) => (entry.name ? [entry.name] : [])));
const includeDependencies = (name) => {
for (const dependency of namedByName.get(name)?.dependsOn ?? []) {
if (clientNames.has(dependency))
continue;
clientNames.add(dependency);
includeDependencies(dependency);
}
};
for (const name of [...clientNames])
includeDependencies(name);
const clientLoads = ast.loads.filter((entry) => !entry.name || clientNames.has(entry.name));
const renderLoads = (exportName, execution, exposed) => {
const declarations = execution
.filter((entry) => entry.name)
.map((entry) => {
const dependencies = (entry.dependsOn ?? [])
.map((dependency) => `const ${dependency} = await __load_${dependency}();`)
.join("\n");
return ` let __promise_${entry.name}: Promise<unknown> | undefined;
const __load_${entry.name} = () => (__promise_${entry.name} ??= (async () => {
${dependencies}
${entry.body}
})());`;
})
.join("\n");
const visible = exposed.filter((entry) => entry.name);
return `export async function ${exportName}(ctx: import("@wrnexus/core").Context) {
${exposed
.filter((entry) => !entry.name)
.map((entry) => entry.body)
.join("\n")}
${declarations}
${visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
const __response = __values.find((value) => value instanceof Response);
if (__response) return __response;
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""}
}`;
};
if (serverLoads.length > 0)
out.push(renderLoads("__wrnexusLoad", serverLoads, serverLoads));
if (publicClientLoads.length > 0)
out.push(renderLoads("__wrnexusClientLoad", clientLoads, publicClientLoads));
}
if (ast.actions.length > 0) {
for (const action of ast.actions) {
if (!action.schema) {
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
continue;
}
out.push(`export async function ${action.name}(input: InferSchema<typeof ${action.schema}>, ctx: import("@wrnexus/core").Context) {
const invalidate = (...tags: string[]) => {
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
bucket.push(...tags.flat());
};
${action.body}
}`);
}
out.push(`export const __wrnexusActions = { ${ast.actions
.map((action) => `${action.name}: { run: ${action.name}, schema: ${action.schema ?? "undefined"} }`)
.join(", ")} };`);
out.push(`export const __wrnexusActionClients = {
${ast.actions
.map((action) => ` ${action.name}: createActionClient<${action.schema ? `InferSchema<typeof ${action.schema}>` : "Record<string, unknown>"}, Awaited<ReturnType<typeof ${action.name}>>>("", ${JSON.stringify(action.name)}),`)
.join("\n")}
};`);
}
// --- API blocks -> method handlers ---
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: import("@wrnexus/core").Context) => {${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 = {\n${entries.join("\n")}\n};`);
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};`);
});
}
// --- Realtime blocks -> a websocket export ---
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}: Event) {${h.body}},`;
}));
out.push(`export const websocket = {\n${handlers.join("\n")}\n};`);
}
return out.join("\n\n") + "\n";
}
/** Parse a `data-for="item in list [key expr]"` / `"item, i in list [key expr]"` directive. */
function parseForExpr(value) {
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)(?:\s+key\s+([\s\S]+?))?\s*$/.exec(value);
if (!m)
return null;
return { item: m[1], index: m[2], list: m[3].trim(), key: m[4]?.trim() };
}
/** The loop variables a node introduces via `data-for`, if any. */
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] : [])] : [];
}
/** JS reserved words that cannot be used as a plain `const` name. */
const 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",
"implements",
"interface",
"package",
"private",
"protected",
"public",
"static",
]);
/** A JS reference for a prop/state name (reserved words get a `__p_` prefix). */
function safeRef(name) {
return JS_RESERVED.has(name) ? `__p_${name}` : name;
}
/** Escape a literal segment so it is safe inside a JS template literal. */
function escLit(s) {
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
}
function componentBehavior(ast) {
const functions = (0, syntax_1.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);
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 &&
ast.outputs.length === 0 &&
computed.length === 0 &&
effects.length === 0 &&
Object.keys(lifecycle).length === 0 &&
watches.length === 0) {
return null;
}
return {
functions,
outputs: ast.outputs,
computed,
effects,
lifecycle,
watches,
};
}
function behaviorAttribute(behavior) {
if (!behavior) {
return "";
}
const encoded = node_buffer_1.Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
return ` data-wrn-behavior="${encoded}"`;
}
const 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 exprRefsComponentReactiveValue(expr, ctx) {
return (exprRefsState(expr, ctx.stateNames) ||
exprRefsState(expr, ctx.propNames) ||
exprRefsState(expr, ctx.functionNames));
}
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 viewHasServerEach(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each")
return true;
if (node.type === "if") {
return node.branches.some((branch) => viewHasServerEach(branch.body));
}
return viewHasServerEach(node.children);
});
}
function viewHasRestAttributeSpread(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each") {
return viewHasRestAttributeSpread(node.body) || viewHasRestAttributeSpread(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => viewHasRestAttributeSpread(branch.body));
}
return (node.attrs.some((attr) => /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.test(attr.name)) ||
viewHasRestAttributeSpread(node.children));
});
}
/**
* Compile a text node. Interpolations that reference state stay as client
* mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
* props/constants are baked server-side (`${__wrnHtml(expr)}`), so static
* components render correct HTML with zero JavaScript.
*/
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:")) {
// i18n sugar: {t:key} → a marker resolved server-side by translateHtml.
out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
}
else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
// Loop variable (from data-for): leave a literal client mustache — the
// list renderer fills it per item; it has no server-side value.
out += escLit(`{${expr}}`);
}
else if (expr === "content") {
out += `\${__wrnRaw(${ctx.resolveExpr(expr)})}`;
}
else if (exprRefsComponentReactiveValue(expr, ctx)) {
// State interpolation: bake the initial value AND keep it reactive via a
// data-text span, so no-JS clients see the real value and hydration
// updates it in place. `count` → `<span data-text="count">0</span>`.
out +=
escLit(`<span data-text="${attrEscape(expr)}">`) +
`\${__wrnHtml(${ctx.resolveExpr(expr)})}` +
escLit(`</span>`);
}
else {
out += `\${__wrnHtml(${ctx.resolveExpr(expr)})}`;
}
last = m.index + m[0].length;
}
return out + escLit(raw.slice(last));
}
/** Compile an attribute value; `{expr}` is baked server-side (loop vars stay literal). */
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}}`); // hydrated per-item by the list renderer
}
else {
out += `\${__wrnAttr(${ctx.resolveExpr(expr)})}`;
}
last = m.index + m[0].length;
}
return out + escLit(attrEscape(raw.slice(last)));
}
/**
* Serialize a control-block body as inert browser-side template markup.
* Values deliberately remain as mustaches: the CSR runtime evaluates them
* against the component scope (and `{#each}` locals) when it materializes the
* template. The string is base64 encoded before it is placed in HTML.
*/
function renderClientControlTemplate(nodes) {
const render = (node) => {
if (node.type === "text") {
return node.value.replace(/\{([^{}]+)\}/g, (whole, rawExpression) => {
const expression = rawExpression.trim();
return expression.startsWith("t:")
? `<span data-t="${attrEscape(expression.slice(2).trim())}"></span>`
: `<span data-text="${attrEscape(expression)}">${whole}</span>`;
});
}
if (node.type === "each") {
return `<template data-wrn-each="${attrEscape(encodeClientControl({
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
}))}"></template><template data-wrn-control-end></template>`;
}
if (node.type === "if") {
return `<template data-wrn-if="${attrEscape(encodeClientControl(node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
}))))}"></template><template data-wrn-control-end></template>`;
}
const componentTag = isComponentTag(node.tag);
let bindIndex = 0;
const attrs = expandBindings(node.attrs)
.map((attribute) => {
const name = attribute.event
? componentTag
? componentEventAttribute(attribute.name)
: eventAttribute(attribute.name)
: attribute.name;
if (attribute.boolean)
return ` ${name}`;
if (attribute.name.startsWith("class:")) {
const expression = unwrapDirectiveExpression(attribute.value);
return ` data-wrn-class-${bindIndex++}="${attrEscape(JSON.stringify([attribute.name.slice("class:".length), expression]))}"`;
}
if (attribute.name === "data-show") {
return ` data-show="${attrEscape(unwrapDirectiveExpression(attribute.value))}"`;
}
const rendered = ` ${name}="${attrEscape(attribute.value)}"`;
return attribute.value.includes("{")
? `${rendered} data-wrn-bind-${bindIndex++}="${attrEscape(JSON.stringify([name, attribute.value]))}"`
: rendered;
})
.join("");
const children = node.children.map(render).join("");
if (node.tag === "Static")
return children;
if (componentTag)
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${children}</div>`;
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase()))
return `<${node.tag}${attrs}>`;
return `<${node.tag}${attrs}>${children}</${node.tag}>`;
};
return nodes.map(render).join("");
}
function encodeClientControl(value) {
return node_buffer_1.Buffer.from(JSON.stringify(value), "utf8").toString("base64");
}
function renderComponentIfNode(node, ctx) {
let expression = "``";
const clientBranches = [];
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 + "`";
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
expression =
branch.cond === null
? bodyExpression
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
}
// Render all branches on the server into an inert, encoded definition.
// A branch first selected in the browser therefore contains the real nested
// component HTML and scope metadata, not a data-component placeholder.
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
}
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("");
const serverBody = "${(() => { const __wl = Array.isArray(" +
list +
") ? (" +
list +
") : []; return __wl.length ? __wl.map((" +
item +
", " +
index +
") => `" +
body +
'`).join("") : `' +
empty +
"`; })()}";
const definition = encodeClientControl({
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
});
return `<template data-wrn-each="${definition}"></template>${serverBody}<template data-wrn-control-end></template>`;
}
function serverLoopLocalsAttribute(ctx) {
const locals = [...(ctx.serverLocals ?? [])];
if (locals.length === 0) {
return "";
}
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}
function unwrapDirectiveExpression(raw) {
const value = raw.trim();
if (!value.startsWith("{") || !value.endsWith("}")) {
return value;
}
let depth = 0;
let quote = null;
let escaped = false;
for (let index = 0; index < value.length; index++) {
const char = value[index];
if (escaped) {
escaped = false;
continue;
}
if (quote) {
if (char === "\\") {
escaped = true;
}
else if (char === quote) {
quote = null;
}
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{")
depth++;
if (char === "}")
depth--;
if (depth === 0 && index < value.length - 1) {
return value;
}
}
return depth === 0 ? value.slice(1, -1).trim() : value;
}
/** Render a component view node into template-literal-ready source. */
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 (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return `<div data-wrn-keepalive="${compileAttrValue(key, ctx)}">${inner}</div>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
const attribute = node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback = node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const raw = node.attrs.find((item) => item.name === source)?.value ?? fallback;
return `<div ${attribute}="${compileAttrValue(raw, ctx)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
const loopVariables = loopVarsOf(node);
const elementContext = {
...ctx,
forwardRestAttrs: false,
...(loopVariables.length > 0
? { loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]) }
: {}),
};
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: unwrapDirectiveExpression(attr.value),
});
}
}
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
const attrs = expandBindings(node.attrs)
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
if (spread) {
return `\${__wrnSpreadAttrs(${elementContext.resolveExpr(spread[1])})}`;
}
if (a.event) {
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
}
if (a.boolean) {
return ` ${a.name}`;
}
if (isHtmlBooleanAttribute(a.name)) {
const expression = wholeAttributeExpression(a.value);
if (expression) {
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
const referencesServerLocal = ctx.serverLocals
? exprRefsState(a.value, ctx.serverLocals)
: false;
const marker = referencesState || referencesLoopVariable || referencesServerLocal
? ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`
: "";
/*
* A boolean attribute whose expression names a loop variable cannot
* be resolved on the server: __wrnBooleanAttr runs at render time,
* where `row` or `item` simply does not exist, and the emitted
* module blew up. Leave the attribute off the server output and let
* the client bind set it -- the runtime toggles boolean attributes
* rather than stringifying them, so `checked={isSelected(row)}`
* behaves correctly once hydrated.
*/
if (referencesLoopVariable) {
return ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`;
}
return `\${__wrnBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
}
if (a.value === "false")
return "";
if (a.value === "true" || a.value === "")
return ` ${a.name}`;
}
const wholeExpression = wholeAttributeExpression(a.value);
/*
* data-show carries an EXPRESSION, not a value. The client re-evaluates
* whatever string it finds in the attribute on every state change, so
* interpolating `{open || visible}` down to the literal "false" at
* render time froze the directive: the element could never be shown
* again, no matter what the state did. A data-wrn-bind marker did not
* save it either -- the bind rewrites the same attribute the directive
* reads, and the directive had already captured "false" as its
* expression. Emitting the expression verbatim (the form Modal uses,
* data-show="isOpen()") makes both authoring styles behave the same.
*/
if (a.name === "data-show" && wholeExpression) {
return ` data-show="${escLit(attrEscape(wholeExpression))}"`;
}
const compiledValue = isExplicitComponentMount && wholeExpression
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
: compileAttrValue(a.value, elementContext);
/*
* A constraint attribute whose value is only known at render time (a
* component prop defaulting to "") must be decided at render time, so
* the whole attribute -- name included -- is emitted by the helper.
* Component mounts are excluded: there the value is a prop being passed
* down, not an HTML attribute being written.
*/
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
if (a.value === "")
return "";
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
}
const rendered = ` ${a.name}="${compiledValue}"`;
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
const referencesServerLocal = ctx.serverLocals
? exprRefsState(a.value, ctx.serverLocals)
: false;
if (!a.value.includes("{") ||
(!referencesState && !referencesLoopVariable && !referencesServerLocal)) {
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 }) => {
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(expression, elementContext.loopVars)
: false;
// data-for variables do not exist during
// initial server rendering.
if (referencesLoopVariable) {
return "";
}
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
})
.join("");
const staticClassValue = staticClasses.join(" ");
const classReferencesState = exprRefsComponentReactiveValue(staticClassValue, ctx);
const classReferencesLoopVariable = elementContext.loopVars
? exprRefsState(staticClassValue, elementContext.loopVars)
: false;
const classReferencesServerLocal = ctx.serverLocals
? exprRefsState(staticClassValue, ctx.serverLocals)
: false;
const classHasReactiveExpression = staticClassValue.includes("{") &&
(classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0
? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"`
: "";
const classReactiveBinding = classHasReactiveExpression
? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"`
: "";
const classBindings = conditionalClasses
.map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression]));
return ` data-wrn-class-${index}="${escLit(marker)}"`;
})
.join("");
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const allAttrs = `${loopLocalsAttribute}` +
`${ctx.forwardRestAttrs ? "${__wrnSpreadAttrs(__attrs)}" : ""}` +
`${ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""}` +
`${classAttribute}` +
`${classReactiveBinding}` +
`${classBindings}` +
`${attrs}`;
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${allAttrs}>`;
}
const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
}
function generateComponent(ast) {
const out = [];
if (ast.imports.length > 0)
out.push(generatedImports(ast).join("\n"));
const hasServerEach = viewHasServerEach(ast.view);
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
? [
{
name: "content",
default: '""',
valueType: "string",
required: false,
},
...ast.props,
]
: ast.props;
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const stateNames = new Set([
...browserStates.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
]);
const nameRefs = new Map();
for (const p of effectiveProps) {
nameRefs.set(p.name, safeRef(p.name));
}
if (!nameRefs.has("attrs")) {
nameRefs.set("attrs", "__attrs");
}
for (const s of ast.states)
nameRefs.set(s.name, safeRef(s.name));
for (const entry of ast.computed)
nameRefs.set(entry.name, safeRef(entry.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,
propNames: new Set(effectiveProps.map((entry) => entry.name)),
functionNames: new Set(ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map((fn) => fn.name)),
resolveExpr,
eventNames: publicOutputNames(ast),
};
const serverFunctions = targetFunctions(ast, "server");
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
const automaticallyForwardRootAttrs = !hasExplicitRestSpread &&
!effectiveProps.some((prop) => prop.name === "attrs") &&
rootElementIndex >= 0;
const viewCode = ast.view
.map((node, index) => renderComponentNode(node, automaticallyForwardRootAttrs && index === rootElementIndex
? { ...ctx, forwardRestAttrs: true }
: ctx))
.join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const styleTag = escLit(localStyleTag(ast, styles));
// Props remain server-rendered and also become signals so a parent can drive
// a mounted child after hydration.
const behavior = componentBehavior(ast);
const needsScope = ast.runtime !== "server" &&
(effectiveProps.length > 0 ||
browserStates.length > 0 ||
ast.computed.length > 0 ||
viewHasEvents(ast.view) ||
behavior !== null);
if (hasServerEach || needsScope) {
out.push(`import { Buffer as __WrnexusBuffer } from "node:buffer";`);
}
const scopeKeys = [
...effectiveProps.map((prop) => prop.name),
...browserStates.map((state) => state.name),
];
const behaviorAttr = behaviorAttribute(behavior);
const decls = [];
for (const prop of effectiveProps) {
if (prop.required) {
decls.push(` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`)});`);
}
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "unknown"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, syntax_1.runtimeTypeOf)(prop.valueType))}, ${JSON.stringify(prop.name)}) as ${prop.valueType ?? "unknown"};`);
}
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
}
for (const state of ast.states) {
decls.push(` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`);
}
for (const entry of ast.computed) {
decls.push(` const ${nameRefs.get(entry.name)} = (${resolveExpr(entry.expr)});`);
}
const returnExpr = needsScope
? "`" +
styleTag +
`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}"${behaviorAttr}${hydrationAttribute(ast)}>` +
viewCode +
"</div>`"
: "`" + styleTag + viewCode + "`";
const scopeLine = needsScope && scopeKeys.length > 0
? ` const __scopeState = { ${scopeKeys
.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
.join(", ")} };\n const __scope = __wrnexusScopeDecl(__scopeState);\n const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");\n`
: needsScope
? ` const __scopeState = {};\n const __scope = "";\n const __scopePayload = __WrnexusBuffer.from("{}", "utf8").toString("base64");\n`
: "";
if (ast.kind === "layout") {
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
}
else {
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
}
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
const componentStyleExport = localStyleExport(ast, styles);
if (componentStyleExport)
out.push(componentStyleExport);
if (behavior) {
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
}
const typeSource = ast.types
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
if (typeSource)
out.push(typeSource);
if (effectiveProps.length > 0) {
out.push(`export interface ${ast.name}Props {\n [attribute: string]: unknown;\n${effectiveProps
.map((prop) => ` ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`)
.join("\n")}\n}`);
}
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: unknown, def: unknown, declared: string = "unknown", propName: string = "prop"): unknown {
if (v === undefined || v === null) {
return def;
}
if (declared === "number" || typeof def === "number") {
const parsed = Number(v);
if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
return parsed;
}
if (declared === "boolean" || typeof def === "boolean") {
if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
if (v === false || v === "false" || v === 0 || v === "0") return false;
throw new TypeError("Expected a boolean prop");
}
if (declared === "array" || Array.isArray(def)) {
if (Array.isArray(v)) {
return v;
}
if (typeof v === "string") {
try {
const parsed = JSON.parse(v);
if (!Array.isArray(parsed)) throw new TypeError("Expected an array prop '" + propName + "'");
return parsed;
} catch {
throw new TypeError("Expected an array prop '" + propName + "'");
}
}
throw new TypeError("Expected an array prop '" + propName + "'");
}
if (declared === "object" || (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);
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new TypeError("Expected an object prop '" + propName + "'");
}
return parsed;
} catch {
throw new TypeError("Expected an object prop '" + propName + "'");
}
}
throw new TypeError("Expected an object prop '" + propName + "'");
}
if (declared === "bigint") return BigInt(v as string | number | bigint | boolean);
if (declared === "function" && typeof v !== "function") {
throw new TypeError("Expected a function prop");
}
return declared === "unknown" && def === undefined ? v : String(v);
}
function __restProps(
props: Record<string, unknown>,
declared: Set<string>,
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(props).filter(([name]) => !declared.has(name)),
);
}
function __wrnHtml(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>]/g,
(c) =>
c === "&"
? "&amp;"
: c === "<"
? "&lt;"
: "&gt;",
);
}
function __wrnOptionalAttr(name: string, value: string): string {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>"]/g,
(c) =>
c === "&"
? "&amp;"
: c === "<"
? "&lt;"
: c === ">"
? "&gt;"
: "&quot;",
);
}
function __wrnBooleanAttr(name: string, value: unknown): string {
return value === true ||
value === "true" ||
value === "" ||
value === 1 ||
value === "1" ||
value === name
? " " + name
: "";
}
function __wrnSpreadAttrs(value: unknown): string {
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
const attributes: string[] = [];
for (const [name, raw] of Object.entries(value)) {
const lowerName = name.toLowerCase();
if (
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
lowerName.startsWith("on") ||
lowerName === "style" ||
lowerName === "slot" ||
lowerName === "data-component" ||
// Internal markers must not leak through a spread. Parent-owned output
// and prop bindings ride from the mount onto the
// rendered child root so the mounting scope can bind them there.
(lowerName.startsWith("data-wrn") &&
!lowerName.startsWith("data-wrn-out-") &&
!lowerName.startsWith("data-wrn-prop-bind-"))
) {
continue;
}
if (booleanAttributes.has(lowerName)) {
attributes.push(__wrnBooleanAttr(name, raw));
continue;
}
if (raw === false || raw === null || raw === undefined) continue;
attributes.push(" " + name + '="' + __wrnAttr(raw) + '"');
}
return attributes.join("");
}
function __wrnProp(v: unknown): string {
const value =
v !== null && typeof v === "object"
? JSON.stringify(v)
: String(v == null ? "" : v);
return __wrnAttr(value);
}
function __wrnRaw(v: unknown): string {
return String(v == null ? "" : v);
}`);
if (hasServerEach) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
}
if (needsScope) {
out.push(`function __wrnexusEncodeControl(value: unknown): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "number") {
return Number.isFinite(value)
? String(value)
: "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
const serialized = JSON.stringify(value);
return serialized === undefined
? "undefined"
: serialized;
} catch {
return "null";
}
}
function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
return Object.keys(obj)
.map(
(key) =>
key +
": " +
__wrnexusSerializeScopeValue(
obj[key],
),
)
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}`);
}
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
out.push(`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"}): string {\n` +
` const __p = props || {};\n` +
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
serverFunctionSource +
scopeLine +
` return ${returnExpr};\n` +
`}`);
out.push(`export default { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.kind)}, render };`);
return out.join("\n\n") + "\n";
}
function __wrnRaw(v) {
return String(v == null ? "" : v);
}
function wholeAttributeExpression(value) {
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
return match?.[1]?.trim() || null;
}
function __wrnHtml(v) {
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
}
function __wrnOptionalAttr(name, value) {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v) {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;");
}
function __wrnProp(v) {
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
return __wrnAttr(value);
}
function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
}
if (attr.boolean) {
return ` ${attr.name}`;
}
const expression = wholeAttributeExpression(attr.value);
if (!expression) {
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
}
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
// The mount disappears during SSR. Preserve every dynamic component prop
// as a parent-owned binding; server-only expressions simply fail closed in
// the browser, while page state can continue driving the mounted child.
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"`;
return ` ${attr.name}="${marker}"${binding}`;
}
},
"packages/compiler/src/component-contract.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createComponentContract = createComponentContract;
function unionOptions(type) {
if (!type || !type.includes("|"))
return undefined;
const values = type
.split("|")
.map((part) => part.trim())
.filter((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))
.map((part) => part.slice(1, -1));
return values.length ? values : undefined;
}
function createComponentContract(ast) {
return {
name: ast.name,
kind: ast.kind,
props: ast.props.map((prop) => ({
name: prop.name,
type: prop.valueType ?? "unknown",
required: prop.required,
...(prop.default !== "undefined" ? { default: prop.default } : {}),
...(unionOptions(prop.valueType) ? { options: unionOptions(prop.valueType) } : {}),
})),
outputs: ast.outputs.map((output) => ({
name: output.name,
...(output.payload
? { payloadName: output.payload.name, payloadType: output.payload.valueType }
: {}),
})),
functions: ast.runtimeFunctions.map((fn) => ({
name: fn.name,
runtime: fn.runtime,
async: fn.async,
parameters: fn.parameters.map((param) => ({
name: param.name,
type: param.valueType ?? "unknown",
optional: param.optional,
})),
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown"),
})),
states: ast.states.map((state) => ({
name: state.name,
runtime: state.runtime,
type: state.valueType ?? "unknown",
initializer: state.expr,
})),
computed: ast.computed.map((entry) => ({
name: entry.name,
type: entry.valueType ?? "unknown",
expression: entry.expr,
})),
imports: ast.structuredImports.map((entry) => ({
source: entry.source,
typeOnly: entry.typeOnly,
...(entry.defaultImport ? { defaultImport: entry.defaultImport } : {}),
namedImports: entry.namedImports.map((named) => named.local),
})),
};
}
},
"packages/compiler/src/import-resolver.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveWrnImport = resolveWrnImport;
exports.resolveWrnImports = resolveWrnImports;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
function candidates(path) {
return (0, node_path_1.extname)(path)
? [path]
: [
path,
`${path}.wrn`,
`${path}.ts`,
`${path}.tsx`,
`${path}.d.ts`,
(0, node_path_1.join)(path, "index.wrn"),
(0, node_path_1.join)(path, "index.ts"),
(0, node_path_1.join)(path, "index.tsx"),
];
}
function resolveWrnImport(declaration, importer, options) {
const source = declaration.source;
const aliases = {
"@": "./app",
...(options.aliases ?? {}),
};
const alias = Object.keys(aliases)
.filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`)))
.sort((left, right) => right.length - left.length)[0];
if (!source.startsWith(".") && !alias)
return { declaration, resolved: source };
const base = alias
? (0, node_path_1.resolve)(options.appRoot, aliases[alias], source === alias ? "" : source.slice(alias.length + 1))
: (0, node_path_1.resolve)((0, node_path_1.dirname)(importer), source);
const found = candidates(base).find((candidate) => {
if (!(0, node_fs_1.existsSync)(candidate))
return false;
try {
return (0, node_fs_1.statSync)(candidate).isFile();
}
catch {
return false;
}
});
if (found) {
const resolved = (0, node_fs_1.realpathSync)(found);
return resolved.endsWith(".tsx")
? { declaration, resolved, kind: "island" }
: { declaration, resolved };
}
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
return {
declaration,
diagnostic: {
code: "WRN-IMPORT-NOT-FOUND",
message: `Cannot resolve import '${source}' from ${importer}`,
severity,
},
};
}
function resolveWrnImports(declarations, importer, options) {
return declarations.map((declaration) => resolveWrnImport(declaration, importer, options));
}
},
"packages/compiler/src/index.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/**
* @wrnexus/compiler — the `.wrn` language compiler.
*
* Parsing and language diagnostics are provided by the canonical
* `@wrnexus/syntax` package. This package owns platform-specific codegen.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.routeNeedsIslands = exports.generateIslandEntry = exports.buildIslands = exports.assertReactAvailable = exports.serializeIslandProps = exports.renderIslandMarker = exports.parseIslandStrategy = exports.islandPropValue = exports.islandNamesFrom = exports.DependencyGraph = exports.createCompilationCache = exports.compilationKey = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.runtimeCapabilities = exports.analyzeRuntimeImports = exports.optimizeAst = exports.analyzeRuntimeRequirements = exports.analyzeOptimizations = exports.createWrnSourceMap = exports.resolveWrnImports = exports.resolveWrnImport = exports.createComponentContract = exports.generateStoreModule = exports.generateStoreBrowserModule = exports.generateDeclarations = exports.rpcManifest = exports.generateServerFunctionsModule = exports.stripBrowserTypes = exports.generateBrowserModule = exports.generateTargets = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = exports.formatWrn = void 0;
exports.compileNativeWrnFile = compileNativeWrnFile;
exports.compileWrnFile = compileWrnFile;
exports.compile = compile;
const syntax_1 = require("@wrnexus/syntax");
var syntax_2 = require("@wrnexus/syntax");
Object.defineProperty(exports, "formatWrn", { enumerable: true, get: function () { return syntax_2.formatWrn; } });
const codegen_ts_1 = require("./codegen.js");
const native_codegen_ts_1 = require("./native-codegen.js");
var syntax_3 = require("@wrnexus/syntax");
Object.defineProperty(exports, "assertValidAst", { enumerable: true, get: function () { return syntax_3.assertValidAst; } });
Object.defineProperty(exports, "diagnose", { enumerable: true, get: function () { return syntax_3.diagnose; } });
Object.defineProperty(exports, "diagnosticFromError", { enumerable: true, get: function () { return syntax_3.diagnosticFromError; } });
Object.defineProperty(exports, "formatDiagnostic", { enumerable: true, get: function () { return syntax_3.formatDiagnostic; } });
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return syntax_3.parse; } });
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return syntax_3.ParseError; } });
var codegen_ts_2 = require("./codegen.js");
Object.defineProperty(exports, "generate", { enumerable: true, get: function () { return codegen_ts_2.generate; } });
var targets_ts_1 = require("./targets.js");
Object.defineProperty(exports, "generateTargets", { enumerable: true, get: function () { return targets_ts_1.generateTargets; } });
var client_codegen_ts_1 = require("./client-codegen.js");
Object.defineProperty(exports, "generateBrowserModule", { enumerable: true, get: function () { return client_codegen_ts_1.generateBrowserModule; } });
var browser_transpile_ts_1 = require("./browser-transpile.js");
Object.defineProperty(exports, "stripBrowserTypes", { enumerable: true, get: function () { return browser_transpile_ts_1.stripBrowserTypes; } });
var server_codegen_ts_1 = require("./server-codegen.js");
Object.defineProperty(exports, "generateServerFunctionsModule", { enumerable: true, get: function () { return server_codegen_ts_1.generateServerFunctionsModule; } });
Object.defineProperty(exports, "rpcManifest", { enumerable: true, get: function () { return server_codegen_ts_1.rpcManifest; } });
var type_codegen_ts_1 = require("./type-codegen.js");
Object.defineProperty(exports, "generateDeclarations", { enumerable: true, get: function () { return type_codegen_ts_1.generateDeclarations; } });
var store_codegen_ts_1 = require("./store-codegen.js");
Object.defineProperty(exports, "generateStoreBrowserModule", { enumerable: true, get: function () { return store_codegen_ts_1.generateStoreBrowserModule; } });
Object.defineProperty(exports, "generateStoreModule", { enumerable: true, get: function () { return store_codegen_ts_1.generateStoreModule; } });
var component_contract_ts_1 = require("./component-contract.js");
Object.defineProperty(exports, "createComponentContract", { enumerable: true, get: function () { return component_contract_ts_1.createComponentContract; } });
var import_resolver_ts_1 = require("./import-resolver.js");
Object.defineProperty(exports, "resolveWrnImport", { enumerable: true, get: function () { return import_resolver_ts_1.resolveWrnImport; } });
Object.defineProperty(exports, "resolveWrnImports", { enumerable: true, get: function () { return import_resolver_ts_1.resolveWrnImports; } });
var source_map_ts_1 = require("./source-map.js");
Object.defineProperty(exports, "createWrnSourceMap", { enumerable: true, get: function () { return source_map_ts_1.createWrnSourceMap; } });
var analysis_ts_1 = require("./analysis.js");
Object.defineProperty(exports, "analyzeOptimizations", { enumerable: true, get: function () { return analysis_ts_1.analyzeOptimizations; } });
Object.defineProperty(exports, "analyzeRuntimeRequirements", { enumerable: true, get: function () { return analysis_ts_1.analyzeRuntimeRequirements; } });
Object.defineProperty(exports, "optimizeAst", { enumerable: true, get: function () { return analysis_ts_1.optimizeAst; } });
var runtime_capabilities_ts_1 = require("./runtime-capabilities.js");
Object.defineProperty(exports, "analyzeRuntimeImports", { enumerable: true, get: function () { return runtime_capabilities_ts_1.analyzeRuntimeImports; } });
Object.defineProperty(exports, "runtimeCapabilities", { enumerable: true, get: function () { return runtime_capabilities_ts_1.runtimeCapabilities; } });
var native_codegen_ts_2 = require("./native-codegen.js");
Object.defineProperty(exports, "generateNative", { enumerable: true, get: function () { return native_codegen_ts_2.generateNative; } });
Object.defineProperty(exports, "NativeCompileError", { enumerable: true, get: function () { return native_codegen_ts_2.NativeCompileError; } });
var syntax_4 = require("@wrnexus/syntax");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return syntax_4.Lexer; } });
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return syntax_4.LexError; } });
var syntax_5 = require("@wrnexus/syntax");
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return syntax_5.eraseFunctionTypes; } });
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return syntax_5.inferredRuntimeType; } });
Object.defineProperty(exports, "runtimeTypeOf", { enumerable: true, get: function () { return syntax_5.runtimeTypeOf; } });
/** Compile `.wrn` source into an Expo Router React Native screen. */
function compileNativeWrnFile(source) {
const ast = (0, syntax_1.parse)(source);
(0, syntax_1.assertValidAst)(ast);
return (0, native_codegen_ts_1.generateNative)(ast);
}
/**
* Compile `.wrn` source into TypeScript source. Errors include a stable code,
* source location, code frame, and actionable hint whenever available.
*/
function compileWrnFile(source, filePath = "<inline .wrn>") {
try {
const ast = (0, syntax_1.parse)(source);
(0, syntax_1.assertValidAst)(ast, { file: filePath, accessibility: true });
return `// compiled from .wrn\n${(0, codegen_ts_1.generate)(ast)}`;
}
catch (error) {
const diagnostic = (0, syntax_1.diagnosticFromError)(source, error, { file: filePath });
throw new Error(`Failed to parse ${filePath}:\n\n${(0, syntax_1.formatDiagnostic)(source, diagnostic)}`, {
cause: error,
});
}
}
/** Richer entry point returning the AST and structured diagnostics. */
function compile(source, filePath = "<inline .wrn>") {
const richDiagnostics = (0, syntax_1.diagnose)(source, { file: filePath, accessibility: true });
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
if (errors.length > 0) {
throw new syntax_1.ParseError(errors.map((diagnostic) => diagnostic.message).join("\n"), errors[0].code);
}
const ast = (0, syntax_1.parse)(source);
return {
code: `// compiled from .wrn\n${(0, codegen_ts_1.generate)(ast)}`,
ast,
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
richDiagnostics,
};
}
var cache_ts_1 = require("./cache.js");
Object.defineProperty(exports, "compilationKey", { enumerable: true, get: function () { return cache_ts_1.compilationKey; } });
Object.defineProperty(exports, "createCompilationCache", { enumerable: true, get: function () { return cache_ts_1.createCompilationCache; } });
Object.defineProperty(exports, "DependencyGraph", { enumerable: true, get: function () { return cache_ts_1.DependencyGraph; } });
var island_codegen_ts_1 = require("./island-codegen.js");
Object.defineProperty(exports, "islandNamesFrom", { enumerable: true, get: function () { return island_codegen_ts_1.islandNamesFrom; } });
Object.defineProperty(exports, "islandPropValue", { enumerable: true, get: function () { return island_codegen_ts_1.islandPropValue; } });
Object.defineProperty(exports, "parseIslandStrategy", { enumerable: true, get: function () { return island_codegen_ts_1.parseIslandStrategy; } });
Object.defineProperty(exports, "renderIslandMarker", { enumerable: true, get: function () { return island_codegen_ts_1.renderIslandMarker; } });
Object.defineProperty(exports, "serializeIslandProps", { enumerable: true, get: function () { return island_codegen_ts_1.serializeIslandProps; } });
var island_bundle_ts_1 = require("./island-bundle.js");
Object.defineProperty(exports, "assertReactAvailable", { enumerable: true, get: function () { return island_bundle_ts_1.assertReactAvailable; } });
Object.defineProperty(exports, "buildIslands", { enumerable: true, get: function () { return island_bundle_ts_1.buildIslands; } });
Object.defineProperty(exports, "generateIslandEntry", { enumerable: true, get: function () { return island_bundle_ts_1.generateIslandEntry; } });
var analysis_ts_2 = require("./analysis.js");
Object.defineProperty(exports, "routeNeedsIslands", { enumerable: true, get: function () { return analysis_ts_2.routeNeedsIslands; } });
},
"packages/compiler/src/island-bundle.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateIslandEntry = generateIslandEntry;
exports.reactJsxPlugin = reactJsxPlugin;
exports.assertReactAvailable = assertReactAvailable;
exports.buildIslands = buildIslands;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const node_module_1 = require("node:module");
const node_path_1 = require("node:path");
/**
* Generates the per-island browser entry.
*
* Never imports react-dom/server — islands are client-only.
*/
function generateIslandEntry(input) {
return [
"/** @jsxImportSource react */",
`import Component from ${JSON.stringify(input.sourcePath)};`,
`export const name = ${JSON.stringify(input.name)};`,
`export default Component;`,
"",
].join("\n");
}
/**
* Compiles every island `.tsx` against React's JSX runtime.
*
* The repo's root tsconfig sets `jsxImportSource` to `@wrnexus/core`, so an
* island would otherwise compile to WRNexus's HTML-string renderer and
* silently never mount. A `@jsxImportSource` pragma applies only to the file
* that carries it, so putting one in the generated entry does nothing for the
* author's own component — the injection has to happen per source file, which
* is what this plugin does. App-authored islands stay plain `.tsx`.
*/
function reactJsxPlugin() {
return {
name: "wrnexus-island-jsx",
setup(build) {
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
// Only first-party island sources need the pragma. Third-party .tsx
// under node_modules is left alone so Bun's own handling is untouched.
if (args.path.includes("node_modules"))
return undefined;
return {
contents: `/** @jsxImportSource react */\n${await Bun.file(args.path).text()}`,
loader: "tsx",
};
});
},
};
}
function assertReactAvailable(appRoot) {
const require = (0, node_module_1.createRequire)((0, node_path_1.join)(appRoot, "package.json"));
try {
require.resolve("react");
require.resolve("react-dom");
return null;
}
catch {
return {
code: "WRN-ISLAND-REACT-MISSING",
severity: "error",
message: "This app imports a .tsx island but react and react-dom are not installed. " +
"Run: bun add react react-dom",
};
}
}
/**
* Bundles island entries. `splitting: true` is required so React is emitted
* once as a shared chunk rather than duplicated into every island.
*/
async function buildIslands(input) {
if (input.islands.length === 0)
return { assets: [], sharedChunks: [] };
// Each island gets its own generated entry file named after the island.
// Passing the component sources directly would dedupe two islands that share
// a source file, and output order is not guaranteed to match input order —
// both of which silently mismatch island names to bundles.
//
// The entries live inside outDir so `react` resolves from the app that
// installed it, exactly as the island's own imports do.
const entryDir = (0, node_path_1.join)(input.outDir, ".entries");
(0, node_fs_1.mkdirSync)(entryDir, { recursive: true });
const islandNames = new Set(input.islands.map((island) => island.name));
for (const island of input.islands) {
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(entryDir, `${island.name}.tsx`), generateIslandEntry(island), "utf8");
}
const entrypoints = input.islands.map((island) => (0, node_path_1.join)(entryDir, `${island.name}.tsx`));
if (input.appRoot) {
const resolveFrom = (0, node_module_1.createRequire)((0, node_path_1.join)(input.appRoot, "package.json"));
const runtimeEntry = (0, node_path_1.join)(entryDir, "runtime.ts");
(0, node_fs_1.writeFileSync)(runtimeEntry, `export * from ${JSON.stringify(resolveFrom.resolve("@wrnexus/react/browser"))};
`, "utf8");
entrypoints.push(runtimeEntry);
}
try {
const result = await Bun.build({
entrypoints,
outdir: input.outDir,
target: "browser",
format: "esm",
splitting: true,
minify: true,
plugins: [reactJsxPlugin()],
});
if (!result.success) {
throw new AggregateError(result.logs, "Island bundling failed");
}
const assets = [];
const sharedChunks = [];
for (const output of result.outputs) {
if (output.kind === "entry-point") {
// Bun names an entry's output after its entry file, so the basename
// identifies the island unambiguously.
const stem = (0, node_path_1.basename)(output.path).replace(/\.js$/, "");
if (stem === "runtime")
continue;
if (!islandNames.has(stem))
continue;
assets.push({
name: stem,
hash: (0, node_crypto_1.createHash)("sha256").update(output.path).digest("hex").slice(0, 16),
path: output.path,
});
}
else if (output.kind === "chunk") {
sharedChunks.push(output.path);
}
}
return { assets, sharedChunks };
}
finally {
(0, node_fs_1.rmSync)(entryDir, { recursive: true, force: true });
}
}
},
"packages/compiler/src/island-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIslandStrategy = parseIslandStrategy;
exports.islandPropValue = islandPropValue;
exports.serializeIslandProps = serializeIslandProps;
exports.renderIslandMarker = renderIslandMarker;
exports.islandNamesFrom = islandNamesFrom;
// Implemented locally rather than imported from @wrnexus/core: compiler modules
// are bundled into the Node-only VS Code extension, which contains no other
// packages, so a runtime import of core would break the editor compiler.
const HTML_ESCAPES = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
function escapeHtml(value) {
return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
}
const SAFE_ISLAND_NAME = /^[A-Za-z0-9_-]+$/;
function isSafeIslandName(name) {
return SAFE_ISLAND_NAME.test(name);
}
const STRATEGIES = {
"client:only": "only",
"client:load": "load",
"client:visible": "visible",
"client:idle": "idle",
};
function parseIslandStrategy(directives) {
for (const directive of directives) {
const match = STRATEGIES[directive];
if (match)
return match;
}
return "only";
}
function unsupportedProp(value) {
const type = typeof value;
if (type === "function" || type === "symbol" || type === "bigint" || type === "undefined") {
return true;
}
if (value === null || type !== "object")
return false;
if (Array.isArray(value))
return value.some(unsupportedProp);
const proto = Object.getPrototypeOf(value);
if (proto !== Object.prototype && proto !== null)
return true;
return Object.values(value).some(unsupportedProp);
}
/**
* Interprets an island attribute value with JSX semantics.
*
* `title="Revenue"` is a string, `start={3}` is a number, `flag` alone is
* `true`. Without this every prop arrives as a string, so `start={3}` would be
* `"3"` and arithmetic in the island silently concatenates.
*
* Returns `dynamic` for a `{…}` value that is not JSON: such expressions are
* evaluated at runtime and cannot cross the serialization boundary.
*/
function islandPropValue(raw) {
if (raw === undefined || raw === "")
return { value: true };
const expression = /^\{([\s\S]*)\}$/.exec(raw);
if (!expression)
return { value: raw };
const inner = expression[1].trim();
try {
return { value: JSON.parse(inner) };
}
catch {
return { dynamic: inner };
}
}
function serializeIslandProps(componentName, props) {
const offenders = Object.entries(props)
.filter(([, value]) => unsupportedProp(value))
.map(([key]) => key);
if (offenders.length > 0) {
return {
diagnostic: {
code: "WRN-ISLAND-PROPS",
severity: "error",
message: `Island '${componentName}' received non-serializable prop(s): ${offenders.join(", ")}. ` +
`Island props cross a serialization boundary and must be JSON-safe ` +
`(no functions, symbols, bigints, undefined, or class instances).`,
},
};
}
return { json: JSON.stringify(props) };
}
function renderIslandMarker(input) {
// The name becomes a path segment when the browser fetches
// /__wrnexus/island/<name>.js, so reuse the framework's conservative charset
// rather than relying on escaping alone.
if (!isSafeIslandName(input.name)) {
throw new Error(`Island name '${input.name}' is not a safe identifier. ` +
`Island names may only contain letters, digits, underscores, and hyphens.`);
}
return (`<div data-wrn-island="${escapeHtml(input.name)}"` +
` data-wrn-island-strategy="${input.strategy}"` +
` data-wrn-island-props="${escapeHtml(input.propsJson)}"></div>`);
}
/**
* Local binding names introduced by island imports.
*
* Codegen sees only component tag names, so it needs the set of names that came
* from `.tsx` imports to tell an island apart from a `.wrn` component.
*/
function islandNamesFrom(imports) {
const names = new Set();
for (const entry of imports) {
if (entry.kind !== "island")
continue;
const local = entry.declaration.defaultImport;
if (local)
names.add(local);
}
return names;
}
},
"packages/compiler/src/native-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeCompileError = void 0;
exports.generateNative = generateNative;
const syntax_1 = require("@wrnexus/syntax");
class NativeCompileError extends Error {
constructor(message) {
super(message);
this.name = "NativeCompileError";
}
}
exports.NativeCompileError = NativeCompileError;
const 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",
};
const 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 segment of (0, syntax_1.extractInterpolations)(value)) {
if (segment.start > last)
pieces.push(value.slice(last, segment.start));
const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
last = segment.end;
}
pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
}
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 renderAttrs(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 expr = expression(attr.value);
return ` source={${expr ? `{ uri: ${expr} }` : `{ 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 renderNode(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) => renderNode(child, states, index === 0 ? (node.index ?? "__index") : undefined))
.join("");
const empty = node.empty.map((child) => renderNode(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) => renderNode(child, states)).join("")}</>`
: `(${branch.cond}) ? <>${branch.body.map((child) => renderNode(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 = renderAttrs(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 renderNode(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(",\n")} });`;
}
/** Compile a parsed `.wrn` page to an Expo Router React Native screen. */
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.valueType ? `<${state.valueType}>` : ""}(${state.expr});`;
})
.join("\n");
const body = ast.view.map((node) => renderNode(node, states)).join("");
const typeSource = ast.types
.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${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`;
}
},
"packages/compiler/src/runtime-capabilities.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runtimeCapabilities = runtimeCapabilities;
exports.analyzeRuntimeImports = analyzeRuntimeImports;
const CAPABILITIES = {
bun: new Set([
"filesystem",
"tcp",
"process",
"websocket",
"crypto",
"streams",
"background-tasks",
]),
node: new Set([
"filesystem",
"tcp",
"process",
"websocket",
"crypto",
"streams",
"background-tasks",
]),
edge: new Set(["websocket", "crypto", "streams", "background-tasks"]),
worker: new Set(["websocket", "crypto", "streams", "background-tasks"]),
"service-worker": new Set(["crypto", "streams", "background-tasks"]),
browser: new Set(["websocket", "crypto", "streams"]),
};
const MODULE_CAPABILITIES = [
[/^(?:node:)?(?:fs|path|os)(?:\/|$)/, "filesystem"],
[/^(?:node:)?(?:net|tls|dgram|http2)(?:\/|$)/, "tcp"],
[/^(?:node:)?(?:child_process|cluster|worker_threads)(?:\/|$)/, "process"],
];
function runtimeCapabilities(runtime) {
return CAPABILITIES[runtime];
}
function analyzeRuntimeImports(source, runtime) {
const modules = [
...source.matchAll(/\b(?:import\s+(?:[\s\S]*?\s+from\s+)?|require\s*\()\s*["']([^"']+)["']/g),
].map((match) => match[1]);
const available = runtimeCapabilities(runtime);
return modules.flatMap((module) => {
const requirement = MODULE_CAPABILITIES.find(([pattern]) => pattern.test(module));
if (!requirement || available.has(requirement[1]))
return [];
return [
{
code: "WRN-RUNTIME-CAPABILITY",
runtime,
module,
capability: requirement[1],
message: `Module '${module}' requires ${requirement[1]}, which is unavailable in the ${runtime} runtime.`,
},
];
});
}
},
"packages/compiler/src/server-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.remotelyReferencedServerFunctions = remotelyReferencedServerFunctions;
exports.rpcManifest = rpcManifest;
exports.generateServerFunctionsModule = generateServerFunctionsModule;
const syntax_1 = require("@wrnexus/syntax");
function stableId(value) {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return `wrn_${(hash >>> 0).toString(36)}`;
}
/**
* Remote exposure is reference based in v0.6. A server function is included in
* the RPC manifest only when browser-capable code calls `server.<name>(...)`.
* Server functions remain available to SSR/server modules without becoming
* remotely callable by default.
*/
function remotelyReferencedServerFunctions(ast) {
const browserSources = ast.runtimeFunctions
.filter((fn) => ["client", "shared"].includes(fn.runtime))
.map((fn) => fn.body);
for (const [hook, body] of Object.entries(ast.storeLifecycle)) {
if (hook !== "serverInit" && body)
browserSources.push(body);
}
const names = new Set();
const call = /\bserver\.([A-Za-z_$][\w$]*)\s*\(/g;
for (const source of browserSources) {
for (const match of source.matchAll(call))
names.add(match[1]);
}
return names;
}
function rpcManifest(ast) {
const exposed = remotelyReferencedServerFunctions(ast);
return ast.runtimeFunctions
.filter((fn) => fn.runtime === "server" && exposed.has(fn.name))
.map((fn) => ({
id: stableId(`${ast.name}:${fn.name}`),
component: ast.name,
function: fn.name,
parameters: fn.parameters.map((param) => ({
name: param.name,
type: param.valueType ?? "unknown",
optional: param.optional,
})),
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown"),
}));
}
function generateServerFunctionsModule(ast) {
const source = ast.functions
.map((body) => (0, syntax_1.stripRuntimeFunctionModifiers)(body, ["server", "shared"]))
.filter(Boolean)
.join("\n\n");
const names = ast.runtimeFunctions
.filter((fn) => ["server", "shared"].includes(fn.runtime))
.map((fn) => fn.name);
const manifest = rpcManifest(ast);
return `// generated WRNexusJS server module for ${ast.name}\n${source}\n\nexport const __wrnexusServerFunctions = { ${[...new Set(names)].join(", ")} };\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n`;
}
},
"packages/compiler/src/source-map.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWrnSourceMap = createWrnSourceMap;
function createWrnSourceMap(source, generated) {
const sourceLines = source.split(/\r?\n/).length;
const generatedLines = generated.split(/\r?\n/).length;
const mappings = Array.from({ length: Math.min(sourceLines, generatedLines) }, (_, index) => ({
generatedLine: index + 1,
sourceLine: index + 1,
sourceColumn: 1,
kind: "line",
}));
return { version: 1, source, generated, mappings };
}
},
"packages/compiler/src/store-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateStoreModule = generateStoreModule;
exports.generateStoreBrowserModule = generateStoreBrowserModule;
const syntax_1 = require("@wrnexus/syntax");
const type_codegen_ts_1 = require("./type-codegen.js");
const server_codegen_ts_1 = require("./server-codegen.js");
const RESERVED_BINDINGS = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
function safeBinding(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function stateObject(ast, runtime) {
const entries = ast.states
.filter((state) => state.runtime === runtime)
.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`);
return `{ ${entries.join(", ")} }`;
}
function actionSource(fn, stateNames, eraseTypes = false) {
const parameterNames = new Set(fn.parameters.map((param) => param.name));
const params = fn.parameters.map((param) => param.name).join(", ");
const aliases = stateNames.filter((name) => safeBinding(name) && !parameterNames.has(name));
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
const runtimeAliases = ["server"]
.filter((name) => !parameterNames.has(name))
.map((name) => `const ${name} = context.${name};`)
.join("\n");
const copyBack = aliases.map((name) => `context.state.${name} = ${name};`).join("\n");
const body = eraseTypes ? (0, syntax_1.eraseFunctionTypes)(fn.body) : fn.body;
return `{ runtime: ${JSON.stringify(fn.runtime)}, handler: ${fn.async ? "async " : ""}function(context${params ? `, ${params}` : ""}) { ${runtimeAliases}\n${aliasSource}\ntry { ${body} } finally { ${copyBack} } } }`;
}
function persistedCallback(source, functionName) {
if (!source?.trim())
return undefined;
const body = (0, syntax_1.eraseFunctionTypes)(source);
if (functionName === "migrate") {
return `(value, fromVersion, toVersion) => {\n${body}\nif (typeof migrate === "function") return migrate(value, fromVersion, toVersion);\nreturn value;\n}`;
}
return `(value) => {\n${body}\nif (typeof validate === "function") return validate(value);\nreturn value && typeof value === "object" && !Array.isArray(value) ? value : null;\n}`;
}
function persistenceSource(ast) {
if (!ast.persist)
return "undefined";
const migrate = persistedCallback(ast.persist.migrations, "migrate");
const validate = persistedCallback(ast.persist.validation, "validate");
return `{ storage: ${JSON.stringify(ast.persist.storage)}, include: ${JSON.stringify(ast.persist.include)}, version: ${ast.persist.version}${migrate ? `, migrate: ${migrate}` : ""}${validate ? `, validate: ${validate}` : ""} }`;
}
function lifecycleSource(ast, stateNames, browser) {
return Object.entries(ast.storeLifecycle)
.filter(([name]) => !browser || name !== "serverInit")
.map(([name, body]) => {
const aliases = stateNames.filter(safeBinding);
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
const runtimeAliases = browser ? "const server = context.server;" : "";
const copyBack = aliases.map((state) => `context.state.${state} = ${state};`).join("\n");
const emittedBody = browser ? (0, syntax_1.eraseFunctionTypes)(body) : body;
return `${name}: async (context) => { ${runtimeAliases} ${aliasSource} try { ${emittedBody} } finally { ${copyBack} } }`;
})
.join(",\n");
}
function generateStoreModule(ast) {
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
throw new Error("generateStoreModule requires a store AST");
}
const stateNames = ast.states.map((state) => state.name);
const safeStateNames = stateNames.filter(safeBinding);
const computed = ast.computed
.map((entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`)
.join(",\n");
const actionGroups = new Map();
for (const fn of ast.runtimeFunctions) {
const group = actionGroups.get(fn.name) ?? [];
group.push(fn);
actionGroups.set(fn.name, group);
}
const actions = Array.from(actionGroups, ([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames)).join(", ")}]`).join(",\n");
const persistence = persistenceSource(ast);
const lifecycle = lifecycleSource(ast, stateNames, false);
const manifest = (0, server_codegen_ts_1.rpcManifest)(ast);
const remoteFunctions = manifest.map((entry) => entry.function);
const rpcWrappers = remoteFunctions
.map((name) => `${JSON.stringify(name)}: async (...received) => {
const rpcContext = received.pop();
if (!rpcContext || !rpcContext.request) throw new Error("WRN-RPC-CONTEXT: request context is required");
let container = __wrnexusRpcContainers.get(rpcContext.request);
if (!container) {
const url = new URL(rpcContext.request.url);
container = createRequestStoreContainer(rpcContext.request, url.pathname + url.search);
__wrnexusRpcContainers.set(rpcContext.request, container);
}
const store = await container.use(${ast.name});
const action = store.actions[${JSON.stringify(name)}];
if (typeof action !== "function") throw new Error(${JSON.stringify(`WRN-RPC-FUNCTION: store action '${name}' is unavailable on the server`)});
return action(...received);
}`)
.join(",\n");
return `${ast.imports.join("\n")}\nimport { defineStore } from "@wrnexus/store";\nimport { createRequestStoreContainer } from "@wrnexus/store/server";\n\n${ast.types.join("\n\n")}\n\nexport const ${ast.name} = defineStore({\n name: ${JSON.stringify(ast.name)},\n kind: ${JSON.stringify(ast.storeKind)},\n createSharedState: () => (${stateObject(ast, "shared")}),\n createClientState: () => (${stateObject(ast, "client")}),\n createServerState: () => (${stateObject(ast, "server")}),\n computed: { ${computed} },\n actions: { ${actions} },\n persist: ${persistence},\n lifecycle: { ${lifecycle} },\n});\n\nexport default ${ast.name};\n\nconst __wrnexusRpcContainers = new WeakMap();\nexport const __wrnexusServerFunctions = {\n${rpcWrappers}\n};\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n\n${(0, type_codegen_ts_1.generateDeclarations)(ast)}\n`;
}
/** Standalone browser artifact for an imported `.wrn` store. */
function generateStoreBrowserModule(ast) {
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
throw new Error("generateStoreBrowserModule requires a store AST");
}
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const stateNames = browserStates.map((state) => state.name);
const safeStateNames = stateNames.filter(safeBinding);
const initialState = `{ ${browserStates.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`).join(", ")} }`;
const computed = ast.computed
.map((entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`)
.join(",\n");
const groups = new Map();
for (const fn of ast.runtimeFunctions.filter((entry) => ["client", "shared"].includes(entry.runtime))) {
const group = groups.get(fn.name) ?? [];
group.push(fn);
groups.set(fn.name, group);
}
const actions = Array.from(groups, ([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames, true)).join(", ")}]`).join(",\n");
const persistence = persistenceSource(ast);
const lifecycleEntries = lifecycleSource(ast, stateNames, true);
return `// generated WRNexusJS browser store module for ${ast.name}
const __root = globalThis;
const __registry = __root.__wrnexusStoreRegistry || (__root.__wrnexusStoreRegistry = new Map());
const __hydrationNode = typeof document !== "undefined" ? document.querySelector("script[data-wrnexus-store-hydration]") : null;
let __hydration = {};
try { __hydration = __hydrationNode ? JSON.parse(__hydrationNode.textContent || "{}") : {}; } catch (_) {}
function __clone(value) { try { return structuredClone(value); } catch (_) { return JSON.parse(JSON.stringify(value)); } }
function __storage(kind) { if (typeof window === "undefined") return null; return kind === "local" ? window.localStorage : kind === "session" ? window.sessionStorage : null; }
function __diagnostic(code, message, details) {
const detail = { code, message, store: ${JSON.stringify(ast.name)}, details };
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-diagnostic", { detail })); } catch (_) {}
if (typeof console !== "undefined" && console.warn) console.warn("[wrnexus:store] " + code + ": " + message, details || "");
}
function __csrfToken() {
if (typeof document === "undefined") return undefined;
const match = /(?:^|;\\s*)wrn-csrf=([^;]+)/.exec(document.cookie || "");
return match ? decodeURIComponent(match[1]) : undefined;
}
async function __callServerFunction(storeName, functionName, args, options) {
options = options || {};
const csrf = options.csrfToken || __csrfToken();
const traceId = options.traceId || (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()));
const response = await fetch(options.endpoint || "/__wrnexus/rpc", {
method: "POST",
credentials: "same-origin",
signal: options.signal,
headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}),
body: JSON.stringify({ component: storeName, function: functionName, args: args }),
});
const payload = await response.json().catch(function () { return null; });
if (!response.ok || !payload || !payload.ok) {
const error = new Error(payload && payload.error && payload.error.message || "Server call failed (" + response.status + ")");
error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED";
error.status = response.status;
error.details = payload && payload.error && payload.error.details;
error.traceId = payload && payload.error && payload.error.traceId || traceId;
throw error;
}
return payload.value;
}
function __compatible(expected, value) {
if (expected === null || value === null) return expected === value || expected === null;
if (Array.isArray(expected)) return Array.isArray(value);
return typeof expected === typeof value;
}
function __create(definition) {
const routeId = typeof location !== "undefined" ? location.pathname + location.search : "default";
const key = definition.kind === "page" ? definition.name + "@" + routeId : definition.name;
if (__registry.has(key)) return __registry.get(key);
let currentDefinition = definition;
const listeners = new Set();
const initial = currentDefinition.createState();
let restored = null;
if (currentDefinition.persist) {
try {
const storage = __storage(currentDefinition.persist.storage);
const rawValue = storage && storage.getItem("wrnexus:store:" + currentDefinition.name);
const parsed = rawValue ? JSON.parse(rawValue) : null;
if (parsed) {
let candidate = parsed.state;
const fromVersion = Number(parsed.version || 0);
if (fromVersion !== currentDefinition.persist.version) {
if (typeof currentDefinition.persist.migrate === "function") candidate = currentDefinition.persist.migrate(candidate, fromVersion, currentDefinition.persist.version);
else { __diagnostic("WRN-PERSIST-VERSION", "Persisted state version cannot be restored without a migration.", { fromVersion, toVersion: currentDefinition.persist.version }); candidate = null; }
}
if (candidate && typeof currentDefinition.persist.validate === "function") candidate = currentDefinition.persist.validate(candidate);
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
restored = {};
currentDefinition.persist.include.forEach(function (name) { if (Object.prototype.hasOwnProperty.call(candidate, name)) restored[name] = candidate[name]; });
} else if (candidate != null) {
__diagnostic("WRN-PERSIST-INVALID", "Persisted state failed validation and was reset.", candidate);
}
}
} catch (error) { __diagnostic("WRN-PERSIST-RESTORE", "Persisted state could not be restored and was reset.", error); }
}
const raw = Object.assign({}, initial, restored || {}, __hydration[currentDefinition.name] || {});
let mutable = false;
let actionName = "direct";
function persistState() {
if (!currentDefinition.persist) return;
try {
const picked = {};
currentDefinition.persist.include.forEach(function (name) { picked[name] = raw[name]; });
const storage = __storage(currentDefinition.persist.storage);
if (storage) storage.setItem("wrnexus:store:" + currentDefinition.name, JSON.stringify({ version: currentDefinition.persist.version, state: picked }));
} catch (error) { __diagnostic("WRN-PERSIST-WRITE", "Persisted state could not be written.", error); }
}
const state = new Proxy(raw, {
set(target, property, value) {
if (!mutable) throw new TypeError("WRN-STORE-READONLY: " + currentDefinition.name + "." + String(property) + " must be changed by a store action.");
if (Object.is(target[property], value)) return true;
target[property] = value;
persistState();
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: actionName, changed: [String(property)] }); });
return true;
},
deleteProperty(target, property) {
if (!mutable) throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions");
return Reflect.deleteProperty(target, property);
},
});
const actions = {};
const server = new Proxy({}, { get: function (_target, property) { return function () { return __callServerFunction(currentDefinition.name, String(property), Array.prototype.slice.call(arguments)); }; } });
function installActions() {
Object.keys(actions).forEach(function (name) { delete actions[name]; });
Object.entries(currentDefinition.actions || {}).forEach(function (pair) {
const name = pair[0], candidates = pair[1];
const selected = candidates.find(function (entry) { return entry.runtime === "client"; }) || candidates.find(function (entry) { return entry.runtime === "shared"; });
if (!selected) return;
actions[name] = async function () {
const args = Array.prototype.slice.call(arguments);
const previousMutable = mutable, previousAction = actionName;
mutable = true; actionName = name;
try { return await selected.handler({ state, snapshot: function () { return __clone(state); }, reset: function () { return instance.reset(); }, runtime: "client", routeId, server }, ...args); }
finally { mutable = previousMutable; actionName = previousAction; }
};
});
}
installActions();
const core = {
name: currentDefinition.name,
kind: currentDefinition.kind,
state,
actions,
whenReady: Promise.resolve(),
reset() {
mutable = true; actionName = "$reset";
try {
const next = currentDefinition.createState();
Object.keys(raw).forEach(function (name) { if (!(name in next)) delete raw[name]; });
Object.assign(raw, next); persistState();
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$reset", changed: Object.keys(next) }); });
} finally { mutable = false; actionName = "direct"; }
},
snapshot() { return Object.freeze(__clone(raw)); },
subscribe(listener) { listeners.add(listener); return function () { listeners.delete(listener); }; },
async dispose() {
mutable = true; actionName = "$dispose";
try { await currentDefinition.lifecycle && currentDefinition.lifecycle.dispose && currentDefinition.lifecycle.dispose({ state, runtime: "client", routeId, server }); }
finally { mutable = false; actionName = "direct"; listeners.clear(); __registry.delete(key); }
},
async __hotUpdate(nextDefinition) {
const previous = __clone(raw);
const nextShape = nextDefinition.createState();
const preserved = [], reset = [], added = [], removed = [];
Object.keys(previous).forEach(function (name) {
if (!(name in nextShape)) { removed.push(name); return; }
if (__compatible(nextShape[name], previous[name])) { nextShape[name] = previous[name]; preserved.push(name); }
else reset.push(name);
});
Object.keys(nextShape).forEach(function (name) { if (!(name in previous)) added.push(name); });
currentDefinition = nextDefinition;
mutable = true; actionName = "$hmr";
try {
Object.keys(raw).forEach(function (name) { delete raw[name]; });
Object.assign(raw, nextShape);
installActions(); persistState();
} finally { mutable = false; actionName = "direct"; }
const result = { store: currentDefinition.name, preserved, reset, added, removed };
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$hmr", changed: added.concat(reset, removed) }); });
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-hmr", { detail: result })); } catch (_) {}
return result;
},
};
const instance = new Proxy(core, {
get(target, property, receiver) {
if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
if (property in actions) return actions[property];
if (property in (currentDefinition.computed || {})) return currentDefinition.computed[property](state);
return state[property];
},
set() { throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); },
});
__registry.set(key, instance);
const hydrationSource = __hydration[currentDefinition.name];
const init = async function () {
const run = async function (name, hook) {
if (!hook) return;
mutable = true; actionName = name;
try { await hook({ state, runtime: "client", routeId, server }); }
finally { mutable = false; actionName = "direct"; }
};
await run("$clientInit", currentDefinition.lifecycle && currentDefinition.lifecycle.clientInit);
if (hydrationSource) await run("$hydrate", currentDefinition.lifecycle && currentDefinition.lifecycle.hydrate);
};
core.whenReady = init();
return instance;
}
if (!__root.__wrnexusApplyStoreHotUpdate) {
__root.__wrnexusApplyStoreHotUpdate = async function (name, definition) {
const results = [];
for (const item of Array.from(__registry.values())) if (item.name === name && typeof item.__hotUpdate === "function") results.push(await item.__hotUpdate(definition));
return results;
};
}
if (!__root.__wrnexusStoreContainer) {
__root.__wrnexusStoreContainer = {
async disposePageStores() { for (const item of Array.from(__registry.values())) if (item.kind === "page") await item.dispose(); },
async hotUpdate(name, definition) { return __root.__wrnexusApplyStoreHotUpdate(name, definition); },
inspect() { return Array.from(__registry.values()).map(function (item) { return { name: item.name, kind: item.kind, state: item.snapshot() }; }); },
};
}
export const ${ast.name}Definition = {
name: ${JSON.stringify(ast.name)},
kind: ${JSON.stringify(ast.storeKind)},
createState: () => (${initialState}),
computed: { ${computed} },
actions: { ${actions} },
persist: ${persistence},
lifecycle: { ${lifecycleEntries} },
};
export const ${ast.name} = __create(${ast.name}Definition);
export default ${ast.name};
`;
}
},
"packages/compiler/src/targets.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTargets = generateTargets;
const component_contract_ts_1 = require("./component-contract.js");
const client_codegen_ts_1 = require("./client-codegen.js");
const server_codegen_ts_1 = require("./server-codegen.js");
const type_codegen_ts_1 = require("./type-codegen.js");
const store_codegen_ts_1 = require("./store-codegen.js");
function generateTargets(ast) {
return {
server: ast.kind === "global-store" || ast.kind === "page-store"
? (0, store_codegen_ts_1.generateStoreModule)(ast)
: (0, server_codegen_ts_1.generateServerFunctionsModule)(ast),
browser: ast.kind === "global-store" || ast.kind === "page-store"
? (0, store_codegen_ts_1.generateStoreBrowserModule)(ast)
: (0, client_codegen_ts_1.generateBrowserModule)(ast),
declarations: (0, type_codegen_ts_1.generateDeclarations)(ast),
contract: (0, component_contract_ts_1.createComponentContract)(ast),
rpc: (0, server_codegen_ts_1.rpcManifest)(ast),
};
}
},
"packages/compiler/src/type-codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDeclarations = generateDeclarations;
function member(name) {
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
}
function params(astParams) {
return astParams
.map((param) => `${member(param.name)}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`)
.join(", ");
}
function generateDeclarations(ast) {
const inline = ast.types
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
if (ast.kind === "global-store" || ast.kind === "page-store") {
const state = ast.states
.filter((entry) => entry.runtime !== "server")
.map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`)
.join("\n");
const computed = ast.computed
.map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`)
.join("\n");
const actions = ast.runtimeFunctions
.filter((fn) => fn.runtime !== "server")
.map((fn) => ` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`)
.join("\n");
return `${inline ? `${inline}\n\n` : ""}export interface ${ast.name}State {\n${state}\n}\n\nexport interface ${ast.name}Computed {\n${computed}\n}\n\nexport interface ${ast.name}Actions {\n${actions}\n}\n\nexport interface ${ast.name}Instance extends ${ast.name}State, ${ast.name}Computed, ${ast.name}Actions {\n reset(): void;\n snapshot(): Readonly<${ast.name}State>;\n}\n\ndeclare const store: ${ast.name}Instance;\nexport default store;\n`;
}
const props = ast.props
.map((prop) => ` readonly ${member(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`)
.join("\n");
const outputs = ast.outputs
.map((output) => ` ${member(output.name)}(${output.payload ? `${member(output.payload.name)}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`)
.join("\n");
const clientFunctions = ast.runtimeFunctions
.filter((fn) => fn.runtime === "client" || fn.runtime === "shared")
.map((fn) => ` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`)
.join("\n");
const serverFunctions = ast.runtimeFunctions
.filter((fn) => fn.runtime === "server")
.map((fn) => ` ${member(fn.name)}(${params(fn.parameters)}): Promise<Awaited<${fn.returnType ?? "unknown"}>>;`)
.join("\n");
return `${inline ? `${inline}\n\n` : ""}export interface ${ast.name}Props {\n${props}\n}\n\nexport interface ${ast.name}Outputs {\n${outputs}\n}\n\nexport interface ${ast.name}ClientFunctions {\n${clientFunctions}\n}\n\nexport interface ${ast.name}ServerCalls {\n${serverFunctions}\n}\n`;
}
},
"packages/syntax/src/api-sections.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseApiSections = parseApiSections;
exports.hasRequestSection = hasRequestSection;
exports.parseApiEntries = parseApiEntries;
/**
* Parse the sectioned form of an `api` block body.
*
* Returns null when no section keyword is present, which is how the legacy
* bare-body form stays valid: the caller keeps treating the body as the
* response expression.
*
* Detection and slicing both drive the tokenizer's own string/comment-aware
* scanning (`skipLiteralOrComment`, `Lexer.readBalancedBraces`) instead of a
* second hand-rolled brace counter, so a `}` inside a string or a `request {`
* mentioned in a comment can't be mistaken for a real section.
*/
const tokenizer_ts_1 = require("./tokenizer.js");
const SECTION_NAMES = ["request", "response", "error"];
const REQUEST_SUBSECTION_NAMES = ["parameters", "body"];
/**
* Walk `source` at brace-depth 0, looking for `name { ... }` where `name` is
* one of `names`. Strings, template literals, and comments are skipped via
* `skipLiteralOrComment` — the same rules `readBalancedBraces` uses — so a
* keyword mentioned inside a string or comment, or nested inside an unrelated
* `{ }` (e.g. an object literal in a legacy body), is never mistaken for a
* section. Matched blocks are sliced via `Lexer.readBalancedBraces()` itself,
* not a reimplementation of it.
*/
function scanTopLevelBlocks(source, names) {
const found = new Map();
const lx = new tokenizer_ts_1.Lexer(source);
let depth = 0;
let i = 0;
let atLineStart = true;
while (i < source.length) {
const c = source[i];
if (c === "\n") {
atLineStart = true;
i++;
continue;
}
const skipped = (0, tokenizer_ts_1.skipLiteralOrComment)(source, i, atLineStart);
if (skipped !== null) {
i = skipped;
atLineStart = false;
continue;
}
if (c !== " " && c !== "\t" && c !== "\r")
atLineStart = false;
if (depth === 0 && (0, tokenizer_ts_1.isIdentStart)(c)) {
let j = i + 1;
while (j < source.length && (0, tokenizer_ts_1.isIdentPart)(source[j]))
j++;
const word = source.slice(i, j);
// Skip trivia between the identifier and a possible '{' without
// treating anything in between as significant yet.
let k = j;
let lineStartAtK = false;
while (k < source.length) {
const kc = source[k];
if (kc === " " || kc === "\t" || kc === "\r") {
k++;
continue;
}
if (kc === "\n") {
lineStartAtK = true;
k++;
continue;
}
const kSkipped = (0, tokenizer_ts_1.skipLiteralOrComment)(source, k, lineStartAtK);
if (kSkipped !== null) {
k = kSkipped;
lineStartAtK = false;
continue;
}
break;
}
if (names.includes(word) && source[k] === "{") {
lx.pos = k;
const start = k + 1;
const text = lx.readBalancedBraces();
if (!found.has(word))
found.set(word, { text, start });
i = lx.pos;
continue;
}
i = j;
continue;
}
if (c === "{")
depth++;
else if (c === "}")
depth = Math.max(0, depth - 1);
i++;
}
return found;
}
/** Rebase a span captured from `outer.text` back onto the original source. */
function absolutize(span, outer) {
if (!span)
return undefined;
return outer ? { text: span.text, start: outer.start + span.start } : span;
}
/** `name?: string` -> { name, optional, type }. Blank lines and comments are skipped. */
function parseFields(span) {
if (!span)
return [];
const fields = [];
let cursor = 0;
for (const rawLine of span.text.split("\n")) {
const lineOffset = span.start + cursor;
cursor += rawLine.length + 1;
const line = rawLine.trim().replace(/,$/, "");
if (!line || line.startsWith("//"))
continue;
const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(\?)?\s*:\s*(.+)$/.exec(line);
if (!match) {
throw new tokenizer_ts_1.LexError(`Expected "name: type" in an api request section, got "${line}" at offset ${lineOffset}`);
}
fields.push({ name: match[1], optional: match[2] === "?", type: match[3].trim() });
}
return fields;
}
function parseApiSections(source) {
const top = scanTopLevelBlocks(source, SECTION_NAMES);
if (top.size === 0)
return null;
const request = top.get("request");
const sub = request
? scanTopLevelBlocks(request.text, REQUEST_SUBSECTION_NAMES)
: new Map();
return {
parameters: parseFields(absolutize(sub.get("parameters"), request)),
body: parseFields(absolutize(sub.get("body"), request)),
response: top.get("response")?.text ?? "",
error: top.get("error")?.text ?? "",
};
}
/** True when the block declares a `request` section. */
function hasRequestSection(source) {
return scanTopLevelBlocks(source, SECTION_NAMES).has("request");
}
/**
* Parse the body of a page-level `apis { }` container: a sequence of
* `<name> <METHOD> <path> { ... }` entries with no leading `api` keyword
* (the container supplies it). Each entry's braces are sliced with the
* tokenizer's own `Lexer.readBalancedBraces()`, so the same string- and
* comment-aware rules that protect `parseApiSections` apply here too.
*/
function parseApiEntries(source) {
const entries = [];
const lx = new tokenizer_ts_1.Lexer(source);
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident") {
throw new tokenizer_ts_1.LexError(`Expected an api entry name at offset ${nameToken.pos}`);
}
const methodToken = lx.next();
if (methodToken.type !== "ident") {
throw new tokenizer_ts_1.LexError(`Expected an HTTP method after "${nameToken.value}"`);
}
const path = lx.readPath();
const entryBody = lx.readBalancedBraces();
const sections = parseApiSections(entryBody);
if (sections === null) {
throw new tokenizer_ts_1.LexError(`Api entry "${nameToken.value}" has a bare body; declare a "response { }" section instead`);
}
entries.push({
mode: "any",
name: nameToken.value,
method: methodToken.value.toUpperCase(),
path,
body: "",
sections,
});
}
return entries;
}
},
"packages/syntax/src/diagnostics.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.containsReadonlyPropMutation = containsReadonlyPropMutation;
exports.positionAt = positionAt;
exports.classifyParseError = classifyParseError;
exports.diagnosticFromError = diagnosticFromError;
exports.diagnose = diagnose;
exports.assertValidAst = assertValidAst;
exports.isHydrationStrategy = isHydrationStrategy;
exports.isRuntimeTarget = isRuntimeTarget;
exports.formatDiagnostic = formatDiagnostic;
const parser_ts_1 = require("./parser.js");
const spec_ts_1 = require("./spec.js");
function stripAsciiControlAndSpace(value) {
let result = "";
for (const character of value) {
if (character.charCodeAt(0) > 0x20)
result += character;
}
return result;
}
function maskJavaScriptTrivia(source) {
let result = "";
let index = 0;
let quote = null;
let lineComment = false;
let blockComment = false;
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
if (lineComment) {
if (char === "\n") {
lineComment = false;
result += "\n";
}
else
result += " ";
index++;
continue;
}
if (blockComment) {
if (char === "*" && next === "/") {
result += " ";
index += 2;
blockComment = false;
}
else {
result += char === "\n" ? "\n" : " ";
index++;
}
continue;
}
if (quote) {
if (char === "\\") {
result += " ";
index += Math.min(2, source.length - index);
}
else if (char === quote) {
result += " ";
index++;
quote = null;
}
else {
result += char === "\n" ? "\n" : " ";
index++;
}
continue;
}
if (char === "/" && next === "/") {
result += " ";
index += 2;
lineComment = true;
continue;
}
if (char === "/" && next === "*") {
result += " ";
index += 2;
blockComment = true;
continue;
}
if (char === "'" || char === '"' || char === "`") {
quote = char;
result += " ";
index++;
continue;
}
result += char;
index++;
}
return result;
}
function containsReadonlyPropMutation(body, propName, parameterNames) {
const code = maskJavaScriptTrivia(body);
const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const operator = String.raw `(?:\+\+|--|(?:\*\*|&&|\|\||\?\?|[+\-*/%&|^])?=(?!=|>))`;
if (new RegExp(String.raw `\bprops\.${escaped}\s*${operator}`).test(code))
return true;
if (parameterNames.has(propName))
return false;
if (new RegExp(String.raw `\b(?:const|let|var)\s+${escaped}\b`).test(code))
return false;
return new RegExp(String.raw `(?:^|[^\w$.])${escaped}\s*${operator}`, "m").test(code);
}
function positionAt(source, offset) {
const safe = Math.max(0, Math.min(offset, source.length));
const before = source.slice(0, safe);
const lines = before.split(/\r?\n/);
return { offset: safe, line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
}
function offsetFromMessage(message) {
const match = /offset\s+(\d+)/i.exec(message);
return match ? Number(match[1]) : undefined;
}
function classifyParseError(message) {
if (/Expected 'page', 'component', or 'layout'/.test(message))
return spec_ts_1.WRN_DIAGNOSTIC_CODES.root;
if (/Unknown (?:page|component|layout|ssr|client) member/.test(message)) {
return spec_ts_1.WRN_DIAGNOSTIC_CODES.member;
}
if (/prop initializer|Expected eq/.test(message))
return spec_ts_1.WRN_DIAGNOSTIC_CODES.propInitializer;
if (/State '.+' requires an initializer/.test(message)) {
return spec_ts_1.WRN_DIAGNOSTIC_CODES.stateInitializer;
}
if (/Cannot watch undeclared state/.test(message))
return spec_ts_1.WRN_DIAGNOSTIC_CODES.watchUndeclared;
return spec_ts_1.WRN_DIAGNOSTIC_CODES.parse;
}
function diagnosticFromError(source, error, options = {}) {
const message = error instanceof Error ? error.message : String(error);
const offset = error instanceof parser_ts_1.ParseError && error.offset !== undefined
? error.offset
: offsetFromMessage(message);
return {
code: error instanceof parser_ts_1.ParseError ? error.code : classifyParseError(message),
severity: "error",
message,
file: options.file,
...(offset === undefined ? {} : { position: positionAt(source, offset) }),
};
}
function walk(nodes, visit) {
for (const node of nodes) {
visit(node);
if (node.type === "element")
walk(node.children, visit);
else if (node.type === "each") {
walk(node.body, visit);
walk(node.empty, visit);
}
else if (node.type === "if") {
for (const branch of node.branches)
walk(branch.body, visit);
}
}
}
function astDiagnostics(ast, options) {
const diagnostics = [];
const seen = new Map();
for (const [kind, declarations] of [
["prop", ast.props],
["state", ast.states],
["computed", ast.computed],
]) {
for (const declaration of declarations) {
const previous = seen.get(declaration.name);
if (previous) {
diagnostics.push({
code: spec_ts_1.WRN_DIAGNOSTIC_CODES.duplicateSymbol,
severity: "error",
message: `Duplicate symbol '${declaration.name}' (${previous} and ${kind}).`,
hint: "Rename one declaration so every prop, state, and computed value is unique.",
file: options.file,
});
}
else {
seen.set(declaration.name, kind);
}
}
}
if (ast.hydrate && !isHydrationStrategy(ast.hydrate)) {
diagnostics.push({
code: spec_ts_1.WRN_DIAGNOSTIC_CODES.invalidHydration,
severity: "error",
message: `Unknown hydration strategy '${ast.hydrate}'.`,
hint: "Use load, idle, visible, interaction, none, or media:<query>.",
file: options.file,
});
}
if (ast.runtime && !isRuntimeTarget(ast.runtime)) {
diagnostics.push({
code: spec_ts_1.WRN_DIAGNOSTIC_CODES.invalidRuntime,
severity: "error",
message: `Unknown runtime target '${ast.runtime}'.`,
hint: "Use server, client, or universal.",
file: options.file,
});
}
let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0;
const urlAttributes = new Set([
"href",
"src",
"action",
"formaction",
"poster",
"cite",
"background",
"xlink:href",
]);
walk(ast.view, (node) => {
if (node.type !== "element")
return;
if (node.attrs.some((attribute) => attribute.event))
interactive = true;
const tag = node.tag.toLowerCase();
for (const attribute of node.attrs) {
if (attribute.event && (attribute.name === "for" || attribute.name === "key")) {
diagnostics.push({
code: "WRN-TEMPLATE-LOOP-DIRECTIVE",
severity: "error",
message: `@${attribute.name} is an event binding, not a loop directive.`,
hint: attribute.name === "for"
? 'Use data-for="item in items".'
: 'Use data-key="item.id" alongside data-for.',
file: options.file,
});
}
if (attribute.event || attribute.boolean || !urlAttributes.has(attribute.name.toLowerCase()))
continue;
if (attribute.value.includes("{"))
continue;
const value = stripAsciiControlAndSpace(attribute.value.trim()).toLowerCase();
if (/^(?:javascript|vbscript|file):/.test(value) ||
/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(value)) {
diagnostics.push({
code: "WRN-SEC-UNSAFE-URL",
severity: "error",
message: `Unsafe URL protocol in ${attribute.name} on <${node.tag}>.`,
hint: "Use a relative URL, https:, mailto:, tel:, or a framework-validated URL helper.",
file: options.file,
});
}
}
if (tag === "a") {
const target = node.attrs.find((attribute) => attribute.name === "target")?.value;
const rel = node.attrs.find((attribute) => attribute.name === "rel")?.value ?? "";
if (target === "_blank" && !/\bnoopener\b/i.test(rel)) {
diagnostics.push({
code: "WRN-SEC-BLANK-REL",
severity: "warning",
message: "A target=_blank link should include rel=noopener.",
hint: 'Add rel="noopener noreferrer".',
file: options.file,
});
}
}
if (!options.accessibility)
return;
if (tag === "img") {
if (!node.attrs.some((attribute) => attribute.name === "alt")) {
diagnostics.push({
code: spec_ts_1.WRN_DIAGNOSTIC_CODES.accessibility,
severity: "warning",
message: "Image is missing an alt attribute.",
hint: 'Add alt text, or alt="" for a decorative image.',
file: options.file,
});
}
const hasWidth = node.attrs.some((attribute) => attribute.name === "width");
const hasHeight = node.attrs.some((attribute) => attribute.name === "height");
if (!hasWidth || !hasHeight) {
diagnostics.push({
code: "WRN-PERF-IMAGE-DIMENSIONS",
severity: "warning",
message: "Image width and height are required to prevent layout shifts.",
hint: "Declare intrinsic width and height, or use @wrnexus/image.",
file: options.file,
});
}
}
});
if (ast.runtime === "server" && interactive) {
diagnostics.push({
code: spec_ts_1.WRN_DIAGNOSTIC_CODES.serverInteractive,
severity: "error",
message: "A server-only WRN root cannot contain client state, effects, watches, or event handlers.",
hint: 'Use runtime = "universal" or remove interactive behavior.',
file: options.file,
});
}
const outputs = new Set(ast.outputs.map((output) => output.name));
for (const fn of ast.runtimeFunctions) {
for (const call of fn.body.matchAll(/\boutput\.([A-Za-z_$][\w$]*)\s*\(/g)) {
const outputName = call[1];
if (fn.runtime === "server") {
diagnostics.push({
code: "WRN-OUTPUT-SERVER-CALL",
severity: "error",
message: `Server function '${fn.name}' cannot call output.${outputName}().`,
hint: "Return a typed value to the browser and call the output from a client function.",
file: options.file,
});
}
else if (!outputs.has(outputName)) {
diagnostics.push({
code: "WRN-OUTPUT-UNKNOWN",
severity: "error",
message: `Unknown output '${outputName}' called from '${fn.name}'.`,
hint: `Declare ${outputName}(payload) inside outputs { ... }.`,
file: options.file,
});
}
}
if (fn.runtime === "client" && /\b(?:process|Bun|Deno|__dirname|require)\b/.test(fn.body)) {
diagnostics.push({
code: "WRN-CLIENT-SERVER-API",
severity: "error",
message: `Client function '${fn.name}' references a server-only API.`,
hint: "Move that operation into a server function and call it through server.name(...).",
file: options.file,
});
}
if (fn.runtime === "server" &&
/\b(?:window|document|localStorage|sessionStorage|navigator)\b/.test(fn.body)) {
diagnostics.push({
code: "WRN-SERVER-BROWSER-API",
severity: "error",
message: `Server function '${fn.name}' references a browser-only API.`,
hint: "Move that code into a client function.",
file: options.file,
});
}
if (fn.runtime !== "server" &&
/\b(?:eval\s*\(|new\s+Function\s*\(|document\.write\s*\(|\.innerHTML\s*=|\.outerHTML\s*=|insertAdjacentHTML\s*\()/.test(fn.body)) {
diagnostics.push({
code: "WRN-SEC-DOM-SINK",
severity: "error",
message: `Client function '${fn.name}' uses an unsafe dynamic-code or HTML sink.`,
hint: "Use compiled templates, textContent, typed outputs, or a reviewed TrustedHTML sanitizer.",
file: options.file,
});
}
if (fn.runtime !== "server" && /\b(?:setTimeout|setInterval)\s*\(\s*["'`]/.test(fn.body)) {
diagnostics.push({
code: "WRN-SEC-STRING-TIMER",
severity: "error",
message: `Client function '${fn.name}' passes a string to a timer.`,
hint: "Pass a function instead of executable text.",
file: options.file,
});
}
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
for (const prop of ast.props) {
if (containsReadonlyPropMutation(fn.body, prop.name, parameterNames)) {
diagnostics.push({
code: "WRN-PROP-READONLY",
severity: "error",
message: `Function '${fn.name}' attempts to mutate readonly prop '${prop.name}'.`,
hint: "Copy the prop into state before mutating it.",
file: options.file,
});
}
}
}
for (const state of ast.states) {
if (state.runtime === "shared" &&
/^(?:new\s+(?:Map|Set|WeakMap|WeakSet)|(?:async\s+)?function\b|.*=>)/.test(state.expr.trim())) {
diagnostics.push({
code: "WRN-STATE-NON-SERIALIZABLE",
severity: "error",
message: `Shared state '${state.name}' is not safely serializable.`,
hint: "Use JSON-compatible data or move the value into client/server state.",
file: options.file,
});
}
if (state.runtime !== "server" &&
/\b(?:process\.env|Bun\.env|Deno\.env|ctx\.env|import\.meta\.env)\b/.test(state.expr)) {
diagnostics.push({
code: "WRN-SEC-SERVER-SECRET-SOURCE",
severity: "error",
message: `Browser-visible state '${state.name}' reads from a server environment source.`,
hint: "Move environment-backed values into server state and return only an explicitly safe result.",
file: options.file,
});
}
}
if (ast.persist) {
const stateNames = new Set(ast.states.filter((state) => state.runtime !== "server").map((state) => state.name));
for (const name of ast.persist.include)
if (!stateNames.has(name))
diagnostics.push({
code: "WRN-PERSIST-UNKNOWN-FIELD",
severity: "error",
message: `Persist include references unknown or server-only state '${name}'.`,
hint: "Persist only declared shared/client state fields.",
file: options.file,
});
for (const name of ast.persist.include)
if (/token|password|secret|otp|api.?key/i.test(name))
diagnostics.push({
code: "WRN-PERSIST-SENSITIVE",
severity: "error",
message: `Sensitive field '${name}' cannot be persisted.`,
hint: "Remove secrets, tokens, passwords, OTPs, and API keys from persistence.",
file: options.file,
});
}
return diagnostics;
}
function diagnose(source, options = {}) {
try {
return astDiagnostics((0, parser_ts_1.parse)(source), options);
}
catch (error) {
return [diagnosticFromError(source, error, options)];
}
}
function assertValidAst(ast, options = {}) {
const errors = astDiagnostics(ast, options).filter((diagnostic) => diagnostic.severity === "error");
if (!errors.length)
return;
const first = errors[0];
throw new parser_ts_1.ParseError(first.message, first.code);
}
function isHydrationStrategy(value) {
return (spec_ts_1.WRN_HYDRATION_STRATEGIES.includes(value) ||
(value.startsWith("media:") && value.length > "media:".length));
}
function isRuntimeTarget(value) {
return spec_ts_1.WRN_RUNTIME_TARGETS.includes(value);
}
function formatDiagnostic(source, diagnostic) {
const location = diagnostic.position
? `${diagnostic.file ?? "<inline .wrn>"}:${diagnostic.position.line}:${diagnostic.position.column}`
: (diagnostic.file ?? "<inline .wrn>");
const lines = [
`${diagnostic.code} ${diagnostic.severity.toUpperCase()}`,
"",
diagnostic.message,
"",
location,
];
if (diagnostic.position) {
const sourceLine = source.split(/\r?\n/)[diagnostic.position.line - 1] ?? "";
lines.push("", sourceLine, `${" ".repeat(Math.max(0, diagnostic.position.column - 1))}^`);
}
if (diagnostic.hint)
lines.push("", `Hint: ${diagnostic.hint}`);
return lines.join("\n");
}
},
"packages/syntax/src/formatter.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
// The formatter intentionally operates on partially written source. Its small
// scanner values are dynamically shaped, while the public API below remains typed.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatWrn = formatWrn;
const VOID_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
function splitPropDeclarations(value) {
const declarations = [];
let start = 0;
let index = 0;
let quote = null;
let escaped = false;
let square = 0;
let brace = 0;
let paren = 0;
let segmentHasColon = false;
let segmentHasEquals = false;
const isIdentifierStart = (character) => /[A-Za-z_]/.test(character || "");
const isIdentifierPart = (character) => /[A-Za-z0-9_]/.test(character || "");
const beginsDeclaration = (position) => {
let cursor = position;
while (cursor < value.length && /[ \t]/.test(value[cursor]))
cursor += 1;
if (value.slice(cursor).startsWith("@event")) {
cursor += "@event".length;
if (!/\s/.test(value[cursor] || ""))
return false;
while (cursor < value.length && /\s/.test(value[cursor]))
cursor += 1;
if (!isIdentifierStart(value[cursor]))
return false;
cursor += 1;
while (cursor < value.length && isIdentifierPart(value[cursor]))
cursor += 1;
while (cursor < value.length && /[ \t]/.test(value[cursor]))
cursor += 1;
return value[cursor] === "=" ? "=" : null;
}
if (!isIdentifierStart(value[cursor]))
return false;
cursor += 1;
while (cursor < value.length && isIdentifierPart(value[cursor]))
cursor += 1;
if (value[cursor] === "?")
cursor += 1;
while (cursor < value.length && /[ \t]/.test(value[cursor]))
cursor += 1;
return value[cursor] === "=" || value[cursor] === ":" ? value[cursor] : null;
};
while (index < value.length) {
const character = value[index];
if (quote !== null) {
if (escaped)
escaped = false;
else if (character === "\\")
escaped = true;
else if (character === quote)
quote = null;
index += 1;
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
index += 1;
continue;
}
if (character === "[")
square += 1;
else if (character === "]" && square > 0)
square -= 1;
else if (character === "{")
brace += 1;
else if (character === "}" && brace > 0)
brace -= 1;
else if (character === "(")
paren += 1;
else if (character === ")" && paren > 0)
paren -= 1;
const topLevel = square === 0 && brace === 0 && paren === 0;
if (topLevel && character === ":")
segmentHasColon = true;
if (topLevel && character === "=")
segmentHasEquals = true;
const candidateDelimiter = topLevel && /\s/.test(character) ? beginsDeclaration(index) : null;
const beginsNext = candidateDelimiter === ":" ||
(candidateDelimiter === "=" && (segmentHasEquals || !segmentHasColon));
if (topLevel &&
/\s/.test(character) &&
value.slice(start, index).trim() !== "@event" &&
beginsNext) {
const declaration = value.slice(start, index).trim();
if (declaration)
declarations.push(declaration);
while (index < value.length && /\s/.test(value[index]))
index += 1;
start = index;
segmentHasColon = false;
segmentHasEquals = false;
continue;
}
index += 1;
}
const declaration = value.slice(start).trim();
if (declaration)
declarations.push(declaration);
return declarations;
}
function splitOutputDeclarations(value) {
const declarations = [];
let start = 0;
let paren = 0;
let angle = 0;
let square = 0;
let quote = null;
let escaped = false;
const startsOutput = (position) => {
let cursor = position;
while (cursor < value.length && /\s/.test(value[cursor]))
cursor += 1;
if (!/[A-Za-z_$]/.test(value[cursor] || ""))
return false;
cursor += 1;
while (cursor < value.length && /[A-Za-z0-9_$]/.test(value[cursor] || ""))
cursor += 1;
while (cursor < value.length && /\s/.test(value[cursor]))
cursor += 1;
return value[cursor] === "(";
};
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (quote !== null) {
if (escaped)
escaped = false;
else if (character === "\\")
escaped = true;
else if (character === quote)
quote = null;
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
if (character === "(")
paren += 1;
else if (character === ")" && paren > 0)
paren -= 1;
else if (character === "[")
square += 1;
else if (character === "]" && square > 0)
square -= 1;
else if (character === "<")
angle += 1;
else if (character === ">" && angle > 0)
angle -= 1;
if (paren === 0 && square === 0 && angle === 0 && /\s/.test(character) && startsOutput(index)) {
const declaration = value.slice(start, index).trim();
if (declaration)
declarations.push(declaration);
while (index < value.length && /\s/.test(value[index]))
index += 1;
start = index;
index -= 1;
}
}
const finalDeclaration = value.slice(start).trim();
if (finalDeclaration)
declarations.push(finalDeclaration);
return declarations;
}
function formatInlineDeclarationBlock(value, unit, depth) {
const match = /^(props|state|computed|outputs)\s*\{([\s\S]*)\}$/.exec(value.trim());
if (!match)
return null;
const declarations = match[1] === "outputs"
? splitOutputDeclarations(match[2].trim())
: splitPropDeclarations(match[2].trim());
return [
`${unit.repeat(depth)}${match[1]} {`,
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
`${unit.repeat(depth)}}`,
];
}
function formatInlinePropsBlock(value, unit, depth) {
const match = /^props\s*\{([\s\S]*)\}$/.exec(value.trim());
if (!match)
return null;
const declarations = splitPropDeclarations(match[1].trim());
if (declarations.length === 0) {
return [`${unit.repeat(depth)}props {`, `${unit.repeat(depth)}}`];
}
return [
`${unit.repeat(depth)}props {`,
...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`),
`${unit.repeat(depth)}}`,
];
}
function findOpeningTagEnd(value) {
let quote = null;
let escaped = false;
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (quote !== null) {
if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
}
if (character === ">") {
return index;
}
}
return -1;
}
function parseAttributes(value) {
const attributes = [];
let index = 0;
while (index < value.length) {
while (index < value.length && /\s/.test(value[index]))
index += 1;
if (index >= value.length)
break;
const start = index;
while (index < value.length && !/[\s=]/.test(value[index]))
index += 1;
while (index < value.length && /\s/.test(value[index]))
index += 1;
if (value[index] === "=") {
index += 1;
while (index < value.length && /\s/.test(value[index]))
index += 1;
const quote = value[index];
if (quote === '"' || quote === "'") {
index += 1;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (escaped)
escaped = false;
else if (character === "\\")
escaped = true;
else if (character === quote)
break;
}
}
else if (value[index] === "{") {
let depth = 0;
let expressionQuote = null;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (expressionQuote !== null) {
if (escaped)
escaped = false;
else if (character === "\\")
escaped = true;
else if (character === expressionQuote)
expressionQuote = null;
continue;
}
if (character === '"' || character === "'" || character === "`") {
expressionQuote = character;
}
else if (character === "{") {
depth += 1;
}
else if (character === "}" && --depth === 0) {
break;
}
}
}
else {
while (index < value.length && !/\s/.test(value[index]))
index += 1;
}
}
const attribute = value.slice(start, index).trim();
if (attribute)
attributes.push(attribute);
}
return attributes;
}
function parseStructuredAttribute(attribute) {
const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute);
if (!match)
return null;
const expression = match[2].trim();
if (!expression.startsWith("[") && !expression.startsWith("{"))
return null;
try {
return {
name: match[1],
value: JSON.parse(expression),
};
}
catch {
return null;
}
}
function formatAttribute(attribute, indentation, unit) {
const structured = parseStructuredAttribute(attribute);
if (!structured)
return [`${indentation}${attribute}`];
const jsonLines = JSON.stringify(structured.value, null, unit).split("\n");
if (jsonLines.length === 1) {
return [`${indentation}${structured.name}={${jsonLines[0]}}`];
}
return [
`${indentation}${structured.name}={${jsonLines[0]}`,
...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`),
`${indentation}${jsonLines.at(-1)}}`,
];
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function parseOpeningTag(value) {
const endIndex = findOpeningTagEnd(value);
if (endIndex === -1) {
return null;
}
const openingPart = value.slice(0, endIndex + 1);
const remainder = value.slice(endIndex + 1).trim();
const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart);
if (!match) {
return null;
}
const tagName = match[1];
const attributes = parseAttributes(match[2].trim());
const selfClosing = match[3] === "/";
const escapedTagName = escapeRegExp(tagName);
const immediateClosing = new RegExp(`^<\\/${escapedTagName}\\s*>`, "i").test(remainder);
const trailingClosingMatch = new RegExp(`^([\\s\\S]*?)<\\/${escapedTagName}\\s*>$`, "i").exec(remainder);
const trailingClosing = trailingClosingMatch !== null;
const inlineContent = trailingClosing ? trailingClosingMatch[1].trim() : "";
const inlineClosing = trailingClosing && inlineContent.length === 0;
const closesInRemainder = immediateClosing || trailingClosing;
return {
tagName,
attributes,
selfClosing,
inlineClosing,
immediateClosing,
trailingClosing,
inlineContent,
closesInRemainder,
remainder,
};
}
function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttributes = true) {
const parsed = parseOpeningTag(value);
if (!parsed) {
return {
lines: [`${unit.repeat(depth)}${value.trim()}`],
opensElement: false,
};
}
const baseIndent = unit.repeat(depth);
const childIndent = unit.repeat(depth + 1);
const normalizedOpening = `<${parsed.tagName}` +
`${parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""}` +
`${parsed.selfClosing ? " /" : ""}>`;
const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`;
const shouldBreak = value.includes("\n") ||
(multilineAttributes && parsed.attributes.length > 0) ||
baseIndent.length + normalizedSingleLine.length > printWidth;
const opensElement = !parsed.selfClosing &&
!parsed.closesInRemainder &&
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
if (!shouldBreak) {
return {
lines: [`${baseIndent}${normalizedSingleLine}`],
opensElement,
};
}
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent)
lines.push(`${childIndent}${parsed.inlineContent}`);
lines.push(`${baseIndent}</${parsed.tagName}>`);
}
else if (parsed.remainder) {
lines.push(`${childIndent}${parsed.remainder}`);
}
return { lines, opensElement };
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
];
if (parsed.selfClosing) {
lines.push(`${baseIndent}/>`);
return {
lines,
opensElement,
};
}
lines.push(`${baseIndent}>`);
if (parsed.trailingClosing) {
if (parsed.inlineContent) {
lines.push(`${childIndent}${parsed.inlineContent}`);
}
lines.push(`${baseIndent}</${parsed.tagName}>`);
}
else if (parsed.remainder) {
lines.push(`${parsed.immediateClosing ? baseIndent : childIndent}${parsed.remainder}`);
}
return {
lines,
opensElement,
};
}
function isMultilineOpeningTagStart(value) {
if (!value.startsWith("<")) {
return false;
}
if (value.startsWith("</") ||
value.startsWith("<!--") ||
value.startsWith("<!") ||
value.startsWith("<?")) {
return false;
}
return findOpeningTagEnd(value) === -1;
}
function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
/**
* Preserve compact, already-balanced HTML fragments as one line.
*
* A line such as `<span>Page</span><b>→</b><span>API</span>` is valid and
* intentionally compact. Expanding only its first opening tag makes later
* formatter passes treat the remaining siblings as children, causing runaway
* indentation and false closing-tag diagnostics. Balanced fragments are kept
* intact while normal multiline opening tags continue through the formatter.
*/
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
}
else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return (tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText));
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
function isControlBlockMiddle(value) {
return /^\{:(?:else(?:\s+if\b[\s\S]*)?|empty)\}$/.test(value);
}
function isControlBlockClose(value) {
return /^\{\/(?:if|each)\}$/.test(value);
}
function countLeadingClosingBraces(value) {
let index = 0;
let count = 0;
while (index < value.length) {
while (index < value.length && /\s/.test(value[index])) {
index += 1;
}
if (value[index] !== "}" && value[index] !== "]") {
break;
}
count += 1;
index += 1;
}
return count;
}
/**
* Count braces outside strings and HTML comments.
*
* This supports WRN blocks, function bodies, lifecycle hooks,
* watcher bodies and multiline JavaScript object literals.
*/
function countStructuralBraces(value) {
let openings = 0;
let closings = 0;
let quote = null;
let escaped = false;
let htmlComment = false;
for (let index = 0; index < value.length; index += 1) {
if (!quote && !htmlComment && value.startsWith("<!--", index)) {
htmlComment = true;
index += 3;
continue;
}
if (htmlComment && value.startsWith("-->", index)) {
htmlComment = false;
index += 2;
continue;
}
if (htmlComment) {
continue;
}
const character = value[index];
if (quote !== null) {
if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
if (character === "{") {
openings += 1;
}
else if (character === "}") {
closings += 1;
}
else if (character === "[") {
openings += 1;
}
else if (character === "]") {
closings += 1;
}
}
return {
openings,
closings,
};
}
function collectOpeningTag(inputLines, startIndex) {
const collected = [inputLines[startIndex].trim()];
let index = startIndex;
while (index + 1 < inputLines.length) {
const joined = collected.join(" ");
if (findOpeningTagEnd(joined) !== -1) {
break;
}
index += 1;
collected.push(inputLines[index].trim());
}
return {
// Preserve the fact that the opening tag was already multiline so a
// second formatter pass cannot collapse it back to one line.
value: collected.join("\n"),
endIndex: index,
};
}
function isPreservedRawBlockStart(value) {
return (/<pre(?:\s|>)/i.test(value) &&
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i))));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
/**
* Put WRN template control markers on their own lines before indentation.
*
* Authors commonly write compact fragments such as
* `{#if loading}<span>…</span>{/if}`. Treating that as one line prevents the
* normal HTML and control-block formatters from seeing its structure.
*/
function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return transformOutsidePreservedRawBlocks(lines, (line) => {
if (!marker.test(line))
return [line];
marker.lastIndex = 0;
const indentation = line.match(/^\s*/)?.[0] ?? "";
const segments = line
.split(marker)
.map((segment) => segment.trim())
.filter(Boolean);
return segments.map((segment) => `${indentation}${segment}`);
});
}
function expandStructuredStateDeclarations(lines, unit) {
return transformOutsidePreservedRawBlocks(lines, (line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match)
return [line];
try {
const parsed = JSON.parse(match[2].trim());
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
if (jsonLines.length === 1)
return [`${match[1]}${jsonLines[0]}`];
const leading = match[1].match(/^\s*/)?.[0] ?? "";
return [
`${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`),
];
}
catch {
return [line];
}
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
if (!match)
return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth)
return null;
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
];
}
catch {
return null;
}
}
function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
const multilineAttributes = options.multilineAttributes !== false;
let codeDepth = 0;
let htmlDepth = 0;
let controlDepth = 0;
let index = 0;
const sourceLines = source.replace(/\r\n/g, "\n").split("\n");
const inputLines = expandInlineControlBlocks(expandStructuredStateDeclarations(sourceLines, unit));
const output = [];
let previousWasBlank = false;
while (index < inputLines.length) {
const originalLine = inputLines[index];
let value = originalLine.trim();
if (value === "") {
if (!previousWasBlank && output.length > 0) {
output.push("");
}
previousWasBlank = true;
index += 1;
continue;
}
previousWasBlank = false;
if (/^import\b/.test(value)) {
const importLines = [value];
while (!/(?:\bfrom\s+)?["'][^"']+["']\s*;?$/.test(importLines[importLines.length - 1]) &&
index + 1 < inputLines.length) {
index += 1;
importLines.push(inputLines[index].trim());
}
output.push(importLines[0], ...importLines.slice(1).map((line) => `${unit}${line}`));
index += 1;
continue;
}
if (isPreservedRawBlockStart(value)) {
const collected = collectPreservedRawBlock(inputLines, index);
const depth = codeDepth + htmlDepth + controlDepth;
output.push(`${unit.repeat(depth)}${collected.lines[0].trimStart()}`);
output.push(...collected.lines.slice(1));
index = collected.endIndex + 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
value = collected.value;
index = collected.endIndex;
}
const inlineDeclaration = formatInlineDeclarationBlock(value, unit, codeDepth + htmlDepth);
const inlineProps = inlineDeclaration ?? formatInlinePropsBlock(value, unit, codeDepth + htmlDepth);
if (inlineProps) {
output.push(...inlineProps);
index += 1;
continue;
}
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
const lineControlDepth = closesControlBlock || continuesControlBlock ? Math.max(0, controlDepth - 1) : controlDepth;
const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces);
let lineHtmlDepth = htmlDepth;
if (isClosingTag(value)) {
lineHtmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
}
else if (value.startsWith("<") &&
!value.startsWith("</") &&
!value.startsWith("<!--") &&
!value.startsWith("<!") &&
!value.startsWith("<?")) {
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
output.push(...formattedTag.lines);
if (formattedTag.opensElement) {
htmlDepth += 1;
}
}
else {
output.push(`${unit.repeat(depth)}${value}`);
}
if (isClosingTag(value)) {
htmlDepth = lineHtmlDepth;
}
const braces = countStructuralBraces(value);
codeDepth = Math.max(0, lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces));
if (isControlBlockOpen(value) || continuesControlBlock) {
controlDepth = lineControlDepth + 1;
}
else if (closesControlBlock) {
controlDepth = lineControlDepth;
}
index += 1;
}
while (output.length > 0 && output[output.length - 1] === "") {
output.pop();
}
return `${output.join("\n")}\n`;
}
/** Format to a bounded fixed point so one call is always safe for editor-on-save and migrations. */
function formatWrn(source, options = {}) {
let current = source;
const seen = new Set();
for (let pass = 0; pass < 8; pass++) {
const formatted = formatWrnPass(current, options);
if (formatted === current)
return formatted;
if (seen.has(formatted))
return [...seen, formatted].sort()[0];
seen.add(current);
current = formatted;
}
return current;
}
},
"packages/syntax/src/index.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.extractInterpolations = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
var tokenizer_ts_1 = require("./tokenizer.js");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
Object.defineProperty(exports, "skipLiteralOrComment", { enumerable: true, get: function () { return tokenizer_ts_1.skipLiteralOrComment; } });
var formatter_ts_1 = require("./formatter.js");
Object.defineProperty(exports, "formatWrn", { enumerable: true, get: function () { return formatter_ts_1.formatWrn; } });
var parser_ts_1 = require("./parser.js");
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return parser_ts_1.parse; } });
Object.defineProperty(exports, "parseHtmlView", { enumerable: true, get: function () { return parser_ts_1.parseHtmlView; } });
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return parser_ts_1.ParseError; } });
Object.defineProperty(exports, "VOID_ELEMENTS", { enumerable: true, get: function () { return parser_ts_1.VOID_ELEMENTS; } });
var interpolation_ts_1 = require("./interpolation.js");
Object.defineProperty(exports, "extractInterpolations", { enumerable: true, get: function () { return interpolation_ts_1.extractInterpolations; } });
var types_ts_1 = require("./types.js");
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } });
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
Object.defineProperty(exports, "runtimeTypeOf", { enumerable: true, get: function () { return types_ts_1.runtimeTypeOf; } });
Object.defineProperty(exports, "validateTypedInitializer", { enumerable: true, get: function () { return types_ts_1.validateTypedInitializer; } });
var diagnostics_ts_1 = require("./diagnostics.js");
Object.defineProperty(exports, "assertValidAst", { enumerable: true, get: function () { return diagnostics_ts_1.assertValidAst; } });
Object.defineProperty(exports, "classifyParseError", { enumerable: true, get: function () { return diagnostics_ts_1.classifyParseError; } });
Object.defineProperty(exports, "containsReadonlyPropMutation", { enumerable: true, get: function () { return diagnostics_ts_1.containsReadonlyPropMutation; } });
Object.defineProperty(exports, "diagnose", { enumerable: true, get: function () { return diagnostics_ts_1.diagnose; } });
Object.defineProperty(exports, "diagnosticFromError", { enumerable: true, get: function () { return diagnostics_ts_1.diagnosticFromError; } });
Object.defineProperty(exports, "formatDiagnostic", { enumerable: true, get: function () { return diagnostics_ts_1.formatDiagnostic; } });
Object.defineProperty(exports, "isHydrationStrategy", { enumerable: true, get: function () { return diagnostics_ts_1.isHydrationStrategy; } });
Object.defineProperty(exports, "isRuntimeTarget", { enumerable: true, get: function () { return diagnostics_ts_1.isRuntimeTarget; } });
Object.defineProperty(exports, "positionAt", { enumerable: true, get: function () { return diagnostics_ts_1.positionAt; } });
__exportStar(require("./spec.js"), exports);
var versioning_ts_1 = require("./versioning.js");
Object.defineProperty(exports, "WRN_SYNTAX_VERSION", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_VERSION; } });
Object.defineProperty(exports, "WRN_SYNTAX_FEATURES", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_FEATURES; } });
Object.defineProperty(exports, "createSourceRange", { enumerable: true, get: function () { return versioning_ts_1.createSourceRange; } });
Object.defineProperty(exports, "sliceSource", { enumerable: true, get: function () { return versioning_ts_1.sliceSource; } });
Object.defineProperty(exports, "diagnosticSummary", { enumerable: true, get: function () { return versioning_ts_1.diagnosticSummary; } });
Object.defineProperty(exports, "supportsSyntaxFeature", { enumerable: true, get: function () { return versioning_ts_1.supportsSyntaxFeature; } });
var v060_ts_1 = require("./v060.js");
Object.defineProperty(exports, "parseComputedDeclarations", { enumerable: true, get: function () { return v060_ts_1.parseComputedDeclarations; } });
Object.defineProperty(exports, "parseOutputs", { enumerable: true, get: function () { return v060_ts_1.parseOutputs; } });
Object.defineProperty(exports, "parsePersist", { enumerable: true, get: function () { return v060_ts_1.parsePersist; } });
Object.defineProperty(exports, "parseRuntimeFunctions", { enumerable: true, get: function () { return v060_ts_1.parseRuntimeFunctions; } });
Object.defineProperty(exports, "parseStateDeclarations", { enumerable: true, get: function () { return v060_ts_1.parseStateDeclarations; } });
Object.defineProperty(exports, "parseStoreLifecycle", { enumerable: true, get: function () { return v060_ts_1.parseStoreLifecycle; } });
Object.defineProperty(exports, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } });
},
"packages/syntax/src/interpolation.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractInterpolations = extractInterpolations;
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
function extractInterpolations(value) {
const segments = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote = null;
while (index < value.length && depth > 0) {
const char = value[index];
if (quote) {
if (char === "\\" && index + 1 < value.length)
index += 2;
else {
if (char === quote)
quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}")
depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
},
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseError = exports.VOID_ELEMENTS = void 0;
exports.parse = parse;
exports.parseHtmlView = parseHtmlView;
const spec_ts_1 = require("./spec.js");
const api_sections_ts_1 = require("./api-sections.js");
/**
* Recursive-descent parser for `.wrn`, producing a small AST.
*
* Grammar (subset of the vision, but real):
*
* page <Name> {
* types { <TypeScript declarations> }
* props { <ident>: <type> [= <expr>] } // no default means required
* state <ident>: <type> = <expr> // type annotation is optional
* view { <html> } // plain HTML (see parseHtmlView)
* seo { title = "Home" description = "..." }
* ssr { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
* client { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
* style { <raw css> } // zero or more, inlined with the page
* functions { <raw js> } // zero or more, shared helpers
* api <METHOD> <path> { <raw js> } // zero or more
* realtime <name> { on <evt>(<args>) { <raw js> } * } // zero or more
* }
*
* The `view` block is written as ordinary HTML — nothing new to learn. Text may
* contain `{expr}` interpolation, attributes may be hyphenated (`data-*`), and
* `@event="..."` declares a client event binding. See `parseHtmlView`.
*/
const tokenizer_ts_1 = require("./tokenizer.js");
const types_ts_1 = require("./types.js");
const v060_ts_1 = require("./v060.js");
/**
* HTML void elements: they have no children and no closing tag.
* @see https://html.spec.whatwg.org/multipage/syntax.html#void-elements
*/
exports.VOID_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
class ParseError extends Error {
code;
offset;
constructor(message, code = "WRN-PARSE-001") {
super(message);
this.name = "ParseError";
this.code = code;
const match = /offset\s+(\d+)/i.exec(message);
this.offset = match ? Number(match[1]) : undefined;
}
}
exports.ParseError = ParseError;
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 "\n";
if (ch === "r")
return "\r";
if (ch === "t")
return "\t";
return ch;
});
}
function parse(source) {
const lx = new tokenizer_ts_1.Lexer(source);
const imports = [];
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) => {
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 {
// A file may contain a page, component, layout, global store, or page store.
const opener = lx.next();
if (opener.type !== "ident" ||
!["page", "component", "layout", "global"].includes(opener.value)) {
throw new ParseError(`Expected 'page', 'component', 'layout', 'global store', or 'page store' but got '${opener.value || opener.type}' at offset ${opener.pos}`);
}
let kind;
let storeKind;
let name;
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;
name = expect("ident").value;
}
expect("lbrace");
let layout;
let layoutIsSymbol = false;
let runtime;
let renderMode;
let hydrate;
const props = [];
const events = [];
const outputs = [];
const types = [];
const states = [];
const computed = [];
const effects = [];
const loads = [];
const actions = [];
const security = {};
const cache = {};
const navigation = {};
const seo = {};
const view = [];
const styles = [];
const functions = [];
const runtimeFunctions = [];
const dataApis = [];
const modeFunctions = [];
const lifecycle = {};
let storeLifecycle = {};
let persist;
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 "auth": {
lx.next();
expect("eq");
const value = lx.next();
if (value.type !== "ident" || !["true", "false"].includes(value.value)) {
throw new ParseError(`Expected auth = true or false at offset ${value.pos}`);
}
security.auth = value.value === "true" ? "required" : "false";
break;
}
case "permission": {
lx.next();
expect("eq");
const value = expect("string").value.trim();
if (!value)
throw new ParseError(`Page permission cannot be empty at offset ${kw.pos}`);
security.permission = value;
break;
}
case "layout": {
// layout = "public" — selects app/layouts/<name>.wrn for this page.
lx.next();
expect("eq");
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": {
lx.next();
expect("eq");
const value = expect("string").value;
if (!spec_ts_1.WRN_RUNTIME_TARGETS.includes(value)) {
throw new ParseError(`Unknown runtime target '${value}' at offset ${kw.pos}`, "WRN-RUNTIME-TARGET");
}
runtime = value;
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;
break;
}
case "hydrate": {
lx.next();
expect("eq");
const value = expect("string").value;
hydrate = value === "never" ? "none" : value;
break;
}
case "props": {
// props { name: Type = <default>; @event name = function }
lx.next();
expect("lbrace");
while (true) {
if (lx.startsWithBlockComment()) {
throw new ParseError("Block comments are not allowed inside props {}; use // line comments instead", "WRN-PROPS-BLOCK-COMMENT");
}
if (lx.peek().type === "rbrace")
break;
const t = lx.peek();
if (t.type === "eof")
throw new ParseError("Unexpected end of input inside props");
if (t.type === "at") {
lx.next();
const declarationKind = expect("ident");
if (declarationKind.value !== "event") {
throw new ParseError(`Expected '@event' but got '@${declarationKind.value}' at offset ${declarationKind.pos}`);
}
const eventName = expect("ident").value;
expect("eq");
const marker = lx.readPropInitializer();
if (marker !== "function") {
throw new ParseError(`Event '${eventName}' must be declared as '@event ${eventName} = function'`);
}
events.push({ name: eventName });
continue;
}
if (t.type !== "ident") {
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;
let hasDefault = false;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
hasDefault = annotation.hasDefault;
}
else {
expect("eq");
hasDefault = true;
}
const defaultValue = hasDefault ? lx.readPropInitializer() : "undefined";
props.push({
name: pName,
valueType,
required: !hasDefault && !optional,
default: defaultValue,
});
}
expect("rbrace");
break;
}
case "state": {
lx.next();
if (lx.peek().type === "lbrace") {
const grouped = (0, v060_ts_1.parseStateDeclarations)(lx.readBalancedBraces(), "shared");
states.push(...grouped);
break;
}
const sName = expect("ident").value;
let valueType;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new ParseError(`State '${sName}' requires an initializer`);
}
else {
expect("eq");
}
states.push({
name: sName,
valueType,
expr: lx.readPropInitializer(),
runtime: "shared",
});
break;
}
case "computed": {
lx.next();
if (lx.peek().type === "lbrace") {
computed.push(...(0, v060_ts_1.parseComputedDeclarations)(lx.readBalancedBraces()));
}
else {
const cName = expect("ident").value;
let valueType;
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(...(0, v060_ts_1.parseOutputs)(lx.readBalancedBraces()));
}
catch (error) {
throw new ParseError(error instanceof Error ? error.message : String(error), "WRN-OUTPUT-DECLARATION");
}
break;
}
case "effect": {
lx.next();
effects.push({ body: lx.readBalancedBraces() });
break;
}
case "types": {
lx.next();
types.push(lx.readBalancedBraces());
break;
}
case "view": {
lx.next();
expect("lbrace");
// The view body is plain HTML. Parse it straight off the source
// (the token lexer isn't used for markup), then resume after the
// block's closing `}`.
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 "security": {
lx.next();
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 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 = [];
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,
name,
...(dependsOn.length ? { dependsOn } : {}),
...(deferred ? { deferred: true } : {}),
body: lx.readBalancedBraces(),
});
break;
}
case "action": {
lx.next();
const actionName = expect("ident").value;
const args = [];
if (lx.peek().type === "lparen") {
lx.next();
while (lx.peek().type !== "rparen") {
args.push(expect("ident").value);
if (lx.peek().type === "comma")
lx.next();
}
expect("rparen");
}
let schema;
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": {
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":
case "server": {
const rawMode = kw.value;
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(...(0, v060_ts_1.parseStateDeclarations)(lx.readBalancedBraces(), rawMode));
break;
}
if (rawMode === "client" && lx.peek().type === "eq") {
lx.next();
hydrate = expect("string").value;
break;
}
if (lx.peek().type === "lbrace") {
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`);
}
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
}
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(...(0, v060_ts_1.parseStateDeclarations)(lx.readBalancedBraces(), "shared"));
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();
const body = lx.readBalancedBraces();
if (kind === "global-store" || kind === "page-store") {
storeLifecycle = (0, v060_ts_1.parseStoreLifecycle)(body);
const allowedStoreHooks = new Set(["serverInit", "clientInit", "hydrate", "dispose"]);
const hookLexer = new tokenizer_ts_1.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();
}
}
else {
const allowedComponentHooks = new Set([
"mount",
"update",
"unmount",
"clientInit",
"dispose",
]);
const hookLexer = new tokenizer_ts_1.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;
if (lifecycle[hook] !== undefined) {
throw new ParseError(`Duplicate lifecycle hook '${hook}'`);
}
lifecycle[hook] = hookBody;
}
}
break;
}
case "watch": {
lx.next();
const stateName = expect("ident").value;
const body = lx.readBalancedBraces();
watches.push({
state: stateName,
body,
});
break;
}
case "functions": {
lx.next();
const body = lx.readBalancedBraces();
functions.push(body);
try {
runtimeFunctions.push(...(0, v060_ts_1.parseRuntimeFunctions)(body));
}
catch (error) {
throw new ParseError(error instanceof Error ? error.message : String(error), "WRN-FUNCTION-DECLARATION");
}
break;
}
case "persist": {
lx.next();
persist = (0, v060_ts_1.parsePersist)(lx.readBalancedBraces());
break;
}
case "apis": {
lx.next();
const body = lx.readBalancedBraces();
dataApis.push(...(0, api_sections_ts_1.parseApiEntries)(body));
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));
if (declaredStates.has("page")) {
throw new ParseError("State name 'page' collides with the WRN 'page' keyword; choose another state name", "WRN-STATE-RESERVED-NAME");
}
for (const watcher of watches) {
if (!declaredStates.has(watcher.state)) {
throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`);
}
}
for (const prop of props) {
const problem = (0, types_ts_1.validateTypedInitializer)(`Prop '${prop.name}'`, prop.valueType, prop.default);
if (problem)
throw new ParseError(problem);
}
for (const state of states) {
const problem = (0, types_ts_1.validateTypedInitializer)(`State '${state.name}'`, state.valueType, state.expr);
if (problem)
throw new ParseError(problem);
}
const symbols = new Set();
for (const declaration of [...props, ...states, ...computed]) {
if (symbols.has(declaration.name)) {
throw new ParseError(`Duplicate symbol '${declaration.name}'`, "WRN-SYMBOL-DUPLICATE");
}
symbols.add(declaration.name);
}
const outputNames = new Set();
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();
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);
}
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();
const visited = new Set();
const visitLoad = (name) => {
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);
const seenApiNames = new Set();
for (const block of dataApis) {
if (seenApiNames.has(block.name)) {
throw new ParseError(`Duplicate api entry "${block.name}"`, "WRN-API-DUPLICATE");
}
seenApiNames.add(block.name);
}
return {
type: "page",
imports,
structuredImports: (0, v060_ts_1.parseStructuredImports)(imports),
kind,
storeKind,
name,
layout,
layoutIsSymbol,
runtime,
renderMode,
hydrate,
cache,
props,
events,
outputs,
types,
states,
computed,
effects,
loads,
actions,
security,
navigation,
seo,
view,
styles,
functions,
runtimeFunctions,
dataApis,
modeFunctions,
lifecycle,
storeLifecycle,
persist,
watches,
apis,
realtimes,
};
}
catch (err) {
if (err instanceof tokenizer_ts_1.LexError)
throw new ParseError(err.message);
throw err;
}
}
/**
* Parse the body of a `view { ... }` block as plain HTML.
*
* `src` is the whole `.wrn` source; `pos` points just past the view block's
* opening `{`. Returns the parsed nodes plus the index of the block's closing
* `}` (left for the caller to consume). It is intentionally lenient — you write
* markup the way you already know:
*
* - `<tag attr="v" @event="expr">children</tag>` — elements with attributes
* - `<tag/>` and HTML void elements (`<br>`, `<img>`, …) — no closing tag
* - text may contain `{expr}` interpolation, kept verbatim for the runtime
* - `@event="..."` becomes a client event binding; hyphenated names are fine
* - `<!-- comments -->` are dropped
*
* `{` and `}` in text are reserved for interpolation; a lone `<` that isn't a
* tag is treated as literal text.
*/
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 isWs = (c) => c === " " || c === "\t" || c === "\n" || c === "\r";
const fail = (msg) => {
throw new ParseError(`${msg} at offset ${i}`);
};
const skipWs = () => {
while (i < src.length && isWs(src[i]))
i++;
};
/** Read a `{...}` interpolation (brace-balanced and quote-aware), braces included. */
const readInterpolation = () => {
const start = i;
let depth = 0;
let quote = null;
for (; i < src.length; i++) {
const char = src[i];
if (quote) {
if (char === "\\" && i + 1 < src.length) {
i++;
continue;
}
if (char === quote)
quote = null;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{")
depth++;
else if (char === "}" && --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++;
// Backslash escapes the delimiter -- so an attribute value such as the
// call expression `api="fn({ a: \"b\" })"` can carry the same quote
// character it's wrapped in -- and escapes itself. Every other backslash
// is a literal one, so a value like "C:\Users\name" survives intact.
let value = "";
while (i < src.length && src[i] !== quote) {
if (src[i] === "\\" && (src[i + 1] === quote || src[i + 1] === "\\")) {
value += src[i + 1];
i += 2;
continue;
}
value += src[i];
i++;
}
if (i >= src.length)
return fail("Unterminated attribute value");
i++; // closing quote
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 === "\n" ||
char === "\r" ||
(char === "/" && next === ">")) {
break;
}
i++;
}
if (i === start) {
return fail("Expected an attribute name");
}
return src.slice(start, i);
};
const parseTag = () => {
i++; // consume '<'
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 name = readAttributeName();
skipWs();
if (src[i] !== "=")
return fail(`Expected '=' after @${name}`);
i++;
skipWs();
attrs.push({ name, value: readQuoted(), event: true });
continue;
}
const name = readAttributeName();
skipWs();
if (src[i] === "=") {
i++;
skipWs();
const value = src[i] === '"' || src[i] === "'"
? readQuoted()
: src[i] === "{"
? readInterpolation()
: fail(`Expected a quoted value or {...} expression after '${name}='`);
attrs.push({ name, value, event: false });
}
else {
attrs.push({ name, value: "", event: false, boolean: true });
}
}
if (exports.VOID_ELEMENTS.has(tag.toLowerCase())) {
return { type: "element", tag, attrs, children: [] };
}
const children = parseNodeList("element");
// parseNodeList stops at the parent's closing tag `</`.
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+key\s+([\s\S]+?))?\s*\}$/;
/** Parse `{#each <list> as <item>[, <index>] [key <expr>]} …body… {:empty} …empty… {/each}`. */
function parseEach() {
const header = readInterpolation(); // reads the full `{#each …}`
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 key = m[4]?.trim();
const body = parseNodeList("each"); // stops at {:empty} or {/each}
let empty = [];
if (src.startsWith("{:empty}", i)) {
i += "{:empty}".length;
empty = parseNodeList("each"); // stops at {/each}
}
if (!src.startsWith("{/each}", i))
return fail("Expected `{/each}` to close `{#each}`");
i += "{/each}".length;
return { type: "each", list, item, index, key, body, empty };
}
/** Parse `{#if <expr>} … {:else if <expr>} … {:else} … {/if}`. */
function parseIf() {
const header = readInterpolation(); // reads the full `{#if …}`
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 };
}
/**
* Parse a run of nodes. `mode` sets the terminator:
* - "root": stops at the view block's closing `}`
* - "element": stops at the parent element's closing tag (`</`)
* - "each": stops (without consuming) at `{:empty}` or `{/each}`
* - "if": stops (without consuming) at `{:else …}` or `{/if}`
* `{#each …}` and `{#if …}` start nested blocks in any mode.
*/
function parseNodeList(mode) {
const nodes = [];
let text = "";
const flush = () => {
if (text.length > 0) {
nodes.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; // parent's closing tag
}
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();
nodes.push(parseTag());
continue;
}
// A lone `<` that doesn't start a tag: treat as literal text.
text += c;
i++;
continue;
}
if (c === "{") {
if (src.startsWith("{#each", i)) {
flush();
nodes.push(parseEach());
continue;
}
if (src.startsWith("{#if", i)) {
flush();
nodes.push(parseIf());
continue;
}
if (mode === "each" && (src.startsWith("{:empty}", i) || src.startsWith("{/each}", i))) {
flush();
break; // loop-section terminator; left for parseEach
}
if (mode === "if" && (src.startsWith("{:else", i) || src.startsWith("{/if}", i))) {
flush();
break; // conditional-section terminator; left for parseIf
}
text += readInterpolation();
continue;
}
if (c === "}" && mode === "root") {
flush();
break; // view terminator; leave `}` for the caller
}
text += c;
i++;
}
return nodes;
}
const nodes = parseNodeList("root");
return { nodes, endPos: i };
}
},
"packages/syntax/src/spec.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WRN_DIAGNOSTIC_CODES = exports.WRN_RUNTIME_TARGETS = exports.WRN_HYDRATION_STRATEGIES = exports.WRN_ROOT_MEMBERS = exports.WRN_ROOT_KINDS = exports.WRN_LANGUAGE_VERSION = void 0;
/** Canonical, machine-readable WRN language capabilities. */
exports.WRN_LANGUAGE_VERSION = "0.6";
exports.WRN_ROOT_KINDS = [
"page",
"component",
"layout",
"global-store",
"page-store",
];
exports.WRN_ROOT_MEMBERS = [
"layout",
"runtime",
"hydrate",
"client",
"types",
"props",
"outputs",
"state",
"shared",
"server",
"computed",
"effect",
"watch",
"lifecycle",
"view",
"seo",
"security",
"load",
"action",
"api",
"ssr",
"realtime",
"style",
"functions",
"persist",
];
exports.WRN_HYDRATION_STRATEGIES = ["load", "idle", "visible", "interaction", "none"];
exports.WRN_RUNTIME_TARGETS = [
"server",
"client",
"universal",
"edge",
"worker",
"service-worker",
];
exports.WRN_DIAGNOSTIC_CODES = {
parse: "WRN-PARSE-001",
root: "WRN-PARSE-ROOT",
member: "WRN-PARSE-MEMBER",
propInitializer: "WRN-PROP-INITIALIZER",
stateInitializer: "WRN-STATE-INITIALIZER",
watchUndeclared: "WRN-WATCH-UNDECLARED",
duplicateSymbol: "WRN-SYMBOL-DUPLICATE",
invalidHydration: "WRN-HYDRATE-STRATEGY",
invalidRuntime: "WRN-RUNTIME-TARGET",
serverInteractive: "WRN-RUNTIME-SERVER-INTERACTIVE",
accessibility: "WRN-A11Y-001",
import: "WRN-IMPORT-001",
function: "WRN-FUNCTION-001",
client: "WRN-CLIENT-001",
server: "WRN-SERVER-001",
output: "WRN-OUTPUT-001",
type: "WRN-TYPE-001",
state: "WRN-STATE-001",
component: "WRN-COMPONENT-001",
template: "WRN-TEMPLATE-001",
store: "WRN-STORE-001",
persist: "WRN-PERSIST-001",
rpc: "WRN-RPC-001",
hydration: "WRN-HYDRATION-001",
migration: "WRN-MIGRATION-001",
};
},
"packages/syntax/src/tokenizer.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/**
* Lexer for the `.wrn` language.
*
* `.wrn` mixes a small structural grammar (page/state/view/api/realtime) with
* raw JavaScript bodies. A pure token stream can't represent the raw JS, so the
* lexer is driven on demand by the parser: it yields structural tokens via
* `next()`/`peek()`, and exposes `readBalancedBraces()`, `readPath()` and
* `readToLineEnd()` for the parser to grab raw spans when grammar demands it.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Lexer = exports.isIdentPart = exports.isIdentStart = exports.LexError = void 0;
exports.skipLiteralOrComment = skipLiteralOrComment;
class LexError extends Error {
}
exports.LexError = LexError;
const isWs = (c) => c === " " || c === "\t" || c === "\n" || c === "\r";
const isIdentStart = (c) => /[A-Za-z_]/.test(c);
exports.isIdentStart = isIdentStart;
const isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
exports.isIdentPart = isIdentPart;
/**
* Skip over a string/template literal or comment starting at `src[i]`, using
* the exact rules `readBalancedBraces` needs to stay comment- and
* string-aware: `/* block *\/` comments anywhere, `//` line comments only at
* the start of a line (so a bare `https://…` in view text isn't mistaken for
* one), and `"`, `'`, `` ` `` strings with backslash escapes.
*
* Returns the index just past what it skipped, or `null` when `src[i]` isn't
* the start of one of those. Exported so any other raw-body scanner that
* needs to walk `.wrn` source without tripping over strings or comments
* (e.g. the `api` section scanner) shares this logic instead of
* reimplementing it — a second hand-rolled scanner is how apostrophes in
* prose used to swallow braces.
*/
/**
* Identifiers that can precede a `/` without ending an expression, so the `/`
* opens a regex rather than dividing.
*/
const REGEX_PRECEDING_KEYWORDS = new Set([
"return",
"typeof",
"instanceof",
"in",
"of",
"new",
"delete",
"void",
"do",
"else",
"yield",
"await",
"case",
]);
/**
* Decide whether the `/` at `i` opens a regex literal or is a division sign.
*
* Scans backwards for the last significant character. `a / b` divides; `(/a/)`,
* `= /a/` and `return /a/` do not. Erring towards division is the safe
* direction -- mistaking division for a regex would swallow everything to the
* next `/` and lose any braces in between.
*/
function opensRegex(src, i) {
let j = i - 1;
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === "\n"))
j--;
if (j < 0)
return true;
const prev = src[j];
if (/[A-Za-z0-9_$]/.test(prev)) {
// An identifier ends an expression, so `/` divides -- unless it is a
// keyword that cannot end one, like `return`.
let k = j;
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
k--;
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
}
// `)` and `]` close an expression, `.` continues one, and a quote ends a
// literal; anything else leaves us in a position where a regex may start.
return (prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`");
}
/**
* Scan a regex literal starting at `i`, returning the index just past its
* closing `/` and flags, or null when this is not in fact a regex.
*
* A regex literal cannot span a newline, so an unterminated one is treated as
* "not a regex" rather than swallowing the rest of the file. That is what keeps
* a bare URL in view text (`https://example.com/a//b`) intact.
*/
function skipRegex(src, i) {
let j = i + 1;
let inClass = false;
while (j < src.length) {
const c = src[j];
if (c === "\n")
return null;
if (c === "\\") {
j += 2;
continue;
}
if (inClass) {
if (c === "]")
inClass = false;
}
else if (c === "[") {
inClass = true;
}
else if (c === "/") {
j++;
while (j < src.length && /[a-z]/.test(src[j]))
j++;
return j;
}
j++;
}
return null;
}
function skipLiteralOrComment(src, i, atLineStart) {
const c = src[i];
if (c === "/" && src[i + 1] === "*") {
const close = src.indexOf("*/", i + 2);
return close === -1 ? src.length : close + 2;
}
if (atLineStart && c === "/" && src[i + 1] === "/") {
const newline = src.indexOf("\n", i + 2);
return newline === -1 ? src.length : newline;
}
if (c === '"' || c === "'" || c === "`") {
let j = i + 1;
while (j < src.length) {
if (src[j] === "\\") {
j += 2;
continue;
}
if (src[j] === c)
return j + 1;
j++;
}
return src.length;
}
// A regex literal is neither a string nor a brace pair, but it can contain
// both. Without this, a quote inside one opened a phantom string that
// swallowed every brace to the next quote, and a lone `{`/`}` miscounted
// block depth.
if (c === "/" && opensRegex(src, i)) {
return skipRegex(src, i);
}
return null;
}
class Lexer {
src;
pos = 0;
constructor(src) {
this.src = src;
}
/**
* Skip whitespace and `// line comments`, but NOT block comments.
*
* Kept separate from `skipTrivia` so `startsWithBlockComment` can still see a
* block comment that `skipTrivia` would otherwise consume.
*/
skipWhitespaceAndLineComments() {
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] !== "\n")
this.pos++;
continue;
}
break;
}
}
/**
* Skip whitespace, line comments and block comments.
*
* Block comments used to be skipped only inside a braced body, so one written
* between two members failed with a bare "Unexpected character '/'" -- the
* same comment parsed or did not depending on where it sat.
*/
skipTrivia() {
const { src } = this;
while (this.pos < src.length) {
this.skipWhitespaceAndLineComments();
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
const close = src.indexOf("*/", this.pos + 2);
this.pos = close === -1 ? src.length : close + 2;
continue;
}
break;
}
}
/**
* True when the next non-trivia characters open a block comment.
*
* Deliberately skips only whitespace and line comments: `props {}` refuses
* block comments with an explained error, and that check must run before
* `skipTrivia` would swallow the comment and drop a declaration silently.
*/
startsWithBlockComment() {
this.skipWhitespaceAndLineComments();
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
}
/** Read and consume the next structural token. */
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: "colon", value: c, pos };
case ",":
this.pos++;
return { type: "comma", value: c, pos };
case "?":
this.pos++;
return { type: "question", value: c, pos };
case '"':
case "'":
return this.readString(c, pos);
}
if ((0, exports.isIdentStart)(c)) {
let v = "";
while (this.pos < src.length && (0, exports.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)})`);
}
/** Look at the next token without consuming it. */
peek() {
const save = this.pos;
const t = this.next();
this.pos = save;
return t;
}
readString(quote, pos) {
const { src } = this;
let v = "";
this.pos++; // opening quote
while (this.pos < src.length) {
const c = src[this.pos++];
if (c === "\\") {
const n = src[this.pos++];
v += n === "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}`);
}
/** Read a route path like `/users/[id]` up to whitespace or `{`. */
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;
}
/**
* Read a prop default initializer. The initializer may contain nested arrays,
* objects, calls, strings, or template literals. At top level it ends at a
* newline, the closing brace of the props block, or the next inline prop
* declaration (`name = ...` / `name: Type = ...`).
*/
readPropInitializer() {
const { src } = this;
while (this.pos < src.length && (src[this.pos] === " " || src[this.pos] === "\t")) {
this.pos++;
}
const start = this.pos;
let square = 0;
let brace = 0;
let paren = 0;
let angle = 0;
let quote = null;
const atTopLevel = () => square === 0 && brace === 0 && paren === 0 && angle === 0;
while (this.pos < src.length) {
const c = src[this.pos];
if (quote) {
this.pos++;
if (c === "\\" && this.pos < src.length) {
this.pos++;
}
else if (c === quote) {
quote = null;
}
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
this.pos++;
continue;
}
if (atTopLevel()) {
if (c === "\n" || c === "\r" || c === "}")
break;
if (c === " " || c === "\t") {
let look = this.pos;
while (look < src.length && (src[look] === " " || src[look] === "\t"))
look++;
const rest = src.slice(look);
if (/^[A-Za-z_][A-Za-z0-9_]*(?:\s*:[^=\r\n{}]+)?\s*=/.test(rest))
break;
}
}
if (c === "[")
square++;
else if (c === "]" && square > 0)
square--;
else if (c === "{")
brace++;
else if (c === "}" && brace > 0)
brace--;
else if (c === "(")
paren++;
else if (c === ")" && paren > 0)
paren--;
else if (c === "<")
angle++;
else if (c === ">" && angle > 0)
angle--;
this.pos++;
}
const value = src.slice(start, this.pos).trim();
if (!value)
throw new LexError(`Expected a prop initializer at offset ${start}`);
return value;
}
/** Read the rest of the current line (used for `state x = <expr>`). */
readToLineEnd() {
const { src } = this;
let v = "";
while (this.pos < src.length && src[this.pos] !== "\n")
v += src[this.pos++];
return v.trim();
}
/**
* Read a TypeScript-style type annotation after `:`. Reading stops at a
* top-level `=` or line ending, while nested object/tuple/generic syntax is
* preserved. The optional `=` is consumed for the caller.
*/
readTypeAnnotation() {
const { src } = this;
let value = "";
let angle = 0;
let square = 0;
let brace = 0;
let paren = 0;
let quote = null;
while (this.pos < src.length) {
const c = src[this.pos];
if (quote) {
value += c;
this.pos++;
if (c === "\\" && this.pos < src.length)
value += src[this.pos++];
else if (c === quote)
quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
value += c;
this.pos++;
continue;
}
if (c === "}" && angle === 0 && square === 0 && brace === 0 && paren === 0)
break;
if (c === "<")
angle++;
else if (c === ">" && angle > 0)
angle--;
else if (c === "[")
square++;
else if (c === "]" && square > 0)
square--;
else if (c === "{")
brace++;
else if (c === "}" && brace > 0)
brace--;
else if (c === "(")
paren++;
else if (c === ")" && paren > 0)
paren--;
if (angle === 0 && square === 0 && brace === 0 && paren === 0) {
if (c === " " || c === "\t") {
let look = this.pos;
while (look < src.length && (src[look] === " " || src[look] === "\t"))
look++;
if (/^[A-Za-z_][A-Za-z0-9_]*\??\s*:/.test(src.slice(look)))
break;
}
if (c === "=") {
this.pos++;
const type = value.trim();
if (!type)
throw new LexError(`Expected a type annotation at offset ${this.pos}`);
return { type, hasDefault: true };
}
if (c === "\n" || c === "\r")
break;
}
value += c;
this.pos++;
}
const type = value.trim();
if (!type)
throw new LexError(`Expected a type annotation at offset ${this.pos}`);
return { type, hasDefault: false };
}
/**
* Read a `{ ... }` block and return its INNER text (no outer braces), with
* brace counting that respects string and template literals so a `}` inside a
* string doesn't end the block early.
*
* Comments are skipped as well. Without that, an apostrophe in ordinary
* prose — `/* the panel's color *\/`, `// the Input's slot` — opened a
* string that ran to the next apostrophe, swallowing every brace in between
* and failing the whole component with "Unbalanced braces" pointing at the
* block's opening line. Comments are where apostrophes actually occur, so
* that error was almost always a false alarm.
*
* A `//` line comment is only recognised at the start of a line (after
* whitespace), which is where every comment in a `.wrn` file is written.
* Recognising it mid-line would break the far more common case of a bare
* URL in view text, where `https://…` is not inside quotes.
*/
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;
/** True while only whitespace has been seen since the last newline. */
let atLineStart = false;
while (i < src.length) {
const c = src[i];
if (c === "\n") {
atLineStart = true;
i++;
continue;
}
const skipped = skipLiteralOrComment(src, i, atLineStart);
if (skipped !== null) {
i = skipped;
atLineStart = false;
continue;
}
if (c !== " " && c !== "\t" && c !== "\r")
atLineStart = false;
if (c === "{")
depth++;
else if (c === "}") {
depth--;
if (depth === 0) {
this.pos = i + 1;
return src.slice(start, i);
}
}
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] === "\n")
line++;
}
return line;
}
}
exports.Lexer = Lexer;
},
"packages/syntax/src/types.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/** Utilities shared by typed `.wrn` parsing, validation, and code generation. */
Object.defineProperty(exports, "__esModule", { value: true });
exports.runtimeTypeOf = runtimeTypeOf;
exports.inferredRuntimeType = inferredRuntimeType;
exports.validateTypedInitializer = validateTypedInitializer;
exports.eraseFunctionTypes = eraseFunctionTypes;
function runtimeTypeOf(annotation) {
if (!annotation)
return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, "");
const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))))
return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 &&
concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part))))
return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part))))
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint";
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(type) || /^\[/.test(type))
return "array";
if (/^(?:Record\s*<|object\b|\{)/.test(type))
return "object";
if (/=>|^(?:Function|\([^)]*\)\s*=>)/.test(type))
return "function";
return "unknown";
}
function inferredRuntimeType(expression) {
const value = expression.trim();
if (/^["'`]/.test(value))
return "string";
if (/^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value))
return "number";
if (/^(?:true|false)$/.test(value))
return "boolean";
if (/^-?\d+n$/.test(value))
return "bigint";
if (value.startsWith("["))
return "array";
if (value.startsWith("{") || /^new\s+(?:Map|Set|Date)\b/.test(value))
return "object";
if (/^(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/.test(value)) {
return "function";
}
return "unknown";
}
function validateTypedInitializer(name, annotation, expression) {
if (!annotation || expression.trim() === "undefined" || expression.trim() === "null")
return null;
const expected = runtimeTypeOf(annotation);
const actual = inferredRuntimeType(expression);
if (expected === "unknown" || actual === "unknown" || expected === actual)
return null;
return `${name} is declared as ${annotation}, but its initializer is ${actual}`;
}
/**
* Browser behavior is evaluated as JavaScript, so erase TypeScript annotations
* from ordinary function declarations before serializing it into HTML.
* Server output retains the original typed source.
*/
function eraseFunctionTypes(source) {
return source
.replace(/\bcatch\s*\(\s*([A-Za-z_$][\w$]*)\s*:\s*(?:any|unknown)\s*\)/g, "catch ($1)")
.replace(/(\b(?:async\s+)?function\s+[A-Za-z_$][\w$]*\s*\()([^)]*)(\)\s*)(?::\s*([^{}=>]+)\s*)?(\{)/g, (_whole, open, params, close, _returnType, brace) => {
const plainParams = params
.split(",")
.map((param) => param.replace(/([A-Za-z_$][\w$]*)(\?)?\s*:\s*([^=]+?)(?=\s*=|$)/, "$1").trim())
.join(", ");
return `${open}${plainParams}${close}${brace}`;
});
}
},
"packages/syntax/src/v060.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseRuntimeFunctions = parseRuntimeFunctions;
exports.stripRuntimeFunctionModifiers = stripRuntimeFunctionModifiers;
exports.parseOutputs = parseOutputs;
exports.parseStructuredImports = parseStructuredImports;
exports.parseStateDeclarations = parseStateDeclarations;
exports.parseComputedDeclarations = parseComputedDeclarations;
exports.parsePersist = parsePersist;
exports.parseStoreLifecycle = parseStoreLifecycle;
const tokenizer_ts_1 = require("./tokenizer.js");
function splitTopLevel(input, separator = ",") {
const parts = [];
let start = 0;
let quote = null;
let angle = 0;
let square = 0;
let brace = 0;
let paren = 0;
for (let i = 0; i < input.length; i++) {
const c = input[i];
if (quote) {
if (c === "\\")
i++;
else if (c === quote)
quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === "<")
angle++;
else if (c === ">" && angle > 0)
angle--;
else if (c === "[")
square++;
else if (c === "]" && square > 0)
square--;
else if (c === "{")
brace++;
else if (c === "}" && brace > 0)
brace--;
else if (c === "(")
paren++;
else if (c === ")" && paren > 0)
paren--;
else if (c === separator && angle === 0 && square === 0 && brace === 0 && paren === 0) {
parts.push(input.slice(start, i).trim());
start = i + 1;
}
}
const tail = input.slice(start).trim();
if (tail)
parts.push(tail);
return parts;
}
function findTopLevelChar(input, wanted) {
let quote = null;
let angle = 0;
let square = 0;
let brace = 0;
let paren = 0;
for (let i = 0; i < input.length; i++) {
const c = input[i];
if (quote) {
if (c === "\\")
i++;
else if (c === quote)
quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === "<")
angle++;
else if (c === ">" && angle > 0)
angle--;
else if (c === "[")
square++;
else if (c === "]" && square > 0)
square--;
else if (c === "{")
brace++;
else if (c === "}" && brace > 0)
brace--;
else if (c === "(")
paren++;
else if (c === ")" && paren > 0)
paren--;
if (c === wanted && angle === 0 && square === 0 && brace === 0 && paren === 0)
return i;
}
return -1;
}
function parseParameters(source) {
return splitTopLevel(source)
.filter(Boolean)
.map((entry) => {
const eq = findTopLevelChar(entry, "=");
const declaration = (eq >= 0 ? entry.slice(0, eq) : entry).trim();
const defaultValue = eq >= 0 ? entry.slice(eq + 1).trim() : undefined;
const colon = findTopLevelChar(declaration, ":");
const rawName = (colon >= 0 ? declaration.slice(0, colon) : declaration).trim();
const optional = rawName.endsWith("?");
const name = optional ? rawName.slice(0, -1).trim() : rawName;
const valueType = colon >= 0 ? declaration.slice(colon + 1).trim() : undefined;
return {
name,
optional,
...(valueType ? { valueType } : {}),
...(defaultValue ? { default: defaultValue } : {}),
};
});
}
function skipTrivia(source, start) {
let i = start;
while (i < source.length) {
if (/\s/.test(source[i])) {
i++;
continue;
}
if (source.startsWith("//", i)) {
const end = source.indexOf("\n", i + 2);
i = end < 0 ? source.length : end + 1;
continue;
}
if (source.startsWith("/*", i)) {
const end = source.indexOf("*/", i + 2);
i = end < 0 ? source.length : end + 2;
continue;
}
break;
}
return i;
}
function readWord(source, start) {
const match = /^[A-Za-z_$][\w$]*/.exec(source.slice(start));
return match ? { word: match[0], end: start + match[0].length } : null;
}
function readBalanced(source, start, open, close) {
if (source[start] !== open)
throw new Error(`Expected '${open}' at offset ${start}`);
let depth = 0;
let quote = null;
for (let i = start; i < source.length; i++) {
const c = source[i];
if (quote) {
if (c === "\\")
i++;
else if (c === quote)
quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === open)
depth++;
else if (c === close && --depth === 0)
return { inner: source.slice(start + 1, i), end: i + 1 };
}
throw new Error(`Unbalanced '${open}${close}' starting at offset ${start}`);
}
function parseRuntimeFunctions(source) {
const declarations = [];
let i = 0;
while (i < source.length) {
i = skipTrivia(source, i);
const start = i;
let token = readWord(source, i);
if (!token) {
i++;
continue;
}
let runtime = "shared";
if (["client", "server", "shared"].includes(token.word)) {
runtime = token.word;
i = skipTrivia(source, token.end);
token = readWord(source, i);
if (!token)
continue;
}
let isAsync = false;
if (token.word === "async") {
isAsync = true;
i = skipTrivia(source, token.end);
token = readWord(source, i);
if (!token)
continue;
}
if (token.word !== "function") {
i = token.end;
continue;
}
i = skipTrivia(source, token.end);
const nameToken = readWord(source, i);
if (!nameToken)
throw new Error(`Expected function name at offset ${i}`);
const name = nameToken.word;
i = skipTrivia(source, nameToken.end);
const params = readBalanced(source, i, "(", ")");
i = skipTrivia(source, params.end);
let returnType;
if (source[i] === ":") {
i++;
const typeStart = i;
let quote = null;
let angle = 0;
let square = 0;
let paren = 0;
while (i < source.length) {
const c = source[i];
if (quote) {
if (c === "\\")
i++;
else if (c === quote)
quote = null;
i++;
continue;
}
if (c === '"' || c === "'" || c === "`")
quote = c;
else if (c === "<")
angle++;
else if (c === ">" && angle > 0)
angle--;
else if (c === "[")
square++;
else if (c === "]" && square > 0)
square--;
else if (c === "(")
paren++;
else if (c === ")" && paren > 0)
paren--;
else if (c === "{" && angle === 0 && square === 0 && paren === 0)
break;
i++;
}
returnType = source.slice(typeStart, i).trim();
}
i = skipTrivia(source, i);
const body = readBalanced(source, i, "{", "}");
i = body.end;
declarations.push({
name,
runtime,
async: isAsync,
parameters: parseParameters(params.inner),
...(returnType ? { returnType } : {}),
body: body.inner,
source: source.slice(start, body.end).trim(),
});
}
return declarations;
}
function stripRuntimeFunctionModifiers(source, include) {
const allowed = new Set(include);
return parseRuntimeFunctions(source)
.filter((entry) => allowed.has(entry.runtime))
.map((entry) => {
const params = entry.parameters
.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : ""}${param.default ? ` = ${param.default}` : ""}`)
.join(", ");
return `${entry.async ? "async " : ""}function ${entry.name}(${params})${entry.returnType ? `: ${entry.returnType}` : ""} {${entry.body}}`;
})
.join("\n\n");
}
function parseOutputs(source) {
const out = [];
let i = 0;
while (i < source.length) {
i = skipTrivia(source, i);
if (i >= source.length)
break;
const nameToken = readWord(source, i);
if (!nameToken)
throw new Error(`Expected output name at offset ${i}`);
i = skipTrivia(source, nameToken.end);
const args = readBalanced(source, i, "(", ")");
i = args.end;
const parameters = parseParameters(args.inner);
if (parameters.length > 1)
throw new Error(`Output '${nameToken.word}' accepts zero or one payload`);
const payload = parameters[0];
if (payload && !payload.valueType)
throw new Error(`Output '${nameToken.word}' payload requires a type`);
out.push({
name: nameToken.word,
...(payload
? {
payload: {
name: payload.name,
valueType: payload.valueType,
optional: payload.optional,
},
}
: {}),
});
}
return out;
}
function parseStructuredImports(imports) {
return imports.map((raw) => {
const sourceMatch = /\sfrom\s+["']([^"']+)["']|^import\s+["']([^"']+)["']/.exec(raw);
const source = sourceMatch?.[1] ?? sourceMatch?.[2] ?? "";
const typeOnly = /^import\s+type\b/.test(raw);
const clause = raw
.replace(/^import\s+(?:type\s+)?/, "")
.replace(/\s+from\s+["'][^"']+["']\s*;?$/, "")
.trim();
const declaration = { source, typeOnly, namedImports: [], raw };
if (!clause || clause.startsWith('"') || clause.startsWith("'"))
return declaration;
if (clause.startsWith("*")) {
declaration.namespaceImport = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause)?.[1];
return declaration;
}
let rest = clause;
if (!rest.startsWith("{")) {
const comma = findTopLevelChar(rest, ",");
declaration.defaultImport = (comma < 0 ? rest : rest.slice(0, comma)).trim();
rest = comma < 0 ? "" : rest.slice(comma + 1).trim();
}
const named = /^\{([\s\S]*)\}$/.exec(rest)?.[1];
if (named !== undefined) {
declaration.namedImports = splitTopLevel(named).map((item) => {
const localTypeOnly = /^type\s+/.test(item);
const cleaned = item.replace(/^type\s+/, "").trim();
const [imported, local] = cleaned.split(/\s+as\s+/);
return {
imported: imported.trim(),
local: (local ?? imported).trim(),
typeOnly: typeOnly || localTypeOnly,
};
});
}
return declaration;
});
}
function parseStateDeclarations(source, runtime) {
const lx = new tokenizer_ts_1.Lexer(source);
const out = [];
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident")
throw new tokenizer_ts_1.LexError(`Expected a state name at offset ${nameToken.pos}`);
let valueType;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new tokenizer_ts_1.LexError(`State '${nameToken.value}' requires an initializer`);
}
else {
const eq = lx.next();
if (eq.type !== "eq")
throw new tokenizer_ts_1.LexError(`Expected '=' after state '${nameToken.value}' at offset ${eq.pos}`);
}
out.push({
name: nameToken.value,
...(valueType ? { valueType } : {}),
expr: lx.readPropInitializer(),
runtime,
});
}
return out;
}
function parseComputedDeclarations(source) {
const lx = new tokenizer_ts_1.Lexer(source);
const out = [];
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident")
throw new tokenizer_ts_1.LexError(`Expected a computed name at offset ${nameToken.pos}`);
let valueType;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new tokenizer_ts_1.LexError(`Computed '${nameToken.value}' requires an expression`);
}
else {
const eq = lx.next();
if (eq.type !== "eq")
throw new tokenizer_ts_1.LexError(`Expected '=' after computed '${nameToken.value}' at offset ${eq.pos}`);
}
out.push({
name: nameToken.value,
...(valueType ? { valueType } : {}),
expr: lx.readPropInitializer(),
});
}
return out;
}
function nestedBlock(source, name) {
const match = new RegExp(`\\b${name}\\s*\\{`).exec(source);
if (!match)
return undefined;
const brace = source.indexOf("{", match.index);
return readBalanced(source, brace, "{", "}").inner.trim() || undefined;
}
function parsePersist(source) {
const storage = /\bstorage\s*=\s*["'](memory|session|local)["']/.exec(source)?.[1];
const includeRaw = /\binclude\s*=\s*\[([\s\S]*?)\]/.exec(source)?.[1] ?? "";
const include = Array.from(includeRaw.matchAll(/["']([^"']+)["']/g), (match) => match[1]);
const version = Number(/\bversion\s*=\s*(\d+)/.exec(source)?.[1] ?? "1");
const migrations = nestedBlock(source, "migrations");
const validation = nestedBlock(source, "validate");
return {
storage: storage ?? "memory",
include,
version,
...(migrations ? { migrations } : {}),
...(validation ? { validation } : {}),
};
}
function parseStoreLifecycle(source) {
const out = {};
for (const hook of ["serverInit", "clientInit", "hydrate", "dispose"]) {
const start = new RegExp(`\\b${hook}\\s*\\{`).exec(source);
if (!start)
continue;
const brace = source.indexOf("{", start.index);
out[hook] = readBalanced(source, brace, "{", "}").inner;
}
return out;
}
},
"packages/syntax/src/versioning.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = void 0;
exports.createSourceRange = createSourceRange;
exports.sliceSource = sliceSource;
exports.diagnosticSummary = diagnosticSummary;
exports.supportsSyntaxFeature = supportsSyntaxFeature;
/** Current stable syntax contract. Bump only when parsers/codegen need migration. */
exports.WRN_SYNTAX_VERSION = "0.4";
exports.WRN_SYNTAX_FEATURES = Object.freeze({
"typed-declarations": true,
layouts: true,
"server-client-blocks": true,
effects: true,
watch: true,
lifecycle: true,
"embedded-api": true,
realtime: true,
"runtime-markers": true,
});
function createSourceRange(start, end) {
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
throw new RangeError(`Invalid source range ${start}..${end}`);
}
return { start, end };
}
function sliceSource(source, range) {
return source.slice(range.start, range.end);
}
function diagnosticSummary(diagnostics) {
const summary = { errors: 0, warnings: 0, info: 0, codes: {} };
for (const diagnostic of diagnostics) {
if (diagnostic.severity === "error")
summary.errors++;
else if (diagnostic.severity === "warning")
summary.warnings++;
else
summary.info++;
summary.codes[diagnostic.code] = (summary.codes[diagnostic.code] ?? 0) + 1;
}
return summary;
}
function supportsSyntaxFeature(feature) {
return Object.prototype.hasOwnProperty.call(exports.WRN_SYNTAX_FEATURES, feature);
}
}
};
const __aliases = {
"@wrnexus/syntax": "packages/syntax/src/index.ts",
"@wrnexus/syntax/parser": "packages/syntax/src/parser.ts",
"@wrnexus/syntax/tokenizer": "packages/syntax/src/tokenizer.ts",
"@wrnexus/syntax/types": "packages/syntax/src/types.ts",
"@wrnexus/syntax/diagnostics": "packages/syntax/src/diagnostics.ts",
"@wrnexus/syntax/spec": "packages/syntax/src/spec.ts"
};
const __cache = Object.create(null);
function __normalize(id) {
const normalized = id.split("\\").join("/");
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
}
function __resolve(request, parent) {
if (__aliases[request]) return __aliases[request];
if (!request.startsWith(".")) return null;
const base = __normalize(__path.posix.join(__path.posix.dirname(parent), request));
const candidates = [
base,
base.endsWith(".js") ? base.slice(0, -3) + ".ts" : base,
base.endsWith(".ts") ? base : base + ".ts",
(base.endsWith("/") ? base.slice(0, -1) : base) + "/index.ts"
];
for (const candidate of candidates) {
if (__modules[candidate]) return candidate;
}
return null;
}
function __load(id) {
if (__cache[id]) return __cache[id].exports;
const factory = __modules[id];
if (!factory) throw new Error("WRN editor compiler module not found: " + id);
const module = { exports: {} };
__cache[id] = module;
const localRequire = (request) => {
const resolved = __resolve(request, id);
return resolved ? __load(resolved) : __nodeRequire(request);
};
factory(module, module.exports, localRequire, id, __path.posix.dirname(id));
return module.exports;
}
module.exports = __load("packages/compiler/src/index.ts");