weifuwu 0.27.2 → 0.27.3
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/dist/ai/provider.d.ts +45 -0
- package/dist/ai/stream.d.ts +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +131 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +72 -0
- package/dist/core/serve.d.ts +38 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +3963 -0
- package/dist/mailer.d.ts +51 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +47 -0
- package/dist/middleware/flash.d.ts +90 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/module.d.ts +16 -0
- package/dist/postgres/schema/columns.d.ts +99 -0
- package/dist/postgres/schema/index.d.ts +6 -0
- package/dist/postgres/schema/sql.d.ts +22 -0
- package/dist/postgres/schema/table.d.ts +141 -0
- package/dist/postgres/schema/where.d.ts +29 -0
- package/dist/postgres/types.d.ts +49 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/redis/client.d.ts +2 -0
- package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +50 -0
- package/package.json +10 -10
- package/ai/provider.ts +0 -129
- package/ai/stream.ts +0 -63
- package/cli.ts +0 -147
- package/core/cookie.ts +0 -114
- package/core/env.ts +0 -142
- package/core/logger.ts +0 -72
- package/core/router.ts +0 -795
- package/core/serve.ts +0 -294
- package/core/sse.ts +0 -85
- package/core/trace.ts +0 -146
- package/graphql.ts +0 -267
- package/hub.ts +0 -133
- package/index.ts +0 -71
- package/mailer.ts +0 -81
- package/middleware/compress.ts +0 -103
- package/middleware/cors.ts +0 -81
- package/middleware/csrf.ts +0 -112
- package/middleware/flash.ts +0 -144
- package/middleware/health.ts +0 -44
- package/middleware/helmet.ts +0 -98
- package/middleware/i18n.ts +0 -175
- package/middleware/rate-limit.ts +0 -167
- package/middleware/request-id.ts +0 -60
- package/middleware/static.ts +0 -149
- package/middleware/theme.ts +0 -84
- package/middleware/upload.ts +0 -168
- package/middleware/validate.ts +0 -186
- package/postgres/client.ts +0 -132
- package/postgres/index.ts +0 -4
- package/postgres/module.ts +0 -37
- package/postgres/schema/columns.ts +0 -186
- package/postgres/schema/index.ts +0 -36
- package/postgres/schema/sql.ts +0 -39
- package/postgres/schema/table.ts +0 -548
- package/postgres/schema/where.ts +0 -99
- package/postgres/types.ts +0 -48
- package/queue/cron.ts +0 -90
- package/queue/index.ts +0 -654
- package/queue/types.ts +0 -60
- package/redis/client.ts +0 -24
- package/redis/types.ts +0 -28
- package/types.ts +0 -78
package/dist/types.d.ts
ADDED
|
@@ -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.
|
|
4
|
+
"version": "0.27.3",
|
|
5
5
|
"description": "Web-standard HTTP microframework for Node.js — (req, ctx) => Response",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./index.
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"weifuwu": "./dist/cli.js"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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 .",
|
|
@@ -51,6 +50,7 @@
|
|
|
51
50
|
"@types/node": "^25.9.3",
|
|
52
51
|
"@types/nodemailer": "^6.4.17",
|
|
53
52
|
"@types/ws": "^8.18.1",
|
|
53
|
+
"esbuild": "^0.28.0",
|
|
54
54
|
"eslint": "^10.5.0",
|
|
55
55
|
"globals": "^17.6.0",
|
|
56
56
|
"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
|
-
}
|