fimo-react-router 0.21.0-experimental.1
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/README.md +19 -0
- package/dist/AppResetScrollOnNavigate.d.ts +2 -0
- package/dist/AppResetScrollOnNavigate.js +12 -0
- package/dist/AppRoutesProvider.d.ts +25 -0
- package/dist/AppRoutesProvider.js +108 -0
- package/dist/FimoProviders.d.ts +30 -0
- package/dist/FimoProviders.js +187 -0
- package/dist/FimoScripts.d.ts +13 -0
- package/dist/FimoScripts.js +45 -0
- package/dist/OptionalTracking.d.ts +13 -0
- package/dist/OptionalTracking.js +28 -0
- package/dist/Pageview.d.ts +9 -0
- package/dist/Pageview.js +118 -0
- package/dist/active-routes.d.ts +12 -0
- package/dist/active-routes.js +60 -0
- package/dist/fixtures/virtual-fimo-config.d.ts +3 -0
- package/dist/fixtures/virtual-fimo-config.js +3 -0
- package/dist/fixtures/virtual-translations.d.ts +5 -0
- package/dist/fixtures/virtual-translations.js +9 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/navigation-utils.d.ts +8 -0
- package/dist/navigation-utils.js +36 -0
- package/dist/navigation.d.ts +12 -0
- package/dist/navigation.js +28 -0
- package/dist/prefetch.d.ts +9 -0
- package/dist/prefetch.js +39 -0
- package/dist/preset.d.ts +30 -0
- package/dist/preset.js +33 -0
- package/dist/routes.d.ts +35 -0
- package/dist/routes.js +85 -0
- package/dist/seo.d.ts +93 -0
- package/dist/seo.js +74 -0
- package/dist/vite.d.ts +20 -0
- package/dist/vite.js +104 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# fimo-react-router
|
|
2
|
+
|
|
3
|
+
The Fimo integration for React Router projects. Install it alongside `fimo` and
|
|
4
|
+
`fimo-react` at the same version:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install fimo fimo-react fimo-react-router
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
| Entry point | What it owns |
|
|
11
|
+
| -------------------------- | -------------------------------------------------------------------------------- |
|
|
12
|
+
| `fimo-react-router` | `FimoProviders`, `FimoScripts`, `FimoLink`, locale helpers, route `meta` helpers |
|
|
13
|
+
| `fimo-react-router/routes` | `fimoRoutes()` — the path-prefix locale route expander |
|
|
14
|
+
| `fimo-react-router/preset` | `fimoPreset()` — connects a client-rendered React Router build to Fimo hosting |
|
|
15
|
+
| `fimo-react-router/vite` | `fimo()` — `fimo-vite`'s plugins plus React Router dependency pre-bundling |
|
|
16
|
+
|
|
17
|
+
Content clients, labels, and the editable primitives come from `fimo-react`; the
|
|
18
|
+
framework-neutral Vite plugins come from `fimo-vite`. A React project that does
|
|
19
|
+
not use React Router only needs those two.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router';
|
|
3
|
+
export function AppResetScrollOnNavigate() {
|
|
4
|
+
const { pathname } = useLocation();
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
try {
|
|
7
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
|
8
|
+
}
|
|
9
|
+
catch { }
|
|
10
|
+
}, [pathname]);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { RouteObject } from 'react-router';
|
|
3
|
+
export interface SlugInfo {
|
|
4
|
+
schema: string;
|
|
5
|
+
queryField: string;
|
|
6
|
+
displayField: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AppRoute extends Omit<RouteObject, 'children' | 'element'> {
|
|
9
|
+
name?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
element: React.ReactNode;
|
|
12
|
+
index?: boolean;
|
|
13
|
+
slugs?: Record<string, SlugInfo>;
|
|
14
|
+
children?: AppRoute[];
|
|
15
|
+
}
|
|
16
|
+
export interface DisplayRoute extends Omit<AppRoute, 'element' | 'children' | 'handle'> {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function getDisplayRoutes(routes: AppRoute[], basePath?: string): DisplayRoute[];
|
|
21
|
+
export declare function AppRoutesProvider({ appRoutes, children }: {
|
|
22
|
+
appRoutes?: AppRoute[];
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=AppRoutesProvider.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AppErrorBoundary, DataSourcesProvider, RefreshContentProvider, ScreenshotProvider, ViteLifecycles, } from 'fimo-react';
|
|
3
|
+
import { onError } from 'fimo-react/internal';
|
|
4
|
+
import { getPreviewLocationChange, getPreviewParentOrigin, getSafePreviewNavigationPath, getDocumentLocale, isPreviewRuntimeEnabled, } from 'fimo/preview';
|
|
5
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
6
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
7
|
+
import { useLocation, useNavigate, useRoutes } from 'react-router';
|
|
8
|
+
import { AppResetScrollOnNavigate } from './AppResetScrollOnNavigate.js';
|
|
9
|
+
import { OptionalTracking } from './OptionalTracking.js';
|
|
10
|
+
const titleCase = (seg) => seg
|
|
11
|
+
.replace(/[_-]+/g, ' ')
|
|
12
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
13
|
+
.split(/\s+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
16
|
+
.join(' ');
|
|
17
|
+
const generateRouteName = (path) => {
|
|
18
|
+
if (path === '/') {
|
|
19
|
+
return 'Home';
|
|
20
|
+
}
|
|
21
|
+
const firstSegment = path.replace(/^\//, '').split('/')[0];
|
|
22
|
+
return titleCase(firstSegment);
|
|
23
|
+
};
|
|
24
|
+
export function getDisplayRoutes(routes, basePath = '') {
|
|
25
|
+
const displayRoutes = [];
|
|
26
|
+
for (const route of routes) {
|
|
27
|
+
const { children, ...rest } = route;
|
|
28
|
+
let fullPath;
|
|
29
|
+
if (rest.path) {
|
|
30
|
+
fullPath = rest.path.startsWith('/') ? rest.path : `${basePath.replace(/\/$/, '')}/${rest.path}`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
fullPath = basePath;
|
|
34
|
+
}
|
|
35
|
+
if (rest.path) {
|
|
36
|
+
displayRoutes.push({
|
|
37
|
+
path: fullPath,
|
|
38
|
+
index: rest.index,
|
|
39
|
+
slugs: rest.slugs,
|
|
40
|
+
name: rest.name || generateRouteName(fullPath),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (children) {
|
|
44
|
+
displayRoutes.push(...getDisplayRoutes(children, fullPath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return displayRoutes;
|
|
48
|
+
}
|
|
49
|
+
export function AppRoutesProvider({ appRoutes, children }) {
|
|
50
|
+
const routing = useRoutes((appRoutes ?? []));
|
|
51
|
+
const navigate = useNavigate();
|
|
52
|
+
const location = useLocation();
|
|
53
|
+
const displayRoutes = useMemo(() => getDisplayRoutes(appRoutes ?? []), [appRoutes]);
|
|
54
|
+
// @ts-ignore - import.meta.env is provided by Vite.
|
|
55
|
+
const previewEnabled = isPreviewRuntimeEnabled(import.meta.env.VITE_PREVIEW_SCRIPT_URL);
|
|
56
|
+
const sendRoutes = useCallback(() => {
|
|
57
|
+
if (!previewEnabled || !appRoutes?.length) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
window.parent.postMessage({ type: 'nav/routes', payload: { routes: displayRoutes } }, getPreviewParentOrigin() ?? '*');
|
|
61
|
+
}, [appRoutes, displayRoutes, previewEnabled]);
|
|
62
|
+
// This runtime predates locale-aware routing and has no fimo-config to read,
|
|
63
|
+
// so the document's own `lang` is the only locale the app has declared.
|
|
64
|
+
const sendLocationChange = useCallback(() => {
|
|
65
|
+
if (!previewEnabled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
69
|
+
window.parent.postMessage({
|
|
70
|
+
type: 'nav/locationChange',
|
|
71
|
+
payload: getPreviewLocationChange(location, getDocumentLocale(document, 'en'), parentOrigin),
|
|
72
|
+
}, parentOrigin ?? '*');
|
|
73
|
+
}, [location.hash, location.pathname, location.search, previewEnabled]);
|
|
74
|
+
useEffect(sendRoutes, [sendRoutes]);
|
|
75
|
+
useEffect(sendLocationChange, [sendLocationChange]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!previewEnabled) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const handleMessage = (event) => {
|
|
81
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
82
|
+
if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const type = event.data?.type;
|
|
86
|
+
const payload = event.data?.payload;
|
|
87
|
+
switch (type) {
|
|
88
|
+
case 'nav/navigate':
|
|
89
|
+
{
|
|
90
|
+
const target = getSafePreviewNavigationPath(payload?.to, window.location.origin);
|
|
91
|
+
if (target) {
|
|
92
|
+
navigate(target);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
case 'nav/read-routes':
|
|
97
|
+
sendRoutes();
|
|
98
|
+
sendLocationChange();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
window.addEventListener('message', handleMessage);
|
|
103
|
+
return () => {
|
|
104
|
+
window.removeEventListener('message', handleMessage);
|
|
105
|
+
};
|
|
106
|
+
}, [navigate, previewEnabled, sendRoutes, sendLocationChange]);
|
|
107
|
+
return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(OptionalTracking, { routes: displayRoutes }), _jsx(AppResetScrollOnNavigate, {}), appRoutes?.length ? routing : children] }) }) }) }) }));
|
|
108
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export { getPreviewLocationChange, getPreviewLocationPath } from 'fimo/preview';
|
|
3
|
+
export interface ParamInfo {
|
|
4
|
+
schema: string;
|
|
5
|
+
queryField: string;
|
|
6
|
+
displayField: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DisplayRoute {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
params?: Record<string, ParamInfo>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The routes the running app can actually navigate to right now.
|
|
16
|
+
*
|
|
17
|
+
* Studio gates preview navigation on this list, so it must come from the
|
|
18
|
+
* router that is currently loaded rather than the build-time config: after a
|
|
19
|
+
* merge, config metadata can describe a page whose route module has not
|
|
20
|
+
* reached this iframe yet. Names and params still come from config, which is
|
|
21
|
+
* where authored metadata lives. Falls back to config when the framework
|
|
22
|
+
* manifest is unreadable (SSR pass, non-framework mode).
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDisplayRoutes(frameworkManifest: unknown): DisplayRoute[];
|
|
25
|
+
export interface FimoProvidersProps {
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
locale?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function FimoProviders({ children, locale }: FimoProvidersProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=FimoProviders.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClient, QueryClientContext, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { AppErrorBoundary, DataSourcesProvider, RefreshContentProvider, ScreenshotProvider, ViteLifecycles, } from 'fimo-react';
|
|
4
|
+
import { FimoLocaleProvider, onError } from 'fimo-react/internal';
|
|
5
|
+
import { getFimoTextDirection, resolveFimoLocale, toHtmlLang } from 'fimo/config';
|
|
6
|
+
import { ensurePreviewScript, getPreviewLocationChange, getPreviewParentOrigin, getSafePreviewNavigationPath, isPreviewRuntimeEnabled, } from 'fimo/preview';
|
|
7
|
+
import { matchFimoRoute } from 'fimo/routes';
|
|
8
|
+
import { Suspense, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
9
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
10
|
+
import { UNSAFE_FrameworkContext, useLocation, useNavigate } from 'react-router';
|
|
11
|
+
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo-vite
|
|
12
|
+
import fimoConfig from 'virtual:fimo-config';
|
|
13
|
+
import { getActiveRouterRoutes } from './active-routes.js';
|
|
14
|
+
import { OptionalTracking } from './OptionalTracking.js';
|
|
15
|
+
export { getPreviewLocationChange, getPreviewLocationPath } from 'fimo/preview';
|
|
16
|
+
const titleCase = (seg) => seg
|
|
17
|
+
.replace(/[_-]+/g, ' ')
|
|
18
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
22
|
+
.join(' ');
|
|
23
|
+
const generateRouteName = (path) => {
|
|
24
|
+
if (path === '/') {
|
|
25
|
+
return 'Home';
|
|
26
|
+
}
|
|
27
|
+
const firstSegment = path.replace(/^\//, '').split('/')[0];
|
|
28
|
+
return titleCase(firstSegment);
|
|
29
|
+
};
|
|
30
|
+
function getConfigDisplayRoutes() {
|
|
31
|
+
const configRoutes = fimoConfig.routes ?? {};
|
|
32
|
+
return Object.entries(configRoutes)
|
|
33
|
+
.filter(([, route]) => route.path)
|
|
34
|
+
.map(([id, route]) => ({
|
|
35
|
+
id,
|
|
36
|
+
path: route.path,
|
|
37
|
+
name: route.name || generateRouteName(route.path),
|
|
38
|
+
params: route.params,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function getCanonicalRoutePath(path) {
|
|
42
|
+
return resolveFimoLocale({ pathname: path }, fimoConfig).routePath;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The locale a pathname renders.
|
|
46
|
+
*
|
|
47
|
+
* The declared route decides it. A route that declares no `:locale` segment is
|
|
48
|
+
* app-owned and renders the default locale whatever its first segment spells,
|
|
49
|
+
* which is what stops `/fr` from switching the page to French when `fr` is
|
|
50
|
+
* really an entry slug under a top-level `/:slug` route.
|
|
51
|
+
*
|
|
52
|
+
* Only a URL that no declared route answers falls back to the compatibility
|
|
53
|
+
* prefix resolver. Existing apps can still rely on that inference while new
|
|
54
|
+
* route-owned integrations pass an explicit locale to `FimoProviders`.
|
|
55
|
+
*/
|
|
56
|
+
function getRouteLocale(pathname) {
|
|
57
|
+
return (matchFimoRoute(pathname, fimoConfig)?.locale ?? resolveFimoLocale({ pathname }, fimoConfig).locale);
|
|
58
|
+
}
|
|
59
|
+
function hasReadableFrameworkManifest(manifest) {
|
|
60
|
+
return Boolean(manifest &&
|
|
61
|
+
typeof manifest === 'object' &&
|
|
62
|
+
'routes' in manifest &&
|
|
63
|
+
manifest.routes &&
|
|
64
|
+
typeof manifest.routes === 'object');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The routes the running app can actually navigate to right now.
|
|
68
|
+
*
|
|
69
|
+
* Studio gates preview navigation on this list, so it must come from the
|
|
70
|
+
* router that is currently loaded rather than the build-time config: after a
|
|
71
|
+
* merge, config metadata can describe a page whose route module has not
|
|
72
|
+
* reached this iframe yet. Names and params still come from config, which is
|
|
73
|
+
* where authored metadata lives. Falls back to config when the framework
|
|
74
|
+
* manifest is unreadable (SSR pass, non-framework mode).
|
|
75
|
+
*/
|
|
76
|
+
export function getDisplayRoutes(frameworkManifest) {
|
|
77
|
+
const configRoutes = getConfigDisplayRoutes();
|
|
78
|
+
if (!hasReadableFrameworkManifest(frameworkManifest)) {
|
|
79
|
+
return configRoutes;
|
|
80
|
+
}
|
|
81
|
+
const configByPath = new Map(configRoutes.map((route) => [getCanonicalRoutePath(route.path), route]));
|
|
82
|
+
return getActiveRouterRoutes(frameworkManifest).map((route) => {
|
|
83
|
+
const metadata = configByPath.get(getCanonicalRoutePath(route.path));
|
|
84
|
+
return {
|
|
85
|
+
id: metadata?.id ?? route.id,
|
|
86
|
+
path: route.path,
|
|
87
|
+
name: metadata?.name ?? generateRouteName(route.path),
|
|
88
|
+
...(metadata?.params ? { params: metadata.params } : {}),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Studio's window into the running app. Framework-neutral on purpose: it
|
|
94
|
+
* speaks only postMessage (`nav/routes`, `nav/locationChange`, `nav/navigate`,
|
|
95
|
+
* `nav/read-routes`) and reads routes through the `readRoutes` callback, so a
|
|
96
|
+
* future non-React-Router runtime reuses it by supplying its own reader.
|
|
97
|
+
*/
|
|
98
|
+
function PreviewBridge({ readRoutes, locale }) {
|
|
99
|
+
const navigate = useNavigate();
|
|
100
|
+
const location = useLocation();
|
|
101
|
+
const sendRoutes = useCallback(() => {
|
|
102
|
+
// Read at send time: the router can change its route table in place
|
|
103
|
+
// during HMR, so an explicit nav/read-routes must see the current state.
|
|
104
|
+
window.parent.postMessage({ type: 'nav/routes', payload: { routes: readRoutes() } }, getPreviewParentOrigin() ?? '*');
|
|
105
|
+
}, [readRoutes]);
|
|
106
|
+
// `locale` is a dependency on purpose: an app that settles a new locale
|
|
107
|
+
// without moving the pathname still has to tell Studio about it.
|
|
108
|
+
const sendLocationChange = useCallback(() => {
|
|
109
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
110
|
+
window.parent.postMessage({ type: 'nav/locationChange', payload: getPreviewLocationChange(location, locale, parentOrigin) }, parentOrigin ?? '*');
|
|
111
|
+
}, [location.hash, location.pathname, location.search, locale]);
|
|
112
|
+
useEffect(sendRoutes, [sendRoutes]);
|
|
113
|
+
useEffect(sendLocationChange, [sendLocationChange]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const handleMessage = (event) => {
|
|
116
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
117
|
+
if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const type = event.data?.type;
|
|
121
|
+
const payload = event.data?.payload;
|
|
122
|
+
switch (type) {
|
|
123
|
+
case 'nav/navigate':
|
|
124
|
+
{
|
|
125
|
+
const target = getSafePreviewNavigationPath(payload?.to, window.location.origin);
|
|
126
|
+
if (target) {
|
|
127
|
+
navigate(target);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
case 'nav/read-routes':
|
|
132
|
+
sendRoutes();
|
|
133
|
+
sendLocationChange();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener('message', handleMessage);
|
|
138
|
+
return () => {
|
|
139
|
+
window.removeEventListener('message', handleMessage);
|
|
140
|
+
};
|
|
141
|
+
}, [navigate, sendRoutes, sendLocationChange]);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function EnsureQueryClient({ children }) {
|
|
145
|
+
const existingQueryClient = useContext(QueryClientContext);
|
|
146
|
+
const fallbackQueryClientRef = useRef(null);
|
|
147
|
+
if (existingQueryClient) {
|
|
148
|
+
return _jsx(_Fragment, { children: children });
|
|
149
|
+
}
|
|
150
|
+
const fallbackQueryClient = fallbackQueryClientRef.current ?? (fallbackQueryClientRef.current = new QueryClient());
|
|
151
|
+
return _jsx(QueryClientProvider, { client: fallbackQueryClient, children: children });
|
|
152
|
+
}
|
|
153
|
+
export function FimoProviders({ children, locale }) {
|
|
154
|
+
const location = useLocation();
|
|
155
|
+
// The React-Router-specific part of the bridge, isolated here: the live
|
|
156
|
+
// route table lives on the framework context's manifest, which React Router
|
|
157
|
+
// may update in place during HMR — hence reading it per render/send instead
|
|
158
|
+
// of caching. A different framework runtime would swap this reader only.
|
|
159
|
+
const frameworkContext = useContext(UNSAFE_FrameworkContext);
|
|
160
|
+
const readRoutes = useCallback(() => getDisplayRoutes(frameworkContext?.manifest), [frameworkContext?.manifest]);
|
|
161
|
+
const routes = readRoutes();
|
|
162
|
+
const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
|
|
163
|
+
// The app is authoritative: an explicit `locale` wins over anything Fimo can
|
|
164
|
+
// infer, and inference itself goes through the declared route contract.
|
|
165
|
+
const activeLocale = useMemo(() => locale ?? getRouteLocale(location.pathname), [locale, location.pathname]);
|
|
166
|
+
const activeDir = getFimoTextDirection(activeLocale);
|
|
167
|
+
// @ts-ignore - import.meta.env is provided by Vite and React Router.
|
|
168
|
+
const previewScriptUrl = import.meta.env.VITE_PREVIEW_SCRIPT_URL;
|
|
169
|
+
const previewEnabled = isPreviewRuntimeEnabled(previewScriptUrl);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (typeof document === 'undefined') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
document.documentElement.lang = toHtmlLang(activeLocale);
|
|
175
|
+
document.documentElement.dir = activeDir;
|
|
176
|
+
}, [activeDir, activeLocale]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (previewEnabled) {
|
|
179
|
+
ensurePreviewScript(previewScriptUrl);
|
|
180
|
+
}
|
|
181
|
+
}, [previewEnabled, previewScriptUrl]);
|
|
182
|
+
return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(EnsureQueryClient, { children: _jsx(FimoLocaleProvider, { value: {
|
|
183
|
+
...localeResolution,
|
|
184
|
+
locale: activeLocale,
|
|
185
|
+
dir: activeDir,
|
|
186
|
+
}, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [previewEnabled ? _jsx(PreviewBridge, { readRoutes: readRoutes, locale: activeLocale }) : null, _jsx(OptionalTracking, { routes: routes }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
|
|
187
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import 'virtual:fimo-overlay-hmr';
|
|
2
|
+
/**
|
|
3
|
+
* Renders extra <script> tags in <head>:
|
|
4
|
+
* - Dev overlay runtime (window.error / unhandledrejection bridge to parent)
|
|
5
|
+
* - JSON-LD Schema.org Organization + WebSite (when seo.siteName + seo.url set)
|
|
6
|
+
*
|
|
7
|
+
* The overlay runtime is embedded inline via the `virtual:fimo-overlay-runtime`
|
|
8
|
+
* module (resolved by `overlayPlugin` in `fimo-vite`). The previous approach
|
|
9
|
+
* used `transformIndexHtml`, which doesn't fire in react-router framework mode
|
|
10
|
+
* since framework mode bypasses `index.html` entirely.
|
|
11
|
+
*/
|
|
12
|
+
export declare function FimoScripts(): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=FimoScripts.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo-vite
|
|
3
|
+
import rawConfig from 'virtual:fimo-config';
|
|
4
|
+
// @ts-ignore - side-effect import; vite-served so import.meta.hot works inside
|
|
5
|
+
import 'virtual:fimo-overlay-hmr';
|
|
6
|
+
// @ts-ignore - virtual module provided by overlayPlugin in fimo-vite; empty in prod
|
|
7
|
+
import overlayRuntime from 'virtual:fimo-overlay-runtime';
|
|
8
|
+
const config = rawConfig;
|
|
9
|
+
const runtime = overlayRuntime;
|
|
10
|
+
function buildSiteSchemas() {
|
|
11
|
+
const seo = config.seo;
|
|
12
|
+
if (!seo?.siteName || !seo?.url) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const organization = {
|
|
16
|
+
'@context': 'https://schema.org',
|
|
17
|
+
'@type': 'Organization',
|
|
18
|
+
name: seo.siteName,
|
|
19
|
+
url: seo.url,
|
|
20
|
+
};
|
|
21
|
+
if (seo.favicon) {
|
|
22
|
+
organization.logo = seo.favicon;
|
|
23
|
+
}
|
|
24
|
+
const website = {
|
|
25
|
+
'@context': 'https://schema.org',
|
|
26
|
+
'@type': 'WebSite',
|
|
27
|
+
name: seo.siteName,
|
|
28
|
+
url: seo.url,
|
|
29
|
+
};
|
|
30
|
+
return [organization, website];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Renders extra <script> tags in <head>:
|
|
34
|
+
* - Dev overlay runtime (window.error / unhandledrejection bridge to parent)
|
|
35
|
+
* - JSON-LD Schema.org Organization + WebSite (when seo.siteName + seo.url set)
|
|
36
|
+
*
|
|
37
|
+
* The overlay runtime is embedded inline via the `virtual:fimo-overlay-runtime`
|
|
38
|
+
* module (resolved by `overlayPlugin` in `fimo-vite`). The previous approach
|
|
39
|
+
* used `transformIndexHtml`, which doesn't fire in react-router framework mode
|
|
40
|
+
* since framework mode bypasses `index.html` entirely.
|
|
41
|
+
*/
|
|
42
|
+
export function FimoScripts() {
|
|
43
|
+
const siteSchemas = buildSiteSchemas();
|
|
44
|
+
return (_jsxs(_Fragment, { children: [runtime ? _jsx("script", { type: "module", dangerouslySetInnerHTML: { __html: runtime } }) : null, siteSchemas.map((schema, index) => (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } }, index)))] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Pageview } from './Pageview.js';
|
|
2
|
+
type PageviewComponent = typeof Pageview;
|
|
3
|
+
type PageviewModule = {
|
|
4
|
+
Pageview: PageviewComponent;
|
|
5
|
+
};
|
|
6
|
+
type PageviewLoader = () => Promise<PageviewModule>;
|
|
7
|
+
type DisplayRoute = Parameters<PageviewComponent>[0]['routes'][number];
|
|
8
|
+
export declare function loadPageviewComponent(loadPageview?: PageviewLoader): Promise<typeof Pageview | null>;
|
|
9
|
+
export declare function OptionalTracking({ routes }: {
|
|
10
|
+
routes: DisplayRoute[];
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=OptionalTracking.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
const defaultLoadPageview = () => import('./Pageview.js');
|
|
4
|
+
export async function loadPageviewComponent(loadPageview = defaultLoadPageview) {
|
|
5
|
+
try {
|
|
6
|
+
return (await loadPageview()).Pageview;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function OptionalTracking({ routes }) {
|
|
13
|
+
const [PageviewComponent, setPageviewComponent] = useState(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
loadPageviewComponent()
|
|
17
|
+
.then((component) => {
|
|
18
|
+
if (!cancelled) {
|
|
19
|
+
setPageviewComponent(() => component);
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
.catch(() => { });
|
|
23
|
+
return () => {
|
|
24
|
+
cancelled = true;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
return PageviewComponent ? _jsx(PageviewComponent, { routes: routes }) : null;
|
|
28
|
+
}
|
package/dist/Pageview.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router';
|
|
3
|
+
const VISITOR_ID_STORAGE_KEY = 'fimo.analytics.visitor-id';
|
|
4
|
+
const SESSION_ID_STORAGE_KEY = 'fimo.analytics.session-id';
|
|
5
|
+
const viteEnv = import.meta.env;
|
|
6
|
+
const API_BASE_URL = viteEnv?.VITE_API_URL?.replace(/\/$/, '');
|
|
7
|
+
const ENABLE_ANALYTICS = (viteEnv?.VITE_ENABLE_ANALYTICS ?? '').toLowerCase() === 'true';
|
|
8
|
+
function isPreviewContext() {
|
|
9
|
+
if (typeof window === 'undefined') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const search = new URLSearchParams(window.location.search);
|
|
13
|
+
return search.has('bl_preview_token');
|
|
14
|
+
}
|
|
15
|
+
function createId() {
|
|
16
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
17
|
+
return crypto.randomUUID();
|
|
18
|
+
}
|
|
19
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
20
|
+
}
|
|
21
|
+
function readOrCreateStoredId(storage, key) {
|
|
22
|
+
const existing = storage.getItem(key);
|
|
23
|
+
if (existing && existing.length > 0) {
|
|
24
|
+
return existing;
|
|
25
|
+
}
|
|
26
|
+
const generated = createId();
|
|
27
|
+
storage.setItem(key, generated);
|
|
28
|
+
return generated;
|
|
29
|
+
}
|
|
30
|
+
function getVisitorId() {
|
|
31
|
+
if (typeof window === 'undefined') {
|
|
32
|
+
return createId();
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return readOrCreateStoredId(window.localStorage, VISITOR_ID_STORAGE_KEY);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return createId();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getSessionId() {
|
|
42
|
+
if (typeof window === 'undefined') {
|
|
43
|
+
return createId();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return readOrCreateStoredId(window.sessionStorage, SESSION_ID_STORAGE_KEY);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return createId();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function normalizePath(path) {
|
|
53
|
+
const trimmed = path.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return '/';
|
|
56
|
+
}
|
|
57
|
+
if (trimmed === '/') {
|
|
58
|
+
return '/';
|
|
59
|
+
}
|
|
60
|
+
return trimmed.replace(/\/+$/, '');
|
|
61
|
+
}
|
|
62
|
+
function findRouteName(pathname, routes) {
|
|
63
|
+
const currentPath = normalizePath(pathname);
|
|
64
|
+
const exactMatch = routes.find((route) => normalizePath(route.path) === currentPath);
|
|
65
|
+
if (exactMatch) {
|
|
66
|
+
return exactMatch.name;
|
|
67
|
+
}
|
|
68
|
+
const dynamicMatch = routes.find((route) => {
|
|
69
|
+
const normalizedPattern = normalizePath(route.path);
|
|
70
|
+
const regex = new RegExp(`^${normalizedPattern.replace(/:\w+/g, '[^/]+')}$`);
|
|
71
|
+
return regex.test(currentPath);
|
|
72
|
+
});
|
|
73
|
+
if (dynamicMatch) {
|
|
74
|
+
return dynamicMatch.name;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function sendPageview(path, pageName) {
|
|
79
|
+
if (typeof window === 'undefined' || !API_BASE_URL || !ENABLE_ANALYTICS || isPreviewContext()) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const payload = new URLSearchParams({
|
|
83
|
+
p: path || '/',
|
|
84
|
+
n: pageName,
|
|
85
|
+
v: getVisitorId(),
|
|
86
|
+
s: getSessionId(),
|
|
87
|
+
});
|
|
88
|
+
if (document.referrer) {
|
|
89
|
+
payload.set('r', document.referrer);
|
|
90
|
+
}
|
|
91
|
+
if (navigator.userAgent) {
|
|
92
|
+
payload.set('u', navigator.userAgent);
|
|
93
|
+
}
|
|
94
|
+
void fetch(`${API_BASE_URL}/e/p`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: payload,
|
|
97
|
+
keepalive: true,
|
|
98
|
+
mode: 'cors',
|
|
99
|
+
credentials: 'omit',
|
|
100
|
+
}).catch(() => {
|
|
101
|
+
// best-effort analytics
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function Pageview({ routes }) {
|
|
105
|
+
const location = useLocation();
|
|
106
|
+
const lastTrackedPath = useRef(null);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const currentPath = `${location.pathname}${location.search}`;
|
|
109
|
+
const routeName = findRouteName(location.pathname, routes);
|
|
110
|
+
const currentPageName = routeName ?? normalizePath(location.pathname);
|
|
111
|
+
if (currentPath === lastTrackedPath.current) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
lastTrackedPath.current = currentPath;
|
|
115
|
+
sendPageview(currentPath, currentPageName);
|
|
116
|
+
}, [location.pathname, location.search, routes]);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ActiveRouterRoute {
|
|
2
|
+
id: string;
|
|
3
|
+
path: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Flatten React Router's framework manifest into the route graph the active
|
|
7
|
+
* router actually loaded. This is intentionally separate from fimo-config:
|
|
8
|
+
* metadata describes what Studio expects, while this manifest describes what
|
|
9
|
+
* the iframe can render after its route modules have converged.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getActiveRouterRoutes(manifest: unknown): ActiveRouterRoute[];
|
|
12
|
+
//# sourceMappingURL=active-routes.d.ts.map
|