fimo 0.9.0-experimental.7 → 0.9.0-experimental.8

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 (44) hide show
  1. package/assets/skills/fimo-migration/references/pages.md +140 -1
  2. package/dist/cli/bundle.json +1 -1
  3. package/dist/cli/index.js +335 -173
  4. package/dist/runtime/app/FimoProviders.js +29 -7
  5. package/dist/runtime/react/FimoSpaProviders.js +18 -1
  6. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  7. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  8. package/dist/runtime/wrapped/server.d.ts +5 -2
  9. package/dist/runtime/wrapped/server.js +15 -45
  10. package/dist/scripts/apply-wrap-templates.js +1 -1
  11. package/dist/scripts/apply-wrap.js +26 -2
  12. package/dist/scripts/cms/strapi/wiring.js +1 -1
  13. package/dist/scripts/detect-framework.js +6 -0
  14. package/dist/scripts/draft-rows.js +36 -0
  15. package/dist/scripts/frameworks/astro/routes.js +2 -18
  16. package/dist/scripts/frameworks/discover.d.ts +19 -6
  17. package/dist/scripts/frameworks/discover.js +96 -3
  18. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  19. package/dist/scripts/frameworks/discovered-route.js +17 -0
  20. package/dist/scripts/frameworks/next/routes.d.ts +7 -0
  21. package/dist/scripts/frameworks/next/routes.js +31 -12
  22. package/dist/scripts/frameworks/nuxt/routes.js +2 -18
  23. package/dist/scripts/frameworks/remix/routes.js +4 -18
  24. package/dist/scripts/frameworks/sveltekit/routes.js +4 -19
  25. package/dist/scripts/frameworks/vite/routes.d.ts +5 -0
  26. package/dist/scripts/frameworks/vite/routes.js +59 -9
  27. package/dist/scripts/frameworks/vite/wiring.d.ts +12 -0
  28. package/dist/scripts/frameworks/vite/wiring.js +68 -23
  29. package/dist/scripts/lib/humanize.d.ts +14 -0
  30. package/dist/scripts/lib/humanize.js +54 -0
  31. package/dist/scripts/render-parity/capture.d.ts +7 -0
  32. package/dist/scripts/render-parity/capture.js +32 -4
  33. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  34. package/dist/scripts/routes/label-prefixes.js +136 -0
  35. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  36. package/dist/scripts/routes/module-resolve.js +78 -0
  37. package/dist/scripts/routes/page-structure.d.ts +72 -0
  38. package/dist/scripts/routes/page-structure.js +234 -0
  39. package/dist/scripts/scan-wrappable.d.ts +4 -0
  40. package/dist/scripts/scan-wrappable.js +15 -2
  41. package/package.json +1 -1
  42. package/templates/react-router/package.json +1 -1
  43. package/dist/scripts/lib/discover-next-routes.d.ts +0 -17
  44. package/dist/scripts/lib/discover-next-routes.js +0 -134
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, useCallback, useEffect, useMemo } from 'react';
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
3
+ import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
3
4
  import { ErrorBoundary } from 'react-error-boundary';
4
5
  import { useLocation, useNavigate } from 'react-router';
5
6
  // @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
@@ -59,6 +60,27 @@ function PreviewBridge() {
59
60
  }, [navigate, sendRoutes, sendLocationChange]);
60
61
  return null;
61
62
  }
63
+ /**
64
+ * Template projects mount their own QueryClientProvider in root.tsx, but an
65
+ * ADOPTED app wired straight onto FimoProviders (react-router library SPAs)
66
+ * has none — and the inner runtime (RefreshContentProvider, content clients)
67
+ * calls useQueryClient(). Mount a fallback client only when no provider
68
+ * exists above, so both worlds work and template caches stay untouched.
69
+ */
70
+ function EnsureQueryClient({ children }) {
71
+ let existing = null;
72
+ try {
73
+ existing = useQueryClient();
74
+ }
75
+ catch {
76
+ existing = null;
77
+ }
78
+ const [fallbackClient] = useState(() => new QueryClient());
79
+ if (existing) {
80
+ return _jsx(_Fragment, { children: children });
81
+ }
82
+ return _jsx(QueryClientProvider, { client: fallbackClient, children: children });
83
+ }
62
84
  export function FimoProviders({ children, locale }) {
63
85
  const location = useLocation();
64
86
  const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
@@ -71,9 +93,9 @@ export function FimoProviders({ children, locale }) {
71
93
  document.documentElement.lang = toHtmlLang(activeLocale);
72
94
  document.documentElement.dir = activeDir;
73
95
  }, [activeDir, activeLocale]);
74
- return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(FimoLocaleProvider, { value: {
75
- ...localeResolution,
76
- locale: activeLocale,
77
- dir: activeDir,
78
- }, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(PreviewBridge, {}), _jsx(OptionalTracking, { routes: getDisplayRoutes() }), _jsx(Suspense, { children: children })] }) }) }) }) }) }));
96
+ return (_jsx(ErrorBoundary, { FallbackComponent: AppErrorBoundary, onError: onError, children: _jsx(EnsureQueryClient, { children: _jsx(FimoLocaleProvider, { value: {
97
+ ...localeResolution,
98
+ locale: activeLocale,
99
+ dir: activeDir,
100
+ }, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [_jsx(PreviewBridge, {}), _jsx(OptionalTracking, { routes: getDisplayRoutes() }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
79
101
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from 'react';
4
4
  import { getFimoDefaultLocale, getFimoLocales } from '../i18n/locale.js';
5
5
  import { fetchLabelBundle, serializeBundle } from '../shared/labels-fetch.pure.js';
6
+ import { fetchWrappedContent } from '../wrapped/content-fetch.pure.js';
6
7
  import { WrappedProviders } from '../wrapped/WrappedProviders.js';
7
8
  /** Browser-safe stable signature (labelSignature uses Buffer, server-only). */
8
9
  function bundleSignature(bundle) {
@@ -34,6 +35,7 @@ function bundleSignature(bundle) {
34
35
  */
35
36
  export function FimoSpaProviders({ config, apiUrl, previewScriptUrl, media, richText, locale, children, }) {
36
37
  const [labels, setLabels] = useState(undefined);
38
+ const [content, setContent] = useState(undefined);
37
39
  useEffect(() => {
38
40
  if (!apiUrl) {
39
41
  return;
@@ -49,15 +51,30 @@ export function FimoSpaProviders({ config, apiUrl, previewScriptUrl, media, rich
49
51
  .catch(() => {
50
52
  // Unreachable API: keep rendering the inline fallbacks.
51
53
  });
54
+ // Wrapped rich text and media live in the plumbing types — without this
55
+ // fetch a client-rendered app only ever shows their inline fallbacks
56
+ // and Studio edits to them render nowhere.
57
+ fetchWrappedContent(apiUrl)
58
+ .then((fetched) => {
59
+ if (!cancelled) {
60
+ setContent(fetched);
61
+ }
62
+ })
63
+ .catch(() => {
64
+ // Unreachable API: keep rendering the inline fallbacks.
65
+ });
52
66
  return () => {
53
67
  cancelled = true;
54
68
  };
55
69
  }, [apiUrl, config]);
70
+ // Explicitly-passed maps win over the fetched ones (caller override).
71
+ const mergedMedia = useMemo(() => (content?.media || media ? { ...(content?.media ?? {}), ...(media ?? {}) } : undefined), [content?.media, media]);
72
+ const mergedRichText = useMemo(() => (content?.richText || richText ? { ...(content?.richText ?? {}), ...(richText ?? {}) } : undefined), [content?.richText, richText]);
56
73
  const navigation = useMemo(() => ({
57
74
  pathname: typeof window !== 'undefined' ? window.location.pathname : null,
58
75
  navigate: (to) => {
59
76
  window.location.assign(to);
60
77
  },
61
78
  }), []);
62
- return (_jsxs(WrappedProviders, { config: config, labels: labels, media: media, richText: richText, locale: locale, navigation: navigation, children: [previewScriptUrl ? _jsx("script", { type: "module", src: previewScriptUrl }) : null, children] }));
79
+ return (_jsxs(WrappedProviders, { config: config, labels: labels, media: mergedMedia, richText: mergedRichText, locale: locale, navigation: navigation, children: [previewScriptUrl ? _jsx("script", { type: "module", src: previewScriptUrl }) : null, children] }));
63
80
  }
@@ -0,0 +1,20 @@
1
+ import type { WrappedMediaEntry, WrappedRichTextEntry } from './WrappedContentContext.js';
2
+ export interface WrappedEntryRow {
3
+ id: string;
4
+ data: Record<string, unknown>;
5
+ }
6
+ /** Every published row of a content type, paginated to the end. Returns []
7
+ * when the API is unreachable so wrapped sites keep their fallbacks. */
8
+ export declare function fetchAllWrappedRows(apiUrl: string, typeName: string): Promise<WrappedEntryRow[]>;
9
+ /** WrappedMedia rows → key → media entry (the FimoMediaImage/Video id map). */
10
+ export declare function mapWrappedMedia(rows: WrappedEntryRow[]): Record<string, WrappedMediaEntry>;
11
+ /** WrappedRichText rows → key → TipTap content (the FimoRichTextBlock map). */
12
+ export declare function mapWrappedRichText(rows: WrappedEntryRow[]): Record<string, WrappedRichTextEntry>;
13
+ export interface WrappedContent {
14
+ media: Record<string, WrappedMediaEntry>;
15
+ richText: Record<string, WrappedRichTextEntry>;
16
+ }
17
+ /** Both plumbing maps in one round: what a client-rendered wrapped app needs
18
+ * so Studio edits to rich text and media are LIVE, not fallback-only. */
19
+ export declare function fetchWrappedContent(apiUrl: string): Promise<WrappedContent>;
20
+ //# sourceMappingURL=content-fetch.pure.d.ts.map
@@ -0,0 +1,106 @@
1
+ // Wrapped-content fetching, PURE core: fetch/URL/AbortController only — no
2
+ // node builtins, importable from client-bundled code (the SPA provider) and
3
+ // from the server loaders alike. The plumbing types (WrappedRichText,
4
+ // WrappedMedia) hold ALL of a site's wrapped rich text and media, so the
5
+ // fetch PAGINATES to the end — the tenant API caps limit at 100 and a real
6
+ // site exceeds it (the Fimo site carries 147 rich-text entries).
7
+ import { fetchWithTimeout } from '../shared/labels-fetch.pure.js';
8
+ const PAGE_LIMIT = 100;
9
+ const MAX_ROWS = 5000;
10
+ function readString(value) {
11
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
12
+ }
13
+ function readNumber(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
15
+ }
16
+ /** Every published row of a content type, paginated to the end. Returns []
17
+ * when the API is unreachable so wrapped sites keep their fallbacks. */
18
+ export async function fetchAllWrappedRows(apiUrl, typeName) {
19
+ const rows = [];
20
+ try {
21
+ for (let offset = 0; offset < MAX_ROWS; offset += PAGE_LIMIT) {
22
+ const url = new URL(`/entries/${typeName}`, apiUrl);
23
+ url.searchParams.set('limit', String(PAGE_LIMIT));
24
+ url.searchParams.set('offset', String(offset));
25
+ const response = await fetchWithTimeout(url, 4000);
26
+ if (!response.ok) {
27
+ break;
28
+ }
29
+ const body = (await response.json());
30
+ const page = Array.isArray(body.data) ? body.data : [];
31
+ for (const row of page) {
32
+ if (!row || typeof row !== 'object') {
33
+ continue;
34
+ }
35
+ const record = row;
36
+ const id = String(record.id ?? record.documentId ?? '');
37
+ if (id.length === 0) {
38
+ continue;
39
+ }
40
+ rows.push({
41
+ id,
42
+ data: (record.data && typeof record.data === 'object' ? record.data : record),
43
+ });
44
+ }
45
+ const hasMore = body.meta?.hasMore === true || page.length === PAGE_LIMIT;
46
+ if (!hasMore) {
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // Unreachable API mid-pagination: return what we have (possibly []).
53
+ }
54
+ return rows;
55
+ }
56
+ /** WrappedMedia rows → key → media entry (the FimoMediaImage/Video id map). */
57
+ export function mapWrappedMedia(rows) {
58
+ const map = {};
59
+ for (const entry of rows) {
60
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
61
+ const asset = entry.data.image;
62
+ if (!key || !asset || typeof asset !== 'object') {
63
+ continue;
64
+ }
65
+ const assetRecord = asset;
66
+ const url = readString(assetRecord.url);
67
+ if (!url) {
68
+ continue;
69
+ }
70
+ map[key] = {
71
+ url,
72
+ alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
73
+ name: readString(assetRecord.name),
74
+ mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
75
+ width: readNumber(assetRecord.width),
76
+ height: readNumber(assetRecord.height),
77
+ source: `WrappedMedia.${entry.id}.image`,
78
+ };
79
+ }
80
+ return map;
81
+ }
82
+ /** WrappedRichText rows → key → TipTap content (the FimoRichTextBlock map). */
83
+ export function mapWrappedRichText(rows) {
84
+ const map = {};
85
+ for (const entry of rows) {
86
+ const key = readString(entry.data.key) ?? readString(entry.data.slug);
87
+ const body = entry.data.body;
88
+ if (!key || !body || typeof body !== 'object') {
89
+ continue;
90
+ }
91
+ map[key] = {
92
+ content: body,
93
+ source: `WrappedRichText.${entry.id}.body`,
94
+ };
95
+ }
96
+ return map;
97
+ }
98
+ /** Both plumbing maps in one round: what a client-rendered wrapped app needs
99
+ * so Studio edits to rich text and media are LIVE, not fallback-only. */
100
+ export async function fetchWrappedContent(apiUrl) {
101
+ const [mediaRows, richTextRows] = await Promise.all([
102
+ fetchAllWrappedRows(apiUrl, 'WrappedMedia'),
103
+ fetchAllWrappedRows(apiUrl, 'WrappedRichText'),
104
+ ]);
105
+ return { media: mapWrappedMedia(mediaRows), richText: mapWrappedRichText(richTextRows) };
106
+ }
@@ -39,12 +39,15 @@ export declare function fetchEntries(typeName: string, options?: FimoServerOptio
39
39
  /**
40
40
  * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
41
41
  * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
42
- * asset-typed `image` field.
42
+ * asset-typed `image` field. Paginates to the end — a site's wrapped media
43
+ * routinely exceeds one API page.
43
44
  */
44
45
  export declare function loadWrappedMedia(options?: FimoServerOptions): Promise<Record<string, WrappedMediaEntry>>;
45
46
  /**
46
47
  * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
47
- * migration for formatted text (bold/italic/underline/links).
48
+ * migration for formatted text (bold/italic/underline/links). Paginates to
49
+ * the end — the Fimo site alone carries 147 entries against a 20-row API
50
+ * default page.
48
51
  */
49
52
  export declare function loadWrappedRichText(options?: FimoServerOptions): Promise<Record<string, WrappedRichTextEntry>>;
50
53
  /**
@@ -5,6 +5,7 @@
5
5
  import { getConfigServer } from '../shared/fimo-config.server.js';
6
6
  import { deepMergeMessages, unflattenMessages } from '../shared/i18n-messages.js';
7
7
  import { fetchLabelDictionary, fetchWithTimeout, labelSignature, loadLabelBundle, readProjectEnv, resolveTenantApiUrl, } from '../shared/labels-fetch.server.js';
8
+ import { fetchAllWrappedRows, mapWrappedMedia, mapWrappedRichText } from './content-fetch.pure.js';
8
9
  export function loadFimoConfig(rootDir = process.cwd()) {
9
10
  return getConfigServer(rootDir);
10
11
  }
@@ -72,62 +73,31 @@ export async function fetchEntries(typeName, options = {}) {
72
73
  return [];
73
74
  }
74
75
  }
75
- function readString(value) {
76
- return typeof value === 'string' && value.length > 0 ? value : undefined;
77
- }
78
- function readNumber(value) {
79
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
80
- }
81
76
  /**
82
77
  * Load the WrappedMedia entries (key → media) seeded by the wrap migration.
83
78
  * Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
84
- * asset-typed `image` field.
79
+ * asset-typed `image` field. Paginates to the end — a site's wrapped media
80
+ * routinely exceeds one API page.
85
81
  */
86
82
  export async function loadWrappedMedia(options = {}) {
87
- const entries = await fetchEntries('WrappedMedia', options);
88
- const map = {};
89
- for (const entry of entries) {
90
- const key = readString(entry.data.key) ?? readString(entry.data.slug);
91
- const asset = entry.data.image;
92
- if (!key || !asset || typeof asset !== 'object') {
93
- continue;
94
- }
95
- const assetRecord = asset;
96
- const url = readString(assetRecord.url);
97
- if (!url) {
98
- continue;
99
- }
100
- map[key] = {
101
- url,
102
- alt: readString(assetRecord.alt) ?? readString(entry.data.alt),
103
- name: readString(assetRecord.name),
104
- mime: readString(assetRecord.mime) ?? readString(assetRecord.mimeType),
105
- width: readNumber(assetRecord.width),
106
- height: readNumber(assetRecord.height),
107
- source: `WrappedMedia.${entry.id}.image`,
108
- };
109
- }
110
- return map;
83
+ const apiUrl = await resolveApiUrl(options);
84
+ if (!apiUrl) {
85
+ return {};
86
+ }
87
+ return mapWrappedMedia(await fetchAllWrappedRows(apiUrl, 'WrappedMedia'));
111
88
  }
112
89
  /**
113
90
  * Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
114
- * migration for formatted text (bold/italic/underline/links).
91
+ * migration for formatted text (bold/italic/underline/links). Paginates to
92
+ * the end — the Fimo site alone carries 147 entries against a 20-row API
93
+ * default page.
115
94
  */
116
95
  export async function loadWrappedRichText(options = {}) {
117
- const entries = await fetchEntries('WrappedRichText', options);
118
- const map = {};
119
- for (const entry of entries) {
120
- const key = readString(entry.data.key) ?? readString(entry.data.slug);
121
- const body = entry.data.body;
122
- if (!key || !body || typeof body !== 'object') {
123
- continue;
124
- }
125
- map[key] = {
126
- content: body,
127
- source: `WrappedRichText.${entry.id}.body`,
128
- };
96
+ const apiUrl = await resolveApiUrl(options);
97
+ if (!apiUrl) {
98
+ return {};
129
99
  }
130
- return map;
100
+ return mapWrappedRichText(await fetchAllWrappedRows(apiUrl, 'WrappedRichText'));
131
101
  }
132
102
  /** Key fields tried, in order, to align a source row with its Fimo entry. */
133
103
  const COLLECTION_KEY_FIELDS = ['slug', 'id', 'key', 'name', 'title', 'question'];
@@ -66,7 +66,7 @@ export function applyTemplateWrapToFile(code, style, items, allocator) {
66
66
  continue;
67
67
  }
68
68
  const collapsed = match[2].replace(/\s+/g, ' ').trim();
69
- const id = allocator.allocate(scope || 'shared', slugify(collapsed));
69
+ const id = allocator.allocate(scope || 'shared', slugify(decodeJsxEntities(collapsed)));
70
70
  const escaped = collapsed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
71
71
  const replacement = `>${match[1]}${style.open}"${id}", "${escaped}"${style.close}${match[3]}<`;
72
72
  next = next.slice(0, match.index) + replacement + next.slice(match.index + match[0].length);
@@ -33,10 +33,27 @@ const JSX_ENTITIES = {
33
33
  '&apos;': "'",
34
34
  '&nbsp;': ' ',
35
35
  '&copy;': '©',
36
+ '&reg;': '®',
37
+ '&trade;': '™',
38
+ '&deg;': '°',
39
+ '&middot;': '·',
40
+ '&bull;': '•',
41
+ '&hellip;': '…',
42
+ '&mdash;': '—',
43
+ '&ndash;': '–',
44
+ '&lsquo;': '‘',
45
+ '&rsquo;': '’',
46
+ '&ldquo;': '“',
47
+ '&rdquo;': '”',
48
+ '&laquo;': '«',
49
+ '&raquo;': '»',
50
+ '&times;': '×',
51
+ '&rarr;': '→',
52
+ '&larr;': '←',
36
53
  };
37
54
  export function decodeJsxEntities(text) {
38
55
  return text
39
- .replace(/&(amp|lt|gt|quot|apos|nbsp|copy);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
56
+ .replace(/&(amp|lt|gt|quot|apos|nbsp|copy|reg|trade|deg|middot|bull|hellip|mdash|ndash|lsquo|rsquo|ldquo|rdquo|laquo|raquo|times|rarr|larr);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
40
57
  .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
41
58
  .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16)));
42
59
  }
@@ -62,7 +79,12 @@ export function keyScope(file) {
62
79
  return slugify(stem) || 'shared';
63
80
  }
64
81
  export function slugify(text) {
82
+ // camelCase boundaries become segments BEFORE lowercasing, so component
83
+ // file names keep their word structure (AgentStrip.tsx → agent-strip.*)
84
+ // and every humanized display of the key reads like the component name.
65
85
  return collapse(text)
86
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
87
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
66
88
  .toLowerCase()
67
89
  .normalize('NFD')
68
90
  .replace(/[̀-ͯ]/g, '')
@@ -154,7 +176,9 @@ export function applyWrapToFile(code, items, allocator, importModule = 'fimo/nex
154
176
  }
155
177
  const lead = raw.slice(0, raw.indexOf(core));
156
178
  const trail = raw.slice(raw.indexOf(core) + core.length);
157
- const id = allocator.allocate(scope, slugify(core));
179
+ // Slug from the DECODED text — "&apos;" must never leak "apos" into a
180
+ // key ("today-apos-s-total" read "Today Apos S Total" in the Studio).
181
+ const id = allocator.allocate(scope, slugify(decodeJsxEntities(core)));
158
182
  edits.push({
159
183
  start: span.start,
160
184
  end: span.end,
@@ -35,7 +35,7 @@ const SINGLETONS = [${singletons.map((name) => `'${name}'`).join(', ')}];
35
35
 
36
36
  // Structural field shapes (single components read as objects; block items and
37
37
  // dynamic zones reshape into the Strapi shape the components expect).
38
- const FIELD_SHAPES = ${fieldShapes};
38
+ const FIELD_SHAPES = ${fieldShapes} as const;
39
39
 
40
40
  export const fimoStrapiFetch = createFimoStrapiFetch({
41
41
  types: TYPES,
@@ -18,6 +18,12 @@ export function detectFramework(cwd) {
18
18
  return { framework: null };
19
19
  }
20
20
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
21
+ // A CMS backend is never an adoptable frontend — Strapi 5 ships vite+react
22
+ // devDeps for its admin panel, which the bare-vite fallback would match
23
+ // (the sandbox/website monorepo suggested `fimo init apps/strapi`).
24
+ if (deps['@strapi/strapi']) {
25
+ return { framework: null };
26
+ }
21
27
  if (deps.next) {
22
28
  const appDir = ['app', 'src/app'].find((dir) => existsSync(join(cwd, dir)));
23
29
  const pagesDir = ['pages', 'src/pages'].find((dir) => existsSync(join(cwd, dir)));
@@ -1,6 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import YAML from 'yaml';
4
+ import { slugify } from './apply-wrap.js';
5
+ /** Display fields tried, in order, when deriving a human entry slug. */
6
+ const DISPLAY_SLUG_FIELDS = ['title', 'name', 'question', 'q', 'label', 'heading'];
4
7
  /**
5
8
  * Machine-staged entry ROWS for every drafted collection: the deterministic
6
9
  * transformation agents kept re-implementing by hand (media envelopes,
@@ -71,6 +74,20 @@ export function writeRowsStaging(rootDir, scan, drafts) {
71
74
  const bySourceLabel = new Map(drafts.map((draft) => [draft.source, draft]));
72
75
  const provisionalByName = new Map(drafts.map((draft) => [draft.sourceName, draft.uid]));
73
76
  const toProvisional = (name) => provisionalByName.get(name) ?? name;
77
+ // Types other rows REFERENCE resolve by slug server-side — their slugs must
78
+ // stay whatever identity the source carries. Every other type gets a HUMAN
79
+ // slug derived from its display field (never `faq-item-<hash>`).
80
+ const referencedUids = new Set();
81
+ for (const draft of drafts) {
82
+ for (const def of Object.values(draft.fields)) {
83
+ const field = def;
84
+ if (field?.type === 'reference') {
85
+ for (const target of field.targets ?? (field.target ? [field.target] : [])) {
86
+ referencedUids.add(target);
87
+ }
88
+ }
89
+ }
90
+ }
74
91
  let written = 0;
75
92
  const stage = (draft, source, rawRows, references) => {
76
93
  const fieldTypes = new Map(Object.entries(draft.fields).map(([name, def]) => [
@@ -78,12 +95,31 @@ export function writeRowsStaging(rootDir, scan, drafts) {
78
95
  def && typeof def === 'object' ? (def.type ?? 'string') : 'string',
79
96
  ]));
80
97
  const referenceFields = new Set(references.map((reference) => reference.field));
98
+ const usedSlugs = new Set();
81
99
  const rows = rawRows.map((row) => {
82
100
  const out = {};
83
101
  for (const [key, value] of Object.entries(row)) {
84
102
  // Reference values stay as SOURCE values for later resolution.
85
103
  out[key] = referenceFields.has(key) ? value : transformValue(fieldTypes.get(key), value);
86
104
  }
105
+ if (typeof out.slug === 'string' && out.slug) {
106
+ usedSlugs.add(out.slug);
107
+ }
108
+ else if (!referencedUids.has(draft.uid)) {
109
+ // Human slug from the display field ("who-is-behind-fimo", never
110
+ // "faq-item-<hash>"). Referenced types keep their source identity —
111
+ // the server resolves references by target slug.
112
+ const display = DISPLAY_SLUG_FIELDS.map((field) => row[field]).find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
113
+ const base = display ? slugify(display) : '';
114
+ if (base) {
115
+ let candidate = base;
116
+ for (let counter = 2; usedSlugs.has(candidate); counter++) {
117
+ candidate = `${base}-${counter}`;
118
+ }
119
+ usedSlugs.add(candidate);
120
+ out.slug = candidate;
121
+ }
122
+ }
87
123
  return out;
88
124
  });
89
125
  const descriptor = {};
@@ -1,23 +1,7 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
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
- }
3
+ import { pushRoute, toSegment } from '../discovered-route.js';
19
4
  const PAGE_EXT = /\.(astro|md|mdx|html)$/;
20
- const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
21
5
  /** Astro file-based routes under src/pages (index files map to the dir path). */
22
6
  export function discoverAstroRoutes(pagesDir) {
23
7
  const routes = [];
@@ -46,7 +30,7 @@ export function discoverAstroRoutes(pagesDir) {
46
30
  continue;
47
31
  }
48
32
  const base = entry.replace(PAGE_EXT, '');
49
- pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)]);
33
+ pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)], full);
50
34
  }
51
35
  };
52
36
  walk(pagesDir, []);
@@ -1,5 +1,6 @@
1
1
  import type { TemplateWrapStyle } from '../apply-wrap-templates.js';
2
2
  import type { FrameworkDetection } from '../detect-framework.js';
3
+ import { type RouteFileResolution, type RouteLike } from '../routes/module-resolve.js';
3
4
  import { type CollectionRewireResult } from './next/rewire.js';
4
5
  import { type DiscoveredRoute } from './next/routes.js';
5
6
  import { type RootWiringResult } from './next/wiring.js';
@@ -31,14 +32,26 @@ export declare function frameworkJsxWrapperModule(detection: Pick<FrameworkDetec
31
32
  * server data path there is no deterministic t() wiring to inject.
32
33
  */
33
34
  export declare function frameworkTemplateWrapStyle(framework: string | null | undefined): TemplateWrapStyle | null;
35
+ export declare function frameworkRouteFiles(framework: string | null): ((cwd: string, routes: Record<string, RouteLike>) => RouteFileResolution[]) | null;
36
+ export interface SectionStyle {
37
+ /** Module extensions a section import may resolve to. */
38
+ extensions: string[];
39
+ /** Wrapped-key scope for a component file (must match the wrap codemod's
40
+ * own scoping so drafted sections join their staged keys). */
41
+ fileScope: (relFile: string) => string;
42
+ /** Directories tried for auto-imported components (Nuxt convention) when
43
+ * a section tag has no import statement. */
44
+ fallbackComponentDirs: string[];
45
+ }
34
46
  /**
35
- * The one-time root-wiring codemod for frameworks whose providers mount in a
36
- * single well-known file (Next.js app router: the root layout; Vite React
37
- * SPA: the entry). Runs during --apply, AFTER the baseline capture — the
38
- * pre-wrap dev-server boot must not require the fimo package, and providers
39
- * are render-transparent so the parity check stays clean. Template
40
- * frameworks wire through their TemplateWrapStyle.ensureWiring hooks instead.
47
+ * How to read a page's SECTION STRUCTURE for this framework — the seam the
48
+ * page-structure drafter builds on. Template frameworks share the ES-import
49
+ * + capitalized-tag shape, differing only in extensions and key scoping.
41
50
  */
51
+ export declare function frameworkSectionStyle(framework: string | null): SectionStyle;
52
+ /** Client-rendered frameworks: no server pass — reads happen in the browser
53
+ * (the prompt's collection recipe and the SPA providers key off this). */
54
+ export declare function isClientRenderedFramework(framework: string | null | undefined): boolean;
42
55
  export declare function frameworkRootWiring(detection: Pick<FrameworkDetection, 'framework' | 'react' | 'appDir'>): ((cwd: string) => RootWiringResult) | null;
43
56
  /**
44
57
  * Collection-page rewiring: pages that render a machine-staged collection get