fimo 0.8.0 → 0.9.0-experimental.10

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 (218) hide show
  1. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
  2. package/assets/agents-starter/claude/settings.json +19 -0
  3. package/assets/content-templates/hooks-template.eta +4 -2
  4. package/assets/content-templates/types-template.eta +67 -1
  5. package/assets/skills/fimo/SKILL.md +1 -1
  6. package/assets/skills/fimo/references/content.md +86 -15
  7. package/assets/skills/fimo/references/translations.md +75 -0
  8. package/assets/skills/fimo-cli/SKILL.md +6 -2
  9. package/assets/skills/fimo-cli/references/content.md +61 -2
  10. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  11. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  12. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  13. package/assets/skills/fimo-migration/SKILL.md +24 -2
  14. package/assets/skills/fimo-migration/references/content.md +24 -0
  15. package/assets/skills/fimo-migration/references/manifest.md +46 -0
  16. package/assets/skills/fimo-migration/references/pages.md +234 -0
  17. package/assets/skills/fimo-migration/references/qa.md +24 -2
  18. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  19. package/assets/skills/fimo-migration/references/wrap.md +662 -0
  20. package/assets/skills/fimo-studio/SKILL.md +5 -3
  21. package/assets/skills/fimo-studio/references/cms.md +9 -1
  22. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  23. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  24. package/dist/build/vite/plugins/translations.d.ts +2 -8
  25. package/dist/build/vite/plugins/translations.js +31 -131
  26. package/dist/cli/bundle.json +1 -1
  27. package/dist/cli/index.js +1337 -189
  28. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  29. package/dist/runtime/app/FimoProviders.js +31 -30
  30. package/dist/runtime/astro/index.d.ts +2 -0
  31. package/dist/runtime/astro/index.js +3 -0
  32. package/dist/runtime/astro/server.d.ts +3 -0
  33. package/dist/runtime/astro/server.js +6 -0
  34. package/dist/runtime/content/client/internal.js +1 -0
  35. package/dist/runtime/content/client/types.d.ts +16 -0
  36. package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
  37. package/dist/runtime/core/BuiltWithBadge.js +68 -0
  38. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  39. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  40. package/dist/runtime/next/FimoNextProviders.js +20 -0
  41. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  42. package/dist/runtime/next/FimoNextScripts.js +19 -0
  43. package/dist/runtime/next/index.d.ts +14 -0
  44. package/dist/runtime/next/index.js +14 -0
  45. package/dist/runtime/next/server.d.ts +2 -0
  46. package/dist/runtime/next/server.js +4 -0
  47. package/dist/runtime/nuxt/composables.d.ts +9 -0
  48. package/dist/runtime/nuxt/composables.js +57 -0
  49. package/dist/runtime/nuxt/index.d.ts +3 -0
  50. package/dist/runtime/nuxt/index.js +6 -0
  51. package/dist/runtime/nuxt/server.d.ts +3 -0
  52. package/dist/runtime/nuxt/server.js +6 -0
  53. package/dist/runtime/primitives/components/RichText.d.ts +5 -1
  54. package/dist/runtime/primitives/components/RichText.js +2 -2
  55. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  56. package/dist/runtime/primitives/labels-hook.js +41 -0
  57. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  58. package/dist/runtime/primitives/labels-store.js +60 -0
  59. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  60. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  61. package/dist/runtime/primitives/translations.d.ts +1 -27
  62. package/dist/runtime/primitives/translations.js +7 -85
  63. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  64. package/dist/runtime/react/FimoSpaProviders.js +122 -0
  65. package/dist/runtime/react/index.d.ts +35 -0
  66. package/dist/runtime/react/index.js +40 -0
  67. package/dist/runtime/react/spa-content-cache.d.ts +38 -0
  68. package/dist/runtime/react/spa-content-cache.js +64 -0
  69. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  70. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  71. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  72. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  73. package/dist/runtime/remix/index.d.ts +12 -0
  74. package/dist/runtime/remix/index.js +11 -0
  75. package/dist/runtime/remix/server.d.ts +2 -0
  76. package/dist/runtime/remix/server.js +2 -0
  77. package/dist/runtime/seo/neutral.d.ts +22 -0
  78. package/dist/runtime/seo/neutral.js +17 -0
  79. package/dist/runtime/shared/display-routes.d.ts +14 -0
  80. package/dist/runtime/shared/display-routes.js +25 -0
  81. package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
  82. package/dist/runtime/shared/fimo-config.server.js +1 -13
  83. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  84. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  85. package/dist/runtime/shared/i18n-messages.js +79 -0
  86. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  87. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  88. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  89. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  90. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  91. package/dist/runtime/shared/labels-t.pure.js +10 -0
  92. package/dist/runtime/strapi/client.d.ts +30 -0
  93. package/dist/runtime/strapi/client.js +132 -0
  94. package/dist/runtime/strapi/index.d.ts +5 -0
  95. package/dist/runtime/strapi/index.js +6 -0
  96. package/dist/runtime/strapi/reshape.d.ts +50 -0
  97. package/dist/runtime/strapi/reshape.js +121 -0
  98. package/dist/runtime/svelte/index.d.ts +3 -0
  99. package/dist/runtime/svelte/index.js +7 -0
  100. package/dist/runtime/svelte/server.d.ts +3 -0
  101. package/dist/runtime/svelte/server.js +6 -0
  102. package/dist/runtime/vue/index.d.ts +3 -0
  103. package/dist/runtime/vue/index.js +5 -0
  104. package/dist/runtime/vue/server.d.ts +3 -0
  105. package/dist/runtime/vue/server.js +6 -0
  106. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  107. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  108. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  109. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  110. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  111. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  112. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
  113. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  114. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  115. package/dist/runtime/wrapped/FimoText.js +20 -0
  116. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  117. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  118. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  119. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  120. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  121. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  122. package/dist/runtime/wrapped/dom.d.ts +4 -0
  123. package/dist/runtime/wrapped/dom.js +23 -0
  124. package/dist/runtime/wrapped/server.d.ts +80 -0
  125. package/dist/runtime/wrapped/server.js +207 -0
  126. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  127. package/dist/scripts/apply-wrap-templates.js +137 -0
  128. package/dist/scripts/apply-wrap.d.ts +73 -0
  129. package/dist/scripts/apply-wrap.js +340 -0
  130. package/dist/scripts/cms/discover.d.ts +2 -0
  131. package/dist/scripts/cms/discover.js +17 -0
  132. package/dist/scripts/cms/example-content.d.ts +33 -0
  133. package/dist/scripts/cms/example-content.js +165 -0
  134. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  135. package/dist/scripts/cms/strapi/detect.js +186 -0
  136. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  137. package/dist/scripts/cms/strapi/index.js +25 -0
  138. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  139. package/dist/scripts/cms/strapi/list-source.js +90 -0
  140. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  141. package/dist/scripts/cms/strapi/schema-map.js +289 -0
  142. package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
  143. package/dist/scripts/cms/strapi/shapes.js +48 -0
  144. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  145. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  146. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  147. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  148. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  149. package/dist/scripts/cms/strapi/wiring.js +46 -0
  150. package/dist/scripts/cms/types.d.ts +145 -0
  151. package/dist/scripts/cms/types.js +11 -0
  152. package/dist/scripts/detect-framework.d.ts +39 -0
  153. package/dist/scripts/detect-framework.js +139 -0
  154. package/dist/scripts/draft-rows.d.ts +19 -0
  155. package/dist/scripts/draft-rows.js +178 -0
  156. package/dist/scripts/draft-schemas.d.ts +27 -0
  157. package/dist/scripts/draft-schemas.js +247 -0
  158. package/dist/scripts/extract-translations.js +16 -11
  159. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  160. package/dist/scripts/frameworks/astro/routes.js +44 -0
  161. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  162. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  163. package/dist/scripts/frameworks/discover.d.ts +83 -0
  164. package/dist/scripts/frameworks/discover.js +288 -0
  165. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  166. package/dist/scripts/frameworks/discovered-route.js +17 -0
  167. package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
  168. package/dist/scripts/frameworks/next/rewire.js +282 -0
  169. package/dist/scripts/frameworks/next/routes.d.ts +30 -0
  170. package/dist/scripts/frameworks/next/routes.js +176 -0
  171. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  172. package/dist/scripts/frameworks/next/wiring.js +186 -0
  173. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  174. package/dist/scripts/frameworks/nuxt/routes.js +38 -0
  175. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  176. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  177. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  178. package/dist/scripts/frameworks/remix/routes.js +36 -0
  179. package/dist/scripts/frameworks/support.d.ts +75 -0
  180. package/dist/scripts/frameworks/support.js +165 -0
  181. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  182. package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
  183. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  184. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  185. package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
  186. package/dist/scripts/frameworks/vite/routes.js +143 -0
  187. package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
  188. package/dist/scripts/frameworks/vite/wiring.js +144 -0
  189. package/dist/scripts/i18n-namespace.d.ts +27 -0
  190. package/dist/scripts/i18n-namespace.js +155 -0
  191. package/dist/scripts/lib/humanize.d.ts +14 -0
  192. package/dist/scripts/lib/humanize.js +54 -0
  193. package/dist/scripts/lib/validation-report.d.ts +1 -3
  194. package/dist/scripts/lib/validation-report.js +3 -9
  195. package/dist/scripts/render-parity/capture.d.ts +64 -0
  196. package/dist/scripts/render-parity/capture.js +436 -0
  197. package/dist/scripts/render-parity/compare.d.ts +59 -0
  198. package/dist/scripts/render-parity/compare.js +0 -0
  199. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  200. package/dist/scripts/render-parity/normalize-html.js +223 -0
  201. package/dist/scripts/render-parity/types.d.ts +41 -0
  202. package/dist/scripts/render-parity/types.js +1 -0
  203. package/dist/scripts/routes/bindings.d.ts +93 -0
  204. package/dist/scripts/routes/bindings.js +117 -0
  205. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  206. package/dist/scripts/routes/label-prefixes.js +136 -0
  207. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  208. package/dist/scripts/routes/module-resolve.js +78 -0
  209. package/dist/scripts/routes/page-structure.d.ts +72 -0
  210. package/dist/scripts/routes/page-structure.js +234 -0
  211. package/dist/scripts/scan-wrappable.d.ts +200 -0
  212. package/dist/scripts/scan-wrappable.js +1355 -0
  213. package/dist/scripts/validate-route-metadata.js +11 -0
  214. package/dist/scripts/validate-schemas.js +42 -1
  215. package/package.json +88 -4
  216. package/release.json +2 -2
  217. package/templates/react-router/package.json +1 -1
  218. package/templates/react-router/translations/en.json +1 -0
@@ -0,0 +1,106 @@
1
+ // Wrapped-content fetching, PURE core: fetch/URL/AbortController only — no
2
+ // node builtins, importable from client-bundled code (the SPA provider) and
3
+ // from the server loaders alike. The plumbing types (WrappedRichText,
4
+ // WrappedMedia) hold ALL of a site's wrapped rich text and media, so the
5
+ // fetch PAGINATES to the end — the tenant API caps limit at 100 and a real
6
+ // site exceeds it (the Fimo site carries 147 rich-text entries).
7
+ import { fetchWithTimeout } from '../shared/labels-fetch.pure.js';
8
+ const PAGE_LIMIT = 100;
9
+ const MAX_ROWS = 5000;
10
+ function readString(value) {
11
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
12
+ }
13
+ function readNumber(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
15
+ }
16
+ /** Every published row of a content type, paginated to the end. Returns []
17
+ * when the API is unreachable so wrapped sites keep their fallbacks. */
18
+ export async function fetchAllWrappedRows(apiUrl, typeName) {
19
+ const rows = [];
20
+ try {
21
+ for (let offset = 0; offset < MAX_ROWS; offset += PAGE_LIMIT) {
22
+ const url = new URL(`/entries/${typeName}`, apiUrl);
23
+ url.searchParams.set('limit', String(PAGE_LIMIT));
24
+ url.searchParams.set('offset', String(offset));
25
+ const response = await fetchWithTimeout(url, 4000);
26
+ if (!response.ok) {
27
+ break;
28
+ }
29
+ const body = (await response.json());
30
+ const page = Array.isArray(body.data) ? body.data : [];
31
+ for (const row of page) {
32
+ if (!row || typeof row !== 'object') {
33
+ continue;
34
+ }
35
+ const record = row;
36
+ const id = String(record.id ?? record.documentId ?? '');
37
+ if (id.length === 0) {
38
+ continue;
39
+ }
40
+ rows.push({
41
+ id,
42
+ data: (record.data && typeof record.data === 'object' ? record.data : record),
43
+ });
44
+ }
45
+ const hasMore = body.meta?.hasMore === true || page.length === PAGE_LIMIT;
46
+ if (!hasMore) {
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // Unreachable API mid-pagination: return what we have (possibly []).
53
+ }
54
+ return rows;
55
+ }
56
+ /** WrappedMedia rows → key → media entry (the FimoMediaImage/Video id map). */
57
+ export function mapWrappedMedia(rows) {
58
+ const map = {};
59
+ for (const entry of rows) {
60
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
61
+ const asset = entry.data.image;
62
+ if (!key || !asset || typeof asset !== 'object') {
63
+ continue;
64
+ }
65
+ const assetRecord = asset;
66
+ const url = readString(assetRecord.url);
67
+ if (!url) {
68
+ continue;
69
+ }
70
+ map[key] = {
71
+ url,
72
+ alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
73
+ name: readString(assetRecord.name),
74
+ mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
75
+ width: readNumber(assetRecord.width),
76
+ height: readNumber(assetRecord.height),
77
+ source: `WrappedMedia.${entry.id}.image`,
78
+ };
79
+ }
80
+ return map;
81
+ }
82
+ /** WrappedRichText rows → key → TipTap content (the FimoRichTextBlock map). */
83
+ export function mapWrappedRichText(rows) {
84
+ const map = {};
85
+ for (const entry of rows) {
86
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
87
+ const body = entry.data.body;
88
+ if (!key || !body || typeof body !== 'object') {
89
+ continue;
90
+ }
91
+ map[key] = {
92
+ content: body,
93
+ source: `WrappedRichText.${entry.id}.body`,
94
+ };
95
+ }
96
+ return map;
97
+ }
98
+ /** Both plumbing maps in one round: what a client-rendered wrapped app needs
99
+ * so Studio edits to rich text and media are LIVE, not fallback-only. */
100
+ export async function fetchWrappedContent(apiUrl) {
101
+ const [mediaRows, richTextRows] = await Promise.all([
102
+ fetchAllWrappedRows(apiUrl, 'WrappedMedia'),
103
+ fetchAllWrappedRows(apiUrl, 'WrappedRichText'),
104
+ ]);
105
+ return { media: mapWrappedMedia(mediaRows), richText: mapWrappedRichText(richTextRows) };
106
+ }
@@ -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,80 @@
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. Paginates to the end — a site's wrapped media
43
+ * routinely exceeds one API page.
44
+ */
45
+ export declare function loadWrappedMedia(options?: FimoServerOptions): Promise<Record<string, WrappedMediaEntry>>;
46
+ /**
47
+ * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
48
+ * migration for formatted text (bold/italic/underline/links). Paginates to
49
+ * the end — the Fimo site alone carries 147 entries against a 20-row API
50
+ * default page.
51
+ */
52
+ export declare function loadWrappedRichText(options?: FimoServerOptions): Promise<Record<string, WrappedRichTextEntry>>;
53
+ /**
54
+ * Overlay a wrap-migrated collection onto its retained source rows: fetch the
55
+ * Fimo entries matching the source rows' keys, merge field-by-field with the
56
+ * type-matched rule above, and return plain data rows in SOURCE order. The
57
+ * page keeps its original render logic and its original fallback — edits made
58
+ * in Fimo show up, everything else stays byte-identical.
59
+ */
60
+ export declare function wrapCollection<T extends Record<string, unknown>>(typeName: string, sourceRows: readonly T[], options?: FimoServerOptions & {
61
+ keyField?: string;
62
+ }): Promise<T[]>;
63
+ /**
64
+ * Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
65
+ * i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
66
+ * labels (flat dot-keys); this fetches them back and un-flattens them into the
67
+ * nested shape the app's own i18n loader expects — so the app keeps its
68
+ * `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
69
+ *
70
+ * Deep-merge over the shipped messages (pass them as `fallback`) so any key
71
+ * not yet in Fimo falls back to the static file instead of showing a missing
72
+ * key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
73
+ *
74
+ * const messages = await loadFimoMessages(locale, { fallback: staticMessages });
75
+ * return { locale, messages };
76
+ */
77
+ export declare function loadFimoMessages(locale: string, options?: FimoServerOptions & {
78
+ fallback?: Record<string, unknown>;
79
+ }): Promise<Record<string, unknown>>;
80
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,207 @@
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
+ import { fetchAllWrappedRows, mapWrappedMedia, mapWrappedRichText } from './content-fetch.pure.js';
9
+ export function loadFimoConfig(rootDir = process.cwd()) {
10
+ return getConfigServer(rootDir);
11
+ }
12
+ export async function loadFimoLabels(options = {}) {
13
+ const rootDir = options.rootDir ?? process.cwd();
14
+ const bundle = await loadLabelBundle(rootDir);
15
+ return { ...bundle, signature: labelSignature(bundle) };
16
+ }
17
+ /**
18
+ * Server-component label accessor: `useLabels()` is client-only, and forcing
19
+ * a client wrapper for one aria-label in a server component is not a
20
+ * migration, it is collateral damage. Load once per request scope and read
21
+ * synchronously: `const t = await loadFimoT(); <a aria-label={t('footer.github', 'GitHub')} …`.
22
+ */
23
+ export async function loadFimoT(options = {}) {
24
+ const labels = await loadFimoLabels(options);
25
+ const locale = options.locale ?? labels.defaultLocale;
26
+ const dictionary = labels.dictionaries[locale] ?? labels.dictionaries[labels.defaultLocale] ?? {};
27
+ return (key, fallback) => {
28
+ const value = dictionary[key];
29
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
30
+ };
31
+ }
32
+ async function resolveApiUrl(options) {
33
+ if (options.apiUrl) {
34
+ return options.apiUrl;
35
+ }
36
+ const env = await readProjectEnv(options.rootDir ?? process.cwd());
37
+ return resolveTenantApiUrl(env);
38
+ }
39
+ /**
40
+ * Fetch published entries of a content type from the public tenant API.
41
+ * Returns [] when the API is unreachable so the wrapped site keeps rendering
42
+ * its fallbacks.
43
+ */
44
+ export async function fetchEntries(typeName, options = {}) {
45
+ const apiUrl = await resolveApiUrl(options);
46
+ if (!apiUrl) {
47
+ return [];
48
+ }
49
+ try {
50
+ const url = new URL(`/entries/${typeName}`, apiUrl);
51
+ if (options.where) {
52
+ url.searchParams.set('where', JSON.stringify(options.where));
53
+ }
54
+ if (options.limit) {
55
+ url.searchParams.set('limit', String(options.limit));
56
+ }
57
+ const response = await fetchWithTimeout(url, 4000);
58
+ if (!response.ok) {
59
+ return [];
60
+ }
61
+ const body = (await response.json());
62
+ const rows = Array.isArray(body.data) ? body.data : [];
63
+ const entries = rows
64
+ .filter((row) => Boolean(row) && typeof row === 'object')
65
+ .map((row) => ({
66
+ id: String(row.id ?? row.documentId ?? ''),
67
+ data: (row.data && typeof row.data === 'object' ? row.data : row),
68
+ }))
69
+ .filter((row) => row.id.length > 0);
70
+ return (options.order ?? 'oldest') === 'oldest' ? entries.reverse() : entries;
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }
76
+ /**
77
+ * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
78
+ * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
79
+ * asset-typed `image` field. Paginates to the end — a site's wrapped media
80
+ * routinely exceeds one API page.
81
+ */
82
+ export async function loadWrappedMedia(options = {}) {
83
+ const apiUrl = await resolveApiUrl(options);
84
+ if (!apiUrl) {
85
+ return {};
86
+ }
87
+ return mapWrappedMedia(await fetchAllWrappedRows(apiUrl, 'WrappedMedia'));
88
+ }
89
+ /**
90
+ * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
91
+ * migration for formatted text (bold/italic/underline/links). Paginates to
92
+ * the end — the Fimo site alone carries 147 entries against a 20-row API
93
+ * default page.
94
+ */
95
+ export async function loadWrappedRichText(options = {}) {
96
+ const apiUrl = await resolveApiUrl(options);
97
+ if (!apiUrl) {
98
+ return {};
99
+ }
100
+ return mapWrappedRichText(await fetchAllWrappedRows(apiUrl, 'WrappedRichText'));
101
+ }
102
+ /** Key fields tried, in order, to align a source row with its Fimo entry. */
103
+ const COLLECTION_KEY_FIELDS = ['slug', 'id', 'key', 'name', 'title', 'question'];
104
+ /** Fimo entries fetched per collection: covers rendered windows and small
105
+ * collections entirely; rows beyond it keep rendering their source values. */
106
+ const COLLECTION_FETCH_CAP = 100;
107
+ function collectionKeyField(rows) {
108
+ for (const field of COLLECTION_KEY_FIELDS) {
109
+ if (rows.every((row) => typeof row[field] === 'string' && row[field].length > 0)) {
110
+ return field;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /** A Fimo value replaces its source value only when the SHAPES agree, so a
116
+ * page that rendered strings keeps rendering strings: media envelopes,
117
+ * richtext docs, reference handles, and nulls all fall back to the source
118
+ * value instead of leaking objects into JSX. */
119
+ function preferTypeMatched(fimoValue, sourceValue) {
120
+ if (fimoValue === null || fimoValue === undefined) {
121
+ return sourceValue;
122
+ }
123
+ if (Array.isArray(sourceValue)) {
124
+ if (!Array.isArray(fimoValue)) {
125
+ return sourceValue;
126
+ }
127
+ return fimoValue.every((element) => typeof element === typeof (sourceValue[0] ?? '') && typeof element !== 'object')
128
+ ? fimoValue
129
+ : sourceValue;
130
+ }
131
+ return typeof fimoValue === typeof sourceValue && typeof fimoValue !== 'object' ? fimoValue : sourceValue;
132
+ }
133
+ /**
134
+ * Overlay a wrap-migrated collection onto its retained source rows: fetch the
135
+ * Fimo entries matching the source rows' keys, merge field-by-field with the
136
+ * type-matched rule above, and return plain data rows in SOURCE order. The
137
+ * page keeps its original render logic and its original fallback — edits made
138
+ * in Fimo show up, everything else stays byte-identical.
139
+ */
140
+ export async function wrapCollection(typeName, sourceRows, options = {}) {
141
+ const rows = sourceRows;
142
+ if (rows.length === 0) {
143
+ return [];
144
+ }
145
+ const keyField = options.keyField ?? collectionKeyField(rows);
146
+ if (!keyField) {
147
+ return sourceRows;
148
+ }
149
+ const keys = rows.slice(0, COLLECTION_FETCH_CAP).map((row) => row[keyField]);
150
+ const entries = await fetchEntries(typeName, {
151
+ ...options,
152
+ where: { [keyField]: { $in: keys } },
153
+ limit: keys.length,
154
+ });
155
+ if (entries.length === 0) {
156
+ return sourceRows;
157
+ }
158
+ const byKey = new Map();
159
+ for (const entry of entries) {
160
+ const key = entry.data[keyField];
161
+ if (typeof key === 'string' && !byKey.has(key)) {
162
+ byKey.set(key, entry.data);
163
+ }
164
+ }
165
+ return rows.map((row) => {
166
+ const fimoRow = byKey.get(row[keyField]);
167
+ if (!fimoRow) {
168
+ return row;
169
+ }
170
+ const merged = { ...row };
171
+ for (const field of Object.keys(row)) {
172
+ merged[field] = preferTypeMatched(fimoRow[field], row[field]);
173
+ }
174
+ return merged;
175
+ });
176
+ }
177
+ /**
178
+ * Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
179
+ * i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
180
+ * labels (flat dot-keys); this fetches them back and un-flattens them into the
181
+ * nested shape the app's own i18n loader expects — so the app keeps its
182
+ * `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
183
+ *
184
+ * Deep-merge over the shipped messages (pass them as `fallback`) so any key
185
+ * not yet in Fimo falls back to the static file instead of showing a missing
186
+ * key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
187
+ *
188
+ * const messages = await loadFimoMessages(locale, { fallback: staticMessages });
189
+ * return { locale, messages };
190
+ */
191
+ export async function loadFimoMessages(locale, options = {}) {
192
+ const fallback = options.fallback ?? {};
193
+ const apiUrl = await resolveApiUrl(options);
194
+ if (!apiUrl) {
195
+ return fallback;
196
+ }
197
+ try {
198
+ const { labels } = await fetchLabelDictionary(apiUrl, locale);
199
+ if (Object.keys(labels).length === 0) {
200
+ return fallback;
201
+ }
202
+ return deepMergeMessages(fallback, unflattenMessages(labels));
203
+ }
204
+ catch {
205
+ return fallback;
206
+ }
207
+ }
@@ -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;
@@ -0,0 +1,137 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { decodeJsxEntities, KeyAllocator, readApplyExemptions, reusableStagedLabels, slugify, } from './apply-wrap.js';
4
+ import { scanWrappable } from './scan-wrappable.js';
5
+ /**
6
+ * Shared injection helper for SFC-style templates: first instance script,
7
+ * never a `<script module>` block (module scope cannot read page data), or a
8
+ * fresh script when the file has none.
9
+ */
10
+ export function injectIntoInstanceScript(code, head, scriptTag) {
11
+ const instance = code.match(/<script(?![^>]*\bmodule\b)[^>]*>\r?\n?/);
12
+ if (instance && instance.index !== undefined) {
13
+ const at = instance.index + instance[0].length;
14
+ return `${code.slice(0, at)}\t${head}\n${code.slice(at)}`;
15
+ }
16
+ return `${scriptTag}\n\t${head}\n</script>\n\n${code}`;
17
+ }
18
+ /**
19
+ * A file that already binds `t` (an i18n library, a custom helper) is agent
20
+ * work: the dogfood proved rewiring foreign t() calls breaks rendering.
21
+ */
22
+ function bindsForeignT(code) {
23
+ return (/\buseI18n\s*\(/.test(code) ||
24
+ /(?:const|let|var)\s+t\s*=/.test(code) ||
25
+ /import\s*\{[^}]*\bt\b[^}]*\}\s*from/.test(code));
26
+ }
27
+ function previewPattern(preview) {
28
+ const core = preview
29
+ .replace(/…$/, '')
30
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
31
+ .replace(/\\? /g, '\\s+');
32
+ return new RegExp(`>(\\s*)(${core}[^<{]*?)(\\s*)<`);
33
+ }
34
+ export function applyTemplateWrapToFile(code, style, items, allocator) {
35
+ const applied = [];
36
+ const skipped = [];
37
+ const labels = {};
38
+ const hasHead = style.headMarker.test(code);
39
+ if (!hasHead && bindsForeignT(code)) {
40
+ return {
41
+ code,
42
+ applied,
43
+ skipped: items.map((item) => ({ item, reason: 'file binds its own t() — agent work' })),
44
+ labels,
45
+ };
46
+ }
47
+ if (!hasHead && style.injectHead(code, style.head) === null) {
48
+ return {
49
+ code,
50
+ applied,
51
+ skipped: items.map((item) => ({ item, reason: 'no safe wiring point in this file — agent work' })),
52
+ labels,
53
+ };
54
+ }
55
+ const file = items[0]?.file ?? '';
56
+ const scope = style.fileScope?.(file) ?? slugify(basename(file).replace(/\.[a-z0-9]+$/i, '')) ?? 'shared';
57
+ let next = code;
58
+ for (const item of items) {
59
+ if (item.kind !== 'text') {
60
+ skipped.push({ item, reason: `${item.kind} stays agent work` });
61
+ continue;
62
+ }
63
+ const match = next.match(previewPattern(item.preview));
64
+ if (!match || match.index === undefined) {
65
+ skipped.push({ item, reason: 'text node not matched in source — agent work' });
66
+ continue;
67
+ }
68
+ const collapsed = match[2].replace(/\s+/g, ' ').trim();
69
+ const value = decodeJsxEntities(collapsed);
70
+ const id = allocator.allocate(scope || 'shared', slugify(value), value);
71
+ const escaped = collapsed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
72
+ const replacement = `>${match[1]}${style.open}"${id}", "${escaped}"${style.close}${match[3]}<`;
73
+ next = next.slice(0, match.index) + replacement + next.slice(match.index + match[0].length);
74
+ labels[id] = value;
75
+ applied.push({ kind: 'text', file: item.file, line: item.line, id, preview: item.preview });
76
+ }
77
+ if (applied.length > 0 && !style.headMarker.test(next)) {
78
+ next = style.injectHead(next, style.head) ?? next;
79
+ }
80
+ return { code: next, applied, skipped, labels };
81
+ }
82
+ /**
83
+ * Apply the template wraps across a project — same contract as applyWrap:
84
+ * scans, mutates source files, returns the generated labels for staging and
85
+ * seeding. The style's ensureWiring hook then writes any one-time project
86
+ * wiring (e.g. a root layout labels loader) when files were wrapped.
87
+ */
88
+ export function applyTemplateWrap(rootDir, opts) {
89
+ const { style } = opts;
90
+ const scan = scanWrappable(rootDir, { excludePrefixes: opts.excludePrefixes });
91
+ const existingLabels = (() => {
92
+ try {
93
+ return JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'labels.json'), 'utf8'));
94
+ }
95
+ catch {
96
+ return {};
97
+ }
98
+ })();
99
+ // Honor recorded exemptions — same contract as the JSX codemod: --apply
100
+ // must never wrap an item the user deliberately kept out.
101
+ const exemptions = readApplyExemptions(rootDir);
102
+ const isExempt = (item) => exemptions.some((exemption) => exemption.file === item.file && exemption.kind === item.kind && exemption.preview === item.preview);
103
+ const allocator = new KeyAllocator([...Object.keys(existingLabels), ...scan.wrappedIds.labels], reusableStagedLabels(existingLabels, scan.wrappedIds.labels));
104
+ const result = { applied: [], skipped: [], labels: {}, filesChanged: [] };
105
+ const byFile = new Map();
106
+ for (const item of scan.items) {
107
+ if (!item.file.endsWith(style.ext) || isExempt(item)) {
108
+ continue;
109
+ }
110
+ const list = byFile.get(item.file) ?? [];
111
+ list.push(item);
112
+ byFile.set(item.file, list);
113
+ }
114
+ for (const [file, items] of byFile) {
115
+ const filePath = join(rootDir, file);
116
+ let code;
117
+ try {
118
+ code = readFileSync(filePath, 'utf8');
119
+ }
120
+ catch {
121
+ result.skipped.push(...items.map((item) => ({ item, reason: 'unreadable file' })));
122
+ continue;
123
+ }
124
+ const fileResult = applyTemplateWrapToFile(code, style, items, allocator);
125
+ result.applied.push(...fileResult.applied);
126
+ result.skipped.push(...fileResult.skipped);
127
+ Object.assign(result.labels, fileResult.labels);
128
+ if (fileResult.applied.length > 0) {
129
+ writeFileSync(filePath, fileResult.code);
130
+ result.filesChanged.push(file);
131
+ }
132
+ }
133
+ if (result.filesChanged.length > 0 && style.ensureWiring) {
134
+ result.filesChanged.push(...style.ensureWiring(rootDir));
135
+ }
136
+ return result;
137
+ }