fimo 0.9.0-experimental.1 → 0.9.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/skills/fimo/SKILL.md +17 -54
- package/assets/skills/fimo-cli/SKILL.md +15 -15
- package/assets/skills/fimo-cli/references/agent-setup.md +36 -53
- package/assets/skills/fimo-cli/references/agents.md +4 -4
- package/assets/skills/fimo-cli/references/content.md +2 -2
- package/assets/skills/fimo-cli/references/deploy.md +1 -1
- package/assets/skills/fimo-cli/references/forms.md +1 -1
- package/assets/skills/fimo-cli/references/init.md +16 -24
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +14 -11
- package/assets/skills/fimo-cli/references/projects.md +1 -1
- package/assets/skills/fimo-cli/references/support.md +11 -9
- package/assets/skills/fimo-cli/references/webhooks.md +3 -1
- package/assets/skills/fimo-code/SKILL.md +67 -0
- package/assets/skills/fimo-migration/SKILL.md +2 -22
- package/assets/skills/fimo-migration/references/content.md +0 -24
- package/assets/skills/fimo-migration/references/manifest.md +0 -35
- package/assets/skills/fimo-migration/references/qa.md +2 -24
- package/assets/skills/fimo-migration/references/workflow.md +1 -12
- package/assets/skills/fimo-studio/references/visual-editing.md +3 -5
- package/assets/skills/fimo-studio/references/webhooks.md +1 -1
- package/dist/build/vite/plugins/translations.d.ts +8 -2
- package/dist/build/vite/plugins/translations.js +121 -3
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +311 -1209
- package/dist/runtime/app/FimoProviders.d.ts +11 -1
- package/dist/runtime/app/FimoProviders.js +23 -2
- package/dist/runtime/content/client/createContentClient.js +2 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +0 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +3 -5
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +4 -11
- package/dist/runtime/primitives/translations.d.ts +27 -1
- package/dist/runtime/primitives/translations.js +85 -7
- package/dist/runtime/shared/fimo-config.server.js +0 -1
- package/dist/runtime/shared/fimo-config.types.d.ts +0 -2
- package/dist/scripts/extract-translations.js +11 -16
- package/dist/scripts/lib/validation-report.d.ts +0 -1
- package/dist/scripts/lib/validation-report.js +0 -3
- package/dist/scripts/validate-route-metadata.js +0 -11
- package/package.json +4 -84
- package/release.json +2 -2
- package/scripts/postinstall.mjs +11 -65
- package/templates/react-router/AGENTS.md +44 -0
- package/templates/react-router/CLAUDE.md +5 -0
- package/templates/react-router/package.json +1 -1
- package/assets/agents-starter/AGENTS.md +0 -66
- package/assets/skills/fimo-migration/references/wrap.md +0 -617
- package/dist/runtime/astro/index.d.ts +0 -2
- package/dist/runtime/astro/index.js +0 -3
- package/dist/runtime/astro/server.d.ts +0 -3
- package/dist/runtime/astro/server.js +0 -6
- package/dist/runtime/next/FimoNextProviders.d.ts +0 -24
- package/dist/runtime/next/FimoNextProviders.js +0 -20
- package/dist/runtime/next/FimoNextScripts.d.ts +0 -10
- package/dist/runtime/next/FimoNextScripts.js +0 -19
- package/dist/runtime/next/index.d.ts +0 -14
- package/dist/runtime/next/index.js +0 -14
- package/dist/runtime/next/server.d.ts +0 -2
- package/dist/runtime/next/server.js +0 -4
- package/dist/runtime/nuxt/composables.d.ts +0 -9
- package/dist/runtime/nuxt/composables.js +0 -57
- package/dist/runtime/nuxt/index.d.ts +0 -3
- package/dist/runtime/nuxt/index.js +0 -6
- package/dist/runtime/nuxt/server.d.ts +0 -3
- package/dist/runtime/nuxt/server.js +0 -6
- package/dist/runtime/primitives/labels-hook.d.ts +0 -33
- package/dist/runtime/primitives/labels-hook.js +0 -41
- package/dist/runtime/primitives/labels-store.d.ts +0 -12
- package/dist/runtime/primitives/labels-store.js +0 -60
- package/dist/runtime/react/FimoSpaProviders.d.ts +0 -36
- package/dist/runtime/react/FimoSpaProviders.js +0 -63
- package/dist/runtime/react/index.d.ts +0 -33
- package/dist/runtime/react/index.js +0 -39
- package/dist/runtime/remix/FimoRemixProviders.d.ts +0 -16
- package/dist/runtime/remix/FimoRemixProviders.js +0 -12
- package/dist/runtime/remix/FimoRemixScripts.d.ts +0 -10
- package/dist/runtime/remix/FimoRemixScripts.js +0 -19
- package/dist/runtime/remix/index.d.ts +0 -12
- package/dist/runtime/remix/index.js +0 -11
- package/dist/runtime/remix/server.d.ts +0 -2
- package/dist/runtime/remix/server.js +0 -2
- package/dist/runtime/shared/display-routes.d.ts +0 -14
- package/dist/runtime/shared/display-routes.js +0 -25
- package/dist/runtime/shared/i18n-messages.d.ts +0 -29
- package/dist/runtime/shared/i18n-messages.js +0 -79
- package/dist/runtime/shared/labels-fetch.pure.d.ts +0 -17
- package/dist/runtime/shared/labels-fetch.pure.js +0 -71
- package/dist/runtime/shared/labels-fetch.server.d.ts +0 -7
- package/dist/runtime/shared/labels-fetch.server.js +0 -97
- package/dist/runtime/shared/labels-t.pure.d.ts +0 -2
- package/dist/runtime/shared/labels-t.pure.js +0 -10
- package/dist/runtime/strapi/client.d.ts +0 -30
- package/dist/runtime/strapi/client.js +0 -89
- package/dist/runtime/strapi/index.d.ts +0 -5
- package/dist/runtime/strapi/index.js +0 -6
- package/dist/runtime/strapi/reshape.d.ts +0 -46
- package/dist/runtime/strapi/reshape.js +0 -114
- package/dist/runtime/svelte/index.d.ts +0 -3
- package/dist/runtime/svelte/index.js +0 -7
- package/dist/runtime/svelte/server.d.ts +0 -3
- package/dist/runtime/svelte/server.js +0 -6
- package/dist/runtime/vue/index.d.ts +0 -3
- package/dist/runtime/vue/index.js +0 -5
- package/dist/runtime/vue/server.d.ts +0 -3
- package/dist/runtime/vue/server.js +0 -6
- package/dist/runtime/wrapped/FimoFileLink.d.ts +0 -17
- package/dist/runtime/wrapped/FimoFileLink.js +0 -19
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +0 -14
- package/dist/runtime/wrapped/FimoMediaImage.js +0 -20
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +0 -14
- package/dist/runtime/wrapped/FimoMediaVideo.js +0 -20
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +0 -23
- package/dist/runtime/wrapped/FimoRichTextBlock.js +0 -22
- package/dist/runtime/wrapped/FimoText.d.ts +0 -22
- package/dist/runtime/wrapped/FimoText.js +0 -20
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +0 -28
- package/dist/runtime/wrapped/WrappedContentContext.js +0 -30
- package/dist/runtime/wrapped/WrappedProviders.d.ts +0 -33
- package/dist/runtime/wrapped/WrappedProviders.js +0 -101
- package/dist/runtime/wrapped/dom.d.ts +0 -4
- package/dist/runtime/wrapped/dom.js +0 -23
- package/dist/runtime/wrapped/server.d.ts +0 -77
- package/dist/runtime/wrapped/server.js +0 -237
- package/dist/scripts/apply-wrap-templates.d.ts +0 -61
- package/dist/scripts/apply-wrap-templates.js +0 -132
- package/dist/scripts/apply-wrap.d.ts +0 -46
- package/dist/scripts/apply-wrap.js +0 -278
- package/dist/scripts/cms/discover.d.ts +0 -2
- package/dist/scripts/cms/discover.js +0 -17
- package/dist/scripts/cms/example-content.d.ts +0 -33
- package/dist/scripts/cms/example-content.js +0 -162
- package/dist/scripts/cms/strapi/detect.d.ts +0 -2
- package/dist/scripts/cms/strapi/detect.js +0 -186
- package/dist/scripts/cms/strapi/index.d.ts +0 -3
- package/dist/scripts/cms/strapi/index.js +0 -25
- package/dist/scripts/cms/strapi/list-source.d.ts +0 -17
- package/dist/scripts/cms/strapi/list-source.js +0 -90
- package/dist/scripts/cms/strapi/schema-map.d.ts +0 -9
- package/dist/scripts/cms/strapi/schema-map.js +0 -277
- package/dist/scripts/cms/strapi/shapes.d.ts +0 -23
- package/dist/scripts/cms/strapi/shapes.js +0 -38
- package/dist/scripts/cms/strapi/touchpoints.d.ts +0 -31
- package/dist/scripts/cms/strapi/touchpoints.js +0 -145
- package/dist/scripts/cms/strapi/wire-client.d.ts +0 -15
- package/dist/scripts/cms/strapi/wire-client.js +0 -107
- package/dist/scripts/cms/strapi/wiring.d.ts +0 -17
- package/dist/scripts/cms/strapi/wiring.js +0 -46
- package/dist/scripts/cms/types.d.ts +0 -145
- package/dist/scripts/cms/types.js +0 -11
- package/dist/scripts/detect-framework.d.ts +0 -39
- package/dist/scripts/detect-framework.js +0 -133
- package/dist/scripts/draft-rows.d.ts +0 -19
- package/dist/scripts/draft-rows.js +0 -135
- package/dist/scripts/draft-schemas.d.ts +0 -27
- package/dist/scripts/draft-schemas.js +0 -188
- package/dist/scripts/frameworks/astro/routes.d.ts +0 -3
- package/dist/scripts/frameworks/astro/routes.js +0 -55
- package/dist/scripts/frameworks/astro/template-style.d.ts +0 -7
- package/dist/scripts/frameworks/astro/template-style.js +0 -19
- package/dist/scripts/frameworks/discover.d.ts +0 -70
- package/dist/scripts/frameworks/discover.js +0 -178
- package/dist/scripts/frameworks/next/rewire.d.ts +0 -30
- package/dist/scripts/frameworks/next/rewire.js +0 -258
- package/dist/scripts/frameworks/next/routes.d.ts +0 -17
- package/dist/scripts/frameworks/next/routes.js +0 -134
- package/dist/scripts/frameworks/next/wiring.d.ts +0 -33
- package/dist/scripts/frameworks/next/wiring.js +0 -186
- package/dist/scripts/frameworks/nuxt/routes.d.ts +0 -3
- package/dist/scripts/frameworks/nuxt/routes.js +0 -54
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +0 -9
- package/dist/scripts/frameworks/nuxt/template-style.js +0 -27
- package/dist/scripts/frameworks/remix/routes.d.ts +0 -3
- package/dist/scripts/frameworks/remix/routes.js +0 -50
- package/dist/scripts/frameworks/support.d.ts +0 -75
- package/dist/scripts/frameworks/support.js +0 -165
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +0 -3
- package/dist/scripts/frameworks/sveltekit/routes.js +0 -50
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +0 -9
- package/dist/scripts/frameworks/sveltekit/template-style.js +0 -54
- package/dist/scripts/frameworks/vite/routes.d.ts +0 -15
- package/dist/scripts/frameworks/vite/routes.js +0 -93
- package/dist/scripts/frameworks/vite/wiring.d.ts +0 -10
- package/dist/scripts/frameworks/vite/wiring.js +0 -99
- package/dist/scripts/i18n-namespace.d.ts +0 -27
- package/dist/scripts/i18n-namespace.js +0 -155
- package/dist/scripts/render-parity/capture.d.ts +0 -56
- package/dist/scripts/render-parity/capture.js +0 -366
- package/dist/scripts/render-parity/compare.d.ts +0 -59
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +0 -13
- package/dist/scripts/render-parity/normalize-html.js +0 -223
- package/dist/scripts/render-parity/types.d.ts +0 -41
- package/dist/scripts/render-parity/types.js +0 -1
- package/dist/scripts/scan-wrappable.d.ts +0 -196
- package/dist/scripts/scan-wrappable.js +0 -1232
- /package/assets/skills/{fimo → fimo-code}/assets/react-router-locales/navigation.tsx +0 -0
- /package/assets/skills/{fimo → fimo-code}/assets/react-router-locales/routes.ts +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/assets.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/content.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/design.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/forms.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/labels.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/locales.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/setup-plain-vite.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/setup-react-router.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/ui.md +0 -0
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
import { parseSync } from 'oxc-parser';
|
|
4
|
-
import { scanWrappable } from './scan-wrappable.js';
|
|
5
|
-
/**
|
|
6
|
-
* The --apply codemod: performs the PROVABLY-SAFE wrap transforms
|
|
7
|
-
* deterministically, leaving every judgment call to the agent.
|
|
8
|
-
*
|
|
9
|
-
* In scope: plain JSX text -> FimoText, <img src="literal"> -> FimoMediaImage,
|
|
10
|
-
* <a href="/file.pdf"> -> FimoFileLink.
|
|
11
|
-
* Out of scope (agent work): attributes (useLabels crosses the RSC boundary),
|
|
12
|
-
* formatted text (marks -> FimoRichTextBlock), next/image <Image>
|
|
13
|
-
* (framework-specific props), collections, layout wiring.
|
|
14
|
-
*
|
|
15
|
-
* Correctness is enforced by the gates, not assumed: the render-parity
|
|
16
|
-
* baseline is captured BEFORE applying, and `fimo migrate wrap --check`
|
|
17
|
-
* verifies the result like any other actor's work.
|
|
18
|
-
*/
|
|
19
|
-
/** Elements whose children cannot contain a <span> wrapper. */
|
|
20
|
-
const NO_ELEMENT_CHILDREN = new Set(['option', 'title', 'textarea', 'script', 'style', 'select']);
|
|
21
|
-
function collapse(text) {
|
|
22
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
23
|
-
}
|
|
24
|
-
// JSX text carries HTML entities (', &, …). The FALLBACK keeps the
|
|
25
|
-
// original source form, but the label VALUE is rendered literally by
|
|
26
|
-
// FimoText — seeding the raw entity re-escapes it on screen (the remix
|
|
27
|
-
// dogfood shipped "What's next?"). Decode when generating values.
|
|
28
|
-
const JSX_ENTITIES = {
|
|
29
|
-
'&': '&',
|
|
30
|
-
'<': '<',
|
|
31
|
-
'>': '>',
|
|
32
|
-
'"': '"',
|
|
33
|
-
''': "'",
|
|
34
|
-
' ': ' ',
|
|
35
|
-
'©': '©',
|
|
36
|
-
};
|
|
37
|
-
export function decodeJsxEntities(text) {
|
|
38
|
-
return text
|
|
39
|
-
.replace(/&(amp|lt|gt|quot|apos|nbsp|copy);/g, (entity) => JSX_ENTITIES[entity] ?? entity)
|
|
40
|
-
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
41
|
-
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16)));
|
|
42
|
-
}
|
|
43
|
-
/** Deterministic scope from the file path: app/about/page.tsx -> about. */
|
|
44
|
-
export function keyScope(file) {
|
|
45
|
-
const normalized = file.replace(/\\/g, '/');
|
|
46
|
-
const appMatch = normalized.match(/(?:^|\/)app\/(.*)\/?page\.[jt]sx?$/);
|
|
47
|
-
if (appMatch) {
|
|
48
|
-
const dir = appMatch[1].replace(/\/$/, '');
|
|
49
|
-
if (!dir) {
|
|
50
|
-
return 'home';
|
|
51
|
-
}
|
|
52
|
-
const segments = dir
|
|
53
|
-
.split('/')
|
|
54
|
-
.filter((segment) => !segment.startsWith('(') && !segment.startsWith('@'))
|
|
55
|
-
.map((segment) => segment.replace(/^\[+|\]+$/g, '').replace(/^\.\.\./, ''));
|
|
56
|
-
return segments.join('.') || 'home';
|
|
57
|
-
}
|
|
58
|
-
if (/(?:^|\/)app\/layout\.[jt]sx?$/.test(normalized)) {
|
|
59
|
-
return 'layout';
|
|
60
|
-
}
|
|
61
|
-
const stem = basename(normalized).replace(/\.[jt]sx?$/, '');
|
|
62
|
-
return slugify(stem) || 'shared';
|
|
63
|
-
}
|
|
64
|
-
export function slugify(text) {
|
|
65
|
-
return collapse(text)
|
|
66
|
-
.toLowerCase()
|
|
67
|
-
.normalize('NFD')
|
|
68
|
-
.replace(/[̀-ͯ]/g, '')
|
|
69
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
70
|
-
.replace(/^-+|-+$/g, '')
|
|
71
|
-
.split('-')
|
|
72
|
-
.slice(0, 4)
|
|
73
|
-
.join('-');
|
|
74
|
-
}
|
|
75
|
-
export class KeyAllocator {
|
|
76
|
-
used = new Set();
|
|
77
|
-
constructor(existing) {
|
|
78
|
-
for (const key of existing) {
|
|
79
|
-
this.used.add(key);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
allocate(scope, slug) {
|
|
83
|
-
const base = `${scope}.${slug || 'item'}`;
|
|
84
|
-
let candidate = base;
|
|
85
|
-
let counter = 2;
|
|
86
|
-
while (this.used.has(candidate)) {
|
|
87
|
-
candidate = `${base}-${counter++}`;
|
|
88
|
-
}
|
|
89
|
-
this.used.add(candidate);
|
|
90
|
-
return candidate;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Merge the fimo/next import with an existing one, or insert a new one AFTER
|
|
95
|
-
* the last import statement — positioned via the AST, never line matching
|
|
96
|
-
* (a multiline `import {\n …` opening line is not the end of the imports,
|
|
97
|
-
* as a real portfolio migration proved).
|
|
98
|
-
*/
|
|
99
|
-
export function ensureFimoImport(code, specifiers, importModule = 'fimo/next') {
|
|
100
|
-
if (specifiers.length === 0) {
|
|
101
|
-
return code;
|
|
102
|
-
}
|
|
103
|
-
const moduleRe = importModule.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
104
|
-
const importMatch = code.match(new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*(['\"])${moduleRe}\\2\\s*;?`));
|
|
105
|
-
if (importMatch) {
|
|
106
|
-
const existing = importMatch[1]
|
|
107
|
-
.split(',')
|
|
108
|
-
.map((token) => token.trim())
|
|
109
|
-
.filter(Boolean);
|
|
110
|
-
const merged = [...new Set([...existing, ...specifiers])].sort();
|
|
111
|
-
return code.replace(importMatch[0], `import { ${merged.join(', ')} } from "${importModule}";`);
|
|
112
|
-
}
|
|
113
|
-
const statement = `import { ${[...new Set(specifiers)].sort().join(', ')} } from "${importModule}";`;
|
|
114
|
-
let insertAt = 0;
|
|
115
|
-
try {
|
|
116
|
-
const program = parseSync('module.tsx', code).program;
|
|
117
|
-
for (const node of program.body ?? []) {
|
|
118
|
-
if (node.type === 'ImportDeclaration') {
|
|
119
|
-
insertAt = node.end;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
insertAt = 0;
|
|
125
|
-
}
|
|
126
|
-
if (insertAt === 0) {
|
|
127
|
-
return `${statement}\n${code}`;
|
|
128
|
-
}
|
|
129
|
-
return `${code.slice(0, insertAt)}\n${statement}${code.slice(insertAt)}`;
|
|
130
|
-
}
|
|
131
|
-
export function applyWrapToFile(code, items, allocator, importModule = 'fimo/next') {
|
|
132
|
-
const applied = [];
|
|
133
|
-
const skipped = [];
|
|
134
|
-
const labels = {};
|
|
135
|
-
const edits = [];
|
|
136
|
-
const specifiers = new Set();
|
|
137
|
-
for (const item of items) {
|
|
138
|
-
const span = item.span;
|
|
139
|
-
if (!span) {
|
|
140
|
-
skipped.push({ item, reason: 'no span info' });
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const scope = keyScope(item.file);
|
|
144
|
-
if (item.kind === 'text') {
|
|
145
|
-
if (span.parentTag && NO_ELEMENT_CHILDREN.has(span.parentTag.toLowerCase())) {
|
|
146
|
-
skipped.push({ item, reason: `<${span.parentTag}> cannot contain a wrapper element` });
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
const raw = code.slice(span.start, span.end);
|
|
150
|
-
const core = raw.trim();
|
|
151
|
-
if (!core) {
|
|
152
|
-
skipped.push({ item, reason: 'whitespace only' });
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const lead = raw.slice(0, raw.indexOf(core));
|
|
156
|
-
const trail = raw.slice(raw.indexOf(core) + core.length);
|
|
157
|
-
const id = allocator.allocate(scope, slugify(core));
|
|
158
|
-
edits.push({
|
|
159
|
-
start: span.start,
|
|
160
|
-
end: span.end,
|
|
161
|
-
replacement: `${lead}<FimoText id="${id}">${core}</FimoText>${trail}`,
|
|
162
|
-
});
|
|
163
|
-
specifiers.add('FimoText');
|
|
164
|
-
labels[id] = decodeJsxEntities(collapse(core));
|
|
165
|
-
applied.push({ kind: 'text', file: item.file, line: item.line, id, preview: item.preview });
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (item.kind === 'image') {
|
|
169
|
-
if (span.openNameStart === undefined ||
|
|
170
|
-
span.openNameEnd === undefined ||
|
|
171
|
-
code.slice(span.openNameStart, span.openNameEnd) !== 'img') {
|
|
172
|
-
skipped.push({ item, reason: 'next/image and custom components stay agent work' });
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
const id = allocator.allocate(scope, slugify(basename(item.preview).replace(/\.[a-z0-9]+$/i, '')));
|
|
176
|
-
edits.push({
|
|
177
|
-
start: span.openNameStart,
|
|
178
|
-
end: span.openNameEnd,
|
|
179
|
-
replacement: `FimoMediaImage id="${id}"`,
|
|
180
|
-
});
|
|
181
|
-
specifiers.add('FimoMediaImage');
|
|
182
|
-
applied.push({ kind: 'image', file: item.file, line: item.line, id, preview: item.preview });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (item.kind === 'file-link') {
|
|
186
|
-
if (span.openNameStart === undefined ||
|
|
187
|
-
span.openNameEnd === undefined ||
|
|
188
|
-
span.closeNameStart === undefined ||
|
|
189
|
-
span.closeNameEnd === undefined) {
|
|
190
|
-
skipped.push({ item, reason: 'self-closing or unparsable anchor' });
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
const id = allocator.allocate(scope, slugify(basename(item.preview).replace(/\.[a-z0-9]+$/i, '')));
|
|
194
|
-
edits.push({ start: span.openNameStart, end: span.openNameEnd, replacement: `FimoFileLink id="${id}"` });
|
|
195
|
-
edits.push({ start: span.closeNameStart, end: span.closeNameEnd, replacement: 'FimoFileLink' });
|
|
196
|
-
specifiers.add('FimoFileLink');
|
|
197
|
-
applied.push({ kind: 'file-link', file: item.file, line: item.line, id, preview: item.preview });
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
skipped.push({ item, reason: `${item.kind} stays agent work` });
|
|
201
|
-
}
|
|
202
|
-
let next = code;
|
|
203
|
-
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
204
|
-
next = next.slice(0, edit.start) + edit.replacement + next.slice(edit.end);
|
|
205
|
-
}
|
|
206
|
-
next = ensureFimoImport(next, [...specifiers], importModule);
|
|
207
|
-
return { code: next, applied, skipped, labels };
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Apply the safe transforms across the project. Mutates source files and
|
|
211
|
-
* merges generated labels into `.fimo/migration/labels.json` (never
|
|
212
|
-
* overwriting existing keys).
|
|
213
|
-
*/
|
|
214
|
-
/** Recorded coverage exemptions the codemod must NOT wrap — items the user
|
|
215
|
-
* deliberately kept out (dev-only UI, OG-image markup, …). Matched by the
|
|
216
|
-
* same (file, kind, preview) triple the coverage gate uses, so "record the
|
|
217
|
-
* exemption first, then --apply auto-wraps the rest" holds. */
|
|
218
|
-
function readApplyExemptions(rootDir) {
|
|
219
|
-
try {
|
|
220
|
-
const manifest = JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'manifest.json'), 'utf8'));
|
|
221
|
-
const raw = manifest?.wrap?.exemptions;
|
|
222
|
-
return Array.isArray(raw)
|
|
223
|
-
? raw.filter((entry) => typeof entry?.file === 'string' &&
|
|
224
|
-
typeof entry?.kind === 'string' &&
|
|
225
|
-
typeof entry?.preview === 'string')
|
|
226
|
-
: [];
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
export function applyWrap(rootDir, opts = {}) {
|
|
233
|
-
const scan = scanWrappable(rootDir, { excludePrefixes: opts.excludePrefixes });
|
|
234
|
-
const existingLabels = (() => {
|
|
235
|
-
try {
|
|
236
|
-
return JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'labels.json'), 'utf8'));
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
return {};
|
|
240
|
-
}
|
|
241
|
-
})();
|
|
242
|
-
// Honor recorded exemptions: the codemod skips them so the coverage gate's
|
|
243
|
-
// exemption and the codemod stay consistent — otherwise --apply would wrap
|
|
244
|
-
// an item the user just exempted and the gate would flag the drift.
|
|
245
|
-
const exemptions = readApplyExemptions(rootDir);
|
|
246
|
-
const isExempt = (item) => exemptions.some((exemption) => exemption.file === item.file && exemption.kind === item.kind && exemption.preview === item.preview);
|
|
247
|
-
const allocator = new KeyAllocator([...Object.keys(existingLabels), ...scan.wrappedIds.labels]);
|
|
248
|
-
const byFile = new Map();
|
|
249
|
-
for (const item of scan.items) {
|
|
250
|
-
if (isExempt(item)) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const list = byFile.get(item.file) ?? [];
|
|
254
|
-
list.push(item);
|
|
255
|
-
byFile.set(item.file, list);
|
|
256
|
-
}
|
|
257
|
-
const result = { applied: [], skipped: [], labels: {}, filesChanged: [] };
|
|
258
|
-
for (const [file, items] of byFile) {
|
|
259
|
-
const filePath = join(rootDir, file);
|
|
260
|
-
let code;
|
|
261
|
-
try {
|
|
262
|
-
code = readFileSync(filePath, 'utf8');
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
result.skipped.push(...items.map((item) => ({ item, reason: 'unreadable file' })));
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
const fileResult = applyWrapToFile(code, items, allocator, opts.importModule ?? 'fimo/next');
|
|
269
|
-
result.applied.push(...fileResult.applied);
|
|
270
|
-
result.skipped.push(...fileResult.skipped);
|
|
271
|
-
Object.assign(result.labels, fileResult.labels);
|
|
272
|
-
if (fileResult.applied.length > 0) {
|
|
273
|
-
writeFileSync(filePath, fileResult.code);
|
|
274
|
-
result.filesChanged.push(file);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { strapiConnector } from './strapi/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* CMS-connector registry. Add a connector factory here to support a new
|
|
4
|
-
* headless CMS; each returns its `CmsConnector` when it recognizes the
|
|
5
|
-
* frontend, or null. First match wins. This is the ONLY CMS-neutral file that
|
|
6
|
-
* enumerates the supported CMSs (the analogue of `frameworks/discover.ts`).
|
|
7
|
-
*/
|
|
8
|
-
const CONNECTORS = [strapiConnector];
|
|
9
|
-
export function detectCms(frontendDir) {
|
|
10
|
-
for (const factory of CONNECTORS) {
|
|
11
|
-
const connector = factory(frontendDir);
|
|
12
|
-
if (connector) {
|
|
13
|
-
return connector;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { CmsCollectionSchema } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Generate example content for a CMS schema WITHOUT connecting to the CMS. The
|
|
4
|
-
* shapes match `api.importEntries`: media fields get a self-contained
|
|
5
|
-
* placeholder, references point at the first example of the target type
|
|
6
|
-
* (`<target>-example-1`) so they resolve against the co-seeded data, blocks
|
|
7
|
-
* (components / dynamic zones) get a filled array so page-builder pages render
|
|
8
|
-
* instead of showing empty, and localized types get per-locale string variance
|
|
9
|
-
* so the per-language editability is visible in Fimo. This is CMS-neutral: it
|
|
10
|
-
* consumes the mapped Fimo schema only.
|
|
11
|
-
*/
|
|
12
|
-
export interface ExampleSeed {
|
|
13
|
-
/** Fimo type name. */
|
|
14
|
-
readonly name: string;
|
|
15
|
-
readonly isSingleton: boolean;
|
|
16
|
-
readonly localized: boolean;
|
|
17
|
-
/** Entry data rows (slug lives inside data for collections). */
|
|
18
|
-
readonly rows: Array<Record<string, unknown>>;
|
|
19
|
-
/** Reference descriptor for `importEntries`. */
|
|
20
|
-
readonly references: Record<string, {
|
|
21
|
-
target?: string;
|
|
22
|
-
targets?: string[];
|
|
23
|
-
by: 'slug';
|
|
24
|
-
many?: boolean;
|
|
25
|
-
}>;
|
|
26
|
-
}
|
|
27
|
-
export interface ExampleOptions {
|
|
28
|
-
/** Rows per collection type (singletons always get exactly one). */
|
|
29
|
-
readonly count?: number;
|
|
30
|
-
/** Locale suffix on string values + the batch locale (for localized types). */
|
|
31
|
-
readonly locale?: string;
|
|
32
|
-
}
|
|
33
|
-
export declare function generateExampleSeed(schema: CmsCollectionSchema, options?: ExampleOptions): ExampleSeed;
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
const DEFAULT_COUNT = 3;
|
|
2
|
-
/** Variants seeded per dynamic-zone (`oneOf`) field — a representative page, not
|
|
3
|
-
* all N components. Real content (the pull) exercises the rest. */
|
|
4
|
-
const EXAMPLE_BLOCK_VARIANTS = 5;
|
|
5
|
-
/** Items seeded for a repeatable fixed-shape block. */
|
|
6
|
-
const EXAMPLE_REPEATABLE_ITEMS = 2;
|
|
7
|
-
/** Recursion cap for nested blocks — the schema is already depth-bounded; this
|
|
8
|
-
* keeps generation finite even for pathological nesting. */
|
|
9
|
-
const MAX_FILL_DEPTH = 3;
|
|
10
|
-
function kebabCase(name) {
|
|
11
|
-
return name
|
|
12
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
13
|
-
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/^-+|-+$/g, '');
|
|
16
|
-
}
|
|
17
|
-
function humanize(field) {
|
|
18
|
-
return field
|
|
19
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
20
|
-
.replace(/[_-]+/g, ' ')
|
|
21
|
-
.replace(/^\w/, (c) => c.toUpperCase());
|
|
22
|
-
}
|
|
23
|
-
function richTextDoc(text) {
|
|
24
|
-
return {
|
|
25
|
-
type: 'doc',
|
|
26
|
-
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** A self-contained placeholder image (inline SVG data-URI) in the media
|
|
30
|
-
* envelope shape the wrap runtime already understands — no external host, no
|
|
31
|
-
* CSP surprises, so cards/heroes render instead of showing broken images. */
|
|
32
|
-
function placeholderMedia(label) {
|
|
33
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630"><rect width="100%" height="100%" fill="#e5e7eb"/><text x="50%" y="50%" font-family="sans-serif" font-size="44" fill="#9ca3af" text-anchor="middle" dominant-baseline="middle">${label}</text></svg>`;
|
|
34
|
-
return {
|
|
35
|
-
source: 'local',
|
|
36
|
-
url: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
|
|
37
|
-
alt: `${label} placeholder`,
|
|
38
|
-
width: 1200,
|
|
39
|
-
height: 630,
|
|
40
|
-
mime: 'image/svg+xml',
|
|
41
|
-
name: 'placeholder.svg',
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** Deterministic ISO date (no wall clock): 2024-01-01 + (n-1) days. */
|
|
45
|
-
function exampleDate(n) {
|
|
46
|
-
return new Date(Date.UTC(2024, 0, n)).toISOString().slice(0, 10);
|
|
47
|
-
}
|
|
48
|
-
function exampleReferenceSlug(field) {
|
|
49
|
-
const targetName = field.target ?? field.targets?.[0];
|
|
50
|
-
if (!targetName) {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
const slug = `${kebabCase(targetName)}-example-1`;
|
|
54
|
-
return field.many ? [slug] : slug;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Fill a NON-reference field value from its Fimo def (scalars, blocks, and any
|
|
58
|
-
* baked default such as a block variant's `__component` discriminator).
|
|
59
|
-
* Reference fields return `undefined` here: the top-level pass owns them, and
|
|
60
|
-
* server-side slug resolution does not reach references nested inside blocks.
|
|
61
|
-
*/
|
|
62
|
-
function fillFieldValue(field, def, ctx) {
|
|
63
|
-
const baked = def.default;
|
|
64
|
-
if (baked !== undefined) {
|
|
65
|
-
return baked;
|
|
66
|
-
}
|
|
67
|
-
switch (def.type) {
|
|
68
|
-
case 'string':
|
|
69
|
-
return `${humanize(field)} example ${ctx.index}${ctx.localeSuffix}`;
|
|
70
|
-
case 'richtext':
|
|
71
|
-
return richTextDoc(`Example ${humanize(ctx.schemaName).toLowerCase()} content ${ctx.index}${ctx.localeSuffix}.`);
|
|
72
|
-
case 'number':
|
|
73
|
-
return ctx.index;
|
|
74
|
-
case 'boolean':
|
|
75
|
-
return ctx.index % 2 === 1;
|
|
76
|
-
case 'date':
|
|
77
|
-
return exampleDate(ctx.index);
|
|
78
|
-
case 'json':
|
|
79
|
-
return {};
|
|
80
|
-
case 'media':
|
|
81
|
-
return placeholderMedia(humanize(field));
|
|
82
|
-
case 'blocks':
|
|
83
|
-
return fillBlocks(def, ctx);
|
|
84
|
-
default:
|
|
85
|
-
// reference (nested) — omitted; the frontend accesses relations defensively.
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Fill a blocks value — always an array so page builders can `.map()` it. A
|
|
91
|
-
* dynamic zone (`oneOf`) seeds one item per variant (representative subset,
|
|
92
|
-
* each tagged with its `_type` discriminator); a fixed component (`fields`)
|
|
93
|
-
* seeds one item (single) or a few (repeatable).
|
|
94
|
-
*/
|
|
95
|
-
function fillBlocks(def, ctx) {
|
|
96
|
-
if (ctx.depth >= MAX_FILL_DEPTH) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
const childCtx = { ...ctx, depth: ctx.depth + 1 };
|
|
100
|
-
const schema = def.schema;
|
|
101
|
-
if ('oneOf' in schema) {
|
|
102
|
-
return schema.oneOf.slice(0, EXAMPLE_BLOCK_VARIANTS).map((variant) => ({
|
|
103
|
-
_type: variant.uid,
|
|
104
|
-
...fillItem(variant.fields, childCtx),
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
const itemCount = def.max === 1 ? 1 : EXAMPLE_REPEATABLE_ITEMS;
|
|
108
|
-
return Array.from({ length: itemCount }, () => fillItem(schema.fields, childCtx));
|
|
109
|
-
}
|
|
110
|
-
/** Fill every non-reference field of a block item. */
|
|
111
|
-
function fillItem(fields, ctx) {
|
|
112
|
-
const item = {};
|
|
113
|
-
for (const [name, def] of Object.entries(fields)) {
|
|
114
|
-
const value = fillFieldValue(name, def, ctx);
|
|
115
|
-
if (value !== undefined) {
|
|
116
|
-
item[name] = value;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return item;
|
|
120
|
-
}
|
|
121
|
-
export function generateExampleSeed(schema, options = {}) {
|
|
122
|
-
const locale = options.locale;
|
|
123
|
-
const localeSuffix = locale ? ` (${locale})` : '';
|
|
124
|
-
const count = schema.isSingleton ? 1 : Math.max(1, options.count ?? DEFAULT_COUNT);
|
|
125
|
-
const references = {};
|
|
126
|
-
const rows = [];
|
|
127
|
-
for (let i = 1; i <= count; i += 1) {
|
|
128
|
-
const row = {};
|
|
129
|
-
if (!schema.isSingleton) {
|
|
130
|
-
row.slug = `${kebabCase(schema.name)}-example-${i}`;
|
|
131
|
-
}
|
|
132
|
-
for (const [field, def] of Object.entries(schema.fields)) {
|
|
133
|
-
// A connector-supplied example (dynamic zones, components) wins over the
|
|
134
|
-
// type-driven default — critical for dynamic zones, which must be arrays.
|
|
135
|
-
if (schema.examples && field in schema.examples) {
|
|
136
|
-
row[field] = schema.examples[field];
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
// References are top-level only: record the descriptor so the import
|
|
140
|
-
// endpoint resolves the slug to a documentId server-side.
|
|
141
|
-
if (def.type === 'reference') {
|
|
142
|
-
const value = exampleReferenceSlug(def);
|
|
143
|
-
if (value !== undefined) {
|
|
144
|
-
row[field] = value;
|
|
145
|
-
references[field] = {
|
|
146
|
-
...(def.target ? { target: def.target } : {}),
|
|
147
|
-
...(def.targets ? { targets: def.targets } : {}),
|
|
148
|
-
by: 'slug',
|
|
149
|
-
...(def.many ? { many: true } : {}),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const value = fillFieldValue(field, def, { localeSuffix, index: i, schemaName: schema.name, depth: 0 });
|
|
155
|
-
if (value !== undefined) {
|
|
156
|
-
row[field] = value;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
rows.push(row);
|
|
160
|
-
}
|
|
161
|
-
return { name: schema.name, isSingleton: schema.isSingleton, localized: schema.localized, rows, references };
|
|
162
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
/**
|
|
4
|
-
* Detect a Strapi backend behind a wrapped frontend. Strapi is consumed via a
|
|
5
|
-
* hand-rolled fetch client against an env-var base URL (no SDK, no hardcoded
|
|
6
|
-
* URL), so the generic external-source scanner misses it entirely — detection
|
|
7
|
-
* keys on the distinctive local signals instead:
|
|
8
|
-
* - a dependency whose name mentions strapi (`@repo/strapi-types`,
|
|
9
|
-
* `@strapi/client`, generated types packages);
|
|
10
|
-
* - a `strapi`-named client module under `src/lib`/`src`;
|
|
11
|
-
* - Strapi's `api::x.y` content-type UID strings in the source;
|
|
12
|
-
* - a `STRAPI`-flavored base-URL env var.
|
|
13
|
-
* The co-located Strapi app (monorepo) is located by walking up to the
|
|
14
|
-
* workspace root and finding the sibling that depends on `@strapi/strapi`; its
|
|
15
|
-
* path drives schema import. A decoupled Strapi (no co-located app) still
|
|
16
|
-
* surfaces as a source for the keep-live decision, with `appDir: null`.
|
|
17
|
-
*/
|
|
18
|
-
const UID_RE = /["'`]api::[a-z0-9-]+\.[a-z0-9-]+["'`]/;
|
|
19
|
-
const STRAPI_ENV_RE = /STRAPI[A-Z0-9_]*_URL|NEXT_PUBLIC_STRAPI/;
|
|
20
|
-
const SOURCE_EXT_RE = /\.(tsx?|jsx?|mjs|cjs)$/;
|
|
21
|
-
const SKIP_DIRS = new Set(['node_modules', '.next', '.turbo', 'dist', 'build', '.git', 'coverage']);
|
|
22
|
-
const MAX_FILES_SCANNED = 400;
|
|
23
|
-
function safeReaddir(dir) {
|
|
24
|
-
try {
|
|
25
|
-
return readdirSync(dir, { withFileTypes: true });
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function readDeps(dir) {
|
|
32
|
-
try {
|
|
33
|
-
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
34
|
-
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function strapiDependency(deps) {
|
|
41
|
-
for (const name of Object.keys(deps)) {
|
|
42
|
-
if (/strapi/i.test(name)) {
|
|
43
|
-
return name;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
/** Shallow look for a `strapi`-named client module under src/lib or src. */
|
|
49
|
-
function strapiLibModule(frontendDir) {
|
|
50
|
-
for (const base of [join(frontendDir, 'src', 'lib'), join(frontendDir, 'src'), join(frontendDir, 'lib')]) {
|
|
51
|
-
if (!existsSync(base)) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
56
|
-
if (/strapi/i.test(entry.name)) {
|
|
57
|
-
return join(base, entry.name);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
// unreadable dir — ignore
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
/** Bounded source scan for `api::x.y` UID strings or a STRAPI base-URL env. */
|
|
68
|
-
function scanSourceSignals(frontendDir) {
|
|
69
|
-
const root = existsSync(join(frontendDir, 'src')) ? join(frontendDir, 'src') : frontendDir;
|
|
70
|
-
let scanned = 0;
|
|
71
|
-
let uid = false;
|
|
72
|
-
let env = false;
|
|
73
|
-
const walk = (dir) => {
|
|
74
|
-
if (scanned >= MAX_FILES_SCANNED || (uid && env)) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
for (const entry of safeReaddir(dir)) {
|
|
78
|
-
if (scanned >= MAX_FILES_SCANNED || (uid && env)) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (entry.isDirectory()) {
|
|
82
|
-
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
83
|
-
walk(join(dir, entry.name));
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (!SOURCE_EXT_RE.test(entry.name)) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
scanned += 1;
|
|
91
|
-
try {
|
|
92
|
-
const text = readFileSync(join(dir, entry.name), 'utf8');
|
|
93
|
-
if (!uid && UID_RE.test(text)) {
|
|
94
|
-
uid = true;
|
|
95
|
-
}
|
|
96
|
-
if (!env && STRAPI_ENV_RE.test(text)) {
|
|
97
|
-
env = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// unreadable file — ignore
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
walk(root);
|
|
106
|
-
return { uid, env };
|
|
107
|
-
}
|
|
108
|
-
function findWorkspaceRoot(startDir) {
|
|
109
|
-
let dir = startDir;
|
|
110
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
111
|
-
if (existsSync(join(dir, 'pnpm-workspace.yaml')) ||
|
|
112
|
-
existsSync(join(dir, 'turbo.json')) ||
|
|
113
|
-
existsSync(join(dir, 'lerna.json'))) {
|
|
114
|
-
return dir;
|
|
115
|
-
}
|
|
116
|
-
const parent = dirname(dir);
|
|
117
|
-
if (parent === dir) {
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
dir = parent;
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
/** Find the sibling app that depends on `@strapi/strapi` (the CMS app). */
|
|
125
|
-
function findStrapiApp(frontendDir) {
|
|
126
|
-
const root = findWorkspaceRoot(frontendDir);
|
|
127
|
-
if (!root) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
const searchRoots = [join(root, 'apps'), root, join(root, 'packages')];
|
|
131
|
-
for (const searchRoot of searchRoots) {
|
|
132
|
-
if (!existsSync(searchRoot)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
for (const entry of safeReaddir(searchRoot)) {
|
|
136
|
-
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const candidate = join(searchRoot, entry.name);
|
|
140
|
-
const deps = readDeps(candidate);
|
|
141
|
-
if (deps['@strapi/strapi'] && existsSync(join(candidate, 'src', 'api'))) {
|
|
142
|
-
return candidate;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
export function detectStrapi(frontendDir) {
|
|
149
|
-
if (!existsSync(frontendDir) || !statSync(frontendDir).isDirectory()) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
const evidence = [];
|
|
153
|
-
const dep = strapiDependency(readDeps(frontendDir));
|
|
154
|
-
if (dep) {
|
|
155
|
-
evidence.push(`dependency ${dep}`);
|
|
156
|
-
}
|
|
157
|
-
const libModule = strapiLibModule(frontendDir);
|
|
158
|
-
if (libModule) {
|
|
159
|
-
evidence.push(`client module ${libModule.slice(frontendDir.length + 1)}`);
|
|
160
|
-
}
|
|
161
|
-
// Only pay for the source scan when the cheap signals are inconclusive.
|
|
162
|
-
if (evidence.length === 0) {
|
|
163
|
-
const signals = scanSourceSignals(frontendDir);
|
|
164
|
-
if (signals.uid) {
|
|
165
|
-
evidence.push('api::* content-type UIDs in source');
|
|
166
|
-
}
|
|
167
|
-
if (signals.env) {
|
|
168
|
-
evidence.push('STRAPI base-URL env var');
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (evidence.length === 0) {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
const appDir = findStrapiApp(frontendDir);
|
|
175
|
-
if (appDir) {
|
|
176
|
-
evidence.push(`schemas in ${appDir.slice(findWorkspaceRoot(frontendDir).length + 1)}`);
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
cms: 'Strapi',
|
|
180
|
-
host: 'Strapi',
|
|
181
|
-
appDir,
|
|
182
|
-
clientDir: libModule,
|
|
183
|
-
evidence,
|
|
184
|
-
locales: [],
|
|
185
|
-
};
|
|
186
|
-
}
|