cumstack 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/LICENSE +21 -0
- package/README.md +3 -0
- package/cli/build.js +19 -0
- package/cli/builder.js +172 -0
- package/cli/create.js +36 -0
- package/cli/dev.js +109 -0
- package/cli/index.js +65 -0
- package/index.js +22 -0
- package/package.json +67 -0
- package/src/app/client/Twink.js +57 -0
- package/src/app/client/components.js +28 -0
- package/src/app/client/hmr.js +161 -0
- package/src/app/client/index.js +144 -0
- package/src/app/client.js +599 -0
- package/src/app/index.js +8 -0
- package/src/app/server/hono-utils.js +292 -0
- package/src/app/server/index.js +457 -0
- package/src/app/server/jsx.js +168 -0
- package/src/app/server.js +373 -0
- package/src/app/shared/i18n.js +271 -0
- package/src/app/shared/language-codes.js +199 -0
- package/src/app/shared/reactivity.js +259 -0
- package/src/app/shared/router.js +153 -0
- package/src/app/shared/utils.js +127 -0
- package/templates/monorepo/README.md +27 -0
- package/templates/monorepo/api/package.json +13 -0
- package/templates/monorepo/app/package.json +19 -0
- package/templates/monorepo/app/src/entry.client.jsx +4 -0
- package/templates/monorepo/app/src/entry.server.jsx +14 -0
- package/templates/monorepo/app/src/main.css +7 -0
- package/templates/monorepo/app/src/pages/404.jsx +9 -0
- package/templates/monorepo/app/src/pages/Home.jsx +8 -0
- package/templates/monorepo/app/wrangler.toml +35 -0
- package/templates/monorepo/package.json +18 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cumstack Hono Utilities
|
|
3
|
+
* helper functions for working with hono
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* create a middleware for i18n detection
|
|
8
|
+
* @param {Object} config - i18n configuration
|
|
9
|
+
* @param {string} config.fallbackLng - Fallback language
|
|
10
|
+
* @param {boolean} config.explicitRouting - Enable explicit language routing
|
|
11
|
+
* @param {Array<string>} config.supportedLanguages - Supported languages
|
|
12
|
+
* @param {string} config.defaultLng - Default language setting
|
|
13
|
+
* @returns {Function} Hono middleware function
|
|
14
|
+
*/
|
|
15
|
+
export function createI18nMiddleware(config) {
|
|
16
|
+
return async (c, next) => {
|
|
17
|
+
const url = new URL(c.req.url);
|
|
18
|
+
let language = config.fallbackLng;
|
|
19
|
+
|
|
20
|
+
// check url path first if explicit routing
|
|
21
|
+
if (config.explicitRouting) {
|
|
22
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
23
|
+
if (pathSegments.length > 0 && config.supportedLanguages.includes(pathSegments[0])) language = pathSegments[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// check accept-language header if auto-detect
|
|
27
|
+
if (language === config.fallbackLng && config.defaultLng === 'auto') {
|
|
28
|
+
const acceptLang = c.req.header('accept-language');
|
|
29
|
+
if (acceptLang) {
|
|
30
|
+
const detected = acceptLang
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((l) => l.split(';')[0].trim().split('-')[0])
|
|
33
|
+
.find((l) => config.supportedLanguages.includes(l));
|
|
34
|
+
if (detected) language = detected;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
c.set('language', language);
|
|
38
|
+
await next();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* create a middleware for CORS handling
|
|
44
|
+
* @param {Object} [options] - CORS configuration options
|
|
45
|
+
* @param {string} [options.origin] - Allowed origin
|
|
46
|
+
* @param {Array<string>} [options.methods] - Allowed HTTP methods
|
|
47
|
+
* @param {Array<string>} [options.allowHeaders] - Allowed headers
|
|
48
|
+
* @param {Array<string>} [options.exposeHeaders] - Exposed headers
|
|
49
|
+
* @param {boolean} [options.credentials] - Allow credentials
|
|
50
|
+
* @param {number} [options.maxAge] - Max age for preflight cache
|
|
51
|
+
* @returns {Function} Hono middleware function
|
|
52
|
+
*/
|
|
53
|
+
export function createCorsMiddleware(options = {}) {
|
|
54
|
+
const {
|
|
55
|
+
origin = '*',
|
|
56
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
57
|
+
allowHeaders = ['Content-Type', 'Authorization'],
|
|
58
|
+
exposeHeaders = [],
|
|
59
|
+
credentials = false,
|
|
60
|
+
maxAge = 86400,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
return async (c, next) => {
|
|
64
|
+
// handle preflight
|
|
65
|
+
if (c.req.method === 'OPTIONS') {
|
|
66
|
+
return new Response(null, {
|
|
67
|
+
status: 204,
|
|
68
|
+
headers: {
|
|
69
|
+
'Access-Control-Allow-Origin': origin,
|
|
70
|
+
'Access-Control-Allow-Methods': methods.join(', '),
|
|
71
|
+
'Access-Control-Allow-Headers': allowHeaders.join(', '),
|
|
72
|
+
'Access-Control-Max-Age': maxAge.toString(),
|
|
73
|
+
...(credentials && { 'Access-Control-Allow-Credentials': 'true' }),
|
|
74
|
+
...(exposeHeaders.length && {
|
|
75
|
+
'Access-Control-Expose-Headers': exposeHeaders.join(', '),
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await next();
|
|
82
|
+
|
|
83
|
+
// add cors headers to response
|
|
84
|
+
c.header('Access-Control-Allow-Origin', origin);
|
|
85
|
+
if (credentials) c.header('Access-Control-Allow-Credentials', 'true');
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* create a middleware for security headers
|
|
91
|
+
* @returns {Function} Hono middleware function that adds security headers
|
|
92
|
+
*/
|
|
93
|
+
export function createSecurityHeadersMiddleware() {
|
|
94
|
+
return async (c, next) => {
|
|
95
|
+
await next();
|
|
96
|
+
|
|
97
|
+
// security headers
|
|
98
|
+
c.header('X-Content-Type-Options', 'nosniff');
|
|
99
|
+
c.header('X-Frame-Options', 'DENY');
|
|
100
|
+
c.header('X-XSS-Protection', '1; mode=block');
|
|
101
|
+
c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* create a middleware for HTTP caching
|
|
107
|
+
* @param {Object} [options] - Cache configuration
|
|
108
|
+
* @param {number} [options.maxAge] - Max age in seconds
|
|
109
|
+
* @param {number} [options.sMaxAge] - Shared cache max age
|
|
110
|
+
* @param {number} [options.staleWhileRevalidate] - Stale while revalidate time
|
|
111
|
+
* @param {number} [options.staleIfError] - Stale if error time
|
|
112
|
+
* @param {boolean} [options.public] - Public cache
|
|
113
|
+
* @param {boolean} [options.private] - Private cache
|
|
114
|
+
* @param {boolean} [options.noCache] - No cache directive
|
|
115
|
+
* @param {boolean} [options.noStore] - No store directive
|
|
116
|
+
* @param {boolean} [options.mustRevalidate] - Must revalidate directive
|
|
117
|
+
* @returns {Function} Hono middleware function
|
|
118
|
+
*/
|
|
119
|
+
export function createCacheMiddleware(options = {}) {
|
|
120
|
+
const {
|
|
121
|
+
maxAge = 0,
|
|
122
|
+
sMaxAge,
|
|
123
|
+
staleWhileRevalidate,
|
|
124
|
+
staleIfError,
|
|
125
|
+
public: isPublic = true,
|
|
126
|
+
private: isPrivate = false,
|
|
127
|
+
noCache = false,
|
|
128
|
+
noStore = false,
|
|
129
|
+
mustRevalidate = false,
|
|
130
|
+
} = options;
|
|
131
|
+
|
|
132
|
+
return async (c, next) => {
|
|
133
|
+
await next();
|
|
134
|
+
const directives = [];
|
|
135
|
+
if (noStore) directives.push('no-store');
|
|
136
|
+
if (noCache) directives.push('no-cache');
|
|
137
|
+
if (isPublic) directives.push('public');
|
|
138
|
+
if (isPrivate) directives.push('private');
|
|
139
|
+
if (mustRevalidate) directives.push('must-revalidate');
|
|
140
|
+
if (maxAge !== undefined) directives.push(`max-age=${maxAge}`);
|
|
141
|
+
if (sMaxAge !== undefined) directives.push(`s-maxage=${sMaxAge}`);
|
|
142
|
+
if (staleWhileRevalidate !== undefined) directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
|
|
143
|
+
if (staleIfError !== undefined) directives.push(`stale-if-error=${staleIfError}`);
|
|
144
|
+
if (directives.length > 0) c.header('Cache-Control', directives.join(', '));
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* create a middleware for response compression
|
|
150
|
+
* @returns {Function} Hono middleware function that adds compression headers
|
|
151
|
+
*/
|
|
152
|
+
export function createCompressionMiddleware() {
|
|
153
|
+
return async (c, next) => {
|
|
154
|
+
await next();
|
|
155
|
+
const acceptEncoding = c.req.header('accept-encoding') || '';
|
|
156
|
+
if (acceptEncoding.includes('gzip')) c.header('Content-Encoding', 'gzip');
|
|
157
|
+
else if (acceptEncoding.includes('deflate')) c.header('Content-Encoding', 'deflate');
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* create a middleware for request logging
|
|
163
|
+
* @returns {Function} Hono middleware function that logs requests
|
|
164
|
+
*/
|
|
165
|
+
export function createLoggerMiddleware() {
|
|
166
|
+
return async (c, next) => {
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
const method = c.req.method;
|
|
169
|
+
const url = c.req.url;
|
|
170
|
+
await next();
|
|
171
|
+
const duration = Date.now() - start;
|
|
172
|
+
const status = c.res.status;
|
|
173
|
+
console.log(`[${new Date().toISOString()}] ${method} ${url} ${status} ${duration}ms`);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* create a middleware for rate limiting
|
|
179
|
+
* @param {Object} [options] - Rate limit configuration
|
|
180
|
+
* @param {number} [options.windowMs] - Time window in milliseconds
|
|
181
|
+
* @param {number} [options.max] - Maximum requests per window
|
|
182
|
+
* @param {Function} [options.keyGenerator] - Function to generate rate limit key
|
|
183
|
+
* @returns {Function} Hono middleware function that enforces rate limits
|
|
184
|
+
*/
|
|
185
|
+
export function createRateLimitMiddleware(options = {}) {
|
|
186
|
+
const {
|
|
187
|
+
windowMs = 60000, // 1 minute
|
|
188
|
+
max = 60, // 60 requests per window
|
|
189
|
+
keyGenerator = (c) => c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
|
|
190
|
+
} = options;
|
|
191
|
+
|
|
192
|
+
const requests = new Map();
|
|
193
|
+
|
|
194
|
+
return async (c, next) => {
|
|
195
|
+
const key = keyGenerator(c);
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
|
|
198
|
+
// clean old entries
|
|
199
|
+
for (const [k, data] of requests.entries()) if (now - data.resetTime > windowMs) requests.delete(k);
|
|
200
|
+
|
|
201
|
+
// get or create entry
|
|
202
|
+
let entry = requests.get(key);
|
|
203
|
+
if (!entry || now - entry.resetTime > windowMs) {
|
|
204
|
+
entry = { count: 0, resetTime: now };
|
|
205
|
+
requests.set(key, entry);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
entry.count++;
|
|
209
|
+
|
|
210
|
+
// check limit
|
|
211
|
+
if (entry.count > max) return c.text('Too Many Requests', 429);
|
|
212
|
+
|
|
213
|
+
// rate limit headers
|
|
214
|
+
c.header('X-RateLimit-Limit', max.toString());
|
|
215
|
+
c.header('X-RateLimit-Remaining', (max - entry.count).toString());
|
|
216
|
+
c.header('X-RateLimit-Reset', new Date(entry.resetTime + windowMs).toISOString());
|
|
217
|
+
|
|
218
|
+
await next();
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* create error response helper
|
|
224
|
+
* @param {Object} c - Hono context
|
|
225
|
+
* @param {Error} error - Error object
|
|
226
|
+
* @param {number} [status=500] - HTTP status code
|
|
227
|
+
* @returns {Response} JSON error response
|
|
228
|
+
*/
|
|
229
|
+
export function errorResponse(c, error, status = 500) {
|
|
230
|
+
console.error('Error:', error);
|
|
231
|
+
const isDev = globalThis.__ENVIRONMENT__ === 'development';
|
|
232
|
+
return c.json(
|
|
233
|
+
{
|
|
234
|
+
error: {
|
|
235
|
+
message: error.message || 'Internal Server Error',
|
|
236
|
+
status,
|
|
237
|
+
...(isDev && { stack: error.stack }),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
status
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* create success response helper
|
|
246
|
+
* @param {Object} c - Hono context
|
|
247
|
+
* @param {*} data - Response data
|
|
248
|
+
* @param {number} [status=200] - HTTP status code
|
|
249
|
+
* @returns {Response} JSON success response
|
|
250
|
+
*/
|
|
251
|
+
export function successResponse(c, data, status = 200) {
|
|
252
|
+
return c.json(
|
|
253
|
+
{
|
|
254
|
+
success: true,
|
|
255
|
+
data,
|
|
256
|
+
},
|
|
257
|
+
status
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* redirect helper
|
|
263
|
+
* @param {Object} c - Hono context
|
|
264
|
+
* @param {string} location - Redirect URL
|
|
265
|
+
* @param {number} [status=302] - HTTP status code
|
|
266
|
+
* @returns {Response} Redirect response
|
|
267
|
+
*/
|
|
268
|
+
export function redirect(c, location, status = 302) {
|
|
269
|
+
return c.redirect(location, status);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* stream HTML response
|
|
274
|
+
* @param {Object} c - Hono context
|
|
275
|
+
* @param {Function} generator - Async generator function that yields HTML chunks
|
|
276
|
+
* @returns {Promise<Response>} Streaming HTML response
|
|
277
|
+
*/
|
|
278
|
+
export async function streamHTML(c, generator) {
|
|
279
|
+
const stream = new ReadableStream({
|
|
280
|
+
async start(controller) {
|
|
281
|
+
controller.enqueue(new TextEncoder().encode('<!DOCTYPE html>'));
|
|
282
|
+
for await (const chunk of generator()) controller.enqueue(new TextEncoder().encode(chunk));
|
|
283
|
+
controller.close();
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
return new Response(stream, {
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
289
|
+
'Transfer-Encoding': 'chunked',
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|