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,350 @@
1
+ /**
2
+ * JSON Loader
3
+ * Utilities for loading and parsing JSON files
4
+ */
5
+
6
+ import { existsSync, readdirSync } from 'fs';
7
+ import type { z } from 'zod';
8
+ import type { ComponentDefinition } from '../shared/types';
9
+ import type { BreakpointConfig } from '../shared/breakpoints';
10
+ import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
11
+ import type { ResponsiveScales } from '../shared/responsiveScaling';
12
+ import { DEFAULT_RESPONSIVE_SCALES } from '../shared/responsiveScaling';
13
+ import type { I18nConfig } from '../shared/types/components';
14
+ import { DEFAULT_I18N_CONFIG, migrateI18nConfig } from '../shared/i18n';
15
+ import { validateComponentDefinition } from '../shared/validation/validators';
16
+ import { projectPaths } from './projectContext';
17
+
18
+ /**
19
+ * Load JSON file from a file path
20
+ */
21
+ export async function loadJSONFile(filePath: string): Promise<string | null> {
22
+ try {
23
+ const file = Bun.file(filePath);
24
+ if (await file.exists()) {
25
+ return await file.text();
26
+ }
27
+ return null;
28
+ } catch (error) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Parse JSON content
35
+ */
36
+ export function parseJSON<T = unknown>(content: string): T {
37
+ return JSON.parse(content);
38
+ }
39
+
40
+ /**
41
+ * Parse and validate JSON content against a zod schema
42
+ * Returns null on validation failure (for backward compatibility)
43
+ */
44
+ export function parseAndValidateJSON<T>(
45
+ content: string,
46
+ schema: z.ZodType<T>
47
+ ): T | null {
48
+ try {
49
+ const parsed = JSON.parse(content);
50
+ const result = schema.safeParse(parsed);
51
+ if (result.success) {
52
+ return result.data;
53
+ }
54
+ // Return parsed value anyway for backward compatibility
55
+ return parsed as T;
56
+ } catch (error) {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Load and parse JSON file
63
+ */
64
+ export async function loadAndParseJSON<T = any>(filePath: string): Promise<T | null> {
65
+ const content = await loadJSONFile(filePath);
66
+ if (!content) return null;
67
+
68
+ try {
69
+ return parseJSON<T>(content);
70
+ } catch (error) {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Load all JSON files from a directory
77
+ */
78
+ export async function loadJSONDirectory(
79
+ dirPath: string
80
+ ): Promise<Map<string, string>> {
81
+ const files = new Map<string, string>();
82
+
83
+ if (!existsSync(dirPath)) {
84
+ return files;
85
+ }
86
+
87
+ const fileList = readdirSync(dirPath);
88
+
89
+ for (const file of fileList) {
90
+ if (file.endsWith('.json')) {
91
+ const name = file.replace('.json', '');
92
+ const content = await loadJSONFile(`${dirPath}/${file}`);
93
+
94
+ if (content) {
95
+ files.set(name, content);
96
+ }
97
+ }
98
+ }
99
+
100
+ return files;
101
+ }
102
+
103
+ /**
104
+ * Load file content as text (for .js, .css, etc.)
105
+ */
106
+ async function loadFileAsText(filePath: string): Promise<string | null> {
107
+ try {
108
+ const file = Bun.file(filePath);
109
+ if (await file.exists()) {
110
+ return await file.text();
111
+ }
112
+ return null;
113
+ } catch (error) {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Load all component definitions from components directory
120
+ * Automatically loads corresponding .js files if they exist
121
+ */
122
+ export async function loadComponentDirectory(
123
+ dirPath: string = './components'
124
+ ): Promise<Map<string, ComponentDefinition>> {
125
+ const components = new Map<string, ComponentDefinition>();
126
+
127
+ if (!existsSync(dirPath)) {
128
+ return components;
129
+ }
130
+
131
+ const files = readdirSync(dirPath);
132
+
133
+ for (const file of files) {
134
+ if (file.endsWith('.json')) {
135
+ const componentName = file.replace('.json', '');
136
+ const content = await loadJSONFile(`${dirPath}/${file}`);
137
+
138
+ if (content) {
139
+ try {
140
+ const parsed = parseJSON<ComponentDefinition>(content);
141
+
142
+ // Validate component definition (logs warnings but doesn't fail - graceful degradation)
143
+ const validationResult = validateComponentDefinition(parsed);
144
+ if (!validationResult.valid) {
145
+ console.warn(`[jsonLoader] Component validation failed for ${componentName}:`,
146
+ validationResult.errors.map(e => `${e.path}: ${e.message}`).join('; '));
147
+ }
148
+ const componentDef = validationResult.valid ? validationResult.data : parsed;
149
+
150
+ // Check if there's a corresponding .js file
151
+ const jsFilePath = `${dirPath}/${componentName}.js`;
152
+ const jsContent = await loadFileAsText(jsFilePath);
153
+
154
+ // Check if there's a corresponding .css file
155
+ const cssFilePath = `${dirPath}/${componentName}.css`;
156
+ const cssContent = await loadFileAsText(cssFilePath);
157
+
158
+ if (!componentDef.component) {
159
+ componentDef.component = {};
160
+ }
161
+
162
+ // If .js file exists, use its content for javascript field
163
+ // This takes precedence over any javascript field in the JSON
164
+ if (jsContent) {
165
+ componentDef.component.javascript = jsContent;
166
+ }
167
+
168
+ // If .css file exists, use its content for css field
169
+ // This takes precedence over any css field in the JSON
170
+ if (cssContent) {
171
+ componentDef.component.css = cssContent;
172
+ }
173
+
174
+ components.set(componentName, componentDef);
175
+ } catch (error) {
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ return components;
182
+ }
183
+
184
+ /**
185
+ * Map page filename to route path
186
+ */
187
+ export function mapPageNameToPath(pageName: string): string {
188
+ return pageName === 'index' ? '/' : `/${pageName}`;
189
+ }
190
+
191
+ /**
192
+ * Map route path to page filename
193
+ */
194
+ export function mapPathToPageName(path: string): string {
195
+ return path === '/' ? 'index' : path.substring(1);
196
+ }
197
+
198
+ // Breakpoint types are now imported from shared/breakpoints
199
+
200
+ /**
201
+ * Load and validate breakpoint configuration from project.config.json
202
+ */
203
+ export async function loadBreakpointConfig(): Promise<BreakpointConfig> {
204
+ try {
205
+ const configContent = await loadJSONFile(projectPaths.config());
206
+ if (configContent) {
207
+ const config = parseJSON<{ breakpoints?: Record<string, number> }>(configContent);
208
+
209
+ if (config.breakpoints && typeof config.breakpoints === 'object') {
210
+ // Preserve all breakpoints from config, filtering out invalid values
211
+ const breakpoints: BreakpointConfig = {};
212
+ for (const [key, value] of Object.entries(config.breakpoints)) {
213
+ if (typeof value === 'number' && value > 0) {
214
+ breakpoints[key] = value;
215
+ }
216
+ }
217
+
218
+ // If we have valid breakpoints, return them; otherwise fall back to defaults
219
+ if (Object.keys(breakpoints).length > 0) {
220
+ return breakpoints;
221
+ }
222
+ }
223
+ }
224
+ } catch (error) {
225
+ }
226
+
227
+ return { ...DEFAULT_BREAKPOINTS };
228
+ }
229
+
230
+ /**
231
+ * Get breakpoint config synchronously (for cases where async is not available)
232
+ * Uses cached value if available, otherwise defaults
233
+ */
234
+ let cachedBreakpoints: BreakpointConfig | null = null;
235
+
236
+ export function getBreakpointConfig(): BreakpointConfig {
237
+ if (cachedBreakpoints) {
238
+ return cachedBreakpoints;
239
+ }
240
+ return DEFAULT_BREAKPOINTS;
241
+ }
242
+
243
+ export function setBreakpointConfig(config: BreakpointConfig): void {
244
+ cachedBreakpoints = config;
245
+ }
246
+
247
+ /**
248
+ * Load and validate responsive scales configuration from project.config.json
249
+ */
250
+ export async function loadResponsiveScalesConfig(): Promise<ResponsiveScales> {
251
+ try {
252
+ const configContent = await loadJSONFile(projectPaths.config());
253
+ if (configContent) {
254
+ const config = parseJSON<{ responsiveScales?: Partial<ResponsiveScales> }>(configContent);
255
+
256
+ if (config.responsiveScales && typeof config.responsiveScales === 'object') {
257
+ // Merge with defaults to fill in missing values
258
+ const scales: ResponsiveScales = {
259
+ ...DEFAULT_RESPONSIVE_SCALES,
260
+ ...config.responsiveScales,
261
+ };
262
+
263
+ return scales;
264
+ }
265
+ }
266
+ } catch (error) {
267
+ }
268
+
269
+ return { ...DEFAULT_RESPONSIVE_SCALES };
270
+ }
271
+
272
+ /**
273
+ * Get responsive scales config synchronously (for cases where async is not available)
274
+ * Uses cached value if available, otherwise defaults
275
+ */
276
+ let cachedResponsiveScales: ResponsiveScales | null = null;
277
+
278
+ export function getResponsiveScalesConfig(): ResponsiveScales {
279
+ if (cachedResponsiveScales) {
280
+ return cachedResponsiveScales;
281
+ }
282
+ return { ...DEFAULT_RESPONSIVE_SCALES };
283
+ }
284
+
285
+ export function setResponsiveScalesConfig(config: ResponsiveScales): void {
286
+ cachedResponsiveScales = config;
287
+ }
288
+
289
+ /**
290
+ * Load and validate i18n configuration from project.config.json
291
+ * Automatically migrates old string[] format to new LocaleConfig[] format
292
+ */
293
+ export async function loadI18nConfig(): Promise<I18nConfig> {
294
+ try {
295
+ const configContent = await loadJSONFile(projectPaths.config());
296
+ if (configContent) {
297
+ const config = parseJSON<{ i18n?: unknown }>(configContent);
298
+ return migrateI18nConfig(config.i18n);
299
+ }
300
+ } catch (error) {
301
+ // Fall through to default
302
+ }
303
+
304
+ return { ...DEFAULT_I18N_CONFIG };
305
+ }
306
+
307
+ /**
308
+ * Get i18n config synchronously (for cases where async is not available)
309
+ * Uses cached value if available, otherwise defaults
310
+ */
311
+ let cachedI18nConfig: I18nConfig | null = null;
312
+
313
+ export function getI18nConfig(): I18nConfig {
314
+ if (cachedI18nConfig) {
315
+ return cachedI18nConfig;
316
+ }
317
+ return { ...DEFAULT_I18N_CONFIG };
318
+ }
319
+
320
+ export function setI18nConfig(config: I18nConfig): void {
321
+ cachedI18nConfig = config;
322
+ }
323
+
324
+ /**
325
+ * Icons configuration from project.config.json
326
+ */
327
+ export interface IconsConfig {
328
+ favicon?: string;
329
+ appleTouchIcon?: string;
330
+ }
331
+
332
+ /**
333
+ * Load icons configuration from project.config.json
334
+ */
335
+ export async function loadIconsConfig(): Promise<IconsConfig> {
336
+ try {
337
+ const configContent = await loadJSONFile(projectPaths.config());
338
+ if (configContent) {
339
+ const config = parseJSON<{ icons?: IconsConfig }>(configContent);
340
+ if (config.icons && typeof config.icons === 'object') {
341
+ return config.icons;
342
+ }
343
+ }
344
+ } catch (error) {
345
+ // Fall through to default
346
+ }
347
+
348
+ return {};
349
+ }
350
+
@@ -0,0 +1,177 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { createCorsHeaders, handleCorsPreflight, type CorsOptions } from './cors';
3
+
4
+ describe('cors middleware', () => {
5
+ describe('createCorsHeaders', () => {
6
+ test('returns default headers with no options', () => {
7
+ const headers = createCorsHeaders();
8
+ expect(headers['Access-Control-Allow-Origin']).toBe('*');
9
+ expect(headers['Access-Control-Allow-Methods']).toBe('GET, POST, PUT, DELETE, OPTIONS');
10
+ expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type, Authorization');
11
+ });
12
+
13
+ test('handles string origin', () => {
14
+ const headers = createCorsHeaders({ origin: 'https://example.com' });
15
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
16
+ });
17
+
18
+ test('handles array origin', () => {
19
+ const headers = createCorsHeaders({
20
+ origin: ['https://example.com', 'https://test.com']
21
+ });
22
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com, https://test.com');
23
+ });
24
+
25
+ test('handles function origin', () => {
26
+ const headers = createCorsHeaders({
27
+ origin: (origin: string) => origin === 'https://example.com'
28
+ });
29
+ // Function origin falls back to '*' since we don't have request origin
30
+ expect(headers['Access-Control-Allow-Origin']).toBe('*');
31
+ });
32
+
33
+ test('handles custom methods', () => {
34
+ const headers = createCorsHeaders({
35
+ methods: ['GET', 'POST']
36
+ });
37
+ expect(headers['Access-Control-Allow-Methods']).toBe('GET, POST');
38
+ });
39
+
40
+ test('handles empty methods array', () => {
41
+ const headers = createCorsHeaders({
42
+ methods: []
43
+ });
44
+ expect(headers['Access-Control-Allow-Methods']).toBeUndefined();
45
+ });
46
+
47
+ test('handles custom allowed headers', () => {
48
+ const headers = createCorsHeaders({
49
+ allowedHeaders: ['X-Custom-Header', 'Content-Type']
50
+ });
51
+ expect(headers['Access-Control-Allow-Headers']).toBe('X-Custom-Header, Content-Type');
52
+ });
53
+
54
+ test('handles empty allowed headers array', () => {
55
+ const headers = createCorsHeaders({
56
+ allowedHeaders: []
57
+ });
58
+ expect(headers['Access-Control-Allow-Headers']).toBeUndefined();
59
+ });
60
+
61
+ test('sets credentials header when enabled', () => {
62
+ const headers = createCorsHeaders({
63
+ credentials: true
64
+ });
65
+ expect(headers['Access-Control-Allow-Credentials']).toBe('true');
66
+ });
67
+
68
+ test('does not set credentials header when disabled', () => {
69
+ const headers = createCorsHeaders({
70
+ credentials: false
71
+ });
72
+ expect(headers['Access-Control-Allow-Credentials']).toBeUndefined();
73
+ });
74
+
75
+ test('merges multiple options', () => {
76
+ const headers = createCorsHeaders({
77
+ origin: 'https://example.com',
78
+ methods: ['GET', 'POST'],
79
+ allowedHeaders: ['X-Token'],
80
+ credentials: true
81
+ });
82
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
83
+ expect(headers['Access-Control-Allow-Methods']).toBe('GET, POST');
84
+ expect(headers['Access-Control-Allow-Headers']).toBe('X-Token');
85
+ expect(headers['Access-Control-Allow-Credentials']).toBe('true');
86
+ });
87
+
88
+ test('overrides defaults with provided options', () => {
89
+ const headers = createCorsHeaders({
90
+ methods: ['GET']
91
+ });
92
+ expect(headers['Access-Control-Allow-Methods']).toBe('GET');
93
+ expect(headers['Access-Control-Allow-Origin']).toBe('*'); // Still defaults
94
+ });
95
+ });
96
+
97
+ describe('handleCorsPreflight', () => {
98
+ test('returns null for non-OPTIONS request', () => {
99
+ const req = new Request('http://localhost:3000/api/test', {
100
+ method: 'GET'
101
+ });
102
+ const response = handleCorsPreflight(req);
103
+ expect(response).toBeNull();
104
+ });
105
+
106
+ test('returns response for OPTIONS request', () => {
107
+ const req = new Request('http://localhost:3000/api/test', {
108
+ method: 'OPTIONS'
109
+ });
110
+ const response = handleCorsPreflight(req);
111
+ expect(response).not.toBeNull();
112
+ expect(response?.status).toBe(204);
113
+ });
114
+
115
+ test('OPTIONS response includes default CORS headers', () => {
116
+ const req = new Request('http://localhost:3000/api/test', {
117
+ method: 'OPTIONS'
118
+ });
119
+ const response = handleCorsPreflight(req);
120
+ expect(response?.headers.get('Access-Control-Allow-Origin')).toBe('*');
121
+ expect(response?.headers.get('Access-Control-Allow-Methods')).toContain('GET');
122
+ });
123
+
124
+ test('OPTIONS response includes custom CORS headers', () => {
125
+ const req = new Request('http://localhost:3000/api/test', {
126
+ method: 'OPTIONS'
127
+ });
128
+ const response = handleCorsPreflight(req, {
129
+ origin: 'https://example.com',
130
+ methods: ['GET', 'POST']
131
+ });
132
+ expect(response?.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
133
+ expect(response?.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST');
134
+ });
135
+
136
+ test('OPTIONS response has no body', async () => {
137
+ const req = new Request('http://localhost:3000/api/test', {
138
+ method: 'OPTIONS'
139
+ });
140
+ const response = handleCorsPreflight(req);
141
+ const body = await response?.text();
142
+ expect(body).toBe('');
143
+ });
144
+
145
+ test('handles POST request', () => {
146
+ const req = new Request('http://localhost:3000/api/test', {
147
+ method: 'POST'
148
+ });
149
+ const response = handleCorsPreflight(req);
150
+ expect(response).toBeNull();
151
+ });
152
+
153
+ test('handles PUT request', () => {
154
+ const req = new Request('http://localhost:3000/api/test', {
155
+ method: 'PUT'
156
+ });
157
+ const response = handleCorsPreflight(req);
158
+ expect(response).toBeNull();
159
+ });
160
+
161
+ test('handles DELETE request', () => {
162
+ const req = new Request('http://localhost:3000/api/test', {
163
+ method: 'DELETE'
164
+ });
165
+ const response = handleCorsPreflight(req);
166
+ expect(response).toBeNull();
167
+ });
168
+
169
+ test('OPTIONS with credentials option', () => {
170
+ const req = new Request('http://localhost:3000/api/test', {
171
+ method: 'OPTIONS'
172
+ });
173
+ const response = handleCorsPreflight(req, { credentials: true });
174
+ expect(response?.headers.get('Access-Control-Allow-Credentials')).toBe('true');
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * CORS Middleware
3
+ * Handles Cross-Origin Resource Sharing headers
4
+ */
5
+
6
+ export interface CorsOptions {
7
+ origin?: string | string[] | ((origin: string) => boolean);
8
+ methods?: string[];
9
+ allowedHeaders?: string[];
10
+ credentials?: boolean;
11
+ }
12
+
13
+ const DEFAULT_OPTIONS: CorsOptions = {
14
+ origin: '*',
15
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
16
+ allowedHeaders: ['Content-Type', 'Authorization'],
17
+ credentials: false,
18
+ };
19
+
20
+ /**
21
+ * Create CORS headers for a response
22
+ */
23
+ export function createCorsHeaders(options: CorsOptions = {}): HeadersInit {
24
+ const opts = { ...DEFAULT_OPTIONS, ...options };
25
+ const headers: HeadersInit = {};
26
+
27
+ // Handle origin
28
+ if (opts.origin) {
29
+ if (typeof opts.origin === 'function') {
30
+ // Dynamic origin - would need request origin to evaluate
31
+ headers['Access-Control-Allow-Origin'] = '*';
32
+ } else if (Array.isArray(opts.origin)) {
33
+ headers['Access-Control-Allow-Origin'] = opts.origin.join(', ');
34
+ } else {
35
+ headers['Access-Control-Allow-Origin'] = opts.origin;
36
+ }
37
+ }
38
+
39
+ // Handle methods
40
+ if (opts.methods && opts.methods.length > 0) {
41
+ headers['Access-Control-Allow-Methods'] = opts.methods.join(', ');
42
+ }
43
+
44
+ // Handle headers
45
+ if (opts.allowedHeaders && opts.allowedHeaders.length > 0) {
46
+ headers['Access-Control-Allow-Headers'] = opts.allowedHeaders.join(', ');
47
+ }
48
+
49
+ // Handle credentials
50
+ if (opts.credentials) {
51
+ headers['Access-Control-Allow-Credentials'] = 'true';
52
+ }
53
+
54
+ return headers;
55
+ }
56
+
57
+ /**
58
+ * Handle CORS preflight requests
59
+ */
60
+ export function handleCorsPreflight(req: Request, options: CorsOptions = {}): Response | null {
61
+ if (req.method === 'OPTIONS') {
62
+ return new Response(null, {
63
+ status: 204,
64
+ headers: createCorsHeaders(options),
65
+ });
66
+ }
67
+ return null;
68
+ }
69
+