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,208 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { createErrorResponse, handleRouteError, withErrorHandling } from './errorHandler';
|
|
3
|
+
|
|
4
|
+
describe('errorHandler middleware', () => {
|
|
5
|
+
describe('createErrorResponse', () => {
|
|
6
|
+
test('creates error response with Error object', () => {
|
|
7
|
+
const error = new Error('Test error');
|
|
8
|
+
const response = createErrorResponse(error);
|
|
9
|
+
expect(response.status).toBe(500);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('creates error response with string error', async () => {
|
|
13
|
+
const error = 'Test error message';
|
|
14
|
+
const response = createErrorResponse(error);
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
expect(data.error).toBe('Test error message');
|
|
17
|
+
expect(data.status).toBe(500);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('uses custom status code', () => {
|
|
21
|
+
const error = new Error('Not found');
|
|
22
|
+
const response = createErrorResponse(error, 404);
|
|
23
|
+
expect(response.status).toBe(404);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('includes custom message', async () => {
|
|
27
|
+
const error = new Error('Internal error');
|
|
28
|
+
const response = createErrorResponse(error, 500, 'Custom message');
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
expect(data.message).toBe('Custom message');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sets correct content type', () => {
|
|
34
|
+
const error = new Error('Test');
|
|
35
|
+
const response = createErrorResponse(error);
|
|
36
|
+
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('response body includes error and status', async () => {
|
|
40
|
+
const error = new Error('Test error');
|
|
41
|
+
const response = createErrorResponse(error, 400);
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
expect(data.error).toBe('Test error');
|
|
44
|
+
expect(data.status).toBe(400);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('handles non-Error objects', async () => {
|
|
48
|
+
const error = { code: 'ERR_001' };
|
|
49
|
+
const response = createErrorResponse(error);
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
expect(data.error).toBe('[object Object]');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('handles null error', async () => {
|
|
55
|
+
const response = createErrorResponse(null);
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
expect(data.error).toBe('null');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('handles undefined error', async () => {
|
|
61
|
+
const response = createErrorResponse(undefined);
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
expect(data.error).toBe('undefined');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('handles number error', async () => {
|
|
67
|
+
const response = createErrorResponse(404);
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
expect(data.error).toBe('404');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('handleRouteError', () => {
|
|
74
|
+
test('returns successful response when no error', async () => {
|
|
75
|
+
const successResponse = new Response('OK', { status: 200 });
|
|
76
|
+
const handler = async () => successResponse;
|
|
77
|
+
const response = await handleRouteError(handler);
|
|
78
|
+
expect(response).toBe(successResponse);
|
|
79
|
+
expect(response.status).toBe(200);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('catches and returns error response on failure', async () => {
|
|
83
|
+
const handler = async () => {
|
|
84
|
+
throw new Error('Handler failed');
|
|
85
|
+
};
|
|
86
|
+
const response = await handleRouteError(handler);
|
|
87
|
+
expect(response.status).toBe(500);
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
expect(data.error).toBe('Handler failed');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('uses default error message', async () => {
|
|
93
|
+
const handler = async () => {
|
|
94
|
+
throw new Error('Test');
|
|
95
|
+
};
|
|
96
|
+
const response = await handleRouteError(handler);
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
expect(data.message).toBe('Internal server error');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('uses custom error message', async () => {
|
|
102
|
+
const handler = async () => {
|
|
103
|
+
throw new Error('Test');
|
|
104
|
+
};
|
|
105
|
+
const response = await handleRouteError(handler, 'Custom error');
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
expect(data.message).toBe('Custom error');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('handles async handler with delay', async () => {
|
|
111
|
+
const handler = async () => {
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
113
|
+
return new Response('OK');
|
|
114
|
+
};
|
|
115
|
+
const response = await handleRouteError(handler);
|
|
116
|
+
expect(response.status).toBe(200);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('handles non-Error thrown values', async () => {
|
|
120
|
+
const handler = async () => {
|
|
121
|
+
throw 'String error';
|
|
122
|
+
};
|
|
123
|
+
const response = await handleRouteError(handler);
|
|
124
|
+
expect(response.status).toBe(500);
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
expect(data.error).toBe('String error');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('withErrorHandling', () => {
|
|
131
|
+
test('returns wrapped handler that handles errors', async () => {
|
|
132
|
+
const handler = async () => {
|
|
133
|
+
throw new Error('Test error');
|
|
134
|
+
};
|
|
135
|
+
const wrapped = withErrorHandling(handler);
|
|
136
|
+
const response = await wrapped();
|
|
137
|
+
expect(response.status).toBe(500);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('wrapped handler passes through successful responses', async () => {
|
|
141
|
+
const handler = async () => new Response('Success', { status: 200 });
|
|
142
|
+
const wrapped = withErrorHandling(handler);
|
|
143
|
+
const response = await wrapped();
|
|
144
|
+
expect(response.status).toBe(200);
|
|
145
|
+
expect(await response.text()).toBe('Success');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('wrapped handler uses custom error message', async () => {
|
|
149
|
+
const handler = async () => {
|
|
150
|
+
throw new Error('Test');
|
|
151
|
+
};
|
|
152
|
+
const wrapped = withErrorHandling(handler, 'Custom error message');
|
|
153
|
+
const response = await wrapped();
|
|
154
|
+
const data = await response.json();
|
|
155
|
+
expect(data.message).toBe('Custom error message');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('wrapped handler passes arguments correctly', async () => {
|
|
159
|
+
const handler = async (req: Request, id: string) => {
|
|
160
|
+
return new Response(`Hello ${id}`);
|
|
161
|
+
};
|
|
162
|
+
const wrapped = withErrorHandling(handler);
|
|
163
|
+
const req = new Request('http://localhost/test');
|
|
164
|
+
const response = await wrapped(req, 'world');
|
|
165
|
+
expect(await response.text()).toBe('Hello world');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('wrapped handler catches errors with arguments', async () => {
|
|
169
|
+
const handler = async (req: Request, shouldFail: boolean) => {
|
|
170
|
+
if (shouldFail) throw new Error('Failed');
|
|
171
|
+
return new Response('OK');
|
|
172
|
+
};
|
|
173
|
+
const wrapped = withErrorHandling(handler);
|
|
174
|
+
const req = new Request('http://localhost/test');
|
|
175
|
+
|
|
176
|
+
const successResponse = await wrapped(req, false);
|
|
177
|
+
expect(successResponse.status).toBe(200);
|
|
178
|
+
|
|
179
|
+
const errorResponse = await wrapped(req, true);
|
|
180
|
+
expect(errorResponse.status).toBe(500);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('wrapped handler preserves response headers', async () => {
|
|
184
|
+
const handler = async () => {
|
|
185
|
+
return new Response('OK', {
|
|
186
|
+
headers: { 'X-Custom': 'value' }
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const wrapped = withErrorHandling(handler);
|
|
190
|
+
const response = await wrapped();
|
|
191
|
+
expect(response.headers.get('X-Custom')).toBe('value');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('multiple wrapped handlers work independently', async () => {
|
|
195
|
+
const handler1 = async () => new Response('One');
|
|
196
|
+
const handler2 = async () => { throw new Error('Two'); };
|
|
197
|
+
|
|
198
|
+
const wrapped1 = withErrorHandling(handler1);
|
|
199
|
+
const wrapped2 = withErrorHandling(handler2);
|
|
200
|
+
|
|
201
|
+
const response1 = await wrapped1();
|
|
202
|
+
const response2 = await wrapped2();
|
|
203
|
+
|
|
204
|
+
expect(response1.status).toBe(200);
|
|
205
|
+
expect(response2.status).toBe(500);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Middleware
|
|
3
|
+
* Centralized error handling for routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ErrorResponse {
|
|
7
|
+
error: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
status: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create an error response
|
|
14
|
+
*/
|
|
15
|
+
export function createErrorResponse(
|
|
16
|
+
error: unknown,
|
|
17
|
+
status: number = 500,
|
|
18
|
+
message?: string
|
|
19
|
+
): Response {
|
|
20
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
21
|
+
const response: ErrorResponse = {
|
|
22
|
+
error: errorMessage,
|
|
23
|
+
status,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (message) {
|
|
27
|
+
response.message = message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Response(JSON.stringify(response), {
|
|
31
|
+
status,
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handle errors in async route handlers
|
|
40
|
+
*/
|
|
41
|
+
export async function handleRouteError(
|
|
42
|
+
handler: () => Promise<Response>,
|
|
43
|
+
errorMessage: string = 'Internal server error'
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
try {
|
|
46
|
+
return await handler();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return createErrorResponse(error, 500, errorMessage);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wrap a route handler with error handling
|
|
54
|
+
*/
|
|
55
|
+
export function withErrorHandling<T extends unknown[]>(
|
|
56
|
+
handler: (...args: T) => Promise<Response>,
|
|
57
|
+
errorMessage?: string
|
|
58
|
+
): (...args: T) => Promise<Response> {
|
|
59
|
+
return async (...args: T) => {
|
|
60
|
+
return handleRouteError(() => handler(...args), errorMessage);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
|
2
|
+
import { logRequest, logError, logResponseTime, withLogging, type LogOptions } from './logger';
|
|
3
|
+
|
|
4
|
+
describe('logger middleware', () => {
|
|
5
|
+
// Note: These functions don't console.log in the implementation,
|
|
6
|
+
// so we're testing they execute without errors and honor the options
|
|
7
|
+
|
|
8
|
+
describe('logRequest', () => {
|
|
9
|
+
test('executes without error when enabled', () => {
|
|
10
|
+
const req = new Request('http://localhost:3000/api/test', {
|
|
11
|
+
method: 'GET'
|
|
12
|
+
});
|
|
13
|
+
expect(() => logRequest(req)).not.toThrow();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('executes without error when disabled', () => {
|
|
17
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
18
|
+
const options: LogOptions = { logRequests: false };
|
|
19
|
+
expect(() => logRequest(req, options)).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('handles different HTTP methods', () => {
|
|
23
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
24
|
+
for (const method of methods) {
|
|
25
|
+
const req = new Request('http://localhost:3000/api/test', { method });
|
|
26
|
+
expect(() => logRequest(req)).not.toThrow();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('handles requests with query parameters', () => {
|
|
31
|
+
const req = new Request('http://localhost:3000/api/test?foo=bar&baz=qux');
|
|
32
|
+
expect(() => logRequest(req)).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('handles requests with different paths', () => {
|
|
36
|
+
const paths = ['/api/test', '/api/users/123', '/health', '/'];
|
|
37
|
+
for (const path of paths) {
|
|
38
|
+
const req = new Request(`http://localhost:3000${path}`);
|
|
39
|
+
expect(() => logRequest(req)).not.toThrow();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('respects default options', () => {
|
|
44
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
45
|
+
expect(() => logRequest(req, {})).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('logError', () => {
|
|
50
|
+
test('executes without error for Error objects', () => {
|
|
51
|
+
const error = new Error('Test error');
|
|
52
|
+
expect(() => logError(error)).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('executes without error for string errors', () => {
|
|
56
|
+
expect(() => logError('String error')).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('executes without error when disabled', () => {
|
|
60
|
+
const error = new Error('Test');
|
|
61
|
+
const options: LogOptions = { logErrors: false };
|
|
62
|
+
expect(() => logError(error, undefined, options)).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('handles error with context', () => {
|
|
66
|
+
const error = new Error('Test error');
|
|
67
|
+
expect(() => logError(error, 'API Handler')).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('handles error without context', () => {
|
|
71
|
+
const error = new Error('Test error');
|
|
72
|
+
expect(() => logError(error)).not.toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('handles Error with stack trace', () => {
|
|
76
|
+
const error = new Error('Test error');
|
|
77
|
+
error.stack = 'Error: Test error\n at test.ts:10:5';
|
|
78
|
+
expect(() => logError(error)).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('handles non-Error objects', () => {
|
|
82
|
+
expect(() => logError({ message: 'Custom error' })).not.toThrow();
|
|
83
|
+
expect(() => logError(null)).not.toThrow();
|
|
84
|
+
expect(() => logError(undefined)).not.toThrow();
|
|
85
|
+
expect(() => logError(42)).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('respects default options', () => {
|
|
89
|
+
const error = new Error('Test');
|
|
90
|
+
expect(() => logError(error, undefined, {})).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('logResponseTime', () => {
|
|
95
|
+
test('executes without error when enabled', () => {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
98
|
+
expect(() => logResponseTime(startTime, req)).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('executes without error when disabled', () => {
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
104
|
+
const options: LogOptions = { logResponseTime: false };
|
|
105
|
+
expect(() => logResponseTime(startTime, req, options)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('handles past start time', () => {
|
|
109
|
+
const startTime = Date.now() - 1000; // 1 second ago
|
|
110
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
111
|
+
expect(() => logResponseTime(startTime, req)).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('handles current start time', () => {
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
117
|
+
expect(() => logResponseTime(startTime, req)).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('respects default options', () => {
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
123
|
+
expect(() => logResponseTime(startTime, req, {})).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('withLogging', () => {
|
|
128
|
+
test('wrapped handler returns successful response', async () => {
|
|
129
|
+
const handler = async () => new Response('OK', { status: 200 });
|
|
130
|
+
const wrapped = withLogging(handler);
|
|
131
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
132
|
+
const response = await wrapped(req);
|
|
133
|
+
expect(response.status).toBe(200);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('wrapped handler logs and rethrows errors', async () => {
|
|
137
|
+
const handler = async () => {
|
|
138
|
+
throw new Error('Handler error');
|
|
139
|
+
};
|
|
140
|
+
const wrapped = withLogging(handler);
|
|
141
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
142
|
+
|
|
143
|
+
await expect(wrapped(req)).rejects.toThrow('Handler error');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('wrapped handler passes arguments correctly', async () => {
|
|
147
|
+
const handler = async (req: Request, id: string) => {
|
|
148
|
+
return new Response(`ID: ${id}`);
|
|
149
|
+
};
|
|
150
|
+
const wrapped = withLogging(handler);
|
|
151
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
152
|
+
const response = await wrapped(req, '123');
|
|
153
|
+
expect(await response.text()).toBe('ID: 123');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('wrapped handler with custom options', async () => {
|
|
157
|
+
const handler = async () => new Response('OK');
|
|
158
|
+
const options: LogOptions = {
|
|
159
|
+
logRequests: false,
|
|
160
|
+
logErrors: false,
|
|
161
|
+
logResponseTime: false
|
|
162
|
+
};
|
|
163
|
+
const wrapped = withLogging(handler, options);
|
|
164
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
165
|
+
const response = await wrapped(req);
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('wrapped handler preserves response headers', async () => {
|
|
170
|
+
const handler = async () => {
|
|
171
|
+
return new Response('OK', {
|
|
172
|
+
headers: { 'X-Custom': 'value' }
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const wrapped = withLogging(handler);
|
|
176
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
177
|
+
const response = await wrapped(req);
|
|
178
|
+
expect(response.headers.get('X-Custom')).toBe('value');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('wrapped handler with multiple arguments', async () => {
|
|
182
|
+
const handler = async (req: Request, a: number, b: number) => {
|
|
183
|
+
return new Response(`Sum: ${a + b}`);
|
|
184
|
+
};
|
|
185
|
+
const wrapped = withLogging(handler);
|
|
186
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
187
|
+
const response = await wrapped(req, 5, 10);
|
|
188
|
+
expect(await response.text()).toBe('Sum: 15');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('wrapped handler logs on success and error', async () => {
|
|
192
|
+
const successHandler = async () => new Response('OK');
|
|
193
|
+
const errorHandler = async () => { throw new Error('Failed'); };
|
|
194
|
+
|
|
195
|
+
const wrappedSuccess = withLogging(successHandler);
|
|
196
|
+
const wrappedError = withLogging(errorHandler);
|
|
197
|
+
|
|
198
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
199
|
+
|
|
200
|
+
const successResponse = await wrappedSuccess(req);
|
|
201
|
+
expect(successResponse.status).toBe(200);
|
|
202
|
+
|
|
203
|
+
await expect(wrappedError(req)).rejects.toThrow('Failed');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('wrapped handler handles async operations', async () => {
|
|
207
|
+
const handler = async () => {
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
209
|
+
return new Response('Delayed OK');
|
|
210
|
+
};
|
|
211
|
+
const wrapped = withLogging(handler);
|
|
212
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
213
|
+
const response = await wrapped(req);
|
|
214
|
+
expect(await response.text()).toBe('Delayed OK');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('multiple wrapped handlers work independently', async () => {
|
|
218
|
+
const handler1 = async () => new Response('One');
|
|
219
|
+
const handler2 = async () => new Response('Two');
|
|
220
|
+
|
|
221
|
+
const wrapped1 = withLogging(handler1);
|
|
222
|
+
const wrapped2 = withLogging(handler2);
|
|
223
|
+
|
|
224
|
+
const req = new Request('http://localhost:3000/api/test');
|
|
225
|
+
|
|
226
|
+
const response1 = await wrapped1(req);
|
|
227
|
+
const response2 = await wrapped2(req);
|
|
228
|
+
|
|
229
|
+
expect(await response1.text()).toBe('One');
|
|
230
|
+
expect(await response2.text()).toBe('Two');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Middleware
|
|
3
|
+
* Request logging for development and debugging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface LogOptions {
|
|
7
|
+
logRequests?: boolean;
|
|
8
|
+
logErrors?: boolean;
|
|
9
|
+
logResponseTime?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_OPTIONS: LogOptions = {
|
|
13
|
+
logRequests: true,
|
|
14
|
+
logErrors: true,
|
|
15
|
+
logResponseTime: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Log a request
|
|
20
|
+
*/
|
|
21
|
+
export function logRequest(
|
|
22
|
+
req: Request,
|
|
23
|
+
options: LogOptions = {}
|
|
24
|
+
): void {
|
|
25
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
26
|
+
|
|
27
|
+
if (!opts.logRequests) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = new URL(req.url);
|
|
32
|
+
const timestamp = new Date().toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Log an error
|
|
37
|
+
*/
|
|
38
|
+
export function logError(
|
|
39
|
+
error: unknown,
|
|
40
|
+
context?: string,
|
|
41
|
+
options: LogOptions = {}
|
|
42
|
+
): void {
|
|
43
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
44
|
+
|
|
45
|
+
if (!opts.logErrors) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const timestamp = new Date().toISOString();
|
|
50
|
+
const contextStr = context ? ` [${context}]` : '';
|
|
51
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
52
|
+
|
|
53
|
+
if (error instanceof Error && error.stack) {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log response time
|
|
59
|
+
*/
|
|
60
|
+
export function logResponseTime(
|
|
61
|
+
startTime: number,
|
|
62
|
+
req: Request,
|
|
63
|
+
options: LogOptions = {}
|
|
64
|
+
): void {
|
|
65
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
66
|
+
|
|
67
|
+
if (!opts.logResponseTime) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = new URL(req.url);
|
|
72
|
+
const duration = Date.now() - startTime;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a route handler with logging
|
|
77
|
+
*/
|
|
78
|
+
export function withLogging<T extends unknown[]>(
|
|
79
|
+
handler: (...args: T) => Promise<Response>,
|
|
80
|
+
options: LogOptions = {}
|
|
81
|
+
): (...args: T) => Promise<Response> {
|
|
82
|
+
return async (...args: T) => {
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
const req = args[0] as Request;
|
|
85
|
+
|
|
86
|
+
logRequest(req, options);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = await handler(...args);
|
|
90
|
+
logResponseTime(startTime, req, options);
|
|
91
|
+
return response;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logError(error, req.method, options);
|
|
94
|
+
logResponseTime(startTime, req, options);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|