fimo 0.8.0 → 0.9.0-experimental.10
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/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
- package/assets/agents-starter/claude/settings.json +19 -0
- package/assets/content-templates/hooks-template.eta +4 -2
- package/assets/content-templates/types-template.eta +67 -1
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +86 -15
- package/assets/skills/fimo/references/translations.md +75 -0
- package/assets/skills/fimo-cli/SKILL.md +6 -2
- package/assets/skills/fimo-cli/references/content.md +61 -2
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
- package/assets/skills/fimo-cli/references/integrations.md +5 -1
- package/assets/skills/fimo-cli/references/webhooks.md +162 -0
- package/assets/skills/fimo-migration/SKILL.md +24 -2
- package/assets/skills/fimo-migration/references/content.md +24 -0
- package/assets/skills/fimo-migration/references/manifest.md +46 -0
- package/assets/skills/fimo-migration/references/pages.md +234 -0
- package/assets/skills/fimo-migration/references/qa.md +24 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +662 -0
- package/assets/skills/fimo-studio/SKILL.md +5 -3
- package/assets/skills/fimo-studio/references/cms.md +9 -1
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/assets/skills/fimo-studio/references/webhooks.md +42 -0
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.js +31 -131
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +1337 -189
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.js +31 -30
- package/dist/runtime/astro/index.d.ts +2 -0
- package/dist/runtime/astro/index.js +3 -0
- package/dist/runtime/astro/server.d.ts +3 -0
- package/dist/runtime/astro/server.js +6 -0
- package/dist/runtime/content/client/internal.js +1 -0
- package/dist/runtime/content/client/types.d.ts +16 -0
- package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
- package/dist/runtime/core/BuiltWithBadge.js +68 -0
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
- package/dist/runtime/next/FimoNextProviders.js +20 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +2 -0
- package/dist/runtime/next/server.js +4 -0
- package/dist/runtime/nuxt/composables.d.ts +9 -0
- package/dist/runtime/nuxt/composables.js +57 -0
- package/dist/runtime/nuxt/index.d.ts +3 -0
- package/dist/runtime/nuxt/index.js +6 -0
- package/dist/runtime/nuxt/server.d.ts +3 -0
- package/dist/runtime/nuxt/server.js +6 -0
- package/dist/runtime/primitives/components/RichText.d.ts +5 -1
- package/dist/runtime/primitives/components/RichText.js +2 -2
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.js +41 -0
- package/dist/runtime/primitives/labels-store.d.ts +12 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
- package/dist/runtime/react/FimoSpaProviders.js +122 -0
- package/dist/runtime/react/index.d.ts +35 -0
- package/dist/runtime/react/index.js +40 -0
- package/dist/runtime/react/spa-content-cache.d.ts +38 -0
- package/dist/runtime/react/spa-content-cache.js +64 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
- package/dist/runtime/remix/FimoRemixProviders.js +12 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
- package/dist/runtime/remix/FimoRemixScripts.js +19 -0
- package/dist/runtime/remix/index.d.ts +12 -0
- package/dist/runtime/remix/index.js +11 -0
- package/dist/runtime/remix/server.d.ts +2 -0
- package/dist/runtime/remix/server.js +2 -0
- package/dist/runtime/seo/neutral.d.ts +22 -0
- package/dist/runtime/seo/neutral.js +17 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -13
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/i18n-messages.d.ts +29 -0
- package/dist/runtime/shared/i18n-messages.js +79 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
- package/dist/runtime/shared/labels-fetch.pure.js +71 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
- package/dist/runtime/shared/labels-fetch.server.js +97 -0
- package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
- package/dist/runtime/shared/labels-t.pure.js +10 -0
- package/dist/runtime/strapi/client.d.ts +30 -0
- package/dist/runtime/strapi/client.js +132 -0
- package/dist/runtime/strapi/index.d.ts +5 -0
- package/dist/runtime/strapi/index.js +6 -0
- package/dist/runtime/strapi/reshape.d.ts +50 -0
- package/dist/runtime/strapi/reshape.js +121 -0
- package/dist/runtime/svelte/index.d.ts +3 -0
- package/dist/runtime/svelte/index.js +7 -0
- package/dist/runtime/svelte/server.d.ts +3 -0
- package/dist/runtime/svelte/server.js +6 -0
- package/dist/runtime/vue/index.d.ts +3 -0
- package/dist/runtime/vue/index.js +5 -0
- package/dist/runtime/vue/server.d.ts +3 -0
- package/dist/runtime/vue/server.js +6 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
- package/dist/runtime/wrapped/FimoFileLink.js +19 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts +22 -0
- package/dist/runtime/wrapped/FimoText.js +20 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
- package/dist/runtime/wrapped/WrappedProviders.js +101 -0
- package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
- package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
- package/dist/runtime/wrapped/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/server.d.ts +80 -0
- package/dist/runtime/wrapped/server.js +207 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +137 -0
- package/dist/scripts/apply-wrap.d.ts +73 -0
- package/dist/scripts/apply-wrap.js +340 -0
- package/dist/scripts/cms/discover.d.ts +2 -0
- package/dist/scripts/cms/discover.js +17 -0
- package/dist/scripts/cms/example-content.d.ts +33 -0
- package/dist/scripts/cms/example-content.js +165 -0
- package/dist/scripts/cms/strapi/detect.d.ts +2 -0
- package/dist/scripts/cms/strapi/detect.js +186 -0
- package/dist/scripts/cms/strapi/index.d.ts +3 -0
- package/dist/scripts/cms/strapi/index.js +25 -0
- package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
- package/dist/scripts/cms/strapi/list-source.js +90 -0
- package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
- package/dist/scripts/cms/strapi/schema-map.js +289 -0
- package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
- package/dist/scripts/cms/strapi/shapes.js +48 -0
- package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
- package/dist/scripts/cms/strapi/touchpoints.js +145 -0
- package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
- package/dist/scripts/cms/strapi/wire-client.js +107 -0
- package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
- package/dist/scripts/cms/strapi/wiring.js +46 -0
- package/dist/scripts/cms/types.d.ts +145 -0
- package/dist/scripts/cms/types.js +11 -0
- package/dist/scripts/detect-framework.d.ts +39 -0
- package/dist/scripts/detect-framework.js +139 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +178 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +247 -0
- package/dist/scripts/extract-translations.js +16 -11
- package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
- package/dist/scripts/frameworks/astro/routes.js +44 -0
- package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
- package/dist/scripts/frameworks/astro/template-style.js +19 -0
- package/dist/scripts/frameworks/discover.d.ts +83 -0
- package/dist/scripts/frameworks/discover.js +288 -0
- package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
- package/dist/scripts/frameworks/discovered-route.js +17 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
- package/dist/scripts/frameworks/next/rewire.js +282 -0
- package/dist/scripts/frameworks/next/routes.d.ts +30 -0
- package/dist/scripts/frameworks/next/routes.js +176 -0
- package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
- package/dist/scripts/frameworks/next/wiring.js +186 -0
- package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
- package/dist/scripts/frameworks/nuxt/routes.js +38 -0
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
- package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
- package/dist/scripts/frameworks/remix/routes.js +36 -0
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
- package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
- package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
- package/dist/scripts/frameworks/vite/routes.js +143 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
- package/dist/scripts/frameworks/vite/wiring.js +144 -0
- package/dist/scripts/i18n-namespace.d.ts +27 -0
- package/dist/scripts/i18n-namespace.js +155 -0
- package/dist/scripts/lib/humanize.d.ts +14 -0
- package/dist/scripts/lib/humanize.js +54 -0
- package/dist/scripts/lib/validation-report.d.ts +1 -3
- package/dist/scripts/lib/validation-report.js +3 -9
- package/dist/scripts/render-parity/capture.d.ts +64 -0
- package/dist/scripts/render-parity/capture.js +436 -0
- package/dist/scripts/render-parity/compare.d.ts +59 -0
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
- package/dist/scripts/render-parity/normalize-html.js +223 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- package/dist/scripts/routes/bindings.d.ts +93 -0
- package/dist/scripts/routes/bindings.js +117 -0
- package/dist/scripts/routes/label-prefixes.d.ts +49 -0
- package/dist/scripts/routes/label-prefixes.js +136 -0
- package/dist/scripts/routes/module-resolve.d.ts +20 -0
- package/dist/scripts/routes/module-resolve.js +78 -0
- package/dist/scripts/routes/page-structure.d.ts +72 -0
- package/dist/scripts/routes/page-structure.js +234 -0
- package/dist/scripts/scan-wrappable.d.ts +200 -0
- package/dist/scripts/scan-wrappable.js +1355 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.js +42 -1
- package/package.json +88 -4
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/translations/en.json +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type RouteFileResolution, type RouteLike } from '../../routes/module-resolve.js';
|
|
2
|
+
import type { DiscoveredRoute } from '../next/routes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Route discovery for Vite React SPAs: there is no file convention, so the
|
|
5
|
+
* routes live in react-router code. v1 reads LITERAL, ABSOLUTE paths from the
|
|
6
|
+
* two mainstream declaration shapes —
|
|
7
|
+
*
|
|
8
|
+
* <Route path="/pricing" element={…} /> (JSX tree)
|
|
9
|
+
* createBrowserRouter([{ path: '/pricing', … }]) (object form)
|
|
10
|
+
*
|
|
11
|
+
* — in files that import react-router. Relative child paths and index routes
|
|
12
|
+
* need the router's nesting semantics to resolve; they (and `*` wildcards)
|
|
13
|
+
* stay out of v1: the fimo-config route registry accepts manual entries for
|
|
14
|
+
* anything the scan cannot see, and scanned routes only ever ADD to it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discoverReactRouterDomRoutes(rootDir: string): DiscoveredRoute[];
|
|
17
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the SPA
|
|
18
|
+
* component files that render them, via <Route element>/object-form route
|
|
19
|
+
* declarations and import resolution (lazy() included). */
|
|
20
|
+
export declare function resolveSpaRouteFiles(cwd: string, routes: Record<string, RouteLike>): RouteFileResolution[];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { discoveredRouteLabel } from '../../lib/humanize.js';
|
|
4
|
+
import { componentImports, resolveModuleFile, walkFiles, } from '../../routes/module-resolve.js';
|
|
5
|
+
const ROUTE_SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
6
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git']);
|
|
7
|
+
/**
|
|
8
|
+
* Route discovery for Vite React SPAs: there is no file convention, so the
|
|
9
|
+
* routes live in react-router code. v1 reads LITERAL, ABSOLUTE paths from the
|
|
10
|
+
* two mainstream declaration shapes —
|
|
11
|
+
*
|
|
12
|
+
* <Route path="/pricing" element={…} /> (JSX tree)
|
|
13
|
+
* createBrowserRouter([{ path: '/pricing', … }]) (object form)
|
|
14
|
+
*
|
|
15
|
+
* — in files that import react-router. Relative child paths and index routes
|
|
16
|
+
* need the router's nesting semantics to resolve; they (and `*` wildcards)
|
|
17
|
+
* stay out of v1: the fimo-config route registry accepts manual entries for
|
|
18
|
+
* anything the scan cannot see, and scanned routes only ever ADD to it.
|
|
19
|
+
*/
|
|
20
|
+
export function discoverReactRouterDomRoutes(rootDir) {
|
|
21
|
+
const srcDir = join(rootDir, 'src');
|
|
22
|
+
const files = [];
|
|
23
|
+
const walk = (dir) => {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (SKIP_DIRS.has(entry)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const full = join(dir, entry);
|
|
36
|
+
let isDir = false;
|
|
37
|
+
try {
|
|
38
|
+
isDir = statSync(full).isDirectory();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (isDir) {
|
|
44
|
+
walk(full);
|
|
45
|
+
}
|
|
46
|
+
else if (ROUTE_SOURCE.test(entry)) {
|
|
47
|
+
files.push(full);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
walk(srcDir);
|
|
52
|
+
const routes = new Map();
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
let code;
|
|
55
|
+
try {
|
|
56
|
+
code = readFileSync(file, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!/from\s+['"]react-router(?:-dom)?['"]/.test(code)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const paths = [
|
|
65
|
+
...[...code.matchAll(/<Route\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|\{\s*(?:"([^"]+)"|'([^']+)')\s*\})/g)],
|
|
66
|
+
...[...code.matchAll(/\bpath\s*:\s*(?:"([^"]+)"|'([^']+)')/g)],
|
|
67
|
+
]
|
|
68
|
+
.map((match) => match.slice(1).find(Boolean))
|
|
69
|
+
.filter((path) => typeof path === 'string');
|
|
70
|
+
for (const raw of paths) {
|
|
71
|
+
if (!raw.startsWith('/') || raw.includes('*')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const path = raw !== '/' && raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
75
|
+
if (routes.has(path)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const parts = path.split('/').filter(Boolean);
|
|
79
|
+
routes.set(path, {
|
|
80
|
+
id: path === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
81
|
+
path,
|
|
82
|
+
label: discoveredRouteLabel(path),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
87
|
+
}
|
|
88
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the SPA
|
|
89
|
+
* component files that render them, via <Route element>/object-form route
|
|
90
|
+
* declarations and import resolution (lazy() included). */
|
|
91
|
+
export function resolveSpaRouteFiles(cwd, routes) {
|
|
92
|
+
const srcDir = join(cwd, 'src');
|
|
93
|
+
const files = [];
|
|
94
|
+
walkFiles(srcDir, (name) => ROUTE_SOURCE.test(name), files);
|
|
95
|
+
const byPath = new Map();
|
|
96
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
97
|
+
if (typeof route?.path === 'string') {
|
|
98
|
+
byPath.set(route.path, routeId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const resolutions = new Map();
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
let code;
|
|
104
|
+
try {
|
|
105
|
+
code = readFileSync(file, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!/from\s+['"]react-router(?:-dom)?['"]/.test(code)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const imports = componentImports(code);
|
|
114
|
+
const pairs = [];
|
|
115
|
+
// JSX shape: one <Route …> tag carries both path and element, in either
|
|
116
|
+
// attribute order.
|
|
117
|
+
for (const tag of code.matchAll(/<Route\b((?:"[^"]*"|'[^']*'|\{[^{}]*\}|[^>"'{}])*)\/?>/g)) {
|
|
118
|
+
const attrs = tag[1];
|
|
119
|
+
const routePath = attrs.match(/\bpath\s*=\s*(?:"([^"]+)"|'([^']+)')/);
|
|
120
|
+
const element = attrs.match(/\belement\s*=\s*\{\s*<(\w+)[\s/>]/) ?? attrs.match(/\bComponent\s*=\s*\{(\w+)\}/);
|
|
121
|
+
if (routePath && element) {
|
|
122
|
+
pairs.push({ path: routePath[1] ?? routePath[2], component: element[1] });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Object shape: { path: '/x', element: <X …/> } or { path: '/x', Component: X }.
|
|
126
|
+
for (const entry of code.matchAll(/path\s*:\s*(?:"([^"]+)"|'([^']+)')\s*,\s*(?:element\s*:\s*<(\w+)[\s/>]|Component\s*:\s*(\w+))/g)) {
|
|
127
|
+
pairs.push({ path: entry[1] ?? entry[2], component: entry[3] ?? entry[4] });
|
|
128
|
+
}
|
|
129
|
+
for (const pair of pairs) {
|
|
130
|
+
const normalized = pair.path !== '/' && pair.path.endsWith('/') ? pair.path.slice(0, -1) : pair.path;
|
|
131
|
+
const routeId = byPath.get(normalized);
|
|
132
|
+
if (!routeId || resolutions.has(routeId)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const spec = imports.get(pair.component);
|
|
136
|
+
const componentFile = spec ? resolveModuleFile(cwd, file, spec) : null;
|
|
137
|
+
if (componentFile) {
|
|
138
|
+
resolutions.set(routeId, componentFile);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...resolutions.entries()].map(([routeId, file]) => ({ routeId, file }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RootWiringResult } from '../next/wiring.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vite React SPA root wiring: wrap the root render argument with
|
|
4
|
+
* `FimoSpaProviders` (client-side label fetch; inline fallbacks until it
|
|
5
|
+
* resolves) and import the project's fimo-config.json. Only the mainstream
|
|
6
|
+
* entry shape is touched — a `createRoot(…).render(<JSX/>)` (or legacy
|
|
7
|
+
* `ReactDOM.render(<JSX/>, …)`) call whose first argument is a JSX element.
|
|
8
|
+
* Anything else bails with the reason and stays agent work via the recipe.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureSpaEntryWiring(cwd: string): RootWiringResult;
|
|
11
|
+
/**
|
|
12
|
+
* The `fimo()` Vite plugin serves the virtual modules the generated content
|
|
13
|
+
* clients import (`virtual:fimo-config`, …). Without it, the first
|
|
14
|
+
* dependency re-optimization 500s every page the moment an agent uses a
|
|
15
|
+
* generated client — both run-5 agents had to discover and add it by hand.
|
|
16
|
+
* Insert `fimo()` into the config's plugins array; unusual configs bail
|
|
17
|
+
* with the recipe.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureViteFimoPlugin(cwd: string): {
|
|
20
|
+
changes: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseSync } from 'oxc-parser';
|
|
4
|
+
/**
|
|
5
|
+
* Vite React SPA root wiring: wrap the root render argument with
|
|
6
|
+
* `FimoSpaProviders` (client-side label fetch; inline fallbacks until it
|
|
7
|
+
* resolves) and import the project's fimo-config.json. Only the mainstream
|
|
8
|
+
* entry shape is touched — a `createRoot(…).render(<JSX/>)` (or legacy
|
|
9
|
+
* `ReactDOM.render(<JSX/>, …)`) call whose first argument is a JSX element.
|
|
10
|
+
* Anything else bails with the reason and stays agent work via the recipe.
|
|
11
|
+
*/
|
|
12
|
+
export function ensureSpaEntryWiring(cwd) {
|
|
13
|
+
const entryFile = ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx']
|
|
14
|
+
.map((candidate) => path.join(cwd, candidate))
|
|
15
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
16
|
+
const bail = (reason, rel = 'src/main.tsx') => ({
|
|
17
|
+
changes: [],
|
|
18
|
+
warnings: [`Root wiring not applied: ${reason} — follow the SPA recipe in the agent prompt (${rel}).`],
|
|
19
|
+
wired: false,
|
|
20
|
+
});
|
|
21
|
+
if (!entryFile) {
|
|
22
|
+
return bail('no src/main or src/index entry file found');
|
|
23
|
+
}
|
|
24
|
+
const rel = path.relative(cwd, entryFile);
|
|
25
|
+
const code = fs.readFileSync(entryFile, 'utf8');
|
|
26
|
+
if (code.includes('FimoSpaProviders')) {
|
|
27
|
+
return { changes: [], warnings: [], wired: true };
|
|
28
|
+
}
|
|
29
|
+
let program;
|
|
30
|
+
try {
|
|
31
|
+
program = parseSync(entryFile, code).program;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return bail('entry file could not be parsed', rel);
|
|
35
|
+
}
|
|
36
|
+
// EVERY root-mount JSX argument, found structurally: `render(<X/>)` calls
|
|
37
|
+
// (createRoot/ReactDOM) AND `hydrateRoot(container, <X/>)` — a prerendered
|
|
38
|
+
// SPA mounts through BOTH branches, and wiring only the createRoot one
|
|
39
|
+
// left the production hydrate path without providers (run-5 finding).
|
|
40
|
+
const renderArgs = [];
|
|
41
|
+
(function find(node) {
|
|
42
|
+
if (!node || typeof node !== 'object') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const candidate = node;
|
|
46
|
+
if (candidate.type === 'CallExpression') {
|
|
47
|
+
const isRender = candidate.callee?.type === 'MemberExpression' && candidate.callee?.property?.name === 'render';
|
|
48
|
+
const isHydrate = candidate.callee?.name === 'hydrateRoot' ||
|
|
49
|
+
(candidate.callee?.type === 'MemberExpression' && candidate.callee?.property?.name === 'hydrateRoot');
|
|
50
|
+
const arg = isRender ? candidate.arguments?.[0] : isHydrate ? candidate.arguments?.[1] : undefined;
|
|
51
|
+
if (arg && (arg.type === 'JSXElement' || arg.type === 'JSXFragment')) {
|
|
52
|
+
renderArgs.push(arg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const value of Object.values(candidate)) {
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
for (const entry of value) {
|
|
58
|
+
find(entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (value && typeof value === 'object') {
|
|
62
|
+
find(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})(program);
|
|
66
|
+
if (renderArgs.length === 0) {
|
|
67
|
+
return bail('no root render call with a JSX argument found', rel);
|
|
68
|
+
}
|
|
69
|
+
const configImportPath = path
|
|
70
|
+
.relative(path.dirname(entryFile), path.join(cwd, 'fimo-config.json'))
|
|
71
|
+
.split(path.sep)
|
|
72
|
+
.join('/');
|
|
73
|
+
const open = '<FimoSpaProviders\n' +
|
|
74
|
+
' config={fimoConfig}\n' +
|
|
75
|
+
' apiUrl={import.meta.env.VITE_API_URL}\n' +
|
|
76
|
+
' previewScriptUrl={import.meta.env.DEV ? import.meta.env.VITE_FIMO_PREVIEW_SCRIPT_URL : undefined}\n' +
|
|
77
|
+
' >';
|
|
78
|
+
let next = code;
|
|
79
|
+
for (const renderArg of [...renderArgs].sort((a, b) => b.start - a.start)) {
|
|
80
|
+
const jsx = next.slice(renderArg.start, renderArg.end);
|
|
81
|
+
next = next.slice(0, renderArg.start) + `${open}\n ${jsx}\n </FimoSpaProviders>` + next.slice(renderArg.end);
|
|
82
|
+
}
|
|
83
|
+
// AST-positioned like ensureFimoImport: a multiline import's first line is
|
|
84
|
+
// not the end of the imports.
|
|
85
|
+
const importBlock = `import { FimoSpaProviders } from 'fimo/react';\nimport fimoConfig from '${configImportPath.startsWith('.') ? configImportPath : `./${configImportPath}`}';`;
|
|
86
|
+
let lastImportEnd = -1;
|
|
87
|
+
for (const node of program.body ?? []) {
|
|
88
|
+
if (node.type === 'ImportDeclaration') {
|
|
89
|
+
lastImportEnd = node.end;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// The wrap edit above only touched positions at/after renderArg.start,
|
|
93
|
+
// which is always beyond the import block, so import spans still hold.
|
|
94
|
+
next =
|
|
95
|
+
lastImportEnd >= 0
|
|
96
|
+
? `${next.slice(0, lastImportEnd)}\n${importBlock}${next.slice(lastImportEnd)}`
|
|
97
|
+
: `${importBlock}\n${next}`;
|
|
98
|
+
fs.writeFileSync(entryFile, next);
|
|
99
|
+
const changes = [`wired FimoSpaProviders into ${rel} (${renderArgs.length} mount point(s))`];
|
|
100
|
+
const pluginResult = ensureViteFimoPlugin(cwd);
|
|
101
|
+
return {
|
|
102
|
+
changes: [...changes, ...pluginResult.changes],
|
|
103
|
+
warnings: pluginResult.warnings,
|
|
104
|
+
wired: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The `fimo()` Vite plugin serves the virtual modules the generated content
|
|
109
|
+
* clients import (`virtual:fimo-config`, …). Without it, the first
|
|
110
|
+
* dependency re-optimization 500s every page the moment an agent uses a
|
|
111
|
+
* generated client — both run-5 agents had to discover and add it by hand.
|
|
112
|
+
* Insert `fimo()` into the config's plugins array; unusual configs bail
|
|
113
|
+
* with the recipe.
|
|
114
|
+
*/
|
|
115
|
+
export function ensureViteFimoPlugin(cwd) {
|
|
116
|
+
const configFile = ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']
|
|
117
|
+
.map((candidate) => path.join(cwd, candidate))
|
|
118
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
119
|
+
if (!configFile) {
|
|
120
|
+
return { changes: [], warnings: ['No vite.config found — add the fimo() plugin per the agent-prompt recipe.'] };
|
|
121
|
+
}
|
|
122
|
+
const rel = path.relative(cwd, configFile);
|
|
123
|
+
const code = fs.readFileSync(configFile, 'utf8');
|
|
124
|
+
if (/\bfimo\s*\(/.test(code) && code.includes('fimo/vite')) {
|
|
125
|
+
return { changes: [], warnings: [] };
|
|
126
|
+
}
|
|
127
|
+
const pluginsMatch = code.match(/\bplugins\s*:\s*\[/);
|
|
128
|
+
if (!pluginsMatch || pluginsMatch.index === undefined) {
|
|
129
|
+
return {
|
|
130
|
+
changes: [],
|
|
131
|
+
warnings: [`No plugins array found in ${rel} — add the fimo() plugin per the agent-prompt recipe.`],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const insertAt = pluginsMatch.index + pluginsMatch[0].length;
|
|
135
|
+
let next = `${code.slice(0, insertAt)}fimo(), ${code.slice(insertAt)}`;
|
|
136
|
+
const importLine = `import { fimo } from 'fimo/vite';`;
|
|
137
|
+
const lastImport = [...next.matchAll(/^import .*$/gm)].pop();
|
|
138
|
+
next =
|
|
139
|
+
lastImport && lastImport.index !== undefined
|
|
140
|
+
? `${next.slice(0, lastImport.index + lastImport[0].length)}\n${importLine}${next.slice(lastImport.index + lastImport[0].length)}`
|
|
141
|
+
: `${importLine}\n${next}`;
|
|
142
|
+
fs.writeFileSync(configFile, next);
|
|
143
|
+
return { changes: [`added the fimo() plugin to ${rel}`], warnings: [] };
|
|
144
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared next-intl namespace resolution for the two independent label-key
|
|
3
|
+
* scanners (`extract-translations` for `fimo validate`, `scan-wrappable` for
|
|
4
|
+
* `migrate wrap --check`). Both must record the SAME key form the i18n
|
|
5
|
+
* ingestion stores, or the seeding gate can never go green.
|
|
6
|
+
*
|
|
7
|
+
* `const t = useTranslations("plans.checkout")` scopes every `t("buyNow")` in
|
|
8
|
+
* that component to `plans.checkout.buyNow`. Root-scoped `useTranslations()`
|
|
9
|
+
* (namespace `""`) and server `getTranslations` are handled; a bare `t(...)`
|
|
10
|
+
* with no binding (Fimo's own label call) keeps its key unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export declare const isFunctionLike: (node: any) => boolean;
|
|
13
|
+
export declare function collectScopedBindings(fnNode: any): Map<string, string | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a `<name>("leaf")` call to its full key given the scope stack
|
|
16
|
+
* (innermost first).
|
|
17
|
+
* - `scoped`: the callee is a resolved `useTranslations` binding — namespace
|
|
18
|
+
* prepended (unless root `""`).
|
|
19
|
+
* - `unresolvable`: the callee is a passed-in parameter — its namespace lives in
|
|
20
|
+
* the caller and cannot be known here, so the caller should count the call as
|
|
21
|
+
* covered but NOT demand the (bare) key.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveScopedKey(calleeName: string, leafKey: string, scopes: Array<Map<string, string | null>>): {
|
|
24
|
+
key: string;
|
|
25
|
+
scoped: boolean;
|
|
26
|
+
unresolvable: boolean;
|
|
27
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared next-intl namespace resolution for the two independent label-key
|
|
3
|
+
* scanners (`extract-translations` for `fimo validate`, `scan-wrappable` for
|
|
4
|
+
* `migrate wrap --check`). Both must record the SAME key form the i18n
|
|
5
|
+
* ingestion stores, or the seeding gate can never go green.
|
|
6
|
+
*
|
|
7
|
+
* `const t = useTranslations("plans.checkout")` scopes every `t("buyNow")` in
|
|
8
|
+
* that component to `plans.checkout.buyNow`. Root-scoped `useTranslations()`
|
|
9
|
+
* (namespace `""`) and server `getTranslations` are handled; a bare `t(...)`
|
|
10
|
+
* with no binding (Fimo's own label call) keeps its key unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export const isFunctionLike = (node) => node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
|
|
13
|
+
/** Collect `useTranslations`/`getTranslations` bindings declared in a function's
|
|
14
|
+
* direct body: variable name → namespace (`""` for root scope). */
|
|
15
|
+
const unwrapAwait = (node) => (node?.type === 'AwaitExpression' ? node.argument : node);
|
|
16
|
+
/**
|
|
17
|
+
* The namespace of a `useTranslations`/`getTranslations` call, or null if the
|
|
18
|
+
* node is not such a call. Handles the string arg (`getTranslations("seo")`),
|
|
19
|
+
* the object arg (`getTranslations({ locale, namespace: "seo" })`), and no arg
|
|
20
|
+
* (root scope → `""`). A dynamic namespace resolves to `""` (best effort).
|
|
21
|
+
*/
|
|
22
|
+
function translationCallNamespace(node) {
|
|
23
|
+
if (node?.type !== 'CallExpression' || node.callee?.type !== 'Identifier') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (node.callee.name !== 'useTranslations' && node.callee.name !== 'getTranslations') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const arg = node.arguments?.[0];
|
|
30
|
+
if (!arg) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
34
|
+
return arg.value;
|
|
35
|
+
}
|
|
36
|
+
if (arg.type === 'ObjectExpression') {
|
|
37
|
+
for (const prop of arg.properties ?? []) {
|
|
38
|
+
const keyName = prop?.key?.name ?? prop?.key?.value;
|
|
39
|
+
if (keyName === 'namespace' && prop.value?.type === 'Literal' && typeof prop.value.value === 'string') {
|
|
40
|
+
return prop.value.value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
/** Mark every identifier bound by a parameter pattern (plain, defaulted, or
|
|
47
|
+
* destructured — `t`, `t = x`, `{ t }`, `[t]`) as unresolvable (null). */
|
|
48
|
+
function markParamBindings(node, bindings) {
|
|
49
|
+
if (!node) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
switch (node.type) {
|
|
53
|
+
case 'Identifier':
|
|
54
|
+
if (!bindings.has(node.name)) {
|
|
55
|
+
bindings.set(node.name, null);
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case 'AssignmentPattern':
|
|
59
|
+
markParamBindings(node.left, bindings);
|
|
60
|
+
break;
|
|
61
|
+
case 'RestElement':
|
|
62
|
+
markParamBindings(node.argument, bindings);
|
|
63
|
+
break;
|
|
64
|
+
case 'ArrayPattern':
|
|
65
|
+
for (const element of node.elements ?? []) {
|
|
66
|
+
markParamBindings(element, bindings);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case 'ObjectPattern':
|
|
70
|
+
for (const prop of node.properties ?? []) {
|
|
71
|
+
markParamBindings(prop.type === 'RestElement' ? prop.argument : prop.value, bindings);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function collectScopedBindings(fnNode) {
|
|
79
|
+
const bindings = new Map();
|
|
80
|
+
const body = fnNode.body;
|
|
81
|
+
const statements = body && Array.isArray(body.body) ? body.body : [];
|
|
82
|
+
for (const stmt of statements) {
|
|
83
|
+
if (stmt?.type !== 'VariableDeclaration') {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const decl of stmt.declarations ?? []) {
|
|
87
|
+
const init = unwrapAwait(decl?.init);
|
|
88
|
+
// `const t = [await] useTranslations/getTranslations(...)`
|
|
89
|
+
if (decl?.id?.type === 'Identifier') {
|
|
90
|
+
const namespace = translationCallNamespace(init);
|
|
91
|
+
if (namespace !== null) {
|
|
92
|
+
bindings.set(decl.id.name, namespace);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// `const [a, t] = await Promise.all([...])` or `= use(Promise.all([...]))`
|
|
97
|
+
if (decl?.id?.type === 'ArrayPattern') {
|
|
98
|
+
// Unwrap React's `use()` wrapper around the promise.
|
|
99
|
+
const source = init?.type === 'CallExpression' && init.callee?.type === 'Identifier' && init.callee.name === 'use'
|
|
100
|
+
? unwrapAwait(init.arguments?.[0])
|
|
101
|
+
: init;
|
|
102
|
+
const promiseAll = source?.type === 'CallExpression' &&
|
|
103
|
+
source.callee?.type === 'MemberExpression' &&
|
|
104
|
+
source.callee.object?.name === 'Promise' &&
|
|
105
|
+
source.callee.property?.name === 'all';
|
|
106
|
+
const elements = promiseAll
|
|
107
|
+
? source.arguments?.[0]?.type === 'ArrayExpression'
|
|
108
|
+
? source.arguments[0].elements
|
|
109
|
+
: null
|
|
110
|
+
: source?.type === 'ArrayExpression'
|
|
111
|
+
? source.elements
|
|
112
|
+
: null;
|
|
113
|
+
if (elements) {
|
|
114
|
+
decl.id.elements?.forEach((el, index) => {
|
|
115
|
+
if (el?.type !== 'Identifier') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const namespace = translationCallNamespace(unwrapAwait(elements[index]));
|
|
119
|
+
if (namespace !== null) {
|
|
120
|
+
bindings.set(el.name, namespace);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Function parameters are bound by the CALLER: a `t` passed in carries the
|
|
128
|
+
// caller's namespace, unknowable here. Mark them null so their t() calls are
|
|
129
|
+
// counted as covered but never demanded by the seeding gate as a bare leaf.
|
|
130
|
+
for (const param of fnNode.params ?? []) {
|
|
131
|
+
markParamBindings(param, bindings);
|
|
132
|
+
}
|
|
133
|
+
return bindings;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a `<name>("leaf")` call to its full key given the scope stack
|
|
137
|
+
* (innermost first).
|
|
138
|
+
* - `scoped`: the callee is a resolved `useTranslations` binding — namespace
|
|
139
|
+
* prepended (unless root `""`).
|
|
140
|
+
* - `unresolvable`: the callee is a passed-in parameter — its namespace lives in
|
|
141
|
+
* the caller and cannot be known here, so the caller should count the call as
|
|
142
|
+
* covered but NOT demand the (bare) key.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveScopedKey(calleeName, leafKey, scopes) {
|
|
145
|
+
for (let i = scopes.length - 1; i >= 0; i -= 1) {
|
|
146
|
+
if (scopes[i].has(calleeName)) {
|
|
147
|
+
const namespace = scopes[i].get(calleeName);
|
|
148
|
+
if (namespace === null || namespace === undefined) {
|
|
149
|
+
return { key: leafKey, scoped: false, unresolvable: true };
|
|
150
|
+
}
|
|
151
|
+
return { key: namespace ? `${namespace}.${leafKey}` : leafKey, scoped: true, unresolvable: false };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { key: leafKey, scoped: false, unresolvable: false };
|
|
155
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-facing names derived from slugs and identifiers — route labels, page
|
|
3
|
+
* names, field titles. One rule set so "ai-stack" reads "AI Stack" and
|
|
4
|
+
* "agentic-cms" reads "Agentic CMS" everywhere the machine writes a name.
|
|
5
|
+
*/
|
|
6
|
+
/** "ai-stack" → "AI Stack"; "agenticCms" → "Agentic CMS"; "blog" → "Blog". */
|
|
7
|
+
export declare function humanizeSegment(segment: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* The discovery-time label for a route path. Dynamic templates never share
|
|
10
|
+
* the parent index page's name (the run-4 tree showed "Blog" twice): the
|
|
11
|
+
* param joins the label ("/blog/:slug" → "Blog Slug") until a schema
|
|
12
|
+
* binding upgrades it to the schema's name ("Blog Post").
|
|
13
|
+
*/
|
|
14
|
+
export declare function discoveredRouteLabel(path: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-facing names derived from slugs and identifiers — route labels, page
|
|
3
|
+
* names, field titles. One rule set so "ai-stack" reads "AI Stack" and
|
|
4
|
+
* "agentic-cms" reads "Agentic CMS" everywhere the machine writes a name.
|
|
5
|
+
*/
|
|
6
|
+
const ACRONYMS = new Set([
|
|
7
|
+
'ai',
|
|
8
|
+
'api',
|
|
9
|
+
'cd',
|
|
10
|
+
'ci',
|
|
11
|
+
'cli',
|
|
12
|
+
'cms',
|
|
13
|
+
'cta',
|
|
14
|
+
'faq',
|
|
15
|
+
'geo',
|
|
16
|
+
'llm',
|
|
17
|
+
'sdk',
|
|
18
|
+
'seo',
|
|
19
|
+
'spa',
|
|
20
|
+
'ssg',
|
|
21
|
+
'ssr',
|
|
22
|
+
'ui',
|
|
23
|
+
'url',
|
|
24
|
+
'ux',
|
|
25
|
+
]);
|
|
26
|
+
/** "ai-stack" → "AI Stack"; "agenticCms" → "Agentic CMS"; "blog" → "Blog". */
|
|
27
|
+
export function humanizeSegment(segment) {
|
|
28
|
+
return segment
|
|
29
|
+
.replace(/[_\-]+/g, ' ')
|
|
30
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((word) => ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
34
|
+
.join(' ');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The discovery-time label for a route path. Dynamic templates never share
|
|
38
|
+
* the parent index page's name (the run-4 tree showed "Blog" twice): the
|
|
39
|
+
* param joins the label ("/blog/:slug" → "Blog Slug") until a schema
|
|
40
|
+
* binding upgrades it to the schema's name ("Blog Post").
|
|
41
|
+
*/
|
|
42
|
+
export function discoveredRouteLabel(path) {
|
|
43
|
+
if (path === '/') {
|
|
44
|
+
return 'Home';
|
|
45
|
+
}
|
|
46
|
+
const parts = path.split('/').filter(Boolean);
|
|
47
|
+
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
48
|
+
const base = humanizeSegment(lastStatic ?? parts[parts.length - 1]?.replace(/^:/, '') ?? path);
|
|
49
|
+
const lastPart = parts[parts.length - 1];
|
|
50
|
+
if (lastPart?.startsWith(':') && lastStatic) {
|
|
51
|
+
return `${base} ${humanizeSegment(lastPart.replace(/^:|\*$/g, ''))}`;
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
@@ -23,6 +23,7 @@ export interface PhaseResult {
|
|
|
23
23
|
skipped?: {
|
|
24
24
|
reason: string;
|
|
25
25
|
};
|
|
26
|
+
warnings?: string[];
|
|
26
27
|
}
|
|
27
28
|
export interface ValidationReport {
|
|
28
29
|
ok: boolean;
|
|
@@ -33,12 +34,9 @@ export interface ValidationReport {
|
|
|
33
34
|
sync: PhaseResult;
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
export declare function emptyPhase(name: string): PhaseResult;
|
|
37
37
|
export declare function skippedPhase(name: string, reason: string): PhaseResult;
|
|
38
38
|
export declare function computeExitCode(report: ValidationReport): 0 | 1 | 2;
|
|
39
39
|
export declare function finalizeReport(report: Omit<ValidationReport, 'ok' | 'exitCode'>): ValidationReport;
|
|
40
40
|
export declare function authorError(opts: Omit<ValidationError, 'kind'>): ValidationError;
|
|
41
|
-
export declare function infraError(opts: Omit<ValidationError, 'kind'>): ValidationError;
|
|
42
41
|
export declare function infraErrorFromException(rule: string, error: unknown): ValidationError;
|
|
43
|
-
export declare function formatReportJson(report: ValidationReport): string;
|
|
44
42
|
export declare function formatReportHuman(report: ValidationReport): string;
|
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
* (the Fimo backend is unreachable or returned a server error — agent should
|
|
7
7
|
* surface to the human and stop). Exit codes follow the same split.
|
|
8
8
|
*/
|
|
9
|
-
export function emptyPhase(name) {
|
|
10
|
-
return { name, ok: true, errors: [] };
|
|
11
|
-
}
|
|
12
9
|
export function skippedPhase(name, reason) {
|
|
13
10
|
return { name, ok: false, errors: [], skipped: { reason } };
|
|
14
11
|
}
|
|
@@ -32,9 +29,6 @@ export function finalizeReport(report) {
|
|
|
32
29
|
export function authorError(opts) {
|
|
33
30
|
return { kind: 'author', ...opts };
|
|
34
31
|
}
|
|
35
|
-
export function infraError(opts) {
|
|
36
|
-
return { kind: 'infra', ...opts };
|
|
37
|
-
}
|
|
38
32
|
export function infraErrorFromException(rule, error) {
|
|
39
33
|
return {
|
|
40
34
|
kind: 'infra',
|
|
@@ -42,9 +36,6 @@ export function infraErrorFromException(rule, error) {
|
|
|
42
36
|
message: error instanceof Error ? error.message : String(error),
|
|
43
37
|
};
|
|
44
38
|
}
|
|
45
|
-
export function formatReportJson(report) {
|
|
46
|
-
return JSON.stringify(report, null, 2);
|
|
47
|
-
}
|
|
48
39
|
export function formatReportHuman(report) {
|
|
49
40
|
const lines = [];
|
|
50
41
|
for (const phase of [report.phases.checks, report.phases.codegen, report.phases.sync]) {
|
|
@@ -76,6 +67,9 @@ function formatPhaseHuman(phase) {
|
|
|
76
67
|
out.push(` ${parts.join(', ')}`);
|
|
77
68
|
}
|
|
78
69
|
}
|
|
70
|
+
for (const warning of phase.warnings ?? []) {
|
|
71
|
+
out.push(` ⚠ ${warning}`);
|
|
72
|
+
}
|
|
79
73
|
for (const err of phase.errors) {
|
|
80
74
|
const loc = err.file ? ` (${err.file}${err.line ? `:${err.line}` : ''}${err.column ? `:${err.column}` : ''})` : '';
|
|
81
75
|
out.push(` [${err.kind}] ${err.rule}: ${err.message}${loc}`);
|