@reckona/mreact-compiler 0.0.153 → 0.0.155

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.
@@ -36,6 +36,7 @@ export interface EmitServerOptions {
36
36
  // name through every signature. Reset at the top of `emitServer`.
37
37
  let currentUrlSafeHelperName: string = "_urlAttrSafe";
38
38
  let currentClientBoundaryHelperName: string | undefined;
39
+ let currentCompatChildHelperName: string | undefined;
39
40
  let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes";
40
41
 
41
42
  export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitResult {
@@ -54,12 +55,16 @@ export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitR
54
55
  const clientBoundaryHelperName = usesClientBoundary(ir)
55
56
  ? allocateHelperName(ir, "_renderClientBoundary")
56
57
  : undefined;
58
+ const compatChildHelperName = usesCompatChildRender(ir)
59
+ ? allocateHelperName(ir, "_renderCompatChild")
60
+ : undefined;
57
61
  const spreadAttributesHelperName = allocateHelperName(ir, "_renderSpreadAttributes");
58
62
  const outAccumulatorName = allocateHelperName(ir, "_out");
59
63
  const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
60
64
  currentUrlSafeHelperName = urlSafeHelperName;
61
65
  setOxcServerStringUrlSafeHelperName(urlSafeHelperName);
62
66
  currentClientBoundaryHelperName = clientBoundaryHelperName;
67
+ currentCompatChildHelperName = compatChildHelperName;
63
68
  currentSpreadAttributesHelperName = spreadAttributesHelperName;
64
69
  const helper = emitEscapeHtmlHelper(escapeHelperName);
65
70
  // Inline URL-scheme guard mirroring packages/server/src/url-safety.ts.
@@ -127,6 +132,7 @@ export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitR
127
132
  contextProviderHelperName,
128
133
  contextConsumerHelperName,
129
134
  reactNodeRenderHelperName,
135
+ compatChildHelperName,
130
136
  );
131
137
  const moduleStatements = emitModuleStatements(ir);
132
138
  const code = createCodeBuilder();
@@ -146,6 +152,7 @@ export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitR
146
152
  contextProviderHelperName,
147
153
  contextConsumerHelperName,
148
154
  reactNodeRenderHelperName,
155
+ compatChildHelperName,
149
156
  ),
150
157
  };
151
158
  }
@@ -154,11 +161,15 @@ function emitContextImport(
154
161
  contextProviderHelperName: string | undefined,
155
162
  contextConsumerHelperName: string | undefined,
156
163
  reactNodeRenderHelperName: string | undefined,
164
+ compatChildHelperName?: string | undefined,
157
165
  ): string {
158
166
  const specifiers = [
159
167
  reactNodeRenderHelperName === undefined
160
168
  ? undefined
161
169
  : `renderToString as ${reactNodeRenderHelperName}`,
170
+ compatChildHelperName === undefined
171
+ ? undefined
172
+ : `renderChildToString as ${compatChildHelperName}`,
162
173
  contextProviderHelperName === undefined
163
174
  ? undefined
164
175
  : `renderContextProviderToString as ${contextProviderHelperName}`,
@@ -176,11 +187,13 @@ function collectContextImports(
176
187
  contextProviderHelperName: string | undefined,
177
188
  contextConsumerHelperName: string | undefined,
178
189
  reactNodeRenderHelperName?: string,
190
+ compatChildHelperName?: string,
179
191
  ): RuntimeImport[] {
180
192
  const specifiers = [
181
193
  reactNodeRenderHelperName === undefined ? undefined : "renderToString",
182
194
  contextProviderHelperName === undefined ? undefined : "renderContextProviderToString",
183
195
  contextConsumerHelperName === undefined ? undefined : "renderContextConsumerToString",
196
+ compatChildHelperName === undefined ? undefined : "renderChildToString",
184
197
  ].filter((specifier): specifier is string => specifier !== undefined);
185
198
 
186
199
  return specifiers.length === 0 ? [] : [{ source: "@reckona/mreact-compat", specifiers }];
@@ -317,6 +330,10 @@ function collectHtmlStatements(
317
330
  return [`${outVar} += ${reactNodeRenderHelperName}(() => (${node.code}));`];
318
331
  }
319
332
 
333
+ if (node.renderMode === "compat-child" && currentCompatChildHelperName !== undefined) {
334
+ return [`${outVar} += ${currentCompatChildHelperName}(${node.code});`];
335
+ }
336
+
320
337
  return [`${outVar} += ${escapeHelperName}(${node.code});`];
321
338
  }
322
339
 
@@ -691,6 +708,10 @@ function collectHtmlParts(
691
708
  return [`${reactNodeRenderHelperName}(() => (${node.code}))`];
692
709
  }
693
710
 
711
+ if (node.renderMode === "compat-child" && currentCompatChildHelperName !== undefined) {
712
+ return [`${currentCompatChildHelperName}(${node.code})`];
713
+ }
714
+
694
715
  return [`${escapeHelperName}(${node.code})`];
695
716
  }
696
717
 
@@ -1013,7 +1034,9 @@ function collectHtmlAttributeParts(
1013
1034
 
1014
1035
  if (attr.name === "style") {
1015
1036
  return [
1016
- emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName),
1037
+ attr.serialization === "compat"
1038
+ ? emitCompatDynamicStyleAttributeExpression(attr.code, escapeHelperName)
1039
+ : emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName),
1017
1040
  ];
1018
1041
  }
1019
1042
 
@@ -1026,7 +1049,11 @@ function collectHtmlAttributeParts(
1026
1049
  ];
1027
1050
  }
1028
1051
 
1029
- return [emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)];
1052
+ return [
1053
+ attr.serialization === "compat" && !isUrlAttribute(htmlName)
1054
+ ? emitCompatDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)
1055
+ : emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName),
1056
+ ];
1030
1057
  }
1031
1058
 
1032
1059
  function collectElementAttributeParts(
@@ -1175,6 +1202,42 @@ function emitDynamicAttributeExpression(
1175
1202
  : `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1176
1203
  }
1177
1204
 
1205
+ // Mirrors packages/react-compat/src/server-render.ts renderHtmlAttribute for
1206
+ // dynamic values: functions and objects drop, booleans serialize as
1207
+ // "true"/"false" for booleanish-string and data attributes, and false drops
1208
+ // elsewhere. Byte parity with the interpreter is pinned by tests.
1209
+ function emitCompatDynamicAttributeExpression(
1210
+ name: string,
1211
+ code: string,
1212
+ escapeHelperName: string,
1213
+ ): string {
1214
+ const lowerCased = name.toLowerCase();
1215
+ const booleanishOrData =
1216
+ lowerCased.startsWith("aria-") ||
1217
+ lowerCased.startsWith("data-") ||
1218
+ lowerCased === "contenteditable" ||
1219
+ lowerCased === "draggable" ||
1220
+ lowerCased === "spellcheck";
1221
+ const booleanBranch = booleanishOrData
1222
+ ? `return ${stringLiteral(` ${name}="`)} + (_value ? "true" : "false") + ${stringLiteral('"')};`
1223
+ : `return _value ? ${stringLiteral(` ${name}=""`)} : "";`;
1224
+
1225
+ return `(() => { const _value = (${code}); if (_value == null || typeof _value === "function") return ""; if (typeof _value === "boolean") { ${booleanBranch} } if (typeof _value === "object") return ""; return ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value) + ${stringLiteral('"')}; })()`;
1226
+ }
1227
+
1228
+ // Mirrors packages/react-compat/src/server-render.ts renderStyleAttribute and
1229
+ // renderCssValue: skips null/boolean/empty entries and appends px to nonzero
1230
+ // numeric values outside the react unitless list.
1231
+ function emitCompatDynamicStyleAttributeExpression(
1232
+ code: string,
1233
+ escapeHelperName: string,
1234
+ ): string {
1235
+ const unitlessCheck =
1236
+ '_styleName === "flex" || _styleName === "fontWeight" || _styleName === "lineHeight" || _styleName === "opacity" || _styleName === "order" || _styleName === "zIndex" || _styleName === "zoom"';
1237
+
1238
+ return `(() => { const _value = (${code}); if (_value == null || typeof _value !== "object") return ""; let _style = ""; for (const _styleName in _value) { const _styleValue = _value[_styleName]; if (_styleValue == null || typeof _styleValue === "boolean" || _styleValue === "") continue; const _cssName = _styleName.startsWith("--") ? _styleName : _styleName.replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); const _css = typeof _styleValue !== "number" || _styleValue === 0 || (${unitlessCheck}) ? String(_styleValue) : _styleValue + "px"; _style += (_style === "" ? "" : ";") + ${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_css); } return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`;
1239
+ }
1240
+
1178
1241
  function emitDynamicStyleAttributeExpression(
1179
1242
  code: string,
1180
1243
  escapeHelperName: string,
@@ -1326,7 +1389,10 @@ function emitBatchedSimpleChildrenExpression(
1326
1389
 
1327
1390
  const dynamicChildren = children.filter(
1328
1391
  (child) =>
1329
- child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
1392
+ child.kind === "expr" &&
1393
+ child.renderMode !== "html" &&
1394
+ child.renderMode !== "react-node" &&
1395
+ child.renderMode !== "compat-child",
1330
1396
  ) as Array<Extract<JsxNodeIr, { kind: "expr" }>>;
1331
1397
 
1332
1398
  if (dynamicChildren.length < 2) {
@@ -1340,7 +1406,8 @@ function emitBatchedSimpleChildrenExpression(
1340
1406
  !(
1341
1407
  child.kind === "expr" &&
1342
1408
  child.renderMode !== "html" &&
1343
- child.renderMode !== "react-node"
1409
+ child.renderMode !== "react-node" &&
1410
+ child.renderMode !== "compat-child"
1344
1411
  ),
1345
1412
  )
1346
1413
  ) {
@@ -1682,6 +1749,38 @@ function usesClientBoundary(ir: ModuleIr): boolean {
1682
1749
  return ir.components.some((component) => containsClientBoundary(component.root));
1683
1750
  }
1684
1751
 
1752
+ function usesCompatChildRender(ir: ModuleIr): boolean {
1753
+ return ir.components.some((component) => containsCompatChildRender(component.root));
1754
+ }
1755
+
1756
+ function containsCompatChildRender(node: JsxNodeIr): boolean {
1757
+ if (node.kind === "expr") {
1758
+ return node.renderMode === "compat-child";
1759
+ }
1760
+
1761
+ if (node.kind === "conditional") {
1762
+ return [...node.whenTrue, ...node.whenFalse].some(containsCompatChildRender);
1763
+ }
1764
+
1765
+ if (node.kind === "list" || node.kind === "element" || node.kind === "fragment") {
1766
+ return node.children.some(containsCompatChildRender);
1767
+ }
1768
+
1769
+ if (node.kind === "async-boundary") {
1770
+ return [
1771
+ ...node.children,
1772
+ ...(node.placeholderChildren ?? []),
1773
+ ...(node.catchChildren ?? []),
1774
+ ].some(containsCompatChildRender);
1775
+ }
1776
+
1777
+ if (node.kind === "component") {
1778
+ return node.children.some(containsCompatChildRender);
1779
+ }
1780
+
1781
+ return false;
1782
+ }
1783
+
1685
1784
  function emitClientBoundaryHelper(name: string): string {
1686
1785
  const propsHelperName = `${name}$hasNonSerializableProps`;
1687
1786
 
package/src/ir.ts CHANGED
@@ -108,7 +108,7 @@ export interface TextIr {
108
108
  export interface ExprIr {
109
109
  kind: "expr";
110
110
  code: string;
111
- renderMode?: "dynamic" | "html" | "react-node" | "stream-node";
111
+ renderMode?: "dynamic" | "html" | "react-node" | "stream-node" | "compat-child";
112
112
  }
113
113
 
114
114
  export interface AsyncBoundaryIr {
@@ -140,6 +140,9 @@ export interface DynamicAttributeIr {
140
140
  kind: "dynamic-attr";
141
141
  name: string;
142
142
  code: string;
143
+ // "compat" applies react-compat serialization semantics (px suffix for
144
+ // numeric style values, interpreter-equivalent filtering).
145
+ serialization?: "compat";
143
146
  }
144
147
 
145
148
  export interface EventAttributeIr {