fimo 0.3.1 → 0.3.2-experimental.1783328062928

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 (130) hide show
  1. package/README.md +6 -12
  2. package/assets/agents-starter/AGENTS.md +14 -2
  3. package/assets/skills/fimo/SKILL.md +1 -1
  4. package/assets/skills/fimo/references/content.md +14 -2
  5. package/assets/skills/fimo/references/setup-plain-vite.md +8 -3
  6. package/assets/skills/fimo/references/setup-react-router.md +13 -6
  7. package/assets/skills/fimo/references/ui.md +1 -1
  8. package/assets/skills/fimo-cli/SKILL.md +30 -18
  9. package/assets/skills/fimo-cli/references/account.md +4 -2
  10. package/assets/skills/fimo-cli/references/agent-setup.md +14 -16
  11. package/assets/skills/fimo-cli/references/agents.md +2 -0
  12. package/assets/skills/fimo-cli/references/analytics.md +2 -0
  13. package/assets/skills/fimo-cli/references/assets.md +2 -0
  14. package/assets/skills/fimo-cli/references/branches.md +2 -0
  15. package/assets/skills/fimo-cli/references/content.md +3 -1
  16. package/assets/skills/fimo-cli/references/credits.md +2 -0
  17. package/assets/skills/fimo-cli/references/deploy.md +10 -4
  18. package/assets/skills/fimo-cli/references/describe.md +2 -0
  19. package/assets/skills/fimo-cli/references/domains.md +2 -0
  20. package/assets/skills/fimo-cli/references/forms.md +2 -0
  21. package/assets/skills/fimo-cli/references/init.md +114 -0
  22. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
  23. package/assets/skills/fimo-cli/references/integrations.md +2 -0
  24. package/assets/skills/fimo-cli/references/invite.md +2 -0
  25. package/assets/skills/fimo-cli/references/labels.md +2 -0
  26. package/assets/skills/fimo-cli/references/locales.md +2 -0
  27. package/assets/skills/fimo-cli/references/media.md +2 -0
  28. package/assets/skills/fimo-cli/references/preview.md +2 -0
  29. package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +3 -1
  30. package/assets/skills/fimo-cli/references/projects.md +5 -3
  31. package/assets/skills/fimo-cli/references/schedules.md +2 -0
  32. package/assets/skills/fimo-migration/SKILL.md +16 -1
  33. package/assets/skills/fimo-migration/references/manifest.md +40 -1
  34. package/assets/skills/fimo-migration/references/qa.md +21 -2
  35. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  36. package/assets/skills/fimo-migration/references/wrap.md +155 -0
  37. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  38. package/dist/build/vite/index.d.ts.map +1 -1
  39. package/dist/build/vite/index.js +8 -6
  40. package/dist/build/vite/plugins/translations.d.ts +2 -8
  41. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  42. package/dist/build/vite/plugins/translations.js +3 -121
  43. package/dist/cli/bundle.json +2 -2
  44. package/dist/cli/index.js +4547 -3255
  45. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  46. package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
  47. package/dist/runtime/app/FimoProviders.js +2 -23
  48. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
  49. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  50. package/dist/runtime/next/FimoFileLink.d.ts +17 -0
  51. package/dist/runtime/next/FimoFileLink.d.ts.map +1 -0
  52. package/dist/runtime/next/FimoFileLink.js +19 -0
  53. package/dist/runtime/next/FimoMediaImage.d.ts +14 -0
  54. package/dist/runtime/next/FimoMediaImage.d.ts.map +1 -0
  55. package/dist/runtime/next/FimoMediaImage.js +20 -0
  56. package/dist/runtime/next/FimoMediaVideo.d.ts +14 -0
  57. package/dist/runtime/next/FimoMediaVideo.d.ts.map +1 -0
  58. package/dist/runtime/next/FimoMediaVideo.js +20 -0
  59. package/dist/runtime/next/FimoNextProviders.d.ts +30 -0
  60. package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
  61. package/dist/runtime/next/FimoNextProviders.js +106 -0
  62. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  63. package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
  64. package/dist/runtime/next/FimoNextScripts.js +19 -0
  65. package/dist/runtime/next/FimoRichTextBlock.d.ts +23 -0
  66. package/dist/runtime/next/FimoRichTextBlock.d.ts.map +1 -0
  67. package/dist/runtime/next/FimoRichTextBlock.js +22 -0
  68. package/dist/runtime/next/FimoText.d.ts +22 -0
  69. package/dist/runtime/next/FimoText.d.ts.map +1 -0
  70. package/dist/runtime/next/FimoText.js +20 -0
  71. package/dist/runtime/next/WrappedContentContext.d.ts +28 -0
  72. package/dist/runtime/next/WrappedContentContext.d.ts.map +1 -0
  73. package/dist/runtime/next/WrappedContentContext.js +30 -0
  74. package/dist/runtime/next/index.d.ts +14 -0
  75. package/dist/runtime/next/index.d.ts.map +1 -0
  76. package/dist/runtime/next/index.js +14 -0
  77. package/dist/runtime/next/server.d.ts +33 -0
  78. package/dist/runtime/next/server.d.ts.map +1 -0
  79. package/dist/runtime/next/server.js +107 -0
  80. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  81. package/dist/runtime/primitives/labels-hook.d.ts.map +1 -0
  82. package/dist/runtime/primitives/labels-hook.js +41 -0
  83. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  84. package/dist/runtime/primitives/labels-store.d.ts.map +1 -0
  85. package/dist/runtime/primitives/labels-store.js +60 -0
  86. package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
  87. package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
  88. package/dist/runtime/primitives/labels-store.test.js +66 -0
  89. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  90. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
  91. package/dist/runtime/primitives/lib/useFimoLiveValue.js +8 -4
  92. package/dist/runtime/primitives/translations.d.ts +1 -27
  93. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  94. package/dist/runtime/primitives/translations.js +7 -85
  95. package/dist/runtime/react/index.d.ts +24 -0
  96. package/dist/runtime/react/index.d.ts.map +1 -0
  97. package/dist/runtime/react/index.js +28 -0
  98. package/dist/runtime/shared/display-routes.d.ts +14 -0
  99. package/dist/runtime/shared/display-routes.d.ts.map +1 -0
  100. package/dist/runtime/shared/display-routes.js +25 -0
  101. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  102. package/dist/runtime/shared/fimo-config.server.js +1 -0
  103. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  104. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  105. package/dist/runtime/shared/labels-fetch.server.d.ts +14 -0
  106. package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
  107. package/dist/runtime/shared/labels-fetch.server.js +145 -0
  108. package/dist/scripts/detect-framework.d.ts +12 -0
  109. package/dist/scripts/detect-framework.js +32 -0
  110. package/dist/scripts/detect-framework.test.d.ts +1 -0
  111. package/dist/scripts/detect-framework.test.js +39 -0
  112. package/dist/scripts/lib/discover-next-routes.d.ts +11 -0
  113. package/dist/scripts/lib/discover-next-routes.js +76 -0
  114. package/dist/scripts/lib/discover-next-routes.test.d.ts +1 -0
  115. package/dist/scripts/lib/discover-next-routes.test.js +40 -0
  116. package/dist/scripts/scan-wrappable.d.ts +29 -0
  117. package/dist/scripts/scan-wrappable.js +191 -0
  118. package/dist/scripts/scan-wrappable.test.d.ts +1 -0
  119. package/dist/scripts/scan-wrappable.test.js +48 -0
  120. package/dist/scripts/validate-route-metadata.js +11 -0
  121. package/dist/scripts/validate-schemas.d.ts +13 -0
  122. package/dist/scripts/validate-schemas.js +113 -0
  123. package/dist/scripts/validate-schemas.test.d.ts +1 -0
  124. package/dist/scripts/validate-schemas.test.js +132 -0
  125. package/package.json +29 -1
  126. package/release.json +2 -2
  127. package/scripts/postinstall.mjs +10 -54
  128. package/templates/react-router/package.json +1 -1
  129. package/assets/agents-starter/claude/settings.json +0 -19
  130. package/assets/skills/fimo-cli/references/create.md +0 -90
@@ -0,0 +1,107 @@
1
+ // fimo/next/server — server-only loaders for wrapped Next.js apps.
2
+ // Content resolves at request time from the public tenant API (no Vite
3
+ // virtual modules, no build plugin): call these in the root layout / RSC
4
+ // pages and pass the results to <FimoNextProviders>.
5
+ import { getConfigServer } from '../shared/fimo-config.server.js';
6
+ import { fetchWithTimeout, labelSignature, loadLabelBundle, readProjectEnv, resolveTenantApiUrl, } from '../shared/labels-fetch.server.js';
7
+ export function loadFimoConfig(rootDir = process.cwd()) {
8
+ return getConfigServer(rootDir);
9
+ }
10
+ export async function loadFimoLabels(options = {}) {
11
+ const rootDir = options.rootDir ?? process.cwd();
12
+ const bundle = await loadLabelBundle(rootDir);
13
+ return { ...bundle, signature: labelSignature(bundle) };
14
+ }
15
+ async function resolveApiUrl(options) {
16
+ if (options.apiUrl) {
17
+ return options.apiUrl;
18
+ }
19
+ const env = await readProjectEnv(options.rootDir ?? process.cwd());
20
+ return resolveTenantApiUrl(env);
21
+ }
22
+ /**
23
+ * Fetch published entries of a content type from the public tenant API.
24
+ * Returns [] when the API is unreachable so the wrapped site keeps rendering
25
+ * its fallbacks.
26
+ */
27
+ export async function fetchEntries(typeName, options = {}) {
28
+ const apiUrl = await resolveApiUrl(options);
29
+ if (!apiUrl) {
30
+ return [];
31
+ }
32
+ try {
33
+ const response = await fetchWithTimeout(new URL(`/entries/${typeName}`, apiUrl), 4000);
34
+ if (!response.ok) {
35
+ return [];
36
+ }
37
+ const body = (await response.json());
38
+ const rows = Array.isArray(body.data) ? body.data : [];
39
+ return rows
40
+ .filter((row) => Boolean(row) && typeof row === 'object')
41
+ .map((row) => ({
42
+ id: String(row.id ?? row.documentId ?? ''),
43
+ data: (row.data && typeof row.data === 'object' ? row.data : row),
44
+ }))
45
+ .filter((row) => row.id.length > 0);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ function readString(value) {
52
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
53
+ }
54
+ function readNumber(value) {
55
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
56
+ }
57
+ /**
58
+ * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
59
+ * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
60
+ * asset-typed `image` field.
61
+ */
62
+ export async function loadWrappedMedia(options = {}) {
63
+ const entries = await fetchEntries('WrappedMedia', options);
64
+ const map = {};
65
+ for (const entry of entries) {
66
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
67
+ const asset = entry.data.image;
68
+ if (!key || !asset || typeof asset !== 'object') {
69
+ continue;
70
+ }
71
+ const assetRecord = asset;
72
+ const url = readString(assetRecord.url);
73
+ if (!url) {
74
+ continue;
75
+ }
76
+ map[key] = {
77
+ url,
78
+ alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
79
+ name: readString(assetRecord.name),
80
+ mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
81
+ width: readNumber(assetRecord.width),
82
+ height: readNumber(assetRecord.height),
83
+ source: `WrappedMedia.${entry.id}.image`,
84
+ };
85
+ }
86
+ return map;
87
+ }
88
+ /**
89
+ * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
90
+ * migration for formatted text (bold/italic/underline/links).
91
+ */
92
+ export async function loadWrappedRichText(options = {}) {
93
+ const entries = await fetchEntries('WrappedRichText', options);
94
+ const map = {};
95
+ for (const entry of entries) {
96
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
97
+ const body = entry.data.body;
98
+ if (!key || !body || typeof body !== 'object') {
99
+ continue;
100
+ }
101
+ map[key] = {
102
+ content: body,
103
+ source: `WrappedRichText.${entry.id}.body`,
104
+ };
105
+ }
106
+ return map;
107
+ }
@@ -0,0 +1,33 @@
1
+ import { FimoString } from './lib/index.js';
2
+ /**
3
+ * Hook to provide a label function (`t`).
4
+ *
5
+ * Returns FimoString with source tracking for inline editing.
6
+ * Use with <Text> component for rendering, or String() for escape hatch.
7
+ *
8
+ * The optional `fallback` renders when the key has no value in the store —
9
+ * used by wrapped (migrated) projects where the original hardcoded string
10
+ * stays in code as a resilience fallback. Template projects keep the
11
+ * single-argument form (labels are DB-owned there).
12
+ *
13
+ * @example
14
+ * const { t } = useLabels();
15
+ *
16
+ * // With Text component (recommended)
17
+ * <Text value={t('welcome')} />
18
+ *
19
+ * // Escape hatch for attributes
20
+ * <input placeholder={String(t('email.placeholder'))} />
21
+ */
22
+ export declare function useLabels(): {
23
+ t: (fullKey: string, fallback?: string) => FimoString;
24
+ locale: string;
25
+ };
26
+ /**
27
+ * Legacy alias for older projects. Prefer useLabels() in new code.
28
+ */
29
+ export declare function useTranslations(): {
30
+ t: (fullKey: string, fallback?: string) => FimoString;
31
+ locale: string;
32
+ };
33
+ //# sourceMappingURL=labels-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels-hook.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/labels-hook.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS;iBAIH,MAAM,aAAa,MAAM,KAAG,UAAU;;EAS3D;AAED;;GAEG;AACH,wBAAgB,eAAe;iBAdT,MAAM,aAAa,MAAM,KAAG,UAAU;;EAgB3D"}
@@ -0,0 +1,41 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { useLocale } from '../i18n/FimoLocaleContext.js';
3
+ import { getLabelSnapshot, subscribeLabels } from './labels-store.js';
4
+ import { FimoString } from './lib/index.js';
5
+ /**
6
+ * Hook to provide a label function (`t`).
7
+ *
8
+ * Returns FimoString with source tracking for inline editing.
9
+ * Use with <Text> component for rendering, or String() for escape hatch.
10
+ *
11
+ * The optional `fallback` renders when the key has no value in the store —
12
+ * used by wrapped (migrated) projects where the original hardcoded string
13
+ * stays in code as a resilience fallback. Template projects keep the
14
+ * single-argument form (labels are DB-owned there).
15
+ *
16
+ * @example
17
+ * const { t } = useLabels();
18
+ *
19
+ * // With Text component (recommended)
20
+ * <Text value={t('welcome')} />
21
+ *
22
+ * // Escape hatch for attributes
23
+ * <input placeholder={String(t('email.placeholder'))} />
24
+ */
25
+ export function useLabels() {
26
+ const localeContext = useLocale();
27
+ const labels = useSyncExternalStore(subscribeLabels, getLabelSnapshot, getLabelSnapshot);
28
+ const t = (fullKey, fallback) => {
29
+ const locale = localeContext.locale || labels.defaultLocale || 'en';
30
+ const dictionary = labels.dictionaries[locale] ?? labels.dictionaries[labels.defaultLocale] ?? {};
31
+ const text = dictionary[fullKey] ?? fallback ?? '';
32
+ return new FimoString(text, `__labels.${locale}.${fullKey}.value`);
33
+ };
34
+ return { t, locale: localeContext.locale };
35
+ }
36
+ /**
37
+ * Legacy alias for older projects. Prefer useLabels() in new code.
38
+ */
39
+ export function useTranslations() {
40
+ return useLabels();
41
+ }
@@ -0,0 +1,12 @@
1
+ export interface LabelState {
2
+ dictionaries: Record<string, Record<string, string>>;
3
+ defaultLocale: string;
4
+ signature: string;
5
+ updatedAt: string | null;
6
+ }
7
+ export declare function subscribeLabels(listener: () => void): () => void;
8
+ export declare function getLabelSnapshot(): LabelState;
9
+ export declare function setLabelState(next: LabelState): void;
10
+ export declare function hydrateLabels(bundle: LabelState): void;
11
+ export declare function applyLabelUpdate(payload: unknown): void;
12
+ //# sourceMappingURL=labels-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels-store.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/labels-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAUD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAKhE;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAE7C;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAQpD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAEtD;AAuBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgBvD"}
@@ -0,0 +1,60 @@
1
+ // Framework-neutral label store. No Vite virtual modules, no HMR, no React:
2
+ // adapters (fimo/ui via ./translations.ts, fimo/next, ...) hydrate it with
3
+ // their own label source and the hook layer subscribes to it.
4
+ const labelListeners = new Set();
5
+ let labelState = {
6
+ dictionaries: {},
7
+ defaultLocale: 'en',
8
+ signature: '__fimo-labels-uninitialized__',
9
+ updatedAt: null,
10
+ };
11
+ export function subscribeLabels(listener) {
12
+ labelListeners.add(listener);
13
+ return () => {
14
+ labelListeners.delete(listener);
15
+ };
16
+ }
17
+ export function getLabelSnapshot() {
18
+ return labelState;
19
+ }
20
+ export function setLabelState(next) {
21
+ if (next.signature === labelState.signature) {
22
+ return;
23
+ }
24
+ labelState = next;
25
+ for (const listener of labelListeners) {
26
+ listener();
27
+ }
28
+ }
29
+ export function hydrateLabels(bundle) {
30
+ setLabelState(bundle);
31
+ }
32
+ function isRecord(value) {
33
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+ function normalizeDictionaries(value) {
36
+ if (!isRecord(value)) {
37
+ return null;
38
+ }
39
+ const dictionaries = {};
40
+ for (const [locale, labels] of Object.entries(value)) {
41
+ if (!isRecord(labels)) {
42
+ continue;
43
+ }
44
+ dictionaries[locale] = Object.fromEntries(Object.entries(labels).filter((entry) => typeof entry[1] === 'string'));
45
+ }
46
+ return dictionaries;
47
+ }
48
+ export function applyLabelUpdate(payload) {
49
+ if (!isRecord(payload)) {
50
+ return;
51
+ }
52
+ const dictionaries = normalizeDictionaries(payload.dictionaries);
53
+ if (!dictionaries) {
54
+ return;
55
+ }
56
+ const defaultLocale = typeof payload.defaultLocale === 'string' ? payload.defaultLocale : labelState.defaultLocale;
57
+ const signature = typeof payload.signature === 'string' ? payload.signature : JSON.stringify({ dictionaries, defaultLocale });
58
+ const updatedAt = typeof payload.updatedAt === 'string' ? payload.updatedAt : null;
59
+ setLabelState({ dictionaries, defaultLocale, signature, updatedAt });
60
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=labels-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels-store.test.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/labels-store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { applyLabelUpdate, getLabelSnapshot, hydrateLabels, subscribeLabels } from './labels-store.js';
3
+ describe('labels-store', () => {
4
+ it('hydrates the store and notifies subscribers', () => {
5
+ const listener = vi.fn();
6
+ const unsubscribe = subscribeLabels(listener);
7
+ hydrateLabels({
8
+ dictionaries: { en: { 'hero.title': 'Welcome' } },
9
+ defaultLocale: 'en',
10
+ signature: 'sig-hydrate-1',
11
+ updatedAt: null,
12
+ });
13
+ expect(getLabelSnapshot().dictionaries.en['hero.title']).toBe('Welcome');
14
+ expect(listener).toHaveBeenCalledTimes(1);
15
+ unsubscribe();
16
+ });
17
+ it('ignores updates with an unchanged signature', () => {
18
+ const listener = vi.fn();
19
+ const unsubscribe = subscribeLabels(listener);
20
+ hydrateLabels({
21
+ dictionaries: { en: { a: '1' } },
22
+ defaultLocale: 'en',
23
+ signature: 'sig-same',
24
+ updatedAt: null,
25
+ });
26
+ hydrateLabels({
27
+ dictionaries: { en: { a: '2' } },
28
+ defaultLocale: 'en',
29
+ signature: 'sig-same',
30
+ updatedAt: null,
31
+ });
32
+ expect(getLabelSnapshot().dictionaries.en.a).toBe('1');
33
+ expect(listener).toHaveBeenCalledTimes(1);
34
+ unsubscribe();
35
+ });
36
+ it('normalizes payloads in applyLabelUpdate and drops non-string values', () => {
37
+ applyLabelUpdate({
38
+ dictionaries: { en: { keep: 'yes', drop: 42 }, fr: 'not-a-record' },
39
+ defaultLocale: 'en',
40
+ signature: 'sig-normalize',
41
+ });
42
+ const snapshot = getLabelSnapshot();
43
+ expect(snapshot.dictionaries.en).toEqual({ keep: 'yes' });
44
+ expect(snapshot.dictionaries.fr).toBeUndefined();
45
+ expect(snapshot.updatedAt).toBeNull();
46
+ });
47
+ it('rejects payloads without dictionaries', () => {
48
+ const before = getLabelSnapshot();
49
+ applyLabelUpdate({ defaultLocale: 'de' });
50
+ applyLabelUpdate(null);
51
+ applyLabelUpdate('nope');
52
+ expect(getLabelSnapshot()).toBe(before);
53
+ });
54
+ it('stops notifying after unsubscribe', () => {
55
+ const listener = vi.fn();
56
+ const unsubscribe = subscribeLabels(listener);
57
+ unsubscribe();
58
+ hydrateLabels({
59
+ dictionaries: { en: {} },
60
+ defaultLocale: 'en',
61
+ signature: 'sig-after-unsub',
62
+ updatedAt: null,
63
+ });
64
+ expect(listener).not.toHaveBeenCalled();
65
+ });
66
+ });
@@ -9,9 +9,11 @@ import { type RefObject } from 'react';
9
9
  * preview knows to dispatch events instead of falling back to raw DOM writes,
10
10
  * and updates internal state when a mutation arrives.
11
11
  *
12
- * In prod (published app): Vite statically inlines `import.meta.env.DEV`, so
13
- * `useFimoLiveValueProd` is the only implementation linked and the listener
14
- * machinery is dead-code-eliminated.
12
+ * In prod (published app): only `useFimoLiveValueProd` is linked. The dev
13
+ * check must work under any bundler (Vite, webpack, Turbopack) and plain
14
+ * Node SSR: `import.meta.env.DEV` only exists under Vite, and
15
+ * `process.env.NODE_ENV` is statically replaced by Next/webpack — checking
16
+ * both keeps the hook correct across adapters.
15
17
  */
16
18
  declare function useFimoLiveValueDev<T, E extends HTMLElement = HTMLElement>(external: T): {
17
19
  value: T;
@@ -1 +1 @@
1
- {"version":3,"file":"useFimoLiveValue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/useFimoLiveValue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AAEH,iBAAS,mBAAmB,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,GAAG,WAAW,EACjE,QAAQ,EAAE,CAAC,GACV;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CAAE,CA8BxC;AASD,eAAO,MAAM,gBAAgB,EAExB,OAAO,mBAAmB,CAAC"}
1
+ {"version":3,"file":"useFimoLiveValue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/useFimoLiveValue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAEpE;;;;;;;;;;;;;;;GAeG;AAEH,iBAAS,mBAAmB,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,GAAG,WAAW,EACjE,QAAQ,EAAE,CAAC,GACV;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CAAE,CA8BxC;AAaD,eAAO,MAAM,gBAAgB,EAExB,OAAO,mBAAmB,CAAC"}
@@ -10,9 +10,11 @@ import { useEffect, useRef, useState } from 'react';
10
10
  * preview knows to dispatch events instead of falling back to raw DOM writes,
11
11
  * and updates internal state when a mutation arrives.
12
12
  *
13
- * In prod (published app): Vite statically inlines `import.meta.env.DEV`, so
14
- * `useFimoLiveValueProd` is the only implementation linked and the listener
15
- * machinery is dead-code-eliminated.
13
+ * In prod (published app): only `useFimoLiveValueProd` is linked. The dev
14
+ * check must work under any bundler (Vite, webpack, Turbopack) and plain
15
+ * Node SSR: `import.meta.env.DEV` only exists under Vite, and
16
+ * `process.env.NODE_ENV` is statically replaced by Next/webpack — checking
17
+ * both keeps the hook correct across adapters.
16
18
  */
17
19
  function useFimoLiveValueDev(external) {
18
20
  const ref = useRef(null);
@@ -46,4 +48,6 @@ function useFimoLiveValueProd(external) {
46
48
  const ref = useRef(null);
47
49
  return { value: external, ref };
48
50
  }
49
- export const useFimoLiveValue = (import.meta.env.DEV ? useFimoLiveValueDev : useFimoLiveValueProd);
51
+ const isDevRuntime = import.meta.env?.DEV === true ||
52
+ (typeof process !== 'undefined' && process.env.NODE_ENV === 'development');
53
+ export const useFimoLiveValue = (isDevRuntime ? useFimoLiveValueDev : useFimoLiveValueProd);
@@ -1,28 +1,2 @@
1
- import { FimoString } from './lib/index.js';
2
- /**
3
- * Hook to provide a label function (`t`).
4
- *
5
- * Returns FimoString with source tracking for inline editing.
6
- * Use with <Text> component for rendering, or String() for escape hatch.
7
- *
8
- * @example
9
- * const { t } = useLabels();
10
- *
11
- * // With Text component (recommended)
12
- * <Text value={t('welcome')} />
13
- *
14
- * // Escape hatch for attributes
15
- * <input placeholder={String(t('email.placeholder'))} />
16
- */
17
- export declare function useLabels(): {
18
- t: (fullKey: string) => FimoString;
19
- locale: string;
20
- };
21
- /**
22
- * Legacy alias for older projects. Prefer useLabels() in new code.
23
- */
24
- export declare function useTranslations(): {
25
- t: (fullKey: string) => FimoString;
26
- locale: string;
27
- };
1
+ export { useLabels, useTranslations } from './labels-hook.js';
28
2
  //# sourceMappingURL=translations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/translations.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA2F5C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS;iBAIH,MAAM,KAAG,UAAU;;EASxC;AAED;;GAEG;AACH,wBAAgB,eAAe;iBAdT,MAAM,KAAG,UAAU;;EAgBxC"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/translations.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,61 +1,14 @@
1
- import { useSyncExternalStore } from 'react';
1
+ // Vite flavor of the label layer: hydrates the framework-neutral store from
2
+ // the `virtual:translations` module (fimo/vite labels plugin) and wires HMR.
3
+ // Framework-agnostic consumers use ./labels-store.ts + ./labels-hook.ts.
2
4
  import { defaultLocale as bundledDefaultLocale, dictionaries as bundledDictionaries, signature as translationSignature, updatedAt as bundledUpdatedAt, } from 'virtual:translations';
3
- import { useLocale } from '../i18n/FimoLocaleContext.js';
4
- import { FimoString } from './lib/index.js';
5
- const labelListeners = new Set();
6
- let labelState = {
5
+ import { applyLabelUpdate, hydrateLabels } from './labels-store.js';
6
+ hydrateLabels({
7
7
  dictionaries: bundledDictionaries ?? {},
8
8
  defaultLocale: bundledDefaultLocale || 'en',
9
9
  signature: translationSignature,
10
10
  updatedAt: bundledUpdatedAt ?? null,
11
- };
12
- function subscribeLabels(listener) {
13
- labelListeners.add(listener);
14
- return () => {
15
- labelListeners.delete(listener);
16
- };
17
- }
18
- function getLabelSnapshot() {
19
- return labelState;
20
- }
21
- function setLabelState(next) {
22
- if (next.signature === labelState.signature) {
23
- return;
24
- }
25
- labelState = next;
26
- for (const listener of labelListeners) {
27
- listener();
28
- }
29
- }
30
- function isRecord(value) {
31
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
32
- }
33
- function normalizeDictionaries(value) {
34
- if (!isRecord(value)) {
35
- return null;
36
- }
37
- const dictionaries = {};
38
- for (const [locale, labels] of Object.entries(value)) {
39
- if (!isRecord(labels)) {
40
- continue;
41
- }
42
- dictionaries[locale] = Object.fromEntries(Object.entries(labels).filter((entry) => typeof entry[1] === 'string'));
43
- }
44
- return dictionaries;
45
- }
46
- function applyLabelUpdate(payload) {
47
- if (!isRecord(payload)) {
48
- return;
49
- }
50
- const dictionaries = normalizeDictionaries(payload.dictionaries);
51
- if (!dictionaries) {
52
- return;
53
- }
54
- const defaultLocale = typeof payload.defaultLocale === 'string' ? payload.defaultLocale : labelState.defaultLocale;
55
- const signature = typeof payload.signature === 'string' ? payload.signature : JSON.stringify({ dictionaries, defaultLocale });
56
- const updatedAt = typeof payload.updatedAt === 'string' ? payload.updatedAt : null;
57
- setLabelState({ dictionaries, defaultLocale, signature, updatedAt });
58
- }
11
+ });
59
12
  // @ts-ignore - import.meta.hot is provided by Vite during development.
60
13
  if (import.meta.hot) {
61
14
  const onLabelsUpdated = (payload) => {
@@ -69,35 +22,4 @@ if (import.meta.hot) {
69
22
  import.meta.hot.off('fimo:labels-updated', onLabelsUpdated);
70
23
  });
71
24
  }
72
- /**
73
- * Hook to provide a label function (`t`).
74
- *
75
- * Returns FimoString with source tracking for inline editing.
76
- * Use with <Text> component for rendering, or String() for escape hatch.
77
- *
78
- * @example
79
- * const { t } = useLabels();
80
- *
81
- * // With Text component (recommended)
82
- * <Text value={t('welcome')} />
83
- *
84
- * // Escape hatch for attributes
85
- * <input placeholder={String(t('email.placeholder'))} />
86
- */
87
- export function useLabels() {
88
- const localeContext = useLocale();
89
- const labels = useSyncExternalStore(subscribeLabels, getLabelSnapshot, getLabelSnapshot);
90
- const t = (fullKey) => {
91
- const locale = localeContext.locale || labels.defaultLocale || 'en';
92
- const dictionary = labels.dictionaries[locale] ?? labels.dictionaries[labels.defaultLocale] ?? {};
93
- const text = dictionary[fullKey] ?? '';
94
- return new FimoString(text, `__labels.${locale}.${fullKey}.value`);
95
- };
96
- return { t, locale: localeContext.locale };
97
- }
98
- /**
99
- * Legacy alias for older projects. Prefer useLabels() in new code.
100
- */
101
- export function useTranslations() {
102
- return useLabels();
103
- }
25
+ export { useLabels, useTranslations } from './labels-hook.js';
@@ -0,0 +1,24 @@
1
+ export { Text } from '../primitives/components/Text.js';
2
+ export { Image } from '../primitives/components/Image.js';
3
+ export { StaticImage } from '../primitives/components/StaticImage.js';
4
+ export { Video } from '../primitives/components/Video.js';
5
+ export { Date } from '../primitives/components/Date.js';
6
+ export { Boolean } from '../primitives/components/Boolean.js';
7
+ export { RichText } from '../primitives/components/RichText.js';
8
+ export { useLabels, useTranslations } from '../primitives/labels-hook.js';
9
+ export { applyLabelUpdate, getLabelSnapshot, hydrateLabels, subscribeLabels } from '../primitives/labels-store.js';
10
+ export type { LabelState } from '../primitives/labels-store.js';
11
+ export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type FimoSource, type FimoPart, type FimoRichTextContent, type FimoRichTextNodeProps, type FimoRichTextMarkProps, type FimoRichTextComponents, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, } from '../primitives/lib/index.js';
12
+ export { fimo } from '../primitives/lib/template.js';
13
+ export { useFimoLiveValue } from '../primitives/lib/useFimoLiveValue.js';
14
+ export { DataSourcesProvider } from '../content/DataSourcesProvider.js';
15
+ export { ConsoleErrorProvider } from '../iframe/ConsoleErrorProvider.js';
16
+ export { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
17
+ export { FimoLocaleProvider, useLocale } from '../i18n/FimoLocaleContext.js';
18
+ export type { FimoLocaleContextValue } from '../i18n/FimoLocaleContext.js';
19
+ export { getFimoDefaultLocale, getFimoLocales, getFimoTextDirection, localizeFimoPath, resolveFimoLocale, toHtmlLang, } from '../i18n/locale.js';
20
+ export type { FimoLocaleResolution } from '../i18n/locale.js';
21
+ export { getDisplayRoutes } from '../shared/display-routes.js';
22
+ export type { DisplayRoute, ParamInfo } from '../shared/display-routes.js';
23
+ export type { FimoConfig, FimoRouteConfig, FimoSEO } from '../shared/fimo-config.types.js';
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/runtime/react/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,IAAI,EAAE,MAAM,kCAAkC,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,kCAAkC,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,qCAAqC,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAC;AAGhE,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACnH,YAAY,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGhE,OAAO,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAC7E,YAAY,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAG9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC3E,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,28 @@
1
+ // fimo/react — the framework-agnostic React core of the Fimo runtime.
2
+ // No Vite virtual modules, no react-router, no bundler assumptions:
3
+ // per-framework adapters (fimo/react-router via fimo/ui, fimo/next, ...)
4
+ // build on this surface.
5
+ // Inline-editing components
6
+ export { Text } from '../primitives/components/Text.js';
7
+ export { Image } from '../primitives/components/Image.js';
8
+ export { StaticImage } from '../primitives/components/StaticImage.js';
9
+ export { Video } from '../primitives/components/Video.js';
10
+ export { Date } from '../primitives/components/Date.js';
11
+ export { Boolean } from '../primitives/components/Boolean.js';
12
+ export { RichText } from '../primitives/components/RichText.js';
13
+ // Labels (framework-neutral store + hook; adapters hydrate the store)
14
+ export { useLabels, useTranslations } from '../primitives/labels-hook.js';
15
+ export { applyLabelUpdate, getLabelSnapshot, hydrateLabels, subscribeLabels } from '../primitives/labels-store.js';
16
+ // Source-tracking primitives & utilities
17
+ export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, } from '../primitives/lib/index.js';
18
+ export { fimo } from '../primitives/lib/template.js';
19
+ export { useFimoLiveValue } from '../primitives/lib/useFimoLiveValue.js';
20
+ // Preview/editor bridge providers (postMessage-based, framework-free)
21
+ export { DataSourcesProvider } from '../content/DataSourcesProvider.js';
22
+ export { ConsoleErrorProvider } from '../iframe/ConsoleErrorProvider.js';
23
+ export { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
24
+ // Locale
25
+ export { FimoLocaleProvider, useLocale } from '../i18n/FimoLocaleContext.js';
26
+ export { getFimoDefaultLocale, getFimoLocales, getFimoTextDirection, localizeFimoPath, resolveFimoLocale, toHtmlLang, } from '../i18n/locale.js';
27
+ // Config + routes
28
+ export { getDisplayRoutes } from '../shared/display-routes.js';
@@ -0,0 +1,14 @@
1
+ import type { FimoConfig } from './fimo-config.types.js';
2
+ export interface ParamInfo {
3
+ schema: string;
4
+ queryField: string;
5
+ displayField: string;
6
+ }
7
+ export interface DisplayRoute {
8
+ id: string;
9
+ name: string;
10
+ path: string;
11
+ params?: Record<string, ParamInfo>;
12
+ }
13
+ export declare function getDisplayRoutes(config: Pick<FimoConfig, 'routes'> | undefined): DisplayRoute[];
14
+ //# sourceMappingURL=display-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-routes.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/display-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC;AAmBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,SAAS,GAAG,YAAY,EAAE,CAU/F"}
@@ -0,0 +1,25 @@
1
+ const titleCase = (seg) => seg
2
+ .replace(/[_\-]+/g, ' ')
3
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
4
+ .split(/\s+/)
5
+ .filter(Boolean)
6
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
7
+ .join(' ');
8
+ const generateRouteName = (path) => {
9
+ if (path === '/') {
10
+ return 'Home';
11
+ }
12
+ const firstSegment = path.replace(/^\//, '').split('/')[0];
13
+ return titleCase(firstSegment);
14
+ };
15
+ export function getDisplayRoutes(config) {
16
+ const configRoutes = config?.routes ?? {};
17
+ return Object.entries(configRoutes)
18
+ .filter(([, route]) => route.path)
19
+ .map(([id, route]) => ({
20
+ id,
21
+ path: route.path,
22
+ name: route.name || route.label || generateRouteName(route.path),
23
+ params: route.params,
24
+ }));
25
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"fimo-config.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAA4C,MAAM,wBAAwB,CAAC;AAiBnG;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAe3D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD"}
1
+ {"version":3,"file":"fimo-config.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAA4C,MAAM,wBAAwB,CAAC;AAkBnG;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAgB3D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD"}
@@ -20,6 +20,7 @@ export function getConfigServer(rootDir) {
20
20
  i18n: raw.i18n,
21
21
  routes: raw.routes,
22
22
  version: raw.version,
23
+ framework: raw.framework,
23
24
  };
24
25
  }
25
26
  catch {