-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathrscProcessor.ts
More file actions
142 lines (120 loc) · 4.13 KB
/
rscProcessor.ts
File metadata and controls
142 lines (120 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { drainStream } from "../util/drainStream";
import { getPayloadIDFor } from "../rsc/rscModule";
import { computeContentHash } from "./contentHash";
import { findReferencedIds, topologicalSort } from "./dependencyGraph";
export interface ProcessedComponent {
finalId: string;
finalContent: string;
name?: string;
}
export interface ProcessResult {
components: ProcessedComponent[];
appRscContent: string;
idMapping: Map<string, string>;
}
interface RawComponent {
id: string;
data: string;
name?: string;
}
/**
* Processes RSC components by replacing temporary UUIDs with content-based hashes.
*
* @param deferRegistryIterator - Iterator yielding components with { id, data }
* @param appRscStream - The main RSC stream
* @param rscPayloadDir - Directory name used as a prefix for RSC payload IDs (e.g. "fun__rsc-payload")
* @param context - Optional context for logging warnings
*/
export async function processRscComponents(
deferRegistryIterator: AsyncIterable<RawComponent>,
appRscStream: ReadableStream,
rscPayloadDir: string,
context?: { warn: (message: string) => void },
): Promise<ProcessResult> {
// Step 1: Collect all components from deferRegistry
const components = new Map<string, string>();
const componentNames = new Map<string, string | undefined>();
for await (const { id, data, name } of deferRegistryIterator) {
components.set(id, data);
componentNames.set(id, name);
}
// Step 2: Drain appRsc stream to string
let appRscContent = await drainStream(appRscStream);
// If no components, return early
if (components.size === 0) {
return {
components: [],
appRscContent,
idMapping: new Map(),
};
}
const allIds = new Set(components.keys());
// Step 3: Build dependency graph
// For each component, find which other component IDs appear in its content
const dependencies = new Map<string, Set<string>>();
for (const [id, content] of components) {
const otherIds = new Set(allIds);
otherIds.delete(id); // Don't include self-references
const refs = findReferencedIds(content, otherIds);
dependencies.set(id, refs);
}
// Step 4: Topologically sort components
const { sorted, inCycle } = topologicalSort(dependencies);
// Step 5: Handle cycles - warn and keep original temp IDs
const idMapping = new Map<string, string>();
if (inCycle.length > 0) {
context?.warn(
`[funstack] Warning: ${inCycle.length} RSC component(s) are in dependency cycles and will keep unstable IDs: ${inCycle.join(", ")}`,
);
for (const id of inCycle) {
idMapping.set(id, id); // Map to itself (keep original ID)
}
}
// Step 6: Process sorted components in order
const processedComponents: ProcessedComponent[] = [];
for (const tempId of sorted) {
let content = components.get(tempId)!;
// Replace all already-finalized temp IDs with their hash-based IDs
for (const [oldId, newId] of idMapping) {
if (oldId !== newId) {
content = content.replaceAll(oldId, newId);
}
}
// Compute content hash for this component
const contentHash = await computeContentHash(content);
const finalId = getPayloadIDFor(contentHash, rscPayloadDir);
// Create mapping
idMapping.set(tempId, finalId);
processedComponents.push({
finalId,
finalContent: content,
name: componentNames.get(tempId),
});
}
// Add cycle members to processed components (with original IDs)
for (const tempId of inCycle) {
let content = components.get(tempId)!;
// Replace finalized IDs in cycle member content
for (const [oldId, newId] of idMapping) {
if (oldId !== newId) {
content = content.replaceAll(oldId, newId);
}
}
processedComponents.push({
finalId: tempId, // Keep original temp ID
finalContent: content,
name: componentNames.get(tempId),
});
}
// Step 7: Process appRsc - replace all temp IDs with final IDs
for (const [oldId, newId] of idMapping) {
if (oldId !== newId) {
appRscContent = appRscContent.replaceAll(oldId, newId);
}
}
return {
components: processedComponents,
appRscContent,
idMapping,
};
}