fimo 0.10.1-staging.4 → 0.11.0-staging.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/skills/fimo/SKILL.md +4 -0
- package/assets/skills/fimo-code/references/content.md +19 -15
- package/assets/skills/fimo-code/references/ui.md +3 -0
- package/assets/skills/fimo-studio/references/ai-chat.md +8 -0
- package/assets/skills/fimo-studio/references/publish-and-deploy.md +6 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/runtime/content/client/createContentClient.d.ts +15 -0
- package/dist/runtime/content/client/createContentClient.js +23 -0
- package/dist/runtime/content/client/createSingletonClient.js +3 -0
- package/dist/runtime/content/client/internal.js +63 -2
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts +13 -0
- package/dist/runtime/content/utils/retrieve-data-sources.js +45 -31
- package/dist/runtime/primitives/components/Json.d.ts +25 -0
- package/dist/runtime/primitives/components/Json.js +29 -0
- package/dist/runtime/primitives/index.d.ts +1 -0
- package/dist/runtime/primitives/index.js +1 -0
- package/package.json +4 -4
- package/templates/react-router/package.json +6 -6
|
@@ -17,6 +17,10 @@ When you are about to do Fimo work:
|
|
|
17
17
|
1. List what's available: `npx fimo@latest skills list`
|
|
18
18
|
2. Load the relevant skill: `npx fimo@latest skills get <name>` (add `--ref <doc>` for a reference doc)
|
|
19
19
|
|
|
20
|
+
Reference paths mentioned in streamed skill output, such as `references/media.md`, are identifiers,
|
|
21
|
+
not files in the project. Load them through the CLI, for example
|
|
22
|
+
`npx fimo@latest skills get fimo-cli --ref media`. Do not use a filesystem read tool for them.
|
|
23
|
+
|
|
20
24
|
Pick by task:
|
|
21
25
|
|
|
22
26
|
- Writing code — schema-driven JSX, schemas/forms JSON, `t()` labels, `fimo/ui` media → `fimo-code`
|
|
@@ -73,7 +73,7 @@ something else (e.g. `handle`, `permalink`).
|
|
|
73
73
|
| `reference` | `{ "id": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
|
|
74
74
|
| `blocks` | ordered array of objects | Fixed groups, repeated sections, or selectable page blocks |
|
|
75
75
|
|
|
76
|
-
A `date` field
|
|
76
|
+
A `date` field generates as `string` in the TS client and wraps in `FimoString` at read time — the same source-tracked shape as `string`/`text` — so `<Date value={post.someDate} />` and plain string methods (`.slice()`, template literals, …) both work directly.
|
|
77
77
|
|
|
78
78
|
## Enumerated strings
|
|
79
79
|
|
|
@@ -167,6 +167,7 @@ A field may declare `showIf` (every clause must match) or `showIfAny` (one claus
|
|
|
167
167
|
- `min` and `max` constrain the total number of blocks. Use `min: 1` to require at least one block; `required: true` has the same minimum-one behavior.
|
|
168
168
|
- Fimo creates a stable `_key` for each item. A `oneOf` item also carries `_type`, matching the variant `uid`.
|
|
169
169
|
- Nested fields support the same defaults and validators as top-level fields.
|
|
170
|
+
- **Each block item's own scalar fields wrap the same way their field type wraps at the top level** — a nested `string`/`text` field is a `FimoString`, a nested `richtext` field is a `FimoRichText`, a nested `media` field is a `FimoMedia`, a nested `boolean` is a `FimoBoolean`, a nested `date` is a `FimoString`, a nested `json` field is a shallow-cloned, source-tagged object, a nested `number` stays a plain `number`, and a nested `blocks` field recurses the same way. Pass those nested values to the matching `fimo/ui` component exactly like a top-level field — `<Text value={section.heading} />` or `<Json value={section.metadata} />` inside a `.map()` over `page.sections` stays inline-editable.
|
|
170
171
|
- Blocks inherit the owning entry's locale.
|
|
171
172
|
- Nested references use one fixed `target`. Unpopulated reads return `{ id, documentId }`, like top-level references.
|
|
172
173
|
- Nested population and filtering are not supported yet.
|
|
@@ -516,22 +517,25 @@ Populate is depth-one. It only works for reference fields; trying to populate a
|
|
|
516
517
|
|
|
517
518
|
The entry objects a hook returns are **not** plain JS values. The generated client wraps each _trackable_ field in a `fimo/ui` primitive that carries source metadata for inline editing. Which fields wrap, and which stay plain:
|
|
518
519
|
|
|
519
|
-
| Schema field type | Field value you get | Notes
|
|
520
|
-
| ----------------- | ----------------------------------- |
|
|
521
|
-
| `string` | `FimoString` | `extends String` — **not** a primitive `string`
|
|
522
|
-
| `text` | `FimoString` | Multiline plain text with the same source tracking as `string`
|
|
523
|
-
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata
|
|
524
|
-
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean`
|
|
525
|
-
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md`
|
|
526
|
-
| `number` | `number` | plain — not wrapped
|
|
527
|
-
| `json` | `Record<string, any>` |
|
|
528
|
-
| `date` | `
|
|
529
|
-
| `
|
|
520
|
+
| Schema field type | Field value you get | Notes |
|
|
521
|
+
| ----------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
522
|
+
| `string` | `FimoString` | `extends String` — **not** a primitive `string` |
|
|
523
|
+
| `text` | `FimoString` | Multiline plain text with the same source tracking as `string` |
|
|
524
|
+
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
|
|
525
|
+
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
|
|
526
|
+
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
|
|
527
|
+
| `number` | `number` | plain — not wrapped. A wrapper would break `<span>{post.views}</span>` (React renders a boxed `String` directly but throws on a boxed `Number`), so it stays untracked and unclickable in the editing surfaces |
|
|
528
|
+
| `json` | `Record<string, any>` | the same shape you wrote, plus a hidden (non-enumerable, symbol-keyed) source tag — render with `<Json>`, see below |
|
|
529
|
+
| `date` | `FimoString` | same wrapper as `string`/`text`, **not** `FimoDate` — see below |
|
|
530
|
+
| `blocks` | array of plain objects | the array/objects themselves are plain; each item's own scalar fields are wrapped by their own field type (a nested `string` is still `FimoString`, a nested `number` stays a plain `number`, …) — see "Blocks" above |
|
|
531
|
+
| `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `id`; use `populate` when embedded targets are needed |
|
|
530
532
|
|
|
531
533
|
Rules of thumb:
|
|
532
534
|
|
|
533
|
-
- **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
|
|
535
|
+
- **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`, `<Date value={post.publishDate} />`, `<Json value={post.metadata} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
|
|
534
536
|
- **`FimoString extends String`**, so `typeof post.title === 'object'` and `post.title === 'Hi'` is `false`. It stringifies fine inside JSX and template literals; call `.toString()` only when something genuinely needs a primitive `string`.
|
|
537
|
+
- **`number` fields are the one scalar type left unwrapped** — `post.views` is a plain `number`, so `{post.views}` renders directly in JSX, `typeof`/`===`/arithmetic all behave normally, and there is no `<Number>` component. This is deliberate, not a gap: wrapping it would break direct JSX rendering in every existing site.
|
|
538
|
+
- **`json` fields are a shallow clone of what you wrote**, tagged with a hidden source symbol — not the original object, and not a class instance. Reading it behaves exactly like plain JSON (`JSON.stringify` round-trips it unchanged; `for...in`/`Object.keys` see only your own keys). Render it with `<Json value={post.metadata} />` (default renders as a `<span>`; pass `as="pre"` for a formatted block, or a children render function for specific keys) so it stays inline-editable and live-updates in the dashboard preview like every other field.
|
|
535
539
|
- **Never hand-construct** a `FimoString` / `FimoMedia` / etc. — that bypasses source tracking. For a literal string use `t()`; for a literal image use `<StaticImage>`.
|
|
536
540
|
- `t()` also returns a `FimoString` (see `references/labels.md`).
|
|
537
541
|
|
|
@@ -553,7 +557,7 @@ class FimoBoolean {
|
|
|
553
557
|
}
|
|
554
558
|
```
|
|
555
559
|
|
|
556
|
-
> **`FimoDate`:** exported from `fimo/ui` (it's the `<Date>` component's value
|
|
560
|
+
> **`FimoDate`:** exported from `fimo/ui` (it's one of the `<Date>` component's accepted value types), but the generated schema client does **not** wrap `date` fields in it — a `date` field wraps as `FimoString` instead (see the table above), because the generated TypeScript type for a `date` field is `string`, and `FimoString` is what keeps that contract's string methods (`.slice()`, `.includes()`, …) working. `<Date value={post.someDate} />` accepts a `FimoString`, a `FimoDate`, a raw `Date`, or a raw string — all four render correctly.
|
|
557
561
|
|
|
558
562
|
## UI code pattern
|
|
559
563
|
|
|
@@ -579,7 +583,7 @@ export function BlogList() {
|
|
|
579
583
|
}
|
|
580
584
|
```
|
|
581
585
|
|
|
582
|
-
For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` components and the rules of thumb, see `references/ui.md`.
|
|
586
|
+
For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` / `<Json>` components and the rules of thumb, see `references/ui.md`.
|
|
583
587
|
|
|
584
588
|
## Read path (published app, no auth)
|
|
585
589
|
|
|
@@ -17,6 +17,7 @@ Use these to render content-type field values consistently. They carry source me
|
|
|
17
17
|
| `<Video>` | Schema `media` video fields | `<Video value={post.heroVideo} />` |
|
|
18
18
|
| `<Date>` | Schema date fields | `<Date value={post.publishDate} />` |
|
|
19
19
|
| `<Boolean>` | Schema boolean fields | `<Boolean value={product.inStock} />` |
|
|
20
|
+
| `<Json>` | Schema `json` fields | `<Json value={post.metadata} as="pre" />` |
|
|
20
21
|
| `` fimo`...` `` | Compose tracked values (mix `t()` + schema text) | ``<Text value={fimo`${t('by')} ${post.author}`} />`` |
|
|
21
22
|
| `useLabels()` | `t('key')` hook | see `references/labels.md` |
|
|
22
23
|
| `useLocale()` | Active locale and route locale metadata | see `references/locales.md` |
|
|
@@ -29,6 +30,7 @@ Use these to render content-type field values consistently. They carry source me
|
|
|
29
30
|
4. **Label text → `<Text value={t('key')} />`**, not `<h1>{t(...)}</h1>`. Label values live in the DB for `i18n.defaultLocale` from `fimo-config.json` unless a non-default locale is explicitly targeted. See `references/labels.md`.
|
|
30
31
|
5. **Mixed schema + translated text** → the `` fimo`...` `` template literal inside `<Text>`.
|
|
31
32
|
6. **Never hand-roll richtext rendering** — always `<RichText>`. To restyle a node, pass `components={{ heading: ..., link: ..., ... }}`.
|
|
33
|
+
7. **Schema `json` → `<Json>`**, not a hand-rolled `<pre data-fimo-source={getFimoSource(post.metadata)}>{JSON.stringify(post.metadata)}</pre>`. Default renders as a `<span>`; pass `as="pre"` (with a `whitespace-pre-wrap` class) for a formatted block, or a children render function for custom display of specific keys.
|
|
32
34
|
|
|
33
35
|
## App code conventions
|
|
34
36
|
|
|
@@ -78,6 +80,7 @@ The template uses **Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + rea
|
|
|
78
80
|
|
|
79
81
|
- Raw `<a href>` for internal nav → use `<FimoLink>` for locale-aware paths or React Router `<Link>` / `<NavLink>` only when intentionally bypassing Fimo locale helpers.
|
|
80
82
|
- Raw `<img>` for schema media → use `<Image>` (with width/height).
|
|
83
|
+
- Raw `<pre data-fimo-source={getFimoSource(...)}>` for schema `json` fields → use `<Json>`.
|
|
81
84
|
- Hardcoded strings in JSX → wrap in `t('key')` and add the DB value with `fimo labels set`.
|
|
82
85
|
- Calling `t()` with a dynamic first argument → key must be a string literal.
|
|
83
86
|
- Editing generated `.ts` files under `src/schemas/` or `src/forms/` → regenerate them through `fimo validate` or the matching push command.
|
|
@@ -33,6 +33,14 @@ To halt the AI mid-generation, use the **Stop button** in the input bar — whil
|
|
|
33
33
|
|
|
34
34
|
While the AI works, you see small badges in the chat showing what it is doing. Each badge displays a tool name and its current status. Examples include reading files, writing code, searching your project, or generating images. These badges help you follow along without needing to understand the technical details.
|
|
35
35
|
|
|
36
|
+
## Reviewing Changes That Could Not Merge
|
|
37
|
+
|
|
38
|
+
Chat changes are prepared separately and then applied to the environment you are viewing. Most changes merge automatically.
|
|
39
|
+
|
|
40
|
+
If the project changed in the same place while the AI was working, the response shows a **Review changes** card instead. Open it to inspect the proposal, resolve any conflicts, and merge when it is ready. Until you merge it, the proposal is not part of the environment and later chat messages will not see it.
|
|
41
|
+
|
|
42
|
+
Discarding the merge leaves the current environment unchanged and keeps the proposal separate.
|
|
43
|
+
|
|
36
44
|
## Tips for Better Results
|
|
37
45
|
|
|
38
46
|
- **Be specific**: Mention colors, layout, text content, and spacing when you can.
|
|
@@ -16,6 +16,12 @@ To push your latest changes live, open the publish panel from the header **Publi
|
|
|
16
16
|
|
|
17
17
|
> **Tip:** Before your site has ever been published, the chat shows a one-click **Publish** button that deploys immediately without opening the panel.
|
|
18
18
|
|
|
19
|
+
## Publishing Changes That Need Review
|
|
20
|
+
|
|
21
|
+
If an AI chat change cannot merge automatically, use the response's **Review changes** card first. Resolve the conflicts and merge the proposal into `main`.
|
|
22
|
+
|
|
23
|
+
When the AI requested publication, the review dialog offers **Merge and publish** after the conflicts are resolved. Publishing starts only after the merge succeeds, so unresolved or discarded proposals never reach the live site.
|
|
24
|
+
|
|
19
25
|
## What You See When You Click Publish
|
|
20
26
|
|
|
21
27
|
The header **Publish** button itself only shows the label "Publish" (or a spinner while a deploy is running). Open the panel to see the current state:
|
package/dist/cli/bundle.json
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -587,7 +587,7 @@ ${V2(this.state)} ${opts.message}
|
|
|
587
587
|
${removed.map(r=>` ${r.skill} \u2192 ${r.agent}`).join(`
|
|
588
588
|
`)}`,"render")})}function listReferenceDocs(skillDir){let refsDir=path31.join(skillDir,"references");if(!fs31.existsSync(refsDir))return[];let out=[],walk=__name((cur,prefix)=>{for(let entry of fs31.readdirSync(cur,{withFileTypes:!0})){let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?walk(path31.join(cur,entry.name),rel):entry.isFile()&&entry.name.endsWith(".md")&&out.push(rel.slice(0,-3))}},"walk");return walk(refsDir,""),out.sort()}function discoverBundledSkills(){if(!fs31.existsSync(SKILLS_ROOT))return[];let skills2=[];for(let entry of fs31.readdirSync(SKILLS_ROOT,{withFileTypes:!0})){if(!entry.isDirectory()||entry.name===BOOTSTRAP_SKILL_NAME)continue;let dir=path31.join(SKILLS_ROOT,entry.name),skillMd=path31.join(dir,"SKILL.md");if(!fs31.existsSync(skillMd))continue;let{data:data2}=parseFrontmatter(fs31.readFileSync(skillMd,"utf8")),description=typeof data2.description=="string"?data2.description.trim():"";skills2.push({name:entry.name,description,refs:listReferenceDocs(dir),dir})}return skills2.sort((a,b)=>a.name.localeCompare(b.name))}function firstLine(text3){let line=text3.split(`
|
|
589
589
|
`,1)[0]?.trim()??"";return line.length>80?`${line.slice(0,79)}\u2026`:line}function skillsListCommand(){let skills2=discoverBundledSkills();if(skills2.length===0){ui.error({command:"skills list",code:"BUNDLED_SKILL_MISSING",message:`No bundled skills found at ${SKILLS_ROOT}.`,hint:"Try reinstalling fimo, or report this as a bug."});return}ui.result({command:"skills list",data:{skills:skills2.map(s=>({name:s.name,description:s.description,refs:s.refs}))},render:__name(()=>{let nameWidth=Math.max(4,...skills2.map(s=>s.name.length)),header=`${"NAME".padEnd(nameWidth)} REFS DESCRIPTION`,lines=skills2.map(s=>{let refs=String(s.refs.length).padStart(4);return`${s.name.padEnd(nameWidth)} ${refs} ${firstLine(s.description)}`});return[header,...lines].join(`
|
|
590
|
-
`)},"render"),hint:"Run `fimo skills get <name>` for a skill, or add `--ref <doc>` for a reference."})}function normalizeRef(ref){return ref.trim().replace(/\\/g,"/").replace(/^references\//,"").replace(/\.md$/,"")}function skillsGetCommand(name,options={}){let skills2=discoverBundledSkills(),skill=skills2.find(s=>s.name===name);if(!skill){ui.error({command:"skills get",code:"SKILL_NOT_FOUND",message:`Unknown skill "${name}".`,hint:skills2.length?`Available: ${skills2.map(s=>s.name).join(", ")}. Run \`fimo skills list\`.`:"No bundled skills found \u2014 try reinstalling fimo."});return}let targetPath,refId=null;if(options.ref){refId=normalizeRef(options.ref);let refsRoot=path31.resolve(skill.dir,"references"),candidate=path31.resolve(refsRoot,`${refId}.md`);if(!(candidate===refsRoot||candidate.startsWith(refsRoot+path31.sep))||!fs31.existsSync(candidate)){ui.error({command:"skills get",code:"SKILL_REF_NOT_FOUND",message:`Reference "${options.ref}" not found in skill "${skill.name}".`,hint:skill.refs.length?`Available refs: ${skill.refs.join(", ")}.`:"This skill has no reference docs."});return}targetPath=candidate}else targetPath=path31.join(skill.dir,"SKILL.md");let content2=fs31.readFileSync(targetPath,"utf8");ui.result({command:"skills get",data:{skill:skill.name,ref:refId,path:targetPath,content:content2},render:__name(()=>content2,"render")})}var SKILLS_ROOT,BOOTSTRAP_SKILL_NAME,BOOTSTRAP_SKILL_SOURCE,STALE_GLOBAL_SKILL_NAMES,DETECTABLE_FIMO_SKILL_NAMES,init_skills=__esm({"src/cli/commands/skills.ts"(){init_dist5();init_dist();init_dist7();init_config();init_agents();SKILLS_ROOT=path31.resolve(CLI_ROOT,"assets","skills"),BOOTSTRAP_SKILL_NAME="fimo",BOOTSTRAP_SKILL_SOURCE=path31.join(SKILLS_ROOT,BOOTSTRAP_SKILL_NAME),STALE_GLOBAL_SKILL_NAMES=["fimo-cli","fimo-studio","fimo-migration","fimo-migrate","fimo-source-migrate"],DETECTABLE_FIMO_SKILL_NAMES=[BOOTSTRAP_SKILL_NAME,...STALE_GLOBAL_SKILL_NAMES];__name(isPrettyMode,"isPrettyMode");__name(runSkillsInstall,"runSkillsInstall");__name(renderSkillsInstall,"renderSkillsInstall");__name(skillsInstallCommand,"skillsInstallCommand");__name(skillsSyncCommand,"skillsSyncCommand");__name(detectAgentsWithFimoSkills,"detectAgentsWithFimoSkills");__name(removeDirIfExists,"removeDirIfExists");__name(removeGlobalSkillDirs,"removeGlobalSkillDirs");__name(hasGlobalSkillInstalled,"hasGlobalSkillInstalled");__name(removeCanonicalSkillDirs,"removeCanonicalSkillDirs");__name(cleanupStaleGlobalSkills,"cleanupStaleGlobalSkills");__name(skillsUninstallCommand,"skillsUninstallCommand");__name(listReferenceDocs,"listReferenceDocs");__name(discoverBundledSkills,"discoverBundledSkills");__name(firstLine,"firstLine");__name(skillsListCommand,"skillsListCommand");__name(normalizeRef,"normalizeRef");__name(skillsGetCommand,"skillsGetCommand")}});import{execFile as execFile8,spawnSync as spawnSync2}from"node:child_process";import path32 from"node:path";import{promisify as promisify8}from"node:util";function explainCreateProjectError(error){return error instanceof ApiRequestError&&error.detail.includes('insert into "projects"')&&(error.detail.includes('"subdomain"')||error.detail.includes('"subdomain_updated_at"'))?["Project creation failed because the local API database schema looks out of date.","The server is trying to write the `projects.subdomain` fields, but your database likely has not applied the latest migration yet.","Run `cd apps/api && pnpm db:migrate`, restart the API if needed, then retry."].join(`
|
|
590
|
+
`)},"render"),hint:"Run `fimo skills get <name>` for a skill, or add `--ref <doc>` for a reference."})}function normalizeRef(ref){return ref.trim().replace(/\\/g,"/").replace(/^references\//,"").replace(/\.md$/,"")}function skillsGetCommand(name,options={}){let skills2=discoverBundledSkills(),skill=skills2.find(s=>s.name===name);if(!skill){ui.error({command:"skills get",code:"SKILL_NOT_FOUND",message:`Unknown skill "${name}".`,hint:skills2.length?`Available: ${skills2.map(s=>s.name).join(", ")}. Run \`fimo skills list\`.`:"No bundled skills found \u2014 try reinstalling fimo."});return}let targetPath,refId=null;if(options.ref){refId=normalizeRef(options.ref);let refsRoot=path31.resolve(skill.dir,"references"),candidate=path31.resolve(refsRoot,`${refId}.md`);if(!(candidate===refsRoot||candidate.startsWith(refsRoot+path31.sep))||!fs31.existsSync(candidate)){ui.error({command:"skills get",code:"SKILL_REF_NOT_FOUND",message:`Reference "${options.ref}" not found in skill "${skill.name}".`,hint:skill.refs.length?`Available refs: ${skill.refs.join(", ")}.`:"This skill has no reference docs."});return}targetPath=candidate}else targetPath=path31.join(skill.dir,"SKILL.md");let content2=fs31.readFileSync(targetPath,"utf8");ui.result({command:"skills get",data:{skill:skill.name,ref:refId,path:targetPath,content:content2},render:__name(()=>content2,"render"),hint:STREAMED_REFERENCE_HINT})}var SKILLS_ROOT,BOOTSTRAP_SKILL_NAME,BOOTSTRAP_SKILL_SOURCE,STREAMED_REFERENCE_HINT,STALE_GLOBAL_SKILL_NAMES,DETECTABLE_FIMO_SKILL_NAMES,init_skills=__esm({"src/cli/commands/skills.ts"(){init_dist5();init_dist();init_dist7();init_config();init_agents();SKILLS_ROOT=path31.resolve(CLI_ROOT,"assets","skills"),BOOTSTRAP_SKILL_NAME="fimo",BOOTSTRAP_SKILL_SOURCE=path31.join(SKILLS_ROOT,BOOTSTRAP_SKILL_NAME),STREAMED_REFERENCE_HINT="Reference paths in this document are identifiers, not project files. Load one with `fimo skills get <skill> --ref <reference>`.",STALE_GLOBAL_SKILL_NAMES=["fimo-cli","fimo-studio","fimo-migration","fimo-migrate","fimo-source-migrate"],DETECTABLE_FIMO_SKILL_NAMES=[BOOTSTRAP_SKILL_NAME,...STALE_GLOBAL_SKILL_NAMES];__name(isPrettyMode,"isPrettyMode");__name(runSkillsInstall,"runSkillsInstall");__name(renderSkillsInstall,"renderSkillsInstall");__name(skillsInstallCommand,"skillsInstallCommand");__name(skillsSyncCommand,"skillsSyncCommand");__name(detectAgentsWithFimoSkills,"detectAgentsWithFimoSkills");__name(removeDirIfExists,"removeDirIfExists");__name(removeGlobalSkillDirs,"removeGlobalSkillDirs");__name(hasGlobalSkillInstalled,"hasGlobalSkillInstalled");__name(removeCanonicalSkillDirs,"removeCanonicalSkillDirs");__name(cleanupStaleGlobalSkills,"cleanupStaleGlobalSkills");__name(skillsUninstallCommand,"skillsUninstallCommand");__name(listReferenceDocs,"listReferenceDocs");__name(discoverBundledSkills,"discoverBundledSkills");__name(firstLine,"firstLine");__name(skillsListCommand,"skillsListCommand");__name(normalizeRef,"normalizeRef");__name(skillsGetCommand,"skillsGetCommand")}});import{execFile as execFile8,spawnSync as spawnSync2}from"node:child_process";import path32 from"node:path";import{promisify as promisify8}from"node:util";function explainCreateProjectError(error){return error instanceof ApiRequestError&&error.detail.includes('insert into "projects"')&&(error.detail.includes('"subdomain"')||error.detail.includes('"subdomain_updated_at"'))?["Project creation failed because the local API database schema looks out of date.","The server is trying to write the `projects.subdomain` fields, but your database likely has not applied the latest migration yet.","Run `cd apps/api && pnpm db:migrate`, restart the API if needed, then retry."].join(`
|
|
591
591
|
`):error instanceof Error?error.message:String(error)}async function scaffoldNewProject(args){let{api,cfg,targetDir,displayName,options}=args,organizationId=await resolveOrganization(api,options.org),createSpinner=spinner("Creating Fimo project"),project;try{project=await api.createProject({organizationId,displayName:displayName??path32.basename(targetDir)})}catch(error){throw createSpinner.fail("Project creation failed"),new ScaffoldError("PROJECT_CREATE_FAILED",explainCreateProjectError(error))}createSpinner.succeed(`Project ${project.displayName??project.name} created (${project.id})`);let releaseAbort=installAbortHandler(api,project.id,project.name);try{return{scaffold:await scaffoldIntoProject({project,api,cfg,targetDir,options}),releaseAbort}}catch(error){throw releaseAbort(),error}}async function scaffoldIntoProject(args){let{project,api,cfg,targetDir,options}=args,remoteSp=spinner("Fetching git remote"),remote=await api.getGitRemote(project.id,MAIN_BRANCH);remoteSp.succeed("Remote ready");let selectedAgents;try{selectedAgents=await selectAgents(options,{promptMessage:"Which agents do you want to set up?",detectFn:detectAgentsWithFimoSkills})}catch(err){throw new ScaffoldError("AGENT_SELECTION_FAILED",err instanceof Error?err.message:String(err))}let scaffoldSp=spinner("Scaffolding template"),templateRoot=options.templateRoot?path32.resolve(options.templateRoot):process.env.FIMO_TEMPLATE_ROOT?path32.resolve(process.env.FIMO_TEMPLATE_ROOT):resolveTemplateRoot();await copyTemplate(templateRoot,targetDir),seedSiteName({targetDir,project}),writeLocalProjectFiles({targetDir,project,apiUrl:cfg.apiUrl,tenantApiUrl:remote.tenantApiUrl});let gitSp=spinner("Initializing git repository");await runGit(["init","-b",MAIN_BRANCH],targetDir),await ensureFimoRemote(targetDir,remote.url),gitSp.succeed("Git initialized with `fimo` remote"),scaffoldSp.succeed("Template scaffolded");let relativeTarget=path32.relative(process.cwd(),targetDir)||".";return{project,targetDir,relativeTarget,selectedAgents,tenantApiUrl:remote.tenantApiUrl}}async function finalizeInitialCommit(args){let{targetDir}=args,gitSp=spinner("Creating the initial commit");await runGit(["add","-A"],targetDir),await runGit(["commit","-m","chore: scaffold Fimo project"],targetDir).catch(()=>{}),gitSp.succeed("Initial commit created");let pushSp=spinner("Pushing initial commit to Fimo");try{return await runGit(["push","fimo",`HEAD:${MAIN_BRANCH}`],targetDir),pushSp.succeed("Initial commit pushed"),{pushedInitial:!0}}catch(error){pushSp.fail("Initial push failed");let failure=classifyPushError(error),retryHint=failure.kind==="auth"?"Sign in with `fimo login`, then run `fimo deploy` to push it.":"Run `fimo deploy` later to retry.";return O2.error(`Could not push the initial commit. ${retryHint}
|
|
592
592
|
${failure.stderr||""}`.trim()),{pushedInitial:!1}}}function installAbortHandler(api,projectId,projectName){let cleaningUp=!1,handler=__name(()=>{cleaningUp&&(process.stderr.write(`
|
|
593
593
|
Force-exiting. Project may not have been fully cleaned up.
|
|
@@ -1150,7 +1150,7 @@ This breaks source tracking for inline editing. Translation keys must be statica
|
|
|
1150
1150
|
\u274C Found ${errors.length} error(s) in ${reactFiles.length} files.
|
|
1151
1151
|
`),process.exit(1)}}import fs21 from"node:fs";import path24 from"node:path";var SCAN_EXTENSIONS=[".ts",".tsx",".js",".jsx",".mjs"],SCAN_EXTENSIONS_SET=new Set(SCAN_EXTENSIONS),SKIP_DIRS=new Set(["node_modules","dist",".next",".fimo",".git","public"]),GLOBAL_PATTERNS=[/\.css$/,/\.scss$/,/tailwind\.config/,/postcss\.config/,/vite\.config/,/tsconfig/,/package\.json$/,/src\/main\.[tj]sx?$/,/src\/index\.[tj]sx?$/,/src\/root\.[tj]sx?$/,/react-router\.config/];function isGlobalFile(filePath){return GLOBAL_PATTERNS.some(p2=>p2.test(filePath))}__name(isGlobalFile,"isGlobalFile");function collectFiles(dir){let files=[];if(!fs21.existsSync(dir))return files;let entries2=fs21.readdirSync(dir,{withFileTypes:!0});for(let entry of entries2){if(SKIP_DIRS.has(entry.name))continue;let fullPath=path24.join(dir,entry.name);entry.isDirectory()?files.push(...collectFiles(fullPath)):SCAN_EXTENSIONS_SET.has(path24.extname(entry.name))&&files.push(fullPath)}return files}__name(collectFiles,"collectFiles");function extractImports(content2){let imports=[],staticRegex=/(?:import|export)\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;for(let match of content2.matchAll(staticRegex))imports.push(match[1]);let dynamicRegex=/import\(\s*['"]([^'"]+)['"]\s*\)/g;for(let match of content2.matchAll(dynamicRegex))imports.push(match[1]);let requireRegex=/require\(\s*['"]([^'"]+)['"]\s*\)/g;for(let match of content2.matchAll(requireRegex))imports.push(match[1]);return imports.filter(s=>s.startsWith(".")||s.startsWith("@/"))}__name(extractImports,"extractImports");function resolveImport(specifier,fromDir,srcDir){let targetPath;if(specifier.startsWith("@/"))targetPath=path24.resolve(srcDir,specifier.slice(2));else if(specifier.startsWith("."))targetPath=path24.resolve(fromDir,specifier);else return null;if(fs21.existsSync(targetPath)&&fs21.statSync(targetPath).isFile())return targetPath;for(let ext of SCAN_EXTENSIONS){let withExt=targetPath+ext;if(fs21.existsSync(withExt))return withExt}for(let ext of SCAN_EXTENSIONS){let indexFile=path24.join(targetPath,"index"+ext);if(fs21.existsSync(indexFile))return indexFile}return null}__name(resolveImport,"resolveImport");function discoverRoutes(srcDir,root2){let routesFilePath=path24.resolve(srcDir,"routes.ts");if(!fs21.existsSync(routesFilePath))return new Map;let source=fs21.readFileSync(routesFilePath,"utf-8"),parsed=parseRoutesSource(source),fileToRoutes=new Map;for(let r of parsed){if(r.isLayout||!r.path||!r.file)continue;let resolved=resolveImport(r.file,srcDir,srcDir);if(!resolved)continue;let rel=path24.relative(root2,resolved);fileToRoutes.has(rel)||fileToRoutes.set(rel,new Set),fileToRoutes.get(rel).add(r.path)}return fileToRoutes}__name(discoverRoutes,"discoverRoutes");function buildReverseImportGraph(srcDir,root2){let fileToImporters=new Map,allFiles=collectFiles(srcDir);for(let filePath of allFiles){let content2;try{content2=fs21.readFileSync(filePath,"utf-8")}catch{continue}let imports=extractImports(content2);for(let specifier of imports){let resolved=resolveImport(specifier,path24.dirname(filePath),srcDir);if(!resolved)continue;let resolvedRel=path24.relative(root2,resolved),importerRel=path24.relative(root2,filePath);fileToImporters.has(resolvedRel)||fileToImporters.set(resolvedRel,new Set),fileToImporters.get(resolvedRel).add(importerRel)}}return fileToImporters}__name(buildReverseImportGraph,"buildReverseImportGraph");function resolveAffectedRoutes(file,fileToImporters,fileToRoutes){let visited=new Set,queue=[file],affectedRoutes=new Set;for(;queue.length>0;){let current=queue.shift();if(visited.has(current))continue;visited.add(current);let routes=fileToRoutes.get(current);if(routes)for(let route of routes)affectedRoutes.add(route);let importers=fileToImporters.get(current);if(importers)for(let importer of importers)visited.has(importer)||queue.push(importer)}return Array.from(affectedRoutes)}__name(resolveAffectedRoutes,"resolveAffectedRoutes");function resolveRoutes(changedFiles,options={}){let root2=options.cwd??process.cwd(),srcDir=path24.resolve(root2,"src"),normalizedChangedFiles=changedFiles.map(f=>f.replace(/^\/+/,""));if(normalizedChangedFiles.length===0)return{routeMap:{},routes:[],unresolvedFiles:[]};let fileToRoutes=discoverRoutes(srcDir,root2),fileToImporters=buildReverseImportGraph(srcDir,root2),routeMap={},unresolvedFiles=[];for(let file of normalizedChangedFiles){if(isGlobalFile(file)){routeMap[file]=["*"];continue}let routes=resolveAffectedRoutes(file,fileToImporters,fileToRoutes);routes.length>0?routeMap[file]=routes:unresolvedFiles.push(file)}let allRoutes=[...new Set(Object.values(routeMap).flat().filter(r=>r!=="*"))];return{routeMap,routes:allRoutes,unresolvedFiles}}__name(resolveRoutes,"resolveRoutes");init_sync_forms();init_sync_schemas();function resolveCwd(options){return options.cwd?resolve6(options.cwd):process.cwd()}__name(resolveCwd,"resolveCwd");function renderPrettyJson4(data2){return JSON.stringify(data2,null,2)}__name(renderPrettyJson4,"renderPrettyJson");function syncSchemasCommand(options){let result=syncSchemas({cwd:resolveCwd(options)});ui.result({command:"scripts sync-schemas",data:result,render:__name(()=>renderPrettyJson4(result),"render")})}__name(syncSchemasCommand,"syncSchemasCommand");function syncFormsCommand(options){let result=syncForms({cwd:resolveCwd(options)});ui.result({command:"scripts sync-forms",data:result,render:__name(()=>renderPrettyJson4(result),"render")})}__name(syncFormsCommand,"syncFormsCommand");async function exportStaticCommand(options){let result=await exportStatic({cwd:resolveCwd(options)});ui.result({command:"export-static",data:result??{ok:!0},render:__name(()=>renderPrettyJson4(result??{ok:!0}),"render")})}__name(exportStaticCommand,"exportStaticCommand");function validateRouteMetadataCommand(options){let result=validateRouteMetadata({cwd:resolveCwd(options)});result.errors.length>0&&(ui.error({command:"scripts validate-route-metadata",code:"ROUTE_METADATA_INVALID",message:`Route metadata validation failed (${result.errors.length} issue(s)):
|
|
1152
1152
|
${result.errors.map(e=>` - ${e}`).join(`
|
|
1153
|
-
`)}`}),process.exit(1)),ui.ok({command:"scripts validate-route-metadata",headline:`Route metadata validation passed (${result.routesChecked} route${result.routesChecked===1?"":"s"}).`,details:{routes_checked:result.routesChecked}})}__name(validateRouteMetadataCommand,"validateRouteMetadataCommand");function extractTranslationsCommand(options){let cwd=resolveCwd(options),data2={keys:extractTranslations({cwd}).keys.length};ui.result({command:"scripts extract-translations",data:data2,render:__name(()=>JSON.stringify(data2),"render")})}__name(extractTranslationsCommand,"extractTranslationsCommand");function resolveRoutesCommand(changedFiles,options){let cwd=resolveCwd(options),files=[];if(changedFiles.length===1&&changedFiles[0].startsWith("{"))try{files=JSON.parse(changedFiles[0]).files??[]}catch{files=[]}else files=changedFiles;let output=resolveRoutes(files,{cwd});ui.result({command:"resolve-routes",data:output,render:__name(()=>JSON.stringify(output),"render")})}__name(resolveRoutesCommand,"resolveRoutesCommand");function lintProject(opts){let files=opts.targetPath?[resolve6(opts.cwd,opts.targetPath)]:collectFiles2(resolve6(opts.cwd,"src")),errors=[],filesChecked=0;for(let file of files)if(/\.(tsx?|jsx?)$/.test(file))try{let code2=readFileSync8(file,"utf-8");errors.push(...lintTranslationKeys(file,code2)),filesChecked++}catch{}return{errors,filesChecked}}__name(lintProject,"lintProject");function lintCommand(targetPath,options){let cwd=resolveCwd(options),result=lintProject({cwd,targetPath});ui.result({command:"scripts lint",data:result,render:__name(()=>JSON.stringify(result),"render")}),result.errors.length>0&&process.exit(1)}__name(lintCommand,"lintCommand");function collectFiles2(root2){if(!existsSync7(root2))return[];let results=[];for(let name of readdirSync6(root2)){if(name==="node_modules"||name==="dist"||name===".git")continue;let full=join9(root2,name);statSync3(full).isDirectory()?results.push(...collectFiles2(full)):results.push(full)}return results}__name(collectFiles2,"collectFiles");async function validateCommand(options,cmd){let cwd=resolveProjectCwd(options.cwd),envOverride2=envOverrideFromCommand(cmd),report=await runValidate({cwd,envOverride:envOverride2}),warnings=[getDbLabelsRuntimeWarning(cwd)].filter(warning=>!!warning);ui.result({command:"validate",data:report,render:__name(()=>formatReportHuman(report),"render"),warnings:warnings.length>0?warnings:void 0}),report.ok||(process.exitCode=report.exitCode)}__name(validateCommand,"validateCommand");async function runValidate(opts){let{cwd,envOverride:envOverride2}=opts,checks=await runChecksPhase({cwd}),codegen,labelKeys=null;if(checks.errors.length>0)codegen=skippedPhase("Codegen","static checks failed");else{let codegenResult=await runCodegenPhase({cwd});codegen=codegenResult.phase,labelKeys=codegenResult.labelKeys}let sync;return checks.errors.length>0?sync=skippedPhase("Labels","static checks failed"):labelKeys===null?sync=skippedPhase("Labels","label key scan failed"):sync=await runLabelsPhase({cwd,labelKeys,envOverride:envOverride2}),finalizeReport({phases:{checks,codegen,sync}})}__name(runValidate,"runValidate");async function runChecksPhase(opts){let{cwd}=opts,[lintResult,routeResult,agentsResult,schemasResult]=await Promise.allSettled([Promise.resolve().then(()=>lintProject({cwd})),Promise.resolve().then(()=>runRouteMetadataCheck({cwd})),Promise.resolve().then(()=>runAgentsCheck({cwd})),Promise.resolve().then(()=>validateSchemas({cwd}))]),errors=[],stats={};if(lintResult.status==="fulfilled"){stats.filesLinted=lintResult.value.filesChecked;for(let err of lintResult.value.errors)errors.push(authorError({rule:`lint/${err.rule}`,message:err.message,file:err.file,line:err.line,column:err.column}))}else errors.push(infraErrorFromException("lint",lintResult.reason));if(routeResult.status==="fulfilled"){if(routeResult.value){stats.routesChecked=routeResult.value.routesChecked;for(let message of routeResult.value.errors)errors.push(authorError({rule:"route-metadata",message}))}}else errors.push(infraErrorFromException("route-metadata",routeResult.reason));if(agentsResult.status==="fulfilled"?(Object.assign(stats,agentsResult.value.stats),errors.push(...agentsResult.value.errors)):errors.push(infraErrorFromException("agents",agentsResult.reason)),schemasResult.status==="fulfilled"){stats.schemasChecked=schemasResult.value.schemasChecked;for(let err of schemasResult.value.errors)errors.push(authorError({rule:err.rule,message:err.message,file:err.file}))}else errors.push(infraErrorFromException("schemas",schemasResult.reason));return{name:"Checks",ok:errors.length===0,errors,stats}}__name(runChecksPhase,"runChecksPhase");async function runAgentsCheck(opts){let{cwd}=opts,report=await runValidateAgents({cwd,json:!0}),errors=[];if(report.policy&&!report.policy.ok)for(let issue of report.policy.issues)errors.push(authorError({rule:"agents/policy",message:issue.message,file:issue.pointer}));for(let bundle of report.bundles)for(let phase of["parse","capabilities","composition","semantics"])for(let issue of bundle.phases[phase].issues)errors.push(authorError({rule:`agents/${phase}`,message:`${bundle.name}: ${issue.message}`,file:issue.pointer}));return{errors,stats:{agentsChecked:report.bundles.length}}}__name(runAgentsCheck,"runAgentsCheck");function runRouteMetadataCheck(opts){return validateRouteMetadata({cwd:opts.cwd})}__name(runRouteMetadataCheck,"runRouteMetadataCheck");async function runCodegenPhase(opts){let{cwd}=opts,[schemasResult,formsResult,labelsResult]=await Promise.allSettled([Promise.resolve().then(()=>syncSchemas({cwd,verbose:!1})),Promise.resolve().then(()=>syncForms({cwd,verbose:!1})),Promise.resolve().then(()=>extractLabelKeys({cwd}))]),errors=[],stats={};schemasResult.status==="fulfilled"?(stats.schemasCreated=schemasResult.value.created,stats.schemasUpdated=schemasResult.value.updated,stats.schemasDeleted=schemasResult.value.deleted):errors.push(infraErrorFromException("sync-schemas",schemasResult.reason)),formsResult.status==="fulfilled"?(stats.formsCreated=formsResult.value.created,stats.formsUpdated=formsResult.value.updated,stats.formsDeleted=formsResult.value.deleted):errors.push(infraErrorFromException("sync-forms",formsResult.reason));let labelKeys=null;return labelsResult.status==="fulfilled"?(labelKeys=labelsResult.value,stats.labelsDeclared=labelKeys.length):errors.push(infraErrorFromException("extract-label-keys",labelsResult.reason)),{phase:{name:"Codegen",ok:errors.length===0,errors,stats},labelKeys}}__name(runCodegenPhase,"runCodegenPhase");async function runLabelsPhase(opts){let{cwd,labelKeys,envOverride:envOverride2}=opts;if(labelKeys.length===0)return{name:"Labels",ok:!0,errors:[],stats:{labelsDeclared:0,labelsMissing:0}};let ctx;try{ctx=await getProjectContext({cwd,envOverride:envOverride2})}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("load-project-settings",error)]}}let api=new FimoApiClient(loadConfig()).withEnv(ctx.env),locale=getConfigServer(cwd).i18n?.defaultLocale??"en",pinnedBranch=(await readProjectEnv(cwd)).VITE_FIMO_BRANCH;try{let labels2;if(pinnedBranch){let branchLabels=await loadPublicTenantLabels(cwd,locale);if(branchLabels==null)return{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",new Error(`Could not read labels from branch "${pinnedBranch}"`))]};labels2=branchLabels}else labels2=labelsToRecord(await api.listLabels(ctx.projectId,locale));let existingKeys=new Set(Object.keys(labels2)),missing=labelKeys.filter(label=>!existingKeys.has(label.key));return{name:"Labels",ok:missing.length===0,errors:missing.map(label=>authorError({rule:"labels/missing",message:`Missing label "${label.key}" for locale "${locale}". Run: fimo labels set ${JSON.stringify(label.key)} --locale ${locale} --value "...".`,file:label.file})),stats:{labelsDeclared:labelKeys.length,labelsMissing:missing.length}}}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",error)]}}}__name(runLabelsPhase,"runLabelsPhase");function labelsToRecord(labels2){return Object.fromEntries(labels2.map(label=>[label.key,label.value??label.defaultValue??""]))}__name(labelsToRecord,"labelsToRecord");async function loadPublicTenantLabels(cwd,locale){let env=await readProjectEnv(cwd),apiUrl=env.VITE_API_URL;if(!apiUrl)return null;try{let url=new URL("/labels",apiUrl);url.searchParams.set("locale",locale);let headers=env.VITE_FIMO_BRANCH?{"X-Fimo-Branch":env.VITE_FIMO_BRANCH}:{},response=await fetchWithTimeout(url,4e3,headers);if(!response.ok)return null;let body=await response.json();return Array.isArray(body.data)?labelsToRecord(body.data):body.data?.labels??null}catch{return null}}__name(loadPublicTenantLabels,"loadPublicTenantLabels");async function fetchWithTimeout(url,timeoutMs,headers){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),timeoutMs);try{return await fetch(url,{signal:controller.signal,headers})}finally{clearTimeout(timeout)}}__name(fetchWithTimeout,"fetchWithTimeout");async function readProjectEnv(cwd){let values={...await readEnvFile(path25.join(cwd,".env")),...await readEnvFile(path25.join(cwd,".env.local"))};return process.env.VITE_API_URL&&(values.VITE_API_URL=process.env.VITE_API_URL),process.env.VITE_FIMO_BRANCH&&(values.VITE_FIMO_BRANCH=process.env.VITE_FIMO_BRANCH),values}__name(readProjectEnv,"readProjectEnv");async function readEnvFile(filePath){try{let content2=await readFile2(filePath,"utf8"),values={};for(let line of content2.split(/\r?\n/)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let index2=trimmed.indexOf("=");if(index2===-1)continue;let key=trimmed.slice(0,index2).trim(),value=trimmed.slice(index2+1).trim().replace(/^['"]|['"]$/g,"");values[key]=value}return values}catch{return{}}}__name(readEnvFile,"readEnvFile");async function deployCommand(options,cmd){let cwd=path26.resolve(options.cwd??process.cwd()),fimoPath=path26.join(cwd,".fimo.settings.json");if(!fs22.existsSync(fimoPath)){ui.error({command:"deploy",code:"NOT_IN_PROJECT",message:"You are not in a Fimo project.",hint:"Run `fimo create` to start one, or cd into a linked project."});return}let local=JSON.parse(fs22.readFileSync(fimoPath,"utf-8"));if(!await isGitRepo(cwd)){ui.error({command:"deploy",code:"NOT_A_GIT_REPO",message:"Not a git repository."});return}let isPretty2=currentMode().format==="pretty";isPretty2&&mt(options.publish?"Deploying and publishing":"Deploying to preview");let envOverride2=envOverrideFromCommand(cmd),ctx=await getProjectContext({cwd,envOverride:envOverride2});if(isPretty2&&process.stderr.write(`${envSourceBanner(ctx)}
|
|
1153
|
+
`)}`}),process.exit(1)),ui.ok({command:"scripts validate-route-metadata",headline:`Route metadata validation passed (${result.routesChecked} route${result.routesChecked===1?"":"s"}).`,details:{routes_checked:result.routesChecked}})}__name(validateRouteMetadataCommand,"validateRouteMetadataCommand");function extractTranslationsCommand(options){let cwd=resolveCwd(options),data2={keys:extractTranslations({cwd}).keys.length};ui.result({command:"scripts extract-translations",data:data2,render:__name(()=>JSON.stringify(data2),"render")})}__name(extractTranslationsCommand,"extractTranslationsCommand");function resolveRoutesCommand(changedFiles,options){let cwd=resolveCwd(options),files=[];if(changedFiles.length===1&&changedFiles[0].startsWith("{"))try{files=JSON.parse(changedFiles[0]).files??[]}catch{files=[]}else files=changedFiles;let output=resolveRoutes(files,{cwd});ui.result({command:"resolve-routes",data:output,render:__name(()=>JSON.stringify(output),"render")})}__name(resolveRoutesCommand,"resolveRoutesCommand");function lintProject(opts){let files=opts.targetPath?[resolve6(opts.cwd,opts.targetPath)]:collectFiles2(resolve6(opts.cwd,"src")),errors=[],filesChecked=0;for(let file of files)if(/\.(tsx?|jsx?)$/.test(file))try{let code2=readFileSync8(file,"utf-8");errors.push(...lintTranslationKeys(file,code2)),filesChecked++}catch{}return{errors,filesChecked}}__name(lintProject,"lintProject");function lintCommand(targetPath,options){let cwd=resolveCwd(options),result=lintProject({cwd,targetPath});ui.result({command:"scripts lint",data:result,render:__name(()=>JSON.stringify(result),"render")}),result.errors.length>0&&process.exit(1)}__name(lintCommand,"lintCommand");function collectFiles2(root2){if(!existsSync7(root2))return[];let results=[];for(let name of readdirSync6(root2)){if(name==="node_modules"||name==="dist"||name===".git")continue;let full=join9(root2,name);statSync3(full).isDirectory()?results.push(...collectFiles2(full)):results.push(full)}return results}__name(collectFiles2,"collectFiles");async function validateCommand(options,cmd){let cwd=resolveProjectCwd(options.cwd),envOverride2=envOverrideFromCommand(cmd),report=await runValidate({cwd,envOverride:envOverride2}),warnings=[getDbLabelsRuntimeWarning(cwd)].filter(warning=>!!warning);ui.result({command:"validate",data:report,render:__name(()=>formatReportHuman(report),"render"),warnings:warnings.length>0?warnings:void 0}),report.ok||(process.exitCode=report.exitCode)}__name(validateCommand,"validateCommand");async function runValidate(opts){let{cwd,envOverride:envOverride2}=opts,checks=await runChecksPhase({cwd}),codegen,labelKeys=null;if(checks.errors.length>0)codegen=skippedPhase("Codegen","static checks failed");else{let codegenResult=await runCodegenPhase({cwd});codegen=codegenResult.phase,labelKeys=codegenResult.labelKeys}let sync;return checks.errors.length>0?sync=skippedPhase("Labels","static checks failed"):labelKeys===null?sync=skippedPhase("Labels","label key scan failed"):sync=await runLabelsPhase({cwd,labelKeys,envOverride:envOverride2}),finalizeReport({phases:{checks,codegen,sync}})}__name(runValidate,"runValidate");async function runChecksPhase(opts){let{cwd}=opts,[lintResult,routeResult,agentsResult,schemasResult]=await Promise.allSettled([Promise.resolve().then(()=>lintProject({cwd})),Promise.resolve().then(()=>runRouteMetadataCheck({cwd})),Promise.resolve().then(()=>runAgentsCheck({cwd})),Promise.resolve().then(()=>validateSchemas({cwd}))]),errors=[],stats={};if(lintResult.status==="fulfilled"){stats.filesLinted=lintResult.value.filesChecked;for(let err of lintResult.value.errors)errors.push(authorError({rule:`lint/${err.rule}`,message:err.message,file:err.file,line:err.line,column:err.column}))}else errors.push(infraErrorFromException("lint",lintResult.reason));if(routeResult.status==="fulfilled"){if(routeResult.value){stats.routesChecked=routeResult.value.routesChecked;for(let message of routeResult.value.errors)errors.push(authorError({rule:"route-metadata",message}))}}else errors.push(infraErrorFromException("route-metadata",routeResult.reason));if(agentsResult.status==="fulfilled"?(Object.assign(stats,agentsResult.value.stats),errors.push(...agentsResult.value.errors)):errors.push(infraErrorFromException("agents",agentsResult.reason)),schemasResult.status==="fulfilled"){stats.schemasChecked=schemasResult.value.schemasChecked;for(let err of schemasResult.value.errors)errors.push(authorError({rule:err.rule,message:err.message,file:err.file}))}else errors.push(infraErrorFromException("schemas",schemasResult.reason));return{name:"Checks",ok:errors.length===0,errors,stats}}__name(runChecksPhase,"runChecksPhase");async function runAgentsCheck(opts){let{cwd}=opts,report=await runValidateAgents({cwd,json:!0}),errors=[];if(report.policy&&!report.policy.ok)for(let issue of report.policy.issues)errors.push(authorError({rule:"agents/policy",message:issue.message,file:issue.pointer}));for(let bundle of report.bundles)for(let phase of["parse","capabilities","composition","semantics"])for(let issue of bundle.phases[phase].issues)errors.push(authorError({rule:`agents/${phase}`,message:`${bundle.name}: ${issue.message}`,file:issue.pointer}));return{errors,stats:{agentsChecked:report.bundles.length}}}__name(runAgentsCheck,"runAgentsCheck");function runRouteMetadataCheck(opts){return validateRouteMetadata({cwd:opts.cwd})}__name(runRouteMetadataCheck,"runRouteMetadataCheck");async function runCodegenPhase(opts){let{cwd}=opts,[schemasResult,formsResult,labelsResult]=await Promise.allSettled([Promise.resolve().then(()=>syncSchemas({cwd,verbose:!1})),Promise.resolve().then(()=>syncForms({cwd,verbose:!1})),Promise.resolve().then(()=>extractLabelKeys({cwd}))]),errors=[],stats={};schemasResult.status==="fulfilled"?(stats.schemasCreated=schemasResult.value.created,stats.schemasUpdated=schemasResult.value.updated,stats.schemasDeleted=schemasResult.value.deleted):errors.push(infraErrorFromException("sync-schemas",schemasResult.reason)),formsResult.status==="fulfilled"?(stats.formsCreated=formsResult.value.created,stats.formsUpdated=formsResult.value.updated,stats.formsDeleted=formsResult.value.deleted):errors.push(infraErrorFromException("sync-forms",formsResult.reason));let labelKeys=null;return labelsResult.status==="fulfilled"?(labelKeys=labelsResult.value,stats.labelsDeclared=labelKeys.length):errors.push(infraErrorFromException("extract-label-keys",labelsResult.reason)),{phase:{name:"Codegen",ok:errors.length===0,errors,stats},labelKeys}}__name(runCodegenPhase,"runCodegenPhase");async function runLabelsPhase(opts){let{cwd,labelKeys,envOverride:envOverride2}=opts;if(labelKeys.length===0)return{name:"Labels",ok:!0,errors:[],stats:{labelsDeclared:0,labelsMissing:0}};let ctx;try{ctx=await getProjectContext({cwd,envOverride:envOverride2})}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("load-project-settings",error)]}}let api=new FimoApiClient(loadConfig()).withEnv(ctx.env),locale=getConfigServer(cwd).i18n?.defaultLocale??"en";try{let labels2=await loadLabelsForValidation({cwd,locale,projectId:ctx.projectId,loadAuthenticatedLabels:__name(()=>api.listLabels(ctx.projectId,locale),"loadAuthenticatedLabels")}),existingKeys=new Set(Object.keys(labels2)),missing=labelKeys.filter(label=>!existingKeys.has(label.key));return{name:"Labels",ok:missing.length===0,errors:missing.map(label=>authorError({rule:"labels/missing",message:`Missing label "${label.key}" for locale "${locale}". Run: fimo labels set ${JSON.stringify(label.key)} --locale ${locale} --value "...".`,file:label.file})),stats:{labelsDeclared:labelKeys.length,labelsMissing:missing.length}}}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",error)]}}}__name(runLabelsPhase,"runLabelsPhase");function labelsToRecord(labels2){return Object.fromEntries(labels2.map(label=>[label.key,label.value??label.defaultValue??""]))}__name(labelsToRecord,"labelsToRecord");async function loadPublicTenantLabels(cwd,locale){let env=await readProjectEnv(cwd),apiUrl=env.VITE_API_URL;if(!apiUrl)return null;try{let url=new URL("/labels",apiUrl);url.searchParams.set("locale",locale);let headers=env.VITE_FIMO_BRANCH?{"X-Fimo-Branch":env.VITE_FIMO_BRANCH}:{},response=await fetchWithTimeout(url,3e4,headers);if(!response.ok)return null;let body=await response.json();return Array.isArray(body.data)?labelsToRecord(body.data):body.data?.labels??null}catch{return null}}__name(loadPublicTenantLabels,"loadPublicTenantLabels");async function loadLabelsForValidation({cwd,locale,projectId,loadAuthenticatedLabels}){let env=await readProjectEnv(cwd);if(!env.VITE_API_URL)return labelsToRecord(await loadAuthenticatedLabels());let labels2=await loadPublicTenantLabels(cwd,locale);if(labels2==null){let source=env.VITE_FIMO_BRANCH?`branch "${env.VITE_FIMO_BRANCH}"`:`project "${projectId}"`;throw new Error(`Could not read labels from ${source}`)}return labels2}__name(loadLabelsForValidation,"loadLabelsForValidation");async function fetchWithTimeout(url,timeoutMs,headers){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),timeoutMs);try{return await fetch(url,{signal:controller.signal,headers})}finally{clearTimeout(timeout)}}__name(fetchWithTimeout,"fetchWithTimeout");async function readProjectEnv(cwd){let values={...await readEnvFile(path25.join(cwd,".env")),...await readEnvFile(path25.join(cwd,".env.local"))};return process.env.VITE_API_URL&&(values.VITE_API_URL=process.env.VITE_API_URL),process.env.VITE_FIMO_BRANCH&&(values.VITE_FIMO_BRANCH=process.env.VITE_FIMO_BRANCH),values}__name(readProjectEnv,"readProjectEnv");async function readEnvFile(filePath){try{let content2=await readFile2(filePath,"utf8"),values={};for(let line of content2.split(/\r?\n/)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let index2=trimmed.indexOf("=");if(index2===-1)continue;let key=trimmed.slice(0,index2).trim(),value=trimmed.slice(index2+1).trim().replace(/^['"]|['"]$/g,"");values[key]=value}return values}catch{return{}}}__name(readEnvFile,"readEnvFile");async function deployCommand(options,cmd){let cwd=path26.resolve(options.cwd??process.cwd()),fimoPath=path26.join(cwd,".fimo.settings.json");if(!fs22.existsSync(fimoPath)){ui.error({command:"deploy",code:"NOT_IN_PROJECT",message:"You are not in a Fimo project.",hint:"Run `fimo create` to start one, or cd into a linked project."});return}let local=JSON.parse(fs22.readFileSync(fimoPath,"utf-8"));if(!await isGitRepo(cwd)){ui.error({command:"deploy",code:"NOT_A_GIT_REPO",message:"Not a git repository."});return}let isPretty2=currentMode().format==="pretty";isPretty2&&mt(options.publish?"Deploying and publishing":"Deploying to preview");let envOverride2=envOverrideFromCommand(cmd),ctx=await getProjectContext({cwd,envOverride:envOverride2});if(isPretty2&&process.stderr.write(`${envSourceBanner(ctx)}
|
|
1154
1154
|
`),ctx.readOnly){ui.error({command:"deploy",code:"ENV_READONLY",message:`Refusing to deploy to read-only env '${ctx.env}'.`,hint:"Switch off the release/* tag or pass --env <writable-env>."});return}if(options.publish&&ctx.env!==DEFAULT_ENV){ui.error({command:"deploy",code:"PUBLISH_REQUIRES_DEFAULT_ENV",message:`Refusing to --publish from env '${ctx.env}'. Publish only runs from '${DEFAULT_ENV}'.`,hint:`Drop --publish to deploy a preview, or switch to '${DEFAULT_ENV}' first.`});return}await requireOperation("fimo.deploy",{projectId:ctx.projectId,env:ctx.env});let cfg=loadConfig(),api=new FimoApiClient(cfg).withEnv(ctx.env),previewEnabled=(await api.getProject(local.projectId)).previewEnabled!==!1,validateSp=spinner("Validating project"),report=await runValidate({cwd,envOverride:envOverride2});if(report.ok)validateSp.succeed("Validation passed");else if(options.force)validateSp.fail("Validation failed \u2014 continuing anyway (--force)"),ui.warn({command:"deploy",message:"Bypassing validation errors because --force was passed. The deploy may still fail later in the sandbox."});else{validateSp.fail("Validation failed"),ui.result({command:"deploy",data:{stage:"validate",report},render:__name(()=>formatReportHuman(report),"render")}),process.exitCode=report.exitCode;return}let devFimoDependency=null;try{devFimoDependency=prepareDevFimoDependencyForDeploy(cwd)}catch(error){ui.error({command:"deploy",code:"DEV_FIMO_VERSION_MISSING",message:errorMessage(error)});return}devFimoDependency&&isPretty2&&O2.info(`Deploying with fimo ${devFimoDependency.version}; restoring local file: dependency after push.`);try{if(await hasUncommittedChanges(cwd)){let message;if(options.message!==void 0)message=options.message;else{assertInteractive('Pass -m "<message>" or commit your changes manually.',"deploy"),isPretty2&&O2.warn("You have uncommitted changes. Commit them before deploying.");let answer=await Ot({message:"Commit message",placeholder:"Deploy",defaultValue:"Deploy"});if(q(answer)){ui.info({command:"deploy",message:"Cancelled."});return}message=String(answer)}let commitSp=spinner("Committing pending changes");await runGit(["add","-A"],cwd),await runGit(["commit","-m",message],cwd),commitSp.succeed("Changes committed")}let remoteSp=spinner("Refreshing git remote URL"),remote=await api.getGitRemote(local.projectId,ctx.env);await ensureFimoRemote(cwd,remote.url);let remoteBranch=remote.branch;remoteSp.succeed(`Remote ready (branch ${remoteBranch})`);let pushSp=spinner(`Pushing to fimo/${remoteBranch}`);try{await runGit(["push","fimo",`HEAD:${remoteBranch}`],cwd),pushSp.succeed("Pushed to Fimo")}catch(error){pushSp.fail("Push failed");let failure=classifyPushError(error);emitPushFailure(failure,remoteBranch);return}}finally{devFimoDependency?.restore()}let stepLabel="",stepStartedAt=0,stepSp=null,finalizeStep=__name((status="ok",failMessage)=>{if(!stepSp)return;let elapsed=((Date.now()-stepStartedAt)/1e3).toFixed(1);status==="ok"?stepSp.succeed(`${stepLabel} (${elapsed}s)`):stepSp.fail(failMessage??`${stepLabel} failed`),stepSp=null},"finalizeStep");try{await api.sandboxPull(local.projectId,({message})=>{finalizeStep("ok"),stepLabel=message,stepStartedAt=Date.now(),stepSp=spinner(message)},ctx.env,{noRestart:options.restart===!1}),finalizeStep("ok")}catch(error){rethrowCliAuthError(error,()=>finalizeStep("fail","Authentication required")),finalizeStep("fail",`Sandbox sync failed: ${errorMessage(error)}`),ui.error({command:"deploy",code:"SANDBOX_SYNC_FAILED",message:`Sandbox sync failed: ${errorMessage(error)}`});return}let previewUrl=null;if(previewEnabled)try{previewUrl=(await api.getPreviewUrl(local.projectId,ctx.env)).url}catch(error){isPretty2&&O2.warn(`Could not fetch preview URL: ${errorMessage(error)}`)}if(!options.publish){ui.result({command:"deploy",data:{project_id:local.projectId,env:ctx.env,preview_enabled:previewEnabled,preview_url:previewUrl,published:!1},render:__name(()=>{let lines=[];return previewUrl&&lines.push(`Preview: ${previewUrl}`),lines.push(previewEnabled?"Deployed to preview. Run `fimo deploy --publish` or `fimo publish` to publish a new version.":"Changes synced. Preview is disabled for this project."),lines.join(`
|
|
1155
1155
|
`)},"render")});return}let publishSp=spinner("Publishing (this can take a few minutes)");try{await api.publishProject(local.projectId)}catch(error){rethrowCliAuthError(error,()=>publishSp.fail("Authentication required")),publishSp.fail(),ui.error({command:"deploy",code:"PUBLISH_FAILED",message:`Publish failed: ${errorMessage(error)}`});return}publishSp.succeed("Published");let appUrl=null;try{appUrl=(await api.getProject(local.projectId)).appUrl}catch(error){isPretty2&&O2.warn(`Could not fetch published URL: ${errorMessage(error)}`)}ui.result({command:"deploy",data:{project_id:local.projectId,env:ctx.env,preview_enabled:previewEnabled,preview_url:previewUrl,published:!0,app_url:appUrl},render:__name(()=>{let lines=[];return previewUrl&&lines.push(`Preview: ${previewUrl}`),appUrl?(lines.push(`Live: ${appUrl}`),lines.push("Your site is live.")):lines.push("Publish completed. Check Fimo for the live URL."),lines.join(`
|
|
1156
1156
|
`)},"render")})}__name(deployCommand,"deployCommand");function emitPushFailure(failure,env){switch(failure.kind){case"non-fast-forward":ui.error({command:"deploy",code:"PUSH_NON_FAST_FORWARD",message:`The remote \`fimo/${env}\` has commits your local branch does not.`,hint:`Fix it manually, then re-run \`fimo deploy\`:
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { CollectionContentClient, ContentClientConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
|
|
4
|
+
* and arrays key-by-key from `Object.keys`, which silently drops the
|
|
5
|
+
* non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
|
|
6
|
+
* A `json` field would then lose its `data-fimo-source` on the first refetch
|
|
7
|
+
* that changes it, making it unselectable in every editing surface. Every other
|
|
8
|
+
* wrapped type is a class instance, which structural sharing returns untouched.
|
|
9
|
+
*
|
|
10
|
+
* Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
|
|
11
|
+
* every fetch, so a wrapped entry never compared equal to its predecessor and
|
|
12
|
+
* sharing was already returning the new object.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SOURCE_TRACKED_QUERY_OPTIONS: {
|
|
15
|
+
readonly structuralSharing: false;
|
|
16
|
+
};
|
|
2
17
|
export declare function createContentClient<T extends {
|
|
3
18
|
id: string;
|
|
4
19
|
}, TInput extends object>(config: ContentClientConfig): CollectionContentClient<T, TInput>;
|
|
@@ -2,6 +2,19 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient, useSuspenseQue
|
|
|
2
2
|
import { parseCollectionResponse } from '../parseCollectionResponse.js';
|
|
3
3
|
import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readCollectionLocale, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
|
|
4
4
|
import { useQueryLocale } from './useQueryLocale.js';
|
|
5
|
+
/**
|
|
6
|
+
* React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
|
|
7
|
+
* and arrays key-by-key from `Object.keys`, which silently drops the
|
|
8
|
+
* non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
|
|
9
|
+
* A `json` field would then lose its `data-fimo-source` on the first refetch
|
|
10
|
+
* that changes it, making it unselectable in every editing surface. Every other
|
|
11
|
+
* wrapped type is a class instance, which structural sharing returns untouched.
|
|
12
|
+
*
|
|
13
|
+
* Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
|
|
14
|
+
* every fetch, so a wrapped entry never compared equal to its predecessor and
|
|
15
|
+
* sharing was already returning the new object.
|
|
16
|
+
*/
|
|
17
|
+
export const SOURCE_TRACKED_QUERY_OPTIONS = { structuralSharing: false };
|
|
5
18
|
export function createContentClient(config) {
|
|
6
19
|
const references = config.references ?? {};
|
|
7
20
|
const info = interpretSchema(config.schema);
|
|
@@ -147,6 +160,7 @@ export function createContentClient(config) {
|
|
|
147
160
|
return useQuery({
|
|
148
161
|
queryKey: qk.byId(id, localizedOptions),
|
|
149
162
|
queryFn: () => getById(id, localizedOptions),
|
|
163
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
150
164
|
});
|
|
151
165
|
}
|
|
152
166
|
function useGetBySlug(slug, options = {}) {
|
|
@@ -155,6 +169,7 @@ export function createContentClient(config) {
|
|
|
155
169
|
return useQuery({
|
|
156
170
|
queryKey: qk.bySlug(slug, localizedOptions),
|
|
157
171
|
queryFn: () => getBySlug(slug, localizedOptions),
|
|
172
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
158
173
|
enabled: !!slug,
|
|
159
174
|
});
|
|
160
175
|
}
|
|
@@ -164,6 +179,7 @@ export function createContentClient(config) {
|
|
|
164
179
|
return useQuery({
|
|
165
180
|
queryKey: qk.byField(fieldName, value, localizedOptions),
|
|
166
181
|
queryFn: () => getByField(fieldName, value, localizedOptions),
|
|
182
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
167
183
|
enabled: !!value,
|
|
168
184
|
});
|
|
169
185
|
}
|
|
@@ -173,6 +189,7 @@ export function createContentClient(config) {
|
|
|
173
189
|
return useQuery({
|
|
174
190
|
queryKey: qk.list(localizedParams),
|
|
175
191
|
queryFn: () => list(localizedParams),
|
|
192
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
176
193
|
select: (page) => page.items,
|
|
177
194
|
});
|
|
178
195
|
}
|
|
@@ -182,6 +199,7 @@ export function createContentClient(config) {
|
|
|
182
199
|
return useQuery({
|
|
183
200
|
queryKey: qk.list(localizedParams),
|
|
184
201
|
queryFn: () => list(localizedParams),
|
|
202
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
185
203
|
});
|
|
186
204
|
}
|
|
187
205
|
function useInfiniteList(params = {}) {
|
|
@@ -190,6 +208,7 @@ export function createContentClient(config) {
|
|
|
190
208
|
const query = useInfiniteQuery({
|
|
191
209
|
queryKey: qk.infiniteList(localizedParams),
|
|
192
210
|
queryFn: ({ pageParam }) => list({ ...localizedParams, offset: pageParam }),
|
|
211
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
193
212
|
initialPageParam: 0,
|
|
194
213
|
getNextPageParam: (lastPage) => lastPage.pageInfo.nextOffset ?? undefined,
|
|
195
214
|
});
|
|
@@ -208,6 +227,7 @@ export function createContentClient(config) {
|
|
|
208
227
|
return useSuspenseQuery({
|
|
209
228
|
queryKey: qk.byId(id, localizedOptions),
|
|
210
229
|
queryFn: () => getById(id, localizedOptions),
|
|
230
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
211
231
|
});
|
|
212
232
|
}
|
|
213
233
|
function useSuspenseGetBySlug(slug, options = {}) {
|
|
@@ -216,6 +236,7 @@ export function createContentClient(config) {
|
|
|
216
236
|
return useSuspenseQuery({
|
|
217
237
|
queryKey: qk.bySlug(slug, localizedOptions),
|
|
218
238
|
queryFn: () => getBySlug(slug, localizedOptions),
|
|
239
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
219
240
|
});
|
|
220
241
|
}
|
|
221
242
|
function useSuspenseGetByField(fieldName, value, options = {}) {
|
|
@@ -224,6 +245,7 @@ export function createContentClient(config) {
|
|
|
224
245
|
return useSuspenseQuery({
|
|
225
246
|
queryKey: qk.byField(fieldName, value, localizedOptions),
|
|
226
247
|
queryFn: () => getByField(fieldName, value, localizedOptions),
|
|
248
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
227
249
|
});
|
|
228
250
|
}
|
|
229
251
|
function useSuspenseGet(params = {}) {
|
|
@@ -232,6 +254,7 @@ export function createContentClient(config) {
|
|
|
232
254
|
return useSuspenseQuery({
|
|
233
255
|
queryKey: qk.list(localizedParams),
|
|
234
256
|
queryFn: () => list(localizedParams),
|
|
257
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
235
258
|
select: (page) => page.items,
|
|
236
259
|
});
|
|
237
260
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
2
|
+
import { SOURCE_TRACKED_QUERY_OPTIONS } from './createContentClient.js';
|
|
2
3
|
import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readSingletonLocale, referenceTargetUids, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
|
|
3
4
|
import { useQueryLocale } from './useQueryLocale.js';
|
|
4
5
|
export function createSingletonClient(config) {
|
|
@@ -78,6 +79,7 @@ export function createSingletonClient(config) {
|
|
|
78
79
|
return useQuery({
|
|
79
80
|
queryKey: qk.document(localizedOptions),
|
|
80
81
|
queryFn: () => get(localizedOptions),
|
|
82
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
81
83
|
});
|
|
82
84
|
}
|
|
83
85
|
function useSuspenseGet(options = {}) {
|
|
@@ -86,6 +88,7 @@ export function createSingletonClient(config) {
|
|
|
86
88
|
return useSuspenseQuery({
|
|
87
89
|
queryKey: qk.document(localizedOptions),
|
|
88
90
|
queryFn: () => get(localizedOptions),
|
|
91
|
+
...SOURCE_TRACKED_QUERY_OPTIONS,
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
function useUpdate() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pascalCase } from 'change-case';
|
|
2
2
|
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
|
|
3
3
|
import fimoConfig from 'virtual:fimo-config';
|
|
4
|
-
import { FimoBoolean, FimoMedia, FimoRichText, FimoString } from '../../primitives/lib/primitives.js';
|
|
4
|
+
import { FIMO_SOURCE, FimoBoolean, FimoMedia, FimoRichText, FimoString } from '../../primitives/lib/primitives.js';
|
|
5
5
|
export function interpretSchema(schema) {
|
|
6
6
|
if (!schema || typeof schema.uid !== 'string' || schema.uid.length === 0) {
|
|
7
7
|
throw new Error('Content schema is missing a "uid".');
|
|
@@ -176,15 +176,59 @@ function wrapReference(value, uid, resolve) {
|
|
|
176
176
|
}
|
|
177
177
|
return target.wrapWithSource(value);
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Blocks values have no wrapper class of their own — the array structure stays
|
|
181
|
+
* plain JSON (matching the API/codegen shape) and only the scalar leaves inside
|
|
182
|
+
* each item are wrapped. `_key`/`_type` are structural metadata, never fields.
|
|
183
|
+
*/
|
|
184
|
+
function blockItemFields(field, item) {
|
|
185
|
+
const schema = field.schema;
|
|
186
|
+
if (!schema) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if ('oneOf' in schema) {
|
|
190
|
+
return schema.oneOf.find((variant) => variant.uid === item._type)?.fields;
|
|
191
|
+
}
|
|
192
|
+
return schema.fields;
|
|
193
|
+
}
|
|
194
|
+
function wrapBlockItem(field, item, source, resolve) {
|
|
195
|
+
if (item == null || typeof item !== 'object' || Array.isArray(item)) {
|
|
196
|
+
return item;
|
|
197
|
+
}
|
|
198
|
+
const record = item;
|
|
199
|
+
const fields = blockItemFields(field, record);
|
|
200
|
+
if (!fields) {
|
|
201
|
+
return item;
|
|
202
|
+
}
|
|
203
|
+
const wrapped = { ...record };
|
|
204
|
+
for (const [key, childField] of Object.entries(fields)) {
|
|
205
|
+
wrapped[key] = wrapField(childField, record[key], `${source}.${key}`, resolve);
|
|
206
|
+
}
|
|
207
|
+
return wrapped;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A JSON field's declared shape is a plain object/array — tagging it in place
|
|
211
|
+
* (rather than boxing it, like the scalar wrappers below) keeps that shape
|
|
212
|
+
* intact so consumers that read it as ordinary JSON see no difference.
|
|
213
|
+
*/
|
|
214
|
+
function tagWithSource(value, source) {
|
|
215
|
+
Object.defineProperty(value, FIMO_SOURCE, { value: source, enumerable: false, configurable: true });
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
179
218
|
function wrapField(field, value, source, resolve) {
|
|
180
219
|
switch (field.type) {
|
|
181
220
|
case 'string':
|
|
182
221
|
case 'text':
|
|
183
|
-
|
|
222
|
+
case 'date':
|
|
223
|
+
return value != null ? new FimoString(String(value), source) : value;
|
|
184
224
|
case 'richtext':
|
|
185
225
|
return value != null ? new FimoRichText(value, source) : value;
|
|
186
226
|
case 'boolean':
|
|
187
227
|
return value != null ? new FimoBoolean(value, source) : value;
|
|
228
|
+
case 'json':
|
|
229
|
+
return value != null && typeof value === 'object'
|
|
230
|
+
? tagWithSource(Array.isArray(value) ? [...value] : { ...value }, source)
|
|
231
|
+
: value;
|
|
188
232
|
case 'media':
|
|
189
233
|
if (field.multiple) {
|
|
190
234
|
return (value?.map((m, i) => new FimoMedia(m, `${source}[${i}]`)) ?? value);
|
|
@@ -207,6 +251,23 @@ function wrapField(field, value, source, resolve) {
|
|
|
207
251
|
}
|
|
208
252
|
return wrapReference(value, uid, resolve);
|
|
209
253
|
}
|
|
254
|
+
case 'blocks':
|
|
255
|
+
return Array.isArray(value)
|
|
256
|
+
? value.map((item, i) => wrapBlockItem(field, item, `${source}[${i}]`, resolve))
|
|
257
|
+
: value;
|
|
258
|
+
// 'number' deliberately falls through here — this is a decision, not a
|
|
259
|
+
// gap. React renders a boxed String directly (`<span>{new FimoString(...)}
|
|
260
|
+
// </span>` works, because React special-cases String objects), but it
|
|
261
|
+
// throws "Objects are not valid as a React child" on a boxed Number. A
|
|
262
|
+
// FimoNumber wrapper — whether a plain class or `extends Number` — would
|
|
263
|
+
// break `{post.views}`, which is exactly how a raw number field renders
|
|
264
|
+
// in every Fimo site today. `extends Number` would recover `.toFixed()`/
|
|
265
|
+
// `.toPrecision()`/`.toLocaleString()` and make `JSON.stringify` emit `7`
|
|
266
|
+
// instead of `{"value":7}`, but it does NOT fix JSX rendering, and
|
|
267
|
+
// `typeof`/`===`/`Number.isInteger` stay broken either way (boxed
|
|
268
|
+
// primitives never satisfy those). Revisiting this needs a `<Number>`
|
|
269
|
+
// component in `fimo/ui` (none exists yet — every other wrapped type has
|
|
270
|
+
// one) plus a migration path for sites already rendering numbers raw.
|
|
210
271
|
default:
|
|
211
272
|
return value;
|
|
212
273
|
}
|
|
@@ -6,6 +6,19 @@ interface RecordData {
|
|
|
6
6
|
visible: boolean;
|
|
7
7
|
}
|
|
8
8
|
export type GroupedDataSources = Record<string, RecordData[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Parses a `data-fimo-source` attribute into its schema, identity, and field
|
|
11
|
+
* parts. The field itself may contain dots (e.g. a blocks leaf such as
|
|
12
|
+
* "body[0].title"), so only the first two segments are fixed positions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseDataSource(source: string): {
|
|
15
|
+
schemaName: string;
|
|
16
|
+
identity: {
|
|
17
|
+
id: string;
|
|
18
|
+
locale?: string;
|
|
19
|
+
};
|
|
20
|
+
fieldName: string;
|
|
21
|
+
} | null;
|
|
9
22
|
/**
|
|
10
23
|
* Finds all visible elements with a `data-fimo-source` attribute and groups them
|
|
11
24
|
* by schema and ID into a structured object.
|
|
@@ -14,6 +14,24 @@ function parseRecordIdentity(token) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses a `data-fimo-source` attribute into its schema, identity, and field
|
|
19
|
+
* parts. The field itself may contain dots (e.g. a blocks leaf such as
|
|
20
|
+
* "body[0].title"), so only the first two segments are fixed positions.
|
|
21
|
+
*/
|
|
22
|
+
export function parseDataSource(source) {
|
|
23
|
+
const parts = source.split('.');
|
|
24
|
+
if (parts.length < 3) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const [schemaName, identityToken] = parts;
|
|
28
|
+
const fieldName = parts.slice(2).join('.');
|
|
29
|
+
const identity = parseRecordIdentity(identityToken);
|
|
30
|
+
if (!identity) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { schemaName, identity, fieldName };
|
|
34
|
+
}
|
|
17
35
|
/**
|
|
18
36
|
* Checks if an element is genuinely visible to the user within the viewport.
|
|
19
37
|
* @param el The element to check.
|
|
@@ -51,37 +69,33 @@ export function retrieveVisibleDataSources() {
|
|
|
51
69
|
if (!source) {
|
|
52
70
|
return;
|
|
53
71
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Add the field and its text content to the record
|
|
82
|
-
if (el.textContent) {
|
|
83
|
-
tempGroupedData[schemaName][recordKey].data[fieldName] = el.textContent.trim();
|
|
84
|
-
}
|
|
72
|
+
const parsed = parseDataSource(source);
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { schemaName, identity, fieldName } = parsed;
|
|
77
|
+
const { id, locale } = identity;
|
|
78
|
+
const recordKey = JSON.stringify([id, locale ?? null]);
|
|
79
|
+
// Skip __translations
|
|
80
|
+
if (schemaName === '__translations') {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Ensure the schema key exists
|
|
84
|
+
if (!tempGroupedData[schemaName]) {
|
|
85
|
+
tempGroupedData[schemaName] = {};
|
|
86
|
+
}
|
|
87
|
+
// Ensure the record (by ID) exists for this schema
|
|
88
|
+
if (!tempGroupedData[schemaName][recordKey]) {
|
|
89
|
+
tempGroupedData[schemaName][recordKey] = {
|
|
90
|
+
id: id,
|
|
91
|
+
...(locale ? { locale } : {}),
|
|
92
|
+
data: {},
|
|
93
|
+
visible: isElementVisible(el),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Add the field and its text content to the record
|
|
97
|
+
if (el.textContent) {
|
|
98
|
+
tempGroupedData[schemaName][recordKey].data[fieldName] = el.textContent.trim();
|
|
85
99
|
}
|
|
86
100
|
});
|
|
87
101
|
// Convert the temporary object into the final desired array structure.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ElementType, type ReactNode } from 'react';
|
|
2
|
+
type JsonProps<T extends ElementType = 'span'> = {
|
|
3
|
+
value: unknown;
|
|
4
|
+
as?: T;
|
|
5
|
+
children?: (value: unknown) => ReactNode;
|
|
6
|
+
} & Omit<ComponentPropsWithoutRef<T>, 'value' | 'as' | 'children'>;
|
|
7
|
+
/**
|
|
8
|
+
* Json component for rendering JSON field values with source tracking.
|
|
9
|
+
*
|
|
10
|
+
* Use children render function to customize how the value is displayed.
|
|
11
|
+
* Pass `as="pre"` (with a `white-space: pre-wrap` class) for formatted output.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Default (single-line JSON text)
|
|
15
|
+
* <Json value={product.metadata} />
|
|
16
|
+
*
|
|
17
|
+
* // Pretty-printed
|
|
18
|
+
* <Json value={product.metadata} as="pre" className="whitespace-pre-wrap" />
|
|
19
|
+
*
|
|
20
|
+
* // With render function for custom display
|
|
21
|
+
* <Json value={product.metadata}>{(val) => val.tags?.join(', ')}</Json>
|
|
22
|
+
*/
|
|
23
|
+
export declare function Json<T extends ElementType = 'span'>({ value, as, children, ...props }: JsonProps<T>): import("react/jsx-runtime").JSX.Element | null;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=Json.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getFimoSource, useFimoLiveValue } from '../lib/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Json component for rendering JSON field values with source tracking.
|
|
5
|
+
*
|
|
6
|
+
* Use children render function to customize how the value is displayed.
|
|
7
|
+
* Pass `as="pre"` (with a `white-space: pre-wrap` class) for formatted output.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Default (single-line JSON text)
|
|
11
|
+
* <Json value={product.metadata} />
|
|
12
|
+
*
|
|
13
|
+
* // Pretty-printed
|
|
14
|
+
* <Json value={product.metadata} as="pre" className="whitespace-pre-wrap" />
|
|
15
|
+
*
|
|
16
|
+
* // With render function for custom display
|
|
17
|
+
* <Json value={product.metadata}>{(val) => val.tags?.join(', ')}</Json>
|
|
18
|
+
*/
|
|
19
|
+
export function Json({ value, as, children, ...props }) {
|
|
20
|
+
const Component = as || 'span';
|
|
21
|
+
const { value: liveValue, ref } = useFimoLiveValue(value);
|
|
22
|
+
// Defensive: return null if no value
|
|
23
|
+
if (value === null || value === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const source = getFimoSource(value);
|
|
27
|
+
const content = children ? children(liveValue) : JSON.stringify(liveValue);
|
|
28
|
+
return (_jsx(Component, { ref: ref, ...props, "data-fimo-source": source, children: content }));
|
|
29
|
+
}
|
|
@@ -4,6 +4,7 @@ export { StaticImage } from './components/StaticImage.js';
|
|
|
4
4
|
export { Video } from './components/Video.js';
|
|
5
5
|
export { Date } from './components/Date.js';
|
|
6
6
|
export { Boolean } from './components/Boolean.js';
|
|
7
|
+
export { Json } from './components/Json.js';
|
|
7
8
|
export { RichText } from './components/RichText.js';
|
|
8
9
|
export { useLabels, useTranslations } from './translations.js';
|
|
9
10
|
export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type FimoPolymorphicReference, type FimoReference, type FimoSource, type FimoPart, type FimoRichTextContent, type FimoRichTextNodeProps, type FimoRichTextMarkProps, type FimoRichTextComponents, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, fimo, useFimoLiveValue, } from './lib/index.js';
|
|
@@ -7,6 +7,7 @@ export { StaticImage } from './components/StaticImage.js';
|
|
|
7
7
|
export { Video } from './components/Video.js';
|
|
8
8
|
export { Date } from './components/Date.js';
|
|
9
9
|
export { Boolean } from './components/Boolean.js';
|
|
10
|
+
export { Json } from './components/Json.js';
|
|
10
11
|
export { RichText } from './components/RichText.js';
|
|
11
12
|
// Labels
|
|
12
13
|
export { useLabels, useTranslations } from './translations.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-staging.1",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"eta": "^3.5.0",
|
|
69
69
|
"oxc-parser": "^0.118.0",
|
|
70
70
|
"react-error-boundary": "^6.0.0",
|
|
71
|
-
"zod": "4.
|
|
71
|
+
"zod": "4.4.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@clack/core": "^1.2.0",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@types/react": "^19.2.14",
|
|
83
83
|
"@types/react-dom": "^19.2.3",
|
|
84
84
|
"agent-install": "^0.0.5",
|
|
85
|
-
"better-auth": "1.6.
|
|
85
|
+
"better-auth": "1.6.25",
|
|
86
86
|
"cheerio": "^1.0.0",
|
|
87
87
|
"commander": "^13.1.0",
|
|
88
88
|
"esbuild": "^0.25.10",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"typescript": "5.9.2",
|
|
95
95
|
"vite": "^8.0.13",
|
|
96
96
|
"vitest": "^4.1.6",
|
|
97
|
-
"wrangler": "
|
|
97
|
+
"wrangler": "4.114.0",
|
|
98
98
|
"yaml": "2.8.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@base-ui/react": "^1.3.0",
|
|
15
|
-
"@hookform/resolvers": "^5.
|
|
15
|
+
"@hookform/resolvers": "^5.5.7",
|
|
16
16
|
"@react-router/node": "^8.2.0",
|
|
17
17
|
"@tailwindcss/typography": "^0.5.19",
|
|
18
18
|
"@tanstack/react-query": "^5.80.6",
|
|
@@ -21,25 +21,25 @@
|
|
|
21
21
|
"cmdk": "^1.1.1",
|
|
22
22
|
"date-fns": "^4.1.0",
|
|
23
23
|
"embla-carousel-react": "^8.6.0",
|
|
24
|
-
"fimo": "0.
|
|
24
|
+
"fimo": "0.11.0-staging.1",
|
|
25
25
|
"input-otp": "^1.4.2",
|
|
26
26
|
"isbot": "^5",
|
|
27
27
|
"lucide-react": "^0.577.0",
|
|
28
|
-
"motion": "^12.
|
|
28
|
+
"motion": "^12.42.2",
|
|
29
29
|
"next-themes": "^0.4.6",
|
|
30
|
-
"radix-ui": "^1.
|
|
30
|
+
"radix-ui": "^1.6.7",
|
|
31
31
|
"react": "^19.2.7",
|
|
32
32
|
"react-day-picker": "^9.14.0",
|
|
33
33
|
"react-dom": "^19.2.7",
|
|
34
34
|
"react-error-boundary": "^6.0.0",
|
|
35
|
-
"react-hook-form": "^7.
|
|
35
|
+
"react-hook-form": "^7.83.0",
|
|
36
36
|
"react-resizable-panels": "^4.9.0",
|
|
37
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",
|
|
41
41
|
"vaul": "^1.1.2",
|
|
42
|
-
"zod": "^4.3
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@react-router/dev": "^8.2.0",
|