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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/deno-web-test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async function copy(src: string, dest: string): Promise<void> {
} else if (stat.isDirectory) {
await copyDir(src, dest);
} else {
await Deno.mkdir(path.dirname(dest), { recursive: true });
await Deno.copyFile(src, dest);
}
}
Expand Down
7 changes: 1 addition & 6 deletions packages/patterns/default-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Cell,
derive,
handler,
lift,
NAME,
navigateTo,
recipe,
Expand Down Expand Up @@ -99,10 +98,6 @@ const spawnNote = handler<void, void>((_, __) => {
}));
});

const getCharmName = lift(({ charm }: { charm: MinimalCharm }) => {
return charm?.[NAME] || "Untitled Charm";
});

export default recipe<CharmsListInput, CharmsListOutput>(
"DefaultCharmList",
(_) => {
Expand Down Expand Up @@ -176,7 +171,7 @@ export default recipe<CharmsListInput, CharmsListOutput>(
<tbody>
{allCharms.map((charm) => (
<tr>
<td>{getCharmName({ charm })}</td>
<td>{charm?.[NAME] || "Untitled Charm"}</td>
<td>
<ct-hstack gap="2">
<ct-button
Expand Down
3 changes: 2 additions & 1 deletion packages/schema-generator/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"exports": {
".": "./src/index.ts",
"./interface": "./src/interface.ts",
"./typescript/cell-brand": "./src/typescript/cell-brand.ts"
"./typescript/cell-brand": "./src/typescript/cell-brand.ts",
"./typescript/type-traversal": "./src/typescript/type-traversal.ts"
},
"imports": {
"typescript": "npm:typescript",
Expand Down
99 changes: 24 additions & 75 deletions packages/schema-generator/src/typescript/cell-brand.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ts from "typescript";
import { traverseTypeHierarchy } from "./type-traversal.ts";

export type CellBrand =
| "opaque"
Expand Down Expand Up @@ -56,50 +57,13 @@ function findCellBrandSymbol(
checker: ts.TypeChecker,
seen: Set<ts.Type>,
): ts.Symbol | undefined {
if (seen.has(type)) return undefined;
seen.add(type);

const direct = getBrandSymbolFromType(type, checker);
if (direct) return direct;

const apparent = checker.getApparentType(type);
if (apparent !== type) {
const fromApparent = findCellBrandSymbol(apparent, checker, seen);
if (fromApparent) return fromApparent;
}

if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
const compound = type as ts.UnionOrIntersectionType;
for (const child of compound.types) {
const childSymbol = findCellBrandSymbol(child, checker, seen);
if (childSymbol) return childSymbol;
}
}

if (!(type.flags & ts.TypeFlags.Object)) {
return undefined;
}

const objectType = type as ts.ObjectType;

if (objectType.objectFlags & ts.ObjectFlags.Reference) {
const typeRef = objectType as ts.TypeReference;
if (typeRef.target) {
const fromTarget = findCellBrandSymbol(typeRef.target, checker, seen);
if (fromTarget) return fromTarget;
}
}

if (objectType.objectFlags & ts.ObjectFlags.ClassOrInterface) {
const baseTypes = checker.getBaseTypes(objectType as ts.InterfaceType) ??
[];
for (const base of baseTypes) {
const fromBase = findCellBrandSymbol(base, checker, seen);
if (fromBase) return fromBase;
}
}

return undefined;
return traverseTypeHierarchy(type, {
checker,
checkType: (t) => getBrandSymbolFromType(t, checker),
visitApparentType: true,
visitTypeReferenceTarget: true,
visitBaseTypes: true,
}, seen);
}

export function getCellBrand(
Expand Down Expand Up @@ -176,38 +140,23 @@ function extractWrapperTypeReference(
checker: ts.TypeChecker,
seen: Set<ts.Type>,
): ts.TypeReference | undefined {
if (seen.has(type)) return undefined;
seen.add(type);

if (type.flags & ts.TypeFlags.Object) {
const objectType = type as ts.ObjectType;
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
const typeRef = objectType as ts.TypeReference;
const typeArgs = typeRef.typeArguments ??
checker.getTypeArguments(typeRef);
if (typeArgs && typeArgs.length > 0) {
return typeRef;
return traverseTypeHierarchy(type, {
checker,
checkType: (t) => {
if (t.flags & ts.TypeFlags.Object) {
const objectType = t as ts.ObjectType;
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
const typeRef = objectType as ts.TypeReference;
const typeArgs = typeRef.typeArguments ??
checker.getTypeArguments(typeRef);
if (typeArgs && typeArgs.length > 0) {
return typeRef;
}
}
}
}
}

if (type.flags & ts.TypeFlags.Intersection) {
const intersectionType = type as ts.IntersectionType;
for (const constituent of intersectionType.types) {
const ref = extractWrapperTypeReference(constituent, checker, seen);
if (ref) return ref;
}
}

if (type.flags & ts.TypeFlags.Union) {
const unionType = type as ts.UnionType;
for (const member of unionType.types) {
const ref = extractWrapperTypeReference(member, checker, seen);
if (ref) return ref;
}
}

return undefined;
return undefined;
},
}, seen);
}

export function getCellWrapperInfo(
Expand Down
121 changes: 121 additions & 0 deletions packages/schema-generator/src/typescript/type-traversal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import ts from "typescript";

export interface TypeTraversalOptions<T> {
/** TypeScript type checker */
readonly checker: ts.TypeChecker;

/** Function to check each type and potentially return a result */
readonly checkType: (type: ts.Type) => T | undefined;

/** How to combine results from union types (default: return first match) */
readonly handleUnion?: "first" | "some" | "every";

/** How to combine results from intersection types (default: return first match) */
readonly handleIntersection?: "first" | "some" | "every";

/** Whether to check apparent type (default: false) */
readonly visitApparentType?: boolean;

/** Whether to traverse base types for interfaces (default: false) */
readonly visitBaseTypes?: boolean;

/** Whether to traverse type reference targets (default: false) */
readonly visitTypeReferenceTarget?: boolean;
}

/**
* Traverses a TypeScript type hierarchy with configurable behavior.
* Handles unions, intersections, type references, and base types.
* Uses a seen set to avoid infinite recursion on circular types.
*
* @param type - The type to traverse
* @param options - Configuration for traversal behavior
* @param seen - Set of already-visited types (for cycle detection)
* @returns Result from checkType function, or undefined if no match found
*/
export function traverseTypeHierarchy<T>(
type: ts.Type,
options: TypeTraversalOptions<T>,
seen = new Set<ts.Type>(),
): T | undefined {
// Prevent infinite recursion on circular types
if (seen.has(type)) return undefined;
seen.add(type);

// Check the type directly first
const direct = options.checkType(type);
if (direct !== undefined) return direct;

// Check apparent type if requested
if (options.visitApparentType) {
const apparent = options.checker.getApparentType(type);
if (apparent !== type) {
const fromApparent = traverseTypeHierarchy(apparent, options, seen);
if (fromApparent !== undefined) return fromApparent;
}
}

// Handle union and intersection types
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
const compound = type as ts.UnionOrIntersectionType;
const isUnion = !!(type.flags & ts.TypeFlags.Union);
const strategy = isUnion
? (options.handleUnion ?? "first")
: (options.handleIntersection ?? "first");

if (strategy === "first") {
// Return first match
for (const child of compound.types) {
const result = traverseTypeHierarchy(child, options, seen);
if (result !== undefined) return result;
}
// No match found in union/intersection
return undefined;
} else if (strategy === "some") {
// Check if any child matches (for boolean results)
const hasMatch = compound.types.some((child) =>
traverseTypeHierarchy(child, options, seen)
);
return (hasMatch as unknown as T);
} else if (strategy === "every") {
// Check if all children match (for boolean results)
const allMatch = compound.types.every((child) =>
traverseTypeHierarchy(child, options, seen)
);
return (allMatch as unknown as T);
}
}

// Handle object types
if (type.flags & ts.TypeFlags.Object) {
const objectType = type as ts.ObjectType;

// Check type reference targets if requested
if (
options.visitTypeReferenceTarget &&
objectType.objectFlags & ts.ObjectFlags.Reference
) {
const typeRef = objectType as ts.TypeReference;
if (typeRef.target) {
const fromTarget = traverseTypeHierarchy(typeRef.target, options, seen);
if (fromTarget !== undefined) return fromTarget;
}
}

// Check base types if requested
if (
options.visitBaseTypes &&
objectType.objectFlags & ts.ObjectFlags.ClassOrInterface
) {
const baseTypes = options.checker.getBaseTypes(
objectType as ts.InterfaceType,
) ?? [];
for (const base of baseTypes) {
const fromBase = traverseTypeHierarchy(base, options, seen);
if (fromBase !== undefined) return fromBase;
}
}
}

return undefined;
}
Loading