weifuwu 0.31.1 → 0.32.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 +238 -120
- package/dist/ai/agent.d.ts +127 -0
- package/dist/ai/index.d.ts +26 -0
- package/dist/ai/types.d.ts +59 -0
- package/dist/core/router.d.ts +11 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +1186 -315
- package/dist/middleware/auth.d.ts +68 -0
- package/dist/middleware/esbuild-dev.d.ts +69 -0
- package/dist/middleware/esbuild-dev.js +335 -0
- package/dist/middleware/sandbox.d.ts +52 -0
- package/dist/middleware/tailwind-dev.d.ts +43 -0
- package/dist/middleware/tailwind-dev.js +199 -0
- package/dist/react/client.d.ts +64 -27
- package/dist/react/client.js +130 -248
- package/dist/react/compile.d.ts +17 -0
- package/dist/react/error-boundary.d.ts +18 -19
- package/dist/react/index.d.ts +80 -16
- package/dist/react/index.js +835 -267
- package/dist/react/types.d.ts +134 -34
- package/dist/types.d.ts +7 -0
- package/package.json +17 -6
- package/dist/react/navigation.d.ts +0 -63
- package/dist/react/navigation.js +0 -154
- package/dist/react/render.d.ts +0 -8
- package/dist/react/route-utils.d.ts +0 -31
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Middleware, User } from '../types.ts';
|
|
2
|
+
export interface AuthOptions {
|
|
3
|
+
/**
|
|
4
|
+
* JWT authentication.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* auth({ jwt: { secret: process.env.JWT_SECRET } })
|
|
8
|
+
*/
|
|
9
|
+
jwt?: {
|
|
10
|
+
/** HMAC secret for HS256. */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** Cookie name for the token (default: 'token'). */
|
|
13
|
+
cookie?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Simple session authentication via signed cookie.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* auth({ session: { secret: '...' } })
|
|
20
|
+
*/
|
|
21
|
+
session?: {
|
|
22
|
+
/** Secret for HMAC cookie signing. */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** Cookie name (default: 'session'). */
|
|
25
|
+
cookie?: string;
|
|
26
|
+
/** Load user from session data. */
|
|
27
|
+
loadUser: (data: Record<string, unknown>) => Promise<User | null> | User | null;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* API key authentication via header.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* auth({ apiKey: { header: 'x-api-key', keys: ['sk-xxx'] } })
|
|
34
|
+
*/
|
|
35
|
+
apiKey?: {
|
|
36
|
+
/** Header name (default: 'authorization'). */
|
|
37
|
+
header?: string;
|
|
38
|
+
/** Prefix to strip (default: 'Bearer '). */
|
|
39
|
+
prefix?: string;
|
|
40
|
+
/** Validate an API key → User or null. */
|
|
41
|
+
validate: (key: string) => Promise<User | null> | User | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Authentication middleware — injects `ctx.user`.
|
|
46
|
+
*
|
|
47
|
+
* Supports JWT (HS256), signed session cookies, and API keys.
|
|
48
|
+
* When no user is authenticated, requests still proceed —
|
|
49
|
+
* check `ctx.user` in your handlers to enforce auth.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* // JWT
|
|
54
|
+
* app.use(auth({ jwt: { secret: 'my-secret' } }))
|
|
55
|
+
*
|
|
56
|
+
* // Session cookie
|
|
57
|
+
* app.use(auth({
|
|
58
|
+
* session: {
|
|
59
|
+
* secret: '...',
|
|
60
|
+
* loadUser: async (data) => db.findUser(data.userId),
|
|
61
|
+
* },
|
|
62
|
+
* }))
|
|
63
|
+
*
|
|
64
|
+
* // API key
|
|
65
|
+
* app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function auth(opts: AuthOptions): Middleware;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* esbuildDev — on-the-fly esbuild compilation middleware for development.
|
|
3
|
+
*
|
|
4
|
+
* Compiles TypeScript/JSX/TSX client bundles in memory on first request,
|
|
5
|
+
* caches results based on source file mtime, and serves with ETag support.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { esbuildDev } from 'weifuwu'
|
|
10
|
+
*
|
|
11
|
+
* app.use(esbuildDev({
|
|
12
|
+
* entries: {
|
|
13
|
+
* '/assets/vendor.js': { entry: './client/vendor.ts', bundle: true },
|
|
14
|
+
* '/assets/client.js': { entry: './client/client.ts', external: ['react', 'react-dom/client'] },
|
|
15
|
+
* },
|
|
16
|
+
* importmap: true, // auto-generate importmap script
|
|
17
|
+
* }))
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import type { Middleware, Context } from '../types.ts';
|
|
21
|
+
export interface EsbuildDevEntry {
|
|
22
|
+
/** Source entry point relative to cwd or absolute. Ignored when `clientRouter` is set. */
|
|
23
|
+
entry?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Auto-generate a client entry from route config — eliminates client.ts.
|
|
26
|
+
* The generated entry imports routes + layout, calls createBrowserRouter().
|
|
27
|
+
*/
|
|
28
|
+
clientRouter?: {
|
|
29
|
+
/** Path to the shared routes file (relative to cwd). Overridden by `pages`. */
|
|
30
|
+
routes?: string;
|
|
31
|
+
/** Inline page definitions — alternative to a separate routes.ts file. */
|
|
32
|
+
pages?: Record<string, string>;
|
|
33
|
+
/** Layout component import path (relative to cwd). */
|
|
34
|
+
layout: string;
|
|
35
|
+
/** Fallback 404 component import path (relative to cwd). */
|
|
36
|
+
fallback?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Bundle all dependencies (default: true). */
|
|
39
|
+
bundle?: boolean;
|
|
40
|
+
/** Packages to leave external (not bundled). */
|
|
41
|
+
external?: string[];
|
|
42
|
+
/** Minify output (default: true). */
|
|
43
|
+
minify?: boolean;
|
|
44
|
+
/** Target platform (default: 'browser'). */
|
|
45
|
+
platform?: 'browser' | 'neutral' | 'node';
|
|
46
|
+
/** Output format (default: 'esm'). */
|
|
47
|
+
format?: 'esm' | 'iife';
|
|
48
|
+
/** Generate sourcemaps (default: false). */
|
|
49
|
+
sourcemap?: boolean | 'inline' | 'external' | 'linked';
|
|
50
|
+
/** Enable code splitting for dynamic import() (default: false). */
|
|
51
|
+
splitting?: boolean;
|
|
52
|
+
/** Compile-time constant substitution. */
|
|
53
|
+
define?: Record<string, string>;
|
|
54
|
+
/** Custom loaders per file extension. */
|
|
55
|
+
loader?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
export interface EsbuildDevOptions {
|
|
58
|
+
/** Map of URL path → entry config (string shorthand for { entry }). */
|
|
59
|
+
entries: Record<string, EsbuildDevEntry | string>;
|
|
60
|
+
/** Auto-generate a /assets/importmap script that maps react → vendor */
|
|
61
|
+
importmap?: boolean;
|
|
62
|
+
/** Importmap overrides (e.g. { 'react': '/assets/custom-vendor.js' }). */
|
|
63
|
+
importmapOverrides?: Record<string, string>;
|
|
64
|
+
/** Cache strategy: 'memory' keeps compiled results, 'none' recompiles every request. */
|
|
65
|
+
cache?: 'memory' | 'none';
|
|
66
|
+
/** Custom HTML template for build errors (receives escaped error text). */
|
|
67
|
+
errorTemplate?: (errors: string) => string;
|
|
68
|
+
}
|
|
69
|
+
export declare function esbuildDev(opts: EsbuildDevOptions): Middleware<Context, Context>;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/middleware/esbuild-dev.ts
|
|
2
|
+
import { resolve, relative, dirname } from "node:path";
|
|
3
|
+
import { stat, readFile, realpath } from "node:fs/promises";
|
|
4
|
+
function escapeHtml(s) {
|
|
5
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6
|
+
}
|
|
7
|
+
function defaultErrorTemplate(errors) {
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html>
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<title>Build Error</title>
|
|
13
|
+
<style>
|
|
14
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
15
|
+
body { font-family: system-ui; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
|
|
16
|
+
.overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
17
|
+
.card { background: #16213e; border: 1px solid #e74c3c; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
|
18
|
+
h1 { color: #e74c3c; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
19
|
+
pre { background: #0f0f23; padding: 1.5rem; border-radius: 8px; overflow: auto; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div class="overlay">
|
|
24
|
+
<div class="card">
|
|
25
|
+
<h1>\u26A0 esbuild Build Error</h1>
|
|
26
|
+
<pre>${escapeHtml(errors)}</pre>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>`;
|
|
31
|
+
}
|
|
32
|
+
async function collectDeps(entryPath) {
|
|
33
|
+
const files = /* @__PURE__ */ new Map();
|
|
34
|
+
const visited = /* @__PURE__ */ new Set();
|
|
35
|
+
const queue = [entryPath];
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const p = queue.shift();
|
|
38
|
+
try {
|
|
39
|
+
const real = await realpath(p);
|
|
40
|
+
if (visited.has(real)) continue;
|
|
41
|
+
visited.add(real);
|
|
42
|
+
const s = await stat(real);
|
|
43
|
+
files.set(real, s.mtimeMs);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(p, "utf-8");
|
|
49
|
+
const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
52
|
+
const spec = m[1] ?? m[2];
|
|
53
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
54
|
+
const dir = dirname(p);
|
|
55
|
+
const resolved = resolve(dir, spec);
|
|
56
|
+
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
57
|
+
const candidate = resolved + ext;
|
|
58
|
+
if (!visited.has(candidate)) {
|
|
59
|
+
queue.push(candidate);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
queue.push(resolved);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
function esbuildDev(opts) {
|
|
73
|
+
const cache = opts.cache ?? "memory";
|
|
74
|
+
const importmap = opts.importmap ?? false;
|
|
75
|
+
const importmapOverrides = opts.importmapOverrides ?? {};
|
|
76
|
+
const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate;
|
|
77
|
+
const entries = Object.entries(opts.entries).map(([p, e]) => {
|
|
78
|
+
const path = p.startsWith("/") ? p : "/" + p;
|
|
79
|
+
const config = typeof e === "string" ? { entry: e } : e;
|
|
80
|
+
return { path, config };
|
|
81
|
+
});
|
|
82
|
+
const cacheStore = /* @__PURE__ */ new Map();
|
|
83
|
+
const chunkMap = /* @__PURE__ */ new Map();
|
|
84
|
+
let esbuild_ = null;
|
|
85
|
+
let esbuildLoadError = null;
|
|
86
|
+
async function getEsbuild() {
|
|
87
|
+
if (esbuild_) return esbuild_;
|
|
88
|
+
try {
|
|
89
|
+
esbuild_ = await import("esbuild");
|
|
90
|
+
return esbuild_;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
esbuildLoadError = `esbuild is not installed. Run: npm install -D esbuild
|
|
93
|
+
|
|
94
|
+
${String(err)}`;
|
|
95
|
+
throw new Error(esbuildLoadError, { cause: err });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let importmapScript = null;
|
|
99
|
+
function buildImportmap() {
|
|
100
|
+
const mappings = {};
|
|
101
|
+
Object.assign(mappings, importmapOverrides);
|
|
102
|
+
for (const { path, config } of entries) {
|
|
103
|
+
const entryName = path.split("/").pop() ?? "";
|
|
104
|
+
if (entryName.includes("vendor") || config.external && config.external.length === 0 || !config.external && config.bundle !== false) {
|
|
105
|
+
if (!mappings["react"]) mappings["react"] = path;
|
|
106
|
+
if (!mappings["react/jsx-runtime"]) mappings["react/jsx-runtime"] = path;
|
|
107
|
+
if (!mappings["react-dom/client"]) mappings["react-dom/client"] = path;
|
|
108
|
+
if (!mappings["react-dom"]) mappings["react-dom"] = path;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const json = JSON.stringify({ imports: mappings });
|
|
112
|
+
return `<script type="importmap">${json}</script>`;
|
|
113
|
+
}
|
|
114
|
+
async function compile(entry) {
|
|
115
|
+
const esbuild = await getEsbuild();
|
|
116
|
+
const isClientRouter = !!entry.clientRouter;
|
|
117
|
+
let entryAbs;
|
|
118
|
+
let buildOpts;
|
|
119
|
+
if (isClientRouter) {
|
|
120
|
+
const cr = entry.clientRouter;
|
|
121
|
+
const virtualModule = "weifuwu:client-entry";
|
|
122
|
+
entryAbs = virtualModule;
|
|
123
|
+
const layoutImport = `import * as _layout from '${cr.layout}'
|
|
124
|
+
const _lx = Object.entries(_layout).filter(([,v]) => typeof v === 'function')
|
|
125
|
+
const Layout = _layout.default || (_lx[0]?.[1])`;
|
|
126
|
+
let routesCode;
|
|
127
|
+
if (cr.pages) {
|
|
128
|
+
const entries2 = Object.entries(cr.pages).map(
|
|
129
|
+
([path, component]) => ` '${path}': () => import('${component}'),`
|
|
130
|
+
);
|
|
131
|
+
routesCode = `const routes = {
|
|
132
|
+
${entries2.join("\n")}
|
|
133
|
+
}`;
|
|
134
|
+
} else if (cr.routes) {
|
|
135
|
+
routesCode = `import { routes } from '${cr.routes}'`;
|
|
136
|
+
} else {
|
|
137
|
+
routesCode = "const routes = {}";
|
|
138
|
+
}
|
|
139
|
+
const fallbackLine = cr.fallback ? `const fallback = () => import('${cr.fallback}')` : "";
|
|
140
|
+
const fallbackOpt = cr.fallback ? " fallback," : "";
|
|
141
|
+
const generatedCode = [
|
|
142
|
+
`import { createBrowserRouter } from 'weifuwu/react/client'`,
|
|
143
|
+
layoutImport,
|
|
144
|
+
routesCode,
|
|
145
|
+
fallbackLine,
|
|
146
|
+
"",
|
|
147
|
+
"createBrowserRouter({",
|
|
148
|
+
" layout: Layout,",
|
|
149
|
+
" routes,",
|
|
150
|
+
fallbackOpt,
|
|
151
|
+
"})"
|
|
152
|
+
].filter(Boolean).join("\n");
|
|
153
|
+
buildOpts = {
|
|
154
|
+
entryPoints: [virtualModule],
|
|
155
|
+
bundle: entry.bundle ?? true,
|
|
156
|
+
external: entry.external ?? [],
|
|
157
|
+
minify: entry.minify ?? true,
|
|
158
|
+
platform: entry.platform ?? "browser",
|
|
159
|
+
format: entry.format ?? "esm",
|
|
160
|
+
sourcemap: entry.sourcemap ?? false,
|
|
161
|
+
splitting: entry.splitting ?? false,
|
|
162
|
+
...entry.splitting ? { outdir: resolve(".weifuwu-esbuild-out") } : {},
|
|
163
|
+
define: entry.define,
|
|
164
|
+
loader: entry.loader,
|
|
165
|
+
write: false,
|
|
166
|
+
logLevel: "silent",
|
|
167
|
+
plugins: [{
|
|
168
|
+
name: "weifuwu-client-router",
|
|
169
|
+
setup(build) {
|
|
170
|
+
build.onResolve({ filter: new RegExp(`^${virtualModule.replace(/:/g, "\\:")}$`) }, () => ({
|
|
171
|
+
path: virtualModule,
|
|
172
|
+
namespace: "weifuwu-client"
|
|
173
|
+
}));
|
|
174
|
+
build.onLoad({ filter: /.*/, namespace: "weifuwu-client" }, () => ({
|
|
175
|
+
contents: generatedCode,
|
|
176
|
+
loader: "ts",
|
|
177
|
+
resolveDir: process.cwd()
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
}]
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
entryAbs = resolve(entry.entry);
|
|
184
|
+
buildOpts = {
|
|
185
|
+
entryPoints: [entryAbs],
|
|
186
|
+
bundle: entry.bundle ?? true,
|
|
187
|
+
external: entry.external ?? [],
|
|
188
|
+
minify: entry.minify ?? true,
|
|
189
|
+
platform: entry.platform ?? "browser",
|
|
190
|
+
format: entry.format ?? "esm",
|
|
191
|
+
sourcemap: entry.sourcemap ?? false,
|
|
192
|
+
splitting: entry.splitting ?? false,
|
|
193
|
+
...entry.splitting ? { outdir: dirname(entryAbs) } : {},
|
|
194
|
+
define: entry.define,
|
|
195
|
+
loader: entry.loader,
|
|
196
|
+
write: false,
|
|
197
|
+
logLevel: "silent"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const result = await esbuild.build(buildOpts);
|
|
201
|
+
const msgs = [];
|
|
202
|
+
for (const w of result.warnings) {
|
|
203
|
+
const loc = w.location ? ` at ${relative(".", w.location.file ?? "")}:${w.location.line}:${w.location.column}` : "";
|
|
204
|
+
msgs.push(`[warn] ${w.text}${loc}`);
|
|
205
|
+
}
|
|
206
|
+
for (const e of result.errors) {
|
|
207
|
+
const loc = e.location ? ` at ${relative(".", e.location.file ?? "")}:${e.location.line}:${e.location.column}` : "";
|
|
208
|
+
msgs.push(`[error] ${e.text}${loc}`);
|
|
209
|
+
}
|
|
210
|
+
if (result.errors.length > 0) {
|
|
211
|
+
throw new Error(msgs.join("\n"));
|
|
212
|
+
}
|
|
213
|
+
const outputFiles = result.outputFiles;
|
|
214
|
+
const entryFile = outputFiles.find((f) => f.path === entryAbs) ?? outputFiles[0];
|
|
215
|
+
const code = entryFile?.text ?? "";
|
|
216
|
+
let chunks;
|
|
217
|
+
if (outputFiles.length > 1) {
|
|
218
|
+
chunks = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const f of outputFiles) {
|
|
220
|
+
if (f === entryFile) continue;
|
|
221
|
+
const chunkName = f.path.split("/").pop() ?? f.path;
|
|
222
|
+
chunks.set(chunkName, { code: f.text, etag: `"esbuild-${hashCode(f.text)}"` });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const etag = `"esbuild-${hashCode(code)}"`;
|
|
226
|
+
if (msgs.length > 0) {
|
|
227
|
+
console.error("[esbuildDev]", msgs.join("\n"));
|
|
228
|
+
}
|
|
229
|
+
return { code, etag, chunks };
|
|
230
|
+
}
|
|
231
|
+
function isCacheValid(cached) {
|
|
232
|
+
return Promise.all(
|
|
233
|
+
[...cached.files].map(async ([file, mtime]) => {
|
|
234
|
+
try {
|
|
235
|
+
const s = await stat(file);
|
|
236
|
+
return s.mtimeMs === mtime;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
).then((results) => results.every(Boolean));
|
|
242
|
+
}
|
|
243
|
+
return async (req, ctx, next) => {
|
|
244
|
+
const url = new URL(req.url);
|
|
245
|
+
const pathname = url.pathname;
|
|
246
|
+
if (importmap && pathname === "/assets/importmap") {
|
|
247
|
+
if (!importmapScript) importmapScript = buildImportmap();
|
|
248
|
+
return new Response(importmapScript, {
|
|
249
|
+
headers: {
|
|
250
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
251
|
+
"Cache-Control": "no-cache"
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const matched = entries.find((e) => e.path === pathname);
|
|
256
|
+
const chunkName = pathname.split("/").pop() ?? "";
|
|
257
|
+
const chunkOwner = chunkMap.get(chunkName);
|
|
258
|
+
if (!matched && !chunkOwner) return next(req, ctx);
|
|
259
|
+
if (!matched && chunkOwner) {
|
|
260
|
+
const ownerCache = cacheStore.get(chunkOwner);
|
|
261
|
+
const chunk = ownerCache?.chunks?.get(chunkName);
|
|
262
|
+
if (chunk) {
|
|
263
|
+
if (req.headers.get("if-none-match") === chunk.etag) {
|
|
264
|
+
return new Response(null, { status: 304 });
|
|
265
|
+
}
|
|
266
|
+
return new Response(chunk.code, {
|
|
267
|
+
headers: {
|
|
268
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
269
|
+
ETag: chunk.etag,
|
|
270
|
+
"Cache-Control": "no-cache"
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return next(req, ctx);
|
|
275
|
+
}
|
|
276
|
+
const { config } = matched;
|
|
277
|
+
try {
|
|
278
|
+
if (cache === "memory") {
|
|
279
|
+
const cached = cacheStore.get(pathname);
|
|
280
|
+
if (cached) {
|
|
281
|
+
const valid = await isCacheValid(cached);
|
|
282
|
+
if (valid) {
|
|
283
|
+
if (req.headers.get("if-none-match") === cached.etag) {
|
|
284
|
+
return new Response(null, { status: 304 });
|
|
285
|
+
}
|
|
286
|
+
return new Response(cached.code, {
|
|
287
|
+
headers: {
|
|
288
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
289
|
+
ETag: cached.etag,
|
|
290
|
+
"Cache-Control": "no-cache"
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
for (const [cn] of cached.chunks ?? []) chunkMap.delete(cn);
|
|
295
|
+
cacheStore.delete(pathname);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const { code, etag, chunks } = await compile(config);
|
|
299
|
+
const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
|
|
300
|
+
const deps = await collectDeps(resolve(depEntry));
|
|
301
|
+
if (cache === "memory") {
|
|
302
|
+
cacheStore.set(pathname, { code, etag, files: deps, chunks });
|
|
303
|
+
if (chunks) {
|
|
304
|
+
for (const [cn] of chunks) chunkMap.set(cn, pathname);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return new Response(code, {
|
|
308
|
+
headers: {
|
|
309
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
310
|
+
ETag: etag,
|
|
311
|
+
"Cache-Control": "no-cache"
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
316
|
+
const html = errorTemplate(message);
|
|
317
|
+
return new Response(html, {
|
|
318
|
+
status: 500,
|
|
319
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function hashCode(s) {
|
|
325
|
+
let hash = 0;
|
|
326
|
+
for (let i = 0; i < s.length; i++) {
|
|
327
|
+
const ch = s.charCodeAt(i);
|
|
328
|
+
hash = (hash << 5) - hash + ch;
|
|
329
|
+
hash |= 0;
|
|
330
|
+
}
|
|
331
|
+
return Math.abs(hash).toString(36);
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
esbuildDev
|
|
335
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Middleware } from '../types.ts';
|
|
2
|
+
declare module '../types.ts' {
|
|
3
|
+
interface Context {
|
|
4
|
+
/** Filesystem sandbox for agent operations. Injected by sandbox(). */
|
|
5
|
+
sandbox: Sandbox;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface SandboxOptions {
|
|
9
|
+
/** Root directory for all workspaces (default: '/tmp/weifuwu-sandboxes'). */
|
|
10
|
+
baseDir?: string;
|
|
11
|
+
/** Default timeout for exec() in ms (default: 30000). */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/** Isolate workspaces by a ctx field. If 'user', uses ctx.user.id. */
|
|
14
|
+
isolateBy?: 'user';
|
|
15
|
+
}
|
|
16
|
+
export interface ExecResult {
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Sandbox {
|
|
21
|
+
/** The workspace directory for the current request. */
|
|
22
|
+
workDir: string;
|
|
23
|
+
/** Read a file inside the workspace. */
|
|
24
|
+
readFile(path: string): Promise<string>;
|
|
25
|
+
/** Write a file inside the workspace. Creates parent directories. */
|
|
26
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
27
|
+
/** Execute a command inside the workspace. */
|
|
28
|
+
exec(cmd: string, opts?: {
|
|
29
|
+
timeout?: number;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): Promise<ExecResult>;
|
|
32
|
+
/** Grep for a pattern inside the workspace. */
|
|
33
|
+
grep(pattern: string): Promise<string>;
|
|
34
|
+
/** Destroy the workspace (called when the session ends). */
|
|
35
|
+
destroy(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Filesystem sandbox middleware.
|
|
39
|
+
*
|
|
40
|
+
* Each request gets an isolated workspace directory. File and exec
|
|
41
|
+
* operations are restricted to the workspace — path escapes are rejected.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* app.use(sandbox({ baseDir: '/tmp/workspaces', isolateBy: 'user' }))
|
|
46
|
+
*
|
|
47
|
+
* // In a handler:
|
|
48
|
+
* await ctx.sandbox.writeFile('hello.txt', 'world')
|
|
49
|
+
* const content = await ctx.sandbox.readFile('hello.txt')
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function sandbox(opts?: SandboxOptions): Middleware;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tailwindDev — on-the-fly Tailwind CSS v4 compilation for development.
|
|
3
|
+
*
|
|
4
|
+
* Compiles a CSS entry file (with @import "tailwindcss") and scans content
|
|
5
|
+
* files for class names, generating optimized CSS in memory on first request.
|
|
6
|
+
*
|
|
7
|
+
* Caches results based on source file mtime and serves with ETag support.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { tailwindDev } from 'weifuwu'
|
|
12
|
+
*
|
|
13
|
+
* app.use(tailwindDev({
|
|
14
|
+
* '/assets/tailwind.css': {
|
|
15
|
+
* entry: './styles/input.css',
|
|
16
|
+
* content: ['./components/**\/*.ts', './pages/**\/*.ts'],
|
|
17
|
+
* },
|
|
18
|
+
* }))
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* CSS entry example (styles/input.css):
|
|
22
|
+
* ```css
|
|
23
|
+
* @import "tailwindcss";
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import type { Middleware, Context } from '../types.ts';
|
|
27
|
+
export interface TailwindDevEntry {
|
|
28
|
+
/** Path to CSS entry file (with @import "tailwindcss"). */
|
|
29
|
+
entry: string;
|
|
30
|
+
/** Glob patterns for content files to scan for class names. */
|
|
31
|
+
content?: string[];
|
|
32
|
+
/** Minify output (default: false in dev). */
|
|
33
|
+
minify?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface TailwindDevOptions {
|
|
36
|
+
/** Map of URL path → entry config (string shorthand for { entry }). */
|
|
37
|
+
entries: Record<string, TailwindDevEntry | string>;
|
|
38
|
+
/** Cache strategy: 'memory' keeps compiled results, 'none' recompiles every request. */
|
|
39
|
+
cache?: 'memory' | 'none';
|
|
40
|
+
/** Custom HTML template for build errors (receives escaped error text). */
|
|
41
|
+
errorTemplate?: (errors: string) => string;
|
|
42
|
+
}
|
|
43
|
+
export declare function tailwindDev(opts: TailwindDevOptions): Middleware<Context, Context>;
|