@teamscala/service-boot 0.23.436 → 0.23.437

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.436",
9
+ "version": "0.23.437",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -25,6 +25,7 @@ import { join } from "node:path";
25
25
  import { getPrisma } from "@teamscala/db/prisma-registry";
26
26
  import { BlockSchemaConfigSchema } from "@teamscala/db-validation/registry-schemas/block-schema";
27
27
  import * as v from "valibot";
28
+ import { isReactElementType } from "./is-react-element-type.ts";
28
29
  import type { RendererComponentEntry } from "./types.ts";
29
30
 
30
31
  export interface DiscoveredBlocks {
@@ -159,7 +160,7 @@ export async function discoverBlocks(
159
160
  ),
160
161
  ])) as Record<string, unknown>;
161
162
  const component = mod[componentExport] ?? mod.default;
162
- if (typeof component !== "function") {
163
+ if (!isReactElementType(component)) {
163
164
  skipped.push(
164
165
  `${row.slug} (${componentExport} not a component in ${componentPath})`,
165
166
  );
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Pins the three React idioms a `typeof === "function"` check silently rejects.
7
+ * The memo case is the measured one: PresenceAvatars rendered blank on every
8
+ * scala-projects page carrying it, because memo() returns an object.
9
+ */
10
+
11
+ import { expect, test } from "bun:test";
12
+ import { forwardRef, lazy, memo } from "react";
13
+ import { isReactElementType } from "./is-react-element-type.ts";
14
+
15
+ function Plain() {
16
+ return null;
17
+ }
18
+
19
+ test("accepts a plain function component", () => {
20
+ expect(isReactElementType(Plain)).toBe(true);
21
+ });
22
+
23
+ test("accepts memo() — the PresenceAvatars shape that rendered blank", () => {
24
+ expect(isReactElementType(memo(Plain))).toBe(true);
25
+ });
26
+
27
+ test("accepts forwardRef()", () => {
28
+ expect(isReactElementType(forwardRef(() => null))).toBe(true);
29
+ });
30
+
31
+ test("accepts lazy()", () => {
32
+ expect(isReactElementType(lazy(async () => ({ default: Plain })))).toBe(true);
33
+ });
34
+
35
+ test("still rejects what genuinely cannot render", () => {
36
+ expect(isReactElementType(undefined)).toBe(false);
37
+ expect(isReactElementType(null)).toBe(false);
38
+ expect(isReactElementType({})).toBe(false);
39
+ expect(isReactElementType("PresenceAvatarsBlock")).toBe(false);
40
+ expect(isReactElementType(42)).toBe(false);
41
+ });
42
+
43
+ test("rejects a plain object wearing a non-symbol $$typeof", () => {
44
+ // A data blob with the key but not the symbol must not pass as renderable.
45
+ expect(isReactElementType({ $$typeof: "react.memo" })).toBe(false);
46
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Is a resolved module export something React can render?
7
+ *
8
+ * `typeof x === "function"` is NOT that question. memo(), forwardRef() and
9
+ * lazy() all return OBJECTS carrying a `$$typeof` symbol, so a shape check on
10
+ * `function` rejects three of React's own idioms as "not a component".
11
+ * Measured 2026-09-01: scala-projects skipped PresenceAvatars for exactly this
12
+ * reason — the block is `memo(function PresenceAvatarsBlock(...))` — and every
13
+ * page rendering it came out blank with no error beyond one boot WARN.
14
+ */
15
+
16
+ export function isReactElementType(value: unknown): boolean {
17
+ if (typeof value === "function") return true;
18
+ if (typeof value !== "object" || value === null) return false;
19
+ return typeof (value as { $$typeof?: unknown }).$$typeof === "symbol";
20
+ }