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
@@ -0,0 +1,335 @@
1
+ // ../../packages/routes/src/index.ts
2
+ function normalizeFimoLocale(locale) {
3
+ const trimmed = locale?.trim();
4
+ if (!trimmed) {
5
+ return null;
6
+ }
7
+ const candidate = trimmed.replace(/_/g, "-");
8
+ try {
9
+ return Intl.getCanonicalLocales(candidate)[0] ?? null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ function getFimoDefaultLocale(config) {
15
+ return normalizeFimoLocale(config?.i18n?.defaultLocale) ?? "en";
16
+ }
17
+ function getFimoLocales(config) {
18
+ const defaultLocale = getFimoDefaultLocale(config);
19
+ const locales = /* @__PURE__ */ new Set([defaultLocale]);
20
+ for (const locale of config?.i18n?.locales ?? []) {
21
+ const clean = normalizeFimoLocale(locale);
22
+ if (clean) {
23
+ locales.add(clean);
24
+ }
25
+ }
26
+ return [...locales];
27
+ }
28
+ var FIMO_LOCALE_PARAM = "locale";
29
+ function normalizeRoutePath(path) {
30
+ const withSlash = path.startsWith("/") ? path : `/${path}`;
31
+ const collapsed = withSlash.replace(/\/{2,}/g, "/");
32
+ return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
33
+ }
34
+ function splitSegments(path) {
35
+ return path.split("/").filter(Boolean);
36
+ }
37
+ function isPlaceholder(segment) {
38
+ return segment.startsWith(":") && segment.length > 1;
39
+ }
40
+ function readPlaceholder(segment) {
41
+ if (!isPlaceholder(segment)) {
42
+ return null;
43
+ }
44
+ return {
45
+ name: segment.slice(1).replace(/[?*]$/, ""),
46
+ catchAll: segment.endsWith("*")
47
+ };
48
+ }
49
+ function hasLocaleSegment(path) {
50
+ return splitSegments(normalizeRoutePath(path)).some(
51
+ (segment) => segment === `:${FIMO_LOCALE_PARAM}` || segment === `:${FIMO_LOCALE_PARAM}?`
52
+ );
53
+ }
54
+ function resolveParamType(name, descriptor) {
55
+ if (descriptor?.type) {
56
+ return descriptor.type;
57
+ }
58
+ return name === FIMO_LOCALE_PARAM ? "locale" : "entry";
59
+ }
60
+ function buildLocaleParam(name, descriptor, index, issues) {
61
+ if (name !== FIMO_LOCALE_PARAM) {
62
+ issues.push({
63
+ param: name,
64
+ message: `Param "${name}" declares \`"type": "locale"\` but "${FIMO_LOCALE_PARAM}" is the only locale parameter name.`,
65
+ fix: `Rename the path segment and the descriptor to ":${FIMO_LOCALE_PARAM}", or use \`"type": "entry"\`.`
66
+ });
67
+ return null;
68
+ }
69
+ if (index !== 0) {
70
+ issues.push({
71
+ param: name,
72
+ message: `The ":${FIMO_LOCALE_PARAM}" segment must be the first segment of the path.`,
73
+ fix: `Move it to the front, for example "/:${FIMO_LOCALE_PARAM}/blog/:slug".`
74
+ });
75
+ return null;
76
+ }
77
+ for (const field of ["schema", "queryField", "displayField", "query"]) {
78
+ if (descriptor?.[field] !== void 0) {
79
+ issues.push({
80
+ param: name,
81
+ message: `Locale param "${name}" must not declare "${field}" \u2014 the locale is read from the URL, never from the CMS.`,
82
+ fix: `Remove "${field}" from \`params.${name}\`.`
83
+ });
84
+ }
85
+ }
86
+ return { type: "locale", name, optional: descriptor?.optional !== false };
87
+ }
88
+ function buildEntryParam(name, descriptor, issues) {
89
+ if (name === FIMO_LOCALE_PARAM) {
90
+ issues.push({
91
+ param: name,
92
+ message: `Param "${name}" is reserved for the locale segment and cannot use \`"type": "entry"\`.`,
93
+ fix: `Drop \`"type": "entry"\` from \`params.${name}\`, or rename the segment.`
94
+ });
95
+ return null;
96
+ }
97
+ if (!descriptor) {
98
+ issues.push({
99
+ param: name,
100
+ message: `":${name}" needs \`params\` metadata before its URLs can be generated.`,
101
+ fix: `Add \`"params": { "${name}": { "schema": "SchemaName", "queryField": "${name}" } }\`.`
102
+ });
103
+ return null;
104
+ }
105
+ if (descriptor.optional !== void 0) {
106
+ issues.push({
107
+ param: name,
108
+ message: `Entry param "${name}" cannot be optional \u2014 only the locale segment can be omitted from a URL.`,
109
+ fix: `Remove "optional" from \`params.${name}\`.`
110
+ });
111
+ }
112
+ if (!descriptor.schema || !descriptor.queryField) {
113
+ issues.push({
114
+ param: name,
115
+ message: `Entry param "${name}" is missing ${!descriptor.schema ? '"schema"' : '"queryField"'}.`,
116
+ fix: `Set \`"${name}": { "schema": "SchemaName", "queryField": "${name}" }\`.`
117
+ });
118
+ return null;
119
+ }
120
+ return {
121
+ type: "entry",
122
+ name,
123
+ schema: descriptor.schema,
124
+ queryField: descriptor.queryField,
125
+ ...descriptor.displayField ? { displayField: descriptor.displayField } : {}
126
+ };
127
+ }
128
+ function resolveRouteContract(id, route) {
129
+ const path = normalizeRoutePath(route.path ?? "/");
130
+ const segments = splitSegments(path);
131
+ const declared = route.params ?? {};
132
+ const issues = [];
133
+ const params = [];
134
+ const unresolvedParams = [];
135
+ const seen = /* @__PURE__ */ new Set();
136
+ segments.forEach((segment, index) => {
137
+ if (!isPlaceholder(segment)) {
138
+ return;
139
+ }
140
+ const placeholder = readPlaceholder(segment);
141
+ if (!placeholder) {
142
+ return;
143
+ }
144
+ const { name } = placeholder;
145
+ if (seen.has(name)) {
146
+ issues.push({
147
+ param: name,
148
+ message: `Placeholder ":${name}" appears more than once.`,
149
+ fix: "Give each placeholder a distinct name."
150
+ });
151
+ unresolvedParams.push(name);
152
+ return;
153
+ }
154
+ seen.add(name);
155
+ const descriptor = declared[name];
156
+ const resolved = resolveParamType(name, descriptor) === "locale" ? buildLocaleParam(name, descriptor, index, issues) : buildEntryParam(name, descriptor, issues);
157
+ if (resolved) {
158
+ params.push(resolved);
159
+ } else {
160
+ unresolvedParams.push(name);
161
+ }
162
+ });
163
+ for (const name of Object.keys(declared)) {
164
+ if (!seen.has(name)) {
165
+ issues.push({
166
+ param: name,
167
+ message: `Declared param "${name}" has no ":${name}" segment in the path.`,
168
+ fix: `Add ":${name}" to the path, or remove it from "params".`
169
+ });
170
+ }
171
+ }
172
+ const localeParam = params.find((param) => param.type === "locale") ?? null;
173
+ const canonicalPath = localeParam ? normalizeRoutePath(`/${segments.filter((segment) => segment !== `:${FIMO_LOCALE_PARAM}`).join("/")}`) : path;
174
+ return {
175
+ id,
176
+ config: route,
177
+ path,
178
+ canonicalPath,
179
+ params,
180
+ entryParams: params.filter((param) => param.type === "entry"),
181
+ localeParam,
182
+ unresolvedParams,
183
+ issues
184
+ };
185
+ }
186
+ function resolveRouteContracts(routes) {
187
+ return Object.entries(routes ?? {}).map(([id, route]) => resolveRouteContract(id, route));
188
+ }
189
+ function readLocaleVocabulary(config) {
190
+ return {
191
+ locales: config.i18n ? getFimoLocales(config) : null,
192
+ defaultLocale: getFimoDefaultLocale(config)
193
+ };
194
+ }
195
+ function acceptsLocaleSegment(segment, localeParam, vocabulary) {
196
+ if (!vocabulary.locales) {
197
+ return segment.length > 0;
198
+ }
199
+ if (!vocabulary.locales.includes(segment)) {
200
+ return false;
201
+ }
202
+ return localeParam.optional ? segment !== vocabulary.defaultLocale : true;
203
+ }
204
+ function matchContract(urlSegments, contract, vocabulary) {
205
+ const patternSegments = splitSegments(contract.path);
206
+ const localeParam = contract.localeParam;
207
+ const catchAllIndex = patternSegments.findIndex((segment) => readPlaceholder(segment)?.catchAll);
208
+ const hasCatchAll = catchAllIndex !== -1;
209
+ const hasLocaleLength = hasCatchAll ? urlSegments.length >= patternSegments.length : urlSegments.length === patternSegments.length;
210
+ const withLocale = localeParam?.optional ? hasLocaleLength && acceptsLocaleSegment(urlSegments[0] ?? "", localeParam, vocabulary) : hasLocaleLength;
211
+ const withoutLocale = Boolean(localeParam?.optional) && (hasCatchAll ? urlSegments.length >= patternSegments.length - 1 : urlSegments.length === patternSegments.length - 1);
212
+ if (!localeParam && !withLocale) {
213
+ return null;
214
+ }
215
+ if (localeParam && !withLocale && !withoutLocale) {
216
+ return null;
217
+ }
218
+ const params = {};
219
+ const entryParams = {};
220
+ let localeFromPath = null;
221
+ let score = 0;
222
+ let offset = 0;
223
+ for (const [index, patternSegment] of patternSegments.entries()) {
224
+ if (localeParam && patternSegment === `:${FIMO_LOCALE_PARAM}`) {
225
+ if (!withLocale) {
226
+ offset = -1;
227
+ continue;
228
+ }
229
+ const value2 = urlSegments[index];
230
+ if (!acceptsLocaleSegment(value2, localeParam, vocabulary)) {
231
+ return null;
232
+ }
233
+ localeFromPath = value2;
234
+ params[FIMO_LOCALE_PARAM] = value2;
235
+ continue;
236
+ }
237
+ const value = urlSegments[index + offset];
238
+ if (value === void 0) {
239
+ return null;
240
+ }
241
+ const placeholder = readPlaceholder(patternSegment);
242
+ if (placeholder) {
243
+ const { name, catchAll } = placeholder;
244
+ const rawValue = catchAll ? urlSegments.slice(index + offset).join("/") : value;
245
+ const decoded = safeDecode(rawValue);
246
+ params[name] = decoded;
247
+ entryParams[name] = decoded;
248
+ if (catchAll) {
249
+ break;
250
+ }
251
+ continue;
252
+ }
253
+ if (patternSegment !== value) {
254
+ return null;
255
+ }
256
+ score += 1;
257
+ }
258
+ return {
259
+ contract,
260
+ params,
261
+ entryParams,
262
+ locale: localeFromPath ?? vocabulary.defaultLocale,
263
+ localeFromPath: localeFromPath !== null,
264
+ score
265
+ };
266
+ }
267
+ function readPathname(input) {
268
+ try {
269
+ return new URL(input, "http://fimo.invalid").pathname;
270
+ } catch {
271
+ return input.split("#")[0].split("?")[0] || "/";
272
+ }
273
+ }
274
+ function safeDecode(value) {
275
+ try {
276
+ return decodeURIComponent(value);
277
+ } catch {
278
+ return value;
279
+ }
280
+ }
281
+ function matchFimoRoute(pathnameOrUrl, config) {
282
+ const urlSegments = splitSegments(normalizeRoutePath(readPathname(pathnameOrUrl)));
283
+ const vocabulary = readLocaleVocabulary(config);
284
+ let best = null;
285
+ for (const contract of resolveRouteContracts(config.routes)) {
286
+ if (!contract.config.path) {
287
+ continue;
288
+ }
289
+ const match = matchContract(urlSegments, contract, vocabulary);
290
+ if (match && (!best || match.score > best.score)) {
291
+ best = match;
292
+ }
293
+ }
294
+ return best;
295
+ }
296
+ function routeLocaleTargets(contract, config) {
297
+ const defaultLocale = getFimoDefaultLocale(config);
298
+ if (!contract.localeParam) {
299
+ return [{ locale: defaultLocale, prefixed: false }];
300
+ }
301
+ const optional = contract.localeParam.optional;
302
+ return getFimoLocales(config).map((locale) => ({
303
+ locale,
304
+ prefixed: optional ? locale !== defaultLocale : true
305
+ }));
306
+ }
307
+ function materializeRoutePath(contract, target) {
308
+ const params = target.params ?? {};
309
+ const substituted = splitSegments(contract.canonicalPath).map((segment) => {
310
+ const placeholder = readPlaceholder(segment);
311
+ if (!placeholder) {
312
+ return segment;
313
+ }
314
+ const value = params[placeholder.name];
315
+ if (value === void 0) {
316
+ return segment;
317
+ }
318
+ return placeholder.catchAll ? value.split("/").map((part) => encodeURIComponent(part)).join("/") : encodeURIComponent(value);
319
+ });
320
+ const prefix = target.prefixed ? [target.locale] : [];
321
+ return normalizeRoutePath(`/${[...prefix, ...substituted].join("/")}`);
322
+ }
323
+ export {
324
+ FIMO_LOCALE_PARAM,
325
+ getFimoDefaultLocale,
326
+ getFimoLocales,
327
+ hasLocaleSegment,
328
+ matchFimoRoute,
329
+ materializeRoutePath,
330
+ normalizeFimoLocale,
331
+ normalizeRoutePath,
332
+ resolveRouteContract,
333
+ resolveRouteContracts,
334
+ routeLocaleTargets
335
+ };
@@ -8,7 +8,7 @@ import { AppErrorBoundary, onError } from '../core/AppErrorBoundary.js';
8
8
  import { ViteLifecycles } from '../core/ViteLifecycles.js';
9
9
  import { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
10
10
  import { getSafePreviewNavigationPath, isPreviewRuntimeEnabled } from '../preview/bridge.js';
11
- import { getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
11
+ import { getDocumentLocale, getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
12
12
  import { OptionalTracking } from '../t/OptionalTracking.js';
13
13
  import { AppResetScrollOnNavigate } from './AppResetScrollOnNavigate.js';
14
14
  const titleCase = (seg) => seg
@@ -63,15 +63,16 @@ export function AppRoutesProvider({ appRoutes, children }) {
63
63
  }
64
64
  window.parent.postMessage({ type: 'nav/routes', payload: { routes: displayRoutes } }, getPreviewParentOrigin() ?? '*');
65
65
  }, [appRoutes, displayRoutes, previewEnabled]);
66
+ // This runtime predates locale-aware routing and has no fimo-config to read,
67
+ // so the document's own `lang` is the only locale the app has declared.
66
68
  const sendLocationChange = useCallback(() => {
67
69
  if (!previewEnabled) {
68
70
  return;
69
71
  }
70
- const locationChange = getPreviewLocationChange(location);
71
72
  const parentOrigin = getPreviewParentOrigin();
72
73
  window.parent.postMessage({
73
74
  type: 'nav/locationChange',
74
- payload: parentOrigin ? locationChange : { path: locationChange.path },
75
+ payload: getPreviewLocationChange(location, getDocumentLocale(document, 'en'), parentOrigin),
75
76
  }, parentOrigin ?? '*');
76
77
  }, [location.hash, location.pathname, location.search, previewEnabled]);
77
78
  useEffect(sendRoutes, [sendRoutes]);
@@ -28,6 +28,11 @@ import type { FimoConfig, FimoRouteConfig, FimoSEO } from '../shared/fimo-config
28
28
  */
29
29
  export declare const SEO_FIELD_KEYS: readonly ["title", "description", "siteName", "url", "image", "favicon", "twitterSite", "robots", "themeColor", "locale"];
30
30
  export declare function normalizePathname(pathname: string): string;
31
+ /**
32
+ * Whether one pattern matches one path, and how specific the match is (the
33
+ * number of static segments it pinned down). Locale-aware: a pattern with a
34
+ * `:locale` segment matches both the prefixed and the canonical URL.
35
+ */
31
36
  export declare function matchRoutePath(pathname: string, pathPattern: string): {
32
37
  matched: boolean;
33
38
  score: number;
@@ -1,3 +1,4 @@
1
+ import { matchFimoRoute } from '../routes/index.js';
1
2
  /**
2
3
  * Shared SEO resolver for the generated app.
3
4
  *
@@ -45,53 +46,20 @@ export function normalizePathname(pathname) {
45
46
  }
46
47
  return normalized;
47
48
  }
48
- function splitSegments(pathname) {
49
- const normalized = normalizePathname(pathname);
50
- if (normalized === '/') {
51
- return [];
52
- }
53
- return normalized.slice(1).split('/');
54
- }
49
+ /**
50
+ * Whether one pattern matches one path, and how specific the match is (the
51
+ * number of static segments it pinned down). Locale-aware: a pattern with a
52
+ * `:locale` segment matches both the prefixed and the canonical URL.
53
+ */
55
54
  export function matchRoutePath(pathname, pathPattern) {
56
- const pathSegments = splitSegments(pathname);
57
- const patternSegments = splitSegments(pathPattern);
58
- if (pathSegments.length !== patternSegments.length) {
59
- return { matched: false, score: -1 };
60
- }
61
- let staticSegments = 0;
62
- for (let i = 0; i < patternSegments.length; i += 1) {
63
- const routeSegment = patternSegments[i];
64
- const inputSegment = pathSegments[i];
65
- if (routeSegment.startsWith(':')) {
66
- continue;
67
- }
68
- if (routeSegment !== inputSegment) {
69
- return { matched: false, score: -1 };
70
- }
71
- staticSegments += 1;
72
- }
73
- return { matched: true, score: staticSegments };
55
+ const match = matchFimoRoute(pathname, { routes: { route: { path: pathPattern } } });
56
+ return match ? { matched: true, score: match.score } : { matched: false, score: -1 };
74
57
  }
75
58
  export function resolveRouteConfig(pathname, routes) {
76
59
  if (!routes) {
77
60
  return undefined;
78
61
  }
79
- const normalizedPath = normalizePathname(pathname);
80
- let bestMatch;
81
- for (const routeConfig of Object.values(routes)) {
82
- const pathPattern = routeConfig.path;
83
- if (!pathPattern) {
84
- continue;
85
- }
86
- const result = matchRoutePath(normalizedPath, pathPattern);
87
- if (!result.matched) {
88
- continue;
89
- }
90
- if (!bestMatch || result.score > bestMatch.score) {
91
- bestMatch = { score: result.score, route: routeConfig };
92
- }
93
- }
94
- return bestMatch?.route;
62
+ return matchFimoRoute(pathname, { routes })?.contract.config;
95
63
  }
96
64
  export function getNestedValue(input, path) {
97
65
  if (!input || typeof input !== 'object') {
@@ -124,7 +92,7 @@ function resolveSeoField(routeValue, globalValue, context) {
124
92
  }
125
93
  export function resolveSeoForPath(pathname, config, routeData) {
126
94
  const globalSeo = config.seo ?? {};
127
- const routeConfig = resolveRouteConfig(pathname, config.routes);
95
+ const routeConfig = matchFimoRoute(pathname, config)?.contract.config;
128
96
  const routeSeo = routeConfig?.seo;
129
97
  if (!routeSeo) {
130
98
  return Object.keys(globalSeo).length > 0 ? globalSeo : undefined;
@@ -1,3 +1,4 @@
1
+ import type { FimoRouteConfig as SharedFimoRouteConfig, FimoRouteParamConfig as SharedFimoRouteParamConfig } from '../routes/index.js';
1
2
  export type FimoFramework = 'astro' | 'nextjs' | 'nuxtjs' | 'react-router' | 'remix' | 'sveltekit' | 'vite' | 'vue';
2
3
  export interface FimoRuntimeCommands {
3
4
  dev?: string;
@@ -40,20 +41,21 @@ export interface RouteSEO {
40
41
  locale?: string;
41
42
  siteName?: string;
42
43
  }
43
- export interface FimoRouteConfig {
44
- label?: string;
45
- path?: string;
46
- params?: Record<string, {
47
- schema: string;
48
- queryField: string;
49
- displayField: string;
50
- }>;
51
- seo?: RouteSEO;
52
- }
44
+ /**
45
+ * A path parameter descriptor as it appears in `.fimo/config.json`.
46
+ *
47
+ * Deliberately permissive — it mirrors hand-written JSON. `resolveRouteContract`
48
+ * from the public `fimo/routes` entry narrows it into the discriminated
49
+ * `FimoRouteParam` union and reports whatever does not add up; `fimo validate`
50
+ * surfaces those issues.
51
+ */
52
+ export type FimoRouteParamConfig = SharedFimoRouteParamConfig<import('@fimo/api-contracts').PageCollectionSourceQuery>;
53
+ export type FimoRouteConfig = SharedFimoRouteConfig<import('@fimo/api-contracts').PageSources, import('@fimo/api-contracts').PageCollectionSourceQuery, RouteSEO>;
53
54
  export interface FimoI18nRoutingConfig {
54
55
  /**
55
- * Supported now:
56
- * - path-prefix: default locale renders at `/pricing`, non-default locales at `/es/pricing`.
56
+ * Legacy compatibility for projects that still carry a global routing
57
+ * setting. New projects declare locale ownership on each route with a
58
+ * `:locale` segment; the scaffold intentionally omits this block.
57
59
  *
58
60
  * Strategies we may support later:
59
61
  * - domain: `example.es/pricing`
@@ -63,7 +65,6 @@ export interface FimoI18nRoutingConfig {
63
65
  * - custom: framework code resolves locale and passes it to `<FimoProviders locale={...}>`
64
66
  */
65
67
  strategy?: 'path-prefix';
66
- prefixDefaultLocale?: boolean;
67
68
  }
68
69
  export interface FimoI18nConfig {
69
70
  defaultLocale?: string;
@@ -1,23 +1,2 @@
1
- export interface PreviewLocationLike {
2
- pathname: string;
3
- search?: string;
4
- hash?: string;
5
- }
6
- export interface PreviewLocationChange {
7
- /** Safe workspace focus value; query/hash state stays navigation-only. */
8
- path: string;
9
- /** Exact app location used to keep the parent's iframe URL in sync. */
10
- navigationPath: string;
11
- }
12
- export interface PreviewBrowserContext {
13
- readonly window: Pick<Window, 'location' | 'sessionStorage'>;
14
- readonly document: Pick<Document, 'referrer'>;
15
- }
16
- /** Prefer the immediate embedding document over a potentially stale referrer. */
17
- export declare function resolvePreviewParentOrigin(referrer: string | undefined, ancestorOrigins: readonly string[] | undefined): string | null;
18
- /** Resolve and remember the trusted Studio origin across full-page navigation. */
19
- export declare function getPreviewParentOrigin(browser?: PreviewBrowserContext): string | null;
20
- /** Keep sandbox credentials and the iframe cache-buster out of app navigation state. */
21
- export declare function getPreviewLocationPath(location: PreviewLocationLike): string;
22
- export declare function getPreviewLocationChange(location: PreviewLocationLike): PreviewLocationChange;
1
+ export * from '../preview/location.js';
23
2
  //# sourceMappingURL=preview-location.d.ts.map
@@ -1,93 +1,5 @@
1
- const PREVIEW_INTERNAL_QUERY_PARAMS = new Set(['bl_preview_token', 'preview_token', 't']);
2
- const PREVIEW_PARENT_ORIGIN_STORAGE_KEY = 'fimo-preview-parent-origin';
3
- let rememberedPreviewParentOrigin;
4
- const getOrigin = (value) => {
5
- if (!value) {
6
- return null;
7
- }
8
- try {
9
- const origin = new URL(value).origin;
10
- return origin === 'null' ? null : origin;
11
- }
12
- catch {
13
- return null;
14
- }
15
- };
16
- /** Prefer the immediate embedding document over a potentially stale referrer. */
17
- export function resolvePreviewParentOrigin(referrer, ancestorOrigins) {
18
- return getOrigin(ancestorOrigins?.[0]) ?? getOrigin(referrer);
19
- }
20
- /** Resolve and remember the trusted Studio origin across full-page navigation. */
21
- export function getPreviewParentOrigin(browser) {
22
- const browserWindow = browser?.window ?? (typeof window === 'undefined' ? undefined : window);
23
- const browserDocument = browser?.document ?? (typeof document === 'undefined' ? undefined : document);
24
- if (!browserWindow) {
25
- return null;
26
- }
27
- const storage = readSessionStorage(browserWindow);
28
- const ancestorOrigin = getOrigin(browserWindow.location.ancestorOrigins?.[0]);
29
- if (ancestorOrigin) {
30
- rememberPreviewParentOrigin(ancestorOrigin, storage);
31
- return ancestorOrigin;
32
- }
33
- const rememberedOrigin = readRememberedPreviewParentOrigin(storage);
34
- if (rememberedOrigin) {
35
- return rememberedOrigin;
36
- }
37
- const referrerOrigin = getOrigin(browserDocument?.referrer);
38
- if (referrerOrigin) {
39
- rememberPreviewParentOrigin(referrerOrigin, storage);
40
- }
41
- return referrerOrigin;
42
- }
43
- function readSessionStorage(browserWindow) {
44
- try {
45
- return browserWindow.sessionStorage;
46
- }
47
- catch {
48
- return undefined;
49
- }
50
- }
51
- function readRememberedPreviewParentOrigin(storage) {
52
- if (rememberedPreviewParentOrigin !== undefined) {
53
- return rememberedPreviewParentOrigin;
54
- }
55
- if (!storage) {
56
- rememberedPreviewParentOrigin = null;
57
- return null;
58
- }
59
- try {
60
- rememberedPreviewParentOrigin = getOrigin(storage.getItem(PREVIEW_PARENT_ORIGIN_STORAGE_KEY) ?? undefined);
61
- }
62
- catch {
63
- rememberedPreviewParentOrigin = null;
64
- }
65
- return rememberedPreviewParentOrigin;
66
- }
67
- function rememberPreviewParentOrigin(origin, storage) {
68
- rememberedPreviewParentOrigin = origin;
69
- if (!storage) {
70
- return;
71
- }
72
- try {
73
- storage.setItem(PREVIEW_PARENT_ORIGIN_STORAGE_KEY, origin);
74
- }
75
- catch {
76
- // Storage availability must not block the preview message channel.
77
- }
78
- }
79
- /** Keep sandbox credentials and the iframe cache-buster out of app navigation state. */
80
- export function getPreviewLocationPath(location) {
81
- const search = new URLSearchParams(location.search ?? '');
82
- for (const key of PREVIEW_INTERNAL_QUERY_PARAMS) {
83
- search.delete(key);
84
- }
85
- const query = search.toString();
86
- return `${location.pathname}${query ? `?${query}` : ''}${location.hash ?? ''}`;
87
- }
88
- export function getPreviewLocationChange(location) {
89
- return {
90
- path: location.pathname,
91
- navigationPath: getPreviewLocationPath(location),
92
- };
93
- }
1
+ // The SvelteKit bundle reaches the preview location contract through this
2
+ // path. Only one implementation may exist: the remembered parent origin is
3
+ // module state, and a second copy would let the two bridges disagree about
4
+ // which Studio origin is trusted.
5
+ export * from '../preview/location.js';
@@ -3,11 +3,20 @@ export interface PreviewNavigation {
3
3
  goto: (to: string) => unknown;
4
4
  /** SvelteKit `invalidateAll` — reruns every `load` against fresh content. */
5
5
  invalidateAll: () => Promise<unknown>;
6
+ /** Optional app-owned locale resolver, evaluated for every bridge message. */
7
+ locale?: () => string | undefined;
6
8
  }
7
9
  /** Load the platform preview script once. Empty configuration keeps preview disabled. */
8
10
  export declare function ensurePreviewScript(src: string | undefined): boolean;
9
- /** Report where the app is now. Studio keeps its URL and route selector in sync from this. */
10
- export declare function sendPreviewLocation(): void;
11
+ /**
12
+ * Report where the app is now, and in which locale. Studio keeps its URL,
13
+ * route selector and content locale in sync from this.
14
+ *
15
+ * `locale` defaults to the locale the SvelteKit shell rendered into
16
+ * `<html lang>`; pass one explicitly when the app resolves it somewhere the
17
+ * document does not show, for example a cookie or a user preference.
18
+ */
19
+ export declare function sendPreviewLocation(locale?: string): void;
11
20
  /**
12
21
  * Listen for the live Studio preview protocol and answer it with SvelteKit's
13
22
  * own navigation APIs. Returns the disconnect function.
@@ -15,5 +24,5 @@ export declare function sendPreviewLocation(): void;
15
24
  * Only messages from the established parent origin are honored. History stays
16
25
  * native, so Studio's back/forward controls need no protocol of their own.
17
26
  */
18
- export declare function connectPreview({ goto, invalidateAll }: PreviewNavigation): () => void;
27
+ export declare function connectPreview({ goto, invalidateAll, locale }: PreviewNavigation): () => void;
19
28
  //# sourceMappingURL=preview.d.ts.map