fimo 0.8.0 → 0.9.0-experimental.1
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/hooks-template.eta +4 -2
- package/assets/content-templates/types-template.eta +66 -1
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +86 -15
- 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 +22 -2
- package/assets/skills/fimo-migration/references/content.md +24 -0
- package/assets/skills/fimo-migration/references/manifest.md +35 -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 +617 -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 +3 -121
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +1051 -144
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.js +2 -23
- 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/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/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 +63 -0
- package/dist/runtime/react/index.d.ts +33 -0
- package/dist/runtime/react/index.js +39 -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/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- 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 +89 -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 +46 -0
- package/dist/runtime/strapi/reshape.js +114 -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 +23 -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/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/server.d.ts +77 -0
- package/dist/runtime/wrapped/server.js +237 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +132 -0
- package/dist/scripts/apply-wrap.d.ts +46 -0
- package/dist/scripts/apply-wrap.js +278 -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 +162 -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 +277 -0
- package/dist/scripts/cms/strapi/shapes.d.ts +23 -0
- package/dist/scripts/cms/strapi/shapes.js +38 -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 +133 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +135 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +188 -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 +55 -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 +70 -0
- package/dist/scripts/frameworks/discover.js +178 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
- package/dist/scripts/frameworks/next/rewire.js +258 -0
- package/dist/scripts/frameworks/next/routes.d.ts +17 -0
- package/dist/scripts/frameworks/next/routes.js +134 -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 +54 -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 +50 -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 +50 -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 +15 -0
- package/dist/scripts/frameworks/vite/routes.js +93 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
- package/dist/scripts/frameworks/vite/wiring.js +99 -0
- package/dist/scripts/i18n-namespace.d.ts +27 -0
- package/dist/scripts/i18n-namespace.js +155 -0
- package/dist/scripts/lib/validation-report.d.ts +1 -0
- package/dist/scripts/lib/validation-report.js +3 -0
- package/dist/scripts/render-parity/capture.d.ts +56 -0
- package/dist/scripts/render-parity/capture.js +366 -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/scan-wrappable.d.ts +196 -0
- package/dist/scripts/scan-wrappable.js +1232 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.js +42 -1
- package/package.json +84 -4
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type StrapiCallReason = 'strapi-sdk-import' | 'env-ref' | 'uid-ref';
|
|
2
|
+
export interface StrapiCallSite {
|
|
3
|
+
/** Path relative to the frontend directory. */
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
reason: StrapiCallReason;
|
|
7
|
+
snippet: string;
|
|
8
|
+
}
|
|
9
|
+
export interface StrapiTouchpoints {
|
|
10
|
+
/** Strapi SDK / client / generated-type packages still declared in package.json. */
|
|
11
|
+
dependencies: string[];
|
|
12
|
+
/** Source lines that still reach Strapi at runtime. */
|
|
13
|
+
callSites: StrapiCallSite[];
|
|
14
|
+
/** `STRAPI_*` keys still present in dotenv files (cleanup; low severity). */
|
|
15
|
+
envVars: string[];
|
|
16
|
+
/** No runtime Strapi coupling remains (no SDK deps, no call sites): the cutover is complete. */
|
|
17
|
+
clean: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Enumerate every remaining Strapi coupling in a wrapped frontend. This is the
|
|
21
|
+
* shared primitive behind both the unwire worklist (the agent's to-do) and the
|
|
22
|
+
* cutover gate (`clean === true`).
|
|
23
|
+
*
|
|
24
|
+
* `clean` is true when no runtime coupling remains — no Strapi SDK/client
|
|
25
|
+
* dependency and no source line that reaches Strapi — i.e. the site can no
|
|
26
|
+
* longer call Strapi. Detection keys on actual Strapi *references* (base-URL
|
|
27
|
+
* env, `api::` UID, SDK import), NOT filenames: a `lib/strapi.ts` the agent has
|
|
28
|
+
* rewritten to call Fimo has no Strapi references and therefore reads as clean.
|
|
29
|
+
* The `fimo/strapi` compat adapter is never counted.
|
|
30
|
+
*/
|
|
31
|
+
export declare function detectStrapiTouchpoints(frontendDir: string): StrapiTouchpoints;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
const SOURCE_EXT_RE = /\.(tsx?|jsx?|mjs|cjs|astro|vue|svelte)$/;
|
|
4
|
+
const SKIP_DIRS = new Set([
|
|
5
|
+
'node_modules',
|
|
6
|
+
'.next',
|
|
7
|
+
'.nuxt',
|
|
8
|
+
'.svelte-kit',
|
|
9
|
+
'.turbo',
|
|
10
|
+
'dist',
|
|
11
|
+
'build',
|
|
12
|
+
'out',
|
|
13
|
+
'coverage',
|
|
14
|
+
'.vercel',
|
|
15
|
+
'.git',
|
|
16
|
+
]);
|
|
17
|
+
const MAX_FILES = 2000;
|
|
18
|
+
/** A runtime reference to Strapi's base-URL / token env config. */
|
|
19
|
+
const ENV_REF_RE = /\bSTRAPI[A-Z0-9_]*_URL\b|\bNEXT_PUBLIC_STRAPI[A-Z0-9_]*\b|\bSTRAPI[A-Z0-9_]*_TOKEN\b/;
|
|
20
|
+
/** A Strapi content-type UID string (`api::article.article`). */
|
|
21
|
+
const UID_REF_RE = /api::[a-z0-9-]+\.[a-z0-9-]+/;
|
|
22
|
+
/** An import/require specifier — captured so a Strapi SDK can be told apart from `fimo/strapi`. */
|
|
23
|
+
const IMPORT_RE = /(?:from\s+|require\(\s*|import\(\s*)['"`]([^'"`]+)['"`]/;
|
|
24
|
+
/** A STRAPI env assignment in a dotenv file. */
|
|
25
|
+
const ENV_KEY_RE = /^\s*([A-Z0-9_]*STRAPI[A-Z0-9_]*)\s*=/;
|
|
26
|
+
function readDeps(dir) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
29
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function classifyLine(line) {
|
|
36
|
+
const importMatch = IMPORT_RE.exec(line);
|
|
37
|
+
// A Strapi SDK/client import — but NOT the `fimo/strapi` compat adapter.
|
|
38
|
+
if (importMatch && /strapi/i.test(importMatch[1]) && !/fimo/i.test(importMatch[1])) {
|
|
39
|
+
return 'strapi-sdk-import';
|
|
40
|
+
}
|
|
41
|
+
if (ENV_REF_RE.test(line)) {
|
|
42
|
+
return 'env-ref';
|
|
43
|
+
}
|
|
44
|
+
if (UID_REF_RE.test(line)) {
|
|
45
|
+
return 'uid-ref';
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function scanCallSites(frontendDir) {
|
|
50
|
+
const sites = [];
|
|
51
|
+
let scanned = 0;
|
|
52
|
+
const walk = (dir) => {
|
|
53
|
+
if (scanned >= MAX_FILES) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (scanned >= MAX_FILES) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
69
|
+
walk(join(dir, entry.name));
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!SOURCE_EXT_RE.test(entry.name)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
scanned += 1;
|
|
77
|
+
const full = join(dir, entry.name);
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = readFileSync(full, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const lines = text.split('\n');
|
|
86
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
87
|
+
const reason = classifyLine(lines[i]);
|
|
88
|
+
if (reason) {
|
|
89
|
+
sites.push({
|
|
90
|
+
file: relative(frontendDir, full),
|
|
91
|
+
line: i + 1,
|
|
92
|
+
reason,
|
|
93
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(frontendDir);
|
|
100
|
+
return sites;
|
|
101
|
+
}
|
|
102
|
+
function scanEnvKeys(frontendDir) {
|
|
103
|
+
const keys = new Set();
|
|
104
|
+
for (const name of ['.env', '.env.local', '.env.development', '.env.production']) {
|
|
105
|
+
const file = join(frontendDir, name);
|
|
106
|
+
if (!existsSync(file)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
111
|
+
const match = ENV_KEY_RE.exec(line);
|
|
112
|
+
if (match) {
|
|
113
|
+
keys.add(match[1]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// unreadable — ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...keys];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Enumerate every remaining Strapi coupling in a wrapped frontend. This is the
|
|
125
|
+
* shared primitive behind both the unwire worklist (the agent's to-do) and the
|
|
126
|
+
* cutover gate (`clean === true`).
|
|
127
|
+
*
|
|
128
|
+
* `clean` is true when no runtime coupling remains — no Strapi SDK/client
|
|
129
|
+
* dependency and no source line that reaches Strapi — i.e. the site can no
|
|
130
|
+
* longer call Strapi. Detection keys on actual Strapi *references* (base-URL
|
|
131
|
+
* env, `api::` UID, SDK import), NOT filenames: a `lib/strapi.ts` the agent has
|
|
132
|
+
* rewritten to call Fimo has no Strapi references and therefore reads as clean.
|
|
133
|
+
* The `fimo/strapi` compat adapter is never counted.
|
|
134
|
+
*/
|
|
135
|
+
export function detectStrapiTouchpoints(frontendDir) {
|
|
136
|
+
const dependencies = Object.keys(readDeps(frontendDir)).filter((name) => /strapi/i.test(name));
|
|
137
|
+
const callSites = scanCallSites(frontendDir);
|
|
138
|
+
const envVars = scanEnvKeys(frontendDir);
|
|
139
|
+
return {
|
|
140
|
+
dependencies,
|
|
141
|
+
callSites,
|
|
142
|
+
envVars,
|
|
143
|
+
clean: dependencies.length === 0 && callSites.length === 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CmsCollectionSchema } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Machine-wire a Strapi frontend to read from Fimo: emit the `fimo-strapi`
|
|
4
|
+
* module in the CMS client dir, then swap every bare `fetch(...)` in that dir
|
|
5
|
+
* to `fimoStrapiFetch(...)`. Because every Strapi client funnels through one
|
|
6
|
+
* `fetch`, this makes the WHOLE site read scaffolded content from Fimo — no
|
|
7
|
+
* per-method or per-call-site knowledge, no agent step. Unscaffolded types fall
|
|
8
|
+
* through to the live CMS inside the shim.
|
|
9
|
+
*/
|
|
10
|
+
export interface StrapiWireResult {
|
|
11
|
+
changes: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
wired: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function wireStrapiClient(cwd: string, clientDir: string, schemas: CmsCollectionSchema[]): StrapiWireResult;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseSync } from 'oxc-parser';
|
|
4
|
+
import { renderFimoStrapiModule } from './wiring.js';
|
|
5
|
+
function walk(node, visit) {
|
|
6
|
+
if (!node || typeof node !== 'object') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(node)) {
|
|
10
|
+
for (const child of node) {
|
|
11
|
+
walk(child, visit);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const record = node;
|
|
16
|
+
if (typeof record.type === 'string') {
|
|
17
|
+
visit(record);
|
|
18
|
+
}
|
|
19
|
+
for (const [key, value] of Object.entries(record)) {
|
|
20
|
+
if (key !== 'type' && value && typeof value === 'object') {
|
|
21
|
+
walk(value, visit);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Callee spans of bare `fetch(...)` calls (not `x.fetch()` / `this.fetch()`). */
|
|
26
|
+
function bareFetchCallees(filePath, code) {
|
|
27
|
+
let program;
|
|
28
|
+
try {
|
|
29
|
+
program = parseSync(filePath, code).program;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const spans = [];
|
|
35
|
+
walk(program, (n) => {
|
|
36
|
+
if (n.type === 'CallExpression' && n.callee?.type === 'Identifier' && n.callee.name === 'fetch') {
|
|
37
|
+
spans.push({ start: n.callee.start, end: n.callee.end });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return spans;
|
|
41
|
+
}
|
|
42
|
+
function applyReplacements(code, edits) {
|
|
43
|
+
let next = code;
|
|
44
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
45
|
+
next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
function relativeImport(fromFile, toFile) {
|
|
50
|
+
const rel = path
|
|
51
|
+
.relative(path.dirname(fromFile), toFile)
|
|
52
|
+
.replace(/\.tsx?$/, '')
|
|
53
|
+
.split(path.sep)
|
|
54
|
+
.join('/');
|
|
55
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
56
|
+
}
|
|
57
|
+
function sourceFiles(dir) {
|
|
58
|
+
const out = [];
|
|
59
|
+
const walkDir = (current) => {
|
|
60
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
61
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const full = path.join(current, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
walkDir(full);
|
|
67
|
+
}
|
|
68
|
+
else if (/\.(tsx?|jsx?|mjs)$/.test(entry.name)) {
|
|
69
|
+
out.push(full);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (fs.existsSync(dir)) {
|
|
74
|
+
walkDir(dir);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
export function wireStrapiClient(cwd, clientDir, schemas) {
|
|
79
|
+
const changes = [];
|
|
80
|
+
const warnings = [];
|
|
81
|
+
if (!fs.existsSync(clientDir)) {
|
|
82
|
+
return { changes, warnings: [`CMS client directory not found: ${path.relative(cwd, clientDir)}`], wired: false };
|
|
83
|
+
}
|
|
84
|
+
const modulePath = path.join(clientDir, 'fimo-strapi.ts');
|
|
85
|
+
fs.writeFileSync(modulePath, renderFimoStrapiModule(schemas));
|
|
86
|
+
changes.push(`generated ${path.relative(cwd, modulePath)}`);
|
|
87
|
+
let swapped = 0;
|
|
88
|
+
for (const file of sourceFiles(clientDir)) {
|
|
89
|
+
if (path.resolve(file) === path.resolve(modulePath)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
93
|
+
const spans = bareFetchCallees(file, code);
|
|
94
|
+
if (spans.length === 0) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let next = applyReplacements(code, spans.map((span) => ({ ...span, text: 'fimoStrapiFetch' })));
|
|
98
|
+
next = `import { fimoStrapiFetch } from '${relativeImport(file, modulePath)}';\n${next}`;
|
|
99
|
+
fs.writeFileSync(file, next);
|
|
100
|
+
swapped += spans.length;
|
|
101
|
+
changes.push(`rewired ${path.relative(cwd, file)}: ${spans.length} fetch call(s) → fimoStrapiFetch`);
|
|
102
|
+
}
|
|
103
|
+
if (swapped === 0) {
|
|
104
|
+
warnings.push(`No bare fetch() call found under ${path.relative(cwd, clientDir)} — wire fimoStrapiFetch into the client manually.`);
|
|
105
|
+
}
|
|
106
|
+
return { changes, warnings, wired: swapped > 0 };
|
|
107
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CmsCollectionSchema } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Machine-wiring for the native `fimo/strapi` read adapter. From the scaffolded
|
|
4
|
+
* schemas it derives the Strapi-path → Fimo-type map (+ the singletons) and
|
|
5
|
+
* emits a tiny repo module that instantiates the Fimo-backed fetch. The
|
|
6
|
+
* migration then swaps the CMS client's `fetch` for `fimoStrapiFetch` — after
|
|
7
|
+
* which the whole site reads scaffolded content from Fimo, deterministically.
|
|
8
|
+
*/
|
|
9
|
+
export interface StrapiWiring {
|
|
10
|
+
/** Strapi REST path segment (pluralName) → Fimo type name. */
|
|
11
|
+
types: Record<string, string>;
|
|
12
|
+
/** Fimo type names that are single types. */
|
|
13
|
+
singletons: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function strapiWiring(schemas: CmsCollectionSchema[]): StrapiWiring;
|
|
16
|
+
/** Emit the repo module (`fimo-strapi.ts`) that wires the fetch to Fimo. */
|
|
17
|
+
export declare function renderFimoStrapiModule(schemas: CmsCollectionSchema[]): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { computeFieldShapes } from './shapes.js';
|
|
2
|
+
export function strapiWiring(schemas) {
|
|
3
|
+
const types = {};
|
|
4
|
+
const singletons = [];
|
|
5
|
+
for (const schema of schemas) {
|
|
6
|
+
const pluralName = schema.sourcePath.replace(/^\/+/, '');
|
|
7
|
+
if (pluralName) {
|
|
8
|
+
types[pluralName] = schema.name;
|
|
9
|
+
}
|
|
10
|
+
if (schema.isSingleton) {
|
|
11
|
+
singletons.push(schema.name);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { types, singletons };
|
|
15
|
+
}
|
|
16
|
+
/** Emit the repo module (`fimo-strapi.ts`) that wires the fetch to Fimo. */
|
|
17
|
+
export function renderFimoStrapiModule(schemas) {
|
|
18
|
+
const { types, singletons } = strapiWiring(schemas);
|
|
19
|
+
const typeEntries = Object.entries(types)
|
|
20
|
+
.map(([pluralName, typeName]) => ` '${pluralName}': '${typeName}',`)
|
|
21
|
+
.join('\n');
|
|
22
|
+
const fieldShapes = JSON.stringify(computeFieldShapes(schemas), null, 2);
|
|
23
|
+
return `// AUTO-GENERATED by \`fimo migrate --scaffold-cms\`. Serves this site's Strapi
|
|
24
|
+
// content reads from Fimo (scaffolded example content) through fimo/strapi.
|
|
25
|
+
// The migration points the CMS client's fetch at \`fimoStrapiFetch\`; reads for
|
|
26
|
+
// scaffolded types resolve from Fimo, everything else falls through to Strapi.
|
|
27
|
+
import { createFimoStrapiFetch } from 'fimo/strapi';
|
|
28
|
+
|
|
29
|
+
// Strapi REST path (pluralName) -> Fimo type name.
|
|
30
|
+
const TYPES: Record<string, string> = {
|
|
31
|
+
${typeEntries}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SINGLETONS = [${singletons.map((name) => `'${name}'`).join(', ')}];
|
|
35
|
+
|
|
36
|
+
// Structural field shapes (single components read as objects; block items and
|
|
37
|
+
// dynamic zones reshape into the Strapi shape the components expect).
|
|
38
|
+
const FIELD_SHAPES = ${fieldShapes};
|
|
39
|
+
|
|
40
|
+
export const fimoStrapiFetch = createFimoStrapiFetch({
|
|
41
|
+
types: TYPES,
|
|
42
|
+
singletons: SINGLETONS,
|
|
43
|
+
fieldShapes: FIELD_SHAPES,
|
|
44
|
+
});
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS-connector contract. A connector recognizes an external headless CMS
|
|
3
|
+
* behind a wrapped frontend and reads its content-type definitions into the
|
|
4
|
+
* neutral Fimo schema shape — WITHOUT connecting to the CMS at runtime.
|
|
5
|
+
*
|
|
6
|
+
* The connector seam mirrors the framework-adapter contract: a CMS's name and
|
|
7
|
+
* quirks live ONLY under `scripts/cms/<cms>/`. Everything above (the registry,
|
|
8
|
+
* the example-content generator, the orchestrator wiring) is CMS-neutral. A
|
|
9
|
+
* CMS name appearing in neutral code is a review finding.
|
|
10
|
+
*/
|
|
11
|
+
/** Scalar Fimo field types (see `FieldTypeSchema` in api-contracts). */
|
|
12
|
+
export type FimoScalarType = 'string' | 'richtext' | 'media' | 'number' | 'boolean' | 'date' | 'json';
|
|
13
|
+
export interface FimoScalarField {
|
|
14
|
+
readonly type: FimoScalarType;
|
|
15
|
+
/** Default applied when an entry omits the field (e.g. a baked block discriminator). */
|
|
16
|
+
readonly default?: unknown;
|
|
17
|
+
/** False hides the field from Studio editing (internal discriminators like `__component`). */
|
|
18
|
+
readonly editable?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FimoReferenceField {
|
|
21
|
+
readonly type: 'reference';
|
|
22
|
+
/** Single-target reference (Fimo type name). */
|
|
23
|
+
readonly target?: string;
|
|
24
|
+
/** Poly-target reference (Fimo type names). */
|
|
25
|
+
readonly targets?: string[];
|
|
26
|
+
/** True for to-many relations. */
|
|
27
|
+
readonly many?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** One variant a `blocks` item may take — the Fimo shape of a source CMS
|
|
30
|
+
* component. `uid` becomes the served block's `_type` (PascalCase, matches
|
|
31
|
+
* `CONTENT_TYPE_NAME_PATTERN`). */
|
|
32
|
+
export interface FimoBlockVariant {
|
|
33
|
+
readonly uid: string;
|
|
34
|
+
readonly label: string;
|
|
35
|
+
readonly fields: Record<string, FimoFieldDef>;
|
|
36
|
+
}
|
|
37
|
+
/** A blocks field's shape: one fixed shape (`fields`, a single/repeatable
|
|
38
|
+
* component) or a discriminated union of variants (`oneOf`, a dynamic zone). */
|
|
39
|
+
export type FimoBlocksSchema = {
|
|
40
|
+
readonly fields: Record<string, FimoFieldDef>;
|
|
41
|
+
} | {
|
|
42
|
+
readonly oneOf: FimoBlockVariant[];
|
|
43
|
+
};
|
|
44
|
+
/** Ordered, Studio-editable structured content — the Fimo representation of a
|
|
45
|
+
* CMS component (fixed) or dynamic zone (`oneOf`). The stored value is always
|
|
46
|
+
* an array of `{ _key, [_type], ...fields }` items. */
|
|
47
|
+
export interface FimoBlocksField {
|
|
48
|
+
readonly type: 'blocks';
|
|
49
|
+
readonly schema: FimoBlocksSchema;
|
|
50
|
+
readonly min?: number;
|
|
51
|
+
readonly max?: number;
|
|
52
|
+
}
|
|
53
|
+
export type FimoFieldDef = FimoScalarField | FimoReferenceField | FimoBlocksField;
|
|
54
|
+
/**
|
|
55
|
+
* One CMS content type mapped to the Fimo schema shape. `fields` is the exact
|
|
56
|
+
* pushable `contentType.fields` payload; `name`/`label`/`isSingleton` complete
|
|
57
|
+
* the `api.createSchema` call.
|
|
58
|
+
*/
|
|
59
|
+
export interface CmsCollectionSchema {
|
|
60
|
+
/** Fimo type name (PascalCase — deterministic from the CMS singular name). */
|
|
61
|
+
readonly name: string;
|
|
62
|
+
/** Human display label. */
|
|
63
|
+
readonly label: string;
|
|
64
|
+
/** A single-document type (e.g. a Strapi singleType) → true; a collection → false. */
|
|
65
|
+
readonly isSingleton: boolean;
|
|
66
|
+
/** The CMS marks this type as localized → seed per configured locale. */
|
|
67
|
+
readonly localized: boolean;
|
|
68
|
+
readonly fields: Record<string, FimoFieldDef>;
|
|
69
|
+
/** Pre-computed example values for fields the generic generator can't shape
|
|
70
|
+
* from the Fimo field type alone (dynamic zones must be arrays, components
|
|
71
|
+
* objects). Keyed by field name; the generator uses these verbatim. */
|
|
72
|
+
readonly examples?: Record<string, unknown>;
|
|
73
|
+
/** Source CMS uid for provenance (e.g. `api::blog-post.blog-post`). */
|
|
74
|
+
readonly sourceUid: string;
|
|
75
|
+
/** Source REST path (e.g. `/blog-posts`) — informational for later rewire. */
|
|
76
|
+
readonly sourcePath: string;
|
|
77
|
+
}
|
|
78
|
+
/** What a connector reports about a detected CMS. */
|
|
79
|
+
export interface CmsDetection {
|
|
80
|
+
/** Connector name, capitalized for the data-source prompt (e.g. `Strapi`). */
|
|
81
|
+
readonly cms: string;
|
|
82
|
+
/** Host label recorded in the data-source decision. */
|
|
83
|
+
readonly host: string;
|
|
84
|
+
/** Absolute path to the CMS app root when co-located (monorepo), else null. */
|
|
85
|
+
readonly appDir: string | null;
|
|
86
|
+
/** Absolute path to the frontend's CMS client module dir (where the fetch
|
|
87
|
+
* lives), for machine-wiring the reads to Fimo. Null if not found. */
|
|
88
|
+
readonly clientDir: string | null;
|
|
89
|
+
/** Repo-relative files/signals that evidenced the detection. */
|
|
90
|
+
readonly evidence: string[];
|
|
91
|
+
/** Locales configured in the CMS, when discoverable (may be empty). */
|
|
92
|
+
readonly locales: string[];
|
|
93
|
+
}
|
|
94
|
+
export interface CmsConnector {
|
|
95
|
+
readonly detection: CmsDetection;
|
|
96
|
+
/**
|
|
97
|
+
* Read every content-type definition into the Fimo schema shape. Returns an
|
|
98
|
+
* empty array when the schemas are not locally available (decoupled CMS with
|
|
99
|
+
* no co-located source) — detection can still surface the source for a
|
|
100
|
+
* keep-live decision even when schema import is impossible.
|
|
101
|
+
*/
|
|
102
|
+
readSchemas(): CmsCollectionSchema[];
|
|
103
|
+
/**
|
|
104
|
+
* Emit a generic list-read helper (as source text) the agent wires into the
|
|
105
|
+
* site's LIST views so they render the scaffolded example content from Fimo.
|
|
106
|
+
* Returns null when the CMS has no such helper. `serverImport` is the
|
|
107
|
+
* framework's Fimo server subpath (e.g. `fimo/next/server`) the helper imports
|
|
108
|
+
* `fetchEntries` from.
|
|
109
|
+
*/
|
|
110
|
+
renderListSource?(schemas: CmsCollectionSchema[], serverImport: string): string | null;
|
|
111
|
+
/**
|
|
112
|
+
* Machine-wire the frontend's CMS reads to Fimo: emit the adapter module and
|
|
113
|
+
* swap the client's fetch, so the whole site reads scaffolded content from
|
|
114
|
+
* Fimo. Deterministic — no agent step.
|
|
115
|
+
*/
|
|
116
|
+
wireClient?(cwd: string, schemas: CmsCollectionSchema[]): CmsWireResult;
|
|
117
|
+
/**
|
|
118
|
+
* Enumerate the frontend's remaining couplings to this CMS — the unwire
|
|
119
|
+
* worklist and the cutover gate (`clean`). Keys on actual CMS references, not
|
|
120
|
+
* filenames, so a client module rewritten to call Fimo reads as clean.
|
|
121
|
+
* Returns undefined for a connector without touchpoint detection.
|
|
122
|
+
*/
|
|
123
|
+
detectTouchpoints?(cwd: string): CmsTouchpoints;
|
|
124
|
+
}
|
|
125
|
+
export interface CmsWireResult {
|
|
126
|
+
changes: string[];
|
|
127
|
+
warnings: string[];
|
|
128
|
+
wired: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface CmsTouchpointSite {
|
|
131
|
+
file: string;
|
|
132
|
+
line: number;
|
|
133
|
+
reason: string;
|
|
134
|
+
snippet: string;
|
|
135
|
+
}
|
|
136
|
+
export interface CmsTouchpoints {
|
|
137
|
+
/** SDK/client/type packages still declared in the frontend's package.json. */
|
|
138
|
+
dependencies: string[];
|
|
139
|
+
/** Source lines that still reach the CMS at runtime. */
|
|
140
|
+
callSites: CmsTouchpointSite[];
|
|
141
|
+
/** CMS base-URL / token env keys still present in dotenv files. */
|
|
142
|
+
envVars: string[];
|
|
143
|
+
/** No runtime coupling remains — the cutover is complete. */
|
|
144
|
+
clean: boolean;
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS-connector contract. A connector recognizes an external headless CMS
|
|
3
|
+
* behind a wrapped frontend and reads its content-type definitions into the
|
|
4
|
+
* neutral Fimo schema shape — WITHOUT connecting to the CMS at runtime.
|
|
5
|
+
*
|
|
6
|
+
* The connector seam mirrors the framework-adapter contract: a CMS's name and
|
|
7
|
+
* quirks live ONLY under `scripts/cms/<cms>/`. Everything above (the registry,
|
|
8
|
+
* the example-content generator, the orchestrator wiring) is CMS-neutral. A
|
|
9
|
+
* CMS name appearing in neutral code is a review finding.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type DetectedFramework = 'next' | 'react-router' | 'remix' | 'astro' | 'nuxt' | 'sveltekit' | 'vue' | 'vite';
|
|
2
|
+
export interface FrameworkDetection {
|
|
3
|
+
framework: DetectedFramework | null;
|
|
4
|
+
/** For Next.js: the app-router directory (`app` or `src/app`) when present. */
|
|
5
|
+
appDir?: string;
|
|
6
|
+
/** For Next.js: the pages-router directory (`pages` or `src/pages`) when present. */
|
|
7
|
+
pagesDir?: string;
|
|
8
|
+
/** A recognized framework that wrap migration does not support yet. */
|
|
9
|
+
unsupported?: string;
|
|
10
|
+
/** For bare Vite: the project depends on react, so .jsx/.tsx is React JSX. */
|
|
11
|
+
react?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detect the framework of an existing site from its package.json and layout.
|
|
15
|
+
* Used by `fimo init` (adopt-existing-site) and the wrap migration to decide
|
|
16
|
+
* how routes are discovered and which env flavor to write.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectFramework(cwd: string): FrameworkDetection;
|
|
19
|
+
/** A frontend app found inside a workspace, adoptable by `fimo init`. */
|
|
20
|
+
export interface AdoptableApp {
|
|
21
|
+
/** Absolute directory of the app. */
|
|
22
|
+
dir: string;
|
|
23
|
+
/** Directory relative to the scan root (for display + as the init dirArg). */
|
|
24
|
+
relDir: string;
|
|
25
|
+
detection: FrameworkDetection;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find frontend apps to adopt anywhere inside a directory tree. Most real
|
|
29
|
+
* sites are monorepos where the framework app is NOT at the root and NOT
|
|
30
|
+
* necessarily under `apps/` (`frontend/`, `sites/web`, `packages/marketing`,
|
|
31
|
+
* …) — `detectFramework(root)` returns null and `fimo init` would wrongly
|
|
32
|
+
* scaffold a new project. This walks the tree up to a bounded depth, running
|
|
33
|
+
* `detectFramework` on every directory that has a package.json and pruning
|
|
34
|
+
* `node_modules`/build output, so it finds the app wherever it lives without
|
|
35
|
+
* relying on a naming convention or a workspace manifest. The root itself is
|
|
36
|
+
* excluded (the caller checks it directly); descent stops at the first
|
|
37
|
+
* framework match in a subtree (an app's own subdirs are not separate apps).
|
|
38
|
+
*/
|
|
39
|
+
export declare function findAdoptableApps(rootDir: string): AdoptableApp[];
|