fimo 0.11.0-staging.12 → 0.11.0-staging.13

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-08-06T13:51:18.875Z",
4
+ "bundledAt": "2026-08-06T15:15:13.492Z",
5
5
  "cliVersion": "0.10.0",
6
6
  "external": [
7
7
  "oxc-parser",
@@ -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 getDisplayRoutes() {
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 PreviewBridge() {
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
- const displayRoutes = getDisplayRoutes();
47
- window.parent.postMessage({ type: 'nav/routes', payload: { routes: displayRoutes } }, '*');
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: location.pathname,
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: getDisplayRoutes() }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
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
  }
@@ -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: location.pathname },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.11.0-staging.12",
3
+ "version": "0.11.0-staging.13",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.11.0-staging.12",
24
+ "fimo": "0.11.0-staging.13",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",