@reckona/mreact-compat 0.0.90 → 0.0.92

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.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/dist/class-component.d.ts +20 -6
  3. package/dist/class-component.d.ts.map +1 -1
  4. package/dist/class-component.js +94 -51
  5. package/dist/class-component.js.map +1 -1
  6. package/dist/context.d.ts +19 -3
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +55 -6
  9. package/dist/context.js.map +1 -1
  10. package/dist/dom-children.d.ts +2 -0
  11. package/dist/dom-children.d.ts.map +1 -1
  12. package/dist/dom-children.js +103 -1
  13. package/dist/dom-children.js.map +1 -1
  14. package/dist/dom-host-rules.d.ts +10 -0
  15. package/dist/dom-host-rules.d.ts.map +1 -0
  16. package/dist/dom-host-rules.js +86 -0
  17. package/dist/dom-host-rules.js.map +1 -0
  18. package/dist/dom-props.d.ts +3 -2
  19. package/dist/dom-props.d.ts.map +1 -1
  20. package/dist/dom-props.js +229 -33
  21. package/dist/dom-props.js.map +1 -1
  22. package/dist/element.d.ts +9 -4
  23. package/dist/element.d.ts.map +1 -1
  24. package/dist/element.js +101 -26
  25. package/dist/element.js.map +1 -1
  26. package/dist/event-listeners.d.ts +4 -4
  27. package/dist/event-listeners.d.ts.map +1 -1
  28. package/dist/event-listeners.js +1 -1
  29. package/dist/event-listeners.js.map +1 -1
  30. package/dist/event-types.d.ts +10 -0
  31. package/dist/event-types.d.ts.map +1 -1
  32. package/dist/event-types.js.map +1 -1
  33. package/dist/events.js +22 -1
  34. package/dist/events.js.map +1 -1
  35. package/dist/fiber-commit.d.ts +2 -1
  36. package/dist/fiber-commit.d.ts.map +1 -1
  37. package/dist/fiber-commit.js +13 -1
  38. package/dist/fiber-commit.js.map +1 -1
  39. package/dist/fiber-reconciler.d.ts.map +1 -1
  40. package/dist/fiber-reconciler.js +28 -7
  41. package/dist/fiber-reconciler.js.map +1 -1
  42. package/dist/fiber-work-loop.d.ts.map +1 -1
  43. package/dist/fiber-work-loop.js +4 -3
  44. package/dist/fiber-work-loop.js.map +1 -1
  45. package/dist/fiber.d.ts +5 -0
  46. package/dist/fiber.d.ts.map +1 -1
  47. package/dist/fiber.js +9 -0
  48. package/dist/fiber.js.map +1 -1
  49. package/dist/hooks-entry.d.ts +3 -0
  50. package/dist/hooks-entry.d.ts.map +1 -0
  51. package/dist/hooks-entry.js +2 -0
  52. package/dist/hooks-entry.js.map +1 -0
  53. package/dist/hooks.d.ts +39 -5
  54. package/dist/hooks.d.ts.map +1 -1
  55. package/dist/hooks.js +373 -326
  56. package/dist/hooks.js.map +1 -1
  57. package/dist/host-reconciler.d.ts +3 -0
  58. package/dist/host-reconciler.d.ts.map +1 -1
  59. package/dist/host-reconciler.js +1152 -64
  60. package/dist/host-reconciler.js.map +1 -1
  61. package/dist/hydration.d.ts +1 -1
  62. package/dist/hydration.d.ts.map +1 -1
  63. package/dist/hydration.js.map +1 -1
  64. package/dist/index.d.ts +2 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/react-default.d.ts +4 -4
  69. package/dist/react-default.d.ts.map +1 -1
  70. package/dist/react-default.js +2 -1
  71. package/dist/react-default.js.map +1 -1
  72. package/dist/reconciler.d.ts.map +1 -1
  73. package/dist/reconciler.js +38 -22
  74. package/dist/reconciler.js.map +1 -1
  75. package/dist/root.d.ts.map +1 -1
  76. package/dist/root.js +48 -13
  77. package/dist/root.js.map +1 -1
  78. package/dist/server-render.d.ts +6 -0
  79. package/dist/server-render.d.ts.map +1 -0
  80. package/dist/server-render.js +307 -0
  81. package/dist/server-render.js.map +1 -0
  82. package/package.json +6 -2
  83. package/src/class-component.ts +216 -51
  84. package/src/context.ts +108 -9
  85. package/src/dom-children.ts +155 -1
  86. package/src/dom-host-rules.ts +115 -0
  87. package/src/dom-props.ts +297 -46
  88. package/src/element.ts +141 -31
  89. package/src/event-listeners.ts +6 -6
  90. package/src/event-types.ts +10 -0
  91. package/src/events.ts +32 -10
  92. package/src/fiber-commit.ts +16 -1
  93. package/src/fiber-reconciler.ts +39 -6
  94. package/src/fiber-work-loop.ts +4 -3
  95. package/src/fiber.ts +14 -0
  96. package/src/hooks-entry.ts +24 -0
  97. package/src/hooks.ts +482 -479
  98. package/src/host-reconciler.ts +1628 -94
  99. package/src/hydration.ts +1 -1
  100. package/src/index.ts +1 -1
  101. package/src/react-default.ts +1 -1
  102. package/src/reconciler.ts +61 -22
  103. package/src/root.ts +55 -12
  104. package/src/server-render.ts +478 -0
@@ -0,0 +1,478 @@
1
+ import {
2
+ Activity,
3
+ FORWARD_REF_TYPE,
4
+ Fragment,
5
+ MEMO_TYPE,
6
+ Profiler,
7
+ isReactCompatElement,
8
+ type ForwardRefType,
9
+ type MemoType,
10
+ type ReactCompatElement,
11
+ type ReactCompatNode,
12
+ } from "./element.js";
13
+ import {
14
+ consumerContext,
15
+ isReactCompatConsumer,
16
+ isReactCompatProvider,
17
+ renderWithContextProvider,
18
+ useContext,
19
+ } from "./context.js";
20
+ import {
21
+ createCacheScope,
22
+ createRootRuntime,
23
+ renderWithRootRuntime,
24
+ runWithCacheScope,
25
+ type RootRuntime,
26
+ type RootRuntimeOptions,
27
+ } from "./hooks.js";
28
+ import { isDangerousHtmlAttribute, isDangerousHtmlOptIn } from "./url-safety.js";
29
+ import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
30
+
31
+ export function renderToString<TProps>(
32
+ component:
33
+ | ((props: TProps) => ReactCompatNode)
34
+ | (new (props: TProps) => { render(): ReactCompatNode }),
35
+ props?: TProps,
36
+ options: RootRuntimeOptions = {},
37
+ ): string {
38
+ const runtime = createRootRuntime(() => undefined, {
39
+ ...options,
40
+ idMode: "server",
41
+ });
42
+
43
+ return runWithCacheScope(createCacheScope(), () => {
44
+ try {
45
+ const rendered = renderWithRootRuntime(runtime, "0", () => {
46
+ if (isClassComponentType(component)) {
47
+ const instance = new component(props as Record<string, unknown>);
48
+ return instance.render();
49
+ }
50
+
51
+ return (component as (props: TProps) => ReactCompatNode)(props as TProps);
52
+ });
53
+ return typeof rendered === "string"
54
+ ? rendered
55
+ : renderNodeToString(rendered, runtime, "0.0");
56
+ } finally {
57
+ runtime.dispose();
58
+ }
59
+ });
60
+ }
61
+
62
+ function renderNodeToString(
63
+ node: ReactCompatNode,
64
+ runtime: RootRuntime,
65
+ path: string,
66
+ ): string {
67
+ if (node === null || node === undefined || typeof node === "boolean") {
68
+ return "";
69
+ }
70
+
71
+ if (typeof node === "string" || typeof node === "number") {
72
+ return escapeHtml(node);
73
+ }
74
+
75
+ if (Array.isArray(node)) {
76
+ let html = "";
77
+ for (let index = 0; index < node.length; index += 1) {
78
+ html += renderNodeToString(node[index], runtime, `${path}.${index}`);
79
+ }
80
+ return html;
81
+ }
82
+
83
+ if (!isReactCompatElement(node)) {
84
+ return "";
85
+ }
86
+
87
+ return renderElementToString(node, runtime, path);
88
+ }
89
+
90
+ function renderElementToString(
91
+ element: ReactCompatElement,
92
+ runtime: RootRuntime,
93
+ path: string,
94
+ ): string {
95
+ if (typeof element.type === "string") {
96
+ if (element.type === "textarea") {
97
+ return renderTextareaToString(element, runtime, path);
98
+ }
99
+
100
+ if (element.type === "select") {
101
+ return renderSelectToString(element, runtime, path);
102
+ }
103
+
104
+ const attributes =
105
+ element.type === "input"
106
+ ? renderInputAttributesToString(element.props)
107
+ : renderAttributesToString(element.props);
108
+ if (voidHtmlElements.has(element.type)) {
109
+ return `<${element.type}${attributes}/>`;
110
+ }
111
+
112
+ return `<${element.type}${attributes}>${renderNodeToString(element.props.children, runtime, `${path}.children`)}</${element.type}>`;
113
+ }
114
+
115
+ if (element.type === Fragment) {
116
+ return renderNodeToString(element.props.children, runtime, `${path}.fragment`);
117
+ }
118
+
119
+ if (element.type === Activity) {
120
+ if ((element.props as { mode?: unknown }).mode === "hidden") {
121
+ return "";
122
+ }
123
+
124
+ return `<!--&-->${renderNodeToString(element.props.children, runtime, `${path}.activity`)}<!--/&-->`;
125
+ }
126
+
127
+ if (element.type === Profiler) {
128
+ return renderNodeToString(element.props.children, runtime, `${path}.profiler`);
129
+ }
130
+
131
+ if (isReactCompatProvider(element.type)) {
132
+ return renderWithContextProvider(
133
+ element.type,
134
+ (element.props as { value?: unknown }).value,
135
+ () => renderNodeToString(element.props.children, runtime, `${path}.provider`),
136
+ );
137
+ }
138
+
139
+ if (isReactCompatConsumer(element.type)) {
140
+ const children = element.props.children;
141
+
142
+ if (typeof children === "function") {
143
+ return renderNodeToString(
144
+ (children as (value: unknown) => ReactCompatNode)(useContext(consumerContext(element.type))),
145
+ runtime,
146
+ `${path}.consumer`,
147
+ );
148
+ }
149
+
150
+ return "";
151
+ }
152
+
153
+ if (isForwardRefType(element.type)) {
154
+ const forwardRefType = element.type;
155
+ return renderNodeToString(
156
+ renderWithRootRuntime(runtime, path, () =>
157
+ forwardRefType.render(element.props, element.ref),
158
+ ),
159
+ runtime,
160
+ `${path}.forwardRef`,
161
+ );
162
+ }
163
+
164
+ if (isMemoType(element.type)) {
165
+ return renderNodeToString(
166
+ {
167
+ ...element,
168
+ type: element.type.type,
169
+ },
170
+ runtime,
171
+ `${path}.memo`,
172
+ );
173
+ }
174
+
175
+ if (isClassComponentType(element.type)) {
176
+ const instance = new element.type(element.props);
177
+ return renderNodeToString(
178
+ renderWithRootRuntime(runtime, path, () => instance.render()),
179
+ runtime,
180
+ `${path}.class`,
181
+ );
182
+ }
183
+
184
+ if (typeof element.type === "function") {
185
+ const component = element.type as (props: typeof element.props) => ReactCompatNode;
186
+ return renderNodeToString(
187
+ renderWithRootRuntime(runtime, path, () => component(element.props)),
188
+ runtime,
189
+ `${path}.0`,
190
+ );
191
+ }
192
+
193
+ return "";
194
+ }
195
+
196
+ function renderAttributesToString(props: Record<string, unknown>): string {
197
+ const entries = Object.entries(props);
198
+ if (
199
+ entries.length === 0 ||
200
+ (entries.length === 1 && entries[0]?.[0] === "children")
201
+ ) {
202
+ return "";
203
+ }
204
+
205
+ let attributes = "";
206
+ for (const [name, value] of entries) {
207
+ attributes += renderHtmlAttribute(name, value);
208
+ }
209
+ return attributes;
210
+ }
211
+
212
+ function isClassComponentType(
213
+ value: unknown,
214
+ ): value is new (props: Record<string, unknown>) => { render(): ReactCompatNode } {
215
+ return (
216
+ typeof value === "function" &&
217
+ typeof (value as { prototype?: { render?: unknown } }).prototype?.render === "function"
218
+ );
219
+ }
220
+
221
+ function renderTextareaToString(
222
+ element: ReactCompatElement,
223
+ runtime: RootRuntime,
224
+ path: string,
225
+ ): string {
226
+ const value =
227
+ (element.props as { value?: unknown; defaultValue?: unknown }).value ??
228
+ (element.props as { value?: unknown; defaultValue?: unknown }).defaultValue ??
229
+ element.props.children;
230
+ const attributes = Object.entries(element.props)
231
+ .filter(([name]) => name !== "value" && name !== "defaultValue")
232
+ .map(([name, child]) => renderHtmlAttribute(name, child))
233
+ .filter((attribute) => attribute !== "")
234
+ .join("");
235
+
236
+ return `<textarea${attributes}>${renderNodeToString(value as ReactCompatNode, runtime, `${path}.textarea`)}</textarea>`;
237
+ }
238
+
239
+ function renderSelectToString(
240
+ element: ReactCompatElement,
241
+ runtime: RootRuntime,
242
+ path: string,
243
+ ): string {
244
+ const selectedValue =
245
+ (element.props as { value?: unknown; defaultValue?: unknown }).value ??
246
+ (element.props as { value?: unknown; defaultValue?: unknown }).defaultValue;
247
+ const attributes = Object.entries(element.props)
248
+ .filter(([name]) => name !== "value" && name !== "defaultValue")
249
+ .map(([name, child]) => renderHtmlAttribute(name, child))
250
+ .filter((attribute) => attribute !== "")
251
+ .join("");
252
+
253
+ return `<select${attributes}>${renderSelectChildrenToString(
254
+ element.props.children,
255
+ selectedValue,
256
+ runtime,
257
+ `${path}.select`,
258
+ )}</select>`;
259
+ }
260
+
261
+ function renderSelectChildrenToString(
262
+ children: ReactCompatNode,
263
+ selectedValue: unknown,
264
+ runtime: RootRuntime,
265
+ path: string,
266
+ ): string {
267
+ const childArray = Array.isArray(children) ? children : [children];
268
+
269
+ return childArray.map((child, index) => {
270
+ if (!isReactCompatElement(child) || child.type !== "option") {
271
+ return renderNodeToString(child, runtime, `${path}.${index}`);
272
+ }
273
+
274
+ const optionValue =
275
+ (child.props as { value?: unknown }).value ?? child.props.children;
276
+ const selected =
277
+ selectedValue !== undefined && String(optionValue) === String(selectedValue);
278
+ const props = selected
279
+ ? { ...child.props, selected: true }
280
+ : child.props;
281
+
282
+ return renderElementToString({ ...child, props }, runtime, `${path}.${index}`);
283
+ }).join("");
284
+ }
285
+
286
+ function renderInputAttributesToString(props: Record<string, unknown>): string {
287
+ const hasValue = props.value !== undefined;
288
+ const hasChecked = props.checked !== undefined;
289
+
290
+ return Object.entries(props)
291
+ .filter(([name]) =>
292
+ !((name === "defaultValue" && hasValue) || (name === "defaultChecked" && hasChecked))
293
+ )
294
+ .sort(([leftName], [rightName]) =>
295
+ Number(isInputValueAttribute(leftName)) - Number(isInputValueAttribute(rightName))
296
+ )
297
+ .map(([name, value]) => renderHtmlAttribute(toInputHtmlAttributeName(name), value))
298
+ .filter((attribute) => attribute !== "")
299
+ .join("");
300
+ }
301
+
302
+ function renderHtmlAttribute(name: string, value: unknown): string {
303
+ if (
304
+ name === "children" ||
305
+ name === "key" ||
306
+ name === "ref" ||
307
+ /^on[A-Z]/.test(name) ||
308
+ value === null ||
309
+ value === undefined ||
310
+ typeof value === "function"
311
+ ) {
312
+ return "";
313
+ }
314
+
315
+ if (name === "style") {
316
+ const style = renderStyleAttribute(value);
317
+ return style === "" ? "" : ` style="${escapeHtml(style)}"`;
318
+ }
319
+
320
+ const attributeName = toHtmlAttributeName(name);
321
+
322
+ if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
323
+ return ` ${attributeName}="${value ? "true" : "false"}"`;
324
+ }
325
+
326
+ if (value === false) {
327
+ return "";
328
+ }
329
+
330
+ if (isDangerousHtmlAttribute(attributeName)) {
331
+ return isDangerousHtmlOptIn(value)
332
+ ? ` ${attributeName}="${escapeHtml(value.__html)}"`
333
+ : "";
334
+ }
335
+
336
+ if (typeof value === "object") {
337
+ return "";
338
+ }
339
+
340
+ if (value === true) {
341
+ return ` ${attributeName}=""`;
342
+ }
343
+
344
+ return ` ${attributeName}="${escapeHtml(value)}"`;
345
+ }
346
+
347
+ function isBooleanishStringAttribute(name: string): boolean {
348
+ const attributeName = toHtmlAttributeName(name).toLowerCase();
349
+ return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
350
+ }
351
+
352
+ const BOOLEANISH_STRING_ATTRIBUTES = new Set<string>([
353
+ "contenteditable",
354
+ "draggable",
355
+ "spellcheck",
356
+ ]);
357
+
358
+ function isInputValueAttribute(name: string): boolean {
359
+ return name === "value" || name === "defaultValue";
360
+ }
361
+
362
+ function toInputHtmlAttributeName(name: string): string {
363
+ if (name === "defaultValue") {
364
+ return "value";
365
+ }
366
+
367
+ if (name === "defaultChecked") {
368
+ return "checked";
369
+ }
370
+
371
+ return name;
372
+ }
373
+
374
+ function toHtmlAttributeName(name: string): string {
375
+ return HTML_ATTRIBUTE_ALIASES[name] ?? name;
376
+ }
377
+
378
+ const HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
379
+ acceptCharset: "accept-charset",
380
+ autoFocus: "autofocus",
381
+ autoPlay: "autoplay",
382
+ charSet: "charset",
383
+ className: "class",
384
+ colSpan: "colspan",
385
+ contentEditable: "contenteditable",
386
+ crossOrigin: "crossorigin",
387
+ encType: "enctype",
388
+ formAction: "formaction",
389
+ frameBorder: "frameborder",
390
+ htmlFor: "for",
391
+ httpEquiv: "http-equiv",
392
+ maxLength: "maxlength",
393
+ minLength: "minlength",
394
+ noValidate: "novalidate",
395
+ playsInline: "playsinline",
396
+ readOnly: "readOnly",
397
+ rowSpan: "rowspan",
398
+ spellCheck: "spellcheck",
399
+ srcDoc: "srcdoc",
400
+ srcSet: "srcset",
401
+ tabIndex: "tabindex",
402
+ useMap: "usemap",
403
+ };
404
+
405
+ function renderStyleAttribute(value: unknown): string {
406
+ if (typeof value !== "object" || value === null) {
407
+ return "";
408
+ }
409
+
410
+ return Object.entries(value)
411
+ .filter(([, propertyValue]) =>
412
+ propertyValue !== null &&
413
+ propertyValue !== undefined &&
414
+ typeof propertyValue !== "boolean" &&
415
+ propertyValue !== "",
416
+ )
417
+ .map(([name, propertyValue]) =>
418
+ `${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`,
419
+ )
420
+ .join(";");
421
+ }
422
+
423
+ function renderCssValue(name: string, value: unknown): string {
424
+ if (typeof value !== "number" || value === 0 || isUnitlessCssProperty(name)) {
425
+ return String(value);
426
+ }
427
+
428
+ return `${value}px`;
429
+ }
430
+
431
+ function toKebabCase(value: string): string {
432
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
433
+ }
434
+
435
+ function isUnitlessCssProperty(name: string): boolean {
436
+ return (
437
+ name === "flex" ||
438
+ name === "fontWeight" ||
439
+ name === "lineHeight" ||
440
+ name === "opacity" ||
441
+ name === "order" ||
442
+ name === "zIndex" ||
443
+ name === "zoom"
444
+ );
445
+ }
446
+
447
+ const voidHtmlElements = new Set([
448
+ "area",
449
+ "base",
450
+ "br",
451
+ "col",
452
+ "embed",
453
+ "hr",
454
+ "img",
455
+ "input",
456
+ "link",
457
+ "meta",
458
+ "param",
459
+ "source",
460
+ "track",
461
+ "wbr",
462
+ ]);
463
+
464
+ function isForwardRefType(value: unknown): value is ForwardRefType {
465
+ return (
466
+ typeof value === "object" &&
467
+ value !== null &&
468
+ (value as { $$typeof?: unknown }).$$typeof === FORWARD_REF_TYPE
469
+ );
470
+ }
471
+
472
+ function isMemoType(value: unknown): value is MemoType {
473
+ return (
474
+ typeof value === "object" &&
475
+ value !== null &&
476
+ (value as { $$typeof?: unknown }).$$typeof === MEMO_TYPE
477
+ );
478
+ }