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.
- package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
- package/assets/agents-starter/claude/settings.json +19 -0
- package/assets/content-templates/hooks-template.eta +4 -2
- package/assets/content-templates/types-template.eta +67 -1
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +86 -15
- package/assets/skills/fimo/references/translations.md +75 -0
- package/assets/skills/fimo-cli/SKILL.md +6 -2
- package/assets/skills/fimo-cli/references/content.md +61 -2
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
- package/assets/skills/fimo-cli/references/integrations.md +5 -1
- package/assets/skills/fimo-cli/references/webhooks.md +162 -0
- package/assets/skills/fimo-migration/SKILL.md +24 -2
- package/assets/skills/fimo-migration/references/content.md +24 -0
- package/assets/skills/fimo-migration/references/manifest.md +46 -0
- package/assets/skills/fimo-migration/references/pages.md +234 -0
- package/assets/skills/fimo-migration/references/qa.md +24 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +662 -0
- package/assets/skills/fimo-studio/SKILL.md +5 -3
- package/assets/skills/fimo-studio/references/cms.md +9 -1
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/assets/skills/fimo-studio/references/webhooks.md +42 -0
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.js +31 -131
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +1337 -189
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.js +31 -30
- package/dist/runtime/astro/index.d.ts +2 -0
- package/dist/runtime/astro/index.js +3 -0
- package/dist/runtime/astro/server.d.ts +3 -0
- package/dist/runtime/astro/server.js +6 -0
- package/dist/runtime/content/client/internal.js +1 -0
- package/dist/runtime/content/client/types.d.ts +16 -0
- package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
- package/dist/runtime/core/BuiltWithBadge.js +68 -0
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
- package/dist/runtime/next/FimoNextProviders.js +20 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +2 -0
- package/dist/runtime/next/server.js +4 -0
- package/dist/runtime/nuxt/composables.d.ts +9 -0
- package/dist/runtime/nuxt/composables.js +57 -0
- package/dist/runtime/nuxt/index.d.ts +3 -0
- package/dist/runtime/nuxt/index.js +6 -0
- package/dist/runtime/nuxt/server.d.ts +3 -0
- package/dist/runtime/nuxt/server.js +6 -0
- package/dist/runtime/primitives/components/RichText.d.ts +5 -1
- package/dist/runtime/primitives/components/RichText.js +2 -2
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.js +41 -0
- package/dist/runtime/primitives/labels-store.d.ts +12 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
- package/dist/runtime/react/FimoSpaProviders.js +122 -0
- package/dist/runtime/react/index.d.ts +35 -0
- package/dist/runtime/react/index.js +40 -0
- package/dist/runtime/react/spa-content-cache.d.ts +38 -0
- package/dist/runtime/react/spa-content-cache.js +64 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
- package/dist/runtime/remix/FimoRemixProviders.js +12 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
- package/dist/runtime/remix/FimoRemixScripts.js +19 -0
- package/dist/runtime/remix/index.d.ts +12 -0
- package/dist/runtime/remix/index.js +11 -0
- package/dist/runtime/remix/server.d.ts +2 -0
- package/dist/runtime/remix/server.js +2 -0
- package/dist/runtime/seo/neutral.d.ts +22 -0
- package/dist/runtime/seo/neutral.js +17 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -13
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/i18n-messages.d.ts +29 -0
- package/dist/runtime/shared/i18n-messages.js +79 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
- package/dist/runtime/shared/labels-fetch.pure.js +71 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
- package/dist/runtime/shared/labels-fetch.server.js +97 -0
- package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
- package/dist/runtime/shared/labels-t.pure.js +10 -0
- package/dist/runtime/strapi/client.d.ts +30 -0
- package/dist/runtime/strapi/client.js +132 -0
- package/dist/runtime/strapi/index.d.ts +5 -0
- package/dist/runtime/strapi/index.js +6 -0
- package/dist/runtime/strapi/reshape.d.ts +50 -0
- package/dist/runtime/strapi/reshape.js +121 -0
- package/dist/runtime/svelte/index.d.ts +3 -0
- package/dist/runtime/svelte/index.js +7 -0
- package/dist/runtime/svelte/server.d.ts +3 -0
- package/dist/runtime/svelte/server.js +6 -0
- package/dist/runtime/vue/index.d.ts +3 -0
- package/dist/runtime/vue/index.js +5 -0
- package/dist/runtime/vue/server.d.ts +3 -0
- package/dist/runtime/vue/server.js +6 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
- package/dist/runtime/wrapped/FimoFileLink.js +19 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts +22 -0
- package/dist/runtime/wrapped/FimoText.js +20 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
- package/dist/runtime/wrapped/WrappedProviders.js +101 -0
- package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
- package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
- package/dist/runtime/wrapped/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/server.d.ts +80 -0
- package/dist/runtime/wrapped/server.js +207 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +137 -0
- package/dist/scripts/apply-wrap.d.ts +73 -0
- package/dist/scripts/apply-wrap.js +340 -0
- package/dist/scripts/cms/discover.d.ts +2 -0
- package/dist/scripts/cms/discover.js +17 -0
- package/dist/scripts/cms/example-content.d.ts +33 -0
- package/dist/scripts/cms/example-content.js +165 -0
- package/dist/scripts/cms/strapi/detect.d.ts +2 -0
- package/dist/scripts/cms/strapi/detect.js +186 -0
- package/dist/scripts/cms/strapi/index.d.ts +3 -0
- package/dist/scripts/cms/strapi/index.js +25 -0
- package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
- package/dist/scripts/cms/strapi/list-source.js +90 -0
- package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
- package/dist/scripts/cms/strapi/schema-map.js +289 -0
- package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
- package/dist/scripts/cms/strapi/shapes.js +48 -0
- package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
- package/dist/scripts/cms/strapi/touchpoints.js +145 -0
- package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
- package/dist/scripts/cms/strapi/wire-client.js +107 -0
- package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
- package/dist/scripts/cms/strapi/wiring.js +46 -0
- package/dist/scripts/cms/types.d.ts +145 -0
- package/dist/scripts/cms/types.js +11 -0
- package/dist/scripts/detect-framework.d.ts +39 -0
- package/dist/scripts/detect-framework.js +139 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +178 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +247 -0
- package/dist/scripts/extract-translations.js +16 -11
- package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
- package/dist/scripts/frameworks/astro/routes.js +44 -0
- package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
- package/dist/scripts/frameworks/astro/template-style.js +19 -0
- package/dist/scripts/frameworks/discover.d.ts +83 -0
- package/dist/scripts/frameworks/discover.js +288 -0
- package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
- package/dist/scripts/frameworks/discovered-route.js +17 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
- package/dist/scripts/frameworks/next/rewire.js +282 -0
- package/dist/scripts/frameworks/next/routes.d.ts +30 -0
- package/dist/scripts/frameworks/next/routes.js +176 -0
- package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
- package/dist/scripts/frameworks/next/wiring.js +186 -0
- package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
- package/dist/scripts/frameworks/nuxt/routes.js +38 -0
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
- package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
- package/dist/scripts/frameworks/remix/routes.js +36 -0
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
- package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
- package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
- package/dist/scripts/frameworks/vite/routes.js +143 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
- package/dist/scripts/frameworks/vite/wiring.js +144 -0
- package/dist/scripts/i18n-namespace.d.ts +27 -0
- package/dist/scripts/i18n-namespace.js +155 -0
- package/dist/scripts/lib/humanize.d.ts +14 -0
- package/dist/scripts/lib/humanize.js +54 -0
- package/dist/scripts/lib/validation-report.d.ts +1 -3
- package/dist/scripts/lib/validation-report.js +3 -9
- package/dist/scripts/render-parity/capture.d.ts +64 -0
- package/dist/scripts/render-parity/capture.js +436 -0
- package/dist/scripts/render-parity/compare.d.ts +59 -0
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
- package/dist/scripts/render-parity/normalize-html.js +223 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- package/dist/scripts/routes/bindings.d.ts +93 -0
- package/dist/scripts/routes/bindings.js +117 -0
- package/dist/scripts/routes/label-prefixes.d.ts +49 -0
- package/dist/scripts/routes/label-prefixes.js +136 -0
- package/dist/scripts/routes/module-resolve.d.ts +20 -0
- package/dist/scripts/routes/module-resolve.js +78 -0
- package/dist/scripts/routes/page-structure.d.ts +72 -0
- package/dist/scripts/routes/page-structure.js +234 -0
- package/dist/scripts/scan-wrappable.d.ts +200 -0
- package/dist/scripts/scan-wrappable.js +1355 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.js +42 -1
- package/package.json +88 -4
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/translations/en.json +1 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Framework-agnostic DOM contract for wrapped content in NON-React
|
|
2
|
+
// frameworks (Astro templates, Vue SFCs, Svelte components). The edit-mode
|
|
3
|
+
// preview client selects elements by these attributes; rendering the value +
|
|
4
|
+
// attributes is all a template needs:
|
|
5
|
+
//
|
|
6
|
+
// Astro: <h1 {...fimoLabelAttrs('home.title')}>{t('home.title', 'Hello')}</h1>
|
|
7
|
+
// Vue: <h1 v-bind="fimoLabelAttrs('home.title')">{{ t('home.title', 'Hello') }}</h1>
|
|
8
|
+
// Svelte: <h1 {...fimoLabelAttrs('home.title')}>{tv}</h1>
|
|
9
|
+
//
|
|
10
|
+
// Values come from the server loaders (loadFimoT / loadWrappedMedia) — after
|
|
11
|
+
// an edit, the page refetches on reload; live DOM patching is handled by the
|
|
12
|
+
// preview client via these same attributes.
|
|
13
|
+
// NOTE: this must be the literal DOM attribute the preview client queries
|
|
14
|
+
// (`document.querySelectorAll('[data-fimo-source]')`) — NOT the FIMO_SOURCE
|
|
15
|
+
// Symbol from the React primitives: a symbol key vanishes in a template
|
|
16
|
+
// attribute spread and in JSON, silently breaking editability.
|
|
17
|
+
export const FIMO_SOURCE_ATTR = 'data-fimo-source';
|
|
18
|
+
export function fimoLabelAttrs(key, locale = 'en') {
|
|
19
|
+
return { [FIMO_SOURCE_ATTR]: `__labels.${locale}.${key}.value` };
|
|
20
|
+
}
|
|
21
|
+
export function fimoMediaAttrs(source) {
|
|
22
|
+
return { [FIMO_SOURCE_ATTR]: source };
|
|
23
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FimoNextLabels } from '../next/FimoNextProviders.js';
|
|
2
|
+
import type { FimoConfig } from '../shared/fimo-config.types.js';
|
|
3
|
+
import type { WrappedMediaEntry, WrappedRichTextEntry } from './WrappedContentContext.js';
|
|
4
|
+
export interface FimoServerOptions {
|
|
5
|
+
/** Project root (where fimo-config.json and .env live). Defaults to cwd. */
|
|
6
|
+
rootDir?: string;
|
|
7
|
+
/** Tenant API base URL. Defaults to NEXT_PUBLIC_FIMO_API_URL / VITE_API_URL. */
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadFimoConfig(rootDir?: string): FimoConfig;
|
|
11
|
+
export declare function loadFimoLabels(options?: FimoServerOptions): Promise<FimoNextLabels>;
|
|
12
|
+
/**
|
|
13
|
+
* Server-component label accessor: `useLabels()` is client-only, and forcing
|
|
14
|
+
* a client wrapper for one aria-label in a server component is not a
|
|
15
|
+
* migration, it is collateral damage. Load once per request scope and read
|
|
16
|
+
* synchronously: `const t = await loadFimoT(); <a aria-label={t('footer.github', 'GitHub')} …`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadFimoT(options?: FimoServerOptions & {
|
|
19
|
+
locale?: string;
|
|
20
|
+
}): Promise<(key: string, fallback: string) => string>;
|
|
21
|
+
export interface FimoEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch published entries of a content type from the public tenant API.
|
|
27
|
+
* Returns [] when the API is unreachable so the wrapped site keeps rendering
|
|
28
|
+
* its fallbacks.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchEntries(typeName: string, options?: FimoServerOptions & {
|
|
31
|
+
/** The public API returns newest-first; rendering wants source order.
|
|
32
|
+
* Default 'oldest' restores insertion order for migrated collections. */
|
|
33
|
+
order?: 'oldest' | 'newest';
|
|
34
|
+
/** Query filter forwarded to the public API (e.g. { slug: { $in: [...] } }). */
|
|
35
|
+
where?: Record<string, unknown>;
|
|
36
|
+
/** Row cap forwarded to the public API. */
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<FimoEntry[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Load the WrappedMedia entries (key → media) seeded by the wrap migration.
|
|
41
|
+
* Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
|
|
42
|
+
* asset-typed `image` field. Paginates to the end — a site's wrapped media
|
|
43
|
+
* routinely exceeds one API page.
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadWrappedMedia(options?: FimoServerOptions): Promise<Record<string, WrappedMediaEntry>>;
|
|
46
|
+
/**
|
|
47
|
+
* Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
|
|
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.
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadWrappedRichText(options?: FimoServerOptions): Promise<Record<string, WrappedRichTextEntry>>;
|
|
53
|
+
/**
|
|
54
|
+
* Overlay a wrap-migrated collection onto its retained source rows: fetch the
|
|
55
|
+
* Fimo entries matching the source rows' keys, merge field-by-field with the
|
|
56
|
+
* type-matched rule above, and return plain data rows in SOURCE order. The
|
|
57
|
+
* page keeps its original render logic and its original fallback — edits made
|
|
58
|
+
* in Fimo show up, everything else stays byte-identical.
|
|
59
|
+
*/
|
|
60
|
+
export declare function wrapCollection<T extends Record<string, unknown>>(typeName: string, sourceRows: readonly T[], options?: FimoServerOptions & {
|
|
61
|
+
keyField?: string;
|
|
62
|
+
}): Promise<T[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
|
|
65
|
+
* i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
|
|
66
|
+
* labels (flat dot-keys); this fetches them back and un-flattens them into the
|
|
67
|
+
* nested shape the app's own i18n loader expects — so the app keeps its
|
|
68
|
+
* `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
|
|
69
|
+
*
|
|
70
|
+
* Deep-merge over the shipped messages (pass them as `fallback`) so any key
|
|
71
|
+
* not yet in Fimo falls back to the static file instead of showing a missing
|
|
72
|
+
* key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
|
|
73
|
+
*
|
|
74
|
+
* const messages = await loadFimoMessages(locale, { fallback: staticMessages });
|
|
75
|
+
* return { locale, messages };
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadFimoMessages(locale: string, options?: FimoServerOptions & {
|
|
78
|
+
fallback?: Record<string, unknown>;
|
|
79
|
+
}): Promise<Record<string, unknown>>;
|
|
80
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Server-only loaders for WRAPPED (adopted) apps — framework-neutral.
|
|
2
|
+
// Content resolves at request time from the public tenant API (no Vite
|
|
3
|
+
// virtual modules, no build plugin). Framework adapters re-export these:
|
|
4
|
+
// fimo/next/server today, fimo/astro tomorrow — same loaders, same shapes.
|
|
5
|
+
import { getConfigServer } from '../shared/fimo-config.server.js';
|
|
6
|
+
import { deepMergeMessages, unflattenMessages } from '../shared/i18n-messages.js';
|
|
7
|
+
import { fetchLabelDictionary, fetchWithTimeout, labelSignature, loadLabelBundle, readProjectEnv, resolveTenantApiUrl, } from '../shared/labels-fetch.server.js';
|
|
8
|
+
import { fetchAllWrappedRows, mapWrappedMedia, mapWrappedRichText } from './content-fetch.pure.js';
|
|
9
|
+
export function loadFimoConfig(rootDir = process.cwd()) {
|
|
10
|
+
return getConfigServer(rootDir);
|
|
11
|
+
}
|
|
12
|
+
export async function loadFimoLabels(options = {}) {
|
|
13
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
14
|
+
const bundle = await loadLabelBundle(rootDir);
|
|
15
|
+
return { ...bundle, signature: labelSignature(bundle) };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Server-component label accessor: `useLabels()` is client-only, and forcing
|
|
19
|
+
* a client wrapper for one aria-label in a server component is not a
|
|
20
|
+
* migration, it is collateral damage. Load once per request scope and read
|
|
21
|
+
* synchronously: `const t = await loadFimoT(); <a aria-label={t('footer.github', 'GitHub')} …`.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadFimoT(options = {}) {
|
|
24
|
+
const labels = await loadFimoLabels(options);
|
|
25
|
+
const locale = options.locale ?? labels.defaultLocale;
|
|
26
|
+
const dictionary = labels.dictionaries[locale] ?? labels.dictionaries[labels.defaultLocale] ?? {};
|
|
27
|
+
return (key, fallback) => {
|
|
28
|
+
const value = dictionary[key];
|
|
29
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function resolveApiUrl(options) {
|
|
33
|
+
if (options.apiUrl) {
|
|
34
|
+
return options.apiUrl;
|
|
35
|
+
}
|
|
36
|
+
const env = await readProjectEnv(options.rootDir ?? process.cwd());
|
|
37
|
+
return resolveTenantApiUrl(env);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch published entries of a content type from the public tenant API.
|
|
41
|
+
* Returns [] when the API is unreachable so the wrapped site keeps rendering
|
|
42
|
+
* its fallbacks.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchEntries(typeName, options = {}) {
|
|
45
|
+
const apiUrl = await resolveApiUrl(options);
|
|
46
|
+
if (!apiUrl) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(`/entries/${typeName}`, apiUrl);
|
|
51
|
+
if (options.where) {
|
|
52
|
+
url.searchParams.set('where', JSON.stringify(options.where));
|
|
53
|
+
}
|
|
54
|
+
if (options.limit) {
|
|
55
|
+
url.searchParams.set('limit', String(options.limit));
|
|
56
|
+
}
|
|
57
|
+
const response = await fetchWithTimeout(url, 4000);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const body = (await response.json());
|
|
62
|
+
const rows = Array.isArray(body.data) ? body.data : [];
|
|
63
|
+
const entries = rows
|
|
64
|
+
.filter((row) => Boolean(row) && typeof row === 'object')
|
|
65
|
+
.map((row) => ({
|
|
66
|
+
id: String(row.id ?? row.documentId ?? ''),
|
|
67
|
+
data: (row.data && typeof row.data === 'object' ? row.data : row),
|
|
68
|
+
}))
|
|
69
|
+
.filter((row) => row.id.length > 0);
|
|
70
|
+
return (options.order ?? 'oldest') === 'oldest' ? entries.reverse() : entries;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load the WrappedMedia entries (key → media) seeded by the wrap migration.
|
|
78
|
+
* Each entry has a `key` (the FimoMediaImage/Video/FileLink id) and an
|
|
79
|
+
* asset-typed `image` field. Paginates to the end — a site's wrapped media
|
|
80
|
+
* routinely exceeds one API page.
|
|
81
|
+
*/
|
|
82
|
+
export async function loadWrappedMedia(options = {}) {
|
|
83
|
+
const apiUrl = await resolveApiUrl(options);
|
|
84
|
+
if (!apiUrl) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return mapWrappedMedia(await fetchAllWrappedRows(apiUrl, 'WrappedMedia'));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load the WrappedRichText entries (key → TipTap JSON) seeded by the wrap
|
|
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.
|
|
94
|
+
*/
|
|
95
|
+
export async function loadWrappedRichText(options = {}) {
|
|
96
|
+
const apiUrl = await resolveApiUrl(options);
|
|
97
|
+
if (!apiUrl) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
return mapWrappedRichText(await fetchAllWrappedRows(apiUrl, 'WrappedRichText'));
|
|
101
|
+
}
|
|
102
|
+
/** Key fields tried, in order, to align a source row with its Fimo entry. */
|
|
103
|
+
const COLLECTION_KEY_FIELDS = ['slug', 'id', 'key', 'name', 'title', 'question'];
|
|
104
|
+
/** Fimo entries fetched per collection: covers rendered windows and small
|
|
105
|
+
* collections entirely; rows beyond it keep rendering their source values. */
|
|
106
|
+
const COLLECTION_FETCH_CAP = 100;
|
|
107
|
+
function collectionKeyField(rows) {
|
|
108
|
+
for (const field of COLLECTION_KEY_FIELDS) {
|
|
109
|
+
if (rows.every((row) => typeof row[field] === 'string' && row[field].length > 0)) {
|
|
110
|
+
return field;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** A Fimo value replaces its source value only when the SHAPES agree, so a
|
|
116
|
+
* page that rendered strings keeps rendering strings: media envelopes,
|
|
117
|
+
* richtext docs, reference handles, and nulls all fall back to the source
|
|
118
|
+
* value instead of leaking objects into JSX. */
|
|
119
|
+
function preferTypeMatched(fimoValue, sourceValue) {
|
|
120
|
+
if (fimoValue === null || fimoValue === undefined) {
|
|
121
|
+
return sourceValue;
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(sourceValue)) {
|
|
124
|
+
if (!Array.isArray(fimoValue)) {
|
|
125
|
+
return sourceValue;
|
|
126
|
+
}
|
|
127
|
+
return fimoValue.every((element) => typeof element === typeof (sourceValue[0] ?? '') && typeof element !== 'object')
|
|
128
|
+
? fimoValue
|
|
129
|
+
: sourceValue;
|
|
130
|
+
}
|
|
131
|
+
return typeof fimoValue === typeof sourceValue && typeof fimoValue !== 'object' ? fimoValue : sourceValue;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Overlay a wrap-migrated collection onto its retained source rows: fetch the
|
|
135
|
+
* Fimo entries matching the source rows' keys, merge field-by-field with the
|
|
136
|
+
* type-matched rule above, and return plain data rows in SOURCE order. The
|
|
137
|
+
* page keeps its original render logic and its original fallback — edits made
|
|
138
|
+
* in Fimo show up, everything else stays byte-identical.
|
|
139
|
+
*/
|
|
140
|
+
export async function wrapCollection(typeName, sourceRows, options = {}) {
|
|
141
|
+
const rows = sourceRows;
|
|
142
|
+
if (rows.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const keyField = options.keyField ?? collectionKeyField(rows);
|
|
146
|
+
if (!keyField) {
|
|
147
|
+
return sourceRows;
|
|
148
|
+
}
|
|
149
|
+
const keys = rows.slice(0, COLLECTION_FETCH_CAP).map((row) => row[keyField]);
|
|
150
|
+
const entries = await fetchEntries(typeName, {
|
|
151
|
+
...options,
|
|
152
|
+
where: { [keyField]: { $in: keys } },
|
|
153
|
+
limit: keys.length,
|
|
154
|
+
});
|
|
155
|
+
if (entries.length === 0) {
|
|
156
|
+
return sourceRows;
|
|
157
|
+
}
|
|
158
|
+
const byKey = new Map();
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const key = entry.data[keyField];
|
|
161
|
+
if (typeof key === 'string' && !byKey.has(key)) {
|
|
162
|
+
byKey.set(key, entry.data);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return rows.map((row) => {
|
|
166
|
+
const fimoRow = byKey.get(row[keyField]);
|
|
167
|
+
if (!fimoRow) {
|
|
168
|
+
return row;
|
|
169
|
+
}
|
|
170
|
+
const merged = { ...row };
|
|
171
|
+
for (const field of Object.keys(row)) {
|
|
172
|
+
merged[field] = preferTypeMatched(fimoRow[field], row[field]);
|
|
173
|
+
}
|
|
174
|
+
return merged;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Load a locale's messages from Fimo for an EXISTING i18n runtime (next-intl,
|
|
179
|
+
* i18next). Wrap-mode i18n ingestion seeds the app's message store into Fimo
|
|
180
|
+
* labels (flat dot-keys); this fetches them back and un-flattens them into the
|
|
181
|
+
* nested shape the app's own i18n loader expects — so the app keeps its
|
|
182
|
+
* `useTranslations`/`t()` unchanged, now editable from Fimo Studio.
|
|
183
|
+
*
|
|
184
|
+
* Deep-merge over the shipped messages (pass them as `fallback`) so any key
|
|
185
|
+
* not yet in Fimo falls back to the static file instead of showing a missing
|
|
186
|
+
* key. Wire it in the app's message loader, e.g. next-intl `i18n/request.ts`:
|
|
187
|
+
*
|
|
188
|
+
* const messages = await loadFimoMessages(locale, { fallback: staticMessages });
|
|
189
|
+
* return { locale, messages };
|
|
190
|
+
*/
|
|
191
|
+
export async function loadFimoMessages(locale, options = {}) {
|
|
192
|
+
const fallback = options.fallback ?? {};
|
|
193
|
+
const apiUrl = await resolveApiUrl(options);
|
|
194
|
+
if (!apiUrl) {
|
|
195
|
+
return fallback;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const { labels } = await fetchLabelDictionary(apiUrl, locale);
|
|
199
|
+
if (Object.keys(labels).length === 0) {
|
|
200
|
+
return fallback;
|
|
201
|
+
}
|
|
202
|
+
return deepMergeMessages(fallback, unflattenMessages(labels));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KeyAllocator, type ApplyWrapResult } from './apply-wrap.js';
|
|
2
|
+
import { type WrappableItem } from './scan-wrappable.js';
|
|
3
|
+
/**
|
|
4
|
+
* The template half of the --apply codemod: wraps plain TEMPLATE text nodes
|
|
5
|
+
* with the framework's t() form and injects the per-file wiring head once.
|
|
6
|
+
* Framework-neutral by contract — everything framework-specific arrives via
|
|
7
|
+
* a TemplateWrapStyle from the framework registry (frameworks/discover.ts).
|
|
8
|
+
*
|
|
9
|
+
* Grown out of the batch-dogfood plan-driven wrapper (392/392 items on a
|
|
10
|
+
* 201-route site); the lessons it encodes:
|
|
11
|
+
* - heads go in the INSTANCE script, never a `<script module>` block;
|
|
12
|
+
* - files that already bind a `t` identifier (i18n libraries, custom
|
|
13
|
+
* helpers) are left whole for the agent — renaming foreign t() calls
|
|
14
|
+
* broke a real site;
|
|
15
|
+
* - label VALUES decode HTML entities while the inline fallback keeps the
|
|
16
|
+
* source form, so rendering never changes;
|
|
17
|
+
* - keys are collision-safe via the shared KeyAllocator.
|
|
18
|
+
*
|
|
19
|
+
* Anything that is not a plain text node (attributes, media, markup the
|
|
20
|
+
* preview matcher cannot anchor) stays in the plan for the agent — same
|
|
21
|
+
* philosophy as the JSX codemod: provably safe or hands off.
|
|
22
|
+
*/
|
|
23
|
+
export interface TemplateWrapStyle {
|
|
24
|
+
/** File extension this style owns, e.g. '.astro'. */
|
|
25
|
+
ext: string;
|
|
26
|
+
/** Wiring lines injected once per file (the t() head). */
|
|
27
|
+
head: string;
|
|
28
|
+
/** Marker proving our wiring (or an equivalent manual one) is present. */
|
|
29
|
+
headMarker: RegExp;
|
|
30
|
+
/** t() delimiters around `"key", "fallback"`. */
|
|
31
|
+
open: string;
|
|
32
|
+
close: string;
|
|
33
|
+
/**
|
|
34
|
+
* Insert the head into a file. Return null when the file has no safe
|
|
35
|
+
* injection point (e.g. an options-API script) — its items stay agent work.
|
|
36
|
+
*/
|
|
37
|
+
injectHead: (code: string, head: string) => string | null;
|
|
38
|
+
/** Override the key scope for route-convention filenames (+page.svelte). */
|
|
39
|
+
fileScope?: (file: string) => string | null;
|
|
40
|
+
/** One-time project wiring once any file was wrapped. Returns files written. */
|
|
41
|
+
ensureWiring?: (rootDir: string) => string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Shared injection helper for SFC-style templates: first instance script,
|
|
45
|
+
* never a `<script module>` block (module scope cannot read page data), or a
|
|
46
|
+
* fresh script when the file has none.
|
|
47
|
+
*/
|
|
48
|
+
export declare function injectIntoInstanceScript(code: string, head: string, scriptTag: string): string;
|
|
49
|
+
export declare function applyTemplateWrapToFile(code: string, style: TemplateWrapStyle, items: WrappableItem[], allocator: KeyAllocator): {
|
|
50
|
+
code: string;
|
|
51
|
+
} & Omit<ApplyWrapResult, 'filesChanged'>;
|
|
52
|
+
/**
|
|
53
|
+
* Apply the template wraps across a project — same contract as applyWrap:
|
|
54
|
+
* scans, mutates source files, returns the generated labels for staging and
|
|
55
|
+
* seeding. The style's ensureWiring hook then writes any one-time project
|
|
56
|
+
* wiring (e.g. a root layout labels loader) when files were wrapped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function applyTemplateWrap(rootDir: string, opts: {
|
|
59
|
+
style: TemplateWrapStyle;
|
|
60
|
+
excludePrefixes?: string[];
|
|
61
|
+
}): ApplyWrapResult;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { decodeJsxEntities, KeyAllocator, readApplyExemptions, reusableStagedLabels, slugify, } from './apply-wrap.js';
|
|
4
|
+
import { scanWrappable } from './scan-wrappable.js';
|
|
5
|
+
/**
|
|
6
|
+
* Shared injection helper for SFC-style templates: first instance script,
|
|
7
|
+
* never a `<script module>` block (module scope cannot read page data), or a
|
|
8
|
+
* fresh script when the file has none.
|
|
9
|
+
*/
|
|
10
|
+
export function injectIntoInstanceScript(code, head, scriptTag) {
|
|
11
|
+
const instance = code.match(/<script(?![^>]*\bmodule\b)[^>]*>\r?\n?/);
|
|
12
|
+
if (instance && instance.index !== undefined) {
|
|
13
|
+
const at = instance.index + instance[0].length;
|
|
14
|
+
return `${code.slice(0, at)}\t${head}\n${code.slice(at)}`;
|
|
15
|
+
}
|
|
16
|
+
return `${scriptTag}\n\t${head}\n</script>\n\n${code}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A file that already binds `t` (an i18n library, a custom helper) is agent
|
|
20
|
+
* work: the dogfood proved rewiring foreign t() calls breaks rendering.
|
|
21
|
+
*/
|
|
22
|
+
function bindsForeignT(code) {
|
|
23
|
+
return (/\buseI18n\s*\(/.test(code) ||
|
|
24
|
+
/(?:const|let|var)\s+t\s*=/.test(code) ||
|
|
25
|
+
/import\s*\{[^}]*\bt\b[^}]*\}\s*from/.test(code));
|
|
26
|
+
}
|
|
27
|
+
function previewPattern(preview) {
|
|
28
|
+
const core = preview
|
|
29
|
+
.replace(/…$/, '')
|
|
30
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
31
|
+
.replace(/\\? /g, '\\s+');
|
|
32
|
+
return new RegExp(`>(\\s*)(${core}[^<{]*?)(\\s*)<`);
|
|
33
|
+
}
|
|
34
|
+
export function applyTemplateWrapToFile(code, style, items, allocator) {
|
|
35
|
+
const applied = [];
|
|
36
|
+
const skipped = [];
|
|
37
|
+
const labels = {};
|
|
38
|
+
const hasHead = style.headMarker.test(code);
|
|
39
|
+
if (!hasHead && bindsForeignT(code)) {
|
|
40
|
+
return {
|
|
41
|
+
code,
|
|
42
|
+
applied,
|
|
43
|
+
skipped: items.map((item) => ({ item, reason: 'file binds its own t() — agent work' })),
|
|
44
|
+
labels,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!hasHead && style.injectHead(code, style.head) === null) {
|
|
48
|
+
return {
|
|
49
|
+
code,
|
|
50
|
+
applied,
|
|
51
|
+
skipped: items.map((item) => ({ item, reason: 'no safe wiring point in this file — agent work' })),
|
|
52
|
+
labels,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const file = items[0]?.file ?? '';
|
|
56
|
+
const scope = style.fileScope?.(file) ?? slugify(basename(file).replace(/\.[a-z0-9]+$/i, '')) ?? 'shared';
|
|
57
|
+
let next = code;
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
if (item.kind !== 'text') {
|
|
60
|
+
skipped.push({ item, reason: `${item.kind} stays agent work` });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const match = next.match(previewPattern(item.preview));
|
|
64
|
+
if (!match || match.index === undefined) {
|
|
65
|
+
skipped.push({ item, reason: 'text node not matched in source — agent work' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const collapsed = match[2].replace(/\s+/g, ' ').trim();
|
|
69
|
+
const value = decodeJsxEntities(collapsed);
|
|
70
|
+
const id = allocator.allocate(scope || 'shared', slugify(value), value);
|
|
71
|
+
const escaped = collapsed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
72
|
+
const replacement = `>${match[1]}${style.open}"${id}", "${escaped}"${style.close}${match[3]}<`;
|
|
73
|
+
next = next.slice(0, match.index) + replacement + next.slice(match.index + match[0].length);
|
|
74
|
+
labels[id] = value;
|
|
75
|
+
applied.push({ kind: 'text', file: item.file, line: item.line, id, preview: item.preview });
|
|
76
|
+
}
|
|
77
|
+
if (applied.length > 0 && !style.headMarker.test(next)) {
|
|
78
|
+
next = style.injectHead(next, style.head) ?? next;
|
|
79
|
+
}
|
|
80
|
+
return { code: next, applied, skipped, labels };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply the template wraps across a project — same contract as applyWrap:
|
|
84
|
+
* scans, mutates source files, returns the generated labels for staging and
|
|
85
|
+
* seeding. The style's ensureWiring hook then writes any one-time project
|
|
86
|
+
* wiring (e.g. a root layout labels loader) when files were wrapped.
|
|
87
|
+
*/
|
|
88
|
+
export function applyTemplateWrap(rootDir, opts) {
|
|
89
|
+
const { style } = opts;
|
|
90
|
+
const scan = scanWrappable(rootDir, { excludePrefixes: opts.excludePrefixes });
|
|
91
|
+
const existingLabels = (() => {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'labels.json'), 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
// Honor recorded exemptions — same contract as the JSX codemod: --apply
|
|
100
|
+
// must never wrap an item the user deliberately kept out.
|
|
101
|
+
const exemptions = readApplyExemptions(rootDir);
|
|
102
|
+
const isExempt = (item) => exemptions.some((exemption) => exemption.file === item.file && exemption.kind === item.kind && exemption.preview === item.preview);
|
|
103
|
+
const allocator = new KeyAllocator([...Object.keys(existingLabels), ...scan.wrappedIds.labels], reusableStagedLabels(existingLabels, scan.wrappedIds.labels));
|
|
104
|
+
const result = { applied: [], skipped: [], labels: {}, filesChanged: [] };
|
|
105
|
+
const byFile = new Map();
|
|
106
|
+
for (const item of scan.items) {
|
|
107
|
+
if (!item.file.endsWith(style.ext) || isExempt(item)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const list = byFile.get(item.file) ?? [];
|
|
111
|
+
list.push(item);
|
|
112
|
+
byFile.set(item.file, list);
|
|
113
|
+
}
|
|
114
|
+
for (const [file, items] of byFile) {
|
|
115
|
+
const filePath = join(rootDir, file);
|
|
116
|
+
let code;
|
|
117
|
+
try {
|
|
118
|
+
code = readFileSync(filePath, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
result.skipped.push(...items.map((item) => ({ item, reason: 'unreadable file' })));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fileResult = applyTemplateWrapToFile(code, style, items, allocator);
|
|
125
|
+
result.applied.push(...fileResult.applied);
|
|
126
|
+
result.skipped.push(...fileResult.skipped);
|
|
127
|
+
Object.assign(result.labels, fileResult.labels);
|
|
128
|
+
if (fileResult.applied.length > 0) {
|
|
129
|
+
writeFileSync(filePath, fileResult.code);
|
|
130
|
+
result.filesChanged.push(file);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (result.filesChanged.length > 0 && style.ensureWiring) {
|
|
134
|
+
result.filesChanged.push(...style.ensureWiring(rootDir));
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|