fimo 0.3.1 → 0.3.2-experimental.1783328062928
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/README.md +6 -12
- package/assets/agents-starter/AGENTS.md +14 -2
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +14 -2
- package/assets/skills/fimo/references/setup-plain-vite.md +8 -3
- package/assets/skills/fimo/references/setup-react-router.md +13 -6
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/SKILL.md +30 -18
- package/assets/skills/fimo-cli/references/account.md +4 -2
- package/assets/skills/fimo-cli/references/agent-setup.md +14 -16
- package/assets/skills/fimo-cli/references/agents.md +2 -0
- package/assets/skills/fimo-cli/references/analytics.md +2 -0
- package/assets/skills/fimo-cli/references/assets.md +2 -0
- package/assets/skills/fimo-cli/references/branches.md +2 -0
- package/assets/skills/fimo-cli/references/content.md +3 -1
- package/assets/skills/fimo-cli/references/credits.md +2 -0
- package/assets/skills/fimo-cli/references/deploy.md +10 -4
- package/assets/skills/fimo-cli/references/describe.md +2 -0
- package/assets/skills/fimo-cli/references/domains.md +2 -0
- package/assets/skills/fimo-cli/references/forms.md +2 -0
- package/assets/skills/fimo-cli/references/init.md +114 -0
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
- package/assets/skills/fimo-cli/references/integrations.md +2 -0
- package/assets/skills/fimo-cli/references/invite.md +2 -0
- package/assets/skills/fimo-cli/references/labels.md +2 -0
- package/assets/skills/fimo-cli/references/locales.md +2 -0
- package/assets/skills/fimo-cli/references/media.md +2 -0
- package/assets/skills/fimo-cli/references/preview.md +2 -0
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +3 -1
- package/assets/skills/fimo-cli/references/projects.md +5 -3
- package/assets/skills/fimo-cli/references/schedules.md +2 -0
- package/assets/skills/fimo-migration/SKILL.md +16 -1
- package/assets/skills/fimo-migration/references/manifest.md +40 -1
- package/assets/skills/fimo-migration/references/qa.md +21 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +155 -0
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/dist/build/vite/index.d.ts.map +1 -1
- package/dist/build/vite/index.js +8 -6
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.d.ts.map +1 -1
- package/dist/build/vite/plugins/translations.js +3 -121
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +4547 -3255
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
- package/dist/runtime/app/FimoProviders.js +2 -23
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoFileLink.d.ts +17 -0
- package/dist/runtime/next/FimoFileLink.d.ts.map +1 -0
- package/dist/runtime/next/FimoFileLink.js +19 -0
- package/dist/runtime/next/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/next/FimoMediaImage.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaImage.js +20 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaVideo.js +20 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +30 -0
- package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextProviders.js +106 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts +23 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts.map +1 -0
- package/dist/runtime/next/FimoRichTextBlock.js +22 -0
- package/dist/runtime/next/FimoText.d.ts +22 -0
- package/dist/runtime/next/FimoText.d.ts.map +1 -0
- package/dist/runtime/next/FimoText.js +20 -0
- package/dist/runtime/next/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/next/WrappedContentContext.d.ts.map +1 -0
- package/dist/runtime/next/WrappedContentContext.js +30 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.d.ts.map +1 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +33 -0
- package/dist/runtime/next/server.d.ts.map +1 -0
- package/dist/runtime/next/server.js +107 -0
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.d.ts.map +1 -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.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
- package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.test.js +66 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +8 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.d.ts.map +1 -1
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/index.d.ts +24 -0
- package/dist/runtime/react/index.d.ts.map +1 -0
- package/dist/runtime/react/index.js +28 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.d.ts.map +1 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
- package/dist/runtime/shared/labels-fetch.server.d.ts +14 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
- package/dist/runtime/shared/labels-fetch.server.js +145 -0
- package/dist/scripts/detect-framework.d.ts +12 -0
- package/dist/scripts/detect-framework.js +32 -0
- package/dist/scripts/detect-framework.test.d.ts +1 -0
- package/dist/scripts/detect-framework.test.js +39 -0
- package/dist/scripts/lib/discover-next-routes.d.ts +11 -0
- package/dist/scripts/lib/discover-next-routes.js +76 -0
- package/dist/scripts/lib/discover-next-routes.test.d.ts +1 -0
- package/dist/scripts/lib/discover-next-routes.test.js +40 -0
- package/dist/scripts/scan-wrappable.d.ts +29 -0
- package/dist/scripts/scan-wrappable.js +191 -0
- package/dist/scripts/scan-wrappable.test.d.ts +1 -0
- package/dist/scripts/scan-wrappable.test.js +48 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.d.ts +13 -0
- package/dist/scripts/validate-schemas.js +113 -0
- package/dist/scripts/validate-schemas.test.d.ts +1 -0
- package/dist/scripts/validate-schemas.test.js +132 -0
- package/package.json +29 -1
- package/release.json +2 -2
- package/scripts/postinstall.mjs +10 -54
- package/templates/react-router/package.json +1 -1
- package/assets/agents-starter/claude/settings.json +0 -19
- package/assets/skills/fimo-cli/references/create.md +0 -90
|
@@ -57,5 +57,7 @@ export interface FimoConfig {
|
|
|
57
57
|
i18n?: FimoI18nConfig;
|
|
58
58
|
routes?: Record<string, FimoRouteConfig>;
|
|
59
59
|
version?: string | number;
|
|
60
|
+
/** The project's framework. Absent means the Fimo template (react-router). */
|
|
61
|
+
framework?: 'react-router' | 'next' | string;
|
|
60
62
|
}
|
|
61
63
|
//# sourceMappingURL=fimo-config.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;CAC9C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LabelBundle {
|
|
2
|
+
dictionaries: Record<string, Record<string, string>>;
|
|
3
|
+
defaultLocale: string;
|
|
4
|
+
locales: string[];
|
|
5
|
+
updatedAt: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function serializeBundle(bundle: LabelBundle): string;
|
|
8
|
+
export declare function labelSignature(bundle: LabelBundle): string;
|
|
9
|
+
export declare function emptyLabelBundle(rootDir: string): LabelBundle;
|
|
10
|
+
export declare function resolveTenantApiUrl(env: Record<string, string>): string | undefined;
|
|
11
|
+
export declare function loadLabelBundle(rootDir: string): Promise<LabelBundle>;
|
|
12
|
+
export declare function fetchWithTimeout(url: URL, timeoutMs: number): Promise<Response>;
|
|
13
|
+
export declare function readProjectEnv(rootDir: string): Promise<Record<string, string>>;
|
|
14
|
+
//# sourceMappingURL=labels-fetch.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels-fetch.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/labels-fetch.server.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAM3D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE1D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAU7D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAOnF;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC3E;AA+BD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQrF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWrF"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Framework-neutral label fetching (server-side). Shared by the Vite
|
|
2
|
+
// translations plugin (fimo/vite) and the Next server loaders (fimo/next/server).
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { getFimoDefaultLocale, getFimoLocales } from '../i18n/locale.js';
|
|
7
|
+
import { getConfigServer } from './fimo-config.server.js';
|
|
8
|
+
// The tenant API base URL env var, in framework flavor order. Vite projects
|
|
9
|
+
// use VITE_API_URL; wrapped Next projects use NEXT_PUBLIC_FIMO_API_URL.
|
|
10
|
+
const API_URL_ENV_KEYS = ['VITE_API_URL', 'NEXT_PUBLIC_FIMO_API_URL'];
|
|
11
|
+
export function serializeBundle(bundle) {
|
|
12
|
+
return JSON.stringify({
|
|
13
|
+
dictionaries: bundle.dictionaries,
|
|
14
|
+
defaultLocale: bundle.defaultLocale,
|
|
15
|
+
locales: bundle.locales,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function labelSignature(bundle) {
|
|
19
|
+
return Buffer.from(serializeBundle(bundle)).toString('base64');
|
|
20
|
+
}
|
|
21
|
+
export function emptyLabelBundle(rootDir) {
|
|
22
|
+
const config = getConfigServer(rootDir);
|
|
23
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
24
|
+
const locales = getFimoLocales(config);
|
|
25
|
+
return {
|
|
26
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
27
|
+
defaultLocale,
|
|
28
|
+
locales,
|
|
29
|
+
updatedAt: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function resolveTenantApiUrl(env) {
|
|
33
|
+
for (const key of API_URL_ENV_KEYS) {
|
|
34
|
+
if (env[key]) {
|
|
35
|
+
return env[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export async function loadLabelBundle(rootDir) {
|
|
41
|
+
const config = getConfigServer(rootDir);
|
|
42
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
43
|
+
const locales = getFimoLocales(config);
|
|
44
|
+
const env = await readProjectEnv(rootDir);
|
|
45
|
+
const apiUrl = resolveTenantApiUrl(env);
|
|
46
|
+
if (!apiUrl) {
|
|
47
|
+
return {
|
|
48
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
49
|
+
defaultLocale,
|
|
50
|
+
locales,
|
|
51
|
+
updatedAt: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const loaded = await Promise.all(locales.map((locale) => fetchLabelDictionary(apiUrl, locale).catch(() => ({ locale, labels: {}, updatedAt: null }))));
|
|
56
|
+
const sortedUpdatedAt = loaded
|
|
57
|
+
.map((entry) => entry.updatedAt)
|
|
58
|
+
.filter((value) => typeof value === 'string')
|
|
59
|
+
.sort();
|
|
60
|
+
const updatedAt = sortedUpdatedAt[sortedUpdatedAt.length - 1];
|
|
61
|
+
return {
|
|
62
|
+
dictionaries: Object.fromEntries(loaded.map((entry) => [entry.locale, entry.labels])),
|
|
63
|
+
defaultLocale,
|
|
64
|
+
locales,
|
|
65
|
+
updatedAt: updatedAt ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {
|
|
70
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
71
|
+
defaultLocale,
|
|
72
|
+
locales,
|
|
73
|
+
updatedAt: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fetchLabelDictionary(apiUrl, locale) {
|
|
78
|
+
const url = new URL('/labels', apiUrl);
|
|
79
|
+
url.searchParams.set('locale', locale);
|
|
80
|
+
const response = await fetchWithTimeout(url, 4000);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return { locale, labels: {}, updatedAt: null };
|
|
83
|
+
}
|
|
84
|
+
const body = (await response.json());
|
|
85
|
+
if (Array.isArray(body.data)) {
|
|
86
|
+
return {
|
|
87
|
+
locale,
|
|
88
|
+
labels: Object.fromEntries(body.data.map((row) => [row.key, row.value ?? row.defaultValue ?? ''])),
|
|
89
|
+
updatedAt: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
locale,
|
|
94
|
+
labels: body.data?.labels ?? {},
|
|
95
|
+
updatedAt: body.data?.updatedAt ?? null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function fetchWithTimeout(url, timeoutMs) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
return await fetch(url, { signal: controller.signal });
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function readProjectEnv(rootDir) {
|
|
109
|
+
const values = {
|
|
110
|
+
...(await readEnvFile(join(rootDir, '.env'))),
|
|
111
|
+
...(await readEnvFile(join(rootDir, '.env.local'))),
|
|
112
|
+
};
|
|
113
|
+
for (const key of API_URL_ENV_KEYS) {
|
|
114
|
+
if (process.env[key]) {
|
|
115
|
+
values[key] = process.env[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
async function readEnvFile(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const content = await readFile(filePath, 'utf8');
|
|
123
|
+
const values = {};
|
|
124
|
+
for (const line of content.split(/\r?\n/)) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const index = trimmed.indexOf('=');
|
|
130
|
+
if (index === -1) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const key = trimmed.slice(0, index).trim();
|
|
134
|
+
const value = trimmed
|
|
135
|
+
.slice(index + 1)
|
|
136
|
+
.trim()
|
|
137
|
+
.replace(/^['"]|['"]$/g, '');
|
|
138
|
+
values[key] = value;
|
|
139
|
+
}
|
|
140
|
+
return values;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DetectedFramework = 'next' | 'react-router' | 'vite';
|
|
2
|
+
export interface FrameworkDetection {
|
|
3
|
+
framework: DetectedFramework | null;
|
|
4
|
+
/** For Next.js: the app-router directory (`app` or `src/app`) when present. */
|
|
5
|
+
appDir?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Detect the framework of an existing site from its package.json and layout.
|
|
9
|
+
* Used by `fimo init` (adopt-existing-site) and the wrap migration to decide
|
|
10
|
+
* how routes are discovered and which env flavor to write.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectFramework(cwd: string): FrameworkDetection;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect the framework of an existing site from its package.json and layout.
|
|
5
|
+
* Used by `fimo init` (adopt-existing-site) and the wrap migration to decide
|
|
6
|
+
* how routes are discovered and which env flavor to write.
|
|
7
|
+
*/
|
|
8
|
+
export function detectFramework(cwd) {
|
|
9
|
+
const pkgPath = join(cwd, 'package.json');
|
|
10
|
+
if (!existsSync(pkgPath)) {
|
|
11
|
+
return { framework: null };
|
|
12
|
+
}
|
|
13
|
+
let pkg;
|
|
14
|
+
try {
|
|
15
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { framework: null };
|
|
19
|
+
}
|
|
20
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
21
|
+
if (deps.next) {
|
|
22
|
+
const appDir = ['app', 'src/app'].find((dir) => existsSync(join(cwd, dir)));
|
|
23
|
+
return { framework: 'next', appDir };
|
|
24
|
+
}
|
|
25
|
+
if (deps['react-router'] || deps['@react-router/dev']) {
|
|
26
|
+
return { framework: 'react-router' };
|
|
27
|
+
}
|
|
28
|
+
if (deps.vite) {
|
|
29
|
+
return { framework: 'vite' };
|
|
30
|
+
}
|
|
31
|
+
return { framework: null };
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { detectFramework } from './detect-framework.js';
|
|
6
|
+
let dir;
|
|
7
|
+
const project = (deps, dirs = []) => {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'detect-framework-'));
|
|
9
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 't', dependencies: deps }));
|
|
10
|
+
for (const d of dirs) {
|
|
11
|
+
mkdirSync(join(dir, d), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return dir;
|
|
14
|
+
};
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (dir) {
|
|
17
|
+
rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
describe('detectFramework', () => {
|
|
21
|
+
it('detects next with the app dir', () => {
|
|
22
|
+
expect(detectFramework(project({ next: '^15.0.0' }, ['app']))).toEqual({ framework: 'next', appDir: 'app' });
|
|
23
|
+
expect(detectFramework(project({ next: '^15.0.0' }, ['src/app']))).toEqual({
|
|
24
|
+
framework: 'next',
|
|
25
|
+
appDir: 'src/app',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('detects react-router and vite', () => {
|
|
29
|
+
expect(detectFramework(project({ 'react-router': '^7.0.0' })).framework).toBe('react-router');
|
|
30
|
+
expect(detectFramework(project({ vite: '^7.0.0' })).framework).toBe('vite');
|
|
31
|
+
});
|
|
32
|
+
it('prefers next over an incidental vite dep', () => {
|
|
33
|
+
expect(detectFramework(project({ next: '^15.0.0', vite: '^7.0.0' }, ['app'])).framework).toBe('next');
|
|
34
|
+
});
|
|
35
|
+
it('returns null without a package.json or known framework', () => {
|
|
36
|
+
expect(detectFramework(join(tmpdir(), 'nope-does-not-exist')).framework).toBeNull();
|
|
37
|
+
expect(detectFramework(project({ express: '^4.0.0' })).framework).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DiscoveredRoute {
|
|
2
|
+
id: string;
|
|
3
|
+
path: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
8
|
+
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
9
|
+
* Studio page switcher can enumerate a wrapped site's pages.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverNextRoutes(appDir: string): DiscoveredRoute[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const PAGE_FILES = new Set(['page.tsx', 'page.jsx', 'page.ts', 'page.js', 'page.mdx']);
|
|
4
|
+
const titleCase = (seg) => seg
|
|
5
|
+
.replace(/[_\-]+/g, ' ')
|
|
6
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
10
|
+
.join(' ');
|
|
11
|
+
function segmentToPath(segment) {
|
|
12
|
+
// Route groups `(marketing)` don't contribute URL segments. Parallel-route
|
|
13
|
+
// slots `@modal` and private folders `_lib` are not standalone pages.
|
|
14
|
+
if (segment.startsWith('(')) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
if (segment.startsWith('@') || segment.startsWith('_')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const catchAll = segment.match(/^\[\.\.\.(.+)\]$/) ?? segment.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
21
|
+
if (catchAll) {
|
|
22
|
+
return `:${catchAll[1]}*`;
|
|
23
|
+
}
|
|
24
|
+
const param = segment.match(/^\[(.+)\]$/);
|
|
25
|
+
if (param) {
|
|
26
|
+
return `:${param[1]}`;
|
|
27
|
+
}
|
|
28
|
+
return segment;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
32
|
+
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
33
|
+
* Studio page switcher can enumerate a wrapped site's pages.
|
|
34
|
+
*/
|
|
35
|
+
export function discoverNextRoutes(appDir) {
|
|
36
|
+
const routes = [];
|
|
37
|
+
const walk = (dir, segments) => {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (entries.some((entry) => PAGE_FILES.has(entry))) {
|
|
46
|
+
const parts = segments.filter((s) => s.length > 0);
|
|
47
|
+
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
48
|
+
const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
|
|
49
|
+
routes.push({
|
|
50
|
+
id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
|
|
51
|
+
path: routePath,
|
|
52
|
+
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const full = join(dir, entry);
|
|
57
|
+
let isDir = false;
|
|
58
|
+
try {
|
|
59
|
+
isDir = statSync(full).isDirectory();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isDir) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const mapped = segmentToPath(entry);
|
|
68
|
+
if (mapped === null) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
walk(full, [...segments, mapped]);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
walk(appDir, []);
|
|
75
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { discoverNextRoutes } from './discover-next-routes.js';
|
|
6
|
+
let dir;
|
|
7
|
+
const app = (pages) => {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'next-routes-'));
|
|
9
|
+
for (const page of pages) {
|
|
10
|
+
const full = join(dir, page);
|
|
11
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
12
|
+
writeFileSync(full, 'export default function Page() { return null }');
|
|
13
|
+
}
|
|
14
|
+
return dir;
|
|
15
|
+
};
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (dir) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
describe('discoverNextRoutes', () => {
|
|
22
|
+
it('discovers static routes', () => {
|
|
23
|
+
const routes = discoverNextRoutes(app(['page.tsx', 'about/page.tsx', 'pricing/page.tsx']));
|
|
24
|
+
expect(routes.map((r) => r.path)).toEqual(['/', '/about', '/pricing']);
|
|
25
|
+
expect(routes[0]).toEqual({ id: 'home', path: '/', label: 'Home' });
|
|
26
|
+
expect(routes[1]).toEqual({ id: 'about', path: '/about', label: 'About' });
|
|
27
|
+
});
|
|
28
|
+
it('maps dynamic and catch-all segments', () => {
|
|
29
|
+
const routes = discoverNextRoutes(app(['blog/[slug]/page.tsx', 'docs/[...path]/page.tsx']));
|
|
30
|
+
expect(routes.map((r) => r.path)).toEqual(['/blog/:slug', '/docs/:path*']);
|
|
31
|
+
expect(routes[0].label).toBe('Blog');
|
|
32
|
+
});
|
|
33
|
+
it('collapses route groups and skips private and parallel segments', () => {
|
|
34
|
+
const routes = discoverNextRoutes(app(['(marketing)/features/page.tsx', '_internal/secret/page.tsx', '@modal/photo/page.tsx']));
|
|
35
|
+
expect(routes.map((r) => r.path)).toEqual(['/features']);
|
|
36
|
+
});
|
|
37
|
+
it('returns [] for a missing directory', () => {
|
|
38
|
+
expect(discoverNextRoutes(join(tmpdir(), 'does-not-exist'))).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
8
|
+
export interface WrappableItem {
|
|
9
|
+
kind: WrappableKind;
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
preview: string;
|
|
13
|
+
}
|
|
14
|
+
export interface WrapScanResult {
|
|
15
|
+
items: WrappableItem[];
|
|
16
|
+
wrappedCount: number;
|
|
17
|
+
filesScanned: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function scanWrappableFile(filePath: string, code: string, relPath: string): {
|
|
20
|
+
items: WrappableItem[];
|
|
21
|
+
wrapped: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Scan a project's source directories for unwrapped content. `dirs` defaults
|
|
25
|
+
* to the common Next/React layouts that exist under `rootDir`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scanWrappable(rootDir: string, opts?: {
|
|
28
|
+
dirs?: string[];
|
|
29
|
+
}): WrapScanResult;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { parseSync } from 'oxc-parser';
|
|
4
|
+
// Elements whose subtree counts as already wrapped.
|
|
5
|
+
const WRAPPING_COMPONENTS = new Set([
|
|
6
|
+
'FimoText',
|
|
7
|
+
'FimoRichTextBlock',
|
|
8
|
+
'FimoMediaImage',
|
|
9
|
+
'FimoMediaVideo',
|
|
10
|
+
'FimoFileLink',
|
|
11
|
+
'Text',
|
|
12
|
+
'RichText',
|
|
13
|
+
]);
|
|
14
|
+
const IMAGE_ELEMENTS = new Set(['img', 'Image']);
|
|
15
|
+
const VIDEO_ELEMENTS = new Set(['video', 'source', 'Video']);
|
|
16
|
+
const WRAPPABLE_ATTRIBUTES = new Set(['placeholder', 'title', 'aria-label']);
|
|
17
|
+
const FILE_LINK_PATTERN = /\.(pdf|zip|docx?|xlsx?|pptx?|csv|mp4|mov|webm)$/i;
|
|
18
|
+
const SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '.fimo']);
|
|
19
|
+
const MIN_TEXT_LENGTH = 3;
|
|
20
|
+
const PREVIEW_LENGTH = 60;
|
|
21
|
+
function toPreview(value) {
|
|
22
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
23
|
+
return collapsed.length > PREVIEW_LENGTH ? `${collapsed.slice(0, PREVIEW_LENGTH)}…` : collapsed;
|
|
24
|
+
}
|
|
25
|
+
function lineOf(code, offset) {
|
|
26
|
+
return code.slice(0, offset).split('\n').length;
|
|
27
|
+
}
|
|
28
|
+
function jsxElementName(node) {
|
|
29
|
+
const name = node?.openingElement?.name ?? node?.name;
|
|
30
|
+
if (!name) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (name.type === 'JSXIdentifier') {
|
|
34
|
+
return name.name;
|
|
35
|
+
}
|
|
36
|
+
if (name.type === 'JSXMemberExpression') {
|
|
37
|
+
return name.property?.name ?? null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function attributeValue(node, attribute) {
|
|
42
|
+
const attrs = node?.openingElement?.attributes ?? [];
|
|
43
|
+
for (const attr of attrs) {
|
|
44
|
+
if (attr.type !== 'JSXAttribute' || attr.name?.name !== attribute) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const value = attr.value;
|
|
48
|
+
if (value?.type === 'Literal' && typeof value.value === 'string' && value.value.length > 0) {
|
|
49
|
+
return { value: value.value, start: attr.start ?? node.start ?? 0 };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function scanWrappableFile(filePath, code, relPath) {
|
|
55
|
+
const items = [];
|
|
56
|
+
let wrapped = 0;
|
|
57
|
+
let ast;
|
|
58
|
+
try {
|
|
59
|
+
ast = parseSync(filePath, code).program;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { items, wrapped };
|
|
63
|
+
}
|
|
64
|
+
function visit(node, insideWrapped) {
|
|
65
|
+
if (!node || typeof node !== 'object') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let childInsideWrapped = insideWrapped;
|
|
69
|
+
if (node.type === 'JSXElement') {
|
|
70
|
+
const name = jsxElementName(node);
|
|
71
|
+
if (name && WRAPPING_COMPONENTS.has(name)) {
|
|
72
|
+
wrapped += 1;
|
|
73
|
+
childInsideWrapped = true;
|
|
74
|
+
}
|
|
75
|
+
if (!insideWrapped && name) {
|
|
76
|
+
if (IMAGE_ELEMENTS.has(name)) {
|
|
77
|
+
const src = attributeValue(node, 'src');
|
|
78
|
+
if (src) {
|
|
79
|
+
items.push({ kind: 'image', file: relPath, line: lineOf(code, src.start), preview: toPreview(src.value) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (VIDEO_ELEMENTS.has(name)) {
|
|
83
|
+
const src = attributeValue(node, 'src');
|
|
84
|
+
if (src) {
|
|
85
|
+
items.push({ kind: 'video', file: relPath, line: lineOf(code, src.start), preview: toPreview(src.value) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (name === 'a') {
|
|
89
|
+
const href = attributeValue(node, 'href');
|
|
90
|
+
if (href && FILE_LINK_PATTERN.test(href.value)) {
|
|
91
|
+
items.push({
|
|
92
|
+
kind: 'file-link',
|
|
93
|
+
file: relPath,
|
|
94
|
+
line: lineOf(code, href.start),
|
|
95
|
+
preview: toPreview(href.value),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const attribute of WRAPPABLE_ATTRIBUTES) {
|
|
100
|
+
const value = attributeValue(node, attribute);
|
|
101
|
+
if (value) {
|
|
102
|
+
items.push({
|
|
103
|
+
kind: 'attribute',
|
|
104
|
+
file: relPath,
|
|
105
|
+
line: lineOf(code, value.start),
|
|
106
|
+
preview: `${attribute}: ${toPreview(value.value)}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (node.type === 'JSXText' && !insideWrapped) {
|
|
113
|
+
const text = String(node.value ?? '');
|
|
114
|
+
const meaningful = text.replace(/[^\p{L}\p{N}]/gu, '');
|
|
115
|
+
if (meaningful.length >= MIN_TEXT_LENGTH) {
|
|
116
|
+
items.push({ kind: 'text', file: relPath, line: lineOf(code, node.start ?? 0), preview: toPreview(text) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// t('key') calls count as wrapped content.
|
|
120
|
+
if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 't') {
|
|
121
|
+
wrapped += 1;
|
|
122
|
+
}
|
|
123
|
+
for (const key of Object.keys(node)) {
|
|
124
|
+
const child = node[key];
|
|
125
|
+
if (Array.isArray(child)) {
|
|
126
|
+
for (const item of child) {
|
|
127
|
+
visit(item, childInsideWrapped);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (typeof child === 'object' && child !== null) {
|
|
131
|
+
visit(child, childInsideWrapped);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
visit(ast, false);
|
|
136
|
+
return { items, wrapped };
|
|
137
|
+
}
|
|
138
|
+
function collectSourceFiles(dir, out = []) {
|
|
139
|
+
if (!existsSync(dir)) {
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
for (const entry of readdirSync(dir)) {
|
|
143
|
+
if (SKIP_DIRS.has(entry)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const full = join(dir, entry);
|
|
147
|
+
let stats;
|
|
148
|
+
try {
|
|
149
|
+
stats = statSync(full);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (stats.isDirectory()) {
|
|
155
|
+
collectSourceFiles(full, out);
|
|
156
|
+
}
|
|
157
|
+
else if (/\.(tsx|jsx)$/.test(entry)) {
|
|
158
|
+
out.push(full);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Scan a project's source directories for unwrapped content. `dirs` defaults
|
|
165
|
+
* to the common Next/React layouts that exist under `rootDir`.
|
|
166
|
+
*/
|
|
167
|
+
export function scanWrappable(rootDir, opts = {}) {
|
|
168
|
+
const dirs = (opts.dirs ?? ['app', 'src', 'pages', 'components']).filter((dir) => existsSync(join(rootDir, dir)));
|
|
169
|
+
const files = new Set();
|
|
170
|
+
for (const dir of dirs) {
|
|
171
|
+
for (const file of collectSourceFiles(join(rootDir, dir))) {
|
|
172
|
+
files.add(file);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const items = [];
|
|
176
|
+
let wrappedCount = 0;
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
let code;
|
|
179
|
+
try {
|
|
180
|
+
code = readFileSync(file, 'utf8');
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const result = scanWrappableFile(file, code, relative(rootDir, file));
|
|
186
|
+
items.push(...result.items);
|
|
187
|
+
wrappedCount += result.wrapped;
|
|
188
|
+
}
|
|
189
|
+
items.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
190
|
+
return { items, wrappedCount, filesScanned: files.size };
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|