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.
- package/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- 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
|
+
|