weifuwu 0.22.3 → 0.23.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 +292 -87
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -0
- package/cli/template/app.ts +3 -2
- package/cli/template/index.ts +2 -1
- package/dist/agent/run.d.ts +4 -3
- package/dist/agent/types.d.ts +3 -0
- package/dist/ai/provider.d.ts +36 -0
- package/dist/ai/utils.d.ts +5 -0
- package/dist/ai/workflow.d.ts +3 -0
- package/dist/ai.d.ts +9 -1
- package/dist/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- package/dist/client-theme.d.ts +1 -1
- package/dist/compile.d.ts +6 -0
- package/dist/cron-utils.d.ts +8 -0
- package/dist/flash.d.ts +24 -0
- package/dist/i18n.d.ts +14 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +1336 -821
- package/dist/kb/index.d.ts +3 -0
- package/dist/kb/types.d.ts +64 -0
- package/dist/permissions.d.ts +49 -0
- package/dist/queue/types.d.ts +12 -6
- package/dist/react.d.ts +1 -1
- package/dist/react.js +91 -86
- package/dist/session.d.ts +0 -1
- package/dist/ssr.d.ts +0 -1
- package/dist/stream.d.ts +5 -5
- package/dist/theme.d.ts +8 -0
- package/dist/tsx-context.d.ts +7 -1
- package/dist/types.d.ts +5 -3
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +21 -0
- package/dist/user/types.d.ts +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { KBOptions, KBModule, KBIngestOptions, KBSearchOptions, KBSearchResult, KBListEntry } from './types.ts';
|
|
2
|
+
export type { KBOptions, KBIngestOptions, KBSearchResult, KBSearchOptions, KBListEntry, KBModule };
|
|
3
|
+
export declare function knowledgeBase(options: KBOptions): KBModule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Middleware } from '../types.ts';
|
|
2
|
+
import type { AIProvider } from '../ai/provider.ts';
|
|
3
|
+
import type { PostgresClient } from '../postgres/types.ts';
|
|
4
|
+
export interface KBOptions {
|
|
5
|
+
/** Postgres client instance. */
|
|
6
|
+
pg: PostgresClient;
|
|
7
|
+
/** AI provider for embedding. */
|
|
8
|
+
provider: AIProvider;
|
|
9
|
+
/** Table name (default: '_kb_docs'). */
|
|
10
|
+
table?: string;
|
|
11
|
+
/** Default chunk size in characters (default: 512). */
|
|
12
|
+
chunkSize?: number;
|
|
13
|
+
/** Default chunk overlap in characters (default: 64). */
|
|
14
|
+
chunkOverlap?: number;
|
|
15
|
+
/** Default search limit (default: 5). */
|
|
16
|
+
searchLimit?: number;
|
|
17
|
+
/** Minimum similarity score threshold (0–1, default: 0). Set higher for stricter matches. */
|
|
18
|
+
searchThreshold?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface KBIngestOptions {
|
|
21
|
+
title?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
chunkSize?: number;
|
|
24
|
+
chunkOverlap?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface KBSearchResult {
|
|
27
|
+
id: number;
|
|
28
|
+
key: string;
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
score: number;
|
|
32
|
+
metadata: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
export interface KBSearchOptions {
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** Minimum cosine similarity score (0–1). Results below this are excluded. */
|
|
37
|
+
threshold?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface KBListEntry {
|
|
40
|
+
key: string;
|
|
41
|
+
title: string;
|
|
42
|
+
chunks: number;
|
|
43
|
+
}
|
|
44
|
+
export interface KBModule {
|
|
45
|
+
/**
|
|
46
|
+
* Ingest a document: chunk → embed → store.
|
|
47
|
+
* If a document with the same key exists, it is replaced (delete + re-insert).
|
|
48
|
+
* Returns the number of chunks created.
|
|
49
|
+
*/
|
|
50
|
+
ingest(key: string, content: string, options?: KBIngestOptions): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Search the knowledge base by semantic similarity.
|
|
53
|
+
* Query is embedded, then vector similarity search returns top results.
|
|
54
|
+
*/
|
|
55
|
+
search(query: string, searchOptions?: KBSearchOptions): Promise<KBSearchResult[]>;
|
|
56
|
+
/** Delete all chunks for a document key. */
|
|
57
|
+
delete(key: string): Promise<void>;
|
|
58
|
+
/** List all document keys with title and chunk count. */
|
|
59
|
+
list(): Promise<KBListEntry[]>;
|
|
60
|
+
/** Create the table and HNSW index. Safe to call multiple times. */
|
|
61
|
+
migrate(): Promise<void>;
|
|
62
|
+
/** Middleware that injects `ctx.kb` with `.search()` method. */
|
|
63
|
+
middleware(): Middleware;
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Sql } from './vendor.ts';
|
|
2
|
+
import type { Middleware, Context } from './types.ts';
|
|
3
|
+
declare module './types.ts' {
|
|
4
|
+
interface Context {
|
|
5
|
+
roles: Set<string>;
|
|
6
|
+
permissions: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export interface PermissionsOptions {
|
|
10
|
+
/** PostgreSQL client. */
|
|
11
|
+
pg: {
|
|
12
|
+
sql: Sql<{}>;
|
|
13
|
+
};
|
|
14
|
+
/** Table prefix (default: '' → _roles, _user_roles, _role_permissions). */
|
|
15
|
+
prefix?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PermissionsModule extends Middleware {
|
|
18
|
+
/**
|
|
19
|
+
* Middleware that injects `ctx.permissions = { roles, permissions }`.
|
|
20
|
+
* Reads `ctx.user.id` to look up role assignments.
|
|
21
|
+
* Must be placed after a middleware that sets `ctx.user`.
|
|
22
|
+
*/
|
|
23
|
+
(req: Request, ctx: Context, next: Handler): Response | Promise<Response>;
|
|
24
|
+
/** Assign a role to a user. Creates the role if it doesn't exist. */
|
|
25
|
+
assignRole(userId: number, role: string): Promise<void>;
|
|
26
|
+
/** Remove a role from a user. */
|
|
27
|
+
removeRole(userId: number, role: string): Promise<void>;
|
|
28
|
+
/** Grant a permission to a role. Creates the role if it doesn't exist. */
|
|
29
|
+
grantPermission(role: string, permission: string): Promise<void>;
|
|
30
|
+
/** Revoke a permission from a role. */
|
|
31
|
+
revokePermission(role: string, permission: string): Promise<void>;
|
|
32
|
+
/** Get all roles assigned to a user. */
|
|
33
|
+
getUserRoles(userId: number): Promise<string[]>;
|
|
34
|
+
/** Get all permissions for a user (union of all role permissions). */
|
|
35
|
+
getUserPermissions(userId: number): Promise<string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Middleware that rejects the request if the user does not have any of the specified roles.
|
|
38
|
+
* Must be placed after `permissions()` middleware (which injects ctx.permissions.roles).
|
|
39
|
+
*/
|
|
40
|
+
requireRole(...roles: string[]): Middleware;
|
|
41
|
+
/**
|
|
42
|
+
* Middleware that rejects the request if the user does not have all specified permissions.
|
|
43
|
+
* Must be placed after `permissions()` middleware (which injects ctx.permissions.permissions).
|
|
44
|
+
*/
|
|
45
|
+
requirePermission(...permissions: string[]): Middleware;
|
|
46
|
+
/** Create the underlying tables. Safe to call multiple times. */
|
|
47
|
+
migrate(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare function permissions(options: PermissionsOptions): PermissionsModule;
|
package/dist/queue/types.d.ts
CHANGED
|
@@ -14,10 +14,16 @@ export interface QueueJob<T = unknown> {
|
|
|
14
14
|
schedule?: string;
|
|
15
15
|
}
|
|
16
16
|
export interface QueueOptions {
|
|
17
|
+
/** Backend store. Default: 'memory'. */
|
|
18
|
+
store?: 'memory' | 'pg' | 'redis';
|
|
17
19
|
redis?: Redis;
|
|
18
20
|
url?: string;
|
|
19
21
|
prefix?: string;
|
|
20
22
|
pollInterval?: number;
|
|
23
|
+
/** PostgreSQL client (required when store: 'pg'). */
|
|
24
|
+
pg?: {
|
|
25
|
+
sql: import('../vendor.ts').Sql<{}>;
|
|
26
|
+
};
|
|
21
27
|
}
|
|
22
28
|
export interface QueueInjected {
|
|
23
29
|
queue: Queue;
|
|
@@ -27,6 +33,10 @@ export interface QueueJobWithError<T = unknown> extends QueueJob<T> {
|
|
|
27
33
|
failedAt: number;
|
|
28
34
|
}
|
|
29
35
|
export interface Queue extends Middleware<Context, Context & QueueInjected> {
|
|
36
|
+
/** Register a cron job. Uses queue's backend (memory/pg/redis) for execution. */
|
|
37
|
+
cron(pattern: string, handler: () => void | Promise<void>): {
|
|
38
|
+
stop: () => void;
|
|
39
|
+
};
|
|
30
40
|
add<T>(type: string, payload: T, opts?: {
|
|
31
41
|
delay?: number;
|
|
32
42
|
schedule?: string;
|
|
@@ -34,7 +44,6 @@ export interface Queue extends Middleware<Context, Context & QueueInjected> {
|
|
|
34
44
|
process<T>(type: string, handler: (job: QueueJob<T>) => Promise<void>): void;
|
|
35
45
|
run(): Promise<void>;
|
|
36
46
|
stop(): void;
|
|
37
|
-
/** Stats: { running, inflight, processed, failed, handlers, maxConcurrent } */
|
|
38
47
|
stats(): {
|
|
39
48
|
running: boolean;
|
|
40
49
|
inflight: number;
|
|
@@ -43,15 +52,12 @@ export interface Queue extends Middleware<Context, Context & QueueInjected> {
|
|
|
43
52
|
handlers: number;
|
|
44
53
|
maxConcurrent: number;
|
|
45
54
|
};
|
|
46
|
-
/** List pending jobs (up to `limit`). */
|
|
47
55
|
jobs(limit?: number): Promise<QueueJob[]>;
|
|
48
|
-
/** List failed jobs (up to `limit`). */
|
|
49
56
|
failedJobs(limit?: number): Promise<QueueJobWithError[]>;
|
|
50
|
-
/** Retry a specific failed job by re-adding it to the queue. */
|
|
51
57
|
retryFailed(jobId: string): Promise<boolean>;
|
|
52
|
-
/** Retry all failed jobs matching a type (or all types if omitted). */
|
|
53
58
|
retryAllFailed(type?: string): Promise<number>;
|
|
54
|
-
/** Returns a Router with management dashboard endpoints (GET/POST). */
|
|
55
59
|
dashboard(): import('../router.ts').Router;
|
|
60
|
+
/** Create the jobs table (PG mode only; safe to call multiple times). */
|
|
61
|
+
migrate?(): Promise<void>;
|
|
56
62
|
close(): Promise<void>;
|
|
57
63
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts
|
|
|
3
3
|
export { useAction } from './use-action.ts';
|
|
4
4
|
export type { UseActionOptions, UseActionReturn } from './use-action.ts';
|
|
5
5
|
export { Link, useNavigate, navigate, useNavigating, addInterceptor } from './client-router.ts';
|
|
6
|
-
export { TsxContext, useLoaderData } from './tsx-context.ts';
|
|
6
|
+
export { TsxContext, useCtx, setCtx, addCtxRebuilder, useLoaderData } from './tsx-context.ts';
|
|
7
7
|
export { Head } from './head.tsx';
|
|
8
8
|
export { createStore, useFetch, useQueryState } from './client-state.ts';
|
|
9
9
|
export type { StoreApi } from './client-state.ts';
|
package/dist/react.js
CHANGED
|
@@ -153,6 +153,65 @@ async function runInterceptors(url) {
|
|
|
153
153
|
return false;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// tsx-context.ts
|
|
157
|
+
import { useSyncExternalStore, createContext } from "react";
|
|
158
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {} };
|
|
159
|
+
var KEY = "__WEIFUWU_CTX_STORE";
|
|
160
|
+
function getStore() {
|
|
161
|
+
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
162
|
+
return globalThis[KEY];
|
|
163
|
+
}
|
|
164
|
+
const s = {
|
|
165
|
+
_ctx: DEFAULT_CTX,
|
|
166
|
+
_snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, theme: DEFAULT_CTX.theme, i18n: DEFAULT_CTX.i18n, loaderData: DEFAULT_CTX.loaderData, env: DEFAULT_CTX.env },
|
|
167
|
+
_listeners: /* @__PURE__ */ new Set(),
|
|
168
|
+
_rebuilders: [],
|
|
169
|
+
_alsGetStore: null
|
|
170
|
+
};
|
|
171
|
+
if (typeof globalThis !== "undefined") {
|
|
172
|
+
globalThis[KEY] = s;
|
|
173
|
+
}
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
var store = getStore();
|
|
177
|
+
function addCtxRebuilder(fn) {
|
|
178
|
+
store._rebuilders.push(fn);
|
|
179
|
+
}
|
|
180
|
+
var subscribe = (cb) => {
|
|
181
|
+
store._listeners.add(cb);
|
|
182
|
+
return () => {
|
|
183
|
+
store._listeners.delete(cb);
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
var getSnapshot = () => store._snapshot;
|
|
187
|
+
function setCtx(value) {
|
|
188
|
+
if (typeof window !== "undefined") {
|
|
189
|
+
for (const r of store._rebuilders) {
|
|
190
|
+
const rebuilt = r(value);
|
|
191
|
+
if (rebuilt) Object.assign(value, rebuilt);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
store._ctx = { ...store._ctx, ...value };
|
|
195
|
+
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, theme: store._ctx.theme, i18n: store._ctx.i18n, loaderData: store._ctx.loaderData, env: store._ctx.env };
|
|
196
|
+
if (typeof window !== "undefined") {
|
|
197
|
+
;
|
|
198
|
+
window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, ...value };
|
|
199
|
+
}
|
|
200
|
+
store._listeners.forEach((fn) => fn());
|
|
201
|
+
}
|
|
202
|
+
function useCtx() {
|
|
203
|
+
if (typeof window !== "undefined") {
|
|
204
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
205
|
+
}
|
|
206
|
+
const alsStore = store._alsGetStore?.();
|
|
207
|
+
return alsStore ?? store._ctx;
|
|
208
|
+
}
|
|
209
|
+
function useLoaderData() {
|
|
210
|
+
const ctx = useCtx();
|
|
211
|
+
return ctx.loaderData;
|
|
212
|
+
}
|
|
213
|
+
var TsxContext = createContext(DEFAULT_CTX);
|
|
214
|
+
|
|
156
215
|
// client-router.ts
|
|
157
216
|
var _navigating = false;
|
|
158
217
|
var _listeners = [];
|
|
@@ -187,37 +246,25 @@ async function navigate(href) {
|
|
|
187
246
|
return;
|
|
188
247
|
}
|
|
189
248
|
const newHtml = rootEl.innerHTML;
|
|
190
|
-
const propsMatch = html.match(/window\.__WEIFUWU_PROPS=(.+?)<\/script>/);
|
|
191
|
-
if (!propsMatch) {
|
|
192
|
-
location.href = href;
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
249
|
const bundleMatch = html.match(/src="(\/__ssr\/[^"]+\.js)"/);
|
|
196
250
|
const bundleUrl = bundleMatch ? bundleMatch[1] : null;
|
|
251
|
+
const ctxMatch = html.match(/window\.__WEIFUWU_CTX=(.+?)<\/script>/);
|
|
252
|
+
if (ctxMatch) {
|
|
253
|
+
try {
|
|
254
|
+
const ctx = JSON.parse(ctxMatch[1]);
|
|
255
|
+
window.__WEIFUWU_CTX = ctx;
|
|
256
|
+
setCtx(ctx);
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
}
|
|
197
260
|
applyHead(html);
|
|
198
261
|
const currentRoot = document.getElementById("__weifuwu_root");
|
|
199
262
|
if (!currentRoot) {
|
|
200
263
|
location.href = href;
|
|
201
264
|
return;
|
|
202
265
|
}
|
|
203
|
-
;
|
|
204
|
-
window.__WEIFUWU_PROPS = JSON.parse(propsMatch[1]);
|
|
205
266
|
history.pushState(null, "", url.pathname + url.search);
|
|
206
267
|
currentRoot.innerHTML = newHtml;
|
|
207
|
-
const ctxMatch = html.match(/window\.__WEIFUWU_CTX=(.+?)<\/script>/);
|
|
208
|
-
if (ctxMatch) {
|
|
209
|
-
try {
|
|
210
|
-
window.__WEIFUWU_CTX = JSON.parse(ctxMatch[1]);
|
|
211
|
-
} catch {
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const localeMatch = html.match(/window\.__LOCALE_DATA__=(.+?)<\/script>/);
|
|
215
|
-
if (localeMatch) {
|
|
216
|
-
try {
|
|
217
|
-
window.__LOCALE_DATA__ = JSON.parse(localeMatch[1]);
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
268
|
if (bundleUrl) {
|
|
222
269
|
try {
|
|
223
270
|
await import(
|
|
@@ -330,52 +377,6 @@ async function prefetchPage(href) {
|
|
|
330
377
|
}
|
|
331
378
|
}
|
|
332
379
|
|
|
333
|
-
// tsx-context.ts
|
|
334
|
-
import { useSyncExternalStore, createContext } from "react";
|
|
335
|
-
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
|
|
336
|
-
var KEY = "__WEIFUWU_CTX_STORE";
|
|
337
|
-
function getStore() {
|
|
338
|
-
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
339
|
-
return globalThis[KEY];
|
|
340
|
-
}
|
|
341
|
-
const s = {
|
|
342
|
-
_ctx: DEFAULT_CTX,
|
|
343
|
-
_snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, prefs: DEFAULT_CTX.prefs, env: DEFAULT_CTX.env },
|
|
344
|
-
_listeners: /* @__PURE__ */ new Set(),
|
|
345
|
-
_alsGetStore: null
|
|
346
|
-
};
|
|
347
|
-
if (typeof globalThis !== "undefined") {
|
|
348
|
-
globalThis[KEY] = s;
|
|
349
|
-
}
|
|
350
|
-
return s;
|
|
351
|
-
}
|
|
352
|
-
var store = getStore();
|
|
353
|
-
var subscribe = (cb) => {
|
|
354
|
-
store._listeners.add(cb);
|
|
355
|
-
return () => {
|
|
356
|
-
store._listeners.delete(cb);
|
|
357
|
-
};
|
|
358
|
-
};
|
|
359
|
-
var getSnapshot = () => store._snapshot;
|
|
360
|
-
function setCtx(value) {
|
|
361
|
-
store._ctx = { ...store._ctx, ...value };
|
|
362
|
-
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
|
|
363
|
-
store._listeners.forEach((fn) => fn());
|
|
364
|
-
}
|
|
365
|
-
function useCtx() {
|
|
366
|
-
if (typeof window !== "undefined") {
|
|
367
|
-
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
|
|
368
|
-
return { ...snapshot, ...window.__WEIFUWU_CTX };
|
|
369
|
-
}
|
|
370
|
-
const alsStore = store._alsGetStore?.();
|
|
371
|
-
return alsStore ?? store._ctx;
|
|
372
|
-
}
|
|
373
|
-
function useLoaderData() {
|
|
374
|
-
const ctx = useCtx();
|
|
375
|
-
return ctx.loaderData;
|
|
376
|
-
}
|
|
377
|
-
var TsxContext = createContext(DEFAULT_CTX);
|
|
378
|
-
|
|
379
380
|
// head.tsx
|
|
380
381
|
import { createElement as createElement2 } from "react";
|
|
381
382
|
function Head({ children }) {
|
|
@@ -516,9 +517,10 @@ function useQueryState(key, defaultValue = "") {
|
|
|
516
517
|
}
|
|
517
518
|
|
|
518
519
|
// client-locale.ts
|
|
519
|
-
function buildT() {
|
|
520
|
-
|
|
521
|
-
|
|
520
|
+
function buildT(messages) {
|
|
521
|
+
if (!messages || Object.keys(messages).length === 0) {
|
|
522
|
+
return (key, _p, fb) => fb ?? key;
|
|
523
|
+
}
|
|
522
524
|
return (key, params, fallback) => {
|
|
523
525
|
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
524
526
|
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
@@ -528,6 +530,12 @@ function buildT() {
|
|
|
528
530
|
return result;
|
|
529
531
|
};
|
|
530
532
|
}
|
|
533
|
+
addCtxRebuilder((value) => {
|
|
534
|
+
if (value.i18n?.messages) {
|
|
535
|
+
return { i18n: { ...value.i18n, t: buildT(value.i18n.messages) } };
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
});
|
|
531
539
|
addInterceptor(async (url) => {
|
|
532
540
|
const m = url.pathname.match(/^\/__lang\/([\w-]+)$/);
|
|
533
541
|
if (!m) return false;
|
|
@@ -536,11 +544,7 @@ addInterceptor(async (url) => {
|
|
|
536
544
|
headers: { accept: "application/json" }
|
|
537
545
|
});
|
|
538
546
|
const data = await res.json();
|
|
539
|
-
|
|
540
|
-
ctx.prefs = { ...ctx.prefs, locale: data.locale };
|
|
541
|
-
if (data.messages) window.__LOCALE_DATA__ = data.messages;
|
|
542
|
-
window.__WEIFUWU_CTX = ctx;
|
|
543
|
-
setCtx(ctx);
|
|
547
|
+
setCtx({ i18n: { locale: data.locale, messages: data.messages || {} } });
|
|
544
548
|
} catch {
|
|
545
549
|
location.href = url.href;
|
|
546
550
|
}
|
|
@@ -549,9 +553,9 @@ addInterceptor(async (url) => {
|
|
|
549
553
|
function useLocale() {
|
|
550
554
|
const ctx = useCtx();
|
|
551
555
|
return {
|
|
552
|
-
locale: ctx.
|
|
556
|
+
locale: ctx.i18n?.locale,
|
|
553
557
|
setLocale: (locale) => navigate("/__lang/" + locale),
|
|
554
|
-
t:
|
|
558
|
+
t: ctx.i18n?.t ?? ((key, _p, fb) => fb ?? key)
|
|
555
559
|
};
|
|
556
560
|
}
|
|
557
561
|
|
|
@@ -573,7 +577,7 @@ function applyTheme(theme) {
|
|
|
573
577
|
if (!_mqListener) {
|
|
574
578
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
575
579
|
mq.addEventListener("change", (e) => {
|
|
576
|
-
if (window.__WEIFUWU_CTX?.
|
|
580
|
+
if (window.__WEIFUWU_CTX?.theme?.value === "system") {
|
|
577
581
|
document.documentElement.dataset.theme = e.matches ? "dark" : "light";
|
|
578
582
|
}
|
|
579
583
|
});
|
|
@@ -589,11 +593,9 @@ addInterceptor(async (url) => {
|
|
|
589
593
|
headers: { accept: "application/json" }
|
|
590
594
|
});
|
|
591
595
|
const data = await res.json();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
window.__WEIFUWU_CTX = ctx;
|
|
596
|
+
window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, theme: { value: data.theme } };
|
|
597
|
+
setCtx({ theme: { value: data.theme } });
|
|
595
598
|
applyTheme(data.theme);
|
|
596
|
-
setCtx(ctx);
|
|
597
599
|
} catch {
|
|
598
600
|
location.href = url.href;
|
|
599
601
|
}
|
|
@@ -601,7 +603,7 @@ addInterceptor(async (url) => {
|
|
|
601
603
|
});
|
|
602
604
|
function useTheme() {
|
|
603
605
|
const ctx = useCtx();
|
|
604
|
-
const theme = ctx.
|
|
606
|
+
const theme = ctx.theme?.value ?? "system";
|
|
605
607
|
useEffect4(() => {
|
|
606
608
|
applyTheme(theme);
|
|
607
609
|
}, [theme]);
|
|
@@ -617,9 +619,9 @@ import { useState as useState5 } from "react";
|
|
|
617
619
|
function useFlashMessage() {
|
|
618
620
|
const [flash] = useState5(() => {
|
|
619
621
|
if (typeof window === "undefined") return null;
|
|
620
|
-
const raw = window.__WEIFUWU_CTX?.
|
|
621
|
-
if (
|
|
622
|
-
return
|
|
622
|
+
const raw = window.__WEIFUWU_CTX?.flash?.value;
|
|
623
|
+
if (raw === void 0 || raw === null) return null;
|
|
624
|
+
return raw;
|
|
623
625
|
});
|
|
624
626
|
return flash;
|
|
625
627
|
}
|
|
@@ -683,12 +685,15 @@ export {
|
|
|
683
685
|
Head,
|
|
684
686
|
Link,
|
|
685
687
|
TsxContext,
|
|
688
|
+
addCtxRebuilder,
|
|
686
689
|
addInterceptor,
|
|
687
690
|
applyTheme,
|
|
688
691
|
createStore,
|
|
689
692
|
navigate,
|
|
693
|
+
setCtx,
|
|
690
694
|
useAction,
|
|
691
695
|
useAgentStream,
|
|
696
|
+
useCtx,
|
|
692
697
|
useFetch,
|
|
693
698
|
useFlashMessage,
|
|
694
699
|
useLoaderData,
|
package/dist/session.d.ts
CHANGED
package/dist/ssr.d.ts
CHANGED
package/dist/stream.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ import type { Context } from './types.ts';
|
|
|
2
2
|
export interface StreamOpts {
|
|
3
3
|
ctx: Context;
|
|
4
4
|
base: string;
|
|
5
|
-
|
|
5
|
+
tailwind?: {
|
|
6
|
+
css: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
6
9
|
isDev: boolean;
|
|
7
10
|
status?: number;
|
|
8
|
-
bundle?: {
|
|
9
|
-
url: string;
|
|
10
|
-
} | null;
|
|
11
11
|
loaderData?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
13
|
export declare function readStream(stream: ReadableStream): Promise<string>;
|
|
14
|
-
export declare function streamResponse(reactStream: ReadableStream, opts: StreamOpts): Response;
|
|
14
|
+
export declare function streamResponse(reactStream: ReadableStream, opts: StreamOpts, hydrationScript?: string): Response;
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Middleware } from './types.ts';
|
|
2
|
+
export interface ThemeOptions {
|
|
3
|
+
/** Default theme value (default: 'system'). */
|
|
4
|
+
default?: string;
|
|
5
|
+
/** Cookie name (default: 'theme'). Set to empty string to disable cookie. */
|
|
6
|
+
cookie?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function theme(options?: ThemeOptions): Middleware;
|
package/dist/tsx-context.d.ts
CHANGED
|
@@ -5,10 +5,16 @@ export interface PageContext {
|
|
|
5
5
|
id?: string;
|
|
6
6
|
};
|
|
7
7
|
parsed: Record<string, unknown>;
|
|
8
|
-
|
|
8
|
+
theme?: string;
|
|
9
|
+
i18n?: {
|
|
10
|
+
locale: string;
|
|
11
|
+
t: (key: string, params?: Record<string, string>, fallback?: string) => string;
|
|
12
|
+
};
|
|
9
13
|
loaderData: Record<string, unknown>;
|
|
10
14
|
env: Record<string, string>;
|
|
11
15
|
}
|
|
16
|
+
type Rebuilder = (value: Partial<PageContext>) => Partial<PageContext> | null;
|
|
17
|
+
export declare function addCtxRebuilder(fn: Rebuilder): void;
|
|
12
18
|
/** @internal Injected by tsx-instance.ts for async-safe context isolation */
|
|
13
19
|
export declare function __registerAls(getStore: () => PageContext | undefined): void;
|
|
14
20
|
declare function setCtx(value: Partial<PageContext>): void;
|
package/dist/types.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ export interface Context {
|
|
|
4
4
|
user?: unknown;
|
|
5
5
|
parsed?: Record<string, unknown>;
|
|
6
6
|
mountPath?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
theme?: string;
|
|
8
|
+
i18n?: {
|
|
9
|
+
locale: string;
|
|
10
|
+
t: (key: string, params?: Record<string, string>, fallback?: string) => string;
|
|
11
|
+
};
|
|
10
12
|
env?: Record<string, string>;
|
|
11
13
|
layoutStack?: {
|
|
12
14
|
path: string;
|
package/dist/user/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { user } from './client.ts';
|
|
2
|
-
export type { UserOptions, UserData, UserModule, AuthResult, OAuth2Client, UserInjected } from './types.ts';
|
|
2
|
+
export type { UserOptions, UserData, UserModule, AuthResult, OAuth2Client, OAuthProviderConfig, UserInjected } from './types.ts';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Sql } from '../vendor.ts';
|
|
2
|
+
import type { Router } from '../router.ts';
|
|
3
|
+
import type { OAuthProviderConfig } from './types.ts';
|
|
4
|
+
interface OAuthLoginDeps {
|
|
5
|
+
sql: Sql<{}>;
|
|
6
|
+
jwtSecret: string;
|
|
7
|
+
expiresIn: string | number;
|
|
8
|
+
usersTable: string;
|
|
9
|
+
/** Table for provider-user link, derived from usersTable. */
|
|
10
|
+
providerTable: string;
|
|
11
|
+
redirectUrl: string;
|
|
12
|
+
signToken: (user: any) => string;
|
|
13
|
+
/** Create a placeholder user for OAuth login (no password). */
|
|
14
|
+
createPlaceholderUser: (email: string, name: string) => Promise<any>;
|
|
15
|
+
/** Find user by internal ID. */
|
|
16
|
+
findUserById: (id: number) => Promise<any | undefined>;
|
|
17
|
+
/** Find user by email. */
|
|
18
|
+
findUserByEmail: (email: string) => Promise<any | undefined>;
|
|
19
|
+
}
|
|
20
|
+
export declare function registerOAuthLoginRoutes(router: Router, deps: OAuthLoginDeps, providers: Record<string, OAuthProviderConfig>): void;
|
|
21
|
+
export {};
|
package/dist/user/types.d.ts
CHANGED
|
@@ -24,12 +24,43 @@ export interface OAuth2Client {
|
|
|
24
24
|
export interface OAuth2ServerOptions {
|
|
25
25
|
server: true;
|
|
26
26
|
}
|
|
27
|
+
export interface OAuthProviderConfig {
|
|
28
|
+
clientId: string;
|
|
29
|
+
clientSecret: string;
|
|
30
|
+
scope?: string;
|
|
31
|
+
/** Custom auth URL (overrides built-in provider default). */
|
|
32
|
+
authUrl?: string;
|
|
33
|
+
/** Custom token URL (overrides built-in provider default). */
|
|
34
|
+
tokenUrl?: string;
|
|
35
|
+
/** Custom user info URL (overrides built-in provider default). */
|
|
36
|
+
userUrl?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Custom user parser.
|
|
39
|
+
* Required when any of authUrl/tokenUrl/userUrl is custom.
|
|
40
|
+
* Receives the raw response from userUrl + the access token.
|
|
41
|
+
*/
|
|
42
|
+
parseUser?: (data: any, accessToken: string) => {
|
|
43
|
+
id: string;
|
|
44
|
+
email: string;
|
|
45
|
+
name: string;
|
|
46
|
+
avatarUrl?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
27
49
|
export interface UserOptions {
|
|
28
50
|
pg: PostgresClient;
|
|
29
51
|
jwtSecret: string;
|
|
30
52
|
table?: string;
|
|
31
53
|
expiresIn?: string | number;
|
|
32
54
|
oauth2?: OAuth2ServerOptions;
|
|
55
|
+
/**
|
|
56
|
+
* OAuth login providers (login with GitHub/Google).
|
|
57
|
+
* Registers GET /auth/:provider and GET /auth/:provider/callback routes.
|
|
58
|
+
*/
|
|
59
|
+
oauthLogin?: {
|
|
60
|
+
providers: Record<string, OAuthProviderConfig>;
|
|
61
|
+
/** Where to redirect after successful login (default: '/'). */
|
|
62
|
+
redirectUrl?: string;
|
|
63
|
+
};
|
|
33
64
|
}
|
|
34
65
|
export interface UserInjected {
|
|
35
66
|
user: UserData;
|