@reckona/mreact-compiler 0.0.65 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/compiler-module-context.ts +31 -0
- package/src/diagnostics.ts +184 -0
- package/src/emit-client.ts +837 -0
- package/src/emit-compat.ts +567 -0
- package/src/emit-escape-helper.ts +45 -0
- package/src/emit-server-shared.ts +384 -0
- package/src/emit-server-stream.ts +2558 -0
- package/src/emit-server.ts +1827 -0
- package/src/index.ts +44 -0
- package/src/internal.ts +1905 -0
- package/src/ir.ts +151 -0
- package/src/oxc-analysis-types.ts +5 -0
- package/src/oxc-await-analysis.ts +165 -0
- package/src/oxc-await-ids.ts +62 -0
- package/src/oxc-await-validation.ts +117 -0
- package/src/oxc-bindings.ts +70 -0
- package/src/oxc-body-lowering.ts +430 -0
- package/src/oxc-child-analysis.ts +791 -0
- package/src/oxc-code-utils.ts +19 -0
- package/src/oxc-component-detection.ts +459 -0
- package/src/oxc-component-props.ts +170 -0
- package/src/oxc-component-references.ts +613 -0
- package/src/oxc-dom-lowering.ts +127 -0
- package/src/oxc-expression-utils.ts +42 -0
- package/src/oxc-jsx-attributes.ts +110 -0
- package/src/oxc-jsx-text.ts +84 -0
- package/src/oxc-nested-lowering.ts +319 -0
- package/src/oxc-node-utils.ts +65 -0
- package/src/oxc-raw-jsx.ts +239 -0
- package/src/oxc-render-values.ts +620 -0
- package/src/oxc-runtime-emit.ts +212 -0
- package/src/oxc-transform.ts +77 -0
- package/src/oxc.ts +932 -0
- package/src/transform.ts +634 -0
- package/src/types.ts +117 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentPropIr,
|
|
3
|
+
ComponentIr,
|
|
4
|
+
JsxNodeIr,
|
|
5
|
+
ModuleIr,
|
|
6
|
+
} from "./ir.js";
|
|
7
|
+
import type { RuntimeImport } from "./types.js";
|
|
8
|
+
import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
|
|
9
|
+
|
|
10
|
+
export interface EmitResult {
|
|
11
|
+
code: string;
|
|
12
|
+
imports: RuntimeImport[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emitClient(ir: ModuleIr): EmitResult {
|
|
16
|
+
const imports = collectImports(ir);
|
|
17
|
+
const helperNames = allocateRuntimeHelperNames(ir, imports[0]?.specifiers ?? []);
|
|
18
|
+
const importLine =
|
|
19
|
+
imports[0]?.specifiers.length === 0 ? "" : emitRuntimeImportLine(imports, helperNames);
|
|
20
|
+
const userImports = emitUserImports(ir);
|
|
21
|
+
const moduleStatements = emitModuleStatements(ir);
|
|
22
|
+
const moduleAllocator = createNameAllocator([]);
|
|
23
|
+
const clientBoundaryHelperName = hasClientReferenceNodes(ir)
|
|
24
|
+
? moduleAllocator("__mreactClientBoundary", ir.moduleBindingNames)
|
|
25
|
+
: undefined;
|
|
26
|
+
const clientBoundaryHelper =
|
|
27
|
+
clientBoundaryHelperName === undefined ? "" : emitClientBoundaryHelper(clientBoundaryHelperName);
|
|
28
|
+
const components = ir.components
|
|
29
|
+
.map((component) =>
|
|
30
|
+
emitComponent(component, moduleAllocator, helperNames, clientBoundaryHelperName),
|
|
31
|
+
)
|
|
32
|
+
.join("\n\n");
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
code: `${[importLine, userImports, moduleStatements, clientBoundaryHelper].filter(Boolean).join("\n")}\n\n${components}\n`,
|
|
36
|
+
imports,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type RuntimeHelperName =
|
|
41
|
+
| "bindList"
|
|
42
|
+
| "bindEvent"
|
|
43
|
+
| "bindProp"
|
|
44
|
+
| "bindSpreadProps"
|
|
45
|
+
| "bindText"
|
|
46
|
+
| "createTemplate"
|
|
47
|
+
| "insertDynamic";
|
|
48
|
+
|
|
49
|
+
type RuntimeHelperNames = Record<RuntimeHelperName, string>;
|
|
50
|
+
|
|
51
|
+
function allocateRuntimeHelperNames(
|
|
52
|
+
ir: ModuleIr,
|
|
53
|
+
specifiers: readonly string[],
|
|
54
|
+
): RuntimeHelperNames {
|
|
55
|
+
const allocator = createNameAllocator([
|
|
56
|
+
...ir.moduleBindingNames,
|
|
57
|
+
...ir.components.flatMap((component) => [
|
|
58
|
+
component.name,
|
|
59
|
+
component.exportName,
|
|
60
|
+
...component.bindingNames,
|
|
61
|
+
]),
|
|
62
|
+
]);
|
|
63
|
+
const helperNames: RuntimeHelperNames = {
|
|
64
|
+
bindList: "bindList",
|
|
65
|
+
bindEvent: "bindEvent",
|
|
66
|
+
bindProp: "bindProp",
|
|
67
|
+
bindSpreadProps: "bindSpreadProps",
|
|
68
|
+
bindText: "bindText",
|
|
69
|
+
createTemplate: "createTemplate",
|
|
70
|
+
insertDynamic: "insertDynamic",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const specifier of specifiers) {
|
|
74
|
+
const helper = specifier as RuntimeHelperName;
|
|
75
|
+
|
|
76
|
+
if (ir.moduleBindingNames.includes(helper)) {
|
|
77
|
+
helperNames[helper] = allocator(`_${helper}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return helperNames;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function emitRuntimeImportLine(
|
|
85
|
+
imports: RuntimeImport[],
|
|
86
|
+
helperNames: RuntimeHelperNames,
|
|
87
|
+
): string {
|
|
88
|
+
const specifiers = imports[0]?.specifiers ?? ["createTemplate"];
|
|
89
|
+
const importedNames = specifiers.map((specifier) => {
|
|
90
|
+
const helper = specifier as RuntimeHelperName;
|
|
91
|
+
const localName = helperNames[helper];
|
|
92
|
+
|
|
93
|
+
return localName === specifier ? specifier : `${specifier} as ${localName}`;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return `import { ${importedNames.join(", ")} } from "@reckona/mreact-reactive-dom";`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function emitUserImports(ir: ModuleIr): string {
|
|
100
|
+
return ir.components.length === 0 ? "" : ir.userImports.join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function emitModuleStatements(ir: ModuleIr): string {
|
|
104
|
+
return ir.components.length === 0 ? "" : ir.moduleStatements.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function collectImports(ir: ModuleIr): RuntimeImport[] {
|
|
108
|
+
if (ir.components.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const specifiers = new Set<string>(["createTemplate"]);
|
|
113
|
+
|
|
114
|
+
for (const component of ir.components) {
|
|
115
|
+
visit(component.root, (node) => {
|
|
116
|
+
if (node.kind === "expr") {
|
|
117
|
+
specifiers.add(
|
|
118
|
+
node.renderMode === "dynamic" ? "insertDynamic" : "bindText",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node.kind === "conditional") {
|
|
123
|
+
specifiers.add("insertDynamic");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (node.kind === "list") {
|
|
127
|
+
specifiers.add("bindList");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (node.kind === "element") {
|
|
131
|
+
for (const attr of node.attributes) {
|
|
132
|
+
if (attr.kind === "dynamic-attr") {
|
|
133
|
+
specifiers.add("bindProp");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (attr.kind === "spread-attr") {
|
|
137
|
+
specifiers.add("bindSpreadProps");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (attr.kind === "event") {
|
|
141
|
+
specifiers.add("bindEvent");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
source: "@reckona/mreact-reactive-dom",
|
|
151
|
+
specifiers: Array.from(specifiers).sort(),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hasClientReferenceNodes(ir: ModuleIr): boolean {
|
|
157
|
+
return ir.components.some((component) => {
|
|
158
|
+
let found = false;
|
|
159
|
+
visit(component.root, (node) => {
|
|
160
|
+
if (
|
|
161
|
+
node.kind === "component" &&
|
|
162
|
+
node.clientReference !== undefined &&
|
|
163
|
+
isCompatClientReferenceModuleId(node.clientReference.moduleId)
|
|
164
|
+
) {
|
|
165
|
+
found = true;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return found;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emitClientBoundaryHelper(name: string): string {
|
|
173
|
+
return `function ${name}(name, props) {
|
|
174
|
+
const fragment = document.createDocumentFragment();
|
|
175
|
+
const placeholder = document.createElement("template");
|
|
176
|
+
placeholder.setAttribute("data-mreact-client-boundary", name);
|
|
177
|
+
const propsElement = document.createElement("script");
|
|
178
|
+
propsElement.type = "application/json";
|
|
179
|
+
propsElement.setAttribute("data-mreact-client-boundary-props", name);
|
|
180
|
+
try {
|
|
181
|
+
propsElement.textContent = JSON.stringify(props ?? {}).replaceAll("<", "\\\\u003c");
|
|
182
|
+
} catch {
|
|
183
|
+
placeholder.setAttribute("data-mreact-client-boundary-nonserializable", "true");
|
|
184
|
+
propsElement.textContent = "{}";
|
|
185
|
+
}
|
|
186
|
+
fragment.append(placeholder, propsElement);
|
|
187
|
+
return fragment;
|
|
188
|
+
}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function emitComponent(
|
|
192
|
+
component: ComponentIr,
|
|
193
|
+
moduleAllocator: NameAllocator,
|
|
194
|
+
helperNames: RuntimeHelperNames,
|
|
195
|
+
clientBoundaryHelperName: string | undefined,
|
|
196
|
+
): string {
|
|
197
|
+
const templateName = moduleAllocator(
|
|
198
|
+
"_tmpl_" + component.name,
|
|
199
|
+
component.bindingNames,
|
|
200
|
+
);
|
|
201
|
+
const allocator = createNameAllocator([...component.bindingNames, templateName]);
|
|
202
|
+
const body = component.bodyStatements.map((statement) => ` ${statement}`);
|
|
203
|
+
const parameters = component.parameters.join(", ");
|
|
204
|
+
|
|
205
|
+
if (component.root.kind === "component") {
|
|
206
|
+
const state = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName };
|
|
207
|
+
return [
|
|
208
|
+
`${component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
|
|
209
|
+
...body,
|
|
210
|
+
` return ${emitComponentCall(
|
|
211
|
+
component.root.name,
|
|
212
|
+
component.root.props,
|
|
213
|
+
component.root.children,
|
|
214
|
+
state,
|
|
215
|
+
component.root.clientReference === undefined
|
|
216
|
+
? undefined
|
|
217
|
+
: { moduleId: component.root.clientReference.moduleId, name: component.root.name },
|
|
218
|
+
)};`,
|
|
219
|
+
`}`,
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (component.root.kind === "conditional") {
|
|
224
|
+
const state = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName };
|
|
225
|
+
const fragmentName = allocator("_fragment");
|
|
226
|
+
const markerName = allocator("_marker");
|
|
227
|
+
return [
|
|
228
|
+
`${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
|
|
229
|
+
...body,
|
|
230
|
+
` const ${fragmentName} = document.createDocumentFragment();`,
|
|
231
|
+
` const ${markerName} = document.createComment("");`,
|
|
232
|
+
` ${fragmentName}.append(${markerName});`,
|
|
233
|
+
` ${helperNames.insertDynamic}(${fragmentName}, ${markerName}, () => ${emitNodeRenderValueExpression(component.root, state)});`,
|
|
234
|
+
` return ${fragmentName};`,
|
|
235
|
+
`}`,
|
|
236
|
+
].join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const fragmentName = allocator("_fragment");
|
|
240
|
+
const rootName = allocator("_root");
|
|
241
|
+
const templateHtml = escapeTemplateHtml(renderStaticHtml(component.root));
|
|
242
|
+
const setup = emitSetup(component.root, rootName, {
|
|
243
|
+
allocateName: allocator,
|
|
244
|
+
textIndex: 0,
|
|
245
|
+
helperNames,
|
|
246
|
+
clientBoundaryHelperName,
|
|
247
|
+
});
|
|
248
|
+
return [
|
|
249
|
+
`const ${templateName} = ${helperNames.createTemplate}("${templateHtml}");`,
|
|
250
|
+
`${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
|
|
251
|
+
...body,
|
|
252
|
+
` const ${fragmentName} = ${templateName}();`,
|
|
253
|
+
component.root.kind === "fragment"
|
|
254
|
+
? ` const ${rootName} = ${fragmentName};`
|
|
255
|
+
: ` const ${rootName} = ${fragmentName}.firstChild;`,
|
|
256
|
+
setup,
|
|
257
|
+
` return ${rootName};`,
|
|
258
|
+
`}`,
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join("\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderStaticHtml(node: JsxNodeIr): string {
|
|
265
|
+
if (node.kind === "text") {
|
|
266
|
+
return escapeHtml(node.value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (node.kind === "expr") {
|
|
270
|
+
return "<!---->";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (node.kind === "conditional" || node.kind === "list") {
|
|
274
|
+
return "<!---->";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (node.kind === "fragment") {
|
|
278
|
+
return node.children.map(renderStaticHtml).join("");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (node.kind === "component") {
|
|
282
|
+
return "<!---->";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (node.kind === "async-boundary") {
|
|
286
|
+
return "<!--mreact-async-boundary-->";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const attrs = node.attributes
|
|
290
|
+
.filter((attr) => attr.kind === "static-attr")
|
|
291
|
+
.map((attr) => ` ${attr.name}="${escapeHtml(attr.value)}"`)
|
|
292
|
+
.join("");
|
|
293
|
+
const children = node.children.map(renderStaticHtml).join("");
|
|
294
|
+
|
|
295
|
+
return `<${node.tagName}${attrs}>${children}</${node.tagName}>`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface EmitSetupState {
|
|
299
|
+
allocateName: (baseName: string) => string;
|
|
300
|
+
textIndex: number;
|
|
301
|
+
helperNames: RuntimeHelperNames;
|
|
302
|
+
clientBoundaryHelperName?: string | undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function emitSetup(
|
|
306
|
+
node: JsxNodeIr,
|
|
307
|
+
path: string,
|
|
308
|
+
state: EmitSetupState,
|
|
309
|
+
): string {
|
|
310
|
+
const lines: string[] = [];
|
|
311
|
+
|
|
312
|
+
if (
|
|
313
|
+
node.kind !== "element" &&
|
|
314
|
+
node.kind !== "fragment" &&
|
|
315
|
+
node.kind !== "component"
|
|
316
|
+
) {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (node.kind === "component") {
|
|
321
|
+
const componentVar = state.allocateName("_component");
|
|
322
|
+
lines.push(
|
|
323
|
+
` const ${componentVar} = ${emitComponentCall(
|
|
324
|
+
node.name,
|
|
325
|
+
node.props,
|
|
326
|
+
node.children,
|
|
327
|
+
state,
|
|
328
|
+
node.clientReference === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: { moduleId: node.clientReference.moduleId, name: node.name },
|
|
331
|
+
)};`,
|
|
332
|
+
);
|
|
333
|
+
lines.push(` if (${componentVar} == null || typeof ${componentVar} === "boolean") {`);
|
|
334
|
+
lines.push(` ${path}.remove();`);
|
|
335
|
+
lines.push(` } else {`);
|
|
336
|
+
lines.push(` ${path}.replaceWith(${componentVar});`);
|
|
337
|
+
lines.push(` }`);
|
|
338
|
+
return lines.join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (node.kind === "element") {
|
|
342
|
+
for (const attr of node.attributes) {
|
|
343
|
+
if (attr.kind === "dynamic-attr") {
|
|
344
|
+
lines.push(
|
|
345
|
+
` ${state.helperNames.bindProp}(${path}, "${attr.name}", () => (${attr.code}));`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (attr.kind === "spread-attr") {
|
|
350
|
+
lines.push(
|
|
351
|
+
` ${state.helperNames.bindSpreadProps}(${path}, () => (${attr.code}));`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (attr.kind === "event") {
|
|
356
|
+
lines.push(
|
|
357
|
+
` ${state.helperNames.bindEvent}(${path}, "${attr.eventName}", ${attr.code});`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const children = node.children;
|
|
364
|
+
const stableChildrenName = hasLiveChildListMutation(children)
|
|
365
|
+
? state.allocateName("_children")
|
|
366
|
+
: undefined;
|
|
367
|
+
let childIndex = 0;
|
|
368
|
+
|
|
369
|
+
if (stableChildrenName !== undefined) {
|
|
370
|
+
lines.push(` const ${stableChildrenName} = Array.from(${path}.childNodes);`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let sawStaticText = false;
|
|
374
|
+
let sawComponentMutation = false;
|
|
375
|
+
|
|
376
|
+
for (const child of children) {
|
|
377
|
+
if (child.kind === "text") {
|
|
378
|
+
sawStaticText = true;
|
|
379
|
+
childIndex += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const childPath =
|
|
384
|
+
stableChildrenName === undefined ||
|
|
385
|
+
(
|
|
386
|
+
child.kind !== "component" &&
|
|
387
|
+
!sawComponentMutation &&
|
|
388
|
+
usesLiveInsertionAnchor(child) &&
|
|
389
|
+
!sawStaticText
|
|
390
|
+
)
|
|
391
|
+
? `${path}.childNodes[${childIndex}]`
|
|
392
|
+
: `${stableChildrenName}[${childIndex}]`;
|
|
393
|
+
|
|
394
|
+
if (child.kind === "expr") {
|
|
395
|
+
if (child.renderMode === "dynamic") {
|
|
396
|
+
lines.push(
|
|
397
|
+
` ${state.helperNames.insertDynamic}(${path}, ${childPath}, () => (${child.code}));`,
|
|
398
|
+
);
|
|
399
|
+
childIndex += 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const textVar = state.allocateName(`_text_${state.textIndex}`);
|
|
404
|
+
state.textIndex += 1;
|
|
405
|
+
lines.push(` const ${textVar} = document.createTextNode("");`);
|
|
406
|
+
lines.push(` ${childPath}.replaceWith(${textVar});`);
|
|
407
|
+
lines.push(
|
|
408
|
+
` ${state.helperNames.bindText}(${textVar}, () => (${child.code}));`,
|
|
409
|
+
);
|
|
410
|
+
childIndex += 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (child.kind === "conditional") {
|
|
415
|
+
lines.push(
|
|
416
|
+
` ${state.helperNames.insertDynamic}(${path}, ${childPath}, () => (${child.conditionCode}) ? ${emitRenderValueExpression(child.whenTrue, state)} : ${emitRenderValueExpression(child.whenFalse, state)});`,
|
|
417
|
+
);
|
|
418
|
+
childIndex += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (child.kind === "list") {
|
|
423
|
+
const parameters =
|
|
424
|
+
child.indexName === undefined
|
|
425
|
+
? child.itemName
|
|
426
|
+
: `${child.itemName}, ${child.indexName}`;
|
|
427
|
+
const optionEntries: string[] = [];
|
|
428
|
+
|
|
429
|
+
if (child.keyCode !== undefined) {
|
|
430
|
+
optionEntries.push(`key: (${parameters}) => (${child.keyCode})`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (
|
|
434
|
+
child.keyCode !== undefined &&
|
|
435
|
+
listReadsNestedItemObject(child, child.itemName)
|
|
436
|
+
) {
|
|
437
|
+
optionEntries.push("nestedObjectFallback: true");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const options =
|
|
441
|
+
optionEntries.length === 0 ? "" : `, { ${optionEntries.join(", ")} }`;
|
|
442
|
+
lines.push(
|
|
443
|
+
` ${state.helperNames.bindList}(${path}, ${childPath}, () => (${child.itemsCode}), ${emitListRenderer(child, parameters, state)}${options});`,
|
|
444
|
+
);
|
|
445
|
+
childIndex += 1;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (child.kind === "async-boundary") {
|
|
450
|
+
lines.push(emitAsyncBoundarySetup(child, childPath, state));
|
|
451
|
+
childIndex += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
lines.push(emitSetup(child, childPath, state));
|
|
456
|
+
if (child.kind === "component") {
|
|
457
|
+
sawComponentMutation = true;
|
|
458
|
+
}
|
|
459
|
+
childIndex += 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return lines.filter(Boolean).join("\n");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function listReadsNestedItemObject(
|
|
466
|
+
node: Extract<JsxNodeIr, { kind: "list" }>,
|
|
467
|
+
itemName: string,
|
|
468
|
+
): boolean {
|
|
469
|
+
return node.children.some((child) => nodeReadsNestedItemObject(child, itemName));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function nodeReadsNestedItemObject(node: JsxNodeIr, itemName: string): boolean {
|
|
473
|
+
switch (node.kind) {
|
|
474
|
+
case "element":
|
|
475
|
+
return (
|
|
476
|
+
codeReadsNestedItemObject(node.keyCode, itemName) ||
|
|
477
|
+
node.attributes.some((attribute) => {
|
|
478
|
+
if (attribute.kind === "spread-attr") {
|
|
479
|
+
return codeReadsNestedItemObject(attribute.code, itemName);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (attribute.kind === "dynamic-attr" || attribute.kind === "event") {
|
|
483
|
+
return codeReadsNestedItemObject(attribute.code, itemName);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return false;
|
|
487
|
+
}) ||
|
|
488
|
+
node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
|
|
489
|
+
);
|
|
490
|
+
case "component":
|
|
491
|
+
return (
|
|
492
|
+
codeReadsNestedItemObject(node.keyCode, itemName) ||
|
|
493
|
+
node.props.some((prop) => {
|
|
494
|
+
if (prop.kind === "spread-prop") {
|
|
495
|
+
return codeReadsNestedItemObject(prop.code, itemName);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (prop.kind === "render-prop") {
|
|
499
|
+
return prop.children.some((child) => nodeReadsNestedItemObject(child, itemName));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return codeReadsNestedItemObject(prop.code, itemName);
|
|
503
|
+
}) ||
|
|
504
|
+
node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
|
|
505
|
+
);
|
|
506
|
+
case "fragment":
|
|
507
|
+
return node.children.some((child) => nodeReadsNestedItemObject(child, itemName));
|
|
508
|
+
case "conditional":
|
|
509
|
+
return (
|
|
510
|
+
codeReadsNestedItemObject(node.conditionCode, itemName) ||
|
|
511
|
+
node.whenTrue.some((child) => nodeReadsNestedItemObject(child, itemName)) ||
|
|
512
|
+
node.whenFalse.some((child) => nodeReadsNestedItemObject(child, itemName))
|
|
513
|
+
);
|
|
514
|
+
case "list":
|
|
515
|
+
return (
|
|
516
|
+
codeReadsNestedItemObject(node.itemsCode, itemName) ||
|
|
517
|
+
codeReadsNestedItemObject(node.keyCode, itemName) ||
|
|
518
|
+
node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
|
|
519
|
+
);
|
|
520
|
+
case "expr":
|
|
521
|
+
return codeReadsNestedItemObject(node.code, itemName);
|
|
522
|
+
case "async-boundary":
|
|
523
|
+
return (
|
|
524
|
+
codeReadsNestedItemObject(node.valueCode, itemName) ||
|
|
525
|
+
node.children.some((child) => nodeReadsNestedItemObject(child, itemName)) ||
|
|
526
|
+
(node.placeholderChildren?.some((child) =>
|
|
527
|
+
nodeReadsNestedItemObject(child, itemName),
|
|
528
|
+
) ?? false) ||
|
|
529
|
+
(node.catchChildren?.some((child) =>
|
|
530
|
+
nodeReadsNestedItemObject(child, itemName),
|
|
531
|
+
) ?? false)
|
|
532
|
+
);
|
|
533
|
+
case "text":
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function codeReadsNestedItemObject(
|
|
539
|
+
code: string | undefined,
|
|
540
|
+
itemName: string,
|
|
541
|
+
): boolean {
|
|
542
|
+
if (code === undefined || code.length === 0) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const escapedItemName = itemName.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
547
|
+
return new RegExp(`\\b${escapedItemName}(?:\\.[A-Za-z_$][\\w$]*){2,}`).test(code);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function usesLiveInsertionAnchor(child: JsxNodeIr): boolean {
|
|
551
|
+
return (
|
|
552
|
+
child.kind === "component" ||
|
|
553
|
+
(child.kind === "expr" && child.renderMode === "dynamic") ||
|
|
554
|
+
child.kind === "conditional" ||
|
|
555
|
+
child.kind === "list" ||
|
|
556
|
+
child.kind === "async-boundary"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hasLiveChildListMutation(children: readonly JsxNodeIr[]): boolean {
|
|
561
|
+
return children.some(usesLiveInsertionAnchor);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function emitRenderValueExpression(
|
|
565
|
+
children: JsxNodeIr[],
|
|
566
|
+
state: EmitSetupState,
|
|
567
|
+
): string {
|
|
568
|
+
if (children.length === 0) {
|
|
569
|
+
return "null";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (children.length === 1) {
|
|
573
|
+
return emitNodeRenderValueExpression(children[0] as JsxNodeIr, state);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return `[${children
|
|
577
|
+
.map((child) => emitNodeRenderValueExpression(child, state))
|
|
578
|
+
.join(", ")}]`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function emitAsyncBoundarySetup(
|
|
582
|
+
node: Extract<JsxNodeIr, { kind: "async-boundary" }>,
|
|
583
|
+
childPath: string,
|
|
584
|
+
state: EmitSetupState,
|
|
585
|
+
): string {
|
|
586
|
+
// Without a stable id the server has no way to tell the client what it
|
|
587
|
+
// resolved. Leave the placeholder comment in place so the server-rendered
|
|
588
|
+
// subtree (preserved via the hydration marker skip) remains the source of
|
|
589
|
+
// truth. The resolved buttons inside it stay non-interactive in that case.
|
|
590
|
+
if (node.awaitId === undefined) {
|
|
591
|
+
return "";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const valueName = node.valueName;
|
|
595
|
+
const renderChildren = emitRenderValueExpression(node.children, state);
|
|
596
|
+
const awaitIdLiteral = JSON.stringify(node.awaitId);
|
|
597
|
+
|
|
598
|
+
return [
|
|
599
|
+
` {`,
|
|
600
|
+
` const _awaitStore = globalThis.__mreactAwaitData;`,
|
|
601
|
+
` const _awaitEntry = _awaitStore === undefined ? undefined : _awaitStore[${awaitIdLiteral}];`,
|
|
602
|
+
` if (_awaitEntry !== undefined) {`,
|
|
603
|
+
` const ${valueName} = _awaitEntry.value;`,
|
|
604
|
+
` const _resolvedAwaitContent = ${renderChildren};`,
|
|
605
|
+
` if (_resolvedAwaitContent != null) {`,
|
|
606
|
+
` ${childPath}.replaceWith(_resolvedAwaitContent);`,
|
|
607
|
+
` }`,
|
|
608
|
+
` }`,
|
|
609
|
+
` }`,
|
|
610
|
+
].join("\n");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function emitNodeRenderValueExpression(
|
|
614
|
+
node: JsxNodeIr,
|
|
615
|
+
state: EmitSetupState,
|
|
616
|
+
): string {
|
|
617
|
+
if (node.kind === "text") {
|
|
618
|
+
return JSON.stringify(node.value);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (node.kind === "expr") {
|
|
622
|
+
return `(${node.code})`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (node.kind === "component") {
|
|
626
|
+
return emitComponentCall(
|
|
627
|
+
node.name,
|
|
628
|
+
node.props,
|
|
629
|
+
node.children,
|
|
630
|
+
state,
|
|
631
|
+
node.clientReference === undefined
|
|
632
|
+
? undefined
|
|
633
|
+
: { moduleId: node.clientReference.moduleId, name: node.name },
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (node.kind === "fragment") {
|
|
638
|
+
return emitRenderValueExpression(node.children, state);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (node.kind === "conditional") {
|
|
642
|
+
return `((${node.conditionCode}) ? ${emitRenderValueExpression(node.whenTrue, state)} : ${emitRenderValueExpression(node.whenFalse, state)})`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (node.kind === "list") {
|
|
646
|
+
const parameters =
|
|
647
|
+
node.indexName === undefined
|
|
648
|
+
? node.itemName
|
|
649
|
+
: `${node.itemName}, ${node.indexName}`;
|
|
650
|
+
return `(${node.itemsCode}).map(${emitListRenderer(node, parameters, state)})`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (node.kind === "async-boundary") {
|
|
654
|
+
return "null";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const templateName = state.allocateName("_dynamicTemplate");
|
|
658
|
+
const fragmentName = state.allocateName("_dynamicFragment");
|
|
659
|
+
const rootName = state.allocateName("_dynamicRoot");
|
|
660
|
+
const templateHtml = escapeTemplateHtml(renderStaticHtml(node));
|
|
661
|
+
const setup = emitSetup(node, rootName, state);
|
|
662
|
+
const setupLines = setup === "" ? [] : setup.split("\n");
|
|
663
|
+
|
|
664
|
+
return [
|
|
665
|
+
"(() => {",
|
|
666
|
+
` const ${templateName} = ${state.helperNames.createTemplate}("${templateHtml}");`,
|
|
667
|
+
` const ${fragmentName} = ${templateName}();`,
|
|
668
|
+
` const ${rootName} = ${fragmentName}.firstChild;`,
|
|
669
|
+
...setupLines,
|
|
670
|
+
` return ${rootName};`,
|
|
671
|
+
"})()",
|
|
672
|
+
].join("\n");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function emitListRenderer(
|
|
676
|
+
node: Extract<JsxNodeIr, { kind: "list" }>,
|
|
677
|
+
parameters: string,
|
|
678
|
+
state: EmitSetupState,
|
|
679
|
+
): string {
|
|
680
|
+
const valueExpression = emitRenderValueExpression(node.children, state);
|
|
681
|
+
|
|
682
|
+
if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
|
|
683
|
+
return `(${parameters}) => ${valueExpression}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return `(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function emitComponentCall(
|
|
690
|
+
name: string,
|
|
691
|
+
props: ComponentPropIr[],
|
|
692
|
+
children: JsxNodeIr[],
|
|
693
|
+
state: EmitSetupState,
|
|
694
|
+
clientReference?: { moduleId: string; name: string } | undefined,
|
|
695
|
+
): string {
|
|
696
|
+
if (
|
|
697
|
+
clientReference !== undefined &&
|
|
698
|
+
state.clientBoundaryHelperName !== undefined &&
|
|
699
|
+
isCompatClientReferenceModuleId(clientReference.moduleId)
|
|
700
|
+
) {
|
|
701
|
+
return `${state.clientBoundaryHelperName}(${JSON.stringify(clientReference.name)}, ${emitPropsObject(props, children, state)})`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return `${name}(${emitPropsObject(props, children, state)})`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function emitPropsObject(
|
|
708
|
+
props: ComponentPropIr[],
|
|
709
|
+
children: JsxNodeIr[],
|
|
710
|
+
state: EmitSetupState,
|
|
711
|
+
): string {
|
|
712
|
+
const entries = props.map((prop) => {
|
|
713
|
+
if (prop.kind === "spread-prop") {
|
|
714
|
+
return `...(${prop.code})`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (prop.kind === "render-prop") {
|
|
718
|
+
return `${emitPropName(prop.name)}: ${emitRenderValueExpression(prop.children, state)}`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (shouldEmitReactiveComponentPropGetter(prop.code)) {
|
|
722
|
+
return `get ${emitGetterPropName(prop.name)}() { return (${prop.code}); }`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return `${emitPropName(prop.name)}: (${prop.code})`;
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (children.length > 0) {
|
|
729
|
+
entries.push(`children: ${emitRenderValueExpression(children, state)}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return `{ ${entries.join(", ")} }`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function emitPropName(name: string): string {
|
|
736
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function emitGetterPropName(name: string): string {
|
|
740
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `[${JSON.stringify(name)}]`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function shouldEmitReactiveComponentPropGetter(code: string): boolean {
|
|
744
|
+
if (!/\.\s*get\s*\(/.test(code)) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return !/^\s*(?:async\s*)?(?:function\b|(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)/.test(code);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function createNameAllocator(
|
|
752
|
+
reservedNames: readonly string[],
|
|
753
|
+
): NameAllocator {
|
|
754
|
+
const usedNames = new Set(reservedNames);
|
|
755
|
+
|
|
756
|
+
return (baseName: string, extraReservedNames: readonly string[] = []): string => {
|
|
757
|
+
const reservedNames = new Set(extraReservedNames);
|
|
758
|
+
let name = baseName;
|
|
759
|
+
let index = 1;
|
|
760
|
+
|
|
761
|
+
while (usedNames.has(name) || reservedNames.has(name)) {
|
|
762
|
+
name = `${baseName}$${index}`;
|
|
763
|
+
index += 1;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
usedNames.add(name);
|
|
767
|
+
return name;
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function isCompatClientReferenceModuleId(moduleId: string): boolean {
|
|
772
|
+
return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(moduleId);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
type NameAllocator = (
|
|
776
|
+
baseName: string,
|
|
777
|
+
extraReservedNames?: readonly string[],
|
|
778
|
+
) => string;
|
|
779
|
+
|
|
780
|
+
function visit(node: JsxNodeIr, fn: (node: JsxNodeIr) => void): void {
|
|
781
|
+
fn(node);
|
|
782
|
+
|
|
783
|
+
if (node.kind === "conditional") {
|
|
784
|
+
for (const child of [...node.whenTrue, ...node.whenFalse]) {
|
|
785
|
+
visit(child, fn);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (node.kind === "list") {
|
|
790
|
+
for (const child of node.children) {
|
|
791
|
+
visit(child, fn);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (node.kind === "component") {
|
|
796
|
+
for (const prop of node.props) {
|
|
797
|
+
if (prop.kind === "render-prop") {
|
|
798
|
+
for (const child of prop.children) {
|
|
799
|
+
visit(child, fn);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
for (const child of node.children) {
|
|
805
|
+
visit(child, fn);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
810
|
+
for (const child of node.children) {
|
|
811
|
+
visit(child, fn);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Async-boundary children participate in client-side rendering when the
|
|
816
|
+
// boundary has an awaitId (hydration data path). Traverse them so their
|
|
817
|
+
// runtime imports (bindList / bindText / bindEvent / etc.) are included.
|
|
818
|
+
if (node.kind === "async-boundary") {
|
|
819
|
+
for (const child of node.children) {
|
|
820
|
+
visit(child, fn);
|
|
821
|
+
}
|
|
822
|
+
if (node.placeholderChildren !== undefined) {
|
|
823
|
+
for (const child of node.placeholderChildren) {
|
|
824
|
+
visit(child, fn);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (node.catchChildren !== undefined) {
|
|
828
|
+
for (const child of node.catchChildren) {
|
|
829
|
+
visit(child, fn);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function escapeTemplateHtml(value: string): string {
|
|
836
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
837
|
+
}
|