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/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..5532885d97 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 @@ -1078,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, E>; + ): ModuleFactory, Stream>>; // Without schemas ( handler: (event: E, props: T) => any, options: { proxy: true }, - ): ModuleFactory, E>; + ): ModuleFactory, Stream>>; ( handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, E>; + ): ModuleFactory, Stream>>; }; /** @@ -1308,12 +1320,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/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/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/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/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", 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( 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: [] }, });