fimo 0.5.0-staging.2 → 0.6.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.
@@ -43,7 +43,7 @@ Workflow / CLI-side concerns (pushing schemas, generating images, managing label
43
43
  ## Decision Tree (specific tasks)
44
44
 
45
45
  - **Setting up Fimo in a new project** — pick the framework:
46
- - React Router 7 framework mode → `references/setup-react-router.md`
46
+ - React Router framework mode (new projects use React Router 8) → `references/setup-react-router.md`
47
47
  - Plain Vite + React (or upgrading a legacy v3 project) → `references/setup-plain-vite.md`
48
48
  - Writing schema JSON / picking field types / Tiptap richtext → `references/content.md`
49
49
  - Writing form JSON / generated form client / form code → `references/forms.md`
@@ -67,6 +67,14 @@ This helper exists so the Fimo UI route selector, preview navigation, labels, an
67
67
 
68
68
  Copyable examples live in `assets/react-router-locales/routes.ts` and `assets/react-router-locales/navigation.tsx`.
69
69
 
70
+ ## Existing Custom Locale Systems
71
+
72
+ Before changing a multilingual app, inspect its locale provider, route table, language switcher, redirects, and current public URLs. Preserve that behavior while migrating.
73
+
74
+ For React Router projects, prefer converging on one locale contract owned by `fimo-config.json`, `fimoRoutes()`, and `FimoLink`. Move existing providers and language-switcher UI into the framework root instead of replacing them or adding a second locale context. Keep route metadata canonical and reuse page modules across locales.
75
+
76
+ If adopting the standard helper would change established public URLs, do not silently rewrite them. Keep the custom router temporarily, pass its resolved locale to `<FimoProviders locale={locale}>`, and ask the user to choose the canonical URLs and redirects before normalizing the route table.
77
+
70
78
  ## Links
71
79
 
72
80
  Use `FimoLink` for internal links that should preserve or switch locale:
@@ -1,4 +1,4 @@
1
- # Setup Fimo on React Router 7
1
+ # Setup Fimo on React Router 8
2
2
 
3
3
  This recipe describes the canonical Fimo + React Router framework-mode setup.
4
4
  It mirrors what `fimo init` scaffolds today; you can also apply it to an
@@ -42,7 +42,7 @@ The template uses **Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + rea
42
42
 
43
43
  ### Routing
44
44
 
45
- - This template uses React Router v7 framework mode (file-based route config in `src/routes.ts`). Use `<FimoLink>` from `fimo/react-router` for locale-aware internal links, especially in projects with more than one configured locale. Use React Router `<Link>` / `<NavLink>` only when you intentionally do not want Fimo's locale behavior. Never raw `<a href="/...">` for internal navigation because it causes full page reloads.
45
+ - This template uses React Router 8 framework mode (file-based route config in `src/routes.ts`). Use `<FimoLink>` from `fimo/react-router` for locale-aware internal links, especially in projects with more than one configured locale. Use React Router `<Link>` / `<NavLink>` only when you intentionally do not want Fimo's locale behavior. Never raw `<a href="/...">` for internal navigation because it causes full page reloads.
46
46
  - Reserve `<a>` for external links, with `target="_blank" rel="noreferrer"`.
47
47
 
48
48
  ### Loading states
@@ -20,6 +20,9 @@ Steps 1, 3, and 5 reference the project skill (`fimo`) for shape and React API.
20
20
 
21
21
  ```bash
22
22
  fimo schemas push [TypeName] # push JSON to API (create or update). Omit TypeName to push all.
23
+ # Pushing all sends every schema as ONE atomic batch, so types that
24
+ # reference each other (even cycles) can be added in a single push.
25
+ # Files not present are never deleted — use `schemas delete` for that.
23
26
  fimo schemas list # list all registered schemas
24
27
  fimo schemas get <TypeName> # get a single schema
25
28
  fimo schemas delete <TypeName> # delete a schema
@@ -67,6 +67,12 @@ import { fimoRoutes } from 'fimo/react-router/routes';
67
67
 
68
68
  For custom frameworks, the project may implement its own URL scheme, but it still needs to pass the active locale into Fimo and keep canonical route metadata if it wants Fimo Studio route selection to work.
69
69
 
70
+ ## Migrate An Existing Locale System
71
+
72
+ Inspect the existing locale provider, route table, switcher, redirects, and public URLs before editing configuration. Preserve working URL behavior during the migration.
73
+
74
+ For React Router projects, prefer one contract based on `fimo-config.json`, `fimoRoutes()`, and `FimoLink`; migrate existing wrappers into the framework root instead of layering a second locale context. If that would change established URLs, keep the custom router temporarily, pass its active locale to `FimoProviders`, and ask the user to choose canonical URLs and redirects before rewriting routes.
75
+
70
76
  ## Labels And Content
71
77
 
72
78
  Labels and content are separate translated surfaces:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-14T16:56:16.308Z",
4
+ "bundledAt": "2026-07-14T13:44:49.449Z",
5
5
  "cliVersion": "0.5.0",
6
6
  "external": [
7
7
  "oxc-parser",
package/dist/cli/index.js CHANGED
@@ -20618,6 +20618,13 @@ var init_api = __esm({
20618
20618
  });
20619
20619
  return body.data;
20620
20620
  }
20621
+ async upsertSchemas(projectId, contentTypes) {
20622
+ const body = await this.request(this.tenantPath(projectId, "/schema/batch"), {
20623
+ method: "POST",
20624
+ body: JSON.stringify({ contentTypes })
20625
+ });
20626
+ return body.data;
20627
+ }
20621
20628
  async deleteSchema(projectId, typeName) {
20622
20629
  const body = await this.request(this.tenantPath(projectId, `/schema/types/${typeName}`), {
20623
20630
  method: "DELETE"
@@ -109785,7 +109792,6 @@ init_dist();
109785
109792
  init_sync_schemas();
109786
109793
  import fs42 from "node:fs";
109787
109794
  import path46 from "node:path";
109788
- init_api();
109789
109795
  init_project_context();
109790
109796
  init_project();
109791
109797
  function toApiShape(schema) {
@@ -109802,10 +109808,6 @@ function renderPrettyJson8(data2) {
109802
109808
  return JSON.stringify(data2, null, 2);
109803
109809
  }
109804
109810
  __name(renderPrettyJson8, "renderPrettyJson");
109805
- function isAlreadyExistsError(error45) {
109806
- return error45 instanceof ApiRequestError && error45.status === 400 && /already exists/i.test(error45.detail);
109807
- }
109808
- __name(isAlreadyExistsError, "isAlreadyExistsError");
109809
109811
  async function schemaListCommand(options, cmd) {
109810
109812
  const { ctx, api } = await bootstrapProjectCommand(options, cmd);
109811
109813
  await requireOperation("cms.schema.list", { projectId: ctx.projectId, env: ctx.env });
@@ -109849,21 +109851,10 @@ async function schemaPushCommand(typeName, options, cmd) {
109849
109851
  return JSON.parse(fs42.readFileSync(filePath, "utf-8"));
109850
109852
  });
109851
109853
  syncSchemas({ cwd, verbose: false });
109852
- const results = [];
109853
- for (const schema of schemas3) {
109854
- const contentType = toApiShape(schema);
109855
- try {
109856
- const result = await api.createSchema(ctx.projectId, contentType);
109857
- results.push(result);
109858
- } catch (error45) {
109859
- if (isAlreadyExistsError(error45)) {
109860
- const result = await api.updateSchema(ctx.projectId, contentType.name, contentType);
109861
- results.push(result);
109862
- } else {
109863
- throw error45;
109864
- }
109865
- }
109866
- }
109854
+ const results = await api.upsertSchemas(
109855
+ ctx.projectId,
109856
+ schemas3.map((schema) => toApiShape(schema))
109857
+ );
109867
109858
  const payload = results.length === 1 ? results[0] : results;
109868
109859
  ui.result({ command: "schemas push", data: payload, render: /* @__PURE__ */ __name(() => renderPrettyJson8(payload), "render") });
109869
109860
  }
@@ -1 +1 @@
1
- {"version":3,"file":"get-fimo-paths.d.ts","sourceRoot":"","sources":["../../../src/runtime/paths/get-fimo-paths.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAoGtF"}
1
+ {"version":3,"file":"get-fimo-paths.d.ts","sourceRoot":"","sources":["../../../src/runtime/paths/get-fimo-paths.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAkFhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAoGtF"}
@@ -7,6 +7,9 @@ import { getFimoLocales, localizeFimoPath } from '../i18n/locale.js';
7
7
  import { resolveSeoForPath } from '../seo/resolver.js';
8
8
  import { getConfigServer } from '../shared/fimo-config.server.js';
9
9
  const isValidationBuild = !!process.env.FIMO_VALIDATION_BUILD;
10
+ const MAX_FETCH_ATTEMPTS = 3;
11
+ const RETRYABLE_FETCH_STATUSES = new Set([502, 503, 504]);
12
+ const RETRY_BASE_DELAY_MS = 250;
10
13
  function readEnvValue(rootDir, key) {
11
14
  if (process.env[key]) {
12
15
  return process.env[key];
@@ -42,12 +45,19 @@ async function fetchEntries(baseUrl, schema, rootDir, locale) {
42
45
  if (branch) {
43
46
  headers['X-Fimo-Branch'] = branch;
44
47
  }
45
- const res = await fetch(url, { headers });
46
- if (!res.ok) {
47
- throw new Error(`Failed to fetch ${schema}: ${res.status}`);
48
+ for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt += 1) {
49
+ const res = await fetch(url, { headers });
50
+ if (res.ok) {
51
+ const payload = (await res.json());
52
+ return Array.isArray(payload?.data) ? payload.data : [];
53
+ }
54
+ if (!RETRYABLE_FETCH_STATUSES.has(res.status) || attempt === MAX_FETCH_ATTEMPTS) {
55
+ const attemptDetail = attempt > 1 ? ` after ${attempt} attempts` : '';
56
+ throw new Error(`Failed to fetch ${schema}: ${res.status}${attemptDetail}`);
57
+ }
58
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, RETRY_BASE_DELAY_MS * attempt));
48
59
  }
49
- const payload = (await res.json());
50
- return Array.isArray(payload?.data) ? payload.data : [];
60
+ throw new Error(`Failed to fetch ${schema}`);
51
61
  }
52
62
  // Glob-ish matcher for route keys: '*' matches one segment, '**' matches any.
53
63
  function compilePattern(pattern) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=get-fimo-paths.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-fimo-paths.test.d.ts","sourceRoot":"","sources":["../../../src/runtime/paths/get-fimo-paths.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { getFimoPaths } from './get-fimo-paths.js';
6
+ const roots = [];
7
+ function createDynamicProject() {
8
+ const root = mkdtempSync(join(tmpdir(), 'fimo-paths-'));
9
+ roots.push(root);
10
+ writeFileSync(join(root, '.env'), 'VITE_API_URL=https://tenant.example.test\n');
11
+ writeFileSync(join(root, 'fimo-config.json'), JSON.stringify({
12
+ routes: {
13
+ 'blog/post': {
14
+ path: '/blog/:slug',
15
+ params: {
16
+ slug: {
17
+ schema: 'BlogPost',
18
+ queryField: 'slug',
19
+ displayField: 'title',
20
+ },
21
+ },
22
+ },
23
+ },
24
+ }));
25
+ return root;
26
+ }
27
+ function response(status, data = []) {
28
+ return new Response(JSON.stringify({ data }), {
29
+ status,
30
+ headers: { 'Content-Type': 'application/json' },
31
+ });
32
+ }
33
+ afterEach(() => {
34
+ vi.useRealTimers();
35
+ vi.unstubAllGlobals();
36
+ for (const root of roots.splice(0)) {
37
+ rmSync(root, { recursive: true, force: true });
38
+ }
39
+ });
40
+ describe('getFimoPaths content fetches', () => {
41
+ it.each([502, 503, 504])('recovers from a transient %s response', async (status) => {
42
+ vi.useFakeTimers();
43
+ const fetchMock = vi
44
+ .fn()
45
+ .mockResolvedValueOnce(response(status))
46
+ .mockResolvedValueOnce(response(200, [{ slug: 'hello-world' }]));
47
+ vi.stubGlobal('fetch', fetchMock);
48
+ const resultPromise = getFimoPaths({ cwd: createDynamicProject() });
49
+ await vi.runAllTimersAsync();
50
+ await expect(resultPromise).resolves.toContainEqual(expect.objectContaining({ routeKey: 'blog/post', path: '/blog/hello-world' }));
51
+ expect(fetchMock).toHaveBeenCalledTimes(2);
52
+ });
53
+ it('fails clearly after exhausting the transient retry budget', async () => {
54
+ vi.useFakeTimers();
55
+ const fetchMock = vi.fn().mockResolvedValue(response(503));
56
+ vi.stubGlobal('fetch', fetchMock);
57
+ const resultPromise = getFimoPaths({ cwd: createDynamicProject() });
58
+ const rejection = expect(resultPromise).rejects.toThrow('Failed to fetch BlogPost: 503 after 3 attempts');
59
+ await vi.runAllTimersAsync();
60
+ await rejection;
61
+ expect(fetchMock).toHaveBeenCalledTimes(3);
62
+ });
63
+ it('does not retry an author response', async () => {
64
+ const fetchMock = vi.fn().mockResolvedValue(response(400));
65
+ vi.stubGlobal('fetch', fetchMock);
66
+ await expect(getFimoPaths({ cwd: createDynamicProject() })).rejects.toThrow('Failed to fetch BlogPost: 400');
67
+ expect(fetchMock).toHaveBeenCalledTimes(1);
68
+ });
69
+ });
@@ -37,9 +37,7 @@ export declare function buildFimoMeta(args: {
37
37
  location: {
38
38
  pathname: string;
39
39
  };
40
- matches: {
41
- data?: unknown;
42
- }[];
40
+ matches: readonly unknown[];
43
41
  }): ({
44
42
  title: string;
45
43
  name?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"buildSeoForPath.d.ts","sourceRoot":"","sources":["../../../src/runtime/seo/buildSeoForPath.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAK1E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAExF;AA+BD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAMrE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAUpG"}
1
+ {"version":3,"file":"buildSeoForPath.d.ts","sourceRoot":"","sources":["../../../src/runtime/seo/buildSeoForPath.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAK1E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAExF;AA+BD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAMrE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAclG"}
@@ -42,7 +42,10 @@ export function buildMetaForPath(pathname, routeData) {
42
42
  }
43
43
  export function buildFimoMeta(args) {
44
44
  const pathname = args.location.pathname;
45
- const rootData = args.matches[0]?.data;
45
+ const firstMatch = args.matches[0];
46
+ const rootData = firstMatch && typeof firstMatch === 'object' && 'data' in firstMatch
47
+ ? firstMatch.data
48
+ : undefined;
46
49
  const routeData = rootData && typeof rootData === 'object' ? rootData.fimoMetaData : undefined;
47
50
  const seo = getSeoForPath(pathname, routeData);
48
51
  if (!seo) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Given a list of changed files, resolves which preview routes are affected
5
5
  * by walking the reverse import graph from changed files up to page entry points
6
- * referenced in src/routes.ts (RR v7 framework-mode config).
6
+ * referenced in src/routes.ts (React Router framework-mode config).
7
7
  *
8
8
  * Output (JSON):
9
9
  * {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.5.0-staging.2",
3
+ "version": "0.6.0-experimental.1",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -99,7 +99,7 @@
99
99
  "@tanstack/react-query": "^5.80.6",
100
100
  "react": "^19.0.0",
101
101
  "react-dom": "^19.0.0",
102
- "react-router": "^7.0.0",
102
+ "react-router": "^7.0.0 || ^8.0.0",
103
103
  "vite": "^7.0.0 || ^8.0.0"
104
104
  },
105
105
  "engines": {
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "react-router dev --port 5173",
8
- "build": "react-router build && fimo export-static",
8
+ "build": "tsc --noEmit && react-router build && fimo export-static",
9
9
  "lint": "oxlint --deny-warnings",
10
10
  "typecheck": "tsc --noEmit",
11
11
  "preview": "vite preview --host 0.0.0.0 --port 4173"
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@base-ui/react": "^1.3.0",
15
15
  "@hookform/resolvers": "^5.2.2",
16
- "@react-router/node": "^7.15.1",
16
+ "@react-router/node": "^8.2.0",
17
17
  "@tailwindcss/typography": "^0.5.19",
18
18
  "@tanstack/react-query": "^5.80.6",
19
19
  "class-variance-authority": "^0.7.1",
@@ -21,20 +21,20 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.5.0-staging.2",
24
+ "fimo": "0.6.0-experimental.1",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",
28
28
  "motion": "^12.36.0",
29
29
  "next-themes": "^0.4.6",
30
30
  "radix-ui": "^1.4.3",
31
- "react": "^19.2.4",
31
+ "react": "^19.2.7",
32
32
  "react-day-picker": "^9.14.0",
33
- "react-dom": "^19.2.4",
33
+ "react-dom": "^19.2.7",
34
34
  "react-error-boundary": "^6.0.0",
35
35
  "react-hook-form": "^7.72.1",
36
36
  "react-resizable-panels": "^4.9.0",
37
- "react-router": "^7.15.1",
37
+ "react-router": "^8.2.0",
38
38
  "recharts": "^3.8.0",
39
39
  "sonner": "^2.0.7",
40
40
  "tailwind-merge": "^3.2.0",
@@ -42,7 +42,7 @@
42
42
  "zod": "^4.3.6"
43
43
  },
44
44
  "devDependencies": {
45
- "@react-router/dev": "^7.15.1",
45
+ "@react-router/dev": "^8.2.0",
46
46
  "@tailwindcss/vite": "^4.3.0",
47
47
  "@types/node": "^24.12.0",
48
48
  "@types/react": "^19.2.14",
@@ -52,5 +52,8 @@
52
52
  "tw-animate-css": "^1.4.0",
53
53
  "typescript": "5.9.3",
54
54
  "vite": "^8.0.13"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.22.0"
55
58
  }
56
59
  }