meno-core 1.0.23 → 1.0.25
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/bin/cli.ts +1 -0
- package/build-static.ts +9 -5
- package/bunfig.toml +4 -1
- package/entries/client-router.tsx +83 -1
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.ts +2 -0
- package/lib/client/core/builders/embedBuilder.ts +5 -1
- package/lib/client/core/builders/linkNodeBuilder.ts +3 -0
- package/lib/client/core/builders/listBuilder.ts +3 -1
- package/lib/client/core/builders/localeListBuilder.ts +7 -0
- package/lib/client/core/cmsTemplateProcessor.ts +10 -9
- package/lib/client/hmr/HMRManager.tsx +17 -19
- package/lib/client/hooks/useVariables.ts +101 -0
- package/lib/client/index.ts +1 -0
- package/lib/client/responsiveStyleResolver.test.ts +14 -9
- package/lib/client/routing/RouteLoader.test.ts +7 -3
- package/lib/client/routing/RouteLoader.ts +4 -2
- package/lib/client/routing/Router.tsx +45 -21
- package/lib/client/services/PrefetchService.ts +1 -1
- package/lib/client/styles/StyleInjector.test.ts +20 -8
- package/lib/client/styles/StyleInjector.ts +103 -108
- package/lib/client/styles/UtilityClassCollector.ts +208 -0
- package/lib/client/templateEngine.ts +61 -3
- package/lib/server/__integration__/cms-integration.test.ts +2 -2
- package/lib/server/createServer.ts +1 -0
- 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/routes/static.ts +3 -1
- 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/cmsService.ts +18 -8
- package/lib/server/services/configService.test.ts +1 -95
- package/lib/server/services/configService.ts +0 -27
- package/lib/server/services/fileWatcherService.ts +33 -4
- package/lib/server/services/pageService.ts +140 -2
- package/lib/server/ssr/cmsSSRProcessor.ts +3 -2
- package/lib/server/ssr/htmlGenerator.ts +35 -7
- package/lib/server/ssr/imageMetadata.ts +5 -1
- package/lib/server/ssr/ssrRenderer.test.ts +183 -117
- package/lib/server/ssr/ssrRenderer.ts +159 -272
- 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 +33 -2
- package/lib/shared/cssGeneration.ts +167 -47
- package/lib/shared/cssProperties.ts +22 -0
- package/lib/shared/fontLoader.test.ts +11 -2
- package/lib/shared/fontLoader.ts +20 -9
- 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.test.ts +20 -0
- package/lib/shared/libraryLoader.ts +49 -3
- 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/cms.ts +4 -6
- 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/libraries.ts +9 -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 +59 -47
- package/lib/test-utils/dom-setup.ts +1 -0
- package/package.json +1 -1
- package/templates/index-router.html +1 -1
package/bin/cli.ts
CHANGED
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,
|
package/bunfig.toml
CHANGED
|
@@ -32,7 +32,10 @@ testNamePattern = ".*"
|
|
|
32
32
|
|
|
33
33
|
# Skip patterns to exclude from test discovery
|
|
34
34
|
# tests/ contains Playwright E2E tests that should be run with `bunx playwright test`
|
|
35
|
-
|
|
35
|
+
# __integration__ tests run in a separate process (bun test:integration)
|
|
36
|
+
# because Bun's mock.module is process-global and unit tests that mock core modules
|
|
37
|
+
# (e.g. ssrRenderer, configService) would contaminate integration tests.
|
|
38
|
+
testPathIgnorePatterns = ["node_modules", "dist", "\\.next", "tests/", "__integration__"]
|
|
36
39
|
|
|
37
40
|
# Coverage reporter options
|
|
38
41
|
# Generates coverage reports for analysis
|
|
@@ -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
|
|
@@ -27,6 +27,7 @@ import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, type V
|
|
|
27
27
|
import { DEFAULT_I18N_CONFIG, resolveI18nValue } from "../../shared/i18n";
|
|
28
28
|
import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
|
|
29
29
|
import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
|
|
30
|
+
import { UtilityClassCollector } from "../styles/UtilityClassCollector";
|
|
30
31
|
import { processCMSTemplate, processCMSPropsTemplate, RAW_HTML_PREFIX } from "./cmsTemplateProcessor";
|
|
31
32
|
import type { PrefetchService } from "../services/PrefetchService";
|
|
32
33
|
import { generateElementClassName, type ElementClassContext } from "../../shared/elementClassName";
|
|
@@ -95,6 +96,7 @@ export class ComponentBuilder {
|
|
|
95
96
|
cached = responsiveStylesToClasses(style as any);
|
|
96
97
|
this.styleClassCache.set(style, cached);
|
|
97
98
|
}
|
|
99
|
+
UtilityClassCollector.collect(cached);
|
|
98
100
|
return cached;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -12,6 +12,7 @@ import { pathToString } from "../../../shared/pathArrayUtils";
|
|
|
12
12
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
14
|
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
15
|
+
import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
|
|
15
16
|
import DOMPurify from "isomorphic-dompurify";
|
|
16
17
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
17
18
|
import type { BuilderContext } from "./types";
|
|
@@ -53,7 +54,8 @@ export function buildEmbed(
|
|
|
53
54
|
const { key, elementPath, parentComponentName, componentContext, componentRootPath, cmsItemIndexPath } = ctx;
|
|
54
55
|
|
|
55
56
|
// Process templates in html property before sanitization (matching SSR behavior)
|
|
56
|
-
|
|
57
|
+
// Mappings should already be resolved by processStructure, this is just a type safety guard
|
|
58
|
+
let htmlContent = typeof node.html === 'string' ? node.html : '';
|
|
57
59
|
|
|
58
60
|
// Process item template strings (for List context) - e.g., {{item.icon}}, {{feature.title}}
|
|
59
61
|
if (ctx.templateContext && hasItemTemplates(htmlContent)) {
|
|
@@ -125,6 +127,7 @@ export function buildEmbed(
|
|
|
125
127
|
) as StyleObject | ResponsiveStyleObject;
|
|
126
128
|
}
|
|
127
129
|
const utilityClasses = responsiveStylesToClasses(processedStyle as StyleObject | ResponsiveStyleObject);
|
|
130
|
+
UtilityClassCollector.collect(utilityClasses);
|
|
128
131
|
classNames.push(...utilityClasses);
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -172,6 +175,7 @@ export function buildEmbed(
|
|
|
172
175
|
for (const rule of nodeInteractiveStyles) {
|
|
173
176
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
174
177
|
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
178
|
+
UtilityClassCollector.collect(styleClasses);
|
|
175
179
|
previewClasses.push(...styleClasses);
|
|
176
180
|
}
|
|
177
181
|
}
|
|
@@ -13,6 +13,7 @@ import { generateElementClassName, type ElementClassContext } from "../../../sha
|
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
14
|
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
15
15
|
import { processItemPropsTemplate, type ValueResolver } from "../../../shared/itemTemplateUtils";
|
|
16
|
+
import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
|
|
16
17
|
import { resolveI18nValue, DEFAULT_I18N_CONFIG } from "../../../shared/i18n";
|
|
17
18
|
import { isCurrentLink } from "../../../shared/linkUtils";
|
|
18
19
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
@@ -101,6 +102,7 @@ export function buildLinkNode(
|
|
|
101
102
|
) as StyleObject | ResponsiveStyleObject;
|
|
102
103
|
}
|
|
103
104
|
const utilityClasses = responsiveStylesToClasses(processedStyle as StyleObject | ResponsiveStyleObject);
|
|
105
|
+
UtilityClassCollector.collect(utilityClasses);
|
|
104
106
|
classNames.push(...utilityClasses);
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -148,6 +150,7 @@ export function buildLinkNode(
|
|
|
148
150
|
for (const rule of nodeInteractiveStyles) {
|
|
149
151
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
150
152
|
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
153
|
+
UtilityClassCollector.collect(styleClasses);
|
|
151
154
|
previewClasses.push(...styleClasses);
|
|
152
155
|
}
|
|
153
156
|
}
|
|
@@ -11,6 +11,7 @@ import type { ListNode } from "../../../shared/registry/nodeTypes/ListNodeType";
|
|
|
11
11
|
import type { CMSItem, CMSFilterCondition, CMSSortConfig, CMSFilterOperator } from "../../../shared/types/cms";
|
|
12
12
|
import type { InteractiveStyles, StyleObject, ResponsiveStyleObject } from "../../../shared/types";
|
|
13
13
|
import { singularize } from "../../../shared/types/cms";
|
|
14
|
+
import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
|
|
14
15
|
import { buildTemplateContext, resolveItemsTemplate, getNestedValue } from "../../../shared/itemTemplateUtils";
|
|
15
16
|
import type { TemplateContext } from "../../../shared/types/cms";
|
|
16
17
|
import { pathToString, getChildPath } from "../../../shared/pathArrayUtils";
|
|
@@ -152,6 +153,7 @@ export function buildList(
|
|
|
152
153
|
for (const rule of nodeInteractiveStyles) {
|
|
153
154
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
154
155
|
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
156
|
+
UtilityClassCollector.collect(styleClasses);
|
|
155
157
|
previewClasses.push(...styleClasses);
|
|
156
158
|
}
|
|
157
159
|
}
|
|
@@ -222,7 +224,7 @@ export function buildList(
|
|
|
222
224
|
const label = isCollectionMode ? 'CMS List' : 'List';
|
|
223
225
|
|
|
224
226
|
// Use configurable tag (defaults to 'div') - null means fragment mode (no wrapper)
|
|
225
|
-
const tag = node.tag ===
|
|
227
|
+
const tag = typeof node.tag === 'string' ? node.tag : null;
|
|
226
228
|
|
|
227
229
|
if (!source && !sourceIsResolved) {
|
|
228
230
|
// No source - render empty container with placeholder
|
|
@@ -12,6 +12,7 @@ import { pathToString } from "../../../shared/pathArrayUtils";
|
|
|
12
12
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
14
|
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
15
|
+
import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
|
|
15
16
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
16
17
|
import type { BuilderContext } from "./types";
|
|
17
18
|
|
|
@@ -75,6 +76,7 @@ export function buildLocaleList(
|
|
|
75
76
|
// Convert container styles to utility classes
|
|
76
77
|
if (node.style) {
|
|
77
78
|
const utilityClasses = responsiveStylesToClasses(node.style as StyleObject | ResponsiveStyleObject);
|
|
79
|
+
UtilityClassCollector.collect(utilityClasses);
|
|
78
80
|
classNames.push(...utilityClasses);
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -122,6 +124,7 @@ export function buildLocaleList(
|
|
|
122
124
|
for (const rule of nodeInteractiveStyles) {
|
|
123
125
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
124
126
|
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
127
|
+
UtilityClassCollector.collect(styleClasses);
|
|
125
128
|
previewClasses.push(...styleClasses);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
@@ -170,6 +173,10 @@ export function buildLocaleList(
|
|
|
170
173
|
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
171
174
|
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
172
175
|
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
176
|
+
UtilityClassCollector.collect(itemClasses);
|
|
177
|
+
UtilityClassCollector.collect(activeItemClasses);
|
|
178
|
+
UtilityClassCollector.collect(separatorClasses);
|
|
179
|
+
UtilityClassCollector.collect(flagClasses);
|
|
173
180
|
|
|
174
181
|
// Build locale links from config
|
|
175
182
|
const linkElements: ReactElement[] = [];
|
|
@@ -29,21 +29,21 @@ function isI18nValue(value: unknown): value is I18nValue {
|
|
|
29
29
|
* Resolve an I18nValue to a string for the given locale
|
|
30
30
|
* Falls back to default locale, then first available translation
|
|
31
31
|
*/
|
|
32
|
-
function resolveI18nValue(value: I18nValue, locale: string, config: I18nConfig):
|
|
32
|
+
function resolveI18nValue(value: I18nValue, locale: string, config: I18nConfig): unknown {
|
|
33
33
|
// Try exact locale match
|
|
34
|
-
if (
|
|
35
|
-
return value[locale]
|
|
34
|
+
if (value[locale] !== undefined) {
|
|
35
|
+
return value[locale];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Try default locale
|
|
39
|
-
if (
|
|
40
|
-
return value[config.defaultLocale]
|
|
39
|
+
if (value[config.defaultLocale] !== undefined) {
|
|
40
|
+
return value[config.defaultLocale];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Get first available translation (skip _i18n marker)
|
|
44
44
|
for (const key of Object.keys(value)) {
|
|
45
|
-
if (key !== '_i18n' &&
|
|
46
|
-
return value[key]
|
|
45
|
+
if (key !== '_i18n' && value[key] !== undefined) {
|
|
46
|
+
return value[key];
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -82,9 +82,10 @@ export function processCMSTemplate(
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Handle i18n values
|
|
85
|
+
// Handle i18n values - resolve to locale-specific value, then continue
|
|
86
|
+
// through rich-text detection (don't early-return, as resolved value may be Tiptap JSON)
|
|
86
87
|
if (isI18nValue(value)) {
|
|
87
|
-
|
|
88
|
+
value = resolveI18nValue(value, effectiveLocale, config);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
// Return string representation
|
|
@@ -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
|
|
|
@@ -53,6 +53,7 @@ export function HMRManager({
|
|
|
53
53
|
// Track callbacks via refs to avoid stale closures and prevent WebSocket recreation
|
|
54
54
|
const currentPathRef = useRef(currentPath);
|
|
55
55
|
const onCMSUpdateRef = useRef(onCMSUpdate);
|
|
56
|
+
const onReloadRef = useRef(onReload);
|
|
56
57
|
|
|
57
58
|
useEffect(() => {
|
|
58
59
|
currentPathRef.current = currentPath;
|
|
@@ -62,24 +63,15 @@ export function HMRManager({
|
|
|
62
63
|
onCMSUpdateRef.current = onCMSUpdate;
|
|
63
64
|
}, [onCMSUpdate]);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
const showHMRIndicator = useCallback(() => {
|
|
67
|
-
const indicator = document.getElementById('hmr-indicator');
|
|
68
|
-
if (indicator) {
|
|
69
|
-
indicator.style.display = 'block';
|
|
70
|
-
setTimeout(() => {
|
|
71
|
-
indicator.style.display = 'none';
|
|
72
|
-
}, 2000);
|
|
73
|
-
}
|
|
74
|
-
}, []);
|
|
66
|
+
useEffect(() => { onReloadRef.current = onReload; }, [onReload]);
|
|
75
67
|
|
|
76
68
|
// Define message handler - reads from refs to always get latest values
|
|
77
69
|
const createMessageHandler = () => (data: any) => {
|
|
78
70
|
if (data.type === 'hmr:update') {
|
|
79
71
|
// Always use current path (preserves locale) when reloading
|
|
80
72
|
// The HMR path tells us which page changed, but we reload with current locale
|
|
81
|
-
if (
|
|
82
|
-
|
|
73
|
+
if (onReloadRef.current) {
|
|
74
|
+
onReloadRef.current(currentPathRef.current);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
// Call update callback with the original HMR path
|
|
@@ -94,8 +86,15 @@ export function HMRManager({
|
|
|
94
86
|
} else if (data.type === 'hmr:colors-update') {
|
|
95
87
|
// Dispatch custom event to notify color hooks of the update
|
|
96
88
|
document.dispatchEvent(new CustomEvent('hmr-colors-update'));
|
|
97
|
-
|
|
98
|
-
|
|
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'));
|
|
99
98
|
}
|
|
100
99
|
};
|
|
101
100
|
|
|
@@ -145,7 +144,7 @@ export function HMRManager({
|
|
|
145
144
|
hmrWs.close();
|
|
146
145
|
}
|
|
147
146
|
};
|
|
148
|
-
}, [onUpdate, onStatusChange
|
|
147
|
+
}, [onUpdate, onStatusChange]);
|
|
149
148
|
|
|
150
149
|
// Bun HMR API integration
|
|
151
150
|
useEffect(() => {
|
|
@@ -157,14 +156,13 @@ export function HMRManager({
|
|
|
157
156
|
});
|
|
158
157
|
|
|
159
158
|
import.meta.hot.on('bun:afterUpdate', () => {
|
|
160
|
-
//
|
|
161
|
-
showHMRIndicator();
|
|
159
|
+
// HMR update complete
|
|
162
160
|
});
|
|
163
161
|
|
|
164
162
|
import.meta.hot.on('bun:error', () => {
|
|
165
163
|
});
|
|
166
164
|
}
|
|
167
|
-
}, [
|
|
165
|
+
}, []);
|
|
168
166
|
|
|
169
167
|
// Render indicators
|
|
170
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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Save original fetch before any test can modify it
|
|
4
|
+
const _originalFetch = globalThis.fetch;
|
|
2
5
|
import {
|
|
3
6
|
resolveResponsiveStyleSync,
|
|
4
7
|
resolveResponsiveStyle,
|
|
@@ -334,10 +337,8 @@ describe("Responsive Style Resolver - resolveResponsiveStyleSync", () => {
|
|
|
334
337
|
});
|
|
335
338
|
|
|
336
339
|
describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Reset global fetch mock
|
|
340
|
-
global.fetch = global.fetch || (() => Promise.reject(new Error('fetch not implemented'))) as any;
|
|
340
|
+
afterEach(() => {
|
|
341
|
+
globalThis.fetch = _originalFetch;
|
|
341
342
|
});
|
|
342
343
|
|
|
343
344
|
test("should resolve non-responsive styles asynchronously", async () => {
|
|
@@ -352,7 +353,7 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
|
|
|
352
353
|
|
|
353
354
|
test("should resolve responsive styles with default breakpoints on fetch error", async () => {
|
|
354
355
|
// Mock fetch to fail
|
|
355
|
-
|
|
356
|
+
globalThis.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
|
|
356
357
|
|
|
357
358
|
const style: ResponsiveStyleObject = {
|
|
358
359
|
base: {
|
|
@@ -371,7 +372,7 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
|
|
|
371
372
|
|
|
372
373
|
test("should handle async breakpoint config loading", async () => {
|
|
373
374
|
// Mock successful fetch
|
|
374
|
-
|
|
375
|
+
globalThis.fetch = (() => Promise.resolve({
|
|
375
376
|
json: () => Promise.resolve({
|
|
376
377
|
breakpoints: {
|
|
377
378
|
tablet: 900,
|
|
@@ -396,9 +397,13 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
|
|
|
396
397
|
});
|
|
397
398
|
|
|
398
399
|
describe("Responsive Style Resolver - initializeBreakpoints", () => {
|
|
400
|
+
afterEach(() => {
|
|
401
|
+
globalThis.fetch = _originalFetch;
|
|
402
|
+
});
|
|
403
|
+
|
|
399
404
|
test("should initialize breakpoint config", async () => {
|
|
400
405
|
// Mock successful fetch
|
|
401
|
-
|
|
406
|
+
globalThis.fetch = (() => Promise.resolve({
|
|
402
407
|
json: () => Promise.resolve({
|
|
403
408
|
breakpoints: {
|
|
404
409
|
tablet: 900,
|
|
@@ -420,7 +425,7 @@ describe("Responsive Style Resolver - initializeBreakpoints", () => {
|
|
|
420
425
|
|
|
421
426
|
test("should handle initialization error gracefully", async () => {
|
|
422
427
|
// Mock fetch to fail
|
|
423
|
-
|
|
428
|
+
globalThis.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
|
|
424
429
|
|
|
425
430
|
// Should not throw
|
|
426
431
|
await expect(initializeBreakpoints()).resolves.toBeUndefined();
|