fimo 0.6.0 → 0.7.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 (297) hide show
  1. package/assets/skills/fimo/SKILL.md +2 -2
  2. package/assets/skills/fimo/references/locales.md +0 -8
  3. package/assets/skills/fimo/references/setup-react-router.md +2 -2
  4. package/assets/skills/fimo/references/ui.md +1 -1
  5. package/assets/skills/fimo-cli/references/content.md +0 -3
  6. package/assets/skills/fimo-cli/references/locales.md +0 -6
  7. package/assets/skills/fimo-migration/SKILL.md +22 -2
  8. package/assets/skills/fimo-migration/references/content.md +8 -0
  9. package/assets/skills/fimo-migration/references/manifest.md +35 -0
  10. package/assets/skills/fimo-migration/references/qa.md +24 -2
  11. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  12. package/assets/skills/fimo-migration/references/wrap.md +617 -0
  13. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  14. package/dist/build/vite/plugins/translations.d.ts +2 -8
  15. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  16. package/dist/build/vite/plugins/translations.js +3 -121
  17. package/dist/cli/bundle.json +1 -1
  18. package/dist/cli/index.js +29490 -21165
  19. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  20. package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
  21. package/dist/runtime/app/FimoProviders.js +2 -23
  22. package/dist/runtime/astro/index.d.ts +2 -0
  23. package/dist/runtime/astro/index.d.ts.map +1 -0
  24. package/dist/runtime/astro/index.js +3 -0
  25. package/dist/runtime/astro/server.d.ts +3 -0
  26. package/dist/runtime/astro/server.d.ts.map +1 -0
  27. package/dist/runtime/astro/server.js +6 -0
  28. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
  29. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  30. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  31. package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
  32. package/dist/runtime/next/FimoNextProviders.js +20 -0
  33. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  34. package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
  35. package/dist/runtime/next/FimoNextScripts.js +19 -0
  36. package/dist/runtime/next/index.d.ts +14 -0
  37. package/dist/runtime/next/index.d.ts.map +1 -0
  38. package/dist/runtime/next/index.js +14 -0
  39. package/dist/runtime/next/server.d.ts +2 -0
  40. package/dist/runtime/next/server.d.ts.map +1 -0
  41. package/dist/runtime/next/server.js +4 -0
  42. package/dist/runtime/nuxt/composables.d.ts +9 -0
  43. package/dist/runtime/nuxt/composables.d.ts.map +1 -0
  44. package/dist/runtime/nuxt/composables.js +57 -0
  45. package/dist/runtime/nuxt/index.d.ts +3 -0
  46. package/dist/runtime/nuxt/index.d.ts.map +1 -0
  47. package/dist/runtime/nuxt/index.js +6 -0
  48. package/dist/runtime/nuxt/server.d.ts +3 -0
  49. package/dist/runtime/nuxt/server.d.ts.map +1 -0
  50. package/dist/runtime/nuxt/server.js +6 -0
  51. package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
  52. package/dist/runtime/paths/get-fimo-paths.js +5 -15
  53. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  54. package/dist/runtime/primitives/labels-hook.d.ts.map +1 -0
  55. package/dist/runtime/primitives/labels-hook.js +41 -0
  56. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  57. package/dist/runtime/primitives/labels-store.d.ts.map +1 -0
  58. package/dist/runtime/primitives/labels-store.js +60 -0
  59. package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
  60. package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
  61. package/dist/runtime/primitives/labels-store.test.js +66 -0
  62. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  63. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
  64. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  65. package/dist/runtime/primitives/translations.d.ts +1 -27
  66. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  67. package/dist/runtime/primitives/translations.js +7 -85
  68. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  69. package/dist/runtime/react/FimoSpaProviders.d.ts.map +1 -0
  70. package/dist/runtime/react/FimoSpaProviders.js +63 -0
  71. package/dist/runtime/react/index.d.ts +33 -0
  72. package/dist/runtime/react/index.d.ts.map +1 -0
  73. package/dist/runtime/react/index.js +39 -0
  74. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  75. package/dist/runtime/remix/FimoRemixProviders.d.ts.map +1 -0
  76. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  77. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  78. package/dist/runtime/remix/FimoRemixScripts.d.ts.map +1 -0
  79. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  80. package/dist/runtime/remix/index.d.ts +12 -0
  81. package/dist/runtime/remix/index.d.ts.map +1 -0
  82. package/dist/runtime/remix/index.js +11 -0
  83. package/dist/runtime/remix/server.d.ts +2 -0
  84. package/dist/runtime/remix/server.d.ts.map +1 -0
  85. package/dist/runtime/remix/server.js +2 -0
  86. package/dist/runtime/seo/buildSeoForPath.d.ts +3 -1
  87. package/dist/runtime/seo/buildSeoForPath.d.ts.map +1 -1
  88. package/dist/runtime/seo/buildSeoForPath.js +1 -4
  89. package/dist/runtime/shared/display-routes.d.ts +14 -0
  90. package/dist/runtime/shared/display-routes.d.ts.map +1 -0
  91. package/dist/runtime/shared/display-routes.js +25 -0
  92. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  93. package/dist/runtime/shared/fimo-config.server.js +1 -0
  94. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  95. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  96. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  97. package/dist/runtime/shared/i18n-messages.d.ts.map +1 -0
  98. package/dist/runtime/shared/i18n-messages.js +79 -0
  99. package/dist/runtime/shared/i18n-messages.test.d.ts +2 -0
  100. package/dist/runtime/shared/i18n-messages.test.d.ts.map +1 -0
  101. package/dist/runtime/shared/i18n-messages.test.js +39 -0
  102. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  103. package/dist/runtime/shared/labels-fetch.pure.d.ts.map +1 -0
  104. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  105. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  106. package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
  107. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  108. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  109. package/dist/runtime/shared/labels-t.pure.d.ts.map +1 -0
  110. package/dist/runtime/shared/labels-t.pure.js +10 -0
  111. package/dist/runtime/strapi/client.d.ts +26 -0
  112. package/dist/runtime/strapi/client.d.ts.map +1 -0
  113. package/dist/runtime/strapi/client.js +88 -0
  114. package/dist/runtime/strapi/client.test.d.ts +2 -0
  115. package/dist/runtime/strapi/client.test.d.ts.map +1 -0
  116. package/dist/runtime/strapi/client.test.js +64 -0
  117. package/dist/runtime/strapi/index.d.ts +5 -0
  118. package/dist/runtime/strapi/index.d.ts.map +1 -0
  119. package/dist/runtime/strapi/index.js +6 -0
  120. package/dist/runtime/strapi/reshape.d.ts +28 -0
  121. package/dist/runtime/strapi/reshape.d.ts.map +1 -0
  122. package/dist/runtime/strapi/reshape.js +79 -0
  123. package/dist/runtime/strapi/reshape.test.d.ts +2 -0
  124. package/dist/runtime/strapi/reshape.test.d.ts.map +1 -0
  125. package/dist/runtime/strapi/reshape.test.js +33 -0
  126. package/dist/runtime/svelte/index.d.ts +3 -0
  127. package/dist/runtime/svelte/index.d.ts.map +1 -0
  128. package/dist/runtime/svelte/index.js +7 -0
  129. package/dist/runtime/svelte/server.d.ts +3 -0
  130. package/dist/runtime/svelte/server.d.ts.map +1 -0
  131. package/dist/runtime/svelte/server.js +6 -0
  132. package/dist/runtime/vue/index.d.ts +3 -0
  133. package/dist/runtime/vue/index.d.ts.map +1 -0
  134. package/dist/runtime/vue/index.js +5 -0
  135. package/dist/runtime/vue/server.d.ts +3 -0
  136. package/dist/runtime/vue/server.d.ts.map +1 -0
  137. package/dist/runtime/vue/server.js +6 -0
  138. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  139. package/dist/runtime/wrapped/FimoFileLink.d.ts.map +1 -0
  140. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  141. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  142. package/dist/runtime/wrapped/FimoMediaImage.d.ts.map +1 -0
  143. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  144. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  145. package/dist/runtime/wrapped/FimoMediaVideo.d.ts.map +1 -0
  146. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  147. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
  148. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts.map +1 -0
  149. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  150. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  151. package/dist/runtime/wrapped/FimoText.d.ts.map +1 -0
  152. package/dist/runtime/wrapped/FimoText.js +20 -0
  153. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  154. package/dist/runtime/wrapped/WrappedContentContext.d.ts.map +1 -0
  155. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  156. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  157. package/dist/runtime/wrapped/WrappedProviders.d.ts.map +1 -0
  158. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  159. package/dist/runtime/wrapped/dom.d.ts +4 -0
  160. package/dist/runtime/wrapped/dom.d.ts.map +1 -0
  161. package/dist/runtime/wrapped/dom.js +23 -0
  162. package/dist/runtime/wrapped/dom.test.d.ts +2 -0
  163. package/dist/runtime/wrapped/dom.test.d.ts.map +1 -0
  164. package/dist/runtime/wrapped/dom.test.js +16 -0
  165. package/dist/runtime/wrapped/server.d.ts +77 -0
  166. package/dist/runtime/wrapped/server.d.ts.map +1 -0
  167. package/dist/runtime/wrapped/server.js +237 -0
  168. package/dist/runtime/wrapped/server.test.d.ts +2 -0
  169. package/dist/runtime/wrapped/server.test.d.ts.map +1 -0
  170. package/dist/runtime/wrapped/server.test.js +91 -0
  171. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  172. package/dist/scripts/apply-wrap-templates.js +132 -0
  173. package/dist/scripts/apply-wrap-templates.test.d.ts +1 -0
  174. package/dist/scripts/apply-wrap-templates.test.js +199 -0
  175. package/dist/scripts/apply-wrap.d.ts +46 -0
  176. package/dist/scripts/apply-wrap.js +278 -0
  177. package/dist/scripts/apply-wrap.test.d.ts +1 -0
  178. package/dist/scripts/apply-wrap.test.js +226 -0
  179. package/dist/scripts/cms/discover.d.ts +2 -0
  180. package/dist/scripts/cms/discover.js +17 -0
  181. package/dist/scripts/cms/example-content.d.ts +32 -0
  182. package/dist/scripts/cms/example-content.js +108 -0
  183. package/dist/scripts/cms/example-content.test.d.ts +1 -0
  184. package/dist/scripts/cms/example-content.test.js +66 -0
  185. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  186. package/dist/scripts/cms/strapi/detect.js +186 -0
  187. package/dist/scripts/cms/strapi/detect.test.d.ts +1 -0
  188. package/dist/scripts/cms/strapi/detect.test.js +56 -0
  189. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  190. package/dist/scripts/cms/strapi/index.js +25 -0
  191. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  192. package/dist/scripts/cms/strapi/list-source.js +90 -0
  193. package/dist/scripts/cms/strapi/list-source.test.d.ts +1 -0
  194. package/dist/scripts/cms/strapi/list-source.test.js +33 -0
  195. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  196. package/dist/scripts/cms/strapi/schema-map.js +149 -0
  197. package/dist/scripts/cms/strapi/schema-map.test.d.ts +1 -0
  198. package/dist/scripts/cms/strapi/schema-map.test.js +91 -0
  199. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  200. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  201. package/dist/scripts/cms/strapi/touchpoints.test.d.ts +1 -0
  202. package/dist/scripts/cms/strapi/touchpoints.test.js +62 -0
  203. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  204. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  205. package/dist/scripts/cms/strapi/wire-client.test.d.ts +1 -0
  206. package/dist/scripts/cms/strapi/wire-client.test.js +51 -0
  207. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  208. package/dist/scripts/cms/strapi/wiring.js +36 -0
  209. package/dist/scripts/cms/strapi/wiring.test.d.ts +1 -0
  210. package/dist/scripts/cms/strapi/wiring.test.js +27 -0
  211. package/dist/scripts/cms/types.d.ts +117 -0
  212. package/dist/scripts/cms/types.js +11 -0
  213. package/dist/scripts/detect-framework.d.ts +39 -0
  214. package/dist/scripts/detect-framework.js +133 -0
  215. package/dist/scripts/detect-framework.test.d.ts +1 -0
  216. package/dist/scripts/detect-framework.test.js +122 -0
  217. package/dist/scripts/draft-rows.d.ts +19 -0
  218. package/dist/scripts/draft-rows.js +135 -0
  219. package/dist/scripts/draft-rows.test.d.ts +1 -0
  220. package/dist/scripts/draft-rows.test.js +113 -0
  221. package/dist/scripts/draft-schemas.d.ts +27 -0
  222. package/dist/scripts/draft-schemas.js +188 -0
  223. package/dist/scripts/draft-schemas.test.d.ts +1 -0
  224. package/dist/scripts/draft-schemas.test.js +152 -0
  225. package/dist/scripts/extract-translations.js +16 -11
  226. package/dist/scripts/extract-translations.test.d.ts +1 -0
  227. package/dist/scripts/extract-translations.test.js +66 -0
  228. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  229. package/dist/scripts/frameworks/astro/routes.js +55 -0
  230. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  231. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  232. package/dist/scripts/frameworks/discover.d.ts +70 -0
  233. package/dist/scripts/frameworks/discover.js +178 -0
  234. package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
  235. package/dist/scripts/frameworks/next/rewire.js +258 -0
  236. package/dist/scripts/frameworks/next/rewire.test.d.ts +1 -0
  237. package/dist/scripts/frameworks/next/rewire.test.js +134 -0
  238. package/dist/scripts/frameworks/next/routes.d.ts +17 -0
  239. package/dist/scripts/frameworks/next/routes.js +134 -0
  240. package/dist/scripts/frameworks/next/routes.test.d.ts +1 -0
  241. package/dist/scripts/frameworks/next/routes.test.js +64 -0
  242. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  243. package/dist/scripts/frameworks/next/wiring.js +186 -0
  244. package/dist/scripts/frameworks/next/wiring.test.d.ts +1 -0
  245. package/dist/scripts/frameworks/next/wiring.test.js +174 -0
  246. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  247. package/dist/scripts/frameworks/nuxt/routes.js +54 -0
  248. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  249. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  250. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  251. package/dist/scripts/frameworks/remix/routes.js +50 -0
  252. package/dist/scripts/frameworks/support.d.ts +75 -0
  253. package/dist/scripts/frameworks/support.js +165 -0
  254. package/dist/scripts/frameworks/support.test.d.ts +1 -0
  255. package/dist/scripts/frameworks/support.test.js +87 -0
  256. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  257. package/dist/scripts/frameworks/sveltekit/routes.js +50 -0
  258. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  259. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  260. package/dist/scripts/frameworks/vite/routes.d.ts +15 -0
  261. package/dist/scripts/frameworks/vite/routes.js +93 -0
  262. package/dist/scripts/frameworks/vite/routes.test.d.ts +1 -0
  263. package/dist/scripts/frameworks/vite/routes.test.js +73 -0
  264. package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
  265. package/dist/scripts/frameworks/vite/wiring.js +99 -0
  266. package/dist/scripts/frameworks/vite/wiring.test.d.ts +1 -0
  267. package/dist/scripts/frameworks/vite/wiring.test.js +88 -0
  268. package/dist/scripts/i18n-namespace.d.ts +25 -0
  269. package/dist/scripts/i18n-namespace.js +53 -0
  270. package/dist/scripts/lib/validation-report.d.ts +1 -0
  271. package/dist/scripts/lib/validation-report.js +3 -0
  272. package/dist/scripts/render-parity/capture.d.ts +56 -0
  273. package/dist/scripts/render-parity/capture.js +366 -0
  274. package/dist/scripts/render-parity/capture.test.d.ts +1 -0
  275. package/dist/scripts/render-parity/capture.test.js +306 -0
  276. package/dist/scripts/render-parity/compare.d.ts +59 -0
  277. package/dist/scripts/render-parity/compare.js +0 -0
  278. package/dist/scripts/render-parity/compare.test.d.ts +1 -0
  279. package/dist/scripts/render-parity/compare.test.js +98 -0
  280. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  281. package/dist/scripts/render-parity/normalize-html.js +223 -0
  282. package/dist/scripts/render-parity/normalize-html.test.d.ts +1 -0
  283. package/dist/scripts/render-parity/normalize-html.test.js +58 -0
  284. package/dist/scripts/render-parity/types.d.ts +41 -0
  285. package/dist/scripts/render-parity/types.js +1 -0
  286. package/dist/scripts/resolve-routes.js +1 -1
  287. package/dist/scripts/scan-wrappable.d.ts +196 -0
  288. package/dist/scripts/scan-wrappable.js +1227 -0
  289. package/dist/scripts/scan-wrappable.test.d.ts +1 -0
  290. package/dist/scripts/scan-wrappable.test.js +521 -0
  291. package/dist/scripts/validate-route-metadata.js +11 -0
  292. package/package.json +92 -5
  293. package/release.json +2 -2
  294. package/templates/react-router/package.json +7 -10
  295. package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
  296. package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
  297. package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
@@ -0,0 +1,199 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { applyTemplateWrap } from './apply-wrap-templates.js';
6
+ import { astroTemplateWrapStyle } from './frameworks/astro/template-style.js';
7
+ import { nuxtTemplateWrapStyle } from './frameworks/nuxt/template-style.js';
8
+ import { svelteTemplateWrapStyle } from './frameworks/sveltekit/template-style.js';
9
+ import { scanWrappable } from './scan-wrappable.js';
10
+ let dir;
11
+ const project = (files) => {
12
+ dir = mkdtempSync(join(tmpdir(), 'apply-wrap-templates-'));
13
+ for (const [rel, content] of Object.entries(files)) {
14
+ const full = join(dir, rel);
15
+ mkdirSync(join(full, '..'), { recursive: true });
16
+ writeFileSync(full, content);
17
+ }
18
+ return dir;
19
+ };
20
+ afterEach(() => {
21
+ if (dir) {
22
+ rmSync(dir, { recursive: true, force: true });
23
+ }
24
+ });
25
+ describe('applyTemplateWrap — astro', () => {
26
+ it('wraps plain text nodes and injects the head into an existing fence', () => {
27
+ const root = project({
28
+ 'src/pages/index.astro': `---
29
+ const year = 2026;
30
+ ---
31
+ <html><body>
32
+ <main>
33
+ <h1>Welcome to the harbor</h1>
34
+ <p>
35
+ Ship your docs
36
+ without the busywork.
37
+ </p>
38
+ <footer>&copy; 2026 Harbor</footer>
39
+ </main>
40
+ </body></html>
41
+ `,
42
+ });
43
+ const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
44
+ const code = readFileSync(join(root, 'src/pages/index.astro'), 'utf8');
45
+ expect(result.applied.length).toBe(3);
46
+ expect(code).toContain('import { loadFimoT } from "fimo/astro/server";');
47
+ expect(code.indexOf('loadFimoT')).toBeGreaterThan(code.indexOf('---'));
48
+ expect(code).toContain('<h1>{t("index.welcome-to-the-harbor", "Welcome to the harbor")}</h1>');
49
+ // Multiline text collapses into the fallback, whitespace outside the wrap survives.
50
+ expect(code).toContain('{t("index.ship-your-docs-without", "Ship your docs without the busywork.")}');
51
+ // Label VALUE decodes entities; the inline fallback keeps the source form.
52
+ expect(code).toContain('"&copy; 2026 Harbor")}');
53
+ expect(result.labels['index.copy-2026-harbor']).toBe('© 2026 Harbor');
54
+ // The injected form counts as wrapped on re-scan: nothing left, keys visible.
55
+ const rescan = scanWrappable(root);
56
+ expect(rescan.items.filter((item) => item.kind === 'text')).toEqual([]);
57
+ expect(rescan.wrappedIds.labels).toContain('index.welcome-to-the-harbor');
58
+ });
59
+ it('creates a frontmatter fence when the file has none', () => {
60
+ const root = project({
61
+ 'src/pages/plain.astro': `<main><p>Just some copy here</p></main>
62
+ `,
63
+ });
64
+ applyTemplateWrap(root, { style: astroTemplateWrapStyle });
65
+ const code = readFileSync(join(root, 'src/pages/plain.astro'), 'utf8');
66
+ expect(code.startsWith('---\nimport { loadFimoT }')).toBe(true);
67
+ expect(code).toContain('{t("plain.just-some-copy-here", "Just some copy here")}');
68
+ });
69
+ it('leaves files that bind their own t() to the agent', () => {
70
+ const source = `---
71
+ import { useTranslations } from "../i18n";
72
+ const t = useTranslations("en");
73
+ ---
74
+ <main><p>Localized copy already</p></main>
75
+ `;
76
+ const root = project({ 'src/pages/i18n.astro': source });
77
+ const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
78
+ expect(result.applied).toEqual([]);
79
+ expect(result.skipped.some((skip) => skip.reason.includes('binds its own t()'))).toBe(true);
80
+ expect(readFileSync(join(root, 'src/pages/i18n.astro'), 'utf8')).toBe(source);
81
+ });
82
+ it('allocates collision-safe keys for repeated copy', () => {
83
+ const root = project({
84
+ 'src/pages/dup.astro': `---
85
+ ---
86
+ <ul><li>Learn more</li><li>Learn more</li></ul>
87
+ `,
88
+ });
89
+ const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
90
+ expect(Object.keys(result.labels).sort()).toEqual(['dup.learn-more', 'dup.learn-more-2']);
91
+ });
92
+ });
93
+ describe('applyTemplateWrap — nuxt', () => {
94
+ it('wraps with mustache t() and injects into script setup', () => {
95
+ const root = project({
96
+ 'pages/about.vue': `<script setup>
97
+ const links = [];
98
+ </script>
99
+
100
+ <template>
101
+ <main>
102
+ <h1>About the crew</h1>
103
+ <p v-if="links.length > 0">We keep the harbor running</p>
104
+ </main>
105
+ </template>
106
+ `,
107
+ });
108
+ const result = applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
109
+ const code = readFileSync(join(root, 'pages/about.vue'), 'utf8');
110
+ expect(result.applied.length).toBe(2);
111
+ expect(code).toContain("import { useFimoT } from 'fimo/nuxt'");
112
+ expect(code.indexOf('useFimoT')).toBeLessThan(code.indexOf('const links'));
113
+ expect(code).toContain('<h1>{{ t("about.about-the-crew", "About the crew") }}</h1>');
114
+ // The quoted `>` inside v-if must not truncate the tag match.
115
+ expect(code).toContain('{{ t("about.we-keep-the-harbor", "We keep the harbor running") }}');
116
+ const rescan = scanWrappable(root);
117
+ expect(rescan.items.filter((item) => item.kind === 'text')).toEqual([]);
118
+ });
119
+ it('skips options-API files (no safe wiring point)', () => {
120
+ const source = `<script>
121
+ export default { name: 'Legacy' };
122
+ </script>
123
+
124
+ <template>
125
+ <p>Legacy component copy</p>
126
+ </template>
127
+ `;
128
+ const root = project({ 'pages/legacy.vue': source });
129
+ const result = applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
130
+ expect(result.applied).toEqual([]);
131
+ expect(result.skipped.some((skip) => skip.reason.includes('no safe wiring point'))).toBe(true);
132
+ expect(readFileSync(join(root, 'pages/legacy.vue'), 'utf8')).toBe(source);
133
+ });
134
+ it('creates a script setup block when the file is template-only', () => {
135
+ const root = project({
136
+ 'pages/bare.vue': `<template>
137
+ <p>Bare template copy</p>
138
+ </template>
139
+ `,
140
+ });
141
+ applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
142
+ const code = readFileSync(join(root, 'pages/bare.vue'), 'utf8');
143
+ expect(code.startsWith('<script setup>')).toBe(true);
144
+ expect(code).toContain('{{ t("bare.bare-template-copy", "Bare template copy") }}');
145
+ });
146
+ });
147
+ describe('applyTemplateWrap — sveltekit', () => {
148
+ it('wraps text, injects into the INSTANCE script, scopes by route dir, wires the layout', () => {
149
+ const root = project({
150
+ 'src/routes/pricing/+page.svelte': `<script lang="ts" module>
151
+ export const prerender = true;
152
+ </script>
153
+
154
+ <script lang="ts">
155
+ let count = 0;
156
+ </script>
157
+
158
+ <main>
159
+ <h1>Simple pricing</h1>
160
+ </main>
161
+ `,
162
+ });
163
+ const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
164
+ const code = readFileSync(join(root, 'src/routes/pricing/+page.svelte'), 'utf8');
165
+ expect(code).toContain('{t("pricing.simple-pricing", "Simple pricing")}');
166
+ // Head goes in the instance script, never the module script.
167
+ const moduleBlock = code.slice(code.indexOf('<script lang="ts" module>'), code.indexOf('</script>'));
168
+ expect(moduleBlock).not.toContain('makeFimoT');
169
+ expect(code.indexOf('makeFimoT')).toBeGreaterThan(code.indexOf('<script lang="ts">'));
170
+ expect(code).toContain('const t = makeFimoT($page.data.fimoLabels);');
171
+ // One-time wiring: the root layout labels loader is written once.
172
+ expect(result.filesChanged).toContain('src/routes/+layout.server.js');
173
+ const layout = readFileSync(join(root, 'src/routes/+layout.server.js'), 'utf8');
174
+ expect(layout).toContain("loadFimoLabels } from 'fimo/svelte/server'");
175
+ expect(layout).toContain('fimoLabels: labels.dictionaries[labels.defaultLocale]');
176
+ });
177
+ it('never overwrites an existing +layout.server file', () => {
178
+ const existing = `export const load = () => ({ session: null });\n`;
179
+ const root = project({
180
+ 'src/routes/+layout.server.ts': existing,
181
+ 'src/routes/+page.svelte': `<h1>Front page headline</h1>\n`,
182
+ });
183
+ const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
184
+ expect(result.applied.length).toBe(1);
185
+ expect(result.labels['home.front-page-headline']).toBe('Front page headline');
186
+ expect(result.filesChanged).not.toContain('src/routes/+layout.server.js');
187
+ expect(existsSync(join(root, 'src/routes/+layout.server.js'))).toBe(false);
188
+ expect(readFileSync(join(root, 'src/routes/+layout.server.ts'), 'utf8')).toBe(existing);
189
+ });
190
+ it('scopes component files by their own name', () => {
191
+ const root = project({
192
+ 'src/lib/Footer.svelte': `<footer><p>Made with care at the docks</p></footer>\n`,
193
+ 'src/routes/+page.svelte': `<h1>Hello sailor</h1>\n`,
194
+ });
195
+ const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
196
+ expect(result.labels['footer.made-with-care-at']).toBe('Made with care at the docks');
197
+ expect(result.labels['home.hello-sailor']).toBe('Hello sailor');
198
+ });
199
+ });
@@ -0,0 +1,46 @@
1
+ import { type WrappableItem } from './scan-wrappable.js';
2
+ export interface AppliedTransform {
3
+ kind: 'text' | 'image' | 'file-link';
4
+ file: string;
5
+ line: number;
6
+ id: string;
7
+ preview: string;
8
+ }
9
+ export interface ApplyWrapResult {
10
+ applied: AppliedTransform[];
11
+ skipped: Array<{
12
+ item: WrappableItem;
13
+ reason: string;
14
+ }>;
15
+ labels: Record<string, string>;
16
+ filesChanged: string[];
17
+ }
18
+ export declare function decodeJsxEntities(text: string): string;
19
+ /** Deterministic scope from the file path: app/about/page.tsx -> about. */
20
+ export declare function keyScope(file: string): string;
21
+ export declare function slugify(text: string): string;
22
+ export declare class KeyAllocator {
23
+ private readonly used;
24
+ constructor(existing: Iterable<string>);
25
+ allocate(scope: string, slug: string): string;
26
+ }
27
+ /**
28
+ * Merge the fimo/next import with an existing one, or insert a new one AFTER
29
+ * the last import statement — positioned via the AST, never line matching
30
+ * (a multiline `import {\n …` opening line is not the end of the imports,
31
+ * as a real portfolio migration proved).
32
+ */
33
+ export declare function ensureFimoImport(code: string, specifiers: string[], importModule?: string): string;
34
+ export declare function applyWrapToFile(code: string, items: WrappableItem[], allocator: KeyAllocator, importModule?: string): {
35
+ code: string;
36
+ applied: AppliedTransform[];
37
+ skipped: Array<{
38
+ item: WrappableItem;
39
+ reason: string;
40
+ }>;
41
+ labels: Record<string, string>;
42
+ };
43
+ export declare function applyWrap(rootDir: string, opts?: {
44
+ excludePrefixes?: string[];
45
+ importModule?: string;
46
+ }): ApplyWrapResult;
@@ -0,0 +1,278 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { parseSync } from 'oxc-parser';
4
+ import { scanWrappable } from './scan-wrappable.js';
5
+ /**
6
+ * The --apply codemod: performs the PROVABLY-SAFE wrap transforms
7
+ * deterministically, leaving every judgment call to the agent.
8
+ *
9
+ * In scope: plain JSX text -> FimoText, <img src="literal"> -> FimoMediaImage,
10
+ * <a href="/file.pdf"> -> FimoFileLink.
11
+ * Out of scope (agent work): attributes (useLabels crosses the RSC boundary),
12
+ * formatted text (marks -> FimoRichTextBlock), next/image <Image>
13
+ * (framework-specific props), collections, layout wiring.
14
+ *
15
+ * Correctness is enforced by the gates, not assumed: the render-parity
16
+ * baseline is captured BEFORE applying, and `fimo migrate wrap --check`
17
+ * verifies the result like any other actor's work.
18
+ */
19
+ /** Elements whose children cannot contain a <span> wrapper. */
20
+ const NO_ELEMENT_CHILDREN = new Set(['option', 'title', 'textarea', 'script', 'style', 'select']);
21
+ function collapse(text) {
22
+ return text.replace(/\s+/g, ' ').trim();
23
+ }
24
+ // JSX text carries HTML entities (&apos;, &amp;, …). The FALLBACK keeps the
25
+ // original source form, but the label VALUE is rendered literally by
26
+ // FimoText — seeding the raw entity re-escapes it on screen (the remix
27
+ // dogfood shipped "What&apos;s next?"). Decode when generating values.
28
+ const JSX_ENTITIES = {
29
+ '&amp;': '&',
30
+ '&lt;': '<',
31
+ '&gt;': '>',
32
+ '&quot;': '"',
33
+ '&apos;': "'",
34
+ '&nbsp;': ' ',
35
+ '&copy;': '©',
36
+ };
37
+ export function decodeJsxEntities(text) {
38
+ return text
39
+ .replace(/&(amp|lt|gt|quot|apos|nbsp|copy);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
40
+ .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
41
+ .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16)));
42
+ }
43
+ /** Deterministic scope from the file path: app/about/page.tsx -> about. */
44
+ export function keyScope(file) {
45
+ const normalized = file.replace(/\\/g, '/');
46
+ const appMatch = normalized.match(/(?:^|\/)app\/(.*)\/?page\.[jt]sx?$/);
47
+ if (appMatch) {
48
+ const dir = appMatch[1].replace(/\/$/, '');
49
+ if (!dir) {
50
+ return 'home';
51
+ }
52
+ const segments = dir
53
+ .split('/')
54
+ .filter((segment) => !segment.startsWith('(') && !segment.startsWith('@'))
55
+ .map((segment) => segment.replace(/^\[+|\]+$/g, '').replace(/^\.\.\./, ''));
56
+ return segments.join('.') || 'home';
57
+ }
58
+ if (/(?:^|\/)app\/layout\.[jt]sx?$/.test(normalized)) {
59
+ return 'layout';
60
+ }
61
+ const stem = basename(normalized).replace(/\.[jt]sx?$/, '');
62
+ return slugify(stem) || 'shared';
63
+ }
64
+ export function slugify(text) {
65
+ return collapse(text)
66
+ .toLowerCase()
67
+ .normalize('NFD')
68
+ .replace(/[̀-ͯ]/g, '')
69
+ .replace(/[^a-z0-9]+/g, '-')
70
+ .replace(/^-+|-+$/g, '')
71
+ .split('-')
72
+ .slice(0, 4)
73
+ .join('-');
74
+ }
75
+ export class KeyAllocator {
76
+ used = new Set();
77
+ constructor(existing) {
78
+ for (const key of existing) {
79
+ this.used.add(key);
80
+ }
81
+ }
82
+ allocate(scope, slug) {
83
+ const base = `${scope}.${slug || 'item'}`;
84
+ let candidate = base;
85
+ let counter = 2;
86
+ while (this.used.has(candidate)) {
87
+ candidate = `${base}-${counter++}`;
88
+ }
89
+ this.used.add(candidate);
90
+ return candidate;
91
+ }
92
+ }
93
+ /**
94
+ * Merge the fimo/next import with an existing one, or insert a new one AFTER
95
+ * the last import statement — positioned via the AST, never line matching
96
+ * (a multiline `import {\n …` opening line is not the end of the imports,
97
+ * as a real portfolio migration proved).
98
+ */
99
+ export function ensureFimoImport(code, specifiers, importModule = 'fimo/next') {
100
+ if (specifiers.length === 0) {
101
+ return code;
102
+ }
103
+ const moduleRe = importModule.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
104
+ const importMatch = code.match(new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*(['\"])${moduleRe}\\2\\s*;?`));
105
+ if (importMatch) {
106
+ const existing = importMatch[1]
107
+ .split(',')
108
+ .map((token) => token.trim())
109
+ .filter(Boolean);
110
+ const merged = [...new Set([...existing, ...specifiers])].sort();
111
+ return code.replace(importMatch[0], `import { ${merged.join(', ')} } from "${importModule}";`);
112
+ }
113
+ const statement = `import { ${[...new Set(specifiers)].sort().join(', ')} } from "${importModule}";`;
114
+ let insertAt = 0;
115
+ try {
116
+ const program = parseSync('module.tsx', code).program;
117
+ for (const node of program.body ?? []) {
118
+ if (node.type === 'ImportDeclaration') {
119
+ insertAt = node.end;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ insertAt = 0;
125
+ }
126
+ if (insertAt === 0) {
127
+ return `${statement}\n${code}`;
128
+ }
129
+ return `${code.slice(0, insertAt)}\n${statement}${code.slice(insertAt)}`;
130
+ }
131
+ export function applyWrapToFile(code, items, allocator, importModule = 'fimo/next') {
132
+ const applied = [];
133
+ const skipped = [];
134
+ const labels = {};
135
+ const edits = [];
136
+ const specifiers = new Set();
137
+ for (const item of items) {
138
+ const span = item.span;
139
+ if (!span) {
140
+ skipped.push({ item, reason: 'no span info' });
141
+ continue;
142
+ }
143
+ const scope = keyScope(item.file);
144
+ if (item.kind === 'text') {
145
+ if (span.parentTag && NO_ELEMENT_CHILDREN.has(span.parentTag.toLowerCase())) {
146
+ skipped.push({ item, reason: `<${span.parentTag}> cannot contain a wrapper element` });
147
+ continue;
148
+ }
149
+ const raw = code.slice(span.start, span.end);
150
+ const core = raw.trim();
151
+ if (!core) {
152
+ skipped.push({ item, reason: 'whitespace only' });
153
+ continue;
154
+ }
155
+ const lead = raw.slice(0, raw.indexOf(core));
156
+ const trail = raw.slice(raw.indexOf(core) + core.length);
157
+ const id = allocator.allocate(scope, slugify(core));
158
+ edits.push({
159
+ start: span.start,
160
+ end: span.end,
161
+ replacement: `${lead}<FimoText id="${id}">${core}</FimoText>${trail}`,
162
+ });
163
+ specifiers.add('FimoText');
164
+ labels[id] = decodeJsxEntities(collapse(core));
165
+ applied.push({ kind: 'text', file: item.file, line: item.line, id, preview: item.preview });
166
+ continue;
167
+ }
168
+ if (item.kind === 'image') {
169
+ if (span.openNameStart === undefined ||
170
+ span.openNameEnd === undefined ||
171
+ code.slice(span.openNameStart, span.openNameEnd) !== 'img') {
172
+ skipped.push({ item, reason: 'next/image and custom components stay agent work' });
173
+ continue;
174
+ }
175
+ const id = allocator.allocate(scope, slugify(basename(item.preview).replace(/\.[a-z0-9]+$/i, '')));
176
+ edits.push({
177
+ start: span.openNameStart,
178
+ end: span.openNameEnd,
179
+ replacement: `FimoMediaImage id="${id}"`,
180
+ });
181
+ specifiers.add('FimoMediaImage');
182
+ applied.push({ kind: 'image', file: item.file, line: item.line, id, preview: item.preview });
183
+ continue;
184
+ }
185
+ if (item.kind === 'file-link') {
186
+ if (span.openNameStart === undefined ||
187
+ span.openNameEnd === undefined ||
188
+ span.closeNameStart === undefined ||
189
+ span.closeNameEnd === undefined) {
190
+ skipped.push({ item, reason: 'self-closing or unparsable anchor' });
191
+ continue;
192
+ }
193
+ const id = allocator.allocate(scope, slugify(basename(item.preview).replace(/\.[a-z0-9]+$/i, '')));
194
+ edits.push({ start: span.openNameStart, end: span.openNameEnd, replacement: `FimoFileLink id="${id}"` });
195
+ edits.push({ start: span.closeNameStart, end: span.closeNameEnd, replacement: 'FimoFileLink' });
196
+ specifiers.add('FimoFileLink');
197
+ applied.push({ kind: 'file-link', file: item.file, line: item.line, id, preview: item.preview });
198
+ continue;
199
+ }
200
+ skipped.push({ item, reason: `${item.kind} stays agent work` });
201
+ }
202
+ let next = code;
203
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
204
+ next = next.slice(0, edit.start) + edit.replacement + next.slice(edit.end);
205
+ }
206
+ next = ensureFimoImport(next, [...specifiers], importModule);
207
+ return { code: next, applied, skipped, labels };
208
+ }
209
+ /**
210
+ * Apply the safe transforms across the project. Mutates source files and
211
+ * merges generated labels into `.fimo/migration/labels.json` (never
212
+ * overwriting existing keys).
213
+ */
214
+ /** Recorded coverage exemptions the codemod must NOT wrap — items the user
215
+ * deliberately kept out (dev-only UI, OG-image markup, …). Matched by the
216
+ * same (file, kind, preview) triple the coverage gate uses, so "record the
217
+ * exemption first, then --apply auto-wraps the rest" holds. */
218
+ function readApplyExemptions(rootDir) {
219
+ try {
220
+ const manifest = JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'manifest.json'), 'utf8'));
221
+ const raw = manifest?.wrap?.exemptions;
222
+ return Array.isArray(raw)
223
+ ? raw.filter((entry) => typeof entry?.file === 'string' &&
224
+ typeof entry?.kind === 'string' &&
225
+ typeof entry?.preview === 'string')
226
+ : [];
227
+ }
228
+ catch {
229
+ return [];
230
+ }
231
+ }
232
+ export function applyWrap(rootDir, opts = {}) {
233
+ const scan = scanWrappable(rootDir, { excludePrefixes: opts.excludePrefixes });
234
+ const existingLabels = (() => {
235
+ try {
236
+ return JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'labels.json'), 'utf8'));
237
+ }
238
+ catch {
239
+ return {};
240
+ }
241
+ })();
242
+ // Honor recorded exemptions: the codemod skips them so the coverage gate's
243
+ // exemption and the codemod stay consistent — otherwise --apply would wrap
244
+ // an item the user just exempted and the gate would flag the drift.
245
+ const exemptions = readApplyExemptions(rootDir);
246
+ const isExempt = (item) => exemptions.some((exemption) => exemption.file === item.file && exemption.kind === item.kind && exemption.preview === item.preview);
247
+ const allocator = new KeyAllocator([...Object.keys(existingLabels), ...scan.wrappedIds.labels]);
248
+ const byFile = new Map();
249
+ for (const item of scan.items) {
250
+ if (isExempt(item)) {
251
+ continue;
252
+ }
253
+ const list = byFile.get(item.file) ?? [];
254
+ list.push(item);
255
+ byFile.set(item.file, list);
256
+ }
257
+ const result = { applied: [], skipped: [], labels: {}, filesChanged: [] };
258
+ for (const [file, items] of byFile) {
259
+ const filePath = join(rootDir, file);
260
+ let code;
261
+ try {
262
+ code = readFileSync(filePath, 'utf8');
263
+ }
264
+ catch {
265
+ result.skipped.push(...items.map((item) => ({ item, reason: 'unreadable file' })));
266
+ continue;
267
+ }
268
+ const fileResult = applyWrapToFile(code, items, allocator, opts.importModule ?? 'fimo/next');
269
+ result.applied.push(...fileResult.applied);
270
+ result.skipped.push(...fileResult.skipped);
271
+ Object.assign(result.labels, fileResult.labels);
272
+ if (fileResult.applied.length > 0) {
273
+ writeFileSync(filePath, fileResult.code);
274
+ result.filesChanged.push(file);
275
+ }
276
+ }
277
+ return result;
278
+ }
@@ -0,0 +1 @@
1
+ export {};