fimo 0.12.0-staging.2 → 0.12.0-staging.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/assets/content-templates/types-template.eta +13 -7
  2. package/assets/skills/fimo/SKILL.md +8 -6
  3. package/assets/skills/fimo-cli/SKILL.md +15 -23
  4. package/assets/skills/fimo-cli/references/agent-setup.md +14 -14
  5. package/assets/skills/fimo-cli/references/assets.md +2 -2
  6. package/assets/skills/fimo-cli/references/content.md +16 -30
  7. package/assets/skills/fimo-cli/references/describe.md +1 -1
  8. package/assets/skills/fimo-cli/references/forms.md +8 -8
  9. package/assets/skills/fimo-cli/references/init.md +5 -4
  10. package/assets/skills/fimo-cli/references/labels.md +4 -4
  11. package/assets/skills/fimo-cli/references/locales.md +3 -3
  12. package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +1 -1
  13. package/assets/skills/fimo-code/SKILL.md +39 -49
  14. package/assets/skills/fimo-code/references/assets.md +6 -4
  15. package/assets/skills/fimo-code/references/content/clients.md +130 -0
  16. package/assets/skills/fimo-code/references/content/primitives.md +5 -0
  17. package/assets/skills/fimo-code/references/content/richtext.md +48 -0
  18. package/assets/skills/fimo-code/references/content/schema.md +122 -0
  19. package/assets/skills/fimo-code/references/content.md +12 -597
  20. package/assets/skills/fimo-code/references/design.md +1 -1
  21. package/assets/skills/fimo-code/references/frameworks/next/app-router.md +116 -0
  22. package/assets/skills/fimo-code/references/frameworks/next/locales.md +59 -0
  23. package/assets/skills/fimo-code/references/frameworks/next/pages-router.md +99 -0
  24. package/assets/skills/fimo-code/references/frameworks/next.md +116 -0
  25. package/assets/skills/fimo-code/references/frameworks/react-router.md +295 -0
  26. package/assets/skills/fimo-code/references/frameworks/vite-react.md +225 -0
  27. package/assets/skills/fimo-code/references/labels.md +24 -83
  28. package/assets/skills/fimo-code/references/locales.md +22 -111
  29. package/assets/skills/fimo-code/references/setup-plain-vite.md +3 -227
  30. package/assets/skills/fimo-code/references/setup-react-router.md +3 -276
  31. package/assets/skills/fimo-code/references/ui.md +32 -78
  32. package/dist/build/vite/plugins/content-sync.js +1 -1
  33. package/dist/cli/bundle.json +1 -1
  34. package/dist/cli/index.js +174 -65
  35. package/dist/runtime/labels/core.d.ts +18 -0
  36. package/dist/runtime/labels/core.js +15 -0
  37. package/dist/runtime/labels/fetch.d.ts +7 -0
  38. package/dist/runtime/labels/fetch.js +41 -0
  39. package/dist/runtime/next/client.d.ts +3 -2
  40. package/dist/runtime/next/client.js +1 -1
  41. package/dist/runtime/next/facade.d.ts +8 -0
  42. package/dist/runtime/next/facade.js +34 -0
  43. package/dist/runtime/next/labels.d.ts +7 -0
  44. package/dist/runtime/next/labels.js +9 -0
  45. package/dist/runtime/next/provider.d.ts +5 -2
  46. package/dist/runtime/next/provider.js +18 -5
  47. package/dist/runtime/next/server.d.ts +5 -0
  48. package/dist/runtime/next/server.js +2 -0
  49. package/dist/scripts/extract-translations.js +8 -1
  50. package/dist/scripts/sync-schemas.d.ts +2 -1
  51. package/dist/scripts/sync-schemas.js +119 -67
  52. package/package.json +2 -1
  53. package/templates/react-router/AGENTS.md +12 -10
  54. package/templates/react-router/package.json +1 -1
  55. package/templates/react-router/react-router.config.ts +1 -1
@@ -0,0 +1,18 @@
1
+ import { FimoString } from '../primitives/lib/index.js';
2
+ export interface LabelsSnapshot {
3
+ locale: string;
4
+ labels: Record<string, string>;
5
+ updatedAt: string | null;
6
+ }
7
+ export interface Labels {
8
+ locale: string;
9
+ snapshot: LabelsSnapshot;
10
+ t(key: string): FimoString;
11
+ }
12
+ /**
13
+ * Build the shared label reader used by server `getLabels()` and client
14
+ * `useLabels()`. Missing keys stay editable but render as an empty string;
15
+ * snapshots never fall back across locales implicitly.
16
+ */
17
+ export declare function createLabels(snapshot: LabelsSnapshot): Labels;
18
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { FimoString } from '../primitives/lib/index.js';
2
+ /**
3
+ * Build the shared label reader used by server `getLabels()` and client
4
+ * `useLabels()`. Missing keys stay editable but render as an empty string;
5
+ * snapshots never fall back across locales implicitly.
6
+ */
7
+ export function createLabels(snapshot) {
8
+ return {
9
+ locale: snapshot.locale,
10
+ snapshot,
11
+ t(key) {
12
+ return new FimoString(snapshot.labels[key] ?? '', `__labels.${snapshot.locale}.${key}.value`);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,7 @@
1
+ import type { ContentRuntime } from '../content/client/runtime.js';
2
+ import { type Labels } from './core.js';
3
+ export interface GetLabelsOptions {
4
+ locale?: string;
5
+ }
6
+ export declare function fetchLabels(runtime: ContentRuntime, { locale: requestedLocale }?: GetLabelsOptions): Promise<Labels>;
7
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { createLabels } from './core.js';
2
+ function dictionaryFrom(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
4
+ return {};
5
+ }
6
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
7
+ }
8
+ function snapshotFrom(body, requestedLocale) {
9
+ if (Array.isArray(body.data)) {
10
+ return {
11
+ locale: requestedLocale,
12
+ labels: Object.fromEntries(body.data.flatMap((row) => {
13
+ if (typeof row.key !== 'string') {
14
+ return [];
15
+ }
16
+ const value = typeof row.value === 'string' ? row.value : row.defaultValue;
17
+ return [[row.key, typeof value === 'string' ? value : '']];
18
+ })),
19
+ updatedAt: null,
20
+ };
21
+ }
22
+ const data = body.data;
23
+ return {
24
+ locale: typeof data?.locale === 'string' && data.locale.trim() ? data.locale : requestedLocale,
25
+ labels: dictionaryFrom(data?.labels),
26
+ updatedAt: typeof data?.updatedAt === 'string' ? data.updatedAt : null,
27
+ };
28
+ }
29
+ export async function fetchLabels(runtime, { locale: requestedLocale } = {}) {
30
+ const locale = requestedLocale?.trim() || runtime.defaultLocale();
31
+ const url = new URL('/labels', runtime.apiBase());
32
+ url.searchParams.set('locale', locale);
33
+ const response = await runtime.fetch(url, {
34
+ headers: runtime.headers({ Accept: 'application/json' }),
35
+ });
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to read Fimo labels (${response.status}).`);
38
+ }
39
+ const body = (await response.json());
40
+ return createLabels(snapshotFrom(body, locale));
41
+ }
@@ -18,6 +18,7 @@ export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
18
18
  export { Text } from '../react/primitives/editable/Text.js';
19
19
  export { Video } from '../react/primitives/editable/Video.js';
20
20
  export type { BoolOps, DateOps, Field, Fields, FilterFor, FimoPart, FimoPolymorphicReference, FimoReference, FimoRichTextComponents, FimoRichTextContent, FimoRichTextMarkProps, FimoRichTextNodeProps, FimoSource, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, Projected, ProjectedWithPopulate, Query, ReferenceField, ReferenceMap, ScalarOps, Sort, SortField, StringOps, Where, WithReferences, } from '../content/index.js';
21
- export { FimoNextProvider } from './provider.js';
22
- export type { FimoNextProviderProps } from './provider.js';
21
+ export { FimoProvider, useLabels } from './provider.js';
22
+ export type { FimoProviderProps } from './provider.js';
23
+ export type { Labels, LabelsSnapshot } from '../labels/core.js';
23
24
  //# sourceMappingURL=client.d.ts.map
@@ -19,4 +19,4 @@ export { RichText } from '../react/primitives/editable/RichText.js';
19
19
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
20
20
  export { Text } from '../react/primitives/editable/Text.js';
21
21
  export { Video } from '../react/primitives/editable/Video.js';
22
- export { FimoNextProvider } from './provider.js';
22
+ export { FimoProvider, useLabels } from './provider.js';
@@ -0,0 +1,8 @@
1
+ import type { ContentClientHandle } from '../content/client/types.js';
2
+ export type NextContentRegistry = Readonly<Record<string, ContentClientHandle>>;
3
+ export interface CreateFimoOptions<TContent extends NextContentRegistry> {
4
+ content: TContent;
5
+ locale?: string;
6
+ }
7
+ export declare function createFimo<const TContent extends NextContentRegistry>({ content, locale, }: CreateFimoOptions<TContent>): TContent;
8
+ //# sourceMappingURL=facade.d.ts.map
@@ -0,0 +1,34 @@
1
+ function withLocale(options, locale) {
2
+ return { ...options, locale: options?.locale ?? locale };
3
+ }
4
+ function isCollectionClient(client) {
5
+ return 'list' in client && typeof client.list === 'function';
6
+ }
7
+ function isSingletonClient(client) {
8
+ return 'get' in client && typeof client.get === 'function';
9
+ }
10
+ function bindLocale(client, locale) {
11
+ if (isCollectionClient(client)) {
12
+ return {
13
+ ...client,
14
+ getById: (id, options) => client.getById(id, withLocale(options, locale)),
15
+ getBySlug: (slug, options) => client.getBySlug(slug, withLocale(options, locale)),
16
+ getByField: (fieldName, value, options) => client.getByField(fieldName, value, withLocale(options, locale)),
17
+ list: (params) => client.list(withLocale(params, locale)),
18
+ get: (params) => client.get(withLocale(params, locale)),
19
+ };
20
+ }
21
+ if (isSingletonClient(client)) {
22
+ return {
23
+ ...client,
24
+ get: (options) => client.get(withLocale(options, locale)),
25
+ };
26
+ }
27
+ return client;
28
+ }
29
+ export function createFimo({ content, locale, }) {
30
+ if (locale === undefined) {
31
+ return content;
32
+ }
33
+ return Object.fromEntries(Object.entries(content).map(([key, client]) => [key, bindLocale(client, locale)]));
34
+ }
@@ -0,0 +1,7 @@
1
+ import { type GetLabelsOptions } from '../labels/fetch.js';
2
+ /**
3
+ * Read the labels for an application-resolved locale. When omitted, locale
4
+ * comes from FIMO_DEFAULT_LOCALE / NEXT_PUBLIC_FIMO_DEFAULT_LOCALE.
5
+ */
6
+ export declare function getLabels(options?: GetLabelsOptions): Promise<import("./client.js").Labels>;
7
+ //# sourceMappingURL=labels.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { fetchLabels } from '../labels/fetch.js';
2
+ import { nextServerContentRuntime } from './server-runtime.js';
3
+ /**
4
+ * Read the labels for an application-resolved locale. When omitted, locale
5
+ * comes from FIMO_DEFAULT_LOCALE / NEXT_PUBLIC_FIMO_DEFAULT_LOCALE.
6
+ */
7
+ export function getLabels(options) {
8
+ return fetchLabels(nextServerContentRuntime, options);
9
+ }
@@ -1,10 +1,13 @@
1
1
  import { QueryClient } from '@tanstack/react-query';
2
2
  import { type ReactNode } from 'react';
3
- export interface FimoNextProviderProps {
3
+ import { type Labels, type LabelsSnapshot } from '../labels/core.js';
4
+ export interface FimoProviderProps {
4
5
  children: ReactNode;
6
+ labels?: LabelsSnapshot;
5
7
  locale?: string;
6
8
  queryClient?: QueryClient;
7
9
  }
8
- export declare function FimoNextProvider({ children, locale, queryClient }: FimoNextProviderProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function FimoProvider({ children, labels, locale, queryClient }: FimoProviderProps): import("react/jsx-runtime").JSX.Element;
9
11
  export declare function useNextContentLocale(requestedLocale: string | undefined, defaultLocale: string): string;
12
+ export declare function useLabels(): Labels;
10
13
  //# sourceMappingURL=provider.d.ts.map
@@ -1,12 +1,25 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
- import { createContext, useContext, useState } from 'react';
5
- const FimoNextLocaleContext = createContext(undefined);
6
- export function FimoNextProvider({ children, locale, queryClient }) {
4
+ import { createContext, useContext, useMemo, useState } from 'react';
5
+ import { createLabels } from '../labels/core.js';
6
+ import { nextClientContentRuntime } from './client-runtime.js';
7
+ const FimoNextContext = createContext({});
8
+ export function FimoProvider({ children, labels, locale, queryClient }) {
7
9
  const [client] = useState(() => queryClient ?? new QueryClient());
8
- return (_jsx(FimoNextLocaleContext.Provider, { value: locale, children: _jsx(QueryClientProvider, { client: client, children: children }) }));
10
+ const activeLocale = locale?.trim() || labels?.locale;
11
+ const value = useMemo(() => ({ labels, locale: activeLocale }), [activeLocale, labels]);
12
+ return (_jsx(FimoNextContext.Provider, { value: value, children: _jsx(QueryClientProvider, { client: client, children: children }) }));
9
13
  }
10
14
  export function useNextContentLocale(requestedLocale, defaultLocale) {
11
- return requestedLocale ?? useContext(FimoNextLocaleContext) ?? defaultLocale;
15
+ const context = useContext(FimoNextContext);
16
+ return requestedLocale ?? context.locale ?? defaultLocale;
17
+ }
18
+ export function useLabels() {
19
+ const context = useContext(FimoNextContext);
20
+ const locale = context.locale ?? nextClientContentRuntime.defaultLocale();
21
+ const snapshot = context.labels?.locale === locale
22
+ ? context.labels
23
+ : { locale, labels: {}, updatedAt: null };
24
+ return useMemo(() => createLabels(snapshot), [snapshot]);
12
25
  }
@@ -2,6 +2,11 @@ import type { CollectionContentClient, ContentClientConfig, SingletonContentClie
2
2
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
3
3
  export { Text } from './primitives/Text.js';
4
4
  export { Boolean, Date, Image, Json, RichText, Video } from './primitives/server.js';
5
+ export { createFimo } from './facade.js';
6
+ export type { CreateFimoOptions, NextContentRegistry } from './facade.js';
7
+ export { getLabels } from './labels.js';
8
+ export type { GetLabelsOptions } from '../labels/fetch.js';
9
+ export type { Labels, LabelsSnapshot } from '../labels/core.js';
5
10
  export declare function createContentClient<T extends {
6
11
  id: string;
7
12
  }, TInput extends object>(config: ContentClientConfig): CollectionContentClient<T, TInput>;
@@ -3,6 +3,8 @@ import { nextServerContentRuntime } from './server-runtime.js';
3
3
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
4
4
  export { Text } from './primitives/Text.js';
5
5
  export { Boolean, Date, Image, Json, RichText, Video } from './primitives/server.js';
6
+ export { createFimo } from './facade.js';
7
+ export { getLabels } from './labels.js';
6
8
  export function createContentClient(config) {
7
9
  return createServerContentClient(config, nextServerContentRuntime);
8
10
  }
@@ -40,7 +40,14 @@ function extractFromFile(filePath) {
40
40
  }
41
41
  if (node.type === 'CallExpression') {
42
42
  const callee = node.callee;
43
- if (callee?.type === 'Identifier' && callee?.name === 't') {
43
+ const isTranslationCall = (callee?.type === 'Identifier' && callee.name === 't') ||
44
+ (callee?.type === 'MemberExpression' &&
45
+ callee.computed === false &&
46
+ callee.object?.type === 'Identifier' &&
47
+ callee.object.name === 'labels' &&
48
+ callee.property?.type === 'Identifier' &&
49
+ callee.property.name === 't');
50
+ if (isTranslationCall) {
44
51
  const args = node.arguments || [];
45
52
  const firstArg = args[0];
46
53
  if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
@@ -1,5 +1,6 @@
1
1
  export interface SyncSchemasOptions {
2
2
  cwd?: string;
3
+ framework: string | null;
3
4
  verbose?: boolean;
4
5
  }
5
6
  export interface SyncResult {
@@ -7,4 +8,4 @@ export interface SyncResult {
7
8
  updated: number;
8
9
  deleted: number;
9
10
  }
10
- export declare function syncSchemas(options?: SyncSchemasOptions): SyncResult;
11
+ export declare function syncSchemas(options: SyncSchemasOptions): SyncResult;
@@ -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 the shared types plus either the Vite facade or the split Next server/client facades.
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 version;
85
+ let packageJson;
71
86
  try {
72
- version = JSON.parse(readFileSync(packagePath, 'utf-8')).version;
87
+ packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
73
88
  }
74
89
  catch {
75
- version = null;
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 { packageRoot: dirname(packagePath), version };
100
+ return {
101
+ packageRoot: dirname(packagePath),
102
+ version,
103
+ providesNextRuntime: packageExportsSubpath(packageJson, './next') && packageExportsSubpath(packageJson, './next/client'),
104
+ };
83
105
  }
84
- function resolveContentRuntimeImport(cwd) {
85
- const packagePath = join(cwd, 'package.json');
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
- schemas.push({ pascal, schema: validation.data });
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 = resolveContentRuntimeImport(cwd);
242
- const usesCurrentNextRuntime = runtimeImport === 'fimo/next' &&
243
- runtime !== null &&
244
- isVersionAtLeast(runtime.version, MINIMUM_THIN_CONTENT_CLIENT_RUNTIME_VERSION);
245
- const templates = usesCurrentNextRuntime
246
- ? [
247
- {
248
- suffix: '.types.ts',
249
- path: templatePath(runtime, 'types-template.eta'),
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.2",
3
+ "version": "0.12.0-staging.4",
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",
@@ -1,31 +1,33 @@
1
1
  # Fimo project
2
2
 
3
- This is a Fimo project — a content + media + forms + analytics platform for React websites with a CMS
3
+ This is a Fimo project — a content + media + forms + analytics platform for web applications with a CMS
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 so they always match this project's installed version. Before
10
- writing schema-driven code, running CLI/deploy workflows, or migrating content, load the relevant skill:
11
+ Before writing schema-driven code, running CLI/deploy workflows, or migrating content, load only the
12
+ skill relevant to the task:
11
13
 
12
14
  ```bash
13
- npx fimo@latest skills list # what's available
14
- npx fimo@latest skills get fimo-code # writing code: schemas, forms, labels, fimo/ui
15
- npx fimo@latest skills get fimo-cli # CLI + workflows: push, deploy, sync, labels, domains
15
+ pnpm exec fimo skills get fimo-code # match the lockfile; code + framework APIs come from this project runtime
16
+ npx fimo@latest skills get fimo-cli # operational workflows: push, deploy, labels, domains
16
17
  ```
17
18
 
18
- Add `--ref <doc>` for a skill's reference docs. Always fetch fresh from the CLI rather than trusting a
19
- cached copy.
19
+ Use `npm exec fimo`, `yarn fimo`, or `bunx --no-install fimo` instead of `pnpm exec fimo` when that
20
+ matches the project lockfile. Add `--ref <doc>` for one reference at a time. Do not load `fimo-cli` for
21
+ a code-only task, or use `npx fimo@latest` for project-local code generation and validation.
20
22
 
21
23
  ## Critical: never hardcode user-facing things
22
24
 
23
25
  Fimo's whole point is that editors change content in the dashboard, not in code. Anything hardcoded
24
26
  breaks that. Load `fimo-code` for the exact primitives, but the rule is:
25
27
 
26
- - **Content** → declare a schema, render with the generated `useList()` / `useGet()` hooks + `fimo/ui`. Never `const posts = [...]` in JSX.
28
+ - **Content** → declare a schema, use its generated client, and render with the active framework adapter. Never `const posts = [...]` in JSX.
27
29
  - **Strings** → `t('key')` + `fimo labels set`. Never `<h1>Welcome</h1>`.
28
- - **Media** → `<Image>` / `<Video>` from `fimo/ui`. Never `<img src>`.
30
+ - **Media** → `<Image>` / `<Video>` from the active framework adapter. Never `<img src>` for tracked media.
29
31
  - **Forms** → the generated Zod schema + `submitX` helper. Never hand-roll fetch/validation.
30
32
 
31
33
  ## Development loop
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.12.0-staging.2",
24
+ "fimo": "0.12.0-staging.4",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",
@@ -11,7 +11,7 @@ export default {
11
11
  // prerender: async () => (await getFimoPaths()).map((p) => p.path),
12
12
  //
13
13
  // `getFimoPaths()` expands dynamic routes (e.g. `/blog/:slug`) by fetching
14
- // schema entries from the content API. See `references/setup-react-router.md`
14
+ // schema entries from the content API. See `references/frameworks/react-router.md`
15
15
  // in the fimo skill for the full setup pattern.
16
16
  buildDirectory: 'dist',
17
17
  // Feed Vite's dependency scanner the client entry + every route module.