7362 lines
294 KiB
JavaScript
7362 lines
294 KiB
JavaScript
"use strict";
|
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
|
// WRN editor compiler source hash: 1d2e0d1e1c38a4513b0fce3631bae003f7c28069920c5386ecc68cbe728bf57b
|
|
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
|
|
// Generated with TypeScript: 5.9.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.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;
|
|
}
|
|
function analyzeRuntimeRequirements(ast) {
|
|
const reasons = [];
|
|
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");
|
|
}
|
|
const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server";
|
|
const serverDisabled = ast.renderMode === "client";
|
|
return {
|
|
kind,
|
|
canPrerender: kind === "static" || kind === "static-interactive",
|
|
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/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",
|
|
"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) => ["legacy", "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));
|
|
const stateNames = ast.states
|
|
.filter((state) => state.runtime !== "server" &&
|
|
safeIdentifier(state.name) &&
|
|
!RUNTIME_BINDINGS.has(state.name) &&
|
|
!parameterNames.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))
|
|
.map((prop) => prop.name);
|
|
const functionAliases = availableFunctions.filter((name) => safeIdentifier(name) &&
|
|
!RUNTIME_BINDINGS.has(name) &&
|
|
!parameterNames.has(name) &&
|
|
!stateSet.has(name) &&
|
|
!propNames.includes(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 = functionAliases
|
|
.map((name) => {
|
|
const call = `context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs)`;
|
|
if (!stateNames.length) {
|
|
return `const ${name} = (...__wrnexusPeerArgs) => ${call};`;
|
|
}
|
|
return `const ${name} = (...__wrnexusPeerArgs) => {
|
|
${syncStateToContext}
|
|
let __wrnexusPeerResult;
|
|
try {
|
|
__wrnexusPeerResult = ${call};
|
|
} catch (__wrnexusPeerError) {
|
|
${syncStateFromContext}
|
|
throw __wrnexusPeerError;
|
|
}
|
|
if (__wrnexusPeerResult && typeof __wrnexusPeerResult.then === "function") {
|
|
return Promise.resolve(__wrnexusPeerResult).finally(() => { ${syncStateFromContext} });
|
|
}
|
|
${syncStateFromContext}
|
|
return __wrnexusPeerResult;
|
|
};`;
|
|
})
|
|
.join("\n");
|
|
const copyBack = stateNames
|
|
.map((name) => `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`)
|
|
.join("\n");
|
|
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}
|
|
${peerAliases}
|
|
${runtimeBindings}
|
|
try {
|
|
${body}
|
|
} finally {
|
|
${copyBack}
|
|
}
|
|
}`;
|
|
}
|
|
function generateBrowserModule(ast) {
|
|
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "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))];
|
|
return `// generated WRNexusJS browser module for ${ast.name}
|
|
${imports}
|
|
export const __wrnexusClientFunctions = {
|
|
${functions.map((fn) => ` ${functionEntry(ast, fn, functionNames)}`).join(",\n")}
|
|
};
|
|
export const __wrnexusClientState = ${JSON.stringify(state)};
|
|
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
|
|
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
|
|
export function bindClientScope(context) {
|
|
const functions = {};
|
|
const scopedContext = { ...context, functions };
|
|
for (const [name, handler] of Object.entries(__wrnexusClientFunctions)) {
|
|
functions[name] = (...args) => handler(scopedContext, ...args);
|
|
}
|
|
return functions;
|
|
}
|
|
`;
|
|
}
|
|
|
|
},
|
|
"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 parser_ts_1 = require("./parser.js");
|
|
const types_ts_1 = require("./types.js");
|
|
const syntax_1 = require("@wrnexus/syntax");
|
|
const store_codegen_ts_1 = require("./store-codegen.js");
|
|
const analysis_ts_1 = require("./analysis.js");
|
|
const client_codegen_ts_1 = require("./client-codegen.js");
|
|
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());
|
|
}
|
|
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, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
/** 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:
|
|
return attr.boolean
|
|
? ` ${attr.name}`
|
|
: ` ${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}`;
|
|
}
|
|
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) {
|
|
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 === "&" ? "&" : c === "<" ? "<" : ">"));
|
|
}
|
|
/**
|
|
* 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) {
|
|
if (node.type === "text") {
|
|
return bakeLoopText(node.value);
|
|
}
|
|
if (node.type === "each") {
|
|
return compileEachExpr(node);
|
|
}
|
|
if (node.type === "if") {
|
|
return compileIfExpr(node);
|
|
}
|
|
const componentTag = isComponentTag(node.tag);
|
|
const attrs = node.attrs
|
|
.filter((attr) => attr.name !== "data-component")
|
|
.map((attr) => {
|
|
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
|
if (attr.boolean) {
|
|
return escLit(` ${name}`);
|
|
}
|
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
|
})
|
|
.join("");
|
|
const inner = node.children.map(renderLoopBody).join("");
|
|
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) {
|
|
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
|
attrs +
|
|
escLit(">") +
|
|
inner +
|
|
escLit("</div>"));
|
|
}
|
|
if (parser_ts_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
|
}
|
|
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
|
}
|
|
/**
|
|
* 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) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const body = node.body.map(renderLoopBody).join("");
|
|
const empty = node.empty.map(renderLoopBody).join("");
|
|
return ("${(() => { const __wl = Array.isArray(" +
|
|
node.list +
|
|
") ? (" +
|
|
node.list +
|
|
") : []; return __wl.length ? __wl.map((" +
|
|
item +
|
|
", " +
|
|
index +
|
|
") => `" +
|
|
body +
|
|
'`).join("") : `' +
|
|
empty +
|
|
"`; })()}");
|
|
}
|
|
/**
|
|
* 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) {
|
|
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(renderLoopBody).join("") + "`";
|
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
|
}
|
|
return "${" + expr + "}";
|
|
}
|
|
/**
|
|
* Collect every server-control expression in a view (recursively): `{#each}` list
|
|
* expressions and `{#if}` conditions. Used to wire up raw SSR data consts.
|
|
*/
|
|
function collectControlExprs(nodes, out = []) {
|
|
for (const node of nodes) {
|
|
if (node.type === "text")
|
|
continue;
|
|
if (node.type === "each") {
|
|
out.push(node.list);
|
|
collectControlExprs(node.body, out);
|
|
collectControlExprs(node.empty, out);
|
|
}
|
|
else if (node.type === "if") {
|
|
for (const b of node.branches) {
|
|
if (b.cond)
|
|
out.push(b.cond);
|
|
collectControlExprs(b.body, out);
|
|
}
|
|
}
|
|
else if (node.type === "element") {
|
|
collectControlExprs(node.children, out);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
|
if (node.type === "text")
|
|
return substituteReactiveText(node.value, reactive, 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));
|
|
return `\x00WRNEACH${loops.length - 1}\x00`;
|
|
}
|
|
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 apiName = attrValue(node.attrs, "api");
|
|
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
|
if (apiName && !apiBinding) {
|
|
throw new Error(`Unknown .wrn api binding "${apiName}"`);
|
|
}
|
|
const ssrGet = attrValue(node.attrs, "ssrGet");
|
|
const ssrText = attrValue(node.attrs, "ssrText");
|
|
const csrGet = attrValue(node.attrs, "csrGet");
|
|
const csrText = attrValue(node.attrs, "csrText");
|
|
const csrId = apiBinding?.mode === "client"
|
|
? csrMarker(csrBindings, renderBinding(apiBinding))
|
|
: csrGet && csrText
|
|
? csrMarker(csrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(csrGet),
|
|
body: expressionBody(csrText),
|
|
helpers: "",
|
|
})
|
|
: undefined;
|
|
// Void elements (<br>, <img>, …) have no closing tag and no children.
|
|
if (parser_ts_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
|
|
}
|
|
const inner = apiBinding?.mode === "ssr"
|
|
? ssrMarker(ssrBindings, renderBinding(apiBinding))
|
|
: ssrGet && ssrText
|
|
? ssrMarker(ssrBindings, {
|
|
method: "GET",
|
|
path: apiRoutePath(ssrGet),
|
|
body: expressionBody(ssrText),
|
|
helpers: "",
|
|
})
|
|
: node.children
|
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
|
.join("");
|
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
|
|
}
|
|
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
|
const attrs = node.attrs
|
|
.filter((attr) => attr.name !== "data-component")
|
|
.map((attr) => renderPageComponentAttr(attr, loops))
|
|
.join("");
|
|
const inner = node.children
|
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
|
.join("");
|
|
return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
|
|
}
|
|
function renderNestedComponentInvocation(node, ctx) {
|
|
let bindIndex = 0;
|
|
const attrs = node.attrs
|
|
.filter((attr) => attr.name !== "data-component")
|
|
.map((attr) => {
|
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
|
if (spread) {
|
|
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
|
|
}
|
|
if (attr.event) {
|
|
return (escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`));
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const wholeExpression = wholeAttributeExpression(attr.value);
|
|
const compiledValue = wholeExpression
|
|
? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}`
|
|
: compileAttrValue(attr.value, ctx);
|
|
const rendered = ` ${attr.name}="${compiledValue}"`;
|
|
if (wholeExpression ||
|
|
!attr.value.includes("{") ||
|
|
!exprRefsState(attr.value, ctx.stateNames)) {
|
|
return rendered;
|
|
}
|
|
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
|
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
|
})
|
|
.join("");
|
|
const loops = loopVarsOf(node);
|
|
const childCtx = loops.length > 0
|
|
? {
|
|
...ctx,
|
|
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 ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
|
`${attrs}>${inner}</div>`);
|
|
}
|
|
function ssrMarker(bindings, binding) {
|
|
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
|
bindings.push({ marker, ...binding });
|
|
return marker;
|
|
}
|
|
function csrMarker(bindings, binding) {
|
|
const id = String(bindings.length);
|
|
bindings.push({ id, ...binding });
|
|
return id;
|
|
}
|
|
function renderBinding(binding) {
|
|
return {
|
|
method: binding.method,
|
|
path: binding.path,
|
|
body: binding.body,
|
|
helpers: binding.helpers,
|
|
};
|
|
}
|
|
function hasClientBehavior(nodes) {
|
|
return nodes.some((node) => {
|
|
// `{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 modeHelpers(ast, mode, sharedHelpers) {
|
|
return [
|
|
sharedHelpers,
|
|
...ast.modeFunctions
|
|
.filter((block) => block.mode === mode)
|
|
.map((block) => block.body.trim())
|
|
.filter(Boolean),
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n\n");
|
|
}
|
|
function apiBindingMap(ast, sharedHelpers) {
|
|
const bindings = new Map();
|
|
for (const block of ast.dataApis) {
|
|
if (bindings.has(block.name)) {
|
|
throw new Error(`Duplicate .wrn api binding "${block.name}"`);
|
|
}
|
|
bindings.set(block.name, {
|
|
mode: block.mode,
|
|
method: block.method,
|
|
path: apiRoutePath(block.path),
|
|
body: dataBody(block.body),
|
|
helpers: modeHelpers(ast, block.mode, sharedHelpers),
|
|
});
|
|
}
|
|
return bindings;
|
|
}
|
|
function ssrRuntimeSource() {
|
|
return `const __wrnexusHtmlEscapes = { "&": "&", "<": "<", ">": ">", "\\"": """, "'": "'" };
|
|
function __wrnexusEscapeHtml(value: unknown): string {
|
|
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
|
}
|
|
|
|
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any): unknown {
|
|
const adapters = {
|
|
cookies: ctx.cookies,
|
|
session: ctx.session,
|
|
localStorage: ctx.localStorage,
|
|
};
|
|
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
|
|
}
|
|
|
|
function __wrnexusPropAttr(
|
|
value: unknown,
|
|
): string {
|
|
const serialized =
|
|
value !== null &&
|
|
typeof value === "object"
|
|
? JSON.stringify(value)
|
|
: String(value == null ? "" : value);
|
|
|
|
return serialized.replace(
|
|
/[&<>"]/g,
|
|
(character) =>
|
|
character === "&"
|
|
? "&"
|
|
: character === "<"
|
|
? "<"
|
|
: character === ">"
|
|
? ">"
|
|
: """,
|
|
);
|
|
}
|
|
|
|
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
|
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
|
return await ctx.__wrnexusCallApi(path, method);
|
|
}
|
|
|
|
const url = new URL(path, ctx.req.url);
|
|
const res = await fetch(new Request(url, { method, headers: ctx.req.headers }));
|
|
if (!res.ok) {
|
|
throw new Error(".wrn data API request failed with status " + res.status);
|
|
}
|
|
|
|
const type = res.headers.get("content-type") || "";
|
|
return type.includes("application/json") ? await res.json() : await res.text();
|
|
}
|
|
|
|
async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<string> {
|
|
for (const binding of __wrnexusSsrBindings) {
|
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
|
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
|
html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
|
|
}
|
|
return html;
|
|
}`;
|
|
}
|
|
function 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"
|
|
? ["legacy", "client", "shared"]
|
|
: ["legacy", "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) {
|
|
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 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);
|
|
}
|
|
}
|
|
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
|
|
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
|
|
const loopConsts = [];
|
|
if (loops.length > 0) {
|
|
const lists = collectControlExprs(ast.view);
|
|
for (const [name, binding] of apiBindings) {
|
|
if (binding.mode !== "ssr")
|
|
continue;
|
|
if (!lists.some((expr) => new RegExp(`\\b${name}\\b`).test(expr)))
|
|
continue;
|
|
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
|
}
|
|
}
|
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
|
|
if (needsSsrRuntime) {
|
|
out.push(ssrRuntimeSource());
|
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
|
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
|
out.push(`export default async function ${ast.name}(ctx: any) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
${decls}
|
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
|
${ssrStateAliases}
|
|
${ssrComputedAliases}
|
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[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, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
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: any) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
|
${ssrStateAliases}
|
|
${ssrComputedAliases}
|
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[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, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
return \`${body}\`.replace(
|
|
"__WRNEXUS_DYNAMIC_SCOPE__",
|
|
__scopeValue,
|
|
);
|
|
}`);
|
|
}
|
|
if (staticShellBody !== undefined) {
|
|
out.push(`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
|
|
${storeDeclarations}
|
|
${serverLoadAliases}
|
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
|
${ssrStateAliases}
|
|
${ssrComputedAliases}
|
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[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, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
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: any) {
|
|
${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(", ")}]);
|
|
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: any, ctx: any) {
|
|
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: any) => {${api.body}};`);
|
|
});
|
|
const entries = ast.apis.map((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
|
out.push(`export const __wrnexusApi = {\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}: any) {${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, types_ts_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.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 (`${__wireHtml(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 (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)}">`) +
|
|
`\${__wireHtml(${ctx.resolveExpr(expr)})}` +
|
|
escLit(`</span>`);
|
|
}
|
|
else if (expr === "content") {
|
|
out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
else {
|
|
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(raw.slice(last));
|
|
}
|
|
/** 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 += `\${__wireAttr(${ctx.resolveExpr(expr)})}`;
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
return out + escLit(attrEscape(raw.slice(last)));
|
|
}
|
|
function renderComponentIfNode(node, ctx) {
|
|
let expression = "``";
|
|
for (let index = node.branches.length - 1; index >= 0; index--) {
|
|
const branch = node.branches[index];
|
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
|
const bodyExpression = "`" + body + "`";
|
|
expression =
|
|
branch.cond === null
|
|
? bodyExpression
|
|
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
|
}
|
|
return "${" + expression + "}";
|
|
}
|
|
function renderComponentEachNode(node, ctx) {
|
|
const item = node.item;
|
|
const index = node.index ?? "__wi";
|
|
const list = ctx.resolveExpr(node.list);
|
|
const childCtx = {
|
|
...ctx,
|
|
serverLocals: new Set([...(ctx.serverLocals ?? []), item, index]),
|
|
};
|
|
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
|
|
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
|
|
return ("${(() => { const __wl = Array.isArray(" +
|
|
list +
|
|
") ? (" +
|
|
list +
|
|
") : []; return __wl.length ? __wl.map((" +
|
|
item +
|
|
", " +
|
|
index +
|
|
") => `" +
|
|
body +
|
|
'`).join("") : `' +
|
|
empty +
|
|
"`; })()}");
|
|
}
|
|
function 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 = 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 `\${__wireSpreadAttrs(${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])))}"`
|
|
: "";
|
|
return `\${__wireBooleanAttr(${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);
|
|
const compiledValue = isExplicitComponentMount && wholeExpression
|
|
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
|
|
: compileAttrValue(a.value, elementContext);
|
|
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 ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
|
`${ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""}` +
|
|
`${classAttribute}` +
|
|
`${classReactiveBinding}` +
|
|
`${classBindings}` +
|
|
`${attrs}`;
|
|
if (parser_ts_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,
|
|
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));
|
|
// A component needs a reactive scope only when it has state or event handlers.
|
|
// Prop-driven text/attributes are baked server-side, so static components ship
|
|
// no JavaScript at all.
|
|
const behavior = componentBehavior(ast);
|
|
const needsScope = ast.runtime !== "server" &&
|
|
(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 ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))});`);
|
|
}
|
|
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: any, def: any, declared: string = "unknown"): any {
|
|
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);
|
|
return Array.isArray(parsed) ? parsed : def;
|
|
} catch {
|
|
if (declared === "array") throw new TypeError("Expected an array prop");
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
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);
|
|
|
|
return (
|
|
parsed !== null &&
|
|
typeof parsed === "object" &&
|
|
!Array.isArray(parsed)
|
|
)
|
|
? parsed
|
|
: def;
|
|
} catch {
|
|
if (declared === "object") throw new TypeError("Expected an object prop");
|
|
return def;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
if (declared === "bigint") return BigInt(v);
|
|
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, any>,
|
|
declared: Set<string>,
|
|
): Record<string, any> {
|
|
return Object.fromEntries(
|
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
|
);
|
|
}
|
|
|
|
function __wireHtml(v: any): string {
|
|
return String(v == null ? "" : v).replace(
|
|
/[&<>]/g,
|
|
(c) =>
|
|
c === "&"
|
|
? "&"
|
|
: c === "<"
|
|
? "<"
|
|
: ">",
|
|
);
|
|
}
|
|
|
|
function __wireAttr(v: any): string {
|
|
return String(v == null ? "" : v).replace(
|
|
/[&<>"]/g,
|
|
(c) =>
|
|
c === "&"
|
|
? "&"
|
|
: c === "<"
|
|
? "<"
|
|
: c === ">"
|
|
? ">"
|
|
: """,
|
|
);
|
|
}
|
|
|
|
function __wireBooleanAttr(name: string, value: any): string {
|
|
return value === true ||
|
|
value === "true" ||
|
|
value === "" ||
|
|
value === 1 ||
|
|
value === "1" ||
|
|
value === name
|
|
? " " + name
|
|
: "";
|
|
}
|
|
|
|
function __wireSpreadAttrs(value: any): 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" ||
|
|
lowerName.startsWith("data-wrn")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (booleanAttributes.has(lowerName)) {
|
|
attributes.push(__wireBooleanAttr(name, raw));
|
|
continue;
|
|
}
|
|
|
|
if (raw === false || raw === null || raw === undefined) continue;
|
|
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
|
}
|
|
|
|
return attributes.join("");
|
|
}
|
|
|
|
function __wireProp(v: any): string {
|
|
const value =
|
|
v !== null && typeof v === "object"
|
|
? JSON.stringify(v)
|
|
: String(v == null ? "" : v);
|
|
|
|
return __wireAttr(value);
|
|
}
|
|
|
|
function __wireRaw(v: any): string {
|
|
return String(v == null ? "" : v);
|
|
}`);
|
|
if (hasServerEach) {
|
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
|
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
}`);
|
|
}
|
|
if (needsScope) {
|
|
out.push(`function __wrnexusSerializeScopeValue(value: any): 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, any>): string {
|
|
return Object.keys(obj)
|
|
.map(
|
|
(key) =>
|
|
key +
|
|
": " +
|
|
__wrnexusSerializeScopeValue(
|
|
obj[key],
|
|
),
|
|
)
|
|
.join(", ")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}`);
|
|
}
|
|
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
|
out.push(`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"}): 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 __wireRaw(v) {
|
|
return String(v == null ? "" : v);
|
|
}
|
|
function wholeAttributeExpression(value) {
|
|
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
|
|
return match?.[1]?.trim() || null;
|
|
}
|
|
function __wireHtml(v) {
|
|
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
}
|
|
function __wireAttr(v) {
|
|
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """);
|
|
}
|
|
function __wireProp(v) {
|
|
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
|
return __wireAttr(value);
|
|
}
|
|
function renderPageComponentAttr(attr, dynamicExpressions) {
|
|
if (attr.event) {
|
|
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
|
}
|
|
if (attr.boolean) {
|
|
return ` ${attr.name}`;
|
|
}
|
|
const expression = wholeAttributeExpression(attr.value);
|
|
if (!expression) {
|
|
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
|
}
|
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
|
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
|
return ` ${attr.name}="${marker}"`;
|
|
}
|
|
|
|
},
|
|
"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}.d.ts`,
|
|
(0, node_path_1.join)(path, "index.wrn"),
|
|
(0, node_path_1.join)(path, "index.ts"),
|
|
];
|
|
}
|
|
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)
|
|
return { declaration, resolved: (0, node_fs_1.realpathSync)(found) };
|
|
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.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.generateBrowserModule = exports.generateTargets = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = exports.formatWrn = void 0;
|
|
exports.compileNativeWireFile = compileNativeWireFile;
|
|
exports.compileWireFile = compileWireFile;
|
|
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 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 compileNativeWireFile(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 compileWireFile(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; } });
|
|
|
|
},
|
|
"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;
|
|
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 match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
if (match.index > last)
|
|
pieces.push(value.slice(last, match.index));
|
|
const expr = match[1].trim();
|
|
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
|
last = match.index + match[0].length;
|
|
}
|
|
pieces.push(value.slice(last));
|
|
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
|
}
|
|
function eventBody(value, states) {
|
|
let body = expression(value) ?? value;
|
|
for (const state of states) {
|
|
const cap = state[0].toUpperCase() + state.slice(1);
|
|
body = body
|
|
.replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`)
|
|
.replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`)
|
|
.replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`);
|
|
}
|
|
return `() => { ${body} }`;
|
|
}
|
|
function 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/parser.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 });
|
|
/** @deprecated Import parser APIs from @wrnexus/syntax. */
|
|
__exportStar(require("@wrnexus/syntax/parser"), exports);
|
|
|
|
},
|
|
"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) => ["legacy", "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, ["legacy", "server", "shared"]))
|
|
.filter(Boolean)
|
|
.join("\n\n");
|
|
const names = ast.runtimeFunctions
|
|
.filter((fn) => ["legacy", "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", "legacy"].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*)wire-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"; }) || candidates.find(function (entry) { return entry.runtime === "legacy"; });
|
|
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/tokenizer.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 });
|
|
/** @deprecated Import tokenizer APIs from @wrnexus/syntax. */
|
|
__exportStar(require("@wrnexus/syntax/tokenizer"), exports);
|
|
|
|
},
|
|
"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" || fn.runtime === "legacy")
|
|
.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/compiler/src/types.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 });
|
|
/** @deprecated Import language type utilities from @wrnexus/syntax. */
|
|
__exportStar(require("@wrnexus/syntax/types"), exports);
|
|
|
|
},
|
|
"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.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 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 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.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = 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; } });
|
|
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 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/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");
|
|
/**
|
|
* 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 "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 (lx.peek().type !== "rbrace") {
|
|
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;
|
|
const mode = rawMode === "client" ? "client" : "ssr";
|
|
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 (mode === "client" && lx.peek().type === "eq") {
|
|
lx.next();
|
|
hydrate = expect("string").value;
|
|
break;
|
|
}
|
|
expect("lbrace");
|
|
while (lx.peek().type !== "rbrace") {
|
|
const member = lx.peek();
|
|
if (member.type === "eof") {
|
|
throw new ParseError(`Unexpected end of input inside ${mode} block`);
|
|
}
|
|
if (member.type !== "ident") {
|
|
throw new ParseError(`Expected a ${mode} member keyword at offset ${member.pos}`);
|
|
}
|
|
switch (member.value) {
|
|
case "api": {
|
|
lx.next();
|
|
const name = expect("ident").value;
|
|
const method = expect("ident").value.toUpperCase();
|
|
const path = lx.readPath();
|
|
const body = lx.readBalancedBraces();
|
|
dataApis.push({ mode, name, method, path, body });
|
|
break;
|
|
}
|
|
case "functions": {
|
|
lx.next();
|
|
modeFunctions.push({ mode, body: lx.readBalancedBraces() });
|
|
break;
|
|
}
|
|
default:
|
|
throw new ParseError(`Unknown ${mode} member '${member.value}' at offset ${member.pos}`);
|
|
}
|
|
}
|
|
expect("rbrace");
|
|
break;
|
|
}
|
|
case "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;
|
|
}
|
|
default:
|
|
throw new ParseError(`Unknown page member '${kw.value}' at offset ${kw.pos}`);
|
|
}
|
|
}
|
|
expect("rbrace");
|
|
const declaredStates = new Set(states.map((state) => state.name));
|
|
for (const watcher of watches) {
|
|
if (!declaredStates.has(watcher.state)) {
|
|
throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`);
|
|
}
|
|
}
|
|
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);
|
|
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++;
|
|
const start = i;
|
|
while (i < src.length && src[i] !== quote)
|
|
i++;
|
|
if (i >= src.length)
|
|
return fail("Unterminated attribute value");
|
|
const value = src.slice(start, i);
|
|
i++; // 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.LexError = void 0;
|
|
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);
|
|
const isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
|
class Lexer {
|
|
src;
|
|
pos = 0;
|
|
constructor(src) {
|
|
this.src = src;
|
|
}
|
|
/** Skip whitespace and `// line comments`. */
|
|
skipTrivia() {
|
|
const { src } = this;
|
|
while (this.pos < src.length) {
|
|
const c = src[this.pos];
|
|
if (isWs(c)) {
|
|
this.pos++;
|
|
continue;
|
|
}
|
|
if (c === "/" && src[this.pos + 1] === "/") {
|
|
while (this.pos < src.length && src[this.pos] !== "\n")
|
|
this.pos++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/** 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 (isIdentStart(c)) {
|
|
let v = "";
|
|
while (this.pos < src.length && isIdentPart(src[this.pos]))
|
|
v += src[this.pos++];
|
|
return { type: "ident", value: v, pos };
|
|
}
|
|
throw new LexError(`Unexpected character '${c}' at offset ${pos} (line ${this.lineAt(pos)})`);
|
|
}
|
|
/** 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.
|
|
*/
|
|
readBalancedBraces() {
|
|
this.skipTrivia();
|
|
const { src } = this;
|
|
if (src[this.pos] !== "{") {
|
|
throw new LexError(`Expected '{' at offset ${this.pos}`);
|
|
}
|
|
const start = this.pos + 1;
|
|
let depth = 0;
|
|
let i = this.pos;
|
|
let str = null;
|
|
for (; i < src.length; i++) {
|
|
const c = src[i];
|
|
if (str) {
|
|
if (c === "\\") {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (c === str)
|
|
str = null;
|
|
continue;
|
|
}
|
|
if (c === '"' || c === "'" || c === "`") {
|
|
str = c;
|
|
continue;
|
|
}
|
|
if (c === "{")
|
|
depth++;
|
|
else if (c === "}") {
|
|
depth--;
|
|
if (depth === 0) {
|
|
this.pos = i + 1;
|
|
return src.slice(start, i);
|
|
}
|
|
}
|
|
}
|
|
throw new LexError(`Unbalanced braces starting at offset ${this.pos}`);
|
|
}
|
|
lineAt(pos) {
|
|
let line = 1;
|
|
for (let i = 0; i < pos && i < this.src.length; i++) {
|
|
if (this.src[i] === "\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(/(\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 = "legacy";
|
|
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");
|