meno-core 1.0.0

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.
Files changed (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,140 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { stylesToClasses, responsiveStylesToClasses, classToStyle, classesToStyles } from './utilityClassMapper';
3
+
4
+ describe('utilityClassMapper', () => {
5
+ describe('stylesToClasses', () => {
6
+ test('converts padding to utility class', () => {
7
+ const classes = stylesToClasses({ padding: '10px' });
8
+ expect(classes).toContain('p-10px');
9
+ });
10
+
11
+ test('converts display flex to short form', () => {
12
+ const classes = stylesToClasses({ display: 'flex' });
13
+ expect(classes).toContain('f');
14
+ });
15
+
16
+ test('converts multiple properties', () => {
17
+ const classes = stylesToClasses({ padding: '10px', margin: '20px' });
18
+ expect(classes).toContain('p-10px');
19
+ expect(classes).toContain('m-20px');
20
+ });
21
+
22
+ test('handles percentage values', () => {
23
+ const classes = stylesToClasses({ width: '50%' });
24
+ expect(classes).toContain('w-50p');
25
+ });
26
+
27
+ test('handles auto values', () => {
28
+ const classes = stylesToClasses({ margin: 'auto' });
29
+ expect(classes).toContain('m-auto');
30
+ });
31
+
32
+ test('handles values with spaces', () => {
33
+ const classes = stylesToClasses({ padding: '92px 0' });
34
+ expect(classes).toContain('p-92px-0');
35
+ });
36
+
37
+ test('returns empty array for null', () => {
38
+ expect(stylesToClasses(null)).toEqual([]);
39
+ expect(stylesToClasses(undefined)).toEqual([]);
40
+ });
41
+
42
+ test('handles color variables', () => {
43
+ const classes = stylesToClasses({ color: 'text' });
44
+ expect(classes).toContain('c-text');
45
+ });
46
+ });
47
+
48
+ describe('responsiveStylesToClasses', () => {
49
+ test('handles flat style object', () => {
50
+ const classes = responsiveStylesToClasses({ padding: '10px' });
51
+ expect(classes).toContain('p-10px');
52
+ });
53
+
54
+ test('handles responsive styles with base', () => {
55
+ const classes = responsiveStylesToClasses({
56
+ base: { padding: '10px' },
57
+ tablet: { padding: '20px' },
58
+ });
59
+ expect(classes).toContain('p-10px');
60
+ expect(classes).toContain('t-p-20px');
61
+ });
62
+
63
+ test('handles mobile styles with mob prefix', () => {
64
+ const classes = responsiveStylesToClasses({
65
+ base: { padding: '10px' },
66
+ mobile: { padding: '5px' },
67
+ });
68
+ expect(classes).toContain('p-10px');
69
+ expect(classes).toContain('mob-p-5px');
70
+ });
71
+
72
+ test('returns empty array for null', () => {
73
+ expect(responsiveStylesToClasses(null)).toEqual([]);
74
+ expect(responsiveStylesToClasses(undefined)).toEqual([]);
75
+ });
76
+ });
77
+
78
+ describe('classToStyle', () => {
79
+ test('parses padding class', () => {
80
+ const style = classToStyle('p-10px');
81
+ expect(style).toEqual({ prop: 'padding', value: '10px' });
82
+ });
83
+
84
+ test('parses special case flex', () => {
85
+ const style = classToStyle('f');
86
+ expect(style).toEqual({ prop: 'display', value: 'flex' });
87
+ });
88
+
89
+ test('handles tablet prefix', () => {
90
+ const style = classToStyle('t-p-20px');
91
+ expect(style).toEqual({ prop: 'padding', value: '20px' });
92
+ });
93
+
94
+ test('handles mobile prefix', () => {
95
+ const style = classToStyle('mob-p-5px');
96
+ expect(style).toEqual({ prop: 'padding', value: '5px' });
97
+ });
98
+
99
+ test('returns null for invalid class', () => {
100
+ expect(classToStyle('invalid')).toBeNull();
101
+ expect(classToStyle('unknown-class')).toBeNull();
102
+ });
103
+ });
104
+
105
+ describe('classesToStyles', () => {
106
+ test('converts base classes to style object', () => {
107
+ const styles = classesToStyles(['p-10px', 'm-20px']);
108
+ expect(styles).toEqual({
109
+ base: { padding: '10px', margin: '20px' },
110
+ });
111
+ });
112
+
113
+ test('handles responsive classes', () => {
114
+ const styles = classesToStyles(['p-10px', 't-p-20px']);
115
+ expect(styles).toEqual({
116
+ base: { padding: '10px' },
117
+ tablet: { padding: '20px' },
118
+ });
119
+ });
120
+
121
+ test('handles mobile classes', () => {
122
+ const styles = classesToStyles(['p-10px', 'mob-p-5px']);
123
+ expect(styles).toEqual({
124
+ base: { padding: '10px' },
125
+ mobile: { padding: '5px' },
126
+ });
127
+ });
128
+
129
+ test('returns null for empty classes', () => {
130
+ expect(classesToStyles([])).toBeNull();
131
+ });
132
+
133
+ test('removes empty breakpoints', () => {
134
+ const styles = classesToStyles(['p-10px']);
135
+ expect(styles).toEqual({ base: { padding: '10px' } });
136
+ expect(styles?.tablet).toBeUndefined();
137
+ expect(styles?.mobile).toBeUndefined();
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Maps CSS property values to utility class names
3
+ * Example: { padding: "10px", display: "flex" } → ["p-10px", "flex"]
4
+ * Uses centralized configuration from utilityClassConfig.ts
5
+ */
6
+
7
+ import type { ResponsiveStyleObject } from './types';
8
+ import { propertyMap, specialValueMappings, classToStyleSpecialCases } from './utilityClassConfig';
9
+
10
+ /**
11
+ * Converts a CSS property value to a utility class name
12
+ * Example: { prop: "padding", value: "10px" } → "p-10px"
13
+ */
14
+ function propertyValueToClass(prop: string, value: string | number): string | null {
15
+ const prefix = propertyMap[prop];
16
+ if (!prefix) return null;
17
+
18
+ const stringValue = String(value);
19
+
20
+ // Check for special value mappings first
21
+ if (specialValueMappings[prop]) {
22
+ const mapping = specialValueMappings[prop];
23
+ if (mapping[stringValue]) {
24
+ return mapping[stringValue];
25
+ }
26
+ }
27
+
28
+ // CSS variables (e.g., var(--background))
29
+ if (stringValue.includes('var(')) {
30
+ const varMatch = stringValue.match(/var\((--[\w-]+)\)/);
31
+ if (varMatch) {
32
+ const varName = varMatch[1].replace('--', '');
33
+ return `${prefix}-${varName}`;
34
+ }
35
+ }
36
+
37
+ // Detect variable names for color/background properties (e.g., color: "text" → c-text)
38
+ // These are stored as plain names without var() wrapper and should be treated as variables
39
+ if ((prop === 'color' || prop === 'backgroundColor' || prop === 'borderColor') &&
40
+ !stringValue.match(/^\d/) && !stringValue.includes('#') && !stringValue.includes('rgb')) {
41
+ return `${prefix}-${stringValue}`;
42
+ }
43
+
44
+ // Direct values (with units)
45
+ if (stringValue.match(/^\d+px$/) || stringValue.match(/^\d+em$/)) {
46
+ return `${prefix}-${stringValue}`;
47
+ }
48
+
49
+ // Percentage values - convert % to p (e.g., "50%" → "50p")
50
+ if (stringValue.match(/^\d+%$/)) {
51
+ const percentValue = stringValue.replace('%', 'p');
52
+ return `${prefix}-${percentValue}`;
53
+ }
54
+
55
+ // Handle values with spaces by replacing spaces with hyphens (e.g., "92px 0" → "p-92px-0")
56
+ if (stringValue.includes(' ')) {
57
+ const spacedValue = stringValue.replace(/\s+/g, '-');
58
+ return `${prefix}-${spacedValue}`;
59
+ }
60
+
61
+ // Special handling for margin/padding "auto"
62
+ if (stringValue === 'auto') {
63
+ return `${prefix}-auto`;
64
+ }
65
+
66
+ // For other cases, append the value
67
+ return `${prefix}-${stringValue}`;
68
+ }
69
+
70
+ // Get property order from propertyMap for consistent class ordering
71
+ const propertyOrder = Object.keys(propertyMap);
72
+
73
+ /**
74
+ * Convert a style object to an array of utility classes
75
+ * Example: { padding: "10px", display: "flex" } → ["p-10px", "flex"]
76
+ *
77
+ * Properties are sorted according to propertyMap order to ensure
78
+ * dependent properties (like borderColor) come after their base (border)
79
+ */
80
+ export function stylesToClasses(
81
+ styles: Record<string, string | number> | null | undefined
82
+ ): string[] {
83
+ if (!styles) return [];
84
+
85
+ // Sort properties by their order in propertyMap
86
+ // This ensures border comes before borderColor, etc.
87
+ const sortedProps = Object.keys(styles).sort((a, b) => {
88
+ const indexA = propertyOrder.indexOf(a);
89
+ const indexB = propertyOrder.indexOf(b);
90
+ // Unknown properties go to the end
91
+ const orderA = indexA === -1 ? Infinity : indexA;
92
+ const orderB = indexB === -1 ? Infinity : indexB;
93
+ return orderA - orderB;
94
+ });
95
+
96
+ const classes: string[] = [];
97
+ for (const prop of sortedProps) {
98
+ const className = propertyValueToClass(prop, styles[prop]);
99
+ if (className) {
100
+ classes.push(className);
101
+ }
102
+ }
103
+ return classes;
104
+ }
105
+
106
+ /**
107
+ * Convert a responsive style object to utility classes with responsive prefixes
108
+ * Example: { base: { padding: "10px" }, tablet: { padding: "24px" } }
109
+ * → ["p-10px", "t-p-24px"]
110
+ *
111
+ * Also handles flat merged style objects:
112
+ * Example: { padding: "10px", display: "flex" } → ["p-10px", "flex"]
113
+ */
114
+ export function responsiveStylesToClasses(
115
+ styles: ResponsiveStyleObject | Record<string, string | number> | null | undefined
116
+ ): string[] {
117
+ if (!styles) return [];
118
+
119
+ const classes: string[] = [];
120
+
121
+ // Check if this is a responsive style object (has base/tablet/mobile properties)
122
+ if ('base' in styles || 'tablet' in styles || 'mobile' in styles) {
123
+ const responsiveStyles = styles as ResponsiveStyleObject;
124
+
125
+ // Base styles (no prefix)
126
+ if (responsiveStyles.base) {
127
+ classes.push(...stylesToClasses(responsiveStyles.base));
128
+ }
129
+
130
+ // Tablet styles (t- prefix)
131
+ if (responsiveStyles.tablet) {
132
+ const tabletClasses = stylesToClasses(responsiveStyles.tablet);
133
+ classes.push(...tabletClasses.map((cls) => `t-${cls}`));
134
+ }
135
+
136
+ // Mobile styles (mob- prefix to avoid conflict with margin)
137
+ if (responsiveStyles.mobile) {
138
+ const mobileClasses = stylesToClasses(responsiveStyles.mobile);
139
+ classes.push(...mobileClasses.map((cls) => `mob-${cls}`));
140
+ }
141
+ } else {
142
+ // Flat style object - treat all as base classes
143
+ classes.push(...stylesToClasses(styles as Record<string, string | number>));
144
+ }
145
+
146
+ return classes;
147
+ }
148
+
149
+ /**
150
+ * Parse a utility class back to CSS property and value
151
+ * Example: "p-10px" → { padding: "10px" }
152
+ */
153
+ export function classToStyle(className: string): { prop: string; value: string } | null {
154
+ // Remove responsive prefixes for parsing
155
+ let cleanClass = className;
156
+ let responsivePrefix = '';
157
+
158
+ if (className.startsWith('t-')) {
159
+ responsivePrefix = 't';
160
+ cleanClass = className.slice(2);
161
+ } else if (className.startsWith('mob-')) {
162
+ responsivePrefix = 'mob';
163
+ cleanClass = className.slice(4);
164
+ }
165
+
166
+ // Check special cases from config
167
+ if (classToStyleSpecialCases[cleanClass]) {
168
+ return classToStyleSpecialCases[cleanClass];
169
+ }
170
+
171
+ // Parse prefix-value pattern (e.g., "p-10px", "m-20px")
172
+ const match = cleanClass.match(/^([a-z-]+)-(.*?)$/);
173
+ if (!match) return null;
174
+
175
+ const [, prefix, value] = match;
176
+
177
+ // Find the property from the prefix
178
+ let prop = '';
179
+ for (const [key, val] of Object.entries(propertyMap)) {
180
+ if (val === prefix) {
181
+ prop = key;
182
+ break;
183
+ }
184
+ }
185
+
186
+ if (!prop) return null;
187
+
188
+ // Handle CSS variables
189
+ if (value.includes('background') || value.includes('text') || value.includes('border')) {
190
+ return { prop, value: `var(--${value})` };
191
+ }
192
+
193
+ return { prop, value };
194
+ }
195
+
196
+ /**
197
+ * Convert utility classes back to a style object
198
+ * Example: ["p-10px", "t-p-24px"] → { base: { padding: "10px" }, tablet: { padding: "24px" } }
199
+ */
200
+ export function classesToStyles(classes: string[]): ResponsiveStyleObject | null {
201
+ const styles: ResponsiveStyleObject = { base: {}, tablet: {}, mobile: {} };
202
+
203
+ for (const className of classes) {
204
+ let breakpoint: 'base' | 'tablet' | 'mobile' = 'base';
205
+ let cleanClass = className;
206
+
207
+ if (className.startsWith('t-')) {
208
+ breakpoint = 'tablet';
209
+ cleanClass = className.slice(2);
210
+ } else if (className.startsWith('mob-')) {
211
+ breakpoint = 'mobile';
212
+ cleanClass = className.slice(4);
213
+ }
214
+
215
+ const styleEntry = classToStyle(cleanClass);
216
+ if (styleEntry) {
217
+ if (!styles[breakpoint]) {
218
+ styles[breakpoint] = {};
219
+ }
220
+ styles[breakpoint]![styleEntry.prop] = styleEntry.value;
221
+ }
222
+ }
223
+
224
+ // Clean up empty breakpoints
225
+ if (Object.keys(styles.tablet || {}).length === 0) delete styles.tablet;
226
+ if (Object.keys(styles.mobile || {}).length === 0) delete styles.mobile;
227
+
228
+ return Object.keys(styles.base || {}).length > 0 ? styles : null;
229
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ isJSONFile,
4
+ isJSFile,
5
+ isCSSFile,
6
+ stripExtension,
7
+ getBaseName,
8
+ mapPageNameToPath,
9
+ mapPathToPageName,
10
+ } from './fileUtils';
11
+
12
+ describe('fileUtils', () => {
13
+ describe('isJSONFile', () => {
14
+ test('returns true for .json files', () => {
15
+ expect(isJSONFile('page.json')).toBe(true);
16
+ expect(isJSONFile('component.json')).toBe(true);
17
+ });
18
+
19
+ test('returns false for non-json files', () => {
20
+ expect(isJSONFile('page.js')).toBe(false);
21
+ expect(isJSONFile('page.ts')).toBe(false);
22
+ expect(isJSONFile('page')).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe('isJSFile', () => {
27
+ test('returns true for .js files', () => {
28
+ expect(isJSFile('script.js')).toBe(true);
29
+ });
30
+
31
+ test('returns false for non-js files', () => {
32
+ expect(isJSFile('script.ts')).toBe(false);
33
+ expect(isJSFile('script.json')).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe('isCSSFile', () => {
38
+ test('returns true for .css files', () => {
39
+ expect(isCSSFile('styles.css')).toBe(true);
40
+ });
41
+
42
+ test('returns false for non-css files', () => {
43
+ expect(isCSSFile('styles.scss')).toBe(false);
44
+ expect(isCSSFile('styles.less')).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe('stripExtension', () => {
49
+ test('removes extension from filename', () => {
50
+ expect(stripExtension('page.json')).toBe('page');
51
+ expect(stripExtension('component.test.ts')).toBe('component.test');
52
+ });
53
+
54
+ test('returns original if no extension', () => {
55
+ expect(stripExtension('page')).toBe('page');
56
+ });
57
+
58
+ test('handles multiple dots correctly', () => {
59
+ expect(stripExtension('my.component.json')).toBe('my.component');
60
+ });
61
+ });
62
+
63
+ describe('getBaseName', () => {
64
+ test('returns filename from path', () => {
65
+ expect(getBaseName('/path/to/file.json')).toBe('file.json');
66
+ expect(getBaseName('/a/b/c/file.ts')).toBe('file.ts');
67
+ });
68
+
69
+ test('returns filename if no path', () => {
70
+ expect(getBaseName('file.json')).toBe('file.json');
71
+ });
72
+
73
+ test('handles trailing slash', () => {
74
+ expect(getBaseName('/path/to/dir/')).toBe('');
75
+ });
76
+ });
77
+
78
+ describe('mapPageNameToPath', () => {
79
+ test('maps index to root path', () => {
80
+ expect(mapPageNameToPath('index')).toBe('/');
81
+ });
82
+
83
+ test('adds leading slash', () => {
84
+ expect(mapPageNameToPath('about')).toBe('/about');
85
+ expect(mapPageNameToPath('blog/post')).toBe('/blog/post');
86
+ });
87
+ });
88
+
89
+ describe('mapPathToPageName', () => {
90
+ test('maps root to index', () => {
91
+ expect(mapPathToPageName('/')).toBe('index');
92
+ });
93
+
94
+ test('removes leading slash', () => {
95
+ expect(mapPathToPageName('/about')).toBe('about');
96
+ expect(mapPathToPageName('/blog/post')).toBe('blog/post');
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * File Utilities
3
+ * Common file path and extension handling functions
4
+ */
5
+
6
+ /**
7
+ * Check if a filename has a .json extension
8
+ */
9
+ export const isJSONFile = (name: string): boolean => name.endsWith('.json');
10
+
11
+ /**
12
+ * Check if a filename has a .js extension
13
+ */
14
+ export const isJSFile = (name: string): boolean => name.endsWith('.js');
15
+
16
+ /**
17
+ * Check if a filename has a .css extension
18
+ */
19
+ export const isCSSFile = (name: string): boolean => name.endsWith('.css');
20
+
21
+ /**
22
+ * Remove the file extension from a filename
23
+ * @example stripExtension('page.json') => 'page'
24
+ * @example stripExtension('component.test.ts') => 'component.test'
25
+ */
26
+ export const stripExtension = (name: string): string => {
27
+ const lastDotIndex = name.lastIndexOf('.');
28
+ return lastDotIndex > 0 ? name.substring(0, lastDotIndex) : name;
29
+ };
30
+
31
+ /**
32
+ * Get the base filename from a path
33
+ * @example getBaseName('/path/to/file.json') => 'file.json'
34
+ */
35
+ export const getBaseName = (filePath: string): string => {
36
+ const parts = filePath.split('/');
37
+ return parts[parts.length - 1] || '';
38
+ };
39
+
40
+ /**
41
+ * Map page filename to route path
42
+ * @example mapPageNameToPath('index') => '/'
43
+ * @example mapPageNameToPath('about') => '/about'
44
+ */
45
+ export const mapPageNameToPath = (pageName: string): string => {
46
+ return pageName === 'index' ? '/' : `/${pageName}`;
47
+ };
48
+
49
+ /**
50
+ * Map route path to page filename
51
+ * @example mapPathToPageName('/') => 'index'
52
+ * @example mapPathToPageName('/about') => 'about'
53
+ */
54
+ export const mapPathToPageName = (path: string): string => {
55
+ return path === '/' ? 'index' : path.substring(1);
56
+ };