Skip to content

Commit 410e2c8

Browse files
committed
Fix order
1 parent cefff94 commit 410e2c8

File tree

2 files changed

+312
-88
lines changed

2 files changed

+312
-88
lines changed

src/codegen/responsive/ResponsiveCodegen.ts

Lines changed: 115 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,118 @@ function firstMapValue<V>(map: Map<unknown, V>): V {
3535
throw new Error('empty map')
3636
}
3737

38-
function firstMapKey<K>(map: Map<K, unknown>): K {
39-
for (const k of map.keys()) return k
40-
throw new Error('empty map')
41-
}
42-
4338
function firstMapEntry<K, V>(map: Map<K, V>): [K, V] {
4439
for (const entry of map.entries()) return entry
4540
throw new Error('empty map')
4641
}
4742

43+
/**
44+
* Build a stable merged order of child names across multiple variants/breakpoints.
45+
* Uses topological sort on a DAG of ordering constraints from all variants,
46+
* with average-position tie-breaking for deterministic output.
47+
*
48+
* Example: variant A has [Icon, TextA, Arrow], variant B has [Icon, TextB, Arrow]
49+
* → edges: Icon→TextA, TextA→Arrow, Icon→TextB, TextB→Arrow
50+
* → topo sort: [Icon, TextA, TextB, Arrow] (Arrow stays last)
51+
*/
52+
function mergeChildNameOrder(
53+
childrenMaps: Map<unknown, Map<string, NodeTree[]>>,
54+
): string[] {
55+
// Collect distinct child name sequences from each variant
56+
const sequences: string[][] = []
57+
for (const childMap of childrenMaps.values()) {
58+
const seq: string[] = []
59+
for (const name of childMap.keys()) {
60+
seq.push(name)
61+
}
62+
sequences.push(seq)
63+
}
64+
65+
// Collect all unique names
66+
const allNames = new Set<string>()
67+
for (const seq of sequences) {
68+
for (const name of seq) {
69+
allNames.add(name)
70+
}
71+
}
72+
73+
if (allNames.size === 0) return []
74+
if (allNames.size === 1) return [...allNames]
75+
76+
// Build DAG: for each variant, add edge from consecutive distinct names
77+
const edges = new Map<string, Set<string>>()
78+
const inDegree = new Map<string, number>()
79+
for (const name of allNames) {
80+
edges.set(name, new Set())
81+
inDegree.set(name, 0)
82+
}
83+
84+
for (const seq of sequences) {
85+
for (let i = 0; i < seq.length - 1; i++) {
86+
const from = seq[i]
87+
const to = seq[i + 1]
88+
const fromEdges = edges.get(from)
89+
if (fromEdges && !fromEdges.has(to)) {
90+
fromEdges.add(to)
91+
inDegree.set(to, (inDegree.get(to) || 0) + 1)
92+
}
93+
}
94+
}
95+
96+
// Compute average normalized position for tie-breaking
97+
const avgPosition = new Map<string, number>()
98+
for (const name of allNames) {
99+
let totalPos = 0
100+
let count = 0
101+
for (const seq of sequences) {
102+
const idx = seq.indexOf(name)
103+
if (idx >= 0) {
104+
// Normalize to 0..1 range
105+
totalPos += seq.length > 1 ? idx / (seq.length - 1) : 0.5
106+
count++
107+
}
108+
}
109+
avgPosition.set(name, count > 0 ? totalPos / count : 0.5)
110+
}
111+
112+
// Kahn's algorithm with priority-based tie-breaking
113+
const queue: string[] = []
114+
for (const [name, deg] of inDegree) {
115+
if (deg === 0) queue.push(name)
116+
}
117+
// Sort initial queue by average position (stable)
118+
queue.sort((a, b) => (avgPosition.get(a) || 0) - (avgPosition.get(b) || 0))
119+
120+
const result: string[] = []
121+
while (queue.length > 0) {
122+
const node = queue.shift()
123+
if (!node) break
124+
result.push(node)
125+
for (const neighbor of edges.get(node) || []) {
126+
const newDeg = (inDegree.get(neighbor) || 1) - 1
127+
inDegree.set(neighbor, newDeg)
128+
if (newDeg === 0) {
129+
queue.push(neighbor)
130+
// Re-sort to maintain priority order
131+
queue.sort(
132+
(a, b) => (avgPosition.get(a) || 0) - (avgPosition.get(b) || 0),
133+
)
134+
}
135+
}
136+
}
137+
138+
// Cycle fallback: append any remaining nodes (shouldn't happen with consistent data)
139+
if (result.length < allNames.size) {
140+
for (const name of allNames) {
141+
if (!result.includes(name)) {
142+
result.push(name)
143+
}
144+
}
145+
}
146+
147+
return result
148+
}
149+
48150
/**
49151
* Generate responsive code by merging children inside a Section.
50152
* Uses Codegen to build NodeTree for each breakpoint, then merges them.
@@ -212,35 +314,15 @@ export class ResponsiveCodegen {
212314

213315
// Merge children by name
214316
const childrenCodes: string[] = []
215-
const processedChildNames = new Set<string>()
216317

217318
// Convert all trees' children to maps
218319
const childrenMaps = new Map<BreakpointKey, Map<string, NodeTree[]>>()
219320
for (const [bp, tree] of treesByBreakpoint) {
220321
childrenMaps.set(bp, this.treeChildrenToMap(tree))
221322
}
222323

223-
// Get all child names in order (first tree's order, then others)
224-
const firstBreakpoint = firstMapKey(treesByBreakpoint)
225-
const firstChildrenMap = childrenMaps.get(firstBreakpoint)
226-
const allChildNames: string[] = []
227-
228-
if (firstChildrenMap) {
229-
for (const name of firstChildrenMap.keys()) {
230-
allChildNames.push(name)
231-
processedChildNames.add(name)
232-
}
233-
}
234-
235-
// Add children that exist only in other breakpoints
236-
for (const childMap of childrenMaps.values()) {
237-
for (const name of childMap.keys()) {
238-
if (!processedChildNames.has(name)) {
239-
allChildNames.push(name)
240-
processedChildNames.add(name)
241-
}
242-
}
243-
}
324+
// Get all child names in stable merged order across all breakpoints
325+
const allChildNames = mergeChildNameOrder(childrenMaps)
244326

245327
for (const childName of allChildNames) {
246328
// Find the maximum number of children with this name across all breakpoints
@@ -991,26 +1073,8 @@ export class ResponsiveCodegen {
9911073
childrenMaps.set(bp, this.treeChildrenToMap(tree))
9921074
}
9931075

994-
const processedChildNames = new Set<string>()
995-
const allChildNames: string[] = []
996-
const firstBreakpoint = firstMapKey(treesByBreakpoint)
997-
const firstChildrenMap = childrenMaps.get(firstBreakpoint)
998-
999-
if (firstChildrenMap) {
1000-
for (const name of firstChildrenMap.keys()) {
1001-
allChildNames.push(name)
1002-
processedChildNames.add(name)
1003-
}
1004-
}
1005-
1006-
for (const childMap of childrenMaps.values()) {
1007-
for (const name of childMap.keys()) {
1008-
if (!processedChildNames.has(name)) {
1009-
allChildNames.push(name)
1010-
processedChildNames.add(name)
1011-
}
1012-
}
1013-
}
1076+
// Get all child names in stable merged order across all breakpoints
1077+
const allChildNames = mergeChildNameOrder(childrenMaps)
10141078

10151079
const mergedChildren: NodeTree[] = []
10161080

@@ -1111,26 +1175,8 @@ export class ResponsiveCodegen {
11111175
childrenMaps.set(variant, this.treeChildrenToMap(tree))
11121176
}
11131177

1114-
const processedChildNames = new Set<string>()
1115-
const allChildNames: string[] = []
1116-
const firstVariant = firstMapKey(treesByVariant)
1117-
const firstChildrenMap = childrenMaps.get(firstVariant)
1118-
1119-
if (firstChildrenMap) {
1120-
for (const name of firstChildrenMap.keys()) {
1121-
allChildNames.push(name)
1122-
processedChildNames.add(name)
1123-
}
1124-
}
1125-
1126-
for (const childMap of childrenMaps.values()) {
1127-
for (const name of childMap.keys()) {
1128-
if (!processedChildNames.has(name)) {
1129-
allChildNames.push(name)
1130-
processedChildNames.add(name)
1131-
}
1132-
}
1133-
}
1178+
// Get all child names in stable merged order across all variants
1179+
const allChildNames = mergeChildNameOrder(childrenMaps)
11341180

11351181
for (const childName of allChildNames) {
11361182
let maxChildCount = 0
@@ -1338,27 +1384,8 @@ export class ResponsiveCodegen {
13381384
childrenMaps.set(compositeKey, this.treeChildrenToMap(tree))
13391385
}
13401386

1341-
// Get all unique child names
1342-
const processedChildNames = new Set<string>()
1343-
const allChildNames: string[] = []
1344-
const firstComposite = firstMapKey(treesByComposite)
1345-
const firstChildrenMap = childrenMaps.get(firstComposite)
1346-
1347-
if (firstChildrenMap) {
1348-
for (const name of firstChildrenMap.keys()) {
1349-
allChildNames.push(name)
1350-
processedChildNames.add(name)
1351-
}
1352-
}
1353-
1354-
for (const childMap of childrenMaps.values()) {
1355-
for (const name of childMap.keys()) {
1356-
if (!processedChildNames.has(name)) {
1357-
allChildNames.push(name)
1358-
processedChildNames.add(name)
1359-
}
1360-
}
1361-
}
1387+
// Get all unique child names in stable merged order across all composites
1388+
const allChildNames = mergeChildNameOrder(childrenMaps)
13621389

13631390
// Process each child
13641391
for (const childName of allChildNames) {

0 commit comments

Comments
 (0)