fimo 0.9.0-experimental.7 → 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/assets/skills/fimo-migration/references/pages.md +140 -1
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +359 -187
- package/dist/runtime/app/FimoProviders.js +29 -7
- 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 +82 -14
- 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/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
- package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
- package/dist/runtime/wrapped/server.d.ts +5 -2
- package/dist/runtime/wrapped/server.js +15 -45
- package/dist/scripts/apply-wrap-templates.js +1 -1
- package/dist/scripts/apply-wrap.js +26 -2
- package/dist/scripts/cms/strapi/wiring.js +1 -1
- package/dist/scripts/detect-framework.js +6 -0
- package/dist/scripts/draft-rows.js +36 -0
- package/dist/scripts/frameworks/astro/routes.js +2 -18
- package/dist/scripts/frameworks/discover.d.ts +19 -6
- package/dist/scripts/frameworks/discover.js +96 -3
- package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
- package/dist/scripts/frameworks/discovered-route.js +17 -0
- package/dist/scripts/frameworks/next/routes.d.ts +7 -0
- package/dist/scripts/frameworks/next/routes.js +31 -12
- package/dist/scripts/frameworks/nuxt/routes.js +2 -18
- package/dist/scripts/frameworks/remix/routes.js +4 -18
- package/dist/scripts/frameworks/sveltekit/routes.js +4 -19
- package/dist/scripts/frameworks/vite/routes.d.ts +5 -0
- package/dist/scripts/frameworks/vite/routes.js +59 -9
- package/dist/scripts/frameworks/vite/wiring.d.ts +12 -0
- package/dist/scripts/frameworks/vite/wiring.js +68 -23
- package/dist/scripts/lib/humanize.d.ts +14 -0
- package/dist/scripts/lib/humanize.js +54 -0
- package/dist/scripts/render-parity/capture.d.ts +7 -0
- package/dist/scripts/render-parity/capture.js +32 -4
- package/dist/scripts/routes/label-prefixes.d.ts +49 -0
- package/dist/scripts/routes/label-prefixes.js +136 -0
- package/dist/scripts/routes/module-resolve.d.ts +20 -0
- package/dist/scripts/routes/module-resolve.js +78 -0
- package/dist/scripts/routes/page-structure.d.ts +72 -0
- package/dist/scripts/routes/page-structure.js +234 -0
- package/dist/scripts/scan-wrappable.d.ts +4 -0
- package/dist/scripts/scan-wrappable.js +15 -2
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
- package/dist/scripts/lib/discover-next-routes.d.ts +0 -17
- package/dist/scripts/lib/discover-next-routes.js +0 -134
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
4
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
4
5
|
import { useLocation, useNavigate } from 'react-router';
|
|
5
6
|
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
|
|
@@ -59,6 +60,27 @@ function PreviewBridge() {
|
|
|
59
60
|
}, [navigate, sendRoutes, sendLocationChange]);
|
|
60
61
|
return null;
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Template projects mount their own QueryClientProvider in root.tsx, but an
|
|
65
|
+
* ADOPTED app wired straight onto FimoProviders (react-router library SPAs)
|
|
66
|
+
* has none — and the inner runtime (RefreshContentProvider, content clients)
|
|
67
|
+
* calls useQueryClient(). Mount a fallback client only when no provider
|
|
68
|
+
* exists above, so both worlds work and template caches stay untouched.
|
|
69
|
+
*/
|
|
70
|
+
function EnsureQueryClient({ children }) {
|
|
71
|
+
let existing = null;
|
|
72
|
+
try {
|
|
73
|
+
existing = useQueryClient();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
existing = null;
|
|
77
|
+
}
|
|
78
|
+
const [fallbackClient] = useState(() => new QueryClient());
|
|
79
|
+
if (existing) {
|
|
80
|
+
return _jsx(_Fragment, { children: children });
|
|
81
|
+
}
|
|
82
|
+
return _jsx(QueryClientProvider, { client: fallbackClient, children: children });
|
|
83
|
+
}
|
|
62
84
|
export function FimoProviders({ children, locale }) {
|
|
63
85
|
const location = useLocation();
|
|
64
86
|
const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
|
|
@@ -71,9 +93,9 @@ export function FimoProviders({ children, locale }) {
|
|
|
71
93
|
document.documentElement.lang = toHtmlLang(activeLocale);
|
|
72
94
|
document.documentElement.dir = activeDir;
|
|
73
95
|
}, [activeDir, activeLocale]);
|
|
74
|
-
return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(FimoLocaleProvider, { value: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(EnsureQueryClient, { children: _jsx(FimoLocaleProvider, { value: {
|
|
97
|
+
...localeResolution,
|
|
98
|
+
locale: activeLocale,
|
|
99
|
+
dir: activeDir,
|
|
100
|
+
}, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(PreviewBridge, {}), _jsx(OptionalTracking, { routes: getDisplayRoutes() }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
|
|
79
101
|
}
|
|
@@ -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,9 +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
|
+
import { fetchWrappedContent } from '../wrapped/content-fetch.pure.js';
|
|
6
7
|
import { WrappedProviders } from '../wrapped/WrappedProviders.js';
|
|
8
|
+
import { readInitialSpaContent, writeSpaContentCache } from './spa-content-cache.js';
|
|
7
9
|
/** Browser-safe stable signature (labelSignature uses Buffer, server-only). */
|
|
8
10
|
function bundleSignature(bundle) {
|
|
9
11
|
const serialized = serializeBundle(bundle);
|
|
@@ -33,31 +35,97 @@ function bundleSignature(bundle) {
|
|
|
33
35
|
* router without depending on one.
|
|
34
36
|
*/
|
|
35
37
|
export function FimoSpaProviders({ config, apiUrl, previewScriptUrl, media, richText, locale, children, }) {
|
|
36
|
-
|
|
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
|
+
});
|
|
37
48
|
useEffect(() => {
|
|
38
49
|
if (!apiUrl) {
|
|
39
50
|
return;
|
|
40
51
|
}
|
|
41
|
-
let
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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();
|
|
47
104
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
};
|
|
106
|
+
const onVisibility = () => {
|
|
107
|
+
if (document.visibilityState === 'visible') {
|
|
108
|
+
throttledRevalidate();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener('message', onMessage);
|
|
112
|
+
window.addEventListener('focus', throttledRevalidate);
|
|
113
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
52
114
|
return () => {
|
|
53
|
-
|
|
115
|
+
disposed = true;
|
|
116
|
+
window.removeEventListener('message', onMessage);
|
|
117
|
+
window.removeEventListener('focus', throttledRevalidate);
|
|
118
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
54
119
|
};
|
|
55
120
|
}, [apiUrl, config]);
|
|
121
|
+
// Explicitly-passed maps win over the fetched ones (caller override).
|
|
122
|
+
const mergedMedia = useMemo(() => (content?.media || media ? { ...(content?.media ?? {}), ...(media ?? {}) } : undefined), [content?.media, media]);
|
|
123
|
+
const mergedRichText = useMemo(() => (content?.richText || richText ? { ...(content?.richText ?? {}), ...(richText ?? {}) } : undefined), [content?.richText, richText]);
|
|
56
124
|
const navigation = useMemo(() => ({
|
|
57
125
|
pathname: typeof window !== 'undefined' ? window.location.pathname : null,
|
|
58
126
|
navigate: (to) => {
|
|
59
127
|
window.location.assign(to);
|
|
60
128
|
},
|
|
61
129
|
}), []);
|
|
62
|
-
return (_jsxs(WrappedProviders, { config: config, labels: labels, media:
|
|
130
|
+
return (_jsxs(WrappedProviders, { config: config, labels: labels, media: mergedMedia, richText: mergedRichText, locale: locale, navigation: navigation, children: [previewScriptUrl ? _jsx("script", { type: "module", src: previewScriptUrl }) : null, children] }));
|
|
63
131
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WrappedMediaEntry, WrappedRichTextEntry } from './WrappedContentContext.js';
|
|
2
|
+
export interface WrappedEntryRow {
|
|
3
|
+
id: string;
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
/** Every published row of a content type, paginated to the end. Returns []
|
|
7
|
+
* when the API is unreachable so wrapped sites keep their fallbacks. */
|
|
8
|
+
export declare function fetchAllWrappedRows(apiUrl: string, typeName: string): Promise<WrappedEntryRow[]>;
|
|
9
|
+
/** WrappedMedia rows → key → media entry (the FimoMediaImage/Video id map). */
|
|
10
|
+
export declare function mapWrappedMedia(rows: WrappedEntryRow[]): Record<string, WrappedMediaEntry>;
|
|
11
|
+
/** WrappedRichText rows → key → TipTap content (the FimoRichTextBlock map). */
|
|
12
|
+
export declare function mapWrappedRichText(rows: WrappedEntryRow[]): Record<string, WrappedRichTextEntry>;
|
|
13
|
+
export interface WrappedContent {
|
|
14
|
+
media: Record<string, WrappedMediaEntry>;
|
|
15
|
+
richText: Record<string, WrappedRichTextEntry>;
|
|
16
|
+
}
|
|
17
|
+
/** Both plumbing maps in one round: what a client-rendered wrapped app needs
|
|
18
|
+
* so Studio edits to rich text and media are LIVE, not fallback-only. */
|
|
19
|
+
export declare function fetchWrappedContent(apiUrl: string): Promise<WrappedContent>;
|
|
20
|
+
//# sourceMappingURL=content-fetch.pure.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Wrapped-content fetching, PURE core: fetch/URL/AbortController only — no
|
|
2
|
+
// node builtins, importable from client-bundled code (the SPA provider) and
|
|
3
|
+
// from the server loaders alike. The plumbing types (WrappedRichText,
|
|
4
|
+
// WrappedMedia) hold ALL of a site's wrapped rich text and media, so the
|
|
5
|
+
// fetch PAGINATES to the end — the tenant API caps limit at 100 and a real
|
|
6
|
+
// site exceeds it (the Fimo site carries 147 rich-text entries).
|
|
7
|
+
import { fetchWithTimeout } from '../shared/labels-fetch.pure.js';
|
|
8
|
+
const PAGE_LIMIT = 100;
|
|
9
|
+
const MAX_ROWS = 5000;
|
|
10
|
+
function readString(value) {
|
|
11
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function readNumber(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Every published row of a content type, paginated to the end. Returns []
|
|
17
|
+
* when the API is unreachable so wrapped sites keep their fallbacks. */
|
|
18
|
+
export async function fetchAllWrappedRows(apiUrl, typeName) {
|
|
19
|
+
const rows = [];
|
|
20
|
+
try {
|
|
21
|
+
for (let offset = 0; offset < MAX_ROWS; offset += PAGE_LIMIT) {
|
|
22
|
+
const url = new URL(`/entries/${typeName}`, apiUrl);
|
|
23
|
+
url.searchParams.set('limit', String(PAGE_LIMIT));
|
|
24
|
+
url.searchParams.set('offset', String(offset));
|
|
25
|
+
const response = await fetchWithTimeout(url, 4000);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
const page = Array.isArray(body.data) ? body.data : [];
|
|
31
|
+
for (const row of page) {
|
|
32
|
+
if (!row || typeof row !== 'object') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const record = row;
|
|
36
|
+
const id = String(record.id ?? record.documentId ?? '');
|
|
37
|
+
if (id.length === 0) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
rows.push({
|
|
41
|
+
id,
|
|
42
|
+
data: (record.data && typeof record.data === 'object' ? record.data : record),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const hasMore = body.meta?.hasMore === true || page.length === PAGE_LIMIT;
|
|
46
|
+
if (!hasMore) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Unreachable API mid-pagination: return what we have (possibly []).
|
|
53
|
+
}
|
|
54
|
+
return rows;
|
|
55
|
+
}
|
|
56
|
+
/** WrappedMedia rows → key → media entry (the FimoMediaImage/Video id map). */
|
|
57
|
+
export function mapWrappedMedia(rows) {
|
|
58
|
+
const map = {};
|
|
59
|
+
for (const entry of rows) {
|
|
60
|
+
const key = readString(entry.data.key) ?? readString(entry.data.slug);
|
|
61
|
+
const asset = entry.data.image;
|
|
62
|
+
if (!key || !asset || typeof asset !== 'object') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const assetRecord = asset;
|
|
66
|
+
const url = readString(assetRecord.url);
|
|
67
|
+
if (!url) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
map[key] = {
|
|
71
|
+
url,
|
|
72
|
+
alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
|
|
73
|
+
name: readString(assetRecord.name),
|
|
74
|
+
mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
|
|
75
|
+
width: readNumber(assetRecord.width),
|
|
76
|
+
height: readNumber(assetRecord.height),
|
|
77
|
+
source: `WrappedMedia.${entry.id}.image`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return map;
|
|
81
|
+
}
|
|
82
|
+
/** WrappedRichText rows → key → TipTap content (the FimoRichTextBlock map). */
|
|
83
|
+
export function mapWrappedRichText(rows) {
|
|
84
|
+
const map = {};
|
|
85
|
+
for (const entry of rows) {
|
|
86
|
+
const key = readString(entry.data.key) ?? readString(entry.data.slug);
|
|
87
|
+
const body = entry.data.body;
|
|
88
|
+
if (!key || !body || typeof body !== 'object') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
map[key] = {
|
|
92
|
+
content: body,
|
|
93
|
+
source: `WrappedRichText.${entry.id}.body`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
/** Both plumbing maps in one round: what a client-rendered wrapped app needs
|
|
99
|
+
* so Studio edits to rich text and media are LIVE, not fallback-only. */
|
|
100
|
+
export async function fetchWrappedContent(apiUrl) {
|
|
101
|
+
const [mediaRows, richTextRows] = await Promise.all([
|
|
102
|
+
fetchAllWrappedRows(apiUrl, 'WrappedMedia'),
|
|
103
|
+
fetchAllWrappedRows(apiUrl, 'WrappedRichText'),
|
|
104
|
+
]);
|
|
105
|
+
return { media: mapWrappedMedia(mediaRows), richText: mapWrappedRichText(richTextRows) };
|
|
106
|
+
}
|
|
@@ -39,12 +39,15 @@ export declare function fetchEntries(typeName: string, options?: FimoServerOptio
|
|
|
39
39
|
/**
|
|
40
40
|
* Load the WrappedMedia entries (key → media) seeded by the wrap migration.
|
|
41
41
|
* Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
|
|
42
|
-
* asset-typed `image` field.
|
|
42
|
+
* asset-typed `image` field. Paginates to the end — a site's wrapped media
|
|
43
|
+
* routinely exceeds one API page.
|
|
43
44
|
*/
|
|
44
45
|
export declare function loadWrappedMedia(options?: FimoServerOptions): Promise<Record<string, WrappedMediaEntry>>;
|
|
45
46
|
/**
|
|
46
47
|
* Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
|
|
47
|
-
* migration for formatted text (bold/italic/underline/links).
|
|
48
|
+
* migration for formatted text (bold/italic/underline/links). Paginates to
|
|
49
|
+
* the end — the Fimo site alone carries 147 entries against a 20-row API
|
|
50
|
+
* default page.
|
|
48
51
|
*/
|
|
49
52
|
export declare function loadWrappedRichText(options?: FimoServerOptions): Promise<Record<string, WrappedRichTextEntry>>;
|
|
50
53
|
/**
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { getConfigServer } from '../shared/fimo-config.server.js';
|
|
6
6
|
import { deepMergeMessages, unflattenMessages } from '../shared/i18n-messages.js';
|
|
7
7
|
import { fetchLabelDictionary, fetchWithTimeout, labelSignature, loadLabelBundle, readProjectEnv, resolveTenantApiUrl, } from '../shared/labels-fetch.server.js';
|
|
8
|
+
import { fetchAllWrappedRows, mapWrappedMedia, mapWrappedRichText } from './content-fetch.pure.js';
|
|
8
9
|
export function loadFimoConfig(rootDir = process.cwd()) {
|
|
9
10
|
return getConfigServer(rootDir);
|
|
10
11
|
}
|
|
@@ -72,62 +73,31 @@ export async function fetchEntries(typeName, options = {}) {
|
|
|
72
73
|
return [];
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
|
-
function readString(value) {
|
|
76
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
77
|
-
}
|
|
78
|
-
function readNumber(value) {
|
|
79
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
80
|
-
}
|
|
81
76
|
/**
|
|
82
77
|
* Load the WrappedMedia entries (key → media) seeded by the wrap migration.
|
|
83
78
|
* Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
|
|
84
|
-
* asset-typed `image` field.
|
|
79
|
+
* asset-typed `image` field. Paginates to the end — a site's wrapped media
|
|
80
|
+
* routinely exceeds one API page.
|
|
85
81
|
*/
|
|
86
82
|
export async function loadWrappedMedia(options = {}) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!key || !asset || typeof asset !== 'object') {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const assetRecord = asset;
|
|
96
|
-
const url = readString(assetRecord.url);
|
|
97
|
-
if (!url) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
map[key] = {
|
|
101
|
-
url,
|
|
102
|
-
alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
|
|
103
|
-
name: readString(assetRecord.name),
|
|
104
|
-
mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
|
|
105
|
-
width: readNumber(assetRecord.width),
|
|
106
|
-
height: readNumber(assetRecord.height),
|
|
107
|
-
source: `WrappedMedia.${entry.id}.image`,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
return map;
|
|
83
|
+
const apiUrl = await resolveApiUrl(options);
|
|
84
|
+
if (!apiUrl) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return mapWrappedMedia(await fetchAllWrappedRows(apiUrl, 'WrappedMedia'));
|
|
111
88
|
}
|
|
112
89
|
/**
|
|
113
90
|
* Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
|
|
114
|
-
* migration for formatted text (bold/italic/underline/links).
|
|
91
|
+
* migration for formatted text (bold/italic/underline/links). Paginates to
|
|
92
|
+
* the end — the Fimo site alone carries 147 entries against a 20-row API
|
|
93
|
+
* default page.
|
|
115
94
|
*/
|
|
116
95
|
export async function loadWrappedRichText(options = {}) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const key = readString(entry.data.key) ?? readString(entry.data.slug);
|
|
121
|
-
const body = entry.data.body;
|
|
122
|
-
if (!key || !body || typeof body !== 'object') {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
map[key] = {
|
|
126
|
-
content: body,
|
|
127
|
-
source: `WrappedRichText.${entry.id}.body`,
|
|
128
|
-
};
|
|
96
|
+
const apiUrl = await resolveApiUrl(options);
|
|
97
|
+
if (!apiUrl) {
|
|
98
|
+
return {};
|
|
129
99
|
}
|
|
130
|
-
return
|
|
100
|
+
return mapWrappedRichText(await fetchAllWrappedRows(apiUrl, 'WrappedRichText'));
|
|
131
101
|
}
|
|
132
102
|
/** Key fields tried, in order, to align a source row with its Fimo entry. */
|
|
133
103
|
const COLLECTION_KEY_FIELDS = ['slug', 'id', 'key', 'name', 'title', 'question'];
|
|
@@ -66,7 +66,7 @@ export function applyTemplateWrapToFile(code, style, items, allocator) {
|
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
68
|
const collapsed = match[2].replace(/\s+/g, ' ').trim();
|
|
69
|
-
const id = allocator.allocate(scope || 'shared', slugify(collapsed));
|
|
69
|
+
const id = allocator.allocate(scope || 'shared', slugify(decodeJsxEntities(collapsed)));
|
|
70
70
|
const escaped = collapsed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
71
71
|
const replacement = `>${match[1]}${style.open}"${id}", "${escaped}"${style.close}${match[3]}<`;
|
|
72
72
|
next = next.slice(0, match.index) + replacement + next.slice(match.index + match[0].length);
|
|
@@ -33,10 +33,27 @@ const JSX_ENTITIES = {
|
|
|
33
33
|
''': "'",
|
|
34
34
|
' ': ' ',
|
|
35
35
|
'©': '©',
|
|
36
|
+
'®': '®',
|
|
37
|
+
'™': '™',
|
|
38
|
+
'°': '°',
|
|
39
|
+
'·': '·',
|
|
40
|
+
'•': '•',
|
|
41
|
+
'…': '…',
|
|
42
|
+
'—': '—',
|
|
43
|
+
'–': '–',
|
|
44
|
+
'‘': '‘',
|
|
45
|
+
'’': '’',
|
|
46
|
+
'“': '“',
|
|
47
|
+
'”': '”',
|
|
48
|
+
'«': '«',
|
|
49
|
+
'»': '»',
|
|
50
|
+
'×': '×',
|
|
51
|
+
'→': '→',
|
|
52
|
+
'←': '←',
|
|
36
53
|
};
|
|
37
54
|
export function decodeJsxEntities(text) {
|
|
38
55
|
return text
|
|
39
|
-
.replace(/&(amp|lt|gt|quot|apos|nbsp|copy);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
|
|
56
|
+
.replace(/&(amp|lt|gt|quot|apos|nbsp|copy|reg|trade|deg|middot|bull|hellip|mdash|ndash|lsquo|rsquo|ldquo|rdquo|laquo|raquo|times|rarr|larr);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
|
|
40
57
|
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
41
58
|
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16)));
|
|
42
59
|
}
|
|
@@ -62,7 +79,12 @@ export function keyScope(file) {
|
|
|
62
79
|
return slugify(stem) || 'shared';
|
|
63
80
|
}
|
|
64
81
|
export function slugify(text) {
|
|
82
|
+
// camelCase boundaries become segments BEFORE lowercasing, so component
|
|
83
|
+
// file names keep their word structure (AgentStrip.tsx → agent-strip.*)
|
|
84
|
+
// and every humanized display of the key reads like the component name.
|
|
65
85
|
return collapse(text)
|
|
86
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
87
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
66
88
|
.toLowerCase()
|
|
67
89
|
.normalize('NFD')
|
|
68
90
|
.replace(/[̀-ͯ]/g, '')
|
|
@@ -154,7 +176,9 @@ export function applyWrapToFile(code, items, allocator, importModule = 'fimo/nex
|
|
|
154
176
|
}
|
|
155
177
|
const lead = raw.slice(0, raw.indexOf(core));
|
|
156
178
|
const trail = raw.slice(raw.indexOf(core) + core.length);
|
|
157
|
-
|
|
179
|
+
// Slug from the DECODED text — "'" must never leak "apos" into a
|
|
180
|
+
// key ("today-apos-s-total" read "Today Apos S Total" in the Studio).
|
|
181
|
+
const id = allocator.allocate(scope, slugify(decodeJsxEntities(core)));
|
|
158
182
|
edits.push({
|
|
159
183
|
start: span.start,
|
|
160
184
|
end: span.end,
|
|
@@ -35,7 +35,7 @@ const SINGLETONS = [${singletons.map((name) => `'${name}'`).join(', ')}];
|
|
|
35
35
|
|
|
36
36
|
// Structural field shapes (single components read as objects; block items and
|
|
37
37
|
// dynamic zones reshape into the Strapi shape the components expect).
|
|
38
|
-
const FIELD_SHAPES = ${fieldShapes};
|
|
38
|
+
const FIELD_SHAPES = ${fieldShapes} as const;
|
|
39
39
|
|
|
40
40
|
export const fimoStrapiFetch = createFimoStrapiFetch({
|
|
41
41
|
types: TYPES,
|
|
@@ -18,6 +18,12 @@ export function detectFramework(cwd) {
|
|
|
18
18
|
return { framework: null };
|
|
19
19
|
}
|
|
20
20
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
21
|
+
// A CMS backend is never an adoptable frontend — Strapi 5 ships vite+react
|
|
22
|
+
// devDeps for its admin panel, which the bare-vite fallback would match
|
|
23
|
+
// (the sandbox/website monorepo suggested `fimo init apps/strapi`).
|
|
24
|
+
if (deps['@strapi/strapi']) {
|
|
25
|
+
return { framework: null };
|
|
26
|
+
}
|
|
21
27
|
if (deps.next) {
|
|
22
28
|
const appDir = ['app', 'src/app'].find((dir) => existsSync(join(cwd, dir)));
|
|
23
29
|
const pagesDir = ['pages', 'src/pages'].find((dir) => existsSync(join(cwd, dir)));
|