diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 6f95b342..50fee246 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -429,6 +429,15 @@ const MIGRATIONS: Migration[] = [ // No generated application files require automatic migration. }, }, + { + version: "0.2.40", + id: "conditions-bug", + description: + "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", + apply() { + // No generated application files require automatic migration. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index bfc265d4..8e459e9c 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { generate, parse } from "../src/index.ts"; import { compileWireFile } from "../src/index.ts"; +import { mountHtml } from "@wrnexus/test"; let seq = 0; /** Compile a `.wrn` source and import the resulting module. */ diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index e45dc24b..78b3cdd1 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -808,12 +808,176 @@ export const REACTIVE_RUNTIME = String.raw` write(name, next); } + function evaluateSpecialExpression( + expression, + read, + ) { + var source = String( + expression || "", + ).trim(); + + // Array literals containing spread: + // + // [...items, value] + // [first, ...others] + // + if ( + source.charAt(0) === "[" && + source.charAt(source.length - 1) === "]" && + source.indexOf("...") !== -1 + ) { + var inner = source.slice(1, -1); + var parts = splitTopLevel(inner, ","); + var result = []; + + parts.forEach(function (part) { + var valueSource = part.trim(); + + if ( + valueSource.indexOf("...") === 0 + ) { + var spreadValue = + evaluateExpression( + valueSource.slice(3), + read, + ); + + if (Array.isArray(spreadValue)) { + Array.prototype.push.apply( + result, + spreadValue, + ); + } + + return; + } + + result.push( + evaluateExpression( + valueSource, + read, + ), + ); + }); + + return { + matched: true, + value: result, + }; + } + + // Array callback methods: + // + // items.filter((item) => item !== value) + // items.map((item, index) => item.title) + // items.some((item) => item.active) + // + var callbackMatch = + /^([\s\S]+)\.(filter|map|some|every|find|findIndex)\(\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*([A-Za-z_$][A-Za-z0-9_$]*))?\s*\)\s*=>\s*([\s\S]+)\s*\)$/.exec( + source, + ); + + if (!callbackMatch) { + callbackMatch = + /^([\s\S]+)\.(filter|map|some|every|find|findIndex)\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*([\s\S]+)\s*\)$/.exec( + source, + ); + + if (callbackMatch) { + callbackMatch = [ + callbackMatch[0], + callbackMatch[1], + callbackMatch[2], + callbackMatch[3], + undefined, + callbackMatch[4], + ]; + } + } + + if (callbackMatch) { + var collection = + evaluateExpression( + callbackMatch[1], + read, + ); + + var method = callbackMatch[2]; + var itemName = callbackMatch[3]; + var indexName = callbackMatch[4]; + var body = callbackMatch[5]; + + if (!Array.isArray(collection)) { + collection = []; + } + + var callback = function ( + item, + index, + ) { + return evaluateExpression( + body, + function (name) { + if (name === itemName) { + return item; + } + + if ( + indexName && + name === indexName + ) { + return index; + } + + return read(name); + }, + ); + }; + + return { + matched: true, + value: + method === "filter" + ? collection.filter(callback) + : method === "map" + ? collection.map(callback) + : method === "some" + ? collection.some(callback) + : method === "every" + ? collection.every(callback) + : method === "find" + ? collection.find(callback) + : collection.findIndex( + callback, + ), + }; + } + + return { + matched: false, + value: undefined, + }; + } + // A small, eval-free expression evaluator (so a strict CSP needs no // 'unsafe-eval'). Supports: literals, identifiers, member access (a.b, a[b]), // function/method calls, arrays, objects, arithmetic, comparison, equality, // logical (&& ||), unary (! - +), and the ternary operator. function evaluateExpression(expr, read) { - var tokens = tokenizeExpression(String(expr || "")); + expr = String(expr || "").trim(); + + var special = + evaluateSpecialExpression( + expr, + read, + ); + + if (special.matched) { + return special.value; + } + + var tokens = + tokenizeExpression(expr); var index = 0; function peek() { return tokens[index]; } function next() { return tokens[index++]; }