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.
@@ -0,0 +1,199 @@
1
+ // src/middleware/tailwind-dev.ts
2
+ import { resolve } from "node:path";
3
+ import { stat, readFile, glob } from "node:fs/promises";
4
+ function escapeHtml(s) {
5
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6
+ }
7
+ function defaultErrorTemplate(errors) {
8
+ return `<!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <title>Tailwind 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 #06b6d4; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
18
+ h1 { color: #06b6d4; 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>\u{1F3A8} Tailwind CSS Build Error</h1>
26
+ <pre>${escapeHtml(errors)}</pre>
27
+ </div>
28
+ </div>
29
+ </body>
30
+ </html>`;
31
+ }
32
+ var TW_CANDIDATE_RE = /[a-z][\w-]*(?::[a-z][\w-]*)*(?:-\[[^\]]*\])*(?:\/[0-9]+)?/gi;
33
+ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"'`\n]{2,})["'`])/g;
34
+ async function extractCandidates(patterns, root) {
35
+ const seen = /* @__PURE__ */ new Set();
36
+ for (const pattern of patterns) {
37
+ const resolved = resolve(root, pattern);
38
+ try {
39
+ for await (const file of glob(resolved)) {
40
+ try {
41
+ const content = await readFile(file, "utf-8");
42
+ for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
43
+ const str = m[1] ?? m[2];
44
+ if (!str) continue;
45
+ for (const c of str.matchAll(TW_CANDIDATE_RE)) {
46
+ seen.add(c[0]);
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ return [...seen];
56
+ }
57
+ async function collectFileMtimes(files) {
58
+ const map = /* @__PURE__ */ new Map();
59
+ await Promise.all(
60
+ files.map(async (f) => {
61
+ try {
62
+ const s = await stat(f);
63
+ map.set(f, s.mtimeMs);
64
+ } catch {
65
+ }
66
+ })
67
+ );
68
+ return map;
69
+ }
70
+ function isCacheValid(cached) {
71
+ return Promise.all(
72
+ [...cached.files].map(async ([file, mtime]) => {
73
+ try {
74
+ const s = await stat(file);
75
+ return s.mtimeMs === mtime;
76
+ } catch {
77
+ return false;
78
+ }
79
+ })
80
+ ).then((results) => results.every(Boolean));
81
+ }
82
+ function tailwindDev(opts) {
83
+ const cacheMode = opts.cache ?? "memory";
84
+ const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate;
85
+ const entries = Object.entries(opts.entries).map(([p, e]) => {
86
+ const path = p.startsWith("/") ? p : "/" + p;
87
+ const config = typeof e === "string" ? { entry: e } : e;
88
+ return { path, config };
89
+ });
90
+ const cacheStore = /* @__PURE__ */ new Map();
91
+ let compileFn = null;
92
+ let loadError = null;
93
+ async function getCompile() {
94
+ if (compileFn) return compileFn;
95
+ try {
96
+ const mod = await import("@tailwindcss/node");
97
+ compileFn = mod.compile;
98
+ return compileFn;
99
+ } catch (err) {
100
+ loadError = `@tailwindcss/node is not installed. Run: npm install -D tailwindcss @tailwindcss/node
101
+
102
+ ${String(err)}`;
103
+ throw new Error(loadError, { cause: err });
104
+ }
105
+ }
106
+ async function compileCss(entry) {
107
+ const compile = await getCompile();
108
+ const entryAbs = resolve(entry.entry);
109
+ const cssSource = await readFile(entryAbs, "utf-8");
110
+ const baseDir = resolve(entry.entry, "..");
111
+ const deps = [entryAbs];
112
+ const result = await compile(cssSource, {
113
+ base: baseDir,
114
+ onDependency: (p) => {
115
+ if (!deps.includes(p)) deps.push(p);
116
+ }
117
+ });
118
+ const contentPatterns = entry.content ?? [];
119
+ for (const src of result.sources) {
120
+ if (!src.negated) {
121
+ contentPatterns.push(resolve(src.base, src.pattern));
122
+ }
123
+ }
124
+ let candidates = [];
125
+ if (contentPatterns.length > 0) {
126
+ candidates = await extractCandidates(contentPatterns, baseDir);
127
+ }
128
+ const css = result.build(candidates);
129
+ for (const pattern of contentPatterns) {
130
+ try {
131
+ for await (const file of glob(resolve(baseDir, pattern))) {
132
+ if (!deps.includes(file)) deps.push(file);
133
+ }
134
+ } catch {
135
+ }
136
+ }
137
+ const etag = `"tw-${hashCode(css)}"`;
138
+ return { css, etag, files: deps };
139
+ }
140
+ return async (req, ctx, next) => {
141
+ const url = new URL(req.url);
142
+ const pathname = url.pathname;
143
+ const matched = entries.find((e) => e.path === pathname);
144
+ if (!matched) return next(req, ctx);
145
+ const { config } = matched;
146
+ try {
147
+ if (cacheMode === "memory") {
148
+ const cached = cacheStore.get(pathname);
149
+ if (cached) {
150
+ const valid = await isCacheValid(cached);
151
+ if (valid) {
152
+ if (req.headers.get("if-none-match") === cached.etag) {
153
+ return new Response(null, { status: 304 });
154
+ }
155
+ return new Response(cached.css, {
156
+ headers: {
157
+ "Content-Type": "text/css; charset=utf-8",
158
+ ETag: cached.etag,
159
+ "Cache-Control": "no-cache"
160
+ }
161
+ });
162
+ }
163
+ cacheStore.delete(pathname);
164
+ }
165
+ }
166
+ const { css, etag, files } = await compileCss(config);
167
+ if (cacheMode === "memory") {
168
+ const mtimes = await collectFileMtimes(files);
169
+ cacheStore.set(pathname, { css, etag, files: mtimes });
170
+ }
171
+ return new Response(css, {
172
+ headers: {
173
+ "Content-Type": "text/css; charset=utf-8",
174
+ ETag: etag,
175
+ "Cache-Control": "no-cache"
176
+ }
177
+ });
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : String(err);
180
+ const html = errorTemplate(message);
181
+ return new Response(html, {
182
+ status: 500,
183
+ headers: { "Content-Type": "text/html; charset=utf-8" }
184
+ });
185
+ }
186
+ };
187
+ }
188
+ function hashCode(s) {
189
+ let hash = 0;
190
+ for (let i = 0; i < s.length; i++) {
191
+ const ch = s.charCodeAt(i);
192
+ hash = (hash << 5) - hash + ch;
193
+ hash |= 0;
194
+ }
195
+ return Math.abs(hash).toString(36);
196
+ }
197
+ export {
198
+ tailwindDev
199
+ };
@@ -1,39 +1,76 @@
1
1
  /**
2
- * Client-side entry for React hydration + SPA navigation.
2
+ * Client-side entry for React hydration and SPA routing.
3
3
  *
4
- * Usage (in a client bundle):
4
+ * Hydration:
5
5
  * ```ts
6
- * import { hydrate, createClientRouter } from 'weifuwu/react/client'
7
- * import HomePage from './pages/HomePage.js'
6
+ * import { hydrate } from 'weifuwu/react/client'
7
+ * hydrate(HomePage, { layout: PageShell })
8
+ * ```
8
9
  *
9
- * const router = createClientRouter([
10
- * { path: '/', component: HomePage },
11
- * { path: '/users/:id', component: UserPage, loader: ... },
12
- * ])
13
- * hydrate(router.App)
10
+ * Client-side router:
11
+ * ```ts
12
+ * import { createBrowserRouter } from 'weifuwu/react/client'
13
+ * createBrowserRouter({
14
+ * layout: PageShell,
15
+ * routes: {
16
+ * '/': () => import('./pages/HomePage.tsx'),
17
+ * '/users/:id': () => import('./pages/UserPage.tsx'),
18
+ * },
19
+ * })
14
20
  * ```
15
21
  */
16
- import { type ComponentType, type ReactElement } from 'react';
17
- import { type LinkProps } from './navigation.ts';
22
+ import { type ComponentType, type ReactNode } from 'react';
18
23
  export interface HydrateOptions {
24
+ /** Hydration target element. Default: document.getElementById('root'). */
19
25
  container?: HTMLElement;
26
+ /**
27
+ * Layout component matching the server-side `react({ layout })` config.
28
+ * Must accept `{ children: ReactNode }`.
29
+ * When provided, wraps the page component before hydrating —
30
+ * producing the same tree the server rendered.
31
+ */
32
+ layout?: ComponentType<{
33
+ children: ReactNode;
34
+ }>;
20
35
  }
21
- export interface ClientRoute {
22
- path: string;
23
- component: ComponentType;
24
- loader?: (params: Record<string, string>) => Promise<Record<string, unknown>>;
36
+ /**
37
+ * Hydrate a page component. When `layout` is provided, the hydration tree
38
+ * matches the server-rendered structure: `<div id="root"><Layout><App /></Layout></div>`.
39
+ */
40
+ export declare function hydrate(App: ComponentType, opts?: HydrateOptions): void;
41
+ export interface ClientRouterOptions {
42
+ /** Layout component shared across all routes. */
43
+ layout?: ComponentType<{
44
+ children: ReactNode;
45
+ }>;
46
+ /**
47
+ * Route map: URL pattern → lazy component loader.
48
+ * Patterns support `:param` segments (e.g. `/users/:id`).
49
+ */
50
+ routes: Record<string, () => Promise<Record<string, unknown>>>;
51
+ /**
52
+ * Fallback component for unmatched routes.
53
+ * When omitted, unmatched routes do a full page navigation.
54
+ */
55
+ fallback?: () => Promise<Record<string, unknown>>;
56
+ /** Target element. Default: document.getElementById('root'). */
57
+ container?: HTMLElement;
25
58
  }
26
- export interface ClientRouter {
27
- App: ComponentType;
28
- Link: (props: LinkProps) => ReactElement;
29
- useParams: () => Record<string, string>;
30
- navigate: (url: string) => Promise<void>;
59
+ interface RouterHandle {
60
+ navigate(url: string): Promise<void>;
61
+ renderPath(pathname: string, isInitial: boolean, data?: Record<string, unknown>): Promise<void>;
31
62
  }
32
- export { Link, useParams, useNavigate, useRevalidate, Form, useNavigation } from './navigation.ts';
33
- export type { LinkProps, FormProps, NavigationState } from './navigation.ts';
34
- export { ErrorBoundary } from './error-boundary.ts';
35
- export type { ErrorBoundaryProps } from './error-boundary.ts';
36
- export { defineRoute } from './route-utils.ts';
37
- export declare function createClientRouter(routes: ClientRoute[]): ClientRouter;
38
- export declare function hydrate(App: ComponentType, opts?: HydrateOptions): void;
63
+ /**
64
+ * Initialise the client-side router.
65
+ *
66
+ * On the initial page load, it hydrates the matching route from the
67
+ * server-rendered HTML. Subsequent in-app navigations fetch fresh
68
+ * server data and render the new page client-side.
69
+ */
70
+ export declare function createBrowserRouter(opts: ClientRouterOptions): RouterHandle;
71
+ /**
72
+ * Programmatic navigation (call from anywhere).
73
+ * Falls back to `window.location.href = url` if the router hasn't been initialised.
74
+ */
75
+ export declare function navigate(url: string): void;
39
76
  export { useServerData } from './hooks.ts';