weifuwu 0.11.0 → 0.12.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/README.md +78 -10
- package/dist/ai.d.ts +5 -17
- package/dist/deploy/types.d.ts +1 -1
- package/dist/graphql.d.ts +3 -1
- package/dist/health.d.ts +6 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +149 -24
- package/dist/mailer.d.ts +20 -0
- package/dist/serve.d.ts +4 -0
- package/dist/types.d.ts +2 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
|
|
|
17
17
|
- **React SSR + Hydration** — `tsx({ dir })` — page.tsx / load.ts / layout.tsx / route.ts / not-found.tsx
|
|
18
18
|
- **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
|
|
19
19
|
- **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
|
|
20
|
-
- **AI streaming** — `
|
|
20
|
+
- **AI streaming** — `aiStream(handler)` sub-Router via Vercel AI SDK
|
|
21
21
|
- **DAG workflow tool** — `runWorkflow()` — multi-step execution engine as a single AI SDK `Tool`
|
|
22
22
|
- **AI Agent** — `agent()` — server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
|
|
23
23
|
- **Messaging** — `messager()` — real-time chat with channels, WebSocket, agent routing, webhook support
|
|
@@ -851,7 +851,7 @@ GraphQL endpoint with GraphiQL IDE. Mount as a sub-Router:
|
|
|
851
851
|
import { serve, Router, graphql } from 'weifuwu'
|
|
852
852
|
|
|
853
853
|
const app = new Router()
|
|
854
|
-
|
|
854
|
+
const gql = graphql(() => ({
|
|
855
855
|
schema: `
|
|
856
856
|
type Query { hello: String }
|
|
857
857
|
type Mutation { setMessage(msg: String!): String }
|
|
@@ -861,7 +861,8 @@ app.use('/graphql', graphql(() => ({
|
|
|
861
861
|
Mutation: { setMessage: (_, { msg }) => msg },
|
|
862
862
|
},
|
|
863
863
|
graphiql: true,
|
|
864
|
-
}))
|
|
864
|
+
}))
|
|
865
|
+
app.use('/graphql', gql.router())
|
|
865
866
|
|
|
866
867
|
serve(app.handler(), { port: 3000 })
|
|
867
868
|
```
|
|
@@ -873,14 +874,15 @@ The handler receives `(req, ctx)` so you can customize the schema based on the r
|
|
|
873
874
|
Server-sent event streaming via the Vercel AI SDK:
|
|
874
875
|
|
|
875
876
|
```ts
|
|
876
|
-
import { serve, Router,
|
|
877
|
+
import { serve, Router, aiStream } from 'weifuwu'
|
|
877
878
|
import { openai } from '@ai-sdk/openai'
|
|
878
879
|
|
|
879
880
|
const app = new Router()
|
|
880
|
-
|
|
881
|
+
const chat = await aiStream(async (req, ctx) => {
|
|
881
882
|
const { messages } = await req.json()
|
|
882
883
|
return { model: openai('gpt-4o'), messages }
|
|
883
|
-
})
|
|
884
|
+
})
|
|
885
|
+
app.use('/chat', chat.router())
|
|
884
886
|
|
|
885
887
|
serve(app.handler(), { port: 3000 })
|
|
886
888
|
```
|
|
@@ -1153,15 +1155,81 @@ export default function NotFound() {
|
|
|
1153
1155
|
}
|
|
1154
1156
|
```
|
|
1155
1157
|
|
|
1158
|
+
## Health check
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
import { serve, Router, health } from 'weifuwu'
|
|
1162
|
+
|
|
1163
|
+
const app = new Router()
|
|
1164
|
+
app.use(health()) // GET /health → 200
|
|
1165
|
+
app.use(health({ path: '/healthz' })) // custom path
|
|
1166
|
+
app.use(health({
|
|
1167
|
+
check: async () => { await db.sql`SELECT 1` }, // fail → 503
|
|
1168
|
+
}))
|
|
1169
|
+
serve(app.handler(), { port: 3000 })
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
Returns a `Router` — mount with `app.use()`.
|
|
1173
|
+
|
|
1174
|
+
## Internationalization
|
|
1175
|
+
|
|
1176
|
+
```ts
|
|
1177
|
+
import { i18n } from 'weifuwu'
|
|
1178
|
+
|
|
1179
|
+
app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
|
|
1180
|
+
|
|
1181
|
+
// In any handler after i18n middleware:
|
|
1182
|
+
app.get('/hello', (req, ctx) => {
|
|
1183
|
+
const msg = ctx.t('greeting', { name: 'World' })
|
|
1184
|
+
return Response.json({ message: msg, locale: ctx.locale })
|
|
1185
|
+
})
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Locale detection: `Cookie: locale=zh` → `Accept-Language: zh-CN` → `defaultLocale`.
|
|
1189
|
+
|
|
1190
|
+
## Email
|
|
1191
|
+
|
|
1192
|
+
```ts
|
|
1193
|
+
import { mailer } from 'weifuwu'
|
|
1194
|
+
|
|
1195
|
+
// SMTP transport
|
|
1196
|
+
const mail = mailer({
|
|
1197
|
+
transport: 'smtp://user:pass@smtp.example.com',
|
|
1198
|
+
from: 'noreply@example.com',
|
|
1199
|
+
})
|
|
1200
|
+
await mail.send({ to: 'user@example.com', subject: 'Welcome', html: '<h1>Hi!</h1>' })
|
|
1201
|
+
await mail.close()
|
|
1202
|
+
|
|
1203
|
+
// Custom transport (Resend, SES, SendGrid, etc.)
|
|
1204
|
+
const mail2 = mailer({
|
|
1205
|
+
send: async (msg) => { await resend.emails.send(msg) },
|
|
1206
|
+
})
|
|
1207
|
+
await mail2.send({ to: 'user@example.com', subject: 'Hi', text: 'Hello' })
|
|
1208
|
+
await mail2.close()
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
## Test utilities
|
|
1212
|
+
|
|
1213
|
+
```ts
|
|
1214
|
+
import { createTestServer } from 'weifuwu'
|
|
1215
|
+
|
|
1216
|
+
const { server, url } = await createTestServer(app.handler())
|
|
1217
|
+
const res = await fetch(`${url}/api/users`)
|
|
1218
|
+
assert.equal(res.status, 200)
|
|
1219
|
+
server.stop()
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1156
1222
|
## Usage within a full app
|
|
1157
1223
|
|
|
1158
1224
|
```ts
|
|
1159
|
-
import { serve, Router,
|
|
1225
|
+
import { serve, Router, aiStream, graphql } from 'weifuwu'
|
|
1160
1226
|
|
|
1161
1227
|
const app = new Router()
|
|
1162
1228
|
app.use('/', await tsx({ dir: './pages/' }))
|
|
1163
|
-
|
|
1164
|
-
app.use('/
|
|
1229
|
+
const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
|
|
1230
|
+
app.use('/chat', chat.router())
|
|
1231
|
+
const gql = graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } }))
|
|
1232
|
+
app.use('/graphql', gql.router())
|
|
1165
1233
|
app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
|
|
1166
1234
|
|
|
1167
1235
|
serve(app.handler(), { websocket: app.websocketHandler() })
|
|
@@ -1382,7 +1450,7 @@ serve(app.handler(), { websocket: app.websocketHandler() })
|
|
|
1382
1450
|
| `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
|
|
1383
1451
|
| `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
|
|
1384
1452
|
| `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
|
|
1385
|
-
| `
|
|
1453
|
+
| `aiStream(handler)` | AI streaming endpoint (POST) |
|
|
1386
1454
|
| `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
|
|
1387
1455
|
|
|
1388
1456
|
### Deploy
|
package/dist/ai.d.ts
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import type { Context } from './types.ts';
|
|
2
2
|
import { Router } from './router.ts';
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
maxTokens?: number;
|
|
9
|
-
temperature?: number;
|
|
10
|
-
[key: string]: unknown;
|
|
11
|
-
};
|
|
12
|
-
export type AIHandler = (req: Request, ctx: Context) => StreamTextParams | Promise<StreamTextParams>;
|
|
13
|
-
export declare const _ai: {
|
|
14
|
-
streamText: (params: StreamTextParams) => {
|
|
15
|
-
toTextStreamResponse: () => Response;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
export declare function ai(handler: AIHandler): Promise<Router>;
|
|
19
|
-
export {};
|
|
3
|
+
export type AIHandler = (req: Request, ctx: Context) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
4
|
+
export declare const _ai: Record<string, any>;
|
|
5
|
+
export declare function aiStream(handler: AIHandler): Promise<{
|
|
6
|
+
router(): Router;
|
|
7
|
+
}>;
|
package/dist/deploy/types.d.ts
CHANGED
package/dist/graphql.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export interface GraphQLOptions {
|
|
|
9
9
|
graphiql?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export type GraphQLHandler = (req: Request, ctx: Context) => GraphQLOptions | Promise<GraphQLOptions>;
|
|
12
|
-
export declare function graphql(handler: GraphQLHandler):
|
|
12
|
+
export declare function graphql(handler: GraphQLHandler): {
|
|
13
|
+
router(): Router;
|
|
14
|
+
};
|
package/dist/health.d.ts
ADDED
package/dist/i18n.d.ts
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
|
|
2
|
-
export { serve } from './serve.ts';
|
|
2
|
+
export { serve, createTestServer } from './serve.ts';
|
|
3
3
|
export type { ServeOptions, Server } from './serve.ts';
|
|
4
4
|
export { Router } from './router.ts';
|
|
5
5
|
export type { WebSocketHandler } from './router.ts';
|
|
@@ -21,7 +21,7 @@ export { compress } from './compress.ts';
|
|
|
21
21
|
export type { CompressOptions } from './compress.ts';
|
|
22
22
|
export { graphql } from './graphql.ts';
|
|
23
23
|
export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
|
|
24
|
-
export {
|
|
24
|
+
export { aiStream } from './ai.ts';
|
|
25
25
|
export type { AIHandler } from './ai.ts';
|
|
26
26
|
export { runWorkflow } from './ai/workflow.ts';
|
|
27
27
|
export { postgres } from './postgres/index.ts';
|
|
@@ -42,3 +42,9 @@ export { deploy, defineConfig } from './deploy/index.ts';
|
|
|
42
42
|
export type { DeployConfig, AppConfig, DeployServer, AppStatus } from './deploy/types.ts';
|
|
43
43
|
export { opencode } from './opencode/index.ts';
|
|
44
44
|
export type { OpencodeOptions, OpencodeModule, SkillDef, OpencodePermissions, Session as OpencodeSession } from './opencode/types.ts';
|
|
45
|
+
export { health } from './health.ts';
|
|
46
|
+
export type { HealthOptions } from './health.ts';
|
|
47
|
+
export { i18n } from './i18n.ts';
|
|
48
|
+
export type { I18nOptions } from './i18n.ts';
|
|
49
|
+
export { mailer } from './mailer.ts';
|
|
50
|
+
export type { MailerOptions, MailOptions, Mailer } from './mailer.ts';
|
package/dist/index.js
CHANGED
|
@@ -63,6 +63,11 @@ async function sendResponse(res, response) {
|
|
|
63
63
|
}
|
|
64
64
|
res.end();
|
|
65
65
|
}
|
|
66
|
+
async function createTestServer(handler) {
|
|
67
|
+
const server = serve(handler, { port: 0 });
|
|
68
|
+
await server.ready;
|
|
69
|
+
return { server, url: `http://localhost:${server.port}` };
|
|
70
|
+
}
|
|
66
71
|
function serve(handler, options) {
|
|
67
72
|
const port = options?.port ?? 0;
|
|
68
73
|
const hostname = options?.hostname ?? "0.0.0.0";
|
|
@@ -329,13 +334,13 @@ var Router = class _Router {
|
|
|
329
334
|
const mw = match.middlewares[index++];
|
|
330
335
|
return mw(innerReq, ctx2, dispatch);
|
|
331
336
|
}
|
|
332
|
-
return await new Promise((
|
|
337
|
+
return await new Promise((resolve10) => {
|
|
333
338
|
try {
|
|
334
339
|
upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
|
|
335
|
-
|
|
340
|
+
resolve10(new Response(null, { status: 101 }));
|
|
336
341
|
} catch {
|
|
337
342
|
socket.destroy();
|
|
338
|
-
|
|
343
|
+
resolve10(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
339
344
|
}
|
|
340
345
|
});
|
|
341
346
|
};
|
|
@@ -1946,24 +1951,34 @@ function graphql(handler) {
|
|
|
1946
1951
|
}
|
|
1947
1952
|
return executeQuery(schema, params, options, req, ctx);
|
|
1948
1953
|
});
|
|
1949
|
-
return r;
|
|
1954
|
+
return { router: () => r };
|
|
1950
1955
|
}
|
|
1951
1956
|
|
|
1952
1957
|
// ai.ts
|
|
1953
|
-
var _ai = {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1958
|
+
var _ai = {};
|
|
1959
|
+
async function getStreamText() {
|
|
1960
|
+
if (!_ai.streamText) _ai.streamText = (await import("ai")).streamText;
|
|
1961
|
+
return _ai.streamText;
|
|
1962
|
+
}
|
|
1963
|
+
async function getStreamObject() {
|
|
1964
|
+
if (!_ai.streamObject) _ai.streamObject = (await import("ai")).streamObject;
|
|
1965
|
+
return _ai.streamObject;
|
|
1966
|
+
}
|
|
1967
|
+
async function aiStream(handler) {
|
|
1960
1968
|
const r = new Router();
|
|
1961
1969
|
r.post("/", async (req, ctx) => {
|
|
1962
1970
|
const options = await handler(req, ctx);
|
|
1963
|
-
|
|
1971
|
+
if (options.schema) {
|
|
1972
|
+
const streamObject = await getStreamObject();
|
|
1973
|
+
const { schema, ...params } = options;
|
|
1974
|
+
const result2 = streamObject({ ...params, schema, output: "object" });
|
|
1975
|
+
return result2.toTextStreamResponse();
|
|
1976
|
+
}
|
|
1977
|
+
const streamText3 = await getStreamText();
|
|
1978
|
+
const result = streamText3(options);
|
|
1964
1979
|
return result.toTextStreamResponse();
|
|
1965
1980
|
});
|
|
1966
|
-
return r;
|
|
1981
|
+
return { router: () => r };
|
|
1967
1982
|
}
|
|
1968
1983
|
|
|
1969
1984
|
// ai/workflow.ts
|
|
@@ -2090,7 +2105,10 @@ async function executeNode(node, ctx) {
|
|
|
2090
2105
|
const test = typeof c.test === "string" ? Boolean(resolveValue(c.test, ctx)) : c.test;
|
|
2091
2106
|
if (test && c.body) {
|
|
2092
2107
|
let last;
|
|
2093
|
-
for (const n of c.body)
|
|
2108
|
+
for (const n of c.body) {
|
|
2109
|
+
last = await executeNode(n, ctx);
|
|
2110
|
+
ctx.nodeOutputs.set(n.id, last);
|
|
2111
|
+
}
|
|
2094
2112
|
return last;
|
|
2095
2113
|
}
|
|
2096
2114
|
}
|
|
@@ -2106,7 +2124,10 @@ async function executeNode(node, ctx) {
|
|
|
2106
2124
|
ctx.stepCount++;
|
|
2107
2125
|
if (ctx.stepCount > ctx.maxSteps) throw new Error(`Step limit exceeded`);
|
|
2108
2126
|
if (!Boolean(evaluateExpression(conditionExpr, ctx))) break;
|
|
2109
|
-
for (const n of body ?? [])
|
|
2127
|
+
for (const n of body ?? []) {
|
|
2128
|
+
last = await executeNode(n, ctx);
|
|
2129
|
+
ctx.nodeOutputs.set(n.id, last);
|
|
2130
|
+
}
|
|
2110
2131
|
}
|
|
2111
2132
|
return last;
|
|
2112
2133
|
}
|
|
@@ -5085,14 +5106,14 @@ function forkApp(opts) {
|
|
|
5085
5106
|
return { child, port: opts.port };
|
|
5086
5107
|
}
|
|
5087
5108
|
function stopProcess(mp, timeout = 1e4) {
|
|
5088
|
-
return new Promise((
|
|
5109
|
+
return new Promise((resolve10) => {
|
|
5089
5110
|
const timer = setTimeout(() => {
|
|
5090
5111
|
mp.child.kill("SIGKILL");
|
|
5091
|
-
|
|
5112
|
+
resolve10();
|
|
5092
5113
|
}, timeout);
|
|
5093
5114
|
mp.child.on("exit", () => {
|
|
5094
5115
|
clearTimeout(timer);
|
|
5095
|
-
|
|
5116
|
+
resolve10();
|
|
5096
5117
|
});
|
|
5097
5118
|
mp.child.kill("SIGTERM");
|
|
5098
5119
|
});
|
|
@@ -5311,7 +5332,7 @@ async function deploy(config) {
|
|
|
5311
5332
|
});
|
|
5312
5333
|
const portSuffix = config.port !== 80 ? `:${config.port}` : "";
|
|
5313
5334
|
return {
|
|
5314
|
-
|
|
5335
|
+
close: async () => {
|
|
5315
5336
|
for (const [, app] of apps) {
|
|
5316
5337
|
if (app.restartTimer) clearTimeout(app.restartTimer);
|
|
5317
5338
|
if (app.process) {
|
|
@@ -5651,10 +5672,10 @@ function createBashTool(ctx) {
|
|
|
5651
5672
|
return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
|
|
5652
5673
|
}
|
|
5653
5674
|
const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
|
|
5654
|
-
return new Promise((
|
|
5675
|
+
return new Promise((resolve10) => {
|
|
5655
5676
|
const child = exec(command, { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
5656
5677
|
const truncated = stdout.length > 1e6 || stderr.length > 1e6;
|
|
5657
|
-
|
|
5678
|
+
resolve10({
|
|
5658
5679
|
stdout: stdout.slice(0, 1e6),
|
|
5659
5680
|
stderr: stderr.slice(0, 1e6),
|
|
5660
5681
|
exitCode: error?.code ?? 0,
|
|
@@ -5882,7 +5903,7 @@ function createQuestionTool(ctx) {
|
|
|
5882
5903
|
options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
|
|
5883
5904
|
}),
|
|
5884
5905
|
execute: async ({ question, options }, { toolCallId }) => {
|
|
5885
|
-
return new Promise((
|
|
5906
|
+
return new Promise((resolve10, reject) => {
|
|
5886
5907
|
const timeout = setTimeout(() => {
|
|
5887
5908
|
ctx.pendingQuestions.delete(toolCallId);
|
|
5888
5909
|
reject(new Error("Question timed out"));
|
|
@@ -5890,7 +5911,7 @@ function createQuestionTool(ctx) {
|
|
|
5890
5911
|
ctx.pendingQuestions.set(toolCallId, {
|
|
5891
5912
|
resolve: (answer) => {
|
|
5892
5913
|
clearTimeout(timeout);
|
|
5893
|
-
|
|
5914
|
+
resolve10(answer);
|
|
5894
5915
|
},
|
|
5895
5916
|
reject: (err) => {
|
|
5896
5917
|
clearTimeout(timeout);
|
|
@@ -6275,20 +6296,124 @@ async function opencode(options) {
|
|
|
6275
6296
|
close: () => base.close()
|
|
6276
6297
|
};
|
|
6277
6298
|
}
|
|
6299
|
+
|
|
6300
|
+
// health.ts
|
|
6301
|
+
function health(options) {
|
|
6302
|
+
const path2 = options?.path ?? "/health";
|
|
6303
|
+
const r = new Router();
|
|
6304
|
+
const handler = async () => {
|
|
6305
|
+
try {
|
|
6306
|
+
await options?.check?.();
|
|
6307
|
+
return new Response("OK", { status: 200 });
|
|
6308
|
+
} catch {
|
|
6309
|
+
return new Response("Service Unavailable", { status: 503 });
|
|
6310
|
+
}
|
|
6311
|
+
};
|
|
6312
|
+
r.get(path2, handler);
|
|
6313
|
+
r.head(path2, handler);
|
|
6314
|
+
return r;
|
|
6315
|
+
}
|
|
6316
|
+
|
|
6317
|
+
// i18n.ts
|
|
6318
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
6319
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
6320
|
+
import { join as join4, resolve as resolve9 } from "node:path";
|
|
6321
|
+
function i18n(options) {
|
|
6322
|
+
const dir = resolve9(options.dir);
|
|
6323
|
+
const defaultLocale = options.defaultLocale ?? "en";
|
|
6324
|
+
const cache = /* @__PURE__ */ new Map();
|
|
6325
|
+
async function load(locale) {
|
|
6326
|
+
const cached = cache.get(locale);
|
|
6327
|
+
if (cached) return cached;
|
|
6328
|
+
const filePath = join4(dir, `${locale}.json`);
|
|
6329
|
+
if (!existsSync3(filePath)) return {};
|
|
6330
|
+
try {
|
|
6331
|
+
const content = await readFile2(filePath, "utf-8");
|
|
6332
|
+
const data = JSON.parse(content);
|
|
6333
|
+
cache.set(locale, data);
|
|
6334
|
+
return data;
|
|
6335
|
+
} catch {
|
|
6336
|
+
return {};
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
function detect(req) {
|
|
6340
|
+
const url = new URL(req.url);
|
|
6341
|
+
const fromCookie = extractCookie(req, "locale");
|
|
6342
|
+
if (fromCookie) return fromCookie;
|
|
6343
|
+
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.split("-")[0];
|
|
6344
|
+
if (fromHeader) return fromHeader;
|
|
6345
|
+
return defaultLocale;
|
|
6346
|
+
}
|
|
6347
|
+
function t(localeMsgs, key, params) {
|
|
6348
|
+
let msg = localeMsgs[key];
|
|
6349
|
+
if (msg === void 0) msg = key;
|
|
6350
|
+
if (params) {
|
|
6351
|
+
for (const [k, v] of Object.entries(params)) {
|
|
6352
|
+
msg = msg.replace(`{${k}}`, v);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
return msg;
|
|
6356
|
+
}
|
|
6357
|
+
return async (req, ctx, next) => {
|
|
6358
|
+
const locale = detect(req);
|
|
6359
|
+
const msgs = await load(locale);
|
|
6360
|
+
ctx.locale = locale;
|
|
6361
|
+
ctx.t = (key, params) => t(msgs, key, params);
|
|
6362
|
+
return next(req, ctx);
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
function extractCookie(req, name) {
|
|
6366
|
+
const cookie = req.headers.get("cookie");
|
|
6367
|
+
if (!cookie) return null;
|
|
6368
|
+
for (const part of cookie.split(";")) {
|
|
6369
|
+
const [k, v] = part.trim().split("=");
|
|
6370
|
+
if (k === name && v) return decodeURIComponent(v);
|
|
6371
|
+
}
|
|
6372
|
+
return null;
|
|
6373
|
+
}
|
|
6374
|
+
|
|
6375
|
+
// mailer.ts
|
|
6376
|
+
import { createTransport } from "nodemailer";
|
|
6377
|
+
function mailer(options) {
|
|
6378
|
+
const sender = options.send;
|
|
6379
|
+
const from = options.from;
|
|
6380
|
+
let transporter = null;
|
|
6381
|
+
if (!sender && options.transport) {
|
|
6382
|
+
transporter = typeof options.transport === "string" ? createTransport(options.transport) : options.transport;
|
|
6383
|
+
}
|
|
6384
|
+
async function send(opts) {
|
|
6385
|
+
if (sender) {
|
|
6386
|
+
await sender(opts);
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6389
|
+
if (!transporter) {
|
|
6390
|
+
throw new Error("mailer: no transport configured \u2014 provide `transport` or `send` option");
|
|
6391
|
+
}
|
|
6392
|
+
await transporter.sendMail({ ...opts, from: opts.from ?? from });
|
|
6393
|
+
}
|
|
6394
|
+
async function close() {
|
|
6395
|
+
if (transporter) transporter.close();
|
|
6396
|
+
}
|
|
6397
|
+
return { send, close };
|
|
6398
|
+
}
|
|
6278
6399
|
export {
|
|
6279
6400
|
Router,
|
|
6280
6401
|
TsxContext,
|
|
6281
6402
|
agent,
|
|
6282
|
-
|
|
6403
|
+
aiStream,
|
|
6283
6404
|
auth,
|
|
6284
6405
|
compress,
|
|
6285
6406
|
cors,
|
|
6407
|
+
createTestServer,
|
|
6286
6408
|
defineConfig,
|
|
6287
6409
|
deleteCookie,
|
|
6288
6410
|
deploy,
|
|
6289
6411
|
getCookies,
|
|
6290
6412
|
graphql,
|
|
6413
|
+
health,
|
|
6414
|
+
i18n,
|
|
6291
6415
|
logger,
|
|
6416
|
+
mailer,
|
|
6292
6417
|
messager,
|
|
6293
6418
|
opencode,
|
|
6294
6419
|
postgres,
|
package/dist/mailer.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Transporter } from 'nodemailer';
|
|
2
|
+
export interface MailOptions {
|
|
3
|
+
to: string | string[];
|
|
4
|
+
subject: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
html?: string;
|
|
7
|
+
from?: string;
|
|
8
|
+
cc?: string | string[];
|
|
9
|
+
bcc?: string | string[];
|
|
10
|
+
}
|
|
11
|
+
export interface MailerOptions {
|
|
12
|
+
transport?: string | Transporter;
|
|
13
|
+
from?: string;
|
|
14
|
+
send?: (opts: MailOptions) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface Mailer {
|
|
17
|
+
send: (opts: MailOptions) => Promise<void>;
|
|
18
|
+
close: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function mailer(options: MailerOptions): Mailer;
|
package/dist/serve.d.ts
CHANGED
|
@@ -17,4 +17,8 @@ export interface Server {
|
|
|
17
17
|
export declare function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer>;
|
|
18
18
|
export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
|
|
19
19
|
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
20
|
+
export declare function createTestServer(handler: Handler): Promise<{
|
|
21
|
+
server: Server;
|
|
22
|
+
url: string;
|
|
23
|
+
}>;
|
|
20
24
|
export declare function serve(handler: Handler, options?: ServeOptions): Server;
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface Context {
|
|
|
4
4
|
user?: unknown;
|
|
5
5
|
parsed?: Record<string, unknown>;
|
|
6
6
|
mountPath?: string;
|
|
7
|
+
locale?: string;
|
|
8
|
+
t?: (key: string, params?: Record<string, string>) => string;
|
|
7
9
|
}
|
|
8
10
|
export type Handler = (req: Request, ctx: Context) => Response | Promise<Response>;
|
|
9
11
|
export type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"graphql": "^16",
|
|
29
29
|
"ioredis": "^5.11.0",
|
|
30
30
|
"jsonwebtoken": "^9.0.3",
|
|
31
|
+
"nodemailer": "^8.0.10",
|
|
31
32
|
"postgres": "^3.4.9",
|
|
32
33
|
"react": "^19",
|
|
33
34
|
"react-dom": "^19",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"license": "MIT",
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/jsonwebtoken": "^9.0.10",
|
|
43
|
+
"@types/nodemailer": "^8.0.0",
|
|
42
44
|
"@types/react": "^19",
|
|
43
45
|
"@types/react-dom": "^19",
|
|
44
46
|
"@types/ws": "^8.18.1",
|