@@ -6,8 +6,10 @@ import type {
66} from "../interface.ts" ;
77import type { SchemaGenerator } from "../schema-generator.ts" ;
88
9+ type WrapperKind = "Cell" | "Stream" | "OpaqueRef" ;
10+
911/**
10- * Formatter for Common Tools specific types (Cell<T>, Stream<T>, Default<T,V>)
12+ * Formatter for Common Tools specific types (Cell<T>, Stream<T>, OpaqueRef<T>, Default<T,V>)
1113 *
1214 * TypeScript handles alias resolution automatically and we don't need to
1315 * manually traverse alias chains.
@@ -33,19 +35,8 @@ export class CommonToolsFormatter implements TypeFormatter {
3335 }
3436 }
3537
36- // Then handle Cell/Stream via resolved type (works for direct and aliased)
37- if ( type . flags & ts . TypeFlags . Object ) {
38- const objectType = type as ts . ObjectType ;
39- if ( objectType . objectFlags & ts . ObjectFlags . Reference ) {
40- const typeRef = objectType as ts . TypeReference ;
41- const resolvedName = typeRef . target ?. symbol ?. name ;
42- if ( resolvedName === "Cell" || resolvedName === "Stream" ) {
43- return true ;
44- }
45- }
46- }
47-
48- return false ;
38+ // Check if this is a wrapper type (Cell/Stream/OpaqueRef)
39+ return this . getWrapperTypeInfo ( type ) !== undefined ;
4940 }
5041
5142 formatType ( type : ts . Type , context : GenerationContext ) : SchemaDefinition {
@@ -61,37 +52,28 @@ export class CommonToolsFormatter implements TypeFormatter {
6152 }
6253 }
6354
64- // Determine wrapper kind using resolved type for Cell/Stream
65- let resolvedTypename : string | undefined ;
66- if ( type . flags & ts . TypeFlags . Object ) {
67- const objectType = type as ts . ObjectType ;
68- if ( objectType . objectFlags & ts . ObjectFlags . Reference ) {
69- const typeRef = objectType as ts . TypeReference ;
70- resolvedTypename = typeRef . target ?. symbol ?. name ;
71- }
72- }
73-
74- // Handle Cell/Stream using unified wrapper path
75- if ( resolvedTypename === "Cell" || resolvedTypename === "Stream" ) {
76- return this . formatCellOrStream (
77- type as ts . TypeReference ,
55+ // Get wrapper type information (Cell/Stream/OpaqueRef)
56+ const wrapperInfo = this . getWrapperTypeInfo ( type ) ;
57+ if ( wrapperInfo ) {
58+ return this . formatWrapperType (
59+ wrapperInfo . typeRef ,
7860 n ,
7961 context ,
80- resolvedTypename === "Stream" ,
62+ wrapperInfo . kind ,
8163 ) ;
8264 }
8365
8466 const nodeName = this . getTypeRefIdentifierName ( n ) ;
8567 throw new Error (
86- `Unexpected CommonTools type: ${ nodeName || resolvedTypename } ` ,
68+ `Unexpected CommonTools type: ${ nodeName } ` ,
8769 ) ;
8870 }
8971
90- private formatCellOrStream (
72+ private formatWrapperType (
9173 typeRef : ts . TypeReference ,
9274 typeRefNode : ts . TypeNode | undefined ,
9375 context : GenerationContext ,
94- asStream : boolean ,
76+ wrapperKind : WrapperKind ,
9577 ) : SchemaDefinition {
9678 const innerTypeFromType = typeRef . typeArguments ?. [ 0 ] ;
9779 const innerTypeNode = typeRefNode && ts . isTypeReferenceNode ( typeRefNode )
@@ -105,7 +87,7 @@ export class CommonToolsFormatter implements TypeFormatter {
10587 }
10688 if ( ! innerType ) {
10789 throw new Error (
108- `${ asStream ? "Stream" : "Cell" } <T> requires type argument` ,
90+ `${ wrapperKind } <T> requires type argument` ,
10991 ) ;
11092 }
11193
@@ -123,26 +105,71 @@ export class CommonToolsFormatter implements TypeFormatter {
123105 ) ;
124106
125107 // Stream<T>: do not reflect inner Cell-ness; only mark asStream
126- if ( asStream ) {
127- // Do not reflect inner Cell markers at stream level
108+ if ( wrapperKind === "Stream" ) {
128109 const { asCell : _drop , ...rest } = innerSchema as Record < string , unknown > ;
129110 return { ...( rest as any ) , asStream : true } as SchemaDefinition ;
130111 }
131112
132113 // Cell<T>: disallow Cell<Stream<T>> to avoid ambiguous semantics
133- if ( this . isStreamType ( innerType ) ) {
114+ if ( wrapperKind === "Cell" && this . isStreamType ( innerType ) ) {
134115 throw new Error (
135116 "Cell<Stream<T>> is unsupported. Wrap the stream: Cell<{ stream: Stream<T> }>." ,
136117 ) ;
137118 }
138119
120+ // Determine the property name to add based on wrapper kind
121+ const propertyName = wrapperKind === "Cell" ? "asCell" : "asOpaque" ;
122+
139123 // Handle case where innerSchema might be boolean (per JSON Schema spec)
140124 if ( typeof innerSchema === "boolean" ) {
141125 return innerSchema === false
142- ? { asCell : true , not : true } // false = "no value is valid"
143- : { asCell : true } ; // true = "any value is valid"
126+ ? { [ propertyName ] : true , not : true } // false = "no value is valid"
127+ : { [ propertyName ] : true } ; // true = "any value is valid"
144128 }
145- return { ...innerSchema , asCell : true } ;
129+ return { ...innerSchema , [ propertyName ] : true } ;
130+ }
131+
132+ /**
133+ * Get wrapper type information (Cell/Stream/OpaqueRef)
134+ * Handles both direct references and intersection types (e.g., OpaqueRef<"literal">)
135+ * Returns the wrapper kind and the TypeReference needed for formatting
136+ */
137+ private getWrapperTypeInfo (
138+ type : ts . Type ,
139+ ) : { kind : WrapperKind ; typeRef : ts . TypeReference } | undefined {
140+ // Check direct object type reference
141+ if ( type . flags & ts . TypeFlags . Object ) {
142+ const objectType = type as ts . ObjectType ;
143+ if ( objectType . objectFlags & ts . ObjectFlags . Reference ) {
144+ const typeRef = objectType as ts . TypeReference ;
145+ const name = typeRef . target ?. symbol ?. name ;
146+ if ( name === "Cell" || name === "Stream" || name === "OpaqueRef" ) {
147+ return { kind : name , typeRef } ;
148+ }
149+ }
150+ }
151+
152+ // OpaqueRef with literal type arguments becomes an intersection
153+ // e.g., OpaqueRef<"initial"> expands to: OpaqueRefMethods<"initial"> & "initial"
154+ // We need to detect OpaqueRefMethods to handle this case
155+ if ( type . flags & ts . TypeFlags . Intersection ) {
156+ const intersectionType = type as ts . IntersectionType ;
157+ for ( const constituent of intersectionType . types ) {
158+ if ( constituent . flags & ts . TypeFlags . Object ) {
159+ const objectType = constituent as ts . ObjectType ;
160+ if ( objectType . objectFlags & ts . ObjectFlags . Reference ) {
161+ const typeRef = objectType as ts . TypeReference ;
162+ const name = typeRef . target ?. symbol ?. name ;
163+ // OpaqueRefMethods is the internal type that OpaqueRef expands to
164+ if ( name === "OpaqueRefMethods" ) {
165+ return { kind : "OpaqueRef" , typeRef } ;
166+ }
167+ }
168+ }
169+ }
170+ }
171+
172+ return undefined ;
146173 }
147174
148175 private getTypeRefIdentifierName (
0 commit comments