@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
package/src/discover-blocks.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
+
}
|