create-workerstack 0.1.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/index.js +918 -0
- package/package.json +25 -0
- package/template/.env.example +6 -0
- package/template/better-auth.config.ts +23 -0
- package/template/drizzle.config.ts +10 -0
- package/template/package.json +40 -0
- package/template/public/.assetsignore +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/src/api/index.ts +22 -0
- package/template/src/api/middlewares/auth.ts +15 -0
- package/template/src/client/App.tsx +316 -0
- package/template/src/client/main.tsx +9 -0
- package/template/src/client/tsconfig.json +17 -0
- package/template/src/client.tsx +36 -0
- package/template/src/database/auth-schema.ts +98 -0
- package/template/src/database/db.ts +111 -0
- package/template/src/database/schema.ts +1 -0
- package/template/src/index.tsx +17 -0
- package/template/src/lib/better-auth/index.ts +28 -0
- package/template/src/lib/better-auth/options.ts +29 -0
- package/template/src/services/email.ts +51 -0
- package/template/src/style.css +45 -0
- package/template/src/utils/logger.ts +211 -0
- package/template/src/utils/noCache.ts +20 -0
- package/template/tsconfig.json +18 -0
- package/template/vite.config.ts +34 -0
- package/template/worker-configuration.d.ts +12049 -0
- package/template/wrangler.jsonc +7 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
2
|
+
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
3
|
+
import type { Logger as DrizzleLogger } from 'drizzle-orm/logger'
|
|
4
|
+
import type { Context } from 'hono'
|
|
5
|
+
import postgres from 'postgres'
|
|
6
|
+
import { Logger } from '@/utils/logger'
|
|
7
|
+
import * as schema from './schema'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Database logger adapter that bridges Drizzle ORM logging with our structured logger
|
|
11
|
+
*
|
|
12
|
+
* Implements Drizzle's Logger interface and formats SQL queries for
|
|
13
|
+
* structured logging in Cloudflare Workers.
|
|
14
|
+
*/
|
|
15
|
+
class DatabaseLogger implements DrizzleLogger {
|
|
16
|
+
private logger: Logger
|
|
17
|
+
|
|
18
|
+
constructor(logger: Logger) {
|
|
19
|
+
this.logger = logger
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Logs SQL queries executed by Drizzle ORM
|
|
24
|
+
*
|
|
25
|
+
* @param query - The SQL query string
|
|
26
|
+
* @param params - Query parameters
|
|
27
|
+
*/
|
|
28
|
+
logQuery(query: string, params: unknown[]): void {
|
|
29
|
+
this.logger.debug('Database query executed', {
|
|
30
|
+
query,
|
|
31
|
+
params,
|
|
32
|
+
paramsCount: params.length,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize Drizzle ORM with PostgreSQL via postgres.js
|
|
39
|
+
*
|
|
40
|
+
* This function creates a Drizzle instance with:
|
|
41
|
+
* - Automatic camelCase → snake_case column mapping
|
|
42
|
+
* - Full schema awareness for type-safe queries
|
|
43
|
+
* - PostgreSQL connection via postgres.js
|
|
44
|
+
* - Optional structured query logging
|
|
45
|
+
* - Prefetch disabled for Transaction pool mode compatibility (e.g. Supabase, PgBouncer)
|
|
46
|
+
*
|
|
47
|
+
* @param connectionString - PostgreSQL connection URL
|
|
48
|
+
* @param logger - Optional Logger instance for query logging
|
|
49
|
+
* @returns Drizzle database instance
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Without logging
|
|
53
|
+
* const db = initDB(env.DATABASE_URL)
|
|
54
|
+
*
|
|
55
|
+
* // With logging
|
|
56
|
+
* const logger = createLogger({ context: 'database' })
|
|
57
|
+
* const db = initDB(env.DATABASE_URL, logger)
|
|
58
|
+
* const rows = await db.select().from(schema.exampleTable)
|
|
59
|
+
*/
|
|
60
|
+
export function initDB(
|
|
61
|
+
connectionString: string,
|
|
62
|
+
logger?: Logger
|
|
63
|
+
): PostgresJsDatabase<typeof schema> {
|
|
64
|
+
const client = postgres(connectionString, { prepare: false })
|
|
65
|
+
return drizzle(client, {
|
|
66
|
+
schema,
|
|
67
|
+
casing: 'snake_case',
|
|
68
|
+
logger: logger ? new DatabaseLogger(logger) : undefined,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Type export for use in Hono context and other places
|
|
74
|
+
*/
|
|
75
|
+
export type Database = ReturnType<typeof initDB>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hono middleware that initializes database and adds it to context
|
|
79
|
+
*
|
|
80
|
+
* Automatically initializes Drizzle with PostgreSQL and
|
|
81
|
+
* attaches the database instance to the context. If a logger is available
|
|
82
|
+
* in the context, it will be used for query logging.
|
|
83
|
+
*
|
|
84
|
+
* Expects DATABASE_URL to be set in the environment bindings.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* app.use('*', loggerMiddleware()) // Set up logger first
|
|
88
|
+
* app.use('*', dbMiddleware()) // Then set up database with logging
|
|
89
|
+
*
|
|
90
|
+
* // Using raw Drizzle queries
|
|
91
|
+
* app.get('/api/example', async (c) => {
|
|
92
|
+
* const rows = await c.var.db.select().from(schema.exampleTable)
|
|
93
|
+
* return c.json(rows)
|
|
94
|
+
* })
|
|
95
|
+
*
|
|
96
|
+
*/
|
|
97
|
+
export function dbMiddleware() {
|
|
98
|
+
return async (c: Context, next: () => Promise<void>) => {
|
|
99
|
+
const logger = c.var.logger?.withContext({ component: 'database' })
|
|
100
|
+
const db = initDB(c.env.DATABASE_URL, logger)
|
|
101
|
+
c.set('db', db)
|
|
102
|
+
await next()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extend Hono's context type to include database
|
|
107
|
+
declare module 'hono' {
|
|
108
|
+
interface ContextVariableMap {
|
|
109
|
+
db: Database
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './auth-schema'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { renderer } from './client'
|
|
3
|
+
import { loggerMiddleware } from './utils/logger'
|
|
4
|
+
import { dbMiddleware } from './database/db'
|
|
5
|
+
import api from './api'
|
|
6
|
+
|
|
7
|
+
const app = new Hono()
|
|
8
|
+
|
|
9
|
+
app.use('/api/*', loggerMiddleware())
|
|
10
|
+
app.use('/api/*', dbMiddleware())
|
|
11
|
+
app.use(renderer)
|
|
12
|
+
app.route('/api', api)
|
|
13
|
+
app.get('*', (c) => {
|
|
14
|
+
return c.render(<div id="root"></div>)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export default app
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { betterAuthOptions } from './options';
|
|
4
|
+
import type { Database } from "@/database/db";
|
|
5
|
+
|
|
6
|
+
import * as schema from "@/database/schema";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Better Auth Instance
|
|
10
|
+
*/
|
|
11
|
+
export const auth = (db: Database, env: CloudflareBindings) => {
|
|
12
|
+
return betterAuth({
|
|
13
|
+
...betterAuthOptions(env),
|
|
14
|
+
database: drizzleAdapter(db, {
|
|
15
|
+
provider: "pg",
|
|
16
|
+
schema,
|
|
17
|
+
}),
|
|
18
|
+
baseURL: env.BETTER_AUTH_URL!,
|
|
19
|
+
secret: env.BETTER_AUTH_SECRET!,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SessionResult = NonNullable<Awaited<ReturnType<ReturnType<typeof auth>["api"]["getSession"]>>>;
|
|
24
|
+
|
|
25
|
+
export type AuthType = {
|
|
26
|
+
user: SessionResult["user"] | null
|
|
27
|
+
session: SessionResult["session"] | null
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createEmailService } from "@/services/email";
|
|
2
|
+
import { BetterAuthOptions } from 'better-auth';
|
|
3
|
+
import { admin } from "better-auth/plugins";
|
|
4
|
+
|
|
5
|
+
export const betterAuthOptions = (env: CloudflareBindings): BetterAuthOptions => {
|
|
6
|
+
const emailService = createEmailService(env.RESEND_API_KEY);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
appName: 'YOUR_APP_NAME',
|
|
10
|
+
basePath: '/api/auth',
|
|
11
|
+
|
|
12
|
+
emailAndPassword: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
requireEmailVerification: false,
|
|
15
|
+
sendResetPassword: async ({ user, url }) => {
|
|
16
|
+
await emailService.sendPasswordReset(user.email, url);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
emailVerification: {
|
|
20
|
+
sendOnSignUp: true,
|
|
21
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
22
|
+
await emailService.sendVerificationEmail(user.email, url);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
plugins: [
|
|
26
|
+
admin(),
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Resend } from "resend";
|
|
2
|
+
|
|
3
|
+
// TODO: Update with your app name and email domain
|
|
4
|
+
const FROM_EMAIL = "App <noreply@example.com>";
|
|
5
|
+
|
|
6
|
+
export function createEmailService(apiKey: string) {
|
|
7
|
+
const resend = new Resend(apiKey);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
async sendPasswordReset(to: string, url: string) {
|
|
11
|
+
await resend.emails.send({
|
|
12
|
+
from: FROM_EMAIL,
|
|
13
|
+
to,
|
|
14
|
+
subject: "Reset your password",
|
|
15
|
+
html: `
|
|
16
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
17
|
+
<h2 style="color: #1a1a2e;">Reset your password</h2>
|
|
18
|
+
<p>You requested a password reset. Click the button below to create a new password:</p>
|
|
19
|
+
<a href="${url}" style="display: inline-block; background-color: #7c3aed; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; margin: 16px 0;">
|
|
20
|
+
Reset Password
|
|
21
|
+
</a>
|
|
22
|
+
<p style="color: #666; font-size: 14px;">If you didn't request this change, you can safely ignore this email.</p>
|
|
23
|
+
<p style="color: #666; font-size: 14px;">This link expires in 1 hour.</p>
|
|
24
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 24px 0;" />
|
|
25
|
+
<p style="color: #999; font-size: 12px;">Sent by your app</p>
|
|
26
|
+
</div>
|
|
27
|
+
`,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async sendVerificationEmail(to: string, url: string) {
|
|
32
|
+
await resend.emails.send({
|
|
33
|
+
from: FROM_EMAIL,
|
|
34
|
+
to,
|
|
35
|
+
subject: "Verify your email",
|
|
36
|
+
html: `
|
|
37
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
38
|
+
<h2 style="color: #1a1a2e;">Verify your email</h2>
|
|
39
|
+
<p>Thanks for signing up! Click the button below to verify your email address:</p>
|
|
40
|
+
<a href="${url}" style="display: inline-block; background-color: #7c3aed; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; margin: 16px 0;">
|
|
41
|
+
Verify Email
|
|
42
|
+
</a>
|
|
43
|
+
<p style="color: #666; font-size: 14px;">If you didn't create an account, you can safely ignore this email.</p>
|
|
44
|
+
<hr style="border: none; border-top: 1px solid #eee; margin: 24px 0;" />
|
|
45
|
+
<p style="color: #999; font-size: 12px;">Sent by your app</p>
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html {
|
|
10
|
+
scroll-behavior: smooth;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
background-color: #0A0A0F;
|
|
15
|
+
color: #F1F5F9;
|
|
16
|
+
-webkit-font-smoothing: antialiased;
|
|
17
|
+
-moz-osx-font-smoothing: grayscale;
|
|
18
|
+
overflow-x: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
a {
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
color: inherit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes float {
|
|
27
|
+
0%, 100% { transform: translateY(0px); }
|
|
28
|
+
50% { transform: translateY(-12px); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes fadeInUp {
|
|
32
|
+
from {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transform: translateY(30px);
|
|
35
|
+
}
|
|
36
|
+
to {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
transform: translateY(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@keyframes pulse {
|
|
43
|
+
0%, 100% { opacity: 0.4; }
|
|
44
|
+
50% { opacity: 0.8; }
|
|
45
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Log levels following standard severity
|
|
5
|
+
*/
|
|
6
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Structured log entry compatible with Cloudflare Workers Logs
|
|
10
|
+
* All fields are automatically indexed for querying
|
|
11
|
+
*/
|
|
12
|
+
export interface LogEntry {
|
|
13
|
+
timestamp: string
|
|
14
|
+
level: LogLevel
|
|
15
|
+
message: string
|
|
16
|
+
requestId?: string
|
|
17
|
+
traceId?: string
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Logger configuration options
|
|
23
|
+
*/
|
|
24
|
+
export interface LoggerConfig {
|
|
25
|
+
minLevel?: LogLevel
|
|
26
|
+
context?: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
30
|
+
debug: 0,
|
|
31
|
+
info: 1,
|
|
32
|
+
warn: 2,
|
|
33
|
+
error: 3,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Structured JSON logger for Cloudflare Workers
|
|
38
|
+
*
|
|
39
|
+
* Outputs logs in JSON format for automatic indexing by Workers Logs.
|
|
40
|
+
* Supports contextual metadata, request correlation, and flexible log levels.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const logger = createLogger({ requestId: 'abc-123' })
|
|
44
|
+
* logger.info('User logged in', { userId: '456' })
|
|
45
|
+
* // Output: {"timestamp":"2025-12-08T...","level":"info","message":"User logged in","requestId":"abc-123","userId":"456"}
|
|
46
|
+
*/
|
|
47
|
+
export class Logger {
|
|
48
|
+
private context: Record<string, unknown>
|
|
49
|
+
private minLevel: number
|
|
50
|
+
|
|
51
|
+
constructor(config: LoggerConfig = {}) {
|
|
52
|
+
this.context = config.context || {}
|
|
53
|
+
this.minLevel = LOG_LEVELS[config.minLevel || 'info']
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a child logger with additional context
|
|
58
|
+
* The new context is merged with the parent context
|
|
59
|
+
*/
|
|
60
|
+
withContext(additionalContext: Record<string, unknown>): Logger {
|
|
61
|
+
return new Logger({
|
|
62
|
+
minLevel: this.getMinLevelName(),
|
|
63
|
+
context: { ...this.context, ...additionalContext },
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Logs a message at the specified level
|
|
69
|
+
*/
|
|
70
|
+
log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
|
|
71
|
+
if (LOG_LEVELS[level] < this.minLevel) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entry: LogEntry = {
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
level,
|
|
78
|
+
message,
|
|
79
|
+
...this.context,
|
|
80
|
+
...context,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Truncate if exceeds Cloudflare's 256 KB limit
|
|
84
|
+
const logString = JSON.stringify(entry)
|
|
85
|
+
if (logString.length > 256000) {
|
|
86
|
+
console.log(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
...entry,
|
|
89
|
+
message: entry.message.substring(0, 1000) + '... [truncated]',
|
|
90
|
+
_truncated: true,
|
|
91
|
+
_originalSize: logString.length,
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
} else {
|
|
95
|
+
console.log(logString)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Log debug message (lowest priority)
|
|
101
|
+
*/
|
|
102
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
103
|
+
this.log('debug', message, context)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Log info message (normal priority)
|
|
108
|
+
*/
|
|
109
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
110
|
+
this.log('info', message, context)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Log warning message (elevated priority)
|
|
115
|
+
*/
|
|
116
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
117
|
+
this.log('warn', message, context)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log error message (highest priority)
|
|
122
|
+
* Automatically extracts stack trace from Error objects
|
|
123
|
+
*/
|
|
124
|
+
error(message: string, context?: Record<string, unknown>): void {
|
|
125
|
+
const errorContext = { ...context }
|
|
126
|
+
|
|
127
|
+
// Extract error details if an Error object is provided
|
|
128
|
+
if (context?.error instanceof Error) {
|
|
129
|
+
errorContext.errorName = context.error.name
|
|
130
|
+
errorContext.errorMessage = context.error.message
|
|
131
|
+
errorContext.errorStack = context.error.stack
|
|
132
|
+
delete errorContext.error
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.log('error', message, errorContext)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private getMinLevelName(): LogLevel {
|
|
139
|
+
const entries = Object.entries(LOG_LEVELS)
|
|
140
|
+
const found = entries.find(([, value]) => value === this.minLevel)
|
|
141
|
+
return (found?.[0] as LogLevel) || 'info'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates a new logger instance with optional context
|
|
147
|
+
*/
|
|
148
|
+
export function createLogger(context?: Record<string, unknown>, minLevel: LogLevel = 'info'): Logger {
|
|
149
|
+
return new Logger({ context, minLevel })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Hono middleware that adds logger to context and logs requests
|
|
154
|
+
*
|
|
155
|
+
* Automatically logs request start/end with timing and status code.
|
|
156
|
+
* Attaches logger instance to c.var.logger for use in handlers.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* app.use('*', loggerMiddleware())
|
|
160
|
+
*
|
|
161
|
+
* app.get('/api/users', (c) => {
|
|
162
|
+
* c.var.logger.info('Fetching users')
|
|
163
|
+
* return c.json({ users: [] })
|
|
164
|
+
* })
|
|
165
|
+
*/
|
|
166
|
+
export function loggerMiddleware() {
|
|
167
|
+
return async (c: Context, next: () => Promise<void>) => {
|
|
168
|
+
const requestId = crypto.randomUUID()
|
|
169
|
+
// Use performance.now() for measuring operation duration
|
|
170
|
+
// (more idiomatic than Date.now() for performance timing)
|
|
171
|
+
const startTime = performance.now()
|
|
172
|
+
|
|
173
|
+
// Create logger with request context
|
|
174
|
+
const logger = createLogger({
|
|
175
|
+
requestId,
|
|
176
|
+
method: c.req.method,
|
|
177
|
+
path: c.req.path,
|
|
178
|
+
userAgent: c.req.header('user-agent'),
|
|
179
|
+
ip: c.req.header('cf-connecting-ip'),
|
|
180
|
+
}, 'debug')
|
|
181
|
+
|
|
182
|
+
// Attach logger to context
|
|
183
|
+
c.set('logger', logger)
|
|
184
|
+
|
|
185
|
+
logger.info('Request started')
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await next()
|
|
189
|
+
|
|
190
|
+
const duration = performance.now() - startTime
|
|
191
|
+
logger.info('Request completed', {
|
|
192
|
+
status: c.res.status,
|
|
193
|
+
durationMs: duration,
|
|
194
|
+
})
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const duration = performance.now() - startTime
|
|
197
|
+
logger.error('Request failed', {
|
|
198
|
+
error,
|
|
199
|
+
durationMs: duration,
|
|
200
|
+
})
|
|
201
|
+
throw error
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extend Hono's context type to include logger
|
|
207
|
+
declare module 'hono' {
|
|
208
|
+
interface ContextVariableMap {
|
|
209
|
+
logger: Logger
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MiddlewareHandler } from 'hono'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware to disable Cloudflare caching for API routes
|
|
5
|
+
*
|
|
6
|
+
* Sets appropriate headers to prevent caching at CDN and browser levels:
|
|
7
|
+
* - Cache-Control: Prevents caching by proxies and browsers
|
|
8
|
+
* - Pragma: HTTP/1.0 backward compatibility
|
|
9
|
+
* - Expires: Sets immediate expiration
|
|
10
|
+
*/
|
|
11
|
+
export const noCacheMiddleware = (): MiddlewareHandler => {
|
|
12
|
+
return async (c, next) => {
|
|
13
|
+
await next()
|
|
14
|
+
|
|
15
|
+
// Prevent caching by Cloudflare and browsers
|
|
16
|
+
c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
|
17
|
+
c.header('Pragma', 'no-cache')
|
|
18
|
+
c.header('Expires', '0')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
9
|
+
"types": ["vite/client", "./worker-configuration.d.ts", "bun-types"],
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"jsxImportSource": "hono/jsx",
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["src/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"exclude": ["src/client", "src/client.tsx"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
|
+
import ssrPlugin from 'vite-ssr-components/plugin'
|
|
4
|
+
import react from '@vitejs/plugin-react'
|
|
5
|
+
|
|
6
|
+
import path from 'path'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
server: {
|
|
10
|
+
port: 5000,
|
|
11
|
+
host: '0.0.0.0',
|
|
12
|
+
allowedHosts: true,
|
|
13
|
+
watch: {
|
|
14
|
+
ignored: ['**/.cache/**', '**/node_modules/**'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
cacheDir: '/tmp/vite-cache',
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {
|
|
20
|
+
'@': path.resolve(__dirname, './src')
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
plugins: [
|
|
24
|
+
cloudflare(),
|
|
25
|
+
ssrPlugin({
|
|
26
|
+
entry: {
|
|
27
|
+
target: 'src/client.tsx',
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
react({
|
|
31
|
+
include: /src\/client\/.*\.tsx?$/,
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
})
|