@teamscala/service-boot 0.23.435 → 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.435",
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 {
@@ -58,7 +59,17 @@ export async function discoverBlocks(
58
59
  const isInstalled = async (pkg: string): Promise<boolean> => {
59
60
  const cached = installedCache.get(pkg);
60
61
  if (cached !== undefined) return cached;
61
- const ok = await Bun.file(join(serviceModules, ...pkg.split("/"))).exists();
62
+ // Probe the package's package.json, NOT its directory. Bun.file() on a
63
+ // DIRECTORY reports exists() === false, so the previous directory probe
64
+ // answered "not installed" for every package that was in fact installed
65
+ // — and because this predicate gates every block below, that skipped
66
+ // every block and booted the service with zero Puck components. Measured
67
+ // 2026-09-01: scala-portal, scala-public, scala-forms and scala-projects
68
+ // all FATAL at ui-service.page-manifest with their packages present on
69
+ // disk at the expected versions. A file probe is the correct shape.
70
+ const ok = await Bun.file(
71
+ join(serviceModules, ...pkg.split("/"), "package.json"),
72
+ ).exists();
62
73
  installedCache.set(pkg, ok);
63
74
  return ok;
64
75
  };
@@ -149,7 +160,7 @@ export async function discoverBlocks(
149
160
  ),
150
161
  ])) as Record<string, unknown>;
151
162
  const component = mod[componentExport] ?? mod.default;
152
- if (typeof component !== "function") {
163
+ if (!isReactElementType(component)) {
153
164
  skipped.push(
154
165
  `${row.slug} (${componentExport} not a component in ${componentPath})`,
155
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
+ }