fimo 0.12.0-staging.1 → 0.12.0-staging.3
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/skills/fimo/SKILL.md +7 -5
- package/assets/skills/fimo-cli/SKILL.md +8 -16
- package/assets/skills/fimo-cli/references/agent-setup.md +13 -13
- package/assets/skills/fimo-cli/references/assets.md +2 -2
- package/assets/skills/fimo-cli/references/content.md +10 -10
- package/assets/skills/fimo-cli/references/forms.md +6 -6
- package/assets/skills/fimo-cli/references/init.md +1 -1
- package/assets/skills/fimo-cli/references/labels.md +3 -3
- package/assets/skills/fimo-cli/references/locales.md +1 -1
- package/assets/skills/fimo-code/SKILL.md +2 -2
- package/assets/skills/fimo-studio/SKILL.md +3 -1
- package/assets/skills/fimo-studio/references/ai-chat.md +1 -1
- package/assets/skills/fimo-studio/references/command-palette.md +31 -0
- package/dist/build/vite/plugins/content-sync.js +1 -1
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +167 -58
- package/dist/runtime/labels/core.d.ts +18 -0
- package/dist/runtime/labels/core.js +15 -0
- package/dist/runtime/labels/fetch.d.ts +7 -0
- package/dist/runtime/labels/fetch.js +41 -0
- package/dist/runtime/next/client.d.ts +11 -3
- package/dist/runtime/next/client.js +9 -2
- package/dist/runtime/next/facade.d.ts +8 -0
- package/dist/runtime/next/facade.js +34 -0
- package/dist/runtime/next/labels.d.ts +7 -0
- package/dist/runtime/next/labels.js +9 -0
- package/dist/runtime/next/{Text.d.ts → primitives/Text.d.ts} +1 -1
- package/dist/runtime/next/{Text.js → primitives/Text.js} +1 -1
- package/dist/runtime/next/primitives/server.d.ts +9 -0
- package/dist/runtime/next/primitives/server.js +46 -0
- package/dist/runtime/next/provider.d.ts +5 -2
- package/dist/runtime/next/provider.js +18 -5
- package/dist/runtime/next/server.d.ts +8 -1
- package/dist/runtime/next/server.js +5 -1
- package/dist/runtime/primitives/index.d.ts +10 -10
- package/dist/runtime/primitives/index.js +10 -10
- package/dist/runtime/primitives/lib/image-optimization.d.ts +10 -1
- package/dist/runtime/primitives/lib/image-optimization.js +12 -12
- package/dist/runtime/primitives/lib/index.d.ts +0 -1
- package/dist/runtime/primitives/lib/index.js +0 -1
- package/dist/runtime/react/primitives/contracts.d.ts +56 -0
- package/dist/runtime/react/primitives/contracts.js +1 -0
- package/dist/runtime/react/primitives/editable/Boolean.d.ts +5 -0
- package/dist/runtime/react/primitives/editable/Boolean.js +11 -0
- package/dist/runtime/react/primitives/editable/Date.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/Date.js +12 -0
- package/dist/runtime/react/primitives/editable/Image.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/Image.js +10 -0
- package/dist/runtime/react/primitives/editable/Json.d.ts +5 -0
- package/dist/runtime/react/primitives/editable/Json.js +11 -0
- package/dist/runtime/react/primitives/editable/RichText.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/RichText.js +11 -0
- package/dist/runtime/{primitives/components → react/primitives/editable}/Text.d.ts +3 -6
- package/dist/runtime/{primitives/components → react/primitives/editable}/Text.js +2 -1
- package/dist/runtime/react/primitives/editable/Video.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/Video.js +10 -0
- package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +11 -0
- package/dist/runtime/react/primitives/renderers/DateRenderer.js +14 -0
- package/dist/runtime/react/primitives/renderers/ImageRenderer.d.ts +8 -0
- package/dist/runtime/react/primitives/renderers/ImageRenderer.js +50 -0
- package/dist/runtime/react/primitives/renderers/RichTextRenderer.d.ts +11 -0
- package/dist/runtime/{primitives/components/RichText.js → react/primitives/renderers/RichTextRenderer.js} +4 -6
- package/dist/runtime/{primitives/components → react/primitives/renderers}/StaticImage.d.ts +2 -11
- package/dist/runtime/{primitives/components → react/primitives/renderers}/StaticImage.js +5 -3
- package/dist/runtime/react/primitives/renderers/VideoRenderer.d.ts +8 -0
- package/dist/runtime/react/primitives/renderers/VideoRenderer.js +10 -0
- package/dist/runtime/{primitives/lib → react}/useFimoLiveValue.d.ts +3 -3
- package/dist/runtime/{primitives/lib → react}/useFimoLiveValue.js +3 -3
- package/dist/runtime/vite/primitives/Images.d.ts +4 -0
- package/dist/runtime/vite/primitives/Images.js +12 -0
- package/dist/runtime/{primitives → vite}/translations.d.ts +1 -1
- package/dist/runtime/{primitives → vite}/translations.js +1 -1
- package/dist/scripts/sync-schemas.d.ts +2 -1
- package/dist/scripts/sync-schemas.js +119 -67
- package/package.json +2 -1
- package/templates/react-router/AGENTS.md +4 -2
- package/templates/react-router/package.json +1 -1
- package/dist/runtime/primitives/components/Boolean.d.ts +0 -29
- package/dist/runtime/primitives/components/Boolean.js +0 -33
- package/dist/runtime/primitives/components/Date.d.ts +0 -30
- package/dist/runtime/primitives/components/Date.js +0 -43
- package/dist/runtime/primitives/components/Image.d.ts +0 -72
- package/dist/runtime/primitives/components/Image.js +0 -108
- package/dist/runtime/primitives/components/Json.d.ts +0 -25
- package/dist/runtime/primitives/components/Json.js +0 -29
- package/dist/runtime/primitives/components/RichText.d.ts +0 -9
- package/dist/runtime/primitives/components/Video.d.ts +0 -31
- package/dist/runtime/primitives/components/Video.js +0 -34
- package/dist/runtime/primitives/components/index.d.ts +0 -6
- package/dist/runtime/primitives/components/index.js +0 -5
|
@@ -5,18 +5,22 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
5
5
|
* hook files generated from the hooks template.
|
|
6
6
|
*
|
|
7
7
|
* - Scans `<cwd>/src/schemas/*.json`.
|
|
8
|
-
* - Renders
|
|
8
|
+
* - Renders shared types plus either the React facade or split Next server/client facades.
|
|
9
9
|
* - Creates/updates `<Pascal>.types.ts` and `<Pascal>.ts`, plus `<Pascal>.client.ts` for Next; deletes orphans.
|
|
10
10
|
*/
|
|
11
|
-
import { pascalCase } from 'change-case';
|
|
11
|
+
import { camelCase, pascalCase } from 'change-case';
|
|
12
12
|
import { Eta } from 'eta';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { assetPath } from './utils/assets.js';
|
|
15
15
|
const SCHEMAS_REL = 'src/schemas';
|
|
16
|
+
const GENERATED_REGISTRY_MARKER = '// AUTO-GENERATED by `fimo` from src/schemas/*.json - DO NOT EDIT.';
|
|
17
|
+
// Before 0.7, generated schema clients contained their own runtime. Keep using
|
|
18
|
+
// those projects' bundled templates instead of generating imports they cannot satisfy.
|
|
16
19
|
const MINIMUM_THIN_CONTENT_CLIENT_RUNTIME_VERSION = '0.7.0';
|
|
17
20
|
const CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND = 'npm install fimo@latest';
|
|
18
21
|
const SCHEMA_DEFINITION_SCHEMA = z.object({
|
|
19
22
|
uid: z.string().min(1),
|
|
23
|
+
name: z.string().min(1).optional(),
|
|
20
24
|
isSingleton: z.boolean().optional(),
|
|
21
25
|
i18n: z.object({ enabled: z.boolean().optional() }).optional(),
|
|
22
26
|
fields: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -27,6 +31,7 @@ function getSchemaFiles(schemasDir) {
|
|
|
27
31
|
}
|
|
28
32
|
return readdirSync(schemasDir)
|
|
29
33
|
.filter((f) => f.endsWith('.json'))
|
|
34
|
+
.sort()
|
|
30
35
|
.map((f) => join(schemasDir, f));
|
|
31
36
|
}
|
|
32
37
|
function getGeneratedFiles(schemasDir) {
|
|
@@ -60,6 +65,16 @@ function isVersionAtLeast(version, minimum) {
|
|
|
60
65
|
}
|
|
61
66
|
return !parsed.prerelease || parsedMinimum.prerelease;
|
|
62
67
|
}
|
|
68
|
+
function packageExportsSubpath(packageJson, subpath) {
|
|
69
|
+
if (!packageJson || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const exports = packageJson.exports;
|
|
73
|
+
return Boolean(exports &&
|
|
74
|
+
typeof exports === 'object' &&
|
|
75
|
+
!Array.isArray(exports) &&
|
|
76
|
+
Object.prototype.hasOwnProperty.call(exports, subpath));
|
|
77
|
+
}
|
|
63
78
|
function resolveProjectRuntime(cwd) {
|
|
64
79
|
const packagePath = join(cwd, 'node_modules', 'fimo', 'package.json');
|
|
65
80
|
if (!existsSync(packagePath)) {
|
|
@@ -67,45 +82,29 @@ function resolveProjectRuntime(cwd) {
|
|
|
67
82
|
`Run \`${CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND}\` in the project, then run \`npx fimo@latest validate\` again. ` +
|
|
68
83
|
'No generated files were changed.');
|
|
69
84
|
}
|
|
70
|
-
let
|
|
85
|
+
let packageJson;
|
|
71
86
|
try {
|
|
72
|
-
|
|
87
|
+
packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
73
88
|
}
|
|
74
89
|
catch {
|
|
75
|
-
|
|
90
|
+
packageJson = null;
|
|
76
91
|
}
|
|
92
|
+
const version = packageJson && typeof packageJson === 'object' && !Array.isArray(packageJson)
|
|
93
|
+
? packageJson.version
|
|
94
|
+
: null;
|
|
77
95
|
if (typeof version !== 'string' || !parseVersion(version)) {
|
|
78
96
|
throw new Error(`Cannot generate schema clients with project-local fimo@${typeof version === 'string' ? version : 'unknown'}. ` +
|
|
79
97
|
`Run \`${CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND}\` in the project, then run \`npx fimo@latest validate\` again. ` +
|
|
80
98
|
'No generated files were changed.');
|
|
81
99
|
}
|
|
82
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
packageRoot: dirname(packagePath),
|
|
102
|
+
version,
|
|
103
|
+
providesNextRuntime: packageExportsSubpath(packageJson, './next') && packageExportsSubpath(packageJson, './next/client'),
|
|
104
|
+
};
|
|
83
105
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
if (!existsSync(packagePath)) {
|
|
87
|
-
return 'fimo/ui';
|
|
88
|
-
}
|
|
89
|
-
let packageJson;
|
|
90
|
-
try {
|
|
91
|
-
packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return 'fimo/ui';
|
|
95
|
-
}
|
|
96
|
-
if (!packageJson || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
|
|
97
|
-
return 'fimo/ui';
|
|
98
|
-
}
|
|
99
|
-
const manifest = packageJson;
|
|
100
|
-
const dependencyGroups = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
101
|
-
const usesNext = dependencyGroups.some((group) => {
|
|
102
|
-
const dependencies = manifest[group];
|
|
103
|
-
return Boolean(dependencies &&
|
|
104
|
-
typeof dependencies === 'object' &&
|
|
105
|
-
!Array.isArray(dependencies) &&
|
|
106
|
-
typeof dependencies.next === 'string');
|
|
107
|
-
});
|
|
108
|
-
return usesNext ? 'fimo/next' : 'fimo/ui';
|
|
106
|
+
function contentRuntimeImportFor(framework) {
|
|
107
|
+
return framework === 'nextjs' ? 'fimo/next' : 'fimo/ui';
|
|
109
108
|
}
|
|
110
109
|
function templatePath(runtime, fileName) {
|
|
111
110
|
if (runtime && !isVersionAtLeast(runtime.version, MINIMUM_THIN_CONTENT_CLIENT_RUNTIME_VERSION)) {
|
|
@@ -113,10 +112,56 @@ function templatePath(runtime, fileName) {
|
|
|
113
112
|
}
|
|
114
113
|
return assetPath('content-templates', fileName);
|
|
115
114
|
}
|
|
115
|
+
function resolveSchemaGenerationTarget(runtimeImport, runtime) {
|
|
116
|
+
if (runtimeImport === 'fimo/ui') {
|
|
117
|
+
return 'react';
|
|
118
|
+
}
|
|
119
|
+
if (!runtime.providesNextRuntime) {
|
|
120
|
+
throw new Error(`Cannot generate schema clients because project-local fimo@${runtime.version} does not provide the ` +
|
|
121
|
+
'fimo/next and fimo/next/client exports required by this Next.js project. ' +
|
|
122
|
+
`Run \`${CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND}\` in the project, then run \`npx fimo@latest validate\` again. ` +
|
|
123
|
+
'No generated files were changed.');
|
|
124
|
+
}
|
|
125
|
+
return 'next';
|
|
126
|
+
}
|
|
127
|
+
function templatesForTarget(target, runtime, runtimeImport) {
|
|
128
|
+
if (target === 'next') {
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
suffix: '.types.ts',
|
|
132
|
+
path: templatePath(runtime, 'types-template.eta'),
|
|
133
|
+
runtimeImport: 'fimo/next',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
suffix: '.ts',
|
|
137
|
+
path: templatePath(runtime, 'next-server-template.eta'),
|
|
138
|
+
runtimeImport: 'fimo/next',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
suffix: '.client.ts',
|
|
142
|
+
path: templatePath(runtime, 'next-client-template.eta'),
|
|
143
|
+
runtimeImport: 'fimo/next/client',
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
suffix: '.types.ts',
|
|
150
|
+
path: templatePath(runtime, 'types-template.eta'),
|
|
151
|
+
runtimeImport,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
suffix: '.ts',
|
|
155
|
+
path: templatePath(runtime, 'hooks-template.eta'),
|
|
156
|
+
runtimeImport,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
116
160
|
function loadSchemas(schemaFiles) {
|
|
117
161
|
const errors = [];
|
|
118
162
|
const schemas = [];
|
|
119
163
|
const outputOwners = new Map();
|
|
164
|
+
const registryOwners = new Map();
|
|
120
165
|
for (const path of schemaFiles) {
|
|
121
166
|
const fileUid = basename(path, '.json');
|
|
122
167
|
const pascal = pascalCase(fileUid);
|
|
@@ -156,13 +201,33 @@ function loadSchemas(schemaFiles) {
|
|
|
156
201
|
errors.push(`${basename(path)} has an invalid schema shape: ${details}`);
|
|
157
202
|
continue;
|
|
158
203
|
}
|
|
159
|
-
|
|
204
|
+
const registryKey = camelCase(validation.data.name ?? validation.data.uid);
|
|
205
|
+
const existingRegistryOwner = registryOwners.get(registryKey);
|
|
206
|
+
if (existingRegistryOwner && existingRegistryOwner !== fileUid) {
|
|
207
|
+
errors.push(`${basename(path)} and ${existingRegistryOwner}.json both generate the content registry key "${registryKey}". Rename one schema display name to keep generated registry keys unique.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
registryOwners.set(registryKey, fileUid);
|
|
211
|
+
schemas.push({ pascal, registryKey, schema: validation.data });
|
|
160
212
|
}
|
|
161
213
|
if (errors.length > 0) {
|
|
162
214
|
throw new Error(`Cannot generate schema clients:\n- ${errors.join('\n- ')}\nNo generated files were changed.`);
|
|
163
215
|
}
|
|
164
216
|
return schemas;
|
|
165
217
|
}
|
|
218
|
+
function renderContentRegistry(schemas) {
|
|
219
|
+
const imports = schemas.map(({ pascal }) => `import * as ${pascal} from "./${pascal}";`).join('\n');
|
|
220
|
+
const entries = schemas.map(({ pascal, registryKey }) => ` ${registryKey}: ${pascal},`).join('\n');
|
|
221
|
+
return `${GENERATED_REGISTRY_MARKER}
|
|
222
|
+
// Server code imports this registry and passes it to createFimo({ content, locale }).
|
|
223
|
+
|
|
224
|
+
${imports}
|
|
225
|
+
|
|
226
|
+
export const content = {
|
|
227
|
+
${entries}
|
|
228
|
+
} as const;
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
166
231
|
function normalizeFields(fields) {
|
|
167
232
|
return Object.fromEntries(Object.entries(fields).map(([key, value]) => {
|
|
168
233
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -226,7 +291,7 @@ function schemaToModel(schema, localizedByUid) {
|
|
|
226
291
|
referenceImports: referenceImportsFor(schema, fields),
|
|
227
292
|
};
|
|
228
293
|
}
|
|
229
|
-
export function syncSchemas(options
|
|
294
|
+
export function syncSchemas(options) {
|
|
230
295
|
const cwd = options.cwd ?? process.cwd();
|
|
231
296
|
const log = (msg) => {
|
|
232
297
|
if (options.verbose !== false) {
|
|
@@ -234,44 +299,20 @@ export function syncSchemas(options = {}) {
|
|
|
234
299
|
}
|
|
235
300
|
};
|
|
236
301
|
const schemasDir = resolve(cwd, SCHEMAS_REL);
|
|
302
|
+
const registryPath = join(schemasDir, 'index.ts');
|
|
237
303
|
const schemaFiles = getSchemaFiles(schemasDir);
|
|
238
304
|
const schemas = loadSchemas(schemaFiles);
|
|
239
305
|
const generatedFilesOnDisk = getGeneratedFiles(schemasDir);
|
|
240
306
|
const runtime = schemas.length > 0 || generatedFilesOnDisk.length > 0 ? resolveProjectRuntime(cwd) : null;
|
|
241
|
-
const runtimeImport =
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
runtimeImport: 'fimo/next',
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
suffix: '.ts',
|
|
254
|
-
path: templatePath(runtime, 'next-server-template.eta'),
|
|
255
|
-
runtimeImport: 'fimo/next',
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
suffix: '.client.ts',
|
|
259
|
-
path: templatePath(runtime, 'next-client-template.eta'),
|
|
260
|
-
runtimeImport: 'fimo/next/client',
|
|
261
|
-
},
|
|
262
|
-
]
|
|
263
|
-
: [
|
|
264
|
-
{
|
|
265
|
-
suffix: '.types.ts',
|
|
266
|
-
path: templatePath(runtime, 'types-template.eta'),
|
|
267
|
-
runtimeImport,
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
suffix: '.ts',
|
|
271
|
-
path: templatePath(runtime, 'hooks-template.eta'),
|
|
272
|
-
runtimeImport,
|
|
273
|
-
},
|
|
274
|
-
];
|
|
307
|
+
const runtimeImport = contentRuntimeImportFor(options.framework);
|
|
308
|
+
const generationTarget = runtime ? resolveSchemaGenerationTarget(runtimeImport, runtime) : null;
|
|
309
|
+
const needsNextRegistry = generationTarget === 'next' && schemas.length > 0;
|
|
310
|
+
const ownsExistingRegistry = existsSync(registryPath) && readFileSync(registryPath, 'utf-8').startsWith(GENERATED_REGISTRY_MARKER);
|
|
311
|
+
const shouldGenerateRegistry = needsNextRegistry && (!existsSync(registryPath) || ownsExistingRegistry);
|
|
312
|
+
if (needsNextRegistry && !shouldGenerateRegistry) {
|
|
313
|
+
log(`Kept project-owned schema barrel: ${registryPath}`);
|
|
314
|
+
}
|
|
315
|
+
const templates = generationTarget && runtime ? templatesForTarget(generationTarget, runtime, runtimeImport) : [];
|
|
275
316
|
const loadedTemplates = templates.map((template) => ({
|
|
276
317
|
...template,
|
|
277
318
|
source: readFileSync(template.path, 'utf-8'),
|
|
@@ -291,6 +332,10 @@ export function syncSchemas(options = {}) {
|
|
|
291
332
|
generatedFiles.set(tsPath, eta.renderString(template.source, { model, runtimeImport: template.runtimeImport }));
|
|
292
333
|
}
|
|
293
334
|
}
|
|
335
|
+
if (shouldGenerateRegistry) {
|
|
336
|
+
expectedTsFiles.add(registryPath);
|
|
337
|
+
generatedFiles.set(registryPath, renderContentRegistry(schemas));
|
|
338
|
+
}
|
|
294
339
|
for (const [tsPath, content] of generatedFiles) {
|
|
295
340
|
const dir = dirname(tsPath);
|
|
296
341
|
if (!existsSync(dir)) {
|
|
@@ -320,6 +365,13 @@ export function syncSchemas(options = {}) {
|
|
|
320
365
|
}
|
|
321
366
|
}
|
|
322
367
|
}
|
|
368
|
+
if (!shouldGenerateRegistry &&
|
|
369
|
+
existsSync(registryPath) &&
|
|
370
|
+
readFileSync(registryPath, 'utf-8').startsWith(GENERATED_REGISTRY_MARKER)) {
|
|
371
|
+
unlinkSync(registryPath);
|
|
372
|
+
deleted++;
|
|
373
|
+
log(`Deleted orphaned: ${registryPath}`);
|
|
374
|
+
}
|
|
323
375
|
if ((created > 0 || updated > 0 || deleted > 0) && options.verbose !== false) {
|
|
324
376
|
log(`Sync schemas: ${created} created, ${updated} updated, ${deleted} deleted`);
|
|
325
377
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.12.0-staging.
|
|
3
|
+
"version": "0.12.0-staging.3",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"@fimo/agents-bundle": "workspace:*",
|
|
94
94
|
"@fimo/api-contracts": "workspace:*",
|
|
95
95
|
"@fimo/cli-kit": "workspace:*",
|
|
96
|
+
"@fimo/framework-detect": "workspace:*",
|
|
96
97
|
"@fimo/mcp": "workspace:*",
|
|
97
98
|
"@fimo/tsconfig": "workspace:*",
|
|
98
99
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
@@ -4,10 +4,12 @@ This is a Fimo project — a content + media + forms + analytics platform for Re
|
|
|
4
4
|
dashboard. Fimo owns everything after the build: content, editing, environments, and hosting stay
|
|
5
5
|
editable by the whole team.
|
|
6
6
|
|
|
7
|
+
When asked to create or manage an agent in this project, use Fimo's agent workflow.
|
|
8
|
+
|
|
7
9
|
## Load the Fimo skills before doing Fimo work
|
|
8
10
|
|
|
9
|
-
Fimo ships its skills through its CLI
|
|
10
|
-
|
|
11
|
+
Fimo ships its skills through its CLI. Before writing schema-driven code, running CLI/deploy workflows,
|
|
12
|
+
or migrating content, load the relevant skill:
|
|
11
13
|
|
|
12
14
|
```bash
|
|
13
15
|
npx fimo@latest skills list # what's available
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type ComponentPropsWithoutRef, type ElementType, type ReactNode } from 'react';
|
|
2
|
-
import { type FimoBoolean } from '../lib/index.js';
|
|
3
|
-
type BooleanProps<T extends ElementType = 'span'> = {
|
|
4
|
-
value: FimoBoolean;
|
|
5
|
-
as?: T;
|
|
6
|
-
children?: (value: boolean) => ReactNode;
|
|
7
|
-
} & Omit<ComponentPropsWithoutRef<T>, 'value' | 'as' | 'children'>;
|
|
8
|
-
/**
|
|
9
|
-
* Boolean component for rendering boolean values with source tracking
|
|
10
|
-
*
|
|
11
|
-
* Use children render function to customize how the boolean is displayed.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* // Default (renders "true" or "false")
|
|
15
|
-
* <Boolean value={product.inStock} />
|
|
16
|
-
*
|
|
17
|
-
* // With render function for custom display
|
|
18
|
-
* <Boolean value={product.inStock}>
|
|
19
|
-
* {(val) => val ? '✓ In Stock' : '✗ Out of Stock'}
|
|
20
|
-
* </Boolean>
|
|
21
|
-
*
|
|
22
|
-
* // Conditional styling
|
|
23
|
-
* <Boolean value={product.featured}>
|
|
24
|
-
* {(val) => val && <span className="badge">Featured</span>}
|
|
25
|
-
* </Boolean>
|
|
26
|
-
*/
|
|
27
|
-
export declare function Boolean<T extends ElementType = 'span'>({ value, as, children, ...props }: BooleanProps<T>): import("react/jsx-runtime").JSX.Element | null;
|
|
28
|
-
export {};
|
|
29
|
-
//# sourceMappingURL=Boolean.d.ts.map
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { FIMO_SOURCE, useFimoLiveValue } from '../lib/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* Boolean component for rendering boolean values with source tracking
|
|
5
|
-
*
|
|
6
|
-
* Use children render function to customize how the boolean is displayed.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* // Default (renders "true" or "false")
|
|
10
|
-
* <Boolean value={product.inStock} />
|
|
11
|
-
*
|
|
12
|
-
* // With render function for custom display
|
|
13
|
-
* <Boolean value={product.inStock}>
|
|
14
|
-
* {(val) => val ? '✓ In Stock' : '✗ Out of Stock'}
|
|
15
|
-
* </Boolean>
|
|
16
|
-
*
|
|
17
|
-
* // Conditional styling
|
|
18
|
-
* <Boolean value={product.featured}>
|
|
19
|
-
* {(val) => val && <span className="badge">Featured</span>}
|
|
20
|
-
* </Boolean>
|
|
21
|
-
*/
|
|
22
|
-
export function Boolean({ value, as, children, ...props }) {
|
|
23
|
-
const Component = as || 'span';
|
|
24
|
-
const { value: liveBool, ref } = useFimoLiveValue(value ? value.valueOf() : false);
|
|
25
|
-
// Defensive: return null if no value
|
|
26
|
-
if (!value) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const source = value[FIMO_SOURCE];
|
|
30
|
-
// Use render function if provided, otherwise display raw boolean
|
|
31
|
-
const content = children ? children(liveBool) : String(liveBool);
|
|
32
|
-
return (_jsx(Component, { ref: ref, ...props, "data-fimo-source": source, children: content }));
|
|
33
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import { type FimoDate, type FimoString } from '../lib/index.js';
|
|
3
|
-
type DateProps = {
|
|
4
|
-
value: FimoDate | FimoString | Date | string;
|
|
5
|
-
children?: (date: Date) => ReactNode;
|
|
6
|
-
className?: string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Date component for rendering dates with source tracking
|
|
10
|
-
*
|
|
11
|
-
* Default format uses browser's locale (e.g., "28 ene 2026" in Spain, "Jan 28, 2026" in US)
|
|
12
|
-
* Use children render function for custom formatting.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* // Default format (locale-aware)
|
|
16
|
-
* <Date value={post.publishedAt} />
|
|
17
|
-
*
|
|
18
|
-
* // Custom format with render function
|
|
19
|
-
* <Date value={post.publishedAt}>
|
|
20
|
-
* {(date) => format(date, 'EEEE, MMMM do yyyy')}
|
|
21
|
-
* </Date>
|
|
22
|
-
*
|
|
23
|
-
* // Relative time
|
|
24
|
-
* <Date value={post.publishedAt}>
|
|
25
|
-
* {(date) => formatDistanceToNow(date, { addSuffix: true })}
|
|
26
|
-
* </Date>
|
|
27
|
-
*/
|
|
28
|
-
export declare function Date({ value, children, className }: DateProps): import("react/jsx-runtime").JSX.Element | null;
|
|
29
|
-
export {};
|
|
30
|
-
//# sourceMappingURL=Date.d.ts.map
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { FIMO_SOURCE, useFimoLiveValue } from '../lib/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* Date component for rendering dates with source tracking
|
|
5
|
-
*
|
|
6
|
-
* Default format uses browser's locale (e.g., "28 ene 2026" in Spain, "Jan 28, 2026" in US)
|
|
7
|
-
* Use children render function for custom formatting.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* // Default format (locale-aware)
|
|
11
|
-
* <Date value={post.publishedAt} />
|
|
12
|
-
*
|
|
13
|
-
* // Custom format with render function
|
|
14
|
-
* <Date value={post.publishedAt}>
|
|
15
|
-
* {(date) => format(date, 'EEEE, MMMM do yyyy')}
|
|
16
|
-
* </Date>
|
|
17
|
-
*
|
|
18
|
-
* // Relative time
|
|
19
|
-
* <Date value={post.publishedAt}>
|
|
20
|
-
* {(date) => formatDistanceToNow(date, { addSuffix: true })}
|
|
21
|
-
* </Date>
|
|
22
|
-
*/
|
|
23
|
-
export function Date({ value, children, className }) {
|
|
24
|
-
const { value: liveValue, ref } = useFimoLiveValue(value ?? null);
|
|
25
|
-
// Defensive: return null if no value
|
|
26
|
-
if (!liveValue) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const source = typeof value === 'object' && FIMO_SOURCE in value ? value[FIMO_SOURCE] : undefined;
|
|
30
|
-
const rawValue = liveValue instanceof String ? liveValue.toString() : liveValue;
|
|
31
|
-
const dateValue = new globalThis.Date(rawValue);
|
|
32
|
-
// Format for datetime attribute (ISO format)
|
|
33
|
-
const isoDate = dateValue.toISOString().split('T')[0];
|
|
34
|
-
// Render content - use children if provided, otherwise locale-aware default
|
|
35
|
-
const content = children
|
|
36
|
-
? children(dateValue)
|
|
37
|
-
: dateValue.toLocaleDateString(undefined, {
|
|
38
|
-
year: 'numeric',
|
|
39
|
-
month: 'short',
|
|
40
|
-
day: 'numeric',
|
|
41
|
-
});
|
|
42
|
-
return (_jsx("time", { ref: ref, dateTime: isoDate, className: className, "data-fimo-source": source, children: content }));
|
|
43
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
-
import { type ImageFit } from '../lib/image-optimization.js';
|
|
3
|
-
import { type FimoMedia } from '../lib/index.js';
|
|
4
|
-
/** Base props for Image, extending native img props with optimization options. */
|
|
5
|
-
type BaseImageProps = Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt' | 'children'> & {
|
|
6
|
-
/** Image quality (1-100). Defaults to 85. */
|
|
7
|
-
quality?: number;
|
|
8
|
-
/** Resize fit mode. Also maps to CSS object-fit. Defaults to 'scale-down'. */
|
|
9
|
-
fit?: ImageFit;
|
|
10
|
-
/** Skip optimization and use original URL. */
|
|
11
|
-
unoptimized?: boolean;
|
|
12
|
-
overrideSrc?: string;
|
|
13
|
-
};
|
|
14
|
-
type ImageProps = BaseImageProps & {
|
|
15
|
-
/** The FimoMedia object from schema data. */
|
|
16
|
-
value: FimoMedia;
|
|
17
|
-
/** Override the alt text from FimoMedia. */
|
|
18
|
-
alt?: string;
|
|
19
|
-
/** Render function for custom markup (bypasses default img rendering). */
|
|
20
|
-
children?: (media: FimoMedia) => ReactNode;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Image component for rendering schema media with optional optimization.
|
|
24
|
-
*
|
|
25
|
-
* Optimization:
|
|
26
|
-
* - Applies only to http(s) URLs on *.fimo.team, *.fimo.ai, and *.fimo.site.
|
|
27
|
-
* - Generates optimized `src` automatically only when at least one sizing hint
|
|
28
|
-
* is provided (`width`, `height`, or `sizes`).
|
|
29
|
-
* - Uses default quality 85 and internal format=auto.
|
|
30
|
-
* - Maps `fit` to CSS object-fit by default.
|
|
31
|
-
*
|
|
32
|
-
* Escape hatch:
|
|
33
|
-
* - Pass `unoptimized` to bypass all URL rewriting.
|
|
34
|
-
*
|
|
35
|
-
* Use children render function for custom rendering (e.g., picture element).
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* <Image value={post.coverImage} />
|
|
39
|
-
* <Image value={post.coverImage} className="rounded-lg" loading="lazy" />
|
|
40
|
-
*
|
|
41
|
-
* // With render function for custom markup
|
|
42
|
-
* <Image value={post.cover}>
|
|
43
|
-
* {(media) => (
|
|
44
|
-
* <img src={media.url} alt={media.alt} />
|
|
45
|
-
* )}
|
|
46
|
-
* </Image>
|
|
47
|
-
*
|
|
48
|
-
* // Fixed-size mode (no `sizes`): uses DPR candidates
|
|
49
|
-
* <Image value={post.coverImage} width={320} />
|
|
50
|
-
* // srcSet:
|
|
51
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=1 1x,
|
|
52
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=2 2x
|
|
53
|
-
* // src:
|
|
54
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=2
|
|
55
|
-
*
|
|
56
|
-
* // Responsive mode (`sizes`): uses width candidates
|
|
57
|
-
* <Image
|
|
58
|
-
* value={post.coverImage}
|
|
59
|
-
* sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
60
|
-
* />
|
|
61
|
-
* // srcSet (example):
|
|
62
|
-
* // ...?w=640&q=85&fit=scale-down&f=auto 640w,
|
|
63
|
-
* // ...?w=750&q=85&fit=scale-down&f=auto 750w,
|
|
64
|
-
* // ...?w=828&q=85&fit=scale-down&f=auto 828w
|
|
65
|
-
*
|
|
66
|
-
* // No `width`/`height`/`sizes`: keep source URL unchanged
|
|
67
|
-
* <Image value={post.coverImage} />
|
|
68
|
-
* // src: original media URL
|
|
69
|
-
*/
|
|
70
|
-
export declare function Image(props: ImageProps): import("react/jsx-runtime").JSX.Element | null;
|
|
71
|
-
export {};
|
|
72
|
-
//# sourceMappingURL=Image.d.ts.map
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { clampQuality, resolveFit, toObjectFit, toPositiveInt, isOptimizableFimoUrl, buildOptimizedUrl, computeImageOptimizationConfig, } from '../lib/image-optimization.js';
|
|
3
|
-
import { FIMO_SOURCE, useFimoLiveValue } from '../lib/index.js';
|
|
4
|
-
/**
|
|
5
|
-
* Image component for rendering schema media with optional optimization.
|
|
6
|
-
*
|
|
7
|
-
* Optimization:
|
|
8
|
-
* - Applies only to http(s) URLs on *.fimo.team, *.fimo.ai, and *.fimo.site.
|
|
9
|
-
* - Generates optimized `src` automatically only when at least one sizing hint
|
|
10
|
-
* is provided (`width`, `height`, or `sizes`).
|
|
11
|
-
* - Uses default quality 85 and internal format=auto.
|
|
12
|
-
* - Maps `fit` to CSS object-fit by default.
|
|
13
|
-
*
|
|
14
|
-
* Escape hatch:
|
|
15
|
-
* - Pass `unoptimized` to bypass all URL rewriting.
|
|
16
|
-
*
|
|
17
|
-
* Use children render function for custom rendering (e.g., picture element).
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* <Image value={post.coverImage} />
|
|
21
|
-
* <Image value={post.coverImage} className="rounded-lg" loading="lazy" />
|
|
22
|
-
*
|
|
23
|
-
* // With render function for custom markup
|
|
24
|
-
* <Image value={post.cover}>
|
|
25
|
-
* {(media) => (
|
|
26
|
-
* <img src={media.url} alt={media.alt} />
|
|
27
|
-
* )}
|
|
28
|
-
* </Image>
|
|
29
|
-
*
|
|
30
|
-
* // Fixed-size mode (no `sizes`): uses DPR candidates
|
|
31
|
-
* <Image value={post.coverImage} width={320} />
|
|
32
|
-
* // srcSet:
|
|
33
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=1 1x,
|
|
34
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=2 2x
|
|
35
|
-
* // src:
|
|
36
|
-
* // ...?w=320&q=85&fit=scale-down&f=auto&dpr=2
|
|
37
|
-
*
|
|
38
|
-
* // Responsive mode (`sizes`): uses width candidates
|
|
39
|
-
* <Image
|
|
40
|
-
* value={post.coverImage}
|
|
41
|
-
* sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
42
|
-
* />
|
|
43
|
-
* // srcSet (example):
|
|
44
|
-
* // ...?w=640&q=85&fit=scale-down&f=auto 640w,
|
|
45
|
-
* // ...?w=750&q=85&fit=scale-down&f=auto 750w,
|
|
46
|
-
* // ...?w=828&q=85&fit=scale-down&f=auto 828w
|
|
47
|
-
*
|
|
48
|
-
* // No `width`/`height`/`sizes`: keep source URL unchanged
|
|
49
|
-
* <Image value={post.coverImage} />
|
|
50
|
-
* // src: original media URL
|
|
51
|
-
*/
|
|
52
|
-
export function Image(props) {
|
|
53
|
-
const { value: media, children, quality, unoptimized = false, fit, width, height, alt: altOverride, overrideSrc, srcSet: srcSetOverride, sizes, style, ...imgProps } = props;
|
|
54
|
-
const { value: liveMedia, ref } = useFimoLiveValue(media ?? null);
|
|
55
|
-
// Defensive: return null if no media
|
|
56
|
-
if (!liveMedia) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const mediaSource = liveMedia[FIMO_SOURCE];
|
|
60
|
-
if (children) {
|
|
61
|
-
return (_jsx("span", { ref: ref, "data-fimo-source": mediaSource, children: children(liveMedia) }));
|
|
62
|
-
}
|
|
63
|
-
const normalizedQuality = clampQuality(quality);
|
|
64
|
-
const normalizedFit = resolveFit(fit);
|
|
65
|
-
const requestedWidth = toPositiveInt(width);
|
|
66
|
-
const requestedHeight = toPositiveInt(height);
|
|
67
|
-
const displayWidth = requestedWidth ?? liveMedia.width;
|
|
68
|
-
const displayHeight = requestedHeight ?? liveMedia.height;
|
|
69
|
-
const originalSrc = liveMedia.url;
|
|
70
|
-
const resolvedAlt = altOverride ?? liveMedia.alt;
|
|
71
|
-
// User-defined sizes attribute
|
|
72
|
-
const hasSizesAttr = typeof sizes === 'string' && sizes.trim().length > 0;
|
|
73
|
-
const hasSizingHint = hasSizesAttr || requestedWidth !== undefined || requestedHeight !== undefined;
|
|
74
|
-
const canOptimizeSource = !unoptimized && isOptimizableFimoUrl(originalSrc);
|
|
75
|
-
const shouldOptimize = canOptimizeSource && hasSizingHint;
|
|
76
|
-
const { generatedSrcSet, transformWidth, transformDpr } = computeImageOptimizationConfig({
|
|
77
|
-
shouldOptimize,
|
|
78
|
-
hasSizesAttr,
|
|
79
|
-
srcSetOverride,
|
|
80
|
-
originalSrc,
|
|
81
|
-
mediaWidth: media.width,
|
|
82
|
-
renderWidth: displayWidth,
|
|
83
|
-
quality: normalizedQuality,
|
|
84
|
-
fit: normalizedFit,
|
|
85
|
-
});
|
|
86
|
-
const finalSrcSet = srcSetOverride ?? generatedSrcSet;
|
|
87
|
-
const optimizedSrc = shouldOptimize
|
|
88
|
-
? buildOptimizedUrl(originalSrc, {
|
|
89
|
-
width: transformWidth,
|
|
90
|
-
quality: normalizedQuality,
|
|
91
|
-
fit: normalizedFit,
|
|
92
|
-
dpr: transformDpr,
|
|
93
|
-
})
|
|
94
|
-
: originalSrc;
|
|
95
|
-
const finalSrc = overrideSrc ?? optimizedSrc;
|
|
96
|
-
/**
|
|
97
|
-
* Apply browser-side object fitting so the rendered image also respects
|
|
98
|
-
* the container box. Cloudflare `fit` controls transformed pixels, while
|
|
99
|
-
* `object-fit` controls layout behavior at render time.
|
|
100
|
-
*/
|
|
101
|
-
const finalStyle = style?.objectFit
|
|
102
|
-
? style
|
|
103
|
-
: {
|
|
104
|
-
...style,
|
|
105
|
-
objectFit: toObjectFit(normalizedFit),
|
|
106
|
-
};
|
|
107
|
-
return (_jsx("img", { ref: ref, src: finalSrc, alt: resolvedAlt, width: displayWidth, height: displayHeight, style: finalStyle, sizes: sizes, srcSet: finalSrcSet, ...imgProps, "data-fimo-source": mediaSource }));
|
|
108
|
-
}
|