fimo 0.9.0-experimental.1 → 0.9.0-experimental.11

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 (80) 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/types-template.eta +1 -0
  4. package/assets/skills/fimo/references/translations.md +75 -0
  5. package/assets/skills/fimo-migration/SKILL.md +2 -0
  6. package/assets/skills/fimo-migration/references/manifest.md +11 -0
  7. package/assets/skills/fimo-migration/references/pages.md +298 -0
  8. package/assets/skills/fimo-migration/references/wrap.md +45 -0
  9. package/dist/build/vite/plugins/translations.js +28 -10
  10. package/dist/cli/bundle.json +1 -1
  11. package/dist/cli/index.js +577 -269
  12. package/dist/runtime/app/FimoProviders.js +29 -7
  13. package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
  14. package/dist/runtime/core/BuiltWithBadge.js +68 -0
  15. package/dist/runtime/primitives/components/RichText.d.ts +5 -1
  16. package/dist/runtime/primitives/components/RichText.js +2 -2
  17. package/dist/runtime/react/FimoSpaProviders.js +83 -24
  18. package/dist/runtime/react/index.d.ts +2 -0
  19. package/dist/runtime/react/index.js +1 -0
  20. package/dist/runtime/react/spa-content-cache.d.ts +38 -0
  21. package/dist/runtime/react/spa-content-cache.js +64 -0
  22. package/dist/runtime/seo/neutral.d.ts +22 -0
  23. package/dist/runtime/seo/neutral.js +17 -0
  24. package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
  25. package/dist/runtime/shared/fimo-config.server.js +0 -13
  26. package/dist/runtime/strapi/client.js +45 -2
  27. package/dist/runtime/strapi/reshape.d.ts +10 -6
  28. package/dist/runtime/strapi/reshape.js +7 -0
  29. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +4 -1
  30. package/dist/runtime/wrapped/FimoRichTextBlock.js +2 -2
  31. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  32. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  33. package/dist/runtime/wrapped/server.d.ts +5 -2
  34. package/dist/runtime/wrapped/server.js +15 -45
  35. package/dist/scripts/apply-wrap-templates.js +10 -5
  36. package/dist/scripts/apply-wrap.d.ts +29 -2
  37. package/dist/scripts/apply-wrap.js +69 -7
  38. package/dist/scripts/cms/example-content.js +16 -13
  39. package/dist/scripts/cms/strapi/schema-map.js +16 -4
  40. package/dist/scripts/cms/strapi/shapes.d.ts +7 -5
  41. package/dist/scripts/cms/strapi/shapes.js +10 -0
  42. package/dist/scripts/cms/strapi/wiring.js +1 -1
  43. package/dist/scripts/detect-framework.js +6 -0
  44. package/dist/scripts/draft-rows.js +44 -1
  45. package/dist/scripts/draft-schemas.js +64 -5
  46. package/dist/scripts/frameworks/astro/routes.js +8 -19
  47. package/dist/scripts/frameworks/discover.d.ts +19 -6
  48. package/dist/scripts/frameworks/discover.js +113 -3
  49. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  50. package/dist/scripts/frameworks/discovered-route.js +17 -0
  51. package/dist/scripts/frameworks/next/rewire.d.ts +13 -0
  52. package/dist/scripts/frameworks/next/rewire.js +26 -2
  53. package/dist/scripts/frameworks/next/routes.d.ts +13 -0
  54. package/dist/scripts/frameworks/next/routes.js +55 -13
  55. package/dist/scripts/frameworks/nuxt/routes.js +2 -18
  56. package/dist/scripts/frameworks/remix/routes.js +4 -18
  57. package/dist/scripts/frameworks/sveltekit/routes.js +4 -19
  58. package/dist/scripts/frameworks/vite/routes.d.ts +5 -0
  59. package/dist/scripts/frameworks/vite/routes.js +59 -9
  60. package/dist/scripts/frameworks/vite/wiring.d.ts +12 -0
  61. package/dist/scripts/frameworks/vite/wiring.js +68 -23
  62. package/dist/scripts/lib/humanize.d.ts +14 -0
  63. package/dist/scripts/lib/humanize.js +54 -0
  64. package/dist/scripts/lib/validation-report.d.ts +0 -3
  65. package/dist/scripts/lib/validation-report.js +0 -9
  66. package/dist/scripts/render-parity/capture.d.ts +12 -4
  67. package/dist/scripts/render-parity/capture.js +94 -24
  68. package/dist/scripts/routes/bindings.d.ts +93 -0
  69. package/dist/scripts/routes/bindings.js +117 -0
  70. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  71. package/dist/scripts/routes/label-prefixes.js +136 -0
  72. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  73. package/dist/scripts/routes/module-resolve.js +78 -0
  74. package/dist/scripts/routes/page-structure.d.ts +136 -0
  75. package/dist/scripts/routes/page-structure.js +393 -0
  76. package/dist/scripts/scan-wrappable.d.ts +5 -1
  77. package/dist/scripts/scan-wrappable.js +126 -3
  78. package/package.json +5 -1
  79. package/templates/react-router/package.json +1 -1
  80. package/templates/react-router/translations/en.json +1 -0
@@ -118,6 +118,36 @@ const WRAPPABLE_ATTRIBUTES = new Set(['placeholder', 'title', 'aria-label']);
118
118
  const FILE_LINK_PATTERN = /\.(pdf|zip|docx?|xlsx?|pptx?|csv|mp4|mov|webm)$/i;
119
119
  const SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '.fimo']);
120
120
  const MIN_TEXT_LENGTH = 3;
121
+ /**
122
+ * Prose containers whose children mix raw text with inline elements or
123
+ * expressions. Wrapping such text node-by-node produces FRAGMENT labels
124
+ * (", and agencies building…") that nobody can find or edit as a sentence —
125
+ * the whole block merges into ONE `formatted-text` item instead (agent work:
126
+ * the fimo`` tagged template or RichText, never the auto-wrap codemod).
127
+ */
128
+ const PROSE_TAGS = new Set([
129
+ 'p',
130
+ 'h1',
131
+ 'h2',
132
+ 'h3',
133
+ 'h4',
134
+ 'h5',
135
+ 'h6',
136
+ 'li',
137
+ 'blockquote',
138
+ 'figcaption',
139
+ 'dt',
140
+ 'dd',
141
+ 'label',
142
+ 'legend',
143
+ 'td',
144
+ 'th',
145
+ 'caption',
146
+ 'summary',
147
+ ]);
148
+ function meaningfulLength(text) {
149
+ return text.replace(/[^\p{L}\p{N}]/gu, '').length;
150
+ }
121
151
  const PREVIEW_LENGTH = 60;
122
152
  function toPreview(value) {
123
153
  const collapsed = value.replace(/\s+/g, ' ').trim();
@@ -212,7 +242,7 @@ function extractTemplate(ext, code) {
212
242
  }
213
243
  function scanTemplateFile(ext, code, relPath) {
214
244
  const items = [];
215
- const wrappedIds = { labels: [], media: [], richText: [], files: [] };
245
+ const wrappedIds = { labels: [], media: [], richText: [], files: [], ordered: [] };
216
246
  let wrapped = 0;
217
247
  const { template, offset, frontmatter } = extractTemplate(ext, code);
218
248
  // t('key', ...) in frontmatter/script and in template expressions = wrapped.
@@ -220,6 +250,7 @@ function scanTemplateFile(ext, code, relPath) {
220
250
  for (const match of source.matchAll(/\bt\(\s*['"]([^'"]+)['"]/g)) {
221
251
  wrapped += 1;
222
252
  wrappedIds.labels.push(match[1]);
253
+ wrappedIds.ordered.push(match[1]);
223
254
  }
224
255
  }
225
256
  // data-fimo-source attributes = wrapped media/labels rendered via attrs.
@@ -444,7 +475,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
444
475
  const items = [];
445
476
  let wrapped = 0;
446
477
  let foreignI18n = 0;
447
- const wrappedIds = { labels: [], media: [], richText: [], files: [] };
478
+ const wrappedIds = { labels: [], media: [], richText: [], files: [], ordered: [] };
448
479
  const collections = [];
449
480
  const templateExt = filePath.match(/\.(astro|vue|svelte)$/)?.[1];
450
481
  if (templateExt) {
@@ -465,6 +496,50 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
465
496
  function detectCollection(declarator) {
466
497
  detectCollectionFromDeclarator(declarator, code, 0, relPath, collections);
467
498
  }
499
+ // Text nodes absorbed into a merged formatted-text item (keyed by offset) —
500
+ // the JSXText visitor skips them so fragments never become labels.
501
+ const mergedText = new Set();
502
+ function containsWrappedContent(node) {
503
+ if (!node || typeof node !== 'object') {
504
+ return false;
505
+ }
506
+ if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 't') {
507
+ return true;
508
+ }
509
+ if (node.type === 'TaggedTemplateExpression' && node.tag?.type === 'Identifier' && node.tag.name === 'fimo') {
510
+ return true;
511
+ }
512
+ if (node.type === 'JSXElement') {
513
+ const name = jsxElementName(node);
514
+ if (name && WRAPPING_COMPONENTS.has(name)) {
515
+ return true;
516
+ }
517
+ }
518
+ for (const key of Object.keys(node)) {
519
+ const child = node[key];
520
+ if (Array.isArray(child)) {
521
+ if (child.some((item) => containsWrappedContent(item))) {
522
+ return true;
523
+ }
524
+ }
525
+ else if (typeof child === 'object' && child !== null && containsWrappedContent(child)) {
526
+ return true;
527
+ }
528
+ }
529
+ return false;
530
+ }
531
+ /** Every JSXText inside a prose element, recursing through inline children
532
+ * (inside a `<p>`, nested elements are inline by HTML rules). */
533
+ function collectProseTextNodes(node, out) {
534
+ for (const child of node.children ?? []) {
535
+ if (child.type === 'JSXText') {
536
+ out.push(child);
537
+ }
538
+ else if (child.type === 'JSXElement') {
539
+ collectProseTextNodes(child, out);
540
+ }
541
+ }
542
+ }
468
543
  function visit(node, insideWrapped, parentTag, scopes) {
469
544
  if (!node || typeof node !== 'object') {
470
545
  return;
@@ -486,6 +561,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
486
561
  const id = attributeValue(node, 'id');
487
562
  if (kind && id) {
488
563
  wrappedIds[kind].push(id.value);
564
+ wrappedIds.ordered.push(id.value);
489
565
  }
490
566
  }
491
567
  if (!insideWrapped && name) {
@@ -548,7 +624,43 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
548
624
  }
549
625
  }
550
626
  }
551
- if (node.type === 'JSXText' && !insideWrapped) {
627
+ // Mixed prose detection runs on the ELEMENT (before its children are
628
+ // visited): a prose tag whose direct children interleave raw text with
629
+ // inline elements/expressions becomes ONE formatted-text item, and its
630
+ // text nodes are recorded so the JSXText visitor below skips them.
631
+ if (node.type === 'JSXElement' && !insideWrapped) {
632
+ const proseName = jsxElementName(node);
633
+ if (proseName && PROSE_TAGS.has(proseName)) {
634
+ const children = node.children ?? [];
635
+ const directText = children.filter((child) => child.type === 'JSXText' && meaningfulLength(String(child.value ?? '')) > 0);
636
+ const inline = children.filter((child) => child.type === 'JSXElement' ||
637
+ (child.type === 'JSXExpressionContainer' && child.expression?.type !== 'JSXEmptyExpression'));
638
+ if (directText.length > 0 &&
639
+ inline.length > 0 &&
640
+ !directText.some((child) => mergedText.has(child.start)) &&
641
+ !children.some((child) => containsWrappedContent(child))) {
642
+ const textNodes = [];
643
+ collectProseTextNodes(node, textNodes);
644
+ const combined = textNodes.map((textNode) => String(textNode.value ?? '')).join(' ');
645
+ if (meaningfulLength(combined) >= MIN_TEXT_LENGTH) {
646
+ for (const textNode of textNodes) {
647
+ if (typeof textNode.start === 'number') {
648
+ mergedText.add(textNode.start);
649
+ }
650
+ }
651
+ const inner = code.slice(node.openingElement?.end ?? node.start ?? 0, node.closingElement?.start ?? node.end ?? 0);
652
+ items.push({
653
+ kind: 'formatted-text',
654
+ file: relPath,
655
+ line: lineOf(code, node.start ?? 0),
656
+ preview: toPreview(inner),
657
+ span: { start: node.start, end: node.end, parentTag: proseName },
658
+ });
659
+ }
660
+ }
661
+ }
662
+ }
663
+ if (node.type === 'JSXText' && !insideWrapped && !mergedText.has(node.start)) {
552
664
  const text = String(node.value ?? '');
553
665
  const meaningful = text.replace(/[^\p{L}\p{N}]/gu, '');
554
666
  if (meaningful.length >= MIN_TEXT_LENGTH) {
@@ -584,6 +696,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
584
696
  const resolved = resolveScopedKey(node.callee.name, firstArg.value, childScopes);
585
697
  if (!resolved.unresolvable) {
586
698
  wrappedIds.labels.push(resolved.key);
699
+ wrappedIds.ordered.push(resolved.key);
587
700
  }
588
701
  }
589
702
  }
@@ -1066,6 +1179,8 @@ export function scanWrappable(rootDir, opts = {}) {
1066
1179
  richText: new Set(),
1067
1180
  files: new Set(),
1068
1181
  };
1182
+ const orderedIds = [];
1183
+ const orderedSeen = new Set();
1069
1184
  for (const file of files) {
1070
1185
  let code;
1071
1186
  try {
@@ -1086,6 +1201,12 @@ export function scanWrappable(rootDir, opts = {}) {
1086
1201
  idSets[kind].add(id);
1087
1202
  }
1088
1203
  }
1204
+ for (const id of result.wrappedIds.ordered) {
1205
+ if (!orderedSeen.has(id)) {
1206
+ orderedSeen.add(id);
1207
+ orderedIds.push(id);
1208
+ }
1209
+ }
1089
1210
  }
1090
1211
  const dataFiles = [];
1091
1212
  // Markdown/MDX documents: group per directory so a 40-post blog is ONE
@@ -1227,6 +1348,8 @@ export function scanWrappable(rootDir, opts = {}) {
1227
1348
  media: [...idSets.media].sort(),
1228
1349
  richText: [...idSets.richText].sort(),
1229
1350
  files: [...idSets.files].sort(),
1351
+ // Encounter order, never sorted — the on-page reading order.
1352
+ ordered: orderedIds,
1230
1353
  },
1231
1354
  };
1232
1355
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.9.0-experimental.1",
3
+ "version": "0.9.0-experimental.11",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/runtime/strapi/index.d.ts",
59
59
  "import": "./dist/runtime/strapi/index.js"
60
60
  },
61
+ "./seo": {
62
+ "types": "./dist/runtime/seo/neutral.d.ts",
63
+ "import": "./dist/runtime/seo/neutral.js"
64
+ },
61
65
  "./remix": {
62
66
  "types": "./dist/runtime/remix/index.d.ts",
63
67
  "import": "./dist/runtime/remix/index.js"
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.9.0-experimental.1",
24
+ "fimo": "0.9.0-experimental.11",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",