fimo 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/assets/astro/components/DateTime.astro +22 -0
  2. package/assets/astro/components/index.d.ts +1 -0
  3. package/assets/astro/components/index.js +1 -0
  4. package/assets/content-templates/shared/types.eta +2 -0
  5. package/assets/skills/fimo-cli/SKILL.md +8 -6
  6. package/assets/skills/fimo-cli/references/branches.md +2 -2
  7. package/assets/skills/fimo-cli/references/content.md +2 -2
  8. package/assets/skills/fimo-cli/references/deploy.md +17 -9
  9. package/assets/skills/fimo-cli/references/hosting.md +114 -0
  10. package/assets/skills/fimo-cli/references/init.md +13 -6
  11. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
  12. package/assets/skills/fimo-cli/references/locales.md +38 -14
  13. package/assets/skills/fimo-cli/references/pages.md +70 -0
  14. package/assets/skills/fimo-cli/references/preview.md +26 -20
  15. package/assets/skills/fimo-cli/references/webhooks.md +1 -1
  16. package/assets/skills/fimo-code/SKILL.md +11 -0
  17. package/assets/skills/fimo-code/assets/react-router-locales/LocaleLayout.tsx +12 -0
  18. package/assets/skills/fimo-code/assets/react-router-locales/routes.ts +7 -6
  19. package/assets/skills/fimo-code/references/best-practices/astro.md +47 -0
  20. package/assets/skills/fimo-code/references/best-practices/next.md +42 -0
  21. package/assets/skills/fimo-code/references/best-practices/sveltekit.md +17 -0
  22. package/assets/skills/fimo-code/references/content/richtext.md +5 -0
  23. package/assets/skills/fimo-code/references/content/schema.md +4 -1
  24. package/assets/skills/fimo-code/references/frameworks/astro.md +5 -3
  25. package/assets/skills/fimo-code/references/frameworks/hosting.md +5 -2
  26. package/assets/skills/fimo-code/references/frameworks/next/app-router.md +6 -0
  27. package/assets/skills/fimo-code/references/frameworks/next/locales.md +12 -2
  28. package/assets/skills/fimo-code/references/frameworks/next.md +7 -0
  29. package/assets/skills/fimo-code/references/frameworks/react-router.md +15 -7
  30. package/assets/skills/fimo-code/references/frameworks/sveltekit.md +15 -8
  31. package/assets/skills/fimo-code/references/labels.md +9 -1
  32. package/assets/skills/fimo-code/references/locales.md +8 -2
  33. package/assets/skills/fimo-code/references/migrations/react-router-route-ownership.md +93 -0
  34. package/assets/skills/fimo-code/references/ui.md +21 -2
  35. package/assets/skills/fimo-migration/SKILL.md +276 -51
  36. package/assets/skills/fimo-migration/references/content.md +119 -35
  37. package/assets/skills/fimo-migration/references/decision.md +72 -0
  38. package/assets/skills/fimo-migration/references/forms.md +38 -6
  39. package/assets/skills/fimo-migration/references/frameworks/nextjs.md +179 -23
  40. package/assets/skills/fimo-migration/references/import-contract.md +71 -19
  41. package/assets/skills/fimo-migration/references/manifest.md +11 -161
  42. package/assets/skills/fimo-migration/references/media.md +33 -1
  43. package/assets/skills/fimo-migration/references/platforms.md +26 -2
  44. package/assets/skills/fimo-migration/references/qa.md +167 -13
  45. package/assets/skills/fimo-migration/references/workers/content-execution.md +39 -0
  46. package/assets/skills/fimo-migration/references/workers/content-planner.md +64 -0
  47. package/assets/skills/fimo-migration/references/workers/framework.md +37 -0
  48. package/assets/skills/fimo-migration/references/workers/validator.md +25 -0
  49. package/assets/skills/fimo-migration/references/workers.md +82 -0
  50. package/assets/skills/fimo-migration/references/workflow.md +140 -192
  51. package/assets/skills/fimo-migration/scripts/apply-decision.mjs +534 -0
  52. package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +7 -4
  53. package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +191 -4
  54. package/assets/skills/fimo-studio/references/ai-chat.md +1 -1
  55. package/assets/skills/fimo-studio/references/cms.md +25 -7
  56. package/assets/skills/fimo-studio/references/webhooks.md +1 -1
  57. package/assets/sveltekit/components/DateTime.svelte +35 -0
  58. package/assets/sveltekit/components/FimoProvider.svelte +2 -2
  59. package/assets/sveltekit/components/index.d.ts +1 -0
  60. package/assets/sveltekit/components/index.js +1 -0
  61. package/dist/build/next/config.d.ts +4 -1
  62. package/dist/build/next/config.js +21 -5
  63. package/dist/build/sveltekit/vite.d.ts +3 -4
  64. package/dist/build/sveltekit/vite.js +3 -4
  65. package/dist/build/vite/plugins/runtime-env.d.ts +2 -1
  66. package/dist/build/vite/plugins/runtime-env.js +14 -10
  67. package/dist/cli/bundle.json +2 -2
  68. package/dist/cli/index.js +113 -108
  69. package/dist/runtime/app/FimoProviders.js +25 -14
  70. package/dist/runtime/app/prefetch.d.ts +3 -1
  71. package/dist/runtime/app/prefetch.js +11 -55
  72. package/dist/runtime/astro/preview.js +7 -9
  73. package/dist/runtime/content/client/internal.js +1 -0
  74. package/dist/runtime/i18n/locale.d.ts +6 -3
  75. package/dist/runtime/i18n/locale.js +16 -41
  76. package/dist/runtime/next/client-runtime.js +5 -1
  77. package/dist/runtime/next/client.d.ts +3 -2
  78. package/dist/runtime/next/client.js +2 -1
  79. package/dist/runtime/next/labels.d.ts +2 -2
  80. package/dist/runtime/next/labels.js +2 -2
  81. package/dist/runtime/next/preview.d.ts +13 -0
  82. package/dist/runtime/next/preview.js +34 -0
  83. package/dist/runtime/next/primitives/server.d.ts +2 -0
  84. package/dist/runtime/next/primitives/server.js +8 -0
  85. package/dist/runtime/next/provider.d.ts +5 -0
  86. package/dist/runtime/next/provider.js +17 -3
  87. package/dist/runtime/next/server-runtime.js +4 -1
  88. package/dist/runtime/next/server.d.ts +1 -1
  89. package/dist/runtime/next/server.js +1 -1
  90. package/dist/runtime/paths/get-fimo-paths.d.ts +6 -5
  91. package/dist/runtime/paths/get-fimo-paths.js +43 -42
  92. package/dist/runtime/preview/location.d.ts +19 -2
  93. package/dist/runtime/preview/location.js +16 -2
  94. package/dist/runtime/primitives/index.d.ts +1 -0
  95. package/dist/runtime/primitives/index.js +1 -0
  96. package/dist/runtime/react/primitives/editable/DateTime.d.ts +4 -0
  97. package/dist/runtime/react/primitives/editable/DateTime.js +13 -0
  98. package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +7 -1
  99. package/dist/runtime/react/primitives/renderers/DateRenderer.js +11 -8
  100. package/dist/runtime/react-router/{routes.d.ts → legacy/routes.d.ts} +6 -1
  101. package/dist/runtime/react-router/{routes.js → legacy/routes.js} +22 -7
  102. package/dist/runtime/routes/index.d.ts +162 -0
  103. package/dist/runtime/routes/index.js +335 -0
  104. package/dist/runtime/routing/AppRoutesProvider.js +4 -3
  105. package/dist/runtime/seo/resolver.d.ts +5 -0
  106. package/dist/runtime/seo/resolver.js +10 -42
  107. package/dist/runtime/shared/fimo-config.types.d.ts +14 -13
  108. package/dist/runtime/shared/preview-location.d.ts +1 -22
  109. package/dist/runtime/shared/preview-location.js +5 -93
  110. package/dist/runtime/sveltekit/preview.d.ts +12 -3
  111. package/dist/runtime/sveltekit/preview.js +13 -8
  112. package/dist/runtime/templates.d.ts +19 -0
  113. package/dist/runtime/templates.js +33 -0
  114. package/dist/scripts/sync-forms.js +1 -1
  115. package/dist/scripts/sync-schemas.js +1 -4
  116. package/dist/scripts/validate-fimo-config.js +5 -1
  117. package/dist/scripts/validate-page-registry.d.ts +2 -0
  118. package/dist/scripts/validate-page-registry.js +167 -0
  119. package/dist/scripts/validate-route-metadata.js +132 -41
  120. package/package.json +10 -5
  121. package/templates/astro/.fimo/config.json +13 -5
  122. package/templates/astro/AGENTS.md +5 -1
  123. package/templates/astro/astro.config.mjs +7 -0
  124. package/templates/astro/pnpm-workspace.yaml +0 -3
  125. package/templates/astro/src/components/ProjectOverview.astro +9 -6
  126. package/templates/astro/src/layouts/Base.astro +1 -1
  127. package/templates/astro/src/locales.ts +4 -0
  128. package/templates/astro/src/pages/[locale]/index.astro +19 -0
  129. package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
  130. package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
  131. package/templates/astro/src/pages/index.astro +5 -2
  132. package/templates/astro/src/pages/projects/[slug].astro +4 -2
  133. package/templates/astro/src/pages/projects/index.astro +5 -2
  134. package/templates/nextjs/.fimo/config.json +2 -1
  135. package/templates/nextjs/AGENTS.md +5 -0
  136. package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
  137. package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
  138. package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
  139. package/templates/nextjs/src/components/Document.tsx +26 -0
  140. package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
  141. package/templates/nextjs/src/locales.ts +13 -0
  142. package/templates/react-router/.fimo/config.json +3 -6
  143. package/templates/react-router/AGENTS.md +4 -0
  144. package/templates/react-router/package.json +1 -1
  145. package/templates/react-router/pnpm-workspace.yaml +1 -3
  146. package/templates/react-router/react-router.config.ts +4 -10
  147. package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
  148. package/templates/react-router/src/root.tsx +2 -4
  149. package/templates/react-router/src/routes.ts +5 -3
  150. package/templates/sveltekit/.fimo/config.json +2 -1
  151. package/templates/sveltekit/AGENTS.md +5 -0
  152. package/templates/sveltekit/src/app.html +1 -1
  153. package/templates/sveltekit/src/hooks.server.ts +11 -0
  154. package/templates/sveltekit/src/locales.ts +8 -0
  155. package/templates/sveltekit/src/params/locale.ts +5 -0
  156. package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
  157. package/templates/sveltekit/src/routes/+layout.svelte +5 -0
  158. package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
  159. package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
  160. package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
  161. package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
  162. package/templates/nextjs/src/app/layout.tsx +0 -17
  163. /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
  164. /package/templates/sveltekit/src/routes/{+page.svelte → [[locale=locale]]/+page.svelte} +0 -0
@@ -3,7 +3,7 @@
3
3
  // dynamic routes are expanded by fetching schema entries from the content API.
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { resolve } from 'node:path';
6
- import { getFimoLocales, localizeFimoPath } from '../i18n/locale.js';
6
+ import { materializeRoutePath, resolveRouteContracts, routeLocaleTargets } from '../routes/index.js';
7
7
  import { resolveSeoForPath } from '../seo/resolver.js';
8
8
  import { getConfigServer } from '../shared/fimo-config.server.js';
9
9
  import { resolveLocalRuntimeEnv } from '../shared/local-runtime-env.js';
@@ -82,11 +82,12 @@ function matchesAny(routeKey, patterns) {
82
82
  * robots.txt, or any per-route logic.
83
83
  *
84
84
  * - Static paths in config: emitted as-is.
85
- * - Dynamic paths (with `:slug`-style params): expanded by fetching schema
86
- * entries from `VITE_API_URL`. One returned entry per dynamic expansion.
87
- * - With `i18n.routing.strategy: "path-prefix"`, paths are emitted for every
88
- * configured locale: default locale unprefixed by default, non-default
89
- * locales prefixed (`/es/...`). The app router still needs matching routes.
85
+ * - Dynamic paths (with `:slug`-style entry params): expanded by fetching
86
+ * schema entries from `VITE_API_URL`. One returned entry per expansion.
87
+ * - Locales come from the route contract, not from the project locale list: a
88
+ * route is emitted once per locale only when its path declares a `:locale`
89
+ * segment. The default locale is always unprefixed. A route without a
90
+ * `:locale` segment yields exactly one default-locale path.
90
91
  * - Pure-string return shape (`p.path`) is suitable for RR's `prerender`.
91
92
  *
92
93
  * If no API URL is set, dynamic routes are skipped with a console warning.
@@ -110,14 +111,20 @@ function matchesAny(routeKey, patterns) {
110
111
  export async function getFimoPaths(opts = {}) {
111
112
  const rootDir = opts.cwd ?? process.cwd();
112
113
  const config = getConfigServer(rootDir);
113
- const locales = getFimoLocales(config);
114
- const routesEntries = Object.entries(config.routes ?? {});
115
114
  // Pre-filter route keys by include/exclude before doing any network I/O.
116
- const filteredRoutes = routesEntries.filter(([routeKey]) => {
117
- if (opts.include && opts.include.length > 0 && !matchesAny(routeKey, opts.include)) {
115
+ const contracts = resolveRouteContracts(config.routes).filter((contract) => {
116
+ if (!contract.config.path) {
118
117
  return false;
119
118
  }
120
- if (opts.exclude && opts.exclude.length > 0 && matchesAny(routeKey, opts.exclude)) {
119
+ // A placeholder no descriptor resolves cannot be expanded into a URL;
120
+ // `fimo validate` is what reports it.
121
+ if (contract.unresolvedParams.length > 0) {
122
+ return false;
123
+ }
124
+ if (opts.include && opts.include.length > 0 && !matchesAny(contract.id, opts.include)) {
125
+ return false;
126
+ }
127
+ if (opts.exclude && opts.exclude.length > 0 && matchesAny(contract.id, opts.exclude)) {
121
128
  return false;
122
129
  }
123
130
  return true;
@@ -125,20 +132,18 @@ export async function getFimoPaths(opts = {}) {
125
132
  const results = [];
126
133
  const schemaCache = new Map();
127
134
  let baseUrl = null; // lazy-resolved on first dynamic route
128
- for (const [routeKey, routeConfig] of filteredRoutes) {
129
- const pathPattern = routeConfig.path;
130
- if (!pathPattern) {
131
- continue;
132
- }
133
- const isDynamic = pathPattern.includes(':') && routeConfig.params;
134
- if (!isDynamic) {
135
- for (const locale of locales) {
135
+ for (const contract of contracts) {
136
+ const targets = routeLocaleTargets(contract, config);
137
+ // Static — no entry param, so no content to fetch. A locale param alone
138
+ // never reaches the CMS.
139
+ if (contract.entryParams.length === 0) {
140
+ for (const target of targets) {
136
141
  results.push({
137
- routeKey,
138
- pathPattern,
139
- path: localizeFimoPath(pathPattern, locale, config),
140
- locale,
141
- seo: resolveSeoForPath(pathPattern, config),
142
+ routeKey: contract.id,
143
+ pathPattern: contract.path,
144
+ path: materializeRoutePath(contract, target),
145
+ locale: target.locale,
146
+ seo: resolveSeoForPath(contract.canonicalPath, config),
142
147
  });
143
148
  }
144
149
  continue;
@@ -153,40 +158,36 @@ export async function getFimoPaths(opts = {}) {
153
158
  if (!baseUrl) {
154
159
  continue;
155
160
  }
156
- const paramEntries = Object.entries(routeConfig.params);
157
- const schemas = [...new Set(paramEntries.map(([, p]) => p.schema))];
161
+ const schemas = [...new Set(contract.entryParams.map((param) => param.schema))];
158
162
  for (const schema of schemas) {
159
- for (const locale of locales) {
160
- const cacheKey = `${locale}:${schema}`;
163
+ for (const target of targets) {
164
+ const cacheKey = `${target.locale}:${schema}`;
161
165
  if (!schemaCache.has(cacheKey)) {
162
- schemaCache.set(cacheKey, await fetchEntries(baseUrl, schema, rootDir, locale));
166
+ schemaCache.set(cacheKey, await fetchEntries(baseUrl, schema, rootDir, target.locale));
163
167
  }
164
168
  }
165
169
  }
166
- for (const locale of locales) {
167
- const entries = schemaCache.get(`${locale}:${paramEntries[0][1].schema}`) ?? [];
170
+ for (const target of targets) {
171
+ const entries = schemaCache.get(`${target.locale}:${contract.entryParams[0].schema}`) ?? [];
168
172
  for (const entry of entries) {
169
- let expandedPath = pathPattern;
170
173
  const params = {};
171
174
  let valid = true;
172
- for (const [paramName, paramInfo] of paramEntries) {
173
- const value = entry[paramInfo.queryField];
175
+ for (const param of contract.entryParams) {
176
+ const value = entry[param.queryField];
174
177
  if (value == null || value === '') {
175
178
  valid = false;
176
179
  break;
177
180
  }
178
- const stringValue = String(value);
179
- params[paramName] = stringValue;
180
- expandedPath = expandedPath.replace(`:${paramName}`, encodeURIComponent(stringValue));
181
+ params[param.name] = String(value);
181
182
  }
182
183
  if (valid) {
183
184
  results.push({
184
- routeKey,
185
- pathPattern,
186
- path: localizeFimoPath(expandedPath, locale, config),
187
- locale,
185
+ routeKey: contract.id,
186
+ pathPattern: contract.path,
187
+ path: materializeRoutePath(contract, { ...target, params }),
188
+ locale: target.locale,
188
189
  params,
189
- seo: resolveSeoForPath(expandedPath, config),
190
+ seo: resolveSeoForPath(materializeRoutePath(contract, { locale: target.locale, prefixed: false, params }), config),
190
191
  });
191
192
  }
192
193
  }
@@ -7,7 +7,13 @@ export interface PreviewLocationChange {
7
7
  /** Safe workspace focus value; query/hash state stays navigation-only. */
8
8
  path: string;
9
9
  /** Exact app location used to keep the parent's iframe URL in sync. */
10
- navigationPath: string;
10
+ navigationPath?: string;
11
+ /**
12
+ * The locale the app settled on for this location. The app owns locale
13
+ * detection, so Studio stores this value rather than deriving one from
14
+ * `path`, and the bridge re-sends it when only the locale changed.
15
+ */
16
+ locale: string;
11
17
  }
12
18
  export interface PreviewBrowserContext {
13
19
  readonly window: Pick<Window, 'location' | 'sessionStorage'>;
@@ -19,5 +25,16 @@ export declare function resolvePreviewParentOrigin(referrer: string | undefined,
19
25
  export declare function getPreviewParentOrigin(browser?: PreviewBrowserContext): string | null;
20
26
  /** Keep sandbox credentials and the iframe cache-buster out of app navigation state. */
21
27
  export declare function getPreviewLocationPath(location: PreviewLocationLike): string;
22
- export declare function getPreviewLocationChange(location: PreviewLocationLike): PreviewLocationChange;
28
+ /**
29
+ * Build the `nav/locationChange` payload. `navigationPath` carries the app's
30
+ * own query and hash state, so it is withheld until a parent origin is
31
+ * established and the message can be addressed to it instead of broadcast.
32
+ */
33
+ export declare function getPreviewLocationChange(location: PreviewLocationLike, locale: string, parentOrigin: string | null): PreviewLocationChange;
34
+ /**
35
+ * The locale the rendered document declares. Every Fimo framework shell writes
36
+ * `<html lang>` once it has settled the locale for the page, which makes it the
37
+ * app's own answer rather than a guess made from the URL.
38
+ */
39
+ export declare function getDocumentLocale(browserDocument: Pick<Document, 'documentElement'> | undefined, fallbackLocale: string): string;
23
40
  //# sourceMappingURL=location.d.ts.map
@@ -85,9 +85,23 @@ export function getPreviewLocationPath(location) {
85
85
  const query = search.toString();
86
86
  return `${location.pathname}${query ? `?${query}` : ''}${location.hash ?? ''}`;
87
87
  }
88
- export function getPreviewLocationChange(location) {
88
+ /**
89
+ * Build the `nav/locationChange` payload. `navigationPath` carries the app's
90
+ * own query and hash state, so it is withheld until a parent origin is
91
+ * established and the message can be addressed to it instead of broadcast.
92
+ */
93
+ export function getPreviewLocationChange(location, locale, parentOrigin) {
89
94
  return {
90
95
  path: location.pathname,
91
- navigationPath: getPreviewLocationPath(location),
96
+ ...(parentOrigin ? { navigationPath: getPreviewLocationPath(location) } : {}),
97
+ locale,
92
98
  };
93
99
  }
100
+ /**
101
+ * The locale the rendered document declares. Every Fimo framework shell writes
102
+ * `<html lang>` once it has settled the locale for the page, which makes it the
103
+ * app's own answer rather than a guess made from the URL.
104
+ */
105
+ export function getDocumentLocale(browserDocument, fallbackLocale) {
106
+ return browserDocument?.documentElement?.lang?.trim() || fallbackLocale;
107
+ }
@@ -2,6 +2,7 @@ export { Text } from '../react/primitives/editable/Text.js';
2
2
  export { Image, StaticImage } from '../vite/primitives/Images.js';
3
3
  export { Video } from '../react/primitives/editable/Video.js';
4
4
  export { Date } from '../react/primitives/editable/Date.js';
5
+ export { DateTime } from '../react/primitives/editable/DateTime.js';
5
6
  export { Boolean } from '../react/primitives/editable/Boolean.js';
6
7
  export { Json } from '../react/primitives/editable/Json.js';
7
8
  export { RichText } from '../react/primitives/editable/RichText.js';
@@ -5,6 +5,7 @@ export { Text } from '../react/primitives/editable/Text.js';
5
5
  export { Image, StaticImage } from '../vite/primitives/Images.js';
6
6
  export { Video } from '../react/primitives/editable/Video.js';
7
7
  export { Date } from '../react/primitives/editable/Date.js';
8
+ export { DateTime } from '../react/primitives/editable/DateTime.js';
8
9
  export { Boolean } from '../react/primitives/editable/Boolean.js';
9
10
  export { Json } from '../react/primitives/editable/Json.js';
10
11
  export { RichText } from '../react/primitives/editable/RichText.js';
@@ -0,0 +1,4 @@
1
+ import type { DateProps } from '../contracts.js';
2
+ /** Renders a `datetime` field: the `dateTime` attribute keeps the whole instant. */
3
+ export declare function DateTime({ value, ...props }: DateProps): import("react/jsx-runtime").JSX.Element | null;
4
+ //# sourceMappingURL=DateTime.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FIMO_SOURCE } from '../../../primitives/lib/index.js';
3
+ import { useFimoLiveValue } from '../../useFimoLiveValue.js';
4
+ import { DateRenderer } from '../renderers/DateRenderer.js';
5
+ /** Renders a `datetime` field: the `dateTime` attribute keeps the whole instant. */
6
+ export function DateTime({ value, ...props }) {
7
+ const { value: liveValue, ref } = useFimoLiveValue(value ?? null);
8
+ if (!liveValue) {
9
+ return null;
10
+ }
11
+ const source = typeof value === 'object' && FIMO_SOURCE in value ? (value[FIMO_SOURCE] ?? undefined) : undefined;
12
+ return _jsx(DateRenderer, { ...props, value: liveValue, source: source, precision: "instant", elementRef: ref });
13
+ }
@@ -3,9 +3,15 @@ import type { FimoDate, FimoString } from '../../../primitives/lib/index.js';
3
3
  export interface DateRendererProps {
4
4
  value: FimoDate | FimoString | Date | string | number;
5
5
  source: string | undefined;
6
+ /**
7
+ * `day` emits the calendar day in `dateTime`; `instant` keeps the full ISO
8
+ * instant, which is the whole point of a `datetime` field. Defaults to `day`
9
+ * so `<Date>` renders exactly what it always has.
10
+ */
11
+ precision?: 'day' | 'instant';
6
12
  children?: (date: Date) => ReactNode;
7
13
  className?: string;
8
14
  elementRef?: Ref<HTMLTimeElement>;
9
15
  }
10
- export declare function DateRenderer({ value, source, children, className, elementRef }: DateRendererProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function DateRenderer({ value, source, precision, children, className, elementRef }: DateRendererProps): import("react/jsx-runtime").JSX.Element;
11
17
  //# sourceMappingURL=DateRenderer.d.ts.map
@@ -1,14 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- export function DateRenderer({ value, source, children, className, elementRef }) {
2
+ export function DateRenderer({ value, source, precision = 'day', children, className, elementRef }) {
3
3
  const rawValue = value instanceof String ? value.toString() : value;
4
4
  const dateValue = new globalThis.Date(rawValue);
5
- const isoDate = dateValue.toISOString().split('T')[0];
5
+ const iso = dateValue.toISOString();
6
+ const dateTime = precision === 'instant' ? iso : iso.split('T')[0];
6
7
  const content = children
7
8
  ? children(dateValue)
8
- : dateValue.toLocaleDateString(undefined, {
9
- year: 'numeric',
10
- month: 'short',
11
- day: 'numeric',
12
- });
13
- return (_jsx("time", { ref: elementRef, dateTime: isoDate, className: className, "data-fimo-source": source, children: content }));
9
+ : precision === 'instant'
10
+ ? dateValue.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
11
+ : dateValue.toLocaleDateString(undefined, {
12
+ year: 'numeric',
13
+ month: 'short',
14
+ day: 'numeric',
15
+ });
16
+ return (_jsx("time", { ref: elementRef, dateTime: dateTime, className: className, "data-fimo-source": source, children: content }));
14
17
  }
@@ -1,4 +1,4 @@
1
- import type { FimoConfig } from '../shared/fimo-config.types.js';
1
+ import type { FimoConfig } from '../../shared/fimo-config.types.js';
2
2
  export interface FimoReactRouterRoute {
3
3
  id?: string;
4
4
  path?: string;
@@ -25,6 +25,11 @@ export interface FimoRoutesOptions {
25
25
  config?: Pick<FimoConfig, 'i18n'>;
26
26
  rootDir?: string;
27
27
  }
28
+ /**
29
+ * @deprecated New React Router projects should declare optional `:locale?`
30
+ * segments directly. This compatibility adapter may be removed in a future
31
+ * breaking release.
32
+ */
28
33
  export declare function fimoRoutes<T extends FimoRouteShape>(routes: readonly T[], options?: FimoRoutesOptions): T[];
29
34
  export {};
30
35
  //# sourceMappingURL=routes.d.ts.map
@@ -1,5 +1,11 @@
1
- import { getFimoDefaultLocale, getFimoLocales } from '../i18n/locale.js';
2
- import { getConfigServer } from '../shared/fimo-config.server.js';
1
+ import { getFimoDefaultLocale, getFimoLocales } from '../../i18n/locale.js';
2
+ import { hasLocaleSegment } from '../../routes/index.js';
3
+ import { getConfigServer } from '../../shared/fimo-config.server.js';
4
+ /**
5
+ * @deprecated New React Router projects should declare optional `:locale?`
6
+ * segments directly. This compatibility adapter may be removed in a future
7
+ * breaking release.
8
+ */
3
9
  export function fimoRoutes(routes, options = {}) {
4
10
  const config = options.config ?? getConfigServer(options.rootDir ?? process.cwd());
5
11
  const strategy = config.i18n?.routing?.strategy ?? 'path-prefix';
@@ -7,21 +13,30 @@ export function fimoRoutes(routes, options = {}) {
7
13
  return [...routes];
8
14
  }
9
15
  const defaultLocale = getFimoDefaultLocale(config);
10
- const locales = getFimoLocales(config);
11
- const prefixDefaultLocale = config.i18n?.routing?.prefixDefaultLocale === true;
12
- const prefixedLocales = locales.filter((locale) => locale !== defaultLocale || prefixDefaultLocale);
13
- if (prefixedLocales.length === 0) {
16
+ // The default locale renders at the canonical unprefixed path, so it never
17
+ // gets an alias.
18
+ const prefixedLocales = getFimoLocales(config).filter((locale) => locale !== defaultLocale);
19
+ // A route that declares its own `:locale` segment already carries the locale
20
+ // in its path; prefixing it again would produce `/es/:locale/...`.
21
+ const aliasable = routes.filter((route) => !declaresLocaleSegment(route));
22
+ if (prefixedLocales.length === 0 || aliasable.length === 0) {
14
23
  return [...routes];
15
24
  }
16
25
  return [
17
26
  ...routes,
18
- ...prefixedLocales.flatMap((locale) => routes.map((route, index) => localizeRouteEntry(route, {
27
+ ...prefixedLocales.flatMap((locale) => aliasable.map((route, index) => localizeRouteEntry(route, {
19
28
  locale,
20
29
  prefixThisEntry: true,
21
30
  routeKey: String(index),
22
31
  }))),
23
32
  ];
24
33
  }
34
+ function declaresLocaleSegment(route) {
35
+ if (route.path && hasLocaleSegment(route.path)) {
36
+ return true;
37
+ }
38
+ return (route.children ?? []).some(declaresLocaleSegment);
39
+ }
25
40
  function localizeRouteEntry(route, options) {
26
41
  const clone = {
27
42
  ...route,
@@ -0,0 +1,162 @@
1
+ export interface FimoRouteSeoConfig {
2
+ title?: string;
3
+ description?: string;
4
+ url?: string;
5
+ image?: string;
6
+ favicon?: string;
7
+ twitterSite?: string;
8
+ robots?: string;
9
+ themeColor?: string;
10
+ locale?: string;
11
+ siteName?: string;
12
+ }
13
+ export interface FimoRouteParamConfig<Query = unknown> {
14
+ type?: 'entry' | 'locale';
15
+ schema?: string;
16
+ queryField?: string;
17
+ displayField?: string;
18
+ query?: Query;
19
+ optional?: boolean;
20
+ }
21
+ export interface FimoRouteConfig<Sources = unknown, Query = unknown, Seo = FimoRouteSeoConfig> {
22
+ label?: string;
23
+ path?: string;
24
+ group?: string;
25
+ content?: {
26
+ labels?: string[];
27
+ singles?: string[];
28
+ };
29
+ sources?: Sources;
30
+ icon?: string;
31
+ params?: Record<string, FimoRouteParamConfig<Query>>;
32
+ seo?: Seo;
33
+ }
34
+ export interface FimoRoutesI18nConfig {
35
+ defaultLocale?: string;
36
+ locales?: string[];
37
+ routing?: {
38
+ strategy?: 'path-prefix';
39
+ };
40
+ }
41
+ export interface FimoRoutesConfig<RouteConfig extends FimoRouteConfig = FimoRouteConfig> {
42
+ i18n?: FimoRoutesI18nConfig;
43
+ routes?: Record<string, RouteConfig>;
44
+ }
45
+ export declare function normalizeFimoLocale(locale: string | null | undefined): string | null;
46
+ export declare function getFimoDefaultLocale(config: Pick<FimoRoutesConfig, 'i18n'> | undefined): string;
47
+ export declare function getFimoLocales(config: Pick<FimoRoutesConfig, 'i18n'> | undefined): string[];
48
+ /**
49
+ * The reserved parameter name for the locale segment. A `:locale` placeholder
50
+ * is always the locale, whether or not the route declares a descriptor for it.
51
+ */
52
+ export declare const FIMO_LOCALE_PARAM = "locale";
53
+ export interface FimoEntryParam {
54
+ type: 'entry';
55
+ name: string;
56
+ schema: string;
57
+ queryField: string;
58
+ displayField?: string;
59
+ }
60
+ export interface FimoLocaleParam {
61
+ type: 'locale';
62
+ name: string;
63
+ /**
64
+ * True (the default) means the default locale renders at the unprefixed
65
+ * path and only other locales carry a `/<locale>` prefix. False means every
66
+ * locale, the default included, renders behind the prefix.
67
+ */
68
+ optional: boolean;
69
+ }
70
+ export type FimoRouteParam = FimoEntryParam | FimoLocaleParam;
71
+ export interface FimoRouteIssue {
72
+ /** The parameter the issue is about, when it is about one. */
73
+ param?: string;
74
+ message: string;
75
+ /** A concrete edit that resolves the issue. */
76
+ fix: string;
77
+ }
78
+ export interface FimoRouteContract<RouteConfig extends FimoRouteConfig = FimoRouteConfig> {
79
+ id: string;
80
+ /** The declared route entry, kept so callers can reach `label` / `seo`. */
81
+ config: RouteConfig;
82
+ /** The declared path, locale placeholder included (`/:locale/blog/:slug`). */
83
+ path: string;
84
+ /** The declared path without the locale placeholder (`/blog/:slug`). */
85
+ canonicalPath: string;
86
+ params: FimoRouteParam[];
87
+ entryParams: FimoEntryParam[];
88
+ localeParam: FimoLocaleParam | null;
89
+ /** Placeholders in the path that no usable descriptor resolves. */
90
+ unresolvedParams: string[];
91
+ issues: FimoRouteIssue[];
92
+ }
93
+ export interface FimoRouteMatch<RouteConfig extends FimoRouteConfig = FimoRouteConfig> {
94
+ contract: FimoRouteContract<RouteConfig>;
95
+ /** Every resolved path parameter, the locale segment included. */
96
+ params: Record<string, string>;
97
+ /** Entry parameters only — the subset a content lookup may use. */
98
+ entryParams: Record<string, string>;
99
+ /** The locale this URL renders. The project default when the path omits it. */
100
+ locale: string;
101
+ /** True when the locale came from a path segment rather than the default. */
102
+ localeFromPath: boolean;
103
+ /** Static segments matched. Higher wins when several routes match. */
104
+ score: number;
105
+ }
106
+ /** One URL shape a route produces for one locale. */
107
+ export interface FimoRouteLocaleTarget {
108
+ locale: string;
109
+ /** Whether this locale renders behind a `/<locale>` prefix. */
110
+ prefixed: boolean;
111
+ }
112
+ export declare function normalizeRoutePath(path: string): string;
113
+ /**
114
+ * True when the path declares a locale segment, in either spelling: the config
115
+ * writes `:locale`, a React Router registration writes `:locale?` when the
116
+ * segment may be omitted.
117
+ */
118
+ export declare function hasLocaleSegment(path: string): boolean;
119
+ /**
120
+ * Normalize one declared route into the contract every consumer reads.
121
+ *
122
+ * Parameter types resolve in this order:
123
+ * 1. an explicit `type`;
124
+ * 2. the reserved name `locale`, which infers the locale type;
125
+ * 3. otherwise the entry type, which is what every route written before
126
+ * locale-aware routing existed relies on.
127
+ */
128
+ export declare function resolveRouteContract<RouteConfig extends FimoRouteConfig>(id: string, route: RouteConfig): FimoRouteContract<RouteConfig>;
129
+ export declare function resolveRouteContracts<RouteConfig extends FimoRouteConfig>(routes: Record<string, RouteConfig> | undefined): FimoRouteContract<RouteConfig>[];
130
+ /**
131
+ * Match a concrete pathname against the project's routes.
132
+ *
133
+ * The truth table, for a route declared `/:locale/x/:slug` in a project whose
134
+ * default locale is `en`:
135
+ *
136
+ * | URL | locale param | resolved locale | entry params |
137
+ * | --------------- | ------------ | --------------- | ------------------- |
138
+ * | `/x/hello` | optional | `en` (default) | `{ slug: 'hello' }` |
139
+ * | `/fr/x/bonjour` | optional | `fr` | `{ slug: 'bonjour'}`|
140
+ * | `/en/x/hello` | optional | no match | — |
141
+ * | `/x/hello` | required | no match | — |
142
+ * | `/en/x/hello` | required | `en` | `{ slug: 'hello' }` |
143
+ *
144
+ * A route with no locale segment always resolves the default locale.
145
+ */
146
+ export declare function matchFimoRoute<RouteConfig extends FimoRouteConfig>(pathnameOrUrl: string, config: Pick<FimoRoutesConfig<RouteConfig>, 'routes' | 'i18n'>): FimoRouteMatch<RouteConfig> | null;
147
+ /**
148
+ * The URL shapes a route produces, one per locale it can render.
149
+ *
150
+ * A route without a locale segment produces a single default-locale URL: under
151
+ * path-prefix routing it has no way to express another locale in its path.
152
+ */
153
+ export declare function routeLocaleTargets(contract: FimoRouteContract, config: Pick<FimoRoutesConfig, 'i18n'>): FimoRouteLocaleTarget[];
154
+ /**
155
+ * Build the concrete URL for one locale target. Entry param values are
156
+ * URL-encoded; the locale segment is a prefix, never a substitution, so the
157
+ * canonical default-locale URL carries no locale at all.
158
+ */
159
+ export declare function materializeRoutePath(contract: FimoRouteContract, target: FimoRouteLocaleTarget & {
160
+ params?: Record<string, string>;
161
+ }): string;
162
+ //# sourceMappingURL=index.d.ts.map