fimo 0.7.0-experimental.1 → 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/content-templates/hooks-template.eta +27 -73
- package/assets/content-templates/types-template.eta +1 -4
- package/assets/skills/fimo/SKILL.md +2 -2
- package/assets/skills/fimo/references/content.md +42 -46
- 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 +14 -17
- 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 +20655 -12551
- 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/content/utils/retrieve-data-sources.d.ts +0 -1
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +1 -1
- package/dist/runtime/content/utils/retrieve-data-sources.js +4 -27
- 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/primitives.d.ts +2 -2
- package/dist/runtime/primitives/lib/primitives.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/query.d.ts +3 -5
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- 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/public-surface.test.js +5 -9
- 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/generated-types.test.js +1 -1
- 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/sync-schemas.test.js +4 -24
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.test.js +2 -2
- package/package.json +92 -5
- 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,91 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { wrapCollection } from './server.js';
|
|
3
|
+
const SOURCE = [
|
|
4
|
+
{
|
|
5
|
+
slug: 'post-1',
|
|
6
|
+
title: 'Charting the bay',
|
|
7
|
+
body: 'Prose one.\n\nProse two.',
|
|
8
|
+
cover: '/images/a.png',
|
|
9
|
+
featured: true,
|
|
10
|
+
tags: ['tides'],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
slug: 'post-2',
|
|
14
|
+
title: 'Berth songs',
|
|
15
|
+
body: 'More prose.',
|
|
16
|
+
cover: '/images/b.png',
|
|
17
|
+
featured: false,
|
|
18
|
+
tags: ['harbor'],
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
function mockEntries(rows) {
|
|
22
|
+
return vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ data: rows.map((data, index) => ({ id: `row-${index}`, data })) }), {
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: { 'content-type': 'application/json' },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
describe('wrapCollection', () => {
|
|
31
|
+
it('overlays type-matched Fimo values and falls back per field on shape mismatches', async () => {
|
|
32
|
+
const spy = mockEntries([
|
|
33
|
+
{
|
|
34
|
+
slug: 'post-1',
|
|
35
|
+
title: 'Charting the bay TOGETHER', // string → replaces
|
|
36
|
+
body: { type: 'doc', content: [] }, // richtext doc → fallback
|
|
37
|
+
cover: { source: 'local', url: '/images/a.png', alt: '' }, // media envelope → fallback
|
|
38
|
+
featured: false, // boolean → replaces
|
|
39
|
+
tags: [{ documentId: 'x' }], // reference handles → fallback
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
const rows = await wrapCollection('Post', SOURCE, { apiUrl: 'http://tenant.test' });
|
|
43
|
+
expect(rows).toHaveLength(2);
|
|
44
|
+
expect(rows[0].title).toBe('Charting the bay TOGETHER');
|
|
45
|
+
expect(rows[0].body).toBe('Prose one.\n\nProse two.');
|
|
46
|
+
expect(rows[0].cover).toBe('/images/a.png');
|
|
47
|
+
expect(rows[0].featured).toBe(false);
|
|
48
|
+
expect(rows[0].tags).toEqual(['tides']);
|
|
49
|
+
// Row without a matching entry stays pure source.
|
|
50
|
+
expect(rows[1]).toEqual(SOURCE[1]);
|
|
51
|
+
const url = new URL(String(spy.mock.calls[0][0]));
|
|
52
|
+
expect(JSON.parse(url.searchParams.get('where') ?? '{}')).toEqual({ slug: { $in: ['post-1', 'post-2'] } });
|
|
53
|
+
});
|
|
54
|
+
it('returns the source rows untouched when the API has nothing', async () => {
|
|
55
|
+
mockEntries([]);
|
|
56
|
+
const rows = await wrapCollection('Post', SOURCE, { apiUrl: 'http://tenant.test' });
|
|
57
|
+
expect(rows).toEqual(SOURCE);
|
|
58
|
+
});
|
|
59
|
+
it('returns the source rows when no key field is detectable', async () => {
|
|
60
|
+
const spy = mockEntries([]);
|
|
61
|
+
const keyless = [{ n: 1 }, { n: 2 }];
|
|
62
|
+
const rows = await wrapCollection('Stat', keyless, { apiUrl: 'http://tenant.test' });
|
|
63
|
+
expect(rows).toEqual(keyless);
|
|
64
|
+
expect(spy).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('loadFimoMessages', () => {
|
|
68
|
+
afterEach(() => vi.restoreAllMocks());
|
|
69
|
+
const mockLabels = (labels) => vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ data: { labels } }), {
|
|
70
|
+
status: 200,
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
}));
|
|
73
|
+
it('un-flattens the Fimo dictionary and deep-merges over the static fallback', async () => {
|
|
74
|
+
const { loadFimoMessages } = await import('./server.js');
|
|
75
|
+
mockLabels({ 'home.title': 'Bienvenue', 'cart.items': '{count, plural, one {# article} other {# articles}}' });
|
|
76
|
+
const fallback = { home: { title: 'FR fallback', cta: 'Commencer' }, nav: { about: 'À propos' } };
|
|
77
|
+
const messages = await loadFimoMessages('fr', { apiUrl: 'http://tenant.test', fallback });
|
|
78
|
+
// Edited key from Fimo wins; fallback-only keys survive; ICU stays opaque.
|
|
79
|
+
expect(messages).toEqual({
|
|
80
|
+
home: { title: 'Bienvenue', cta: 'Commencer' },
|
|
81
|
+
nav: { about: 'À propos' },
|
|
82
|
+
cart: { items: '{count, plural, one {# article} other {# articles}}' },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it('returns the fallback untouched when Fimo has no labels for the locale', async () => {
|
|
86
|
+
const { loadFimoMessages } = await import('./server.js');
|
|
87
|
+
mockLabels({});
|
|
88
|
+
const fallback = { home: { title: 'Static only' } };
|
|
89
|
+
expect(await loadFimoMessages('de', { apiUrl: 'http://tenant.test', fallback })).toEqual(fallback);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KeyAllocator, type ApplyWrapResult } from './apply-wrap.js';
|
|
2
|
+
import { type WrappableItem } from './scan-wrappable.js';
|
|
3
|
+
/**
|
|
4
|
+
* The template half of the --apply codemod: wraps plain TEMPLATE text nodes
|
|
5
|
+
* with the framework's t() form and injects the per-file wiring head once.
|
|
6
|
+
* Framework-neutral by contract — everything framework-specific arrives via
|
|
7
|
+
* a TemplateWrapStyle from the framework registry (frameworks/discover.ts).
|
|
8
|
+
*
|
|
9
|
+
* Grown out of the batch-dogfood plan-driven wrapper (392/392 items on a
|
|
10
|
+
* 201-route site); the lessons it encodes:
|
|
11
|
+
* - heads go in the INSTANCE script, never a `<script module>` block;
|
|
12
|
+
* - files that already bind a `t` identifier (i18n libraries, custom
|
|
13
|
+
* helpers) are left whole for the agent — renaming foreign t() calls
|
|
14
|
+
* broke a real site;
|
|
15
|
+
* - label VALUES decode HTML entities while the inline fallback keeps the
|
|
16
|
+
* source form, so rendering never changes;
|
|
17
|
+
* - keys are collision-safe via the shared KeyAllocator.
|
|
18
|
+
*
|
|
19
|
+
* Anything that is not a plain text node (attributes, media, markup the
|
|
20
|
+
* preview matcher cannot anchor) stays in the plan for the agent — same
|
|
21
|
+
* philosophy as the JSX codemod: provably safe or hands off.
|
|
22
|
+
*/
|
|
23
|
+
export interface TemplateWrapStyle {
|
|
24
|
+
/** File extension this style owns, e.g. '.astro'. */
|
|
25
|
+
ext: string;
|
|
26
|
+
/** Wiring lines injected once per file (the t() head). */
|
|
27
|
+
head: string;
|
|
28
|
+
/** Marker proving our wiring (or an equivalent manual one) is present. */
|
|
29
|
+
headMarker: RegExp;
|
|
30
|
+
/** t() delimiters around `"key", "fallback"`. */
|
|
31
|
+
open: string;
|
|
32
|
+
close: string;
|
|
33
|
+
/**
|
|
34
|
+
* Insert the head into a file. Return null when the file has no safe
|
|
35
|
+
* injection point (e.g. an options-API script) — its items stay agent work.
|
|
36
|
+
*/
|
|
37
|
+
injectHead: (code: string, head: string) => string | null;
|
|
38
|
+
/** Override the key scope for route-convention filenames (+page.svelte). */
|
|
39
|
+
fileScope?: (file: string) => string | null;
|
|
40
|
+
/** One-time project wiring once any file was wrapped. Returns files written. */
|
|
41
|
+
ensureWiring?: (rootDir: string) => string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Shared injection helper for SFC-style templates: first instance script,
|
|
45
|
+
* never a `<script module>` block (module scope cannot read page data), or a
|
|
46
|
+
* fresh script when the file has none.
|
|
47
|
+
*/
|
|
48
|
+
export declare function injectIntoInstanceScript(code: string, head: string, scriptTag: string): string;
|
|
49
|
+
export declare function applyTemplateWrapToFile(code: string, style: TemplateWrapStyle, items: WrappableItem[], allocator: KeyAllocator): {
|
|
50
|
+
code: string;
|
|
51
|
+
} & Omit<ApplyWrapResult, 'filesChanged'>;
|
|
52
|
+
/**
|
|
53
|
+
* Apply the template wraps across a project — same contract as applyWrap:
|
|
54
|
+
* scans, mutates source files, returns the generated labels for staging and
|
|
55
|
+
* seeding. The style's ensureWiring hook then writes any one-time project
|
|
56
|
+
* wiring (e.g. a root layout labels loader) when files were wrapped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function applyTemplateWrap(rootDir: string, opts: {
|
|
59
|
+
style: TemplateWrapStyle;
|
|
60
|
+
excludePrefixes?: string[];
|
|
61
|
+
}): ApplyWrapResult;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { decodeJsxEntities, KeyAllocator, slugify } from './apply-wrap.js';
|
|
4
|
+
import { scanWrappable } from './scan-wrappable.js';
|
|
5
|
+
/**
|
|
6
|
+
* Shared injection helper for SFC-style templates: first instance script,
|
|
7
|
+
* never a `<script module>` block (module scope cannot read page data), or a
|
|
8
|
+
* fresh script when the file has none.
|
|
9
|
+
*/
|
|
10
|
+
export function injectIntoInstanceScript(code, head, scriptTag) {
|
|
11
|
+
const instance = code.match(/<script(?![^>]*\bmodule\b)[^>]*>\r?\n?/);
|
|
12
|
+
if (instance && instance.index !== undefined) {
|
|
13
|
+
const at = instance.index + instance[0].length;
|
|
14
|
+
return `${code.slice(0, at)}\t${head}\n${code.slice(at)}`;
|
|
15
|
+
}
|
|
16
|
+
return `${scriptTag}\n\t${head}\n</script>\n\n${code}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A file that already binds `t` (an i18n library, a custom helper) is agent
|
|
20
|
+
* work: the dogfood proved rewiring foreign t() calls breaks rendering.
|
|
21
|
+
*/
|
|
22
|
+
function bindsForeignT(code) {
|
|
23
|
+
return (/\buseI18n\s*\(/.test(code) ||
|
|
24
|
+
/(?:const|let|var)\s+t\s*=/.test(code) ||
|
|
25
|
+
/import\s*\{[^}]*\bt\b[^}]*\}\s*from/.test(code));
|
|
26
|
+
}
|
|
27
|
+
function previewPattern(preview) {
|
|
28
|
+
const core = preview
|
|
29
|
+
.replace(/…$/, '')
|
|
30
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
31
|
+
.replace(/\\? /g, '\\s+');
|
|
32
|
+
return new RegExp(`>(\\s*)(${core}[^<{]*?)(\\s*)<`);
|
|
33
|
+
}
|
|
34
|
+
export function applyTemplateWrapToFile(code, style, items, allocator) {
|
|
35
|
+
const applied = [];
|
|
36
|
+
const skipped = [];
|
|
37
|
+
const labels = {};
|
|
38
|
+
const hasHead = style.headMarker.test(code);
|
|
39
|
+
if (!hasHead && bindsForeignT(code)) {
|
|
40
|
+
return {
|
|
41
|
+
code,
|
|
42
|
+
applied,
|
|
43
|
+
skipped: items.map((item) => ({ item, reason: 'file binds its own t() — agent work' })),
|
|
44
|
+
labels,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!hasHead && style.injectHead(code, style.head) === null) {
|
|
48
|
+
return {
|
|
49
|
+
code,
|
|
50
|
+
applied,
|
|
51
|
+
skipped: items.map((item) => ({ item, reason: 'no safe wiring point in this file — agent work' })),
|
|
52
|
+
labels,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const file = items[0]?.file ?? '';
|
|
56
|
+
const scope = style.fileScope?.(file) ?? slugify(basename(file).replace(/\.[a-z0-9]+$/i, '')) ?? 'shared';
|
|
57
|
+
let next = code;
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
if (item.kind !== 'text') {
|
|
60
|
+
skipped.push({ item, reason: `${item.kind} stays agent work` });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const match = next.match(previewPattern(item.preview));
|
|
64
|
+
if (!match || match.index === undefined) {
|
|
65
|
+
skipped.push({ item, reason: 'text node not matched in source — agent work' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const collapsed = match[2].replace(/\s+/g, ' ').trim();
|
|
69
|
+
const id = allocator.allocate(scope || 'shared', slugify(collapsed));
|
|
70
|
+
const escaped = collapsed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
71
|
+
const replacement = `>${match[1]}${style.open}"${id}", "${escaped}"${style.close}${match[3]}<`;
|
|
72
|
+
next = next.slice(0, match.index) + replacement + next.slice(match.index + match[0].length);
|
|
73
|
+
labels[id] = decodeJsxEntities(collapsed);
|
|
74
|
+
applied.push({ kind: 'text', file: item.file, line: item.line, id, preview: item.preview });
|
|
75
|
+
}
|
|
76
|
+
if (applied.length > 0 && !style.headMarker.test(next)) {
|
|
77
|
+
next = style.injectHead(next, style.head) ?? next;
|
|
78
|
+
}
|
|
79
|
+
return { code: next, applied, skipped, labels };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Apply the template wraps across a project — same contract as applyWrap:
|
|
83
|
+
* scans, mutates source files, returns the generated labels for staging and
|
|
84
|
+
* seeding. The style's ensureWiring hook then writes any one-time project
|
|
85
|
+
* wiring (e.g. a root layout labels loader) when files were wrapped.
|
|
86
|
+
*/
|
|
87
|
+
export function applyTemplateWrap(rootDir, opts) {
|
|
88
|
+
const { style } = opts;
|
|
89
|
+
const scan = scanWrappable(rootDir, { excludePrefixes: opts.excludePrefixes });
|
|
90
|
+
const existingLabels = (() => {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(readFileSync(join(rootDir, '.fimo', 'migration', 'labels.json'), 'utf8'));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
const allocator = new KeyAllocator([...Object.keys(existingLabels), ...scan.wrappedIds.labels]);
|
|
99
|
+
const result = { applied: [], skipped: [], labels: {}, filesChanged: [] };
|
|
100
|
+
const byFile = new Map();
|
|
101
|
+
for (const item of scan.items) {
|
|
102
|
+
if (!item.file.endsWith(style.ext)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const list = byFile.get(item.file) ?? [];
|
|
106
|
+
list.push(item);
|
|
107
|
+
byFile.set(item.file, list);
|
|
108
|
+
}
|
|
109
|
+
for (const [file, items] of byFile) {
|
|
110
|
+
const filePath = join(rootDir, file);
|
|
111
|
+
let code;
|
|
112
|
+
try {
|
|
113
|
+
code = readFileSync(filePath, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
result.skipped.push(...items.map((item) => ({ item, reason: 'unreadable file' })));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const fileResult = applyTemplateWrapToFile(code, style, items, allocator);
|
|
120
|
+
result.applied.push(...fileResult.applied);
|
|
121
|
+
result.skipped.push(...fileResult.skipped);
|
|
122
|
+
Object.assign(result.labels, fileResult.labels);
|
|
123
|
+
if (fileResult.applied.length > 0) {
|
|
124
|
+
writeFileSync(filePath, fileResult.code);
|
|
125
|
+
result.filesChanged.push(file);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (result.filesChanged.length > 0 && style.ensureWiring) {
|
|
129
|
+
result.filesChanged.push(...style.ensureWiring(rootDir));
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, 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 { applyTemplateWrap } from './apply-wrap-templates.js';
|
|
6
|
+
import { astroTemplateWrapStyle } from './frameworks/astro/template-style.js';
|
|
7
|
+
import { nuxtTemplateWrapStyle } from './frameworks/nuxt/template-style.js';
|
|
8
|
+
import { svelteTemplateWrapStyle } from './frameworks/sveltekit/template-style.js';
|
|
9
|
+
import { scanWrappable } from './scan-wrappable.js';
|
|
10
|
+
let dir;
|
|
11
|
+
const project = (files) => {
|
|
12
|
+
dir = mkdtempSync(join(tmpdir(), 'apply-wrap-templates-'));
|
|
13
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
14
|
+
const full = join(dir, rel);
|
|
15
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
16
|
+
writeFileSync(full, content);
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
};
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (dir) {
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
describe('applyTemplateWrap — astro', () => {
|
|
26
|
+
it('wraps plain text nodes and injects the head into an existing fence', () => {
|
|
27
|
+
const root = project({
|
|
28
|
+
'src/pages/index.astro': `---
|
|
29
|
+
const year = 2026;
|
|
30
|
+
---
|
|
31
|
+
<html><body>
|
|
32
|
+
<main>
|
|
33
|
+
<h1>Welcome to the harbor</h1>
|
|
34
|
+
<p>
|
|
35
|
+
Ship your docs
|
|
36
|
+
without the busywork.
|
|
37
|
+
</p>
|
|
38
|
+
<footer>© 2026 Harbor</footer>
|
|
39
|
+
</main>
|
|
40
|
+
</body></html>
|
|
41
|
+
`,
|
|
42
|
+
});
|
|
43
|
+
const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
|
|
44
|
+
const code = readFileSync(join(root, 'src/pages/index.astro'), 'utf8');
|
|
45
|
+
expect(result.applied.length).toBe(3);
|
|
46
|
+
expect(code).toContain('import { loadFimoT } from "fimo/astro/server";');
|
|
47
|
+
expect(code.indexOf('loadFimoT')).toBeGreaterThan(code.indexOf('---'));
|
|
48
|
+
expect(code).toContain('<h1>{t("index.welcome-to-the-harbor", "Welcome to the harbor")}</h1>');
|
|
49
|
+
// Multiline text collapses into the fallback, whitespace outside the wrap survives.
|
|
50
|
+
expect(code).toContain('{t("index.ship-your-docs-without", "Ship your docs without the busywork.")}');
|
|
51
|
+
// Label VALUE decodes entities; the inline fallback keeps the source form.
|
|
52
|
+
expect(code).toContain('"© 2026 Harbor")}');
|
|
53
|
+
expect(result.labels['index.copy-2026-harbor']).toBe('© 2026 Harbor');
|
|
54
|
+
// The injected form counts as wrapped on re-scan: nothing left, keys visible.
|
|
55
|
+
const rescan = scanWrappable(root);
|
|
56
|
+
expect(rescan.items.filter((item) => item.kind === 'text')).toEqual([]);
|
|
57
|
+
expect(rescan.wrappedIds.labels).toContain('index.welcome-to-the-harbor');
|
|
58
|
+
});
|
|
59
|
+
it('creates a frontmatter fence when the file has none', () => {
|
|
60
|
+
const root = project({
|
|
61
|
+
'src/pages/plain.astro': `<main><p>Just some copy here</p></main>
|
|
62
|
+
`,
|
|
63
|
+
});
|
|
64
|
+
applyTemplateWrap(root, { style: astroTemplateWrapStyle });
|
|
65
|
+
const code = readFileSync(join(root, 'src/pages/plain.astro'), 'utf8');
|
|
66
|
+
expect(code.startsWith('---\nimport { loadFimoT }')).toBe(true);
|
|
67
|
+
expect(code).toContain('{t("plain.just-some-copy-here", "Just some copy here")}');
|
|
68
|
+
});
|
|
69
|
+
it('leaves files that bind their own t() to the agent', () => {
|
|
70
|
+
const source = `---
|
|
71
|
+
import { useTranslations } from "../i18n";
|
|
72
|
+
const t = useTranslations("en");
|
|
73
|
+
---
|
|
74
|
+
<main><p>Localized copy already</p></main>
|
|
75
|
+
`;
|
|
76
|
+
const root = project({ 'src/pages/i18n.astro': source });
|
|
77
|
+
const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
|
|
78
|
+
expect(result.applied).toEqual([]);
|
|
79
|
+
expect(result.skipped.some((skip) => skip.reason.includes('binds its own t()'))).toBe(true);
|
|
80
|
+
expect(readFileSync(join(root, 'src/pages/i18n.astro'), 'utf8')).toBe(source);
|
|
81
|
+
});
|
|
82
|
+
it('allocates collision-safe keys for repeated copy', () => {
|
|
83
|
+
const root = project({
|
|
84
|
+
'src/pages/dup.astro': `---
|
|
85
|
+
---
|
|
86
|
+
<ul><li>Learn more</li><li>Learn more</li></ul>
|
|
87
|
+
`,
|
|
88
|
+
});
|
|
89
|
+
const result = applyTemplateWrap(root, { style: astroTemplateWrapStyle });
|
|
90
|
+
expect(Object.keys(result.labels).sort()).toEqual(['dup.learn-more', 'dup.learn-more-2']);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('applyTemplateWrap — nuxt', () => {
|
|
94
|
+
it('wraps with mustache t() and injects into script setup', () => {
|
|
95
|
+
const root = project({
|
|
96
|
+
'pages/about.vue': `<script setup>
|
|
97
|
+
const links = [];
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<main>
|
|
102
|
+
<h1>About the crew</h1>
|
|
103
|
+
<p v-if="links.length > 0">We keep the harbor running</p>
|
|
104
|
+
</main>
|
|
105
|
+
</template>
|
|
106
|
+
`,
|
|
107
|
+
});
|
|
108
|
+
const result = applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
|
|
109
|
+
const code = readFileSync(join(root, 'pages/about.vue'), 'utf8');
|
|
110
|
+
expect(result.applied.length).toBe(2);
|
|
111
|
+
expect(code).toContain("import { useFimoT } from 'fimo/nuxt'");
|
|
112
|
+
expect(code.indexOf('useFimoT')).toBeLessThan(code.indexOf('const links'));
|
|
113
|
+
expect(code).toContain('<h1>{{ t("about.about-the-crew", "About the crew") }}</h1>');
|
|
114
|
+
// The quoted `>` inside v-if must not truncate the tag match.
|
|
115
|
+
expect(code).toContain('{{ t("about.we-keep-the-harbor", "We keep the harbor running") }}');
|
|
116
|
+
const rescan = scanWrappable(root);
|
|
117
|
+
expect(rescan.items.filter((item) => item.kind === 'text')).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
it('skips options-API files (no safe wiring point)', () => {
|
|
120
|
+
const source = `<script>
|
|
121
|
+
export default { name: 'Legacy' };
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<p>Legacy component copy</p>
|
|
126
|
+
</template>
|
|
127
|
+
`;
|
|
128
|
+
const root = project({ 'pages/legacy.vue': source });
|
|
129
|
+
const result = applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
|
|
130
|
+
expect(result.applied).toEqual([]);
|
|
131
|
+
expect(result.skipped.some((skip) => skip.reason.includes('no safe wiring point'))).toBe(true);
|
|
132
|
+
expect(readFileSync(join(root, 'pages/legacy.vue'), 'utf8')).toBe(source);
|
|
133
|
+
});
|
|
134
|
+
it('creates a script setup block when the file is template-only', () => {
|
|
135
|
+
const root = project({
|
|
136
|
+
'pages/bare.vue': `<template>
|
|
137
|
+
<p>Bare template copy</p>
|
|
138
|
+
</template>
|
|
139
|
+
`,
|
|
140
|
+
});
|
|
141
|
+
applyTemplateWrap(root, { style: nuxtTemplateWrapStyle });
|
|
142
|
+
const code = readFileSync(join(root, 'pages/bare.vue'), 'utf8');
|
|
143
|
+
expect(code.startsWith('<script setup>')).toBe(true);
|
|
144
|
+
expect(code).toContain('{{ t("bare.bare-template-copy", "Bare template copy") }}');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('applyTemplateWrap — sveltekit', () => {
|
|
148
|
+
it('wraps text, injects into the INSTANCE script, scopes by route dir, wires the layout', () => {
|
|
149
|
+
const root = project({
|
|
150
|
+
'src/routes/pricing/+page.svelte': `<script lang="ts" module>
|
|
151
|
+
export const prerender = true;
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<script lang="ts">
|
|
155
|
+
let count = 0;
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<main>
|
|
159
|
+
<h1>Simple pricing</h1>
|
|
160
|
+
</main>
|
|
161
|
+
`,
|
|
162
|
+
});
|
|
163
|
+
const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
|
|
164
|
+
const code = readFileSync(join(root, 'src/routes/pricing/+page.svelte'), 'utf8');
|
|
165
|
+
expect(code).toContain('{t("pricing.simple-pricing", "Simple pricing")}');
|
|
166
|
+
// Head goes in the instance script, never the module script.
|
|
167
|
+
const moduleBlock = code.slice(code.indexOf('<script lang="ts" module>'), code.indexOf('</script>'));
|
|
168
|
+
expect(moduleBlock).not.toContain('makeFimoT');
|
|
169
|
+
expect(code.indexOf('makeFimoT')).toBeGreaterThan(code.indexOf('<script lang="ts">'));
|
|
170
|
+
expect(code).toContain('const t = makeFimoT($page.data.fimoLabels);');
|
|
171
|
+
// One-time wiring: the root layout labels loader is written once.
|
|
172
|
+
expect(result.filesChanged).toContain('src/routes/+layout.server.js');
|
|
173
|
+
const layout = readFileSync(join(root, 'src/routes/+layout.server.js'), 'utf8');
|
|
174
|
+
expect(layout).toContain("loadFimoLabels } from 'fimo/svelte/server'");
|
|
175
|
+
expect(layout).toContain('fimoLabels: labels.dictionaries[labels.defaultLocale]');
|
|
176
|
+
});
|
|
177
|
+
it('never overwrites an existing +layout.server file', () => {
|
|
178
|
+
const existing = `export const load = () => ({ session: null });\n`;
|
|
179
|
+
const root = project({
|
|
180
|
+
'src/routes/+layout.server.ts': existing,
|
|
181
|
+
'src/routes/+page.svelte': `<h1>Front page headline</h1>\n`,
|
|
182
|
+
});
|
|
183
|
+
const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
|
|
184
|
+
expect(result.applied.length).toBe(1);
|
|
185
|
+
expect(result.labels['home.front-page-headline']).toBe('Front page headline');
|
|
186
|
+
expect(result.filesChanged).not.toContain('src/routes/+layout.server.js');
|
|
187
|
+
expect(existsSync(join(root, 'src/routes/+layout.server.js'))).toBe(false);
|
|
188
|
+
expect(readFileSync(join(root, 'src/routes/+layout.server.ts'), 'utf8')).toBe(existing);
|
|
189
|
+
});
|
|
190
|
+
it('scopes component files by their own name', () => {
|
|
191
|
+
const root = project({
|
|
192
|
+
'src/lib/Footer.svelte': `<footer><p>Made with care at the docks</p></footer>\n`,
|
|
193
|
+
'src/routes/+page.svelte': `<h1>Hello sailor</h1>\n`,
|
|
194
|
+
});
|
|
195
|
+
const result = applyTemplateWrap(root, { style: svelteTemplateWrapStyle });
|
|
196
|
+
expect(result.labels['footer.made-with-care-at']).toBe('Made with care at the docks');
|
|
197
|
+
expect(result.labels['home.hello-sailor']).toBe('Hello sailor');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type WrappableItem } from './scan-wrappable.js';
|
|
2
|
+
export interface AppliedTransform {
|
|
3
|
+
kind: 'text' | 'image' | 'file-link';
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
id: string;
|
|
7
|
+
preview: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ApplyWrapResult {
|
|
10
|
+
applied: AppliedTransform[];
|
|
11
|
+
skipped: Array<{
|
|
12
|
+
item: WrappableItem;
|
|
13
|
+
reason: string;
|
|
14
|
+
}>;
|
|
15
|
+
labels: Record<string, string>;
|
|
16
|
+
filesChanged: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function decodeJsxEntities(text: string): string;
|
|
19
|
+
/** Deterministic scope from the file path: app/about/page.tsx -> about. */
|
|
20
|
+
export declare function keyScope(file: string): string;
|
|
21
|
+
export declare function slugify(text: string): string;
|
|
22
|
+
export declare class KeyAllocator {
|
|
23
|
+
private readonly used;
|
|
24
|
+
constructor(existing: Iterable<string>);
|
|
25
|
+
allocate(scope: string, slug: string): string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Merge the fimo/next import with an existing one, or insert a new one AFTER
|
|
29
|
+
* the last import statement — positioned via the AST, never line matching
|
|
30
|
+
* (a multiline `import {\n …` opening line is not the end of the imports,
|
|
31
|
+
* as a real portfolio migration proved).
|
|
32
|
+
*/
|
|
33
|
+
export declare function ensureFimoImport(code: string, specifiers: string[], importModule?: string): string;
|
|
34
|
+
export declare function applyWrapToFile(code: string, items: WrappableItem[], allocator: KeyAllocator, importModule?: string): {
|
|
35
|
+
code: string;
|
|
36
|
+
applied: AppliedTransform[];
|
|
37
|
+
skipped: Array<{
|
|
38
|
+
item: WrappableItem;
|
|
39
|
+
reason: string;
|
|
40
|
+
}>;
|
|
41
|
+
labels: Record<string, string>;
|
|
42
|
+
};
|
|
43
|
+
export declare function applyWrap(rootDir: string, opts?: {
|
|
44
|
+
excludePrefixes?: string[];
|
|
45
|
+
importModule?: string;
|
|
46
|
+
}): ApplyWrapResult;
|