fimo 0.8.0 → 0.9.0-experimental.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
  2. package/assets/agents-starter/claude/settings.json +19 -0
  3. package/assets/content-templates/hooks-template.eta +4 -2
  4. package/assets/content-templates/types-template.eta +67 -1
  5. package/assets/skills/fimo/SKILL.md +1 -1
  6. package/assets/skills/fimo/references/content.md +86 -15
  7. package/assets/skills/fimo/references/translations.md +75 -0
  8. package/assets/skills/fimo-cli/SKILL.md +6 -2
  9. package/assets/skills/fimo-cli/references/content.md +61 -2
  10. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  11. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  12. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  13. package/assets/skills/fimo-migration/SKILL.md +24 -2
  14. package/assets/skills/fimo-migration/references/content.md +24 -0
  15. package/assets/skills/fimo-migration/references/manifest.md +46 -0
  16. package/assets/skills/fimo-migration/references/pages.md +234 -0
  17. package/assets/skills/fimo-migration/references/qa.md +24 -2
  18. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  19. package/assets/skills/fimo-migration/references/wrap.md +662 -0
  20. package/assets/skills/fimo-studio/SKILL.md +5 -3
  21. package/assets/skills/fimo-studio/references/cms.md +9 -1
  22. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  23. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  24. package/dist/build/vite/plugins/translations.d.ts +2 -8
  25. package/dist/build/vite/plugins/translations.js +31 -131
  26. package/dist/cli/bundle.json +1 -1
  27. package/dist/cli/index.js +1337 -189
  28. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  29. package/dist/runtime/app/FimoProviders.js +31 -30
  30. package/dist/runtime/astro/index.d.ts +2 -0
  31. package/dist/runtime/astro/index.js +3 -0
  32. package/dist/runtime/astro/server.d.ts +3 -0
  33. package/dist/runtime/astro/server.js +6 -0
  34. package/dist/runtime/content/client/internal.js +1 -0
  35. package/dist/runtime/content/client/types.d.ts +16 -0
  36. package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
  37. package/dist/runtime/core/BuiltWithBadge.js +68 -0
  38. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  39. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  40. package/dist/runtime/next/FimoNextProviders.js +20 -0
  41. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  42. package/dist/runtime/next/FimoNextScripts.js +19 -0
  43. package/dist/runtime/next/index.d.ts +14 -0
  44. package/dist/runtime/next/index.js +14 -0
  45. package/dist/runtime/next/server.d.ts +2 -0
  46. package/dist/runtime/next/server.js +4 -0
  47. package/dist/runtime/nuxt/composables.d.ts +9 -0
  48. package/dist/runtime/nuxt/composables.js +57 -0
  49. package/dist/runtime/nuxt/index.d.ts +3 -0
  50. package/dist/runtime/nuxt/index.js +6 -0
  51. package/dist/runtime/nuxt/server.d.ts +3 -0
  52. package/dist/runtime/nuxt/server.js +6 -0
  53. package/dist/runtime/primitives/components/RichText.d.ts +5 -1
  54. package/dist/runtime/primitives/components/RichText.js +2 -2
  55. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  56. package/dist/runtime/primitives/labels-hook.js +41 -0
  57. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  58. package/dist/runtime/primitives/labels-store.js +60 -0
  59. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  60. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  61. package/dist/runtime/primitives/translations.d.ts +1 -27
  62. package/dist/runtime/primitives/translations.js +7 -85
  63. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  64. package/dist/runtime/react/FimoSpaProviders.js +122 -0
  65. package/dist/runtime/react/index.d.ts +35 -0
  66. package/dist/runtime/react/index.js +40 -0
  67. package/dist/runtime/react/spa-content-cache.d.ts +38 -0
  68. package/dist/runtime/react/spa-content-cache.js +64 -0
  69. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  70. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  71. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  72. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  73. package/dist/runtime/remix/index.d.ts +12 -0
  74. package/dist/runtime/remix/index.js +11 -0
  75. package/dist/runtime/remix/server.d.ts +2 -0
  76. package/dist/runtime/remix/server.js +2 -0
  77. package/dist/runtime/seo/neutral.d.ts +22 -0
  78. package/dist/runtime/seo/neutral.js +17 -0
  79. package/dist/runtime/shared/display-routes.d.ts +14 -0
  80. package/dist/runtime/shared/display-routes.js +25 -0
  81. package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
  82. package/dist/runtime/shared/fimo-config.server.js +1 -13
  83. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  84. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  85. package/dist/runtime/shared/i18n-messages.js +79 -0
  86. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  87. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  88. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  89. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  90. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  91. package/dist/runtime/shared/labels-t.pure.js +10 -0
  92. package/dist/runtime/strapi/client.d.ts +30 -0
  93. package/dist/runtime/strapi/client.js +132 -0
  94. package/dist/runtime/strapi/index.d.ts +5 -0
  95. package/dist/runtime/strapi/index.js +6 -0
  96. package/dist/runtime/strapi/reshape.d.ts +50 -0
  97. package/dist/runtime/strapi/reshape.js +121 -0
  98. package/dist/runtime/svelte/index.d.ts +3 -0
  99. package/dist/runtime/svelte/index.js +7 -0
  100. package/dist/runtime/svelte/server.d.ts +3 -0
  101. package/dist/runtime/svelte/server.js +6 -0
  102. package/dist/runtime/vue/index.d.ts +3 -0
  103. package/dist/runtime/vue/index.js +5 -0
  104. package/dist/runtime/vue/server.d.ts +3 -0
  105. package/dist/runtime/vue/server.js +6 -0
  106. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  107. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  108. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  109. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  110. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  111. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  112. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
  113. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  114. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  115. package/dist/runtime/wrapped/FimoText.js +20 -0
  116. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  117. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  118. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  119. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  120. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  121. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  122. package/dist/runtime/wrapped/dom.d.ts +4 -0
  123. package/dist/runtime/wrapped/dom.js +23 -0
  124. package/dist/runtime/wrapped/server.d.ts +80 -0
  125. package/dist/runtime/wrapped/server.js +207 -0
  126. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  127. package/dist/scripts/apply-wrap-templates.js +137 -0
  128. package/dist/scripts/apply-wrap.d.ts +73 -0
  129. package/dist/scripts/apply-wrap.js +340 -0
  130. package/dist/scripts/cms/discover.d.ts +2 -0
  131. package/dist/scripts/cms/discover.js +17 -0
  132. package/dist/scripts/cms/example-content.d.ts +33 -0
  133. package/dist/scripts/cms/example-content.js +165 -0
  134. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  135. package/dist/scripts/cms/strapi/detect.js +186 -0
  136. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  137. package/dist/scripts/cms/strapi/index.js +25 -0
  138. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  139. package/dist/scripts/cms/strapi/list-source.js +90 -0
  140. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  141. package/dist/scripts/cms/strapi/schema-map.js +289 -0
  142. package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
  143. package/dist/scripts/cms/strapi/shapes.js +48 -0
  144. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  145. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  146. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  147. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  148. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  149. package/dist/scripts/cms/strapi/wiring.js +46 -0
  150. package/dist/scripts/cms/types.d.ts +145 -0
  151. package/dist/scripts/cms/types.js +11 -0
  152. package/dist/scripts/detect-framework.d.ts +39 -0
  153. package/dist/scripts/detect-framework.js +139 -0
  154. package/dist/scripts/draft-rows.d.ts +19 -0
  155. package/dist/scripts/draft-rows.js +178 -0
  156. package/dist/scripts/draft-schemas.d.ts +27 -0
  157. package/dist/scripts/draft-schemas.js +247 -0
  158. package/dist/scripts/extract-translations.js +16 -11
  159. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  160. package/dist/scripts/frameworks/astro/routes.js +44 -0
  161. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  162. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  163. package/dist/scripts/frameworks/discover.d.ts +83 -0
  164. package/dist/scripts/frameworks/discover.js +288 -0
  165. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  166. package/dist/scripts/frameworks/discovered-route.js +17 -0
  167. package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
  168. package/dist/scripts/frameworks/next/rewire.js +282 -0
  169. package/dist/scripts/frameworks/next/routes.d.ts +30 -0
  170. package/dist/scripts/frameworks/next/routes.js +176 -0
  171. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  172. package/dist/scripts/frameworks/next/wiring.js +186 -0
  173. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  174. package/dist/scripts/frameworks/nuxt/routes.js +38 -0
  175. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  176. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  177. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  178. package/dist/scripts/frameworks/remix/routes.js +36 -0
  179. package/dist/scripts/frameworks/support.d.ts +75 -0
  180. package/dist/scripts/frameworks/support.js +165 -0
  181. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  182. package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
  183. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  184. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  185. package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
  186. package/dist/scripts/frameworks/vite/routes.js +143 -0
  187. package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
  188. package/dist/scripts/frameworks/vite/wiring.js +144 -0
  189. package/dist/scripts/i18n-namespace.d.ts +27 -0
  190. package/dist/scripts/i18n-namespace.js +155 -0
  191. package/dist/scripts/lib/humanize.d.ts +14 -0
  192. package/dist/scripts/lib/humanize.js +54 -0
  193. package/dist/scripts/lib/validation-report.d.ts +1 -3
  194. package/dist/scripts/lib/validation-report.js +3 -9
  195. package/dist/scripts/render-parity/capture.d.ts +64 -0
  196. package/dist/scripts/render-parity/capture.js +436 -0
  197. package/dist/scripts/render-parity/compare.d.ts +59 -0
  198. package/dist/scripts/render-parity/compare.js +0 -0
  199. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  200. package/dist/scripts/render-parity/normalize-html.js +223 -0
  201. package/dist/scripts/render-parity/types.d.ts +41 -0
  202. package/dist/scripts/render-parity/types.js +1 -0
  203. package/dist/scripts/routes/bindings.d.ts +93 -0
  204. package/dist/scripts/routes/bindings.js +117 -0
  205. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  206. package/dist/scripts/routes/label-prefixes.js +136 -0
  207. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  208. package/dist/scripts/routes/module-resolve.js +78 -0
  209. package/dist/scripts/routes/page-structure.d.ts +72 -0
  210. package/dist/scripts/routes/page-structure.js +234 -0
  211. package/dist/scripts/scan-wrappable.d.ts +200 -0
  212. package/dist/scripts/scan-wrappable.js +1355 -0
  213. package/dist/scripts/validate-route-metadata.js +11 -0
  214. package/dist/scripts/validate-schemas.js +42 -1
  215. package/package.json +88 -4
  216. package/release.json +2 -2
  217. package/templates/react-router/package.json +1 -1
  218. package/templates/react-router/translations/en.json +1 -0
@@ -0,0 +1,78 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', '.fimo']);
4
+ export const DEFAULT_COMPONENT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
5
+ export function walkFiles(dir, matches, out) {
6
+ let entries;
7
+ try {
8
+ entries = readdirSync(dir);
9
+ }
10
+ catch {
11
+ return;
12
+ }
13
+ for (const entry of entries) {
14
+ if (SKIP_DIRS.has(entry)) {
15
+ continue;
16
+ }
17
+ const full = join(dir, entry);
18
+ let isDir = false;
19
+ try {
20
+ isDir = statSync(full).isDirectory();
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ if (isDir) {
26
+ walkFiles(full, matches, out);
27
+ }
28
+ else if (matches(entry)) {
29
+ out.push(full);
30
+ }
31
+ }
32
+ }
33
+ /** Import map of a routes file: local component name → module specifier. */
34
+ export function componentImports(code) {
35
+ const imports = new Map();
36
+ for (const match of code.matchAll(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from\s+['"]([^'"]+)['"]/g)) {
37
+ imports.set(match[1], match[2]);
38
+ }
39
+ for (const match of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
40
+ for (const token of match[1].split(',')) {
41
+ const name = token.replace(/\s+as\s+(\w+)\s*$/, '$1').trim();
42
+ if (/^\w+$/.test(name)) {
43
+ imports.set(name, match[2]);
44
+ }
45
+ }
46
+ }
47
+ for (const match of code.matchAll(/(?:const|let)\s+(\w+)\s*=\s*(?:React\.)?lazy\(\s*(?:async\s*)?\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
48
+ imports.set(match[1], match[2]);
49
+ }
50
+ return imports;
51
+ }
52
+ export function resolveModuleFile(cwd, fromFile, spec, extensions = DEFAULT_COMPONENT_EXTENSIONS) {
53
+ let base;
54
+ if (spec.startsWith('.')) {
55
+ base = join(dirname(fromFile), spec);
56
+ }
57
+ else if (spec.startsWith('~/') || spec.startsWith('@/')) {
58
+ base = join(cwd, 'src', spec.slice(2));
59
+ }
60
+ else {
61
+ return null;
62
+ }
63
+ for (const candidate of [
64
+ ...extensions.map((ext) => `${base}${ext}`),
65
+ ...extensions.map((ext) => join(base, `index${ext}`)),
66
+ base,
67
+ ]) {
68
+ try {
69
+ if (statSync(candidate).isFile()) {
70
+ return candidate;
71
+ }
72
+ }
73
+ catch {
74
+ /* try next */
75
+ }
76
+ }
77
+ return null;
78
+ }
@@ -0,0 +1,72 @@
1
+ import type { CollectionCandidate } from '../scan-wrappable.js';
2
+ /**
3
+ * Page-structure drafts — the machine's proposed NESTED model of a page.
4
+ * The scanner already sees everything the model needs: the page file's
5
+ * section components (capitalized JSX children, in document order), each
6
+ * section's label keys (its file-derived prefix) and its in-code collection
7
+ * candidates (typed rows). Joined, that IS the page's Single: one nested
8
+ * block field per section, recursing into the section's own components so
9
+ * deep structure (a Features grid whose groups live in their own files)
10
+ * surfaces too. The agent refines names and shapes — it no longer decides
11
+ * WHETHER to model, and real marketing copy stops dying as "page furniture"
12
+ * exemptions.
13
+ */
14
+ export interface PageSectionDraft {
15
+ /** Proposed field key on the page Single (camelCase of the component). */
16
+ key: string;
17
+ component: string;
18
+ file: string;
19
+ /** The section's label-key prefix (file-derived). */
20
+ prefix: string;
21
+ /** Staged label keys the section owns — its scalar copy fields. */
22
+ labelKeys: string[];
23
+ /** In-code collection candidates found in the section's file — its
24
+ * repeated block items (cards, quotes, logos), with their source rows so
25
+ * the Single's document can be assembled without reopening the code. */
26
+ collections: Array<{
27
+ name: string;
28
+ fields: string[];
29
+ count: number;
30
+ rows?: Array<Record<string, unknown>>;
31
+ }>;
32
+ /** Content-bearing components the section renders — nested blocks. */
33
+ children?: PageSectionDraft[];
34
+ /** The same file drafts on several pages — chrome. Model it ONCE as a
35
+ * Single Type, never as a per-page block. */
36
+ shared?: boolean;
37
+ }
38
+ export interface PageStructureDraft {
39
+ pageId: string;
40
+ /** Proposed Single uid (PascalCase of the page id). */
41
+ single: string;
42
+ sections: PageSectionDraft[];
43
+ }
44
+ export declare function draftPageStructures(args: {
45
+ cwd: string;
46
+ framework: string | null;
47
+ routes: Record<string, {
48
+ path?: string;
49
+ labelPrefixes?: unknown;
50
+ [key: string]: unknown;
51
+ }>;
52
+ stagedKeys: Iterable<string>;
53
+ collections: CollectionCandidate[];
54
+ }): PageStructureDraft[];
55
+ /** Flat section count across the nesting — the summary's visibility number. */
56
+ export declare function countDraftSections(draft: PageStructureDraft): number;
57
+ /**
58
+ * The machine-BUILT page Single: a ready-to-push singleton schema plus its
59
+ * filled document, assembled from the draft's non-shared sections (shared
60
+ * sections are chrome — modeled once elsewhere). One `max: 1` block per
61
+ * section; the section's labels become scalar fields carrying the staged
62
+ * values, its repeated arrays become nested blocks carrying the source
63
+ * rows, its child components nest the same way. The agent renames fields
64
+ * to editorial names and rewires reads — it no longer composes anything.
65
+ */
66
+ export interface PageSingleDraft {
67
+ uid: string;
68
+ pageId: string;
69
+ schema: Record<string, unknown>;
70
+ document: Record<string, unknown>;
71
+ }
72
+ export declare function buildPageSingleDraft(draft: PageStructureDraft, labelValues: Record<string, string>): PageSingleDraft | null;
@@ -0,0 +1,234 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { frameworkSectionStyle } from '../frameworks/discover.js';
4
+ import { componentImports, resolveModuleFile, resolveRouteFiles } from './label-prefixes.js';
5
+ const MAX_SECTION_DEPTH = 3;
6
+ const MAX_DRAFT_ROWS = 50;
7
+ const pascal = (value) => value
8
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
9
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
10
+ .split(/[-_.\s]/)
11
+ .filter(Boolean)
12
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
13
+ .join('');
14
+ const camel = (value) => {
15
+ const p = pascal(value);
16
+ return p.charAt(0).toLowerCase() + p.slice(1);
17
+ };
18
+ /** Capitalized JSX tags in DOCUMENT order (deduped, first appearance). */
19
+ function sectionTags(code) {
20
+ const seen = new Set();
21
+ const tags = [];
22
+ for (const match of code.matchAll(/<([A-Z]\w*)[\s/>]/g)) {
23
+ if (!seen.has(match[1])) {
24
+ seen.add(match[1]);
25
+ tags.push(match[1]);
26
+ }
27
+ }
28
+ return tags;
29
+ }
30
+ function draftSection(context, tag, file, depth) {
31
+ const rel = relative(context.cwd, file);
32
+ const prefix = context.style.fileScope(rel);
33
+ const labelKeys = context.keysByPrefix.get(prefix) ?? [];
34
+ const collections = (context.collectionsByFile.get(rel) ?? []).map((candidate) => ({
35
+ name: candidate.name,
36
+ fields: candidate.fields,
37
+ count: candidate.count,
38
+ ...(candidate.typedRows ? { rows: candidate.typedRows.slice(0, MAX_DRAFT_ROWS) } : {}),
39
+ }));
40
+ const children = depth < MAX_SECTION_DEPTH ? draftChildSections(context, file, depth + 1) : [];
41
+ if (labelKeys.length === 0 && collections.length === 0 && children.length === 0) {
42
+ return null; // pure layout — nothing to model
43
+ }
44
+ const draft = { key: camel(tag), component: tag, file: rel, prefix, labelKeys, collections };
45
+ if (children.length > 0) {
46
+ draft.children = children;
47
+ }
48
+ return draft;
49
+ }
50
+ /** Auto-imported components (Nuxt's components/ convention): a tag with no
51
+ * import statement may still be a local file at a conventional path. */
52
+ function resolveAutoImportedComponent(context, tag) {
53
+ for (const dir of context.style.fallbackComponentDirs) {
54
+ for (const ext of context.style.extensions) {
55
+ const candidate = join(context.cwd, dir, `${tag}${ext}`);
56
+ try {
57
+ if (statSync(candidate).isFile()) {
58
+ return candidate;
59
+ }
60
+ }
61
+ catch {
62
+ /* try next */
63
+ }
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function draftChildSections(context, file, depth) {
69
+ let code;
70
+ try {
71
+ code = readFileSync(file, 'utf8');
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ const imports = componentImports(code);
77
+ const sections = [];
78
+ for (const tag of sectionTags(code)) {
79
+ const spec = imports.get(tag);
80
+ const sectionFile = spec
81
+ ? resolveModuleFile(context.cwd, file, spec, context.style.extensions)
82
+ : resolveAutoImportedComponent(context, tag);
83
+ if (!sectionFile || context.visited.has(sectionFile)) {
84
+ continue; // library components (Button, Link) have no local file
85
+ }
86
+ context.visited.add(sectionFile);
87
+ const section = draftSection(context, tag, sectionFile, depth);
88
+ if (section) {
89
+ sections.push(section);
90
+ }
91
+ }
92
+ return sections;
93
+ }
94
+ export function draftPageStructures(args) {
95
+ const { cwd, framework, routes, collections } = args;
96
+ const keysByPrefix = new Map();
97
+ for (const key of args.stagedKeys) {
98
+ const dot = key.indexOf('.');
99
+ if (dot > 0) {
100
+ const prefix = key.slice(0, dot);
101
+ keysByPrefix.set(prefix, [...(keysByPrefix.get(prefix) ?? []), key]);
102
+ }
103
+ }
104
+ const collectionsByFile = new Map();
105
+ for (const candidate of collections) {
106
+ collectionsByFile.set(candidate.file, [...(collectionsByFile.get(candidate.file) ?? []), candidate]);
107
+ }
108
+ const style = frameworkSectionStyle(framework);
109
+ const drafts = [];
110
+ for (const { routeId, file } of resolveRouteFiles(cwd, framework, routes)) {
111
+ const context = { cwd, style, keysByPrefix, collectionsByFile, visited: new Set([file]) };
112
+ const sections = draftChildSections(context, file, 1);
113
+ if (sections.length > 1) {
114
+ drafts.push({ pageId: routeId, single: pascal(routeId), sections });
115
+ }
116
+ }
117
+ markSharedSections(drafts);
118
+ return drafts;
119
+ }
120
+ /** A section file drafted on several pages is chrome — flag it so the agent
121
+ * models it once (Single Type) instead of nesting it under every page. */
122
+ function markSharedSections(drafts) {
123
+ const pagesByFile = new Map();
124
+ const index = (pageId, sections) => {
125
+ for (const section of sections) {
126
+ pagesByFile.set(section.file, (pagesByFile.get(section.file) ?? new Set()).add(pageId));
127
+ index(pageId, section.children ?? []);
128
+ }
129
+ };
130
+ for (const draft of drafts) {
131
+ index(draft.pageId, draft.sections);
132
+ }
133
+ const flag = (sections) => {
134
+ for (const section of sections) {
135
+ if ((pagesByFile.get(section.file)?.size ?? 0) > 1) {
136
+ section.shared = true;
137
+ }
138
+ flag(section.children ?? []);
139
+ }
140
+ };
141
+ for (const draft of drafts) {
142
+ flag(draft.sections);
143
+ }
144
+ }
145
+ /** Flat section count across the nesting — the summary's visibility number. */
146
+ export function countDraftSections(draft) {
147
+ const walk = (sections) => sections.reduce((total, section) => total + 1 + walk(section.children ?? []), 0);
148
+ return walk(draft.sections);
149
+ }
150
+ const labelFieldName = (key) => camel(key.split('.').pop() ?? key);
151
+ const scalarType = (value) => value.length > 80 || value.includes('\n') ? 'text' : 'string';
152
+ function sectionBlock(section, labelValues) {
153
+ const fields = {};
154
+ const value = {};
155
+ let fieldCount = 0;
156
+ for (const key of section.labelKeys) {
157
+ const name = labelFieldName(key);
158
+ if (fields[name]) {
159
+ continue;
160
+ }
161
+ const labelValue = labelValues[key] ?? '';
162
+ fields[name] = { type: scalarType(labelValue) };
163
+ value[name] = labelValue;
164
+ fieldCount += 1;
165
+ }
166
+ for (const collection of section.collections) {
167
+ const name = camel(collection.name);
168
+ if (fields[name]) {
169
+ continue;
170
+ }
171
+ const itemFields = {};
172
+ for (const field of collection.fields) {
173
+ itemFields[field] = { type: 'string' };
174
+ }
175
+ if (Object.keys(itemFields).length === 0) {
176
+ continue;
177
+ }
178
+ fields[name] = { type: 'blocks', schema: { fields: itemFields } };
179
+ value[name] = (collection.rows ?? []).map((row) => {
180
+ const item = {};
181
+ for (const field of collection.fields) {
182
+ const cell = row[field];
183
+ if (cell != null && typeof cell !== 'object') {
184
+ item[field] = typeof cell === 'string' ? cell : String(cell);
185
+ }
186
+ }
187
+ return item;
188
+ });
189
+ fieldCount += 1;
190
+ }
191
+ for (const child of section.children ?? []) {
192
+ if (child.shared) {
193
+ continue;
194
+ }
195
+ const block = sectionBlock(child, labelValues);
196
+ if (!block || fields[child.key]) {
197
+ continue;
198
+ }
199
+ fields[child.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
200
+ value[child.key] = [block.value];
201
+ fieldCount += block.fieldCount;
202
+ }
203
+ if (Object.keys(fields).length === 0) {
204
+ return null;
205
+ }
206
+ return { fields, value, fieldCount };
207
+ }
208
+ export function buildPageSingleDraft(draft, labelValues) {
209
+ const fields = {};
210
+ const document = {};
211
+ let totalFields = 0;
212
+ for (const section of draft.sections) {
213
+ if (section.shared || fields[section.key]) {
214
+ continue;
215
+ }
216
+ const block = sectionBlock(section, labelValues);
217
+ if (!block) {
218
+ continue;
219
+ }
220
+ fields[section.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
221
+ document[section.key] = [block.value];
222
+ totalFields += block.fieldCount;
223
+ }
224
+ // A one-field page is a label, not a Single — only real structure drafts.
225
+ if (Object.keys(fields).length === 0 || totalFields < 3) {
226
+ return null;
227
+ }
228
+ return {
229
+ uid: draft.single,
230
+ pageId: draft.pageId,
231
+ schema: { uid: draft.single, name: draft.single, isSingleton: true, fields },
232
+ document,
233
+ };
234
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Inverse of extract-translations: finds content that is NOT yet wrapped with
3
+ * Fimo primitives — hardcoded JSX text, image/video sources, file links, and
4
+ * user-visible attribute strings. Drives the wrap migration's coverage gate:
5
+ * every item must end wrapped, ignored-with-a-reason, or unresolved.
6
+ */
7
+ export type WrappableKind = 'text' | 'formatted-text' | 'image' | 'video' | 'file-link' | 'attribute';
8
+ export interface WrappableItem {
9
+ kind: WrappableKind;
10
+ file: string;
11
+ line: number;
12
+ preview: string;
13
+ /** AST span info for the --apply codemod (offsets into the file). */
14
+ span?: {
15
+ start: number;
16
+ end: number;
17
+ parentTag?: string;
18
+ openNameStart?: number;
19
+ openNameEnd?: number;
20
+ closeNameStart?: number;
21
+ closeNameEnd?: number;
22
+ };
23
+ }
24
+ /**
25
+ * In-code collection candidate: an array literal of same-shaped objects with
26
+ * user-visible string fields (services cards, FAQ items, testimonials, …).
27
+ * Flat labels are the wrong shape for these — they belong in a Fimo
28
+ * collection (schema + entries), migrated by the agent.
29
+ */
30
+ export interface CollectionCandidate {
31
+ name: string;
32
+ file: string;
33
+ line: number;
34
+ count: number;
35
+ fields: string[];
36
+ sample: string;
37
+ /**
38
+ * Cross-collection references detected by value matching: this candidate's
39
+ * field values point at another candidate's identity values. Migrated as
40
+ * Fimo `reference` fields (`target`/`targets` + `many`), never as strings.
41
+ */
42
+ references?: CandidateReference[];
43
+ /** Per-field string values (capped) — internal input to reference
44
+ * detection; stripped before the plan is written. */
45
+ fieldValues?: Record<string, string[]>;
46
+ /** First row with NATIVE value types — internal input to schema-draft
47
+ * type inference; stripped before the plan is written. */
48
+ typedSample?: Record<string, unknown>;
49
+ /** All rows (capped) with NATIVE value types — internal input to the
50
+ * rows-staging generator; stripped before the plan is written. */
51
+ typedRows?: Record<string, unknown>[];
52
+ manyFields?: string[];
53
+ }
54
+ export interface CandidateReference {
55
+ field: string;
56
+ /** Candidate name(s) the values point at: one = fixed target, several = polymorphic. */
57
+ targets: string[];
58
+ many: boolean;
59
+ }
60
+ interface ReferenceMatchable {
61
+ name: string;
62
+ references?: CandidateReference[];
63
+ fieldValues?: Record<string, string[]>;
64
+ manyFields?: string[];
65
+ }
66
+ /**
67
+ * Annotate candidates whose field values point at another candidate's
68
+ * identity values: one full-coverage target = fixed reference, several
69
+ * partial targets covering together = polymorphic. Mutates in place.
70
+ *
71
+ * Coverage is OCCURRENCE-weighted and dangling-adjusted: values matching no
72
+ * candidate's identity set are dangling references by definition (deleted
73
+ * targets, sentinels) and drop out of the denominator, weighted by how
74
+ * often they occur — 1,700 case studies pointing at 12 clients with 190
75
+ * distinct ghost values is still a reference field, because 89% of the ROWS
76
+ * resolve. Guards against coincidence: at least 2 distinct matched values
77
+ * and at least half the occurrences must resolve somewhere.
78
+ */
79
+ export declare function annotateCandidateReferences(candidates: ReferenceMatchable[]): void;
80
+ /**
81
+ * External live data source: content fetched at render time from a
82
+ * third-party API (GitHub stats, dev.to posts, a headless CMS). Invisible to
83
+ * the wrap scan by nature — the migration must ASK the user what to do with
84
+ * it (keep live vs ingest) instead of guessing.
85
+ */
86
+ export interface ExternalSource {
87
+ host: string;
88
+ files: string[];
89
+ count: number;
90
+ /** 'cms' when detected via a headless-CMS SDK dependency: the site's
91
+ * CONTENT lives there — migrating it is the point, not an option. */
92
+ kind?: 'api' | 'cms';
93
+ }
94
+ /** Identities of already-wrapped content, for the seeding gate. */
95
+ export interface WrappedIds {
96
+ labels: string[];
97
+ media: string[];
98
+ richText: string[];
99
+ files: string[];
100
+ /** Every wrapped key, ALL kinds, in ENCOUNTER order (per-file walk, files
101
+ * in walk order) — the on-page reading order the Studio Pages view sorts
102
+ * by. Deduplicated keeping the first occurrence; never sorted. */
103
+ ordered: string[];
104
+ }
105
+ export interface WrapScanResult {
106
+ items: WrappableItem[];
107
+ wrappedCount: number;
108
+ filesScanned: number;
109
+ wrappedIds: WrappedIds;
110
+ collections: CollectionCandidate[];
111
+ externalSources: ExternalSource[];
112
+ dataFiles: DataCopyFile[];
113
+ /** `t()` calls bound to a FOREIGN i18n library (next-intl, i18next, …), not
114
+ * Fimo — surfaced so the migration never claims them as editable coverage. */
115
+ foreignI18nCount: number;
116
+ }
117
+ /**
118
+ * A .json data file whose values are site copy (config-driven sites keep ALL
119
+ * their text there and templates render only expressions, so template/JSX
120
+ * scanning finds nothing). Each detected file must be triaged in the manifest
121
+ * (`wrap.dataFiles`) — otherwise a copy-in-config site would pass the
122
+ * coverage gate with zero editable content.
123
+ */
124
+ export interface DataCopyFile {
125
+ file: string;
126
+ strings: number;
127
+ samples: string[];
128
+ collections: DataFileCollection[];
129
+ /** 'markdown' groups: prose documents (md/mdx) aggregated per directory —
130
+ * content the wrap cannot label-ify (Tier 1b ingestion territory), but the
131
+ * triage decision must still be explicit. */
132
+ kind?: 'markdown';
133
+ /** Number of documents behind a markdown group entry. */
134
+ count?: number;
135
+ }
136
+ export interface DataFileCollection {
137
+ path: string;
138
+ length: number;
139
+ copyFields: string[];
140
+ references?: CandidateReference[];
141
+ /** Internal input to reference detection; stripped before the plan is written. */
142
+ fieldValues?: Record<string, string[]>;
143
+ /** First row with NATIVE value types — internal input to schema-draft
144
+ * type inference; stripped before the plan is written. */
145
+ typedSample?: Record<string, unknown>;
146
+ /** All rows (capped) with NATIVE value types — internal to rows staging. */
147
+ typedRows?: Record<string, unknown>[];
148
+ manyFields?: string[];
149
+ /** Fields forced to `string` regardless of inferred type — markdown/MDX
150
+ * bodies stay verbatim so the user's existing renderer keeps working
151
+ * (wrap mode never reshapes what the page already renders). */
152
+ verbatimFields?: string[];
153
+ }
154
+ export declare function scanWrappableFile(filePath: string, code: string, relPath: string, opts?: {
155
+ i18nFimoWired?: boolean;
156
+ }): {
157
+ items: WrappableItem[];
158
+ wrapped: number;
159
+ wrappedIds: WrappedIds;
160
+ collections: CollectionCandidate[];
161
+ foreignI18n?: number;
162
+ };
163
+ /**
164
+ * Parse ONE markdown/MDX document into a typed row: YAML frontmatter fields
165
+ * (natively typed — dates, booleans, numbers, arrays) plus a `slug` (from
166
+ * frontmatter or the filename stem) and a `body` holding the raw markdown
167
+ * after the frontmatter, verbatim. The body stays a string so the user's
168
+ * existing markdown renderer keeps rendering it unchanged (wrap mode).
169
+ */
170
+ export declare function parseMarkdownDoc(fileName: string, raw: string): Record<string, unknown> | null;
171
+ /**
172
+ * Read every markdown/MDX document in a directory (non-recursively — one
173
+ * group per directory) into typed rows. Shared by the scanner (sampling for
174
+ * schema drafts + reference detection) and the rows stager (full dataset).
175
+ */
176
+ export declare function readMarkdownGroupRows(dir: string): Array<Record<string, unknown>>;
177
+ export declare function scanDataJson(relPath: string, raw: string): DataCopyFile | null;
178
+ /**
179
+ * The label keys a `decision: "labels"` data file is expected to seed —
180
+ * one per prose string, keyed by JSON path under the recorded prefix. The
181
+ * seeding gate verifies each exists in the database.
182
+ */
183
+ export declare function dataFileLabelKeys(raw: string, prefix: string, filePath?: string): string[];
184
+ /** Find render-time fetches of third-party APIs (fetch with an http(s) literal or template prefix). */
185
+ export declare function scanExternalSources(rootDir: string, dirs: string[]): ExternalSource[];
186
+ /**
187
+ * Scan a project's source directories for unwrapped content. `dirs` defaults
188
+ * to the common Next/React layouts that exist under `rootDir`.
189
+ */
190
+ export declare function scanWrappable(rootDir: string, opts?: {
191
+ dirs?: string[];
192
+ excludePrefixes?: string[];
193
+ dataDirs?: string[];
194
+ keepInternal?: boolean;
195
+ /** The app's i18n message loader is wired to Fimo (imports
196
+ * `loadFimoMessages`), so its foreign `t()` calls ARE Fimo-backed and
197
+ * editable — count them as covered instead of excluding them. */
198
+ i18nFimoWired?: boolean;
199
+ }): WrapScanResult;
200
+ export {};