Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/specs/recipe-construction/rollout-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
behavior, i.e. each key is an `OpaqueRef` again. That's just for now, until
the AST does a .key transformation under the hood.
- [x] Update `CellLike` to be based on `BrandedCell` but allow nesting.
- [ ] `Opaque<T>` accepts `T` or any `CellLike<T>` at any nesting level
- [x] `Opaque<T>` accepts `T` or any `CellLike<T>` at any nesting level
- [ ] Simplify most wrap/unwrap types to use `CellLike`. We need
- [ ] "Accept any T where any sub part of T can be wrapped in one or more
- [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
outputs of node factories, where T is the output of the inner function)
Expand Down
20 changes: 16 additions & 4 deletions packages/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,17 @@ type MaybeCellWrapped<T> =
*/
export type Opaque<T> =
| T
// We have to list them explicitly so Typescript can unwrap them. Doesn't seem
// to work if we just say BrandedCell<T>
| OpaqueRef<T>
| AnyCell<T>
| BrandedCell<T>
| OpaqueCell<T>
| Cell<T>
| Stream<T>
| ComparableCell<T>
| ReadonlyCell<T>
| WriteonlyCell<T>
| (T extends Array<infer U> ? Array<Opaque<U>>
: T extends object ? { [K in keyof T]: Opaque<T[K]> }
: T);
Expand Down Expand Up @@ -1273,14 +1283,16 @@ export type Props = {

/** A child in a view can be one of a few things */
export type RenderNode =
| InnerRenderNode
| BrandedCell<InnerRenderNode>
| Array<RenderNode>;

type InnerRenderNode =
| VNode
| string
| number
| boolean
| Cell<RenderNode>
| undefined
| Opaque<any>
| RenderNode[];
| undefined;

/** A "virtual view node", e.g. a virtual DOM element */
export type VNode = {
Expand Down
10 changes: 5 additions & 5 deletions packages/html/test/html-recipes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type Cell,
createBuilder,
type IExtendedStorageTransaction,
Opaque,
type OpaqueRef,
Runtime,
} from "@commontools/runner";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
Expand Down Expand Up @@ -98,7 +98,7 @@ describe("recipes with HTML", () => {
h(
"ul",
null,
items.map((item: Opaque<Item>, i: Opaque<number>) =>
items.map((item, i: number) =>
h("li", { key: i.toString() }, item.title)
) as VNode[],
),
Expand Down Expand Up @@ -213,12 +213,12 @@ describe("recipes with HTML", () => {
[UI]: h(
"div",
null,
data.map((row: Opaque<Record<string, unknown>>) =>
data.map((row: Record<string, unknown>) =>
h(
"ul",
null,
entries(row).map((input: Opaque<[string, unknown]>) =>
h("li", null, [input[0] as string, ": ", str`${input[1]}`])
entries(row).map((input: OpaqueRef<[string, unknown]>) =>
h("li", null, [input[0], ": ", str`${input[1]}`])
) as VNode[],
)
) as VNode[],
Expand Down
1 change: 1 addition & 0 deletions packages/runner/src/builder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const schema: typeof schemaFunction = (schema) => schema;
export { AuthSchema } from "./schema-lib.ts";
export type {
AnyCell,
AnyCellWrapping,
Cell,
CreateCellFunction,
Handler,
Expand Down
7 changes: 4 additions & 3 deletions packages/runner/test/schema-to-ts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "@commontools/utils/equal-ignoring-symbols";
import { handler, lift } from "../src/builder/module.ts";
import { str } from "../src/builder/built-in.ts";
import {
type AnyCellWrapping,
type Frame,
type JSONSchema,
type OpaqueRef,
Expand Down Expand Up @@ -760,7 +761,7 @@ describe("Schema-to-TS Type Conversion", () => {

// Type aliases to verify the schema inference
type ExpectedInput = {
name: string;
name: Cell<string>;
count?: number;
options?: {
enabled: boolean;
Expand Down Expand Up @@ -801,11 +802,11 @@ describe("Schema-to-TS Type Conversion", () => {
type InferredInput = Parameters<typeof processRecipe>[0];
type InferredOutput = ReturnType<typeof processRecipe>;

expectType<ExpectedInput, Schema<typeof inputSchema>>();
expectType<AnyCellWrapping<ExpectedInput>, Schema<typeof inputSchema>>();
expectType<ExpectedOutput, Schema<typeof outputSchema>>();

// Verify that the recipe function parameter matches our expected input type
expectType<ExpectedInput, InferredInput>();
expectType<InferredInput, ExpectedInput>();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated expectType order now only checks that ExpectedInput is assignable to InferredInput, so regressions where InferredInput loses required fields (like name) would no longer be caught. Please restore the original order so the test still enforces that the inferred recipe input type includes all expected fields.

Prompt for AI agents
Address the following comment on packages/runner/test/schema-to-ts.test.ts at line 809:

<comment>The updated expectType order now only checks that ExpectedInput is assignable to InferredInput, so regressions where InferredInput loses required fields (like `name`) would no longer be caught. Please restore the original order so the test still enforces that the inferred recipe input type includes all expected fields.</comment>

<file context>
@@ -801,11 +802,11 @@ describe(&quot;Schema-to-TS Type Conversion&quot;, () =&gt; {
 
     // Verify that the recipe function parameter matches our expected input type
-    expectType&lt;ExpectedInput, InferredInput&gt;();
+    expectType&lt;InferredInput, ExpectedInput&gt;();
 
     // The expected output is the output schema wrapped in a single OpaqueRef.
</file context>
Suggested change
expectType<InferredInput, ExpectedInput>();
expectType<ExpectedInput, InferredInput>();
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually the correct order


// The expected output is the output schema wrapped in a single OpaqueRef.
type DeepOpaqueOutput = OpaqueRef<Schema<typeof outputSchema>>;
Expand Down
9 changes: 7 additions & 2 deletions packages/runner/test/type-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import { isModule, isRecipe, type Opaque } from "../src/builder/types.ts";
import {
isModule,
isRecipe,
type Opaque,
type OpaqueRef,
} from "../src/builder/types.ts";
import { isWriteRedirectLink } from "../src/link-utils.ts";
import { LINK_V1_TAG } from "../src/sigil-types.ts";

Expand All @@ -9,7 +14,7 @@ describe("value type", () => {
const { foo, bar }: { foo: Opaque<string>; bar: Opaque<string> } = {
foo: "foo",
bar: "bar",
} as Opaque<{
} as OpaqueRef<{
foo: string;
bar: string;
}>;
Expand Down