@multiplatform.one/core 7.4.0 → 7.5.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/core",
3
- "version": "7.4.0",
3
+ "version": "7.5.0",
4
4
  "description": "Core framework shell, provider composition, and layout factories for multiplatform.one",
5
5
  "keywords": [
6
6
  "app",
@@ -47,11 +47,11 @@
47
47
  "@tanstack/react-devtools": "^0.10.2",
48
48
  "@tanstack/react-form-devtools": "0.2.22",
49
49
  "react-cookie": "^8.1.2",
50
- "@multiplatform.one/i18n": "7.4.0",
51
- "@multiplatform.one/logger": "7.4.0",
52
- "@multiplatform.one/platform": "7.4.0",
53
- "@multiplatform.one/store": "7.4.0",
54
- "@multiplatform.one/theme": "7.4.0"
50
+ "@multiplatform.one/i18n": "7.5.0",
51
+ "@multiplatform.one/store": "7.5.0",
52
+ "@multiplatform.one/theme": "7.5.0",
53
+ "@multiplatform.one/platform": "7.5.0",
54
+ "@multiplatform.one/logger": "7.5.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sentry/react": "^10.51.0",
@@ -69,8 +69,8 @@
69
69
  "tamagui": "2.7.6",
70
70
  "typescript": "~5.9.3",
71
71
  "vitest": "^4.1.5",
72
- "@multiplatform.one/frappe": "7.4.0",
73
- "@multiplatform.one/web3": "7.4.0"
72
+ "@multiplatform.one/web3": "7.5.0",
73
+ "@multiplatform.one/frappe": "7.5.0"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@sentry/react": "^9.0.0",
@@ -86,10 +86,10 @@
86
86
  "react-native-gesture-handler": ">=2.0.0",
87
87
  "react-native-safe-area-context": ">=4.0.0",
88
88
  "tamagui": "^2.0.0-rc",
89
- "@multiplatform.one/frappe": "^7.4.0",
90
- "@multiplatform.one/frappe-ui": "^7.4.0",
91
- "@multiplatform.one/keycloak": "^7.4.0",
92
- "@multiplatform.one/web3": "^7.4.0"
89
+ "@multiplatform.one/frappe": "^7.5.0",
90
+ "@multiplatform.one/web3": "^7.5.0",
91
+ "@multiplatform.one/frappe-ui": "^7.5.0",
92
+ "@multiplatform.one/keycloak": "^7.5.0"
93
93
  },
94
94
  "peerDependenciesMeta": {
95
95
  "@multiplatform.one/web3": {
@@ -1,7 +1,7 @@
1
1
  import { SchemeProvider, useUserScheme } from "@vxrn/color-scheme";
2
2
  import { useFonts } from "expo-font";
3
3
  import i18n from "i18next";
4
- import { Slot } from "one";
4
+ import { Slot, routerStore } from "one";
5
5
  import type { ComponentType, ReactNode } from "react";
6
6
  import { StyleSheet } from "react-native";
7
7
  import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -58,6 +58,21 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
58
58
  }
59
59
  DeepLinkBridge.displayName = "DeepLinkBridge";
60
60
 
61
+ // One's native usePathname() reads useRouteInfo().pathname. For a layout
62
+ // node that is the store's routeInfo, which is undefined from module load
63
+ // until initialize() runs setupLinkingAndRouteInfo() (and then notifies
64
+ // store subscribers — not root-state subscribers). Rendering RootLayout
65
+ // in that window throws `Cannot read property 'pathname' of undefined`
66
+ // into RootErrorBoundary and the app never boots (MPO-24). useOneRouter
67
+ // is the store subscription, so the consumer layout's first paint is the
68
+ // one where routeInfo already exists (initial path is "/").
69
+ function NativeRootLayout({ children }: { children: ReactNode }) {
70
+ const snap = routerStore.useOneRouter();
71
+ if (!snap?.routeInfo) return children;
72
+ return <RootLayout>{children}</RootLayout>;
73
+ }
74
+ NativeRootLayout.displayName = "NativeRootLayout";
75
+
61
76
  function Layout() {
62
77
  const [fontsLoaded, fontError] = useFonts(fontsMap);
63
78
  // Gate on fonts being ready, but never block forever: if font loading
@@ -70,9 +85,9 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
70
85
  <SchemeProvider>
71
86
  <NativeRootProvider>
72
87
  {cfg.deepLinks !== false && <DeepLinkBridge />}
73
- <RootLayout>
88
+ <NativeRootLayout>
74
89
  <Slot />
75
- </RootLayout>
90
+ </NativeRootLayout>
76
91
  </NativeRootProvider>
77
92
  </SchemeProvider>
78
93
  </SafeAreaProvider>
@@ -1,10 +1,23 @@
1
1
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
- import { Suspense, lazy, type PropsWithChildren } from "react";
2
+ import { Suspense, lazy, useEffect, useState, type PropsWithChildren } from "react";
3
3
  import { platform } from "@multiplatform.one/platform";
4
4
  import type { ProviderComponent, TanstackConfig } from "./CreateApp";
5
5
 
6
6
  const LazyDevtools = lazy(() => import("./TanstackDevtools"));
7
7
 
8
+ /**
9
+ * True only after hydration has finished on the client. `useEffect` never
10
+ * runs on the server and does not run during the hydration render, so the
11
+ * server markup and the first client render agree on `false`.
12
+ */
13
+ function useHydrated(): boolean {
14
+ const [hydrated, setHydrated] = useState(false);
15
+ useEffect(() => {
16
+ setHydrated(true);
17
+ }, []);
18
+ return hydrated;
19
+ }
20
+
8
21
  export function createTanstackProvider(tanstackConfig: TanstackConfig): ProviderComponent {
9
22
  const queryClient = new QueryClient({
10
23
  defaultOptions: {
@@ -20,7 +33,18 @@ export function createTanstackProvider(tanstackConfig: TanstackConfig): Provider
20
33
  // TODO(native): TanStack Devtools does not yet support React Native. When it does,
21
34
  // revisit this guard to also handle native (e.g. !platform.isNative) so devtools
22
35
  // are enabled on native when supported.
23
- const showDevtools = tanstackConfig.debug && !platform.isStorybook;
36
+ const wantsDevtools = tanstackConfig.debug && !platform.isStorybook;
37
+ const hydrated = useHydrated();
38
+
39
+ // Devtools mount CLIENT-ONLY, after hydration. Rendering the lazy
40
+ // component inside a fallback-less <Suspense> during SSR made the server
41
+ // suspend on a chunk that only exists in the browser, and React raced
42
+ // that boundary against Fizz streaming: suspense markers leaked into the
43
+ // served document under a cold module cache, which broke the marketplace
44
+ // SSR smoke (SHC-329). Apps worked around it with per-app vite rewrites;
45
+ // gating here retires those. Nothing devtools-related reaches the server
46
+ // renderer now, so the served document is identical with debug on or off.
47
+ const showDevtools = wantsDevtools && hydrated;
24
48
 
25
49
  const scheme = tanstackConfig.useUserScheme?.();
26
50
  const resolvedScheme = scheme?.value ?? "light";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * MPO-8 / SHC-329 regression guard.
3
+ *
4
+ * The devtools used to render as `lazy()` inside a fallback-less <Suspense>
5
+ * whenever `debug` was on. On the server that boundary suspends on a chunk
6
+ * that only exists in the browser, and React raced it against Fizz
7
+ * streaming — suspense markers (`<!--$?-->`, `<template id="B:0">`) landed in
8
+ * the served document under a cold module cache and broke the marketplace SSR
9
+ * smoke. Apps papered over it with per-app vite rewrites.
10
+ *
11
+ * The rule now: nothing devtools-related reaches the server renderer, so the
12
+ * served document is identical with debug on or off, and the overlay mounts
13
+ * client-side after hydration instead.
14
+ */
15
+
16
+ import { renderToString } from "react-dom/server";
17
+ import { describe, expect, it } from "vitest";
18
+ import { createTanstackProvider } from "./TanstackProvider";
19
+ import type { TanstackConfig } from "./CreateApp";
20
+
21
+ function ssr(config: TanstackConfig) {
22
+ const Provider = createTanstackProvider(config);
23
+ return renderToString(
24
+ <Provider>
25
+ <main>app</main>
26
+ </Provider>,
27
+ );
28
+ }
29
+
30
+ describe("createTanstackProvider on the server", () => {
31
+ it("serves the same document with debug on and off", () => {
32
+ expect(ssr({ debug: true })).toBe(ssr({ debug: false }));
33
+ });
34
+
35
+ // Without the client-only gate this renders
36
+ // `<!--$!--><template data-msg="Switched to client rendering…` — the exact
37
+ // marker family that leaked into the marketplace document.
38
+ it("emits no suspense seam when debug is on", () => {
39
+ const html = ssr({ debug: true });
40
+ expect(html).toBe("<main>app</main>");
41
+ expect(html).not.toContain("<!--$");
42
+ expect(html).not.toContain("<template");
43
+ });
44
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"CreateRootLayout.native.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.native.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CAgD3E"}
1
+ {"version":3,"file":"CreateRootLayout.native.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.native.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CA+D3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"TanstackProvider.d.ts","sourceRoot":"","sources":["../../src/app/TanstackProvider.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIrE,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,cAAc,GAAG,iBAAiB,CAiCxF"}
1
+ {"version":3,"file":"TanstackProvider.d.ts","sourceRoot":"","sources":["../../src/app/TanstackProvider.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAiBrE,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,cAAc,GAAG,iBAAiB,CA4CxF"}