feat(preview): use waku beta 2

This commit is contained in:
Fuma Nama
2026-06-05 17:47:24 +08:00
parent e714fff621
commit 6dc8812030
5 changed files with 104 additions and 149 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-preview': patch
---
Use Waku beta 2
-147
View File
@@ -1,147 +0,0 @@
import path from 'node:path';
import { serve } from '@hono/node-server';
import { serveStatic } from '@hono/node-server/serve-static';
import type { MiddlewareHandler } from 'hono';
import { Hono } from 'hono/tiny';
import { unstable_createServerEntryAdapter as createServerEntryAdapter } from 'waku/adapter-builders';
import {
unstable_constants as constants,
unstable_honoMiddleware as honoMiddleware,
} from 'waku/internals';
import type { BuildOptions } from 'waku/adapters/node-build-enhancer';
import { createNodeWebSocket } from '@hono/node-ws';
import { startWatcher } from '../source/watcher';
import { getConfigRuntime } from '@/config/load-runtime';
import { type WSContext } from 'hono/ws';
import { encodeEvent, WebSocketEvent } from './hot-reload';
import { filesCache } from '../source/storage';
import { getSource } from '../source';
const { DIST_PUBLIC } = constants;
const { contextMiddleware, rscMiddleware, middlewareRunner } = honoMiddleware;
export default createServerEntryAdapter(
(
{ processRequest, processBuild, config, isBuild, notFoundHtml },
options?: {
middlewareFns?: (() => MiddlewareHandler)[];
middlewareModules?: Record<string, () => Promise<unknown>>;
},
) => {
const { middlewareFns = [], middlewareModules = {} } = options || {};
const app = new Hono();
let serveFn = serve;
app.notFound((c) => {
if (notFoundHtml) {
return c.html(notFoundHtml, 404);
}
return c.text('404 Not Found', 404);
});
if (isBuild) {
app.use(
`${config.basePath}*`,
serveStatic({
root: path.join(config.distDir, DIST_PUBLIC),
rewriteRequestPath: (path) => path.slice(config.basePath.length - 1),
}),
);
}
if (isBuild && process.env.HOT_RELOAD === '1') {
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
const clients = new Set<WSContext>();
void initHotReload(clients).catch((e) => {
console.error(e);
process.exit(1);
});
app.get(
'/_ws',
upgradeWebSocket(() => ({
onOpen(_, ws) {
clients.add(ws);
},
onClose(_, ws) {
clients.delete(ws);
},
})),
);
serveFn = (...args) => {
const server = serve(...args);
injectWebSocket(server);
for (const signal of ['SIGTERM', 'SIGINT'])
process.once(signal, () => {
server.close();
});
return server;
};
}
app.use(contextMiddleware() as never);
for (const middlewareFn of middlewareFns) {
app.use(middlewareFn());
}
app.use(middlewareRunner(middlewareModules as never) as never);
app.use(rscMiddleware({ processRequest }) as never);
const buildOptions: BuildOptions = {
distDir: config.distDir,
};
return {
fetch: app.fetch,
build: processBuild,
buildOptions,
buildEnhancers: ['waku/adapters/node-build-enhancer'],
serve: serveFn,
};
},
);
async function initHotReload(clients: Set<WSContext>) {
const watcher = await startWatcher(await getConfigRuntime());
function send(event: WebSocketEvent) {
const encoded = encodeEvent(event);
for (const client of clients) client.send(encoded);
}
watcher.on('all', (event, filePath) => {
switch (event) {
case 'change':
case 'unlink':
// if file wasn't parsed, no revalidation is needed
if (!filesCache.has(filePath)) return;
}
if (event === 'change') {
filesCache.delete(filePath);
send({
type: 'clear-cache',
absolutePath: filePath,
});
}
switch (event) {
case 'add':
case 'change':
case 'unlink':
getSource.revalidate(false);
send({
type: 'revalidate',
});
break;
}
});
for (const signal of ['SIGTERM', 'SIGINT'])
process.once(signal, () => {
watcher.close();
});
}
+1 -1
View File
@@ -18,6 +18,6 @@ export default async function RootElement({ children }: { children: ReactNode })
export async function getConfig() {
return {
render: 'static',
render: 'dynamic',
} as const;
}
+98
View File
@@ -0,0 +1,98 @@
import { createNodeWebSocket, NodeWebSocket } from '@hono/node-ws';
import adapter from 'waku/adapters/node';
import { fsRouter } from 'waku/router/server';
import { startWatcher } from './lib/source/watcher';
import { getConfigRuntime } from './config/load-runtime';
import { encodeEvent, WebSocketEvent } from './lib/waku/hot-reload';
import { filesCache } from './lib/source/storage';
import { serve } from '@hono/node-server';
import { getSource } from './lib/source';
const modules = import.meta.glob('./pages/**/*.{ts,tsx,js,jsx}', {
base: '/src',
});
const pages = fsRouter(modules);
let nodeWs: NodeWebSocket | undefined;
const v = adapter(pages, {
middlewareFns: [
({ app }) => {
if (process.env.HOT_RELOAD === '1') {
nodeWs = createNodeWebSocket({ app });
void initHotReload().catch((e) => {
console.error(e);
process.exit(1);
});
app.get(
'/_ws',
nodeWs.upgradeWebSocket(() => ({})),
);
}
return (_c, next) => next();
},
],
});
if ('serve' in v) {
v.serve = ((...args) => {
const server = serve(...args);
nodeWs?.injectWebSocket(server);
for (const signal of ['SIGTERM', 'SIGINT'])
process.once(signal, () => {
server.close();
});
return server;
}) as typeof serve;
}
export default v;
async function initHotReload() {
const wss = nodeWs!.wss;
const watcher = await startWatcher(await getConfigRuntime());
function send(event: WebSocketEvent) {
const encoded = encodeEvent(event);
for (const client of wss.clients) client.send(encoded);
}
watcher.on('all', (event, filePath) => {
switch (event) {
case 'change':
case 'unlink':
// if file wasn't parsed, no revalidation is needed
if (!filesCache.has(filePath)) return;
}
if (event === 'change') {
filesCache.delete(filePath);
send({
type: 'clear-cache',
absolutePath: filePath,
});
}
switch (event) {
case 'add':
case 'change':
case 'unlink':
getSource.revalidate(false);
send({
type: 'revalidate',
});
break;
}
});
for (const signal of ['SIGTERM', 'SIGINT'])
process.once(signal, () => {
watcher.close();
});
}
-1
View File
@@ -4,7 +4,6 @@ import { getConfig } from '@fumadocs/vite';
export default defineConfig({
distDir: 'dist/waku',
unstable_adapter: './src/lib/waku/adapter.ts',
vite: {
resolve: {
tsconfigPaths: true,