fimo 0.11.0-staging.9 → 0.11.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/assets/agent-templates/_default/config.yaml +2 -0
- package/assets/skills/fimo-cli/references/agents.md +5 -1
- package/assets/skills/fimo-cli/references/webhooks.md +5 -1
- package/assets/skills/fimo-studio/references/preview-and-responsive.md +1 -1
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -14
- package/dist/build/vite/index.d.ts +10 -0
- package/dist/build/vite/index.js +56 -8
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +6 -6
- package/dist/runtime/app/FimoProviders.d.ts +12 -0
- package/dist/runtime/app/FimoProviders.js +69 -10
- package/dist/runtime/core/ViteLifecycles.js +6 -2
- package/dist/runtime/react-router/active-routes.d.ts +12 -0
- package/dist/runtime/react-router/active-routes.js +60 -0
- package/dist/runtime/react-router/preview-location.d.ts +29 -0
- package/dist/runtime/react-router/preview-location.js +94 -0
- package/dist/runtime/routing/AppRoutesProvider.js +11 -4
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/react-router.config.ts +6 -0
- package/templates/react-router/index.html +0 -15
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
export { getPreviewLocationChange, getPreviewLocationPath } from '../react-router/preview-location.js';
|
|
2
3
|
export interface ParamInfo {
|
|
3
4
|
schema: string;
|
|
4
5
|
queryField: string;
|
|
@@ -10,6 +11,17 @@ export interface DisplayRoute {
|
|
|
10
11
|
path: string;
|
|
11
12
|
params?: Record<string, ParamInfo>;
|
|
12
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[];
|
|
13
25
|
export interface FimoProvidersProps {
|
|
14
26
|
children?: React.ReactNode;
|
|
15
27
|
locale?: string;
|
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { QueryClient, QueryClientContext, QueryClientProvider } from '@tanstack/react-query';
|
|
3
3
|
import { Suspense, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
5
|
-
import { useLocation, useNavigate } from 'react-router';
|
|
5
|
+
import { UNSAFE_FrameworkContext, useLocation, useNavigate } from 'react-router';
|
|
6
6
|
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
|
|
7
7
|
import fimoConfig from 'virtual:fimo-config';
|
|
8
8
|
import { DataSourcesProvider } from '../content/DataSourcesProvider.js';
|
|
@@ -12,7 +12,10 @@ import { ViteLifecycles } from '../core/ViteLifecycles.js';
|
|
|
12
12
|
import { FimoLocaleProvider } from '../i18n/FimoLocaleContext.js';
|
|
13
13
|
import { getFimoTextDirection, resolveFimoLocale, toHtmlLang } from '../i18n/locale.js';
|
|
14
14
|
import { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
|
|
15
|
+
import { getActiveRouterRoutes } from '../react-router/active-routes.js';
|
|
16
|
+
import { getPreviewLocationChange, getPreviewParentOrigin, } from '../react-router/preview-location.js';
|
|
15
17
|
import { OptionalTracking } from '../t/OptionalTracking.js';
|
|
18
|
+
export { getPreviewLocationChange, getPreviewLocationPath } from '../react-router/preview-location.js';
|
|
16
19
|
const titleCase = (seg) => seg
|
|
17
20
|
.replace(/[_-]+/g, ' ')
|
|
18
21
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
@@ -27,7 +30,7 @@ const generateRouteName = (path) => {
|
|
|
27
30
|
const firstSegment = path.replace(/^\//, '').split('/')[0];
|
|
28
31
|
return titleCase(firstSegment);
|
|
29
32
|
};
|
|
30
|
-
function
|
|
33
|
+
function getConfigDisplayRoutes() {
|
|
31
34
|
const configRoutes = fimoConfig.routes ?? {};
|
|
32
35
|
return Object.entries(configRoutes)
|
|
33
36
|
.filter(([, route]) => route.path)
|
|
@@ -38,28 +41,77 @@ function getDisplayRoutes() {
|
|
|
38
41
|
params: route.params,
|
|
39
42
|
}));
|
|
40
43
|
}
|
|
41
|
-
function
|
|
44
|
+
function getCanonicalRoutePath(path) {
|
|
45
|
+
return resolveFimoLocale({ pathname: path }, fimoConfig).routePath;
|
|
46
|
+
}
|
|
47
|
+
function hasReadableFrameworkManifest(manifest) {
|
|
48
|
+
return Boolean(manifest &&
|
|
49
|
+
typeof manifest === 'object' &&
|
|
50
|
+
'routes' in manifest &&
|
|
51
|
+
manifest.routes &&
|
|
52
|
+
typeof manifest.routes === 'object');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The routes the running app can actually navigate to right now.
|
|
56
|
+
*
|
|
57
|
+
* Studio gates preview navigation on this list, so it must come from the
|
|
58
|
+
* router that is currently loaded rather than the build-time config: after a
|
|
59
|
+
* merge, config metadata can describe a page whose route module has not
|
|
60
|
+
* reached this iframe yet. Names and params still come from config, which is
|
|
61
|
+
* where authored metadata lives. Falls back to config when the framework
|
|
62
|
+
* manifest is unreadable (SSR pass, non-framework mode).
|
|
63
|
+
*/
|
|
64
|
+
export function getDisplayRoutes(frameworkManifest) {
|
|
65
|
+
const configRoutes = getConfigDisplayRoutes();
|
|
66
|
+
if (!hasReadableFrameworkManifest(frameworkManifest)) {
|
|
67
|
+
return configRoutes;
|
|
68
|
+
}
|
|
69
|
+
const configByPath = new Map(configRoutes.map((route) => [getCanonicalRoutePath(route.path), route]));
|
|
70
|
+
return getActiveRouterRoutes(frameworkManifest).map((route) => {
|
|
71
|
+
const metadata = configByPath.get(getCanonicalRoutePath(route.path));
|
|
72
|
+
return {
|
|
73
|
+
id: metadata?.id ?? route.id,
|
|
74
|
+
path: route.path,
|
|
75
|
+
name: metadata?.name ?? generateRouteName(route.path),
|
|
76
|
+
...(metadata?.params ? { params: metadata.params } : {}),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Studio's window into the running app. Framework-neutral on purpose: it
|
|
82
|
+
* speaks only postMessage (`nav/routes`, `nav/locationChange`, `nav/navigate`,
|
|
83
|
+
* `nav/read-routes`) and reads routes through the `readRoutes` callback, so a
|
|
84
|
+
* future non-React-Router runtime reuses it by supplying its own reader.
|
|
85
|
+
*/
|
|
86
|
+
function PreviewBridge({ readRoutes }) {
|
|
42
87
|
const navigate = useNavigate();
|
|
43
88
|
const location = useLocation();
|
|
44
89
|
const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
|
|
45
90
|
const sendRoutes = useCallback(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
91
|
+
// Read at send time: the router can change its route table in place
|
|
92
|
+
// during HMR, so an explicit nav/read-routes must see the current state.
|
|
93
|
+
window.parent.postMessage({ type: 'nav/routes', payload: { routes: readRoutes() } }, getPreviewParentOrigin() ?? '*');
|
|
94
|
+
}, [readRoutes]);
|
|
49
95
|
const sendLocationChange = useCallback(() => {
|
|
96
|
+
const locationChange = getPreviewLocationChange(location);
|
|
97
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
50
98
|
window.parent.postMessage({
|
|
51
99
|
type: 'nav/locationChange',
|
|
52
100
|
payload: {
|
|
53
|
-
path:
|
|
101
|
+
...(parentOrigin ? locationChange : { path: locationChange.path }),
|
|
54
102
|
routePath: localeResolution.routePath,
|
|
55
103
|
locale: localeResolution.locale,
|
|
56
104
|
},
|
|
57
|
-
}, '*');
|
|
58
|
-
}, [location.pathname, localeResolution.locale, localeResolution.routePath]);
|
|
105
|
+
}, parentOrigin ?? '*');
|
|
106
|
+
}, [location.hash, location.pathname, location.search, localeResolution.locale, localeResolution.routePath]);
|
|
59
107
|
useEffect(sendRoutes, [sendRoutes]);
|
|
60
108
|
useEffect(sendLocationChange, [sendLocationChange]);
|
|
61
109
|
useEffect(() => {
|
|
62
110
|
const handleMessage = (event) => {
|
|
111
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
112
|
+
if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
63
115
|
const type = event.data?.type;
|
|
64
116
|
const payload = event.data?.payload;
|
|
65
117
|
switch (type) {
|
|
@@ -92,6 +144,13 @@ function EnsureQueryClient({ children }) {
|
|
|
92
144
|
}
|
|
93
145
|
export function FimoProviders({ children, locale }) {
|
|
94
146
|
const location = useLocation();
|
|
147
|
+
// The React-Router-specific part of the bridge, isolated here: the live
|
|
148
|
+
// route table lives on the framework context's manifest, which React Router
|
|
149
|
+
// may update in place during HMR — hence reading it per render/send instead
|
|
150
|
+
// of caching. A different framework runtime would swap this reader only.
|
|
151
|
+
const frameworkContext = useContext(UNSAFE_FrameworkContext);
|
|
152
|
+
const readRoutes = useCallback(() => getDisplayRoutes(frameworkContext?.manifest), [frameworkContext?.manifest]);
|
|
153
|
+
const routes = readRoutes();
|
|
95
154
|
const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
|
|
96
155
|
const activeLocale = locale ?? localeResolution.locale;
|
|
97
156
|
const activeDir = getFimoTextDirection(activeLocale);
|
|
@@ -106,5 +165,5 @@ export function FimoProviders({ children, locale }) {
|
|
|
106
165
|
...localeResolution,
|
|
107
166
|
locale: activeLocale,
|
|
108
167
|
dir: activeDir,
|
|
109
|
-
}, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(PreviewBridge, {}), _jsx(OptionalTracking, { routes:
|
|
168
|
+
}, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(PreviewBridge, { readRoutes: readRoutes }), _jsx(OptionalTracking, { routes: routes }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
|
|
110
169
|
}
|
|
@@ -15,8 +15,12 @@ export function ViteLifecycles({ children }) {
|
|
|
15
15
|
const afterUpdate = () => {
|
|
16
16
|
window.parent.postMessage({ type: 'hmr/afterUpdate', payload: { ts: Date.now() } }, '*');
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Vite names the module that forced the reload (`path`/`triggeredBy`).
|
|
19
|
+
// Forward it: a full reload costs seconds and resets client state, and
|
|
20
|
+
// without the culprit there is no way to tell an unavoidable reload from
|
|
21
|
+
// one our own plugins caused by regenerating a module nobody can accept.
|
|
22
|
+
const beforeFullReload = (payload) => {
|
|
23
|
+
window.parent.postMessage({ type: 'hmr/beforeFullReload', payload: { ts: Date.now(), payload } }, '*');
|
|
20
24
|
};
|
|
21
25
|
const onError = (err) => {
|
|
22
26
|
window.parent.postMessage({ type: 'hmr/error', payload: { ts: Date.now(), err } }, '*');
|
|
@@ -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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const normalizeRoutePath = (path) => {
|
|
2
|
+
const withSlash = path.startsWith('/') ? path : `/${path}`;
|
|
3
|
+
const normalized = withSlash.replace(/\/{2,}/g, '/');
|
|
4
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
|
|
5
|
+
};
|
|
6
|
+
const joinRoutePath = (parentPath, childPath) => {
|
|
7
|
+
if (!childPath) {
|
|
8
|
+
return parentPath;
|
|
9
|
+
}
|
|
10
|
+
if (childPath.startsWith('/')) {
|
|
11
|
+
return normalizeRoutePath(childPath);
|
|
12
|
+
}
|
|
13
|
+
return normalizeRoutePath(`${parentPath}/${childPath}`);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Flatten React Router's framework manifest into the route graph the active
|
|
17
|
+
* router actually loaded. This is intentionally separate from fimo-config:
|
|
18
|
+
* metadata describes what Studio expects, while this manifest describes what
|
|
19
|
+
* the iframe can render after its route modules have converged.
|
|
20
|
+
*/
|
|
21
|
+
export function getActiveRouterRoutes(manifest) {
|
|
22
|
+
const routeRecord = manifest?.routes;
|
|
23
|
+
if (!routeRecord) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const entries = Object.values(routeRecord).filter(isActiveRouteManifestEntry);
|
|
27
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
28
|
+
const pathCache = new Map();
|
|
29
|
+
const resolvePath = (entry, resolving = new Set()) => {
|
|
30
|
+
const cached = pathCache.get(entry.id);
|
|
31
|
+
if (cached !== undefined) {
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
if (resolving.has(entry.id)) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const nextResolving = new Set(resolving).add(entry.id);
|
|
38
|
+
const parentPath = entry.parentId
|
|
39
|
+
? resolvePath(byId.get(entry.parentId) ?? { id: entry.parentId }, nextResolving)
|
|
40
|
+
: '';
|
|
41
|
+
const path = entry.index ? parentPath || '/' : joinRoutePath(parentPath, entry.path ?? '');
|
|
42
|
+
pathCache.set(entry.id, path);
|
|
43
|
+
return path;
|
|
44
|
+
};
|
|
45
|
+
return entries.flatMap((entry) => {
|
|
46
|
+
// A pathless/root layout is part of the framework manifest, but it is not
|
|
47
|
+
// a URL the router can navigate to. Treat only an explicit index route as
|
|
48
|
+
// the root path; including the layout would add a phantom `/` entry and
|
|
49
|
+
// make exact route-graph convergence impossible for apps without a home
|
|
50
|
+
// page.
|
|
51
|
+
if ((entry.path === undefined || entry.path === '') && !entry.index) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const path = normalizeRoutePath(resolvePath(entry));
|
|
55
|
+
return [{ id: entry.id, path }];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function isActiveRouteManifestEntry(value) {
|
|
59
|
+
return Boolean(value && typeof value === 'object' && typeof value.id === 'string');
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PreviewLocationLike {
|
|
2
|
+
pathname: string;
|
|
3
|
+
search?: string;
|
|
4
|
+
hash?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PreviewLocationChange {
|
|
7
|
+
/** Safe workspace focus value; query/hash state stays navigation-only. */
|
|
8
|
+
path: string;
|
|
9
|
+
/** Exact app location used to keep the parent's iframe URL in sync. */
|
|
10
|
+
navigationPath: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `ancestorOrigins[0]` identifies the immediate embedding document and stays
|
|
14
|
+
* stable when the preview document itself navigates or reloads. The referrer
|
|
15
|
+
* is the fallback for browsers without ancestor origins (Firefox), where it
|
|
16
|
+
* can go stale after in-app navigation — which is why the runtime wrapper
|
|
17
|
+
* below remembers the first successful resolution.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolvePreviewParentOrigin(referrer: string | undefined, ancestorOrigins: readonly string[] | undefined): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the parent origin for the iframe's authenticated preview channel
|
|
22
|
+
* from the live browser context. Standalone app loads have no trusted parent
|
|
23
|
+
* origin and must not receive query/hash-bearing location data.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getPreviewParentOrigin(): string | null;
|
|
26
|
+
/** Keep sandbox credentials and the iframe cache-buster out of app navigation state. */
|
|
27
|
+
export declare function getPreviewLocationPath(location: PreviewLocationLike): string;
|
|
28
|
+
export declare function getPreviewLocationChange(location: PreviewLocationLike): PreviewLocationChange;
|
|
29
|
+
//# sourceMappingURL=preview-location.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const PREVIEW_INTERNAL_QUERY_PARAMS = new Set(['bl_preview_token', 'preview_token', 't']);
|
|
2
|
+
const PREVIEW_PARENT_ORIGIN_STORAGE_KEY = 'fimo-preview-parent-origin';
|
|
3
|
+
let rememberedPreviewParentOrigin;
|
|
4
|
+
const getOrigin = (value) => {
|
|
5
|
+
if (!value) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const origin = new URL(value).origin;
|
|
10
|
+
return origin === 'null' ? null : origin;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* `ancestorOrigins[0]` identifies the immediate embedding document and stays
|
|
18
|
+
* stable when the preview document itself navigates or reloads. The referrer
|
|
19
|
+
* is the fallback for browsers without ancestor origins (Firefox), where it
|
|
20
|
+
* can go stale after in-app navigation — which is why the runtime wrapper
|
|
21
|
+
* below remembers the first successful resolution.
|
|
22
|
+
*/
|
|
23
|
+
export function resolvePreviewParentOrigin(referrer, ancestorOrigins) {
|
|
24
|
+
return getOrigin(ancestorOrigins?.[0]) ?? getOrigin(referrer);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the parent origin for the iframe's authenticated preview channel
|
|
28
|
+
* from the live browser context. Standalone app loads have no trusted parent
|
|
29
|
+
* origin and must not receive query/hash-bearing location data.
|
|
30
|
+
*/
|
|
31
|
+
export function getPreviewParentOrigin() {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const ancestorOrigin = getOrigin(window.location.ancestorOrigins?.[0]);
|
|
36
|
+
if (ancestorOrigin) {
|
|
37
|
+
rememberPreviewParentOrigin(ancestorOrigin);
|
|
38
|
+
return ancestorOrigin;
|
|
39
|
+
}
|
|
40
|
+
// The referrer becomes the previous preview URL after in-app navigation, so
|
|
41
|
+
// a remembered first resolution outranks it on reloads.
|
|
42
|
+
const rememberedOrigin = readRememberedPreviewParentOrigin();
|
|
43
|
+
if (rememberedOrigin) {
|
|
44
|
+
return rememberedOrigin;
|
|
45
|
+
}
|
|
46
|
+
const referrerOrigin = getOrigin(typeof document === 'undefined' ? undefined : document.referrer);
|
|
47
|
+
if (referrerOrigin) {
|
|
48
|
+
rememberPreviewParentOrigin(referrerOrigin);
|
|
49
|
+
}
|
|
50
|
+
return referrerOrigin;
|
|
51
|
+
}
|
|
52
|
+
function readRememberedPreviewParentOrigin() {
|
|
53
|
+
if (rememberedPreviewParentOrigin !== undefined) {
|
|
54
|
+
return rememberedPreviewParentOrigin;
|
|
55
|
+
}
|
|
56
|
+
if (typeof sessionStorage === 'undefined') {
|
|
57
|
+
rememberedPreviewParentOrigin = null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
rememberedPreviewParentOrigin = getOrigin(sessionStorage.getItem(PREVIEW_PARENT_ORIGIN_STORAGE_KEY) ?? undefined);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
rememberedPreviewParentOrigin = null;
|
|
65
|
+
}
|
|
66
|
+
return rememberedPreviewParentOrigin;
|
|
67
|
+
}
|
|
68
|
+
function rememberPreviewParentOrigin(origin) {
|
|
69
|
+
rememberedPreviewParentOrigin = origin;
|
|
70
|
+
if (typeof sessionStorage === 'undefined') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
sessionStorage.setItem(PREVIEW_PARENT_ORIGIN_STORAGE_KEY, origin);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Storage availability must not block the preview message channel.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Keep sandbox credentials and the iframe cache-buster out of app navigation state. */
|
|
81
|
+
export function getPreviewLocationPath(location) {
|
|
82
|
+
const search = new URLSearchParams(location.search ?? '');
|
|
83
|
+
for (const key of PREVIEW_INTERNAL_QUERY_PARAMS) {
|
|
84
|
+
search.delete(key);
|
|
85
|
+
}
|
|
86
|
+
const query = search.toString();
|
|
87
|
+
return `${location.pathname}${query ? `?${query}` : ''}${location.hash ?? ''}`;
|
|
88
|
+
}
|
|
89
|
+
export function getPreviewLocationChange(location) {
|
|
90
|
+
return {
|
|
91
|
+
path: location.pathname,
|
|
92
|
+
navigationPath: getPreviewLocationPath(location),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -7,6 +7,7 @@ import { RefreshContentProvider } from '../content/RefreshContentProvider.js';
|
|
|
7
7
|
import { AppErrorBoundary, onError } from '../core/AppErrorBoundary.js';
|
|
8
8
|
import { ViteLifecycles } from '../core/ViteLifecycles.js';
|
|
9
9
|
import { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
|
|
10
|
+
import { getPreviewLocationChange, getPreviewParentOrigin } from '../react-router/preview-location.js';
|
|
10
11
|
import { OptionalTracking } from '../t/OptionalTracking.js';
|
|
11
12
|
import { AppResetScrollOnNavigate } from './AppResetScrollOnNavigate.js';
|
|
12
13
|
const titleCase = (seg) => seg
|
|
@@ -57,18 +58,24 @@ export function AppRoutesProvider({ appRoutes, children }) {
|
|
|
57
58
|
if (!appRoutes?.length) {
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
|
-
window.parent.postMessage({ type: 'nav/routes', payload: { routes: displayRoutes } }, '*');
|
|
61
|
+
window.parent.postMessage({ type: 'nav/routes', payload: { routes: displayRoutes } }, getPreviewParentOrigin() ?? '*');
|
|
61
62
|
}, [appRoutes, displayRoutes]);
|
|
62
63
|
const sendLocationChange = useCallback(() => {
|
|
64
|
+
const locationChange = getPreviewLocationChange(location);
|
|
65
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
63
66
|
window.parent.postMessage({
|
|
64
67
|
type: 'nav/locationChange',
|
|
65
|
-
payload: { path:
|
|
66
|
-
}, '*');
|
|
67
|
-
}, [location.pathname]);
|
|
68
|
+
payload: parentOrigin ? locationChange : { path: locationChange.path },
|
|
69
|
+
}, parentOrigin ?? '*');
|
|
70
|
+
}, [location.hash, location.pathname, location.search]);
|
|
68
71
|
useEffect(sendRoutes, [sendRoutes]);
|
|
69
72
|
useEffect(sendLocationChange, [sendLocationChange]);
|
|
70
73
|
useEffect(() => {
|
|
71
74
|
const handleMessage = (event) => {
|
|
75
|
+
const parentOrigin = getPreviewParentOrigin();
|
|
76
|
+
if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
72
79
|
const type = event.data?.type;
|
|
73
80
|
const payload = event.data?.payload;
|
|
74
81
|
switch (type) {
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -14,4 +14,10 @@ export default {
|
|
|
14
14
|
// schema entries from the content API. See `references/setup-react-router.md`
|
|
15
15
|
// in the fimo skill for the full setup pattern.
|
|
16
16
|
buildDirectory: 'dist',
|
|
17
|
+
// Feed Vite's dependency scanner the client entry + every route module.
|
|
18
|
+
// Without it React Router hands Vite an EMPTY entry list, nothing is
|
|
19
|
+
// pre-bundled, and each route's deps are discovered on that route's first
|
|
20
|
+
// visit — which Vite answers with a full page reload, so preview links stop
|
|
21
|
+
// behaving like SPA links. Dev-only: it never touches the production build.
|
|
22
|
+
future: { unstable_optimizeDeps: true },
|
|
17
23
|
} satisfies Config;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
-
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
-
<title>Welcome to your Fimo website</title>
|
|
9
|
-
</head>
|
|
10
|
-
|
|
11
|
-
<body>
|
|
12
|
-
<div id="root"></div>
|
|
13
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|