diff --git a/AGENTS.md b/AGENTS.md index 0bf07e63f..916161db9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,8 @@ The instructions in this document apply to the entire repository. - Prefer named exports over default exports. - Use package names for internal imports. - Destructure when importing multiple names from the same module. +- Import either from `@commontools/builder` (internal API) or + `@commontools/builder/interface` (external API), but not both. ### Error Handling diff --git a/packages/builder/deno.json b/packages/builder/deno.json index 17db75dfc..e01f3fe0a 100644 --- a/packages/builder/deno.json +++ b/packages/builder/deno.json @@ -1,6 +1,9 @@ { "name": "@commontools/builder", - "exports": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./interface": "./src/interface.ts" + }, "tasks": { "test": "deno test --allow-env" } diff --git a/packages/builder/src/built-in.ts b/packages/builder/src/built-in.ts index 8d2eb3fad..1bddf816a 100644 --- a/packages/builder/src/built-in.ts +++ b/packages/builder/src/built-in.ts @@ -1,7 +1,12 @@ import { createNodeFactory, lift } from "./module.ts"; -import { type Cell } from "@commontools/runner"; -import type { JSONSchema, NodeFactory, Opaque, OpaqueRef } from "./types.ts"; -import type { Schema } from "./schema-to-ts.ts"; +import type { + Cell, + JSONSchema, + NodeFactory, + Opaque, + OpaqueRef, + Schema, +} from "./types.ts"; export interface BuiltInLLMParams { messages?: string[]; diff --git a/packages/builder/src/factory.ts b/packages/builder/src/factory.ts new file mode 100644 index 000000000..4bb15d1d0 --- /dev/null +++ b/packages/builder/src/factory.ts @@ -0,0 +1,112 @@ +/** + * Factory function to create builder functions with runtime dependency injection + */ + +import type { + BuilderFunctionsAndConstants, + Cell, + CreateCellFunction, + JSONSchema, +} from "./types.ts"; +import { AuthSchema, ID, ID_FIELD, NAME, schema, TYPE, UI } from "./types.ts"; +import { opaqueRef, stream } from "./opaque-ref.ts"; +import { getTopFrame, recipe } from "./recipe.ts"; +import { byRef, compute, derive, handler, lift, render } from "./module.ts"; +import { + compileAndRun, + fetchData, + ifElse, + llm, + navigateTo, + str, + streamData, +} from "./built-in.ts"; +import { getCellLinkOrThrow, type Runtime } from "@commontools/runner"; +import { getRecipeEnvironment } from "./env.ts"; + +/** + * Creates a set of builder functions with the given runtime + * @param runtime - The runtime instance to use for cell creation + * @returns An object containing all builder functions + */ +export const createBuilder = ( + runtime: Runtime, +): BuilderFunctionsAndConstants => { + // Implementation of createCell moved from runner/harness + const createCell: CreateCellFunction = function createCell( + schema?: JSONSchema, + name?: string, + value?: T, + ): Cell { + const frame = getTopFrame(); + // This is a rather hacky way to get the context, based on the + // unsafe_binding pattern. Once we replace that mechanism, let's add nicer + // abstractions for context here as well. + const cellLink = frame?.unsafe_binding?.materialize([]); + if (!frame || !frame.cause || !cellLink) { + throw new Error( + "Can't invoke createCell outside of a lifted function or handler", + ); + } + if (!getCellLinkOrThrow) { + throw new Error( + "getCellLinkOrThrow function not provided to createBuilder", + ); + } + const space = getCellLinkOrThrow(cellLink).cell.space; + + const cause = { parent: frame.cause } as Record; + if (name) cause.name = name; + else cause.number = frame.generatedIdCounter++; + + // Cast to Cell is necessary to cast to interface-only Cell type + const cell = runtime.getCell(space, cause, schema) as Cell; + + if (value !== undefined) cell.set(value); + + return cell; + } as CreateCellFunction; + + return { + // Recipe creation + recipe, + + // Module creation + lift, + handler, + derive, + compute, + render, + + // Built-in modules + str, + ifElse, + llm, + fetchData, + streamData, + compileAndRun, + navigateTo, + + // Cell creation + createCell, + cell: opaqueRef, + stream, + + // Utility + byRef, + + // Environment + getRecipeEnvironment, + + // Constants + ID, + ID_FIELD, + TYPE, + NAME, + UI, + + // Schema utilities + schema, + AuthSchema, + }; +}; diff --git a/packages/builder/src/index.ts b/packages/builder/src/index.ts index 65f073d8b..75b42daf8 100644 --- a/packages/builder/src/index.ts +++ b/packages/builder/src/index.ts @@ -1,14 +1,11 @@ -export { opaqueRef as cell, stream } from "./opaque-ref.ts"; -export { $, event, select, Spell } from "./spell.ts"; -export { - byRef, - compute, - createNodeFactory, - derive, - handler, - lift, - render, -} from "./module.ts"; +// Export the factory function +export { createBuilder } from "./factory.ts"; +export type { + BuilderFunctionsAndConstants as BuilderFunctions, + BuilderRuntime, +} from "./types.ts"; + +// Internal functions and exports needed by other packages export { getRecipeEnvironment, type RecipeEnvironment, @@ -19,26 +16,14 @@ export { popFrame, pushFrame, pushFrameFromCause, - recipe, recipeFromFrame, } from "./recipe.ts"; -export { - type BuiltInCompileAndRunParams, - type BuiltInCompileAndRunState, - type BuiltInLLMParams, - type BuiltInLLMState, - compileAndRun, - type createCell, - fetchData, - ifElse, - llm, - navigateTo, - str, - streamData, -} from "./built-in.ts"; export { type Alias, + AuthSchema, + type Cell, type Frame, + type HandlerFactory, ID, ID_FIELD, isAlias, @@ -54,16 +39,17 @@ export { markAsStatic, type Module, type ModuleFactory, - type Mutable, NAME, - type Node, type NodeFactory, type Opaque, type OpaqueRef, type OpaqueRefMethods, type Recipe, type RecipeFactory, - type Static, + type Schema, + schema, + type SchemaWithoutCell, + type Stream, type StreamAlias, type toJSON, toOpaqueRef, @@ -74,8 +60,9 @@ export { unsafe_parentRecipe, type UnsafeBinding, } from "./types.ts"; -export { type Schema, schema } from "./schema-to-ts.ts"; -export { AuthSchema } from "./schema-lib.ts"; +export { createNodeFactory } from "./module.ts"; +export { opaqueRef as cell } from "./opaque-ref.ts"; +export type { Mutable } from "@commontools/utils/types"; // This should be a separate package, but for now it's easier to keep it here. export { diff --git a/packages/builder/src/interface.ts b/packages/builder/src/interface.ts new file mode 100644 index 000000000..6d5ad6732 --- /dev/null +++ b/packages/builder/src/interface.ts @@ -0,0 +1,385 @@ +/** + * Public interface for the builder package. This module exports only the types + * and functions that are part of the public API. + * + * Import these types via `types.ts` for internal code. + * + * Other packages should either import from `@commontools/builder` or + * `@commontools/builder/interface`, but not both. + */ + +import type { Schema, SchemaWithoutCell } from "./schema-to-ts.ts"; + +export const ID: unique symbol = Symbol("ID, unique to the context"); +export const ID_FIELD: unique symbol = Symbol( + "ID_FIELD, name of sibling that contains id", +); + +// Should be Symbol("UI") or so, but this makes repeat() use these when +// iterating over recipes. +export const TYPE = "$TYPE"; +export const NAME = "$NAME"; +export const UI = "$UI"; + +// Re-export Schema type +export type { Schema, SchemaWithoutCell } from "./schema-to-ts.ts"; + +// Re-export schema utilities +export { schema } from "./schema-to-ts.ts"; +export { AuthSchema } from "./schema-lib.ts"; + +// Cell type with only public methods +export interface Cell { + // Public methods available in spell code and system + get(): T; + set(value: T): void; + send(value: T): void; // alias for set + update(values: Partial): void; + push(...value: T extends (infer U)[] ? U[] : never): void; + equals(other: Cell): boolean; + key(valueKey: K): Cell; +} + +// Cell type with only public methods +export interface Stream { + send(event: T): void; +} + +export type OpaqueRef = + & OpaqueRefMethods + & (T extends Array ? Array> + : T extends object ? { [K in keyof T]: OpaqueRef } + : T); + +// Any OpaqueRef is also an Opaque, but can also have static values. +// Use Opaque in APIs that get inputs from the developer and use OpaqueRef +// when data gets passed into what developers see (either recipe inputs or +// module outputs). +export type Opaque = + | OpaqueRef + | (T extends Array ? Array> + : T extends object ? { [K in keyof T]: Opaque } + : T); + +// OpaqueRefMethods type with only public methods +export interface OpaqueRefMethods { + get(): OpaqueRef; + set(value: Opaque | T): void; + key(key: K): OpaqueRef; + setDefault(value: Opaque | T): void; + setName(name: string): void; + setSchema(schema: JSONSchema): void; + map( + fn: ( + element: T extends Array ? Opaque : Opaque, + index: Opaque, + array: T, + ) => Opaque, + ): Opaque; +} + +// Factory types + +// TODO(seefeld): Subset of internal type, just enough to make it +// differentiated. But this isn't part of the public API, so we need to find a +// different way to handle this. +export interface Recipe { + argumentSchema: JSONSchema; + resultSchema: JSONSchema; +} +export interface Module { + type: "ref" | "javascript" | "recipe" | "raw" | "isolated" | "passthrough"; +} + +export type toJSON = { + toJSON(): unknown; +}; + +export type Handler = Module & { + with: (inputs: Opaque) => OpaqueRef; +}; + +export type NodeFactory = + & ((inputs: Opaque) => OpaqueRef) + & (Module | Handler | Recipe) + & toJSON; + +export type RecipeFactory = + & ((inputs: Opaque) => OpaqueRef) + & Recipe + & toJSON; + +export type ModuleFactory = + & ((inputs: Opaque) => OpaqueRef) + & Module + & toJSON; + +export type HandlerFactory = + & ((inputs: Opaque) => OpaqueRef) + & Handler + & toJSON; + +// JSON types + +export type JSONValue = + | null + | boolean + | number + | string + | JSONArray + | JSONObject & IDFields; + +export interface JSONArray extends ArrayLike {} + +export interface JSONObject extends Record {} + +// Annotations when writing data that help determine the entity id. They are +// removed before sending to storage. +export interface IDFields { + [ID]?: unknown; + [ID_FIELD]?: unknown; +} + +// TODO(@ubik2) When specifying a JSONSchema, you can often use a boolean +// This is particularly useful for specifying the schema of a property. +// That will require reworking some things, so for now, I'm not doing it +export type JSONSchema = { + readonly [ID]?: unknown; + readonly [ID_FIELD]?: unknown; + readonly type?: + | "object" + | "array" + | "string" + | "integer" + | "number" + | "boolean" + | "null"; + readonly properties?: Readonly>; + readonly description?: string; + readonly default?: Readonly; + readonly title?: string; + readonly example?: Readonly; + readonly required?: readonly string[]; + readonly enum?: readonly string[]; + readonly items?: Readonly; + readonly $ref?: string; + readonly $defs?: Readonly>; + readonly asCell?: boolean; + readonly asStream?: boolean; + readonly anyOf?: readonly JSONSchema[]; + readonly additionalProperties?: Readonly | boolean; + readonly ifc?: { classification?: string[]; integrity?: string[] }; // temporarily used to assign labels like "confidential" +}; + +// Built-in types +export interface BuiltInLLMParams { + messages?: string[]; + model?: string; + system?: string; + stop?: string; + maxTokens?: number; + mode?: "json"; +} + +export interface BuiltInLLMState { + pending: boolean; + result?: T; + partial?: string; + error: unknown; +} + +export interface BuiltInCompileAndRunParams { + files: Record; + main: string; + input?: T; +} + +export interface BuiltInCompileAndRunState { + pending: boolean; + result?: T; + error?: any; +} + +// Function type definitions +export type RecipeFunction = { + ( + argumentSchema: S, + fn: (input: OpaqueRef>>) => any, + ): RecipeFactory, ReturnType>; + + ( + argumentSchema: S, + fn: (input: OpaqueRef>>) => Opaque, + ): RecipeFactory, R>; + + ( + argumentSchema: S, + resultSchema: RS, + fn: ( + input: OpaqueRef>>, + ) => Opaque>, + ): RecipeFactory, SchemaWithoutCell>; + + ( + argumentSchema: string | JSONSchema, + fn: (input: OpaqueRef>) => any, + ): RecipeFactory>; + + ( + argumentSchema: string | JSONSchema, + fn: (input: OpaqueRef>) => Opaque, + ): RecipeFactory; + + ( + argumentSchema: string | JSONSchema, + resultSchema: JSONSchema, + fn: (input: OpaqueRef>) => Opaque, + ): RecipeFactory; +}; + +export type LiftFunction = { + ( + argumentSchema: T, + resultSchema: R, + implementation: (input: Schema) => Schema, + ): ModuleFactory, SchemaWithoutCell>; + + ( + implementation: (input: T) => R, + ): ModuleFactory; + + ( + implementation: (input: T) => any, + ): ModuleFactory>; + + any>( + implementation: T, + ): ModuleFactory[0], ReturnType>; +}; + +export type HandlerFunction = { + ( + eventSchema: E, + stateSchema: T, + handler: (event: Schema, props: Schema) => any, + ): ModuleFactory, SchemaWithoutCell>; + + ( + eventSchema: JSONSchema, + stateSchema: JSONSchema, + handler: (event: E, props: T) => any, + ): ModuleFactory; + + ( + handler: (event: E, props: T) => any, + ): ModuleFactory; +}; + +export type DeriveFunction = ( + input: Opaque, + f: (input: In) => Out | Promise, +) => OpaqueRef; + +export type ComputeFunction = (fn: () => T) => OpaqueRef; + +export type RenderFunction = (fn: () => T) => OpaqueRef; + +export type StrFunction = ( + strings: TemplateStringsArray, + ...values: any[] +) => OpaqueRef; + +export type IfElseFunction = ( + condition: Opaque, + ifTrue: Opaque, + ifFalse: Opaque, +) => OpaqueRef; + +export type LLMFunction = ( + params: Opaque, +) => OpaqueRef>; + +export type FetchDataFunction = ( + params: Opaque<{ + url: string; + mode?: "json" | "text"; + options?: RequestInit; + result?: T; + }>, +) => Opaque<{ pending: boolean; result: T; error: any }>; + +export type StreamDataFunction = ( + params: Opaque<{ + url: string; + options?: RequestInit; + result?: T; + }>, +) => Opaque<{ pending: boolean; result: T; error: any }>; + +export type CompileAndRunFunction = ( + params: Opaque>, +) => OpaqueRef>; + +export type NavigateToFunction = (cell: OpaqueRef) => OpaqueRef; + +export type CreateNodeFactoryFunction = ( + moduleSpec: Module, +) => ModuleFactory; + +export type CreateCellFunction = { + ( + schema?: JSONSchema, + name?: string, + value?: T, + ): Cell; + + ( + schema: S, + name?: string, + value?: Schema, + ): Cell>; +}; + +// Re-export opaque ref creators +export type CellFunction = (value?: T, schema?: JSONSchema) => OpaqueRef; +export type StreamFunction = (initial?: T) => OpaqueRef; +export type ByRefFunction = (ref: string) => ModuleFactory; + +// Recipe environment types +export interface RecipeEnvironment { + readonly apiUrl: URL; +} + +export type GetRecipeEnvironmentFunction = () => RecipeEnvironment; + +// Re-export all function types as values for destructuring imports +// These will be implemented by the factory +export declare const recipe: RecipeFunction; +export declare const lift: LiftFunction; +export declare const handler: HandlerFunction; +export declare const derive: DeriveFunction; +export declare const compute: ComputeFunction; +export declare const render: RenderFunction; +export declare const str: StrFunction; +export declare const ifElse: IfElseFunction; +export declare const llm: LLMFunction; +export declare const fetchData: FetchDataFunction; +export declare const streamData: StreamDataFunction; +export declare const compileAndRun: CompileAndRunFunction; +export declare const navigateTo: NavigateToFunction; +export declare const createNodeFactory: CreateNodeFactoryFunction; +export declare const createCell: CreateCellFunction; +export declare const cell: CellFunction; +export declare const stream: StreamFunction; +export declare const byRef: ByRefFunction; +export declare const getRecipeEnvironment: GetRecipeEnvironmentFunction; + +/** + * Helper type to recursively remove `readonly` properties from type `T`. + * + * (Duplicated from @commontools/utils/types.ts, but we want to keep this + * independent for now) + */ +export type Mutable = T extends ReadonlyArray ? Mutable[] + : T extends object ? ({ -readonly [P in keyof T]: Mutable }) + : T; diff --git a/packages/builder/src/module.ts b/packages/builder/src/module.ts index d02edfe1e..c8b12572f 100644 --- a/packages/builder/src/module.ts +++ b/packages/builder/src/module.ts @@ -7,6 +7,8 @@ import type { NodeRef, Opaque, OpaqueRef, + Schema, + SchemaWithoutCell, toJSON, } from "./types.ts"; import { isModule, isOpaqueRef } from "./types.ts"; @@ -18,7 +20,6 @@ import { traverseValue, } from "./utils.ts"; import { getTopFrame } from "./recipe.ts"; -import { Schema, SchemaWithoutCell } from "./schema-to-ts.ts"; export function createNodeFactory( moduleSpec: Module, diff --git a/packages/builder/src/opaque-ref.ts b/packages/builder/src/opaque-ref.ts index 5610575f2..974ec2577 100644 --- a/packages/builder/src/opaque-ref.ts +++ b/packages/builder/src/opaque-ref.ts @@ -7,6 +7,7 @@ import { type OpaqueRef, type OpaqueRefMethods, type Recipe, + type SchemaWithoutCell, type ShadowRef, toOpaqueRef, type UnsafeBinding, @@ -14,7 +15,6 @@ import { import { hasValueAtPath, setValueAtPath } from "./utils.ts"; import { getTopFrame, recipe } from "./recipe.ts"; import { createNodeFactory } from "./module.ts"; -import { SchemaWithoutCell } from "./schema-to-ts.ts"; import { ContextualFlowControl } from "../../runner/src/index.ts"; import { isRecord } from "@commontools/utils/types"; diff --git a/packages/builder/src/recipe.ts b/packages/builder/src/recipe.ts index 973545ecc..4bfe50065 100644 --- a/packages/builder/src/recipe.ts +++ b/packages/builder/src/recipe.ts @@ -13,6 +13,7 @@ import { type OpaqueRef, type Recipe, type RecipeFactory, + type SchemaWithoutCell, type ShadowRef, type toJSON, UI, @@ -30,7 +31,6 @@ import { toJSONWithAliases, traverseValue, } from "./utils.ts"; -import { SchemaWithoutCell } from "./schema-to-ts.ts"; import { isRecord } from "@commontools/utils/types"; /** Declare a recipe diff --git a/packages/builder/src/schema-to-ts.ts b/packages/builder/src/schema-to-ts.ts index 94bae2f60..7728ae9e5 100644 --- a/packages/builder/src/schema-to-ts.ts +++ b/packages/builder/src/schema-to-ts.ts @@ -1,5 +1,4 @@ -import type { IDFields, JSONSchema } from "./types.ts"; -import type { Cell, Stream } from "@commontools/runner"; +import type { Cell, IDFields, JSONSchema, Stream } from "./interface.ts"; export const schema = (schema: T) => schema; diff --git a/packages/builder/src/spell.ts b/packages/builder/src/spell.ts deleted file mode 100644 index e96cd2f42..000000000 --- a/packages/builder/src/spell.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { derive, type OpaqueRef, recipe, stream, UI } from "./index.ts"; - -// $ is a proxy that just collect paths, so that one can call [getPath] on it -// and get an array. For example for `q = $.foo.bar[0]` `q[getPath]` yields -// `["foo", "bar", 0]`. This is used to generate queries. - -type PathSegment = PropertyKey | { fn: string; args: any[] }; -const getPath = Symbol("getPath"); -type PathCollector = ((this: any, ...args: any[]) => any) & { - [getPath]: PathSegment[]; - [key: string]: PathCollector; -}; - -// Create the path collector proxy -function createPathCollector(path: PathSegment[] = []): PathCollector { - return new Proxy( - function () {} as unknown as PathCollector, // Base target is a function to support function calls - { - get(target, prop) { - if (prop === getPath) return path; - if (typeof prop === "symbol") return (target as any)[prop]; - - // Continue collecting path for property access - return createPathCollector([...path, prop]); - }, - - // Catch any function calls - apply(_target, _thisArg, args) { - // Get the last segment which should be the function name - const lastSegment = path[path.length - 1]; - if (typeof lastSegment !== "string") { - throw new Error("Invalid function call"); - } - - // Remove the function name from the path and add it as a function call - const newPath = path.slice(0, -1); - return createPathCollector([...newPath, { fn: lastSegment, args }]); - }, - }, - ); -} - -// // Create the root $ proxy -export const $ = createPathCollector(); - -// Resolve $ to a paths on `self` -// TODO(seefeld): Also for non-top-level ones -function resolve$(self: OpaqueRef, query: PathCollector) { - const entries = Object.entries(query); - const result: Record = {}; - - for (const [key, value] of entries) { - if (value && typeof value === "function" && value[getPath]) { - const path = value[getPath] as PathSegment[]; - - let current = self; - for (const segment of path) { - if (typeof segment === "object" && "fn" in segment) { - // Execute any function with its arguments - current = current[segment.fn].apply(current, segment.args); - } else { - current = current[segment as PropertyKey]; - } - } - - result[key] = current; - } else { - result[key] = value; - } - } - - return result; -} - -export function select(query: any) { - const generateQuery = (self: OpaqueRef) => resolve$(self, query); - Object.assign(generateQuery, { - with: (schema: any) => - select({ - ...resolve$(self, query), - ...Object.fromEntries( - Object.keys(schema.properties ?? {}).map(( - key, - ) => [key, (self as any)[key]]), - ), - }), - }); - return generateQuery; -} - -// addRule(event("update"), ({ $event }) => { ... }) -export function event(name: string) { - // .compile() will replace $event with actual stream - return select({ $event: name }); -} - -export abstract class Spell> { - private eventListeners: Array<{ - type: string; - handlerFn: (self: any, ev: any) => any; - }> = []; - private rules: Array<{ - condition: any; - handlerFn: (ctx: any) => any; - }> = []; - - private streams: Record> = {}; - - constructor() {} - - /** - * Merges existing state with new values - * @param self The current state proxy object - * @param values Partial state updates to apply - */ - update(self: any, values: Partial) { - Object.entries(values).forEach(([key, value]) => { - self[key] = value; - }); - } - - /** - * Returns a stream reference for the given event type - * Used in JSX event handlers, e.g. onClick={this.dispatch('click')} - * @param event The event type to dispatch - * @returns An OpaqueRef stream for the event - */ - dispatch(event: string) { - return this.streams[event]; - } - - /** - * Registers an event listener that will be called when events are dispatched - * @param type The event type to listen for - * @param handlerFn Function called when event occurs, receives (state, event) - */ - addEventListener(type: string, handlerFn: (self: any, ev: any) => any) { - this.eventListeners.push({ type, handlerFn }); - } - - /** - * Adds a reactive rule that runs when its conditions are met - * @param condition Query condition that determines when rule runs - * @param handlerFn Function called when condition is met, receives query context - */ - addRule(condition: any, handlerFn: (ctx: any) => any) { - this.rules.push({ condition, handlerFn }); - } - - /** - * Initializes the spell's state - * Must be implemented by subclasses - * @returns Initial state object - */ - abstract init(): T; - - /** - * Renders the spell's UI - * Must be implemented by subclasses - * @param state Current spell state - * @returns JSX element or other render output - */ - abstract render(state: T): any; - - // Used when chaining the query, e.g. `with(this.get('meta', ""))` - /* - get(field: S, defaultValue?: any) { - if (defaultValue) { - return select({ [field]: $[field] } as const).clause(defaultTo($.self, field, $[field], defaultValue)); - } - return select({ [field]: $[field] } as const).match($.self, field, $[field]) - } - */ - - compile(title: string = "Spell") { - return recipe(title, (self: OpaqueRef) => { - const initialState = this.init() ?? {}; - const state: Record> = {}; - - Object.entries(initialState).forEach(([key, value]) => { - self[key].setDefault(value); - state[key] = self[key]; - }); - - this.eventListeners.forEach(({ type, handlerFn }) => { - this.streams[type] ??= stream(); - derive( - { self, $event: this.streams[type] }, - ({ self, $event }) => handlerFn(self, $event), - ); - }); - - this.rules.forEach((rule) => { - // condition: - // ($) => { foo: $.foo } - // select({ foo: $.foo }) - // select({ foo: $.foo, bar: $.bar.map(item => item.foo) }) - // .filter(fn), count(), take(n), skip(n), - // .sortBy(fn, dir?), groupBy(key), distinct(key), - // .join(ref, key?) - // event("update") - // ["foo", "bar"] - - let condition = rule.condition(self); - - if (Array.isArray(condition)) { - condition = Object.fromEntries( - condition.map((key) => [key, self[key]]), - ); - } else if ( - condition && - typeof condition === "object" && - condition !== null && - "$event" in condition - ) { - condition["$event"] = this.streams[condition["$event"]]; - } - - condition.self = self; - - derive(condition, rule.handlerFn); - }); - - return { - [UI]: this.render(self), - ...this.streams, - ...state, - }; - }); - } -} diff --git a/packages/builder/src/types.ts b/packages/builder/src/types.ts index cea2d0048..c10d06864 100644 --- a/packages/builder/src/types.ts +++ b/packages/builder/src/types.ts @@ -1,70 +1,112 @@ -import { isObject, Mutable } from "@commontools/utils/types"; - -export const ID: unique symbol = Symbol("ID, unique to the context"); -export const ID_FIELD: unique symbol = Symbol( - "ID_FIELD, name of sibling that contains id", -); - -// Should be Symbol("UI") or so, but this makes repeat() use these when -// iterating over recipes. -export const TYPE = "$TYPE"; -export const NAME = "$NAME"; -export const UI = "$UI"; - -export type OpaqueRef = - & OpaqueRefMethods - & (T extends Array ? Array> - : T extends object ? { [K in keyof T]: OpaqueRef } - : T); - -// Any OpaqueRef is also an Opaque, but can also have static values. -// Use Opaque in APIs that get inputs from the developer and use OpaqueRef -// when data gets passed into what developers see (either recipe inputs or -// module outputs). -export type Opaque = - | OpaqueRef - | (T extends Array ? Array> - : T extends object ? { [K in keyof T]: Opaque } - : T); - -export type OpaqueRefMethods = { - get(): OpaqueRef; - set(value: Opaque | T): void; - key(key: K): OpaqueRef; - setDefault(value: Opaque | T): void; - setPreExisting(ref: unknown): void; - setName(name: string): void; - setSchema(schema: JSONSchema): void; - connect(node: NodeRef): void; - export(): { - cell: OpaqueRef; - path: PropertyKey[]; - value?: Opaque; - defaultValue?: Opaque; - nodes: Set; - external?: unknown; - name?: string; - schema?: JSONSchema; - rootSchema?: JSONSchema; - frame: Frame; - }; - unsafe_bindToRecipeAndPath( - recipe: Recipe, - path: PropertyKey[], - ): void; - unsafe_getExternal(): OpaqueRef; - map( - fn: ( - element: T extends Array ? Opaque : Opaque, - index: Opaque, - array: T, - ) => Opaque, - ): Opaque; - toJSON(): unknown; - [Symbol.iterator](): Iterator; - [Symbol.toPrimitive](hint: string): T; - [isOpaqueRefMarker]: true; -}; +import { isObject, type Mutable } from "@commontools/utils/types"; + +import type { + ByRefFunction, + Cell, + CellFunction, + CompileAndRunFunction, + ComputeFunction, + CreateCellFunction, + DeriveFunction, + FetchDataFunction, + GetRecipeEnvironmentFunction, + HandlerFunction, + IfElseFunction, + JSONSchema, + JSONValue, + LiftFunction, + LLMFunction, + Module, + NavigateToFunction, + Opaque, + OpaqueRef, + Recipe, + RecipeFunction, + RenderFunction, + Schema, + StreamDataFunction, + StreamFunction, + StrFunction, +} from "./interface.ts"; +import { + AuthSchema, + ID, + ID_FIELD, + NAME, + schema, + TYPE, + UI, +} from "./interface.ts"; + +export { AuthSchema, ID, ID_FIELD, NAME, TYPE, UI } from "./interface.ts"; +export type { + Cell, + CreateCellFunction, + Handler, + HandlerFactory, + JSONObject, + JSONSchema, + JSONValue, + Module, + ModuleFactory, + NodeFactory, + Opaque, + OpaqueRef, + Recipe, + RecipeFactory, + Stream, + toJSON, +} from "./interface.ts"; +export { type Schema, schema, type SchemaWithoutCell } from "./schema-to-ts.ts"; + +export type JSONSchemaMutable = Mutable; + +// Augment the public interface with the internal OpaqueRefMethods interface. +// Deliberately repeating the original interface to catch any inconsistencies: +// This here then reflects the entire interface the internal implementation +// implements. +declare module "./interface.ts" { + interface OpaqueRefMethods { + get(): OpaqueRef; + set(value: Opaque | T): void; + key(key: K): OpaqueRef; + setDefault(value: Opaque | T): void; + setPreExisting(ref: unknown): void; + setName(name: string): void; + setSchema(schema: JSONSchema): void; + connect(node: NodeRef): void; + export(): { + cell: OpaqueRef; + path: PropertyKey[]; + value?: Opaque; + defaultValue?: Opaque; + nodes: Set; + external?: unknown; + name?: string; + schema?: JSONSchema; + rootSchema?: JSONSchema; + frame: Frame; + }; + unsafe_bindToRecipeAndPath( + recipe: Recipe, + path: PropertyKey[], + ): void; + unsafe_getExternal(): OpaqueRef; + map( + fn: ( + element: T extends Array ? Opaque : Opaque, + index: Opaque, + array: T, + ) => Opaque, + ): Opaque; + toJSON(): unknown; + [Symbol.iterator](): Iterator; + [Symbol.toPrimitive](hint: string): T; + [isOpaqueRefMarker]: true; + } +} + +export type { OpaqueRefMethods } from "./interface.ts"; export const isOpaqueRefMarker = Symbol("isOpaqueRef"); @@ -80,83 +122,6 @@ export type NodeRef = { frame: Frame | undefined; }; -export type toJSON = { - toJSON(): unknown; -}; - -export type NodeFactory = - & ((inputs: Opaque) => OpaqueRef) - & (Module | Handler | Recipe) - & toJSON; - -export type RecipeFactory = - & ((inputs: Opaque) => OpaqueRef) - & Recipe - & toJSON; - -export type ModuleFactory = - & ((inputs: Opaque) => OpaqueRef) - & Module - & toJSON; - -export type HandlerFactory = - & ((inputs: Opaque) => OpaqueRef) - & Handler - & toJSON; - -export type JSONValue = - | null - | boolean - | number - | string - | JSONArray - | JSONObject & IDFields; - -export interface JSONArray extends ArrayLike {} - -export interface JSONObject extends Record {} - -// Annotations when writing data that help determine the entity id. They are -// removed before sending to storage. -export interface IDFields { - [ID]?: unknown; - [ID_FIELD]?: unknown; -} - -// TODO(@ubik2) When specifying a JSONSchema, you can often use a boolean -// This is particularly useful for specifying the schema of a property. -// That will require reworking some things, so for now, I'm not doing it -export type JSONSchema = { - readonly [ID]?: unknown; - readonly [ID_FIELD]?: unknown; - readonly type?: - | "object" - | "array" - | "string" - | "integer" - | "number" - | "boolean" - | "null"; - readonly properties?: Readonly>; - readonly description?: string; - readonly default?: Readonly; - readonly title?: string; - readonly example?: Readonly; - readonly required?: readonly string[]; - readonly enum?: readonly string[]; - readonly items?: Readonly; - readonly $ref?: string; - readonly $defs?: Readonly>; - readonly asCell?: boolean; - readonly asStream?: boolean; - readonly anyOf?: readonly JSONSchema[]; - readonly additionalProperties?: Readonly | boolean; - readonly ifc?: { classification?: string[]; integrity?: string[] }; // temporarily used to assign labels like "confidential" -}; - -export { type Mutable }; -export type JSONSchemaMutable = Mutable; - export type Alias = { $alias: { cell?: unknown; @@ -180,17 +145,15 @@ export function isStreamAlias(value: unknown): value is StreamAlias { return isObject(value) && "$stream" in value && value.$stream === true; } -export type Module = { - type: "ref" | "javascript" | "recipe" | "raw" | "isolated" | "passthrough"; - implementation?: ((...args: any[]) => any) | Recipe | string; - wrapper?: "handler"; - argumentSchema?: JSONSchema; - resultSchema?: JSONSchema; -}; - -export type Handler = Module & { - with: (inputs: Opaque) => OpaqueRef; -}; +declare module "./interface.ts" { + export interface Module { + type: "ref" | "javascript" | "recipe" | "raw" | "isolated" | "passthrough"; + implementation?: ((...args: any[]) => any) | Recipe | string; + wrapper?: "handler"; + argumentSchema?: JSONSchema; + resultSchema?: JSONSchema; + } +} export function isModule(value: unknown): value is Module { return ( @@ -211,16 +174,18 @@ export const unsafe_originalRecipe = Symbol("unsafe_originalRecipe"); export const unsafe_parentRecipe = Symbol("unsafe_parentRecipe"); export const unsafe_materializeFactory = Symbol("unsafe_materializeFactory"); -export type Recipe = { - argumentSchema: JSONSchema; - resultSchema: JSONSchema; - initial?: JSONValue; - result: JSONValue; - nodes: Node[]; - [unsafe_originalRecipe]?: Recipe; - [unsafe_parentRecipe]?: Recipe; - [unsafe_materializeFactory]?: (log: any) => (path: PropertyKey[]) => any; -}; +declare module "./interface.ts" { + interface Recipe { + argumentSchema: JSONSchema; + resultSchema: JSONSchema; + initial?: JSONValue; + result: JSONValue; + nodes: Node[]; + [unsafe_originalRecipe]?: Recipe; + [unsafe_parentRecipe]?: Recipe; + [unsafe_materializeFactory]?: (log: any) => (path: PropertyKey[]) => any; + } +} export function isRecipe(value: unknown): value is Recipe { return ( @@ -292,3 +257,69 @@ export function markAsStatic(value: unknown): unknown { (value as any)[isStaticMarker] = true; return value; } + +// Builder functions interface +export interface BuilderFunctionsAndConstants { + // Recipe creation + recipe: RecipeFunction; + + // Module creation + lift: LiftFunction; + handler: HandlerFunction; + derive: DeriveFunction; + compute: ComputeFunction; + render: RenderFunction; + + // Built-in modules + str: StrFunction; + ifElse: IfElseFunction; + llm: LLMFunction; + fetchData: FetchDataFunction; + streamData: StreamDataFunction; + compileAndRun: CompileAndRunFunction; + navigateTo: NavigateToFunction; + + // Cell creation + createCell: CreateCellFunction; + cell: CellFunction; + stream: StreamFunction; + + // Utility + byRef: ByRefFunction; + + // Environment + getRecipeEnvironment: GetRecipeEnvironmentFunction; + + // Constants + ID: typeof ID; + ID_FIELD: typeof ID_FIELD; + TYPE: typeof TYPE; + NAME: typeof NAME; + UI: typeof UI; + + // Schema utilities + schema: typeof schema; + AuthSchema: typeof AuthSchema; +} + +// Runtime interface needed by createCell +export interface BuilderRuntime { + getCell( + space: string, + cause: any, + schema?: JSONSchema, + log?: any, + ): Cell; + getCell( + space: string, + cause: any, + schema: S, + log?: any, + ): Cell>; +} + +// Factory function to create builder with runtime +export type CreateBuilder = ( + runtime: BuilderRuntime, + getCellLinkOrThrow?: (value: any) => any, +) => BuilderFunctionsAndConstants; diff --git a/packages/builder/test/opaque-ref-schema.test.ts b/packages/builder/test/opaque-ref-schema.test.ts index 52d2bd534..d3bcd68ff 100644 --- a/packages/builder/test/opaque-ref-schema.test.ts +++ b/packages/builder/test/opaque-ref-schema.test.ts @@ -1,20 +1,31 @@ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; -import { recipe } from "../src/index.ts"; +import { createBuilder } from "../src/index.ts"; import { popFrame, pushFrame } from "../src/recipe.ts"; -import { opaqueRef } from "../src/opaque-ref.ts"; import type { Frame, JSONSchema } from "../src/types.ts"; +import { Runtime } from "@commontools/runner"; describe("OpaqueRef Schema Support", () => { let frame: Frame; + let runtime: Runtime; + let recipe: ReturnType["recipe"]; + let cell: ReturnType["cell"]; beforeEach(() => { // Setup frame for the test frame = pushFrame(); + + // Set up runtime and builder + runtime = new Runtime({ + storageUrl: "volatile://", + }); + const builder = createBuilder(runtime); + ({ recipe, cell } = builder); }); - afterEach(() => { + afterEach(async () => { popFrame(frame); + await runtime?.dispose(); }); describe("Schema Setting and Retrieval", () => { @@ -29,7 +40,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref - const ref = opaqueRef<{ name: string; age: number }>(undefined, schema); + const ref = cell<{ name: string; age: number }>(undefined, schema); // Export the ref and check the schema is included const exported = ref.export(); @@ -54,7 +65,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref - const ref = opaqueRef<{ name: string; details: { age: number } }>( + const ref = cell<{ name: string; details: { age: number } }>( undefined, rootSchema, ).key("details"); @@ -80,7 +91,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref - const ref = opaqueRef<{ name: string; age: number }>(undefined, schema); + const ref = cell<{ name: string; age: number }>(undefined, schema); // Get a child property const nameRef = ref.key("name"); @@ -111,7 +122,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref with an array - const ref = opaqueRef<{ items: Array<{ id: number; text: string }> }>( + const ref = cell<{ items: Array<{ id: number; text: string }> }>( undefined, schema, ); @@ -153,7 +164,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref with nested objects - const ref = opaqueRef<{ + const ref = cell<{ user: { profile: { name: string; @@ -192,7 +203,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref with nested objects - const ref = opaqueRef<{ + const ref = cell<{ details: { name: string; age: number; @@ -232,7 +243,7 @@ describe("OpaqueRef Schema Support", () => { } as const satisfies JSONSchema; // Create an opaque ref with nested objects, and a property that isn't in the schema - const ref = opaqueRef<{ + const ref = cell<{ details: { name: string; age: number; diff --git a/packages/charm/src/search.ts b/packages/charm/src/search.ts index 9d0696b68..2ffc9aa9b 100644 --- a/packages/charm/src/search.ts +++ b/packages/charm/src/search.ts @@ -4,7 +4,7 @@ import { CharmManager, DEFAULT_MODEL, } from "@commontools/charm"; -import { NAME, Recipe, recipe } from "@commontools/builder"; +import { NAME, Recipe } from "@commontools/builder/interface"; import { LLMClient } from "@commontools/llm"; import { Cell } from "@commontools/runner"; diff --git a/packages/html/test/html-recipes.test.ts b/packages/html/test/html-recipes.test.ts index a4d9eaaf2..f0bbea47c 100644 --- a/packages/html/test/html-recipes.test.ts +++ b/packages/html/test/html-recipes.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, it, afterEach } from "@std/testing/bdd"; +import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { h, render, VNode } from "../src/index.ts"; -import { lift, recipe, str, UI } from "@commontools/builder"; +import { createBuilder } from "@commontools/builder"; import { Runtime } from "@commontools/runner"; import * as assert from "./assert.ts"; import { JSDOM } from "jsdom"; @@ -9,6 +9,10 @@ describe("recipes with HTML", () => { let dom: JSDOM; let document: Document; let runtime: Runtime; + let lift: ReturnType["lift"]; + let recipe: ReturnType["recipe"]; + let str: ReturnType["str"]; + let UI: ReturnType["UI"]; beforeEach(() => { // Set up a fresh JSDOM instance for each test @@ -23,8 +27,11 @@ describe("recipes with HTML", () => { // Set up runtime runtime = new Runtime({ - storageUrl: "volatile://" + storageUrl: "volatile://", }); + + const builder = createBuilder(runtime); + ({ lift, recipe, str, UI } = builder); }); afterEach(async () => { @@ -39,9 +46,11 @@ describe("recipes with HTML", () => { }, ); - const space = "test"; - const resultCell = runtime.documentMap.getDoc(undefined, "simple-ui-result", space); - const result = runtime.runner.run(simpleRecipe, { value: 5 }, resultCell); + const result = runtime.run( + simpleRecipe, + { value: 5 }, + runtime.documentMap.getDoc(undefined, "simple-ui-result", "test"), + ); await runtime.idle(); const resultValue = result.get(); @@ -81,15 +90,17 @@ describe("recipes with HTML", () => { }; }); - const space = "test"; - const resultCell = runtime.documentMap.getDoc(undefined, "todo-list-result", space); - const result = runtime.runner.run(todoList, { - title: "test", - items: [ - { title: "item 1", done: false }, - { title: "item 2", done: true }, - ], - }, resultCell); + const result = runtime.run( + todoList, + { + title: "test", + items: [ + { title: "item 1", done: false }, + { title: "item 2", done: true }, + ], + }, + runtime.documentMap.getDoc(undefined, "todo-list-result", "test"), + ); await runtime.idle(); @@ -121,15 +132,17 @@ describe("recipes with HTML", () => { return { [UI]: h("div", null, summaryUI as any) }; }); - const space = "test"; - const resultCell = runtime.documentMap.getDoc(undefined, "nested-todo-result", space); - const result = runtime.runner.run(todoList, { - title: { name: "test" }, - items: [ - { title: "item 1", done: false }, - { title: "item 2", done: true }, - ], - }, resultCell); + const result = runtime.run( + todoList, + { + title: { name: "test" }, + items: [ + { title: "item 1", done: false }, + { title: "item 2", done: true }, + ], + }, + runtime.documentMap.getDoc(undefined, "nested-todo-result", "test"), + ); await runtime.idle(); @@ -146,9 +159,11 @@ describe("recipes with HTML", () => { return { [UI]: h("div", null, str`Hello, ${name}!`) }; }); - const space = "test"; - const resultCell = runtime.documentMap.getDoc(undefined, "str-recipe-result", space); - const result = runtime.runner.run(strRecipe, { name: "world" }, resultCell); + const result = runtime.run( + strRecipe, + { name: "world" }, + runtime.documentMap.getDoc(undefined, "str-recipe-result", "test"), + ); await runtime.idle(); @@ -185,9 +200,11 @@ describe("recipes with HTML", () => { ), })); - const space = "test"; - const resultCell = runtime.documentMap.getDoc(undefined, "nested-map-result", space); - const result = runtime.runner.run(nestedMapRecipe, data, resultCell); + const result = runtime.run( + nestedMapRecipe, + data, + runtime.documentMap.getDoc(undefined, "nested-map-result", "test"), + ); await runtime.idle(); diff --git a/packages/jumble/src/main.tsx b/packages/jumble/src/main.tsx index 00abeb310..c59b24b99 100644 --- a/packages/jumble/src/main.tsx +++ b/packages/jumble/src/main.tsx @@ -1,9 +1,8 @@ import { StrictMode, useEffect } from "react"; import { createRoot } from "react-dom/client"; -import { ConsoleMethod, Runtime } from "@commontools/runner"; +import { Runtime } from "@commontools/runner"; import { BrowserRouter as Router, - createBrowserRouter, createRoutesFromChildren, matchRoutes, Route, @@ -31,12 +30,6 @@ import { ActivityProvider } from "@/contexts/ActivityContext.tsx"; import { RuntimeProvider } from "@/contexts/RuntimeContext.tsx"; import { ROUTES } from "@/routes.ts"; -declare global { - interface ImportMetaEnv { - readonly VITE_COMMIT_SHA?: string; - } -} - // Determine environment based on hostname const determineEnvironment = () => { const hostname = globalThis.location.hostname; @@ -66,7 +59,7 @@ if (envConfig) { Sentry.init({ dsn: envConfig.dsn, environment: envConfig.environment, - release: import.meta.env.VITE_COMMIT_SHA || "development", + release: (import.meta as any).env.VITE_COMMIT_SHA || "development", tracesSampleRate: 1.0, integrations: [ Sentry.reactRouterV7BrowserTracingIntegration({ diff --git a/packages/jumble/src/recipes/smolIframe.ts b/packages/jumble/src/recipes/smolIframe.ts index 3f9abc8a7..ec606a3a8 100644 --- a/packages/jumble/src/recipes/smolIframe.ts +++ b/packages/jumble/src/recipes/smolIframe.ts @@ -1,5 +1,5 @@ import { h } from "@commontools/html"; -import { JSONSchema, NAME, recipe, UI } from "@commontools/builder"; +import { JSONSchema, NAME, recipe, UI } from "@commontools/builder/interface"; import src from "./smolIFrame.html?raw"; diff --git a/packages/jumble/src/utils/api.ts b/packages/jumble/src/utils/api.ts index 189b6304e..02a7d52b8 100644 --- a/packages/jumble/src/utils/api.ts +++ b/packages/jumble/src/utils/api.ts @@ -1,6 +1,6 @@ import React from "react"; -const TOOLSHED_API_URL = import.meta.env.VITE_TOOLSHED_API_URL || +const TOOLSHED_API_URL = (import.meta as any).env.VITE_TOOLSHED_API_URL || "http://localhost:8000"; export async function getAllBlobs(): Promise { diff --git a/packages/runner/src/builtins/compile-and-run.ts b/packages/runner/src/builtins/compile-and-run.ts index ddd9e539a..5bff7c1ea 100644 --- a/packages/runner/src/builtins/compile-and-run.ts +++ b/packages/runner/src/builtins/compile-and-run.ts @@ -2,7 +2,7 @@ import { type DocImpl } from "../doc.ts"; import { type Action } from "../scheduler.ts"; import { refer } from "merkle-reference"; import { type ReactivityLog } from "../scheduler.ts"; -import { BuiltInCompileAndRunParams } from "@commontools/builder"; +import { BuiltInCompileAndRunParams } from "@commontools/builder/interface"; import type { IRuntime } from "../runtime.ts"; /** diff --git a/packages/runner/src/builtins/llm.ts b/packages/runner/src/builtins/llm.ts index f325836e7..b186079b3 100644 --- a/packages/runner/src/builtins/llm.ts +++ b/packages/runner/src/builtins/llm.ts @@ -4,7 +4,10 @@ import { type Action } from "../scheduler.ts"; import type { IRuntime } from "../runtime.ts"; import { refer } from "merkle-reference"; import { type ReactivityLog } from "../scheduler.ts"; -import { BuiltInLLMParams, BuiltInLLMState } from "@commontools/builder"; +import { + BuiltInLLMParams, + BuiltInLLMState, +} from "@commontools/builder/interface"; const client = new LLMClient(); @@ -38,7 +41,11 @@ export function llm( parentDoc: DocImpl, runtime: IRuntime, // Runtime will be injected by the registration function ): Action { - const pending = runtime.documentMap.getDoc(false, { llm: { pending: cause } }, parentDoc.space); + const pending = runtime.documentMap.getDoc( + false, + { llm: { pending: cause } }, + parentDoc.space, + ); const result = runtime.documentMap.getDoc( undefined, { diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index ce5931ea3..aa62fad46 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -1,4 +1,4 @@ -import { isStreamAlias, TYPE } from "@commontools/builder"; +import { type Cell, isStreamAlias } from "@commontools/builder"; import { getTopFrame, ID, @@ -109,69 +109,73 @@ import { ContextualFlowControl } from "./index.ts"; * @property cellLink The cell link representing this cell. * @returns {CellLink} */ -export interface Cell { - get(): T; - set(value: Cellify | T): void; - send(value: Cellify | T): void; - update | Partial>>( - values: V extends object ? V : never, - ): void; - push( - ...value: Array< - | (T extends Array ? (Cellify | U | DocImpl) : any) - | CellLink - > - ): void; - equals(other: Cell): boolean; - key ? keyof S : keyof T>( - valueKey: K, - ): Cell< - T extends Cell ? S[K & keyof S] : T[K] extends never ? any : T[K] - >; - - asSchema( - schema?: JSONSchema, - ): Cell; - asSchema( - schema: S, - ): Cell>; - withLog(log: ReactivityLog): Cell; - sink(callback: (value: T) => Cancel | undefined | void): Cancel; - getAsQueryResult( - path?: Path, - log?: ReactivityLog, - ): QueryResult>; - getAsCellLink(): CellLink; - getDoc(): DocImpl; - getSourceCell( - schema?: JSONSchema, - ): Cell< - & T - // Add default types for TYPE and `argument`. A more specific type in T will - // take precedence. - & { [TYPE]: string | undefined } - & ("argument" extends keyof T ? unknown : { argument: any }) - >; - getSourceCell( - schema: S, - ): Cell< - & Schema - // Add default types for TYPE and `argument`. A more specific type in - // `schema` will take precedence. - & { [TYPE]: string | undefined } - & ("argument" extends keyof Schema ? unknown - : { argument: any }) - >; - toJSON(): { cell: { "/": string } | undefined; path: PropertyKey[] }; - schema?: JSONSchema; - rootSchema?: JSONSchema; - value: T; - cellLink: CellLink; - entityId: EntityId | undefined; - [isCellMarker]: true; - copyTrap: boolean; +declare module "@commontools/builder/interface" { + interface Cell { + get(): T; + set(value: Cellify | T): void; + send(value: Cellify | T): void; + update | Partial>>( + values: V extends object ? V : never, + ): void; + push( + ...value: Array< + | (T extends Array ? (Cellify | U | DocImpl) : any) + | CellLink + > + ): void; + equals(other: Cell): boolean; + key ? keyof S : keyof T>( + valueKey: K, + ): Cell< + T extends Cell ? S[K & keyof S] : T[K] extends never ? any : T[K] + >; + + asSchema( + schema?: JSONSchema, + ): Cell; + asSchema( + schema: S, + ): Cell>; + withLog(log: ReactivityLog): Cell; + sink(callback: (value: T) => Cancel | undefined | void): Cancel; + getAsQueryResult( + path?: Path, + log?: ReactivityLog, + ): QueryResult>; + getAsCellLink(): CellLink; + getDoc(): DocImpl; + getSourceCell( + schema?: JSONSchema, + ): Cell< + & T + // Add default types for TYPE and `argument`. A more specific type in T will + // take precedence. + & { [TYPE]: string | undefined } + & ("argument" extends keyof T ? unknown : { argument: any }) + >; + getSourceCell( + schema: S, + ): Cell< + & Schema + // Add default types for TYPE and `argument`. A more specific type in + // `schema` will take precedence. + & { [TYPE]: string | undefined } + & ("argument" extends keyof Schema ? unknown + : { argument: any }) + >; + toJSON(): { cell: { "/": string } | undefined; path: PropertyKey[] }; + schema?: JSONSchema; + rootSchema?: JSONSchema; + value: T; + cellLink: CellLink; + entityId: EntityId | undefined; + [isCellMarker]: true; + copyTrap: boolean; + } } +export type { Cell } from "@commontools/builder/interface"; + /** * Cellify is a type utility that allows any part of type T to be wrapped in * Cell<>, and allow any part of T that is currently wrapped in Cell<> to be diff --git a/packages/runner/src/harness/local-build.ts b/packages/runner/src/harness/local-build.ts index 1ea998de9..f7f4a3bd6 100644 --- a/packages/runner/src/harness/local-build.ts +++ b/packages/runner/src/harness/local-build.ts @@ -1,12 +1,11 @@ import ts from "typescript"; import { RawSourceMap, SourceMapConsumer } from "source-map-js"; import * as commonHtml from "@commontools/html"; -import * as commonBuilder from "@commontools/builder"; import * as zod from "zod"; import * as zodToJsonSchema from "zod-to-json-schema"; import * as merkleReference from "merkle-reference"; import turndown from "turndown"; -import { createCellFactory } from "./create-cell.ts"; +import { createBuilder } from "@commontools/builder"; import { type IRuntime } from "../runtime.ts"; let DOMParser: any; @@ -209,7 +208,8 @@ export const tsToExports = async ( case "@commontools/html": return commonHtml; case "@commontools/builder": - return commonBuilder; + case "@commontools/builder/interface": + return createBuilder(config.runtime); case "zod": return zod; case "merkle-reference": @@ -248,10 +248,6 @@ return exports; `; } - // TODO(seefeld): This should eventually be how we create the entire builder - // interface - as context for the eval. - const createCell = createCellFactory(config.runtime); - try { return await eval(wrappedCode)(customRequire); } catch (e) { diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index 425c44fe5..aadc5b599 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -1,28 +1,37 @@ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { - byRef, - handler, - JSONSchema, - lift, - recipe, - TYPE, + type Cell, + createBuilder, + type JSONSchema, } from "@commontools/builder"; import { Runtime } from "../src/runtime.ts"; import { type ErrorWithContext } from "../src/scheduler.ts"; -import { type Cell, isCell } from "../src/cell.ts"; +import { isCell } from "../src/cell.ts"; import { resolveLinks } from "../src/utils.ts"; -import { createCellFactory } from "../src/harness/create-cell.ts"; describe("Recipe Runner", () => { let runtime: Runtime; - let createCell: ReturnType; + let lift: ReturnType["lift"]; + let recipe: ReturnType["recipe"]; + let createCell: ReturnType["createCell"]; + let handler: ReturnType["handler"]; + let byRef: ReturnType["byRef"]; + let TYPE: ReturnType["TYPE"]; beforeEach(() => { runtime = new Runtime({ storageUrl: "volatile://", }); - createCell = createCellFactory(runtime); + const builder = createBuilder(runtime); + ({ + lift, + recipe, + createCell, + handler, + byRef, + TYPE, + } = builder); }); afterEach(async () => { @@ -643,7 +652,9 @@ describe("Recipe Runner", () => { }, } as const satisfies JSONSchema; - const dynamicRecipe = recipe<{ context: Record }>( + const dynamicRecipe = recipe< + { context: Record } + >( "Dynamic Context", ({ context }) => { const result = lift( @@ -651,7 +662,7 @@ describe("Recipe Runner", () => { { type: "number" }, ({ context }) => Object.values(context ?? {}).reduce( - (sum: number, val) => sum + (val as Cell).get(), + (sum: number, val) => sum + val.get(), 0, ), )({ context }); diff --git a/packages/runner/test/schema-lineage.test.ts b/packages/runner/test/schema-lineage.test.ts index 86b566874..007dcbaf0 100644 --- a/packages/runner/test/schema-lineage.test.ts +++ b/packages/runner/test/schema-lineage.test.ts @@ -1,16 +1,24 @@ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; -import { type Cell, isCell } from "../src/cell.ts"; +import { + type Cell, + createBuilder, + type JSONSchema, +} from "@commontools/builder"; +import { isCell } from "../src/cell.ts"; import { Runtime } from "../src/runtime.ts"; -import { type JSONSchema, recipe, UI } from "@commontools/builder"; describe("Schema Lineage", () => { let runtime: Runtime; + let recipe: ReturnType["recipe"]; + let UI: ReturnType["UI"]; beforeEach(() => { runtime = new Runtime({ storageUrl: "volatile://", }); + const builder = createBuilder(runtime); + ({ recipe, UI } = builder); }); afterEach(async () => { @@ -225,11 +233,15 @@ describe("Schema Lineage", () => { describe("Schema propagation end-to-end example", () => { let runtime: Runtime; + let recipe: ReturnType["recipe"]; + let UI: ReturnType["UI"]; beforeEach(() => { runtime = new Runtime({ storageUrl: "volatile://", }); + const builder = createBuilder(runtime); + ({ recipe, UI } = builder); }); afterEach(async () => { diff --git a/recipes/agentic.tsx b/recipes/agentic.tsx index 2743f9d4a..9defe131e 100644 --- a/recipes/agentic.tsx +++ b/recipes/agentic.tsx @@ -10,7 +10,7 @@ import { recipe, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; // Define input schema const InputSchema = { diff --git a/recipes/bgAdmin.tsx b/recipes/bgAdmin.tsx index 5ad02a900..04601d2ae 100644 --- a/recipes/bgAdmin.tsx +++ b/recipes/bgAdmin.tsx @@ -9,7 +9,7 @@ import { recipe, Schema, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const DISABLED_VIA_UI = "Disabled via UI"; diff --git a/recipes/bgCounter.tsx b/recipes/bgCounter.tsx index 7f0e54421..187cee694 100644 --- a/recipes/bgCounter.tsx +++ b/recipes/bgCounter.tsx @@ -9,7 +9,7 @@ import { schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const updaterSchema = { type: "object", diff --git a/recipes/counter.tsx b/recipes/counter.tsx index 9008fa8d8..68258184e 100644 --- a/recipes/counter.tsx +++ b/recipes/counter.tsx @@ -8,7 +8,7 @@ import { schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; // Different way to define the same schema, using 'schema' helper function, // let's as leave off `as const satisfies JSONSchema`. diff --git a/recipes/discord.tsx b/recipes/discord.tsx index a7b57ef36..81fa14ac2 100644 --- a/recipes/discord.tsx +++ b/recipes/discord.tsx @@ -3,15 +3,12 @@ import { cell, derive, handler, - ID, - JSONSchema, + type JSONSchema, NAME, recipe, - Schema, - str, + type Schema, UI, -} from "@commontools/builder"; -import { Cell } from "@commontools/runner"; +} from "@commontools/builder/interface"; // README: // sudo tailscale serve --https=443 localhost:8080 diff --git a/recipes/discordIntegrationsManagement.tsx b/recipes/discordIntegrationsManagement.tsx index c7c085f6a..059d135fb 100644 --- a/recipes/discordIntegrationsManagement.tsx +++ b/recipes/discordIntegrationsManagement.tsx @@ -6,7 +6,7 @@ import { recipe, Schema, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const IntegrationSpaceCharmSchema = { type: "object", diff --git a/recipes/email-date-extractor.tsx b/recipes/email-date-extractor.tsx index 850d6ed1f..b5b57abea 100644 --- a/recipes/email-date-extractor.tsx +++ b/recipes/email-date-extractor.tsx @@ -10,7 +10,7 @@ import { recipe, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; // Reuse email schema from email-summarizer.tsx const EmailProperties = { diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx index 9428308ae..9a0dabb52 100644 --- a/recipes/email-summarizer.tsx +++ b/recipes/email-summarizer.tsx @@ -10,7 +10,7 @@ import { Schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; // Email schema based on Gmail recipe const EmailProperties = { diff --git a/recipes/gcal.tsx b/recipes/gcal.tsx index 91bd0af8b..dc99dab3c 100644 --- a/recipes/gcal.tsx +++ b/recipes/gcal.tsx @@ -13,7 +13,7 @@ import { Schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; import { Cell } from "@commontools/runner"; const Classification = { @@ -286,7 +286,8 @@ export async function fetchCalendar( ) }/events?maxResults=${maxResults}&timeMin=${ encodeURIComponent(now) - }&singleEvents=true&orderBy=startTime`); + }&singleEvents=true&orderBy=startTime`, + ); const listResponse = await googleRequest( auth, google_cal_url, diff --git a/recipes/github.tsx b/recipes/github.tsx index 0bf5c8216..db6ae9fef 100644 --- a/recipes/github.tsx +++ b/recipes/github.tsx @@ -8,7 +8,7 @@ import { recipe, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; import { sleep } from "@commontools/utils/sleep"; interface GitHubCommit { @@ -148,6 +148,7 @@ const refreshCommits = handler({}, { repo: { type: "string" }, owner: { type: "string" }, }, + required: ["commits", "repo", "owner"], }, async (_, state) => { console.log("refreshing commits", JSON.stringify(state, null, 2)); const commits = await getRecentCommits(state.owner, state.repo); diff --git a/recipes/gmail.tsx b/recipes/gmail.tsx index 9b85cd1cb..92cb61bf9 100644 --- a/recipes/gmail.tsx +++ b/recipes/gmail.tsx @@ -13,7 +13,7 @@ import { Schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; import TurndownService from "turndown"; import { Cell } from "@commontools/runner"; diff --git a/recipes/ifelse_test.tsx b/recipes/ifelse_test.tsx index fa2984ea1..e35dec144 100644 --- a/recipes/ifelse_test.tsx +++ b/recipes/ifelse_test.tsx @@ -10,7 +10,7 @@ import { schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const inputSchema = schema({ type: "object", @@ -18,7 +18,7 @@ const inputSchema = schema({ counter: { type: "integer", default: 0, - asCell: true + asCell: true, }, }, default: { counter: 0 }, @@ -34,7 +34,7 @@ const incCounter = handler({}, { properties: { counter: { type: "integer", - asCell: true + asCell: true, }, }, }, (_event, state) => { @@ -42,15 +42,14 @@ const incCounter = handler({}, { const current_count = state.counter.value; console.log("current count=", current_count); state.counter.set(current_count + 1); - } - else { + } else { console.log("counter is undefined, ignoring"); } }); const isEven = lift(({ counter }) => { return counter % 2; -}) +}); export default recipe( inputSchema, @@ -64,8 +63,9 @@ export default recipe( [NAME]: str`counter: ${counter}`, [UI]: (
- {ifElse(isEvenVal, -

counter is odd : {counter}

, + {ifElse( + isEvenVal, +

counter is odd : {counter}

,

counter is even : {counter}

, )}

@@ -78,5 +78,5 @@ export default recipe(

), }; - } + }, ); diff --git a/recipes/recipe.tsx b/recipes/recipe.tsx index 6d8e0d20f..e7ca4b8ba 100644 --- a/recipes/recipe.tsx +++ b/recipes/recipe.tsx @@ -1,12 +1,11 @@ import { h } from "@commontools/html"; import { - derive, handler, JSONSchema, NAME, recipe, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const InputSchema = { type: "object", diff --git a/recipes/rss.tsx b/recipes/rss.tsx index 16e47cfed..3b368f981 100644 --- a/recipes/rss.tsx +++ b/recipes/rss.tsx @@ -1,17 +1,17 @@ import { h } from "@commontools/html"; import { + type Cell, cell, derive, handler, ID, - JSONSchema, + type JSONSchema, NAME, recipe, - Schema, + type Schema, str, UI, -} from "@commontools/builder"; -import { Cell } from "@commontools/runner"; +} from "@commontools/builder/interface"; const FeedItemProperties = { id: { type: "string" }, diff --git a/recipes/simpleValue.tsx b/recipes/simpleValue.tsx index 40cb9aa74..86c2cf788 100644 --- a/recipes/simpleValue.tsx +++ b/recipes/simpleValue.tsx @@ -1,15 +1,14 @@ import { h } from "@commontools/html"; import { - cell, derive, handler, - JSONSchema, + type JSONSchema, NAME, recipe, schema, str, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const updaterSchema = { type: "object", diff --git a/recipes/todo-list.tsx b/recipes/todo-list.tsx index 56c5a8910..8370a866b 100644 --- a/recipes/todo-list.tsx +++ b/recipes/todo-list.tsx @@ -7,7 +7,7 @@ import { recipe, Schema, UI, -} from "@commontools/builder"; +} from "@commontools/builder/interface"; const TodoItemSchema = { type: "object",