fimo 0.8.0 → 0.9.0-experimental.2

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.
Files changed (190) hide show
  1. package/assets/content-templates/hooks-template.eta +4 -2
  2. package/assets/content-templates/types-template.eta +66 -1
  3. package/assets/skills/fimo/SKILL.md +1 -1
  4. package/assets/skills/fimo/references/content.md +86 -15
  5. package/assets/skills/fimo-cli/SKILL.md +6 -2
  6. package/assets/skills/fimo-cli/references/content.md +61 -2
  7. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  8. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  9. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  10. package/assets/skills/fimo-migration/SKILL.md +22 -2
  11. package/assets/skills/fimo-migration/references/content.md +24 -0
  12. package/assets/skills/fimo-migration/references/manifest.md +35 -0
  13. package/assets/skills/fimo-migration/references/qa.md +24 -2
  14. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  15. package/assets/skills/fimo-migration/references/wrap.md +617 -0
  16. package/assets/skills/fimo-studio/SKILL.md +5 -3
  17. package/assets/skills/fimo-studio/references/cms.md +9 -1
  18. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  19. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  20. package/dist/build/vite/plugins/translations.d.ts +2 -8
  21. package/dist/build/vite/plugins/translations.js +3 -121
  22. package/dist/cli/bundle.json +1 -1
  23. package/dist/cli/index.js +1051 -144
  24. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  25. package/dist/runtime/app/FimoProviders.js +2 -23
  26. package/dist/runtime/astro/index.d.ts +2 -0
  27. package/dist/runtime/astro/index.js +3 -0
  28. package/dist/runtime/astro/server.d.ts +3 -0
  29. package/dist/runtime/astro/server.js +6 -0
  30. package/dist/runtime/content/client/internal.js +1 -0
  31. package/dist/runtime/content/client/types.d.ts +16 -0
  32. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  33. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  34. package/dist/runtime/next/FimoNextProviders.js +20 -0
  35. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  36. package/dist/runtime/next/FimoNextScripts.js +19 -0
  37. package/dist/runtime/next/index.d.ts +14 -0
  38. package/dist/runtime/next/index.js +14 -0
  39. package/dist/runtime/next/server.d.ts +2 -0
  40. package/dist/runtime/next/server.js +4 -0
  41. package/dist/runtime/nuxt/composables.d.ts +9 -0
  42. package/dist/runtime/nuxt/composables.js +57 -0
  43. package/dist/runtime/nuxt/index.d.ts +3 -0
  44. package/dist/runtime/nuxt/index.js +6 -0
  45. package/dist/runtime/nuxt/server.d.ts +3 -0
  46. package/dist/runtime/nuxt/server.js +6 -0
  47. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  48. package/dist/runtime/primitives/labels-hook.js +41 -0
  49. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  50. package/dist/runtime/primitives/labels-store.js +60 -0
  51. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  52. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  53. package/dist/runtime/primitives/translations.d.ts +1 -27
  54. package/dist/runtime/primitives/translations.js +7 -85
  55. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  56. package/dist/runtime/react/FimoSpaProviders.js +63 -0
  57. package/dist/runtime/react/index.d.ts +33 -0
  58. package/dist/runtime/react/index.js +39 -0
  59. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  60. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  61. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  62. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  63. package/dist/runtime/remix/index.d.ts +12 -0
  64. package/dist/runtime/remix/index.js +11 -0
  65. package/dist/runtime/remix/server.d.ts +2 -0
  66. package/dist/runtime/remix/server.js +2 -0
  67. package/dist/runtime/shared/display-routes.d.ts +14 -0
  68. package/dist/runtime/shared/display-routes.js +25 -0
  69. package/dist/runtime/shared/fimo-config.server.js +1 -0
  70. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  71. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  72. package/dist/runtime/shared/i18n-messages.js +79 -0
  73. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  74. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  75. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  76. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  77. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  78. package/dist/runtime/shared/labels-t.pure.js +10 -0
  79. package/dist/runtime/strapi/client.d.ts +30 -0
  80. package/dist/runtime/strapi/client.js +89 -0
  81. package/dist/runtime/strapi/index.d.ts +5 -0
  82. package/dist/runtime/strapi/index.js +6 -0
  83. package/dist/runtime/strapi/reshape.d.ts +46 -0
  84. package/dist/runtime/strapi/reshape.js +114 -0
  85. package/dist/runtime/svelte/index.d.ts +3 -0
  86. package/dist/runtime/svelte/index.js +7 -0
  87. package/dist/runtime/svelte/server.d.ts +3 -0
  88. package/dist/runtime/svelte/server.js +6 -0
  89. package/dist/runtime/vue/index.d.ts +3 -0
  90. package/dist/runtime/vue/index.js +5 -0
  91. package/dist/runtime/vue/server.d.ts +3 -0
  92. package/dist/runtime/vue/server.js +6 -0
  93. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  94. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  95. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  96. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  97. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  98. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  99. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
  100. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  101. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  102. package/dist/runtime/wrapped/FimoText.js +20 -0
  103. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  104. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  105. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  106. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  107. package/dist/runtime/wrapped/dom.d.ts +4 -0
  108. package/dist/runtime/wrapped/dom.js +23 -0
  109. package/dist/runtime/wrapped/server.d.ts +77 -0
  110. package/dist/runtime/wrapped/server.js +237 -0
  111. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  112. package/dist/scripts/apply-wrap-templates.js +132 -0
  113. package/dist/scripts/apply-wrap.d.ts +46 -0
  114. package/dist/scripts/apply-wrap.js +278 -0
  115. package/dist/scripts/cms/discover.d.ts +2 -0
  116. package/dist/scripts/cms/discover.js +17 -0
  117. package/dist/scripts/cms/example-content.d.ts +33 -0
  118. package/dist/scripts/cms/example-content.js +163 -0
  119. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  120. package/dist/scripts/cms/strapi/detect.js +186 -0
  121. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  122. package/dist/scripts/cms/strapi/index.js +25 -0
  123. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  124. package/dist/scripts/cms/strapi/list-source.js +90 -0
  125. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  126. package/dist/scripts/cms/strapi/schema-map.js +279 -0
  127. package/dist/scripts/cms/strapi/shapes.d.ts +23 -0
  128. package/dist/scripts/cms/strapi/shapes.js +38 -0
  129. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  130. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  131. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  132. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  133. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  134. package/dist/scripts/cms/strapi/wiring.js +46 -0
  135. package/dist/scripts/cms/types.d.ts +145 -0
  136. package/dist/scripts/cms/types.js +11 -0
  137. package/dist/scripts/detect-framework.d.ts +39 -0
  138. package/dist/scripts/detect-framework.js +133 -0
  139. package/dist/scripts/draft-rows.d.ts +19 -0
  140. package/dist/scripts/draft-rows.js +135 -0
  141. package/dist/scripts/draft-schemas.d.ts +27 -0
  142. package/dist/scripts/draft-schemas.js +188 -0
  143. package/dist/scripts/extract-translations.js +16 -11
  144. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  145. package/dist/scripts/frameworks/astro/routes.js +55 -0
  146. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  147. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  148. package/dist/scripts/frameworks/discover.d.ts +70 -0
  149. package/dist/scripts/frameworks/discover.js +178 -0
  150. package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
  151. package/dist/scripts/frameworks/next/rewire.js +258 -0
  152. package/dist/scripts/frameworks/next/routes.d.ts +17 -0
  153. package/dist/scripts/frameworks/next/routes.js +134 -0
  154. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  155. package/dist/scripts/frameworks/next/wiring.js +186 -0
  156. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  157. package/dist/scripts/frameworks/nuxt/routes.js +54 -0
  158. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  159. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  160. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  161. package/dist/scripts/frameworks/remix/routes.js +50 -0
  162. package/dist/scripts/frameworks/support.d.ts +75 -0
  163. package/dist/scripts/frameworks/support.js +165 -0
  164. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  165. package/dist/scripts/frameworks/sveltekit/routes.js +50 -0
  166. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  167. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  168. package/dist/scripts/frameworks/vite/routes.d.ts +15 -0
  169. package/dist/scripts/frameworks/vite/routes.js +93 -0
  170. package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
  171. package/dist/scripts/frameworks/vite/wiring.js +99 -0
  172. package/dist/scripts/i18n-namespace.d.ts +27 -0
  173. package/dist/scripts/i18n-namespace.js +155 -0
  174. package/dist/scripts/lib/validation-report.d.ts +1 -0
  175. package/dist/scripts/lib/validation-report.js +3 -0
  176. package/dist/scripts/render-parity/capture.d.ts +56 -0
  177. package/dist/scripts/render-parity/capture.js +366 -0
  178. package/dist/scripts/render-parity/compare.d.ts +59 -0
  179. package/dist/scripts/render-parity/compare.js +0 -0
  180. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  181. package/dist/scripts/render-parity/normalize-html.js +223 -0
  182. package/dist/scripts/render-parity/types.d.ts +41 -0
  183. package/dist/scripts/render-parity/types.js +1 -0
  184. package/dist/scripts/scan-wrappable.d.ts +196 -0
  185. package/dist/scripts/scan-wrappable.js +1232 -0
  186. package/dist/scripts/validate-route-metadata.js +11 -0
  187. package/dist/scripts/validate-schemas.js +42 -1
  188. package/package.json +84 -4
  189. package/release.json +2 -2
  190. package/templates/react-router/package.json +1 -1
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Text } from '../primitives/components/Text.js';
4
+ import { useLabels } from '../primitives/labels-hook.js';
5
+ /**
6
+ * Wraps a hardcoded string so it becomes editable in Fimo. The original
7
+ * string stays in code as the fallback:
8
+ *
9
+ * <h1><FimoText id="home.hero.title">Ship your site in hours</FimoText></h1>
10
+ *
11
+ * For attribute strings use the two-argument label function instead:
12
+ *
13
+ * const { t } = useLabels();
14
+ * <input placeholder={String(t('home.newsletter.placeholder', 'Your work email'))} />
15
+ */
16
+ export function FimoText({ id, children, as, ...props }) {
17
+ const { t } = useLabels();
18
+ const textProps = { value: t(id, children), as, ...props };
19
+ return _jsx(Text, { ...textProps });
20
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { FIMO_SOURCE, FimoMedia, type FimoRichTextContent } from '../primitives/lib/index.js';
3
+ export interface WrappedMediaEntry {
4
+ url: string;
5
+ alt?: string;
6
+ name?: string;
7
+ mime?: string;
8
+ width?: number;
9
+ height?: number;
10
+ /** data-fimo-source path, e.g. `WrappedMedia.<entryId>.image` */
11
+ source: string;
12
+ }
13
+ export interface WrappedRichTextEntry {
14
+ content: FimoRichTextContent;
15
+ /** data-fimo-source path, e.g. `WrappedRichText.<entryId>.body` */
16
+ source: string;
17
+ }
18
+ export interface WrappedContentValue {
19
+ media: Record<string, WrappedMediaEntry>;
20
+ richText: Record<string, WrappedRichTextEntry>;
21
+ }
22
+ export declare function WrappedContentProvider({ media, richText, children, }: Partial<WrappedContentValue> & {
23
+ children?: React.ReactNode;
24
+ }): import("react/jsx-runtime").JSX.Element;
25
+ export declare function useWrappedMedia(id: string): FimoMedia | null;
26
+ export declare function useWrappedRichText(id: string): WrappedRichTextEntry | null;
27
+ export { FIMO_SOURCE };
28
+ //# sourceMappingURL=WrappedContentContext.d.ts.map
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useMemo } from 'react';
4
+ import { FIMO_SOURCE, FimoMedia } from '../primitives/lib/index.js';
5
+ const emptyValue = { media: {}, richText: {} };
6
+ const WrappedContentContext = createContext(emptyValue);
7
+ export function WrappedContentProvider({ media, richText, children, }) {
8
+ const value = useMemo(() => ({ media: media ?? {}, richText: richText ?? {} }), [media, richText]);
9
+ return _jsx(WrappedContentContext.Provider, { value: value, children: children });
10
+ }
11
+ export function useWrappedMedia(id) {
12
+ const { media } = useContext(WrappedContentContext);
13
+ const entry = media[id];
14
+ if (!entry) {
15
+ return null;
16
+ }
17
+ return new FimoMedia({
18
+ url: entry.url,
19
+ alt: entry.alt ?? '',
20
+ name: entry.name ?? id,
21
+ mime: entry.mime ?? '',
22
+ width: entry.width,
23
+ height: entry.height,
24
+ }, entry.source);
25
+ }
26
+ export function useWrappedRichText(id) {
27
+ const { richText } = useContext(WrappedContentContext);
28
+ return richText[id] ?? null;
29
+ }
30
+ export { FIMO_SOURCE };
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import type { FimoConfig } from '../shared/fimo-config.types.js';
3
+ import { type WrappedMediaEntry, type WrappedRichTextEntry } from './WrappedContentContext.js';
4
+ export interface WrappedLabels {
5
+ dictionaries: Record<string, Record<string, string>>;
6
+ defaultLocale: string;
7
+ locales?: string[];
8
+ updatedAt?: string | null;
9
+ signature: string;
10
+ }
11
+ /** Framework navigation adapter: the ONLY thing an adapter must supply. */
12
+ export interface WrappedNavigation {
13
+ pathname: string | null;
14
+ navigate: (to: string) => void;
15
+ }
16
+ export interface WrappedProvidersProps {
17
+ config: FimoConfig;
18
+ labels?: WrappedLabels;
19
+ media?: Record<string, WrappedMediaEntry>;
20
+ richText?: Record<string, WrappedRichTextEntry>;
21
+ locale?: string;
22
+ navigation: WrappedNavigation;
23
+ children?: React.ReactNode;
24
+ }
25
+ /**
26
+ * Framework-neutral providers for WRAPPED (adopted) apps. Adapters
27
+ * (fimo/next, fimo/remix, ...) supply only the navigation hooks; everything
28
+ * else — label hydration, locale resolution, preview bridge, error boundary —
29
+ * is shared. Content arrives as serializable props from the wrapped server
30
+ * loaders.
31
+ */
32
+ export declare function WrappedProviders({ config, labels, media, richText, locale, navigation, children }: WrappedProvidersProps): import("react/jsx-runtime").JSX.Element;
33
+ //# sourceMappingURL=WrappedProviders.d.ts.map
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { ErrorBoundary } from 'react-error-boundary';
6
+ import { DataSourcesProvider } from '../content/DataSourcesProvider.js';
7
+ import { AppErrorBoundary, onError } from '../core/AppErrorBoundary.js';
8
+ import { FimoLocaleProvider } from '../i18n/FimoLocaleContext.js';
9
+ import { getFimoTextDirection, resolveFimoLocale, toHtmlLang } from '../i18n/locale.js';
10
+ import { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
11
+ import { hydrateLabels } from '../primitives/labels-store.js';
12
+ import { getDisplayRoutes } from '../shared/display-routes.js';
13
+ import { WrappedContentProvider } from './WrappedContentContext.js';
14
+ function toLabelState(labels) {
15
+ return {
16
+ dictionaries: labels.dictionaries,
17
+ defaultLocale: labels.defaultLocale,
18
+ signature: labels.signature,
19
+ updatedAt: labels.updatedAt ?? null,
20
+ };
21
+ }
22
+ function PreviewBridge({ config, navigation }) {
23
+ const { pathname, navigate } = navigation;
24
+ const localeResolution = useMemo(() => resolveFimoLocale({ pathname: pathname ?? undefined }, config), [pathname, config]);
25
+ const sendRoutes = useCallback(() => {
26
+ window.parent.postMessage({ type: 'nav/routes', payload: { routes: getDisplayRoutes(config) } }, '*');
27
+ }, [config]);
28
+ const sendLocationChange = useCallback(() => {
29
+ window.parent.postMessage({
30
+ type: 'nav/locationChange',
31
+ payload: {
32
+ path: pathname,
33
+ routePath: localeResolution.routePath,
34
+ locale: localeResolution.locale,
35
+ },
36
+ }, '*');
37
+ }, [pathname, localeResolution.locale, localeResolution.routePath]);
38
+ useEffect(sendRoutes, [sendRoutes]);
39
+ useEffect(sendLocationChange, [sendLocationChange]);
40
+ useEffect(() => {
41
+ const handleMessage = (event) => {
42
+ const type = event.data?.type;
43
+ const payload = event.data?.payload;
44
+ switch (type) {
45
+ case 'nav/navigate':
46
+ if (payload?.to) {
47
+ navigate(payload.to);
48
+ }
49
+ return;
50
+ case 'nav/read-routes':
51
+ sendRoutes();
52
+ sendLocationChange();
53
+ return;
54
+ }
55
+ };
56
+ window.addEventListener('message', handleMessage);
57
+ return () => {
58
+ window.removeEventListener('message', handleMessage);
59
+ };
60
+ }, [navigate, sendRoutes, sendLocationChange]);
61
+ return null;
62
+ }
63
+ /**
64
+ * Framework-neutral providers for WRAPPED (adopted) apps. Adapters
65
+ * (fimo/next, fimo/remix, ...) supply only the navigation hooks; everything
66
+ * else — label hydration, locale resolution, preview bridge, error boundary —
67
+ * is shared. Content arrives as serializable props from the wrapped server
68
+ * loaders.
69
+ */
70
+ export function WrappedProviders({ config, labels, media, richText, locale, navigation, children }) {
71
+ // Wrapped apps have no QueryClientProvider of their own; ScreenshotProvider
72
+ // (and future data-refresh machinery) need one, so the adapter owns it.
73
+ const [queryClient] = useState(() => new QueryClient());
74
+ useState(() => {
75
+ if (labels) {
76
+ hydrateLabels(toLabelState(labels));
77
+ }
78
+ return null;
79
+ });
80
+ useEffect(() => {
81
+ if (labels) {
82
+ hydrateLabels(toLabelState(labels));
83
+ }
84
+ }, [labels]);
85
+ const pathname = navigation.pathname;
86
+ const localeResolution = useMemo(() => resolveFimoLocale({ pathname: pathname ?? undefined }, config), [pathname, config]);
87
+ const activeLocale = locale ?? localeResolution.locale;
88
+ const activeDir = getFimoTextDirection(activeLocale);
89
+ useEffect(() => {
90
+ if (typeof document === 'undefined') {
91
+ return;
92
+ }
93
+ document.documentElement.lang = toHtmlLang(activeLocale);
94
+ document.documentElement.dir = activeDir;
95
+ }, [activeDir, activeLocale]);
96
+ return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(QueryClientProvider, { client: queryClient, children: _jsx(FimoLocaleProvider, { value: {
97
+ ...localeResolution,
98
+ locale: activeLocale,
99
+ dir: activeDir,
100
+ }, children: _jsx(WrappedContentProvider, { media: media, richText: richText, children: _jsx(DataSourcesProvider, { children: _jsxs(ScreenshotProvider, { children: [_jsx(PreviewBridge, { config: config, navigation: navigation }), _jsx(Suspense, { children: children })] }) }) }) }) }) }));
101
+ }
@@ -0,0 +1,4 @@
1
+ export declare const FIMO_SOURCE_ATTR = "data-fimo-source";
2
+ export declare function fimoLabelAttrs(key: string, locale?: string): Record<string, string>;
3
+ export declare function fimoMediaAttrs(source: string): Record<string, string>;
4
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1,23 @@
1
+ // Framework-agnostic DOM contract for wrapped content in NON-React
2
+ // frameworks (Astro templates, Vue SFCs, Svelte components). The edit-mode
3
+ // preview client selects elements by these attributes; rendering the value +
4
+ // attributes is all a template needs:
5
+ //
6
+ // Astro: <h1 {...fimoLabelAttrs('home.title')}>{t('home.title', 'Hello')}</h1>
7
+ // Vue: <h1 v-bind="fimoLabelAttrs('home.title')">{{ t('home.title', 'Hello') }}</h1>
8
+ // Svelte: <h1 {...fimoLabelAttrs('home.title')}>{tv}</h1>
9
+ //
10
+ // Values come from the server loaders (loadFimoT / loadWrappedMedia) — after
11
+ // an edit, the page refetches on reload; live DOM patching is handled by the
12
+ // preview client via these same attributes.
13
+ // NOTE: this must be the literal DOM attribute the preview client queries
14
+ // (`document.querySelectorAll('[data-fimo-source]')`) — NOT the FIMO_SOURCE
15
+ // Symbol from the React primitives: a symbol key vanishes in a template
16
+ // attribute spread and in JSON, silently breaking editability.
17
+ export const FIMO_SOURCE_ATTR = 'data-fimo-source';
18
+ export function fimoLabelAttrs(key, locale = 'en') {
19
+ return { [FIMO_SOURCE_ATTR]: `__labels.${locale}.${key}.value` };
20
+ }
21
+ export function fimoMediaAttrs(source) {
22
+ return { [FIMO_SOURCE_ATTR]: source };
23
+ }
@@ -0,0 +1,77 @@
1
+ import type { FimoNextLabels } from '../next/FimoNextProviders.js';
2
+ import type { FimoConfig } from '../shared/fimo-config.types.js';
3
+ import type { WrappedMediaEntry, WrappedRichTextEntry } from './WrappedContentContext.js';
4
+ export interface FimoServerOptions {
5
+ /** Project root (where fimo-config.json and .env live). Defaults to cwd. */
6
+ rootDir?: string;
7
+ /** Tenant API base URL. Defaults to NEXT_PUBLIC_FIMO_API_URL / VITE_API_URL. */
8
+ apiUrl?: string;
9
+ }
10
+ export declare function loadFimoConfig(rootDir?: string): FimoConfig;
11
+ export declare function loadFimoLabels(options?: FimoServerOptions): Promise<FimoNextLabels>;
12
+ /**
13
+ * Server-component label accessor: `useLabels()` is client-only, and forcing
14
+ * a client wrapper for one aria-label in a server component is not a
15
+ * migration, it is collateral damage. Load once per request scope and read
16
+ * synchronously: `const t = await loadFimoT(); <a aria-label={t('footer.github', 'GitHub')} …`.
17
+ */
18
+ export declare function loadFimoT(options?: FimoServerOptions & {
19
+ locale?: string;
20
+ }): Promise<(key: string, fallback: string) => string>;
21
+ export interface FimoEntry {
22
+ id: string;
23
+ data: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * Fetch published entries of a content type from the public tenant API.
27
+ * Returns [] when the API is unreachable so the wrapped site keeps rendering
28
+ * its fallbacks.
29
+ */
30
+ export declare function fetchEntries(typeName: string, options?: FimoServerOptions & {
31
+ /** The public API returns newest-first; rendering wants source order.
32
+ * Default 'oldest' restores insertion order for migrated collections. */
33
+ order?: 'oldest' | 'newest';
34
+ /** Query filter forwarded to the public API (e.g. { slug: { $in: [...] } }). */
35
+ where?: Record<string, unknown>;
36
+ /** Row cap forwarded to the public API. */
37
+ limit?: number;
38
+ }): Promise<FimoEntry[]>;
39
+ /**
40
+ * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
41
+ * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
42
+ * asset-typed `image` field.
43
+ */
44
+ export declare function loadWrappedMedia(options?: FimoServerOptions): Promise<Record<string, WrappedMediaEntry>>;
45
+ /**
46
+ * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
47
+ * migration for formatted text (bold/italic/underline/links).
48
+ */
49
+ export declare function loadWrappedRichText(options?: FimoServerOptions): Promise<Record<string, WrappedRichTextEntry>>;
50
+ /**
51
+ * Overlay a wrap-migrated collection onto its retained source rows: fetch the
52
+ * Fimo entries matching the source rows' keys, merge field-by-field with the
53
+ * type-matched rule above, and return plain data rows in SOURCE order. The
54
+ * page keeps its original render logic and its original fallback — edits made
55
+ * in Fimo show up, everything else stays byte-identical.
56
+ */
57
+ export declare function wrapCollection<T extends Record<string, unknown>>(typeName: string, sourceRows: readonly T[], options?: FimoServerOptions & {
58
+ keyField?: string;
59
+ }): Promise<T[]>;
60
+ /**
61
+ * Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
62
+ * i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
63
+ * labels (flat dot-keys); this fetches them back and un-flattens them into the
64
+ * nested shape the app's own i18n loader expects — so the app keeps its
65
+ * `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
66
+ *
67
+ * Deep-merge over the shipped messages (pass them as `fallback`) so any key
68
+ * not yet in Fimo falls back to the static file instead of showing a missing
69
+ * key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
70
+ *
71
+ * const messages = await loadFimoMessages(locale, { fallback: staticMessages });
72
+ * return { locale, messages };
73
+ */
74
+ export declare function loadFimoMessages(locale: string, options?: FimoServerOptions & {
75
+ fallback?: Record<string, unknown>;
76
+ }): Promise<Record<string, unknown>>;
77
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,237 @@
1
+ // Server-only loaders for WRAPPED (adopted) apps — framework-neutral.
2
+ // Content resolves at request time from the public tenant API (no Vite
3
+ // virtual modules, no build plugin). Framework adapters re-export these:
4
+ // fimo/next/server today, fimo/astro tomorrow — same loaders, same shapes.
5
+ import { getConfigServer } from '../shared/fimo-config.server.js';
6
+ import { deepMergeMessages, unflattenMessages } from '../shared/i18n-messages.js';
7
+ import { fetchLabelDictionary, fetchWithTimeout, labelSignature, loadLabelBundle, readProjectEnv, resolveTenantApiUrl, } from '../shared/labels-fetch.server.js';
8
+ export function loadFimoConfig(rootDir = process.cwd()) {
9
+ return getConfigServer(rootDir);
10
+ }
11
+ export async function loadFimoLabels(options = {}) {
12
+ const rootDir = options.rootDir ?? process.cwd();
13
+ const bundle = await loadLabelBundle(rootDir);
14
+ return { ...bundle, signature: labelSignature(bundle) };
15
+ }
16
+ /**
17
+ * Server-component label accessor: `useLabels()` is client-only, and forcing
18
+ * a client wrapper for one aria-label in a server component is not a
19
+ * migration, it is collateral damage. Load once per request scope and read
20
+ * synchronously: `const t = await loadFimoT(); <a aria-label={t('footer.github', 'GitHub')} …`.
21
+ */
22
+ export async function loadFimoT(options = {}) {
23
+ const labels = await loadFimoLabels(options);
24
+ const locale = options.locale ?? labels.defaultLocale;
25
+ const dictionary = labels.dictionaries[locale] ?? labels.dictionaries[labels.defaultLocale] ?? {};
26
+ return (key, fallback) => {
27
+ const value = dictionary[key];
28
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
29
+ };
30
+ }
31
+ async function resolveApiUrl(options) {
32
+ if (options.apiUrl) {
33
+ return options.apiUrl;
34
+ }
35
+ const env = await readProjectEnv(options.rootDir ?? process.cwd());
36
+ return resolveTenantApiUrl(env);
37
+ }
38
+ /**
39
+ * Fetch published entries of a content type from the public tenant API.
40
+ * Returns [] when the API is unreachable so the wrapped site keeps rendering
41
+ * its fallbacks.
42
+ */
43
+ export async function fetchEntries(typeName, options = {}) {
44
+ const apiUrl = await resolveApiUrl(options);
45
+ if (!apiUrl) {
46
+ return [];
47
+ }
48
+ try {
49
+ const url = new URL(`/entries/${typeName}`, apiUrl);
50
+ if (options.where) {
51
+ url.searchParams.set('where', JSON.stringify(options.where));
52
+ }
53
+ if (options.limit) {
54
+ url.searchParams.set('limit', String(options.limit));
55
+ }
56
+ const response = await fetchWithTimeout(url, 4000);
57
+ if (!response.ok) {
58
+ return [];
59
+ }
60
+ const body = (await response.json());
61
+ const rows = Array.isArray(body.data) ? body.data : [];
62
+ const entries = rows
63
+ .filter((row) => Boolean(row) && typeof row === 'object')
64
+ .map((row) => ({
65
+ id: String(row.id ?? row.documentId ?? ''),
66
+ data: (row.data && typeof row.data === 'object' ? row.data : row),
67
+ }))
68
+ .filter((row) => row.id.length > 0);
69
+ return (options.order ?? 'oldest') === 'oldest' ? entries.reverse() : entries;
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
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
+ /**
82
+ * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
83
+ * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
84
+ * asset-typed `image` field.
85
+ */
86
+ export async function loadWrappedMedia(options = {}) {
87
+ const entries = await fetchEntries('WrappedMedia', options);
88
+ const map = {};
89
+ for (const entry of entries) {
90
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
91
+ const asset = entry.data.image;
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;
111
+ }
112
+ /**
113
+ * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
114
+ * migration for formatted text (bold/italic/underline/links).
115
+ */
116
+ export async function loadWrappedRichText(options = {}) {
117
+ const entries = await fetchEntries('WrappedRichText', options);
118
+ const map = {};
119
+ for (const entry of entries) {
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
+ };
129
+ }
130
+ return map;
131
+ }
132
+ /** Key fields tried, in order, to align a source row with its Fimo entry. */
133
+ const COLLECTION_KEY_FIELDS = ['slug', 'id', 'key', 'name', 'title', 'question'];
134
+ /** Fimo entries fetched per collection: covers rendered windows and small
135
+ * collections entirely; rows beyond it keep rendering their source values. */
136
+ const COLLECTION_FETCH_CAP = 100;
137
+ function collectionKeyField(rows) {
138
+ for (const field of COLLECTION_KEY_FIELDS) {
139
+ if (rows.every((row) => typeof row[field] === 'string' && row[field].length > 0)) {
140
+ return field;
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ /** A Fimo value replaces its source value only when the SHAPES agree, so a
146
+ * page that rendered strings keeps rendering strings: media envelopes,
147
+ * richtext docs, reference handles, and nulls all fall back to the source
148
+ * value instead of leaking objects into JSX. */
149
+ function preferTypeMatched(fimoValue, sourceValue) {
150
+ if (fimoValue === null || fimoValue === undefined) {
151
+ return sourceValue;
152
+ }
153
+ if (Array.isArray(sourceValue)) {
154
+ if (!Array.isArray(fimoValue)) {
155
+ return sourceValue;
156
+ }
157
+ return fimoValue.every((element) => typeof element === typeof (sourceValue[0] ?? '') && typeof element !== 'object')
158
+ ? fimoValue
159
+ : sourceValue;
160
+ }
161
+ return typeof fimoValue === typeof sourceValue && typeof fimoValue !== 'object' ? fimoValue : sourceValue;
162
+ }
163
+ /**
164
+ * Overlay a wrap-migrated collection onto its retained source rows: fetch the
165
+ * Fimo entries matching the source rows' keys, merge field-by-field with the
166
+ * type-matched rule above, and return plain data rows in SOURCE order. The
167
+ * page keeps its original render logic and its original fallback — edits made
168
+ * in Fimo show up, everything else stays byte-identical.
169
+ */
170
+ export async function wrapCollection(typeName, sourceRows, options = {}) {
171
+ const rows = sourceRows;
172
+ if (rows.length === 0) {
173
+ return [];
174
+ }
175
+ const keyField = options.keyField ?? collectionKeyField(rows);
176
+ if (!keyField) {
177
+ return sourceRows;
178
+ }
179
+ const keys = rows.slice(0, COLLECTION_FETCH_CAP).map((row) => row[keyField]);
180
+ const entries = await fetchEntries(typeName, {
181
+ ...options,
182
+ where: { [keyField]: { $in: keys } },
183
+ limit: keys.length,
184
+ });
185
+ if (entries.length === 0) {
186
+ return sourceRows;
187
+ }
188
+ const byKey = new Map();
189
+ for (const entry of entries) {
190
+ const key = entry.data[keyField];
191
+ if (typeof key === 'string' && !byKey.has(key)) {
192
+ byKey.set(key, entry.data);
193
+ }
194
+ }
195
+ return rows.map((row) => {
196
+ const fimoRow = byKey.get(row[keyField]);
197
+ if (!fimoRow) {
198
+ return row;
199
+ }
200
+ const merged = { ...row };
201
+ for (const field of Object.keys(row)) {
202
+ merged[field] = preferTypeMatched(fimoRow[field], row[field]);
203
+ }
204
+ return merged;
205
+ });
206
+ }
207
+ /**
208
+ * Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
209
+ * i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
210
+ * labels (flat dot-keys); this fetches them back and un-flattens them into the
211
+ * nested shape the app's own i18n loader expects — so the app keeps its
212
+ * `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
213
+ *
214
+ * Deep-merge over the shipped messages (pass them as `fallback`) so any key
215
+ * not yet in Fimo falls back to the static file instead of showing a missing
216
+ * key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
217
+ *
218
+ * const messages = await loadFimoMessages(locale, { fallback: staticMessages });
219
+ * return { locale, messages };
220
+ */
221
+ export async function loadFimoMessages(locale, options = {}) {
222
+ const fallback = options.fallback ?? {};
223
+ const apiUrl = await resolveApiUrl(options);
224
+ if (!apiUrl) {
225
+ return fallback;
226
+ }
227
+ try {
228
+ const { labels } = await fetchLabelDictionary(apiUrl, locale);
229
+ if (Object.keys(labels).length === 0) {
230
+ return fallback;
231
+ }
232
+ return deepMergeMessages(fallback, unflattenMessages(labels));
233
+ }
234
+ catch {
235
+ return fallback;
236
+ }
237
+ }
@@ -0,0 +1,61 @@
1
+ import { KeyAllocator, type ApplyWrapResult } from './apply-wrap.js';
2
+ import { type WrappableItem } from './scan-wrappable.js';
3
+ /**
4
+ * The template half of the --apply codemod: wraps plain TEMPLATE text nodes
5
+ * with the framework's t() form and injects the per-file wiring head once.
6
+ * Framework-neutral by contract — everything framework-specific arrives via
7
+ * a TemplateWrapStyle from the framework registry (frameworks/discover.ts).
8
+ *
9
+ * Grown out of the batch-dogfood plan-driven wrapper (392/392 items on a
10
+ * 201-route site); the lessons it encodes:
11
+ * - heads go in the INSTANCE script, never a `<script module>` block;
12
+ * - files that already bind a `t` identifier (i18n libraries, custom
13
+ * helpers) are left whole for the agent — renaming foreign t() calls
14
+ * broke a real site;
15
+ * - label VALUES decode HTML entities while the inline fallback keeps the
16
+ * source form, so rendering never changes;
17
+ * - keys are collision-safe via the shared KeyAllocator.
18
+ *
19
+ * Anything that is not a plain text node (attributes, media, markup the
20
+ * preview matcher cannot anchor) stays in the plan for the agent — same
21
+ * philosophy as the JSX codemod: provably safe or hands off.
22
+ */
23
+ export interface TemplateWrapStyle {
24
+ /** File extension this style owns, e.g. '.astro'. */
25
+ ext: string;
26
+ /** Wiring lines injected once per file (the t() head). */
27
+ head: string;
28
+ /** Marker proving our wiring (or an equivalent manual one) is present. */
29
+ headMarker: RegExp;
30
+ /** t() delimiters around `"key", "fallback"`. */
31
+ open: string;
32
+ close: string;
33
+ /**
34
+ * Insert the head into a file. Return null when the file has no safe
35
+ * injection point (e.g. an options-API script) — its items stay agent work.
36
+ */
37
+ injectHead: (code: string, head: string) => string | null;
38
+ /** Override the key scope for route-convention filenames (+page.svelte). */
39
+ fileScope?: (file: string) => string | null;
40
+ /** One-time project wiring once any file was wrapped. Returns files written. */
41
+ ensureWiring?: (rootDir: string) => string[];
42
+ }
43
+ /**
44
+ * Shared injection helper for SFC-style templates: first instance script,
45
+ * never a `<script module>` block (module scope cannot read page data), or a
46
+ * fresh script when the file has none.
47
+ */
48
+ export declare function injectIntoInstanceScript(code: string, head: string, scriptTag: string): string;
49
+ export declare function applyTemplateWrapToFile(code: string, style: TemplateWrapStyle, items: WrappableItem[], allocator: KeyAllocator): {
50
+ code: string;
51
+ } & Omit<ApplyWrapResult, 'filesChanged'>;
52
+ /**
53
+ * Apply the template wraps across a project — same contract as applyWrap:
54
+ * scans, mutates source files, returns the generated labels for staging and
55
+ * seeding. The style's ensureWiring hook then writes any one-time project
56
+ * wiring (e.g. a root layout labels loader) when files were wrapped.
57
+ */
58
+ export declare function applyTemplateWrap(rootDir: string, opts: {
59
+ style: TemplateWrapStyle;
60
+ excludePrefixes?: string[];
61
+ }): ApplyWrapResult;