From e4dd4b5622e2b7c0cedfc318a84d1ee8ca675a51 Mon Sep 17 00:00:00 2001 From: Ellyse <141240083+ellyxir@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:07:26 +0200 Subject: [PATCH 1/5] smoketest version of ralph, basic files (#1951) * smoketest version of ralph, basic files * temporarily use git branch * ask ralph to use playwright * telling it how to deploy locally * how to use smoketest doc * cleanup formatting for TASKS.md * added script to run several ralph smoketests and stop them * clean up to point back to main, ready for landing * added note to self to clean up sleep infinity later --- .gitignore | 1 + tools/ralph/Dockerfile | 8 +-- tools/ralph/README.md | 68 +++++++++++++++++++++++- tools/ralph/SMOKETEST_PROMPT.md | 40 ++++++++++++++ tools/ralph/TASKS.md | 83 +++++++++++++++++++----------- tools/ralph/bin/ralph-smoketest.sh | 44 ++++++++++++++++ tools/ralph/bin/run_smoketest.sh | 33 ++++++++++++ tools/ralph/bin/stop_smoketest.sh | 10 ++++ 8 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 tools/ralph/SMOKETEST_PROMPT.md create mode 100755 tools/ralph/bin/ralph-smoketest.sh create mode 100755 tools/ralph/bin/run_smoketest.sh create mode 100755 tools/ralph/bin/stop_smoketest.sh diff --git a/.gitignore b/.gitignore index e2c94be332..1161665638 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ claude-research.key local-dev-toolshed.log local-dev-shell.log tools/ralph/logs/ +tools/ralph/smoketest/ diff --git a/tools/ralph/Dockerfile b/tools/ralph/Dockerfile index 891a6e6b21..9656242168 100644 --- a/tools/ralph/Dockerfile +++ b/tools/ralph/Dockerfile @@ -76,6 +76,8 @@ RUN npm install -g @anthropic-ai/claude-code && \ # --no-sandbox is required because Docker containers restrict namespace creation RUN claude mcp add --scope user playwright npx "@playwright/mcp@latest" -- --headless --isolated --no-sandbox -# Start Common Tool servers in background and keep container alive -# the sleep ensures that restarting the server doesnt cause the container to exit -CMD ["/bin/sh", "-c", "/app/start-servers.sh & sleep infinity"] +# Start servers in background, run smoketest, then exit +# If you want to run ralph normally without the smoketest, then replace with: +# CMD ["/bin/sh", "-c", "/app/start-servers.sh & sleep infinity"] +# TODO(@ellyxir): remove sleep infinity, smoketest should exit when its done +CMD ["/bin/sh", "-c", "/app/start-servers.sh & /app/labs/tools/ralph/bin/ralph-smoketest.sh; sleep infinity"] diff --git a/tools/ralph/README.md b/tools/ralph/README.md index c0f812ccce..ff3c425271 100644 --- a/tools/ralph/README.md +++ b/tools/ralph/README.md @@ -6,7 +6,73 @@ Ability to run [Ralph](https://ghuntley.com/ralph/) Claude CLI and Codex are installed -## How to run Ralph +## How to use Smoketest Ralph + +Smoketest Ralph runs a single task from `TASKS.md` and exits when complete. Each +Ralph instance is assigned a specific task via the `RALPH_ID` environment +variable. + +### Running smoketests + +**Option 1: Use the automated script (recommended)** + +Run multiple smoketests in parallel (tasks 1-3): + +```bash +./tools/ralph/bin/run_smoketest.sh +``` + +This script will: + +- Clean up old results +- Stop/remove any existing ralph containers +- Start 3 smoketest containers in parallel (RALPH_ID 1, 2, and 3) +- Results are written to `./tools/ralph/smoketest//` + +Monitor progress with: + +```bash +docker logs ralph_1 # or ralph_2, ralph_3 +``` + +To stop all running smoketests: + +```bash +./tools/ralph/bin/stop_smoketest.sh +``` + +**Option 2: Run a single smoketest manually** + +1. Build the Docker image (if not using pre-built): + +```bash +docker build -t ellyxir/ralph tools/ralph/ +``` + +2. Run the container with your RALPH_ID and mounted credentials: + +```bash +cd ~/labs +docker run -e RALPH_ID=3 -d -v ~/.claude.json:/home/ralph/.claude.json \ + -v ~/.claude/.credentials.json:/home/ralph/.claude/.credentials.json \ + -v ./tools/ralph/smoketest:/app/smoketest --name ralph ellyxir/ralph +``` + +Note: The container will exit automatically when the smoketest completes. + +### Retrieving results + +Results are available on the host machine in +`./tools/ralph/smoketest/${RALPH_ID}/`: + +- `SCORE.txt` - Contains SUCCESS, PARTIAL, or FAILURE +- `RESULTS.md` - Summary of work including test results +- Pattern files created during the task + +No need to copy files from the container - the bind mount makes results +immediately available on your host. + +## How to run Ralph (not smoketest) ### Using pre-built image from Docker Hub (recommended) diff --git a/tools/ralph/SMOKETEST_PROMPT.md b/tools/ralph/SMOKETEST_PROMPT.md new file mode 100644 index 0000000000..5182046212 --- /dev/null +++ b/tools/ralph/SMOKETEST_PROMPT.md @@ -0,0 +1,40 @@ +# Smoketest Ralph General Prompt + +Goal: implement the unchecked item from `./tools/ralph/TASKS.md` that matches +your assigned RALPH_ID + +1. Open `./tools/ralph/TASKS.md` and find the task numbered with your RALPH_ID. + +2. If your assigned task is already checked `[x]`, exit with a message saying + the task is already complete. + +3. Use Claude Skills "recipe-dev" to work on the task that corresponds to your + RALPH_ID number. + +4. Format with `deno fmt` for the changed files. + +5. Once tests pass, deploy it locally using `./docs/common/RECIPE_DEV_DEPLOY.md` + for info on how to deploy. It should deploy to http://localhost:8080 and not + to toolshed. Servers are already running. + +6. Once it is deployed locally, use a subagent to test your work with MCP + playwright + +7. Check off the completed items in `TASKS.md`: + +8. git stage and commit with a message + +9. Copy the files you created for the task to /app/smoketest/${RALPH_ID}/ + +10. Create a summary of your work in the same directory, be sure to include + playwright results and what you tested. file location: + /app/smoketest/${RALPH_ID}/RESULTS.md + +11. Create a /app/smoketest/${RALPH_ID}/SCORE.txt which has one of the following + values based on your results: SUCCESS, PARTIAL, FAILURE + +12. Add feedback to documentation to `./tools/ralph/LEARNINGS.md`. + +13. Exit + +Please begin. diff --git a/tools/ralph/TASKS.md b/tools/ralph/TASKS.md index 908f53f687..227fbffb9f 100644 --- a/tools/ralph/TASKS.md +++ b/tools/ralph/TASKS.md @@ -1,33 +1,54 @@ # Ralph Task List -A running checklist of tasks. New items include brief implementation notes for a -future Ralph pass. - -Tasks marked with [UI] mean they should add/remove/modify the UI of the pattern. -UI tasks should wire up the functionality and call the appropriate handlers. - -- [] Create a counter - - [] Add [UI] buttons for incrementing counter - - **Test with Playwright**: click increment 3 times, verify counter shows 3, - click decrement once, verify counter shows 2 - - **State**: The displayed count matches the pattern's `count` output field - - [] Create multiple counters - - [] Add [UI] buttons to create multiple counters - - **Test with Playwright**: create 3 counters, test each one - - **State**: Each counter maintains its own value in the pattern's - `counters` array -- [] Create a shopping list - - [] Create [UI] for shopping list - - **Test with Playwright**: add "milk" and "bread", make sure you see both, - remove "bread", verify only "milk" remains - - **State**: The list shows all items from pattern's `items` array with - correct `completed` status -- [] Lunch voter - list of destinations (just a string) (dedup) - - [] [UI] for adding list of destination (just a string) and displaying it - - **UI must**: Show an editable list with add/remove buttons for - destinations - - **Test with Playwright**: Deploy pattern, add at least 2 destinations via - UI, verify they appear in the list, remove one destination, verify it's - removed from both UI and charm output - - **State**: The displayed list matches the pattern's `destinations` output - field +1. [ ] Counter + - Components: Count display + Increment/decrement buttons + Reset button + - Data: Current count value + - Features: Increment, decrement, reset to zero + +2. [ ] Shopping List with sort-by-category and budget tracking. These should be + 3 different patterns (shopping list, category list, and budget tracker) + and a final pattern that combines them together and acts as a launcher. + - Components: Shopping list (item input + checkboxes + clear button) + + Category list (category input + item assignment) + Budget tracker (price + input + total display) + Launcher (tabs/buttons to switch between views) + - Data: Shopping items with name, category, price, checked status; Categories + with names + - Features: Add/remove items, assign categories, track prices, sort by + category, view budget totals, check off purchased items + +3. [ ] Calendar + - Components: Month view with day cells + Event list displayed in calendar + + Day editor (opens when clicking a day) + - Data: Events with date, time, description + - Features: View one month at a time, click day to edit its event list, + events shown in calendar UI + +4. [ ] Fitness Workout Planner + - Components: Exercise routine builder + Set/rep counter + Progress chart + - Data: Exercises with sets, reps, weight + - Features: Track personal records, show strength gains over time + +5. [ ] Lunch Voter + - Components: Restaurant list + Voting buttons + Vote tally display + + Add/remove restaurant form + - Data: Restaurants with vote counts + - Features: Add/remove restaurants, vote for favorites, see most popular + choice + +6. [ ] Study Schedule with Focus Timer + - Components: Study task list + Time block scheduler + Pomodoro timer + Break + reminders + - Data: Study topics, estimated duration, completion status + - Features: Schedule study sessions, track time spent, enforce breaks + +7. [ ] Travel Itinerary with Budget Tracker + - Components: Activity scheduler + Day-by-day timeline + Expense tracker + + Budget dashboard + - Data: Activities with time, location, cost + - Features: Plan entire trip, track expenses by category, budget warnings + +8. [ ] Contact Manager with Birthday Reminders + - Components: Contact list + Upcoming birthdays view + Gift idea notes + + Calendar integration + - Data: Contacts with birthdays, gift history + - Features: Birthday notifications, gift suggestions, relationship notes diff --git a/tools/ralph/bin/ralph-smoketest.sh b/tools/ralph/bin/ralph-smoketest.sh new file mode 100755 index 0000000000..fb5d6333a3 --- /dev/null +++ b/tools/ralph/bin/ralph-smoketest.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Get the ralph directory (parent of bin) +RALPH_DIR="$(dirname "$SCRIPT_DIR")" +# Get the labs directory (two levels up from bin) +LABS_DIR="$(dirname "$(dirname "$RALPH_DIR")")" + +# Change to labs directory for relative paths to work +cd "$LABS_DIR" + +# Check RALPH_ID is set +if [ -z "$RALPH_ID" ]; then + echo "Error: RALPH_ID environment variable is not set" + exit 1 +fi + +# Ensure logs directory exists +mkdir -p ./tools/ralph/logs + +# Rotate logs keeping last 5 +for i in 4 3 2 1; do + [ -f ./tools/ralph/logs/ralph-claude.log.$i ] && mv ./tools/ralph/logs/ralph-claude.log.$i ./tools/ralph/logs/ralph-claude.log.$((i+1)) +done +[ -f ./tools/ralph/logs/ralph-claude.log ] && mv ./tools/ralph/logs/ralph-claude.log ./tools/ralph/logs/ralph-claude.log.1 + +# llm command to summarize changes +LLM="./tools/ralph/bin/llm.sh" + +{ printf "Your RALPH_ID is %s.\n\n" "$RALPH_ID"; cat ./tools/ralph/SMOKETEST_PROMPT.md; } | \ +claude --print --dangerously-skip-permissions \ +--verbose --output-format=stream-json 2>&1 | \ +tee -a ./tools/ralph/logs/ralph-claude.log + +# Auto-stash changes if any exist +if [[ -n "$(git status --porcelain)" ]]; then + git add -A + + # Generate commit message from staged changes + commit_msg=$(git diff --staged | $LLM "Summarize these changes into a short one-line description, output just that one line") + + git stash push -m "$commit_msg" +fi diff --git a/tools/ralph/bin/run_smoketest.sh b/tools/ralph/bin/run_smoketest.sh new file mode 100755 index 0000000000..9079726083 --- /dev/null +++ b/tools/ralph/bin/run_smoketest.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Get the ralph directory (parent of bin) +RALPH_DIR="$(dirname "$SCRIPT_DIR")" +# Get the labs directory (two levels up from bin) +LABS="$(dirname "$(dirname "$RALPH_DIR")")" + +# Change to labs directory +cd "$LABS" + +# Stop and remove any existing ralph containers first +for ID in 1 2 3; do + docker stop ralph_$ID 2>/dev/null || true + docker rm ralph_$ID 2>/dev/null || true +done + +# Remove existing results after containers are stopped +rm -rf "$LABS/tools/ralph/smoketest"/[0-9]* + +# Run smoketests for IDs 1 through 3 +for ID in 1 2 3; do + echo "Starting smoketest for RALPH_ID=$ID" + docker run --rm -e RALPH_ID=$ID -d \ + -v ~/.claude.json:/home/ralph/.claude.json \ + -v ~/.claude/.credentials.json:/home/ralph/.claude/.credentials.json \ + -v "$LABS/tools/ralph/smoketest:/app/smoketest" \ + --name ralph_$ID \ + ellyxir/ralph +done + +echo "All smoketests started. Use 'docker logs ralph_' to monitor progress." diff --git a/tools/ralph/bin/stop_smoketest.sh b/tools/ralph/bin/stop_smoketest.sh new file mode 100755 index 0000000000..bd219bb83e --- /dev/null +++ b/tools/ralph/bin/stop_smoketest.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Stop and remove all ralph smoketest containers +for ID in 1 2 3; do + echo "Stopping ralph_$ID..." + docker stop ralph_$ID 2>/dev/null || true + docker rm ralph_$ID 2>/dev/null || true +done + +echo "All smoketest containers stopped and removed." From d495fac3fbc2f86b46d172a8fb297dc7558c9c8d Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 24 Oct 2025 11:20:58 -0700 Subject: [PATCH 2/5] fix(transform): synthetic map params leak (#1955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Filter synthetic map params from outer scope derives Fixes bug where element/index/array leaked into outer scope derives. Adds special case handling for single non-synthetic dataflow with synthetic map params. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Refactor: Replace heuristics with explicit map callback tracking Replaces fragile heuristic-based detection of map callbacks with explicit marking using a shared WeakSet registry. This fixes bugs where synthetic identifiers (element, index, array) leaked into outer scope derives. **Problem:** - ClosureTransformer creates synthetic identifiers without symbols for map params - OpaqueRefJSXTransformer analyzes transformed AST and needs to distinguish: 1. Synthetic params INSIDE map callbacks (keep) 2. Synthetic params that leaked to outer scope (filter out) - Previous heuristics based on counting dataflows were brittle and error-prone **Solution:** 1. Add `mapCallbackRegistry: WeakSet` to TransformationOptions 2. ClosureTransformer marks arrow functions when creating map callbacks 3. OpaqueRefJSXTransformer checks marking to detect map callback scopes 4. Filter logic: - Only synthetic dataflows → inside callback (keep all) - Mixed synthetic + non-synthetic → check if in marked callback - If not in marked callback → filter out synthetic params **Benefits:** - Direct knowledge instead of heuristics (no more magic thresholds) - Clear contract between transformers via shared registry - More maintainable - reduced from ~130 lines to ~100 lines - Fixes "element is not defined" runtime errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .../src/closures/transformer.ts | 3 + packages/ts-transformers/src/core/context.ts | 17 ++ .../ts-transformers/src/core/transformers.ts | 1 + packages/ts-transformers/src/ct-pipeline.ts | 1 + .../src/transformers/opaque-ref/helpers.ts | 147 ++++++++++-------- .../closures/map-single-capture.expected.tsx | 2 +- .../map-single-capture.expected.tsx | 46 ++++++ .../map-single-capture.input.tsx | 23 +++ 8 files changed, 175 insertions(+), 65 deletions(-) create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.input.tsx diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index e28eaf89df..b3888e52b5 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -1015,6 +1015,9 @@ function createRecipeCallWithParams( transformedBody, ); + // Mark this as a map callback for later transformers (e.g., OpaqueRefJSXTransformer) + context.markAsMapCallback(newCallback); + // Build a TypeNode for the callback parameter to pass as a type argument to recipe() const callbackParamTypeNode = buildCallbackParamTypeNode( mapCall, diff --git a/packages/ts-transformers/src/core/context.ts b/packages/ts-transformers/src/core/context.ts index 6497485b11..e5c1146459 100644 --- a/packages/ts-transformers/src/core/context.ts +++ b/packages/ts-transformers/src/core/context.ts @@ -68,4 +68,21 @@ export class TransformationContext { column: location.character + 1, }); } + + /** + * Mark an arrow function as a map callback created by ClosureTransformer. + * This allows later transformers to identify synthetic map callback scopes. + */ + markAsMapCallback(node: ts.Node): void { + if (this.options.mapCallbackRegistry) { + this.options.mapCallbackRegistry.add(node); + } + } + + /** + * Check if a node is a map callback created by ClosureTransformer. + */ + isMapCallback(node: ts.Node): boolean { + return this.options.mapCallbackRegistry?.has(node) ?? false; + } } diff --git a/packages/ts-transformers/src/core/transformers.ts b/packages/ts-transformers/src/core/transformers.ts index 6fd9169f4d..8b1d523d76 100644 --- a/packages/ts-transformers/src/core/transformers.ts +++ b/packages/ts-transformers/src/core/transformers.ts @@ -8,6 +8,7 @@ export interface TransformationOptions { readonly debug?: boolean; readonly logger?: (message: string) => void; readonly typeRegistry?: TypeRegistry; + readonly mapCallbackRegistry?: WeakSet; } export interface TransformationDiagnostic { diff --git a/packages/ts-transformers/src/ct-pipeline.ts b/packages/ts-transformers/src/ct-pipeline.ts index 04dc29558a..8238b1f27f 100644 --- a/packages/ts-transformers/src/ct-pipeline.ts +++ b/packages/ts-transformers/src/ct-pipeline.ts @@ -10,6 +10,7 @@ export class CommonToolsTransformerPipeline extends Pipeline { constructor(options: TransformationOptions = {}) { const ops = { typeRegistry: new WeakMap(), + mapCallbackRegistry: new WeakSet(), ...options, }; super([ diff --git a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts index 1957877853..177c2503f8 100644 --- a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts +++ b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts @@ -115,81 +115,100 @@ export function filterRelevantDataFlows( const syntheticDataFlows = dataFlows.filter((df) => hasSyntheticRoot(df.expression) ); - const nonSyntheticDataFlows = dataFlows.filter((df) => - !hasSyntheticRoot(df.expression) - ); - - // If we have both synthetic and non-synthetic dataflows, we need to determine - // if we're inside or outside the map callback - if (syntheticDataFlows.length > 0 && nonSyntheticDataFlows.length > 0) { - // Check if any dataflow is in a scope with parameters from a map callback - // If so, we're inside a callback being transformed and should keep all dataflows - const isInMapCallbackScope = dataFlows.some((df) => { - const scope = analysis.graph.scopes.find((s) => s.id === df.scopeId); - if (!scope) return false; - // Check if any parameter in this scope comes from a builder or array-map - return scope.parameters.some((param) => { - if (!param.declaration) return false; - const callKind = getOpaqueCallKindForParameter( - param.declaration, - context.checker, - ); - return callKind === "builder" || callKind === "array-map"; - }); + // If we have synthetic dataflows (e.g., element, index, array from map callbacks), + // these are identifiers without symbols that were created by ClosureTransformer. + // We need to determine if they're being used in the correct scope or if they leaked. + if (syntheticDataFlows.length > 0) { + // Check if the synthetic identifiers are standard map callback parameter names + const hasSyntheticMapParams = syntheticDataFlows.some((df) => { + let rootExpr: ts.Expression = df.expression; + while ( + ts.isPropertyAccessExpression(rootExpr) || + ts.isElementAccessExpression(rootExpr) + ) { + rootExpr = rootExpr.expression; + } + if (ts.isIdentifier(rootExpr)) { + const name = rootExpr.text; + // Standard map callback parameter names created by ClosureTransformer + return name === "element" || name === "index" || name === "array"; + } + return false; }); - // If we're inside a map callback scope, keep all dataflows - if (isInMapCallbackScope) { - // Keep all dataflows - we're inside a callback with both synthetic params and captures - return dataFlows.filter((dataFlow) => { - if ( - originatesFromIgnoredParameter( - dataFlow.expression, - dataFlow.scopeId, - analysis, - context.checker, - ) - ) { - return false; - } - return true; - }); - } + if (hasSyntheticMapParams) { + // We have synthetic map callback params. These could be: + // 1. Inside a map callback (keep them) + // 2. In outer scope where they leaked (filter them out) - // Heuristic: Check if the non-synthetic dataflows have symbols in the outer - // scope If they reference outer-scope variables (like cells), we're at the - // outer scope and should filter synthetic params. - const nonSyntheticHaveOuterScopeSymbols = nonSyntheticDataFlows.every( - (df) => { - let rootExpr: ts.Expression = df.expression; - while ( - ts.isPropertyAccessExpression(rootExpr) || - ts.isElementAccessExpression(rootExpr) - ) { - rootExpr = rootExpr.expression; - } - if (ts.isIdentifier(rootExpr)) { - const symbol = context.checker.getSymbolAtLocation(rootExpr); - // If it has a symbol, it's likely from outer scope - return !!symbol; + const nonSyntheticDataFlows = dataFlows.filter((df) => + !hasSyntheticRoot(df.expression) + ); + + // If we have ONLY synthetic dataflows, we're definitely inside a map callback + if (nonSyntheticDataFlows.length === 0) { + // Pure synthetic - we're inside a map callback, keep all + return dataFlows.filter((dataFlow) => { + if ( + originatesFromIgnoredParameter( + dataFlow.expression, + dataFlow.scopeId, + analysis, + context.checker, + ) + ) { + return false; + } + return true; + }); + } + + // We have both synthetic and non-synthetic. This could be: + // 1. Inside a map callback with captures (keep all) + // 2. Outer scope with leaked synthetic params (filter synthetics) + + // Try to find if any dataflow is from a scope with parameters that's a marked callback + const isInMarkedCallback = dataFlows.some((df) => { + const scope = analysis.graph.scopes.find((s) => s.id === df.scopeId); + if (!scope || scope.parameters.length === 0) return false; + + const firstParam = scope.parameters[0]; + if (!firstParam || !firstParam.declaration) return false; + + let node: ts.Node | undefined = firstParam.declaration.parent; + while (node) { + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + return context.isMapCallback(node); + } + node = node.parent; } return false; - }, - ); + }); - // If all non-synthetic dataflows have outer-scope symbols AND we have 2 or - // more of them, we're likely analyzing an outer-scope expression that - // contains nested map callbacks - if ( - nonSyntheticHaveOuterScopeSymbols && nonSyntheticDataFlows.length >= 2 - ) { + if (isInMarkedCallback) { + // Inside a map callback - keep all except ignored params + return dataFlows.filter((dataFlow) => { + if ( + originatesFromIgnoredParameter( + dataFlow.expression, + dataFlow.scopeId, + analysis, + context.checker, + ) + ) { + return false; + } + return true; + }); + } + + // Synthetic map params in outer scope - filter them out return dataFlows.filter((df) => !hasSyntheticRoot(df.expression)); } - - // Otherwise, we're inside a map callback with captures - keep all dataflows } + // No synthetic dataflows, use standard filtering return dataFlows.filter((dataFlow) => { if ( originatesFromIgnoredParameter( diff --git a/packages/ts-transformers/test/fixtures/closures/map-single-capture.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-single-capture.expected.tsx index ca27b444a9..1bdc9609d2 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-single-capture.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-single-capture.expected.tsx @@ -61,4 +61,4 @@ export default recipe({ // @ts-ignore: Internals function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } // @ts-ignore: Internals -h.fragment = __ctHelpers.h.fragment; \ No newline at end of file +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx new file mode 100644 index 0000000000..652f429fbb --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx @@ -0,0 +1,46 @@ +import * as __ctHelpers from "commontools"; +import { cell, recipe, UI } from "commontools"; +export default recipe("MapSingleCapture", (_state) => { + const people = cell([ + { id: "1", name: "Alice" }, + { id: "2", name: "Bob" }, + ]); + return { + [UI]: (
+ {__ctHelpers.derive(people, people => people.length > 0 && (
    + {people.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + $ref: "#/$defs/__object" + }, + params: { + type: "object", + properties: {} + } + }, + required: ["element", "params"], + $defs: { + __object: { + type: "object", + properties: { + id: { + type: "string" + }, + name: { + type: "string" + } + }, + required: ["id", "name"] + } + } + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => (
  • {element.name}
  • )), {})} +
))} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.input.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.input.tsx new file mode 100644 index 0000000000..3b378144d3 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.input.tsx @@ -0,0 +1,23 @@ +/// +import { cell, recipe, UI } from "commontools"; + +export default recipe("MapSingleCapture", (_state) => { + const people = cell([ + { id: "1", name: "Alice" }, + { id: "2", name: "Bob" }, + ]); + + return { + [UI]: ( +
+ {people.length > 0 && ( +
    + {people.map((person) => ( +
  • {person.name}
  • + ))} +
+ )} +
+ ), + }; +}); From 79648aafe71799522954452841fe59a0b1a996f4 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 24 Oct 2025 12:07:45 -0700 Subject: [PATCH 3/5] spec(runner): added more details for OpaqueRef / RegularCell merge (#1956) spec(runner): added more details for OpaqueRef / RegularCell merge --- .../specs/recipe-construction/rollout-plan.md | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/specs/recipe-construction/rollout-plan.md b/docs/specs/recipe-construction/rollout-plan.md index 66f693431a..0061dc1f47 100644 --- a/docs/specs/recipe-construction/rollout-plan.md +++ b/docs/specs/recipe-construction/rollout-plan.md @@ -2,27 +2,36 @@ - [ ] Disable ShadowRef/unsafe_ and see what breaks, ideally remove it - [ ] Update Cell API types to already unify them - - [ ] Create a CellLike<> type with a symbol based brand, with the value be + - [ ] Create an `AnyCell<>` type with a symbol based brand, with the value be `Record` - [ ] Factor out parts of the cell interfaces along reading, writing, .send (for stream-like) and derives (which is currently just .map) - - [ ] Define `OpaqueRef<>`, `Cell<>` and `Stream<>` by using these factored + - [ ] Define `OpaqueCell<>`, `Cell<>` and `Stream<>` by using these factored out parts, combined with the brand set to `{ opaque: true, read: false, write: false, stream: false }` for `OpaqueRef`, `{ opaque: false, read: true, write: true, stream: true }` for `Cell`, and `{ opaque: false, read: - false, write: false, stream: true }` for `Stream`. We can go ahead and add - ReadonlyCell and WriteonlyCell accordingly as well. - - [ ] Add `ComparableCell<>` that is all `false` above - - [ ] Alias `OpaqueCell<>` to `OpaqueRef<>` (maintain backward compatibility) - - [ ] For `OpaqueRef` we keep the proxy behavior, i.e. each key is an - `OpaqueRef` again. - - [ ] Simplify most wrap/unwrap types to use `CellLike`. + false, write: false, stream: true }` for `Stream`. + - [ ] Add `ComparableCell<>` that is all `false` above. + - [ ] Add `ReadonlyCell` and `WriteonlyCell`. + - [ ] Make `OpaqueRef` a variant of `OpaqueCell` with the current proxy + behavior, i.e. each key is an `OpaqueRef` again. That's just for now, until + the AST does a .key transformation under the hood. + - [ ] Update `CellLike` to be based on `AnyCell` but allow nesting. + - [ ] `Opaque` accepts `T` or any `CellLike` at any nesting level + - [ ] Simplify most wrap/unwrap types to use `CellLike`. We need + - [ ] "Accept any T where any sub part of T can be wrapped in one or more + `AnyCell`" (for inputs to node factories) + - [ ] "Strip any `AnyCell` from T and then wrap it in OpaqueRef<>" (for + outputs of node factories, where T is the output of the inner function) + - [ ] Make passing the output of the second into the first work. Tricky + because we're doing almost opposite expansions on the type. - [ ] Add ability to create a cell without a link yet. - [ ] Change constructor for RegularCell to make link optional - [ ] Add .for method to set a cause (within current context) - [ ] second parameter to make it optional/flexible: - [ ] ignores the .for if link already exists - [ ] adds extension if cause already exists (see tracker below) + - [ ] Make .key work even if there is no cause yet. - [ ] Add some method to force creation of cause, which errors if in non-handler context and no other information was given (as e.g. deriving nodes, which do have ids, after asking for them -- this walks the graph up @@ -31,8 +40,22 @@ isn't there, e.g. because we need to create a link to the cell (when passed into `anotherCell.set()` for example). We want to encourage .for use in ambiguous cases. -- First merge of OpaqueRef and RegularCell +- [ ] First merge of OpaqueRef and RegularCell - [ ] Add methods that allow linking to node invocations + - [ ] `setPreExisting` can be deprecated (used in toOpaqueRef which itself + can go away, see below) + - [ ] `setDefault` can be deprecated + - [ ] `setSchema` is tricky (asSchema is cleaner). Let's support it for now, + but only if the cause isn't set yet. + - [ ] `connect` copy over and add a direction field, so can distinguish + where this node is used as input vs where the passed node is an input to + this node. + - [ ] `export` make the analogous version, if link is present use that as + `external`. + - [ ] `map` and `mapWithPattern`: Copy over + - [ ] `toJSON` return `null` when no link otherwise what Cell does. + - [ ] No need for `toOpaqueRef` anymore, since all cells are now also + OpaqueRef. So remove all that. - [ ] Call that for returned value in lift/handler, with a .for("assigned variable of property", true) - [ ] For now treat result as recipe, but it should be one where all nodes @@ -80,7 +103,7 @@ - [ ] Add `.remove` and `.removeAll` which removes the element matching the parameter from the list. - [ ] Add overload to `.key` that accepts an array of keys - +- [ ] Make name parameter in recipe optional ## Planned Future Work From b587a4a9bdcebea033ea3c27148f8d83ed07bc40 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Fri, 24 Oct 2025 13:19:48 -0700 Subject: [PATCH 4/5] feat: Migrate @commontools/html into a jsx-runtime implementation. (#1958) * @commontools/html is now a valid jsx-runtime implementation, like preact * @commontools/html is set as the Deno workspace jsx runtime * The pattern runtime still uses a global `h` render function that can be aligned in the future. * Removed react/react-dom/@types providing the React jsx runtime * Hack to support serving `iframe-bootstrap.js`, which has runtime deps not in our workspace, by renaming the file extension to work around this upstream issue: https://github.com/denoland/deno/issues/27505 * Removes @babel/standalone, and enables the full removal of React deps --- deno.json | 7 +- deno.lock | 371 ++++++++---------- packages/html/deno.jsonc | 10 +- packages/html/src/jsx-dev-runtime.ts | 80 ++++ packages/html/src/jsx-runtime.ts | 72 ++++ packages/html/test/jsx-dev-runtime.test.tsx | 144 +++++++ packages/html/test/jsx-runtime.test.tsx | 196 +++++++++ packages/html/test/jsx.test.tsx | 2 +- packages/static/assets/scripts/README.md | 6 + ...rame-bootstrap.js => iframe-bootstrap._js} | 1 + packages/static/cache.ts | 10 +- 11 files changed, 673 insertions(+), 226 deletions(-) create mode 100644 packages/html/src/jsx-dev-runtime.ts create mode 100644 packages/html/src/jsx-runtime.ts create mode 100644 packages/html/test/jsx-dev-runtime.test.tsx create mode 100644 packages/html/test/jsx-runtime.test.tsx create mode 100644 packages/static/assets/scripts/README.md rename packages/static/assets/scripts/{iframe-bootstrap.js => iframe-bootstrap._js} (99%) diff --git a/deno.json b/deno.json index 2dddaeed13..665f2d563e 100644 --- a/deno.json +++ b/deno.json @@ -37,10 +37,11 @@ "initialize-db": "./tasks/initialize-db.sh" }, "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@commontools/html", "types": [ "./packages/static/assets/types/jsx.d.ts" ], - "jsx": "react-jsxdev", "lib": [ "deno.ns", "dom", @@ -96,10 +97,6 @@ ] }, "imports": { - "react": "npm:react@^18.3.1", - "react-dom": "npm:react-dom@^18.3.1", - "@types/react": "npm:@types/react@^18.3.1", - "@babel/standalone": "npm:@babel/standalone@^7.28.2", "commontools": "./packages/api/index.ts", "core-js/proposals/explicit-resource-management": "https://esm.sh/core-js/proposals/explicit-resource-management", "@astral/astral": "./packages/vendor-astral/mod.ts", diff --git a/deno.lock b/deno.lock index 3364d2f196..e91fc2cb93 100644 --- a/deno.lock +++ b/deno.lock @@ -20,7 +20,7 @@ "jsr:@std/assert@^1.0.14": "1.0.14", "jsr:@std/async@1": "1.0.14", "jsr:@std/async@^1.0.13": "1.0.14", - "jsr:@std/bytes@^1.0.5": "1.0.6", + "jsr:@std/bytes@^1.0.2": "1.0.6", "jsr:@std/cli@1": "1.0.21", "jsr:@std/cli@^1.0.12": "1.0.21", "jsr:@std/cli@^1.0.21": "1.0.21", @@ -43,7 +43,7 @@ "jsr:@std/http@1": "1.0.20", "jsr:@std/internal@^1.0.10": "1.0.10", "jsr:@std/internal@^1.0.9": "1.0.10", - "jsr:@std/io@0.225": "0.225.2", + "jsr:@std/io@0.225": "0.225.0", "jsr:@std/io@0.225.0": "0.225.0", "jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/net@^1.0.4": "1.0.5", @@ -54,31 +54,30 @@ "jsr:@std/streams@^1.0.10": "1.0.11", "jsr:@std/testing@1": "1.0.15", "jsr:@std/text@~1.0.7": "1.0.16", - "jsr:@zip-js/zip-js@^2.7.52": "2.7.73", + "jsr:@zip-js/zip-js@^2.7.52": "2.7.72", "npm:@ai-sdk/anthropic@^1.1.6": "1.2.12_zod@3.25.76", - "npm:@ai-sdk/anthropic@^2.0.9": "2.0.21_zod@3.25.76", - "npm:@ai-sdk/google-vertex@^3.0.16": "3.0.32_zod@3.25.76", - "npm:@ai-sdk/groq@^2.0.16": "2.0.22_zod@3.25.76", + "npm:@ai-sdk/anthropic@^2.0.9": "2.0.35_zod@3.25.76", + "npm:@ai-sdk/google-vertex@^3.0.16": "3.0.51_zod@3.25.76", + "npm:@ai-sdk/groq@^2.0.16": "2.0.24_zod@3.25.76", "npm:@ai-sdk/openai@^1.1.9": "1.3.24_zod@3.25.76", - "npm:@ai-sdk/openai@^2.0.22": "2.0.40_zod@3.25.76", + "npm:@ai-sdk/openai@^2.0.22": "2.0.53_zod@3.25.76", "npm:@arizeai/openinference-semantic-conventions@^1.1.0": "1.1.0", "npm:@arizeai/openinference-vercel@^2.0.1": "2.3.4_@opentelemetry+api@1.9.0", - "npm:@babel/standalone@^7.28.2": "7.28.4", "npm:@codemirror/autocomplete@^6.15.0": "6.19.0", - "npm:@codemirror/commands@^6.8.1": "6.8.1", + "npm:@codemirror/commands@^6.8.1": "6.9.0", "npm:@codemirror/lang-css@^6.3.1": "6.3.1", - "npm:@codemirror/lang-html@^6.4.9": "6.4.10", + "npm:@codemirror/lang-html@^6.4.9": "6.4.11", "npm:@codemirror/lang-javascript@^6.2.2": "6.2.4", "npm:@codemirror/lang-json@^6.0.1": "6.0.2", - "npm:@codemirror/lang-markdown@^6.3.0": "6.3.4", + "npm:@codemirror/lang-markdown@^6.3.0": "6.4.0", "npm:@codemirror/language@^6.10.8": "6.11.3", "npm:@codemirror/state@^6.5.1": "6.5.2", "npm:@codemirror/theme-one-dark@^6.1.2": "6.1.3", - "npm:@codemirror/view@^6.26.0": "6.38.4", - "npm:@fal-ai/client@^1.2.2": "1.6.2", - "npm:@hono/sentry@^1.2.0": "1.2.2_hono@4.9.9", - "npm:@hono/zod-openapi@~0.18.3": "0.18.4_hono@4.9.9_zod@3.25.76", - "npm:@hono/zod-validator@~0.4.2": "0.4.3_hono@4.9.9_zod@3.25.76", + "npm:@codemirror/view@^6.26.0": "6.38.6", + "npm:@fal-ai/client@^1.2.2": "1.7.0", + "npm:@hono/sentry@^1.2.0": "1.2.2_hono@4.10.1", + "npm:@hono/zod-openapi@~0.18.3": "0.18.4_hono@4.10.1_zod@3.25.76", + "npm:@hono/zod-validator@~0.4.2": "0.4.3_hono@4.10.1_zod@3.25.76", "npm:@jitl/quickjs-singlefile-mjs-debug-sync@*": "0.31.0", "npm:@lit/context@^1.1.2": "1.1.6", "npm:@lit/context@^1.1.5": "1.1.6", @@ -92,20 +91,19 @@ "npm:@opentelemetry/resources@^1.19.0": "1.30.1_@opentelemetry+api@1.9.0", "npm:@opentelemetry/sdk-trace-base@^1.19.0": "1.30.1_@opentelemetry+api@1.9.0", "npm:@opentelemetry/semantic-conventions@^1.19.0": "1.37.0", - "npm:@scalar/hono-api-reference@~0.5.165": "0.5.184_hono@4.9.9", + "npm:@scalar/hono-api-reference@~0.5.165": "0.5.184_hono@4.10.1", "npm:@scure/bip39@^1.5.4": "1.6.0", "npm:@sentry/deno@^9.3.0": "9.46.0", "npm:@types/node@*": "24.2.0", - "npm:@types/react@^18.3.1": "18.3.24", - "npm:ai@^5.0.27": "5.0.57_zod@3.25.76", + "npm:ai@^5.0.27": "5.0.76_zod@3.25.76", "npm:ajv@^8.17.1": "8.17.1", "npm:codemirror@^6.0.1": "6.0.2", "npm:dom-serializer@*": "2.0.0", "npm:domhandler@*": "5.0.3", - "npm:esbuild@~0.25.5": "0.25.10", + "npm:esbuild@~0.25.5": "0.25.11", "npm:gcp-metadata@6.1.0": "6.1.0", - "npm:hono-pino@0.7": "0.7.2_hono@4.9.9_pino@9.12.0", - "npm:hono@^4.7.0": "4.9.9", + "npm:hono-pino@0.7": "0.7.2_hono@4.10.1_pino@9.14.0", + "npm:hono@^4.7.0": "4.10.1", "npm:htmlparser2@*": "10.0.0", "npm:json5@^2.2.3": "2.2.3", "npm:jsonschema@^1.5.0": "1.5.0", @@ -114,16 +112,14 @@ "npm:merkle-reference@^2.2.0": "2.2.0", "npm:mistreevous@4.2.0": "4.2.0", "npm:multiformats@^13.3.2": "13.4.1", - "npm:pino-pretty@13": "13.1.1", - "npm:pino@^9.6.0": "9.12.0", + "npm:pino-pretty@13": "13.1.2", + "npm:pino@^9.6.0": "9.14.0", "npm:plaid@36": "36.0.0", "npm:quickjs-emscripten-core@*": "0.31.0", - "npm:react-dom@^18.3.1": "18.3.1_react@18.3.1", - "npm:react@^18.3.1": "18.3.1", "npm:source-map-js@^1.2.1": "1.2.1", - "npm:stoker@^1.4.2": "1.4.3_@hono+zod-openapi@0.18.4__hono@4.9.9__zod@3.25.76_hono@4.9.9_zod@3.25.76", + "npm:stoker@^1.4.2": "1.4.3_@hono+zod-openapi@0.18.4__hono@4.10.1__zod@3.25.76_hono@4.10.1_zod@3.25.76", "npm:turndown@^7.1.2": "7.2.1", - "npm:typescript@*": "5.9.2", + "npm:typescript@*": "5.9.3", "npm:zod@^3.24.1": "3.25.76" }, "jsr": { @@ -276,10 +272,7 @@ "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" }, "@std/io@0.225.0": { - "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3" - }, - "@std/io@0.225.2": { - "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", + "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3", "dependencies": [ "jsr:@std/bytes" ] @@ -319,8 +312,8 @@ "@std/text@1.0.16": { "integrity": "ddb9853b75119a2473857d691cf1ec02ad90793a2e8b4a4ac49d7354281a0cf8" }, - "@zip-js/zip-js@2.7.73": { - "integrity": "14c123f0e534377a6f47c5ba5293bb6c0f3e72e78c6a687108011605420a4867" + "@zip-js/zip-js@2.7.72": { + "integrity": "b72877f90aaefa1f1bd265d51f354bb58b6dd0d0e2799c865584acf49eae9115" } }, "npm": { @@ -332,46 +325,47 @@ "zod" ] }, - "@ai-sdk/anthropic@2.0.21_zod@3.25.76": { - "integrity": "sha512-a62O7xUaY3MliDVhA7NQZXheSWw6XscxFzoC9CENp/rY5f7RNGbgaA/vdiW86cGJpZZIoEVXN1xG8kCNwyJ7sg==", + "@ai-sdk/anthropic@2.0.35_zod@3.25.76": { + "integrity": "sha512-R0HtYqnKhxH67qpfKJwPCzRJLeW6M/adFM0E4YyF2+m80UvaigmiVwEODcODHEhsA3hQdf1hLNXzq4AEbkz8xw==", "dependencies": [ "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "zod" ] }, - "@ai-sdk/gateway@1.0.30_zod@3.25.76": { - "integrity": "sha512-QdrSUryr/CLcsCISokLHOImcHj1adGXk1yy4B3qipqLhcNc33Kj/O/3crI790Qp85oDx7sc4vm7R4raf9RA/kg==", + "@ai-sdk/gateway@2.0.0_zod@3.25.76": { + "integrity": "sha512-Gj0PuawK7NkZuyYgO/h5kDK/l6hFOjhLdTq3/Lli1FTl47iGmwhH1IZQpAL3Z09BeFYWakcwUmn02ovIm2wy9g==", "dependencies": [ "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", + "@vercel/oidc", "zod" ] }, - "@ai-sdk/google-vertex@3.0.32_zod@3.25.76": { - "integrity": "sha512-6nZJM/OF4c+foIJvE5e0/rhbhxTeXGFq4K3v5VMaT9gu4BllRkoZP7/0PNcsapqVUCfo47l8Cc0Q/bW9ak96Vw==", + "@ai-sdk/google-vertex@3.0.51_zod@3.25.76": { + "integrity": "sha512-0g/jGGm0nCSWZX8hXUWXYwDCYAd7gh12e0EVX5+BCHBJImk68Y80rjFGAgCr02I9qdFuQ9cH4Wf8dPtjwzizvA==", "dependencies": [ - "@ai-sdk/anthropic@2.0.21_zod@3.25.76", + "@ai-sdk/anthropic@2.0.35_zod@3.25.76", "@ai-sdk/google", "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "google-auth-library", "zod" ] }, - "@ai-sdk/google@2.0.17_zod@3.25.76": { - "integrity": "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA==", + "@ai-sdk/google@2.0.23_zod@3.25.76": { + "integrity": "sha512-VbCnKR+6aWUVLkAiSW5gUEtST7KueEmlt+d6qwDikxlLnFG9pzy59je8MiDVeM5G2tuSXbvZQF78PGIfXDBmow==", "dependencies": [ "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "zod" ] }, - "@ai-sdk/groq@2.0.22_zod@3.25.76": { - "integrity": "sha512-/AswqcXnMuZnpLzRxddB/WBEi0hM6IpqafrGGQE/jGQPIuIKPb8HyNatX67vJgHcD2s12YIKuFccaBPDYlUIVg==", + "@ai-sdk/groq@2.0.24_zod@3.25.76": { + "integrity": "sha512-PCtNwFsakxR6B/o+l3gtxlPIwN8lawK3vvOjRdC759Y8WtNxCv5RUs0JsxIKyAZxO+RBEy0AoL8xTQUy8fn3gw==", "dependencies": [ "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "zod" ] }, @@ -383,11 +377,11 @@ "zod" ] }, - "@ai-sdk/openai@2.0.40_zod@3.25.76": { - "integrity": "sha512-VFPS6zuDkMTXuZCR7QvYdcrilk1xTa+vfQedK2IBOLDU52GgdC7ywPqR5NScb7vHuxCwm/CKfk6X4WZ08kCr9Q==", + "@ai-sdk/openai@2.0.53_zod@3.25.76": { + "integrity": "sha512-GIkR3+Fyif516ftXv+YPSPstnAHhcZxNoR2s8uSHhQ1yBT7I7aQYTVwpjAuYoT3GR+TeP50q7onj2/nDRbT2FQ==", "dependencies": [ "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "zod" ] }, @@ -400,8 +394,8 @@ "zod" ] }, - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76": { - "integrity": "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ==", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76": { + "integrity": "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==", "dependencies": [ "@ai-sdk/provider@2.0.0", "@standard-schema/spec", @@ -451,9 +445,6 @@ "zod" ] }, - "@babel/standalone@7.28.4": { - "integrity": "sha512-Qc1BNCfuJZBKs2SC5lqRmSYOw7Ka0X7urZQ7oVsGIax4eGDUIHX+CDg752N4jDxC2rbBh3li098ReGOtjT0x4g==" - }, "@codemirror/autocomplete@6.19.0": { "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==", "dependencies": [ @@ -463,8 +454,8 @@ "@lezer/common" ] }, - "@codemirror/commands@6.8.1": { - "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "@codemirror/commands@6.9.0": { + "integrity": "sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==", "dependencies": [ "@codemirror/language", "@codemirror/state", @@ -482,8 +473,8 @@ "@lezer/css" ] }, - "@codemirror/lang-html@6.4.10": { - "integrity": "sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==", + "@codemirror/lang-html@6.4.11": { + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "dependencies": [ "@codemirror/autocomplete", "@codemirror/lang-css", @@ -515,8 +506,8 @@ "@lezer/json" ] }, - "@codemirror/lang-markdown@6.3.4": { - "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==", + "@codemirror/lang-markdown@6.4.0": { + "integrity": "sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==", "dependencies": [ "@codemirror/autocomplete", "@codemirror/lang-html", @@ -538,8 +529,8 @@ "style-mod" ] }, - "@codemirror/lint@6.8.5": { - "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "@codemirror/lint@6.9.0": { + "integrity": "sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==", "dependencies": [ "@codemirror/state", "@codemirror/view", @@ -569,8 +560,8 @@ "@lezer/highlight" ] }, - "@codemirror/view@6.38.4": { - "integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==", + "@codemirror/view@6.38.6": { + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "dependencies": [ "@codemirror/state", "crelt", @@ -578,152 +569,152 @@ "w3c-keyname" ] }, - "@esbuild/aix-ppc64@0.25.10": { - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "@esbuild/aix-ppc64@0.25.11": { + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/android-arm64@0.25.10": { - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "@esbuild/android-arm64@0.25.11": { + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm@0.25.10": { - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "@esbuild/android-arm@0.25.11": { + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-x64@0.25.10": { - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "@esbuild/android-x64@0.25.11": { + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/darwin-arm64@0.25.10": { - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "@esbuild/darwin-arm64@0.25.11": { + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-x64@0.25.10": { - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "@esbuild/darwin-x64@0.25.11": { + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/freebsd-arm64@0.25.10": { - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "@esbuild/freebsd-arm64@0.25.11": { + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-x64@0.25.10": { - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "@esbuild/freebsd-x64@0.25.11": { + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/linux-arm64@0.25.10": { - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "@esbuild/linux-arm64@0.25.11": { + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm@0.25.10": { - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "@esbuild/linux-arm@0.25.11": { + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-ia32@0.25.10": { - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "@esbuild/linux-ia32@0.25.11": { + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-loong64@0.25.10": { - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "@esbuild/linux-loong64@0.25.11": { + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-mips64el@0.25.10": { - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "@esbuild/linux-mips64el@0.25.11": { + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-ppc64@0.25.10": { - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "@esbuild/linux-ppc64@0.25.11": { + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-riscv64@0.25.10": { - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "@esbuild/linux-riscv64@0.25.11": { + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-s390x@0.25.10": { - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "@esbuild/linux-s390x@0.25.11": { + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-x64@0.25.10": { - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "@esbuild/linux-x64@0.25.11": { + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/netbsd-arm64@0.25.10": { - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "@esbuild/netbsd-arm64@0.25.11": { + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-x64@0.25.10": { - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "@esbuild/netbsd-x64@0.25.11": { + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-arm64@0.25.10": { - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "@esbuild/openbsd-arm64@0.25.11": { + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-x64@0.25.10": { - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "@esbuild/openbsd-x64@0.25.11": { + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openharmony-arm64@0.25.10": { - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "@esbuild/openharmony-arm64@0.25.11": { + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@esbuild/sunos-x64@0.25.10": { - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "@esbuild/sunos-x64@0.25.11": { + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/win32-arm64@0.25.10": { - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "@esbuild/win32-arm64@0.25.11": { + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-ia32@0.25.10": { - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "@esbuild/win32-ia32@0.25.11": { + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-x64@0.25.10": { - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "@esbuild/win32-x64@0.25.11": { + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "os": ["win32"], "cpu": ["x64"] }, - "@fal-ai/client@1.6.2": { - "integrity": "sha512-y89jNGAZUpvt+IZfsIczULN95fGQlPxTt2c9bNFWkKe6OBnhOY+qHHH7IO4XgCsbmCwgfzzaSHUItx1nQ3ifHQ==", + "@fal-ai/client@1.7.0": { + "integrity": "sha512-lZ1KuLc4iqBalIqwlQGJLBD2pfAmWQqM8pvT2clqhU8FjPrZxLBNnAWiQsaB3b7GXzItSA1C6k5W8TfUJPT5eA==", "dependencies": [ "@msgpack/msgpack", "eventsource-parser@1.1.2", "robot3" ] }, - "@hono/sentry@1.2.2_hono@4.9.9": { + "@hono/sentry@1.2.2_hono@4.10.1": { "integrity": "sha512-027grZBrRGDPor8mRd+QOBcSpUlF07YrTp/WFDXZhbvWZ+1LrZdERUqcdg1gBGDUTanHhd9ucblpNNN6+V1bxg==", "dependencies": [ "hono", "toucan-js" ] }, - "@hono/zod-openapi@0.18.4_hono@4.9.9_zod@3.25.76": { + "@hono/zod-openapi@0.18.4_hono@4.10.1_zod@3.25.76": { "integrity": "sha512-6NHMHU96Hh32B1yDhb94Z4Z5/POsmEu2AXpWLWcBq9arskRnOMt2752yEoXoADV8WUAc7H1IkNaQHGj1ytXbYw==", "dependencies": [ "@asteasolutions/zod-to-openapi", @@ -732,7 +723,7 @@ "zod" ] }, - "@hono/zod-validator@0.4.3_hono@4.9.9_zod@3.25.76": { + "@hono/zod-validator@0.4.3_hono@4.10.1_zod@3.25.76": { "integrity": "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==", "dependencies": [ "hono", @@ -748,8 +739,8 @@ "@jitl/quickjs-ffi-types" ] }, - "@lezer/common@1.2.3": { - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + "@lezer/common@1.3.0": { + "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==" }, "@lezer/css@1.3.0": { "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", @@ -759,8 +750,8 @@ "@lezer/lr" ] }, - "@lezer/highlight@1.2.1": { - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "@lezer/highlight@1.2.2": { + "integrity": "sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A==", "dependencies": [ "@lezer/common" ] @@ -795,8 +786,8 @@ "@lezer/common" ] }, - "@lezer/markdown@1.4.3": { - "integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==", + "@lezer/markdown@1.5.1": { + "integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==", "dependencies": [ "@lezer/common", "@lezer/highlight" @@ -972,6 +963,9 @@ "@opentelemetry/semantic-conventions@1.37.0": { "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==" }, + "@pinojs/redact@0.4.0": { + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, "@protobufjs/aspromise@1.1.2": { "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, @@ -1012,7 +1006,7 @@ "@scalar/types" ] }, - "@scalar/hono-api-reference@0.5.184_hono@4.9.9": { + "@scalar/hono-api-reference@0.5.184_hono@4.10.1": { "integrity": "sha512-vRSRwJkN1Xo5dW9KYQJlGpKZ+Nh9qH+x1sn0qf6/Lx8QLPyyEpNm1EEddKaIN6qd5wrtVjDN6adQhfAfcYGHzw==", "dependencies": [ "@scalar/core", @@ -1074,20 +1068,10 @@ "undici-types@7.10.0" ] }, - "@types/node@24.5.2": { - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "dependencies": [ - "undici-types@7.12.0" - ] - }, - "@types/prop-types@15.7.15": { - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" - }, - "@types/react@18.3.24": { - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "@types/node@24.9.1": { + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dependencies": [ - "@types/prop-types", - "csstype" + "undici-types@7.16.0" ] }, "@types/trusted-types@2.0.7": { @@ -1100,15 +1084,18 @@ "zhead" ] }, + "@vercel/oidc@3.0.3": { + "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==" + }, "agent-base@7.1.4": { "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" }, - "ai@5.0.57_zod@3.25.76": { - "integrity": "sha512-g4K881HFl9aGqvnp1Z/gRXYmfjORn5q5pyPIVmMndT+5AjiBRvUVzFZn1rXdzL61cjbMNyjO4NPfEbBE6Z/W3A==", + "ai@5.0.76_zod@3.25.76": { + "integrity": "sha512-ZCxi1vrpyCUnDbtYrO/W8GLvyacV9689f00yshTIQ3mFFphbD7eIv40a2AOZBv3GGRA7SSRYIDnr56wcS/gyQg==", "dependencies": [ "@ai-sdk/gateway", "@ai-sdk/provider@2.0.0", - "@ai-sdk/provider-utils@3.0.10_zod@3.25.76", + "@ai-sdk/provider-utils@3.0.12_zod@3.25.76", "@opentelemetry/api", "zod" ] @@ -1176,9 +1163,6 @@ "crelt@1.0.6": { "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, - "csstype@3.1.3": { - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, "dateformat@4.6.3": { "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" }, @@ -1266,8 +1250,8 @@ "hasown" ] }, - "esbuild@0.25.10": { - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "esbuild@0.25.11": { + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "optionalDependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", @@ -1414,7 +1398,7 @@ "help-me@5.0.0": { "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" }, - "hono-pino@0.7.2_hono@4.9.9_pino@9.12.0": { + "hono-pino@0.7.2_hono@4.10.1_pino@9.14.0": { "integrity": "sha512-uLJOngId4Ia2eHXnCPE8xpyMVkh+AGxAkHZKgvZk8YkmuTbcVDDUMe7aHMEz+YLqCDgd/Hk9ytVmmoQ8QTUXgQ==", "dependencies": [ "defu", @@ -1422,8 +1406,8 @@ "pino" ] }, - "hono@4.9.9": { - "integrity": "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw==" + "hono@4.10.1": { + "integrity": "sha512-rpGNOfacO4WEPClfkEt1yfl8cbu10uB1lNpiI33AKoiAHwOS8lV748JiLx4b5ozO/u4qLjIvfpFsPXdY5Qjkmg==" }, "hookable@5.5.3": { "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" @@ -1450,9 +1434,6 @@ "joycon@3.1.1": { "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" }, - "js-tokens@4.0.0": { - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "json-bigint@1.0.0": { "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "dependencies": [ @@ -1515,13 +1496,6 @@ "long@5.3.2": { "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, - "loose-envify@1.4.0": { - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": [ - "js-tokens" - ], - "bin": true - }, "lotto-draw@1.0.2": { "integrity": "sha512-1ih414A35BWpApfNlWAHBKOBLSxTj45crAJ+CMWF/kVY5nx6N22DA1OVF/FWW5WM5CGJbIMRh1O+xe8ukyoQ8Q==" }, @@ -1594,8 +1568,8 @@ "split2" ] }, - "pino-pretty@13.1.1": { - "integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==", + "pino-pretty@13.1.2": { + "integrity": "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==", "dependencies": [ "colorette", "dateformat", @@ -1607,7 +1581,7 @@ "on-exit-leak-free", "pino-abstract-transport", "pump", - "secure-json-parse@4.0.0", + "secure-json-parse@4.1.0", "sonic-boom", "strip-json-comments" ], @@ -1616,9 +1590,10 @@ "pino-std-serializers@7.0.0": { "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" }, - "pino@9.12.0": { - "integrity": "sha512-0Gd0OezGvqtqMwgYxpL7P0pSHHzTJ0Lx992h+mNlMtRVfNnqweWmf0JmRWk5gJzHalyd2mxTzKjhiNbGS2Ztfw==", + "pino@9.14.0": { + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", "dependencies": [ + "@pinojs/redact", "atomic-sleep", "on-exit-leak-free", "pino-abstract-transport", @@ -1627,7 +1602,6 @@ "quick-format-unescaped", "real-require", "safe-stable-stringify", - "slow-redact", "sonic-boom", "thread-stream" ], @@ -1655,7 +1629,7 @@ "@protobufjs/path", "@protobufjs/pool", "@protobufjs/utf8", - "@types/node@24.5.2", + "@types/node@24.9.1", "long" ], "scripts": true @@ -1679,20 +1653,6 @@ "@jitl/quickjs-ffi-types" ] }, - "react-dom@18.3.1_react@18.3.1": { - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": [ - "loose-envify", - "react", - "scheduler" - ] - }, - "react@18.3.1": { - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": [ - "loose-envify" - ] - }, "real-require@0.2.0": { "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" }, @@ -1708,20 +1668,11 @@ "safe-stable-stringify@2.5.0": { "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" }, - "scheduler@0.23.2": { - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": [ - "loose-envify" - ] - }, "secure-json-parse@2.7.0": { "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, - "secure-json-parse@4.0.0": { - "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==" - }, - "slow-redact@0.3.0": { - "integrity": "sha512-cf723wn9JeRIYP9tdtd86GuqoR5937u64Io+CYjlm2i7jvu7g0H+Cp0l0ShAf/4ZL+ISUTVT+8Qzz7RZmp9FjA==" + "secure-json-parse@4.1.0": { + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==" }, "sonic-boom@4.2.0": { "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", @@ -1735,7 +1686,7 @@ "split2@4.2.0": { "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, - "stoker@1.4.3_@hono+zod-openapi@0.18.4__hono@4.9.9__zod@3.25.76_hono@4.9.9_zod@3.25.76": { + "stoker@1.4.3_@hono+zod-openapi@0.18.4__hono@4.10.1__zod@3.25.76_hono@4.10.1_zod@3.25.76": { "integrity": "sha512-kijg+1PKUY6laFbNcY7hw5OPgg3QhWD+2wAZsk35IqiZfVwU3S/E3DYbemecRT7vdWbWrZ2mzewQrqD4zoJSeQ==", "dependencies": [ "@hono/zod-openapi", @@ -1748,8 +1699,8 @@ "strip-json-comments@5.0.3": { "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==" }, - "style-mod@4.1.2": { - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + "style-mod@4.1.3": { + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==" }, "thread-stream@3.1.0": { "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", @@ -1774,15 +1725,15 @@ "@mixmark-io/domino" ] }, - "typescript@5.9.2": { - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "bin": true }, "undici-types@7.10.0": { "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" }, - "undici-types@7.12.0": { - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" + "undici-types@7.16.0": { + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" }, "uuid@9.0.1": { "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", @@ -1816,11 +1767,11 @@ } }, "redirects": { - "https://esm.sh/core-js/proposals/explicit-resource-management": "https://esm.sh/core-js@3.45.1/proposals/explicit-resource-management" + "https://esm.sh/core-js/proposals/explicit-resource-management": "https://esm.sh/core-js@3.44.0/proposals/explicit-resource-management" }, "remote": { - "https://esm.sh/core-js@3.45.1/denonext/proposals/explicit-resource-management.mjs": "b557f1ce71f3ccf477d129eb4aeead0e4cda3e18232e622e6a49882c0d2a84c8", - "https://esm.sh/core-js@3.45.1/proposals/explicit-resource-management": "fe52fa3b11925adee8873c15c019e9c05a47cd9dfce6d91f03aa4eb9311c4e71" + "https://esm.sh/core-js@3.44.0/denonext/proposals/explicit-resource-management.mjs": "4850503a2650ba47368eb95b1aa2565b47bad32efe9738766811f6de75cf51e1", + "https://esm.sh/core-js@3.44.0/proposals/explicit-resource-management": "06969d679705ab37fd058cc66a50cd7648f79bf5fd86fb13b29cb93c2667ebc7" }, "workspace": { "dependencies": [ @@ -1836,13 +1787,9 @@ "jsr:@std/http@1", "jsr:@std/path@1", "jsr:@std/testing@1", - "npm:@babel/standalone@^7.28.2", - "npm:@types/react@^18.3.1", "npm:lit@^3.3.0", "npm:merkle-reference@^2.2.0", "npm:multiformats@^13.3.2", - "npm:react-dom@^18.3.1", - "npm:react@^18.3.1", "npm:turndown@^7.1.2", "npm:zod@^3.24.1" ], diff --git a/packages/html/deno.jsonc b/packages/html/deno.jsonc index b48e033f56..643aee7540 100644 --- a/packages/html/deno.jsonc +++ b/packages/html/deno.jsonc @@ -5,17 +5,13 @@ }, "exports": { ".": "./src/index.ts", - "./utils": "./src/utils.ts" + "./utils": "./src/utils.ts", + "./jsx-runtime": "./src/jsx-runtime.ts", + "./jsx-dev-runtime": "./src/jsx-dev-runtime.ts" }, "imports": { "htmlparser2": "npm:htmlparser2", "domhandler": "npm:domhandler", "dom-serializer": "npm:dom-serializer" - }, - - "compilerOptions": { - "jsx": "react", - "jsxFactory": "h", - "jsxFragmentFactory": "h.fragment" } } diff --git a/packages/html/src/jsx-dev-runtime.ts b/packages/html/src/jsx-dev-runtime.ts new file mode 100644 index 0000000000..07801a849a --- /dev/null +++ b/packages/html/src/jsx-dev-runtime.ts @@ -0,0 +1,80 @@ +/** + * JSX development runtime for @commontools/html + * + * This module provides the JSX development runtime implementation compatible with + * TypeScript's "jsx": "react-jsxdev" configuration. + * + * The development runtime includes additional debugging information like source + * file paths and line numbers, though our current implementation doesn't use these yet. + * + * @module jsx-dev-runtime + */ + +import { h } from "@commontools/api"; +import type { RenderNode, VNode } from "@commontools/api"; + +/** + * Props type for JSX elements in development mode, including children and debug info + */ +export interface JSXDevProps { + children?: RenderNode | RenderNode[]; + key?: string | number; + [prop: string]: any; +} + +/** + * Source location information for debugging + */ +export interface Source { + fileName: string; + lineNumber: number; + columnNumber: number; +} + +/** + * Creates a VNode for a JSX element with development-time debugging information. + * + * This function is used by the JSX automatic runtime in development mode. + * It accepts additional parameters for debugging (__source, __self) which can be + * used to provide better error messages and developer experience. + * + * @param type - The element type (string for HTML/SVG, function for components) + * @param props - Element properties including children + * @param key - Optional key for list reconciliation + * @param isStaticChildren - Whether children are static (unused in our implementation) + * @param __source - Source location information for debugging + * @param __self - Reference to the component instance (unused in our implementation) + * @returns A virtual DOM node + */ +export function jsxDEV( + type: string | ((props: any) => VNode), + props: JSXDevProps | null, + _key?: string | number, + _isStaticChildren?: boolean, + __source?: Source, + __self?: any, +): VNode { + const { children, ...restProps } = props ?? {}; + + // Convert children to array format expected by h() + const childArray = children === undefined + ? [] + : Array.isArray(children) + ? children + : [children]; + + // In the future, we could use __source to provide better error messages + // or enhance debugging capabilities. For now, we just create the VNode. + return h(type, restProps, ...childArray); +} + +/** + * Fragment component for grouping elements without adding DOM nodes. + * + * Used when you write <> or in JSX. + * Renders as a "common-fragment" element in the virtual DOM. + */ +export const Fragment = h.fragment; + +// Type exports +export type { RenderNode, VNode }; diff --git a/packages/html/src/jsx-runtime.ts b/packages/html/src/jsx-runtime.ts new file mode 100644 index 0000000000..9032edffc5 --- /dev/null +++ b/packages/html/src/jsx-runtime.ts @@ -0,0 +1,72 @@ +/** + * JSX automatic runtime for @commontools/html + * + * This module provides the JSX runtime implementation compatible with + * TypeScript's "jsx": "react-jsx" configuration. + * + * @module jsx-runtime + */ + +import { h } from "@commontools/api"; +import type { RenderNode, VNode } from "@commontools/api"; + +/** + * Props type for JSX elements, including children + */ +export interface JSXProps { + children?: RenderNode | RenderNode[]; + key?: string | number; + [prop: string]: any; +} + +/** + * Creates a VNode for a JSX element. + * + * This is the core function used by the JSX automatic runtime for creating elements. + * It handles both HTML/SVG elements (string types) and component functions. + * + * @param type - The element type (string for HTML/SVG, function for components) + * @param props - Element properties including children + * @param key - Optional key for list reconciliation (currently unused but part of JSX spec) + * @returns A virtual DOM node + */ +export function jsx( + type: string | ((props: any) => VNode), + props: JSXProps | null, + _key?: string | number, +): VNode { + const { children, ...restProps } = props ?? {}; + + // Convert children to array format expected by h() + const childArray = children === undefined + ? [] + : Array.isArray(children) + ? children + : [children]; + + return h(type, restProps, ...childArray); +} + +/** + * Creates a VNode for a JSX element with static children. + * + * The TypeScript compiler uses this when it can determine that children are static. + * For our implementation, it's identical to jsx() since we don't optimize for static children. + * + * @param type - The element type (string for HTML/SVG, function for components) + * @param props - Element properties including children + * @param key - Optional key for list reconciliation + * @returns A virtual DOM node + */ +export const jsxs = jsx; + +/** + * Fragment component for grouping elements without adding DOM nodes. + * + * Used when you write <> or in JSX. + * Renders as a "common-fragment" element in the virtual DOM. + */ +export const Fragment = h.fragment; + +// Type exports +export type { RenderNode, VNode }; diff --git a/packages/html/test/jsx-dev-runtime.test.tsx b/packages/html/test/jsx-dev-runtime.test.tsx new file mode 100644 index 0000000000..14c4c6326d --- /dev/null +++ b/packages/html/test/jsx-dev-runtime.test.tsx @@ -0,0 +1,144 @@ +/** + * Tests for the JSX development runtime + * + * These tests verify that @commontools/html provides a development runtime + * compatible with TypeScript's "jsx": "react-jsxdev" configuration. + */ + +import { describe, it } from "@std/testing/bdd"; +import * as assert from "./assert.ts"; + +import { Fragment, jsxDEV } from "../src/jsx-dev-runtime.ts"; + +describe("JSX development runtime", () => { + it("jsxDEV() creates a simple element", () => { + const element = jsxDEV("div", { className: "test" }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + props: { className: "test" }, + children: [], + }); + }); + + it("jsxDEV() creates an element with children", () => { + const element = jsxDEV("div", { + children: [jsxDEV("p", { children: "Hello" })], + }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: [ + { + type: "vnode", + name: "p", + children: ["Hello"], + }, + ], + }); + }); + + it("jsxDEV() accepts debug parameters", () => { + const element = jsxDEV( + "div", + { children: "Test" }, + "test-key", + false, + { + fileName: "test.tsx", + lineNumber: 42, + columnNumber: 10, + }, + undefined, + ); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: ["Test"], + }); + }); + + it("jsxDEV() handles null props", () => { + const element = jsxDEV("div", null); + + assert.matchObject(element, { + type: "vnode", + name: "div", + props: {}, + children: [], + }); + }); + + it("jsxDEV() handles component functions", () => { + const MyComponent = ({ name }: { name: string }) => + jsxDEV("div", { children: `Hello, ${name}` }); + + const element = jsxDEV(MyComponent, { name: "World" }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: ["Hello, World"], + }); + }); + + it("Fragment creates a common-fragment element", () => { + const fragment = Fragment({ + children: [ + jsxDEV("p", { children: "Paragraph 1" }), + jsxDEV("p", { children: "Paragraph 2" }), + ], + }); + + assert.matchObject(fragment, { + type: "vnode", + name: "common-fragment", + children: [ + { + type: "vnode", + name: "p", + children: ["Paragraph 1"], + }, + { + type: "vnode", + name: "p", + children: ["Paragraph 2"], + }, + ], + }); + }); + + it("jsxDEV() with static children flag", () => { + const element = jsxDEV( + "ul", + { + children: [ + jsxDEV("li", { children: "Item 1" }), + jsxDEV("li", { children: "Item 2" }), + ], + }, + undefined, + true, // isStaticChildren + ); + + assert.matchObject(element, { + type: "vnode", + name: "ul", + children: [ + { + type: "vnode", + name: "li", + children: ["Item 1"], + }, + { + type: "vnode", + name: "li", + children: ["Item 2"], + }, + ], + }); + }); +}); diff --git a/packages/html/test/jsx-runtime.test.tsx b/packages/html/test/jsx-runtime.test.tsx new file mode 100644 index 0000000000..6ed766c48f --- /dev/null +++ b/packages/html/test/jsx-runtime.test.tsx @@ -0,0 +1,196 @@ +/** + * Tests for the JSX automatic runtime + * + * These tests verify that @commontools/html can be used as a JSX runtime + * compatible with TypeScript's "jsx": "react-jsx" configuration. + */ + +import { describe, it } from "@std/testing/bdd"; +import * as assert from "./assert.ts"; + +// Note: To properly test the automatic JSX runtime, this file should be +// compiled with jsxImportSource set to "@commontools/html" +// However, for this test to work with the current deno.jsonc configuration, +// we'll import the functions directly and verify they work correctly. + +import { Fragment, jsx, jsxs } from "../src/jsx-runtime.ts"; + +describe("JSX automatic runtime", () => { + it("jsx() creates a simple element", () => { + const element = jsx("div", { className: "test" }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + props: { className: "test" }, + children: [], + }); + }); + + it("jsx() creates an element with children", () => { + const element = jsx("div", { + children: [jsx("p", { children: "Hello" })], + }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: [ + { + type: "vnode", + name: "p", + children: ["Hello"], + }, + ], + }); + }); + + it("jsx() creates an element with a single child", () => { + const element = jsx("div", { + children: "Hello", + }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: ["Hello"], + }); + }); + + it("jsx() handles null props", () => { + const element = jsx("div", null); + + assert.matchObject(element, { + type: "vnode", + name: "div", + props: {}, + children: [], + }); + }); + + it("jsx() accepts a key parameter", () => { + // The key parameter is accepted but currently not stored in VNode + const element = jsx("li", { children: "Item 1" }, "item-1"); + + assert.matchObject(element, { + type: "vnode", + name: "li", + children: ["Item 1"], + }); + }); + + it("jsxs() works identically to jsx()", () => { + const element = jsxs("ul", { + children: [ + jsx("li", { children: "Item 1" }), + jsx("li", { children: "Item 2" }), + ], + }); + + assert.matchObject(element, { + type: "vnode", + name: "ul", + children: [ + { + type: "vnode", + name: "li", + children: ["Item 1"], + }, + { + type: "vnode", + name: "li", + children: ["Item 2"], + }, + ], + }); + }); + + it("jsx() handles component functions", () => { + const MyComponent = ({ name }: { name: string }) => + jsx("div", { children: `Hello, ${name}` }); + + const element = jsx(MyComponent, { name: "World" }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + children: ["Hello, World"], + }); + }); + + it("Fragment creates a common-fragment element", () => { + const fragment = Fragment({ + children: [ + jsx("p", { children: "Paragraph 1" }), + jsx("p", { children: "Paragraph 2" }), + ], + }); + + assert.matchObject(fragment, { + type: "vnode", + name: "common-fragment", + children: [ + { + type: "vnode", + name: "p", + children: ["Paragraph 1"], + }, + { + type: "vnode", + name: "p", + children: ["Paragraph 2"], + }, + ], + }); + }); + + it("jsx() with complex nested structure", () => { + const element = jsx("div", { + className: "container", + children: [ + jsx("h1", { children: "Title" }), + jsx("p", { children: "Description" }), + jsx("ul", { + children: [ + jsx("li", { children: "Item 1" }), + jsx("li", { children: "Item 2" }), + ], + }), + ], + }); + + assert.matchObject(element, { + type: "vnode", + name: "div", + props: { className: "container" }, + children: [ + { + type: "vnode", + name: "h1", + children: ["Title"], + }, + { + type: "vnode", + name: "p", + children: ["Description"], + }, + { + type: "vnode", + name: "ul", + children: [ + { + type: "vnode", + name: "li", + children: ["Item 1"], + }, + { + type: "vnode", + name: "li", + children: ["Item 2"], + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/html/test/jsx.test.tsx b/packages/html/test/jsx.test.tsx index 81f82513af..c98b6d792e 100644 --- a/packages/html/test/jsx.test.tsx +++ b/packages/html/test/jsx.test.tsx @@ -1,5 +1,5 @@ import { describe, it } from "@std/testing/bdd"; -import { h, UI } from "@commontools/api"; +import { UI } from "@commontools/api"; import * as assert from "./assert.ts"; import { isVNode } from "../src/jsx.ts"; diff --git a/packages/static/assets/scripts/README.md b/packages/static/assets/scripts/README.md new file mode 100644 index 0000000000..2a1b6aa974 --- /dev/null +++ b/packages/static/assets/scripts/README.md @@ -0,0 +1,6 @@ +We use `._js` extensions here, otherwise Deno typechecks these files during compilation. +In the `iframe-bootstrap._js` case, an iframes import map resolves the imports, a different +environment than our Deno workspace. A translation layer is handled such that requesting +assets can be done without the strange file extension. + +This could be remedied by the closing of https://github.com/denoland/deno/issues/27505 diff --git a/packages/static/assets/scripts/iframe-bootstrap.js b/packages/static/assets/scripts/iframe-bootstrap._js similarity index 99% rename from packages/static/assets/scripts/iframe-bootstrap.js rename to packages/static/assets/scripts/iframe-bootstrap._js index f87742f88d..9bb80908ca 100644 --- a/packages/static/assets/scripts/iframe-bootstrap.js +++ b/packages/static/assets/scripts/iframe-bootstrap._js @@ -1,3 +1,4 @@ +// @ts-nocheck // Import React immediately import * as React from "react" import * as ReactDOM from "react-dom/client" diff --git a/packages/static/cache.ts b/packages/static/cache.ts index 11feb47909..dae5555352 100644 --- a/packages/static/cache.ts +++ b/packages/static/cache.ts @@ -13,6 +13,14 @@ export const FS_URL = (import.meta.dirname && isDeno()) ? toFileUrl(join(import.meta.dirname, "assets")) : undefined; +// @see ./assets/scripts/README.md +function mapStaticAssetHack(assetName: string): string { + if (assetName.startsWith("scripts/") && assetName.endsWith(".js")) { + return assetName.substring(0, assetName.length - 3) + "._js"; + } + return assetName; +} + /** * Represents a cached static asset with its content and ETag. */ @@ -68,7 +76,7 @@ export class InnerCache { } const url = this.getBaseUrl(); - url.pathname = join(url.pathname, assetName); + url.pathname = join(url.pathname, mapStaticAssetHack(assetName)); return url; } From 36d60c5ed5d6800320aa037e4582292a14f739c8 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 24 Oct 2025 13:59:44 -0700 Subject: [PATCH 5/5] Fix: Prevent map transformation when inside derive and exclude untransformed map parameters (#1960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Skip map transformation when map will be inside derive When a map is nested in an expression with opaque refs (e.g., `list.length > 0 && list.map(...)`), the OpaqueRefJSXTransformer wraps the entire expression in `derive(list, list => ...)`, which unwraps the array to a plain array. If ClosureTransformer had already transformed the map to `mapWithPattern`, this causes runtime errors since plain arrays don't have `mapWithPattern`. This fix adds `shouldTransformMap()` which uses dataflow analysis to detect when a map will be inside a derive and skips the transformation, keeping it as plain `.map()` which works correctly on unwrapped arrays. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix: Exclude untransformed map callback parameters from outer derive capture When a plain .map() call is inside an expression that gets wrapped in derive(), the map callback parameters should not be treated as opaque parameters for the purposes of the outer derive. This is because: 1. The map is NOT being transformed to mapWithPattern (stays as plain .map) 2. The callback runs on unwrapped array elements inside the derive 3. The callback parameters are local to that scope, not outer scope variables This fix modifies getOpaqueCallKindForParameter() to check if a callback was actually transformed (using context.isMapCallback) before treating its parameters as opaque. Untransformed callbacks have regular parameters that should not be captured in outer derives. Example that's now fixed: ```tsx {items.map((item) =>
{item.name && {item.name}}
)} ``` Before: derive({ items, item_name: item.name }, ...) ❌ After: derive({ items }, ...) ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/closures/transformer.ts | 74 ++++++++++++++++++- .../src/transformers/opaque-ref/helpers.ts | 21 +++++- .../map-array-length-conditional.expected.tsx | 16 ++++ .../map-array-length-conditional.input.tsx | 20 +++++ .../map-nested-conditional.expected.tsx | 30 +------- .../map-single-capture.expected.tsx | 29 +------- .../opaque-ref-cell-map.expected.tsx | 23 +----- 7 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index b3888e52b5..71e36bdf50 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -1,7 +1,7 @@ import ts from "typescript"; import { TransformationContext, Transformer } from "../core/mod.ts"; import { isOpaqueRefType } from "../transformers/opaque-ref/opaque-ref.ts"; -import { visitEachChildWithJsx } from "../ast/mod.ts"; +import { createDataFlowAnalyzer, visitEachChildWithJsx } from "../ast/mod.ts"; export class ClosureTransformer extends Transformer { override filter(context: TransformationContext): boolean { @@ -715,6 +715,72 @@ function expressionsMatch(a: ts.Expression, b: ts.Expression): boolean { return false; } +/** + * Check if a map call should be transformed to mapWithPattern. + * Returns false if the map will end up inside a derive (where the array is unwrapped). + * + * This happens when the map is nested inside a larger expression with opaque refs, + * e.g., `list.length > 0 && list.map(...)` becomes `derive(list, list => ...)` + */ +function shouldTransformMap( + mapCall: ts.CallExpression, + context: TransformationContext, +): boolean { + // Find the closest containing JSX expression + let node: ts.Node = mapCall; + let closestJsxExpression: ts.JsxExpression | undefined; + + while (node.parent) { + if (ts.isJsxExpression(node.parent)) { + closestJsxExpression = node.parent; + break; + } + node = node.parent; + } + + // If we didn't find a JSX expression, default to transforming + // (this handles maps in regular statements like `const x = items.map(...)`) + if (!closestJsxExpression || !closestJsxExpression.expression) { + return true; + } + + const analyze = createDataFlowAnalyzer(context.checker); + + //Case 1: Map is nested in a larger expression within the same JSX expression + // Example: {list.length > 0 && list.map(...)} + // Only check THIS expression for derive wrapping + if (closestJsxExpression.expression !== mapCall) { + const analysis = analyze(closestJsxExpression.expression); + // Check if this will be wrapped in a derive (not just transformed in some other way) + // Array-map calls have skip-call-rewrite hint, so they won't be wrapped in derive + const willBeWrappedInDerive = analysis.requiresRewrite && + !(analysis.rewriteHint?.kind === "skip-call-rewrite" && + analysis.rewriteHint.reason === "array-map"); + return !willBeWrappedInDerive; + } + + // Case 2: Map IS the direct content of the JSX expression + // Example:
{list.map(...)}
+ // Check if an ANCESTOR JSX expression will wrap this in a derive + node = closestJsxExpression.parent; + while (node) { + if (ts.isJsxExpression(node) && node.expression) { + const analysis = analyze(node.expression); + const willBeWrappedInDerive = analysis.requiresRewrite && + !(analysis.rewriteHint?.kind === "skip-call-rewrite" && + analysis.rewriteHint.reason === "array-map"); + if (willBeWrappedInDerive) { + // An ancestor JSX expression will wrap this in a derive + return false; + } + } + node = node.parent; + } + + // No ancestor will wrap in derive, transform normally + return true; +} + /** * Create a visitor function that transforms OpaqueRef map calls. * This visitor can be reused for both top-level and nested transformations. @@ -734,7 +800,11 @@ function createMapTransformVisitor( callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) ) { - return transformMapCallback(node, callback, context, visit); + // Check if this map will end up inside a derive (where array is unwrapped) + // If so, skip transformation and keep it as plain .map + if (shouldTransformMap(node, context)) { + return transformMapCallback(node, callback, context, visit); + } } } diff --git a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts index 177c2503f8..ce2ba883d9 100644 --- a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts +++ b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts @@ -15,6 +15,7 @@ function originatesFromIgnoredParameter( scopeId: number, analysis: DataFlowAnalysis, checker: ts.TypeChecker, + context?: TransformationContext, ): boolean { const scope = analysis.graph.scopes.find((candidate) => candidate.id === scopeId @@ -28,7 +29,7 @@ function originatesFromIgnoredParameter( if (parameter.symbol === symbol || parameter.name === symbolName) { if ( parameter.declaration && - getOpaqueCallKindForParameter(parameter.declaration, checker) + getOpaqueCallKindForParameter(parameter.declaration, checker, context) ) { return false; } @@ -70,6 +71,7 @@ function originatesFromIgnoredParameter( function getOpaqueCallKindForParameter( declaration: ts.ParameterDeclaration, checker: ts.TypeChecker, + context?: TransformationContext, ): "builder" | "array-map" | undefined { let functionNode: ts.Node | undefined = declaration.parent; while (functionNode && !ts.isFunctionLike(functionNode)) { @@ -84,8 +86,18 @@ function getOpaqueCallKindForParameter( if (!candidate) return undefined; const callKind = detectCallKind(candidate, checker); - if (callKind?.kind === "builder" || callKind?.kind === "array-map") { - return callKind.kind; + if (callKind?.kind === "builder") { + return "builder"; + } + if (callKind?.kind === "array-map") { + // For array-map calls, only treat parameters as opaque if the callback + // was actually transformed (marked in mapCallbackRegistry) + // Untransformed maps (plain .map inside derives) should have regular parameters + if (context && !context.isMapCallback(functionNode)) { + // Callback was not transformed, parameters are not opaque + return undefined; + } + return "array-map"; } return undefined; } @@ -156,6 +168,7 @@ export function filterRelevantDataFlows( dataFlow.scopeId, analysis, context.checker, + context, ) ) { return false; @@ -195,6 +208,7 @@ export function filterRelevantDataFlows( dataFlow.scopeId, analysis, context.checker, + context, ) ) { return false; @@ -216,6 +230,7 @@ export function filterRelevantDataFlows( dataFlow.scopeId, analysis, context.checker, + context, ) ) { return false; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx new file mode 100644 index 0000000000..d527c2d25c --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx @@ -0,0 +1,16 @@ +import * as __ctHelpers from "commontools"; +import { cell, recipe, UI } from "commontools"; +export default recipe("MapArrayLengthConditional", (_state) => { + const list = cell(["apple", "banana", "cherry"]); + return { + [UI]: (
+ {__ctHelpers.derive(list, list => list.length > 0 && (
+ {list.map((name) => ({name}))} +
))} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx new file mode 100644 index 0000000000..107b1ebaea --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx @@ -0,0 +1,20 @@ +/// +import { cell, recipe, UI } from "commontools"; + +export default recipe("MapArrayLengthConditional", (_state) => { + const list = cell(["apple", "banana", "cherry"]); + + return { + [UI]: ( +
+ {list.length > 0 && ( +
+ {list.map((name) => ( + {name} + ))} +
+ )} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional.expected.tsx index ce879dc264..0ea3d41132 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional.expected.tsx @@ -6,33 +6,9 @@ export default recipe("MapNestedConditional", (_state) => { return { [UI]: (
{__ctHelpers.derive({ showList, items }, ({ showList: showList, items: items }) => showList && (
- {items.mapWithPattern(__ctHelpers.recipe({ - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - element: { - $ref: "#/$defs/__object" - }, - params: { - type: "object", - properties: {} - } - }, - required: ["element", "params"], - $defs: { - __object: { - type: "object", - properties: { - name: { - type: "string" - } - }, - required: ["name"] - } - } - } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => (
- {__ctHelpers.derive(element.name, _v1 => _v1 && {_v1})} -
)), {})} + {items.map((item) => (
+ {__ctHelpers.derive(item.name, _v1 => _v1 && {_v1})} +
))}
))}
), }; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx index 652f429fbb..261ad143c7 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture.expected.tsx @@ -8,34 +8,7 @@ export default recipe("MapSingleCapture", (_state) => { return { [UI]: (
{__ctHelpers.derive(people, people => people.length > 0 && (
    - {people.mapWithPattern(__ctHelpers.recipe({ - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - element: { - $ref: "#/$defs/__object" - }, - params: { - type: "object", - properties: {} - } - }, - required: ["element", "params"], - $defs: { - __object: { - type: "object", - properties: { - id: { - type: "string" - }, - name: { - type: "string" - } - }, - required: ["id", "name"] - } - } - } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => (
  • {element.name}
  • )), {})} + {people.map((person) => (
  • {person.name}
  • ))}
))}
), }; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx index 7b16b1f7aa..a3d245604a 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx @@ -101,26 +101,12 @@ export default recipe("Charms Launcher", () => { [UI]: (

Stored Charms:

{ifElse(__ctHelpers.derive(typedCellRef, typedCellRef => !typedCellRef?.length),
No charms created yet
,
    - {typedCellRef.mapWithPattern(__ctHelpers.recipe({ - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - element: true, - index: { - type: "number" - }, - params: { - type: "object", - properties: {} - } - }, - required: ["element", "params"] - } as const satisfies __ctHelpers.JSONSchema, ({ element, index, params: {} }) => (
  • - + {typedCellRef.map((charm: any, index: number) => (
  • + Go to Charm {__ctHelpers.derive(index, index => index + 1)} - Charm {__ctHelpers.derive(index, index => index + 1)}: {__ctHelpers.derive(element, element => element[NAME] || "Unnamed")} -
  • )), {})} + Charm {__ctHelpers.derive(index, index => index + 1)}: {__ctHelpers.derive(charm, charm => charm[NAME] || "Unnamed")} + ))}
)} @@ -134,4 +120,3 @@ export default recipe("Charms Launcher", () => { function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } // @ts-ignore: Internals h.fragment = __ctHelpers.h.fragment; -