diff --git a/packages/builder/src/built-in.ts b/packages/builder/src/built-in.ts index 6d5c79208..8d2eb3fad 100644 --- a/packages/builder/src/built-in.ts +++ b/packages/builder/src/built-in.ts @@ -40,7 +40,7 @@ export interface BuiltInLLMState { pending: boolean; result?: T; partial?: string; - error: any; + error: unknown; } export const llm = createNodeFactory({ @@ -60,7 +60,7 @@ export const fetchData = createNodeFactory({ options?: RequestInit; result?: T; }>, -) => Opaque<{ pending: boolean; result: T; error: any }>; +) => Opaque<{ pending: boolean; result: T; error: unknown }>; export const streamData = createNodeFactory({ type: "ref", @@ -71,9 +71,9 @@ export const streamData = createNodeFactory({ options?: RequestInit; result?: T; }>, -) => Opaque<{ pending: boolean; result: T; error: any }>; +) => Opaque<{ pending: boolean; result: T; error: unknown }>; -export function ifElse( +export function ifElse( condition: Opaque, ifTrue: Opaque, ifFalse: Opaque, @@ -82,15 +82,15 @@ export function ifElse( type: "ref", implementation: "ifElse", }); - return ifElseFactory([condition, ifTrue, ifFalse]); + return ifElseFactory([condition, ifTrue, ifFalse]) as OpaqueRef; } -let ifElseFactory: NodeFactory<[any, any, any], any> | undefined; +let ifElseFactory: NodeFactory<[unknown, unknown, unknown], any> | undefined; export const navigateTo = createNodeFactory({ type: "ref", implementation: "navigateTo", -}) as (cell: OpaqueRef) => OpaqueRef; +}) as (cell: OpaqueRef) => OpaqueRef; // Example: // str`Hello, ${name}!` @@ -98,14 +98,14 @@ export const navigateTo = createNodeFactory({ // TODO(seefeld): This should be a built-in module export function str( strings: TemplateStringsArray, - ...values: any[] + ...values: unknown[] ): OpaqueRef { const interpolatedString = ({ strings, values, }: { strings: TemplateStringsArray; - values: any[]; + values: unknown[]; }) => strings.reduce( (result, str, i) => result + str + (i < values.length ? values[i] : ""), diff --git a/packages/builder/src/opaque-ref.ts b/packages/builder/src/opaque-ref.ts index d79f7c940..5610575f2 100644 --- a/packages/builder/src/opaque-ref.ts +++ b/packages/builder/src/opaque-ref.ts @@ -16,6 +16,7 @@ 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"; let mapFactory: NodeFactory; @@ -110,8 +111,11 @@ export function opaqueRef( unsafe_getExternal: () => { if (!unsafe_binding) return proxy; const value = unsafe_materialize(unsafe_binding, path); - if (typeof value === "object" && value !== null && value[toOpaqueRef]) { - return value[toOpaqueRef](); + if ( + isRecord(value) && value[toOpaqueRef] && + typeof value[toOpaqueRef] === "function" + ) { + return (value[toOpaqueRef] as () => OpaqueRef)(); } else return proxy; }, map: ( diff --git a/packages/builder/src/recipe.ts b/packages/builder/src/recipe.ts index f85d7e580..973545ecc 100644 --- a/packages/builder/src/recipe.ts +++ b/packages/builder/src/recipe.ts @@ -31,6 +31,7 @@ import { traverseValue, } from "./utils.ts"; import { SchemaWithoutCell } from "./schema-to-ts.ts"; +import { isRecord } from "@commontools/utils/types"; /** Declare a recipe * @@ -187,13 +188,11 @@ function factoryFromRecipe( // Fill in reasonable names for all cells, where possible: // First from results - if (typeof outputs === "object" && outputs !== null) { - Object.entries(outputs).forEach(([key, value]) => { - if ( - isOpaqueRef(value) && !value.export().path.length && - !value.export().name - ) { - value.setName(key); + if (isRecord(outputs)) { + Object.entries(outputs).forEach(([key, value]: [string, unknown]) => { + if (isOpaqueRef(value)) { + const ref = value; // Typescript needs this to avoid type errors + if (!ref.export().path.length && !ref.export().name) ref.setName(key); } }); } @@ -202,7 +201,7 @@ function factoryFromRecipe( cells.forEach((cell) => { if (cell.export().path.length) return; cell.export().nodes.forEach((node: NodeRef) => { - if (typeof node.inputs === "object" && node.inputs !== null) { + if (isRecord(node.inputs)) { Object.entries(node.inputs).forEach(([key, input]) => { if ( isOpaqueRef(input) && input.cell === cell && !cell.export().name diff --git a/packages/builder/src/types.ts b/packages/builder/src/types.ts index b32bebae9..cea2d0048 100644 --- a/packages/builder/src/types.ts +++ b/packages/builder/src/types.ts @@ -32,7 +32,7 @@ export type OpaqueRefMethods = { set(value: Opaque | T): void; key(key: K): OpaqueRef; setDefault(value: Opaque | T): void; - setPreExisting(ref: any): void; + setPreExisting(ref: unknown): void; setName(name: string): void; setSchema(schema: JSONSchema): void; connect(node: NodeRef): void; @@ -42,7 +42,7 @@ export type OpaqueRefMethods = { value?: Opaque; defaultValue?: Opaque; nodes: Set; - external?: any; + external?: unknown; name?: string; schema?: JSONSchema; rootSchema?: JSONSchema; @@ -60,7 +60,7 @@ export type OpaqueRefMethods = { array: T, ) => Opaque, ): Opaque; - toJSON(): any; + toJSON(): unknown; [Symbol.iterator](): Iterator; [Symbol.toPrimitive](hint: string): T; [isOpaqueRefMarker]: true; @@ -68,8 +68,9 @@ export type OpaqueRefMethods = { export const isOpaqueRefMarker = Symbol("isOpaqueRef"); -export function isOpaqueRef(value: any): value is OpaqueRef { - return value && typeof value[isOpaqueRefMarker] === "boolean"; +export function isOpaqueRef(value: unknown): value is OpaqueRef { + return !!value && + typeof (value as OpaqueRef)[isOpaqueRefMarker] === "boolean"; } export type NodeRef = { @@ -80,7 +81,7 @@ export type NodeRef = { }; export type toJSON = { - toJSON(): any; + toJSON(): unknown; }; export type NodeFactory = @@ -118,16 +119,16 @@ 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]?: any; - [ID_FIELD]?: any; + [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]?: any; - readonly [ID_FIELD]?: any; + readonly [ID]?: unknown; + readonly [ID_FIELD]?: unknown; readonly type?: | "object" | "array" @@ -158,14 +159,14 @@ export type JSONSchemaMutable = Mutable; export type Alias = { $alias: { - cell?: any; + cell?: unknown; path: PropertyKey[]; schema?: JSONSchema; rootSchema?: JSONSchema; }; }; -export function isAlias(value: any): value is Alias { +export function isAlias(value: unknown): value is Alias { return isObject(value) && "$alias" in value && isObject(value.$alias) && "path" in value.$alias && Array.isArray(value.$alias.path); @@ -175,8 +176,8 @@ export type StreamAlias = { $stream: true; }; -export function isStreamAlias(value: any): value is StreamAlias { - return !!value && typeof value.$stream === "boolean" && value.$stream; +export function isStreamAlias(value: unknown): value is StreamAlias { + return isObject(value) && "$stream" in value && value.$stream === true; } export type Module = { @@ -191,10 +192,10 @@ export type Handler = Module & { with: (inputs: Opaque) => OpaqueRef; }; -export function isModule(value: any): value is Module { +export function isModule(value: unknown): value is Module { return ( (typeof value === "function" || typeof value === "object") && - typeof value.type === "string" + value !== null && typeof (value as unknown as Module).type === "string" ); } @@ -221,24 +222,24 @@ export type Recipe = { [unsafe_materializeFactory]?: (log: any) => (path: PropertyKey[]) => any; }; -export function isRecipe(value: any): value is Recipe { +export function isRecipe(value: unknown): value is Recipe { return ( (typeof value === "function" || typeof value === "object") && value !== null && - !!value.argumentSchema && - !!value.resultSchema && - !!value.nodes && - Array.isArray(value.nodes) + !!(value as any).argumentSchema && + !!(value as any).resultSchema && + !!(value as any).nodes && + Array.isArray((value as any).nodes) ); } type CanBeOpaqueRef = { [toOpaqueRef]: () => OpaqueRef }; -export function canBeOpaqueRef(value: any): value is CanBeOpaqueRef { +export function canBeOpaqueRef(value: unknown): value is CanBeOpaqueRef { return ( (typeof value === "object" || typeof value === "function") && value !== null && - typeof value[toOpaqueRef] === "function" + typeof (value as any)[toOpaqueRef] === "function" ); } @@ -252,12 +253,13 @@ export type ShadowRef = { shadowOf: OpaqueRef | ShadowRef; }; -export function isShadowRef(value: any): value is ShadowRef { +export function isShadowRef(value: unknown): value is ShadowRef { return ( !!value && typeof value === "object" && "shadowOf" in value && - (isOpaqueRef(value.shadowOf) || isShadowRef(value.shadowOf)) + (isOpaqueRef((value as ShadowRef).shadowOf) || + isShadowRef((value as ShadowRef).shadowOf)) ); } @@ -269,7 +271,7 @@ export type UnsafeBinding = { export type Frame = { parent?: Frame; - cause?: any; + cause?: unknown; generatedIdCounter: number; opaqueRefs: Set>; unsafe_binding?: UnsafeBinding; @@ -281,12 +283,12 @@ export type Static = { [isStaticMarker]: true; }; -export function isStatic(value: any): value is Static { +export function isStatic(value: unknown): value is Static { return typeof value === "object" && value !== null && - value[isStaticMarker] === true; + (value as any)[isStaticMarker] === true; } -export function markAsStatic(value: any): any { - value[isStaticMarker] = true; +export function markAsStatic(value: unknown): unknown { + (value as any)[isStaticMarker] = true; return value; } diff --git a/packages/builder/src/utils.ts b/packages/builder/src/utils.ts index 936859047..0b8f33caf 100644 --- a/packages/builder/src/utils.ts +++ b/packages/builder/src/utils.ts @@ -27,6 +27,7 @@ import { isCellLink, isDoc, } from "@commontools/runner"; +import { isObject, isRecord } from "@commontools/utils/types"; /** * Traverse a value, _not_ entering cells @@ -52,8 +53,7 @@ export function traverseValue( (!isOpaqueRef(value) && !canBeOpaqueRef(value) && !isShadowRef(value) && - typeof value === "object" && - value !== null) || + isRecord(value)) || isRecipe(value) ) { return staticWrap( @@ -105,7 +105,7 @@ export function getValueAtPath(obj: any, path: PropertyKey[]): any { export function hasValueAtPath(obj: any, path: PropertyKey[]): boolean { let current = obj; for (const key of path) { - if (!current || typeof current !== "object" || !(key in current)) { + if (!isRecord(current) || !(key in current)) { return false; } current = current[key]; @@ -115,7 +115,7 @@ export function hasValueAtPath(obj: any, path: PropertyKey[]): boolean { export const deepEqual = (a: any, b: any): boolean => { if (a === b) return true; - if (a && b && typeof a === "object" && typeof b === "object") { + if (isRecord(a) && isRecord(b)) { if (a.constructor !== b.constructor) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); @@ -138,8 +138,8 @@ export function toJSONWithAliases( ): JSONValue | undefined { if (isStatic(value) && !processStatic) { return markAsStatic( - toJSONWithAliases(value, paths, ignoreSelfAliases, path, true), - ); + toJSONWithAliases(value, paths, ignoreSelfAliases, path, true) as any, + ) as JSONValue; } // Convert regular cells to opaque refs else if (canBeOpaqueRef(value)) value = makeOpaqueRef(value); // Convert parent opaque refs to shadow refs @@ -196,7 +196,7 @@ export function toJSONWithAliases( } else if (!("cell" in alias) || typeof alias.cell === "number") { return { $alias: { - cell: (alias.cell ?? 0) + 1, + cell: ((alias.cell as number) ?? 0) + 1, path: alias.path as (string | number)[], }, } satisfies Alias; @@ -211,7 +211,7 @@ export function toJSONWithAliases( ); } - if (typeof value === "object" || isRecipe(value)) { + if (isRecord(value) || isRecipe(value)) { const result: any = {}; let hasValue = false; for (const key in value as any) { @@ -278,11 +278,11 @@ export function createJsonSchema( schema.items = {}; } else { const first = value[0]; - if (first && typeof first === "object" && !Array.isArray(first)) { + if (isObject(first)) { const properties: { [key: string]: any } = {}; for (let i = 0; i < value.length; i++) { const item = value?.[i]; - if (typeof item === "object" && item !== null) { + if (isRecord(item)) { Object.keys(item).forEach((key) => { if (!(key in properties)) { properties[key] = analyzeType( @@ -426,7 +426,7 @@ function attachCfcToOutputs( const cfcSchema: JSONSchema = { ...outputSchema, ifc }; (outputs as OpaqueRef).setSchema(cfcSchema); return; - } else if (typeof outputs === "object" && outputs !== null) { + } else if (isRecord(outputs)) { // Descend into objects and arrays for (const [key, value] of Object.entries(outputs)) { attachCfcToOutputs(value, cfc, lubClassification); diff --git a/packages/deno-vite-plugin/src/prefixPlugin.ts b/packages/deno-vite-plugin/src/prefixPlugin.ts index fa1c10b63..c741dee00 100644 --- a/packages/deno-vite-plugin/src/prefixPlugin.ts +++ b/packages/deno-vite-plugin/src/prefixPlugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "vite"; +import { type Plugin, type ResolvedConfig } from "vite"; import { DenoResolveResult, resolveDeno, @@ -14,10 +14,10 @@ export default function denoPrefixPlugin( return { name: "deno:prefix", enforce: "pre", - configResolved(config) { + configResolved(config: ResolvedConfig) { root = config.root; }, - async resolveId(id, importer) { + async resolveId(id: string, importer: string | undefined) { if (id.startsWith("npm:")) { const resolved = await resolveDeno(id, root); if (resolved === null) return; diff --git a/packages/deno-vite-plugin/src/resolvePlugin.ts b/packages/deno-vite-plugin/src/resolvePlugin.ts index 0e0eb92bf..c47d2d2c8 100644 --- a/packages/deno-vite-plugin/src/resolvePlugin.ts +++ b/packages/deno-vite-plugin/src/resolvePlugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "vite"; +import { type Plugin, type ResolvedConfig } from "vite"; import { type DenoMediaType, type DenoResolveResult, @@ -17,16 +17,16 @@ export default function denoPlugin( return { name: "deno", - configResolved(config) { + configResolved(config: ResolvedConfig) { root = config.root; }, - async resolveId(id, importer) { + async resolveId(id: string, importer: string | undefined) { // The "pre"-resolve plugin already resolved it if (isDenoSpecifier(id)) return; return await resolveViteSpecifier(id, cache, root, importer); }, - async load(id) { + async load(id: string) { if (!isDenoSpecifier(id)) return; const { loader, resolved } = parseDenoSpecifier(id); diff --git a/packages/js-runtime/interface.ts b/packages/js-runtime/interface.ts index 6c0efb4d8..8610f3b3c 100644 --- a/packages/js-runtime/interface.ts +++ b/packages/js-runtime/interface.ts @@ -9,8 +9,8 @@ export class CompilerError extends Error { // A reference to a runtime value from a `JsIsolate`. export interface JsValue { - invoke(...args: any[]): JsValue; - inner(): any; + invoke(...args: unknown[]): JsValue; + inner(): unknown; asObject(): object; isObject(): boolean; } diff --git a/packages/jumble/src/main.tsx b/packages/jumble/src/main.tsx index f4cc8f5fc..00abeb310 100644 --- a/packages/jumble/src/main.tsx +++ b/packages/jumble/src/main.tsx @@ -31,6 +31,12 @@ 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; diff --git a/packages/memory/traverse.ts b/packages/memory/traverse.ts index c91ccba12..c53a5d831 100644 --- a/packages/memory/traverse.ts +++ b/packages/memory/traverse.ts @@ -166,8 +166,8 @@ export abstract class BaseObjectTraverser return Object.fromEntries( Object.entries(value).map(( [k, v], - ): any => [k, this.traverseDAG(doc, docRoot, v, tracker)]), - ); + ) => [k, this.traverseDAG(doc, docRoot, v, tracker)]), + ) as JSONValue; } finally { tracker.exit(value); } @@ -233,7 +233,7 @@ export function getAtPath( ); } let cursor = fact; - for (const [index, part] of path.entries()) { + for (const [_index, part] of path.entries()) { // TODO(@ubik2) Call toJSON on object if it's a function? if (isPointer(cursor)) { [doc, docRoot, cursor] = followPointer( @@ -247,7 +247,7 @@ export function getAtPath( const cursorObj = cursor as JSONObject; cursor = cursorObj[part] as JSONValue; } else if (Array.isArray(cursor)) { - cursor = elementAt(cursor, part); + cursor = elementAt(cursor, part) as JSONValue | undefined; } else { // we can only descend into pointers, objects, and arrays return [doc, docRoot, undefined]; @@ -352,7 +352,7 @@ export function getPointerInfo(value: JSONObject): CellTarget { return { path: [], cellTarget: undefined }; } -export function isPointer(value: any): boolean { +export function isPointer(value: unknown): boolean { return (isAlias(value) || isJSONCellLink(value)); } @@ -362,20 +362,23 @@ export function isPointer(value: any): boolean { * @param {any} value - The value to check. * @returns {boolean} */ -function isJSONCellLink(value: any): value is JSONCellLink { +function isJSONCellLink(value: unknown): value is JSONCellLink { return (isObject(value) && "cell" in value && isObject(value.cell) && "/" in value.cell && "path" in value && Array.isArray(value.path)); } -export function indexFromPath(array: unknown[], path: string): any { +export function indexFromPath( + array: unknown[], + path: string, +): number | undefined { const number = new Number(path).valueOf(); return (Number.isInteger(number) && number >= 0 && number < array.length) ? number : undefined; } -export function elementAt(array: unknown[], path: string): any { +export function elementAt(array: unknown[], path: string): unknown { const index = indexFromPath(array, path); return (index === undefined) ? undefined : array[index]; } diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 9020349f1..ce5931ea3 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -18,6 +18,7 @@ import { } from "./utils.ts"; import type { ReactivityLog } from "./scheduler.ts"; import { type EntityId } from "./doc-map.ts"; +import { isObject, isRecord } from "@commontools/utils/types"; import { type Cancel, isCancel, useCancelGroup } from "./cancel.ts"; import { validateAndTransform } from "./schema.ts"; import { type Schema } from "@commontools/builder"; @@ -303,7 +304,7 @@ function createRegularCell( ), send: (newValue: Cellify) => self.set(newValue), update: (values: Cellify>) => { - if (typeof values !== "object" || values === null) { + if (!isRecord(values)) { throw new Error("Can't update with non-object value"); } for (const [key, value] of Object.entries(values)) { @@ -331,9 +332,8 @@ function createRegularCell( const valuesToWrite = values.map((value: any) => { if ( !isCell(value) && !isCellLink(value) && !isDoc(value) && - !Array.isArray(value) && typeof value === "object" && - value !== null && - value[ID] === undefined && getTopFrame() + isObject(value) && + (value as { [ID]?: unknown })[ID] === undefined && getTopFrame() ) { return { [ID]: getTopFrame()!.generatedIdCounter++, @@ -362,9 +362,9 @@ function createRegularCell( // Hacky retry logic for push only. See storage.ts for details on this // retry approach and what we should really be doing instead. if (!ref.cell.retry) ref.cell.retry = []; - ref.cell.retry.push((newBaseValue: any[]) => { + ref.cell.retry.push((newBaseValue) => { // Unlikely, but maybe the conflict reset to undefined? - if (newBaseValue === undefined) { + if (!Array.isArray(newBaseValue)) { newBaseValue = Array.isArray(schema?.default) ? schema.default : []; } @@ -373,7 +373,7 @@ function createRegularCell( const newValues = JSON.parse(JSON.stringify(appended)); // Reappend the new values. - return [...newBaseValue, ...newValues]; + return [...(newBaseValue as unknown[]), ...newValues]; }); }, equals: (other: Cell) => @@ -498,8 +498,7 @@ function subscribeToReferencedDocs( * @returns {boolean} */ export function isCell(value: any): value is Cell { - return typeof value === "object" && value !== null && - value[isCellMarker] === true; + return isRecord(value) && value[isCellMarker] === true; } const isCellMarker = Symbol("isCell"); @@ -510,8 +509,7 @@ const isCellMarker = Symbol("isCell"); * @returns True if the value is a Stream */ export function isStream(value: any): value is Stream { - return typeof value === "object" && value !== null && - value[isStreamMarker] === true; + return isRecord(value) && value[isStreamMarker] === true; } const isStreamMarker = Symbol("isStream"); @@ -524,7 +522,6 @@ const isStreamMarker = Symbol("isStream"); */ export function isCellLink(value: any): value is CellLink { return ( - typeof value === "object" && value !== null && isDoc(value.cell) && - Array.isArray(value.path) + isRecord(value) && isDoc(value.cell) && Array.isArray(value.path) ); } diff --git a/packages/runner/src/doc-map.ts b/packages/runner/src/doc-map.ts index 9d9f43fc1..dc902f838 100644 --- a/packages/runner/src/doc-map.ts +++ b/packages/runner/src/doc-map.ts @@ -7,6 +7,7 @@ import { import { type CellLink, isCell, isCellLink } from "./cell.ts"; import { refer } from "merkle-reference"; import type { IDocumentMap, IRuntime } from "./runtime.ts"; +import { isRecord } from "@commontools/utils/types"; export type EntityId = { "/": string | Uint8Array; @@ -34,11 +35,14 @@ export function createRef( seen.add(obj); // Don't traverse into ids. - if (typeof obj === "object" && obj !== null && "/" in obj) return obj; + if (isRecord(obj) && "/" in obj) return obj; // If there is a .toJSON method, replace obj with it, then descend. + // TODO(seefeld): We have to accept functions for now as the recipe factory + // is a function and has a .toJSON method. But we plan to move away from + // that kind of serialization anyway, so once we did, remove this. if ( - (typeof obj === "object" || typeof obj === "function") && obj !== null && + (isRecord(obj) || typeof obj === "function") && typeof obj.toJSON === "function" ) { obj = obj.toJSON() ?? obj; @@ -54,7 +58,7 @@ export function createRef( // If referencing other docs, return their ids (or random as fallback). if (isDoc(obj) || isCell(obj)) return obj.entityId ?? crypto.randomUUID(); else if (Array.isArray(obj)) return obj.map(traverse); - else if (typeof obj === "object" && obj !== null) { + else if (isRecord(obj)) { return Object.fromEntries( Object.entries(obj).map(([key, value]) => [key, traverse(value)]), ); @@ -76,7 +80,7 @@ export function getEntityId(value: any): { "/": string } | undefined { if (typeof value === "string") { return value.startsWith("{") ? JSON.parse(value) : { "/": value }; } - if (typeof value === "object" && value !== null && "/" in value) { + if (isRecord(value) && "/" in value) { return JSON.parse(JSON.stringify(value)); } @@ -225,7 +229,7 @@ export class DocumentMap implements IDocumentMap { private generateEntityId(value: any, cause?: any): EntityId { return createRef( - typeof value === "object" && value !== null + isRecord(value) ? (value as object) : value !== undefined ? { value } diff --git a/packages/runner/src/doc.ts b/packages/runner/src/doc.ts index a01b796ab..bd86da9f1 100644 --- a/packages/runner/src/doc.ts +++ b/packages/runner/src/doc.ts @@ -18,6 +18,7 @@ import { import { type EntityId } from "./doc-map.ts"; import type { IRuntime } from "./runtime.ts"; import { type ReactivityLog } from "./scheduler.ts"; +import { isRecord } from "@commontools/utils/types"; import { type Cancel } from "./cancel.ts"; import { arrayEqual } from "./utils.ts"; import { ContextualFlowControl } from "./index.ts"; @@ -422,8 +423,7 @@ export function makeOpaqueRef( * @returns {boolean} */ export function isDoc(value: any): value is DocImpl { - return typeof value === "object" && value !== null && - value[isDocMarker] === true; + return isRecord(value) && value[isDocMarker] === true; } const isDocMarker = Symbol("isDoc"); diff --git a/packages/runner/src/query-result-proxy.ts b/packages/runner/src/query-result-proxy.ts index 17bd54158..7aa3d4586 100644 --- a/packages/runner/src/query-result-proxy.ts +++ b/packages/runner/src/query-result-proxy.ts @@ -3,6 +3,7 @@ import { type DocImpl, makeOpaqueRef } from "./doc.ts"; import { type CellLink } from "./cell.ts"; import { type ReactivityLog } from "./scheduler.ts"; import { diffAndUpdate, resolveLinkToValue, setNestedValue } from "./utils.ts"; +import { isRecord } from "@commontools/utils/types"; // Array.prototype's entries, and whether they modify the array enum ArrayMethodType { @@ -60,7 +61,7 @@ export function createQueryResultProxy( log?.reads.push({ cell: valueCell, path: valuePath }); const target = valueCell.getAtPath(valuePath) as any; - if (typeof target !== "object" || target === null) return target; + if (!isRecord(target)) return target; return new Proxy(target as object, { get: (target, prop, receiver) => { @@ -199,7 +200,10 @@ export function createQueryResultProxy( ) { log?.writes.push({ cell: valueCell, path: [...valuePath, i] }); if (valueCell.runtime) { - valueCell.runtime.scheduler.queueEvent({ cell: valueCell, path: [...valuePath, i] }, undefined); + valueCell.runtime.scheduler.queueEvent({ + cell: valueCell, + path: [...valuePath, i], + }, undefined); } } } @@ -247,7 +251,7 @@ const createProxyForArrayValue = ( }; function isProxyForArrayValue(value: any): value is ProxyForArrayValue { - return typeof value === "object" && value !== null && originalIndex in value; + return isRecord(value) && originalIndex in value; } /** @@ -280,8 +284,7 @@ export function getCellLinkOrThrow(value: any): CellLink { * @returns {boolean} */ export function isQueryResult(value: any): value is QueryResult { - return typeof value === "object" && value !== null && - value[getCellLink] !== undefined; + return isRecord(value) && value[getCellLink] !== undefined; } const getCellLink = Symbol("isQueryResultProxy"); diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 084b56c6f..d28e8fbe2 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -38,6 +38,7 @@ import { type CellLink, isCell, isCellLink } from "./cell.ts"; import { isQueryResultForDereferencing } from "./query-result-proxy.ts"; import { getCellLinkOrThrow } from "./query-result-proxy.ts"; import type { IRunner, IRuntime } from "./runtime.ts"; +import { isRecord } from "@commontools/utils/types"; const moduleWrappers = { handler: (fn: (event: any, ...props: any[]) => any) => (props: any) => @@ -89,7 +90,7 @@ export class Runner implements IRunner { let processCell: DocImpl<{ [TYPE]: string; argument?: T; - internal?: { [key: string]: any }; + internal?: JSONValue; resultRef: { cell: DocImpl; path: PropertyKey[] }; }>; @@ -181,18 +182,25 @@ export class Runner implements IRunner { } // Walk the recipe's schema and extract all default values - const defaults = extractDefaultValues(recipe.argumentSchema); - - const internal = { - ...deepCopy((defaults as { internal: any })?.internal), - ...deepCopy((recipe.initial as { internal: any })?.internal), - ...processCell.get()?.internal, - }; + const defaults = extractDefaultValues(recipe.argumentSchema) as Partial; + + // Important to use DeepCopy here, as the resulting object will be modified! + const previousInternal = processCell.get()?.internal; + const internal: JSONValue = Object.assign( + {}, + deepCopy((defaults as unknown as { internal: JSONValue })?.internal), + deepCopy( + isRecord(recipe.initial) && isRecord(recipe.initial.internal) + ? recipe.initial.internal + : {}, + ), + isRecord(previousInternal) ? previousInternal : {}, + ); // Still necessary until we consistently use schema for defaults. // Only do it on first load. if (!processCell.get()?.argument) { - argument = mergeObjects(argument, defaults); + argument = mergeObjects(argument as any, defaults); } const recipeChanged = recipeId !== processCell.get()?.[TYPE]; @@ -218,7 +226,7 @@ export class Runner implements IRunner { // TODO(seefeld): Be smarter about merging in case result changed. But since // we don't yet update recipes, this isn't urgent yet. resultCell.send( - unwrapOneLevelAndBindtoDoc(recipe.result as R, processCell), + unwrapOneLevelAndBindtoDoc(recipe.result as R, processCell), ); } @@ -289,7 +297,7 @@ export class Runner implements IRunner { if (link && link.cell) { const maybePromise = this.runtime.storage.syncCell(link.cell); if (maybePromise instanceof Promise) promises.add(maybePromise); - } else if (typeof value === "object" && value !== null) { + } else if (isRecord(value)) { for (const key in value) syncAllMentionedCells(value[key]); } }; diff --git a/packages/runner/src/schema.ts b/packages/runner/src/schema.ts index deba60be1..a83361c31 100644 --- a/packages/runner/src/schema.ts +++ b/packages/runner/src/schema.ts @@ -1,14 +1,10 @@ -import { isAlias, JSONSchema } from "@commontools/builder"; +import { isAlias, type JSONSchema, type JSONValue } from "@commontools/builder"; import { type DocImpl } from "./doc.ts"; -import { - type CellLink, - createCell, - isCell, - isCellLink, -} from "./cell.ts"; +import { type CellLink, createCell, isCell, isCellLink } from "./cell.ts"; import { type ReactivityLog } from "./scheduler.ts"; import { resolveLinks, resolveLinkToAlias } from "./utils.ts"; import { ContextualFlowControl } from "./index.ts"; +import { isObject, isRecord, type Mutable } from "@commontools/utils/types"; /** * Schemas are mostly a subset of JSONSchema. @@ -128,13 +124,17 @@ function processDefaultValue( ); // This can receive events, but at first nothing will be bound to it. // Normally these get created by a handler call. - return doc.runtime.getImmutableCell(doc.space, { $stream: true }, resolvedSchema, log); + return doc.runtime.getImmutableCell( + doc.space, + { $stream: true }, + resolvedSchema, + log, + ); } // Handle object type defaults if ( - resolvedSchema?.type === "object" && typeof defaultValue === "object" && - defaultValue !== null + resolvedSchema?.type === "object" && isObject(defaultValue) ) { const result: Record = {}; const processedKeys = new Set(); @@ -148,7 +148,7 @@ function processDefaultValue( result[key] = processDefaultValue( doc, [...path, key], - defaultValue[key], + defaultValue[key as keyof typeof defaultValue], propSchema, log, rootSchema, @@ -202,7 +202,7 @@ function processDefaultValue( result[key] = processDefaultValue( doc, [...path, key], - defaultValue[key], + defaultValue[key as keyof typeof defaultValue], additionalPropertiesSchema, log, rootSchema, @@ -237,17 +237,21 @@ function processDefaultValue( function mergeDefaults( schema: JSONSchema | undefined, - defaultValue: any, -): any { - const result = { ...schema }; + defaultValue: Readonly, +): JSONSchema { + const result: Mutable = { ...(schema as Mutable) }; // TODO(seefeld): What's the right thing to do for arrays? if ( result.type === "object" && - typeof result.default === "object" && - typeof defaultValue === "object" - ) result.default = { ...result.default, ...defaultValue }; - else result.default = defaultValue; + isRecord(result.default) && + isRecord(defaultValue) + ) { + result.default = { + ...result.default, + ...defaultValue, + } as Readonly; + } else result.default = defaultValue; return result; } @@ -287,8 +291,7 @@ export function validateAndTransform( // anyOf gets handled here if all options are cells, so we don't read the // data. Below we handle the case where some options are meant to be cells. if ( - typeof schema === "object" && - schema !== null && + isRecord(schema) && ((schema!.asCell || schema!.asStream) || (Array.isArray(resolvedSchema?.anyOf) && resolvedSchema.anyOf.every(( @@ -413,7 +416,7 @@ export function validateAndTransform( rootSchema, seen, ); - } else if (typeof value === "object" && value !== null) { + } else if (isRecord(value)) { let objectCandidates = options.filter((option) => option.type === "object" ); @@ -468,7 +471,7 @@ export function validateAndTransform( if (isCell(result)) { log?.reads.push(...extraLog.reads); return result; // TODO(seefeld): Complain if it's a mix of cells and non-cells? - } else if (typeof result === "object" && result !== null) { + } else if (isRecord(result)) { merged = { ...merged, ...result }; extraReads.push(...extraLog.reads); } else { @@ -516,7 +519,7 @@ export function validateAndTransform( } if (resolvedSchema.type === "object") { - if (typeof value !== "object" || value === null) value = {}; + if (!isRecord(value)) value = {}; const result: Record = {}; diff --git a/packages/runner/src/storage.ts b/packages/runner/src/storage.ts index 68bc7f4a4..1cfdee16a 100644 --- a/packages/runner/src/storage.ts +++ b/packages/runner/src/storage.ts @@ -25,6 +25,7 @@ import { TransactionResult } from "@commontools/memory"; import { refer } from "@commontools/memory/reference"; import { SchemaContext, SchemaNone } from "@commontools/memory/interface"; import type { IRuntime, IStorage } from "./runtime.ts"; +import { isRecord } from "@commontools/utils/types"; export type { Labels }; @@ -331,7 +332,7 @@ export class Storage implements IStorage { return { ...value, cell: value.cell.toJSON() /* = the id */ }; } else if (isStatic(value) && !processStatic) { return { $static: traverse(value, path, true) }; - } else if (typeof value === "object" && value !== null) { + } else if (isRecord(value)) { if (Array.isArray(value)) { return value.map((value, index) => traverse(value, [...path, index])); } else { @@ -395,8 +396,7 @@ export class Storage implements IStorage { // If we see a doc link with just an id, then we replace it with // the actual doc: if ( - typeof value.cell === "object" && - value.cell !== null && + isRecord(value.cell) && "/" in value.cell && Array.isArray(value.path) ) { diff --git a/packages/runner/src/utils.ts b/packages/runner/src/utils.ts index f015cc234..79eb7fcf4 100644 --- a/packages/runner/src/utils.ts +++ b/packages/runner/src/utils.ts @@ -1,11 +1,12 @@ import { - deepEqual, + type Alias, ID, ID_FIELD, isAlias, isOpaqueRef, isStatic, type JSONSchema, + type JSONValue, markAsStatic, type Recipe, unsafe_materializeFactory, @@ -22,23 +23,28 @@ import { import { type CellLink, isCell, isCellLink } from "./cell.ts"; import { type ReactivityLog } from "./scheduler.ts"; import { ContextualFlowControl } from "./index.ts"; +import { isObject, isRecord, type Mutable } from "@commontools/utils/types"; /** * Extracts default values from a JSON schema object. * @param schema - The JSON schema to extract defaults from * @returns An object containing the default values, or undefined if none found */ -export function extractDefaultValues(schema: any): any { +export function extractDefaultValues( + schema: JSONSchema, +): JSONValue | undefined { if (typeof schema !== "object" || schema === null) return undefined; - if (schema.type === "object") { - const obj: any = {}; - for (const [key, value] of Object.entries(schema)) { - if (key === "properties" && typeof value === "object" && value !== null) { - for (const [propKey, propValue] of Object.entries(value)) { - const value = extractDefaultValues(propValue); - if (value !== undefined) obj[propKey] = value; - } + if ( + schema.type === "object" && schema.properties && isObject(schema.properties) + ) { + // Ignore the schema.default if it's not an object, since it's not a valid + // default value for an object. + const obj = deepCopy(isRecord(schema.default) ? schema.default : {}); + for (const [propKey, propSchema] of Object.entries(schema.properties)) { + const value = extractDefaultValues(propSchema); + if (value !== undefined) { + (obj as Record)[propKey] = value; } } @@ -55,13 +61,15 @@ export function extractDefaultValues(schema: any): any { * @param objects - Objects to merge * @returns A merged object, or undefined if no objects provided */ -export function mergeObjects(...objects: any[]): any { +export function mergeObjects( + ...objects: (Partial | undefined)[] +): T { objects = objects.filter((obj) => obj !== undefined); - if (objects.length === 0) return undefined; - if (objects.length === 1) return objects[0]; + if (objects.length === 0) return {} as T; + if (objects.length === 1) return objects[0] as T; const seen = new Set(); - const result: any = {}; + const result: Record = {}; for (const obj of objects) { // If we have a literal value, return it. Same for arrays, since we wouldn't @@ -77,19 +85,23 @@ export function mergeObjects(...objects: any[]): any { isCell(obj) || isStatic(obj) ) { - return obj; + return obj as T; } // Then merge objects, only passing those on that have any values. for (const key of Object.keys(obj)) { if (seen.has(key)) continue; seen.add(key); - const merged = mergeObjects(...objects.map((obj) => obj[key])); + const merged = mergeObjects( + ...objects.map((obj) => + (obj as Record)?.[key] as T[keyof T] + ), + ); if (merged !== undefined) result[key] = merged; } } - return result; + return result as T; } /** @@ -103,12 +115,12 @@ export function mergeObjects(...objects: any[]): any { * @param value - The value to send * @param log - Optional reactivity log */ -export function sendValueToBinding( - doc: DocImpl, - binding: any, - value: any, +export function sendValueToBinding( + doc: DocImpl, + binding: unknown, + value: unknown, log?: ReactivityLog, -) { +): void { if (isAlias(binding)) { const ref = followAliases(binding, doc, log); diffAndUpdate(ref, value, log, { doc, binding }); @@ -118,9 +130,16 @@ export function sendValueToBinding( sendValueToBinding(doc, binding[i], value[i], log); } } - } else if (typeof binding === "object" && binding !== null) { + } else if (isRecord(binding) && isRecord(value)) { for (const key of Object.keys(binding)) { - if (key in value) sendValueToBinding(doc, binding[key], value[key], log); + if (key in value) { + sendValueToBinding( + doc, + binding[key], + value[key], + log, + ); + } } } else { if (binding !== value) { @@ -132,10 +151,10 @@ export function sendValueToBinding( // Sets a value at a path, following aliases and recursing into objects. Returns // success, meaning no frozen docs were in the way. That is, also returns true // if there was no change. -export function setNestedValue( - doc: DocImpl, +export function setNestedValue( + doc: DocImpl, path: PropertyKey[], - value: any, + value: unknown, log?: ReactivityLog, ): boolean { const destValue = doc.getAtPath(path); @@ -147,10 +166,8 @@ export function setNestedValue( // Compare destValue and value, if they are the same, recurse, otherwise write // value with setAtPath if ( - typeof destValue === "object" && - destValue !== null && - typeof value === "object" && - value !== null && + isRecord(destValue) && + isRecord(value) && Array.isArray(value) === Array.isArray(destValue) && !isDoc(value) && !isCellLink(value) && @@ -215,11 +232,11 @@ export function setNestedValue( * @param doc - The doc to bind to. * @returns The unwrapped binding. */ -export function unwrapOneLevelAndBindtoDoc( +export function unwrapOneLevelAndBindtoDoc( binding: T, - doc: DocImpl, + doc: DocImpl, ): T { - function convert(binding: any, processStatic = false): any { + function convert(binding: unknown, processStatic = false): unknown { if (isStatic(binding) && !processStatic) { return markAsStatic(convert(binding, true)); } else if (isAlias(binding)) { @@ -240,8 +257,8 @@ export function unwrapOneLevelAndBindtoDoc( return binding; // Don't enter docs } else if (Array.isArray(binding)) { return binding.map((value) => convert(value)); - } else if (typeof binding === "object" && binding !== null) { - const result: any = Object.fromEntries( + } else if (isRecord(binding)) { + const result: Record = Object.fromEntries( Object.entries(binding).map(([key, value]) => [key, convert(value)]), ); if (binding[unsafe_originalRecipe]) { @@ -253,13 +270,16 @@ export function unwrapOneLevelAndBindtoDoc( return convert(binding) as T; } -export function unsafe_noteParentOnRecipes(recipe: Recipe, binding: any) { - if (typeof binding !== "object" || binding === null) return; - +export function unsafe_noteParentOnRecipes( + recipe: Recipe, + binding: unknown, +): void { // For now we just do top-level bindings - for (const key in binding) { - if (binding[key][unsafe_originalRecipe]) { - binding[key][unsafe_parentRecipe] = recipe; + if (isRecord(binding)) { + for (const key in binding) { + if (isRecord(binding[key]) && binding[key][unsafe_originalRecipe]) { + binding[key][unsafe_parentRecipe] = recipe; + } } } } @@ -279,19 +299,19 @@ export function unsafe_createParentBindings( } // Traverses binding and returns all docs reacheable through aliases. -export function findAllAliasedCells( - binding: any, - doc: DocImpl, +export function findAllAliasedCells( + binding: unknown, + doc: DocImpl, ): CellLink[] { const docs: CellLink[] = []; - function find(binding: any, origDoc: DocImpl) { + function find(binding: unknown, origDoc: DocImpl): void { if (isAlias(binding)) { // Numbered docs are yet to be unwrapped nested recipes. Ignore them. if (typeof binding.$alias.cell === "number") return; - const doc = binding.$alias.cell ?? origDoc; + const doc = (binding.$alias.cell ?? origDoc) as DocImpl; const path = binding.$alias.path; if (docs.find((c) => c.cell === doc && c.path === path)) return; - docs.push({ cell: doc, path }); + docs.push({ cell: doc as DocImpl, path }); find(doc.getAtPath(path), doc); } else if (Array.isArray(binding)) { for (const value of binding) find(value, origDoc); @@ -336,16 +356,16 @@ function createVisits(): Visits { /** * Creates a cache key for a doc and path combination. */ -function createPathCacheKey( - doc: DocImpl, +function createPathCacheKey( + doc: DocImpl, path: PropertyKey[], aliases: boolean = false, ): string { return JSON.stringify([doc.space, doc.toJSON(), path, aliases]); } -export function resolveLinkToValue( - doc: DocImpl, +export function resolveLinkToValue( + doc: DocImpl, path: PropertyKey[], log?: ReactivityLog, schema?: JSONSchema, @@ -356,8 +376,8 @@ export function resolveLinkToValue( return followLinks(ref, log, visits); } -export function resolveLinkToAlias( - doc: DocImpl, +export function resolveLinkToAlias( + doc: DocImpl, path: PropertyKey[], log?: ReactivityLog, schema?: JSONSchema, @@ -373,8 +393,8 @@ export function resolveLinks(ref: CellLink, log?: ReactivityLog): CellLink { return followLinks(ref, log, visits); } -function resolvePath( - doc: DocImpl, +function resolvePath( + doc: DocImpl, path: PropertyKey[], log?: ReactivityLog, schema?: JSONSchema, @@ -537,9 +557,9 @@ function followLinks( return result; } -export function maybeGetCellLink( - value: any, - parent?: DocImpl, +export function maybeGetCellLink( + value: unknown, + parent?: DocImpl, ): CellLink | undefined { if (isQueryResultForDereferencing(value)) return getCellLinkOrThrow(value); else if (isCellLink(value)) return value; @@ -551,16 +571,21 @@ export function maybeGetCellLink( // Follows aliases and returns cell reference describing the last alias. // Only logs interim aliases, not the first one, and not the non-alias value. -export function followAliases( - alias: any, - doc: DocImpl, +export function followAliases( + alias: Alias, + doc: DocImpl, log?: ReactivityLog, ): CellLink { - if (!isAlias(alias)) { + if (isAlias(alias)) { + return followLinks( + { cell: doc, ...alias.$alias } as CellLink, + log, + createVisits(), + true, + ); + } else { throw new Error(`Alias expected: ${JSON.stringify(alias)}`); } - - return followLinks({ cell: doc, ...alias.$alias }, log, createVisits(), true); } /** @@ -580,16 +605,16 @@ export function followAliases( */ export function diffAndUpdate( current: CellLink, - newValue: any, + newValue: unknown, log?: ReactivityLog, - context?: any, + context?: unknown, ): boolean { const changes = normalizeAndDiff(current, newValue, log, context); applyChangeSet(changes, log); return changes.length > 0; } -type ChangeSet = { location: CellLink; value: any }[]; +type ChangeSet = { location: CellLink; value: unknown }[]; /** * Traverses objects and returns an array of changes that should be written. An @@ -614,9 +639,9 @@ type ChangeSet = { location: CellLink; value: any }[]; */ export function normalizeAndDiff( current: CellLink, - newValue: any, + newValue: unknown, log?: ReactivityLog, - context?: any, + context?: unknown, ): ChangeSet { const changes: ChangeSet = []; @@ -629,11 +654,11 @@ export function normalizeAndDiff( // semantically a new item (in fact we otherwise run into compare-and-swap // transaction errors). if ( - typeof newValue === "object" && newValue !== null && + isRecord(newValue) && newValue[ID_FIELD] !== undefined ) { const { [ID_FIELD]: fieldName, ...rest } = newValue; - const id = newValue[fieldName]; + const id = newValue[fieldName as PropertyKey]; if (current.path.length > 1) { const parent = current.cell.getAtPath(current.path.slice(0, -1)); if (Array.isArray(parent)) { @@ -641,8 +666,8 @@ export function normalizeAndDiff( if (isCellLink(v)) { const sibling = v.cell.getAtPath(v.path); if ( - typeof sibling === "object" && sibling !== null && - sibling[fieldName] === id + isRecord(sibling) && + sibling[fieldName as PropertyKey] === id ) { // We found a sibling with the same id, so ... return [ @@ -706,7 +731,7 @@ export function normalizeAndDiff( // Handle ID-based object (convert to entity) if ( - typeof newValue === "object" && newValue !== null && + isRecord(newValue) && newValue[ID] !== undefined ) { const { [ID]: id, ...rest } = newValue; @@ -800,7 +825,7 @@ export function normalizeAndDiff( } // Handle objects - if (typeof newValue === "object" && newValue !== null) { + if (isRecord(newValue)) { // If the current value is not a (regular) object, set it to an empty object // Note that the alias case is handled above if ( @@ -893,26 +918,27 @@ export function applyChangeSet( * We'll want to revisit once iframes become more sophisticated in what they can * express, e.g. we could have the inner shim do some of this work instead. */ -export function addCommonIDfromObjectID(obj: any, fieldName: string = "id") { - function traverse(obj: any) { - if (typeof obj === "object" && obj !== null && fieldName in obj) { +export function addCommonIDfromObjectID( + obj: unknown, + fieldName: string = "id", +): void { + function traverse(obj: unknown): void { + if (isRecord(obj) && fieldName in obj) { obj[ID_FIELD] = fieldName; } if ( - typeof obj === "object" && obj !== null && !isCell(obj) && + isRecord(obj) && !isCell(obj) && !isCellLink(obj) && !isDoc(obj) ) { - Object.values(obj).forEach((v: any) => { - traverse(v); - }); + Object.values(obj).forEach((v) => traverse(v)); } } traverse(obj); } -export function maybeUnwrapProxy(value: any): any { +export function maybeUnwrapProxy(value: unknown): unknown { return isQueryResultForDereferencing(value) ? getCellLinkOrThrow(value) : value; @@ -929,23 +955,26 @@ export function isEqualCellLink(a: CellLink, b: CellLink): boolean { arrayEqual(a.path, b.path); } -export function containsOpaqueRef(value: any): boolean { +export function containsOpaqueRef(value: unknown): boolean { if (isOpaqueRef(value)) return true; if (isCell(value) || isCellLink(value) || isDoc(value)) return false; - if (typeof value === "object" && value !== null) { + if (isRecord(value)) { return Object.values(value).some(containsOpaqueRef); } return false; } -export function deepCopy(value: any): any { +export function deepCopy(value: T): Mutable { if (isQueryResultForDereferencing(value)) { - return deepCopy(getCellLinkOrThrow(value)); + return deepCopy(getCellLinkOrThrow(value)) as unknown as Mutable; } - if (isDoc(value) || isCell(value)) return value; - if (typeof value === "object" && value !== null) { - return Array.isArray(value) ? value.map(deepCopy) : Object.fromEntries( - Object.entries(value).map(([key, value]) => [key, deepCopy(value)]), - ); - } else return value; + if (isDoc(value) || isCell(value)) return value as Mutable; + if (isRecord(value)) { + return Array.isArray(value) + ? value.map(deepCopy) as unknown as Mutable + : Object.fromEntries( + Object.entries(value).map(([key, value]) => [key, deepCopy(value)]), + ) as unknown as Mutable; + // Literal value: + } else return value as Mutable; } diff --git a/packages/runner/test/utils.test.ts b/packages/runner/test/utils.test.ts index 79515b56a..7024b7508 100644 --- a/packages/runner/test/utils.test.ts +++ b/packages/runner/test/utils.test.ts @@ -34,15 +34,15 @@ describe("Utils", () => { describe("extractDefaultValues", () => { it("should extract default values from a schema", () => { const schema = { - type: "object", + type: "object" as const, properties: { - name: { type: "string", default: "John" }, - age: { type: "number", default: 30 }, + name: { type: "string" as const, default: "John" }, + age: { type: "number" as const, default: 30 }, address: { - type: "object", + type: "object" as const, properties: { - street: { type: "string", default: "Main St" }, - city: { type: "string", default: "New York" }, + street: { type: "string" as const, default: "Main St" }, + city: { type: "string" as const, default: "New York" }, }, }, }, @@ -66,7 +66,7 @@ describe("Utils", () => { const obj2 = { b: { y: 20 }, c: 3 }; const obj3 = { a: 4, d: 5 }; - const result = mergeObjects(obj1, obj2, obj3); + const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: 1, b: { x: 10, y: 20 }, @@ -80,7 +80,7 @@ describe("Utils", () => { const obj2 = undefined; const obj3 = { b: 2 }; - const result = mergeObjects(obj1, obj2, obj3); + const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: 1, b: 2 }); }); @@ -106,7 +106,7 @@ describe("Utils", () => { b: { c: 4 }, }; - const result = mergeObjects(obj1, obj2, obj3); + const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: { $alias: { path: [] } }, b: { c: { cell: testCell, path: [] } }, diff --git a/packages/toolshed/routes/ai/spell/handlers/fulfill.ts b/packages/toolshed/routes/ai/spell/handlers/fulfill.ts index 04541e14d..49befa822 100644 --- a/packages/toolshed/routes/ai/spell/handlers/fulfill.ts +++ b/packages/toolshed/routes/ai/spell/handlers/fulfill.ts @@ -4,12 +4,8 @@ import { getAllBlobs } from "@/routes/ai/spell/behavior/effects.ts"; import type { AppRouteHandler } from "@/lib/types.ts"; import type { FulfillSchemaRoute } from "@/routes/ai/spell/spell.routes.ts"; -import { Spell } from "@/routes/ai/spell/spell.ts"; -import { performSearch } from "../behavior/search.ts"; import { Logger } from "@/lib/prefixed-logger.ts"; -import { processSpellSearch } from "@/routes/ai/spell/behavior/spell-search.ts"; import { captureException } from "@sentry/deno"; -import { areSchemaCompatible } from "@/routes/ai/spell/schema-compatibility.ts"; import { generateText } from "@/lib/llm.ts"; import { @@ -20,6 +16,8 @@ import { SchemaFragment, } from "@/routes/ai/spell/schema.ts"; import { extractJSON } from "@/routes/ai/spell/json.ts"; +import { isRecord } from "@commontools/utils/types"; +import { isTemplateLiteral } from "typescript"; export const FulfillSchemaRequestSchema = z.object({ schema: z.record( @@ -305,13 +303,11 @@ function constructSchemaPrompt( } else if (Array.isArray(value)) { // Recursively sanitize array elements sanitized[key] = value.map((item) => - typeof item === "object" && item !== null - ? sanitizeObject(item as Record) - : item + isRecord(item) ? sanitizeObject(item) : item ); - } else if (typeof value === "object" && value !== null) { + } else if (isRecord(value)) { // Recursively sanitize nested objects - sanitized[key] = sanitizeObject(value as Record); + sanitized[key] = sanitizeObject(value); } else { // Keep primitives as-is sanitized[key] = value;