From f1224f6108b6e6c6be323578e5c5dd52ef273e36 Mon Sep 17 00:00:00 2001 From: gideon Date: Fri, 14 Nov 2025 09:35:36 -0800 Subject: [PATCH 1/4] Test/cell get in ifelse predicate (#2075) * test: add fixture for Cell.get() in ifElse predicate bug Reproduces issue where transformer generates derive without schema for ifElse predicates, causing Cell parameters to be unwrapped when they should remain as Cells (with asCell: true in schema). Expected: derive with full schema preserving asCell: true Actual: derive without schema, unwrapping all values * change order of transformers --- .../ts-transformers/ISSUES_TO_FOLLOW_UP.md | 332 ++++++++++ .../src/closures/computed-aliases.ts | 1 + packages/ts-transformers/src/ct-pipeline.ts | 2 +- .../src/transformers/builtins/derive.ts | 83 ++- .../src/transformers/opaque-ref/helpers.ts | 1 + .../test/derive/create-derive-call.test.ts | 19 + .../builder-conditional.expected.tsx | 24 +- .../counter-pattern.expected.tsx | 20 +- .../counter-recipe-no-name.expected.tsx | 19 +- .../ast-transform/counter-recipe.expected.tsx | 19 +- .../event-handler-no-derive.expected.tsx | 13 +- .../ast-transform/ternary_derive.expected.tsx | 38 +- .../cell-map-with-captures.expected.tsx | 21 + .../closures/filter-map-chain.expected.tsx | 87 ++- ...ap-computed-alias-side-effect.expected.tsx | 7 + .../map-computed-alias-strict.expected.tsx | 20 +- ...uted-alias-with-plain-binding.expected.tsx | 22 + .../map-conditional-expression.expected.tsx | 54 ++ .../map-destructured-alias.expected.tsx | 21 + ...p-destructured-computed-alias.expected.tsx | 7 + .../map-destructured-param.expected.tsx | 42 ++ .../map-element-access-opaque.expected.tsx | 26 + .../map-import-reference.expected.tsx | 19 +- .../map-index-param-used.expected.tsx | 21 + .../map-multiple-captures.expected.tsx | 35 + ...map-multiple-similar-captures.expected.tsx | 43 ++ .../map-plain-array-no-transform.expected.tsx | 20 + .../map-single-capture-no-name.expected.tsx | 25 + ...capture-with-type-arg-no-name.expected.tsx | 27 + .../closures/map-single-capture.expected.tsx | 27 + .../map-template-literal.expected.tsx | 31 + .../cell-get-in-ifelse-predicate.expected.tsx | 61 ++ .../cell-get-in-ifelse-predicate.input.tsx | 23 + .../complex-expressions.expected.tsx | 34 + ...perty-access-with-derived-key.expected.tsx | 81 +++ .../element-access-both-opaque.expected.tsx | 21 +- .../element-access-both-opaque.input.tsx | 2 +- .../element-access-complex.expected.tsx | 540 +++++++++++++++- .../element-access-simple.expected.tsx | 84 ++- .../jsx-arithmetic-operations.expected.tsx | 252 +++++++- .../jsx-complex-mixed.expected.tsx | 334 +++++++++- ...conditional-rendering-no-name.expected.tsx | 272 +++++++- .../jsx-conditional-rendering.expected.tsx | 272 +++++++- .../jsx-function-calls.expected.tsx | 477 +++++++++++++- .../jsx-property-access.expected.tsx | 312 ++++++++- .../jsx-string-operations.expected.tsx | 260 +++++++- .../map-array-length-conditional.expected.tsx | 120 +++- .../map-array-length-conditional.input.tsx | 2 +- ...ap-nested-conditional-no-name.expected.tsx | 125 +++- .../map-nested-conditional.expected.tsx | 125 +++- .../map-single-capture-no-name.expected.tsx | 128 +++- .../map-single-capture.expected.tsx | 128 +++- .../method-chains.expected.tsx | 609 +++++++++++++++++- .../opaque-ref-cell-map.expected.tsx | 44 +- .../opaque-ref-operations.expected.tsx | 38 +- .../optional-chain-captures.expected.tsx | 29 +- .../optional-chain-predicate.expected.tsx | 120 +++- .../optional-element-access.expected.tsx | 120 +++- .../parent-suppression-edge.expected.tsx | 302 ++++++++- .../pattern-statements-vs-jsx.expected.tsx | 77 ++- .../pattern-with-cells.expected.tsx | 39 +- .../recipe-statements-vs-jsx.expected.tsx | 77 ++- .../recipe-with-cells.expected.tsx | 39 +- .../test/opaque-ref/map-callbacks.test.ts | 12 +- 64 files changed, 6099 insertions(+), 186 deletions(-) create mode 100644 packages/ts-transformers/ISSUES_TO_FOLLOW_UP.md create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.input.tsx diff --git a/packages/ts-transformers/ISSUES_TO_FOLLOW_UP.md b/packages/ts-transformers/ISSUES_TO_FOLLOW_UP.md new file mode 100644 index 0000000000..a4584d9a55 --- /dev/null +++ b/packages/ts-transformers/ISSUES_TO_FOLLOW_UP.md @@ -0,0 +1,332 @@ +# Issues to Follow Up + +This document tracks issues noticed during test expectation updates for the Cell +preservation fix. + +## 1. Result Schema Falls Back to `true` for Array Element Access + +**File:** +`test/fixtures/jsx-expressions/element-access-both-opaque.expected.tsx` + +**Issue:** When deriving `items[index]` where `items` is `Cell` and +`index` is `Cell`, the result schema is `true` instead of a proper +schema. + +**Current behavior:** + +```typescript +__ctHelpers.derive( + { + type: "object", + properties: { + items: { + type: "array", + items: { type: "string" }, + asCell: true, + }, + index: { + type: "number", + asCell: true, + }, + }, + required: ["items", "index"], + } as const satisfies __ctHelpers.JSONSchema, + true as const satisfies __ctHelpers.JSONSchema, // ← Falls back to `true` + { + items: items, + index: index, + }, + ({ items, index }) => items[index], +); +``` + +**Expected behavior:** The result schema should probably be: + +```typescript +{ + type: "string"; +} +``` + +**Root cause:** The expression `items[index]` has type `string | undefined` +(because array access can be out of bounds). The type inference may not be able +to create a proper schema for union types with undefined, so it falls back to +`true`. + +**Next steps:** Investigate whether we can improve result type schema generation +for: + +- Array element access expressions +- Union types that include undefined +- Optional/nullable types + +--- + +## 2. Property Chain Access Does Not Mark Leaf Properties as `asOpaque` + +**File:** `test/fixtures/jsx-expressions/jsx-complex-mixed.expected.tsx` + +**Issue:** When accessing properties through a chain (e.g., +`state.filter.length`), the leaf property gets a plain type without +`asOpaque: true`. + +**Current behavior:** + +```typescript +// Input: +{state.filter.length > 0} + +// Generated schema: +__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + filter: { + type: "object", + properties: { + length: { + type: "number" // ❌ Leaf property has NO asOpaque flag + } + }, + required: ["length"] + } + }, + required: ["filter"] + } + }, + required: ["state"] +}, ...) +``` + +**Contrast with direct access:** + +```typescript +// Direct state property access: +{state.filter} + +// Schema: +{ + filter: { + type: "string", + asOpaque: true // ✅ Direct state properties ARE marked as opaque + } +} +``` + +**Question to resolve:** Should leaf properties in a property chain also have +`asOpaque: true`? + +**Arguments for current behavior:** + +- The leaf value (`length`) is not itself an OpaqueRef - it's a plain number + property +- At runtime: `state.filter` unwraps to a string, then `.length` returns a plain + number +- The schema accurately reflects that the final value is plain, not wrapped + +**Arguments for marking as opaque:** + +- The value still comes from state, which is reactive +- Consistency with how other state-derived values are marked +- May be needed for runtime tracking/reactivity + +**Next steps:** Discuss with team whether this is correct or needs to be +changed. + +--- + +## 3. Boolean Schema Behavior is Inconsistent and Confusing + +**File:** `test/fixtures/jsx-expressions/jsx-conditional-rendering.expected.tsx` + +**Issue:** Boolean values and expressions generate different schema patterns +depending on context, and it's unclear why. + +**Observed patterns:** + +1. **Simple boolean state properties used directly in conditions:** Plain + `type: "boolean"` + ```typescript + // Input: {state.isActive ? "Active" : "Inactive"} + // No derive needed - isActive used directly + ``` + +2. **Boolean state properties captured in complex expressions:** `anyOf` with + separate `true`/`false` enums + ```typescript + // Input: state.isPremium ? "Premium" : "Regular" + // Schema: + { + isPremium: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true, + }, { + type: "boolean", + enum: [true], + asOpaque: true, + }]; + } + } + ``` + +3. **Boolean AND expression results:** `anyOf` with `true`/`false` enums + ```typescript + // Input: state.isActive && state.hasPermission + // Result schema: + { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true, + }, { + type: "boolean", + enum: [true], + asOpaque: true, + }]; + } + ``` + +4. **Boolean OR expression results:** Plain `type: "boolean"` + ```typescript + // Input: state.isPremium || state.score > 100 + // Result schema: + { + type: "boolean"; // ← Why no anyOf here? + } + ``` + +5. **Boolean comparison results:** Plain `type: "boolean"` + ```typescript + // Input: state.count > 10 + // Result schema: + { + type: "boolean"; + } + ``` + +**Questions to resolve:** + +- Why do `&&` expressions get `anyOf` but `||` expressions get plain + `type: "boolean"`? +- Why do boolean state captures sometimes get `anyOf` and sometimes not? +- Is the `anyOf` pattern actually necessary, or could we use plain + `type: "boolean"` everywhere? +- What's the semantic difference between `anyOf` with boolean enums vs plain + boolean type? +- Is this TypeScript's literal type narrowing being reflected in schemas? + +**Hypothesis:** The `anyOf` pattern might be TypeScript's way of representing +boolean literal types (`true` | `false`) as distinct from the generic `boolean` +type. The `&&` operator might be preserving literal types while `||` widens to +`boolean`. But this needs verification. + +**Next steps:** + +- Understand the semantic meaning of these schema patterns +- Determine if the inconsistency is intentional or a bug +- Document the rules for when each pattern should be used + +--- + +## 4. JSX Stored in Derives Now Emits the Entire Render-Node Schema + +**Files:**\ +`test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx`\ +`test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx`\ +`test/fixtures/jsx-expressions/map-nested-conditional.expected.tsx` + +**What changed:** With the OpaqueRef JSX transformer running before schema +injection, the injector now understands that expressions like +`list.length > 0 && (
)` return `false | VNode`. Every derive that +wraps JSX now emits a full JSON schema for the render-node union, including +`$defs.Element`, `$defs.VNode`, `$defs.RenderNode`, and `$defs.Props`. Example: + +```tsx +// Input +{ + list.length > 0 && ( +
+ {list.map((name) => {name})} +
+ ); +} + +// Expected output +__ctHelpers.derive( + { + type: "object", + properties: { + list: { + type: "array", + items: { type: "string" }, + asCell: true, + }, + }, + required: ["list"], + } as const satisfies __ctHelpers.JSONSchema, + { + anyOf: [ + { type: "boolean", enum: [false] }, + { $ref: "#/$defs/Element" }, + ], + $defs: { + Element: {/* vnode schema */}, + VNode: {/* vnode schema */}, + RenderNode: {/* recursive union */}, + Props: {/* prop map */}, + }, + } as const satisfies __ctHelpers.JSONSchema, + { list }, + ({ list }) => list.length > 0 &&
, +); +``` + +The same boilerplate now appears in `map-nested-conditional.expected.tsx` where +we map over cell values and render nested `
` trees. + +**Where the schema comes from:** The `$defs` block is the JSON-schema +translation of our runtime `VNode`/`RenderNode` types from `@commontools/html`. +Type inference infers the derive return type (`false | VNode`), and schema +injection faithfully emits that structure. + +**Questions for management:** + +1. Is this level of schema detail desirable in fixtures, or should we collapse + it to a shared alias/reference? Each guarded JSX expression now adds ~100 + lines of output that obscure the interesting differences. +2. If the detail is necessary, can we document that decision so the verbosity + doesn’t raise red flags during review? +3. Alternatively, should schema injection skip result schemas when it’s the + standard render-node shape to keep fixtures readable? + +**Next steps:** Await guidance before updating the remaining fixtures. Depending +on the answer we will either: + +- proceed with the verbose schemas, +- or prototype a shared `$ref`/alias (e.g., `#/RenderNode`) to keep expectations + manageable, +- or adjust the injector to elide the schema when appropriate. + +## 5. `map` in map-array-length-conditional Isn’t Transformed to mapWithPattern + +**Files:** +`test/fixtures/jsx-expressions/map-array-length-conditional.input.tsx` +`test/fixtures/jsx-expressions/map-array-length-conditional.expected.tsx` + +**Observation:** Even after the pipeline changes, the fixture still shows +`list.map((name) => {name})` inside the derive, rather than our +`mapWithPattern` helper that carries explicit schemas. Other fixtures (e.g., +`method-chains`) now use `mapWithPattern` for similar patterns. + +**Open question:** Is this intentional (because the map result is directly +wrapped by JSX and doesn’t need the closure transform), or is the map +transformer failing to recognize this scenario now that the schema injector runs +later? It feels like we’d still want `mapWithPattern` here for consistency and +to keep closures typed. + +**Next steps:** Investigate why the closure transformer skips this case and +confirm with the team whether the current behavior is correct. diff --git a/packages/ts-transformers/src/closures/computed-aliases.ts b/packages/ts-transformers/src/closures/computed-aliases.ts index bb04b822ed..38348d335c 100644 --- a/packages/ts-transformers/src/closures/computed-aliases.ts +++ b/packages/ts-transformers/src/closures/computed-aliases.ts @@ -321,6 +321,7 @@ function createDerivedAliasExpression( factory, tsContext, ctHelpers, + context, }, ); diff --git a/packages/ts-transformers/src/ct-pipeline.ts b/packages/ts-transformers/src/ct-pipeline.ts index 36f857f83c..578a631d40 100644 --- a/packages/ts-transformers/src/ct-pipeline.ts +++ b/packages/ts-transformers/src/ct-pipeline.ts @@ -17,8 +17,8 @@ export class CommonToolsTransformerPipeline extends Pipeline { super([ new ComputedTransformer(ops), new ClosureTransformer(ops), - new SchemaInjectionTransformer(ops), new OpaqueRefJSXTransformer(ops), + new SchemaInjectionTransformer(ops), new SchemaGeneratorTransformer(ops), ]); } diff --git a/packages/ts-transformers/src/transformers/builtins/derive.ts b/packages/ts-transformers/src/transformers/builtins/derive.ts index 13560fdeef..8a9bdfe76e 100644 --- a/packages/ts-transformers/src/transformers/builtins/derive.ts +++ b/packages/ts-transformers/src/transformers/builtins/derive.ts @@ -13,6 +13,11 @@ import { createPropertyParamNames, reserveIdentifier, } from "../../utils/identifiers.ts"; +import { + buildTypeElementsFromCaptureTree, + expressionToTypeNode, +} from "../../ast/type-building.ts"; +import type { TransformationContext } from "../../core/mod.ts"; function replaceOpaqueRefsWithParams( expression: ts.Expression, @@ -41,6 +46,7 @@ export interface DeriveCallOptions { readonly factory: ts.NodeFactory; readonly tsContext: ts.TransformationContext; readonly ctHelpers: CTHelpers; + readonly context: TransformationContext; } function planDeriveEntries( @@ -163,7 +169,7 @@ export function createDeriveCall( ): ts.Expression | undefined { if (refs.length === 0) return undefined; - const { factory, tsContext, ctHelpers } = options; + const { factory, tsContext, ctHelpers, context } = options; const { captureTree, fallbackEntries, refToParamName } = planDeriveEntries( refs, ); @@ -200,9 +206,82 @@ export function createDeriveCall( arrowFunction, ]; + // Build input type node that preserves Cell types + const inputTypeNode = buildInputTypeNode( + captureTree, + fallbackEntries, + context, + ); + + // Build result type node from expression type + const resultTypeNode = buildResultTypeNode(expression, context); + + // Create derive call with type arguments for SchemaInjectionTransformer return factory.createCallExpression( deriveExpr, - undefined, + [inputTypeNode, resultTypeNode], deriveArgs, ); } + +function buildInputTypeNode( + captureTree: ReturnType, + fallbackEntries: readonly FallbackEntry[], + context: TransformationContext, +): ts.TypeNode { + const { factory } = context; + const typeElements: ts.TypeElement[] = []; + + // Add type elements from capture tree (preserves Cell) + const captureTypeElements = buildTypeElementsFromCaptureTree( + captureTree, + context, + ); + typeElements.push(...captureTypeElements); + + // Add type elements for fallback entries + for (const entry of fallbackEntries) { + const typeNode = expressionToTypeNode(entry.ref, context); + typeElements.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier(entry.propertyName), + undefined, + typeNode, + ), + ); + } + + const typeLiteral = factory.createTypeLiteralNode(typeElements); + + return typeLiteral; +} + +function buildResultTypeNode( + expression: ts.Expression, + context: TransformationContext, +): ts.TypeNode { + const { factory, checker } = context; + + // Try to get the type of the result expression + const resultType = checker.getTypeAtLocation(expression); + + // Convert to TypeNode, preserving Cell if present + const resultTypeNode = checker.typeToTypeNode( + resultType, + context.sourceFile, + ts.NodeBuilderFlags.NoTruncation | + ts.NodeBuilderFlags.UseStructuralFallback, + ); + + if (resultTypeNode) { + // Register the type in typeRegistry for SchemaGeneratorTransformer + if (context.options.typeRegistry) { + context.options.typeRegistry.set(resultTypeNode, resultType); + } + return resultTypeNode; + } + + // Fallback to unknown if we can't infer + return factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); +} diff --git a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts index ce2ba883d9..9096a32a07 100644 --- a/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts +++ b/packages/ts-transformers/src/transformers/opaque-ref/helpers.ts @@ -324,6 +324,7 @@ export function createDeriveCallForExpression( factory: context.factory, tsContext: context.tsContext, ctHelpers: context.ctHelpers, + context: context, }); return deriveCall; diff --git a/packages/ts-transformers/test/derive/create-derive-call.test.ts b/packages/ts-transformers/test/derive/create-derive-call.test.ts index 8f736dfabe..d10dc99f55 100644 --- a/packages/ts-transformers/test/derive/create-derive-call.test.ts +++ b/packages/ts-transformers/test/derive/create-derive-call.test.ts @@ -27,6 +27,24 @@ Deno.test("createDeriveCall keeps fallback refs synced when names collide", () = }, } as unknown as CTHelpers; + // Create a minimal program for type checking + const program = ts.createProgram(["test.tsx"], { + target: ts.ScriptTarget.ES2022, + jsx: ts.JsxEmit.React, + }); + const checker = program.getTypeChecker(); + + const transformContext = { + factory, + tsContext: context, + checker, + sourceFile: source, + ctHelpers, + options: { + typeRegistry: new WeakMap(), + }, + } as any; + const rootIdentifier = factory.createIdentifier("_v1"); const fallbackExpr = factory.createParenthesizedExpression(rootIdentifier); @@ -37,6 +55,7 @@ Deno.test("createDeriveCall keeps fallback refs synced when names collide", () = factory, tsContext: context, ctHelpers, + context: transformContext, }); if (!derive) { diff --git a/packages/ts-transformers/test/fixtures/ast-transform/builder-conditional.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/builder-conditional.expected.tsx index 7c83d243be..58dfdaaaf0 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/builder-conditional.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/builder-conditional.expected.tsx @@ -21,7 +21,29 @@ export default recipe({ return { [NAME]: state.label, [UI]: (
- {__ctHelpers.ifElse(__ctHelpers.derive({ state: state }, ({ state }) => state && state.count > 0),

Positive

,

Non-positive

)} + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + default: 0 + }, + label: { + type: "string", + default: "" + } + }, + required: ["count", "label"], + asOpaque: true + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: state }, ({ state }) => state && state.count > 0),

Positive

,

Non-positive

)}
), }; }); diff --git a/packages/ts-transformers/test/fixtures/ast-transform/counter-pattern.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/counter-pattern.expected.tsx index 5732638e1d..6d1bad7459 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/counter-pattern.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/counter-pattern.expected.tsx @@ -38,7 +38,24 @@ export default pattern((state) => { [UI]: (
-
    -
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ state: { +
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 1), "unknown")}
@@ -175,3 +192,4 @@ export default pattern((state) => { 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/ast-transform/counter-recipe-no-name.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe-no-name.expected.tsx index c4b78d7bba..4ba5fc1a73 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe-no-name.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe-no-name.expected.tsx @@ -47,7 +47,24 @@ export default recipe({ [UI]: (
-
    -
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ state: { +
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 1), "unknown")}
diff --git a/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe.expected.tsx index c4b78d7bba..4ba5fc1a73 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/counter-recipe.expected.tsx @@ -47,7 +47,24 @@ export default recipe({ [UI]: (
-
    -
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ state: { +
  • next number: {__ctHelpers.ifElse(state.value, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 1), "unknown")}
diff --git a/packages/ts-transformers/test/fixtures/ast-transform/event-handler-no-derive.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/event-handler-no-derive.expected.tsx index deb45d7b7b..79c7c05b40 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/event-handler-no-derive.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/event-handler-no-derive.expected.tsx @@ -32,7 +32,18 @@ export default recipe({ return { [UI]: (
{/* Regular JSX expression - should be wrapped in derive */} - Count: {__ctHelpers.derive({ count: count }, ({ count }) => count + 1)} + Count: {__ctHelpers.derive({ + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { count: count }, ({ count }) => count + 1)} {/* Event handler with OpaqueRef - should NOT be wrapped in derive */} diff --git a/packages/ts-transformers/test/fixtures/ast-transform/ternary_derive.expected.tsx b/packages/ts-transformers/test/fixtures/ast-transform/ternary_derive.expected.tsx index c90445a181..9b89729c78 100644 --- a/packages/ts-transformers/test/fixtures/ast-transform/ternary_derive.expected.tsx +++ b/packages/ts-transformers/test/fixtures/ast-transform/ternary_derive.expected.tsx @@ -16,9 +16,43 @@ export default recipe({ return { [NAME]: "test ternary with derive", [UI]: (
- {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value - } }, ({ state }) => state.value + 1), __ctHelpers.derive({ state: { + } }, ({ state }) => state.value + 1), __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 2), "undefined")}
), diff --git a/packages/ts-transformers/test/fixtures/closures/cell-map-with-captures.expected.tsx b/packages/ts-transformers/test/fixtures/closures/cell-map-with-captures.expected.tsx index bf6582b065..552d4498f4 100644 --- a/packages/ts-transformers/test/fixtures/closures/cell-map-with-captures.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/cell-map-with-captures.expected.tsx @@ -48,6 +48,27 @@ export default recipe({ }, required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: value, params: { state } }) => ({__ctHelpers.derive({ + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + multiplier: { + type: "number", + asOpaque: true + } + }, + required: ["multiplier"] + } + }, + required: ["value", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { value: value, state: { multiplier: state.multiplier diff --git a/packages/ts-transformers/test/fixtures/closures/filter-map-chain.expected.tsx b/packages/ts-transformers/test/fixtures/closures/filter-map-chain.expected.tsx index 47ab607e66..d979e83a0f 100644 --- a/packages/ts-transformers/test/fixtures/closures/filter-map-chain.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/filter-map-chain.expected.tsx @@ -44,7 +44,65 @@ export default recipe({ return { [UI]: (
{/* Method chain: filter then map, both with captures */} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "array", + items: { + $ref: "#/$defs/Item", + asOpaque: true + }, + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items .filter((item) => item.active)).mapWithPattern(__ctHelpers.recipe({ @@ -91,6 +149,33 @@ export default recipe({ } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => (
Total: {__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + taxRate: { + type: "number", + asOpaque: true + } + }, + required: ["taxRate"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx index d89d4ccf51..5f63c8c3a7 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx @@ -44,6 +44,13 @@ export default recipe({ } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => { const __ct_amount_key = nextKey(); const amount = __ctHelpers.derive({ + type: "object", + properties: { + element: true, + __ct_amount_key: true + }, + required: ["element", "__ct_amount_key"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { element: element, __ct_amount_key: __ct_amount_key }, ({ element, __ct_amount_key }) => element[__ct_amount_key]); diff --git a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-strict.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-strict.expected.tsx index 337e497c80..039ca3cb3d 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-strict.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-strict.expected.tsx @@ -65,11 +65,29 @@ export default recipe({ } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => { const __ct_val_key = dynamicKey; const val = __ctHelpers.derive({ + type: "object", + properties: { + element: true, + __ct_val_key: true + }, + required: ["element", "__ct_val_key"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { element: element, __ct_val_key: __ct_val_key }, ({ element, __ct_val_key }) => element[__ct_val_key]); "use strict"; - return {__ctHelpers.derive({ val: val }, ({ val }) => val * 2)}; + return {__ctHelpers.derive({ + type: "object", + properties: { + val: { + type: "number", + asOpaque: true + } + }, + required: ["val"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { val: val }, ({ val }) => val * 2)}; }), {})}
), }; diff --git a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-with-plain-binding.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-with-plain-binding.expected.tsx index 7fe50f330f..4aca2d43bf 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-with-plain-binding.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-with-plain-binding.expected.tsx @@ -68,10 +68,32 @@ export default recipe({ const __ct_val_key = dynamicKey(); const { foo } = element; const val = __ctHelpers.derive({ + type: "object", + properties: { + element: true, + __ct_val_key: true + }, + required: ["element", "__ct_val_key"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { element: element, __ct_val_key: __ct_val_key }, ({ element, __ct_val_key }) => element[__ct_val_key]); return ({__ctHelpers.derive({ + type: "object", + properties: { + foo: { + type: "number", + asOpaque: true + }, + val: { + type: "number", + asOpaque: true + } + }, + required: ["foo", "val"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { foo: foo, val: val }, ({ foo, val }) => foo + val)}); diff --git a/packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx index 03bbd8b53e..618c9959e6 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx @@ -88,6 +88,33 @@ export default recipe({ } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => (
Price: ${__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + threshold: { + type: "number", + asOpaque: true + } + }, + required: ["threshold"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, @@ -95,6 +122,33 @@ export default recipe({ threshold: state.threshold } }, ({ item, state }) => item.price > state.threshold), __ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx index d58e918b77..7588b05b52 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx @@ -60,6 +60,27 @@ export default recipe({ }, required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: { price: cost }, params: { state } }) => ({__ctHelpers.derive({ + type: "object", + properties: { + cost: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["cost", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { cost: cost, state: { discount: state.discount diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx index 9517d91346..f8e1110543 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx @@ -65,6 +65,13 @@ export default recipe({ } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => { const __ct_val_key = dynamicKey; const val = __ctHelpers.derive({ + type: "object", + properties: { + element: true, + __ct_val_key: true + }, + required: ["element", "__ct_val_key"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { element: element, __ct_val_key: __ct_val_key }, ({ element, __ct_val_key }) => element[__ct_val_key]); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-param.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-param.expected.tsx index 9009d5662e..0f08c5a4cd 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-param.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-param.expected.tsx @@ -80,11 +80,53 @@ export default recipe({ } } as const satisfies __ctHelpers.JSONSchema, ({ element: { x, y }, params: { state } }) => (
Point: ({__ctHelpers.derive({ + type: "object", + properties: { + x: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + scale: { + type: "number", + asOpaque: true + } + }, + required: ["scale"] + } + }, + required: ["x", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { x: x, state: { scale: state.scale } }, ({ x, state }) => x * state.scale)}, {__ctHelpers.derive({ + type: "object", + properties: { + y: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + scale: { + type: "number", + asOpaque: true + } + }, + required: ["scale"] + } + }, + required: ["y", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { y: y, state: { scale: state.scale diff --git a/packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.expected.tsx index daefa25fe0..177a406eb7 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.expected.tsx @@ -55,6 +55,32 @@ export default recipe({ required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: tag, params: { state } }) => ( {tag}: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + tagCounts: { + type: "object", + properties: {}, + additionalProperties: { + type: "number" + }, + asOpaque: true + } + }, + required: ["tagCounts"] + }, + tag: { + type: "string", + asOpaque: true + } + }, + required: ["state", "tag"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { tagCounts: state.tagCounts }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-import-reference.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-import-reference.expected.tsx index 1a5723ea07..7d556e2314 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-import-reference.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-import-reference.expected.tsx @@ -69,7 +69,24 @@ export default recipe({ } } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: {} }) => (
- Item: {__ctHelpers.derive({ item: { + Item: {__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["item"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price } }, ({ item }) => formatPrice(item.price * (1 + TAX_RATE)))}
)), {})} diff --git a/packages/ts-transformers/test/fixtures/closures/map-index-param-used.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-index-param-used.expected.tsx index 543fc68701..3d7b8f53df 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-index-param-used.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-index-param-used.expected.tsx @@ -83,6 +83,27 @@ export default recipe({ } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, index: index, params: { state } }) => (
Item #{__ctHelpers.derive({ + type: "object", + properties: { + index: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + offset: { + type: "number", + asOpaque: true + } + }, + required: ["offset"] + } + }, + required: ["index", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { index: index, state: { offset: state.offset diff --git a/packages/ts-transformers/test/fixtures/closures/map-multiple-captures.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-multiple-captures.expected.tsx index 7fe7e5ca4a..7022bd7b05 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-multiple-captures.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-multiple-captures.expected.tsx @@ -93,6 +93,41 @@ export default recipe({ } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state, multiplier } }) => ( Total: {__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + quantity: { + type: "number", + asOpaque: true + } + }, + required: ["price", "quantity"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + }, + taxRate: { + type: "number", + asOpaque: true + } + }, + required: ["discount", "taxRate"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price, quantity: item.quantity diff --git a/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx index 720da23596..9803158038 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx @@ -97,6 +97,49 @@ export default recipe({ required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => ( {__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + checkout: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + }, + upsell: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["checkout", "upsell"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-plain-array-no-transform.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-plain-array-no-transform.expected.tsx index 89315294f2..03914d1b2c 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-plain-array-no-transform.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-plain-array-no-transform.expected.tsx @@ -17,6 +17,26 @@ export default recipe({ [UI]: (
{/* Plain array should NOT be transformed, even with captures */} {plainArray.map((n) => ({__ctHelpers.derive({ + type: "object", + properties: { + n: { + type: "number" + }, + state: { + type: "object", + properties: { + multiplier: { + type: "number", + asOpaque: true + } + }, + required: ["multiplier"] + } + }, + required: ["n", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { n: n, state: { multiplier: state.multiplier diff --git a/packages/ts-transformers/test/fixtures/closures/map-single-capture-no-name.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-single-capture-no-name.expected.tsx index ca4b50a42e..1d231c7855 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-single-capture-no-name.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-single-capture-no-name.expected.tsx @@ -30,6 +30,31 @@ export default recipe({ return { [UI]: (
{state.items.map((item) => ({__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number" + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number" + } + }, + required: ["discount"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-single-capture-with-type-arg-no-name.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-single-capture-with-type-arg-no-name.expected.tsx index c7b20e0205..40c3d1bcb0 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-single-capture-with-type-arg-no-name.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-single-capture-with-type-arg-no-name.expected.tsx @@ -60,6 +60,33 @@ export default recipe({ }, required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => ({__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, 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 c7b20e0205..40c3d1bcb0 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 @@ -60,6 +60,33 @@ export default recipe({ }, required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => ({__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, diff --git a/packages/ts-transformers/test/fixtures/closures/map-template-literal.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-template-literal.expected.tsx index c4dda1fb22..1314eecae8 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-template-literal.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-template-literal.expected.tsx @@ -87,6 +87,37 @@ export default recipe({ } } } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: { state } }) => (
{__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + prefix: { + type: "string", + asOpaque: true + }, + suffix: { + type: "string", + asOpaque: true + } + }, + required: ["prefix", "suffix"] + }, + item: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["state", "item"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { prefix: state.prefix, suffix: state.suffix diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.expected.tsx new file mode 100644 index 0000000000..5d05304a26 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.expected.tsx @@ -0,0 +1,61 @@ +import * as __ctHelpers from "commontools"; +import { Cell, ifElse, recipe, UI } from "commontools"; +// Reproduction of bug: .get() called on Cell inside ifElse predicate +// The transformer wraps predicates in derive(), which unwraps Cells, +// but fails to remove the .get() calls +export default recipe({ + type: "object", + properties: { + showHistory: { + type: "boolean" + }, + messageCount: { + type: "number" + }, + dismissedIndex: { + type: "number", + asCell: true + } + }, + required: ["showHistory", "messageCount", "dismissedIndex"] +} as const satisfies __ctHelpers.JSONSchema, ({ showHistory, messageCount, dismissedIndex }) => { + return { + [UI]: (
+ {ifElse(__ctHelpers.derive({ + type: "object", + properties: { + showHistory: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + messageCount: { + type: "number", + asOpaque: true + }, + dismissedIndex: { + type: "number", + asCell: true + } + }, + required: ["showHistory", "messageCount", "dismissedIndex"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { + showHistory: showHistory, + messageCount: messageCount, + dismissedIndex: dismissedIndex + }, ({ showHistory, messageCount, dismissedIndex }) => showHistory && messageCount !== dismissedIndex.get()),
Show notification
,
Hide notification
)} +
), + }; +}); +// @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/cell-get-in-ifelse-predicate.input.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.input.tsx new file mode 100644 index 0000000000..8b3d93e868 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/cell-get-in-ifelse-predicate.input.tsx @@ -0,0 +1,23 @@ +/// +import { Cell, ifElse, recipe, UI } from "commontools"; + +// Reproduction of bug: .get() called on Cell inside ifElse predicate +// The transformer wraps predicates in derive(), which unwraps Cells, +// but fails to remove the .get() calls +export default recipe<{ + showHistory: boolean; + messageCount: number; + dismissedIndex: Cell; +}>("Cell .get() in ifElse predicate", ({ showHistory, messageCount, dismissedIndex }) => { + return { + [UI]: ( +
+ {ifElse( + showHistory && messageCount !== dismissedIndex.get(), +
Show notification
, +
Hide notification
+ )} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/complex-expressions.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/complex-expressions.expected.tsx index b36cf2c17e..ebbfbfa786 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/complex-expressions.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/complex-expressions.expected.tsx @@ -24,10 +24,44 @@ export default recipe({ [UI]: (

Price: {price}

Discount: {__ctHelpers.derive({ + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + } + }, + required: ["price", "discount"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { price: price, discount: discount }, ({ price, discount }) => price - discount)}

With tax: {__ctHelpers.derive({ + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + }, + tax: { + type: "number", + asOpaque: true + } + }, + required: ["price", "discount", "tax"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { price: price, discount: discount, tax: tax diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/derived-property-access-with-derived-key.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/derived-property-access-with-derived-key.expected.tsx index 84dd3d4792..43288caf4f 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/derived-property-access-with-derived-key.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/derived-property-access-with-derived-key.expected.tsx @@ -296,6 +296,86 @@ export default recipe({ } as const satisfies __ctHelpers.JSONSchema, ({ element: aisleName, params: { groupedByAisle } }) => (

{aisleName}

{(__ctHelpers.derive({ + type: "object", + properties: { + groupedByAisle: { + type: "object", + properties: {}, + additionalProperties: { + type: "array", + items: { + $ref: "#/$defs/Assignment" + } + }, + asOpaque: true + }, + aisleName: { + type: "string", + asOpaque: true + } + }, + required: ["groupedByAisle", "aisleName"], + $defs: { + Assignment: { + type: "object", + properties: { + aisle: { + type: "string" + }, + item: { + $ref: "#/$defs/Item" + } + }, + required: ["aisle", "item"] + }, + Item: { + type: "object", + properties: { + name: { + type: "string" + }, + done: { + type: "boolean", + asCell: true + } + }, + required: ["name", "done"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "array", + items: { + $ref: "#/$defs/Assignment" + }, + asOpaque: true, + $defs: { + Assignment: { + type: "object", + properties: { + aisle: { + type: "string" + }, + item: { + $ref: "#/$defs/Item" + } + }, + required: ["aisle", "item"] + }, + Item: { + type: "object", + properties: { + name: { + type: "string" + }, + done: { + type: "boolean", + asCell: true + } + }, + required: ["name", "done"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { groupedByAisle: groupedByAisle, aisleName: aisleName }, ({ groupedByAisle, aisleName }) => groupedByAisle[aisleName] ?? [])).mapWithPattern(__ctHelpers.recipe({ @@ -351,3 +431,4 @@ export default recipe({ 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/element-access-both-opaque.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.expected.tsx index 115888b187..34ea68d86e 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.expected.tsx @@ -8,9 +8,27 @@ export default recipe("ElementAccessBothOpaque", (_state) => {

Element Access with Both OpaqueRefs

{/* Both items and index are OpaqueRefs */}

Selected item: {__ctHelpers.derive({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asCell: true + }, + index: { + type: "number", + asCell: true + } + }, + required: ["items", "index"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { items: items, index: index - }, ({ items, index }) => items[index])}

+ }, ({ items, index }) => items.get()[index.get()])}

), }; }); @@ -18,3 +36,4 @@ export default recipe("ElementAccessBothOpaque", (_state) => { 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/element-access-both-opaque.input.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.input.tsx index 92de522332..ee16cbfc76 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.input.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-both-opaque.input.tsx @@ -10,7 +10,7 @@ export default recipe("ElementAccessBothOpaque", (_state) => {

Element Access with Both OpaqueRefs

{/* Both items and index are OpaqueRefs */} -

Selected item: {items[index]}

+

Selected item: {items.get()[index.get()]}

), }; diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-complex.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-complex.expected.tsx index c91f00599f..0c66f38dee 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-complex.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-complex.expected.tsx @@ -111,14 +111,84 @@ export default recipe({ [UI]: (

Nested Element Access

{/* Double indexing into matrix */} -

Matrix value: {__ctHelpers.derive({ state: { +

Matrix value: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + matrix: { + type: "array", + items: { + type: "array", + items: { + type: "number" + } + }, + asOpaque: true + }, + row: { + type: "number", + asOpaque: true + }, + col: { + type: "number", + asOpaque: true + } + }, + required: ["matrix", "row", "col"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { matrix: state.matrix, row: state.row, col: state.col } }, ({ state }) => state.matrix[state.row][state.col])}

{/* Triple nested access */} -

Deep nested: {__ctHelpers.derive({ state: { +

Deep nested: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + nested: { + type: "object", + properties: { + arrays: { + type: "array", + items: { + type: "array", + items: { + type: "string" + } + }, + asOpaque: true + }, + index: { + type: "number", + asOpaque: true + } + }, + required: ["arrays", "index"] + }, + row: { + type: "number", + asOpaque: true + } + }, + required: ["nested", "row"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { nested: { arrays: state.nested.arrays, index: state.nested.index @@ -130,32 +200,152 @@ export default recipe({ {/* Same array accessed multiple times with different indices */}

First and last: {state.items[0]} and{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items[state.items.length - 1])}

{/* Array used in computation and access */} -

Sum of ends: {__ctHelpers.derive({ state: { +

Sum of ends: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["arr"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr } }, ({ state }) => state.arr[0] + state.arr[state.arr.length - 1])}

Computed Indices

{/* Index from multiple state values */} -

Computed index: {__ctHelpers.derive({ state: { +

Computed index: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + a: { + type: "number", + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["arr", "a", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr, a: state.a, b: state.b } }, ({ state }) => state.arr[state.a + state.b])}

{/* Index from computation involving array */} -

Modulo index: {__ctHelpers.derive({ state: { +

Modulo index: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + row: { + type: "number", + asOpaque: true + } + }, + required: ["items", "row"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, row: state.row } }, ({ state }) => state.items[state.row % state.items.length])}

{/* Complex index expression */} -

Complex: {__ctHelpers.derive({ state: { +

Complex: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + a: { + type: "number", + asOpaque: true + } + }, + required: ["arr", "a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr, a: state.a } }, ({ state }) => state.arr[Math.min(state.a * 2, state.arr.length - 1)])}

@@ -164,7 +354,48 @@ export default recipe({ {/* Element access returning array, then accessing that */}

User score:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + scores: { + type: "array", + items: { + type: "number" + } + } + }, + required: ["name", "scores"] + }, + asOpaque: true + }, + selectedUser: { + type: "number", + asOpaque: true + }, + selectedScore: { + type: "number", + asOpaque: true + } + }, + required: ["users", "selectedUser", "selectedScore"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { users: state.users, selectedUser: state.selectedUser, selectedScore: state.selectedScore @@ -172,19 +403,101 @@ export default recipe({

{/* Using one array element as index for another */} -

Indirect: {__ctHelpers.derive({ state: { +

Indirect: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + indices: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["items", "indices"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, indices: state.indices } }, ({ state }) => state.items[state.indices[0]])}

{/* Array element used as index for same array */} -

Self reference: {__ctHelpers.derive({ state: { +

Self reference: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["arr"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr } }, ({ state }) => state.arr[state.arr[0]])}

Mixed Property and Element Access

{/* Property access followed by element access with computed index */} -

Mixed: {__ctHelpers.derive({ state: { +

Mixed: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + nested: { + type: "object", + properties: { + arrays: { + type: "array", + items: { + type: "array", + items: { + type: "string" + } + }, + asOpaque: true + }, + index: { + type: "number", + asOpaque: true + } + }, + required: ["arrays", "index"] + } + }, + required: ["nested"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { nested: { arrays: state.nested.arrays, index: state.nested.index @@ -192,7 +505,43 @@ export default recipe({ } }, ({ state }) => state.nested.arrays[state.nested.index].length)}

{/* Element access followed by property access */} -

User name length: {__ctHelpers.derive({ state: { +

User name length: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + scores: { + type: "array", + items: { + type: "number" + } + } + }, + required: ["name", "scores"] + }, + asOpaque: true + }, + selectedUser: { + type: "number", + asOpaque: true + } + }, + required: ["users", "selectedUser"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { users: state.users, selectedUser: state.selectedUser } }, ({ state }) => state.users[state.selectedUser].name.length)}

@@ -201,10 +550,59 @@ export default recipe({ {/* Element access in ternary */}

Conditional:{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + a: { + type: "number", + asOpaque: true + } + }, + required: ["arr", "a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr, a: state.a - } }, ({ state }) => state.arr[state.a] > 10), __ctHelpers.derive({ state: { + } }, ({ state }) => state.arr[state.a] > 10), __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["items", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, b: state.b } }, ({ state }) => state.items[state.b]), state.items[0])} @@ -212,7 +610,38 @@ export default recipe({ {/* Element access in boolean expression */}

- Has value: {ifElse(__ctHelpers.derive({ state: { + Has value: {ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + matrix: { + type: "array", + items: { + type: "array", + items: { + type: "number" + } + }, + asOpaque: true + }, + row: { + type: "number", + asOpaque: true + }, + col: { + type: "number", + asOpaque: true + } + }, + required: ["matrix", "row", "col"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { matrix: state.matrix, row: state.row, col: state.col @@ -221,20 +650,95 @@ export default recipe({

Element Access with Operators

{/* Element access with arithmetic */} -

Product: {__ctHelpers.derive({ state: { +

Product: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + a: { + type: "number", + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["arr", "a", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr, a: state.a, b: state.b } }, ({ state }) => state.arr[state.a] * state.arr[state.b])}

{/* Element access with string concatenation */} -

Concat: {__ctHelpers.derive({ state: { +

Concat: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + indices: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["items", "indices"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, indices: state.indices } }, ({ state }) => state.items[0] + " - " + state.items[state.indices[0]])}

{/* Multiple element accesses in single expression */} -

Sum: {__ctHelpers.derive({ state: { +

Sum: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + arr: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["arr"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { arr: state.arr } }, ({ state }) => state.arr[0] + state.arr[1] + state.arr[2])}

), diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-simple.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-simple.expected.tsx index 131e833499..3759217a2e 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-simple.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/element-access-simple.expected.tsx @@ -41,18 +41,96 @@ export default recipe({ [UI]: (

Dynamic Element Access

{/* Basic dynamic index */} -

Item: {__ctHelpers.derive({ state: { +

Item: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + index: { + type: "number", + asOpaque: true + } + }, + required: ["items", "index"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, index: state.index } }, ({ state }) => state.items[state.index])}

{/* Computed index */} -

Last: {__ctHelpers.derive({ state: { +

Last: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items[state.items.length - 1])}

{/* Double indexing */} -

Matrix: {__ctHelpers.derive({ state: { +

Matrix: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + matrix: { + type: "array", + items: { + type: "array", + items: { + type: "number" + } + }, + asOpaque: true + }, + row: { + type: "number", + asOpaque: true + }, + col: { + type: "number", + asOpaque: true + } + }, + required: ["matrix", "row", "col"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { matrix: state.matrix, row: state.row, col: state.col diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-arithmetic-operations.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-arithmetic-operations.expected.tsx index e8f7ac3f58..4ef6daef03 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-arithmetic-operations.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-arithmetic-operations.expected.tsx @@ -27,37 +27,214 @@ export default recipe({ return { [UI]: (

Basic Arithmetic

-

Count + 1: {__ctHelpers.derive({ state: { +

Count + 1: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count + 1)}

-

Count - 1: {__ctHelpers.derive({ state: { +

Count - 1: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count - 1)}

-

Count * 2: {__ctHelpers.derive({ state: { +

Count * 2: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count * 2)}

-

Price / 2: {__ctHelpers.derive({ state: { +

Price / 2: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => state.price / 2)}

-

Count % 3: {__ctHelpers.derive({ state: { +

Count % 3: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count % 3)}

Complex Expressions

-

Discounted Price: {__ctHelpers.derive({ state: { +

Discounted Price: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + } + }, + required: ["price", "discount"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price, discount: state.discount } }, ({ state }) => state.price - (state.price * state.discount))}

-

Total: {__ctHelpers.derive({ state: { +

Total: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + quantity: { + type: "number", + asOpaque: true + } + }, + required: ["price", "quantity"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price, quantity: state.quantity } }, ({ state }) => state.price * state.quantity)}

-

With Tax (8%): {__ctHelpers.derive({ state: { +

With Tax (8%): {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + }, + quantity: { + type: "number", + asOpaque: true + } + }, + required: ["price", "quantity"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price, quantity: state.quantity } }, ({ state }) => (state.price * state.quantity) * 1.08)}

- Complex: {__ctHelpers.derive({ state: { + Complex: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + }, + quantity: { + type: "number", + asOpaque: true + }, + price: { + type: "number", + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + } + }, + required: ["count", "quantity", "price", "discount"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count, quantity: state.quantity, price: state.price, @@ -67,12 +244,63 @@ export default recipe({

Multiple Same Ref

-

Count³: {__ctHelpers.derive({ state: { +

Count³: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count * state.count * state.count)}

-

Price Range: ${__ctHelpers.derive({ state: { +

Price Range: ${__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price - } }, ({ state }) => state.price - 10)} - ${__ctHelpers.derive({ state: { + } }, ({ state }) => state.price - 10)} - ${__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => state.price + 10)}

), diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-complex-mixed.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-complex-mixed.expected.tsx index ffb3b7f974..b0ad97fa3e 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-complex-mixed.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-complex-mixed.expected.tsx @@ -59,7 +59,51 @@ export default recipe({

Total items: {state.items.length}

Filtered count:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + }, + filter: { + type: "string", + asOpaque: true + } + }, + required: ["items", "filter"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + name: { + type: "string" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "name", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, filter: state.filter } }, ({ state }) => state.items.filter((i) => i.name.includes(state.filter)).length)} @@ -120,6 +164,33 @@ export default recipe({ - Original: ${item.price} - Discounted: ${__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, @@ -131,6 +202,37 @@ export default recipe({ - With tax: ${__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + }, + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + }, + taxRate: { + type: "number", + asOpaque: true + } + }, + required: ["discount", "taxRate"] + } + }, + required: ["item", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { item: { price: item.price }, @@ -151,34 +253,250 @@ export default recipe({

Array Methods

Item count: {state.items.length}

-

Active items: {__ctHelpers.derive({ state: { +

Active items: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + name: { + type: "string" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "name", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items.filter((i) => i.active).length)}

Simple Operations

-

Discount percent: {__ctHelpers.derive({ state: { +

Discount percent: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { discount: state.discount } }, ({ state }) => state.discount * 100)}%

-

Tax percent: {__ctHelpers.derive({ state: { +

Tax percent: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + taxRate: { + type: "number", + asOpaque: true + } + }, + required: ["taxRate"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { taxRate: state.taxRate } }, ({ state }) => state.taxRate * 100)}%

Array Predicates

-

All active: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { +

All active: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + name: { + type: "string" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "name", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items.every((i) => i.active)), "Yes", "No")}

-

Any active: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { +

Any active: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + name: { + type: "string" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "name", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items.some((i) => i.active)), "Yes", "No")}

Has expensive (gt 100):{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"], + $defs: { + Item: { + type: "object", + properties: { + id: { + type: "number" + }, + name: { + type: "string" + }, + price: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["id", "name", "price", "active"] + } + } + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items.some((i) => i.price > 100)), "Yes", "No")}

Object Operations

-
{__ctHelpers.ifElse(state.hasPermission, "Authorized", "Denied")}

Ternary with Comparisons

- {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 10), "High", "Low")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + score: { + type: "number", + asOpaque: true + } + }, + required: ["score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { score: state.score - } }, ({ state }) => state.score >= 90), "A", __ctHelpers.derive({ state: { + } }, ({ state }) => state.score >= 90), "A", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + score: { + type: "number", + asOpaque: true + } + }, + required: ["score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["B", "C"] + } as const satisfies __ctHelpers.JSONSchema, { state: { score: state.score } }, ({ state }) => state.score >= 80 ? "B" : "C"))} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count - } }, ({ state }) => state.count === 0), "Empty", __ctHelpers.derive({ state: { + } }, ({ state }) => state.count === 0), "Empty", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["Single", "Multiple"] + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count === 1 ? "Single" @@ -59,14 +144,72 @@ export default recipe({

Nested Ternary

- {__ctHelpers.ifElse(state.isActive, __ctHelpers.derive({ state: { + {__ctHelpers.ifElse(state.isActive, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isPremium: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["isPremium"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["Premium Active", "Regular Active"] + } as const satisfies __ctHelpers.JSONSchema, { state: { isPremium: state.isPremium } }, ({ state }) => (state.isPremium ? "Premium Active" : "Regular Active")), "Inactive")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + userType: { + type: "string", + asOpaque: true + } + }, + required: ["userType"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { userType: state.userType - } }, ({ state }) => state.userType === "admin"), "Admin", __ctHelpers.derive({ state: { + } }, ({ state }) => state.userType === "admin"), "Admin", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + userType: { + type: "string", + asOpaque: true + } + }, + required: ["userType"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["User", "Guest"] + } as const satisfies __ctHelpers.JSONSchema, { state: { userType: state.userType } }, ({ state }) => state.userType === "user" ? "User" @@ -75,18 +218,106 @@ export default recipe({

Complex Conditions

- {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isActive: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + hasPermission: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["isActive", "hasPermission"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } as const satisfies __ctHelpers.JSONSchema, { state: { isActive: state.isActive, hasPermission: state.hasPermission } }, ({ state }) => state.isActive && state.hasPermission), "Full Access", "Limited Access")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 0 && state.count < 10), "In Range", "Out of Range")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isPremium: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + score: { + type: "number", + asOpaque: true + } + }, + required: ["isPremium", "score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { isPremium: state.isPremium, score: state.score } }, ({ state }) => state.isPremium || state.score > 100), "Premium Features", "Basic Features")} @@ -95,7 +326,24 @@ export default recipe({

IfElse Component

{ifElse(state.isActive,
User is active with {state.count} items
,
User is inactive
)} - {ifElse(__ctHelpers.derive({ state: { + {ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 5),
  • Many items: {state.count}
  • diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-conditional-rendering.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-conditional-rendering.expected.tsx index 05fe9f2151..40f2d155dc 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-conditional-rendering.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-conditional-rendering.expected.tsx @@ -39,18 +39,103 @@ export default recipe({ {__ctHelpers.ifElse(state.hasPermission, "Authorized", "Denied")}

    Ternary with Comparisons

    - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 10), "High", "Low")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + score: { + type: "number", + asOpaque: true + } + }, + required: ["score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { score: state.score - } }, ({ state }) => state.score >= 90), "A", __ctHelpers.derive({ state: { + } }, ({ state }) => state.score >= 90), "A", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + score: { + type: "number", + asOpaque: true + } + }, + required: ["score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["B", "C"] + } as const satisfies __ctHelpers.JSONSchema, { state: { score: state.score } }, ({ state }) => state.score >= 80 ? "B" : "C"))} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count - } }, ({ state }) => state.count === 0), "Empty", __ctHelpers.derive({ state: { + } }, ({ state }) => state.count === 0), "Empty", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["Single", "Multiple"] + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count === 1 ? "Single" @@ -59,14 +144,72 @@ export default recipe({

    Nested Ternary

    - {__ctHelpers.ifElse(state.isActive, __ctHelpers.derive({ state: { + {__ctHelpers.ifElse(state.isActive, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isPremium: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["isPremium"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["Premium Active", "Regular Active"] + } as const satisfies __ctHelpers.JSONSchema, { state: { isPremium: state.isPremium } }, ({ state }) => (state.isPremium ? "Premium Active" : "Regular Active")), "Inactive")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + userType: { + type: "string", + asOpaque: true + } + }, + required: ["userType"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { userType: state.userType - } }, ({ state }) => state.userType === "admin"), "Admin", __ctHelpers.derive({ state: { + } }, ({ state }) => state.userType === "admin"), "Admin", __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + userType: { + type: "string", + asOpaque: true + } + }, + required: ["userType"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + enum: ["User", "Guest"] + } as const satisfies __ctHelpers.JSONSchema, { state: { userType: state.userType } }, ({ state }) => state.userType === "user" ? "User" @@ -75,18 +218,106 @@ export default recipe({

    Complex Conditions

    - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isActive: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + hasPermission: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["isActive", "hasPermission"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } as const satisfies __ctHelpers.JSONSchema, { state: { isActive: state.isActive, hasPermission: state.hasPermission } }, ({ state }) => state.isActive && state.hasPermission), "Full Access", "Limited Access")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 0 && state.count < 10), "In Range", "Out of Range")} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + isPremium: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + score: { + type: "number", + asOpaque: true + } + }, + required: ["isPremium", "score"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { isPremium: state.isPremium, score: state.score } }, ({ state }) => state.isPremium || state.score > 100), "Premium Features", "Basic Features")} @@ -95,7 +326,24 @@ export default recipe({

    IfElse Component

    {ifElse(state.isActive,
    User is active with {state.count} items
    ,
    User is inactive
    )} - {ifElse(__ctHelpers.derive({ state: { + {ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => state.count > 5),
    • Many items: {state.count}
    • diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-function-calls.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-function-calls.expected.tsx index 180253fb88..338155aeb1 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-function-calls.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-function-calls.expected.tsx @@ -42,88 +42,517 @@ export default recipe({ return { [UI]: (

      Math Functions

      -

      Max: {__ctHelpers.derive({ state: { +

      Max: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["a", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a, b: state.b } }, ({ state }) => Math.max(state.a, state.b))}

      -

      Min: {__ctHelpers.derive({ state: { +

      Min: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + } + }, + required: ["a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a } }, ({ state }) => Math.min(state.a, 10))}

      -

      Abs: {__ctHelpers.derive({ state: { +

      Abs: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["a", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a, b: state.b } }, ({ state }) => Math.abs(state.a - state.b))}

      -

      Round: {__ctHelpers.derive({ state: { +

      Round: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => Math.round(state.price))}

      -

      Floor: {__ctHelpers.derive({ state: { +

      Floor: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => Math.floor(state.price))}

      -

      Ceiling: {__ctHelpers.derive({ state: { +

      Ceiling: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => Math.ceil(state.price))}

      -

      Square root: {__ctHelpers.derive({ state: { +

      Square root: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + } + }, + required: ["a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a } }, ({ state }) => Math.sqrt(state.a))}

      String Methods as Function Calls

      -

      Uppercase: {__ctHelpers.derive({ state: { +

      Uppercase: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { name: state.name } }, ({ state }) => state.name.toUpperCase())}

      -

      Lowercase: {__ctHelpers.derive({ state: { +

      Lowercase: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { name: state.name } }, ({ state }) => state.name.toLowerCase())}

      -

      Substring: {__ctHelpers.derive({ state: { +

      Substring: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + } + }, + required: ["text"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text } }, ({ state }) => state.text.substring(0, 5))}

      -

      Replace: {__ctHelpers.derive({ state: { +

      Replace: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + } + }, + required: ["text"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text } }, ({ state }) => state.text.replace("old", "new"))}

      -

      Includes: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { +

      Includes: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + } + }, + required: ["text"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text } }, ({ state }) => state.text.includes("test")), "Yes", "No")}

      -

      Starts with: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { +

      Starts with: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { name: state.name } }, ({ state }) => state.name.startsWith("A")), "Yes", "No")}

      Number Methods

      -

      To Fixed: {__ctHelpers.derive({ state: { +

      To Fixed: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => state.price.toFixed(2))}

      -

      To Precision: {__ctHelpers.derive({ state: { +

      To Precision: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + price: { + type: "number", + asOpaque: true + } + }, + required: ["price"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { price: state.price } }, ({ state }) => state.price.toPrecision(4))}

      Parse Functions

      -

      Parse Int: {__ctHelpers.derive({ state: { +

      Parse Int: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + float: { + type: "string", + asOpaque: true + } + }, + required: ["float"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { float: state.float } }, ({ state }) => parseInt(state.float))}

      -

      Parse Float: {__ctHelpers.derive({ state: { +

      Parse Float: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + float: { + type: "string", + asOpaque: true + } + }, + required: ["float"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { float: state.float } }, ({ state }) => parseFloat(state.float))}

      Array Method Calls

      -

      Sum: {__ctHelpers.derive({ state: { +

      Sum: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + values: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["values"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { values: state.values } }, ({ state }) => state.values.reduce((a, b) => a + b, 0))}

      -

      Max value: {__ctHelpers.derive({ state: { +

      Max value: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + values: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["values"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { values: state.values } }, ({ state }) => Math.max(...state.values))}

      -

      Joined: {__ctHelpers.derive({ state: { +

      Joined: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + values: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + } + }, + required: ["values"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { values: state.values } }, ({ state }) => state.values.join(", "))}

      Complex Function Calls

      -

      Multiple args: {__ctHelpers.derive({ state: { +

      Multiple args: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + } + }, + required: ["a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a } }, ({ state }) => Math.pow(state.a, 2))}

      -

      Nested calls: {__ctHelpers.derive({ state: { +

      Nested calls: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + } + }, + required: ["a"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a } }, ({ state }) => Math.round(Math.sqrt(state.a)))}

      -

      Chained calls: {__ctHelpers.derive({ state: { +

      Chained calls: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { name: state.name } }, ({ state }) => state.name.trim().toUpperCase())}

      -

      With expressions: {__ctHelpers.derive({ state: { +

      With expressions: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + a: { + type: "number", + asOpaque: true + }, + b: { + type: "number", + asOpaque: true + } + }, + required: ["a", "b"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { a: state.a, b: state.b } }, ({ state }) => Math.max(state.a + 1, state.b * 2))}

      diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-property-access.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-property-access.expected.tsx index 7aef115c91..15575a9a84 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-property-access.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-property-access.expected.tsx @@ -149,20 +149,95 @@ export default recipe({

      Property Access with Operations

      -

      Age + 1: {__ctHelpers.derive({ state: { +

      Age + 1: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + age: { + type: "number", + asOpaque: true + } + }, + required: ["age"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { age: state.user.age } } }, ({ state }) => state.user.age + 1)}

      Name length: {state.user.name.length}

      -

      Uppercase name: {__ctHelpers.derive({ state: { +

      Uppercase name: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name } } }, ({ state }) => state.user.name.toUpperCase())}

      Location includes city:{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + profile: { + type: "object", + properties: { + location: { + type: "string", + asOpaque: true + } + }, + required: ["location"] + } + }, + required: ["profile"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { profile: { location: state.user.profile.location @@ -172,15 +247,86 @@ export default recipe({

      Array Element Access

      -

      Item at index: {__ctHelpers.derive({ state: { +

      Item at index: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + index: { + type: "number", + asOpaque: true + } + }, + required: ["items", "index"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, index: state.index } }, ({ state }) => state.items[state.index])}

      First item: {state.items[0]}

      -

      Last item: {__ctHelpers.derive({ state: { +

      Last item: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + } + }, + required: ["items"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items } }, ({ state }) => state.items[state.items.length - 1])}

      -

      Number at index: {__ctHelpers.derive({ state: { +

      Number at index: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + numbers: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + index: { + type: "number", + asOpaque: true + } + }, + required: ["numbers", "index"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { numbers: state.numbers, index: state.index } }, ({ state }) => state.numbers[state.index])}

      @@ -188,7 +334,36 @@ export default recipe({

      Config Access with Styles

      Complex Property Chains

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + }, + profile: { + type: "object", + properties: { + location: { + type: "string", + asOpaque: true + } + }, + required: ["location"] + } + }, + required: ["name", "profile"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name, profile: { @@ -214,7 +422,36 @@ export default recipe({ } } } }, ({ state }) => state.user.name + " from " + state.user.profile.location)}

      -

      Font size + 2: {__ctHelpers.derive({ state: { +

      Font size + 2: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + config: { + type: "object", + properties: { + theme: { + type: "object", + properties: { + fontSize: { + type: "number", + asOpaque: true + } + }, + required: ["fontSize"] + } + }, + required: ["theme"] + } + }, + required: ["config"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { config: { theme: { fontSize: state.config.theme.fontSize @@ -223,7 +460,62 @@ export default recipe({ } }, ({ state }) => state.config.theme.fontSize + 2)}px

      Has beta and dark mode:{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + config: { + type: "object", + properties: { + features: { + type: "object", + properties: { + beta: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + darkMode: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["beta", "darkMode"] + } + }, + required: ["features"] + } + }, + required: ["config"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } as const satisfies __ctHelpers.JSONSchema, { state: { config: { features: { beta: state.config.features.beta, diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-string-operations.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-string-operations.expected.tsx index cff6a43e4f..d21f49d130 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-string-operations.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/jsx-string-operations.expected.tsx @@ -31,55 +31,291 @@ export default recipe({ return { [UI]: (

      String Concatenation

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + title: { + type: "string", + asOpaque: true + }, + firstName: { + type: "string", + asOpaque: true + }, + lastName: { + type: "string", + asOpaque: true + } + }, + required: ["title", "firstName", "lastName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { title: state.title, firstName: state.firstName, lastName: state.lastName } }, ({ state }) => state.title + ": " + state.firstName + " " + state.lastName)}

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + }, + lastName: { + type: "string", + asOpaque: true + } + }, + required: ["firstName", "lastName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName, lastName: state.lastName } }, ({ state }) => state.firstName + state.lastName)}

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + } + }, + required: ["firstName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName } }, ({ state }) => "Hello, " + state.firstName + "!")}

      Template Literals

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + } + }, + required: ["firstName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName } }, ({ state }) => `Welcome, ${state.firstName}!`)}

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + }, + lastName: { + type: "string", + asOpaque: true + } + }, + required: ["firstName", "lastName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName, lastName: state.lastName } }, ({ state }) => `Full name: ${state.firstName} ${state.lastName}`)}

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + title: { + type: "string", + asOpaque: true + }, + firstName: { + type: "string", + asOpaque: true + }, + lastName: { + type: "string", + asOpaque: true + } + }, + required: ["title", "firstName", "lastName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { title: state.title, firstName: state.firstName, lastName: state.lastName } }, ({ state }) => `${state.title}: ${state.firstName} ${state.lastName}`)}

      String Methods

      -

      Uppercase: {__ctHelpers.derive({ state: { +

      Uppercase: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + } + }, + required: ["firstName"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName } }, ({ state }) => state.firstName.toUpperCase())}

      -

      Lowercase: {__ctHelpers.derive({ state: { +

      Lowercase: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + title: { + type: "string", + asOpaque: true + } + }, + required: ["title"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { title: state.title } }, ({ state }) => state.title.toLowerCase())}

      Length: {state.message.length}

      -

      Substring: {__ctHelpers.derive({ state: { +

      Substring: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + message: { + type: "string", + asOpaque: true + } + }, + required: ["message"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { message: state.message } }, ({ state }) => state.message.substring(0, 5))}

      Mixed String and Number

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + }, + count: { + type: "number", + asOpaque: true + } + }, + required: ["firstName", "count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName, count: state.count } }, ({ state }) => state.firstName + " has " + state.count + " items")}

      -

      {__ctHelpers.derive({ state: { +

      {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + firstName: { + type: "string", + asOpaque: true + }, + count: { + type: "number", + asOpaque: true + } + }, + required: ["firstName", "count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { firstName: state.firstName, count: state.count } }, ({ state }) => `${state.firstName} has ${state.count} items`)}

      -

      Count as string: {__ctHelpers.derive({ state: { +

      Count as string: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + count: { + type: "number", + asOpaque: true + } + }, + required: ["count"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { count: state.count } }, ({ state }) => "Count: " + state.count)}

      ), 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 index bc912c2fb4..9d9d5fc0a4 100644 --- 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 @@ -4,7 +4,124 @@ export default recipe("MapArrayLengthConditional", (_state) => { const list = cell(["apple", "banana", "cherry"]); return { [UI]: (
      - {__ctHelpers.derive({ list: list }, ({ list }) => list.length > 0 && (
      + {__ctHelpers.derive({ + type: "object", + properties: { + list: { + type: "array", + items: { + type: "string" + }, + asCell: true + } + }, + required: ["list"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false] + }, { + $ref: "#/$defs/Element" + }], + $defs: { + Element: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { list: list }, ({ list }) => list.get().length > 0 && (
      {list.map((name) => ({name}))}
      ))}
      ), @@ -14,3 +131,4 @@ export default recipe("MapArrayLengthConditional", (_state) => { 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 index 107b1ebaea..2e2e228075 100644 --- 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 @@ -7,7 +7,7 @@ export default recipe("MapArrayLengthConditional", (_state) => { return { [UI]: (
      - {list.length > 0 && ( + {list.get().length > 0 && (
      {list.map((name) => ( {name} diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional-no-name.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional-no-name.expected.tsx index 8643933454..c66722b22e 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional-no-name.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-nested-conditional-no-name.expected.tsx @@ -6,11 +6,134 @@ export default recipe(true as const satisfies __ctHelpers.JSONSchema, (_state: a return { [UI]: (
      {__ctHelpers.derive({ + type: "object", + properties: { + showList: { + type: "boolean", + asCell: true + }, + items: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + } + }, + required: ["name"] + }, + asCell: true + } + }, + required: ["showList", "items"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"], + $defs: { + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { showList: showList, items: items }, ({ showList, items }) => showList && (
      {items.map((item) => (
      - {__ctHelpers.derive({ item: { + {__ctHelpers.derive<{ + item: { + name: __ctHelpers.OpaqueCell & string; + }; + }, JSX.Element>({ item: { name: item.name } }, ({ item }) => item.name && {item.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 7fd00c1766..ef22d9c05c 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,11 +6,134 @@ export default recipe("MapNestedConditional", (_state) => { return { [UI]: (
      {__ctHelpers.derive({ + type: "object", + properties: { + showList: { + type: "boolean", + asCell: true + }, + items: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + } + }, + required: ["name"] + }, + asCell: true + } + }, + required: ["showList", "items"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"], + $defs: { + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { showList: showList, items: items }, ({ showList, items }) => showList && (
      {items.map((item) => (
      - {__ctHelpers.derive({ item: { + {__ctHelpers.derive<{ + item: { + name: __ctHelpers.OpaqueCell & string; + }; + }, JSX.Element>({ item: { name: item.name } }, ({ item }) => item.name && {item.name})}
      ))} diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture-no-name.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture-no-name.expected.tsx index 0e06f95b90..5df2d62d55 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture-no-name.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/map-single-capture-no-name.expected.tsx @@ -7,7 +7,133 @@ export default recipe(true as const satisfies __ctHelpers.JSONSchema, (_state: a ]); return { [UI]: (
      - {__ctHelpers.derive({ people: people }, ({ people }) => people.length > 0 && (
        + {__ctHelpers.derive({ + type: "object", + properties: { + people: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string" + }, + name: { + type: "string" + } + }, + required: ["id", "name"] + }, + asCell: true + } + }, + required: ["people"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false] + }, { + $ref: "#/$defs/Element" + }], + $defs: { + Element: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { people: people }, ({ people }) => people.length > 0 && (
          {people.map((person) => (
        • {person.name}
        • ))}
        ))}
      ), 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 62a280ccae..a616089dc7 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 @@ -7,7 +7,133 @@ export default recipe("MapSingleCapture", (_state) => { ]); return { [UI]: (
      - {__ctHelpers.derive({ people: people }, ({ people }) => people.length > 0 && (
        + {__ctHelpers.derive({ + type: "object", + properties: { + people: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string" + }, + name: { + type: "string" + } + }, + required: ["id", "name"] + }, + asCell: true + } + }, + required: ["people"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false] + }, { + $ref: "#/$defs/Element" + }], + $defs: { + Element: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { people: people }, ({ people }) => people.length > 0 && (
          {people.map((person) => (
        • {person.name}
        • ))}
        ))}
      ), diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/method-chains.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/method-chains.expected.tsx index aff148f636..9c6b215170 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/method-chains.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/method-chains.expected.tsx @@ -107,14 +107,52 @@ export default recipe({ [UI]: (

      Chained String Methods

      {/* Simple chain */} -

      Trimmed lower: {__ctHelpers.derive({ state: { +

      Trimmed lower: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + } + }, + required: ["text"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text } }, ({ state }) => state.text.trim().toLowerCase())}

      {/* Chain with reactive argument */}

      Contains search:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + }, + searchTerm: { + type: "string", + asOpaque: true + } + }, + required: ["text", "searchTerm"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text, searchTerm: state.searchTerm } }, ({ state }) => state.text.toLowerCase().includes(state.searchTerm.toLowerCase()))} @@ -123,7 +161,24 @@ export default recipe({ {/* Longer chain */}

      Processed:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + } + }, + required: ["text"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text } }, ({ state }) => state.text.trim().toLowerCase().replace("old", "new").toUpperCase())}

      @@ -132,7 +187,31 @@ export default recipe({ {/* Filter then length */}

      Count above threshold:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + threshold: { + type: "number", + asOpaque: true + } + }, + required: ["items", "threshold"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, threshold: state.threshold } }, ({ state }) => state.items.filter((x) => x > state.threshold).length)} @@ -140,7 +219,35 @@ export default recipe({ {/* Filter then map */}

        - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + threshold: { + type: "number", + asOpaque: true + } + }, + required: ["items", "threshold"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "array", + items: { + type: "number", + asOpaque: true + } + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, threshold: state.threshold } }, ({ state }) => state.items.filter((x) => x > state.threshold)).mapWithPattern(__ctHelpers.recipe({ @@ -169,6 +276,27 @@ export default recipe({ }, required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: x, params: { state } }) => (
      • Value: {__ctHelpers.derive({ + type: "object", + properties: { + x: { + type: "number", + asOpaque: true + }, + state: { + type: "object", + properties: { + factor: { + type: "number", + asOpaque: true + } + }, + required: ["factor"] + } + }, + required: ["x", "state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { x: x, state: { factor: state.factor @@ -183,7 +311,35 @@ export default recipe({ {/* Multiple filters */}

        Double filter count:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + start: { + type: "number", + asOpaque: true + }, + end: { + type: "number", + asOpaque: true + } + }, + required: ["items", "start", "end"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, start: state.start, end: state.end @@ -193,7 +349,35 @@ export default recipe({

        Methods with Reactive Arguments

        {/* Slice with reactive indices */}

        - Sliced items: {__ctHelpers.derive({ state: { + Sliced items: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + start: { + type: "number", + asOpaque: true + }, + end: { + type: "number", + asOpaque: true + } + }, + required: ["items", "start", "end"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, start: state.start, end: state.end @@ -203,7 +387,31 @@ export default recipe({ {/* String methods with reactive args */}

        Starts with:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + names: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + prefix: { + type: "string", + asOpaque: true + } + }, + required: ["names", "prefix"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { names: state.names, prefix: state.prefix } }, ({ state }) => state.names.filter((n) => n.startsWith(state.prefix)).join(", "))} @@ -211,7 +419,32 @@ export default recipe({ {/* Array find with reactive predicate */}

        - First match: {__ctHelpers.derive({ state: { + First match: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + names: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + searchTerm: { + type: "string", + asOpaque: true + } + }, + required: ["names", "searchTerm"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string", + asOpaque: true + } as const satisfies __ctHelpers.JSONSchema, { state: { names: state.names, searchTerm: state.searchTerm } }, ({ state }) => state.names.find((n) => n.includes(state.searchTerm)))} @@ -232,12 +465,47 @@ export default recipe({ } }, required: ["element", "params"] - } as const satisfies __ctHelpers.JSONSchema, ({ element: name, params: {} }) => (

      • {__ctHelpers.derive({ name: name }, ({ name }) => name.trim().toLowerCase().replace(" ", "-"))}
      • )), {})} + } as const satisfies __ctHelpers.JSONSchema, ({ element: name, params: {} }) => (
      • {__ctHelpers.derive({ + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { name: name }, ({ name }) => name.trim().toLowerCase().replace(" ", "-"))}
      • )), {})}
      {/* Reduce with reactive accumulator */}

      - Total with discount: {__ctHelpers.derive({ state: { + Total with discount: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + prices: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + } + }, + required: ["prices", "discount"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { prices: state.prices, discount: state.discount } }, ({ state }) => state.prices.reduce((sum, price) => sum + price * (1 - state.discount), 0))} @@ -246,7 +514,31 @@ export default recipe({ {/* Method result used in computation */}

      Average * factor:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + factor: { + type: "number", + asOpaque: true + } + }, + required: ["items", "factor"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { items: state.items, factor: state.factor } }, ({ state }) => (state.items.reduce((a, b) => a + b, 0) / state.items.length) * @@ -256,7 +548,31 @@ export default recipe({

      Methods on Computed Values

      {/* Method on binary expression result */}

      - Formatted price: {__ctHelpers.derive({ state: { + Formatted price: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + prices: { + type: "array", + items: { + type: "number" + }, + asOpaque: true + }, + discount: { + type: "number", + asOpaque: true + } + }, + required: ["prices", "discount"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { prices: state.prices, discount: state.discount } }, ({ state }) => (state.prices[0] * (1 - state.discount)).toFixed(2))} @@ -265,7 +581,28 @@ export default recipe({ {/* Method on conditional result */}

      Conditional trim:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + }, + prefix: { + type: "string", + asOpaque: true + } + }, + required: ["text", "prefix"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text, prefix: state.prefix } }, ({ state }) => (state.text.length > 10 ? state.text : state.prefix).trim())} @@ -274,7 +611,28 @@ export default recipe({ {/* Method chain on computed value */}

      Complex:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + }, + prefix: { + type: "string", + asOpaque: true + } + }, + required: ["text", "prefix"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text, prefix: state.prefix } }, ({ state }) => (state.text + " " + state.prefix).trim().toLowerCase().split(" ") @@ -285,7 +643,43 @@ export default recipe({ {/* Filter with multiple conditions */}

      Active adults:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + age: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["name", "age", "active"] + }, + asOpaque: true + }, + minAge: { + type: "number", + asOpaque: true + } + }, + required: ["users", "minAge"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { users: state.users, minAge: state.minAge } }, ({ state }) => state.users.filter((u) => u.age >= state.minAge && u.active).length)} @@ -317,9 +711,43 @@ export default recipe({ } }, required: ["element", "params"] - } as const satisfies __ctHelpers.JSONSchema, ({ element: u, params: {} }) => (

    • {__ctHelpers.ifElse(u.active, __ctHelpers.derive({ u: { + } as const satisfies __ctHelpers.JSONSchema, ({ element: u, params: {} }) => (
    • {__ctHelpers.ifElse(u.active, __ctHelpers.derive({ + type: "object", + properties: { + u: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["u"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { u: { name: u.name - } }, ({ u }) => u.name.toUpperCase()), __ctHelpers.derive({ u: { + } }, ({ u }) => u.name.toUpperCase()), __ctHelpers.derive({ + type: "object", + properties: { + u: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["u"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { u: { name: u.name } }, ({ u }) => u.name.toLowerCase()))}
    • )), {})}
    @@ -327,19 +755,108 @@ export default recipe({ {/* Some/every with reactive predicates */}

    Has adults:{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + age: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["name", "age", "active"] + }, + asOpaque: true + }, + minAge: { + type: "number", + asOpaque: true + } + }, + required: ["users", "minAge"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { users: state.users, minAge: state.minAge } }, ({ state }) => state.users.some((u) => u.age >= state.minAge)), "Yes", "No")}

    -

    All active: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { +

    All active: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + age: { + type: "number" + }, + active: { + type: "boolean" + } + }, + required: ["name", "age", "active"] + }, + asOpaque: true + } + }, + required: ["users"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { users: state.users } }, ({ state }) => state.users.every((u) => u.active)), "Yes", "No")}

    Method Calls in Expressions

    {/* Method result in arithmetic */}

    - Length sum: {__ctHelpers.derive({ state: { + Length sum: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + }, + prefix: { + type: "string", + asOpaque: true + } + }, + required: ["text", "prefix"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text, prefix: state.prefix } }, ({ state }) => state.text.trim().length + state.prefix.trim().length)} @@ -347,14 +864,59 @@ export default recipe({ {/* Method result in comparison */}

    - Is long: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + Is long: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + text: { + type: "string", + asOpaque: true + }, + threshold: { + type: "number", + asOpaque: true + } + }, + required: ["text", "threshold"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { text: state.text, threshold: state.threshold } }, ({ state }) => state.text.trim().length > state.threshold), "Yes", "No")}

    {/* Multiple method results combined */} -

    Joined: {__ctHelpers.derive({ state: { +

    Joined: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + words: { + type: "array", + items: { + type: "string" + }, + asOpaque: true + }, + separator: { + type: "string", + asOpaque: true + } + }, + required: ["words", "separator"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { words: state.words, separator: state.separator } }, ({ state }) => state.words.join(state.separator).toUpperCase())}

    @@ -365,3 +927,4 @@ export default recipe({ 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/opaque-ref-cell-map.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx index 3f1c73199d..827f5c3660 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 @@ -100,7 +100,19 @@ export default recipe("Charms Launcher", () => { [NAME]: "Charms Launcher", [UI]: (

    Stored Charms:

    - {ifElse(__ctHelpers.derive({ typedCellRef: typedCellRef }, ({ typedCellRef }) => !typedCellRef?.length),
    No charms created yet
    ,
      + {ifElse(__ctHelpers.derive({ + type: "object", + properties: { + typedCellRef: { + type: "array", + items: true, + asOpaque: true + } + }, + required: ["typedCellRef"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { typedCellRef: typedCellRef }, ({ typedCellRef }) => !typedCellRef?.length),
      No charms created yet
      ,
        {typedCellRef.mapWithPattern(__ctHelpers.recipe({ type: "object", properties: { @@ -116,9 +128,35 @@ export default recipe("Charms Launcher", () => { required: ["element", "params"] } as const satisfies __ctHelpers.JSONSchema, ({ element: charm, index: index, params: {} }) => (
      • - Go to Charm {__ctHelpers.derive({ index: index }, ({ index }) => index + 1)} + Go to Charm {__ctHelpers.derive({ + type: "object", + properties: { + index: { + type: "number" + } + }, + required: ["index"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { index: index }, ({ index }) => index + 1)} - Charm {__ctHelpers.derive({ index: index }, ({ index }) => index + 1)}: {__ctHelpers.derive({ charm: charm }, ({ charm }) => charm[NAME] || "Unnamed")} + Charm {__ctHelpers.derive({ + type: "object", + properties: { + index: { + type: "number" + } + }, + required: ["index"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { index: index }, ({ index }) => index + 1)}: {__ctHelpers.derive({ + type: "object", + properties: { + charm: true + }, + required: ["charm"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { charm: charm }, ({ charm }) => charm[NAME] || "Unnamed")}
      • )), {})}
      )} diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-operations.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-operations.expected.tsx index 6f035f9d00..e59c9cc24e 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-operations.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-operations.expected.tsx @@ -6,9 +6,40 @@ export default recipe("OpaqueRefOperations", (_state) => { return { [UI]: (

      Count: {count}

      -

      Next: {__ctHelpers.derive({ count: count }, ({ count }) => count + 1)}

      -

      Double: {__ctHelpers.derive({ count: count }, ({ count }) => count * 2)}

      -

      Total: {__ctHelpers.derive({ price: price }, ({ price }) => price * 1.1)}

      +

      Next: {__ctHelpers.derive({ + type: "object", + properties: { + count: { + type: "number", + asCell: true + } + }, + required: ["count"] + } as const satisfies __ctHelpers.JSONSchema, true as const satisfies __ctHelpers.JSONSchema, { count: count }, ({ count }) => count + 1)}

      +

      Double: {__ctHelpers.derive({ + type: "object", + properties: { + count: { + type: "number", + asCell: true + } + }, + required: ["count"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { count: count }, ({ count }) => count * 2)}

      +

      Total: {__ctHelpers.derive({ + type: "object", + properties: { + price: { + type: "number", + asCell: true + } + }, + required: ["price"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { price: price }, ({ price }) => price * 1.1)}

      ), }; }); @@ -16,3 +47,4 @@ export default recipe("OpaqueRefOperations", (_state) => { 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/optional-chain-captures.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-captures.expected.tsx index 69d20db138..f51dc328ad 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-captures.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-captures.expected.tsx @@ -79,7 +79,34 @@ export default recipe({ } } } - } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: {} }) => ({__ctHelpers.derive({ item: { + } as const satisfies __ctHelpers.JSONSchema, ({ element: item, params: {} }) => ({__ctHelpers.derive({ + type: "object", + properties: { + item: { + type: "object", + properties: { + maybe: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + } + } + } + } + }, + required: ["item"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "number", + enum: [0] + }, { + type: "number", + asOpaque: true + }] + } as const satisfies __ctHelpers.JSONSchema, { item: { maybe: { value: item.maybe?.value } diff --git a/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-predicate.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-predicate.expected.tsx index 76d3d4fa5d..7cec48e54d 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-predicate.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-predicate.expected.tsx @@ -5,7 +5,124 @@ export default recipe("Optional Chain Predicate", () => { return { [NAME]: "Optional chain predicate", [UI]: (
      - {__ctHelpers.derive({ items: items }, ({ items }) => !items?.length && No items)} + {__ctHelpers.derive({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string" + }, + asCell: true + } + }, + required: ["items"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false] + }, { + $ref: "#/$defs/Element" + }], + $defs: { + Element: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { items: items }, ({ items }) => !items?.length && No items)}
      ), }; }); @@ -13,3 +130,4 @@ export default recipe("Optional Chain Predicate", () => { 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/optional-element-access.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-element-access.expected.tsx index a3bfc2a785..f0fc7bbb00 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/optional-element-access.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/optional-element-access.expected.tsx @@ -5,7 +5,124 @@ export default recipe("Optional Element Access", () => { return { [NAME]: "Optional element access", [UI]: (
      - {__ctHelpers.derive({ list: list }, ({ list }) => !list?.[0] && No first entry)} + {__ctHelpers.derive({ + type: "object", + properties: { + list: { + type: "array", + items: { + type: "string" + }, + asCell: true + } + }, + required: ["list"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false] + }, { + $ref: "#/$defs/Element" + }], + $defs: { + Element: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + VNode: { + type: "object", + properties: { + type: { + type: "string", + enum: ["vnode"] + }, + name: { + type: "string" + }, + props: { + $ref: "#/$defs/Props" + }, + children: { + $ref: "#/$defs/RenderNode" + }, + $UI: { + $ref: "#/$defs/VNode" + } + }, + required: ["type", "name", "props"] + }, + RenderNode: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + $ref: "#/$defs/VNode" + }, { + type: "object", + properties: {} + }, { + type: "array", + items: { + $ref: "#/$defs/RenderNode" + } + }] + }, + Props: { + type: "object", + properties: {}, + additionalProperties: { + anyOf: [{ + type: "string" + }, { + type: "number" + }, { + type: "boolean", + enum: [false] + }, { + type: "boolean", + enum: [true] + }, { + type: "object", + additionalProperties: true + }, { + type: "array", + items: true + }, { + asCell: true + }, { + asStream: true + }, { + type: "null" + }] + } + } + } + } as const satisfies __ctHelpers.JSONSchema, { list: list }, ({ list }) => !list?.[0] && No first entry)}
      ), }; }); @@ -13,3 +130,4 @@ export default recipe("Optional Element Access", () => { 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/parent-suppression-edge.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/parent-suppression-edge.expected.tsx index 0e2387ebd9..ef2db994bd 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/parent-suppression-edge.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/parent-suppression-edge.expected.tsx @@ -327,7 +327,44 @@ export default recipe({ {/* String concatenation with multiple property accesses */}

      Full profile:{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + }, + profile: { + type: "object", + properties: { + location: { + type: "string", + asOpaque: true + }, + bio: { + type: "string", + asOpaque: true + } + }, + required: ["location", "bio"] + } + }, + required: ["name", "profile"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name, profile: { @@ -341,12 +378,58 @@ export default recipe({ {/* Arithmetic with multiple properties from same base */}

      - Age calculation: {__ctHelpers.derive({ state: { + Age calculation: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + age: { + type: "number", + asOpaque: true + } + }, + required: ["age"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { age: state.user.age } } }, ({ state }) => state.user.age * 12)} months, or{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + age: { + type: "number", + asOpaque: true + } + }, + required: ["age"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { age: state.user.age } @@ -415,7 +498,40 @@ export default recipe({

      Complex Expressions with Shared Bases

      {/* Conditional with multiple property accesses */}

      - Status: {__ctHelpers.ifElse(state.user.settings.notifications, __ctHelpers.derive({ state: { + Status: {__ctHelpers.ifElse(state.user.settings.notifications, __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + }, + settings: { + type: "object", + properties: { + theme: { + type: "string", + asOpaque: true + } + }, + required: ["theme"] + } + }, + required: ["name", "settings"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name, settings: { @@ -423,7 +539,30 @@ export default recipe({ } } } }, ({ state }) => state.user.name + " has notifications on with " + - state.user.settings.theme + " theme"), __ctHelpers.derive({ state: { + state.user.settings.theme + " theme"), __ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name } @@ -432,7 +571,50 @@ export default recipe({ {/* Computed expression with shared base */}

      - Spacing calc: {__ctHelpers.derive({ state: { + Spacing calc: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + config: { + type: "object", + properties: { + theme: { + type: "object", + properties: { + spacing: { + type: "object", + properties: { + small: { + type: "number", + asOpaque: true + }, + medium: { + type: "number", + asOpaque: true + }, + large: { + type: "number", + asOpaque: true + } + }, + required: ["small", "medium", "large"] + } + }, + required: ["spacing"] + } + }, + required: ["theme"] + } + }, + required: ["config"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { config: { theme: { spacing: { @@ -450,7 +632,62 @@ export default recipe({ {/* Boolean expressions with multiple properties */}

      Features:{" "} - {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + config: { + type: "object", + properties: { + features: { + type: "object", + properties: { + darkMode: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + }, + animations: { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } + }, + required: ["darkMode", "animations"] + } + }, + required: ["features"] + } + }, + required: ["config"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + anyOf: [{ + type: "boolean", + enum: [false], + asOpaque: true + }, { + type: "boolean", + enum: [true], + asOpaque: true + }] + } as const satisfies __ctHelpers.JSONSchema, { state: { config: { features: { darkMode: state.config.features.darkMode, @@ -463,12 +700,58 @@ export default recipe({

      Method Calls on Shared Bases

      {/* Multiple method calls on properties from same base */}

      - Formatted: {__ctHelpers.derive({ state: { + Formatted: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { + type: "string", + asOpaque: true + } + }, + required: ["name"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { name: state.user.name } } }, ({ state }) => state.user.name.toUpperCase())} -{" "} - {__ctHelpers.derive({ state: { + {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + user: { + type: "object", + properties: { + email: { + type: "string", + asOpaque: true + } + }, + required: ["email"] + } + }, + required: ["user"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "string" + } as const satisfies __ctHelpers.JSONSchema, { state: { user: { email: state.user.email } @@ -515,3 +798,4 @@ export default recipe({ 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/pattern-statements-vs-jsx.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-statements-vs-jsx.expected.tsx index 12fcf6de18..65cc7d7033 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-statements-vs-jsx.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-statements-vs-jsx.expected.tsx @@ -50,19 +50,87 @@ export default pattern((state) => { {/* These SHOULD be transformed (JSX expression context) */} Current: {state.value}
      - Next number: {__ctHelpers.derive({ state: { + Next number: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 1)}
      - Previous: {__ctHelpers.derive({ state: { + Previous: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value - 1)}
      - Doubled: {__ctHelpers.derive({ state: { + Doubled: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value * 2)}
      - Status: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + Status: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value > 10), "High", "Low")}

      @@ -220,3 +288,4 @@ export default pattern((state) => { 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/pattern-with-cells.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-with-cells.expected.tsx index 91c1f6d906..4908ec75c5 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-with-cells.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/pattern-with-cells.expected.tsx @@ -4,10 +4,44 @@ export default pattern((cell) => { return { [UI]: (

      Current value: {cell.value}

      -

      Next value: {__ctHelpers.derive({ cell: { +

      Next value: {__ctHelpers.derive({ + type: "object", + properties: { + cell: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["cell"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { cell: { value: cell.value } }, ({ cell }) => cell.value + 1)}

      -

      Double: {__ctHelpers.derive({ cell: { +

      Double: {__ctHelpers.derive({ + type: "object", + properties: { + cell: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["cell"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { cell: { value: cell.value } }, ({ cell }) => cell.value * 2)}

      ), @@ -137,3 +171,4 @@ export default pattern((cell) => { 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/recipe-statements-vs-jsx.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-statements-vs-jsx.expected.tsx index 72c61710c5..c394723d17 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-statements-vs-jsx.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-statements-vs-jsx.expected.tsx @@ -58,19 +58,87 @@ export default recipe({ {/* These SHOULD be transformed (JSX expression context) */} Current: {state.value}
      - Next number: {__ctHelpers.derive({ state: { + Next number: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value + 1)}
      - Previous: {__ctHelpers.derive({ state: { + Previous: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value - 1)}
      - Doubled: {__ctHelpers.derive({ state: { + Doubled: {__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value * 2)}
      - Status: {__ctHelpers.ifElse(__ctHelpers.derive({ state: { + Status: {__ctHelpers.ifElse(__ctHelpers.derive({ + type: "object", + properties: { + state: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["state"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "boolean" + } as const satisfies __ctHelpers.JSONSchema, { state: { value: state.value } }, ({ state }) => state.value > 10), "High", "Low")}

      @@ -90,3 +158,4 @@ export default recipe({ 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/recipe-with-cells.expected.tsx b/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-with-cells.expected.tsx index 54fdae1c14..c0db400323 100644 --- a/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-with-cells.expected.tsx +++ b/packages/ts-transformers/test/fixtures/jsx-expressions/recipe-with-cells.expected.tsx @@ -12,10 +12,44 @@ export default recipe({ return { [UI]: (

      Current value: {cell.value}

      -

      Next value: {__ctHelpers.derive({ cell: { +

      Next value: {__ctHelpers.derive({ + type: "object", + properties: { + cell: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["cell"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { cell: { value: cell.value } }, ({ cell }) => cell.value + 1)}

      -

      Double: {__ctHelpers.derive({ cell: { +

      Double: {__ctHelpers.derive({ + type: "object", + properties: { + cell: { + type: "object", + properties: { + value: { + type: "number", + asOpaque: true + } + }, + required: ["value"] + } + }, + required: ["cell"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { cell: { value: cell.value } }, ({ cell }) => cell.value * 2)}

      ), @@ -26,3 +60,4 @@ export default recipe({ 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/opaque-ref/map-callbacks.test.ts b/packages/ts-transformers/test/opaque-ref/map-callbacks.test.ts index 7dda4bc0e7..8a62ec8e94 100644 --- a/packages/ts-transformers/test/opaque-ref/map-callbacks.test.ts +++ b/packages/ts-transformers/test/opaque-ref/map-callbacks.test.ts @@ -61,7 +61,17 @@ describe("OpaqueRef map callbacks", () => { // Index parameter still gets derive wrapping for the arithmetic operation assertStringIncludes( output, - "__ctHelpers.derive({ index: index }, ({ index }) => index + 1)", + `__ctHelpers.derive({ + type: "object", + properties: { + index: { + type: "number" + } + }, + required: ["index"] + } as const satisfies __ctHelpers.JSONSchema, { + type: "number" + } as const satisfies __ctHelpers.JSONSchema, { index: index }, ({ index }) => index + 1)`, ); // element[NAME] uses NAME from module scope (import), defaultName from params assertStringIncludes( From 88bfdaf4a60f326f8a155deb25f0552a5a57c738 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Fri, 14 Nov 2025 11:00:18 -0800 Subject: [PATCH 2/4] chore: Remove redundant workspace imports (#2077) --- deno.json | 3 +-- packages/schema-generator/deno.json | 8 +++----- packages/ts-transformers/deno.json | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/deno.json b/deno.json index 47d0d40612..1343ae1c06 100644 --- a/deno.json +++ b/deno.json @@ -116,7 +116,6 @@ "merkle-reference": "npm:merkle-reference@^2.2.0", "multiformats": "npm:multiformats@^13.3.2", "turndown": "npm:turndown@^7.1.2", - "zod": "npm:zod@^3.24.1", - "@commontools/schema-generator/cell-brand": "./packages/schema-generator/src/typescript/cell-brand.ts" + "zod": "npm:zod@^3.24.1" } } diff --git a/packages/schema-generator/deno.json b/packages/schema-generator/deno.json index 25423f8324..8dc8163da2 100644 --- a/packages/schema-generator/deno.json +++ b/packages/schema-generator/deno.json @@ -4,13 +4,11 @@ "exports": { ".": "./src/index.ts", "./interface": "./src/interface.ts", - "./typescript/cell-brand": "./src/typescript/cell-brand.ts", - "./typescript/type-traversal": "./src/typescript/type-traversal.ts" + "./cell-brand": "./src/typescript/cell-brand.ts", + "./type-traversal": "./src/typescript/type-traversal.ts" }, "imports": { - "typescript": "npm:typescript", - "@commontools/utils": "../utils/src/index.ts", - "@commontools/static": "../static/index.ts" + "typescript": "npm:typescript" }, "tasks": { "test": "deno test --allow-read --allow-write --allow-run --allow-env=API_URL,\"TSC_*\",NODE_INSPECTOR_IPC,VSCODE_INSPECTOR_OPTIONS,NODE_ENV,UPDATE_GOLDENS test/**/*.test.ts", diff --git a/packages/ts-transformers/deno.json b/packages/ts-transformers/deno.json index 18e0d0eaa7..3b70079f45 100644 --- a/packages/ts-transformers/deno.json +++ b/packages/ts-transformers/deno.json @@ -7,8 +7,7 @@ "./core/imports": "./src/core/imports.ts" }, "imports": { - "typescript": "npm:typescript", - "@commontools/schema-generator/cell-brand": "../schema-generator/src/typescript/cell-brand.ts" + "typescript": "npm:typescript" }, "tasks": { "test": "deno test --allow-read --allow-write --allow-env test/**/*.test.ts", From a34b8e83533e45f8ce00299fe4cf295805eacd5f Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 14 Nov 2025 11:33:51 -0800 Subject: [PATCH 3/4] chore(examples): Correctly tag inputs as Cell (#2078) `pattern`, `recipe`, `lift` and `handler` now consistently produce factories that strip any Cell annotations, so that the type the developer declares matches the internal view while external callers can call with or without Cells. --- Also fixed TypeScript types in patterns by: 1. Adding Cell<> wrappers to input schema properties where UI components expect CellLike values: - recipes: bgCounter, email-summarizer, gcal, gmail-importer, input, research-report, rss - patterns: chatbot, ct-checkbox-*, ct-list, ct-select, fetch-data, note 2. Removing extraneous asCell: true from simpleValue.tsx that was causing .length property errors 3. Making recipe input properties optional where they have Default<> values and adding fallback handling in recipe implementations 4. Fixing Cell import from type-only to regular import in note.tsx --- .../specs/recipe-construction/rollout-plan.md | 6 +-- packages/api/index.ts | 49 ++++++++++--------- packages/html/test/html-recipes.test.ts | 3 +- packages/patterns/chatbot-list-view.tsx | 6 +-- packages/patterns/chatbot-note-composed.tsx | 8 +-- packages/patterns/chatbot-outliner.tsx | 8 +-- packages/patterns/chatbot.tsx | 4 +- packages/patterns/ct-checkbox-cell.tsx | 4 +- packages/patterns/ct-checkbox-handler.tsx | 4 +- packages/patterns/ct-list.tsx | 6 +-- packages/patterns/ct-select.tsx | 8 +-- packages/patterns/fetch-data.tsx | 3 +- packages/patterns/note.tsx | 6 +-- packages/patterns/omnibox-fab.tsx | 1 - .../integration/iframe-counter-recipe.tsx | 6 ++- recipes/bgCounter.tsx | 2 +- recipes/email-summarizer.tsx | 34 +++++++------ recipes/gcal.tsx | 6 ++- recipes/gmail-importer.tsx | 10 ++-- recipes/input.tsx | 1 + recipes/research-report.tsx | 5 +- recipes/rss.tsx | 5 +- recipes/simpleValue.tsx | 2 +- 23 files changed, 98 insertions(+), 89 deletions(-) diff --git a/docs/specs/recipe-construction/rollout-plan.md b/docs/specs/recipe-construction/rollout-plan.md index 80fb5fbcd7..e7600259d2 100644 --- a/docs/specs/recipe-construction/rollout-plan.md +++ b/docs/specs/recipe-construction/rollout-plan.md @@ -2,7 +2,7 @@ - [x] Disable ShadowRef/unsafe_ and see what breaks, ideally remove it (will merge later as it'll break a few patterns) -- [ ] Update Cell API types to already unify them +- [x] Update Cell API types to already unify them - [x] Create an `BrandedCell<>` type with a symbol based brand, with the value be `string` - [x] Factor out parts of the cell interfaces along reading, writing, .send @@ -19,9 +19,9 @@ - [ ] Simplify most wrap/unwrap types to use `CellLike`. We need - [x] "Accept any T where any sub part of T can be wrapped in one or more `BrandedCell`" (for inputs to node factories) - - [ ] "Strip any `BrandedCell` from T and then wrap it in OpaqueRef<>" (for + - [x] "Strip any `BrandedCell` 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 + - [x] 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. - [x] Merge StreamCell into RegularCell and rename RegularCell to CellImpl diff --git a/packages/api/index.ts b/packages/api/index.ts index ae7c62ab6e..8953979068 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -515,6 +515,15 @@ type MaybeCellWrapped = : never); export declare const CELL_LIKE: unique symbol; +/** + * Helper type to transform Cell to Opaque in pattern/lift/handler inputs + */ +export type StripCell = T extends AnyBrandedCell ? StripCell + : T extends ArrayBuffer | ArrayBufferView | URL | Date ? T + : T extends Array ? StripCell[] + : T extends object ? { [K in keyof T]: StripCell } + : T; + /** * Opaque accepts T or any cell wrapping T, recursively at any nesting level. * Used in APIs that accept inputs from developers - can be static values @@ -969,11 +978,11 @@ export interface BuiltInCompileAndRunState { export type PatternFunction = { ( fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( fn: (input: OpaqueRef>) => unknown, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( fn: ( @@ -981,7 +990,7 @@ export type PatternFunction = { ) => Opaque>, argumentSchema: IS, resultSchema: OS, - ): RecipeFactory, Schema>; + ): RecipeFactory, SchemaWithoutCell>; }; /** @deprecated Use pattern() instead */ @@ -989,21 +998,21 @@ export type RecipeFunction = { // Function-only overload ( fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( fn: (input: OpaqueRef>) => any, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( argumentSchema: S, fn: (input: OpaqueRef>>) => any, - ): RecipeFactory, ReturnType>; + ): RecipeFactory, StripCell>>; ( argumentSchema: S, fn: (input: OpaqueRef>>) => Opaque, - ): RecipeFactory, R>; + ): RecipeFactory, StripCell>; ( argumentSchema: S, @@ -1016,18 +1025,18 @@ export type RecipeFunction = { ( argumentSchema: string | JSONSchema, fn: (input: OpaqueRef>) => any, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( argumentSchema: string | JSONSchema, fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( argumentSchema: string | JSONSchema, resultSchema: JSONSchema, fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; }; export type PatternToolFunction = < @@ -1047,21 +1056,21 @@ export type LiftFunction = { ( implementation: (input: T) => R, - ): ModuleFactory; + ): ModuleFactory, StripCell>; ( implementation: (input: T) => any, - ): ModuleFactory>; + ): ModuleFactory, StripCell>>; any>( implementation: T, - ): ModuleFactory[0], ReturnType>; + ): ModuleFactory[0]>, StripCell>>; ( argumentSchema?: JSONSchema, resultSchema?: JSONSchema, implementation?: (input: T) => R, - ): ModuleFactory; + ): ModuleFactory, StripCell>; }; // Helper type to make non-Cell and non-Stream properties readonly in handler state @@ -1085,17 +1094,17 @@ export type HandlerFunction = { eventSchema: JSONSchema, stateSchema: JSONSchema, handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; // Without schemas ( handler: (event: E, props: T) => any, options: { proxy: true }, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; ( handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; }; /** @@ -1308,12 +1317,6 @@ export type Mutable = T extends ReadonlyArray ? Mutable[] : T extends object ? ({ -readonly [P in keyof T]: Mutable }) : T; -// Helper type to transform Cell to Opaque in handler inputs -export type StripCell = T extends Cell ? StripCell - : T extends Array ? StripCell[] - : T extends object ? { [K in keyof T]: StripCell } - : T; - export type WishKey = `/${string}` | `#${string}`; // ===== JSON Pointer Path Resolution Utilities ===== diff --git a/packages/html/test/html-recipes.test.ts b/packages/html/test/html-recipes.test.ts index 5bfbc71cd3..0b33389ac8 100644 --- a/packages/html/test/html-recipes.test.ts +++ b/packages/html/test/html-recipes.test.ts @@ -5,7 +5,6 @@ import { type Cell, createBuilder, type IExtendedStorageTransaction, - type OpaqueRef, Runtime, } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; @@ -215,7 +214,7 @@ describe("recipes with HTML", () => { h( "ul", null, - entries(row).map((input: OpaqueRef<[string, unknown]>) => + entries(row).map((input) => h("li", null, [input[0], ": ", str`${input[1]}`]) ) as VNode[], ) diff --git a/packages/patterns/chatbot-list-view.tsx b/packages/patterns/chatbot-list-view.tsx index 05beea12bc..59429e5a89 100644 --- a/packages/patterns/chatbot-list-view.tsx +++ b/packages/patterns/chatbot-list-view.tsx @@ -30,9 +30,9 @@ type Input = { selectedCharm: Default<{ charm: any }, { charm: undefined }>; charmsList: Default; theme?: { - accentColor: Default; - fontFace: Default; - borderRadius: Default; + accentColor: Cell>; + fontFace: Cell>; + borderRadius: Cell>; }; }; diff --git a/packages/patterns/chatbot-note-composed.tsx b/packages/patterns/chatbot-note-composed.tsx index 79745f3b16..cf8a9c520f 100644 --- a/packages/patterns/chatbot-note-composed.tsx +++ b/packages/patterns/chatbot-note-composed.tsx @@ -31,8 +31,8 @@ function schemaifyWish(path: string, def: T) { } type ChatbotNoteInput = { - title: Default; - messages: Default, []>; + title?: Cell>; + messages?: Cell, []>>; }; type ChatbotNoteResult = { @@ -57,11 +57,11 @@ const newNote = handler< try { const n = Note({ title: args.title, - content: args.content || "", + content: args.content ?? "", }); args.result.set( - `Created note ${args.title}!`, + `Created note ${args.title}`, ); // TODO(bf): we have to navigate here until DX1 lands diff --git a/packages/patterns/chatbot-outliner.tsx b/packages/patterns/chatbot-outliner.tsx index 32e03559e7..3181f2fe44 100644 --- a/packages/patterns/chatbot-outliner.tsx +++ b/packages/patterns/chatbot-outliner.tsx @@ -79,10 +79,10 @@ export const Page = recipe( ); type LLMTestInput = { - title: Default; - messages: Default, []>; - expandChat: Default; - outline: Default< + title?: Cell>; + messages?: Cell, []>>; + expandChat?: Cell>; + outline?: Default< Outliner, { root: { body: "Untitled Page"; children: []; attachments: [] } } >; diff --git a/packages/patterns/chatbot.tsx b/packages/patterns/chatbot.tsx index 74f1818e65..ac6c6efbc3 100644 --- a/packages/patterns/chatbot.tsx +++ b/packages/patterns/chatbot.tsx @@ -118,8 +118,8 @@ const clearChat = handler( ); type ChatInput = { - messages: Default, []>; - tools: any; + messages?: Cell, []>>; + tools?: any; theme?: any; system?: string; }; diff --git a/packages/patterns/ct-checkbox-cell.tsx b/packages/patterns/ct-checkbox-cell.tsx index 5928170e14..09ec717187 100644 --- a/packages/patterns/ct-checkbox-cell.tsx +++ b/packages/patterns/ct-checkbox-cell.tsx @@ -2,8 +2,8 @@ import { Cell, Default, handler, ifElse, NAME, recipe, UI } from "commontools"; interface CheckboxDemoInput { - simpleEnabled: Default; - trackedEnabled: Default; + simpleEnabled: Cell>; + trackedEnabled: Cell>; } interface CheckboxDemoOutput extends CheckboxDemoInput {} diff --git a/packages/patterns/ct-checkbox-handler.tsx b/packages/patterns/ct-checkbox-handler.tsx index de74a0b6ef..29c2e3a95d 100644 --- a/packages/patterns/ct-checkbox-handler.tsx +++ b/packages/patterns/ct-checkbox-handler.tsx @@ -1,8 +1,8 @@ /// -import { Default, ifElse, NAME, recipe, UI } from "commontools"; +import { Cell, Default, ifElse, NAME, recipe, UI } from "commontools"; interface CheckboxSimpleInput { - enabled: Default; + enabled: Cell>; } interface CheckboxSimpleOutput extends CheckboxSimpleInput {} diff --git a/packages/patterns/ct-list.tsx b/packages/patterns/ct-list.tsx index 9a8470b906..5f7a8ff526 100644 --- a/packages/patterns/ct-list.tsx +++ b/packages/patterns/ct-list.tsx @@ -1,13 +1,13 @@ /// -import { Default, NAME, recipe, UI } from "commontools"; +import { Cell, Default, NAME, recipe, UI } from "commontools"; interface Item { title: string; } interface ListInput { - title: Default; - items: Default; + title: Cell>; + items: Cell>; } interface ListOutput extends ListInput {} diff --git a/packages/patterns/ct-select.tsx b/packages/patterns/ct-select.tsx index bba324f854..6e2bc7df19 100644 --- a/packages/patterns/ct-select.tsx +++ b/packages/patterns/ct-select.tsx @@ -1,11 +1,11 @@ /// -import { Default, NAME, recipe, UI } from "commontools"; +import { Cell, Default, NAME, recipe, UI } from "commontools"; type Input = { - selected: Default; - numericChoice: Default; - category: Default; + selected: Cell>; + numericChoice: Cell>; + category: Cell>; }; type Result = { diff --git a/packages/patterns/fetch-data.tsx b/packages/patterns/fetch-data.tsx index 4750a95314..cced11c923 100644 --- a/packages/patterns/fetch-data.tsx +++ b/packages/patterns/fetch-data.tsx @@ -1,5 +1,6 @@ /// import { + Cell, Default, derive, fetchData, @@ -151,7 +152,7 @@ function parseUrl(url: string): { org: string; user: string } { } export default recipe< - { repoUrl: Default } + { repoUrl: Cell> } >( "Github Fetcher Demo", (state) => { diff --git a/packages/patterns/note.tsx b/packages/patterns/note.tsx index 657969e918..c5c16fc884 100644 --- a/packages/patterns/note.tsx +++ b/packages/patterns/note.tsx @@ -1,6 +1,6 @@ /// import { - type Cell, + Cell, cell, type Default, derive, @@ -17,8 +17,8 @@ import { } from "commontools"; import { type MentionableCharm } from "./backlinks-index.tsx"; type Input = { - title: Default; - content: Default; + title?: Cell>; + content?: Cell>; }; type Output = { diff --git a/packages/patterns/omnibox-fab.tsx b/packages/patterns/omnibox-fab.tsx index 914e94c2ef..ea85c92113 100644 --- a/packages/patterns/omnibox-fab.tsx +++ b/packages/patterns/omnibox-fab.tsx @@ -63,7 +63,6 @@ export default recipe( const omnibot = Chatbot({ system: "You are a polite but efficient assistant. Think Star Trek computer - helpful and professional without unnecessary conversation. Let your actions speak for themselves.\n\nTool usage priority:\n- Search this space first: listMentionable → addAttachment to access items\n- Search externally only when clearly needed: searchWeb for current events, external information, or when nothing relevant exists in the space\n\nBe matter-of-fact. Prefer action to explanation.", - messages: [], tools: { searchWeb: { pattern: searchWeb, diff --git a/packages/shell/integration/iframe-counter-recipe.tsx b/packages/shell/integration/iframe-counter-recipe.tsx index b7eb0ac1d1..3e3f23dc77 100644 --- a/packages/shell/integration/iframe-counter-recipe.tsx +++ b/packages/shell/integration/iframe-counter-recipe.tsx @@ -1,5 +1,5 @@ /// -import { type JSONSchema, NAME, recipe, UI } from "commontools"; +import { CellLike, type JSONSchema, NAME, recipe, UI } from "commontools"; type IFrameRecipe = { src: string; @@ -58,7 +58,9 @@ const runIframeRecipe = ( ) => recipe(argumentSchema, resultSchema, (data) => ({ [NAME]: name, - [UI]: , + [UI]: ( + }> + ), count: data.count, })); diff --git a/recipes/bgCounter.tsx b/recipes/bgCounter.tsx index c4d09f468c..2750b59796 100644 --- a/recipes/bgCounter.tsx +++ b/recipes/bgCounter.tsx @@ -33,7 +33,7 @@ const updateError = handler< ); export default recipe< - { error: Default; counter: Default } + { error: Cell>; counter: Cell> } >( "bgCounter", ({ counter, error }) => { diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx index 04f96ef4ab..80c0ca401a 100644 --- a/recipes/email-summarizer.tsx +++ b/recipes/email-summarizer.tsx @@ -110,11 +110,13 @@ const EmailSummarizerInputSchema = { enum: ["short", "medium", "long"], default: "medium", description: "Length of the summary", + asCell: true, }, includeTags: { type: "boolean", default: true, description: "Include tags in the summary", + asCell: true, }, }, required: ["summaryLength", "includeTags"], @@ -166,7 +168,7 @@ const updateSummaryLength = handler( properties: { summaryLength: { type: "string", - asCell: true, // Mark as cell + asCell: true, }, }, required: ["summaryLength"], @@ -198,7 +200,7 @@ const updateIncludeTags = handler( properties: { includeTags: { type: "boolean", - asCell: true, // Mark as cell + asCell: true, }, }, required: ["includeTags"], @@ -258,19 +260,23 @@ export default recipe( // Create prompts using the str template literal for proper reactivity // This ensures the prompts update when settings change - const lengthInstructions = str`${ - settings.summaryLength === "short" - ? "in 1-2 sentences" - : settings.summaryLength === "long" - ? "in 5-7 sentences" - : "in 3-4 sentences" - }`; + const lengthInstructions = derive( + settings.summaryLength, + (length: "short" | "medium" | "long") => + length === "short" + ? "in 1-2 sentences" + : length === "long" + ? "in 5-7 sentences" + : "in 3-4 sentences", + ); - const tagInstructions = str`${ - settings.includeTags - ? "Include up to 3 relevant tags or keywords in the format #tag at the end of the summary." - : "" - }`; + const tagInstructions = derive( + settings.includeTags, + (includeTags: boolean) => + includeTags + ? "Include up to 3 relevant tags or keywords in the format #tag at the end of the summary." + : "", + ); // Create system prompt with str to maintain reactivity const systemPrompt = str` diff --git a/recipes/gcal.tsx b/recipes/gcal.tsx index ca7bf94f81..0252604a2b 100644 --- a/recipes/gcal.tsx +++ b/recipes/gcal.tsx @@ -105,11 +105,13 @@ const GcalImporterInputs = { type: "string", description: "Calendar ID to fetch events from", default: "primary", + asCell: true, }, limit: { type: "number", description: "number of events to import", default: 250, + asCell: true, }, syncToken: { type: "string", @@ -222,8 +224,8 @@ const calendarUpdater = handler( const settings = state.settings.get(); const result = await fetchCalendar( state.auth, - settings.limit, - settings.calendarId, + settings.limit.get(), + settings.calendarId.get(), settings.syncToken, state, ); diff --git a/recipes/gmail-importer.tsx b/recipes/gmail-importer.tsx index ca5d2ef044..6188f891ea 100644 --- a/recipes/gmail-importer.tsx +++ b/recipes/gmail-importer.tsx @@ -77,9 +77,9 @@ type Email = { type Settings = { // Gmail filter query to use for fetching emails - gmailFilterQuery: Default; + gmailFilterQuery: Cell>; // Maximum number of emails to fetch - limit: Default; + limit: Cell>; // Gmail history ID for incremental sync historyId: Default; }; @@ -809,11 +809,7 @@ const updateGmailFilterQuery = handler< ); export default recipe<{ - settings: Default; + settings: Settings; auth: Auth; }>( "gmail-importer", diff --git a/recipes/input.tsx b/recipes/input.tsx index 9ba714ba7c..ed4529dc2c 100644 --- a/recipes/input.tsx +++ b/recipes/input.tsx @@ -7,6 +7,7 @@ const InputSchema = { content: { type: "string", default: "", + asCell: true, }, }, required: ["content"], diff --git a/recipes/research-report.tsx b/recipes/research-report.tsx index 71beebedfe..f489613925 100644 --- a/recipes/research-report.tsx +++ b/recipes/research-report.tsx @@ -6,13 +6,14 @@ const InputSchema = { title: { type: "string", default: "Untitled Research Report", + asCell: true, }, content: { type: "string", default: "", + asCell: true, }, }, - required: ["title", "content"], } as const satisfies JSONSchema; const OutputSchema = InputSchema; @@ -22,7 +23,7 @@ export default recipe( OutputSchema, ({ title, content }) => { return { - [NAME]: title || "Untitled Research Report", + [NAME]: title, [UI]: (
      diff --git a/recipes/rss.tsx b/recipes/rss.tsx index eb5159ca40..21094ac714 100644 --- a/recipes/rss.tsx +++ b/recipes/rss.tsx @@ -14,7 +14,7 @@ import { import { type FeedItem, parseRSSFeed } from "./rss-utils.ts"; interface Settings { - feedUrl: Default; + feedUrl: Cell>; limit: Default; } @@ -43,7 +43,7 @@ const feedUpdater = handler } + { settings: Settings } >( "rss importer", ({ settings }) => { @@ -76,7 +76,6 @@ export default recipe< placeholder="https://example.com/feed.xml or https://example.com/atom.xml" />
      -
      diff --git a/recipes/simpleValue.tsx b/recipes/simpleValue.tsx index 437cfc4cc5..eb90cecbaa 100644 --- a/recipes/simpleValue.tsx +++ b/recipes/simpleValue.tsx @@ -50,7 +50,7 @@ const updaterSchema = { const inputSchema = schema({ type: "object", properties: { - values: { type: "array", items: { type: "string" }, asCell: true }, + values: { type: "array", items: { type: "string" } }, }, default: { values: [] }, }); From fa83089c362c438e7af4fe7fea0c804cffa215d6 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 14 Nov 2025 11:46:31 -0800 Subject: [PATCH 4/4] fix(api): handler returns OpaqueRef> to be compatible with explicit types of stream (#2076) --- packages/api/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/api/index.ts b/packages/api/index.ts index 8953979068..5532885d97 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -1087,24 +1087,27 @@ export type HandlerFunction = { eventSchema: E, stateSchema: T, handler: (event: Schema, props: Schema) => any, - ): ModuleFactory>, SchemaWithoutCell>; + ): ModuleFactory< + StripCell>, + Stream> + >; // With inferred types ( eventSchema: JSONSchema, stateSchema: JSONSchema, handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, StripCell>; + ): ModuleFactory, Stream>>; // Without schemas ( handler: (event: E, props: T) => any, options: { proxy: true }, - ): ModuleFactory, StripCell>; + ): ModuleFactory, Stream>>; ( handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, StripCell>; + ): ModuleFactory, Stream>>; }; /**