fimo 0.9.0-experimental.8 → 0.9.0-experimental.9
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/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +45 -35
- package/dist/runtime/primitives/components/RichText.d.ts +5 -1
- package/dist/runtime/primitives/components/RichText.js +2 -2
- package/dist/runtime/react/FimoSpaProviders.js +76 -25
- package/dist/runtime/react/index.d.ts +1 -0
- package/dist/runtime/react/index.js +1 -0
- package/dist/runtime/react/spa-content-cache.d.ts +27 -0
- package/dist/runtime/react/spa-content-cache.js +36 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +4 -1
- package/dist/runtime/wrapped/FimoRichTextBlock.js +2 -2
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
|
@@ -3,7 +3,11 @@ import { type FimoRichText, type FimoRichTextComponents } from '../lib/index.js'
|
|
|
3
3
|
type RichTextProps = {
|
|
4
4
|
value: FimoRichText;
|
|
5
5
|
components?: FimoRichTextComponents;
|
|
6
|
+
/** Wrapper element. Use `span` when the block sits inside phrasing
|
|
7
|
+
* content (a `<p>`): a div there is invalid HTML — the browser closes
|
|
8
|
+
* the paragraph early and React trashes the hydrated tree. */
|
|
9
|
+
as?: 'div' | 'span';
|
|
6
10
|
} & Omit<ComponentPropsWithoutRef<'div'>, 'children'>;
|
|
7
|
-
export declare function RichText({ value, components, ...props }: RichTextProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function RichText({ value, components, as: Wrapper, ...props }: RichTextProps): import("react/jsx-runtime").JSX.Element;
|
|
8
12
|
export {};
|
|
9
13
|
//# sourceMappingURL=RichText.d.ts.map
|
|
@@ -128,8 +128,8 @@ function renderNode(node, components, key) {
|
|
|
128
128
|
}
|
|
129
129
|
return _jsx(Fragment, { children: children }, key);
|
|
130
130
|
}
|
|
131
|
-
export function RichText({ value, components, ...props }) {
|
|
131
|
+
export function RichText({ value, components, as: Wrapper = 'div', ...props }) {
|
|
132
132
|
const source = value[FIMO_SOURCE];
|
|
133
133
|
const { value: liveContent, ref } = useFimoLiveValue(value.content);
|
|
134
|
-
return (_jsx(
|
|
134
|
+
return (_jsx(Wrapper, { ref: ref, ...props, "data-fimo-source": source, "data-fimo-richtext": "", children: renderNode(liveContent, components, 0) }));
|
|
135
135
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { getFimoDefaultLocale, getFimoLocales } from '../i18n/locale.js';
|
|
5
5
|
import { fetchLabelBundle, serializeBundle } from '../shared/labels-fetch.pure.js';
|
|
6
6
|
import { fetchWrappedContent } from '../wrapped/content-fetch.pure.js';
|
|
7
7
|
import { WrappedProviders } from '../wrapped/WrappedProviders.js';
|
|
8
|
+
import { readInitialSpaContent, writeSpaContentCache } from './spa-content-cache.js';
|
|
8
9
|
/** Browser-safe stable signature (labelSignature uses Buffer, server-only). */
|
|
9
10
|
function bundleSignature(bundle) {
|
|
10
11
|
const serialized = serializeBundle(bundle);
|
|
@@ -34,37 +35,87 @@ function bundleSignature(bundle) {
|
|
|
34
35
|
* router without depending on one.
|
|
35
36
|
*/
|
|
36
37
|
export function FimoSpaProviders({ config, apiUrl, previewScriptUrl, media, richText, locale, children, }) {
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Synchronous first paint: baked SSG payload or the last visit's cached
|
|
39
|
+
// one renders IMMEDIATELY (the SDK owns rendering — no fallback flash
|
|
40
|
+
// while the network round-trips); the fetches below revalidate it.
|
|
41
|
+
const initial = useMemo(() => readInitialSpaContent(apiUrl), [apiUrl]);
|
|
42
|
+
const [labels, setLabels] = useState(initial?.labels ?? undefined);
|
|
43
|
+
const [content, setContent] = useState(initial?.content ?? undefined);
|
|
44
|
+
const latestRef = useRef({
|
|
45
|
+
labels: initial?.labels ?? undefined,
|
|
46
|
+
content: initial?.content ?? undefined,
|
|
47
|
+
});
|
|
39
48
|
useEffect(() => {
|
|
40
49
|
if (!apiUrl) {
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
|
-
let
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
let disposed = false;
|
|
53
|
+
const persist = () => {
|
|
54
|
+
writeSpaContentCache(apiUrl, { labels: latestRef.current.labels, content: latestRef.current.content });
|
|
55
|
+
};
|
|
56
|
+
const revalidate = () => {
|
|
57
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
58
|
+
fetchLabelBundle(apiUrl, getFimoLocales(config), defaultLocale)
|
|
59
|
+
.then((bundle) => {
|
|
60
|
+
if (!disposed) {
|
|
61
|
+
const next = { ...bundle, signature: bundleSignature(bundle) };
|
|
62
|
+
latestRef.current.labels = next;
|
|
63
|
+
setLabels(next);
|
|
64
|
+
persist();
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
// Unreachable API: keep rendering the cached/baked values or fallbacks.
|
|
69
|
+
});
|
|
70
|
+
// Wrapped rich text and media live in the plumbing types — without this
|
|
71
|
+
// fetch a client-rendered app only ever shows their inline fallbacks
|
|
72
|
+
// and Studio edits to them render nowhere.
|
|
73
|
+
fetchWrappedContent(apiUrl)
|
|
74
|
+
.then((fetched) => {
|
|
75
|
+
if (!disposed) {
|
|
76
|
+
latestRef.current.content = fetched;
|
|
77
|
+
setContent(fetched);
|
|
78
|
+
persist();
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {
|
|
82
|
+
// Unreachable API: keep rendering the cached/baked values or fallbacks.
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
revalidate();
|
|
86
|
+
// Live updates, no refresh needed:
|
|
87
|
+
// 1. Inside the Fimo Studio preview, every save posts
|
|
88
|
+
// `{ type: 'content/refresh' }` into the iframe — refetch on the spot
|
|
89
|
+
// (same contract the template runtime's RefreshContentProvider uses).
|
|
90
|
+
// 2. In a plain tab, coming back to the site (alt-tab from the Studio)
|
|
91
|
+
// revalidates on focus/visibility — edits appear without a reload.
|
|
92
|
+
let lastRun = Date.now();
|
|
93
|
+
const throttledRevalidate = () => {
|
|
94
|
+
if (Date.now() - lastRun < 1500) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
lastRun = Date.now();
|
|
98
|
+
revalidate();
|
|
99
|
+
};
|
|
100
|
+
const onMessage = (event) => {
|
|
101
|
+
if (event.data?.type === 'content/refresh') {
|
|
102
|
+
lastRun = Date.now();
|
|
103
|
+
revalidate();
|
|
49
104
|
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Wrapped rich text and media live in the plumbing types — without this
|
|
55
|
-
// fetch a client-rendered app only ever shows their inline fallbacks
|
|
56
|
-
// and Studio edits to them render nowhere.
|
|
57
|
-
fetchWrappedContent(apiUrl)
|
|
58
|
-
.then((fetched) => {
|
|
59
|
-
if (!cancelled) {
|
|
60
|
-
setContent(fetched);
|
|
105
|
+
};
|
|
106
|
+
const onVisibility = () => {
|
|
107
|
+
if (document.visibilityState === 'visible') {
|
|
108
|
+
throttledRevalidate();
|
|
61
109
|
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener('message', onMessage);
|
|
112
|
+
window.addEventListener('focus', throttledRevalidate);
|
|
113
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
66
114
|
return () => {
|
|
67
|
-
|
|
115
|
+
disposed = true;
|
|
116
|
+
window.removeEventListener('message', onMessage);
|
|
117
|
+
window.removeEventListener('focus', throttledRevalidate);
|
|
118
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
68
119
|
};
|
|
69
120
|
}, [apiUrl, config]);
|
|
70
121
|
// Explicitly-passed maps win over the fetched ones (caller override).
|
|
@@ -30,4 +30,5 @@ export type { FimoLocaleResolution } from '../i18n/locale.js';
|
|
|
30
30
|
export { getDisplayRoutes } from '../shared/display-routes.js';
|
|
31
31
|
export type { DisplayRoute, ParamInfo } from '../shared/display-routes.js';
|
|
32
32
|
export type { FimoConfig, FimoRouteConfig, FimoSEO } from '../shared/fimo-config.types.js';
|
|
33
|
+
export { serializeFimoSpaContent } from './spa-content-cache.js';
|
|
33
34
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -37,3 +37,4 @@ export { FimoLocaleProvider, useLocale } from '../i18n/FimoLocaleContext.js';
|
|
|
37
37
|
export { getFimoDefaultLocale, getFimoLocales, getFimoTextDirection, localizeFimoPath, resolveFimoLocale, toHtmlLang, } from '../i18n/locale.js';
|
|
38
38
|
// Config + routes
|
|
39
39
|
export { getDisplayRoutes } from '../shared/display-routes.js';
|
|
40
|
+
export { serializeFimoSpaContent } from './spa-content-cache.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WrappedContent } from '../wrapped/content-fetch.pure.js';
|
|
2
|
+
import type { WrappedLabels } from '../wrapped/WrappedProviders.js';
|
|
3
|
+
/**
|
|
4
|
+
* Instant first paint for client-rendered wrapped apps: the SDK controls
|
|
5
|
+
* rendering, so it never has to flash fallbacks while the network round
|
|
6
|
+
* trips. Two synchronous sources, in priority order:
|
|
7
|
+
*
|
|
8
|
+
* 1. `window.__FIMO_CONTENT__` — baked by the site's SSG/prerender step
|
|
9
|
+
* (`serializeFimoSpaContent`), zero flash even on the first ever visit;
|
|
10
|
+
* 2. the last fetched payload cached in localStorage — instant from the
|
|
11
|
+
* second visit on, revalidated in the background (stale-while-revalidate).
|
|
12
|
+
*/
|
|
13
|
+
export interface SpaContentPayload {
|
|
14
|
+
labels?: WrappedLabels;
|
|
15
|
+
content?: WrappedContent;
|
|
16
|
+
}
|
|
17
|
+
declare global {
|
|
18
|
+
interface Window {
|
|
19
|
+
__FIMO_CONTENT__?: SpaContentPayload;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export declare function readInitialSpaContent(apiUrl: string | undefined): SpaContentPayload | null;
|
|
23
|
+
export declare function writeSpaContentCache(apiUrl: string, payload: SpaContentPayload): void;
|
|
24
|
+
/** SSG helper: a script tag baking the payload into prerendered HTML so the
|
|
25
|
+
* provider hydrates it synchronously — zero content flash. */
|
|
26
|
+
export declare function serializeFimoSpaContent(payload: SpaContentPayload): string;
|
|
27
|
+
//# sourceMappingURL=spa-content-cache.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const CACHE_PREFIX = 'fimo:spa-content:';
|
|
2
|
+
export function readInitialSpaContent(apiUrl) {
|
|
3
|
+
if (typeof window === 'undefined') {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
if (window.__FIMO_CONTENT__ && typeof window.__FIMO_CONTENT__ === 'object') {
|
|
7
|
+
return window.__FIMO_CONTENT__;
|
|
8
|
+
}
|
|
9
|
+
if (!apiUrl) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const raw = window.localStorage.getItem(CACHE_PREFIX + apiUrl);
|
|
14
|
+
return raw ? JSON.parse(raw) : null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writeSpaContentCache(apiUrl, payload) {
|
|
21
|
+
if (typeof window === 'undefined') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
window.localStorage.setItem(CACHE_PREFIX + apiUrl, JSON.stringify(payload));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Quota/private mode: the cache is an optimization, never a requirement.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** SSG helper: a script tag baking the payload into prerendered HTML so the
|
|
32
|
+
* provider hydrates it synchronously — zero content flash. */
|
|
33
|
+
export function serializeFimoSpaContent(payload) {
|
|
34
|
+
const json = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
35
|
+
return `<script>window.__FIMO_CONTENT__=${json}</script>`;
|
|
36
|
+
}
|
|
@@ -7,6 +7,9 @@ interface FimoRichTextBlockProps {
|
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
components?: FimoRichTextComponents;
|
|
9
9
|
className?: string;
|
|
10
|
+
/** Wrapper element — `span` when the block sits inside a `<p>` (a div
|
|
11
|
+
* there is invalid HTML and breaks hydration). */
|
|
12
|
+
as?: 'div' | 'span';
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Wraps formatted text (bold, italic, underline, links) so it becomes
|
|
@@ -18,6 +21,6 @@ interface FimoRichTextBlockProps {
|
|
|
18
21
|
* <p>We help teams <strong>ship faster</strong> and <em>stay safe</em>.</p>
|
|
19
22
|
* </FimoRichTextBlock>
|
|
20
23
|
*/
|
|
21
|
-
export declare function FimoRichTextBlock({ id, children, components, className }: FimoRichTextBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function FimoRichTextBlock({ id, children, components, className, as }: FimoRichTextBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
22
25
|
export {};
|
|
23
26
|
//# sourceMappingURL=FimoRichTextBlock.d.ts.map
|
|
@@ -13,10 +13,10 @@ import { useWrappedRichText } from './WrappedContentContext.js';
|
|
|
13
13
|
* <p>We help teams <strong>ship faster</strong> and <em>stay safe</em>.</p>
|
|
14
14
|
* </FimoRichTextBlock>
|
|
15
15
|
*/
|
|
16
|
-
export function FimoRichTextBlock({ id, children, components, className }) {
|
|
16
|
+
export function FimoRichTextBlock({ id, children, components, className, as }) {
|
|
17
17
|
const entry = useWrappedRichText(id);
|
|
18
18
|
if (!entry) {
|
|
19
19
|
return _jsx(_Fragment, { children: children });
|
|
20
20
|
}
|
|
21
|
-
return (_jsx(RichText, { value: new FimoRichText(entry.content, entry.source), components: components, className: className }));
|
|
21
|
+
return (_jsx(RichText, { value: new FimoRichText(entry.content, entry.source), components: components, className: className, as: as }));
|
|
22
22
|
}
|
package/package.json
CHANGED