fimo 0.8.0 → 0.9.0-experimental.2

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 (190) hide show
  1. package/assets/content-templates/hooks-template.eta +4 -2
  2. package/assets/content-templates/types-template.eta +66 -1
  3. package/assets/skills/fimo/SKILL.md +1 -1
  4. package/assets/skills/fimo/references/content.md +86 -15
  5. package/assets/skills/fimo-cli/SKILL.md +6 -2
  6. package/assets/skills/fimo-cli/references/content.md +61 -2
  7. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  8. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  9. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  10. package/assets/skills/fimo-migration/SKILL.md +22 -2
  11. package/assets/skills/fimo-migration/references/content.md +24 -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/SKILL.md +5 -3
  17. package/assets/skills/fimo-studio/references/cms.md +9 -1
  18. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  19. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  20. package/dist/build/vite/plugins/translations.d.ts +2 -8
  21. package/dist/build/vite/plugins/translations.js +3 -121
  22. package/dist/cli/bundle.json +1 -1
  23. package/dist/cli/index.js +1051 -144
  24. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  25. package/dist/runtime/app/FimoProviders.js +2 -23
  26. package/dist/runtime/astro/index.d.ts +2 -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.js +6 -0
  30. package/dist/runtime/content/client/internal.js +1 -0
  31. package/dist/runtime/content/client/types.d.ts +16 -0
  32. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  33. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  34. package/dist/runtime/next/FimoNextProviders.js +20 -0
  35. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  36. package/dist/runtime/next/FimoNextScripts.js +19 -0
  37. package/dist/runtime/next/index.d.ts +14 -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.js +4 -0
  41. package/dist/runtime/nuxt/composables.d.ts +9 -0
  42. package/dist/runtime/nuxt/composables.js +57 -0
  43. package/dist/runtime/nuxt/index.d.ts +3 -0
  44. package/dist/runtime/nuxt/index.js +6 -0
  45. package/dist/runtime/nuxt/server.d.ts +3 -0
  46. package/dist/runtime/nuxt/server.js +6 -0
  47. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  48. package/dist/runtime/primitives/labels-hook.js +41 -0
  49. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  50. package/dist/runtime/primitives/labels-store.js +60 -0
  51. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  52. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  53. package/dist/runtime/primitives/translations.d.ts +1 -27
  54. package/dist/runtime/primitives/translations.js +7 -85
  55. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  56. package/dist/runtime/react/FimoSpaProviders.js +63 -0
  57. package/dist/runtime/react/index.d.ts +33 -0
  58. package/dist/runtime/react/index.js +39 -0
  59. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  60. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  61. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  62. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  63. package/dist/runtime/remix/index.d.ts +12 -0
  64. package/dist/runtime/remix/index.js +11 -0
  65. package/dist/runtime/remix/server.d.ts +2 -0
  66. package/dist/runtime/remix/server.js +2 -0
  67. package/dist/runtime/shared/display-routes.d.ts +14 -0
  68. package/dist/runtime/shared/display-routes.js +25 -0
  69. package/dist/runtime/shared/fimo-config.server.js +1 -0
  70. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  71. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  72. package/dist/runtime/shared/i18n-messages.js +79 -0
  73. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  74. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  75. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  76. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  77. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  78. package/dist/runtime/shared/labels-t.pure.js +10 -0
  79. package/dist/runtime/strapi/client.d.ts +30 -0
  80. package/dist/runtime/strapi/client.js +89 -0
  81. package/dist/runtime/strapi/index.d.ts +5 -0
  82. package/dist/runtime/strapi/index.js +6 -0
  83. package/dist/runtime/strapi/reshape.d.ts +46 -0
  84. package/dist/runtime/strapi/reshape.js +114 -0
  85. package/dist/runtime/svelte/index.d.ts +3 -0
  86. package/dist/runtime/svelte/index.js +7 -0
  87. package/dist/runtime/svelte/server.d.ts +3 -0
  88. package/dist/runtime/svelte/server.js +6 -0
  89. package/dist/runtime/vue/index.d.ts +3 -0
  90. package/dist/runtime/vue/index.js +5 -0
  91. package/dist/runtime/vue/server.d.ts +3 -0
  92. package/dist/runtime/vue/server.js +6 -0
  93. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  94. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  95. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  96. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  97. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  98. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  99. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
  100. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  101. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  102. package/dist/runtime/wrapped/FimoText.js +20 -0
  103. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  104. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  105. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  106. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  107. package/dist/runtime/wrapped/dom.d.ts +4 -0
  108. package/dist/runtime/wrapped/dom.js +23 -0
  109. package/dist/runtime/wrapped/server.d.ts +77 -0
  110. package/dist/runtime/wrapped/server.js +237 -0
  111. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  112. package/dist/scripts/apply-wrap-templates.js +132 -0
  113. package/dist/scripts/apply-wrap.d.ts +46 -0
  114. package/dist/scripts/apply-wrap.js +278 -0
  115. package/dist/scripts/cms/discover.d.ts +2 -0
  116. package/dist/scripts/cms/discover.js +17 -0
  117. package/dist/scripts/cms/example-content.d.ts +33 -0
  118. package/dist/scripts/cms/example-content.js +163 -0
  119. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  120. package/dist/scripts/cms/strapi/detect.js +186 -0
  121. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  122. package/dist/scripts/cms/strapi/index.js +25 -0
  123. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  124. package/dist/scripts/cms/strapi/list-source.js +90 -0
  125. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  126. package/dist/scripts/cms/strapi/schema-map.js +279 -0
  127. package/dist/scripts/cms/strapi/shapes.d.ts +23 -0
  128. package/dist/scripts/cms/strapi/shapes.js +38 -0
  129. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  130. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  131. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  132. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  133. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  134. package/dist/scripts/cms/strapi/wiring.js +46 -0
  135. package/dist/scripts/cms/types.d.ts +145 -0
  136. package/dist/scripts/cms/types.js +11 -0
  137. package/dist/scripts/detect-framework.d.ts +39 -0
  138. package/dist/scripts/detect-framework.js +133 -0
  139. package/dist/scripts/draft-rows.d.ts +19 -0
  140. package/dist/scripts/draft-rows.js +135 -0
  141. package/dist/scripts/draft-schemas.d.ts +27 -0
  142. package/dist/scripts/draft-schemas.js +188 -0
  143. package/dist/scripts/extract-translations.js +16 -11
  144. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  145. package/dist/scripts/frameworks/astro/routes.js +55 -0
  146. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  147. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  148. package/dist/scripts/frameworks/discover.d.ts +70 -0
  149. package/dist/scripts/frameworks/discover.js +178 -0
  150. package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
  151. package/dist/scripts/frameworks/next/rewire.js +258 -0
  152. package/dist/scripts/frameworks/next/routes.d.ts +17 -0
  153. package/dist/scripts/frameworks/next/routes.js +134 -0
  154. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  155. package/dist/scripts/frameworks/next/wiring.js +186 -0
  156. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  157. package/dist/scripts/frameworks/nuxt/routes.js +54 -0
  158. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  159. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  160. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  161. package/dist/scripts/frameworks/remix/routes.js +50 -0
  162. package/dist/scripts/frameworks/support.d.ts +75 -0
  163. package/dist/scripts/frameworks/support.js +165 -0
  164. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  165. package/dist/scripts/frameworks/sveltekit/routes.js +50 -0
  166. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  167. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  168. package/dist/scripts/frameworks/vite/routes.d.ts +15 -0
  169. package/dist/scripts/frameworks/vite/routes.js +93 -0
  170. package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
  171. package/dist/scripts/frameworks/vite/wiring.js +99 -0
  172. package/dist/scripts/i18n-namespace.d.ts +27 -0
  173. package/dist/scripts/i18n-namespace.js +155 -0
  174. package/dist/scripts/lib/validation-report.d.ts +1 -0
  175. package/dist/scripts/lib/validation-report.js +3 -0
  176. package/dist/scripts/render-parity/capture.d.ts +56 -0
  177. package/dist/scripts/render-parity/capture.js +366 -0
  178. package/dist/scripts/render-parity/compare.d.ts +59 -0
  179. package/dist/scripts/render-parity/compare.js +0 -0
  180. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  181. package/dist/scripts/render-parity/normalize-html.js +223 -0
  182. package/dist/scripts/render-parity/types.d.ts +41 -0
  183. package/dist/scripts/render-parity/types.js +1 -0
  184. package/dist/scripts/scan-wrappable.d.ts +196 -0
  185. package/dist/scripts/scan-wrappable.js +1232 -0
  186. package/dist/scripts/validate-route-metadata.js +11 -0
  187. package/dist/scripts/validate-schemas.js +42 -1
  188. package/package.json +84 -4
  189. package/release.json +2 -2
  190. package/templates/react-router/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const PAGE_FILES = new Set(['page.tsx', 'page.jsx', 'page.ts', 'page.js', 'page.mdx']);
4
+ const titleCase = (seg) => seg
5
+ .replace(/[_\-]+/g, ' ')
6
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
7
+ .split(/\s+/)
8
+ .filter(Boolean)
9
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
10
+ .join(' ');
11
+ function segmentToPath(segment) {
12
+ // Route groups `(marketing)` don't contribute URL segments. Parallel-route
13
+ // slots `@modal` and private folders `_lib` are not standalone pages.
14
+ if (segment.startsWith('(')) {
15
+ return '';
16
+ }
17
+ if (segment.startsWith('@') || segment.startsWith('_')) {
18
+ return null;
19
+ }
20
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/) ?? segment.match(/^\[\[\.\.\.(.+)\]\]$/);
21
+ if (catchAll) {
22
+ return `:${catchAll[1]}*`;
23
+ }
24
+ const param = segment.match(/^\[(.+)\]$/);
25
+ if (param) {
26
+ return `:${param[1]}`;
27
+ }
28
+ return segment;
29
+ }
30
+ /**
31
+ * Discover page routes of a Next.js app-router directory (`app/` or
32
+ * `src/app/`). Returns display routes for `fimo-config.json#routes` so the
33
+ * Studio page switcher can enumerate a wrapped site's pages.
34
+ */
35
+ export function discoverNextRoutes(appDir) {
36
+ const routes = [];
37
+ const walk = (dir, segments) => {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir);
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ if (entries.some((entry) => PAGE_FILES.has(entry))) {
46
+ const parts = segments.filter((s) => s.length > 0);
47
+ const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
48
+ const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
49
+ routes.push({
50
+ id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
51
+ path: routePath,
52
+ label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
53
+ });
54
+ }
55
+ for (const entry of entries) {
56
+ const full = join(dir, entry);
57
+ let isDir = false;
58
+ try {
59
+ isDir = statSync(full).isDirectory();
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ if (!isDir) {
65
+ continue;
66
+ }
67
+ const mapped = segmentToPath(entry);
68
+ if (mapped === null) {
69
+ continue;
70
+ }
71
+ walk(full, [...segments, mapped]);
72
+ }
73
+ };
74
+ walk(appDir, []);
75
+ return routes.sort((a, b) => a.path.localeCompare(b.path));
76
+ }
77
+ const PAGES_EXTENSIONS = /\.(tsx|jsx|ts|js)$/;
78
+ const PAGES_SKIP = new Set(['api']);
79
+ /**
80
+ * Pages-router discovery (`pages/` or `src/pages/`): every module is a route
81
+ * except `_app`/`_document`/`_error` and `api/`. `[param]` segments map to
82
+ * `:param` (the parity capture skips dynamic routes, same as app router).
83
+ */
84
+ export function discoverNextPagesRoutes(pagesDir) {
85
+ const routes = [];
86
+ const toSegment = (name) => name
87
+ .replace(/^\[\.\.\.(.+)\]$/, ':$1')
88
+ .replace(/^\[\[\.\.\.(.+)\]\]$/, ':$1')
89
+ .replace(/^\[(.+)\]$/, ':$1');
90
+ const walk = (dir, segments) => {
91
+ let entries;
92
+ try {
93
+ entries = readdirSync(dir);
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ for (const entry of entries) {
99
+ const full = join(dir, entry);
100
+ let isDir = false;
101
+ try {
102
+ isDir = statSync(full).isDirectory();
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ if (isDir) {
108
+ // api/ is only special at the pages root.
109
+ if (!(segments.length === 0 && PAGES_SKIP.has(entry))) {
110
+ walk(full, [...segments, toSegment(entry)]);
111
+ }
112
+ continue;
113
+ }
114
+ if (!PAGES_EXTENSIONS.test(entry)) {
115
+ continue;
116
+ }
117
+ const base = entry.replace(PAGES_EXTENSIONS, '');
118
+ if (base.startsWith('_')) {
119
+ continue;
120
+ }
121
+ const parts = base === 'index' ? segments : [...segments, toSegment(base)];
122
+ const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
123
+ const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
124
+ routes.push({
125
+ id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
126
+ path: routePath,
127
+ label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
128
+ });
129
+ }
130
+ };
131
+ walk(pagesDir, []);
132
+ routes.sort((a, b) => a.path.localeCompare(b.path));
133
+ return routes;
134
+ }
@@ -0,0 +1,33 @@
1
+ import type { FrameworkDetection } from '../../detect-framework.js';
2
+ /**
3
+ * Next.js-side wrap wiring: everything the wrap migration must touch that is
4
+ * specific to this framework lives HERE, not in the generic flow. Mirror this
5
+ * module when adding a framework (see the fimo-framework-adapter skill).
6
+ */
7
+ export declare function ensureNextWiring(cwd: string): {
8
+ changes: string[];
9
+ warnings: string[];
10
+ };
11
+ export interface RootWiringResult {
12
+ changes: string[];
13
+ warnings: string[];
14
+ /** True when the root wiring is in place (pre-existing or just applied). */
15
+ wired: boolean;
16
+ }
17
+ /**
18
+ * Candidate app-router root-layout files: the direct `<appDir>/layout.*` plus,
19
+ * for i18n apps that place the `<html>` layout under a dynamic segment
20
+ * (next-intl's `app/[locale]/layout.tsx`), any `<appDir>/[segment]/layout.*`.
21
+ * Existing files only, in resolution order.
22
+ */
23
+ export declare function nextRootLayoutFiles(cwd: string, appDir: string): string[];
24
+ /**
25
+ * Wire the app-router root layout deterministically: imports, async default
26
+ * export, loader calls, `<FimoNextScripts />` in head, and `{children}`
27
+ * wrapped in `<FimoNextProviders>`. Only the provably safe shape is touched —
28
+ * one default-exported function whose JSX returns `<html>…<body>` with a
29
+ * `{children}` expression somewhere inside body (create-next-app and its
30
+ * variants). Anything else bails with the reason, and the agent prompt keeps
31
+ * the manual recipe.
32
+ */
33
+ export declare function ensureNextLayoutWiring(cwd: string, detection: Pick<FrameworkDetection, 'appDir'>): RootWiringResult;
@@ -0,0 +1,186 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseSync } from 'oxc-parser';
4
+ import { ensureFimoImport } from '../../apply-wrap.js';
5
+ /**
6
+ * Next.js-side wrap wiring: everything the wrap migration must touch that is
7
+ * specific to this framework lives HERE, not in the generic flow. Mirror this
8
+ * module when adding a framework (see the fimo-framework-adapter skill).
9
+ */
10
+ export function ensureNextWiring(cwd) {
11
+ const changes = [];
12
+ const warnings = [];
13
+ const configFile = ['next.config.ts', 'next.config.mjs', 'next.config.js', 'next.config.cjs']
14
+ .map((candidate) => path.join(cwd, candidate))
15
+ .find((candidate) => fs.existsSync(candidate));
16
+ if (configFile) {
17
+ const source = fs.readFileSync(configFile, 'utf8');
18
+ if (!source.includes('allowedDevOrigins')) {
19
+ // The three mainstream config shapes: a named object binding, a direct
20
+ // `export default {` literal, and CJS `module.exports = {`.
21
+ let patched = source.replace(/(const\s+nextConfig[^=]*=\s*\{)/, `$1\n allowedDevOrigins: ['*.preview.fimo.ai'],`);
22
+ if (patched === source) {
23
+ patched = source.replace(/((?:export\s+default|module\.exports\s*=)\s*\{)/, `$1\n allowedDevOrigins: ['*.preview.fimo.ai'],\n`);
24
+ }
25
+ if (patched !== source) {
26
+ fs.writeFileSync(configFile, patched);
27
+ changes.push(`added allowedDevOrigins to ${path.basename(configFile)}`);
28
+ }
29
+ else {
30
+ warnings.push(`Could not patch ${path.basename(configFile)} automatically — add: allowedDevOrigins: ['*.preview.fimo.ai']`);
31
+ }
32
+ }
33
+ }
34
+ else {
35
+ warnings.push("No next.config found — create one with: allowedDevOrigins: ['*.preview.fimo.ai']");
36
+ }
37
+ return { changes, warnings };
38
+ }
39
+ function findFirst(node, predicate) {
40
+ if (!node || typeof node !== 'object') {
41
+ return null;
42
+ }
43
+ if (predicate(node)) {
44
+ return node;
45
+ }
46
+ for (const value of Object.values(node)) {
47
+ if (Array.isArray(value)) {
48
+ for (const entry of value) {
49
+ const found = findFirst(entry, predicate);
50
+ if (found) {
51
+ return found;
52
+ }
53
+ }
54
+ }
55
+ else if (value && typeof value === 'object') {
56
+ const found = findFirst(value, predicate);
57
+ if (found) {
58
+ return found;
59
+ }
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ const jsxName = (element) => element?.openingElement?.name?.name;
65
+ /**
66
+ * Candidate app-router root-layout files: the direct `<appDir>/layout.*` plus,
67
+ * for i18n apps that place the `<html>` layout under a dynamic segment
68
+ * (next-intl's `app/[locale]/layout.tsx`), any `<appDir>/[segment]/layout.*`.
69
+ * Existing files only, in resolution order.
70
+ */
71
+ export function nextRootLayoutFiles(cwd, appDir) {
72
+ const names = ['layout.tsx', 'layout.jsx', 'layout.js'];
73
+ const dirs = [path.join(cwd, appDir)];
74
+ try {
75
+ for (const entry of fs.readdirSync(path.join(cwd, appDir), { withFileTypes: true })) {
76
+ if (entry.isDirectory() && entry.name.startsWith('[') && entry.name.endsWith(']')) {
77
+ dirs.push(path.join(cwd, appDir, entry.name));
78
+ }
79
+ }
80
+ }
81
+ catch {
82
+ // appDir missing — no candidates
83
+ }
84
+ return dirs.flatMap((dir) => names.map((name) => path.join(dir, name))).filter((file) => fs.existsSync(file));
85
+ }
86
+ /**
87
+ * Wire the app-router root layout deterministically: imports, async default
88
+ * export, loader calls, `<FimoNextScripts />` in head, and `{children}`
89
+ * wrapped in `<FimoNextProviders>`. Only the provably safe shape is touched —
90
+ * one default-exported function whose JSX returns `<html>…<body>` with a
91
+ * `{children}` expression somewhere inside body (create-next-app and its
92
+ * variants). Anything else bails with the reason, and the agent prompt keeps
93
+ * the manual recipe.
94
+ */
95
+ export function ensureNextLayoutWiring(cwd, detection) {
96
+ if (!detection.appDir) {
97
+ return {
98
+ changes: [],
99
+ warnings: ['Root wiring not applied: pages-router project (no app directory) — wire pages/_app manually.'],
100
+ wired: false,
101
+ };
102
+ }
103
+ const candidates = nextRootLayoutFiles(cwd, detection.appDir);
104
+ // Already wired anywhere — an agent may have mounted the [locale] layout by hand.
105
+ if (candidates.some((file) => fs.readFileSync(file, 'utf8').includes('FimoNextProviders'))) {
106
+ return { changes: [], warnings: [], wired: true };
107
+ }
108
+ // The real root layout is the one that returns <html> (next-intl puts it under
109
+ // app/[locale]/); fall back to the first candidate for the bail message.
110
+ const layoutFile = candidates.find((file) => fs.readFileSync(file, 'utf8').includes('<html')) ?? candidates[0];
111
+ const rel = layoutFile ? path.relative(cwd, layoutFile) : `${detection.appDir}/layout.tsx`;
112
+ const bail = (reason) => ({
113
+ changes: [],
114
+ warnings: [`Root wiring not applied: ${reason} — follow the layout recipe in the agent prompt (${rel}).`],
115
+ wired: false,
116
+ });
117
+ if (!layoutFile) {
118
+ return bail('no root layout file found');
119
+ }
120
+ const code = fs.readFileSync(layoutFile, 'utf8');
121
+ let program;
122
+ try {
123
+ program = parseSync(layoutFile, code).program;
124
+ }
125
+ catch {
126
+ return bail('root layout could not be parsed');
127
+ }
128
+ const exportDefault = (program.body ?? []).find((node) => node.type === 'ExportDefaultDeclaration');
129
+ if (!exportDefault || exportDefault.declaration?.type !== 'FunctionDeclaration') {
130
+ return bail('the default export is not a plain function declaration');
131
+ }
132
+ const layoutFn = exportDefault.declaration;
133
+ const body = layoutFn.body;
134
+ if (!body || (body.type !== 'FunctionBody' && body.type !== 'BlockStatement')) {
135
+ return bail('the layout function has no block body');
136
+ }
137
+ const html = findFirst(body, (node) => node?.type === 'JSXElement' && jsxName(node) === 'html');
138
+ if (!html) {
139
+ return bail('the layout does not return an <html> element');
140
+ }
141
+ const htmlBody = (html.children ?? []).find((child) => child?.type === 'JSXElement' && jsxName(child) === 'body');
142
+ if (!htmlBody) {
143
+ return bail('no <body> element found under <html>');
144
+ }
145
+ const childrenExpr = findFirst(htmlBody, (node) => node?.type === 'JSXExpressionContainer' &&
146
+ node.expression?.type === 'Identifier' &&
147
+ node.expression?.name === 'children');
148
+ if (!childrenExpr) {
149
+ return bail('no {children} expression found inside <body>');
150
+ }
151
+ const edits = [];
152
+ if (!layoutFn.async) {
153
+ edits.push({ start: layoutFn.start, end: layoutFn.start, replacement: 'async ' });
154
+ }
155
+ edits.push({
156
+ start: body.start + 1,
157
+ end: body.start + 1,
158
+ replacement: '\n const config = loadFimoConfig();\n const labels = await loadFimoLabels();\n',
159
+ });
160
+ edits.push({
161
+ start: childrenExpr.start,
162
+ end: childrenExpr.end,
163
+ replacement: `<FimoNextProviders config={config} labels={labels}>${code.slice(childrenExpr.start, childrenExpr.end)}</FimoNextProviders>`,
164
+ });
165
+ const head = (html.children ?? []).find((child) => child?.type === 'JSXElement' && jsxName(child) === 'head');
166
+ if (head) {
167
+ const at = head.openingElement.end;
168
+ edits.push({ start: at, end: at, replacement: '<FimoNextScripts />' });
169
+ }
170
+ else {
171
+ const at = html.openingElement.end;
172
+ edits.push({
173
+ start: at,
174
+ end: at,
175
+ replacement: '\n <head>\n <FimoNextScripts />\n </head>',
176
+ });
177
+ }
178
+ let next = code;
179
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
180
+ next = next.slice(0, edit.start) + edit.replacement + next.slice(edit.end);
181
+ }
182
+ next = ensureFimoImport(next, ['FimoNextProviders', 'FimoNextScripts'], 'fimo/next');
183
+ next = ensureFimoImport(next, ['loadFimoConfig', 'loadFimoLabels'], 'fimo/next/server');
184
+ fs.writeFileSync(layoutFile, next);
185
+ return { changes: [`wired Fimo providers into ${rel}`], warnings: [], wired: true };
186
+ }
@@ -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 {};