weifuwu 0.27.2 → 0.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +0 -19
  2. package/dist/ai/provider.d.ts +45 -0
  3. package/dist/ai/stream.d.ts +13 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +131 -0
  6. package/dist/core/cookie.d.ts +36 -0
  7. package/dist/core/env.d.ts +69 -0
  8. package/dist/core/logger.d.ts +16 -0
  9. package/dist/core/router.d.ts +72 -0
  10. package/dist/core/serve.d.ts +38 -0
  11. package/dist/core/sse.d.ts +47 -0
  12. package/dist/core/trace.d.ts +95 -0
  13. package/dist/graphql.d.ts +16 -0
  14. package/dist/hub.d.ts +36 -0
  15. package/dist/index.d.ts +59 -0
  16. package/dist/index.js +3937 -0
  17. package/dist/mailer.d.ts +51 -0
  18. package/dist/middleware/compress.d.ts +20 -0
  19. package/dist/middleware/cors.d.ts +25 -0
  20. package/dist/middleware/csrf.d.ts +47 -0
  21. package/dist/middleware/flash.d.ts +90 -0
  22. package/dist/middleware/health.d.ts +24 -0
  23. package/dist/middleware/helmet.d.ts +33 -0
  24. package/dist/middleware/i18n.d.ts +39 -0
  25. package/dist/middleware/rate-limit.d.ts +44 -0
  26. package/dist/middleware/request-id.d.ts +40 -0
  27. package/dist/middleware/static.d.ts +23 -0
  28. package/dist/middleware/theme.d.ts +31 -0
  29. package/dist/middleware/upload.d.ts +55 -0
  30. package/dist/middleware/validate.d.ts +32 -0
  31. package/dist/postgres/client.d.ts +4 -0
  32. package/dist/postgres/index.d.ts +4 -0
  33. package/dist/postgres/module.d.ts +16 -0
  34. package/dist/postgres/schema/columns.d.ts +99 -0
  35. package/dist/postgres/schema/index.d.ts +6 -0
  36. package/dist/postgres/schema/sql.d.ts +22 -0
  37. package/dist/postgres/schema/table.d.ts +141 -0
  38. package/dist/postgres/schema/where.d.ts +29 -0
  39. package/dist/postgres/types.d.ts +49 -0
  40. package/dist/queue/cron.d.ts +9 -0
  41. package/dist/queue/index.d.ts +2 -0
  42. package/dist/queue/types.d.ts +61 -0
  43. package/dist/redis/client.d.ts +2 -0
  44. package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
  45. package/dist/redis/types.d.ts +17 -0
  46. package/dist/test/test-utils.d.ts +193 -0
  47. package/dist/types.d.ts +50 -0
  48. package/package.json +10 -12
  49. package/ai/provider.ts +0 -129
  50. package/ai/stream.ts +0 -63
  51. package/cli.ts +0 -147
  52. package/core/cookie.ts +0 -114
  53. package/core/env.ts +0 -142
  54. package/core/logger.ts +0 -72
  55. package/core/router.ts +0 -795
  56. package/core/serve.ts +0 -294
  57. package/core/sse.ts +0 -85
  58. package/core/trace.ts +0 -146
  59. package/graphql.ts +0 -267
  60. package/hub.ts +0 -133
  61. package/index.ts +0 -71
  62. package/mailer.ts +0 -81
  63. package/middleware/compress.ts +0 -103
  64. package/middleware/cors.ts +0 -81
  65. package/middleware/csrf.ts +0 -112
  66. package/middleware/flash.ts +0 -144
  67. package/middleware/health.ts +0 -44
  68. package/middleware/helmet.ts +0 -98
  69. package/middleware/i18n.ts +0 -175
  70. package/middleware/rate-limit.ts +0 -167
  71. package/middleware/request-id.ts +0 -60
  72. package/middleware/static.ts +0 -149
  73. package/middleware/theme.ts +0 -84
  74. package/middleware/upload.ts +0 -168
  75. package/middleware/validate.ts +0 -186
  76. package/postgres/client.ts +0 -132
  77. package/postgres/index.ts +0 -4
  78. package/postgres/module.ts +0 -37
  79. package/postgres/schema/columns.ts +0 -186
  80. package/postgres/schema/index.ts +0 -36
  81. package/postgres/schema/sql.ts +0 -39
  82. package/postgres/schema/table.ts +0 -548
  83. package/postgres/schema/where.ts +0 -99
  84. package/postgres/types.ts +0 -48
  85. package/queue/cron.ts +0 -90
  86. package/queue/index.ts +0 -654
  87. package/queue/types.ts +0 -60
  88. package/redis/client.ts +0 -24
  89. package/redis/types.ts +0 -28
  90. package/types.ts +0 -78
@@ -0,0 +1,50 @@
1
+ import type postgres from 'postgres';
2
+ /** Untyped postgres.js SQL client. Use typed `Sql<{ table: { col: type } }>` for schemas. */
3
+ export type SqlClient = postgres.Sql<Record<string, unknown>>;
4
+ /** Re-export for downstream usage. */
5
+ export type { Sql } from 'postgres';
6
+ export type { WebSocket } from 'ws';
7
+ export type { Redis, RedisOptions } from 'ioredis';
8
+ export interface Context {
9
+ params: Record<string, string>;
10
+ query: Record<string, string>;
11
+ mountPath?: string;
12
+ [key: string]: unknown;
13
+ }
14
+ export type Handler<T extends Context = Context> = (req: Request, ctx: T) => Response | Promise<Response>;
15
+ /**
16
+ * Metadata for middleware dependency checking.
17
+ * Middleware factories attach this for runtime validation.
18
+ */
19
+ export interface MiddlewareMeta {
20
+ /** Fields this middleware injects into ctx. */
21
+ injects: string[];
22
+ /** Fields this middleware depends on (must be injected earlier). */
23
+ depends: string[];
24
+ }
25
+ export type Middleware<In extends Context = Context, Out extends In = In> = {
26
+ (req: Request, ctx: In, next: Handler<Out>): Response | Promise<Response>;
27
+ __meta?: MiddlewareMeta;
28
+ };
29
+ export type ErrorHandler<T extends Context = Context> = (error: Error, req: Request, ctx: T) => Response | Promise<Response>;
30
+ /**
31
+ * Interface for resources that require explicit cleanup (connections, pools, timers).
32
+ * All stateful modules implement this.
33
+ */
34
+ export interface Closeable {
35
+ /** Release all resources. Call once when shutting down. */
36
+ close(): Promise<void>;
37
+ }
38
+ /**
39
+ * HTTP error with an explicit status code.
40
+ * Throw from a handler or middleware to return a non-200 response.
41
+ *
42
+ * ```ts
43
+ * if (!resource) throw new HttpError('Not found', 404)
44
+ * serve() catches it and returns the status code.
45
+ * ```
46
+ */
47
+ export declare class HttpError extends Error {
48
+ status: number;
49
+ constructor(message: string, status: number);
50
+ }
package/package.json CHANGED
@@ -1,22 +1,21 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.27.2",
4
+ "version": "0.27.4",
5
5
  "description": "Web-standard HTTP microframework for Node.js — (req, ctx) => Response",
6
6
  "exports": {
7
- ".": "./index.ts"
7
+ ".": "./dist/index.js"
8
+ },
9
+ "bin": {
10
+ "weifuwu": "./dist/cli.js"
8
11
  },
9
12
  "files": [
10
- "*.ts",
11
- "core/*.ts",
12
- "middleware/*.ts",
13
- "ai/*.ts",
14
- "postgres/*.ts",
15
- "postgres/**/*.ts",
16
- "queue/*.ts",
17
- "redis/*.ts"
13
+ "dist/",
14
+ "README.md"
18
15
  ],
19
16
  "scripts": {
17
+ "build": "node scripts/build.mjs",
18
+ "prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
20
19
  "typecheck": "tsc --noEmit",
21
20
  "format": "prettier --write .",
22
21
  "format:check": "prettier --check .",
@@ -32,7 +31,6 @@
32
31
  "ai": "^6",
33
32
  "graphql": "^16",
34
33
  "ioredis": "^5.11.0",
35
- "nodemailer": "^8.0.10",
36
34
  "postgres": "^3.4.9",
37
35
  "ws": "^8",
38
36
  "zod": "^4.4.3"
@@ -49,8 +47,8 @@
49
47
  "devDependencies": {
50
48
  "@eslint/js": "^10.0.1",
51
49
  "@types/node": "^25.9.3",
52
- "@types/nodemailer": "^6.4.17",
53
50
  "@types/ws": "^8.18.1",
51
+ "esbuild": "^0.28.0",
54
52
  "eslint": "^10.5.0",
55
53
  "globals": "^17.6.0",
56
54
  "husky": "^9.1.7",
package/ai/provider.ts DELETED
@@ -1,129 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { Context, Middleware } from '../types.ts'
3
- import { createOpenAI } from '@ai-sdk/openai'
4
- import {
5
- embed as aiEmbed,
6
- embedMany as aiEmbedMany,
7
- generateText as aiGenerateText,
8
- streamText as aiStreamText,
9
- type LanguageModel,
10
- type EmbeddingModel,
11
- } from 'ai'
12
-
13
- // Augment Context with ai property
14
- declare module '../types.ts' {
15
- interface Context {
16
- ai: AIProvider
17
- }
18
- }
19
-
20
- // ── Types ───────────────────────────────────────────────────────────────────
21
-
22
- export interface AIProviderInjected {
23
- ai: AIProvider
24
- }
25
-
26
- export interface AIProviderOptions {
27
- /** API base URL (default: OPENAI_BASE_URL env or http://localhost:11434/v1). */
28
- baseURL?: string
29
- /** API key (default: OPENAI_API_KEY env or 'ollama'). */
30
- apiKey?: string
31
- /** Chat model name (default: OPENAI_MODEL env or 'qwen3:0.6b'). */
32
- model?: string
33
- /** Embedding model name (default: OPENAI_EMBEDDING_MODEL env or 'qwen3-embedding:0.6b'). */
34
- embeddingModel?: string
35
- /** Vector dimension (default: EMBEDDING_DIMENSION env or 1024). */
36
- embeddingDimension?: number
37
- }
38
-
39
- export interface AIProvider {
40
- /** Get the language model. Caches by default; pass a name to override. */
41
- model(name?: string): LanguageModel
42
- /** Get the embedding model. Caches by default; pass a name to override. */
43
- embeddingModel(name?: string): EmbeddingModel
44
- /** Embed a single text string into a vector. */
45
- embed(text: string): Promise<number[]>
46
- /** Embed multiple text strings in batch. */
47
- embedMany(texts: string[]): Promise<number[][]>
48
- /** The configured vector dimension. */
49
- readonly dimension: number
50
-
51
- /**
52
- * Generate text using the configured model.
53
- * All options are passed through to the AI SDK's `generateText`, with `model` auto-injected.
54
- */
55
- generateText(
56
- params: Omit<Parameters<typeof aiGenerateText>[0], 'model'>,
57
- ): ReturnType<typeof aiGenerateText>
58
- /**
59
- * Stream text using the configured model.
60
- * All options are passed through to the AI SDK's `streamText`, with `model` auto-injected.
61
- */
62
- streamText(
63
- params: Omit<Parameters<typeof aiStreamText>[0], 'model'>,
64
- ): ReturnType<typeof aiStreamText>
65
- }
66
-
67
- // ── Factory ─────────────────────────────────────────────────────────────────
68
-
69
- export function aiProvider(
70
- options?: AIProviderOptions,
71
- ): Middleware<Context, Context & AIProviderInjected> & AIProvider {
72
- const baseURL = options?.baseURL ?? process.env.OPENAI_BASE_URL ?? 'http://localhost:11434/v1'
73
- const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY ?? 'ollama'
74
- const modelName = options?.model ?? process.env.OPENAI_MODEL ?? 'qwen3:0.6b'
75
- const embedModelName =
76
- options?.embeddingModel ?? process.env.OPENAI_EMBEDDING_MODEL ?? 'qwen3-embedding:0.6b'
77
- const dimension =
78
- options?.embeddingDimension ?? parseInt(process.env.EMBEDDING_DIMENSION || '1024', 10)
79
-
80
- const client = createOpenAI({ baseURL, apiKey })
81
-
82
- let _model: LanguageModel | undefined
83
- let _embedModel: EmbeddingModel | undefined
84
-
85
- const provider: AIProvider = {
86
- get dimension() {
87
- return dimension
88
- },
89
-
90
- model(name?: string): LanguageModel {
91
- const m = name ?? modelName
92
- if (!_model) _model = client(m)
93
- return _model
94
- },
95
-
96
- embeddingModel(name?: string): EmbeddingModel {
97
- const m = name ?? embedModelName
98
- if (!_embedModel) _embedModel = client.embedding(m)
99
- return _embedModel
100
- },
101
-
102
- async embed(text: string): Promise<number[]> {
103
- const result = await aiEmbed({ model: this.embeddingModel(), value: text })
104
- return result.embedding
105
- },
106
-
107
- async embedMany(texts: string[]): Promise<number[][]> {
108
- const result = await aiEmbedMany({ model: this.embeddingModel(), values: texts })
109
- return result.embeddings
110
- },
111
-
112
- generateText(params: Omit<Parameters<typeof aiGenerateText>[0], 'model'>) {
113
- return aiGenerateText({ ...params, model: this.model() } as any)
114
- },
115
-
116
- streamText(params: Omit<Parameters<typeof aiStreamText>[0], 'model'>) {
117
- return aiStreamText({ ...params, model: this.model() } as any)
118
- },
119
- }
120
-
121
- const mw: Middleware<Context, Context & AIProviderInjected> = async (req, ctx, next) => {
122
- ;(ctx as Context & AIProviderInjected).ai = provider
123
- return next(req, ctx as Context & AIProviderInjected)
124
- }
125
- mw.__meta = { injects: ['ai'], depends: [] }
126
-
127
- return Object.assign(mw, provider) as Middleware<Context, Context & AIProviderInjected> &
128
- AIProvider
129
- }
package/ai/stream.ts DELETED
@@ -1,63 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { Context } from '../types.ts'
3
- import { Router } from '../core/router.ts'
4
- import type { AIProvider } from './provider.ts'
5
-
6
- export type AIHandler = (
7
- req: Request,
8
- ctx: Context,
9
- ) => Record<string, unknown> | Promise<Record<string, unknown>>
10
-
11
- export const _ai: Record<string, any> = {}
12
-
13
- async function getStreamText() {
14
- if (!_ai.streamText) _ai.streamText = (await import('ai')).streamText
15
- return _ai.streamText
16
- }
17
-
18
- async function getStreamObject() {
19
- if (!_ai.streamObject) _ai.streamObject = (await import('ai')).streamObject
20
- return _ai.streamObject
21
- }
22
-
23
- /**
24
- * Create a streaming AI endpoint.
25
- *
26
- * @param handler - Returns options for `streamText` or `streamObject` (if `schema` is present).
27
- * @param provider - Optional AI provider. If provided and the handler does not return a `model`,
28
- * `provider.model()` is used as the default.
29
- */
30
- export async function aiStream(handler: AIHandler, provider?: AIProvider): Promise<Router> {
31
- const r = new Router()
32
-
33
- r.post('/', async (req, ctx) => {
34
- let options
35
- try {
36
- options = await handler(req, ctx)
37
- } catch (err) {
38
- const message = err instanceof Error ? err.message : String(err)
39
- return new Response(JSON.stringify({ error: message }), {
40
- status: 500,
41
- headers: { 'Content-Type': 'application/json' },
42
- })
43
- }
44
-
45
- // Inject default model from provider if handler didn't specify one
46
- if (provider && !options.model) {
47
- options.model = provider.model()
48
- }
49
-
50
- if (options.schema) {
51
- const streamObject = await getStreamObject()
52
- const { schema, ...params } = options
53
- const result = streamObject({ ...params, schema: schema as any, output: 'object' as const })
54
- return result.toTextStreamResponse()
55
- }
56
-
57
- const streamText = await getStreamText()
58
- const result = streamText(options)
59
- return result.toTextStreamResponse()
60
- })
61
-
62
- return r
63
- }
package/cli.ts DELETED
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable no-console */
3
- import { mkdir, writeFile } from 'node:fs/promises'
4
- import { existsSync } from 'node:fs'
5
- import { execSync } from 'node:child_process'
6
- import { join, dirname, resolve } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { parseArgs } from 'node:util'
9
-
10
- const __filename = fileURLToPath(import.meta.url)
11
- const __dirname = dirname(__filename)
12
-
13
- const pkgRoot = existsSync(join(__dirname, 'package.json')) ? __dirname : resolve(__dirname, '..')
14
-
15
- async function readPkg() {
16
- return JSON.parse(
17
- await import('node:fs/promises').then((fs) =>
18
- fs.readFile(join(pkgRoot, 'package.json'), 'utf-8'),
19
- ),
20
- )
21
- }
22
-
23
- async function cmdVersion() {
24
- const pkg = await readPkg()
25
- console.log(pkg.version)
26
- }
27
-
28
- async function cmdInit(name: string, opts: { skipInstall?: boolean }) {
29
- const targetDir = resolve(process.cwd(), name)
30
-
31
- if (existsSync(targetDir)) {
32
- console.error(`Directory ${name} already exists.`)
33
- process.exit(1)
34
- }
35
-
36
- const pkg = await readPkg()
37
-
38
- await mkdir(targetDir, { recursive: true })
39
-
40
- await writeFile(
41
- join(targetDir, 'app.ts'),
42
- [
43
- `import { Router } from 'weifuwu'`,
44
- ``,
45
- `export const app = new Router()`,
46
- ``,
47
- `app.get('/', () => new Response('Hello from ${name}!'))`,
48
- `app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))`,
49
- ``,
50
- ].join('\n'),
51
- )
52
-
53
- await writeFile(
54
- join(targetDir, 'index.ts'),
55
- [
56
- `import { loadEnv, serve } from 'weifuwu'`,
57
- `import { app } from './app.ts'`,
58
- ``,
59
- `loadEnv()`,
60
- `const port = Number(process.env.PORT) || 3000`,
61
- `serve(app.handler(), { port })`,
62
- ``,
63
- ].join('\n'),
64
- )
65
-
66
- await writeFile(
67
- join(targetDir, 'package.json'),
68
- JSON.stringify(
69
- {
70
- name,
71
- type: 'module',
72
- scripts: {
73
- dev: 'node --watch index.ts',
74
- start: 'node index.ts',
75
- },
76
- dependencies: {
77
- weifuwu: `^${pkg.version}`,
78
- },
79
- },
80
- null,
81
- 2,
82
- ) + '\n',
83
- )
84
-
85
- await writeFile(
86
- join(targetDir, 'tsconfig.json'),
87
- JSON.stringify(
88
- {
89
- compilerOptions: {
90
- target: 'ESNext',
91
- module: 'NodeNext',
92
- moduleResolution: 'NodeNext',
93
- strict: true,
94
- skipLibCheck: true,
95
- noEmit: true,
96
- allowImportingTsExtensions: true,
97
- },
98
- include: ['*.ts'],
99
- },
100
- null,
101
- 2,
102
- ) + '\n',
103
- )
104
-
105
- await writeFile(join(targetDir, '.gitignore'), 'node_modules\n.env\n')
106
- await writeFile(join(targetDir, '.env'), 'PORT=3000\n')
107
-
108
- if (!opts.skipInstall) {
109
- console.log('\nInstalling dependencies...')
110
- execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
111
- }
112
-
113
- console.log(`\n✅ Created ${name}/`)
114
- console.log(` cd ${name}`)
115
- if (opts.skipInstall) console.log(` npm install`)
116
- console.log(` npm run dev`)
117
- }
118
-
119
- const cmd = process.argv[2]
120
-
121
- const HELP = `
122
- weifuwu — Web-standard HTTP microframework for Node.js
123
-
124
- Usage:
125
- npx weifuwu init <name> Create a new project
126
- npx weifuwu init <name> --skip-install Create project, skip npm install
127
- npx weifuwu version Print version
128
- `
129
-
130
- if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
131
- cmdVersion().catch(console.error)
132
- } else if (cmd === 'init') {
133
- const { values, positionals } = parseArgs({
134
- args: process.argv.slice(3),
135
- options: { 'skip-install': { type: 'boolean' } },
136
- strict: false,
137
- allowPositionals: true,
138
- })
139
- const name = positionals[0]
140
- if (!name) {
141
- console.error('Usage: npx weifuwu init <name> [--skip-install]')
142
- process.exit(1)
143
- }
144
- cmdInit(name, { skipInstall: !!values['skip-install'] }).catch(console.error)
145
- } else {
146
- console.log(HELP)
147
- }
package/core/cookie.ts DELETED
@@ -1,114 +0,0 @@
1
- /** Options for setting a cookie. All fields map to standard Set-Cookie attributes. */
2
- export interface CookieOptions {
3
- domain?: string
4
- path?: string
5
- maxAge?: number
6
- expires?: Date
7
- httpOnly?: boolean
8
- secure?: boolean
9
- sameSite?: 'strict' | 'lax' | 'none'
10
- }
11
-
12
- /** Parse cookies from a Request's `Cookie` header.
13
- *
14
- * @example
15
- * ```ts
16
- * const cookies = getCookies(req)
17
- * console.log(cookies.session_id) // value or undefined
18
- * ``` */
19
- export function getCookies(req: Request): Record<string, string> {
20
- const header = req.headers.get('cookie')
21
- if (!header) return {}
22
-
23
- const cookies: Record<string, string> = {}
24
- for (const pair of header.split(';')) {
25
- const idx = pair.indexOf('=')
26
- if (idx === -1) continue
27
- let name = pair.slice(0, idx).trim()
28
- let value = pair.slice(idx + 1).trim()
29
- if (!name) continue
30
- // Decode cookie name (consistent with value)
31
- try {
32
- name = decodeURIComponent(name)
33
- } catch {}
34
- // Strip surrounding quotes from value (RFC 6265)
35
- if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
36
- value = value.slice(1, -1)
37
- }
38
- try {
39
- cookies[name] = decodeURIComponent(value)
40
- } catch {
41
- cookies[name] = value
42
- }
43
- }
44
- return cookies
45
- }
46
-
47
- function serializeCookie(name: string, value: string, options?: CookieOptions): string {
48
- // Reject control characters and special chars per RFC 6265
49
- // eslint-disable-next-line no-control-regex
50
- if (/[\x00-\x1F\x7F-\x9F;,]/.test(name) || /[\x00-\x1F\x7F-\x9F;,]/.test(value)) {
51
- throw new Error(`Invalid cookie name or value: contains control characters or special chars`)
52
- }
53
- const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
54
- if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`)
55
- if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`)
56
- if (options?.domain) parts.push(`Domain=${options.domain}`)
57
- if (options?.path) parts.push(`Path=${options.path}`)
58
- if (options?.httpOnly) parts.push('HttpOnly')
59
- if (options?.secure) parts.push('Secure')
60
- if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`)
61
- return parts.join('; ')
62
- }
63
-
64
- /** Set a cookie on a Response.
65
- *
66
- * Appends a `Set-Cookie` header to the existing response headers.
67
- * Returns a new Response with the added header.
68
- *
69
- * @example
70
- * ```ts
71
- * const res = new Response('ok')
72
- * return setCookie(res, 'session', token, { httpOnly: true, path: '/' })
73
- * ``` */
74
- export function setCookie(
75
- res: Response,
76
- name: string,
77
- value: string,
78
- options?: CookieOptions,
79
- ): Response {
80
- const headers = new Headers(res.headers)
81
- headers.append('Set-Cookie', serializeCookie(name, value, options))
82
- return new Response(res.body, {
83
- status: res.status,
84
- statusText: res.statusText,
85
- headers,
86
- })
87
- }
88
-
89
- /** Delete a cookie by setting `Max-Age=0`.
90
- *
91
- * @example
92
- * ```ts
93
- * return deleteCookie(res, 'session')
94
- * ``` */
95
- export function deleteCookie(
96
- res: Response,
97
- name: string,
98
- options?: Omit<CookieOptions, 'maxAge'>,
99
- ): Response {
100
- const headers = new Headers(res.headers)
101
- headers.append(
102
- 'Set-Cookie',
103
- serializeCookie(name, '', {
104
- ...options,
105
- maxAge: 0,
106
- expires: new Date(0),
107
- }),
108
- )
109
- return new Response(res.body, {
110
- status: res.status,
111
- statusText: res.statusText,
112
- headers,
113
- })
114
- }
package/core/env.ts DELETED
@@ -1,142 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import type { Context, Middleware } from '../types.ts'
4
-
5
- /** Build-time injection from esbuild --define. `true` in dist/index.js, undefined in TS source. */
6
- declare let __WFW_BUNDLED__: boolean | undefined
7
-
8
- const PUBLIC_PREFIX = 'WEIFUWU_PUBLIC_'
9
-
10
- /**
11
- * Get all public environment variables (those prefixed with `WEIFUWU_PUBLIC_`),
12
- * with the prefix stripped.
13
- *
14
- * ```ts
15
- * const pub = getPublicEnv()
16
- * // WEIFUWU_PUBLIC_API_URL=http://api.example.com → { API_URL: 'http://api.example.com' }
17
- * ```
18
- */
19
- export function getPublicEnv(): Record<string, string> {
20
- const result: Record<string, string> = {}
21
- for (const [key, value] of Object.entries(process.env)) {
22
- if (key.startsWith(PUBLIC_PREFIX) && value !== undefined) {
23
- result[key.slice(PUBLIC_PREFIX.length)] = value
24
- }
25
- }
26
- return result
27
- }
28
-
29
- // Augment Context with env property
30
- declare module '../types.ts' {
31
- interface Context {
32
- env?: Record<string, string>
33
- }
34
- }
35
-
36
- /**
37
- * Whether this code is running from the compiled `dist/index.js` bundle.
38
- * `false` when running TypeScript source directly (dev workflow in weifuwu repo).
39
- *
40
- * Used by modules that need to resolve package-internal files differently
41
- * depending on whether they are compiled (published npm package) or raw TS.
42
- */
43
- export function isBundled(): boolean {
44
- return typeof __WFW_BUNDLED__ !== 'undefined' ? __WFW_BUNDLED__ : false
45
- }
46
-
47
- /**
48
- * Whether `NODE_ENV` is explicitly set to `'development'`.
49
- *
50
- * Used for dev-only features: HMR, livereload, React `createRoot` (not hydrate).
51
- * **Not** the opposite of {@link isProd} — when `NODE_ENV` is unset, both return `false`.
52
- */
53
- export function isDev(): boolean {
54
- const env = process.env.NODE_ENV
55
- return env !== 'production' && env !== 'test'
56
- }
57
-
58
- /**
59
- * Whether `NODE_ENV` is explicitly set to `'production'`.
60
- *
61
- * Used for production-only behavior: plain-text 404, suppressed warnings, minified output.
62
- */
63
- export function isProd(): boolean {
64
- return process.env.NODE_ENV === 'production'
65
- }
66
-
67
- /**
68
- * Load environment variables from a `.env` file into `process.env`.
69
- *
70
- * Does **not** override existing `process.env` values.
71
- * Supports quoted values and inline comments.
72
- *
73
- * @param path - Path to `.env` file (default: `'.env'` relative to cwd).
74
- *
75
- * ```ts
76
- * import { loadEnv } from 'weifuwu'
77
- * loadEnv()
78
- * console.log(process.env.PORT)
79
- * ```
80
- */
81
- export function loadEnv(path?: string): void {
82
- const filePath = resolve(process.cwd(), path ?? '.env')
83
-
84
- let content: string
85
- try {
86
- content = readFileSync(filePath, 'utf-8')
87
- } catch {
88
- return
89
- }
90
-
91
- for (const line of content.split('\n')) {
92
- const trimmed = line.trim()
93
- if (!trimmed || trimmed.startsWith('#')) continue
94
-
95
- const eqIdx = trimmed.indexOf('=')
96
- if (eqIdx === -1) continue
97
-
98
- const key = trimmed.slice(0, eqIdx).trim()
99
- if (!key) continue
100
-
101
- if (process.env[key] !== undefined) continue
102
-
103
- let value = trimmed.slice(eqIdx + 1).trim()
104
-
105
- if (
106
- (value.startsWith('"') && value.endsWith('"')) ||
107
- (value.startsWith("'") && value.endsWith("'"))
108
- ) {
109
- value = value.slice(1, -1)
110
- } else {
111
- // Strip inline comments: space before #, or # at start of value
112
- const commentIdx = value.search(/\s#/)
113
- if (commentIdx !== -1) {
114
- value = value.slice(0, commentIdx).trimEnd()
115
- }
116
- }
117
-
118
- process.env[key] = value
119
- }
120
- }
121
-
122
- /**
123
- * Public env middleware.
124
- *
125
- * Injects `ctx.env` with all environment variables prefixed with `WEIFUWU_PUBLIC_`,
126
- * with the prefix stripped. Safe to expose to the client.
127
- *
128
- * ```ts
129
- * import { env } from 'weifuwu'
130
- * app.use(env())
131
- *
132
- * // .env: WEIFUWU_PUBLIC_API_URL=https://api.example.com
133
- * // ctx: ctx.env.API_URL === 'https://api.example.com'
134
- * ```
135
- */
136
- export function env(): Middleware<Context, Context & { env: Record<string, string> }> {
137
- const entries = getPublicEnv()
138
- return async (req, ctx, next) => {
139
- ;(ctx as Context & { env: Record<string, string> }).env = entries
140
- return next(req, ctx as Context & { env: Record<string, string> })
141
- }
142
- }