meno-core 1.0.24 → 1.0.26
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-static.ts +9 -5
- package/entries/client-router.tsx +83 -1
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/builders/embedBuilder.ts +2 -1
- package/lib/client/core/builders/listBuilder.ts +1 -1
- package/lib/client/hmr/HMRManager.tsx +12 -17
- package/lib/client/hooks/useVariables.ts +101 -0
- package/lib/client/index.ts +1 -0
- package/lib/client/meno-filter/bindings.ts +1 -0
- package/lib/client/routing/RouteLoader.test.ts +1 -1
- package/lib/client/routing/RouteLoader.ts +1 -1
- package/lib/client/scripts/ScriptExecutor.ts +13 -4
- package/lib/client/services/PrefetchService.ts +1 -1
- package/lib/client/styles/UtilityClassCollector.ts +109 -13
- package/lib/client/templateEngine.ts +61 -3
- package/lib/server/__integration__/cms-integration.test.ts +2 -2
- package/lib/server/cssGenerator.ts +91 -3
- package/lib/server/fileWatcher.ts +81 -3
- package/lib/server/index.ts +8 -3
- package/lib/server/migrateTemplates.ts +22 -0
- package/lib/server/projectContext.ts +3 -1
- package/lib/server/providers/fileSystemCMSProvider.test.ts +6 -7
- package/lib/server/providers/fileSystemCMSProvider.ts +6 -11
- package/lib/server/providers/fileSystemPageProvider.ts +86 -40
- package/lib/server/routes/api/colors.test.ts +103 -0
- package/lib/server/routes/api/colors.ts +10 -42
- package/lib/server/routes/api/core-routes.ts +42 -2
- package/lib/server/routes/api/enums.test.ts +53 -0
- package/lib/server/routes/api/enums.ts +16 -0
- package/lib/server/routes/api/pages.ts +3 -2
- package/lib/server/routes/api/variables.test.ts +74 -0
- package/lib/server/routes/api/variables.ts +32 -0
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +4 -4
- package/lib/server/services/CachedConfigLoader.ts +55 -0
- package/lib/server/services/ColorService.test.ts +216 -0
- package/lib/server/services/ColorService.ts +11 -30
- package/lib/server/services/EnumService.test.ts +192 -0
- package/lib/server/services/EnumService.ts +118 -0
- package/lib/server/services/VariableService.test.ts +183 -0
- package/lib/server/services/VariableService.ts +100 -0
- package/lib/server/services/cmsService.test.ts +11 -11
- package/lib/server/services/configService.test.ts +1 -95
- package/lib/server/services/configService.ts +0 -27
- package/lib/server/services/fileWatcherService.ts +18 -2
- package/lib/server/services/pageService.ts +140 -2
- package/lib/server/ssr/htmlGenerator.ts +35 -7
- package/lib/server/ssr/imageMetadata.ts +5 -1
- package/lib/server/ssr/jsCollector.ts +3 -2
- package/lib/server/ssr/ssrRenderer.test.ts +129 -121
- package/lib/server/ssr/ssrRenderer.ts +131 -271
- package/lib/server/websocketManager.ts +36 -0
- package/lib/shared/colorConversions.test.ts +140 -0
- package/lib/shared/colorConversions.ts +181 -0
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +26 -1
- package/lib/shared/cssGeneration.ts +56 -4
- package/lib/shared/cssProperties.ts +22 -0
- package/lib/shared/fontLoader.ts +4 -0
- package/lib/shared/gradientUtils.test.ts +183 -0
- package/lib/shared/gradientUtils.ts +184 -0
- package/lib/shared/index.ts +6 -0
- package/lib/shared/libraryLoader.ts +42 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +6 -4
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +4 -32
- package/lib/shared/responsiveScaling.ts +3 -3
- package/lib/shared/treePathUtils.ts +8 -0
- package/lib/shared/types/api.ts +1 -1
- package/lib/shared/types/colors.ts +10 -0
- package/lib/shared/types/components.ts +4 -0
- package/lib/shared/types/index.ts +21 -0
- package/lib/shared/types/styles.ts +10 -0
- package/lib/shared/types/variables.test.ts +132 -0
- package/lib/shared/types/variables.ts +215 -0
- package/lib/shared/utilityClassConfig.ts +4 -0
- package/lib/shared/validation/propValidator.ts +3 -2
- package/lib/shared/validation/schemas.ts +53 -47
- package/package.json +1 -1
- package/templates/index-router.html +1 -1
package/build-static.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { SlugMap } from "./lib/shared/slugTranslator";
|
|
|
30
30
|
import { buildItemUrl } from "./lib/shared/itemTemplateUtils";
|
|
31
31
|
import { generateMiddleware, generateTrackFunction, generateResultsFunction } from "./lib/server/ab/generateFunctions";
|
|
32
32
|
import { generateTrackingScript } from "./lib/server/ab/trackingScript";
|
|
33
|
+
import { migrateTemplatesDirectory } from "./lib/server/migrateTemplates";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Collect build errors for error overlay
|
|
@@ -387,7 +388,7 @@ async function generateStaticDataFiles(
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
/**
|
|
390
|
-
* Build CMS templates from
|
|
391
|
+
* Build CMS templates from root templates/ directory
|
|
391
392
|
*/
|
|
392
393
|
async function buildCMSTemplates(
|
|
393
394
|
templatesDir: string,
|
|
@@ -566,7 +567,7 @@ async function buildCMSTemplates(
|
|
|
566
567
|
|
|
567
568
|
console.error(`❌ Error processing ${file}:`, error);
|
|
568
569
|
buildErrors.push({
|
|
569
|
-
file: `
|
|
570
|
+
file: `templates/${file}`,
|
|
570
571
|
message: errorMessage,
|
|
571
572
|
type: errorMessage.includes('minification') || errorMessage.includes('minify') ? 'minify' : 'cms',
|
|
572
573
|
});
|
|
@@ -664,6 +665,9 @@ export async function buildStaticPages(): Promise<void> {
|
|
|
664
665
|
const i18nConfig = await loadI18nConfig();
|
|
665
666
|
console.log(`🌐 Locales: ${i18nConfig.locales.map(l => l.code).join(", ")} (default: ${i18nConfig.defaultLocale})\n`);
|
|
666
667
|
|
|
668
|
+
// Auto-migrate pages/templates/ → templates/ if needed
|
|
669
|
+
await migrateTemplatesDirectory();
|
|
670
|
+
|
|
667
671
|
// Clean dist directory (removes editor files, old HTML)
|
|
668
672
|
cleanDist();
|
|
669
673
|
|
|
@@ -770,7 +774,7 @@ export async function buildStaticPages(): Promise<void> {
|
|
|
770
774
|
console.log(`✅ Loaded ${components.size} global component(s)\n`);
|
|
771
775
|
|
|
772
776
|
// Initialize CMS service for CMSList rendering
|
|
773
|
-
const cmsProvider = new FileSystemCMSProvider(projectPaths.
|
|
777
|
+
const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
|
|
774
778
|
const cmsService = new CMSService(cmsProvider);
|
|
775
779
|
await cmsService.initialize();
|
|
776
780
|
console.log(`✅ CMS service initialized\n`);
|
|
@@ -947,8 +951,8 @@ export async function buildStaticPages(): Promise<void> {
|
|
|
947
951
|
}
|
|
948
952
|
}
|
|
949
953
|
|
|
950
|
-
// Build CMS templates from
|
|
951
|
-
const templatesDir =
|
|
954
|
+
// Build CMS templates from root templates/ directory
|
|
955
|
+
const templatesDir = projectPaths.templates();
|
|
952
956
|
const staticCollections = new Map<string, ClientDataCollection>();
|
|
953
957
|
const cmsResult = await buildCMSTemplates(
|
|
954
958
|
templatesDir,
|
|
@@ -7,6 +7,7 @@ import type { PrefetchConfig } from "../lib/shared/types/prefetch";
|
|
|
7
7
|
declare global {
|
|
8
8
|
interface Window {
|
|
9
9
|
__hmrColorsInitialized?: boolean;
|
|
10
|
+
__hmrVariablesInitialized?: boolean;
|
|
10
11
|
__MENO_CONFIG__?: {
|
|
11
12
|
prefetch?: Partial<PrefetchConfig>;
|
|
12
13
|
};
|
|
@@ -91,8 +92,89 @@ async function injectUpdatedThemeCSS() {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
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
|
|
95
174
|
setupColorsHMR();
|
|
175
|
+
setupVariablesHMR();
|
|
176
|
+
setupFontsHMR();
|
|
177
|
+
setupLibrariesHMR();
|
|
96
178
|
|
|
97
179
|
// Render app with HMR support and prefetching enabled
|
|
98
180
|
const rootElement = document.getElementById('root');
|
|
@@ -14,16 +14,20 @@ import { FileSystemPageProvider } from '../lib/server/providers/fileSystemPagePr
|
|
|
14
14
|
import { FileSystemCMSProvider } from '../lib/server/providers/fileSystemCMSProvider';
|
|
15
15
|
import { configService } from '../lib/server/services/configService';
|
|
16
16
|
import { projectPaths } from '../lib/server/projectContext';
|
|
17
|
+
import { migrateTemplatesDirectory } from '../lib/server/migrateTemplates';
|
|
18
|
+
|
|
19
|
+
// Auto-migrate pages/templates/ → templates/ if needed
|
|
20
|
+
await migrateTemplatesDirectory();
|
|
17
21
|
|
|
18
22
|
// Initialize services
|
|
19
23
|
const pageCache = new PageCache();
|
|
20
|
-
const pageProvider = new FileSystemPageProvider(projectPaths.pages());
|
|
24
|
+
const pageProvider = new FileSystemPageProvider(projectPaths.pages(), projectPaths.templates());
|
|
21
25
|
const pageService = new PageService(pageCache, pageProvider);
|
|
22
26
|
const componentService = new ComponentService();
|
|
23
27
|
const wsManager = new WebSocketManager();
|
|
24
28
|
|
|
25
29
|
// Initialize CMS services
|
|
26
|
-
const cmsProvider = new FileSystemCMSProvider(projectPaths.
|
|
30
|
+
const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
|
|
27
31
|
const cmsService = new CMSService(cmsProvider);
|
|
28
32
|
|
|
29
33
|
// Initialize file watcher with CMS service for template change detection
|
|
@@ -54,7 +54,8 @@ export function buildEmbed(
|
|
|
54
54
|
const { key, elementPath, parentComponentName, componentContext, componentRootPath, cmsItemIndexPath } = ctx;
|
|
55
55
|
|
|
56
56
|
// Process templates in html property before sanitization (matching SSR behavior)
|
|
57
|
-
|
|
57
|
+
// Mappings should already be resolved by processStructure, this is just a type safety guard
|
|
58
|
+
let htmlContent = typeof node.html === 'string' ? node.html : '';
|
|
58
59
|
|
|
59
60
|
// Process item template strings (for List context) - e.g., {{item.icon}}, {{feature.title}}
|
|
60
61
|
if (ctx.templateContext && hasItemTemplates(htmlContent)) {
|
|
@@ -224,7 +224,7 @@ export function buildList(
|
|
|
224
224
|
const label = isCollectionMode ? 'CMS List' : 'List';
|
|
225
225
|
|
|
226
226
|
// Use configurable tag (defaults to 'div') - null means fragment mode (no wrapper)
|
|
227
|
-
const tag = node.tag ===
|
|
227
|
+
const tag = typeof node.tag === 'string' ? node.tag : null;
|
|
228
228
|
|
|
229
229
|
if (!source && !sourceIsResolved) {
|
|
230
230
|
// No source - render empty container with placeholder
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages Hot Module Replacement WebSocket connection, status tracking, and visual indicators.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement as h, useState, useEffect, useRef
|
|
6
|
+
import { createElement as h, useState, useEffect, useRef } from "react";
|
|
7
7
|
import type { ReactElement } from "react";
|
|
8
8
|
import { HMRWebSocket } from "../hmrWebSocket";
|
|
9
9
|
|
|
@@ -65,17 +65,6 @@ export function HMRManager({
|
|
|
65
65
|
|
|
66
66
|
useEffect(() => { onReloadRef.current = onReload; }, [onReload]);
|
|
67
67
|
|
|
68
|
-
// Helper function to show HMR indicator
|
|
69
|
-
const showHMRIndicator = useCallback(() => {
|
|
70
|
-
const indicator = document.getElementById('hmr-indicator');
|
|
71
|
-
if (indicator) {
|
|
72
|
-
indicator.style.display = 'block';
|
|
73
|
-
setTimeout(() => {
|
|
74
|
-
indicator.style.display = 'none';
|
|
75
|
-
}, 2000);
|
|
76
|
-
}
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
68
|
// Define message handler - reads from refs to always get latest values
|
|
80
69
|
const createMessageHandler = () => (data: any) => {
|
|
81
70
|
if (data.type === 'hmr:update') {
|
|
@@ -97,8 +86,15 @@ export function HMRManager({
|
|
|
97
86
|
} else if (data.type === 'hmr:colors-update') {
|
|
98
87
|
// Dispatch custom event to notify color hooks of the update
|
|
99
88
|
document.dispatchEvent(new CustomEvent('hmr-colors-update'));
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
} else if (data.type === 'hmr:variables-update') {
|
|
90
|
+
// Dispatch custom event to notify variable hooks of the update
|
|
91
|
+
document.dispatchEvent(new CustomEvent('hmr-variables-update'));
|
|
92
|
+
} else if (data.type === 'hmr:fonts-update') {
|
|
93
|
+
// Dispatch custom event to notify font CSS injection
|
|
94
|
+
document.dispatchEvent(new CustomEvent('hmr-fonts-update'));
|
|
95
|
+
} else if (data.type === 'hmr:libraries-update') {
|
|
96
|
+
// Dispatch custom event to trigger full page reload for library changes
|
|
97
|
+
document.dispatchEvent(new CustomEvent('hmr-libraries-update'));
|
|
102
98
|
}
|
|
103
99
|
};
|
|
104
100
|
|
|
@@ -160,14 +156,13 @@ export function HMRManager({
|
|
|
160
156
|
});
|
|
161
157
|
|
|
162
158
|
import.meta.hot.on('bun:afterUpdate', () => {
|
|
163
|
-
//
|
|
164
|
-
showHMRIndicator();
|
|
159
|
+
// HMR update complete
|
|
165
160
|
});
|
|
166
161
|
|
|
167
162
|
import.meta.hot.on('bun:error', () => {
|
|
168
163
|
});
|
|
169
164
|
}
|
|
170
|
-
}, [
|
|
165
|
+
}, []);
|
|
171
166
|
|
|
172
167
|
// Render indicators
|
|
173
168
|
return h(HMRIndicator, { status });
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for fetching and caching CSS variables (variables.json)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useEffect, useRef } from 'react';
|
|
6
|
+
import type { CSSVariable } from '../../shared/types/variables';
|
|
7
|
+
|
|
8
|
+
let cachedVariables: CSSVariable[] | null = null;
|
|
9
|
+
let hmrCallbacks: Set<() => void> = new Set();
|
|
10
|
+
|
|
11
|
+
// Setup HMR listener immediately when module loads
|
|
12
|
+
function initializeHMRListener() {
|
|
13
|
+
if (typeof window === 'undefined') return;
|
|
14
|
+
|
|
15
|
+
// Only setup once
|
|
16
|
+
if ((window as any).__hmrVariablesInitialized) return;
|
|
17
|
+
(window as any).__hmrVariablesInitialized = true;
|
|
18
|
+
|
|
19
|
+
// Listen for custom HMR events from HMRManager
|
|
20
|
+
document.addEventListener('hmr-variables-update', () => {
|
|
21
|
+
// Clear cache
|
|
22
|
+
cachedVariables = null;
|
|
23
|
+
|
|
24
|
+
// Notify all listeners to refresh their data
|
|
25
|
+
hmrCallbacks.forEach(callback => callback());
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize immediately
|
|
30
|
+
if (typeof window !== 'undefined') {
|
|
31
|
+
initializeHMRListener();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useVariables() {
|
|
35
|
+
const [variables, setVariables] = useState<CSSVariable[] | null>(cachedVariables);
|
|
36
|
+
const [loading, setLoading] = useState(!cachedVariables);
|
|
37
|
+
const [error, setError] = useState<Error | null>(null);
|
|
38
|
+
const callbackRef = useRef<(() => void) | null>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// Create a callback to refresh variables when HMR update is received
|
|
42
|
+
const refreshCallback = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch('/api/variables-status');
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error('Failed to fetch variables');
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json() as { status: string; config: { variables: CSSVariable[] } };
|
|
49
|
+
cachedVariables = data.config.variables;
|
|
50
|
+
setVariables(cachedVariables);
|
|
51
|
+
setError(null);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Register callback for HMR updates
|
|
58
|
+
callbackRef.current = refreshCallback;
|
|
59
|
+
hmrCallbacks.add(refreshCallback);
|
|
60
|
+
|
|
61
|
+
// Return cached variables immediately
|
|
62
|
+
if (cachedVariables) {
|
|
63
|
+
setVariables(cachedVariables);
|
|
64
|
+
setLoading(false);
|
|
65
|
+
return () => {
|
|
66
|
+
if (callbackRef.current) {
|
|
67
|
+
hmrCallbacks.delete(callbackRef.current);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fetch variables
|
|
73
|
+
const fetchVariables = async () => {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch('/api/variables-status');
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error('Failed to fetch variables');
|
|
78
|
+
}
|
|
79
|
+
const data = await response.json() as { status: string; config: { variables: CSSVariable[] } };
|
|
80
|
+
cachedVariables = data.config.variables;
|
|
81
|
+
setVariables(cachedVariables);
|
|
82
|
+
setError(null);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
85
|
+
setVariables(null);
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
fetchVariables();
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
if (callbackRef.current) {
|
|
95
|
+
hmrCallbacks.delete(callbackRef.current);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return { variables, loading, error };
|
|
101
|
+
}
|
package/lib/client/index.ts
CHANGED
|
@@ -584,7 +584,7 @@ describe('RouteLoader', () => {
|
|
|
584
584
|
config.prefetchService = mockPrefetchService as any;
|
|
585
585
|
routeLoader = new RouteLoader(config);
|
|
586
586
|
|
|
587
|
-
// Mock fetch - should NOT be called for /api/
|
|
587
|
+
// Mock fetch - should NOT be called for /api/page-content since we have cache
|
|
588
588
|
mockFetch
|
|
589
589
|
.mockResolvedValueOnce({
|
|
590
590
|
ok: true,
|
|
@@ -169,7 +169,7 @@ export class RouteLoader {
|
|
|
169
169
|
return tree;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const response = await fetch(`${API_ROUTES.
|
|
172
|
+
const response = await fetch(`${API_ROUTES.PAGE_CONTENT}?page=${encodeURIComponent(pathWithoutLocale)}`, {
|
|
173
173
|
cache: 'no-store',
|
|
174
174
|
signal: abortController.signal,
|
|
175
175
|
});
|
|
@@ -123,10 +123,11 @@ export class ScriptExecutor {
|
|
|
123
123
|
const wrappedJS = `(function() {
|
|
124
124
|
// Component: ${componentName} (defineVars)
|
|
125
125
|
try {
|
|
126
|
-
var elements = document.querySelectorAll('[data-component
|
|
126
|
+
var elements = document.querySelectorAll('[data-component~="${componentName}"]');
|
|
127
127
|
elements.forEach(function(el) {
|
|
128
128
|
var propsStr = el.getAttribute('data-props');
|
|
129
|
-
var
|
|
129
|
+
var allProps = propsStr ? JSON.parse(propsStr) : {};
|
|
130
|
+
var props = allProps["${componentName}"] || {};
|
|
130
131
|
(function(el, props) {
|
|
131
132
|
${destructure}
|
|
132
133
|
${js}
|
|
@@ -275,7 +276,7 @@ export class ScriptExecutor {
|
|
|
275
276
|
const js = component.component.javascript;
|
|
276
277
|
const destructure = generateDestructure(defineVars, component.component.interface);
|
|
277
278
|
|
|
278
|
-
// Update data-props attribute
|
|
279
|
+
// Update data-props attribute (keyed by component name)
|
|
279
280
|
const varsToExpose = defineVars === true
|
|
280
281
|
? Object.keys(component.component.interface || {})
|
|
281
282
|
: defineVars;
|
|
@@ -286,7 +287,15 @@ export class ScriptExecutor {
|
|
|
286
287
|
propsForJS[varName] = newProps[varName];
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
+
|
|
291
|
+
// Preserve existing keyed props from other components
|
|
292
|
+
let allProps: Record<string, unknown> = {};
|
|
293
|
+
const existingStr = element.getAttribute('data-props');
|
|
294
|
+
if (existingStr) {
|
|
295
|
+
try { allProps = JSON.parse(existingStr); } catch {}
|
|
296
|
+
}
|
|
297
|
+
allProps[componentName] = propsForJS;
|
|
298
|
+
element.setAttribute('data-props', JSON.stringify(allProps));
|
|
290
299
|
|
|
291
300
|
// Execute JS for this element only
|
|
292
301
|
const wrappedJS = `(function(el, props) {
|
|
@@ -134,7 +134,7 @@ export class PrefetchService {
|
|
|
134
134
|
try {
|
|
135
135
|
// Fetch page data (same endpoint RouteLoader uses)
|
|
136
136
|
const response = await fetch(
|
|
137
|
-
`${API_ROUTES.
|
|
137
|
+
`${API_ROUTES.PAGE_CONTENT}?page=${encodeURIComponent(path)}`,
|
|
138
138
|
{
|
|
139
139
|
signal: abortController.signal,
|
|
140
140
|
// Allow browser caching for prefetched content
|
|
@@ -7,18 +7,103 @@
|
|
|
7
7
|
* CSS is injected eagerly during React's render phase (inside collect()),
|
|
8
8
|
* eliminating the micro-gap between DOM commit and CSS injection (FOUC).
|
|
9
9
|
* This is the same pattern CSS-in-JS libraries (Emotion, styled-components) use.
|
|
10
|
+
*
|
|
11
|
+
* All injected CSS is kept sorted by CSS property precedence so that shorthand
|
|
12
|
+
* properties (border) always appear before longhands (border-color), regardless
|
|
13
|
+
* of which render batch introduced each class.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
import { generateSingleClassCSS } from '../../shared/cssGeneration';
|
|
16
|
+
import { generateSingleClassCSS, sortClassesByPropertyOrder, generateRuleForClass } from '../../shared/cssGeneration';
|
|
13
17
|
import { getCachedBreakpointConfig, getCachedResponsiveScalesConfig } from '../responsiveStyleResolver';
|
|
14
|
-
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
18
|
+
import { DEFAULT_BREAKPOINTS, getBreakpointValues } from '../../shared/breakpoints';
|
|
19
|
+
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
15
20
|
import { DEFAULT_RESPONSIVE_SCALES } from '../../shared/responsiveScaling';
|
|
16
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Build a map from responsive prefix (e.g. 't', 'mob') to breakpoint value.
|
|
24
|
+
* Same logic as generateSingleClassCSS / generateUtilityCSS.
|
|
25
|
+
*/
|
|
26
|
+
function buildResponsivePrefixMap(breakpoints: BreakpointConfig): Record<string, number> {
|
|
27
|
+
const breakpointValues = getBreakpointValues(breakpoints);
|
|
28
|
+
const map: Record<string, number> = {};
|
|
29
|
+
for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
|
|
30
|
+
let prefix = breakpointName.charAt(0).toLowerCase();
|
|
31
|
+
if (breakpointName.toLowerCase() === 'mobile') {
|
|
32
|
+
prefix = 'mob';
|
|
33
|
+
}
|
|
34
|
+
map[prefix] = breakpointValue;
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the responsive breakpoint value for a class, or 0 if it's not responsive.
|
|
41
|
+
* A class is responsive if it starts with a known breakpoint prefix and the
|
|
42
|
+
* remainder generates a valid CSS rule (same heuristic as generateSingleClassCSS).
|
|
43
|
+
*/
|
|
44
|
+
function getClassBreakpointValue(
|
|
45
|
+
className: string,
|
|
46
|
+
prefixMap: Record<string, number>
|
|
47
|
+
): number {
|
|
48
|
+
for (const prefix of Object.keys(prefixMap)) {
|
|
49
|
+
if (className.startsWith(`${prefix}-`) && className.length > prefix.length + 1) {
|
|
50
|
+
const potentialClass = className.substring(prefix.length + 1);
|
|
51
|
+
const rule = generateRuleForClass(potentialClass);
|
|
52
|
+
if (rule && !potentialClass.match(/^(auto|0|[\d.]+px|[\d.]+p)$/)) {
|
|
53
|
+
return prefixMap[prefix];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sort utility classes with breakpoint-aware ordering:
|
|
62
|
+
* 1. Non-responsive classes first, sorted by CSS property order (shorthand before longhand)
|
|
63
|
+
* 2. Responsive classes after, sorted by breakpoint value descending (largest first),
|
|
64
|
+
* then by property order within each breakpoint group.
|
|
65
|
+
*
|
|
66
|
+
* This matches the SSR output from generateUtilityCSS which sorts responsive
|
|
67
|
+
* @media blocks by breakpoint value descending so that smaller breakpoints
|
|
68
|
+
* (mobile 540px) can correctly override larger ones (tablet 1024px) in the cascade.
|
|
69
|
+
*/
|
|
70
|
+
function sortClassesWithBreakpointOrder(
|
|
71
|
+
classes: Iterable<string>,
|
|
72
|
+
breakpoints: BreakpointConfig
|
|
73
|
+
): string[] {
|
|
74
|
+
const prefixMap = buildResponsivePrefixMap(breakpoints);
|
|
75
|
+
const arr = Array.from(classes);
|
|
76
|
+
|
|
77
|
+
// Pre-compute sort keys for each class
|
|
78
|
+
const keys = arr.map(cls => {
|
|
79
|
+
const bpValue = getClassBreakpointValue(cls, prefixMap);
|
|
80
|
+
return { cls, isResponsive: bpValue > 0 ? 1 : 0, bpValue };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Get property-order sorted array to derive per-class indices
|
|
84
|
+
const propertySorted = sortClassesByPropertyOrder(arr);
|
|
85
|
+
const propertyOrderIndex = new Map<string, number>();
|
|
86
|
+
for (let i = 0; i < propertySorted.length; i++) {
|
|
87
|
+
propertyOrderIndex.set(propertySorted[i], i);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
keys.sort((a, b) => {
|
|
91
|
+
// Non-responsive (0) before responsive (1)
|
|
92
|
+
if (a.isResponsive !== b.isResponsive) return a.isResponsive - b.isResponsive;
|
|
93
|
+
// Within responsive: larger breakpoint first (descending)
|
|
94
|
+
if (a.bpValue !== b.bpValue) return b.bpValue - a.bpValue;
|
|
95
|
+
// Within same group: property order
|
|
96
|
+
return (propertyOrderIndex.get(a.cls) ?? Infinity) - (propertyOrderIndex.get(b.cls) ?? Infinity);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return keys.map(k => k.cls);
|
|
100
|
+
}
|
|
101
|
+
|
|
17
102
|
class UtilityClassCollectorImpl {
|
|
18
103
|
private classes: Set<string> = new Set();
|
|
19
104
|
|
|
20
|
-
/**
|
|
21
|
-
private
|
|
105
|
+
/** Map of injected class names → their generated CSS text */
|
|
106
|
+
private injectedRules: Map<string, string> = new Map();
|
|
22
107
|
|
|
23
108
|
/** Cached reference to <style id="utility-css"> */
|
|
24
109
|
private styleEl: HTMLStyleElement | null = null;
|
|
@@ -53,34 +138,45 @@ class UtilityClassCollectorImpl {
|
|
|
53
138
|
* Collect utility class names from a render pass and eagerly inject CSS.
|
|
54
139
|
* Called by ComponentBuilder and builder modules after computing style classes.
|
|
55
140
|
* CSS is injected synchronously during render — before React commits the DOM.
|
|
141
|
+
*
|
|
142
|
+
* When new classes are added, the full style content is rebuilt in sorted order
|
|
143
|
+
* so that shorthand properties (border) always precede longhands (border-color),
|
|
144
|
+
* even when they arrive in different render batches.
|
|
56
145
|
*/
|
|
57
146
|
collect(classNames: string[]): void {
|
|
58
147
|
const breakpointConfig = getCachedBreakpointConfig() || DEFAULT_BREAKPOINTS;
|
|
59
148
|
const responsiveScalesConfig = getCachedResponsiveScalesConfig() || DEFAULT_RESPONSIVE_SCALES;
|
|
60
|
-
const cssParts: string[] = [];
|
|
61
149
|
|
|
150
|
+
let hasNew = false;
|
|
62
151
|
for (const name of classNames) {
|
|
63
152
|
this.classes.add(name);
|
|
64
153
|
|
|
65
|
-
if (this.
|
|
66
|
-
this.injectedClasses.add(name);
|
|
154
|
+
if (this.injectedRules.has(name)) continue;
|
|
67
155
|
|
|
68
156
|
const css = generateSingleClassCSS(name, breakpointConfig, responsiveScalesConfig);
|
|
69
|
-
if (css)
|
|
157
|
+
if (css) {
|
|
158
|
+
this.injectedRules.set(name, css);
|
|
159
|
+
hasNew = true;
|
|
160
|
+
}
|
|
70
161
|
}
|
|
71
162
|
|
|
72
|
-
|
|
73
|
-
|
|
163
|
+
if (hasNew) {
|
|
164
|
+
// Rebuild style element with all rules sorted by property order.
|
|
165
|
+
// This ensures shorthands always precede longhands regardless of
|
|
166
|
+
// which collect() batch introduced each class.
|
|
167
|
+
const sorted = sortClassesWithBreakpointOrder(this.injectedRules.keys(), breakpointConfig);
|
|
74
168
|
const styleEl = this.ensureStyleElement();
|
|
75
169
|
if (styleEl) {
|
|
76
|
-
styleEl.
|
|
170
|
+
styleEl.textContent = sorted
|
|
171
|
+
.map(name => this.injectedRules.get(name)!)
|
|
172
|
+
.join('\n');
|
|
77
173
|
}
|
|
78
174
|
}
|
|
79
175
|
}
|
|
80
176
|
|
|
81
177
|
/**
|
|
82
178
|
* Clear collected classes for route change.
|
|
83
|
-
* Preserves
|
|
179
|
+
* Preserves injectedRules and style tag — old page CSS stays
|
|
84
180
|
* (needed during transition via previousComponentTree).
|
|
85
181
|
*/
|
|
86
182
|
clear(): void {
|
|
@@ -93,7 +189,7 @@ class UtilityClassCollectorImpl {
|
|
|
93
189
|
*/
|
|
94
190
|
destroy(): void {
|
|
95
191
|
this.classes.clear();
|
|
96
|
-
this.
|
|
192
|
+
this.injectedRules.clear();
|
|
97
193
|
if (this.styleEl && this.styleEl.isConnected) {
|
|
98
194
|
this.styleEl.remove();
|
|
99
195
|
}
|