fimo 0.17.0 → 0.19.0
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/astro/components/DateTime.astro +22 -0
- package/assets/astro/components/index.d.ts +1 -0
- package/assets/astro/components/index.js +1 -0
- package/assets/content-templates/shared/types.eta +2 -0
- package/assets/skills/fimo-cli/SKILL.md +8 -6
- package/assets/skills/fimo-cli/references/branches.md +2 -2
- package/assets/skills/fimo-cli/references/content.md +2 -2
- package/assets/skills/fimo-cli/references/deploy.md +17 -9
- package/assets/skills/fimo-cli/references/hosting.md +114 -0
- package/assets/skills/fimo-cli/references/init.md +13 -6
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
- package/assets/skills/fimo-cli/references/locales.md +38 -14
- package/assets/skills/fimo-cli/references/pages.md +70 -0
- package/assets/skills/fimo-cli/references/preview.md +26 -20
- package/assets/skills/fimo-cli/references/webhooks.md +1 -1
- package/assets/skills/fimo-code/SKILL.md +11 -0
- package/assets/skills/fimo-code/assets/react-router-locales/LocaleLayout.tsx +12 -0
- package/assets/skills/fimo-code/assets/react-router-locales/routes.ts +7 -6
- package/assets/skills/fimo-code/references/best-practices/astro.md +47 -0
- package/assets/skills/fimo-code/references/best-practices/next.md +42 -0
- package/assets/skills/fimo-code/references/best-practices/sveltekit.md +17 -0
- package/assets/skills/fimo-code/references/content/richtext.md +5 -0
- package/assets/skills/fimo-code/references/content/schema.md +4 -1
- package/assets/skills/fimo-code/references/frameworks/astro.md +5 -3
- package/assets/skills/fimo-code/references/frameworks/hosting.md +5 -2
- package/assets/skills/fimo-code/references/frameworks/next/app-router.md +6 -0
- package/assets/skills/fimo-code/references/frameworks/next/locales.md +12 -2
- package/assets/skills/fimo-code/references/frameworks/next.md +7 -0
- package/assets/skills/fimo-code/references/frameworks/react-router.md +15 -7
- package/assets/skills/fimo-code/references/frameworks/sveltekit.md +15 -8
- package/assets/skills/fimo-code/references/labels.md +9 -1
- package/assets/skills/fimo-code/references/locales.md +8 -2
- package/assets/skills/fimo-code/references/migrations/react-router-route-ownership.md +93 -0
- package/assets/skills/fimo-code/references/ui.md +21 -2
- package/assets/skills/fimo-migration/SKILL.md +276 -51
- package/assets/skills/fimo-migration/references/content.md +119 -35
- package/assets/skills/fimo-migration/references/decision.md +72 -0
- package/assets/skills/fimo-migration/references/forms.md +38 -6
- package/assets/skills/fimo-migration/references/frameworks/nextjs.md +179 -23
- package/assets/skills/fimo-migration/references/import-contract.md +71 -19
- package/assets/skills/fimo-migration/references/manifest.md +11 -161
- package/assets/skills/fimo-migration/references/media.md +33 -1
- package/assets/skills/fimo-migration/references/platforms.md +26 -2
- package/assets/skills/fimo-migration/references/qa.md +167 -13
- package/assets/skills/fimo-migration/references/workers/content-execution.md +39 -0
- package/assets/skills/fimo-migration/references/workers/content-planner.md +64 -0
- package/assets/skills/fimo-migration/references/workers/framework.md +37 -0
- package/assets/skills/fimo-migration/references/workers/validator.md +25 -0
- package/assets/skills/fimo-migration/references/workers.md +82 -0
- package/assets/skills/fimo-migration/references/workflow.md +140 -192
- package/assets/skills/fimo-migration/scripts/apply-decision.mjs +534 -0
- package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +7 -4
- package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +191 -4
- package/assets/skills/fimo-studio/references/ai-chat.md +1 -1
- package/assets/skills/fimo-studio/references/cms.md +25 -7
- package/assets/skills/fimo-studio/references/webhooks.md +1 -1
- package/assets/sveltekit/components/DateTime.svelte +35 -0
- package/assets/sveltekit/components/FimoProvider.svelte +2 -2
- package/assets/sveltekit/components/index.d.ts +1 -0
- package/assets/sveltekit/components/index.js +1 -0
- package/dist/build/next/config.d.ts +4 -1
- package/dist/build/next/config.js +21 -5
- package/dist/build/sveltekit/vite.d.ts +3 -4
- package/dist/build/sveltekit/vite.js +3 -4
- package/dist/build/vite/plugins/runtime-env.d.ts +2 -1
- package/dist/build/vite/plugins/runtime-env.js +14 -10
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +113 -108
- package/dist/runtime/app/FimoProviders.js +25 -14
- package/dist/runtime/app/prefetch.d.ts +3 -1
- package/dist/runtime/app/prefetch.js +11 -55
- package/dist/runtime/astro/preview.js +7 -9
- package/dist/runtime/content/client/internal.js +1 -0
- package/dist/runtime/i18n/locale.d.ts +6 -3
- package/dist/runtime/i18n/locale.js +16 -41
- package/dist/runtime/next/client-runtime.js +5 -1
- package/dist/runtime/next/client.d.ts +3 -2
- package/dist/runtime/next/client.js +2 -1
- package/dist/runtime/next/labels.d.ts +2 -2
- package/dist/runtime/next/labels.js +2 -2
- package/dist/runtime/next/preview.d.ts +13 -0
- package/dist/runtime/next/preview.js +34 -0
- package/dist/runtime/next/primitives/server.d.ts +2 -0
- package/dist/runtime/next/primitives/server.js +8 -0
- package/dist/runtime/next/provider.d.ts +5 -0
- package/dist/runtime/next/provider.js +17 -3
- package/dist/runtime/next/server-runtime.js +4 -1
- package/dist/runtime/next/server.d.ts +1 -1
- package/dist/runtime/next/server.js +1 -1
- package/dist/runtime/paths/get-fimo-paths.d.ts +6 -5
- package/dist/runtime/paths/get-fimo-paths.js +43 -42
- package/dist/runtime/preview/location.d.ts +19 -2
- package/dist/runtime/preview/location.js +16 -2
- package/dist/runtime/primitives/index.d.ts +1 -0
- package/dist/runtime/primitives/index.js +1 -0
- package/dist/runtime/react/primitives/editable/DateTime.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/DateTime.js +13 -0
- package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +7 -1
- package/dist/runtime/react/primitives/renderers/DateRenderer.js +11 -8
- package/dist/runtime/react-router/{routes.d.ts → legacy/routes.d.ts} +6 -1
- package/dist/runtime/react-router/{routes.js → legacy/routes.js} +22 -7
- package/dist/runtime/routes/index.d.ts +162 -0
- package/dist/runtime/routes/index.js +335 -0
- package/dist/runtime/routing/AppRoutesProvider.js +4 -3
- package/dist/runtime/seo/resolver.d.ts +5 -0
- package/dist/runtime/seo/resolver.js +10 -42
- package/dist/runtime/shared/fimo-config.types.d.ts +14 -13
- package/dist/runtime/shared/preview-location.d.ts +1 -22
- package/dist/runtime/shared/preview-location.js +5 -93
- package/dist/runtime/sveltekit/preview.d.ts +12 -3
- package/dist/runtime/sveltekit/preview.js +13 -8
- package/dist/runtime/templates.d.ts +19 -0
- package/dist/runtime/templates.js +33 -0
- package/dist/scripts/sync-forms.js +1 -1
- package/dist/scripts/sync-schemas.js +1 -4
- package/dist/scripts/validate-fimo-config.js +5 -1
- package/dist/scripts/validate-page-registry.d.ts +2 -0
- package/dist/scripts/validate-page-registry.js +167 -0
- package/dist/scripts/validate-route-metadata.js +132 -41
- package/package.json +10 -5
- package/templates/astro/.fimo/config.json +13 -5
- package/templates/astro/AGENTS.md +5 -1
- package/templates/astro/astro.config.mjs +7 -0
- package/templates/astro/pnpm-workspace.yaml +0 -3
- package/templates/astro/src/components/ProjectOverview.astro +9 -6
- package/templates/astro/src/layouts/Base.astro +1 -1
- package/templates/astro/src/locales.ts +4 -0
- package/templates/astro/src/pages/[locale]/index.astro +19 -0
- package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
- package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
- package/templates/astro/src/pages/index.astro +5 -2
- package/templates/astro/src/pages/projects/[slug].astro +4 -2
- package/templates/astro/src/pages/projects/index.astro +5 -2
- package/templates/nextjs/.fimo/config.json +2 -1
- package/templates/nextjs/AGENTS.md +5 -0
- package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
- package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
- package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
- package/templates/nextjs/src/components/Document.tsx +26 -0
- package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
- package/templates/nextjs/src/locales.ts +13 -0
- package/templates/react-router/.fimo/config.json +3 -6
- package/templates/react-router/AGENTS.md +4 -0
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/pnpm-workspace.yaml +1 -3
- package/templates/react-router/react-router.config.ts +4 -10
- package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
- package/templates/react-router/src/root.tsx +2 -4
- package/templates/react-router/src/routes.ts +5 -3
- package/templates/sveltekit/.fimo/config.json +2 -1
- package/templates/sveltekit/AGENTS.md +5 -0
- package/templates/sveltekit/src/app.html +1 -1
- package/templates/sveltekit/src/hooks.server.ts +11 -0
- package/templates/sveltekit/src/locales.ts +8 -0
- package/templates/sveltekit/src/params/locale.ts +5 -0
- package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
- package/templates/sveltekit/src/routes/+layout.svelte +5 -0
- package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
- package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
- package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
- package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
- package/templates/nextjs/src/app/layout.tsx +0 -17
- /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
- /package/templates/sveltekit/src/routes/{+page.svelte → [[locale=locale]]/+page.svelte} +0 -0
|
@@ -11,17 +11,19 @@ function main() {
|
|
|
11
11
|
const manifest = readJson(manifestPath);
|
|
12
12
|
|
|
13
13
|
const checks = buildChecks(manifest, projectRoot);
|
|
14
|
+
const dimensions = buildDimensions(manifest);
|
|
14
15
|
const status = determineStatus(checks, manifest);
|
|
15
16
|
const updatedManifest = {
|
|
16
17
|
...manifest,
|
|
17
18
|
qa: {
|
|
18
19
|
status,
|
|
19
20
|
checks,
|
|
21
|
+
dimensions,
|
|
20
22
|
reportPath: toPosix(path.relative(projectRoot, reportPath)),
|
|
21
23
|
updatedAt: new Date().toISOString(),
|
|
22
24
|
},
|
|
23
25
|
};
|
|
24
|
-
const report = buildReport(updatedManifest, checks);
|
|
26
|
+
const report = buildReport(updatedManifest, checks, dimensions, readTimings(projectRoot));
|
|
25
27
|
|
|
26
28
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
27
29
|
fs.writeFileSync(reportPath, report, 'utf8');
|
|
@@ -167,7 +169,169 @@ function determineStatus(checks, manifest) {
|
|
|
167
169
|
return 'passed-with-unresolved';
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
// Each migration surface is reported on its own row. There is deliberately no
|
|
173
|
+
// aggregate score: a perfect route coverage must never average away missing
|
|
174
|
+
// entries or an unuploaded asset.
|
|
175
|
+
const DIMENSIONS = [
|
|
176
|
+
{ id: 'routes', gate: 'blocking', items: (manifest) => manifest.routes },
|
|
177
|
+
{ id: 'content-types', gate: 'blocking', items: (manifest) => manifest.contentTypes },
|
|
178
|
+
{ id: 'entries', gate: 'blocking', items: (manifest) => manifest.entries },
|
|
179
|
+
{ id: 'media', gate: 'blocking', items: (manifest) => manifest.media },
|
|
180
|
+
{ id: 'forms', gate: 'blocking', items: (manifest) => manifest.forms },
|
|
181
|
+
{ id: 'labels', gate: 'advisory', items: (manifest) => manifest.labels },
|
|
182
|
+
{ id: 'locales', gate: 'advisory', items: (manifest) => manifest.locales },
|
|
183
|
+
{ id: 'redirects', gate: 'advisory', items: (manifest) => manifest.redirects },
|
|
184
|
+
{ id: 'seo', gate: 'advisory', items: (manifest) => manifest.seo },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const UNRESOLVED_DIMENSION_AREAS = {
|
|
188
|
+
routes: ['route', 'routes'],
|
|
189
|
+
'content-types': ['content-type', 'schema'],
|
|
190
|
+
entries: ['content', 'entry'],
|
|
191
|
+
media: ['asset', 'media'],
|
|
192
|
+
forms: ['form', 'forms'],
|
|
193
|
+
labels: ['label', 'labels'],
|
|
194
|
+
locales: ['locale', 'locales'],
|
|
195
|
+
redirects: ['redirect', 'redirects'],
|
|
196
|
+
seo: ['seo'],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const MIGRATED_STATUSES = ['imported', 'verified'];
|
|
200
|
+
|
|
201
|
+
function buildDimensions(manifest) {
|
|
202
|
+
return DIMENSIONS.map((dimension) => {
|
|
203
|
+
const items = asArray(dimension.items(manifest));
|
|
204
|
+
const unresolved = unresolvedForDimension(manifest, dimension.id, items);
|
|
205
|
+
const withStatus = (status) => items.filter((item) => item.status === status).length;
|
|
206
|
+
const discovered = items.length + unresolved.length;
|
|
207
|
+
const migrated = items.filter((item) => MIGRATED_STATUSES.includes(item.status)).length;
|
|
208
|
+
const unresolvedCount = withStatus('unresolved') + unresolved.length;
|
|
209
|
+
const unsupported = withStatus('unsupported');
|
|
210
|
+
// Deliberate drops are out of scope, so they do not count against coverage.
|
|
211
|
+
const denominator = discovered - withStatus('ignored');
|
|
212
|
+
const accounted = migrated + unresolvedCount + unsupported;
|
|
213
|
+
return {
|
|
214
|
+
dimension: dimension.id,
|
|
215
|
+
gate: dimension.gate,
|
|
216
|
+
discovered,
|
|
217
|
+
migrated,
|
|
218
|
+
verified: withStatus('verified'),
|
|
219
|
+
unresolved: unresolvedCount,
|
|
220
|
+
unsupported,
|
|
221
|
+
denominator,
|
|
222
|
+
coverage: denominator === 0 ? null : Math.round((accounted / denominator) * 1000) / 1000,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function unresolvedForDimension(manifest, dimensionId, items) {
|
|
228
|
+
const areas = UNRESOLVED_DIMENSION_AREAS[dimensionId] ?? [];
|
|
229
|
+
const itemIds = new Set(items.map((item) => item.id).filter(isNonEmptyString));
|
|
230
|
+
return asArray(manifest.unresolved).filter(
|
|
231
|
+
(item) => areas.includes(item.area) && (!isNonEmptyString(item.id) || !itemIds.has(item.id))
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function dimensionTable(dimensions) {
|
|
236
|
+
const columns = [
|
|
237
|
+
'dimension',
|
|
238
|
+
'gate',
|
|
239
|
+
'discovered',
|
|
240
|
+
'migrated',
|
|
241
|
+
'verified',
|
|
242
|
+
'unresolved',
|
|
243
|
+
'unsupported',
|
|
244
|
+
'denominator',
|
|
245
|
+
'coverage',
|
|
246
|
+
];
|
|
247
|
+
return [
|
|
248
|
+
`| ${columns.join(' | ')} |`,
|
|
249
|
+
`| ${columns.map(() => '---').join(' | ')} |`,
|
|
250
|
+
...dimensions.map(
|
|
251
|
+
(row) => `| ${columns.map((column) => (row[column] === null ? 'n/a' : String(row[column]))).join(' | ')} |`
|
|
252
|
+
),
|
|
253
|
+
].join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Durations live in run receipts, never in the manifest, so the canonical
|
|
258
|
+
* ledger stays byte-stable across identical reruns. The harness timing file is
|
|
259
|
+
* included when a standalone agent acceptance produced one.
|
|
260
|
+
*/
|
|
261
|
+
function readTimings(projectRoot) {
|
|
262
|
+
return [...readHarnessTimings(projectRoot), ...readWorkerTimings(projectRoot), ...readApplyTimings(projectRoot)];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readHarnessTimings(projectRoot) {
|
|
266
|
+
const timing = readJsonOrNull(path.join(projectRoot, '.fimo/migration/timings.json'));
|
|
267
|
+
if (!timing || typeof timing !== 'object') {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
stage: 'agent',
|
|
273
|
+
step: timing.model ?? 'claude',
|
|
274
|
+
durationMs: numberOr(timing.wallTimeMs, 0),
|
|
275
|
+
status: timing.timedOut ? 'timeout' : 'complete',
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readWorkerTimings(projectRoot) {
|
|
281
|
+
const workersDir = path.join(projectRoot, '.fimo/migration/workers');
|
|
282
|
+
if (!fs.existsSync(workersDir)) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
return fs
|
|
286
|
+
.readdirSync(workersDir)
|
|
287
|
+
.filter((file) => file.endsWith('.json'))
|
|
288
|
+
.sort()
|
|
289
|
+
.flatMap((file) => {
|
|
290
|
+
const receipt = readJsonOrNull(path.join(workersDir, file));
|
|
291
|
+
if (!receipt) {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
stage: 'worker',
|
|
297
|
+
step: receipt.id ?? path.basename(file, '.json'),
|
|
298
|
+
durationMs: numberOr(receipt.durationMs, 0),
|
|
299
|
+
status: receipt.status ?? 'complete',
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readApplyTimings(projectRoot) {
|
|
306
|
+
const run = readJsonOrNull(path.join(projectRoot, '.fimo/migration/apply-run.json'));
|
|
307
|
+
return asArray(run?.phases).map((item) => ({
|
|
308
|
+
stage: 'apply',
|
|
309
|
+
step: item.phase ?? 'unknown',
|
|
310
|
+
durationMs: numberOr(item.durationMs, 0),
|
|
311
|
+
status: item.status ?? 'complete',
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readJsonOrNull(filePath) {
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function timingSection(timings) {
|
|
324
|
+
if (timings.length === 0) {
|
|
325
|
+
return '- No run receipts recorded.';
|
|
326
|
+
}
|
|
327
|
+
return [
|
|
328
|
+
'| stage | step | status | duration_ms |',
|
|
329
|
+
'| --- | --- | --- | --- |',
|
|
330
|
+
...timings.map((item) => `| ${item.stage} | ${item.step} | ${item.status} | ${item.durationMs} |`),
|
|
331
|
+
].join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildReport(manifest, checks, dimensions, timings) {
|
|
171
335
|
const statusLabel =
|
|
172
336
|
manifest.qa.status === 'passed'
|
|
173
337
|
? 'complete'
|
|
@@ -188,6 +352,11 @@ function buildReport(manifest, checks) {
|
|
|
188
352
|
bullet('Platform', manifest.source?.platform),
|
|
189
353
|
bullet('Public URL', manifest.source?.publicUrl),
|
|
190
354
|
bullet('Root path', manifest.source?.rootPath),
|
|
355
|
+
bullet('CLI', manifest.source?.cli),
|
|
356
|
+
'',
|
|
357
|
+
'## Coverage by dimension',
|
|
358
|
+
'',
|
|
359
|
+
dimensionTable(dimensions),
|
|
191
360
|
'',
|
|
192
361
|
'## Routes',
|
|
193
362
|
'',
|
|
@@ -211,6 +380,11 @@ function buildReport(manifest, checks) {
|
|
|
211
380
|
`- SEO items: ${asArray(manifest.seo).length}`,
|
|
212
381
|
`- Redirects: ${asArray(manifest.redirects).length}`,
|
|
213
382
|
`- Locales: ${asArray(manifest.locales).length}`,
|
|
383
|
+
`- Labels: ${asArray(manifest.labels).length}`,
|
|
384
|
+
'',
|
|
385
|
+
'## Timing',
|
|
386
|
+
'',
|
|
387
|
+
timingSection(timings),
|
|
214
388
|
'',
|
|
215
389
|
'## Checks',
|
|
216
390
|
'',
|
|
@@ -335,7 +509,7 @@ function contentCountsPass(manifest) {
|
|
|
335
509
|
function schemasPushedPass(manifest) {
|
|
336
510
|
const requiredTypes = entryBatchTypesNeedingSchema(manifest);
|
|
337
511
|
if (requiredTypes.size === 0) return true;
|
|
338
|
-
const byName =
|
|
512
|
+
const byName = contentTypeIndex(manifest);
|
|
339
513
|
for (const name of requiredTypes) {
|
|
340
514
|
const type = byName.get(name);
|
|
341
515
|
if (!type || !['imported', 'verified'].includes(type.status)) return false;
|
|
@@ -343,9 +517,22 @@ function schemasPushedPass(manifest) {
|
|
|
343
517
|
return true;
|
|
344
518
|
}
|
|
345
519
|
|
|
520
|
+
// An entry batch names its content type by uid (that is what `fimo entries`
|
|
521
|
+
// takes), while a manifest content type also carries a human display name.
|
|
522
|
+
// Index both so neither spelling looks like a missing schema.
|
|
523
|
+
function contentTypeIndex(manifest) {
|
|
524
|
+
const index = new Map();
|
|
525
|
+
for (const type of asArray(manifest.contentTypes)) {
|
|
526
|
+
for (const key of [type.uid, type.name]) {
|
|
527
|
+
if (isNonEmptyString(key)) index.set(key, type);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return index;
|
|
531
|
+
}
|
|
532
|
+
|
|
346
533
|
function schemaPushStats(manifest) {
|
|
347
534
|
const requiredTypes = entryBatchTypesNeedingSchema(manifest);
|
|
348
|
-
const byName =
|
|
535
|
+
const byName = contentTypeIndex(manifest);
|
|
349
536
|
const pushed = [...requiredTypes].filter((name) => ['imported', 'verified'].includes(byName.get(name)?.status));
|
|
350
537
|
return {
|
|
351
538
|
requiredTypes: requiredTypes.size,
|
|
@@ -22,7 +22,7 @@ Give the AI more to work with:
|
|
|
22
22
|
- **+ button**: Opens a dropdown with options:
|
|
23
23
|
- **Attach Assets** -- browse your media library and attach files.
|
|
24
24
|
- **Attach Entries** -- attach CMS content for the AI to reference.
|
|
25
|
-
- **Upload from Computer** -- upload a
|
|
25
|
+
- **Upload from Computer** -- upload a file directly. ZIP archives are supported up to 20 MiB; the AI can download and inspect them under `/tmp` in its sandbox while the original stays attached to the conversation.
|
|
26
26
|
- **Draw** -- opens the drawing tool where you can sketch a wireframe or layout idea for the AI to follow.
|
|
27
27
|
- **@ mentions**: Type **@** in the chat to reference specific files, pages, or content inline.
|
|
28
28
|
|
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
## How the CMS Works
|
|
2
2
|
|
|
3
|
-
The CMS is where you manage your site's structured content
|
|
3
|
+
The CMS is where you manage your site's structured content:
|
|
4
4
|
|
|
5
|
+
- **Pages** compose declared Labels, Singles, and Collection queries around the routes where they appear.
|
|
5
6
|
- **Collections** hold multiple entries of the same kind, such as Blog posts, Team members, or Testimonials.
|
|
6
7
|
- **Singles** hold one set of content, such as Site settings, a Homepage, or an About page.
|
|
7
8
|
- **Entries** are the individual records inside a Collection.
|
|
8
9
|
|
|
9
10
|
Both use schemas to define their fields. The difference is whether you edit one set of content or manage a repeatable list.
|
|
10
11
|
|
|
12
|
+
Pages is still a pre-release Studio surface. It is visible in development and staging, but hidden in production until the public release. If Pages is absent, use Collections, Singles, and Labels & Text normally.
|
|
13
|
+
|
|
11
14
|
## Browsing Your Content
|
|
12
15
|
|
|
13
16
|
1. Click the **Content tab** (the database icon) in the top navigation bar.
|
|
14
|
-
2. Choose **Collections
|
|
17
|
+
2. Choose **Pages**, **Collections**, or **Singles** in the left sidebar. **Forms** and **Labels & Text** follow them in the same list.
|
|
15
18
|
3. Hover over or focus the info icon beside any item for a short explanation.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
18
|
-
6. Click a
|
|
19
|
+
4. Pages shows the site's route tree. Collections and Singles show cards for the schemas in that group.
|
|
20
|
+
5. Select a page to edit its declared sources together. For a dynamic route, choose the concrete entry from the header selector.
|
|
21
|
+
6. Click a Collection to see its entries, then click an entry to edit it.
|
|
22
|
+
7. Click a Single to edit it directly.
|
|
19
23
|
|
|
20
24
|
Collection cards show their entry count. Single cards are labeled **Single** instead because they always represent one set of content.
|
|
21
25
|
|
|
26
|
+
## Editing by Page
|
|
27
|
+
|
|
28
|
+
1. Open **Content → Pages**.
|
|
29
|
+
2. Select a route from the page tree. The homepage stays first; related routes can be grouped into folders.
|
|
30
|
+
3. Edit its Labels, Singles, bound entry, or Collection-source results in one page-shaped view.
|
|
31
|
+
4. Use **Open in Preview** to switch to the selected route.
|
|
32
|
+
|
|
33
|
+
Agents declare each route's `params` and `sources`; Studio uses those declarations to build the Pages tree and show the content editors manage for that route.
|
|
34
|
+
|
|
35
|
+
Label and Single sources use the page as their editing home. Collection sources remain reusable and stay visible in Collections; Pages only shows the entries selected by the declared query. For a dynamic page, selecting a concrete entry resolves any `$params.<param>.<field>` values and refreshes dependent sources.
|
|
36
|
+
|
|
37
|
+
A route with several parameters, such as `/shop/:category/:product`, is chosen left to right. Studio asks for the category first, then offers only products matching `where: { "category": "$params.category.id" }`. Changing the category clears the product selection so the page cannot keep a product from another category.
|
|
38
|
+
|
|
22
39
|
## Editing a Single
|
|
23
40
|
|
|
24
41
|
1. Open **Content → Singles**.
|
|
@@ -53,7 +70,7 @@ Fimo does not send the prompt automatically. After the AI finishes, translated e
|
|
|
53
70
|
2. Click the **+** (plus) icon button at the top of the Collection (labeled **New entry** on hover). If the Collection is empty, use the **Create entry** button in the center of the empty state instead.
|
|
54
71
|
3. Fill in the form fields (text, images, dates, whatever the collection defines).
|
|
55
72
|
|
|
56
|
-
Changes save automatically as you fill in each field (the save is debounced), so there is no Save button. The save status appears in the top bar (Saving / Saved). An incomplete new entry waits until its required fields are valid before it becomes part of the Collection.
|
|
73
|
+
Changes save automatically as you fill in each field (the save is debounced), so there is no Save button. The save status appears in the top bar (Saving / Saved). An incomplete new entry waits until its required fields are valid before it becomes part of the Collection. A page source can make matching entries visible from the Pages workspace.
|
|
57
74
|
|
|
58
75
|
## Editing an Entry
|
|
59
76
|
|
|
@@ -100,7 +117,8 @@ The AI chat can help you manage content without navigating through menus:
|
|
|
100
117
|
|
|
101
118
|
- **Collections** are best for repeatable content. Open one to search, sort, create, and manage its entries.
|
|
102
119
|
- **Singles** are best for one-off content. Open one and edit it directly.
|
|
103
|
-
- **
|
|
120
|
+
- **Pages** organize existing labels and entries around the structure of the site.
|
|
121
|
+
- **Page sources organize usage, not entry membership.** A Collection query decides which reusable entries the Pages workspace shows; the entries remain available under Collections.
|
|
104
122
|
- **Forms tab**: View and manage form submissions collected from your site's forms.
|
|
105
123
|
- **Labels & Text tab**: Edit your site's UI labels and static text strings as key-and-value rows. This is for your site's interface copy, not for translated copies of collection entries.
|
|
106
124
|
- **Search and sort**: Use the search bar to filter entries and the sort dropdown to order them quickly.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Webhooks
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Test each destination and review its delivery history before depending on it in production.
|
|
4
4
|
|
|
5
5
|
Webhooks send project activity to an external HTTPS endpoint. Use them for focused reactions such as adding a waitlist submission to an email tool, notifying Slack after a publish, or starting a GitHub workflow after an agent review.
|
|
6
6
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLTimeAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
import type { FimoString } from 'fimo/sveltekit';
|
|
5
|
+
import { getFimoSource } from 'fimo/sveltekit';
|
|
6
|
+
|
|
7
|
+
type Props = Omit<HTMLTimeAttributes, 'datetime' | 'children'> & {
|
|
8
|
+
value: FimoString | globalThis.Date | string | number | null | undefined;
|
|
9
|
+
locale?: string;
|
|
10
|
+
options?: Intl.DateTimeFormatOptions;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let { value, locale, options, ...props }: Props = $props();
|
|
14
|
+
const source = $derived(getFimoSource(value));
|
|
15
|
+
const rendered = $derived.by(() => {
|
|
16
|
+
if (value == null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const raw = value instanceof String ? value.toString() : value;
|
|
20
|
+
const date = new globalThis.Date(raw as string | number | globalThis.Date);
|
|
21
|
+
if (Number.isNaN(date.getTime())) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
// The whole instant, not just the day: a `datetime` field exists to name
|
|
26
|
+
// a moment, and truncating it here would throw the time away.
|
|
27
|
+
datetime: date.toISOString(),
|
|
28
|
+
text: date.toLocaleString(locale, options ?? { dateStyle: 'medium', timeStyle: 'short' }),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#if rendered}
|
|
34
|
+
<time {...props} datetime={rendered.datetime} data-fimo-source={source}>{rendered.text}</time>
|
|
35
|
+
{/if}
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
return connectPreview({ goto, invalidateAll });
|
|
25
|
+
return connectPreview({ goto, invalidateAll, locale: () => labels.locale });
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
afterNavigate(() => {
|
|
29
29
|
if (previewEnabled) {
|
|
30
|
-
sendPreviewLocation();
|
|
30
|
+
sendPreviewLocation(labels.locale);
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Boolean } from './Boolean.svelte';
|
|
2
2
|
export { default as Date } from './Date.svelte';
|
|
3
|
+
export { default as DateTime } from './DateTime.svelte';
|
|
3
4
|
export { default as FimoProvider } from './FimoProvider.svelte';
|
|
4
5
|
export { default as Image } from './Image.svelte';
|
|
5
6
|
export { default as Json } from './Json.svelte';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Boolean } from './Boolean.svelte';
|
|
2
2
|
export { default as Date } from './Date.svelte';
|
|
3
|
+
export { default as DateTime } from './DateTime.svelte';
|
|
3
4
|
export { default as FimoProvider } from './FimoProvider.svelte';
|
|
4
5
|
export { default as Image } from './Image.svelte';
|
|
5
6
|
export { default as Json } from './Json.svelte';
|
|
@@ -5,8 +5,11 @@ export interface FimoNextConfigContext {
|
|
|
5
5
|
export type FimoNextConfigFactory<TConfig extends object = object> = (phase: string, context: FimoNextConfigContext) => TConfig | Promise<TConfig>;
|
|
6
6
|
export type FimoNextConfig<TConfig extends object = object> = TConfig | FimoNextConfigFactory<TConfig>;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Wire a Next application to its linked Fimo project.
|
|
9
9
|
*
|
|
10
|
+
* Fills the Fimo runtime env from the local link in `next dev`, `next build`,
|
|
11
|
+
* and `next start` — per key, only when nothing else set it — and, in
|
|
12
|
+
* development only, connects the dev server to Fimo's hosted preview gateway.
|
|
10
13
|
* The platform supplies allowed preview origins at runtime, so applications do
|
|
11
14
|
* not need to know which preview provider Fimo currently uses.
|
|
12
15
|
*/
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { findStaleFimoOverride, resolveLocalRuntimeEnv, staleOverrideMessage, } from '../../runtime/shared/local-runtime-env.js';
|
|
2
2
|
const NEXT_DEVELOPMENT_PHASE = 'phase-development-server';
|
|
3
|
+
// The content runtime reads process.env only, so every phase that runs
|
|
4
|
+
// application code needs the derived env: the dev server, the production build
|
|
5
|
+
// (prerendering + NEXT_PUBLIC_* inlining), and the production server.
|
|
6
|
+
const RUNTIME_ENV_PHASES = new Set([
|
|
7
|
+
NEXT_DEVELOPMENT_PHASE,
|
|
8
|
+
'phase-production-build',
|
|
9
|
+
'phase-production-server',
|
|
10
|
+
]);
|
|
3
11
|
const PREVIEW_ORIGINS_ENV = 'FIMO_PREVIEW_ALLOWED_ORIGINS';
|
|
4
12
|
function readPreviewOrigins() {
|
|
5
13
|
return (process.env[PREVIEW_ORIGINS_ENV] ?? '')
|
|
@@ -8,8 +16,11 @@ function readPreviewOrigins() {
|
|
|
8
16
|
.filter(Boolean);
|
|
9
17
|
}
|
|
10
18
|
/**
|
|
11
|
-
*
|
|
19
|
+
* Wire a Next application to its linked Fimo project.
|
|
12
20
|
*
|
|
21
|
+
* Fills the Fimo runtime env from the local link in `next dev`, `next build`,
|
|
22
|
+
* and `next start` — per key, only when nothing else set it — and, in
|
|
23
|
+
* development only, connects the dev server to Fimo's hosted preview gateway.
|
|
13
24
|
* The platform supplies allowed preview origins at runtime, so applications do
|
|
14
25
|
* not need to know which preview provider Fimo currently uses.
|
|
15
26
|
*/
|
|
@@ -18,7 +29,7 @@ export function withFimo(applicationConfig) {
|
|
|
18
29
|
const config = typeof applicationConfig === 'function'
|
|
19
30
|
? await applicationConfig(phase, context)
|
|
20
31
|
: applicationConfig;
|
|
21
|
-
if (phase
|
|
32
|
+
if (!RUNTIME_ENV_PHASES.has(phase)) {
|
|
22
33
|
return config;
|
|
23
34
|
}
|
|
24
35
|
// Next has already loaded `.env*` into `process.env` by the time this runs,
|
|
@@ -26,9 +37,11 @@ export function withFimo(applicationConfig) {
|
|
|
26
37
|
// env files supplied it — only then does the git branch decide, per key.
|
|
27
38
|
const derived = resolveLocalRuntimeEnv({ cwd: process.cwd() });
|
|
28
39
|
if (derived) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
if (phase === NEXT_DEVELOPMENT_PHASE) {
|
|
41
|
+
const stale = findStaleFimoOverride(process.env, derived);
|
|
42
|
+
if (stale) {
|
|
43
|
+
console.warn(staleOverrideMessage(stale));
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
for (const [key, value] of Object.entries(derived.values)) {
|
|
34
47
|
if (!process.env[key]) {
|
|
@@ -36,6 +49,9 @@ export function withFimo(applicationConfig) {
|
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
}
|
|
52
|
+
if (phase !== NEXT_DEVELOPMENT_PHASE) {
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
39
55
|
const previewOrigins = readPreviewOrigins();
|
|
40
56
|
if (previewOrigins.length === 0) {
|
|
41
57
|
return config;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { PluginOption } from 'vite';
|
|
2
2
|
/**
|
|
3
|
-
* Narrow SvelteKit build connector. Points
|
|
4
|
-
* Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
|
|
3
|
+
* Narrow SvelteKit build connector. Points dev and production builds at the
|
|
4
|
+
* git branch's Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
|
|
5
5
|
* template elements with deterministic `data-fimo-id="<path>#<line>-<column>"`
|
|
6
6
|
* attributes during `vite dev` so the Fimo preview can map DOM nodes back to
|
|
7
|
-
* source. Explicit `data-fimo-id` attributes are preserved
|
|
8
|
-
* builds are untouched.
|
|
7
|
+
* source. Explicit `data-fimo-id` attributes are preserved.
|
|
9
8
|
*
|
|
10
9
|
* Usage in the app's vite.config.ts: `plugins: [fimo(), sveltekit()]`.
|
|
11
10
|
*/
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import runtimeEnvPlugin from '../vite/plugins/runtime-env.js';
|
|
2
2
|
import svelteDataIdPlugin from './data-id.js';
|
|
3
3
|
/**
|
|
4
|
-
* Narrow SvelteKit build connector. Points
|
|
5
|
-
* Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
|
|
4
|
+
* Narrow SvelteKit build connector. Points dev and production builds at the
|
|
5
|
+
* git branch's Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
|
|
6
6
|
* template elements with deterministic `data-fimo-id="<path>#<line>-<column>"`
|
|
7
7
|
* attributes during `vite dev` so the Fimo preview can map DOM nodes back to
|
|
8
|
-
* source. Explicit `data-fimo-id` attributes are preserved
|
|
9
|
-
* builds are untouched.
|
|
8
|
+
* source. Explicit `data-fimo-id` attributes are preserved.
|
|
10
9
|
*
|
|
11
10
|
* Usage in the app's vite.config.ts: `plugins: [fimo(), sveltekit()]`.
|
|
12
11
|
*/
|
|
@@ -5,7 +5,8 @@ import { type Plugin } from 'vite';
|
|
|
5
5
|
* One rule, per variable: anything already set — by the process env (sandbox,
|
|
6
6
|
* shell) or the user's own `.env*` files, read through Vite's `loadEnv` — is
|
|
7
7
|
* left alone; derivation fills only what is missing. A value a user wrote by
|
|
8
|
-
* hand is an override, and this plugin must never win over it.
|
|
8
|
+
* hand is an override, and this plugin must never win over it. Empty or
|
|
9
|
+
* `undefined` `define` placeholders generated from a missing env are not values.
|
|
9
10
|
*
|
|
10
11
|
* Two injection paths, both needed:
|
|
11
12
|
* - `process.env`, so Vite's own `VITE_*` exposure, `loadEnv`, and server-side
|
|
@@ -6,7 +6,8 @@ import { findStaleFimoOverride, resolveLocalRuntimeEnv, staleOverrideMessage, }
|
|
|
6
6
|
* One rule, per variable: anything already set — by the process env (sandbox,
|
|
7
7
|
* shell) or the user's own `.env*` files, read through Vite's `loadEnv` — is
|
|
8
8
|
* left alone; derivation fills only what is missing. A value a user wrote by
|
|
9
|
-
* hand is an override, and this plugin must never win over it.
|
|
9
|
+
* hand is an override, and this plugin must never win over it. Empty or
|
|
10
|
+
* `undefined` `define` placeholders generated from a missing env are not values.
|
|
10
11
|
*
|
|
11
12
|
* Two injection paths, both needed:
|
|
12
13
|
* - `process.env`, so Vite's own `VITE_*` exposure, `loadEnv`, and server-side
|
|
@@ -33,24 +34,27 @@ export default function runtimeEnvPlugin() {
|
|
|
33
34
|
// `import.meta.env`, which `define` patches at transform time.
|
|
34
35
|
order: 'pre',
|
|
35
36
|
handler(config, { command, mode }) {
|
|
36
|
-
// Production builds get their env from the platform. Only a local dev
|
|
37
|
-
// server has a git branch worth deriving from.
|
|
38
|
-
if (command !== 'serve') {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
37
|
const cwd = config.root ?? process.cwd();
|
|
42
38
|
const derived = resolveLocalRuntimeEnv({ cwd });
|
|
43
39
|
if (!derived) {
|
|
44
40
|
return;
|
|
45
41
|
}
|
|
46
42
|
const explicit = { ...loadEnv(mode, config.envDir ?? cwd, ''), ...process.env };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
if (command === 'serve') {
|
|
44
|
+
const stale = findStaleFimoOverride(explicit, derived);
|
|
45
|
+
if (stale) {
|
|
46
|
+
console.warn(staleOverrideMessage(stale));
|
|
47
|
+
}
|
|
50
48
|
}
|
|
51
49
|
// Decide what is missing BEFORE writing to process.env, or the define
|
|
52
50
|
// step below would see the values this loop just set.
|
|
53
|
-
const missing = Object.entries(derived.values).filter(([key]) =>
|
|
51
|
+
const missing = Object.entries(derived.values).filter(([key]) => {
|
|
52
|
+
if (explicit[key]) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const defined = config.define?.[`import.meta.env.${key}`];
|
|
56
|
+
return defined === undefined || defined === 'undefined' || defined === JSON.stringify('');
|
|
57
|
+
});
|
|
54
58
|
for (const [key, value] of missing) {
|
|
55
59
|
process.env[key] = value;
|
|
56
60
|
}
|
package/dist/cli/bundle.json
CHANGED