fimo 0.9.0-experimental.6 → 0.9.0-experimental.7
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.
- package/assets/content-templates/types-template.eta +1 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +120 -117
- package/dist/scripts/frameworks/discover.js +17 -0
- package/dist/scripts/scan-wrappable.d.ts +1 -1
- package/dist/scripts/scan-wrappable.js +111 -1
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
|
@@ -66,6 +66,11 @@ export function frameworkJsxWrapperModule(detection) {
|
|
|
66
66
|
return 'fimo/next';
|
|
67
67
|
case 'remix':
|
|
68
68
|
return 'fimo/remix';
|
|
69
|
+
case 'react-router':
|
|
70
|
+
// Library-mode React Router SPAs (react-router dep on Vite) wrap with
|
|
71
|
+
// the client-side fimo/react primitives — same shape as vite+react.
|
|
72
|
+
// Proven on the Fimo marketing site (1211 items auto-wrapped).
|
|
73
|
+
return 'fimo/react';
|
|
69
74
|
case 'vite':
|
|
70
75
|
return detection.react ? 'fimo/react' : null;
|
|
71
76
|
default:
|
|
@@ -101,6 +106,10 @@ export function frameworkRootWiring(detection) {
|
|
|
101
106
|
switch (detection.framework) {
|
|
102
107
|
case 'next':
|
|
103
108
|
return (cwd) => ensureNextLayoutWiring(cwd, detection);
|
|
109
|
+
case 'react-router':
|
|
110
|
+
// Same createRoot(...).render entry shape as vite+react; framework-mode
|
|
111
|
+
// RR apps have no src/main.* and bail with a reason (agent §0).
|
|
112
|
+
return ensureSpaEntryWiring;
|
|
104
113
|
case 'vite':
|
|
105
114
|
return detection.react ? ensureSpaEntryWiring : null;
|
|
106
115
|
default:
|
|
@@ -160,6 +169,14 @@ export function frameworkWiringMarker(detection, cwd) {
|
|
|
160
169
|
marker: /FimoSpaProviders/,
|
|
161
170
|
hint: 'wrap the root render with FimoSpaProviders from fimo/react (agent prompt section 0)',
|
|
162
171
|
};
|
|
172
|
+
case 'react-router':
|
|
173
|
+
// Accept either provider: FimoSpaProviders (the vite+react codemod) or
|
|
174
|
+
// FimoProviders (the fimo/react-router adapter, hand-wired on RR sites).
|
|
175
|
+
return {
|
|
176
|
+
files: ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx', 'app/root.tsx', 'app/root.jsx'],
|
|
177
|
+
marker: /Fimo(Spa)?Providers/,
|
|
178
|
+
hint: 'wrap the root render with FimoSpaProviders (fimo/react) or FimoProviders (fimo/react-router) — agent prompt section 0',
|
|
179
|
+
};
|
|
163
180
|
case 'remix':
|
|
164
181
|
return {
|
|
165
182
|
files: ['app/root.tsx', 'app/root.jsx'],
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* user-visible attribute strings. Drives the wrap migration's coverage gate:
|
|
5
5
|
* every item must end wrapped, ignored-with-a-reason, or unresolved.
|
|
6
6
|
*/
|
|
7
|
-
export type WrappableKind = 'text' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
7
|
+
export type WrappableKind = 'text' | 'formatted-text' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
8
8
|
export interface WrappableItem {
|
|
9
9
|
kind: WrappableKind;
|
|
10
10
|
file: string;
|
|
@@ -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();
|
|
@@ -465,6 +495,50 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
465
495
|
function detectCollection(declarator) {
|
|
466
496
|
detectCollectionFromDeclarator(declarator, code, 0, relPath, collections);
|
|
467
497
|
}
|
|
498
|
+
// Text nodes absorbed into a merged formatted-text item (keyed by offset) —
|
|
499
|
+
// the JSXText visitor skips them so fragments never become labels.
|
|
500
|
+
const mergedText = new Set();
|
|
501
|
+
function containsWrappedContent(node) {
|
|
502
|
+
if (!node || typeof node !== 'object') {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 't') {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
if (node.type === 'TaggedTemplateExpression' && node.tag?.type === 'Identifier' && node.tag.name === 'fimo') {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (node.type === 'JSXElement') {
|
|
512
|
+
const name = jsxElementName(node);
|
|
513
|
+
if (name && WRAPPING_COMPONENTS.has(name)) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const key of Object.keys(node)) {
|
|
518
|
+
const child = node[key];
|
|
519
|
+
if (Array.isArray(child)) {
|
|
520
|
+
if (child.some((item) => containsWrappedContent(item))) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (typeof child === 'object' && child !== null && containsWrappedContent(child)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
/** Every JSXText inside a prose element, recursing through inline children
|
|
531
|
+
* (inside a `<p>`, nested elements are inline by HTML rules). */
|
|
532
|
+
function collectProseTextNodes(node, out) {
|
|
533
|
+
for (const child of node.children ?? []) {
|
|
534
|
+
if (child.type === 'JSXText') {
|
|
535
|
+
out.push(child);
|
|
536
|
+
}
|
|
537
|
+
else if (child.type === 'JSXElement') {
|
|
538
|
+
collectProseTextNodes(child, out);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
468
542
|
function visit(node, insideWrapped, parentTag, scopes) {
|
|
469
543
|
if (!node || typeof node !== 'object') {
|
|
470
544
|
return;
|
|
@@ -548,7 +622,43 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
548
622
|
}
|
|
549
623
|
}
|
|
550
624
|
}
|
|
551
|
-
|
|
625
|
+
// Mixed prose detection runs on the ELEMENT (before its children are
|
|
626
|
+
// visited): a prose tag whose direct children interleave raw text with
|
|
627
|
+
// inline elements/expressions becomes ONE formatted-text item, and its
|
|
628
|
+
// text nodes are recorded so the JSXText visitor below skips them.
|
|
629
|
+
if (node.type === 'JSXElement' && !insideWrapped) {
|
|
630
|
+
const proseName = jsxElementName(node);
|
|
631
|
+
if (proseName && PROSE_TAGS.has(proseName)) {
|
|
632
|
+
const children = node.children ?? [];
|
|
633
|
+
const directText = children.filter((child) => child.type === 'JSXText' && meaningfulLength(String(child.value ?? '')) > 0);
|
|
634
|
+
const inline = children.filter((child) => child.type === 'JSXElement' ||
|
|
635
|
+
(child.type === 'JSXExpressionContainer' && child.expression?.type !== 'JSXEmptyExpression'));
|
|
636
|
+
if (directText.length > 0 &&
|
|
637
|
+
inline.length > 0 &&
|
|
638
|
+
!directText.some((child) => mergedText.has(child.start)) &&
|
|
639
|
+
!children.some((child) => containsWrappedContent(child))) {
|
|
640
|
+
const textNodes = [];
|
|
641
|
+
collectProseTextNodes(node, textNodes);
|
|
642
|
+
const combined = textNodes.map((textNode) => String(textNode.value ?? '')).join(' ');
|
|
643
|
+
if (meaningfulLength(combined) >= MIN_TEXT_LENGTH) {
|
|
644
|
+
for (const textNode of textNodes) {
|
|
645
|
+
if (typeof textNode.start === 'number') {
|
|
646
|
+
mergedText.add(textNode.start);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const inner = code.slice(node.openingElement?.end ?? node.start ?? 0, node.closingElement?.start ?? node.end ?? 0);
|
|
650
|
+
items.push({
|
|
651
|
+
kind: 'formatted-text',
|
|
652
|
+
file: relPath,
|
|
653
|
+
line: lineOf(code, node.start ?? 0),
|
|
654
|
+
preview: toPreview(inner),
|
|
655
|
+
span: { start: node.start, end: node.end, parentTag: proseName },
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (node.type === 'JSXText' && !insideWrapped && !mergedText.has(node.start)) {
|
|
552
662
|
const text = String(node.value ?? '');
|
|
553
663
|
const meaningful = text.replace(/[^\p{L}\p{N}]/gu, '');
|
|
554
664
|
if (meaningful.length >= MIN_TEXT_LENGTH) {
|
package/package.json
CHANGED