fimo 0.8.0 → 0.9.0-experimental.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
  2. package/assets/agents-starter/claude/settings.json +19 -0
  3. package/assets/content-templates/hooks-template.eta +4 -2
  4. package/assets/content-templates/types-template.eta +67 -1
  5. package/assets/skills/fimo/SKILL.md +1 -1
  6. package/assets/skills/fimo/references/content.md +86 -15
  7. package/assets/skills/fimo/references/translations.md +75 -0
  8. package/assets/skills/fimo-cli/SKILL.md +6 -2
  9. package/assets/skills/fimo-cli/references/content.md +61 -2
  10. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  11. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  12. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  13. package/assets/skills/fimo-migration/SKILL.md +24 -2
  14. package/assets/skills/fimo-migration/references/content.md +24 -0
  15. package/assets/skills/fimo-migration/references/manifest.md +46 -0
  16. package/assets/skills/fimo-migration/references/pages.md +234 -0
  17. package/assets/skills/fimo-migration/references/qa.md +24 -2
  18. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  19. package/assets/skills/fimo-migration/references/wrap.md +662 -0
  20. package/assets/skills/fimo-studio/SKILL.md +5 -3
  21. package/assets/skills/fimo-studio/references/cms.md +9 -1
  22. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  23. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  24. package/dist/build/vite/plugins/translations.d.ts +2 -8
  25. package/dist/build/vite/plugins/translations.js +31 -131
  26. package/dist/cli/bundle.json +1 -1
  27. package/dist/cli/index.js +1337 -189
  28. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  29. package/dist/runtime/app/FimoProviders.js +31 -30
  30. package/dist/runtime/astro/index.d.ts +2 -0
  31. package/dist/runtime/astro/index.js +3 -0
  32. package/dist/runtime/astro/server.d.ts +3 -0
  33. package/dist/runtime/astro/server.js +6 -0
  34. package/dist/runtime/content/client/internal.js +1 -0
  35. package/dist/runtime/content/client/types.d.ts +16 -0
  36. package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
  37. package/dist/runtime/core/BuiltWithBadge.js +68 -0
  38. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  39. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  40. package/dist/runtime/next/FimoNextProviders.js +20 -0
  41. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  42. package/dist/runtime/next/FimoNextScripts.js +19 -0
  43. package/dist/runtime/next/index.d.ts +14 -0
  44. package/dist/runtime/next/index.js +14 -0
  45. package/dist/runtime/next/server.d.ts +2 -0
  46. package/dist/runtime/next/server.js +4 -0
  47. package/dist/runtime/nuxt/composables.d.ts +9 -0
  48. package/dist/runtime/nuxt/composables.js +57 -0
  49. package/dist/runtime/nuxt/index.d.ts +3 -0
  50. package/dist/runtime/nuxt/index.js +6 -0
  51. package/dist/runtime/nuxt/server.d.ts +3 -0
  52. package/dist/runtime/nuxt/server.js +6 -0
  53. package/dist/runtime/primitives/components/RichText.d.ts +5 -1
  54. package/dist/runtime/primitives/components/RichText.js +2 -2
  55. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  56. package/dist/runtime/primitives/labels-hook.js +41 -0
  57. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  58. package/dist/runtime/primitives/labels-store.js +60 -0
  59. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  60. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  61. package/dist/runtime/primitives/translations.d.ts +1 -27
  62. package/dist/runtime/primitives/translations.js +7 -85
  63. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  64. package/dist/runtime/react/FimoSpaProviders.js +122 -0
  65. package/dist/runtime/react/index.d.ts +35 -0
  66. package/dist/runtime/react/index.js +40 -0
  67. package/dist/runtime/react/spa-content-cache.d.ts +38 -0
  68. package/dist/runtime/react/spa-content-cache.js +64 -0
  69. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  70. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  71. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  72. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  73. package/dist/runtime/remix/index.d.ts +12 -0
  74. package/dist/runtime/remix/index.js +11 -0
  75. package/dist/runtime/remix/server.d.ts +2 -0
  76. package/dist/runtime/remix/server.js +2 -0
  77. package/dist/runtime/seo/neutral.d.ts +22 -0
  78. package/dist/runtime/seo/neutral.js +17 -0
  79. package/dist/runtime/shared/display-routes.d.ts +14 -0
  80. package/dist/runtime/shared/display-routes.js +25 -0
  81. package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
  82. package/dist/runtime/shared/fimo-config.server.js +1 -13
  83. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  84. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  85. package/dist/runtime/shared/i18n-messages.js +79 -0
  86. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  87. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  88. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  89. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  90. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  91. package/dist/runtime/shared/labels-t.pure.js +10 -0
  92. package/dist/runtime/strapi/client.d.ts +30 -0
  93. package/dist/runtime/strapi/client.js +132 -0
  94. package/dist/runtime/strapi/index.d.ts +5 -0
  95. package/dist/runtime/strapi/index.js +6 -0
  96. package/dist/runtime/strapi/reshape.d.ts +50 -0
  97. package/dist/runtime/strapi/reshape.js +121 -0
  98. package/dist/runtime/svelte/index.d.ts +3 -0
  99. package/dist/runtime/svelte/index.js +7 -0
  100. package/dist/runtime/svelte/server.d.ts +3 -0
  101. package/dist/runtime/svelte/server.js +6 -0
  102. package/dist/runtime/vue/index.d.ts +3 -0
  103. package/dist/runtime/vue/index.js +5 -0
  104. package/dist/runtime/vue/server.d.ts +3 -0
  105. package/dist/runtime/vue/server.js +6 -0
  106. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  107. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  108. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  109. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  110. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  111. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  112. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
  113. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  114. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  115. package/dist/runtime/wrapped/FimoText.js +20 -0
  116. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  117. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  118. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  119. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  120. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  121. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  122. package/dist/runtime/wrapped/dom.d.ts +4 -0
  123. package/dist/runtime/wrapped/dom.js +23 -0
  124. package/dist/runtime/wrapped/server.d.ts +80 -0
  125. package/dist/runtime/wrapped/server.js +207 -0
  126. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  127. package/dist/scripts/apply-wrap-templates.js +137 -0
  128. package/dist/scripts/apply-wrap.d.ts +73 -0
  129. package/dist/scripts/apply-wrap.js +340 -0
  130. package/dist/scripts/cms/discover.d.ts +2 -0
  131. package/dist/scripts/cms/discover.js +17 -0
  132. package/dist/scripts/cms/example-content.d.ts +33 -0
  133. package/dist/scripts/cms/example-content.js +165 -0
  134. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  135. package/dist/scripts/cms/strapi/detect.js +186 -0
  136. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  137. package/dist/scripts/cms/strapi/index.js +25 -0
  138. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  139. package/dist/scripts/cms/strapi/list-source.js +90 -0
  140. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  141. package/dist/scripts/cms/strapi/schema-map.js +289 -0
  142. package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
  143. package/dist/scripts/cms/strapi/shapes.js +48 -0
  144. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  145. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  146. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  147. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  148. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  149. package/dist/scripts/cms/strapi/wiring.js +46 -0
  150. package/dist/scripts/cms/types.d.ts +145 -0
  151. package/dist/scripts/cms/types.js +11 -0
  152. package/dist/scripts/detect-framework.d.ts +39 -0
  153. package/dist/scripts/detect-framework.js +139 -0
  154. package/dist/scripts/draft-rows.d.ts +19 -0
  155. package/dist/scripts/draft-rows.js +178 -0
  156. package/dist/scripts/draft-schemas.d.ts +27 -0
  157. package/dist/scripts/draft-schemas.js +247 -0
  158. package/dist/scripts/extract-translations.js +16 -11
  159. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  160. package/dist/scripts/frameworks/astro/routes.js +44 -0
  161. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  162. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  163. package/dist/scripts/frameworks/discover.d.ts +83 -0
  164. package/dist/scripts/frameworks/discover.js +288 -0
  165. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  166. package/dist/scripts/frameworks/discovered-route.js +17 -0
  167. package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
  168. package/dist/scripts/frameworks/next/rewire.js +282 -0
  169. package/dist/scripts/frameworks/next/routes.d.ts +30 -0
  170. package/dist/scripts/frameworks/next/routes.js +176 -0
  171. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  172. package/dist/scripts/frameworks/next/wiring.js +186 -0
  173. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  174. package/dist/scripts/frameworks/nuxt/routes.js +38 -0
  175. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  176. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  177. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  178. package/dist/scripts/frameworks/remix/routes.js +36 -0
  179. package/dist/scripts/frameworks/support.d.ts +75 -0
  180. package/dist/scripts/frameworks/support.js +165 -0
  181. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  182. package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
  183. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  184. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  185. package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
  186. package/dist/scripts/frameworks/vite/routes.js +143 -0
  187. package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
  188. package/dist/scripts/frameworks/vite/wiring.js +144 -0
  189. package/dist/scripts/i18n-namespace.d.ts +27 -0
  190. package/dist/scripts/i18n-namespace.js +155 -0
  191. package/dist/scripts/lib/humanize.d.ts +14 -0
  192. package/dist/scripts/lib/humanize.js +54 -0
  193. package/dist/scripts/lib/validation-report.d.ts +1 -3
  194. package/dist/scripts/lib/validation-report.js +3 -9
  195. package/dist/scripts/render-parity/capture.d.ts +64 -0
  196. package/dist/scripts/render-parity/capture.js +436 -0
  197. package/dist/scripts/render-parity/compare.d.ts +59 -0
  198. package/dist/scripts/render-parity/compare.js +0 -0
  199. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  200. package/dist/scripts/render-parity/normalize-html.js +223 -0
  201. package/dist/scripts/render-parity/types.d.ts +41 -0
  202. package/dist/scripts/render-parity/types.js +1 -0
  203. package/dist/scripts/routes/bindings.d.ts +93 -0
  204. package/dist/scripts/routes/bindings.js +117 -0
  205. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  206. package/dist/scripts/routes/label-prefixes.js +136 -0
  207. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  208. package/dist/scripts/routes/module-resolve.js +78 -0
  209. package/dist/scripts/routes/page-structure.d.ts +72 -0
  210. package/dist/scripts/routes/page-structure.js +234 -0
  211. package/dist/scripts/scan-wrappable.d.ts +200 -0
  212. package/dist/scripts/scan-wrappable.js +1355 -0
  213. package/dist/scripts/validate-route-metadata.js +11 -0
  214. package/dist/scripts/validate-schemas.js +42 -1
  215. package/package.json +88 -4
  216. package/release.json +2 -2
  217. package/templates/react-router/package.json +1 -1
  218. package/templates/react-router/translations/en.json +1 -0
@@ -0,0 +1,223 @@
1
+ import * as cheerio from 'cheerio';
2
+ // `title` included: Next dev streams document metadata into the body end
3
+ // until the shell settles — title text is never visible page content, and
4
+ // whether it lands in head or body depends on layout asyncness (a wrap
5
+ // migration legitimately makes the root layout async).
6
+ const REMOVED_TAGS = new Set([
7
+ 'script',
8
+ 'style',
9
+ 'noscript',
10
+ 'template',
11
+ 'link',
12
+ 'meta',
13
+ 'svg',
14
+ 'iframe',
15
+ 'head',
16
+ 'title',
17
+ ]);
18
+ const BLOCK_TAGS = new Set([
19
+ 'p',
20
+ 'h1',
21
+ 'h2',
22
+ 'h3',
23
+ 'h4',
24
+ 'h5',
25
+ 'h6',
26
+ 'li',
27
+ 'td',
28
+ 'th',
29
+ 'dt',
30
+ 'dd',
31
+ 'blockquote',
32
+ 'figcaption',
33
+ 'pre',
34
+ 'button',
35
+ 'label',
36
+ 'summary',
37
+ 'caption',
38
+ 'option',
39
+ ]);
40
+ const FALLBACK_BLOCK_TAGS = new Set(['div', 'section', 'article', 'main', 'aside', 'header', 'footer', 'nav', 'body']);
41
+ const MARK_KIND = {
42
+ strong: 'strong',
43
+ b: 'strong',
44
+ em: 'em',
45
+ i: 'em',
46
+ u: 'u',
47
+ s: 's',
48
+ del: 's',
49
+ strike: 's',
50
+ code: 'code',
51
+ a: 'a',
52
+ };
53
+ function collapse(text) {
54
+ return text.replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
55
+ }
56
+ /** Strip the capture origin so differing ports between runs compare equal. */
57
+ export function normalizeHref(href, origin) {
58
+ if (href.startsWith(origin)) {
59
+ const stripped = href.slice(origin.length);
60
+ return stripped.startsWith('/') ? stripped : `/${stripped}`;
61
+ }
62
+ return href;
63
+ }
64
+ /**
65
+ * Media src normalization: origin strip + Next image-optimizer decode, so a
66
+ * pre-wrap `next/image` (`/_next/image?url=…&w=…`) and a post-wrap plain
67
+ * `<img src>` of the same asset compare equal.
68
+ */
69
+ export function normalizeMediaSrc(src, origin) {
70
+ let value = normalizeHref(src, origin);
71
+ const optimizerMatch = value.match(/^\/_next\/image\?(.+)$/);
72
+ if (optimizerMatch) {
73
+ const params = new URLSearchParams(optimizerMatch[1]);
74
+ const url = params.get('url');
75
+ if (url) {
76
+ value = url;
77
+ }
78
+ }
79
+ return value;
80
+ }
81
+ function isExcluded(node) {
82
+ const tag = node.name?.toLowerCase() ?? '';
83
+ if (REMOVED_TAGS.has(tag)) {
84
+ return true;
85
+ }
86
+ // Hidden elements are not visible content — except Next streaming
87
+ // containers (div[hidden][id^="S:"]), whose content replaces Suspense
88
+ // holes; kept in document order so streamed content is captured
89
+ // symmetrically between baseline and check.
90
+ if (node.attribs && 'hidden' in node.attribs) {
91
+ const isStreamingContainer = tag === 'div' && (node.attribs.id ?? '').startsWith('S:');
92
+ if (!isStreamingContainer) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ function nearestBlock(node) {
99
+ let current = node.parent;
100
+ let fallback = null;
101
+ while (current) {
102
+ const tag = current.name?.toLowerCase();
103
+ if (tag && BLOCK_TAGS.has(tag)) {
104
+ return current;
105
+ }
106
+ if (!fallback && tag && FALLBACK_BLOCK_TAGS.has(tag)) {
107
+ fallback = current;
108
+ }
109
+ current = current.parent;
110
+ }
111
+ return fallback;
112
+ }
113
+ function collectText(node) {
114
+ if (node.type === 'text') {
115
+ return String(node.data ?? '');
116
+ }
117
+ if (node.type !== 'tag' || isExcluded(node)) {
118
+ return '';
119
+ }
120
+ return (node.children ?? []).map(collectText).join('');
121
+ }
122
+ export function extractRouteSignature(html, opts) {
123
+ const $ = cheerio.load(html);
124
+ const bodyNode = $('body').get(0);
125
+ const blocks = [];
126
+ const blockIndex = new Map();
127
+ const images = [];
128
+ const videos = [];
129
+ const links = [];
130
+ const visit = (node) => {
131
+ if (node.type === 'text') {
132
+ const text = collapse(String(node.data ?? ''));
133
+ if (!text) {
134
+ return;
135
+ }
136
+ const block = nearestBlock(node);
137
+ if (!block) {
138
+ return;
139
+ }
140
+ const existing = blockIndex.get(block);
141
+ if (existing === undefined) {
142
+ blockIndex.set(block, blocks.length);
143
+ blocks.push({ tag: block.name?.toLowerCase() ?? 'div', text, marks: [] });
144
+ }
145
+ else {
146
+ blocks[existing].text = collapse(`${blocks[existing].text} ${text}`);
147
+ }
148
+ return;
149
+ }
150
+ if (node.type !== 'tag' && node.type !== 'root') {
151
+ return;
152
+ }
153
+ if (node.type === 'tag' && isExcluded(node)) {
154
+ return;
155
+ }
156
+ const tag = node.name?.toLowerCase() ?? '';
157
+ const attribs = node.attribs ?? {};
158
+ // Inline-marks signature: the formatting that must survive wrapping and
159
+ // the TipTap seed round-trip. Attached to the nearest block.
160
+ const markKind = MARK_KIND[tag];
161
+ if (markKind) {
162
+ const text = collapse(collectText(node));
163
+ if (text) {
164
+ const block = nearestBlock(node);
165
+ if (block) {
166
+ if (!blockIndex.has(block)) {
167
+ blockIndex.set(block, blocks.length);
168
+ blocks.push({ tag: block.name?.toLowerCase() ?? 'div', text: '', marks: [] });
169
+ }
170
+ const mark = { kind: markKind, text };
171
+ if (markKind === 'a') {
172
+ if (attribs.href) {
173
+ mark.href = normalizeHref(attribs.href, opts.origin);
174
+ blocks[blockIndex.get(block)].marks.push(mark);
175
+ }
176
+ }
177
+ else {
178
+ blocks[blockIndex.get(block)].marks.push(mark);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ if (tag === 'img' && attribs.src) {
184
+ images.push(normalizeMediaSrc(attribs.src, opts.origin));
185
+ }
186
+ if (tag === 'source' && attribs.src) {
187
+ const parentTag = node.parent?.name?.toLowerCase();
188
+ if (parentTag === 'picture') {
189
+ images.push(normalizeMediaSrc(attribs.src, opts.origin));
190
+ }
191
+ else if (parentTag === 'video') {
192
+ videos.push(normalizeMediaSrc(attribs.src, opts.origin));
193
+ }
194
+ }
195
+ if (tag === 'video') {
196
+ if (attribs.src) {
197
+ videos.push(normalizeMediaSrc(attribs.src, opts.origin));
198
+ }
199
+ if (attribs.poster) {
200
+ videos.push(normalizeMediaSrc(attribs.poster, opts.origin));
201
+ }
202
+ }
203
+ if (tag === 'a' && attribs.href) {
204
+ links.push({ href: normalizeHref(attribs.href, opts.origin), text: collapse(collectText(node)) });
205
+ }
206
+ for (const child of node.children ?? []) {
207
+ visit(child);
208
+ }
209
+ };
210
+ if (bodyNode) {
211
+ for (const child of bodyNode.children ?? []) {
212
+ visit(child);
213
+ }
214
+ }
215
+ // Blocks created empty by a mark preceding any direct text (e.g. a <p>
216
+ // starting with <strong>) get their text from the block's full content.
217
+ for (const [block, index] of blockIndex) {
218
+ if (!blocks[index].text) {
219
+ blocks[index].text = collapse(collectText(block));
220
+ }
221
+ }
222
+ return { status: opts.status, blocks: blocks.filter((block) => block.text.length > 0), images, videos, links };
223
+ }
@@ -0,0 +1,41 @@
1
+ export declare const PARITY_BASELINE_VERSION = 1;
2
+ export interface InlineMark {
3
+ kind: 'strong' | 'em' | 'u' | 's' | 'code' | 'a';
4
+ text: string;
5
+ href?: string;
6
+ }
7
+ export interface TextBlock {
8
+ tag: string;
9
+ text: string;
10
+ marks: InlineMark[];
11
+ }
12
+ export interface RouteSignature {
13
+ status: number;
14
+ blocks: TextBlock[];
15
+ images: string[];
16
+ videos: string[];
17
+ links: Array<{
18
+ href: string;
19
+ text: string;
20
+ }>;
21
+ }
22
+ export type RouteCapture = {
23
+ signature: RouteSignature;
24
+ } | {
25
+ skipped: 'dynamic-params' | 'fetch-failed';
26
+ error?: string;
27
+ };
28
+ /**
29
+ * Normalized pre-wrap snapshot of every route's rendered content. Written to
30
+ * `.fimo/migration/parity-baseline.json`; `fimo migrate wrap --check`
31
+ * recaptures with the identical pipeline and compares.
32
+ */
33
+ export interface ParityBaseline {
34
+ version: typeof PARITY_BASELINE_VERSION;
35
+ capturedAt: string;
36
+ engine: 'dev-server';
37
+ framework: string | null;
38
+ command: string;
39
+ unwrappedAtCapture?: number;
40
+ routes: Record<string, RouteCapture>;
41
+ }
@@ -0,0 +1 @@
1
+ export const PARITY_BASELINE_VERSION = 1;
@@ -0,0 +1,93 @@
1
+ import type { CmsCollectionSchema } from '../cms/types.js';
2
+ /**
3
+ * Page-binding evidence for the page registry (`fimo-config.json#routes`).
4
+ * A dynamic page template (`/blog/:slug`) is only navigable in the Studio
5
+ * preview once its content param carries a binding
6
+ * `{ schema, queryField, displayField }` — the schema whose entries
7
+ * materialize the page instances. No binding can be inferred with certainty,
8
+ * so this module grades evidence: `strong` bindings are written by the
9
+ * machine, `candidate` ones ship to the agent prompt as ready-to-apply
10
+ * suggestions. Neutral: evidence comes in as plain route/schema facts; the
11
+ * framework and CMS specifics stay in their adapters.
12
+ */
13
+ export interface PageParamBinding {
14
+ schema: string;
15
+ queryField: string;
16
+ displayField: string;
17
+ }
18
+ export interface PageBinding {
19
+ routeId: string;
20
+ path: string;
21
+ param: string;
22
+ binding: PageParamBinding;
23
+ confidence: 'strong' | 'candidate';
24
+ source: 'rewire' | 'cms-wiring';
25
+ }
26
+ export interface UnboundPage {
27
+ id: string;
28
+ path: string;
29
+ candidates: PageBinding[];
30
+ }
31
+ export interface PageBindingsResult {
32
+ /** Written into the registry by the caller. */
33
+ strong: PageBinding[];
34
+ /** Dynamic pages left without params — with any candidate suggestions. */
35
+ unbound: UnboundPage[];
36
+ /** Dynamic pages that already carry params (manual or a prior run). */
37
+ bound: Array<{
38
+ id: string;
39
+ path: string;
40
+ }>;
41
+ }
42
+ /** A list page known to render a collection (the page-rewire join). */
43
+ export interface ListPageEvidence {
44
+ routePath: string | null;
45
+ schema: string;
46
+ }
47
+ export interface SchemaFieldsLike {
48
+ readonly [field: string]: {
49
+ readonly type?: string;
50
+ };
51
+ }
52
+ export declare function isLocaleParam(name: string): boolean;
53
+ interface ParsedDynamicPath {
54
+ /** The trailing content param name (without `:`/`*`). */
55
+ contentParam: string;
56
+ /** Route path minus the trailing content-param segment. */
57
+ parentPath: string;
58
+ }
59
+ /**
60
+ * A bindable dynamic path = optional locale-ish params + ONE trailing content
61
+ * param (catch-all or plain). Anything else (multiple content params) needs
62
+ * judgment beyond a single binding.
63
+ */
64
+ export declare function parseDynamicPath(path: string): ParsedDynamicPath | null;
65
+ /** First title-ish string field of a schema — the picker label the Studio
66
+ * shows. Null when the schema has none (binding downgrades to candidate). */
67
+ export declare function displayFieldFor(fields: SchemaFieldsLike | undefined): string | null;
68
+ /** CMS collections that structurally look like page-builder page types: a
69
+ * top-level dynamic-zone (blocks oneOf) field. Structural, never name-based. */
70
+ export declare function cmsPageTypeEvidence(schemas: readonly CmsCollectionSchema[]): Array<{
71
+ schema: string;
72
+ fields: SchemaFieldsLike;
73
+ }>;
74
+ export declare function collectPageBindings(options: {
75
+ routes: Record<string, {
76
+ path?: string;
77
+ params?: unknown;
78
+ }>;
79
+ /** Rewired list pages (route-space, from the framework adapter). */
80
+ listPages?: readonly ListPageEvidence[];
81
+ /** Structural CMS page types (from `cmsPageTypeEvidence`), only when the
82
+ * CMS reads are actually wired to Fimo. */
83
+ cmsPageTypes?: ReadonlyArray<{
84
+ schema: string;
85
+ fields: SchemaFieldsLike;
86
+ }>;
87
+ /** Uids still provisional (machine drafts not yet adopted) — their bindings
88
+ * are candidates: `adopt-drafts` will rename them. */
89
+ provisionalUids?: ReadonlySet<string>;
90
+ /** Known fields per schema, for the displayField pick. */
91
+ schemaFields?: Record<string, SchemaFieldsLike>;
92
+ }): PageBindingsResult;
93
+ export {};
@@ -0,0 +1,117 @@
1
+ /** Params that identify a locale, not content — never schema-bound. The
2
+ * Studio preview substitutes content params only; locale params are a known
3
+ * limitation documented in the prompt. Mirrors the parity capture's list. */
4
+ const LOCALE_PARAMS = /^(locale|lang|language)$/i;
5
+ export function isLocaleParam(name) {
6
+ return LOCALE_PARAMS.test(name);
7
+ }
8
+ const TITLE_FIELDS = /^(title|name|label|heading)$/i;
9
+ /**
10
+ * A bindable dynamic path = optional locale-ish params + ONE trailing content
11
+ * param (catch-all or plain). Anything else (multiple content params) needs
12
+ * judgment beyond a single binding.
13
+ */
14
+ export function parseDynamicPath(path) {
15
+ const params = [...path.matchAll(/:([^/*]+)\*?/g)].map((match) => match[1]);
16
+ if (params.length === 0) {
17
+ return null;
18
+ }
19
+ const contentParams = params.filter((param) => !LOCALE_PARAMS.test(param));
20
+ if (contentParams.length !== 1) {
21
+ return null;
22
+ }
23
+ const contentParam = contentParams[0];
24
+ if (!new RegExp(`/:${contentParam}\\*?$`).test(path)) {
25
+ return null; // the content param must be the trailing segment
26
+ }
27
+ const parentPath = path.replace(new RegExp(`/:${contentParam}\\*?$`), '') || '/';
28
+ return { contentParam, parentPath };
29
+ }
30
+ function isCatchAll(path, param) {
31
+ return path.endsWith(`:${param}*`);
32
+ }
33
+ /** First title-ish string field of a schema — the picker label the Studio
34
+ * shows. Null when the schema has none (binding downgrades to candidate). */
35
+ export function displayFieldFor(fields) {
36
+ for (const [name, def] of Object.entries(fields ?? {})) {
37
+ if (TITLE_FIELDS.test(name) && (def.type === undefined || def.type === 'string')) {
38
+ return name;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ /** CMS collections that structurally look like page-builder page types: a
44
+ * top-level dynamic-zone (blocks oneOf) field. Structural, never name-based. */
45
+ export function cmsPageTypeEvidence(schemas) {
46
+ return schemas
47
+ .filter((schema) => !schema.isSingleton &&
48
+ Object.values(schema.fields).some((field) => field.type === 'blocks' && 'oneOf' in field.schema))
49
+ .map((schema) => ({ schema: schema.name, fields: schema.fields }));
50
+ }
51
+ export function collectPageBindings(options) {
52
+ const { routes, listPages = [], cmsPageTypes = [], provisionalUids, schemaFields = {} } = options;
53
+ const strong = [];
54
+ const unbound = [];
55
+ const bound = [];
56
+ for (const [id, route] of Object.entries(routes)) {
57
+ const path = route.path;
58
+ if (typeof path !== 'string' || !path.includes(':')) {
59
+ continue;
60
+ }
61
+ if (route.params && typeof route.params === 'object' && Object.keys(route.params).length > 0) {
62
+ bound.push({ id, path });
63
+ continue;
64
+ }
65
+ const params = [...path.matchAll(/:([^/*]+)\*?/g)].map((match) => match[1]);
66
+ if (params.every((param) => isLocaleParam(param))) {
67
+ continue; // locale-only page — nothing to bind, effectively static
68
+ }
69
+ const parsed = parseDynamicPath(path);
70
+ if (!parsed) {
71
+ unbound.push({ id, path, candidates: [] });
72
+ continue;
73
+ }
74
+ const candidates = [];
75
+ const make = (schema, source, baseConfidence) => {
76
+ const fields = schemaFields[schema] ?? cmsPageTypes.find((type) => type.schema === schema)?.fields;
77
+ const display = displayFieldFor(fields);
78
+ const confidence = provisionalUids?.has(schema) || display === null ? 'candidate' : baseConfidence;
79
+ return {
80
+ routeId: id,
81
+ path,
82
+ param: parsed.contentParam,
83
+ binding: { schema, queryField: 'slug', displayField: display ?? 'slug' },
84
+ confidence,
85
+ source,
86
+ };
87
+ };
88
+ // Rewire join: a list page rendering collection X at the dynamic page's
89
+ // parent path is strong evidence the detail param resolves against X.
90
+ const parents = listPages.filter((page) => page.routePath === parsed.parentPath);
91
+ const parentSchemas = [...new Set(parents.map((page) => page.schema))];
92
+ if (parentSchemas.length === 1) {
93
+ candidates.push(make(parentSchemas[0], 'rewire', 'strong'));
94
+ }
95
+ else {
96
+ for (const schema of parentSchemas) {
97
+ candidates.push(make(schema, 'rewire', 'candidate'));
98
+ }
99
+ }
100
+ // CMS-structural join: a catch-all page of a CMS-wired site renders the
101
+ // CMS's page-builder type. Unique page-builder type → strong.
102
+ if (isCatchAll(path, parsed.contentParam) && cmsPageTypes.length > 0) {
103
+ const unique = cmsPageTypes.length === 1;
104
+ for (const type of cmsPageTypes) {
105
+ candidates.push(make(type.schema, 'cms-wiring', unique ? 'strong' : 'candidate'));
106
+ }
107
+ }
108
+ const winner = candidates.find((candidate) => candidate.confidence === 'strong');
109
+ if (winner && candidates.filter((candidate) => candidate.confidence === 'strong').length === 1) {
110
+ strong.push(winner);
111
+ }
112
+ else {
113
+ unbound.push({ id, path, candidates });
114
+ }
115
+ }
116
+ return { strong, unbound, bound };
117
+ }
@@ -0,0 +1,49 @@
1
+ import { type RouteFileResolution, type RouteLike } from './module-resolve.js';
2
+ /**
3
+ * The route ↔ file ↔ label-keys join. A page's Studio view resolves its
4
+ * labels by key prefix, defaulting to `<pageId>.` — but wrapped keys derive
5
+ * from FILE names (AiCms.tsx → ai-cms.*), which can diverge from the
6
+ * URL-derived page id (agentic-cms). Every divergence left unmapped is an
7
+ * EMPTY page in the Studio and an orphan pile in Labels & Text, so migrate
8
+ * prefills `labelPrefixes` wherever the join is provable and reports every
9
+ * staged prefix no page claims.
10
+ *
11
+ * Evidence rule: a page claims a prefix only when one of that prefix's
12
+ * staged keys appears VERBATIM in the page's own component file — never by
13
+ * name similarity. Shared prefixes (data files, chrome) match no page file
14
+ * and surface in the unclaimed report for the agent/user to place.
15
+ *
16
+ * This module is CORE: it never tests a framework name. The per-framework
17
+ * route-file resolvers live in `scripts/frameworks/<name>/routes.ts` behind
18
+ * the `frameworkRouteFiles` registry.
19
+ */
20
+ export { componentImports, resolveModuleFile, type RouteFileResolution } from './module-resolve.js';
21
+ export interface UnclaimedPrefix {
22
+ prefix: string;
23
+ exampleKey: string;
24
+ keys: number;
25
+ /** Shared site chrome (header/footer/nav/…) — belongs in a Single Type,
26
+ * never on a page and never left as loose labels. */
27
+ chrome: boolean;
28
+ }
29
+ export declare function isChromePrefix(prefix: string): boolean;
30
+ export declare function resolveRouteFiles(cwd: string, framework: string | null, routes: Record<string, RouteLike>): RouteFileResolution[];
31
+ /**
32
+ * For each resolved page file, the staged prefixes whose keys appear verbatim
33
+ * in it. Returns only the routes whose evidence DIFFERS from the default
34
+ * (`{routeId}`) — and never touches a route that already declares prefixes.
35
+ */
36
+ export declare function collectPageLabelPrefixes(resolutions: RouteFileResolution[], stagedKeys: Iterable<string>, routes: Record<string, RouteLike>): Map<string, string[]>;
37
+ /**
38
+ * Staged prefixes no page claims (neither as its id nor via labelPrefixes) —
39
+ * each is either shared content (leave in Labels & Text) or a missed page
40
+ * mapping the agent must place with `fimo pages set --label-prefix`.
41
+ */
42
+ export declare function unclaimedLabelPrefixes(stagedKeys: Iterable<string>, routes: Record<string, RouteLike>): UnclaimedPrefix[];
43
+ /**
44
+ * Page ↔ Single Type claims, by verbatim read evidence: a page owns single S
45
+ * only when its component file provably READS S's document (`documents/S`,
46
+ * `useFimoSingle('S')`, `fetchDocument('S')`). Claimed singles edit inline
47
+ * in the Studio's page view; unclaimed ones stay in the Singles tab.
48
+ */
49
+ export declare function collectPageSingles(resolutions: RouteFileResolution[], singletonUids: string[], routes: Record<string, RouteLike>): Map<string, string[]>;
@@ -0,0 +1,136 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { frameworkRouteFiles } from '../frameworks/discover.js';
3
+ /**
4
+ * The route ↔ file ↔ label-keys join. A page's Studio view resolves its
5
+ * labels by key prefix, defaulting to `<pageId>.` — but wrapped keys derive
6
+ * from FILE names (AiCms.tsx → ai-cms.*), which can diverge from the
7
+ * URL-derived page id (agentic-cms). Every divergence left unmapped is an
8
+ * EMPTY page in the Studio and an orphan pile in Labels & Text, so migrate
9
+ * prefills `labelPrefixes` wherever the join is provable and reports every
10
+ * staged prefix no page claims.
11
+ *
12
+ * Evidence rule: a page claims a prefix only when one of that prefix's
13
+ * staged keys appears VERBATIM in the page's own component file — never by
14
+ * name similarity. Shared prefixes (data files, chrome) match no page file
15
+ * and surface in the unclaimed report for the agent/user to place.
16
+ *
17
+ * This module is CORE: it never tests a framework name. The per-framework
18
+ * route-file resolvers live in `scripts/frameworks/<name>/routes.ts` behind
19
+ * the `frameworkRouteFiles` registry.
20
+ */
21
+ export { componentImports, resolveModuleFile } from './module-resolve.js';
22
+ const CHROME_PREFIX = /(header|footer|nav|menu|topbar|sidebar|banner|cookie|breadcrumb|social)/i;
23
+ export function isChromePrefix(prefix) {
24
+ return CHROME_PREFIX.test(prefix);
25
+ }
26
+ export function resolveRouteFiles(cwd, framework, routes) {
27
+ return frameworkRouteFiles(framework)?.(cwd, routes) ?? [];
28
+ }
29
+ function keyPrefix(key) {
30
+ const dot = key.indexOf('.');
31
+ return dot > 0 ? key.slice(0, dot) : null;
32
+ }
33
+ function groupKeysByPrefix(stagedKeys) {
34
+ const groups = new Map();
35
+ for (const key of stagedKeys) {
36
+ const prefix = keyPrefix(key);
37
+ if (!prefix) {
38
+ continue;
39
+ }
40
+ groups.set(prefix, [...(groups.get(prefix) ?? []), key]);
41
+ }
42
+ return groups;
43
+ }
44
+ /**
45
+ * For each resolved page file, the staged prefixes whose keys appear verbatim
46
+ * in it. Returns only the routes whose evidence DIFFERS from the default
47
+ * (`{routeId}`) — and never touches a route that already declares prefixes.
48
+ */
49
+ export function collectPageLabelPrefixes(resolutions, stagedKeys, routes) {
50
+ const groups = groupKeysByPrefix(stagedKeys);
51
+ const updates = new Map();
52
+ for (const { routeId, file } of resolutions) {
53
+ const route = routes[routeId];
54
+ if (!route || (Array.isArray(route.labelPrefixes) && route.labelPrefixes.length > 0)) {
55
+ continue;
56
+ }
57
+ let code;
58
+ try {
59
+ code = readFileSync(file, 'utf8');
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ const matched = [];
65
+ for (const [prefix, keys] of groups) {
66
+ if (keys.some((key) => code.includes(key))) {
67
+ matched.push(prefix);
68
+ }
69
+ }
70
+ matched.sort();
71
+ if (matched.length > 0 && (matched.length > 1 || matched[0] !== routeId)) {
72
+ updates.set(routeId, matched);
73
+ }
74
+ }
75
+ return updates;
76
+ }
77
+ /**
78
+ * Staged prefixes no page claims (neither as its id nor via labelPrefixes) —
79
+ * each is either shared content (leave in Labels & Text) or a missed page
80
+ * mapping the agent must place with `fimo pages set --label-prefix`.
81
+ */
82
+ export function unclaimedLabelPrefixes(stagedKeys, routes) {
83
+ const claimed = new Set();
84
+ for (const [routeId, route] of Object.entries(routes)) {
85
+ claimed.add(routeId);
86
+ if (Array.isArray(route?.labelPrefixes)) {
87
+ for (const prefix of route.labelPrefixes) {
88
+ if (typeof prefix === 'string') {
89
+ claimed.add(prefix.replace(/\.$/, ''));
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return [...groupKeysByPrefix(stagedKeys)]
95
+ .filter(([prefix]) => !claimed.has(prefix))
96
+ .map(([prefix, keys]) => ({ prefix, exampleKey: keys[0], keys: keys.length, chrome: isChromePrefix(prefix) }))
97
+ .sort((a, b) => a.prefix.localeCompare(b.prefix));
98
+ }
99
+ /**
100
+ * Page ↔ Single Type claims, by verbatim read evidence: a page owns single S
101
+ * only when its component file provably READS S's document (`documents/S`,
102
+ * `useFimoSingle('S')`, `fetchDocument('S')`). Claimed singles edit inline
103
+ * in the Studio's page view; unclaimed ones stay in the Singles tab.
104
+ */
105
+ export function collectPageSingles(resolutions, singletonUids, routes) {
106
+ const updates = new Map();
107
+ if (singletonUids.length === 0) {
108
+ return updates;
109
+ }
110
+ for (const { routeId, file } of resolutions) {
111
+ const route = routes[routeId];
112
+ if (!route || (Array.isArray(route.singles) && route.singles.length > 0)) {
113
+ continue;
114
+ }
115
+ let code;
116
+ try {
117
+ code = readFileSync(file, 'utf8');
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ const claimed = singletonUids
123
+ .filter((uid) => [
124
+ `documents/${uid}`,
125
+ `useFimoSingle('${uid}'`,
126
+ `useFimoSingle("${uid}"`,
127
+ `fetchDocument('${uid}'`,
128
+ `fetchDocument("${uid}"`,
129
+ ].some((marker) => code.includes(marker)))
130
+ .sort();
131
+ if (claimed.length > 0) {
132
+ updates.set(routeId, claimed);
133
+ }
134
+ }
135
+ return updates;
136
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Framework-agnostic source-tree helpers shared by the per-framework route
3
+ * resolvers and the page-structure drafter: file walking, import maps, and
4
+ * module-specifier resolution. No framework name may appear here — framework
5
+ * knowledge lives in `scripts/frameworks/<name>/` behind the registry.
6
+ */
7
+ export interface RouteFileResolution {
8
+ routeId: string;
9
+ file: string;
10
+ }
11
+ export interface RouteLike {
12
+ path?: string;
13
+ labelPrefixes?: unknown;
14
+ [key: string]: unknown;
15
+ }
16
+ export declare const DEFAULT_COMPONENT_EXTENSIONS: string[];
17
+ export declare function walkFiles(dir: string, matches: (name: string) => boolean, out: string[]): void;
18
+ /** Import map of a routes file: local component name → module specifier. */
19
+ export declare function componentImports(code: string): Map<string, string>;
20
+ export declare function resolveModuleFile(cwd: string, fromFile: string, spec: string, extensions?: readonly string[]): string | null;