meno-core 1.0.52 → 1.0.53
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/build-astro.ts +183 -13
- package/build-next.ts +1361 -0
- package/build-static.ts +7 -5
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
- package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
- package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
- package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
- package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
- package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
- package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
- package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
- package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
- package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
- package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
- package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
- package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
- package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
- package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
- package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
- package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
- package/dist/chunks/chunk-X754AHS5.js.map +7 -0
- package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
- package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
- package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +354 -59
- package/dist/lib/client/index.js.map +4 -4
- package/dist/lib/server/index.js +1458 -190
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +202 -34
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/test-utils/index.js +1 -1
- package/entries/client-router.tsx +5 -165
- package/lib/client/ErrorBoundary.test.tsx +27 -25
- package/lib/client/ErrorBoundary.tsx +34 -19
- package/lib/client/core/ComponentBuilder.ts +19 -2
- package/lib/client/core/builders/embedBuilder.ts +8 -4
- package/lib/client/core/builders/listBuilder.ts +23 -4
- package/lib/client/fontFamiliesService.test.ts +76 -0
- package/lib/client/fontFamiliesService.ts +69 -0
- package/lib/client/hmrCssReload.ts +160 -0
- package/lib/client/hooks/useColorVariables.ts +2 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/meno-filter/ui.ts +2 -0
- package/lib/client/routing/RouteLoader.test.ts +2 -2
- package/lib/client/routing/RouteLoader.ts +8 -2
- package/lib/client/routing/Router.tsx +81 -15
- package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
- package/lib/client/scripts/ScriptExecutor.ts +56 -2
- package/lib/client/styles/StyleInjector.ts +20 -5
- package/lib/client/styles/UtilityClassCollector.ts +7 -1
- package/lib/client/styles/cspNonce.test.ts +67 -0
- package/lib/client/styles/cspNonce.ts +63 -0
- package/lib/client/templateEngine.test.ts +80 -0
- package/lib/client/templateEngine.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +35 -5
- package/lib/server/astro/componentEmitter.ts +61 -5
- package/lib/server/astro/nodeToAstro.ts +149 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
- package/lib/server/createServer.ts +11 -0
- package/lib/server/draftPageStore.ts +49 -0
- package/lib/server/fileWatcher.ts +62 -2
- package/lib/server/index.ts +13 -1
- package/lib/server/providers/fileSystemPageProvider.ts +8 -0
- package/lib/server/routes/api/components.ts +9 -4
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/pages.ts +14 -22
- package/lib/server/routes/api/shared.ts +56 -0
- package/lib/server/routes/index.ts +90 -0
- package/lib/server/routes/pages.ts +13 -6
- package/lib/server/services/componentService.test.ts +199 -2
- package/lib/server/services/componentService.ts +354 -49
- package/lib/server/services/fileWatcherService.ts +4 -24
- package/lib/server/services/pageService.test.ts +23 -0
- package/lib/server/services/pageService.ts +124 -6
- package/lib/server/ssr/attributeBuilder.ts +8 -2
- package/lib/server/ssr/buildErrorOverlay.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +21 -2
- package/lib/server/ssr/errorOverlay.ts +38 -11
- package/lib/server/ssr/htmlGenerator.test.ts +53 -13
- package/lib/server/ssr/htmlGenerator.ts +71 -27
- package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
- package/lib/server/ssr/ssrRenderer.test.ts +67 -0
- package/lib/server/ssr/ssrRenderer.ts +94 -9
- package/lib/server/websocketManager.ts +0 -1
- package/lib/shared/componentRefs.ts +45 -0
- package/lib/shared/constants.ts +8 -0
- package/lib/shared/cssGeneration.ts +2 -0
- package/lib/shared/cssProperties.ts +184 -0
- package/lib/shared/expressionEvaluator.ts +54 -0
- package/lib/shared/fontCss.ts +101 -0
- package/lib/shared/fontLoader.ts +8 -86
- package/lib/shared/friendlyError.test.ts +87 -0
- package/lib/shared/friendlyError.ts +121 -0
- package/lib/shared/hrefRefs.test.ts +130 -0
- package/lib/shared/hrefRefs.ts +100 -0
- package/lib/shared/index.ts +52 -0
- package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
- package/lib/shared/inlineSvgStyleRules.ts +134 -0
- package/lib/shared/interfaces/contentProvider.ts +13 -0
- package/lib/shared/itemTemplateUtils.test.ts +14 -0
- package/lib/shared/itemTemplateUtils.ts +4 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
- package/lib/shared/slugTranslator.test.ts +24 -0
- package/lib/shared/slugTranslator.ts +24 -0
- package/lib/shared/styleNodeUtils.ts +4 -1
- package/lib/shared/tree/PathBuilder.test.ts +128 -1
- package/lib/shared/tree/PathBuilder.ts +83 -31
- package/lib/shared/types/comment.ts +99 -0
- package/lib/shared/types/index.ts +12 -0
- package/lib/shared/types/rendering.ts +8 -0
- package/lib/shared/utilityClassConfig.ts +4 -2
- package/lib/shared/utilityClassMapper.test.ts +24 -0
- package/lib/shared/validation/commentValidators.ts +69 -0
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/viewportUnits.integration.test.ts +42 -0
- package/lib/shared/viewportUnits.test.ts +103 -0
- package/lib/shared/viewportUnits.ts +63 -0
- package/lib/test-utils/dom-setup.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
- package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
- package/dist/chunks/chunk-A725KYFK.js.map +0 -7
- package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
- package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
- package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
- package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
- package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
- package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
- package/dist/chunks/chunk-LPVETICS.js.map +0 -7
- /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
|
@@ -1,180 +1,21 @@
|
|
|
1
1
|
import { createElement as h } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { Router, type RouterProps } from "../lib/client/routing/Router";
|
|
4
|
+
import { setupCssHmrListeners } from "../lib/client/hmrCssReload";
|
|
4
5
|
import type { PrefetchConfig } from "../lib/shared/types/prefetch";
|
|
5
6
|
|
|
6
|
-
// Extend Window interface for
|
|
7
|
+
// Extend Window interface for config
|
|
7
8
|
declare global {
|
|
8
9
|
interface Window {
|
|
9
|
-
__hmrColorsInitialized?: boolean;
|
|
10
|
-
__hmrVariablesInitialized?: boolean;
|
|
11
10
|
__MENO_CONFIG__?: {
|
|
12
11
|
prefetch?: Partial<PrefetchConfig>;
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Mark as initialized
|
|
22
|
-
if (window.__hmrColorsInitialized) return;
|
|
23
|
-
window.__hmrColorsInitialized = true;
|
|
24
|
-
|
|
25
|
-
// Listen for color updates from HMR
|
|
26
|
-
document.addEventListener('hmr-colors-update', async () => {
|
|
27
|
-
await injectUpdatedThemeCSS();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Fetch and inject updated theme CSS
|
|
32
|
-
async function injectUpdatedThemeCSS() {
|
|
33
|
-
try {
|
|
34
|
-
// Fetch the theme config
|
|
35
|
-
const themesResponse = await fetch('/api/themes');
|
|
36
|
-
if (!themesResponse.ok) return;
|
|
37
|
-
|
|
38
|
-
const themesData = await themesResponse.json() as {
|
|
39
|
-
themes: Array<{ name: string; label: string }>;
|
|
40
|
-
default: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Fetch colors for each theme
|
|
44
|
-
const themeColors: Record<string, any> = {};
|
|
45
|
-
for (const theme of themesData.themes) {
|
|
46
|
-
const colorResponse = await fetch(
|
|
47
|
-
`/api/colors?theme=${encodeURIComponent(theme.name)}`
|
|
48
|
-
);
|
|
49
|
-
if (colorResponse.ok) {
|
|
50
|
-
themeColors[theme.name] = (await colorResponse.json()).colors;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Also get default theme colors
|
|
55
|
-
const defaultResponse = await fetch('/api/colors');
|
|
56
|
-
if (defaultResponse.ok) {
|
|
57
|
-
themeColors['default'] = (await defaultResponse.json()).colors;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Generate CSS
|
|
61
|
-
let css = '';
|
|
62
|
-
|
|
63
|
-
// Default theme in :root
|
|
64
|
-
if (themeColors['default']) {
|
|
65
|
-
const vars = Object.entries(themeColors['default'])
|
|
66
|
-
.map(([name, value]) => ` --${name}: ${value};`)
|
|
67
|
-
.join('\n');
|
|
68
|
-
css += `:root {\n${vars}\n}\n\n`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Each theme in [theme="..."]
|
|
72
|
-
for (const theme of themesData.themes) {
|
|
73
|
-
if (themeColors[theme.name]) {
|
|
74
|
-
const vars = Object.entries(themeColors[theme.name])
|
|
75
|
-
.map(([name, value]) => ` --${name}: ${value};`)
|
|
76
|
-
.join('\n');
|
|
77
|
-
css += `[theme="${theme.name}"] {\n${vars}\n}\n\n`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Find or create style tag
|
|
82
|
-
let styleTag = document.getElementById('hmr-theme-variables');
|
|
83
|
-
if (!styleTag) {
|
|
84
|
-
styleTag = document.createElement('style');
|
|
85
|
-
styleTag.id = 'hmr-theme-variables';
|
|
86
|
-
document.head.appendChild(styleTag);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
styleTag.textContent = css;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
// Silently fail - not critical if CSS injection doesn't work
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Setup HMR variables update listener immediately on app load
|
|
96
|
-
function setupVariablesHMR() {
|
|
97
|
-
if (typeof window === 'undefined') return;
|
|
98
|
-
|
|
99
|
-
if (window.__hmrVariablesInitialized) return;
|
|
100
|
-
window.__hmrVariablesInitialized = true;
|
|
101
|
-
|
|
102
|
-
document.addEventListener('hmr-variables-update', async () => {
|
|
103
|
-
await injectUpdatedVariablesCSS();
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Fetch and inject updated variables CSS
|
|
108
|
-
async function injectUpdatedVariablesCSS() {
|
|
109
|
-
try {
|
|
110
|
-
const response = await fetch('/api/variables-css');
|
|
111
|
-
if (!response.ok) return;
|
|
112
|
-
|
|
113
|
-
const css = await response.text();
|
|
114
|
-
|
|
115
|
-
let styleTag = document.getElementById('hmr-css-variables');
|
|
116
|
-
if (!styleTag) {
|
|
117
|
-
styleTag = document.createElement('style');
|
|
118
|
-
styleTag.id = 'hmr-css-variables';
|
|
119
|
-
document.head.appendChild(styleTag);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
styleTag.textContent = css;
|
|
123
|
-
} catch (error) {
|
|
124
|
-
// Silently fail - not critical if CSS injection doesn't work
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Setup HMR fonts update listener immediately on app load
|
|
129
|
-
function setupFontsHMR() {
|
|
130
|
-
if (typeof window === 'undefined') return;
|
|
131
|
-
|
|
132
|
-
if ((window as any).__hmrFontsCSSInitialized) return;
|
|
133
|
-
(window as any).__hmrFontsCSSInitialized = true;
|
|
134
|
-
|
|
135
|
-
document.addEventListener('hmr-fonts-update', async () => {
|
|
136
|
-
await injectUpdatedFontsCSS();
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Fetch and inject updated fonts CSS
|
|
141
|
-
async function injectUpdatedFontsCSS() {
|
|
142
|
-
try {
|
|
143
|
-
const response = await fetch('/api/fonts-css');
|
|
144
|
-
if (!response.ok) return;
|
|
145
|
-
|
|
146
|
-
const css = await response.text();
|
|
147
|
-
|
|
148
|
-
let styleTag = document.getElementById('hmr-fonts-css');
|
|
149
|
-
if (!styleTag) {
|
|
150
|
-
styleTag = document.createElement('style');
|
|
151
|
-
styleTag.id = 'hmr-fonts-css';
|
|
152
|
-
document.head.appendChild(styleTag);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
styleTag.textContent = css;
|
|
156
|
-
} catch (error) {
|
|
157
|
-
// Silently fail
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Setup HMR libraries update listener - triggers full page reload
|
|
162
|
-
function setupLibrariesHMR() {
|
|
163
|
-
if (typeof window === 'undefined') return;
|
|
164
|
-
|
|
165
|
-
if ((window as any).__hmrLibrariesInitialized) return;
|
|
166
|
-
(window as any).__hmrLibrariesInitialized = true;
|
|
167
|
-
|
|
168
|
-
document.addEventListener('hmr-libraries-update', () => {
|
|
169
|
-
location.reload();
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Initialize HMR listeners
|
|
174
|
-
setupColorsHMR();
|
|
175
|
-
setupVariablesHMR();
|
|
176
|
-
setupFontsHMR();
|
|
177
|
-
setupLibrariesHMR();
|
|
16
|
+
// Wire CSS/asset hot-reload listeners (colors, variables, fonts, libraries).
|
|
17
|
+
// Shared with the editor preview router — see lib/client/hmrCssReload.ts.
|
|
18
|
+
setupCssHmrListeners();
|
|
178
19
|
|
|
179
20
|
// Render app with HMR support and prefetching enabled
|
|
180
21
|
const rootElement = document.getElementById('root');
|
|
@@ -203,4 +44,3 @@ if (rootElement) {
|
|
|
203
44
|
if (import.meta.hot) {
|
|
204
45
|
import.meta.hot.accept();
|
|
205
46
|
}
|
|
206
|
-
|
|
@@ -64,10 +64,9 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
64
64
|
root.render(errorBoundary);
|
|
65
65
|
await wait(10); // Wait for React to process error
|
|
66
66
|
|
|
67
|
-
// Verify fallback UI is displayed with
|
|
68
|
-
expect(container.textContent).toContain("
|
|
67
|
+
// Verify friendly fallback UI is displayed with the component name
|
|
68
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
69
69
|
expect(container.textContent).toContain("TestComponent");
|
|
70
|
-
expect(container.textContent).toContain("Test render error");
|
|
71
70
|
} finally {
|
|
72
71
|
root.unmount();
|
|
73
72
|
cleanupContainer(container);
|
|
@@ -91,7 +90,7 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
91
90
|
// Should render the safe content
|
|
92
91
|
expect(container.textContent).toContain("Safe content");
|
|
93
92
|
// Should NOT show error UI
|
|
94
|
-
expect(container.textContent).not.toContain("
|
|
93
|
+
expect(container.textContent).not.toContain("This section ran into a problem");
|
|
95
94
|
} finally {
|
|
96
95
|
root.unmount();
|
|
97
96
|
cleanupContainer(container);
|
|
@@ -148,8 +147,9 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
148
147
|
root.render(errorBoundary);
|
|
149
148
|
await wait(10);
|
|
150
149
|
|
|
151
|
-
// Should show page-level error UI
|
|
152
|
-
expect(container.textContent).toContain("
|
|
150
|
+
// Should show friendly page-level error UI
|
|
151
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
152
|
+
// Raw message is preserved in the collapsible technical details.
|
|
153
153
|
expect(container.textContent).toContain("Page rendering failed");
|
|
154
154
|
} finally {
|
|
155
155
|
root.unmount();
|
|
@@ -171,7 +171,7 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
171
171
|
await wait(10);
|
|
172
172
|
|
|
173
173
|
expect(container.textContent).toContain("Page content");
|
|
174
|
-
expect(container.textContent).not.toContain("
|
|
174
|
+
expect(container.textContent).not.toContain("This section ran into a problem");
|
|
175
175
|
} finally {
|
|
176
176
|
root.unmount();
|
|
177
177
|
cleanupContainer(container);
|
|
@@ -200,7 +200,7 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
200
200
|
// Should show custom fallback
|
|
201
201
|
expect(container.textContent).toContain("Custom fallback: Custom error");
|
|
202
202
|
// Should NOT show default error UI
|
|
203
|
-
expect(container.textContent).not.toContain("
|
|
203
|
+
expect(container.textContent).not.toContain("This section ran into a problem");
|
|
204
204
|
} finally {
|
|
205
205
|
root.unmount();
|
|
206
206
|
cleanupContainer(container);
|
|
@@ -250,7 +250,8 @@ describe("ErrorBoundary - Error display", () => {
|
|
|
250
250
|
root.render(errorBoundary);
|
|
251
251
|
await wait(10);
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
// Component-level shows friendly copy; the raw message is dev-only.
|
|
254
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
254
255
|
} finally {
|
|
255
256
|
root.unmount();
|
|
256
257
|
cleanupContainer(container);
|
|
@@ -271,7 +272,7 @@ describe("ErrorBoundary - Error display", () => {
|
|
|
271
272
|
await wait(10);
|
|
272
273
|
|
|
273
274
|
// Should still show error UI even without message
|
|
274
|
-
expect(container.textContent).toContain("
|
|
275
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
275
276
|
} finally {
|
|
276
277
|
root.unmount();
|
|
277
278
|
cleanupContainer(container);
|
|
@@ -295,6 +296,7 @@ describe("ErrorBoundary - Error display", () => {
|
|
|
295
296
|
await wait(10);
|
|
296
297
|
|
|
297
298
|
expect(container.textContent).toContain("MyCustomComponent");
|
|
299
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
298
300
|
} finally {
|
|
299
301
|
root.unmount();
|
|
300
302
|
cleanupContainer(container);
|
|
@@ -314,7 +316,7 @@ describe("ErrorBoundary - Error display", () => {
|
|
|
314
316
|
root.render(errorBoundary);
|
|
315
317
|
await wait(10);
|
|
316
318
|
|
|
317
|
-
expect(container.textContent).toContain("
|
|
319
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
318
320
|
} finally {
|
|
319
321
|
root.unmount();
|
|
320
322
|
cleanupContainer(container);
|
|
@@ -336,10 +338,10 @@ describe("ErrorBoundary - Error display", () => {
|
|
|
336
338
|
root.render(errorBoundary);
|
|
337
339
|
await wait(10);
|
|
338
340
|
|
|
339
|
-
// Component-level shows
|
|
340
|
-
expect(container.textContent).toContain("
|
|
341
|
-
//
|
|
342
|
-
expect(container.textContent).not.toContain("Page
|
|
341
|
+
// Component-level shows the friendly title...
|
|
342
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
343
|
+
// ...but NOT the page-level "Reload Page" affordance.
|
|
344
|
+
expect(container.textContent).not.toContain("Reload Page");
|
|
343
345
|
} finally {
|
|
344
346
|
root.unmount();
|
|
345
347
|
cleanupContainer(container);
|
|
@@ -483,8 +485,7 @@ describe("ErrorBoundary - Error types", () => {
|
|
|
483
485
|
root.render(errorBoundary);
|
|
484
486
|
await wait(10);
|
|
485
487
|
|
|
486
|
-
expect(container.textContent).toContain("
|
|
487
|
-
expect(container.textContent).toContain("Component Error");
|
|
488
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
488
489
|
} finally {
|
|
489
490
|
root.unmount();
|
|
490
491
|
cleanupContainer(container);
|
|
@@ -504,7 +505,8 @@ describe("ErrorBoundary - Error types", () => {
|
|
|
504
505
|
root.render(errorBoundary);
|
|
505
506
|
await wait(10);
|
|
506
507
|
|
|
507
|
-
|
|
508
|
+
// Raw message is dev-only for component-level; friendly title always shows.
|
|
509
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
508
510
|
} finally {
|
|
509
511
|
root.unmount();
|
|
510
512
|
cleanupContainer(container);
|
|
@@ -524,7 +526,8 @@ describe("ErrorBoundary - Error types", () => {
|
|
|
524
526
|
root.render(errorBoundary);
|
|
525
527
|
await wait(10);
|
|
526
528
|
|
|
527
|
-
|
|
529
|
+
// Friendly, plain-language copy replaces the raw JS message for end users.
|
|
530
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
528
531
|
} finally {
|
|
529
532
|
root.unmount();
|
|
530
533
|
cleanupContainer(container);
|
|
@@ -556,7 +559,7 @@ describe("ErrorBoundary - Multiple instances", () => {
|
|
|
556
559
|
await wait(10);
|
|
557
560
|
|
|
558
561
|
// Both should be in the DOM
|
|
559
|
-
expect(container.textContent).toContain("
|
|
562
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
560
563
|
expect(container.textContent).toContain("Safe content");
|
|
561
564
|
} finally {
|
|
562
565
|
root.unmount();
|
|
@@ -582,11 +585,10 @@ describe("ErrorBoundary - Multiple instances", () => {
|
|
|
582
585
|
root.render(app);
|
|
583
586
|
await wait(10);
|
|
584
587
|
|
|
585
|
-
// Inner boundary
|
|
586
|
-
|
|
587
|
-
expect(container.textContent).toContain("
|
|
588
|
-
|
|
589
|
-
expect(container.textContent).not.toContain("Page Rendering Error");
|
|
588
|
+
// Inner (component-level) boundary catches it: friendly title shows,
|
|
589
|
+
// and the page-level "Reload Page" affordance does not.
|
|
590
|
+
expect(container.textContent).toContain("This section ran into a problem");
|
|
591
|
+
expect(container.textContent).not.toContain("Reload Page");
|
|
590
592
|
} finally {
|
|
591
593
|
root.unmount();
|
|
592
594
|
cleanupContainer(container);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Component, createElement as h, ReactNode } from 'react';
|
|
7
|
+
import { toFriendlyError } from '../shared/friendlyError';
|
|
7
8
|
|
|
8
9
|
interface ErrorBoundaryProps {
|
|
9
10
|
children?: ReactNode;
|
|
@@ -67,6 +68,12 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
67
68
|
return fallback(error, errorInfo);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Plain-language version of the raw error. Backticks (used for code spans
|
|
72
|
+
// in the overlay) are stripped for plain-text rendering here.
|
|
73
|
+
const friendly = toFriendlyError(error);
|
|
74
|
+
const friendlyMessage = friendly.friendlyMessage.replace(/`/g, '');
|
|
75
|
+
const friendlyHint = friendly.hint?.replace(/`/g, '');
|
|
76
|
+
|
|
70
77
|
// Default fallback UI
|
|
71
78
|
if (level === 'page') {
|
|
72
79
|
return h('div', {
|
|
@@ -92,7 +99,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
92
99
|
fontSize: '24px',
|
|
93
100
|
fontWeight: '600',
|
|
94
101
|
}
|
|
95
|
-
},
|
|
102
|
+
}, `⚠️ ${friendly.title}`),
|
|
96
103
|
h('p', {
|
|
97
104
|
style: {
|
|
98
105
|
margin: '0 0 16px 0',
|
|
@@ -100,21 +107,8 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
100
107
|
fontSize: '16px',
|
|
101
108
|
lineHeight: '1.5',
|
|
102
109
|
}
|
|
103
|
-
},
|
|
104
|
-
h('
|
|
105
|
-
style: {
|
|
106
|
-
background: '#fff',
|
|
107
|
-
border: '1px solid #ddd',
|
|
108
|
-
borderRadius: '4px',
|
|
109
|
-
padding: '16px',
|
|
110
|
-
marginBottom: '16px',
|
|
111
|
-
fontFamily: 'monospace',
|
|
112
|
-
fontSize: '14px',
|
|
113
|
-
color: '#c00',
|
|
114
|
-
overflowX: 'auto',
|
|
115
|
-
}
|
|
116
|
-
}, error.message),
|
|
117
|
-
process.env.NODE_ENV === 'development' && errorInfo?.componentStack && h('details', {
|
|
110
|
+
}, friendlyHint ? `${friendlyMessage} ${friendlyHint}` : friendlyMessage),
|
|
111
|
+
h('details', {
|
|
118
112
|
style: {
|
|
119
113
|
marginTop: '16px',
|
|
120
114
|
fontSize: '14px',
|
|
@@ -127,8 +121,21 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
127
121
|
fontWeight: '600',
|
|
128
122
|
marginBottom: '8px',
|
|
129
123
|
}
|
|
130
|
-
}, '
|
|
131
|
-
h('
|
|
124
|
+
}, 'Technical details'),
|
|
125
|
+
h('div', {
|
|
126
|
+
style: {
|
|
127
|
+
background: '#fff',
|
|
128
|
+
border: '1px solid #ddd',
|
|
129
|
+
borderRadius: '4px',
|
|
130
|
+
padding: '16px',
|
|
131
|
+
margin: '8px 0',
|
|
132
|
+
fontFamily: 'monospace',
|
|
133
|
+
fontSize: '14px',
|
|
134
|
+
color: '#c00',
|
|
135
|
+
overflowX: 'auto',
|
|
136
|
+
}
|
|
137
|
+
}, error.message),
|
|
138
|
+
process.env.NODE_ENV === 'development' && errorInfo?.componentStack && h('pre', {
|
|
132
139
|
style: {
|
|
133
140
|
background: '#f5f5f5',
|
|
134
141
|
border: '1px solid #ddd',
|
|
@@ -187,10 +194,18 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
187
194
|
color: '#c00',
|
|
188
195
|
fontSize: '14px',
|
|
189
196
|
}
|
|
190
|
-
},
|
|
197
|
+
}, `${friendly.title}${componentName ? ` (${componentName})` : ''}`)
|
|
191
198
|
),
|
|
192
199
|
h('div', {
|
|
193
200
|
style: {
|
|
201
|
+
fontSize: '13px',
|
|
202
|
+
color: '#666',
|
|
203
|
+
lineHeight: '1.5',
|
|
204
|
+
}
|
|
205
|
+
}, friendlyHint ? `${friendlyMessage} ${friendlyHint}` : friendlyMessage),
|
|
206
|
+
process.env.NODE_ENV === 'development' && h('div', {
|
|
207
|
+
style: {
|
|
208
|
+
marginTop: '8px',
|
|
194
209
|
fontSize: '12px',
|
|
195
210
|
color: '#666',
|
|
196
211
|
fontFamily: 'monospace',
|
|
@@ -318,6 +318,14 @@ export class ComponentBuilder {
|
|
|
318
318
|
|
|
319
319
|
if (!node) return null;
|
|
320
320
|
|
|
321
|
+
// Verbatim-code markers ({ _code, expr }) come from the meno-astro dialect for
|
|
322
|
+
// arbitrary JS the model can't represent as a binding. They render natively in the
|
|
323
|
+
// Astro build; the editor preview has nothing to evaluate, so render nothing (the
|
|
324
|
+
// marker must never reach the element dispatch below, which would treat it as a tag).
|
|
325
|
+
if (typeof node === 'object' && (node as { _code?: unknown })._code === true) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
321
329
|
// Resolve `_i18n` value objects to a single string before node-shape
|
|
322
330
|
// dispatch. Authors can write a localized string anywhere `children` is
|
|
323
331
|
// accepted — on raw `type: "node"` elements as well as component props.
|
|
@@ -824,7 +832,12 @@ export class ComponentBuilder {
|
|
|
824
832
|
key = 0, elementPath = [0], parentComponentName = null, viewportWidth = 1920,
|
|
825
833
|
componentContext = null, locale, i18nConfig, cmsContext = null, cmsLocale = null,
|
|
826
834
|
collectionItemsMap = {}, itemContext = null, cmsItemIndexPath = null,
|
|
827
|
-
cmsListPaths = null, templateContext = null
|
|
835
|
+
cmsListPaths = null, templateContext = null,
|
|
836
|
+
// The host (outer) component's resolved props. Forwarded to
|
|
837
|
+
// `processStructure` as `parentProps` so a `{{x}}` in this
|
|
838
|
+
// component's structure falls back to the host's `x` when this
|
|
839
|
+
// component's interface doesn't declare it.
|
|
840
|
+
componentResolvedProps: parentResolvedProps = null
|
|
828
841
|
} = options;
|
|
829
842
|
|
|
830
843
|
const componentDef = this.componentRegistry.get(componentName);
|
|
@@ -870,7 +883,11 @@ export class ComponentBuilder {
|
|
|
870
883
|
const markedChildren = typedChildren ? markAsSlotContent(typedChildren) : undefined;
|
|
871
884
|
const processedStructure = processStructure(
|
|
872
885
|
structuredComponentDef.structure,
|
|
873
|
-
{
|
|
886
|
+
{
|
|
887
|
+
props: resolvedProps,
|
|
888
|
+
componentDef: structuredComponentDef,
|
|
889
|
+
parentProps: parentResolvedProps ?? undefined,
|
|
890
|
+
},
|
|
874
891
|
viewportWidth,
|
|
875
892
|
markedChildren
|
|
876
893
|
);
|
|
@@ -15,6 +15,7 @@ import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
|
15
15
|
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
16
16
|
import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
|
|
17
17
|
import DOMPurify from "isomorphic-dompurify";
|
|
18
|
+
import { inlineSvgStyleRules } from "../../../shared/inlineSvgStyleRules";
|
|
18
19
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
19
20
|
import type { BuilderContext } from "./types";
|
|
20
21
|
import { hasItemTemplates, processItemTemplate, processItemPropsTemplate, type ValueResolver } from "../../../shared/itemTemplateUtils";
|
|
@@ -39,8 +40,8 @@ export interface EmbedBuilderDeps {
|
|
|
39
40
|
* Script tags and event handlers are still removed for security
|
|
40
41
|
*/
|
|
41
42
|
const SANITIZE_CONFIG = {
|
|
42
|
-
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'style', 'animate', 'animateTransform', 'animateMotion', 'set'],
|
|
43
|
-
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity', 'frameborder', 'allowfullscreen', 'allow', 'title', 'attributeName', 'values', 'dur', 'begin', 'end', 'repeatCount', 'repeatDur', 'keyTimes', 'keySplines', 'calcMode', 'from', 'to', 'by', 'additive', 'accumulate', 'type', 'rotate', 'keyPoints', 'path'],
|
|
43
|
+
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'style', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter', 'feGaussianBlur', 'feOffset', 'feMerge', 'feMergeNode', 'feColorMatrix', 'feComposite', 'feFlood', 'feMorphology', 'feBlend', 'feDropShadow', 'feTurbulence', 'feDisplacementMap', 'foreignObject'],
|
|
44
|
+
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'xmlns:xlink', 'xlink:href', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'fill-rule', 'clip-rule', 'clip-path', 'clipPathUnits', 'mask', 'mask-type', 'maskUnits', 'maskContentUnits', 'patternUnits', 'patternContentUnits', 'patternTransform', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'preserveAspectRatio', 'marker-start', 'marker-mid', 'marker-end', 'markerUnits', 'markerWidth', 'markerHeight', 'refX', 'refY', 'orient', 'paint-order', 'vector-effect', 'filter', 'filterUnits', 'primitiveUnits', 'in', 'in2', 'result', 'stdDeviation', 'flood-color', 'flood-opacity', 'stroke-opacity', 'font-size', 'font-family', 'font-weight', 'font-style', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity', 'frameborder', 'allowfullscreen', 'allow', 'title', 'attributeName', 'values', 'dur', 'begin', 'end', 'repeatCount', 'repeatDur', 'keyTimes', 'keySplines', 'calcMode', 'from', 'to', 'by', 'additive', 'accumulate', 'type', 'rotate', 'keyPoints', 'path'],
|
|
44
45
|
KEEP_CONTENT: true
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -75,8 +76,11 @@ export function buildEmbed(
|
|
|
75
76
|
htmlContent = processCMSTemplate(htmlContent, ctx.cmsContext, effectiveLocale);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
// Sanitize HTML with allowlist
|
|
79
|
-
|
|
79
|
+
// Sanitize HTML with allowlist, then inline simple SVG <style> rules so
|
|
80
|
+
// class-scoped declarations survive `innerHTML` reparses (editor preview's
|
|
81
|
+
// HMR `smartUpdate` fallback in `ssr/htmlGenerator.ts:574` strips CSSOM
|
|
82
|
+
// registration for SVG <style> elements re-inserted that way).
|
|
83
|
+
const sanitizedHtml = inlineSvgStyleRules(DOMPurify.sanitize(htmlContent, SANITIZE_CONFIG));
|
|
80
84
|
const effectiveParentComponentName = deps.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
81
85
|
|
|
82
86
|
// Extract attributes from node
|
|
@@ -80,6 +80,20 @@ export function buildList(
|
|
|
80
80
|
containerProps['data-source'] = source || (sourceIsResolved ? 'resolved' : '');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// When this list is itself rendered inside an outer list iteration, stamp
|
|
84
|
+
// the outer iteration context onto its container so the click handler and
|
|
85
|
+
// selection overlay can disambiguate the N sibling duplicates (one per
|
|
86
|
+
// outer item). Mirrors ComponentBuilder's write so both stay consistent.
|
|
87
|
+
if (cmsItemIndexPath && cmsItemIndexPath.length > 0) {
|
|
88
|
+
containerProps['data-cms-item-index'] = cmsItemIndexPath.join('.');
|
|
89
|
+
if (cmsListPaths && cmsListPaths.length === cmsItemIndexPath.length) {
|
|
90
|
+
containerProps['data-cms-context'] = JSON.stringify({
|
|
91
|
+
itemIndexPath: cmsItemIndexPath,
|
|
92
|
+
listPaths: cmsListPaths,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
// Add extracted attributes
|
|
84
98
|
if (Object.keys(extractedAttributes).length > 0) {
|
|
85
99
|
Object.assign(containerProps, extractedAttributes);
|
|
@@ -209,20 +223,25 @@ export function buildList(
|
|
|
209
223
|
componentResolvedProps
|
|
210
224
|
});
|
|
211
225
|
|
|
212
|
-
// Add children directly (no wrapper div)
|
|
226
|
+
// Add children directly (no wrapper div).
|
|
227
|
+
// Use a stable identifier (CMS `_id` or `_filename`) as the React key
|
|
228
|
+
// instead of the loop index, so reordering / filtering moves DOM nodes
|
|
229
|
+
// rather than unmounting & re-mounting every row. Falls back to index
|
|
230
|
+
// for prop-mode lists whose items lack those fields.
|
|
231
|
+
const cmsItem = item as Partial<CMSItem> | undefined;
|
|
232
|
+
const itemKey = cmsItem?._id ?? cmsItem?._filename ?? index;
|
|
213
233
|
if (itemChildren === null) continue;
|
|
214
234
|
if (Array.isArray(itemChildren)) {
|
|
215
|
-
// Add unique keys by combining original key with item index
|
|
216
235
|
for (const child of itemChildren) {
|
|
217
236
|
if (typeof child === 'object' && child !== null && 'key' in child) {
|
|
218
|
-
renderedItems.push({ ...child, key: `${child.key}-item-${
|
|
237
|
+
renderedItems.push({ ...child, key: `${child.key}-item-${itemKey}` } as ReactElement);
|
|
219
238
|
} else {
|
|
220
239
|
renderedItems.push(child);
|
|
221
240
|
}
|
|
222
241
|
}
|
|
223
242
|
} else if (typeof itemChildren === 'object' && itemChildren !== null && 'key' in itemChildren) {
|
|
224
243
|
// Single child - ensure unique key
|
|
225
|
-
renderedItems.push({ ...itemChildren, key: `${(itemChildren as ReactElement).key}-item-${
|
|
244
|
+
renderedItems.push({ ...itemChildren, key: `${(itemChildren as ReactElement).key}-item-${itemKey}` } as ReactElement);
|
|
226
245
|
} else {
|
|
227
246
|
renderedItems.push(itemChildren);
|
|
228
247
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
fetchFontFamilies,
|
|
4
|
+
getCachedFontFamilies,
|
|
5
|
+
invalidateFontFamilies,
|
|
6
|
+
} from './fontFamiliesService';
|
|
7
|
+
|
|
8
|
+
const realFetch = globalThis.fetch;
|
|
9
|
+
|
|
10
|
+
function mockFetchOnce(response: any) {
|
|
11
|
+
globalThis.fetch = mock(async () => ({
|
|
12
|
+
ok: true,
|
|
13
|
+
json: async () => response,
|
|
14
|
+
})) as any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('fontFamiliesService', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
invalidateFontFamilies();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
globalThis.fetch = realFetch;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('getCachedFontFamilies returns empty array before fetch', () => {
|
|
27
|
+
expect(getCachedFontFamilies()).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('fetchFontFamilies extracts explicit family names', async () => {
|
|
31
|
+
mockFetchOnce({ fonts: [{ family: 'Inter' }, { family: 'Geomanist' }] });
|
|
32
|
+
const families = await fetchFontFamilies();
|
|
33
|
+
expect(families).toEqual(['Inter', 'Geomanist']);
|
|
34
|
+
expect(getCachedFontFamilies()).toEqual(['Inter', 'Geomanist']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('fetchFontFamilies dedupes families that repeat across weights/unicode-ranges', async () => {
|
|
38
|
+
mockFetchOnce({
|
|
39
|
+
fonts: [
|
|
40
|
+
{ family: 'Fraunces', weight: 400 },
|
|
41
|
+
{ family: 'Fraunces', weight: 700 },
|
|
42
|
+
{ family: 'Inter' },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
const families = await fetchFontFamilies();
|
|
46
|
+
expect(families).toEqual(['Fraunces', 'Inter']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('fetchFontFamilies derives a family name from path when family is missing', async () => {
|
|
50
|
+
mockFetchOnce({
|
|
51
|
+
fonts: [{ path: '/fonts/roboto-mono-regular.ttf' }],
|
|
52
|
+
});
|
|
53
|
+
const families = await fetchFontFamilies();
|
|
54
|
+
expect(families).toEqual(['Roboto Mono Regular']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('fetchFontFamilies handles empty fonts array', async () => {
|
|
58
|
+
mockFetchOnce({ fonts: [] });
|
|
59
|
+
const families = await fetchFontFamilies();
|
|
60
|
+
expect(families).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('fetchFontFamilies handles config without fonts key', async () => {
|
|
64
|
+
mockFetchOnce({});
|
|
65
|
+
const families = await fetchFontFamilies();
|
|
66
|
+
expect(families).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('invalidateFontFamilies clears the cache', async () => {
|
|
70
|
+
mockFetchOnce({ fonts: [{ family: 'Inter' }] });
|
|
71
|
+
await fetchFontFamilies();
|
|
72
|
+
expect(getCachedFontFamilies()).toEqual(['Inter']);
|
|
73
|
+
invalidateFontFamilies();
|
|
74
|
+
expect(getCachedFontFamilies()).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
});
|