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,78 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', '.fimo']);
|
|
4
|
+
export const DEFAULT_COMPONENT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
5
|
+
export function walkFiles(dir, matches, out) {
|
|
6
|
+
let entries;
|
|
7
|
+
try {
|
|
8
|
+
entries = readdirSync(dir);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (SKIP_DIRS.has(entry)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const full = join(dir, entry);
|
|
18
|
+
let isDir = false;
|
|
19
|
+
try {
|
|
20
|
+
isDir = statSync(full).isDirectory();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (isDir) {
|
|
26
|
+
walkFiles(full, matches, out);
|
|
27
|
+
}
|
|
28
|
+
else if (matches(entry)) {
|
|
29
|
+
out.push(full);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Import map of a routes file: local component name → module specifier. */
|
|
34
|
+
export function componentImports(code) {
|
|
35
|
+
const imports = new Map();
|
|
36
|
+
for (const match of code.matchAll(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
37
|
+
imports.set(match[1], match[2]);
|
|
38
|
+
}
|
|
39
|
+
for (const match of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
40
|
+
for (const token of match[1].split(',')) {
|
|
41
|
+
const name = token.replace(/\s+as\s+(\w+)\s*$/, '$1').trim();
|
|
42
|
+
if (/^\w+$/.test(name)) {
|
|
43
|
+
imports.set(name, match[2]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const match of code.matchAll(/(?:const|let)\s+(\w+)\s*=\s*(?:React\.)?lazy\(\s*(?:async\s*)?\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
48
|
+
imports.set(match[1], match[2]);
|
|
49
|
+
}
|
|
50
|
+
return imports;
|
|
51
|
+
}
|
|
52
|
+
export function resolveModuleFile(cwd, fromFile, spec, extensions = DEFAULT_COMPONENT_EXTENSIONS) {
|
|
53
|
+
let base;
|
|
54
|
+
if (spec.startsWith('.')) {
|
|
55
|
+
base = join(dirname(fromFile), spec);
|
|
56
|
+
}
|
|
57
|
+
else if (spec.startsWith('~/') || spec.startsWith('@/')) {
|
|
58
|
+
base = join(cwd, 'src', spec.slice(2));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
for (const candidate of [
|
|
64
|
+
...extensions.map((ext) => `${base}${ext}`),
|
|
65
|
+
...extensions.map((ext) => join(base, `index${ext}`)),
|
|
66
|
+
base,
|
|
67
|
+
]) {
|
|
68
|
+
try {
|
|
69
|
+
if (statSync(candidate).isFile()) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* try next */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CollectionCandidate } from '../scan-wrappable.js';
|
|
2
|
+
/**
|
|
3
|
+
* Page-structure drafts — the machine's proposed NESTED model of a page.
|
|
4
|
+
* The scanner already sees everything the model needs: the page file's
|
|
5
|
+
* section components (capitalized JSX children, in document order), each
|
|
6
|
+
* section's label keys (its file-derived prefix) and its in-code collection
|
|
7
|
+
* candidates (typed rows). Joined, that IS the page's Single: one nested
|
|
8
|
+
* block field per section, recursing into the section's own components so
|
|
9
|
+
* deep structure (a Features grid whose groups live in their own files)
|
|
10
|
+
* surfaces too. The agent refines names and shapes — it no longer decides
|
|
11
|
+
* WHETHER to model, and real marketing copy stops dying as "page furniture"
|
|
12
|
+
* exemptions.
|
|
13
|
+
*/
|
|
14
|
+
export interface PageSectionDraft {
|
|
15
|
+
/** Proposed field key on the page Single (camelCase of the component). */
|
|
16
|
+
key: string;
|
|
17
|
+
component: string;
|
|
18
|
+
file: string;
|
|
19
|
+
/** The section's label-key prefix (file-derived). */
|
|
20
|
+
prefix: string;
|
|
21
|
+
/** Staged label keys the section owns — its scalar copy fields. */
|
|
22
|
+
labelKeys: string[];
|
|
23
|
+
/** In-code collection candidates found in the section's file — its
|
|
24
|
+
* repeated block items (cards, quotes, logos), with their source rows so
|
|
25
|
+
* the Single's document can be assembled without reopening the code. */
|
|
26
|
+
collections: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
fields: string[];
|
|
29
|
+
count: number;
|
|
30
|
+
rows?: Array<Record<string, unknown>>;
|
|
31
|
+
}>;
|
|
32
|
+
/** Content-bearing components the section renders — nested blocks. */
|
|
33
|
+
children?: PageSectionDraft[];
|
|
34
|
+
/** The same file drafts on several pages — chrome. Model it ONCE as a
|
|
35
|
+
* Single Type, never as a per-page block. */
|
|
36
|
+
shared?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface PageStructureDraft {
|
|
39
|
+
pageId: string;
|
|
40
|
+
/** Proposed Single uid (PascalCase of the page id). */
|
|
41
|
+
single: string;
|
|
42
|
+
sections: PageSectionDraft[];
|
|
43
|
+
}
|
|
44
|
+
export declare function draftPageStructures(args: {
|
|
45
|
+
cwd: string;
|
|
46
|
+
framework: string | null;
|
|
47
|
+
routes: Record<string, {
|
|
48
|
+
path?: string;
|
|
49
|
+
labelPrefixes?: unknown;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
stagedKeys: Iterable<string>;
|
|
53
|
+
collections: CollectionCandidate[];
|
|
54
|
+
}): PageStructureDraft[];
|
|
55
|
+
/** Flat section count across the nesting — the summary's visibility number. */
|
|
56
|
+
export declare function countDraftSections(draft: PageStructureDraft): number;
|
|
57
|
+
/**
|
|
58
|
+
* The machine-BUILT page Single: a ready-to-push singleton schema plus its
|
|
59
|
+
* filled document, assembled from the draft's non-shared sections (shared
|
|
60
|
+
* sections are chrome — modeled once elsewhere). One `max: 1` block per
|
|
61
|
+
* section; the section's labels become scalar fields carrying the staged
|
|
62
|
+
* values, its repeated arrays become nested blocks carrying the source
|
|
63
|
+
* rows, its child components nest the same way. The agent renames fields
|
|
64
|
+
* to editorial names and rewires reads — it no longer composes anything.
|
|
65
|
+
*/
|
|
66
|
+
export interface PageSingleDraft {
|
|
67
|
+
uid: string;
|
|
68
|
+
pageId: string;
|
|
69
|
+
schema: Record<string, unknown>;
|
|
70
|
+
document: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
export declare function buildPageSingleDraft(draft: PageStructureDraft, labelValues: Record<string, string>): PageSingleDraft | null;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { frameworkSectionStyle } from '../frameworks/discover.js';
|
|
4
|
+
import { componentImports, resolveModuleFile, resolveRouteFiles } from './label-prefixes.js';
|
|
5
|
+
const MAX_SECTION_DEPTH = 3;
|
|
6
|
+
const MAX_DRAFT_ROWS = 50;
|
|
7
|
+
const pascal = (value) => value
|
|
8
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
9
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
10
|
+
.split(/[-_.\s]/)
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
13
|
+
.join('');
|
|
14
|
+
const camel = (value) => {
|
|
15
|
+
const p = pascal(value);
|
|
16
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
17
|
+
};
|
|
18
|
+
/** Capitalized JSX tags in DOCUMENT order (deduped, first appearance). */
|
|
19
|
+
function sectionTags(code) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const tags = [];
|
|
22
|
+
for (const match of code.matchAll(/<([A-Z]\w*)[\s/>]/g)) {
|
|
23
|
+
if (!seen.has(match[1])) {
|
|
24
|
+
seen.add(match[1]);
|
|
25
|
+
tags.push(match[1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return tags;
|
|
29
|
+
}
|
|
30
|
+
function draftSection(context, tag, file, depth) {
|
|
31
|
+
const rel = relative(context.cwd, file);
|
|
32
|
+
const prefix = context.style.fileScope(rel);
|
|
33
|
+
const labelKeys = context.keysByPrefix.get(prefix) ?? [];
|
|
34
|
+
const collections = (context.collectionsByFile.get(rel) ?? []).map((candidate) => ({
|
|
35
|
+
name: candidate.name,
|
|
36
|
+
fields: candidate.fields,
|
|
37
|
+
count: candidate.count,
|
|
38
|
+
...(candidate.typedRows ? { rows: candidate.typedRows.slice(0, MAX_DRAFT_ROWS) } : {}),
|
|
39
|
+
}));
|
|
40
|
+
const children = depth < MAX_SECTION_DEPTH ? draftChildSections(context, file, depth + 1) : [];
|
|
41
|
+
if (labelKeys.length === 0 && collections.length === 0 && children.length === 0) {
|
|
42
|
+
return null; // pure layout — nothing to model
|
|
43
|
+
}
|
|
44
|
+
const draft = { key: camel(tag), component: tag, file: rel, prefix, labelKeys, collections };
|
|
45
|
+
if (children.length > 0) {
|
|
46
|
+
draft.children = children;
|
|
47
|
+
}
|
|
48
|
+
return draft;
|
|
49
|
+
}
|
|
50
|
+
/** Auto-imported components (Nuxt's components/ convention): a tag with no
|
|
51
|
+
* import statement may still be a local file at a conventional path. */
|
|
52
|
+
function resolveAutoImportedComponent(context, tag) {
|
|
53
|
+
for (const dir of context.style.fallbackComponentDirs) {
|
|
54
|
+
for (const ext of context.style.extensions) {
|
|
55
|
+
const candidate = join(context.cwd, dir, `${tag}${ext}`);
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(candidate).isFile()) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* try next */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function draftChildSections(context, file, depth) {
|
|
69
|
+
let code;
|
|
70
|
+
try {
|
|
71
|
+
code = readFileSync(file, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const imports = componentImports(code);
|
|
77
|
+
const sections = [];
|
|
78
|
+
for (const tag of sectionTags(code)) {
|
|
79
|
+
const spec = imports.get(tag);
|
|
80
|
+
const sectionFile = spec
|
|
81
|
+
? resolveModuleFile(context.cwd, file, spec, context.style.extensions)
|
|
82
|
+
: resolveAutoImportedComponent(context, tag);
|
|
83
|
+
if (!sectionFile || context.visited.has(sectionFile)) {
|
|
84
|
+
continue; // library components (Button, Link) have no local file
|
|
85
|
+
}
|
|
86
|
+
context.visited.add(sectionFile);
|
|
87
|
+
const section = draftSection(context, tag, sectionFile, depth);
|
|
88
|
+
if (section) {
|
|
89
|
+
sections.push(section);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sections;
|
|
93
|
+
}
|
|
94
|
+
export function draftPageStructures(args) {
|
|
95
|
+
const { cwd, framework, routes, collections } = args;
|
|
96
|
+
const keysByPrefix = new Map();
|
|
97
|
+
for (const key of args.stagedKeys) {
|
|
98
|
+
const dot = key.indexOf('.');
|
|
99
|
+
if (dot > 0) {
|
|
100
|
+
const prefix = key.slice(0, dot);
|
|
101
|
+
keysByPrefix.set(prefix, [...(keysByPrefix.get(prefix) ?? []), key]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const collectionsByFile = new Map();
|
|
105
|
+
for (const candidate of collections) {
|
|
106
|
+
collectionsByFile.set(candidate.file, [...(collectionsByFile.get(candidate.file) ?? []), candidate]);
|
|
107
|
+
}
|
|
108
|
+
const style = frameworkSectionStyle(framework);
|
|
109
|
+
const drafts = [];
|
|
110
|
+
for (const { routeId, file } of resolveRouteFiles(cwd, framework, routes)) {
|
|
111
|
+
const context = { cwd, style, keysByPrefix, collectionsByFile, visited: new Set([file]) };
|
|
112
|
+
const sections = draftChildSections(context, file, 1);
|
|
113
|
+
if (sections.length > 1) {
|
|
114
|
+
drafts.push({ pageId: routeId, single: pascal(routeId), sections });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
markSharedSections(drafts);
|
|
118
|
+
return drafts;
|
|
119
|
+
}
|
|
120
|
+
/** A section file drafted on several pages is chrome — flag it so the agent
|
|
121
|
+
* models it once (Single Type) instead of nesting it under every page. */
|
|
122
|
+
function markSharedSections(drafts) {
|
|
123
|
+
const pagesByFile = new Map();
|
|
124
|
+
const index = (pageId, sections) => {
|
|
125
|
+
for (const section of sections) {
|
|
126
|
+
pagesByFile.set(section.file, (pagesByFile.get(section.file) ?? new Set()).add(pageId));
|
|
127
|
+
index(pageId, section.children ?? []);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
for (const draft of drafts) {
|
|
131
|
+
index(draft.pageId, draft.sections);
|
|
132
|
+
}
|
|
133
|
+
const flag = (sections) => {
|
|
134
|
+
for (const section of sections) {
|
|
135
|
+
if ((pagesByFile.get(section.file)?.size ?? 0) > 1) {
|
|
136
|
+
section.shared = true;
|
|
137
|
+
}
|
|
138
|
+
flag(section.children ?? []);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
for (const draft of drafts) {
|
|
142
|
+
flag(draft.sections);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Flat section count across the nesting — the summary's visibility number. */
|
|
146
|
+
export function countDraftSections(draft) {
|
|
147
|
+
const walk = (sections) => sections.reduce((total, section) => total + 1 + walk(section.children ?? []), 0);
|
|
148
|
+
return walk(draft.sections);
|
|
149
|
+
}
|
|
150
|
+
const labelFieldName = (key) => camel(key.split('.').pop() ?? key);
|
|
151
|
+
const scalarType = (value) => value.length > 80 || value.includes('\n') ? 'text' : 'string';
|
|
152
|
+
function sectionBlock(section, labelValues) {
|
|
153
|
+
const fields = {};
|
|
154
|
+
const value = {};
|
|
155
|
+
let fieldCount = 0;
|
|
156
|
+
for (const key of section.labelKeys) {
|
|
157
|
+
const name = labelFieldName(key);
|
|
158
|
+
if (fields[name]) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const labelValue = labelValues[key] ?? '';
|
|
162
|
+
fields[name] = { type: scalarType(labelValue) };
|
|
163
|
+
value[name] = labelValue;
|
|
164
|
+
fieldCount += 1;
|
|
165
|
+
}
|
|
166
|
+
for (const collection of section.collections) {
|
|
167
|
+
const name = camel(collection.name);
|
|
168
|
+
if (fields[name]) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const itemFields = {};
|
|
172
|
+
for (const field of collection.fields) {
|
|
173
|
+
itemFields[field] = { type: 'string' };
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(itemFields).length === 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
fields[name] = { type: 'blocks', schema: { fields: itemFields } };
|
|
179
|
+
value[name] = (collection.rows ?? []).map((row) => {
|
|
180
|
+
const item = {};
|
|
181
|
+
for (const field of collection.fields) {
|
|
182
|
+
const cell = row[field];
|
|
183
|
+
if (cell != null && typeof cell !== 'object') {
|
|
184
|
+
item[field] = typeof cell === 'string' ? cell : String(cell);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return item;
|
|
188
|
+
});
|
|
189
|
+
fieldCount += 1;
|
|
190
|
+
}
|
|
191
|
+
for (const child of section.children ?? []) {
|
|
192
|
+
if (child.shared) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const block = sectionBlock(child, labelValues);
|
|
196
|
+
if (!block || fields[child.key]) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
fields[child.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
|
|
200
|
+
value[child.key] = [block.value];
|
|
201
|
+
fieldCount += block.fieldCount;
|
|
202
|
+
}
|
|
203
|
+
if (Object.keys(fields).length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return { fields, value, fieldCount };
|
|
207
|
+
}
|
|
208
|
+
export function buildPageSingleDraft(draft, labelValues) {
|
|
209
|
+
const fields = {};
|
|
210
|
+
const document = {};
|
|
211
|
+
let totalFields = 0;
|
|
212
|
+
for (const section of draft.sections) {
|
|
213
|
+
if (section.shared || fields[section.key]) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const block = sectionBlock(section, labelValues);
|
|
217
|
+
if (!block) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
fields[section.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
|
|
221
|
+
document[section.key] = [block.value];
|
|
222
|
+
totalFields += block.fieldCount;
|
|
223
|
+
}
|
|
224
|
+
// A one-field page is a label, not a Single — only real structure drafts.
|
|
225
|
+
if (Object.keys(fields).length === 0 || totalFields < 3) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
uid: draft.single,
|
|
230
|
+
pageId: draft.pageId,
|
|
231
|
+
schema: { uid: draft.single, name: draft.single, isSingleton: true, fields },
|
|
232
|
+
document,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inverse of extract-translations: finds content that is NOT yet wrapped with
|
|
3
|
+
* Fimo primitives — hardcoded JSX text, image/video sources, file links, and
|
|
4
|
+
* user-visible attribute strings. Drives the wrap migration's coverage gate:
|
|
5
|
+
* every item must end wrapped, ignored-with-a-reason, or unresolved.
|
|
6
|
+
*/
|
|
7
|
+
export type WrappableKind = 'text' | 'formatted-text' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
8
|
+
export interface WrappableItem {
|
|
9
|
+
kind: WrappableKind;
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
preview: string;
|
|
13
|
+
/** AST span info for the --apply codemod (offsets into the file). */
|
|
14
|
+
span?: {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
parentTag?: string;
|
|
18
|
+
openNameStart?: number;
|
|
19
|
+
openNameEnd?: number;
|
|
20
|
+
closeNameStart?: number;
|
|
21
|
+
closeNameEnd?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* In-code collection candidate: an array literal of same-shaped objects with
|
|
26
|
+
* user-visible string fields (services cards, FAQ items, testimonials, …).
|
|
27
|
+
* Flat labels are the wrong shape for these — they belong in a Fimo
|
|
28
|
+
* collection (schema + entries), migrated by the agent.
|
|
29
|
+
*/
|
|
30
|
+
export interface CollectionCandidate {
|
|
31
|
+
name: string;
|
|
32
|
+
file: string;
|
|
33
|
+
line: number;
|
|
34
|
+
count: number;
|
|
35
|
+
fields: string[];
|
|
36
|
+
sample: string;
|
|
37
|
+
/**
|
|
38
|
+
* Cross-collection references detected by value matching: this candidate's
|
|
39
|
+
* field values point at another candidate's identity values. Migrated as
|
|
40
|
+
* Fimo `reference` fields (`target`/`targets` + `many`), never as strings.
|
|
41
|
+
*/
|
|
42
|
+
references?: CandidateReference[];
|
|
43
|
+
/** Per-field string values (capped) — internal input to reference
|
|
44
|
+
* detection; stripped before the plan is written. */
|
|
45
|
+
fieldValues?: Record<string, string[]>;
|
|
46
|
+
/** First row with NATIVE value types — internal input to schema-draft
|
|
47
|
+
* type inference; stripped before the plan is written. */
|
|
48
|
+
typedSample?: Record<string, unknown>;
|
|
49
|
+
/** All rows (capped) with NATIVE value types — internal input to the
|
|
50
|
+
* rows-staging generator; stripped before the plan is written. */
|
|
51
|
+
typedRows?: Record<string, unknown>[];
|
|
52
|
+
manyFields?: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface CandidateReference {
|
|
55
|
+
field: string;
|
|
56
|
+
/** Candidate name(s) the values point at: one = fixed target, several = polymorphic. */
|
|
57
|
+
targets: string[];
|
|
58
|
+
many: boolean;
|
|
59
|
+
}
|
|
60
|
+
interface ReferenceMatchable {
|
|
61
|
+
name: string;
|
|
62
|
+
references?: CandidateReference[];
|
|
63
|
+
fieldValues?: Record<string, string[]>;
|
|
64
|
+
manyFields?: string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Annotate candidates whose field values point at another candidate's
|
|
68
|
+
* identity values: one full-coverage target = fixed reference, several
|
|
69
|
+
* partial targets covering together = polymorphic. Mutates in place.
|
|
70
|
+
*
|
|
71
|
+
* Coverage is OCCURRENCE-weighted and dangling-adjusted: values matching no
|
|
72
|
+
* candidate's identity set are dangling references by definition (deleted
|
|
73
|
+
* targets, sentinels) and drop out of the denominator, weighted by how
|
|
74
|
+
* often they occur — 1,700 case studies pointing at 12 clients with 190
|
|
75
|
+
* distinct ghost values is still a reference field, because 89% of the ROWS
|
|
76
|
+
* resolve. Guards against coincidence: at least 2 distinct matched values
|
|
77
|
+
* and at least half the occurrences must resolve somewhere.
|
|
78
|
+
*/
|
|
79
|
+
export declare function annotateCandidateReferences(candidates: ReferenceMatchable[]): void;
|
|
80
|
+
/**
|
|
81
|
+
* External live data source: content fetched at render time from a
|
|
82
|
+
* third-party API (GitHub stats, dev.to posts, a headless CMS). Invisible to
|
|
83
|
+
* the wrap scan by nature — the migration must ASK the user what to do with
|
|
84
|
+
* it (keep live vs ingest) instead of guessing.
|
|
85
|
+
*/
|
|
86
|
+
export interface ExternalSource {
|
|
87
|
+
host: string;
|
|
88
|
+
files: string[];
|
|
89
|
+
count: number;
|
|
90
|
+
/** 'cms' when detected via a headless-CMS SDK dependency: the site's
|
|
91
|
+
* CONTENT lives there — migrating it is the point, not an option. */
|
|
92
|
+
kind?: 'api' | 'cms';
|
|
93
|
+
}
|
|
94
|
+
/** Identities of already-wrapped content, for the seeding gate. */
|
|
95
|
+
export interface WrappedIds {
|
|
96
|
+
labels: string[];
|
|
97
|
+
media: string[];
|
|
98
|
+
richText: string[];
|
|
99
|
+
files: string[];
|
|
100
|
+
/** Every wrapped key, ALL kinds, in ENCOUNTER order (per-file walk, files
|
|
101
|
+
* in walk order) — the on-page reading order the Studio Pages view sorts
|
|
102
|
+
* by. Deduplicated keeping the first occurrence; never sorted. */
|
|
103
|
+
ordered: string[];
|
|
104
|
+
}
|
|
105
|
+
export interface WrapScanResult {
|
|
106
|
+
items: WrappableItem[];
|
|
107
|
+
wrappedCount: number;
|
|
108
|
+
filesScanned: number;
|
|
109
|
+
wrappedIds: WrappedIds;
|
|
110
|
+
collections: CollectionCandidate[];
|
|
111
|
+
externalSources: ExternalSource[];
|
|
112
|
+
dataFiles: DataCopyFile[];
|
|
113
|
+
/** `t()` calls bound to a FOREIGN i18n library (next-intl, i18next, …), not
|
|
114
|
+
* Fimo — surfaced so the migration never claims them as editable coverage. */
|
|
115
|
+
foreignI18nCount: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A .json data file whose values are site copy (config-driven sites keep ALL
|
|
119
|
+
* their text there and templates render only expressions, so template/JSX
|
|
120
|
+
* scanning finds nothing). Each detected file must be triaged in the manifest
|
|
121
|
+
* (`wrap.dataFiles`) — otherwise a copy-in-config site would pass the
|
|
122
|
+
* coverage gate with zero editable content.
|
|
123
|
+
*/
|
|
124
|
+
export interface DataCopyFile {
|
|
125
|
+
file: string;
|
|
126
|
+
strings: number;
|
|
127
|
+
samples: string[];
|
|
128
|
+
collections: DataFileCollection[];
|
|
129
|
+
/** 'markdown' groups: prose documents (md/mdx) aggregated per directory —
|
|
130
|
+
* content the wrap cannot label-ify (Tier 1b ingestion territory), but the
|
|
131
|
+
* triage decision must still be explicit. */
|
|
132
|
+
kind?: 'markdown';
|
|
133
|
+
/** Number of documents behind a markdown group entry. */
|
|
134
|
+
count?: number;
|
|
135
|
+
}
|
|
136
|
+
export interface DataFileCollection {
|
|
137
|
+
path: string;
|
|
138
|
+
length: number;
|
|
139
|
+
copyFields: string[];
|
|
140
|
+
references?: CandidateReference[];
|
|
141
|
+
/** Internal input to reference detection; stripped before the plan is written. */
|
|
142
|
+
fieldValues?: Record<string, string[]>;
|
|
143
|
+
/** First row with NATIVE value types — internal input to schema-draft
|
|
144
|
+
* type inference; stripped before the plan is written. */
|
|
145
|
+
typedSample?: Record<string, unknown>;
|
|
146
|
+
/** All rows (capped) with NATIVE value types — internal to rows staging. */
|
|
147
|
+
typedRows?: Record<string, unknown>[];
|
|
148
|
+
manyFields?: string[];
|
|
149
|
+
/** Fields forced to `string` regardless of inferred type — markdown/MDX
|
|
150
|
+
* bodies stay verbatim so the user's existing renderer keeps working
|
|
151
|
+
* (wrap mode never reshapes what the page already renders). */
|
|
152
|
+
verbatimFields?: string[];
|
|
153
|
+
}
|
|
154
|
+
export declare function scanWrappableFile(filePath: string, code: string, relPath: string, opts?: {
|
|
155
|
+
i18nFimoWired?: boolean;
|
|
156
|
+
}): {
|
|
157
|
+
items: WrappableItem[];
|
|
158
|
+
wrapped: number;
|
|
159
|
+
wrappedIds: WrappedIds;
|
|
160
|
+
collections: CollectionCandidate[];
|
|
161
|
+
foreignI18n?: number;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Parse ONE markdown/MDX document into a typed row: YAML frontmatter fields
|
|
165
|
+
* (natively typed — dates, booleans, numbers, arrays) plus a `slug` (from
|
|
166
|
+
* frontmatter or the filename stem) and a `body` holding the raw markdown
|
|
167
|
+
* after the frontmatter, verbatim. The body stays a string so the user's
|
|
168
|
+
* existing markdown renderer keeps rendering it unchanged (wrap mode).
|
|
169
|
+
*/
|
|
170
|
+
export declare function parseMarkdownDoc(fileName: string, raw: string): Record<string, unknown> | null;
|
|
171
|
+
/**
|
|
172
|
+
* Read every markdown/MDX document in a directory (non-recursively — one
|
|
173
|
+
* group per directory) into typed rows. Shared by the scanner (sampling for
|
|
174
|
+
* schema drafts + reference detection) and the rows stager (full dataset).
|
|
175
|
+
*/
|
|
176
|
+
export declare function readMarkdownGroupRows(dir: string): Array<Record<string, unknown>>;
|
|
177
|
+
export declare function scanDataJson(relPath: string, raw: string): DataCopyFile | null;
|
|
178
|
+
/**
|
|
179
|
+
* The label keys a `decision: "labels"` data file is expected to seed —
|
|
180
|
+
* one per prose string, keyed by JSON path under the recorded prefix. The
|
|
181
|
+
* seeding gate verifies each exists in the database.
|
|
182
|
+
*/
|
|
183
|
+
export declare function dataFileLabelKeys(raw: string, prefix: string, filePath?: string): string[];
|
|
184
|
+
/** Find render-time fetches of third-party APIs (fetch with an http(s) literal or template prefix). */
|
|
185
|
+
export declare function scanExternalSources(rootDir: string, dirs: string[]): ExternalSource[];
|
|
186
|
+
/**
|
|
187
|
+
* Scan a project's source directories for unwrapped content. `dirs` defaults
|
|
188
|
+
* to the common Next/React layouts that exist under `rootDir`.
|
|
189
|
+
*/
|
|
190
|
+
export declare function scanWrappable(rootDir: string, opts?: {
|
|
191
|
+
dirs?: string[];
|
|
192
|
+
excludePrefixes?: string[];
|
|
193
|
+
dataDirs?: string[];
|
|
194
|
+
keepInternal?: boolean;
|
|
195
|
+
/** The app's i18n message loader is wired to Fimo (imports
|
|
196
|
+
* `loadFimoMessages`), so its foreign `t()` calls ARE Fimo-backed and
|
|
197
|
+
* editable — count them as covered instead of excluding them. */
|
|
198
|
+
i18nFimoWired?: boolean;
|
|
199
|
+
}): WrapScanResult;
|
|
200
|
+
export {};
|