Skip to content

Commit 5eee7d6

Browse files
authored
Fix/hierarchical params followups (#2014)
* Fix hierarchical params regressions and restore derive callbacks * fix for case with both destructured local bindings as well as computed keys * deduplicate capture functionality * unify how we create property/param names in different cases * consolidate type traversal for union/intersection types
1 parent 545bbf1 commit 5eee7d6

29 files changed

+954
-321
lines changed

packages/deno-web-test/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ async function copy(src: string, dest: string): Promise<void> {
8181
} else if (stat.isDirectory) {
8282
await copyDir(src, dest);
8383
} else {
84+
await Deno.mkdir(path.dirname(dest), { recursive: true });
8485
await Deno.copyFile(src, dest);
8586
}
8687
}

packages/patterns/default-app.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
Cell,
44
derive,
55
handler,
6-
lift,
76
NAME,
87
navigateTo,
98
recipe,
@@ -99,10 +98,6 @@ const spawnNote = handler<void, void>((_, __) => {
9998
}));
10099
});
101100

102-
const getCharmName = lift(({ charm }: { charm: MinimalCharm }) => {
103-
return charm?.[NAME] || "Untitled Charm";
104-
});
105-
106101
export default recipe<CharmsListInput, CharmsListOutput>(
107102
"DefaultCharmList",
108103
(_) => {
@@ -176,7 +171,7 @@ export default recipe<CharmsListInput, CharmsListOutput>(
176171
<tbody>
177172
{allCharms.map((charm) => (
178173
<tr>
179-
<td>{getCharmName({ charm })}</td>
174+
<td>{charm?.[NAME] || "Untitled Charm"}</td>
180175
<td>
181176
<ct-hstack gap="2">
182177
<ct-button

packages/schema-generator/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"exports": {
55
".": "./src/index.ts",
66
"./interface": "./src/interface.ts",
7-
"./typescript/cell-brand": "./src/typescript/cell-brand.ts"
7+
"./typescript/cell-brand": "./src/typescript/cell-brand.ts",
8+
"./typescript/type-traversal": "./src/typescript/type-traversal.ts"
89
},
910
"imports": {
1011
"typescript": "npm:typescript",

packages/schema-generator/src/typescript/cell-brand.ts

Lines changed: 24 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ts from "typescript";
2+
import { traverseTypeHierarchy } from "./type-traversal.ts";
23

34
export type CellBrand =
45
| "opaque"
@@ -56,50 +57,13 @@ function findCellBrandSymbol(
5657
checker: ts.TypeChecker,
5758
seen: Set<ts.Type>,
5859
): ts.Symbol | undefined {
59-
if (seen.has(type)) return undefined;
60-
seen.add(type);
61-
62-
const direct = getBrandSymbolFromType(type, checker);
63-
if (direct) return direct;
64-
65-
const apparent = checker.getApparentType(type);
66-
if (apparent !== type) {
67-
const fromApparent = findCellBrandSymbol(apparent, checker, seen);
68-
if (fromApparent) return fromApparent;
69-
}
70-
71-
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
72-
const compound = type as ts.UnionOrIntersectionType;
73-
for (const child of compound.types) {
74-
const childSymbol = findCellBrandSymbol(child, checker, seen);
75-
if (childSymbol) return childSymbol;
76-
}
77-
}
78-
79-
if (!(type.flags & ts.TypeFlags.Object)) {
80-
return undefined;
81-
}
82-
83-
const objectType = type as ts.ObjectType;
84-
85-
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
86-
const typeRef = objectType as ts.TypeReference;
87-
if (typeRef.target) {
88-
const fromTarget = findCellBrandSymbol(typeRef.target, checker, seen);
89-
if (fromTarget) return fromTarget;
90-
}
91-
}
92-
93-
if (objectType.objectFlags & ts.ObjectFlags.ClassOrInterface) {
94-
const baseTypes = checker.getBaseTypes(objectType as ts.InterfaceType) ??
95-
[];
96-
for (const base of baseTypes) {
97-
const fromBase = findCellBrandSymbol(base, checker, seen);
98-
if (fromBase) return fromBase;
99-
}
100-
}
101-
102-
return undefined;
60+
return traverseTypeHierarchy(type, {
61+
checker,
62+
checkType: (t) => getBrandSymbolFromType(t, checker),
63+
visitApparentType: true,
64+
visitTypeReferenceTarget: true,
65+
visitBaseTypes: true,
66+
}, seen);
10367
}
10468

10569
export function getCellBrand(
@@ -176,38 +140,23 @@ function extractWrapperTypeReference(
176140
checker: ts.TypeChecker,
177141
seen: Set<ts.Type>,
178142
): ts.TypeReference | undefined {
179-
if (seen.has(type)) return undefined;
180-
seen.add(type);
181-
182-
if (type.flags & ts.TypeFlags.Object) {
183-
const objectType = type as ts.ObjectType;
184-
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
185-
const typeRef = objectType as ts.TypeReference;
186-
const typeArgs = typeRef.typeArguments ??
187-
checker.getTypeArguments(typeRef);
188-
if (typeArgs && typeArgs.length > 0) {
189-
return typeRef;
143+
return traverseTypeHierarchy(type, {
144+
checker,
145+
checkType: (t) => {
146+
if (t.flags & ts.TypeFlags.Object) {
147+
const objectType = t as ts.ObjectType;
148+
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
149+
const typeRef = objectType as ts.TypeReference;
150+
const typeArgs = typeRef.typeArguments ??
151+
checker.getTypeArguments(typeRef);
152+
if (typeArgs && typeArgs.length > 0) {
153+
return typeRef;
154+
}
155+
}
190156
}
191-
}
192-
}
193-
194-
if (type.flags & ts.TypeFlags.Intersection) {
195-
const intersectionType = type as ts.IntersectionType;
196-
for (const constituent of intersectionType.types) {
197-
const ref = extractWrapperTypeReference(constituent, checker, seen);
198-
if (ref) return ref;
199-
}
200-
}
201-
202-
if (type.flags & ts.TypeFlags.Union) {
203-
const unionType = type as ts.UnionType;
204-
for (const member of unionType.types) {
205-
const ref = extractWrapperTypeReference(member, checker, seen);
206-
if (ref) return ref;
207-
}
208-
}
209-
210-
return undefined;
157+
return undefined;
158+
},
159+
}, seen);
211160
}
212161

213162
export function getCellWrapperInfo(
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import ts from "typescript";
2+
3+
export interface TypeTraversalOptions<T> {
4+
/** TypeScript type checker */
5+
readonly checker: ts.TypeChecker;
6+
7+
/** Function to check each type and potentially return a result */
8+
readonly checkType: (type: ts.Type) => T | undefined;
9+
10+
/** How to combine results from union types (default: return first match) */
11+
readonly handleUnion?: "first" | "some" | "every";
12+
13+
/** How to combine results from intersection types (default: return first match) */
14+
readonly handleIntersection?: "first" | "some" | "every";
15+
16+
/** Whether to check apparent type (default: false) */
17+
readonly visitApparentType?: boolean;
18+
19+
/** Whether to traverse base types for interfaces (default: false) */
20+
readonly visitBaseTypes?: boolean;
21+
22+
/** Whether to traverse type reference targets (default: false) */
23+
readonly visitTypeReferenceTarget?: boolean;
24+
}
25+
26+
/**
27+
* Traverses a TypeScript type hierarchy with configurable behavior.
28+
* Handles unions, intersections, type references, and base types.
29+
* Uses a seen set to avoid infinite recursion on circular types.
30+
*
31+
* @param type - The type to traverse
32+
* @param options - Configuration for traversal behavior
33+
* @param seen - Set of already-visited types (for cycle detection)
34+
* @returns Result from checkType function, or undefined if no match found
35+
*/
36+
export function traverseTypeHierarchy<T>(
37+
type: ts.Type,
38+
options: TypeTraversalOptions<T>,
39+
seen = new Set<ts.Type>(),
40+
): T | undefined {
41+
// Prevent infinite recursion on circular types
42+
if (seen.has(type)) return undefined;
43+
seen.add(type);
44+
45+
// Check the type directly first
46+
const direct = options.checkType(type);
47+
if (direct !== undefined) return direct;
48+
49+
// Check apparent type if requested
50+
if (options.visitApparentType) {
51+
const apparent = options.checker.getApparentType(type);
52+
if (apparent !== type) {
53+
const fromApparent = traverseTypeHierarchy(apparent, options, seen);
54+
if (fromApparent !== undefined) return fromApparent;
55+
}
56+
}
57+
58+
// Handle union and intersection types
59+
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
60+
const compound = type as ts.UnionOrIntersectionType;
61+
const isUnion = !!(type.flags & ts.TypeFlags.Union);
62+
const strategy = isUnion
63+
? (options.handleUnion ?? "first")
64+
: (options.handleIntersection ?? "first");
65+
66+
if (strategy === "first") {
67+
// Return first match
68+
for (const child of compound.types) {
69+
const result = traverseTypeHierarchy(child, options, seen);
70+
if (result !== undefined) return result;
71+
}
72+
// No match found in union/intersection
73+
return undefined;
74+
} else if (strategy === "some") {
75+
// Check if any child matches (for boolean results)
76+
const hasMatch = compound.types.some((child) =>
77+
traverseTypeHierarchy(child, options, seen)
78+
);
79+
return (hasMatch as unknown as T);
80+
} else if (strategy === "every") {
81+
// Check if all children match (for boolean results)
82+
const allMatch = compound.types.every((child) =>
83+
traverseTypeHierarchy(child, options, seen)
84+
);
85+
return (allMatch as unknown as T);
86+
}
87+
}
88+
89+
// Handle object types
90+
if (type.flags & ts.TypeFlags.Object) {
91+
const objectType = type as ts.ObjectType;
92+
93+
// Check type reference targets if requested
94+
if (
95+
options.visitTypeReferenceTarget &&
96+
objectType.objectFlags & ts.ObjectFlags.Reference
97+
) {
98+
const typeRef = objectType as ts.TypeReference;
99+
if (typeRef.target) {
100+
const fromTarget = traverseTypeHierarchy(typeRef.target, options, seen);
101+
if (fromTarget !== undefined) return fromTarget;
102+
}
103+
}
104+
105+
// Check base types if requested
106+
if (
107+
options.visitBaseTypes &&
108+
objectType.objectFlags & ts.ObjectFlags.ClassOrInterface
109+
) {
110+
const baseTypes = options.checker.getBaseTypes(
111+
objectType as ts.InterfaceType,
112+
) ?? [];
113+
for (const base of baseTypes) {
114+
const fromBase = traverseTypeHierarchy(base, options, seen);
115+
if (fromBase !== undefined) return fromBase;
116+
}
117+
}
118+
}
119+
120+
return undefined;
121+
}

0 commit comments

Comments
 (0)