fimo 0.7.0-experimental.1 → 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 (310) hide show
  1. package/assets/content-templates/hooks-template.eta +27 -73
  2. package/assets/content-templates/types-template.eta +1 -4
  3. package/assets/skills/fimo/SKILL.md +2 -2
  4. package/assets/skills/fimo/references/content.md +42 -46
  5. package/assets/skills/fimo/references/locales.md +0 -8
  6. package/assets/skills/fimo/references/setup-react-router.md +2 -2
  7. package/assets/skills/fimo/references/ui.md +1 -1
  8. package/assets/skills/fimo-cli/references/content.md +14 -17
  9. package/assets/skills/fimo-cli/references/locales.md +0 -6
  10. package/assets/skills/fimo-migration/SKILL.md +22 -2
  11. package/assets/skills/fimo-migration/references/content.md +8 -0
  12. package/assets/skills/fimo-migration/references/manifest.md +35 -0
  13. package/assets/skills/fimo-migration/references/qa.md +24 -2
  14. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  15. package/assets/skills/fimo-migration/references/wrap.md +617 -0
  16. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  17. package/dist/build/vite/plugins/translations.d.ts +2 -8
  18. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  19. package/dist/build/vite/plugins/translations.js +3 -121
  20. package/dist/cli/bundle.json +1 -1
  21. package/dist/cli/index.js +20655 -12551
  22. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  23. package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
  24. package/dist/runtime/app/FimoProviders.js +2 -23
  25. package/dist/runtime/astro/index.d.ts +2 -0
  26. package/dist/runtime/astro/index.d.ts.map +1 -0
  27. package/dist/runtime/astro/index.js +3 -0
  28. package/dist/runtime/astro/server.d.ts +3 -0
  29. package/dist/runtime/astro/server.d.ts.map +1 -0
  30. package/dist/runtime/astro/server.js +6 -0
  31. package/dist/runtime/content/utils/retrieve-data-sources.d.ts +0 -1
  32. package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +1 -1
  33. package/dist/runtime/content/utils/retrieve-data-sources.js +4 -27
  34. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
  35. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  36. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  37. package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
  38. package/dist/runtime/next/FimoNextProviders.js +20 -0
  39. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  40. package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
  41. package/dist/runtime/next/FimoNextScripts.js +19 -0
  42. package/dist/runtime/next/index.d.ts +14 -0
  43. package/dist/runtime/next/index.d.ts.map +1 -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.d.ts.map +1 -0
  47. package/dist/runtime/next/server.js +4 -0
  48. package/dist/runtime/nuxt/composables.d.ts +9 -0
  49. package/dist/runtime/nuxt/composables.d.ts.map +1 -0
  50. package/dist/runtime/nuxt/composables.js +57 -0
  51. package/dist/runtime/nuxt/index.d.ts +3 -0
  52. package/dist/runtime/nuxt/index.d.ts.map +1 -0
  53. package/dist/runtime/nuxt/index.js +6 -0
  54. package/dist/runtime/nuxt/server.d.ts +3 -0
  55. package/dist/runtime/nuxt/server.d.ts.map +1 -0
  56. package/dist/runtime/nuxt/server.js +6 -0
  57. package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
  58. package/dist/runtime/paths/get-fimo-paths.js +5 -15
  59. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  60. package/dist/runtime/primitives/labels-hook.d.ts.map +1 -0
  61. package/dist/runtime/primitives/labels-hook.js +41 -0
  62. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  63. package/dist/runtime/primitives/labels-store.d.ts.map +1 -0
  64. package/dist/runtime/primitives/labels-store.js +60 -0
  65. package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
  66. package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
  67. package/dist/runtime/primitives/labels-store.test.js +66 -0
  68. package/dist/runtime/primitives/lib/primitives.d.ts +2 -2
  69. package/dist/runtime/primitives/lib/primitives.d.ts.map +1 -1
  70. package/dist/runtime/primitives/lib/query.d.ts +3 -5
  71. package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
  72. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  73. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
  74. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  75. package/dist/runtime/primitives/public-surface.test.js +5 -9
  76. package/dist/runtime/primitives/translations.d.ts +1 -27
  77. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  78. package/dist/runtime/primitives/translations.js +7 -85
  79. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  80. package/dist/runtime/react/FimoSpaProviders.d.ts.map +1 -0
  81. package/dist/runtime/react/FimoSpaProviders.js +63 -0
  82. package/dist/runtime/react/index.d.ts +33 -0
  83. package/dist/runtime/react/index.d.ts.map +1 -0
  84. package/dist/runtime/react/index.js +39 -0
  85. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  86. package/dist/runtime/remix/FimoRemixProviders.d.ts.map +1 -0
  87. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  88. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  89. package/dist/runtime/remix/FimoRemixScripts.d.ts.map +1 -0
  90. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  91. package/dist/runtime/remix/index.d.ts +12 -0
  92. package/dist/runtime/remix/index.d.ts.map +1 -0
  93. package/dist/runtime/remix/index.js +11 -0
  94. package/dist/runtime/remix/server.d.ts +2 -0
  95. package/dist/runtime/remix/server.d.ts.map +1 -0
  96. package/dist/runtime/remix/server.js +2 -0
  97. package/dist/runtime/seo/buildSeoForPath.d.ts +3 -1
  98. package/dist/runtime/seo/buildSeoForPath.d.ts.map +1 -1
  99. package/dist/runtime/seo/buildSeoForPath.js +1 -4
  100. package/dist/runtime/shared/display-routes.d.ts +14 -0
  101. package/dist/runtime/shared/display-routes.d.ts.map +1 -0
  102. package/dist/runtime/shared/display-routes.js +25 -0
  103. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  104. package/dist/runtime/shared/fimo-config.server.js +1 -0
  105. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  106. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  107. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  108. package/dist/runtime/shared/i18n-messages.d.ts.map +1 -0
  109. package/dist/runtime/shared/i18n-messages.js +79 -0
  110. package/dist/runtime/shared/i18n-messages.test.d.ts +2 -0
  111. package/dist/runtime/shared/i18n-messages.test.d.ts.map +1 -0
  112. package/dist/runtime/shared/i18n-messages.test.js +39 -0
  113. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  114. package/dist/runtime/shared/labels-fetch.pure.d.ts.map +1 -0
  115. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  116. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  117. package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
  118. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  119. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  120. package/dist/runtime/shared/labels-t.pure.d.ts.map +1 -0
  121. package/dist/runtime/shared/labels-t.pure.js +10 -0
  122. package/dist/runtime/strapi/client.d.ts +26 -0
  123. package/dist/runtime/strapi/client.d.ts.map +1 -0
  124. package/dist/runtime/strapi/client.js +88 -0
  125. package/dist/runtime/strapi/client.test.d.ts +2 -0
  126. package/dist/runtime/strapi/client.test.d.ts.map +1 -0
  127. package/dist/runtime/strapi/client.test.js +64 -0
  128. package/dist/runtime/strapi/index.d.ts +5 -0
  129. package/dist/runtime/strapi/index.d.ts.map +1 -0
  130. package/dist/runtime/strapi/index.js +6 -0
  131. package/dist/runtime/strapi/reshape.d.ts +28 -0
  132. package/dist/runtime/strapi/reshape.d.ts.map +1 -0
  133. package/dist/runtime/strapi/reshape.js +79 -0
  134. package/dist/runtime/strapi/reshape.test.d.ts +2 -0
  135. package/dist/runtime/strapi/reshape.test.d.ts.map +1 -0
  136. package/dist/runtime/strapi/reshape.test.js +33 -0
  137. package/dist/runtime/svelte/index.d.ts +3 -0
  138. package/dist/runtime/svelte/index.d.ts.map +1 -0
  139. package/dist/runtime/svelte/index.js +7 -0
  140. package/dist/runtime/svelte/server.d.ts +3 -0
  141. package/dist/runtime/svelte/server.d.ts.map +1 -0
  142. package/dist/runtime/svelte/server.js +6 -0
  143. package/dist/runtime/vue/index.d.ts +3 -0
  144. package/dist/runtime/vue/index.d.ts.map +1 -0
  145. package/dist/runtime/vue/index.js +5 -0
  146. package/dist/runtime/vue/server.d.ts +3 -0
  147. package/dist/runtime/vue/server.d.ts.map +1 -0
  148. package/dist/runtime/vue/server.js +6 -0
  149. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  150. package/dist/runtime/wrapped/FimoFileLink.d.ts.map +1 -0
  151. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  152. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  153. package/dist/runtime/wrapped/FimoMediaImage.d.ts.map +1 -0
  154. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  155. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  156. package/dist/runtime/wrapped/FimoMediaVideo.d.ts.map +1 -0
  157. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  158. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
  159. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts.map +1 -0
  160. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  161. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  162. package/dist/runtime/wrapped/FimoText.d.ts.map +1 -0
  163. package/dist/runtime/wrapped/FimoText.js +20 -0
  164. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  165. package/dist/runtime/wrapped/WrappedContentContext.d.ts.map +1 -0
  166. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  167. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  168. package/dist/runtime/wrapped/WrappedProviders.d.ts.map +1 -0
  169. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  170. package/dist/runtime/wrapped/dom.d.ts +4 -0
  171. package/dist/runtime/wrapped/dom.d.ts.map +1 -0
  172. package/dist/runtime/wrapped/dom.js +23 -0
  173. package/dist/runtime/wrapped/dom.test.d.ts +2 -0
  174. package/dist/runtime/wrapped/dom.test.d.ts.map +1 -0
  175. package/dist/runtime/wrapped/dom.test.js +16 -0
  176. package/dist/runtime/wrapped/server.d.ts +77 -0
  177. package/dist/runtime/wrapped/server.d.ts.map +1 -0
  178. package/dist/runtime/wrapped/server.js +237 -0
  179. package/dist/runtime/wrapped/server.test.d.ts +2 -0
  180. package/dist/runtime/wrapped/server.test.d.ts.map +1 -0
  181. package/dist/runtime/wrapped/server.test.js +91 -0
  182. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  183. package/dist/scripts/apply-wrap-templates.js +132 -0
  184. package/dist/scripts/apply-wrap-templates.test.d.ts +1 -0
  185. package/dist/scripts/apply-wrap-templates.test.js +199 -0
  186. package/dist/scripts/apply-wrap.d.ts +46 -0
  187. package/dist/scripts/apply-wrap.js +278 -0
  188. package/dist/scripts/apply-wrap.test.d.ts +1 -0
  189. package/dist/scripts/apply-wrap.test.js +226 -0
  190. package/dist/scripts/cms/discover.d.ts +2 -0
  191. package/dist/scripts/cms/discover.js +17 -0
  192. package/dist/scripts/cms/example-content.d.ts +32 -0
  193. package/dist/scripts/cms/example-content.js +108 -0
  194. package/dist/scripts/cms/example-content.test.d.ts +1 -0
  195. package/dist/scripts/cms/example-content.test.js +66 -0
  196. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  197. package/dist/scripts/cms/strapi/detect.js +186 -0
  198. package/dist/scripts/cms/strapi/detect.test.d.ts +1 -0
  199. package/dist/scripts/cms/strapi/detect.test.js +56 -0
  200. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  201. package/dist/scripts/cms/strapi/index.js +25 -0
  202. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  203. package/dist/scripts/cms/strapi/list-source.js +90 -0
  204. package/dist/scripts/cms/strapi/list-source.test.d.ts +1 -0
  205. package/dist/scripts/cms/strapi/list-source.test.js +33 -0
  206. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  207. package/dist/scripts/cms/strapi/schema-map.js +149 -0
  208. package/dist/scripts/cms/strapi/schema-map.test.d.ts +1 -0
  209. package/dist/scripts/cms/strapi/schema-map.test.js +91 -0
  210. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  211. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  212. package/dist/scripts/cms/strapi/touchpoints.test.d.ts +1 -0
  213. package/dist/scripts/cms/strapi/touchpoints.test.js +62 -0
  214. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  215. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  216. package/dist/scripts/cms/strapi/wire-client.test.d.ts +1 -0
  217. package/dist/scripts/cms/strapi/wire-client.test.js +51 -0
  218. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  219. package/dist/scripts/cms/strapi/wiring.js +36 -0
  220. package/dist/scripts/cms/strapi/wiring.test.d.ts +1 -0
  221. package/dist/scripts/cms/strapi/wiring.test.js +27 -0
  222. package/dist/scripts/cms/types.d.ts +117 -0
  223. package/dist/scripts/cms/types.js +11 -0
  224. package/dist/scripts/detect-framework.d.ts +39 -0
  225. package/dist/scripts/detect-framework.js +133 -0
  226. package/dist/scripts/detect-framework.test.d.ts +1 -0
  227. package/dist/scripts/detect-framework.test.js +122 -0
  228. package/dist/scripts/draft-rows.d.ts +19 -0
  229. package/dist/scripts/draft-rows.js +135 -0
  230. package/dist/scripts/draft-rows.test.d.ts +1 -0
  231. package/dist/scripts/draft-rows.test.js +113 -0
  232. package/dist/scripts/draft-schemas.d.ts +27 -0
  233. package/dist/scripts/draft-schemas.js +188 -0
  234. package/dist/scripts/draft-schemas.test.d.ts +1 -0
  235. package/dist/scripts/draft-schemas.test.js +152 -0
  236. package/dist/scripts/extract-translations.js +16 -11
  237. package/dist/scripts/extract-translations.test.d.ts +1 -0
  238. package/dist/scripts/extract-translations.test.js +66 -0
  239. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  240. package/dist/scripts/frameworks/astro/routes.js +55 -0
  241. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  242. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  243. package/dist/scripts/frameworks/discover.d.ts +70 -0
  244. package/dist/scripts/frameworks/discover.js +178 -0
  245. package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
  246. package/dist/scripts/frameworks/next/rewire.js +258 -0
  247. package/dist/scripts/frameworks/next/rewire.test.d.ts +1 -0
  248. package/dist/scripts/frameworks/next/rewire.test.js +134 -0
  249. package/dist/scripts/frameworks/next/routes.d.ts +17 -0
  250. package/dist/scripts/frameworks/next/routes.js +134 -0
  251. package/dist/scripts/frameworks/next/routes.test.d.ts +1 -0
  252. package/dist/scripts/frameworks/next/routes.test.js +64 -0
  253. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  254. package/dist/scripts/frameworks/next/wiring.js +186 -0
  255. package/dist/scripts/frameworks/next/wiring.test.d.ts +1 -0
  256. package/dist/scripts/frameworks/next/wiring.test.js +174 -0
  257. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  258. package/dist/scripts/frameworks/nuxt/routes.js +54 -0
  259. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  260. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  261. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  262. package/dist/scripts/frameworks/remix/routes.js +50 -0
  263. package/dist/scripts/frameworks/support.d.ts +75 -0
  264. package/dist/scripts/frameworks/support.js +165 -0
  265. package/dist/scripts/frameworks/support.test.d.ts +1 -0
  266. package/dist/scripts/frameworks/support.test.js +87 -0
  267. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  268. package/dist/scripts/frameworks/sveltekit/routes.js +50 -0
  269. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  270. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  271. package/dist/scripts/frameworks/vite/routes.d.ts +15 -0
  272. package/dist/scripts/frameworks/vite/routes.js +93 -0
  273. package/dist/scripts/frameworks/vite/routes.test.d.ts +1 -0
  274. package/dist/scripts/frameworks/vite/routes.test.js +73 -0
  275. package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
  276. package/dist/scripts/frameworks/vite/wiring.js +99 -0
  277. package/dist/scripts/frameworks/vite/wiring.test.d.ts +1 -0
  278. package/dist/scripts/frameworks/vite/wiring.test.js +88 -0
  279. package/dist/scripts/generated-types.test.js +1 -1
  280. package/dist/scripts/i18n-namespace.d.ts +25 -0
  281. package/dist/scripts/i18n-namespace.js +53 -0
  282. package/dist/scripts/lib/validation-report.d.ts +1 -0
  283. package/dist/scripts/lib/validation-report.js +3 -0
  284. package/dist/scripts/render-parity/capture.d.ts +56 -0
  285. package/dist/scripts/render-parity/capture.js +366 -0
  286. package/dist/scripts/render-parity/capture.test.d.ts +1 -0
  287. package/dist/scripts/render-parity/capture.test.js +306 -0
  288. package/dist/scripts/render-parity/compare.d.ts +59 -0
  289. package/dist/scripts/render-parity/compare.js +0 -0
  290. package/dist/scripts/render-parity/compare.test.d.ts +1 -0
  291. package/dist/scripts/render-parity/compare.test.js +98 -0
  292. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  293. package/dist/scripts/render-parity/normalize-html.js +223 -0
  294. package/dist/scripts/render-parity/normalize-html.test.d.ts +1 -0
  295. package/dist/scripts/render-parity/normalize-html.test.js +58 -0
  296. package/dist/scripts/render-parity/types.d.ts +41 -0
  297. package/dist/scripts/render-parity/types.js +1 -0
  298. package/dist/scripts/resolve-routes.js +1 -1
  299. package/dist/scripts/scan-wrappable.d.ts +196 -0
  300. package/dist/scripts/scan-wrappable.js +1227 -0
  301. package/dist/scripts/scan-wrappable.test.d.ts +1 -0
  302. package/dist/scripts/scan-wrappable.test.js +521 -0
  303. package/dist/scripts/sync-schemas.test.js +4 -24
  304. package/dist/scripts/validate-route-metadata.js +11 -0
  305. package/dist/scripts/validate-schemas.test.js +2 -2
  306. package/package.json +92 -5
  307. package/templates/react-router/package.json +7 -10
  308. package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
  309. package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
  310. package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
@@ -0,0 +1,174 @@
1
+ import { 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 { ensureNextLayoutWiring, ensureNextWiring } from './wiring.js';
6
+ let dir;
7
+ const project = (files) => {
8
+ dir = mkdtempSync(join(tmpdir(), 'next-wiring-'));
9
+ for (const [rel, content] of Object.entries(files)) {
10
+ const full = join(dir, rel);
11
+ mkdirSync(join(full, '..'), { recursive: true });
12
+ writeFileSync(full, content);
13
+ }
14
+ return dir;
15
+ };
16
+ afterEach(() => {
17
+ if (dir) {
18
+ rmSync(dir, { recursive: true, force: true });
19
+ }
20
+ });
21
+ const CREATE_NEXT_APP_LAYOUT = `import type { Metadata } from 'next';
22
+ import './globals.css';
23
+
24
+ export const metadata: Metadata = { title: 'Harbor Docs' };
25
+
26
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
27
+ return (
28
+ <html lang="en">
29
+ <body className="antialiased">{children}</body>
30
+ </html>
31
+ );
32
+ }
33
+ `;
34
+ describe('ensureNextLayoutWiring', () => {
35
+ it('wires the create-next-app default layout end to end', () => {
36
+ const root = project({ 'app/layout.tsx': CREATE_NEXT_APP_LAYOUT });
37
+ const result = ensureNextLayoutWiring(root, { appDir: 'app' });
38
+ const code = readFileSync(join(root, 'app/layout.tsx'), 'utf8');
39
+ expect(result.wired).toBe(true);
40
+ expect(result.changes).toEqual(['wired Fimo providers into app/layout.tsx']);
41
+ expect(code).toContain('import { FimoNextProviders, FimoNextScripts } from "fimo/next";');
42
+ expect(code).toContain('import { loadFimoConfig, loadFimoLabels } from "fimo/next/server";');
43
+ expect(code).toContain('export default async function RootLayout');
44
+ expect(code).toContain('const config = loadFimoConfig();');
45
+ expect(code).toContain('const labels = await loadFimoLabels();');
46
+ expect(code).toContain('<body className="antialiased"><FimoNextProviders config={config} labels={labels}>{children}</FimoNextProviders></body>');
47
+ expect(code).toContain('<head>');
48
+ expect(code).toContain('<FimoNextScripts />');
49
+ // Head lands inside <html>, before <body>.
50
+ expect(code.indexOf('<FimoNextScripts />')).toBeGreaterThan(code.indexOf('<html'));
51
+ expect(code.indexOf('<FimoNextScripts />')).toBeLessThan(code.indexOf('<body'));
52
+ });
53
+ it('wires the root layout under a [locale] segment (next-intl)', () => {
54
+ const root = project({ 'src/app/[locale]/layout.tsx': CREATE_NEXT_APP_LAYOUT });
55
+ const result = ensureNextLayoutWiring(root, { appDir: 'src/app' });
56
+ const code = readFileSync(join(root, 'src/app/[locale]/layout.tsx'), 'utf8');
57
+ expect(result.wired).toBe(true);
58
+ expect(result.changes).toEqual(['wired Fimo providers into src/app/[locale]/layout.tsx']);
59
+ expect(code).toContain('<FimoNextProviders config={config} labels={labels}>{children}</FimoNextProviders>');
60
+ expect(code).toContain('<FimoNextScripts />');
61
+ });
62
+ it('picks the <html> layout when a passthrough app/layout also exists', () => {
63
+ const root = project({
64
+ 'src/app/layout.tsx': 'export default function Root({ children }) {\n return children;\n}\n',
65
+ 'src/app/[locale]/layout.tsx': CREATE_NEXT_APP_LAYOUT,
66
+ });
67
+ const result = ensureNextLayoutWiring(root, { appDir: 'src/app' });
68
+ expect(result.wired).toBe(true);
69
+ expect(result.changes).toEqual(['wired Fimo providers into src/app/[locale]/layout.tsx']);
70
+ // the passthrough root layout stays untouched
71
+ expect(readFileSync(join(root, 'src/app/layout.tsx'), 'utf8')).not.toContain('FimoNextProviders');
72
+ });
73
+ it('treats a [locale] layout already wired by hand as a no-op', () => {
74
+ const root = project({
75
+ 'src/app/[locale]/layout.tsx': CREATE_NEXT_APP_LAYOUT.replace('{children}', '<FimoNextProviders config={config} labels={labels}>{children}</FimoNextProviders>'),
76
+ });
77
+ const result = ensureNextLayoutWiring(root, { appDir: 'src/app' });
78
+ expect(result.wired).toBe(true);
79
+ expect(result.changes).toEqual([]);
80
+ });
81
+ it('reuses an existing <head> element and keeps an async layout async', () => {
82
+ const root = project({
83
+ 'src/app/layout.jsx': `export default async function Layout({ children }) {
84
+ return (
85
+ <html>
86
+ <head>
87
+ <link rel="icon" href="/favicon.ico" />
88
+ </head>
89
+ <body>
90
+ <div id="shell">{children}</div>
91
+ </body>
92
+ </html>
93
+ );
94
+ }
95
+ `,
96
+ });
97
+ const result = ensureNextLayoutWiring(root, { appDir: 'src/app' });
98
+ const code = readFileSync(join(root, 'src/app/layout.jsx'), 'utf8');
99
+ expect(result.wired).toBe(true);
100
+ expect(code).not.toContain('async async');
101
+ expect(code).toContain('<head><FimoNextScripts />');
102
+ // children was NOT a direct body child — the nested container still wraps.
103
+ expect(code).toContain('<FimoNextProviders config={config} labels={labels}>{children}</FimoNextProviders>');
104
+ expect(code.match(/<head>/g)).toHaveLength(1);
105
+ });
106
+ it('is a no-op when the layout is already wired', () => {
107
+ const wired = CREATE_NEXT_APP_LAYOUT.replace('{children}', '<FimoNextProviders>{children}</FimoNextProviders>');
108
+ const root = project({ 'app/layout.tsx': wired });
109
+ const result = ensureNextLayoutWiring(root, { appDir: 'app' });
110
+ expect(result.wired).toBe(true);
111
+ expect(result.changes).toEqual([]);
112
+ expect(readFileSync(join(root, 'app/layout.tsx'), 'utf8')).toBe(wired);
113
+ });
114
+ it('bails on pages-router-only projects', () => {
115
+ const root = project({ 'pages/index.tsx': 'export default function Home() { return <p>hi</p>; }\n' });
116
+ const result = ensureNextLayoutWiring(root, { appDir: undefined });
117
+ expect(result.wired).toBe(false);
118
+ expect(result.warnings[0]).toContain('pages-router');
119
+ });
120
+ it('bails when the default export is not a plain function', () => {
121
+ const source = `const Layout = ({ children }) => <html><body>{children}</body></html>;
122
+ export default Layout;
123
+ `;
124
+ const root = project({ 'app/layout.tsx': source });
125
+ const result = ensureNextLayoutWiring(root, { appDir: 'app' });
126
+ expect(result.wired).toBe(false);
127
+ expect(result.warnings[0]).toContain('not a plain function declaration');
128
+ expect(readFileSync(join(root, 'app/layout.tsx'), 'utf8')).toBe(source);
129
+ });
130
+ it('bails when no {children} expression exists inside <body>', () => {
131
+ const root = project({
132
+ 'app/layout.tsx': `export default function Layout(props) {
133
+ return (
134
+ <html>
135
+ <body>{props.children}</body>
136
+ </html>
137
+ );
138
+ }
139
+ `,
140
+ });
141
+ const result = ensureNextLayoutWiring(root, { appDir: 'app' });
142
+ expect(result.wired).toBe(false);
143
+ expect(result.warnings[0]).toContain('no {children} expression');
144
+ });
145
+ });
146
+ describe('ensureNextWiring config patch', () => {
147
+ it('patches the named nextConfig binding', () => {
148
+ const root = project({
149
+ 'next.config.ts': `const nextConfig = {\n reactStrictMode: true,\n};\nexport default nextConfig;\n`,
150
+ });
151
+ const result = ensureNextWiring(root);
152
+ expect(result.changes).toEqual(['added allowedDevOrigins to next.config.ts']);
153
+ expect(readFileSync(join(root, 'next.config.ts'), 'utf8')).toContain("allowedDevOrigins: ['*.preview.fimo.ai'],");
154
+ });
155
+ it('patches a direct export default object literal (.mjs)', () => {
156
+ const root = project({
157
+ 'next.config.mjs': `/** @type {import('next').NextConfig} */\nexport default {};\n`,
158
+ });
159
+ const result = ensureNextWiring(root);
160
+ expect(result.changes).toEqual(['added allowedDevOrigins to next.config.mjs']);
161
+ const code = readFileSync(join(root, 'next.config.mjs'), 'utf8');
162
+ expect(code).toContain("allowedDevOrigins: ['*.preview.fimo.ai'],");
163
+ expect(code).toContain('export default {');
164
+ });
165
+ it('patches module.exports form and warns on unrecognized shapes', () => {
166
+ const cjs = project({ 'next.config.cjs': `module.exports = {\n poweredByHeader: false,\n};\n` });
167
+ expect(ensureNextWiring(cjs).changes).toHaveLength(1);
168
+ rmSync(cjs, { recursive: true, force: true });
169
+ const weird = project({ 'next.config.js': `module.exports = buildConfig();\n` });
170
+ const result = ensureNextWiring(weird);
171
+ expect(result.changes).toEqual([]);
172
+ expect(result.warnings[0]).toContain('Could not patch next.config.js');
173
+ });
174
+ });
@@ -0,0 +1,3 @@
1
+ import type { DiscoveredRoute } from '../next/routes.js';
2
+ /** Nuxt file-based routes under pages/ (index.vue maps to the dir path). */
3
+ export declare function discoverNuxtRoutes(pagesDir: string): DiscoveredRoute[];
@@ -0,0 +1,54 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const titleCase = (seg) => seg
4
+ .replace(/[_\-]+/g, ' ')
5
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
6
+ .split(/\s+/)
7
+ .filter(Boolean)
8
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
9
+ .join(' ');
10
+ function pushRoute(routes, parts) {
11
+ const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
12
+ const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
13
+ routes.push({
14
+ id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
15
+ path: routePath,
16
+ label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
17
+ });
18
+ }
19
+ const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
20
+ /** Nuxt file-based routes under pages/ (index.vue maps to the dir path). */
21
+ export function discoverNuxtRoutes(pagesDir) {
22
+ const routes = [];
23
+ const walk = (dir, segments) => {
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir);
27
+ }
28
+ catch {
29
+ return;
30
+ }
31
+ for (const entry of entries) {
32
+ const full = join(dir, entry);
33
+ let isDir = false;
34
+ try {
35
+ isDir = statSync(full).isDirectory();
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ if (isDir) {
41
+ walk(full, [...segments, toSegment(entry)]);
42
+ continue;
43
+ }
44
+ if (!entry.endsWith('.vue')) {
45
+ continue;
46
+ }
47
+ const base = entry.replace(/\.vue$/, '');
48
+ pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)]);
49
+ }
50
+ };
51
+ walk(pagesDir, []);
52
+ routes.sort((a, b) => a.path.localeCompare(b.path));
53
+ return routes;
54
+ }
@@ -0,0 +1,9 @@
1
+ import type { TemplateWrapStyle } from '../../apply-wrap-templates.js';
2
+ /**
3
+ * Nuxt template wraps: the useFimoT composable is client-safe (fimo/nuxt
4
+ * never pulls server loaders into the bundle) and transfers the dictionary
5
+ * through the Nuxt payload, so a single `<script setup>` line wires a file.
6
+ * Options-API scripts have no safe injection point (top-level await needs
7
+ * setup) — those files stay agent work.
8
+ */
9
+ export declare const nuxtTemplateWrapStyle: TemplateWrapStyle;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Nuxt template wraps: the useFimoT composable is client-safe (fimo/nuxt
3
+ * never pulls server loaders into the bundle) and transfers the dictionary
4
+ * through the Nuxt payload, so a single `<script setup>` line wires a file.
5
+ * Options-API scripts have no safe injection point (top-level await needs
6
+ * setup) — those files stay agent work.
7
+ */
8
+ export const nuxtTemplateWrapStyle = {
9
+ ext: '.vue',
10
+ head: "import { useFimoT } from 'fimo/nuxt'\nconst t = await useFimoT()",
11
+ headMarker: /useFimoT/,
12
+ open: '{{ t(',
13
+ close: ') }}',
14
+ injectHead: (code, head) => {
15
+ // An SFC may carry BOTH <script> and <script setup>: target the setup
16
+ // block specifically (top-level await is only legal there).
17
+ const setup = code.match(/<script[^>]*\bsetup\b[^>]*>\r?\n?/);
18
+ if (setup && setup.index !== undefined) {
19
+ const at = setup.index + setup[0].length;
20
+ return `${code.slice(0, at)}${head}\n${code.slice(at)}`;
21
+ }
22
+ if (/<script\b/.test(code)) {
23
+ return null;
24
+ }
25
+ return `<script setup>\n${head}\n</script>\n\n${code}`;
26
+ },
27
+ };
@@ -0,0 +1,3 @@
1
+ import type { DiscoveredRoute } from '../next/routes.js';
2
+ /** Remix v2 flat routes under app/routes: dots are slashes, $param is dynamic. */
3
+ export declare function discoverRemixRoutes(routesDir: string): DiscoveredRoute[];
@@ -0,0 +1,50 @@
1
+ import { readdirSync } from 'node:fs';
2
+ const titleCase = (seg) => seg
3
+ .replace(/[_\-]+/g, ' ')
4
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
5
+ .split(/\s+/)
6
+ .filter(Boolean)
7
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
8
+ .join(' ');
9
+ function pushRoute(routes, parts) {
10
+ const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
11
+ const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
12
+ routes.push({
13
+ id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
14
+ path: routePath,
15
+ label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
16
+ });
17
+ }
18
+ const FILE_EXT = /\.(tsx|jsx|ts|js|mdx)$/;
19
+ /** Remix v2 flat routes under app/routes: dots are slashes, $param is dynamic. */
20
+ export function discoverRemixRoutes(routesDir) {
21
+ const routes = [];
22
+ let entries;
23
+ try {
24
+ entries = readdirSync(routesDir);
25
+ }
26
+ catch {
27
+ return routes;
28
+ }
29
+ for (const entry of entries) {
30
+ if (!FILE_EXT.test(entry)) {
31
+ continue;
32
+ }
33
+ const base = entry.replace(FILE_EXT, '');
34
+ if (base === '_index') {
35
+ pushRoute(routes, []);
36
+ continue;
37
+ }
38
+ // Pathless layout segments start with _ ; trailing ._index maps to the parent.
39
+ const parts = base
40
+ .replace(/\._index$/, '')
41
+ .split('.')
42
+ .filter((seg) => !seg.startsWith('_'))
43
+ .map((seg) => (seg.startsWith('$') ? `:${seg.slice(1) || 'splat'}` : seg));
44
+ if (parts.length > 0) {
45
+ pushRoute(routes, parts);
46
+ }
47
+ }
48
+ routes.sort((a, b) => a.path.localeCompare(b.path));
49
+ return routes;
50
+ }
@@ -0,0 +1,75 @@
1
+ import type { FrameworkDetection } from '../detect-framework.js';
2
+ /**
3
+ * A host dependency the wrap gates on, with the floor Fimo's runtime requires.
4
+ * The floor is honest to the API surface the in-tree runtime actually uses —
5
+ * React frameworks gate on `react` 18 (the runtime touches `useSyncExternalStore`),
6
+ * Vue/Svelte/Astro on their own framework floor. The version gate is what turns
7
+ * an unsupported host into one actionable sentence instead of an npm `ERESOLVE`
8
+ * or an install-then-crash.
9
+ */
10
+ export interface SupportCheck {
11
+ pkg: string;
12
+ /** Floor, `X.Y.Z`. */
13
+ min: string;
14
+ label: string;
15
+ /** Why this floor exists — shown to the user. */
16
+ reason: string;
17
+ /** One actionable next step — shown as the error hint. */
18
+ remedy: string;
19
+ }
20
+ /**
21
+ * The runtime-floor checks for a detected framework. React-based frameworks all
22
+ * gate on `react`; the template frameworks gate on their own package. Frameworks
23
+ * with no runtime coupling (or none detected) return [] — nothing to gate.
24
+ */
25
+ export declare function supportChecksFor(detection: Pick<FrameworkDetection, 'framework' | 'react'>): SupportCheck[];
26
+ interface ParsedVersion {
27
+ major: number;
28
+ minor: number;
29
+ patch: number;
30
+ /** The original spec, for display. */
31
+ raw: string;
32
+ }
33
+ /**
34
+ * The lowest concrete version a spec implies. Handles the shapes seen in real
35
+ * package.json / installed manifests: `^18.2.0`, `~3.4`, `>=2.0.0`, `18 || 19`,
36
+ * `18.3.1`, ` >= 14.0.0 `, bare `18`. Returns null for anything without a
37
+ * numeric floor (`latest`, `*`, `workspace:*`, `catalog:`, a git/file url) — we
38
+ * never gate on a version we cannot read.
39
+ */
40
+ export declare function parseVersionFloor(spec: string): ParsedVersion | null;
41
+ /**
42
+ * The host's version of `pkg`. Prefers the INSTALLED manifest
43
+ * (`node_modules/<pkg>/package.json`) — the ground truth of what will run —
44
+ * and falls back to the declared range floor in the host package.json (a fresh
45
+ * clone before install). Returns null when neither is readable.
46
+ */
47
+ export declare function readHostVersion(cwd: string, pkg: string): ParsedVersion | null;
48
+ export interface HostSupportViolation {
49
+ subject: string;
50
+ /** The version we found on the host. */
51
+ found: string;
52
+ /** The floor Fimo requires. */
53
+ min: string;
54
+ /** Headline sentence for the error message. */
55
+ headline: string;
56
+ /** Actionable next step for the error hint. */
57
+ remedy: string;
58
+ }
59
+ export interface HostSupportResult {
60
+ ok: boolean;
61
+ violations: HostSupportViolation[];
62
+ /** Versions read successfully — for the summary line ("React 18.3.1"). */
63
+ detected: Array<{
64
+ label: string;
65
+ version: string;
66
+ }>;
67
+ }
68
+ /**
69
+ * Gate the wrap on the host's framework/runtime versions. Silent for supported
70
+ * hosts (the common case); returns one violation per below-floor dependency so
71
+ * the caller can stop with a clear message. A version we cannot read is never a
72
+ * violation — the peer range and the install still catch a genuinely absent dep.
73
+ */
74
+ export declare function checkHostSupport(cwd: string, detection: Pick<FrameworkDetection, 'framework' | 'react'>): HostSupportResult;
75
+ export {};
@@ -0,0 +1,165 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const REACT_CHECK = {
4
+ pkg: 'react',
5
+ min: '18.0.0',
6
+ label: 'React',
7
+ reason: "Fimo's inline-editing runtime uses React 18 APIs (useSyncExternalStore).",
8
+ remedy: 'Upgrade React to 18 or newer. The content still migrates on older React; only inline editing needs 18+.',
9
+ };
10
+ /**
11
+ * The runtime-floor checks for a detected framework. React-based frameworks all
12
+ * gate on `react`; the template frameworks gate on their own package. Frameworks
13
+ * with no runtime coupling (or none detected) return [] — nothing to gate.
14
+ */
15
+ export function supportChecksFor(detection) {
16
+ switch (detection.framework) {
17
+ case 'next':
18
+ case 'remix':
19
+ case 'react-router':
20
+ return [REACT_CHECK];
21
+ case 'vite':
22
+ return detection.react ? [REACT_CHECK] : [];
23
+ case 'nuxt':
24
+ return [
25
+ {
26
+ pkg: 'nuxt',
27
+ min: '3.0.0',
28
+ label: 'Nuxt',
29
+ reason: 'Fimo supports Nuxt 3+ (Vue 3). Nuxt 2 is end-of-life.',
30
+ remedy: 'Upgrade to Nuxt 3 or newer.',
31
+ },
32
+ ];
33
+ case 'vue':
34
+ return [
35
+ {
36
+ pkg: 'vue',
37
+ min: '3.0.0',
38
+ label: 'Vue',
39
+ reason: 'Fimo supports Vue 3+. Vue 2 is end-of-life.',
40
+ remedy: 'Upgrade to Vue 3 or newer.',
41
+ },
42
+ ];
43
+ case 'astro':
44
+ return [
45
+ {
46
+ pkg: 'astro',
47
+ min: '4.0.0',
48
+ label: 'Astro',
49
+ reason: 'Fimo supports Astro 4+.',
50
+ remedy: 'Upgrade Astro to 4 or newer.',
51
+ },
52
+ ];
53
+ case 'sveltekit':
54
+ return [
55
+ {
56
+ pkg: '@sveltejs/kit',
57
+ min: '2.0.0',
58
+ label: 'SvelteKit',
59
+ reason: 'Fimo supports SvelteKit 2+.',
60
+ remedy: 'Upgrade @sveltejs/kit to 2 or newer.',
61
+ },
62
+ ];
63
+ default:
64
+ return [];
65
+ }
66
+ }
67
+ /**
68
+ * The lowest concrete version a spec implies. Handles the shapes seen in real
69
+ * package.json / installed manifests: `^18.2.0`, `~3.4`, `>=2.0.0`, `18 || 19`,
70
+ * `18.3.1`, ` >= 14.0.0 `, bare `18`. Returns null for anything without a
71
+ * numeric floor (`latest`, `*`, `workspace:*`, `catalog:`, a git/file url) — we
72
+ * never gate on a version we cannot read.
73
+ */
74
+ export function parseVersionFloor(spec) {
75
+ if (!spec) {
76
+ return null;
77
+ }
78
+ // A `||` union — the floor is the lowest branch, i.e. the first token.
79
+ const first = spec.split('||')[0];
80
+ const match = first.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
81
+ if (!match) {
82
+ return null;
83
+ }
84
+ return {
85
+ major: Number(match[1]),
86
+ minor: match[2] ? Number(match[2]) : 0,
87
+ patch: match[3] ? Number(match[3]) : 0,
88
+ raw: spec.trim(),
89
+ };
90
+ }
91
+ function isBelowFloor(found, min) {
92
+ const floor = parseVersionFloor(min);
93
+ if (!floor) {
94
+ return false;
95
+ }
96
+ if (found.major !== floor.major) {
97
+ return found.major < floor.major;
98
+ }
99
+ if (found.minor !== floor.minor) {
100
+ return found.minor < floor.minor;
101
+ }
102
+ return found.patch < floor.patch;
103
+ }
104
+ /**
105
+ * The host's version of `pkg`. Prefers the INSTALLED manifest
106
+ * (`node_modules/<pkg>/package.json`) — the ground truth of what will run —
107
+ * and falls back to the declared range floor in the host package.json (a fresh
108
+ * clone before install). Returns null when neither is readable.
109
+ */
110
+ export function readHostVersion(cwd, pkg) {
111
+ const installed = join(cwd, 'node_modules', pkg, 'package.json');
112
+ if (existsSync(installed)) {
113
+ try {
114
+ const version = JSON.parse(readFileSync(installed, 'utf8')).version;
115
+ const parsed = parseVersionFloor(String(version));
116
+ if (parsed) {
117
+ return parsed;
118
+ }
119
+ }
120
+ catch {
121
+ // fall through to the declared range
122
+ }
123
+ }
124
+ const rootPkg = join(cwd, 'package.json');
125
+ if (existsSync(rootPkg)) {
126
+ try {
127
+ const pkgJson = JSON.parse(readFileSync(rootPkg, 'utf8'));
128
+ const spec = { ...pkgJson.dependencies, ...pkgJson.devDependencies }[pkg];
129
+ if (spec) {
130
+ return parseVersionFloor(String(spec));
131
+ }
132
+ }
133
+ catch {
134
+ // unreadable — nothing to gate on
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Gate the wrap on the host's framework/runtime versions. Silent for supported
141
+ * hosts (the common case); returns one violation per below-floor dependency so
142
+ * the caller can stop with a clear message. A version we cannot read is never a
143
+ * violation — the peer range and the install still catch a genuinely absent dep.
144
+ */
145
+ export function checkHostSupport(cwd, detection) {
146
+ const violations = [];
147
+ const detected = [];
148
+ for (const check of supportChecksFor(detection)) {
149
+ const found = readHostVersion(cwd, check.pkg);
150
+ if (!found) {
151
+ continue;
152
+ }
153
+ detected.push({ label: check.label, version: found.raw });
154
+ if (isBelowFloor(found, check.min)) {
155
+ violations.push({
156
+ subject: check.label,
157
+ found: found.raw,
158
+ min: check.min,
159
+ headline: `${check.label} ${found.raw} is below Fimo's floor of ${check.label} ${check.min}. ${check.reason}`,
160
+ remedy: check.remedy,
161
+ });
162
+ }
163
+ }
164
+ return { ok: violations.length === 0, violations, detected };
165
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ import { mkdirSync, mkdtempSync, 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 { checkHostSupport, parseVersionFloor, readHostVersion } from './support.js';
6
+ let dir;
7
+ function project(pkg, installed = {}) {
8
+ dir = mkdtempSync(join(tmpdir(), 'host-support-'));
9
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg));
10
+ for (const [name, version] of Object.entries(installed)) {
11
+ const modDir = join(dir, 'node_modules', name);
12
+ mkdirSync(modDir, { recursive: true });
13
+ writeFileSync(join(modDir, 'package.json'), JSON.stringify({ name, version }));
14
+ }
15
+ return dir;
16
+ }
17
+ afterEach(() => {
18
+ if (dir) {
19
+ rmSync(dir, { recursive: true, force: true });
20
+ }
21
+ });
22
+ describe('parseVersionFloor', () => {
23
+ it('reads the lowest concrete version from real spec shapes', () => {
24
+ expect(parseVersionFloor('^18.2.0')).toMatchObject({ major: 18, minor: 2, patch: 0 });
25
+ expect(parseVersionFloor('>=2.0.0')).toMatchObject({ major: 2, minor: 0, patch: 0 });
26
+ expect(parseVersionFloor('~3.4')).toMatchObject({ major: 3, minor: 4, patch: 0 });
27
+ expect(parseVersionFloor('18')).toMatchObject({ major: 18, minor: 0, patch: 0 });
28
+ // a `||` union floors on the lowest (first) branch
29
+ expect(parseVersionFloor('^17.0.0 || ^18.0.0')).toMatchObject({ major: 17 });
30
+ });
31
+ it('returns null for specs with no numeric floor', () => {
32
+ expect(parseVersionFloor('latest')).toBeNull();
33
+ expect(parseVersionFloor('*')).toBeNull();
34
+ expect(parseVersionFloor('workspace:*')).toBeNull();
35
+ expect(parseVersionFloor('')).toBeNull();
36
+ });
37
+ });
38
+ describe('readHostVersion', () => {
39
+ it('prefers the installed manifest over the declared range', () => {
40
+ const cwd = project({ dependencies: { react: '^18.0.0' } }, { react: '18.3.1' });
41
+ expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 18, minor: 3, patch: 1 });
42
+ });
43
+ it('falls back to the declared range when nothing is installed', () => {
44
+ const cwd = project({ dependencies: { react: '^19.0.0' } });
45
+ expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 19, minor: 0, patch: 0 });
46
+ });
47
+ it('returns null when the dependency is absent', () => {
48
+ const cwd = project({ dependencies: {} });
49
+ expect(readHostVersion(cwd, 'react')).toBeNull();
50
+ });
51
+ });
52
+ describe('checkHostSupport', () => {
53
+ it('passes a Next.js site on React 18', () => {
54
+ const cwd = project({ dependencies: { next: '16.2.11', react: '^18.2.0' } }, { react: '18.3.1' });
55
+ const result = checkHostSupport(cwd, { framework: 'next' });
56
+ expect(result.ok).toBe(true);
57
+ expect(result.violations).toEqual([]);
58
+ expect(result.detected).toContainEqual({ label: 'React', version: '18.3.1' });
59
+ });
60
+ it('passes a Next.js site on React 19', () => {
61
+ const cwd = project({ dependencies: { next: '16.2.11', react: '^19.0.0' } }, { react: '19.0.0' });
62
+ expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
63
+ });
64
+ it('flags a React 17 site below the floor with an actionable message', () => {
65
+ const cwd = project({ dependencies: { next: '13.0.0', react: '^17.0.2' } }, { react: '17.0.2' });
66
+ const result = checkHostSupport(cwd, { framework: 'next' });
67
+ expect(result.ok).toBe(false);
68
+ expect(result.violations).toHaveLength(1);
69
+ expect(result.violations[0]).toMatchObject({ subject: 'React', min: '18.0.0' });
70
+ expect(result.violations[0].headline).toContain('React 17.0.2');
71
+ expect(result.violations[0].remedy).toMatch(/upgrade react/i);
72
+ });
73
+ it('flags a Nuxt 2 site below the floor', () => {
74
+ const cwd = project({ dependencies: { nuxt: '^2.17.0' } }, { nuxt: '2.17.0' });
75
+ const result = checkHostSupport(cwd, { framework: 'nuxt' });
76
+ expect(result.ok).toBe(false);
77
+ expect(result.violations[0]).toMatchObject({ subject: 'Nuxt', min: '3.0.0' });
78
+ });
79
+ it('does not gate a non-React bare Vite site', () => {
80
+ const cwd = project({ dependencies: { vite: '^5.0.0' } });
81
+ expect(checkHostSupport(cwd, { framework: 'vite', react: false }).ok).toBe(true);
82
+ });
83
+ it('does not gate on a version it cannot read', () => {
84
+ const cwd = project({ dependencies: { next: '16.0.0', react: 'workspace:*' } });
85
+ expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
86
+ });
87
+ });
@@ -0,0 +1,3 @@
1
+ import type { DiscoveredRoute } from '../next/routes.js';
2
+ /** SvelteKit routes: every directory under src/routes holding a +page file. */
3
+ export declare function discoverSvelteKitRoutes(routesDir: string): DiscoveredRoute[];