Skip to content

Conversation

@anotherjesse
Copy link
Contributor

No description provided.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic found 15 issues across 133 files. Review them in cubic.dev

React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai to give specific feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

commontools_1 is used without being imported or defined, which will cause a ReferenceError at runtime. Use the imported derive function directly or ensure commontools_1 is properly imported.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/opaque-refs/complex-function-with-ternary.expected.ts at line 8:

<comment>commontools_1 is used without being imported or defined, which will cause a ReferenceError at runtime. Use the imported derive function directly or ensure commontools_1 is properly imported.</comment>

<file context>
@@ -0,0 +1,8 @@
+/// &lt;cts-enable /&gt;
+import { cell, derive } from &quot;commontools&quot;;
+const price = cell(100);
+const discount = cell(20);
+const pitance = cell(5);
+const prime = cell(true);
+// Function with complex expression including ternary
+const total = commontools_1.derive({ price, prime, discount, pitance }, ({ price: _v1, prime: _v2, discount: _v3, pitance: _v4 }) =&gt; _v1 - (_v2 ? _v3 : _v4));
\ No newline at end of file
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

JSON Schema has no "undefined" primitive type; emitting this value produces an invalid schema and will fail validation tools.

Prompt for AI agents
Address the following comment on packages/js-runtime/typescript/transformer/schema.ts at line 190:

<comment>JSON Schema has no &quot;undefined&quot; primitive type; emitting this value produces an invalid schema and will fail validation tools.</comment>

<file context>
@@ -0,0 +1,675 @@
+import ts from &quot;typescript&quot;;
+import { addCommonToolsImport, hasCommonToolsImport, removeCommonToolsImport } from &quot;./imports.ts&quot;;
+import { createDebugger, TransformerOptions } from &quot;./debug.ts&quot;;
+
+/**
+ * Transformer that converts TypeScript types to JSONSchema objects.
+ * Transforms `toSchema&lt;T&gt;()` calls into JSONSchema literals.
+ */
+export function createSchemaTransformer(
+  program: ts.Program,
+  options: TransformerOptions = {},
+): ts.TransformerFactory&lt;ts.SourceFile&gt; {
+  const debugLogger = createDebugger(&#39;SchemaTransformer&#39;, options);
+  const checker = program.getTypeChecker();
+
+  return (context: ts.TransformationContext) =&gt; {
+    return (sourceFile: ts.SourceFile) =&gt; {
+      // Check if this file has the /// &lt;cts-enable /&gt; directive
+      const hasCtsEnableDirective = (): boolean =&gt; {
+        const text = sourceFile.getFullText();
+        const tripleSlashDirectives = ts.getLeadingCommentRanges(text, 0) || [];
+
+        for (const comment of tripleSlashDirectives) {
+          const commentText = text.substring(comment.pos, comment.end);
+          if (/^\/\/\/\s*&lt;cts-enable\s*\/&gt;/m.test(commentText)) {
+            return true;
+          }
+        }
+        return false;
+      };
+
+      // Skip transformation if directive is not present
+      if (!hasCtsEnableDirective()) {
+        return sourceFile;
+      }
+
+      let needsJSONSchemaImport = false;
+      let hasTransformedToSchema = false;
+
+      const visit: ts.Visitor = (node) =&gt; {
+        // Look for toSchema&lt;T&gt;() calls
+        if (
+          ts.isCallExpression(node) &amp;&amp;
+          ts.isIdentifier(node.expression) &amp;&amp;
+          node.expression.text === &quot;toSchema&quot; &amp;&amp;
+          node.typeArguments &amp;&amp;
+          node.typeArguments.length === 1
+        ) {
+          const typeArg = node.typeArguments[0];
+          const type = checker.getTypeFromTypeNode(typeArg);
+
+          if (debugLogger.isEnabled() &amp;&amp; typeArg) {
+            debugLogger.log(
+              `Found toSchema&lt;${typeArg.getText()}&gt;() call`
+            );
+          }
+
+          // Extract options from the call arguments
+          const options = node.arguments[0];
+          let optionsObj: any = {};
+          if (options &amp;&amp; ts.isObjectLiteralExpression(options)) {
+            optionsObj = evaluateObjectLiteral(options, checker);
+          }
+
+          // Generate JSONSchema from the type
+          const schema = typeToJsonSchema(type, checker, typeArg);
+
+          // Merge with options
+          const finalSchema = { ...schema, ...optionsObj };
+
+          // Create the AST for the schema object
+          const schemaAst = createSchemaAst(finalSchema, context.factory);
+
+          // Add type assertion: as const satisfies JSONSchema
+          const constAssertion = context.factory.createAsExpression(
+            schemaAst,
+            context.factory.createTypeReferenceNode(
+              context.factory.createIdentifier(&quot;const&quot;),
+              undefined,
+            ),
+          );
+
+          const satisfiesExpression = context.factory.createSatisfiesExpression(
+            constAssertion,
+            context.factory.createTypeReferenceNode(
+              context.factory.createIdentifier(&quot;JSONSchema&quot;),
+              undefined,
+            ),
+          );
+
+          // Mark that we need JSONSchema import
+          if (!hasCommonToolsImport(sourceFile, &quot;JSONSchema&quot;)) {
+            needsJSONSchemaImport = true;
+          }
+
+          hasTransformedToSchema = true;
+          return satisfiesExpression;
+        }
+
+        return ts.visitEachChild(node, visit, context);
+      };
+
+      let result = ts.visitNode(sourceFile, visit) as ts.SourceFile;
+      
+      // Add JSONSchema import if needed
+      if (needsJSONSchemaImport) {
+        result = addCommonToolsImport(result, context.factory, &quot;JSONSchema&quot;);
+      }
+      
+      // Log for debugging the handler-object-literal case
+      if (sourceFile.fileName.includes(&quot;handler-object-literal&quot;)) {
+        console.log(`[SchemaTransformer] Processing ${sourceFile.fileName}`);
+        console.log(`  - hasTransformedToSchema: ${hasTransformedToSchema}`);
+        console.log(`  - hasCommonToolsImport(result, &quot;toSchema&quot;): ${hasCommonToolsImport(result, &quot;toSchema&quot;)}`);
+      }
+      
+      // Remove toSchema import if we transformed all its uses
+      if (hasCommonToolsImport(result, &quot;toSchema&quot;)) {
+        // Check if toSchema is still used anywhere in the transformed code
+        const stillUsesToSchema = containsToSchemaReference(result);
+        
+        if (debugLogger.isEnabled()) {
+          debugLogger.log(`Checking toSchema import removal:`);
+          debugLogger.log(`  - hasCommonToolsImport(toSchema): true`);
+          debugLogger.log(`  - stillUsesToSchema: ${stillUsesToSchema}`);
+        }
+        
+        if (!stillUsesToSchema) {
+          if (debugLogger.isEnabled()) {
+            debugLogger.log(`Removing toSchema import`);
+          }
+          result = removeCommonToolsImport(result, context.factory, &quot;toSchema&quot;);
+        }
+      }
+      
+      return result;
+    };
+  };
+}
+
+/**
+ * Convert a TypeScript type to JSONSchema
+ */
+function typeToJsonSchema(
+  type: ts.Type,
+  checker: ts.TypeChecker,
+  typeNode?: ts.TypeNode,
+): any {
+
+  // If we have a type node, check if it&#39;s a type reference to Default&lt;T, V&gt;
+  if (typeNode &amp;&amp; ts.isTypeReferenceNode(typeNode)) {
+    const typeName = typeNode.typeName;
+    if (ts.isIdentifier(typeName) &amp;&amp; typeName.text === &quot;Default&quot;) {
+      if (typeNode.typeArguments &amp;&amp; typeNode.typeArguments.length &gt;= 2) {
+        const innerTypeNode = typeNode.typeArguments[0];
+        const defaultValueNode = typeNode.typeArguments[1];
+        
+        // Get the inner type
+        const innerType = checker.getTypeFromTypeNode(innerTypeNode);
+        const schema = typeToJsonSchema(innerType, checker, innerTypeNode);
+        
+        // Extract the default value from the type node
+        const defaultValue = extractValueFromTypeNode(defaultValueNode, checker);
+        if (defaultValue !== undefined) {
+          schema.default = defaultValue;
+        }
+        
+        return schema;
+      }
+    }
+  }
+  // Handle primitive types
+  if (type.flags &amp; ts.TypeFlags.String) {
+    return { type: &quot;string&quot; };
+  }
+  if (type.flags &amp; ts.TypeFlags.Number) {
+    return { type: &quot;number&quot; };
+  }
+  if (type.flags &amp; ts.TypeFlags.Boolean) {
+    return { type: &quot;boolean&quot; };
+  }
+  if (type.flags &amp; ts.TypeFlags.BooleanLiteral) {
+    // Handle boolean literals (true/false) as boolean type
+    return { type: &quot;boolean&quot; };
+  }
+  if (type.flags &amp; ts.TypeFlags.Null) {
+    return { type: &quot;null&quot; };
+  }
+  if (type.flags &amp; ts.TypeFlags.Undefined) {
+    return { type: &quot;undefined&quot; };
+  }
+
+  // Handle arrays
+  if (type.symbol &amp;&amp; type.symbol.name === &quot;Array&quot;) {
+    const typeRef = type as ts.TypeReference;
+    if (typeRef.typeArguments &amp;&amp; typeRef.typeArguments.length &gt; 0) {
+      return {
+        type: &quot;array&quot;,
+        items: typeToJsonSchema(typeRef.typeArguments[0], checker),
+      };
+    }
+    return { type: &quot;array&quot; };
+  }
+
+  // Also check if it&#39;s an array type using the checker
+  const typeString = checker.typeToString(type);
+  if (typeString.endsWith(&quot;[]&quot;)) {
+    // Extract the element type
+    const elementTypeString = typeString.slice(0, -2);
+    // This is a simplified approach - in a real implementation we&#39;d need to get the actual element type
+    if (elementTypeString === &quot;string&quot;) {
+      return { type: &quot;array&quot;, items: { type: &quot;string&quot; } };
+    } else if (elementTypeString === &quot;number&quot;) {
+      return { type: &quot;array&quot;, items: { type: &quot;number&quot; } };
+    } else if (elementTypeString === &quot;boolean&quot;) {
+      return { type: &quot;array&quot;, items: { type: &quot;boolean&quot; } };
+    }
+    return { type: &quot;array&quot; };
+  }
+
+  // Handle Date
+  const symbol = type.getSymbol();
+  if (symbol &amp;&amp; symbol.name === &quot;Date&quot;) {
+    return { type: &quot;string&quot;, format: &quot;date-time&quot; };
+  }
+
+  // Check if this is a type reference (e.g., Default&lt;T, V&gt;)
+  if ((type as any).target) {
+    const typeRef = type as ts.TypeReference;
+    const target = (typeRef as any).target;
+    
+    // Check if it&#39;s Default type by checking the symbol name
+    if (target.symbol &amp;&amp; target.symbol.name === &quot;Default&quot;) {
+      // This is a generic type Default&lt;T, V&gt;
+      if (typeRef.typeArguments &amp;&amp; typeRef.typeArguments.length &gt;= 2) {
+        const innerType = typeRef.typeArguments[0];
+        const defaultValueType = typeRef.typeArguments[1];
+        
+        // Get the schema for the inner type
+        const schema = typeToJsonSchema(innerType, checker, typeNode);
+        
+        // Try to extract the literal value from the default value type
+        if (defaultValueType.isNumberLiteral &amp;&amp; defaultValueType.isNumberLiteral()) {
+          // @ts-ignore - accessing value property 
+          schema.default = (defaultValueType as any).value;
+        } else if (defaultValueType.isStringLiteral &amp;&amp; defaultValueType.isStringLiteral()) {
+          // @ts-ignore - accessing value property
+          schema.default = (defaultValueType as any).value;
+        } else if ((defaultValueType as any).intrinsicName === &quot;true&quot;) {
+          schema.default = true;
+        } else if ((defaultValueType as any).intrinsicName === &quot;false&quot;) {
+          schema.default = false;
+        } else if (defaultValueType.flags &amp; ts.TypeFlags.NumberLiteral) {
+          // @ts-ignore - accessing value property 
+          schema.default = (defaultValueType as any).value;
+        } else if (defaultValueType.flags &amp; ts.TypeFlags.StringLiteral) {
+          // @ts-ignore - accessing value property
+          schema.default = (defaultValueType as any).value;
+        } else if (defaultValueType.flags &amp; ts.TypeFlags.BooleanLiteral) {
+          // @ts-ignore - accessing intrinsicName property
+          schema.default = (defaultValueType as any).intrinsicName === &quot;true&quot;;
+        }
+        
+        return schema;
+      }
+    }
+  }
+
+  // Handle Default&lt;T, V&gt; type via symbol check (fallback)
+  if (symbol &amp;&amp; symbol.name === &quot;Default&quot;) {
+    // This is a generic type Default&lt;T, V&gt;
+    const typeRef = type as ts.TypeReference;
+    if (typeRef.typeArguments &amp;&amp; typeRef.typeArguments.length &gt;= 2) {
+      const innerType = typeRef.typeArguments[0];
+      const defaultValueType = typeRef.typeArguments[1];
+      
+      // Get the schema for the inner type
+      const schema = typeToJsonSchema(innerType, checker, typeNode);
+      
+      // Try to extract the literal value from the default value type
+      if (defaultValueType.isNumberLiteral &amp;&amp; defaultValueType.isNumberLiteral()) {
+        // @ts-ignore - accessing value property 
+        schema.default = (defaultValueType as any).value;
+      } else if (defaultValueType.isStringLiteral &amp;&amp; defaultValueType.isStringLiteral()) {
+        // @ts-ignore - accessing value property
+        schema.default = (defaultValueType as any).value;
+      } else if ((defaultValueType as any).intrinsicName === &quot;true&quot;) {
+        schema.default = true;
+      } else if ((defaultValueType as any).intrinsicName === &quot;false&quot;) {
+        schema.default = false;
+      } else if (defaultValueType.flags &amp; ts.TypeFlags.NumberLiteral) {
+        // @ts-ignore - accessing value property 
+        schema.default = (defaultValueType as any).value;
+      } else if (defaultValueType.flags &amp; ts.TypeFlags.StringLiteral) {
+        // @ts-ignore - accessing value property
+        schema.default = (defaultValueType as any).value;
+      } else if (defaultValueType.flags &amp; ts.TypeFlags.BooleanLiteral) {
+        // @ts-ignore - accessing intrinsicName property
+        schema.default = (defaultValueType as any).intrinsicName === &quot;true&quot;;
+      }
+      
+      return schema;
+    }
+    // If we can&#39;t extract type arguments, return a permissive schema
+    return { type: &quot;object&quot;, additionalProperties: true };
+  }
+
+  // Handle object types (interfaces, type literals)
+  if (type.flags &amp; ts.TypeFlags.Object) {
+    const properties: any = {};
+    const required: string[] = [];
+
+    // Get all properties (including inherited ones)
+    const props = type.getProperties();
+    for (const prop of props) {
+      const propName = prop.getName();
+
+      // Skip symbol properties
+      if (propName.startsWith(&quot;__&quot;)) continue;
+
+      const propType = checker.getTypeOfSymbolAtLocation(
+        prop,
+        prop.valueDeclaration || typeNode || prop.declarations?.[0]!,
+      );
+
+      // Check if property is optional
+      const isOptional = prop.flags &amp; ts.SymbolFlags.Optional;
+      if (!isOptional) {
+        required.push(propName);
+      }
+
+      // Check if the property has a type node we can analyze directly
+      const propDecl = prop.valueDeclaration;
+      let propTypeNode: ts.TypeNode | undefined;
+      if (propDecl &amp;&amp; ts.isPropertySignature(propDecl) &amp;&amp; propDecl.type) {
+        propTypeNode = propDecl.type;
+      }
+
+      // Check if the property type is Cell&lt;T&gt;, Stream&lt;T&gt;, or Default&lt;T, V&gt;
+      const propTypeString = checker.typeToString(propType);
+      let actualPropType = propType;
+      let isCell = false;
+      let isStream = false;
+
+      // Check if we have a Cell&lt;T&gt; or Stream&lt;T&gt; type node
+      let innerTypeNode: ts.TypeNode | undefined;
+      
+      // Check if this is a Cell&lt;T&gt; type
+      if (propTypeString.startsWith(&quot;Cell&lt;&quot;) &amp;&amp; propTypeString.endsWith(&quot;&gt;&quot;)) {
+        isCell = true;
+        // Extract the inner type
+        if (propType.symbol &amp;&amp; propType.symbol.getName() === &quot;Cell&quot;) {
+          // This is a type alias, get its type arguments
+          const typeRef = propType as ts.TypeReference;
+          if (typeRef.typeArguments &amp;&amp; typeRef.typeArguments.length &gt; 0) {
+            actualPropType = typeRef.typeArguments[0];
+          }
+        } else if ((propType as any).resolvedTypeArguments) {
+          // Handle resolved type arguments
+          const resolvedArgs = (propType as any).resolvedTypeArguments;
+          if (resolvedArgs.length &gt; 0) {
+            actualPropType = resolvedArgs[0];
+          }
+        }
+        // If we have a type node, extract the inner type node
+        if (propTypeNode &amp;&amp; ts.isTypeReferenceNode(propTypeNode) &amp;&amp; 
+            propTypeNode.typeArguments &amp;&amp; propTypeNode.typeArguments.length &gt; 0) {
+          innerTypeNode = propTypeNode.typeArguments[0];
+        }
+      } // Check if this is a Stream&lt;T&gt; type
+      else if (
+        propTypeString.startsWith(&quot;Stream&lt;&quot;) &amp;&amp; propTypeString.endsWith(&quot;&gt;&quot;)
+      ) {
+        isStream = true;
+        // Extract the inner type
+        const typeRef = propType as ts.TypeReference;
+        if (typeRef.typeArguments &amp;&amp; typeRef.typeArguments.length &gt; 0) {
+          actualPropType = typeRef.typeArguments[0];
+        }
+        // If we have a type node, extract the inner type node
+        if (propTypeNode &amp;&amp; ts.isTypeReferenceNode(propTypeNode) &amp;&amp; 
+            propTypeNode.typeArguments &amp;&amp; propTypeNode.typeArguments.length &gt; 0) {
+          innerTypeNode = propTypeNode.typeArguments[0];
+        }
+      }
+
+      // Get property schema for the actual type (unwrapped if it was Cell/Stream)
+      const propSchema = typeToJsonSchema(actualPropType, checker, innerTypeNode || propTypeNode);
+
+      // Add asCell/asStream flags
+      if (isCell) {
+        propSchema.asCell = true;
+      }
+      if (isStream) {
+        propSchema.asStream = true;
+      }
+
+      properties[propName] = propSchema;
+    }
+
+    const schema: any = {
+      type: &quot;object&quot;,
+      properties,
+    };
+
+    if (required.length &gt; 0) {
+      schema.required = required;
+    }
+
+    return schema;
+  }
+
+  // Handle union types
+  if (type.isUnion()) {
+    const unionTypes = (type as ts.UnionType).types;
+    // Check if it&#39;s a nullable type (T | undefined)
+    const nonNullTypes = unionTypes.filter((t) =&gt;
+      !(t.flags &amp; ts.TypeFlags.Undefined)
+    );
+    
+    // Special handling for boolean | undefined (which appears as false | true | undefined)
+    if (unionTypes.length === 3 &amp;&amp; 
+        unionTypes.filter(t =&gt; t.flags &amp; ts.TypeFlags.BooleanLiteral).length === 2 &amp;&amp;
+        unionTypes.filter(t =&gt; t.flags &amp; ts.TypeFlags.Undefined).length === 1) {
+      // This is boolean | undefined, return boolean schema
+      return { type: &quot;boolean&quot; };
+    }
+    
+    if (nonNullTypes.length === 1 &amp;&amp; unionTypes.length === 2) {
+      // This is an optional type, just return the non-null type schema
+      return typeToJsonSchema(nonNullTypes[0], checker, typeNode);
+    }
+    // Otherwise, use oneOf
+    return {
+      oneOf: unionTypes.map((t) =&gt; typeToJsonSchema(t, checker, typeNode)),
+    };
+  }
+
+  // Default fallback - for &quot;any&quot; type, use a permissive schema
+  return { type: &quot;object&quot;, additionalProperties: true };
+}
+
+/**
+ * Create AST for a schema object
+ */
+function createSchemaAst(schema: any, factory: ts.NodeFactory): ts.Expression {
+  if (schema === null) {
+    return factory.createNull();
+  }
+
+  if (typeof schema === &quot;string&quot;) {
+    return factory.createStringLiteral(schema);
+  }
+
+  if (typeof schema === &quot;number&quot;) {
+    return factory.createNumericLiteral(schema);
+  }
+
+  if (typeof schema === &quot;boolean&quot;) {
+    return schema ? factory.createTrue() : factory.createFalse();
+  }
+
+  if (Array.isArray(schema)) {
+    return factory.createArrayLiteralExpression(
+      schema.map((item) =&gt; createSchemaAst(item, factory)),
+    );
+  }
+
+  if (typeof schema === &quot;object&quot;) {
+    const properties: ts.PropertyAssignment[] = [];
+
+    for (const [key, value] of Object.entries(schema)) {
+      // Handle nested schemas that might be references
+      if (key === &quot;asStream&quot; &amp;&amp; value === true &amp;&amp; schema.type) {
+        // For asStream properties, we need to spread the base schema
+        const baseSchema = { ...schema };
+        delete baseSchema.asStream;
+
+        return factory.createObjectLiteralExpression([
+          factory.createSpreadAssignment(createSchemaAst(baseSchema, factory)),
+          factory.createPropertyAssignment(
+            factory.createIdentifier(&quot;asStream&quot;),
+            factory.createTrue(),
+          ),
+        ]);
+      }
+
+      properties.push(
+        factory.createPropertyAssignment(
+          factory.createIdentifier(key),
+          createSchemaAst(value, factory),
+        ),
+      );
+    }
+
+    return factory.createObjectLiteralExpression(properties, true);
+  }
+
+  return factory.createIdentifier(&quot;undefined&quot;);
+}
+
+/**
+ * Evaluate an object literal to extract its values
+ */
+function evaluateObjectLiteral(
+  node: ts.ObjectLiteralExpression,
+  checker: ts.TypeChecker,
+): any {
+  const result: any = {};
+
+  for (const prop of node.properties) {
+    if (ts.isPropertyAssignment(prop) &amp;&amp; ts.isIdentifier(prop.name)) {
+      const key = prop.name.text;
+      const value = evaluateExpression(prop.initializer, checker);
+      if (value !== undefined) {
+        result[key] = value;
+      }
+    }
+  }
+
+  return result;
+}
+
+// Helper function to evaluate any expression to a literal value
+function evaluateExpression(
+  node: ts.Expression,
+  checker: ts.TypeChecker,
+): any {
+  if (ts.isStringLiteral(node)) {
+    return node.text;
+  } else if (ts.isNumericLiteral(node)) {
+    return Number(node.text);
+  } else if (node.kind === ts.SyntaxKind.TrueKeyword) {
+    return true;
+  } else if (node.kind === ts.SyntaxKind.FalseKeyword) {
+    return false;
+  } else if (node.kind === ts.SyntaxKind.NullKeyword) {
+    return null;
+  } else if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
+    return undefined;
+  } else if (ts.isObjectLiteralExpression(node)) {
+    return evaluateObjectLiteral(node, checker);
+  } else if (ts.isArrayLiteralExpression(node)) {
+    const values: any[] = [];
+    for (const elem of node.elements) {
+      const value = evaluateExpression(elem, checker);
+      // Keep undefined values in arrays
+      values.push(value);
+    }
+    return values;
+  }
+  // Return a special marker for unknown expressions
+  return undefined;
+}
+
+// Helper function to extract values from type nodes (for Default&lt;T, V&gt;)
+function extractValueFromTypeNode(
+  node: ts.TypeNode,
+  checker: ts.TypeChecker,
+): any {
+  // Handle literal type nodes
+  if (ts.isLiteralTypeNode(node)) {
+    const literal = node.literal;
+    if (ts.isStringLiteral(literal)) {
+      return literal.text;
+    } else if (ts.isNumericLiteral(literal)) {
+      return Number(literal.text);
+    } else if (literal.kind === ts.SyntaxKind.TrueKeyword) {
+      return true;
+    } else if (literal.kind === ts.SyntaxKind.FalseKeyword) {
+      return false;
+    } else if (literal.kind === ts.SyntaxKind.NullKeyword) {
+      return null;
+    }
+  }
+  
+  // Handle tuple types (array literals in type position)
+  if (ts.isTupleTypeNode(node)) {
+    const values: any[] = [];
+    for (const elem of node.elements) {
+      const value = extractValueFromTypeNode(elem, checker);
+      values.push(value);
+    }
+    return values;
+  }
+  
+  // Handle type literals (object literals in type position)
+  if (ts.isTypeLiteralNode(node)) {
+    const obj: any = {};
+    for (const member of node.members) {
+      if (ts.isPropertySignature(member) &amp;&amp; member.name &amp;&amp; ts.isIdentifier(member.name)) {
+        const key = member.name.text;
+        if (member.type) {
+          const value = extractValueFromTypeNode(member.type, checker);
+          if (value !== undefined) {
+            obj[key] = value;
+          }
+        }
+      }
+    }
+    return obj;
+  }
+  
+  // Handle array type with literal elements
+  if (ts.isArrayTypeNode(node)) {
+    // For array types like string[], we can&#39;t extract a default value
+    return undefined;
+  }
+  
+  // Handle union types (for nullable types)
+  if (ts.isUnionTypeNode(node)) {
+    // Check if one of the types is null
+    for (const type of node.types) {
+      if (type.kind === ts.SyntaxKind.NullKeyword) {
+        return null;
+      }
+      if (type.kind === ts.SyntaxKind.UndefinedKeyword) {
+        return undefined;
+      }
+    }
+  }
+  
+  // Handle direct null/undefined keywords
+  if (node.kind === ts.SyntaxKind.NullKeyword) {
+    return null;
+  }
+  if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
+    return undefined;
+  }
+  
+  return undefined;
+}
+
+/**
+ * Check if the source file contains any remaining references to toSchema
+ * after transformation.
+ */
+function containsToSchemaReference(sourceFile: ts.SourceFile): boolean {
+  let found = false;
+  let count = 0;
+
+  const visit: ts.Visitor = (node) =&gt; {
+    if (found) return node;
+
+    // Check for toSchema identifier references
+    if (ts.isIdentifier(node) &amp;&amp; node.text === &quot;toSchema&quot;) {
+      count++;
+      // Make sure it&#39;s not part of an import declaration
+      let parent = node.parent;
+      while (parent) {
+        if (ts.isImportDeclaration(parent)) {
+          // This is part of an import, not a usage
+          return node;
+        }
+        parent = parent.parent;
+      }
+      // Found a usage of toSchema outside of imports
+      found = true;
+      return node;
+    }
+
+    return ts.visitEachChild(node, visit, undefined);
+  };
+
+  ts.visitNode(sourceFile, visit);
+  
+  // Debug log for handler-object-literal
+  if (sourceFile.fileName.includes(&quot;handler-object-literal&quot;)) {
+    console.log(`[containsToSchemaReference] ${sourceFile.fileName}:`);
+    console.log(`  - Total toSchema identifiers found: ${count}`);
+    console.log(`  - Found non-import usage: ${found}`);
+  }
+  
+  return found;
+}
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

'commontools_1' is used without being imported or defined, which will cause a ReferenceError at runtime. Use the imported 'derive' function instead.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/opaque-refs/multiple-refs-operations.expected.ts at line 33:

<comment>&#39;commontools_1&#39; is used without being imported or defined, which will cause a ReferenceError at runtime. Use the imported &#39;derive&#39; function instead.</comment>

<file context>
@@ -0,0 +1,33 @@
+/// &lt;cts-enable /&gt;
+import { cell, derive } from &quot;commontools&quot;;
+// Basic string concatenation with multiple OpaqueRefs
+const firstName = cell(&quot;John&quot;);
+const lastName = cell(&quot;Doe&quot;);
+const fullName = commontools_1.derive({ firstName, lastName }, ({ firstName: _v1, lastName: _v2 }) =&gt; _v1 + &quot; &quot; + _v2);
+// String template with multiple OpaqueRefs
+const greeting = commontools_1.derive({ firstName, lastName }, ({ firstName: _v1, lastName: _v2 }) =&gt; `Hello, ${_v1} ${_v2}!`);
+// Multiple OpaqueRefs in different operations
+const x = cell(10);
+const y = cell(20);
+const z = cell(30);
+// Arithmetic with multiple refs
+const sum = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; _v1 + _v2 + _v3);
+const product = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; _v1 * _v2 * _v3);
+const complex = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; (_v1 + _v2) * _v3 - (_v1 * _v2));
+// Boolean operations with multiple refs
+const allPositive = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; _v1 &gt; 0 &amp;&amp; _v2 &gt; 0 &amp;&amp; _v3 &gt; 0);
+const anyNegative = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; _v1 &lt; 0 || _v2 &lt; 0 || _v3 &lt; 0);
+// Mixed operations
+const description = commontools_1.derive({ x, y, z }, ({ x: _v1, y: _v2, z: _v3 }) =&gt; `Sum: ${_v1 + _v2 + _v3}, Product: ${_v1 * _v2 * _v3}`);
+// TODO(ja): Array operations with OpaqueRef arrays
+// const items = cell([1, 2, 3]);
+// const doubled = items.map(x =&gt; x * 2);  // Not yet supported - needs different transformation approach
+// const filtered = items.filter(x =&gt; x &gt; 2); // Not yet supported - needs different transformation approach
+// TODO(ja): Async operations with OpaqueRef
+// const url = cell(&quot;https://api.example.com/data&quot;);
+// const response = await fetch(url); // Not yet supported - async/await with OpaqueRef needs special handling
+// Nested object property access with multiple refs
+const user1 = cell({ name: &quot;Alice&quot;, age: 30 });
+const user2 = cell({ name: &quot;Bob&quot;, age: 25 });
+const combinedAge = commontools_1.derive({ user1_age: user1.age, user2_age: user2.age }, ({ user1_age: _v1, user2_age: _v2 }) =&gt; _v1 + _v2);
+const nameComparison = commontools_1.derive({ user1_name: user1.name, user2_name: user2.name }, ({ user1_name: _v1, user2_name: _v2 }) =&gt; _v1 === _v2);
\ No newline at end of file
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

commontools_1 is not imported or defined in this file, which will cause a ReferenceError at runtime. Use the imported ifElse function directly or import commontools_1 if needed.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/opaque-refs/ternary-with-cell.expected.ts at line 4:

<comment>commontools_1 is not imported or defined in this file, which will cause a ReferenceError at runtime. Use the imported ifElse function directly or import commontools_1 if needed.</comment>

<file context>
@@ -0,0 +1,4 @@
+/// &lt;cts-enable /&gt;
+import { OpaqueRef, ifElse, cell } from &quot;commontools&quot;;
+const isActive = cell&lt;boolean&gt;(false);
+const result = commontools_1.ifElse(isActive, &quot;active&quot;, &quot;inactive&quot;);
\ No newline at end of file
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

increment is a handler function and should be passed as a reference or called with the correct arguments. Calling increment({ value: state.value }) here will immediately invoke the handler during render, not on click.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/ast-transform/counter-recipe.input.tsx at line 29:

<comment>increment is a handler function and should be passed as a reference or called with the correct arguments. Calling increment({ value: state.value }) here will immediately invoke the handler during render, not on click.</comment>

<file context>
@@ -0,0 +1,34 @@
+/// &lt;cts-enable /&gt;
+import { Cell, Default, h, handler, NAME, recipe, str, UI } from &quot;commontools&quot;;
+
+interface CounterState {
+  value: Cell&lt;number&gt;;
+}
+
+interface RecipeState {
+  value: Default&lt;number, 0&gt;;
+}
+
+const increment = handler&lt;unknown, CounterState&gt;((e, state) =&gt; {
+  state.value.set(state.value.get() + 1);
+});
+
+const decrement = handler((_, state: { value: Cell&lt;number&gt; }) =&gt; {
+  state.value.set(state.value.get() - 1);
+});
+
+export default recipe&lt;RecipeState&gt;(&quot;Counter&quot;, (state) =&gt; {
+  return {
+    [NAME]: str`Simple counter: ${state.value}`,
+    [UI]: (
+      &lt;div&gt;
+        &lt;ct-button onClick={decrement(state)}&gt;-&lt;/ct-button&gt;
+        &lt;ul&gt;
+          &lt;li&gt;next number: {state.value + 1}&lt;/li&gt;
+        &lt;/ul&gt;
+        &lt;ct-button onClick={increment({ value: state.value })}&gt;+&lt;/ct-button&gt;
+      &lt;/div&gt;
+    ),
+    value: state.value,
+  };
+});
</file context>
Suggested change
<ct-button onClick={increment({ value: state.value })}>+</ct-button>
<ct-button onClick={() => increment({ value: state.value })}>+</ct-button>

Copy link
Contributor

Choose a reason for hiding this comment

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

commontools_1 is used but not imported or defined in this file, which will cause a ReferenceError at runtime.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/jsx-expressions/complex-expressions.expected.tsx at line 6:

<comment>commontools_1 is used but not imported or defined in this file, which will cause a ReferenceError at runtime.</comment>

<file context>
@@ -0,0 +1,8 @@
+/// &lt;cts-enable /&gt;
+import { OpaqueRef, derive, h } from &quot;commontools&quot;;
+const price: OpaqueRef&lt;number&gt; = {} as any;
+const element = (&lt;div&gt;
+    &lt;p&gt;Price: {price}&lt;/p&gt;
+    &lt;p&gt;With tax: {commontools_1.derive(price, _v1 =&gt; _v1 * 1.1)}&lt;/p&gt;
+    &lt;p&gt;Discount: {commontools_1.derive(price, _v1 =&gt; _v1 - 10)}&lt;/p&gt;
+  &lt;/div&gt;);
\ No newline at end of file
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

TransformationResult is not disposed; failing to call result.dispose() leaks AST nodes and symbols, which can grow memory when many tests run.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/test-utils.ts at line 191:

<comment>TransformationResult is not disposed; failing to call result.dispose() leaks AST nodes and symbols, which can grow memory when many tests run.</comment>

<file context>
@@ -0,0 +1,273 @@
+import ts from &quot;typescript&quot;;
+import {
+  createOpaqueRefTransformer,
+  createSchemaTransformer,
+} from &quot;../typescript/transformer/mod.ts&quot;;
+import { getTypeScriptEnvironmentTypes } from &quot;../mod.ts&quot;;
+import { join } from &quot;@std/path&quot;;
+import { StaticCache } from &quot;@commontools/static&quot;;
+
+// Cache environment types
+let envTypesCache: Record&lt;string, string&gt; | undefined;
+
+/**
+ * Test utility for transforming TypeScript source code with the OpaqueRef transformer.
+ */
+export async function transformSource(
+  source: string,
+  options: {
+    mode?: &quot;transform&quot; | &quot;error&quot;;
+    types?: Record&lt;string, string&gt;;
+    logger?: (message: string) =&gt; void;
+    applySchemaTransformer?: boolean;
+  } = {},
+): Promise&lt;string&gt; {
+  const {
+    mode = &quot;transform&quot;,
+    types = {},
+    logger,
+    applySchemaTransformer = false,
+  } = options;
+
+  // Get environment types if not cached
+  if (!envTypesCache) {
+    const cache = new StaticCache();
+    envTypesCache = await getTypeScriptEnvironmentTypes(cache);
+  }
+
+  // Create a minimal program for testing
+  const fileName = &quot;/test.tsx&quot;;
+  const compilerOptions: ts.CompilerOptions = {
+    target: ts.ScriptTarget.ES2020,
+    module: ts.ModuleKind.CommonJS,
+    jsx: ts.JsxEmit.React,
+    jsxFactory: &quot;h&quot;,
+    strict: true,
+  };
+
+  // Combine all types
+  const allTypes = { ...envTypesCache, ...types };
+
+  // Create a custom compiler host
+  const host: ts.CompilerHost = {
+    getSourceFile: (name) =&gt; {
+      if (name === fileName) {
+        return ts.createSourceFile(name, source, compilerOptions.target!, true);
+      }
+
+      // Check for lib.d.ts -&gt; map to es2023
+      if (name === &quot;lib.d.ts&quot; || name.endsWith(&quot;/lib.d.ts&quot;)) {
+        return ts.createSourceFile(
+          name,
+          allTypes.es2023 || &quot;&quot;,
+          compilerOptions.target!,
+          true,
+        );
+      }
+
+      // Handle type files
+      if (allTypes[name]) {
+        return ts.createSourceFile(
+          name,
+          allTypes[name],
+          compilerOptions.target!,
+          true,
+        );
+      }
+      // Check for commontools.d.ts without path
+      const baseName = name.split(&quot;/&quot;).pop();
+      if (baseName &amp;&amp; allTypes[baseName]) {
+        return ts.createSourceFile(
+          name,
+          allTypes[baseName],
+          compilerOptions.target!,
+          true,
+        );
+      }
+      return undefined;
+    },
+    writeFile: () =&gt; {},
+    getCurrentDirectory: () =&gt; &quot;/&quot;,
+    getDirectories: () =&gt; [],
+    fileExists: (name) =&gt; {
+      if (name === fileName) return true;
+      if (name === &quot;lib.d.ts&quot; || name.endsWith(&quot;/lib.d.ts&quot;)) return true;
+      if (allTypes[name]) return true;
+      const baseName = name.split(&quot;/&quot;).pop();
+      if (baseName &amp;&amp; allTypes[baseName]) return true;
+      return false;
+    },
+    readFile: (name) =&gt; {
+      if (name === fileName) return source;
+      if (name === &quot;lib.d.ts&quot; || name.endsWith(&quot;/lib.d.ts&quot;)) {
+        return allTypes.es2023;
+      }
+      if (allTypes[name]) return allTypes[name];
+      const baseName = name.split(&quot;/&quot;).pop();
+      if (baseName &amp;&amp; allTypes[baseName]) return allTypes[baseName];
+      return undefined;
+    },
+    getCanonicalFileName: (name) =&gt; name,
+    useCaseSensitiveFileNames: () =&gt; true,
+    getNewLine: () =&gt; &quot;\n&quot;,
+    getDefaultLibFileName: () =&gt; &quot;lib.d.ts&quot;,
+    resolveModuleNames: (moduleNames, containingFile) =&gt; {
+      return moduleNames.map((name) =&gt; {
+        if (name === &quot;commontools&quot; &amp;&amp; types[&quot;commontools.d.ts&quot;]) {
+          return {
+            resolvedFileName: &quot;commontools.d.ts&quot;,
+            extension: ts.Extension.Dts,
+            isExternalLibraryImport: false,
+          };
+        }
+        if (name === &quot;@commontools/common&quot; &amp;&amp; types[&quot;commontools.d.ts&quot;]) {
+          return {
+            resolvedFileName: &quot;commontools.d.ts&quot;,
+            extension: ts.Extension.Dts,
+            isExternalLibraryImport: false,
+          };
+        }
+        return undefined;
+      });
+    },
+    resolveTypeReferenceDirectives: (
+      typeDirectiveNames,
+      containingFile,
+      redirectedReference,
+      options,
+    ) =&gt; {
+      return typeDirectiveNames.map((directive) =&gt; {
+        const name = typeof directive === &quot;string&quot;
+          ? directive
+          : directive.fileName;
+        if (allTypes[name]) {
+          return {
+            primary: true,
+            resolvedFileName: name,
+            extension: ts.Extension.Dts,
+            isExternalLibraryImport: false,
+          };
+        }
+        return undefined;
+      });
+    },
+  };
+
+  // Create the program
+  const program = ts.createProgram([fileName], compilerOptions, host);
+
+  // Check for errors when logger is provided
+  if (logger) {
+    const diagnostics = ts.getPreEmitDiagnostics(program);
+    if (diagnostics.length &gt; 0) {
+      logger(&quot;=== TypeScript Diagnostics ===&quot;);
+      diagnostics.forEach((diagnostic) =&gt; {
+        const message = ts.flattenDiagnosticMessageText(
+          diagnostic.messageText,
+          &quot;\n&quot;,
+        );
+        logger(`${diagnostic.file?.fileName || &quot;unknown&quot;}: ${message}`);
+      });
+      logger(&quot;=== End Diagnostics ===&quot;);
+    }
+  }
+
+  // Create the transformers
+  const transformers: ts.TransformerFactory&lt;ts.SourceFile&gt;[] = [];
+
+  // Always add OpaqueRef transformer first
+  transformers.push(createOpaqueRefTransformer(program, {
+    mode,
+    logger,
+  }));
+
+  // Optionally add schema transformer
+  if (applySchemaTransformer) {
+    transformers.push(createSchemaTransformer(program, {}));
+  }
+
+  // Transform the source file
+  const sourceFile = program.getSourceFile(fileName)!;
+  const result = ts.transform(sourceFile, transformers);
+
+  // Print the result with 2-space indentation to match Deno style
+  const printer = ts.createPrinter({
+    newLine: ts.NewLineKind.LineFeed,
+    removeComments: false,
+  });
+  const output = printer.printFile(result.transformed[0]);
+
+  if (logger) {
+    logger(`\n=== TEST TRANSFORMER OUTPUT ===\n${output}\n=== END OUTPUT ===`);
+  }
+
+  return output;
+}
+
+/**
+ * Test utility for checking if a transformation would occur without actually transforming.
+ */
+export async function checkWouldTransform(
+  source: string,
+  types: Record&lt;string, string&gt; = {},
+): Promise&lt;boolean&gt; {
+  try {
+    await transformSource(source, { mode: &quot;error&quot;, types });
+    return false; // No error means no transformation needed
+  } catch (e) {
+    return true; // Error means transformation would occur
+  }
+}
+
+/**
+ * Load a fixture file from the fixtures directory
+ */
+export async function loadFixture(path: string): Promise&lt;string&gt; {
+  const fixturesDir = join(import.meta.dirname!, &quot;fixtures&quot;);
+  const fullPath = join(fixturesDir, path);
+  return await Deno.readTextFile(fullPath);
+}
+
+/**
+ * Load multiple fixtures as a record
+ */
+export async function loadFixtures(
+  paths: string[],
+): Promise&lt;Record&lt;string, string&gt;&gt; {
+  const fixtures: Record&lt;string, string&gt; = {};
+  for (const path of paths) {
+    fixtures[path] = await loadFixture(path);
+  }
+  return fixtures;
+}
+
+/**
+ * Transform a fixture file
+ */
+export async function transformFixture(
+  fixturePath: string,
+  options?: Parameters&lt;typeof transformSource&gt;[1],
+): Promise&lt;string&gt; {
+  const source = await loadFixture(fixturePath);
+  return transformSource(source, options);
+}
+
+/**
+ * Compare fixture transformations
+ */
+export async function compareFixtureTransformation(
+  inputPath: string,
+  expectedPath: string,
+  options?: Parameters&lt;typeof transformSource&gt;[1],
+): Promise&lt;{ actual: string; expected: string; matches: boolean }&gt; {
+  const [actual, expected] = await Promise.all([
+    transformFixture(inputPath, options),
+    loadFixture(expectedPath),
+  ]);
+
+  return {
+    actual,
+    expected,
+    matches: actual.trim() === expected.trim(),
+  };
+}
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

The adder handler is invoked immediately during render instead of being passed as a callback to onClick. This causes the handler to run on every render rather than on button click.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/ast-transform/recipe-array-map.input.tsx at line 18:

<comment>The adder handler is invoked immediately during render instead of being passed as a callback to onClick. This causes the handler to run on every render rather than on button click.</comment>

<file context>
@@ -0,0 +1,31 @@
+/// &lt;cts-enable /&gt;
+import { Cell, derive, h, handler, NAME, recipe, str, UI } from &quot;commontools&quot;;
+
+const adder = handler((_, state: { values: Cell&lt;string[]&gt; }) =&gt; {
+  state.values.push(Math.random().toString(36).substring(2, 15));
+});
+
+export default recipe&lt;{ values: string[] }&gt;(
+  &quot;Simple Value&quot;,
+  ({ values }) =&gt; {
+    derive(values, (values) =&gt; {
+      console.log(&quot;values#&quot;, values?.length);
+    });
+    return {
+      [NAME]: str`Simple Value: ${values.length || 0}`,
+      [UI]: (
+        &lt;div&gt;
+          &lt;button type=&quot;button&quot; onClick={adder({ values })}&gt;Add Value&lt;/button&gt;
+          &lt;div&gt;
+            {values.map((value, index) =&gt; (
+              &lt;div&gt;
+                {index}: {value}
+              &lt;/div&gt;
+            ))}
+          &lt;/div&gt;
+        &lt;/div&gt;
+      ),
+      values,
+    };
+  },
+);
\ No newline at end of file
</file context>
Suggested change
<button type="button" onClick={adder({ values })}>Add Value</button>
<button type="button" onClick={() => adder({ values })}>Add Value</button>

Copy link
Contributor

Choose a reason for hiding this comment

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

The named import "cell" is never used in this file, which will trigger the TypeScript no-unused-locals compiler error and adds unnecessary noise.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/opaque-refs/handler-with-types.input.ts at line 2:

<comment>The named import &quot;cell&quot; is never used in this file, which will trigger the TypeScript no-unused-locals compiler error and adds unnecessary noise.</comment>

<file context>
@@ -0,0 +1,88 @@
+/// &lt;cts-enable /&gt;
+import { handler, cell, Cell } from &quot;commontools&quot;;
+
+// Test case 1: Simple handler with type parameters
+interface EventType {
+  message: string;
+  count: number;
+}
+
+interface StateType {
+  value: number;
+  items: string[];
+}
+
+const simpleHandler = handler&lt;EventType, StateType&gt;((event, state) =&gt; {
+  state.value = event.count;
+  state.items.push(event.message);
+});
+
+// Test case 2: Handler with Cell types
+interface CellEvent {
+  data: string;
+  cellValue: Cell&lt;number&gt;;
+}
+
+interface CellState {
+  count: Cell&lt;number&gt;;
+  messages: string[];
+}
+
+const cellHandler = handler&lt;CellEvent, CellState&gt;((event, state) =&gt; {
+  state.count.set(event.cellValue.get() + 1);
+  state.messages.push(event.data);
+});
+
+// Test case 3: Handler with nested types
+interface NestedEvent {
+  user: {
+    name: string;
+    age: number;
+  };
+  timestamp: Date;
+}
+
+interface NestedState {
+  users: Array&lt;{name: string, age: number}&gt;;
+  lastUpdate: Date;
+}
+
+const nestedHandler = handler&lt;NestedEvent, NestedState&gt;((event, state) =&gt; {
+  state.users.push(event.user);
+  state.lastUpdate = event.timestamp;
+});
+
+// Test case 4: Handler with optional and union types
+interface ComplexEvent {
+  type: &quot;add&quot; | &quot;remove&quot;;
+  item?: string;
+  priority?: number;
+}
+
+interface ComplexState {
+  items: string[];
+  priorities: Map&lt;string, number&gt;;
+}
+
+const complexHandler = handler&lt;ComplexEvent, ComplexState&gt;((event, state) =&gt; {
+  if (event.type === &quot;add&quot; &amp;&amp; event.item) {
+    state.items.push(event.item);
+    if (event.priority) {
+      state.priorities.set(event.item, event.priority);
+    }
+  }
+});
+
+// Test case 5: Handler with generic constraints
+interface GenericEvent&lt;T&gt; {
+  data: T;
+  id: string;
+}
+
+interface GenericState&lt;T&gt; {
+  items: Map&lt;string, T&gt;;
+}
+
+const genericHandler = handler&lt;GenericEvent&lt;string&gt;, GenericState&lt;string&gt;&gt;((event, state) =&gt; {
+  state.items.set(event.id, event.data);
+});
\ No newline at end of file
</file context>

Copy link
Contributor

Choose a reason for hiding this comment

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

commontools_1 is not imported or defined in this file, which will cause a ReferenceError at runtime. Use the imported derive function directly or ensure commontools_1 is properly imported.

Prompt for AI agents
Address the following comment on packages/js-runtime/test/fixtures/opaque-refs/property-access.expected.ts at line 7:

<comment>commontools_1 is not imported or defined in this file, which will cause a ReferenceError at runtime. Use the imported derive function directly or ensure commontools_1 is properly imported.</comment>

<file context>
@@ -0,0 +1,7 @@
+/// &lt;cts-enable /&gt;
+import { OpaqueRef, derive } from &quot;commontools&quot;;
+interface User {
+    age: number;
+}
+const user: OpaqueRef&lt;User&gt; = {} as any;
+const result = commontools_1.derive(user.age, _v1 =&gt; _v1 + 1);
\ No newline at end of file
</file context>

anotherjesse and others added 25 commits July 17, 2025 14:46
{c1 + c2} or {c1 ? c1 : c2} should work
The tests were failing because TypeScript couldn't resolve types properly,
treating OpaqueRef types as 'any'. Fixed by:
- Loading TypeScript environment types (es2023.d.ts) in test utilities
- Making transformSource and checkWouldTransform async
- Updating all test functions to handle async operations
- Properly mapping lib.d.ts requests to es2023 types

This ensures proper type resolution so the OpaqueRef transformer can
correctly identify and transform OpaqueRef expressions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
The transformer now recursively visits whenTrue and whenFalse branches
before creating ifElse calls, ensuring nested ternaries are properly
transformed. Also strips parentheses from AST to match expected output.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add fixture loading utilities to test-utils.ts
- Create test/fixtures directory structure for transformation tests
- Extract 36 test cases from hardcoded strings into .ts/.tsx fixtures
- Add example test file demonstrating fixture usage
- Document fixture structure and usage in README

This makes tests more maintainable with real TypeScript files that have
proper syntax highlighting, IDE support, and clearer version control diffs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Verifies that expressions like `5 - (sale ? 1 : 0)` where sale is an
OpaqueRef are correctly transformed to derive(sale, _v1 => 5 - (_v1 ? 1 : 0))

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Verifies that array elements containing OpaqueRef operations are
transformed independently. For example, [a + 1, b - 1] becomes
[derive(a, _v1 => _v1 + 1), derive(b, _v1 => _v1 - 1)]

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Document OpaqueRef transformation rules and rationale in fixtures README
- Add "principle of independent reactivity" for array/object transformations
- List future test cases to implement
- Add test fixture for object literals with OpaqueRef operations
- Verify each property transforms independently: { x: a + 1, y: b * 2 }

The principle of independent reactivity ensures minimal recomputation by
making each reactive value depend only on its specific OpaqueRef inputs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update check.sh to explicitly list js-runtime files and directories,
excluding the test/fixtures directory that contains TypeScript fixture
files used for transformer testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Special handling for ternary expressions to check after child transformation
- Transforms (opaque > 5 ? 1 : 2) to ifElse(derive(opaque, _v1 => _v1 > 5), 1, 2)
- Detects when condition contains OpaqueRef or was transformed to derive()
- Add test fixture to verify correct transformation

This ensures ternary expressions are properly transformed to ifElse when
their condition becomes an OpaqueRef through transformation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed the return type of OpaqueRefMethods<T>.get() from OpaqueRef<T> to T.
This fixes the type definition to match the actual runtime behavior where
calling .get() on an OpaqueRef returns the underlying value, not another
OpaqueRef.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added support for transforming function calls that contain OpaqueRef
expressions in their arguments. When a function call has arguments with
OpaqueRef operations (like `a + 1`), the entire call is wrapped in a
derive() function.

Examples:
- someFunction(a + 1, "prefix") → derive(a, _v1 => someFunction(_v1 + 1, "prefix"))
- complexFunction(a * 2, "test") → derive(a, _v1 => complexFunction(_v1 * 2, "test"))

This allows OpaqueRef values to be used in function arguments while
maintaining reactivity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
The transformer now consistently outputs imports in alphabetical order
(cell, derive) instead of (derive, cell).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updated test expectations to match the new behavior where function calls
containing OpaqueRef expressions are wrapped entirely in derive(). This
is more efficient than individual derive calls for each expression.

- Updated JSX expression test to expect pre-extracted values
- Updated ifElse transform test to reflect wrapped recipe calls
- Updated nested ternary test to expect both ternaries transformed
- Fixed TypeScript compiler host type signatures in test-utils

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added the function call transformation test to the fixtures test suite
and cleaned up the array input file to use consistent imports.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Removed console.log statements from test files
- Commented out verbose transformer logging
- Made test expectations more concise

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added test case for: total = price - (prime ? discount : pitance)
This tests the transformation of complex expressions containing ternary
operators within binary operations with OpaqueRef values.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Transforms function calls with OpaqueRef arguments into derive() calls:
- Math.max(a, 10) becomes derive(a, _v1 => Math.max(_v1, 10))
- someFunction(a + 1, "prefix") becomes derive(a, _v1 => someFunction(_v1 + 1, "prefix"))

Also:
- Add support for both "@commontools/common" and legacy "commontools" imports
- Fix import handling to use AMD module alias only for "commontools" imports
- Add test coverage for JSX expression transformations
- Update fixture files to match expected transformation behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Simplify import handling to only support "commontools" imports.
The transformer now consistently uses AMD-style module references
(commontools_1.derive, commontools_1.ifElse) for all commontools imports.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
anotherjesse and others added 5 commits July 17, 2025 14:46
All schema transform test fixtures now include the /// <cts-enable />
directive, except for no-directive.input.ts which specifically tests
behavior without the directive.

This ensures tests pass with the new requirement that transformers
only operate on files with the directive.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add support for extracting default values from complex type literals in the
Default<T, V> type. Previously only primitive literals (strings, numbers,
booleans) were supported.

Now supports:
- Empty arrays: Default<string[], []>
- Array literals: Default<string[], ["a", "b", "c"]>
- Object literals: Default<{x: number}, {x: 42}>
- Nested structures: Default<number[][], [[1, 2], [3, 4]]>
- Null values: Default<string  < /dev/null |  null, null>

The implementation adds extractValueFromTypeNode() which can parse TypeScript
type nodes (TupleType, TypeLiteral, etc.) and extract their literal values
for use as JSON schema defaults.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Change handler API to require explicit opt-in for proxy mode. By default,
handlers now receive read-only state objects that work with the AST
transformer.

Key changes:
- Add optional `options` parameter to handler() with `proxy` boolean
- Update HandlerFunction type with conditional overloads:
  - When proxy: true - handlers receive mutable proxy types
  - When proxy: false/undefined - handlers receive read-only HandlerState types
- HandlerState<T> makes non-Cell/Stream properties readonly
- Update all tests using proxy mutations to include { proxy: true }

This ensures handlers work correctly with the TypeScript AST transformer
by default, while still supporting the proxy style when explicitly requested.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add type overloads to support handler functions that use explicit 'this'
parameter typing. The overloads respect the proxy mode setting:

- With proxy: true - 'this' has mutable proxy types
- With proxy: false/undefined - 'this' should have readonly HandlerState types

Note: There's a TypeScript limitation where explicit 'this' type annotations
override our type transformations. When users write:
  function(this: { counter: { value: number } }, ...)
TypeScript uses that explicit type, bypassing our readonly protections.

This is a known limitation - the runtime still enforces proxy mode correctly,
but compile-time type checking can't catch mutations when 'this' is explicitly
typed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update expected output files to match the actual formatting produced by
the schema transformer when handling complex default values:

- Object defaults are formatted with properties on separate lines
- Array oneOf formatting uses array literal syntax
- Add missing default values for arrays in default-type test
- Remove default: undefined (undefined values don't have defaults)

All js-runtime tests now pass with the enhanced Default type support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
anotherjesse and others added 6 commits July 17, 2025 22:47
- Modified OpaqueRef transformer to only transform operations within JSX expression contexts
- Statement-level transformations (if, while, variable declarations) are no longer performed
- Added helper function isInsideJsxExpression() to check context
- Fixed commontools_1 import alias issue
- Updated test fixtures to reflect correct transformation behavior
- Added applySchemaTransformer option to JSX expression tests
- Updated AST documentation to clarify transformation scope and goals

This is an architectural decision to avoid complex control flow analysis
required for statement-level transformations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Converted all JSX expression test fixtures to use recipe format
- Updated expected outputs to include commontools_1.derive transformations
- All JSX Expression Transformer tests now passing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Restored AMD module alias in getCommonToolsModuleAlias for runtime compatibility
- Updated AST documentation to clarify OpaqueRef transformations limited to JSX

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Added jsx-arithmetic-operations for all math operations
- Added jsx-string-operations for string concat and methods
- Added jsx-conditional-rendering for ternary/ifElse cases
- Added jsx-property-access for object/array access patterns
- Added jsx-function-calls for Math/string/array methods
- Added jsx-complex-mixed for array operations and attributes
- Removed simple-opaque-ref fixture (redundant)
- Updated existing fixtures to match current transformer behavior

These new fixtures provide better coverage of OpaqueRef transformations
in JSX expressions and will replace the non-JSX fixtures.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove ~20 non-JSX fixtures that tested OpaqueRef transformations
outside of JSX contexts. These have been replaced by comprehensive
JSX expression fixtures that test transformations where they actually
apply.

Deleted:
- All fixtures in test/fixtures/opaque-refs/
- ast-transform/binary-operation fixture
- transformations/ifelse/odd-even-ternary fixture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update test infrastructure after limiting OpaqueRef transformations
to JSX expression contexts only:

- Remove deleted fixture directories from fixture-based.test.ts
- Update opaque-ref.test.ts to wrap all operations in JSX contexts
- Fix schema transformer expected output
- Remove redundant CTS-Enable directive test

All tests now pass with JSX-only transformations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants