fimo 0.6.0 → 0.7.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/skills/fimo/SKILL.md +2 -2
- package/assets/skills/fimo/references/locales.md +0 -8
- package/assets/skills/fimo/references/setup-react-router.md +2 -2
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/references/content.md +0 -3
- package/assets/skills/fimo-cli/references/locales.md +0 -6
- package/assets/skills/fimo-migration/SKILL.md +22 -2
- package/assets/skills/fimo-migration/references/content.md +8 -0
- package/assets/skills/fimo-migration/references/manifest.md +35 -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 +617 -0
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- 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 +1 -1
- package/dist/cli/index.js +29490 -21165
- 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/astro/index.d.ts +2 -0
- package/dist/runtime/astro/index.d.ts.map +1 -0
- package/dist/runtime/astro/index.js +3 -0
- package/dist/runtime/astro/server.d.ts +3 -0
- package/dist/runtime/astro/server.d.ts.map +1 -0
- package/dist/runtime/astro/server.js +6 -0
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
- package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextProviders.js +20 -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/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 +2 -0
- package/dist/runtime/next/server.d.ts.map +1 -0
- package/dist/runtime/next/server.js +4 -0
- package/dist/runtime/nuxt/composables.d.ts +9 -0
- package/dist/runtime/nuxt/composables.d.ts.map +1 -0
- package/dist/runtime/nuxt/composables.js +57 -0
- package/dist/runtime/nuxt/index.d.ts +3 -0
- package/dist/runtime/nuxt/index.d.ts.map +1 -0
- package/dist/runtime/nuxt/index.js +6 -0
- package/dist/runtime/nuxt/server.d.ts +3 -0
- package/dist/runtime/nuxt/server.d.ts.map +1 -0
- package/dist/runtime/nuxt/server.js +6 -0
- package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +5 -15
- 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 +11 -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/FimoSpaProviders.d.ts +36 -0
- package/dist/runtime/react/FimoSpaProviders.d.ts.map +1 -0
- package/dist/runtime/react/FimoSpaProviders.js +63 -0
- package/dist/runtime/react/index.d.ts +33 -0
- package/dist/runtime/react/index.d.ts.map +1 -0
- package/dist/runtime/react/index.js +39 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
- package/dist/runtime/remix/FimoRemixProviders.d.ts.map +1 -0
- package/dist/runtime/remix/FimoRemixProviders.js +12 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts.map +1 -0
- package/dist/runtime/remix/FimoRemixScripts.js +19 -0
- package/dist/runtime/remix/index.d.ts +12 -0
- package/dist/runtime/remix/index.d.ts.map +1 -0
- package/dist/runtime/remix/index.js +11 -0
- package/dist/runtime/remix/server.d.ts +2 -0
- package/dist/runtime/remix/server.d.ts.map +1 -0
- package/dist/runtime/remix/server.js +2 -0
- package/dist/runtime/seo/buildSeoForPath.d.ts +3 -1
- package/dist/runtime/seo/buildSeoForPath.d.ts.map +1 -1
- package/dist/runtime/seo/buildSeoForPath.js +1 -4
- 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/i18n-messages.d.ts +29 -0
- package/dist/runtime/shared/i18n-messages.d.ts.map +1 -0
- package/dist/runtime/shared/i18n-messages.js +79 -0
- package/dist/runtime/shared/i18n-messages.test.d.ts +2 -0
- package/dist/runtime/shared/i18n-messages.test.d.ts.map +1 -0
- package/dist/runtime/shared/i18n-messages.test.js +39 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts.map +1 -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.d.ts.map +1 -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.d.ts.map +1 -0
- package/dist/runtime/shared/labels-t.pure.js +10 -0
- package/dist/runtime/strapi/client.d.ts +26 -0
- package/dist/runtime/strapi/client.d.ts.map +1 -0
- package/dist/runtime/strapi/client.js +88 -0
- package/dist/runtime/strapi/client.test.d.ts +2 -0
- package/dist/runtime/strapi/client.test.d.ts.map +1 -0
- package/dist/runtime/strapi/client.test.js +64 -0
- package/dist/runtime/strapi/index.d.ts +5 -0
- package/dist/runtime/strapi/index.d.ts.map +1 -0
- package/dist/runtime/strapi/index.js +6 -0
- package/dist/runtime/strapi/reshape.d.ts +28 -0
- package/dist/runtime/strapi/reshape.d.ts.map +1 -0
- package/dist/runtime/strapi/reshape.js +79 -0
- package/dist/runtime/strapi/reshape.test.d.ts +2 -0
- package/dist/runtime/strapi/reshape.test.d.ts.map +1 -0
- package/dist/runtime/strapi/reshape.test.js +33 -0
- package/dist/runtime/svelte/index.d.ts +3 -0
- package/dist/runtime/svelte/index.d.ts.map +1 -0
- package/dist/runtime/svelte/index.js +7 -0
- package/dist/runtime/svelte/server.d.ts +3 -0
- package/dist/runtime/svelte/server.d.ts.map +1 -0
- package/dist/runtime/svelte/server.js +6 -0
- package/dist/runtime/vue/index.d.ts +3 -0
- package/dist/runtime/vue/index.d.ts.map +1 -0
- package/dist/runtime/vue/index.js +5 -0
- package/dist/runtime/vue/server.d.ts +3 -0
- package/dist/runtime/vue/server.d.ts.map +1 -0
- package/dist/runtime/vue/server.js +6 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoFileLink.js +19 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +23 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts.map +1 -0
- package/dist/runtime/wrapped/FimoText.js +20 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts.map +1 -0
- package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts.map +1 -0
- package/dist/runtime/wrapped/WrappedProviders.js +101 -0
- package/dist/runtime/wrapped/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.d.ts.map +1 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/dom.test.d.ts +2 -0
- package/dist/runtime/wrapped/dom.test.d.ts.map +1 -0
- package/dist/runtime/wrapped/dom.test.js +16 -0
- package/dist/runtime/wrapped/server.d.ts +77 -0
- package/dist/runtime/wrapped/server.d.ts.map +1 -0
- package/dist/runtime/wrapped/server.js +237 -0
- package/dist/runtime/wrapped/server.test.d.ts +2 -0
- package/dist/runtime/wrapped/server.test.d.ts.map +1 -0
- package/dist/runtime/wrapped/server.test.js +91 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +132 -0
- package/dist/scripts/apply-wrap-templates.test.d.ts +1 -0
- package/dist/scripts/apply-wrap-templates.test.js +199 -0
- package/dist/scripts/apply-wrap.d.ts +46 -0
- package/dist/scripts/apply-wrap.js +278 -0
- package/dist/scripts/apply-wrap.test.d.ts +1 -0
- package/dist/scripts/apply-wrap.test.js +226 -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 +32 -0
- package/dist/scripts/cms/example-content.js +108 -0
- package/dist/scripts/cms/example-content.test.d.ts +1 -0
- package/dist/scripts/cms/example-content.test.js +66 -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/detect.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/detect.test.js +56 -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/list-source.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/list-source.test.js +33 -0
- package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
- package/dist/scripts/cms/strapi/schema-map.js +149 -0
- package/dist/scripts/cms/strapi/schema-map.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/schema-map.test.js +91 -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/touchpoints.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/touchpoints.test.js +62 -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/wire-client.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/wire-client.test.js +51 -0
- package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
- package/dist/scripts/cms/strapi/wiring.js +36 -0
- package/dist/scripts/cms/strapi/wiring.test.d.ts +1 -0
- package/dist/scripts/cms/strapi/wiring.test.js +27 -0
- package/dist/scripts/cms/types.d.ts +117 -0
- package/dist/scripts/cms/types.js +11 -0
- package/dist/scripts/detect-framework.d.ts +39 -0
- package/dist/scripts/detect-framework.js +133 -0
- package/dist/scripts/detect-framework.test.d.ts +1 -0
- package/dist/scripts/detect-framework.test.js +122 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +135 -0
- package/dist/scripts/draft-rows.test.d.ts +1 -0
- package/dist/scripts/draft-rows.test.js +113 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +188 -0
- package/dist/scripts/draft-schemas.test.d.ts +1 -0
- package/dist/scripts/draft-schemas.test.js +152 -0
- package/dist/scripts/extract-translations.js +16 -11
- package/dist/scripts/extract-translations.test.d.ts +1 -0
- package/dist/scripts/extract-translations.test.js +66 -0
- package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
- package/dist/scripts/frameworks/astro/routes.js +55 -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 +70 -0
- package/dist/scripts/frameworks/discover.js +178 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +30 -0
- package/dist/scripts/frameworks/next/rewire.js +258 -0
- package/dist/scripts/frameworks/next/rewire.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/rewire.test.js +134 -0
- package/dist/scripts/frameworks/next/routes.d.ts +17 -0
- package/dist/scripts/frameworks/next/routes.js +134 -0
- package/dist/scripts/frameworks/next/routes.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/routes.test.js +64 -0
- package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
- package/dist/scripts/frameworks/next/wiring.js +186 -0
- package/dist/scripts/frameworks/next/wiring.test.d.ts +1 -0
- package/dist/scripts/frameworks/next/wiring.test.js +174 -0
- package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
- package/dist/scripts/frameworks/nuxt/routes.js +54 -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 +50 -0
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/support.test.d.ts +1 -0
- package/dist/scripts/frameworks/support.test.js +87 -0
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
- package/dist/scripts/frameworks/sveltekit/routes.js +50 -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 +15 -0
- package/dist/scripts/frameworks/vite/routes.js +93 -0
- package/dist/scripts/frameworks/vite/routes.test.d.ts +1 -0
- package/dist/scripts/frameworks/vite/routes.test.js +73 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +10 -0
- package/dist/scripts/frameworks/vite/wiring.js +99 -0
- package/dist/scripts/frameworks/vite/wiring.test.d.ts +1 -0
- package/dist/scripts/frameworks/vite/wiring.test.js +88 -0
- package/dist/scripts/i18n-namespace.d.ts +25 -0
- package/dist/scripts/i18n-namespace.js +53 -0
- package/dist/scripts/lib/validation-report.d.ts +1 -0
- package/dist/scripts/lib/validation-report.js +3 -0
- package/dist/scripts/render-parity/capture.d.ts +56 -0
- package/dist/scripts/render-parity/capture.js +366 -0
- package/dist/scripts/render-parity/capture.test.d.ts +1 -0
- package/dist/scripts/render-parity/capture.test.js +306 -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/compare.test.d.ts +1 -0
- package/dist/scripts/render-parity/compare.test.js +98 -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/normalize-html.test.d.ts +1 -0
- package/dist/scripts/render-parity/normalize-html.test.js +58 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- package/dist/scripts/resolve-routes.js +1 -1
- package/dist/scripts/scan-wrappable.d.ts +196 -0
- package/dist/scripts/scan-wrappable.js +1227 -0
- package/dist/scripts/scan-wrappable.test.d.ts +1 -0
- package/dist/scripts/scan-wrappable.test.js +521 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/package.json +92 -5
- package/release.json +2 -2
- package/templates/react-router/package.json +7 -10
- package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
- package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
- package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const REACT_CHECK = {
|
|
4
|
+
pkg: 'react',
|
|
5
|
+
min: '18.0.0',
|
|
6
|
+
label: 'React',
|
|
7
|
+
reason: "Fimo's inline-editing runtime uses React 18 APIs (useSyncExternalStore).",
|
|
8
|
+
remedy: 'Upgrade React to 18 or newer. The content still migrates on older React; only inline editing needs 18+.',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The runtime-floor checks for a detected framework. React-based frameworks all
|
|
12
|
+
* gate on `react`; the template frameworks gate on their own package. Frameworks
|
|
13
|
+
* with no runtime coupling (or none detected) return [] — nothing to gate.
|
|
14
|
+
*/
|
|
15
|
+
export function supportChecksFor(detection) {
|
|
16
|
+
switch (detection.framework) {
|
|
17
|
+
case 'next':
|
|
18
|
+
case 'remix':
|
|
19
|
+
case 'react-router':
|
|
20
|
+
return [REACT_CHECK];
|
|
21
|
+
case 'vite':
|
|
22
|
+
return detection.react ? [REACT_CHECK] : [];
|
|
23
|
+
case 'nuxt':
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
pkg: 'nuxt',
|
|
27
|
+
min: '3.0.0',
|
|
28
|
+
label: 'Nuxt',
|
|
29
|
+
reason: 'Fimo supports Nuxt 3+ (Vue 3). Nuxt 2 is end-of-life.',
|
|
30
|
+
remedy: 'Upgrade to Nuxt 3 or newer.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
case 'vue':
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
pkg: 'vue',
|
|
37
|
+
min: '3.0.0',
|
|
38
|
+
label: 'Vue',
|
|
39
|
+
reason: 'Fimo supports Vue 3+. Vue 2 is end-of-life.',
|
|
40
|
+
remedy: 'Upgrade to Vue 3 or newer.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
case 'astro':
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
pkg: 'astro',
|
|
47
|
+
min: '4.0.0',
|
|
48
|
+
label: 'Astro',
|
|
49
|
+
reason: 'Fimo supports Astro 4+.',
|
|
50
|
+
remedy: 'Upgrade Astro to 4 or newer.',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
case 'sveltekit':
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
pkg: '@sveltejs/kit',
|
|
57
|
+
min: '2.0.0',
|
|
58
|
+
label: 'SvelteKit',
|
|
59
|
+
reason: 'Fimo supports SvelteKit 2+.',
|
|
60
|
+
remedy: 'Upgrade @sveltejs/kit to 2 or newer.',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
default:
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The lowest concrete version a spec implies. Handles the shapes seen in real
|
|
69
|
+
* package.json / installed manifests: `^18.2.0`, `~3.4`, `>=2.0.0`, `18 || 19`,
|
|
70
|
+
* `18.3.1`, ` >= 14.0.0 `, bare `18`. Returns null for anything without a
|
|
71
|
+
* numeric floor (`latest`, `*`, `workspace:*`, `catalog:`, a git/file url) — we
|
|
72
|
+
* never gate on a version we cannot read.
|
|
73
|
+
*/
|
|
74
|
+
export function parseVersionFloor(spec) {
|
|
75
|
+
if (!spec) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// A `||` union — the floor is the lowest branch, i.e. the first token.
|
|
79
|
+
const first = spec.split('||')[0];
|
|
80
|
+
const match = first.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
major: Number(match[1]),
|
|
86
|
+
minor: match[2] ? Number(match[2]) : 0,
|
|
87
|
+
patch: match[3] ? Number(match[3]) : 0,
|
|
88
|
+
raw: spec.trim(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function isBelowFloor(found, min) {
|
|
92
|
+
const floor = parseVersionFloor(min);
|
|
93
|
+
if (!floor) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (found.major !== floor.major) {
|
|
97
|
+
return found.major < floor.major;
|
|
98
|
+
}
|
|
99
|
+
if (found.minor !== floor.minor) {
|
|
100
|
+
return found.minor < floor.minor;
|
|
101
|
+
}
|
|
102
|
+
return found.patch < floor.patch;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The host's version of `pkg`. Prefers the INSTALLED manifest
|
|
106
|
+
* (`node_modules/<pkg>/package.json`) — the ground truth of what will run —
|
|
107
|
+
* and falls back to the declared range floor in the host package.json (a fresh
|
|
108
|
+
* clone before install). Returns null when neither is readable.
|
|
109
|
+
*/
|
|
110
|
+
export function readHostVersion(cwd, pkg) {
|
|
111
|
+
const installed = join(cwd, 'node_modules', pkg, 'package.json');
|
|
112
|
+
if (existsSync(installed)) {
|
|
113
|
+
try {
|
|
114
|
+
const version = JSON.parse(readFileSync(installed, 'utf8')).version;
|
|
115
|
+
const parsed = parseVersionFloor(String(version));
|
|
116
|
+
if (parsed) {
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// fall through to the declared range
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const rootPkg = join(cwd, 'package.json');
|
|
125
|
+
if (existsSync(rootPkg)) {
|
|
126
|
+
try {
|
|
127
|
+
const pkgJson = JSON.parse(readFileSync(rootPkg, 'utf8'));
|
|
128
|
+
const spec = { ...pkgJson.dependencies, ...pkgJson.devDependencies }[pkg];
|
|
129
|
+
if (spec) {
|
|
130
|
+
return parseVersionFloor(String(spec));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// unreadable — nothing to gate on
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Gate the wrap on the host's framework/runtime versions. Silent for supported
|
|
141
|
+
* hosts (the common case); returns one violation per below-floor dependency so
|
|
142
|
+
* the caller can stop with a clear message. A version we cannot read is never a
|
|
143
|
+
* violation — the peer range and the install still catch a genuinely absent dep.
|
|
144
|
+
*/
|
|
145
|
+
export function checkHostSupport(cwd, detection) {
|
|
146
|
+
const violations = [];
|
|
147
|
+
const detected = [];
|
|
148
|
+
for (const check of supportChecksFor(detection)) {
|
|
149
|
+
const found = readHostVersion(cwd, check.pkg);
|
|
150
|
+
if (!found) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
detected.push({ label: check.label, version: found.raw });
|
|
154
|
+
if (isBelowFloor(found, check.min)) {
|
|
155
|
+
violations.push({
|
|
156
|
+
subject: check.label,
|
|
157
|
+
found: found.raw,
|
|
158
|
+
min: check.min,
|
|
159
|
+
headline: `${check.label} ${found.raw} is below Fimo's floor of ${check.label} ${check.min}. ${check.reason}`,
|
|
160
|
+
remedy: check.remedy,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { ok: violations.length === 0, violations, detected };
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { checkHostSupport, parseVersionFloor, readHostVersion } from './support.js';
|
|
6
|
+
let dir;
|
|
7
|
+
function project(pkg, installed = {}) {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'host-support-'));
|
|
9
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg));
|
|
10
|
+
for (const [name, version] of Object.entries(installed)) {
|
|
11
|
+
const modDir = join(dir, 'node_modules', name);
|
|
12
|
+
mkdirSync(modDir, { recursive: true });
|
|
13
|
+
writeFileSync(join(modDir, 'package.json'), JSON.stringify({ name, version }));
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (dir) {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
describe('parseVersionFloor', () => {
|
|
23
|
+
it('reads the lowest concrete version from real spec shapes', () => {
|
|
24
|
+
expect(parseVersionFloor('^18.2.0')).toMatchObject({ major: 18, minor: 2, patch: 0 });
|
|
25
|
+
expect(parseVersionFloor('>=2.0.0')).toMatchObject({ major: 2, minor: 0, patch: 0 });
|
|
26
|
+
expect(parseVersionFloor('~3.4')).toMatchObject({ major: 3, minor: 4, patch: 0 });
|
|
27
|
+
expect(parseVersionFloor('18')).toMatchObject({ major: 18, minor: 0, patch: 0 });
|
|
28
|
+
// a `||` union floors on the lowest (first) branch
|
|
29
|
+
expect(parseVersionFloor('^17.0.0 || ^18.0.0')).toMatchObject({ major: 17 });
|
|
30
|
+
});
|
|
31
|
+
it('returns null for specs with no numeric floor', () => {
|
|
32
|
+
expect(parseVersionFloor('latest')).toBeNull();
|
|
33
|
+
expect(parseVersionFloor('*')).toBeNull();
|
|
34
|
+
expect(parseVersionFloor('workspace:*')).toBeNull();
|
|
35
|
+
expect(parseVersionFloor('')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('readHostVersion', () => {
|
|
39
|
+
it('prefers the installed manifest over the declared range', () => {
|
|
40
|
+
const cwd = project({ dependencies: { react: '^18.0.0' } }, { react: '18.3.1' });
|
|
41
|
+
expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 18, minor: 3, patch: 1 });
|
|
42
|
+
});
|
|
43
|
+
it('falls back to the declared range when nothing is installed', () => {
|
|
44
|
+
const cwd = project({ dependencies: { react: '^19.0.0' } });
|
|
45
|
+
expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 19, minor: 0, patch: 0 });
|
|
46
|
+
});
|
|
47
|
+
it('returns null when the dependency is absent', () => {
|
|
48
|
+
const cwd = project({ dependencies: {} });
|
|
49
|
+
expect(readHostVersion(cwd, 'react')).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('checkHostSupport', () => {
|
|
53
|
+
it('passes a Next.js site on React 18', () => {
|
|
54
|
+
const cwd = project({ dependencies: { next: '16.2.11', react: '^18.2.0' } }, { react: '18.3.1' });
|
|
55
|
+
const result = checkHostSupport(cwd, { framework: 'next' });
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
expect(result.violations).toEqual([]);
|
|
58
|
+
expect(result.detected).toContainEqual({ label: 'React', version: '18.3.1' });
|
|
59
|
+
});
|
|
60
|
+
it('passes a Next.js site on React 19', () => {
|
|
61
|
+
const cwd = project({ dependencies: { next: '16.2.11', react: '^19.0.0' } }, { react: '19.0.0' });
|
|
62
|
+
expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('flags a React 17 site below the floor with an actionable message', () => {
|
|
65
|
+
const cwd = project({ dependencies: { next: '13.0.0', react: '^17.0.2' } }, { react: '17.0.2' });
|
|
66
|
+
const result = checkHostSupport(cwd, { framework: 'next' });
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
expect(result.violations).toHaveLength(1);
|
|
69
|
+
expect(result.violations[0]).toMatchObject({ subject: 'React', min: '18.0.0' });
|
|
70
|
+
expect(result.violations[0].headline).toContain('React 17.0.2');
|
|
71
|
+
expect(result.violations[0].remedy).toMatch(/upgrade react/i);
|
|
72
|
+
});
|
|
73
|
+
it('flags a Nuxt 2 site below the floor', () => {
|
|
74
|
+
const cwd = project({ dependencies: { nuxt: '^2.17.0' } }, { nuxt: '2.17.0' });
|
|
75
|
+
const result = checkHostSupport(cwd, { framework: 'nuxt' });
|
|
76
|
+
expect(result.ok).toBe(false);
|
|
77
|
+
expect(result.violations[0]).toMatchObject({ subject: 'Nuxt', min: '3.0.0' });
|
|
78
|
+
});
|
|
79
|
+
it('does not gate a non-React bare Vite site', () => {
|
|
80
|
+
const cwd = project({ dependencies: { vite: '^5.0.0' } });
|
|
81
|
+
expect(checkHostSupport(cwd, { framework: 'vite', react: false }).ok).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('does not gate on a version it cannot read', () => {
|
|
84
|
+
const cwd = project({ dependencies: { next: '16.0.0', react: 'workspace:*' } });
|
|
85
|
+
expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const titleCase = (seg) => seg
|
|
4
|
+
.replace(/[_\-]+/g, ' ')
|
|
5
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
+
.join(' ');
|
|
10
|
+
function pushRoute(routes, parts) {
|
|
11
|
+
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
12
|
+
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
13
|
+
routes.push({
|
|
14
|
+
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
15
|
+
path: routePath,
|
|
16
|
+
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
|
|
20
|
+
/** SvelteKit routes: every directory under src/routes holding a +page file. */
|
|
21
|
+
export function discoverSvelteKitRoutes(routesDir) {
|
|
22
|
+
const routes = [];
|
|
23
|
+
const walk = (dir, segments) => {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (entries.some((entry) => /^\+page\.(svelte|md)$/.test(entry))) {
|
|
32
|
+
// Route groups (parens) are layout-only segments.
|
|
33
|
+
pushRoute(routes, segments.filter((seg) => !seg.startsWith('(')));
|
|
34
|
+
}
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const full = join(dir, entry);
|
|
37
|
+
try {
|
|
38
|
+
if (statSync(full).isDirectory()) {
|
|
39
|
+
walk(full, [...segments, toSegment(entry)]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(routesDir, []);
|
|
48
|
+
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
49
|
+
return routes;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type TemplateWrapStyle } from '../../apply-wrap-templates.js';
|
|
2
|
+
/**
|
|
3
|
+
* SvelteKit template wraps. The t binding is deliberately a mode-agnostic
|
|
4
|
+
* `const` — a `$derived` flips a legacy component into runes mode (breaking
|
|
5
|
+
* its own `$:` statements) and a `$:` reactive runs after init (arrays built
|
|
6
|
+
* at init see undefined). Labels only change with a reload in edit mode,
|
|
7
|
+
* which the wrap contract accepts.
|
|
8
|
+
*/
|
|
9
|
+
export declare const svelteTemplateWrapStyle: TemplateWrapStyle;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { injectIntoInstanceScript } from '../../apply-wrap-templates.js';
|
|
4
|
+
import { slugify } from '../../apply-wrap.js';
|
|
5
|
+
/**
|
|
6
|
+
* SvelteKit template wraps. The t binding is deliberately a mode-agnostic
|
|
7
|
+
* `const` — a `$derived` flips a legacy component into runes mode (breaking
|
|
8
|
+
* its own `$:` statements) and a `$:` reactive runs after init (arrays built
|
|
9
|
+
* at init see undefined). Labels only change with a reload in edit mode,
|
|
10
|
+
* which the wrap contract accepts.
|
|
11
|
+
*/
|
|
12
|
+
export const svelteTemplateWrapStyle = {
|
|
13
|
+
ext: '.svelte',
|
|
14
|
+
head: "import { page } from '$app/stores';\n\timport { makeFimoT } from 'fimo/svelte';\n\tconst t = makeFimoT($page.data.fimoLabels);",
|
|
15
|
+
headMarker: /makeFimoT/,
|
|
16
|
+
open: '{t(',
|
|
17
|
+
close: ')}',
|
|
18
|
+
injectHead: (code, head) => injectIntoInstanceScript(code, head, '<script>'),
|
|
19
|
+
// Route-convention filenames carry no meaning: scope +page/+layout keys by
|
|
20
|
+
// their route directory (src/routes/pricing/+page.svelte -> "pricing").
|
|
21
|
+
fileScope: (file) => {
|
|
22
|
+
const stem = basename(file).replace(/\.svelte$/, '');
|
|
23
|
+
if (!stem.startsWith('+')) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const dir = basename(dirname(file));
|
|
27
|
+
if (!dir || dir === 'routes' || dir === 'src') {
|
|
28
|
+
return stem === '+layout' ? 'layout' : 'home';
|
|
29
|
+
}
|
|
30
|
+
return slugify(dir) || 'home';
|
|
31
|
+
},
|
|
32
|
+
// The store form reads $page.data.fimoLabels: wire the root server load
|
|
33
|
+
// once when the project has none. An existing +layout.server.* is agent
|
|
34
|
+
// work (merging into a user load function is judgment, and until then the
|
|
35
|
+
// inline fallbacks keep rendering identical).
|
|
36
|
+
ensureWiring: (rootDir) => {
|
|
37
|
+
const routesDir = join(rootDir, 'src', 'routes');
|
|
38
|
+
const existing = ['+layout.server.js', '+layout.server.ts'].some((name) => existsSync(join(routesDir, name)));
|
|
39
|
+
if (existing || !existsSync(routesDir)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const layoutPath = join(routesDir, '+layout.server.js');
|
|
43
|
+
writeFileSync(layoutPath, [
|
|
44
|
+
"import { loadFimoLabels } from 'fimo/svelte/server';",
|
|
45
|
+
'',
|
|
46
|
+
'export const load = async () => {',
|
|
47
|
+
'\tconst labels = await loadFimoLabels();',
|
|
48
|
+
'\treturn { fimoLabels: labels.dictionaries[labels.defaultLocale] ?? {} };',
|
|
49
|
+
'};',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n'));
|
|
52
|
+
return ['src/routes/+layout.server.js'];
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DiscoveredRoute } from '../next/routes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Route discovery for Vite React SPAs: there is no file convention, so the
|
|
4
|
+
* routes live in react-router code. v1 reads LITERAL, ABSOLUTE paths from the
|
|
5
|
+
* two mainstream declaration shapes —
|
|
6
|
+
*
|
|
7
|
+
* <Route path="/pricing" element={…} /> (JSX tree)
|
|
8
|
+
* createBrowserRouter([{ path: '/pricing', … }]) (object form)
|
|
9
|
+
*
|
|
10
|
+
* — in files that import react-router. Relative child paths and index routes
|
|
11
|
+
* need the router's nesting semantics to resolve; they (and `*` wildcards)
|
|
12
|
+
* stay out of v1: the fimo-config route registry accepts manual entries for
|
|
13
|
+
* anything the scan cannot see, and scanned routes only ever ADD to it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function discoverReactRouterDomRoutes(rootDir: string): DiscoveredRoute[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const titleCase = (seg) => seg
|
|
4
|
+
.replace(/[_\-]+/g, ' ')
|
|
5
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
+
.join(' ');
|
|
10
|
+
const ROUTE_SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
11
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git']);
|
|
12
|
+
/**
|
|
13
|
+
* Route discovery for Vite React SPAs: there is no file convention, so the
|
|
14
|
+
* routes live in react-router code. v1 reads LITERAL, ABSOLUTE paths from the
|
|
15
|
+
* two mainstream declaration shapes —
|
|
16
|
+
*
|
|
17
|
+
* <Route path="/pricing" element={…} /> (JSX tree)
|
|
18
|
+
* createBrowserRouter([{ path: '/pricing', … }]) (object form)
|
|
19
|
+
*
|
|
20
|
+
* — in files that import react-router. Relative child paths and index routes
|
|
21
|
+
* need the router's nesting semantics to resolve; they (and `*` wildcards)
|
|
22
|
+
* stay out of v1: the fimo-config route registry accepts manual entries for
|
|
23
|
+
* anything the scan cannot see, and scanned routes only ever ADD to it.
|
|
24
|
+
*/
|
|
25
|
+
export function discoverReactRouterDomRoutes(rootDir) {
|
|
26
|
+
const srcDir = join(rootDir, 'src');
|
|
27
|
+
const files = [];
|
|
28
|
+
const walk = (dir) => {
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(dir);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (SKIP_DIRS.has(entry)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const full = join(dir, entry);
|
|
41
|
+
let isDir = false;
|
|
42
|
+
try {
|
|
43
|
+
isDir = statSync(full).isDirectory();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (isDir) {
|
|
49
|
+
walk(full);
|
|
50
|
+
}
|
|
51
|
+
else if (ROUTE_SOURCE.test(entry)) {
|
|
52
|
+
files.push(full);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(srcDir);
|
|
57
|
+
const routes = new Map();
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
let code;
|
|
60
|
+
try {
|
|
61
|
+
code = readFileSync(file, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!/from\s+['"]react-router(?:-dom)?['"]/.test(code)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const paths = [
|
|
70
|
+
...[...code.matchAll(/<Route\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|\{\s*(?:"([^"]+)"|'([^']+)')\s*\})/g)],
|
|
71
|
+
...[...code.matchAll(/\bpath\s*:\s*(?:"([^"]+)"|'([^']+)')/g)],
|
|
72
|
+
]
|
|
73
|
+
.map((match) => match.slice(1).find(Boolean))
|
|
74
|
+
.filter((path) => typeof path === 'string');
|
|
75
|
+
for (const raw of paths) {
|
|
76
|
+
if (!raw.startsWith('/') || raw.includes('*')) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const path = raw !== '/' && raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
80
|
+
if (routes.has(path)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const parts = path.split('/').filter(Boolean);
|
|
84
|
+
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
85
|
+
routes.set(path, {
|
|
86
|
+
id: path === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
87
|
+
path,
|
|
88
|
+
label: path === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? path),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { discoverReactRouterDomRoutes } from './routes.js';
|
|
6
|
+
let dir;
|
|
7
|
+
const project = (files) => {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'vite-routes-'));
|
|
9
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
10
|
+
const full = join(dir, rel);
|
|
11
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
12
|
+
writeFileSync(full, content);
|
|
13
|
+
}
|
|
14
|
+
return dir;
|
|
15
|
+
};
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (dir) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
describe('discoverReactRouterDomRoutes', () => {
|
|
22
|
+
it('reads literal absolute paths from JSX Route trees', () => {
|
|
23
|
+
const root = project({
|
|
24
|
+
'src/App.tsx': `import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
25
|
+
export function App() {
|
|
26
|
+
return (
|
|
27
|
+
<BrowserRouter>
|
|
28
|
+
<Routes>
|
|
29
|
+
<Route path="/" element={<Home />} />
|
|
30
|
+
<Route path="/about" element={<About />} />
|
|
31
|
+
<Route path="/features/pricing" element={<Pricing />} />
|
|
32
|
+
<Route path={'/contact'} element={<Contact />} />
|
|
33
|
+
</Routes>
|
|
34
|
+
</BrowserRouter>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
});
|
|
39
|
+
const routes = discoverReactRouterDomRoutes(root);
|
|
40
|
+
expect(routes.map((route) => route.path)).toEqual(['/', '/about', '/contact', '/features/pricing']);
|
|
41
|
+
expect(routes[0]).toEqual({ id: 'home', path: '/', label: 'Home' });
|
|
42
|
+
expect(routes.find((route) => route.path === '/features/pricing')).toEqual({
|
|
43
|
+
id: 'features-pricing',
|
|
44
|
+
path: '/features/pricing',
|
|
45
|
+
label: 'Pricing',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('reads object-form routers and keeps :params, skipping wildcards and relative paths', () => {
|
|
49
|
+
const root = project({
|
|
50
|
+
'src/router.ts': `import { createBrowserRouter } from 'react-router-dom';
|
|
51
|
+
export const router = createBrowserRouter([
|
|
52
|
+
{ path: '/', element: null },
|
|
53
|
+
{ path: '/posts/:slug', element: null },
|
|
54
|
+
{ path: '*', element: null },
|
|
55
|
+
{ path: 'relative-child', element: null },
|
|
56
|
+
]);
|
|
57
|
+
`,
|
|
58
|
+
});
|
|
59
|
+
const routes = discoverReactRouterDomRoutes(root);
|
|
60
|
+
expect(routes.map((route) => route.path)).toEqual(['/', '/posts/:slug']);
|
|
61
|
+
expect(routes.find((route) => route.path === '/posts/:slug')).toEqual({
|
|
62
|
+
id: 'posts-slug',
|
|
63
|
+
path: '/posts/:slug',
|
|
64
|
+
label: 'Posts',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('ignores path-shaped literals in files that do not import react-router', () => {
|
|
68
|
+
const root = project({
|
|
69
|
+
'src/config.ts': `export const nav = [{ path: '/dashboard', label: 'Dashboard' }];\n`,
|
|
70
|
+
});
|
|
71
|
+
expect(discoverReactRouterDomRoutes(root)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RootWiringResult } from '../next/wiring.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vite React SPA root wiring: wrap the root render argument with
|
|
4
|
+
* `FimoSpaProviders` (client-side label fetch; inline fallbacks until it
|
|
5
|
+
* resolves) and import the project's fimo-config.json. Only the mainstream
|
|
6
|
+
* entry shape is touched — a `createRoot(…).render(<JSX/>)` (or legacy
|
|
7
|
+
* `ReactDOM.render(<JSX/>, …)`) call whose first argument is a JSX element.
|
|
8
|
+
* Anything else bails with the reason and stays agent work via the recipe.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureSpaEntryWiring(cwd: string): RootWiringResult;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseSync } from 'oxc-parser';
|
|
4
|
+
/**
|
|
5
|
+
* Vite React SPA root wiring: wrap the root render argument with
|
|
6
|
+
* `FimoSpaProviders` (client-side label fetch; inline fallbacks until it
|
|
7
|
+
* resolves) and import the project's fimo-config.json. Only the mainstream
|
|
8
|
+
* entry shape is touched — a `createRoot(…).render(<JSX/>)` (or legacy
|
|
9
|
+
* `ReactDOM.render(<JSX/>, …)`) call whose first argument is a JSX element.
|
|
10
|
+
* Anything else bails with the reason and stays agent work via the recipe.
|
|
11
|
+
*/
|
|
12
|
+
export function ensureSpaEntryWiring(cwd) {
|
|
13
|
+
const entryFile = ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx']
|
|
14
|
+
.map((candidate) => path.join(cwd, candidate))
|
|
15
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
16
|
+
const bail = (reason, rel = 'src/main.tsx') => ({
|
|
17
|
+
changes: [],
|
|
18
|
+
warnings: [`Root wiring not applied: ${reason} — follow the SPA recipe in the agent prompt (${rel}).`],
|
|
19
|
+
wired: false,
|
|
20
|
+
});
|
|
21
|
+
if (!entryFile) {
|
|
22
|
+
return bail('no src/main or src/index entry file found');
|
|
23
|
+
}
|
|
24
|
+
const rel = path.relative(cwd, entryFile);
|
|
25
|
+
const code = fs.readFileSync(entryFile, 'utf8');
|
|
26
|
+
if (code.includes('FimoSpaProviders')) {
|
|
27
|
+
return { changes: [], warnings: [], wired: true };
|
|
28
|
+
}
|
|
29
|
+
let program;
|
|
30
|
+
try {
|
|
31
|
+
program = parseSync(entryFile, code).program;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return bail('entry file could not be parsed', rel);
|
|
35
|
+
}
|
|
36
|
+
// The JSX argument of the root render call, found structurally: a call
|
|
37
|
+
// whose callee property is `render` with a JSX element first argument
|
|
38
|
+
// (covers `createRoot(…).render(<X/>)` and `ReactDOM.render(<X/>, node)`).
|
|
39
|
+
const renderArg = (function find(node) {
|
|
40
|
+
if (!node || typeof node !== 'object') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const candidate = node;
|
|
44
|
+
if (candidate.type === 'CallExpression' &&
|
|
45
|
+
candidate.callee?.type === 'MemberExpression' &&
|
|
46
|
+
candidate.callee?.property?.name === 'render' &&
|
|
47
|
+
(candidate.arguments?.[0]?.type === 'JSXElement' || candidate.arguments?.[0]?.type === 'JSXFragment')) {
|
|
48
|
+
return candidate.arguments[0];
|
|
49
|
+
}
|
|
50
|
+
for (const value of Object.values(candidate)) {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
const found = find(entry);
|
|
54
|
+
if (found) {
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (value && typeof value === 'object') {
|
|
60
|
+
const found = find(value);
|
|
61
|
+
if (found) {
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
})(program);
|
|
68
|
+
if (!renderArg) {
|
|
69
|
+
return bail('no root render call with a JSX argument found', rel);
|
|
70
|
+
}
|
|
71
|
+
const configImportPath = path
|
|
72
|
+
.relative(path.dirname(entryFile), path.join(cwd, 'fimo-config.json'))
|
|
73
|
+
.split(path.sep)
|
|
74
|
+
.join('/');
|
|
75
|
+
const jsx = code.slice(renderArg.start, renderArg.end);
|
|
76
|
+
const open = '<FimoSpaProviders\n' +
|
|
77
|
+
' config={fimoConfig}\n' +
|
|
78
|
+
' apiUrl={import.meta.env.VITE_API_URL}\n' +
|
|
79
|
+
' previewScriptUrl={import.meta.env.DEV ? import.meta.env.VITE_FIMO_PREVIEW_SCRIPT_URL : undefined}\n' +
|
|
80
|
+
' >';
|
|
81
|
+
let next = code.slice(0, renderArg.start) + `${open}\n ${jsx}\n </FimoSpaProviders>` + code.slice(renderArg.end);
|
|
82
|
+
// AST-positioned like ensureFimoImport: a multiline import's first line is
|
|
83
|
+
// not the end of the imports.
|
|
84
|
+
const importBlock = `import { FimoSpaProviders } from 'fimo/react';\nimport fimoConfig from '${configImportPath.startsWith('.') ? configImportPath : `./${configImportPath}`}';`;
|
|
85
|
+
let lastImportEnd = -1;
|
|
86
|
+
for (const node of program.body ?? []) {
|
|
87
|
+
if (node.type === 'ImportDeclaration') {
|
|
88
|
+
lastImportEnd = node.end;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// The wrap edit above only touched positions at/after renderArg.start,
|
|
92
|
+
// which is always beyond the import block, so import spans still hold.
|
|
93
|
+
next =
|
|
94
|
+
lastImportEnd >= 0
|
|
95
|
+
? `${next.slice(0, lastImportEnd)}\n${importBlock}${next.slice(lastImportEnd)}`
|
|
96
|
+
: `${importBlock}\n${next}`;
|
|
97
|
+
fs.writeFileSync(entryFile, next);
|
|
98
|
+
return { changes: [`wired FimoSpaProviders into ${rel}`], warnings: [], wired: true };
|
|
99
|
+
}
|