@reckona/mreact-compiler 0.0.120 → 0.0.121

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.
@@ -1,10 +1,4 @@
1
- import type {
2
- AttributeIr,
3
- ComponentPropIr,
4
- ComponentIr,
5
- JsxNodeIr,
6
- ModuleIr,
7
- } from "./ir.js";
1
+ import type { AttributeIr, ComponentPropIr, ComponentIr, JsxNodeIr, ModuleIr } from "./ir.js";
8
2
  import type { RuntimeImport, ServerEscapeOptions } from "./types.js";
9
3
  import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
10
4
  import { createCodeBuilder } from "./emit-code-builder.js";
@@ -41,14 +35,10 @@ let currentUrlSafeHelperName: string = "_urlAttrSafe";
41
35
  let currentClientBoundaryHelperName: string | undefined;
42
36
  let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes";
43
37
 
44
- export function emitServer(
45
- ir: ModuleIr,
46
- options: EmitServerOptions = {},
47
- ): EmitResult {
38
+ export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitResult {
48
39
  const escapeHelperName = allocateEscapeHelperName(ir);
49
- const escapeBatchHelperName = options.escape === undefined
50
- ? undefined
51
- : allocateHelperName(ir, "_escapeHtmlBatch");
40
+ const escapeBatchHelperName =
41
+ options.escape === undefined ? undefined : allocateHelperName(ir, "_escapeHtmlBatch");
52
42
  const contextProviderHelperName = usesContextProvider(ir)
53
43
  ? allocateHelperName(ir, "_renderContextProviderToString")
54
44
  : undefined;
@@ -185,17 +175,11 @@ function collectContextImports(
185
175
  ): RuntimeImport[] {
186
176
  const specifiers = [
187
177
  reactNodeRenderHelperName === undefined ? undefined : "renderToString",
188
- contextProviderHelperName === undefined
189
- ? undefined
190
- : "renderContextProviderToString",
191
- contextConsumerHelperName === undefined
192
- ? undefined
193
- : "renderContextConsumerToString",
178
+ contextProviderHelperName === undefined ? undefined : "renderContextProviderToString",
179
+ contextConsumerHelperName === undefined ? undefined : "renderContextConsumerToString",
194
180
  ].filter((specifier): specifier is string => specifier !== undefined);
195
181
 
196
- return specifiers.length === 0
197
- ? []
198
- : [{ source: "@reckona/mreact-compat", specifiers }];
182
+ return specifiers.length === 0 ? [] : [{ source: "@reckona/mreact-compat", specifiers }];
199
183
  }
200
184
 
201
185
  function emitUserImports(ir: ModuleIr): string {
@@ -218,8 +202,9 @@ function emitComponent(
218
202
  contextConsumerHelperName?: string,
219
203
  reactNodeRenderHelperName?: string,
220
204
  ): string {
221
- const body = component.bodyStatements.map((statement) =>
222
- ` ${replaceOxcServerStringReactNodeRenderHelper(statement, reactNodeRenderHelperName)}`,
205
+ const body = component.bodyStatements.map(
206
+ (statement) =>
207
+ ` ${replaceOxcServerStringReactNodeRenderHelper(statement, reactNodeRenderHelperName)}`,
223
208
  );
224
209
  const parameters = component.parameters.join(", ");
225
210
  const htmlStatements = collectHtmlStatements(
@@ -280,7 +265,7 @@ function emitHtmlExpression(
280
265
  );
281
266
 
282
267
  if (parts.length === 0) {
283
- return "\"\"";
268
+ return '""';
284
269
  }
285
270
 
286
271
  return parts.join(" + ");
@@ -400,10 +385,7 @@ function collectHtmlStatements(
400
385
  }
401
386
 
402
387
  if (node.kind === "list") {
403
- const isAsync = containsAsyncServerOperationInChildren(
404
- node.children,
405
- asyncComponentNames,
406
- );
388
+ const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
407
389
 
408
390
  if (isAsync) {
409
391
  // Parallel async path keeps the existing renderer + Promise.all + join
@@ -427,8 +409,7 @@ function collectHtmlStatements(
427
409
  // Sync list — inline for-loop appending to the caller's accumulator.
428
410
  // No inner IIFE wrapper and no intermediate string concat per iteration.
429
411
  const itemBinding = `const ${node.itemName} = _arr[_i];`;
430
- const indexBinding =
431
- node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
412
+ const indexBinding = node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
432
413
  const arrayBinding =
433
414
  node.arrayName === undefined ? undefined : `const ${node.arrayName} = _arr;`;
434
415
  const bodyStatements = node.bodyStatements ?? [];
@@ -521,18 +502,31 @@ function collectHtmlStatements(
521
502
  if (isClientBoundaryPlaceholder(node)) {
522
503
  const helperName = currentClientBoundaryHelperName;
523
504
  if (helperName !== undefined) {
505
+ const boundaryProps = emitPropsObject(
506
+ node.props,
507
+ [],
508
+ escapeHelperName,
509
+ escapeBatchHelperName,
510
+ asyncComponentNames,
511
+ dynamicAttributes,
512
+ contextProviderHelperName,
513
+ contextConsumerHelperName,
514
+ reactNodeRenderHelperName,
515
+ );
516
+ const fallbackHtml = shouldRenderClientBoundaryFallback(node)
517
+ ? emitComponentCallExpression(node.name, boundaryProps, asyncComponentNames)
518
+ : emitHtmlExpressionFromChildren(
519
+ node.children,
520
+ escapeHelperName,
521
+ escapeBatchHelperName,
522
+ asyncComponentNames,
523
+ dynamicAttributes,
524
+ contextProviderHelperName,
525
+ contextConsumerHelperName,
526
+ reactNodeRenderHelperName,
527
+ );
524
528
  return [
525
- `${outVar} += ${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
526
- node.props,
527
- [],
528
- escapeHelperName,
529
- escapeBatchHelperName,
530
- asyncComponentNames,
531
- dynamicAttributes,
532
- contextProviderHelperName,
533
- contextConsumerHelperName,
534
- reactNodeRenderHelperName,
535
- )}, ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
529
+ `${outVar} += ${helperName}(${stringLiteral(node.name)}, ${boundaryProps}, ${fallbackHtml});`,
536
530
  ];
537
531
  }
538
532
 
@@ -635,9 +629,8 @@ function collectHtmlStatements(
635
629
  node.children,
636
630
  escapeBatchHelperName,
637
631
  );
638
- const childSelectedValueCode = node.tagName === "select"
639
- ? attributeScan.formValueAttributeCode
640
- : undefined;
632
+ const childSelectedValueCode =
633
+ node.tagName === "select" ? attributeScan.formValueAttributeCode : undefined;
641
634
 
642
635
  if (childrenExpression !== undefined && childSelectedValueCode === undefined) {
643
636
  statements.push(`${outVar} += ${childrenExpression};`);
@@ -693,8 +686,26 @@ function collectHtmlParts(
693
686
  }
694
687
 
695
688
  if (node.kind === "conditional") {
696
- const whenTrue = emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
697
- const whenFalse = emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
689
+ const whenTrue = emitHtmlExpressionFromChildren(
690
+ node.whenTrue,
691
+ escapeHelperName,
692
+ escapeBatchHelperName,
693
+ asyncComponentNames,
694
+ dynamicAttributes,
695
+ contextProviderHelperName,
696
+ contextConsumerHelperName,
697
+ reactNodeRenderHelperName,
698
+ );
699
+ const whenFalse = emitHtmlExpressionFromChildren(
700
+ node.whenFalse,
701
+ escapeHelperName,
702
+ escapeBatchHelperName,
703
+ asyncComponentNames,
704
+ dynamicAttributes,
705
+ contextProviderHelperName,
706
+ contextConsumerHelperName,
707
+ reactNodeRenderHelperName,
708
+ );
698
709
 
699
710
  return [
700
711
  node.conditionValueName === undefined
@@ -704,10 +715,7 @@ function collectHtmlParts(
704
715
  }
705
716
 
706
717
  if (node.kind === "list") {
707
- const isAsync = containsAsyncServerOperationInChildren(
708
- node.children,
709
- asyncComponentNames,
710
- );
718
+ const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
711
719
 
712
720
  if (isAsync) {
713
721
  // Async lists rely on Promise.all() for parallel resolution; the
@@ -732,16 +740,18 @@ function collectHtmlParts(
732
740
  // Synchronous list — imperative accumulator avoids the per-render
733
741
  // callback allocation, the intermediate `.map()` result array, and the
734
742
  // trailing `.join("")` call.
735
- return [emitSyncListIife(
736
- node,
737
- escapeHelperName,
738
- escapeBatchHelperName,
739
- asyncComponentNames,
740
- dynamicAttributes,
741
- contextProviderHelperName,
742
- contextConsumerHelperName,
743
- reactNodeRenderHelperName,
744
- )];
743
+ return [
744
+ emitSyncListIife(
745
+ node,
746
+ escapeHelperName,
747
+ escapeBatchHelperName,
748
+ asyncComponentNames,
749
+ dynamicAttributes,
750
+ contextProviderHelperName,
751
+ contextConsumerHelperName,
752
+ reactNodeRenderHelperName,
753
+ ),
754
+ ];
745
755
  }
746
756
 
747
757
  if (node.kind === "fragment") {
@@ -885,9 +895,8 @@ function collectHtmlParts(
885
895
  escapeBatchHelperName,
886
896
  );
887
897
  const attributeScan = scanElementAttributes(node.tagName, node.attributes);
888
- const childSelectedValueCode = node.tagName === "select"
889
- ? attributeScan.formValueAttributeCode
890
- : undefined;
898
+ const childSelectedValueCode =
899
+ node.tagName === "select" ? attributeScan.formValueAttributeCode : undefined;
891
900
  const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
892
901
  const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes);
893
902
  const childrenParts =
@@ -980,7 +989,9 @@ function collectHtmlAttributeParts(
980
989
  }
981
990
 
982
991
  if (attr.name === "style") {
983
- return [emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName)];
992
+ return [
993
+ emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName),
994
+ ];
984
995
  }
985
996
 
986
997
  if (isDangerousHtmlAttribute(htmlName)) {
@@ -988,7 +999,7 @@ function collectHtmlAttributeParts(
988
999
  // else at runtime so a value computed from a loader cannot inject
989
1000
  // executable HTML into the iframe document.
990
1001
  return [
991
- `(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${htmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral("\"")}; return ""; })()`,
1002
+ `(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${htmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral('"')}; return ""; })()`,
992
1003
  ];
993
1004
  }
994
1005
 
@@ -1009,11 +1020,11 @@ function collectElementAttributeParts(
1009
1020
 
1010
1021
  return attrs.flatMap((attr) =>
1011
1022
  attr.kind !== "spread-attr" &&
1012
- ((tagName === "input" &&
1013
- ((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) ||
1014
- (attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) ||
1015
- ((tagName === "textarea" || tagName === "select") &&
1016
- (attr.name === "value" || attr.name === "defaultValue")))
1023
+ ((tagName === "input" &&
1024
+ ((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) ||
1025
+ (attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) ||
1026
+ ((tagName === "textarea" || tagName === "select") &&
1027
+ (attr.name === "value" || attr.name === "defaultValue")))
1017
1028
  ? []
1018
1029
  : collectHtmlAttributeParts(
1019
1030
  tagName,
@@ -1087,10 +1098,7 @@ function scanElementAttributes(
1087
1098
 
1088
1099
  if ((tagName === "textarea" || tagName === "select") && attr.name === "value") {
1089
1100
  valueAttributeCode = readFormValueAttributeCode(attr);
1090
- } else if (
1091
- (tagName === "textarea" || tagName === "select") &&
1092
- attr.name === "defaultValue"
1093
- ) {
1101
+ } else if ((tagName === "textarea" || tagName === "select") && attr.name === "defaultValue") {
1094
1102
  defaultValueAttributeCode = readFormValueAttributeCode(attr);
1095
1103
  }
1096
1104
  }
@@ -1122,7 +1130,7 @@ function emitDynamicAttributeExpression(
1122
1130
  // returns the value when safe and `undefined` when the attribute
1123
1131
  // should be dropped. Using an IIFE here is necessary because we
1124
1132
  // need to capture the value once and branch on the helper output.
1125
- return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral("\"")}; })()`;
1133
+ return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral('"')}; })()`;
1126
1134
  }
1127
1135
 
1128
1136
  const inlineExpr = simpleSideEffectFreeExpression(code);
@@ -1132,10 +1140,10 @@ function emitDynamicAttributeExpression(
1132
1140
  // Safe because `simpleSideEffectFreeExpression` only matches expressions
1133
1141
  // whose evaluation has no observable side effects (identifier read,
1134
1142
  // member chain, literal, this).
1135
- return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
1143
+ return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`;
1136
1144
  }
1137
1145
 
1138
- return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
1146
+ return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1139
1147
  }
1140
1148
 
1141
1149
  function emitDynamicStyleAttributeExpression(
@@ -1149,11 +1157,12 @@ function emitDynamicStyleAttributeExpression(
1149
1157
  return staticStyleExpression;
1150
1158
  }
1151
1159
 
1152
- const escapedPair = escapeBatchHelperName === undefined
1153
- ? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
1154
- : `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
1160
+ const escapedPair =
1161
+ escapeBatchHelperName === undefined
1162
+ ? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
1163
+ : `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
1155
1164
 
1156
- return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; if (typeof _value === "string") { const _style = ${escapeHelperName}(_value); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; } const _style = Object.entries(_value).filter(([, _styleValue]) => _styleValue != null && _styleValue !== false).map(([_styleName, _styleValue]) => { const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); return ${escapedPair}; }).join(";"); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
1165
+ return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; if (typeof _value === "string") { const _style = ${escapeHelperName}(_value); return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; } const _style = Object.entries(_value).filter(([, _styleValue]) => _styleValue != null && _styleValue !== false).map(([_styleName, _styleValue]) => { const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); return ${escapedPair}; }).join(";"); return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`;
1157
1166
  }
1158
1167
 
1159
1168
  function emitStaticStyleObjectAttributeExpression(
@@ -1191,11 +1200,12 @@ function emitStaticStyleObjectAttributeExpression(
1191
1200
 
1192
1201
  // Stage A — needSep tracking with inline string accumulator, no intermediate
1193
1202
  // array allocation and no `.join(";")` per render.
1194
- const statements = entries.map((entry) =>
1195
- `{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`
1203
+ const statements = entries.map(
1204
+ (entry) =>
1205
+ `{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`,
1196
1206
  );
1197
1207
 
1198
- return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
1208
+ return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`;
1199
1209
  }
1200
1210
 
1201
1211
  function collectTextareaValueParts(
@@ -1224,7 +1234,7 @@ function collectTextareaValueParts(
1224
1234
  contextProviderHelperName,
1225
1235
  contextConsumerHelperName,
1226
1236
  reactNodeRenderHelperName,
1227
- )
1237
+ ),
1228
1238
  );
1229
1239
  }
1230
1240
 
@@ -1245,9 +1255,13 @@ function collectOptionSelectedAttributePart(
1245
1255
  }
1246
1256
 
1247
1257
  function findOptionValueCode(node: Extract<JsxNodeIr, { kind: "element" }>): string | undefined {
1248
- const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
1258
+ const valueAttr = node.attributes.find(
1259
+ (attr) => attr.kind !== "spread-attr" && attr.name === "value",
1260
+ );
1249
1261
  if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
1250
- return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
1262
+ return valueAttr.kind === "static-attr"
1263
+ ? stringLiteral(valueAttr.value)
1264
+ : `(${valueAttr.code})`;
1251
1265
  }
1252
1266
 
1253
1267
  return node.children.every((child) => child.kind === "text")
@@ -1282,7 +1296,8 @@ function emitBatchedSimpleChildrenExpression(
1282
1296
  }
1283
1297
 
1284
1298
  const dynamicChildren = children.filter(
1285
- (child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
1299
+ (child) =>
1300
+ child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
1286
1301
  ) as Array<Extract<JsxNodeIr, { kind: "expr" }>>;
1287
1302
 
1288
1303
  if (dynamicChildren.length < 2) {
@@ -1293,7 +1308,11 @@ function emitBatchedSimpleChildrenExpression(
1293
1308
  children.some(
1294
1309
  (child) =>
1295
1310
  child.kind !== "text" &&
1296
- !(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"),
1311
+ !(
1312
+ child.kind === "expr" &&
1313
+ child.renderMode !== "html" &&
1314
+ child.renderMode !== "react-node"
1315
+ ),
1297
1316
  )
1298
1317
  ) {
1299
1318
  return undefined;
@@ -1326,7 +1345,7 @@ function emitHtmlExpressionFromChildren(
1326
1345
  reactNodeRenderHelperName?: string,
1327
1346
  ): string {
1328
1347
  if (children.length === 0) {
1329
- return "\"\"";
1348
+ return '""';
1330
1349
  }
1331
1350
 
1332
1351
  return emitHtmlExpression(
@@ -1362,10 +1381,8 @@ function emitSyncListIife(
1362
1381
  reactNodeRenderHelperName,
1363
1382
  );
1364
1383
  const itemBinding = `const ${node.itemName} = _arr[_i];`;
1365
- const indexBinding =
1366
- node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
1367
- const arrayBinding =
1368
- node.arrayName === undefined ? "" : ` const ${node.arrayName} = _arr;`;
1384
+ const indexBinding = node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
1385
+ const arrayBinding = node.arrayName === undefined ? "" : ` const ${node.arrayName} = _arr;`;
1369
1386
  const bodyStatements =
1370
1387
  node.bodyStatements === undefined || node.bodyStatements.length === 0
1371
1388
  ? ""
@@ -1395,10 +1412,7 @@ function emitListRenderer(
1395
1412
  contextConsumerHelperName,
1396
1413
  reactNodeRenderHelperName,
1397
1414
  );
1398
- const asyncKeyword = containsAsyncServerOperationInChildren(
1399
- node.children,
1400
- asyncComponentNames,
1401
- )
1415
+ const asyncKeyword = containsAsyncServerOperationInChildren(node.children, asyncComponentNames)
1402
1416
  ? "async "
1403
1417
  : "";
1404
1418
 
@@ -1476,22 +1490,30 @@ function emitComponentCallExpression(
1476
1490
  asyncComponentNames: ReadonlySet<string>,
1477
1491
  ): string {
1478
1492
  const call = `${name}(${propsCode})`;
1479
- return asyncComponentNames.has(name) ? `(await ${call})` : call;
1493
+ return emitRenderableHtmlExpression(asyncComponentNames.has(name) ? `(await ${call})` : call);
1494
+ }
1495
+
1496
+ function emitRenderableHtmlExpression(code: string): string {
1497
+ return `((_value) => _value == null || typeof _value === "boolean" ? "" : _value)(${code})`;
1480
1498
  }
1481
1499
 
1482
1500
  function isClientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
1483
1501
  return node.clientReference !== undefined;
1484
1502
  }
1485
1503
 
1504
+ function shouldRenderClientBoundaryFallback(
1505
+ node: Extract<JsxNodeIr, { kind: "component" }>,
1506
+ ): boolean {
1507
+ return node.clientReference?.ssrFallback === true;
1508
+ }
1509
+
1486
1510
  function clientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): string {
1487
1511
  return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
1488
1512
  }
1489
1513
 
1490
1514
  function collectAsyncServerComponentNames(components: readonly ComponentIr[]): Set<string> {
1491
1515
  const names = new Set(
1492
- components
1493
- .filter((component) => component.async === true)
1494
- .map((component) => component.name),
1516
+ components.filter((component) => component.async === true).map((component) => component.name),
1495
1517
  );
1496
1518
 
1497
1519
  let changed = true;
@@ -1500,10 +1522,7 @@ function collectAsyncServerComponentNames(components: readonly ComponentIr[]): S
1500
1522
  changed = false;
1501
1523
 
1502
1524
  for (const component of components) {
1503
- if (
1504
- !names.has(component.name) &&
1505
- containsAsyncServerOperation(component.root, names)
1506
- ) {
1525
+ if (!names.has(component.name) && containsAsyncServerOperation(component.root, names)) {
1507
1526
  names.add(component.name);
1508
1527
  changed = true;
1509
1528
  }
package/src/ir.ts CHANGED
@@ -52,6 +52,7 @@ export interface ComponentRefIr {
52
52
  export interface ClientReferenceIr {
53
53
  moduleId: string;
54
54
  exportName: string;
55
+ ssrFallback?: boolean;
55
56
  }
56
57
 
57
58
  export type ComponentPropIr = ComponentNamedPropIr | ComponentRenderPropIr | ComponentSpreadPropIr;