diff --git a/e2e/fixtures/ssr-basic/src/main.tsx b/e2e/fixtures/ssr-basic/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/e2e/fixtures/ssr-basic/src/main.tsx +++ b/e2e/fixtures/ssr-basic/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/e2e/fixtures/ssr-swr/src/main.tsx b/e2e/fixtures/ssr-swr/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/e2e/fixtures/ssr-swr/src/main.tsx +++ b/e2e/fixtures/ssr-swr/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/e2e/fixtures/ssr-target-bundle/src/main.tsx b/e2e/fixtures/ssr-target-bundle/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/e2e/fixtures/ssr-target-bundle/src/main.tsx +++ b/e2e/fixtures/ssr-target-bundle/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/01_template/src/main.tsx b/examples/01_template/src/main.tsx index 2ac42e6ae..a17518496 100644 --- a/examples/01_template/src/main.tsx +++ b/examples/01_template/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/02_demo/src/main.tsx b/examples/02_demo/src/main.tsx index 2ac42e6ae..a17518496 100644 --- a/examples/02_demo/src/main.tsx +++ b/examples/02_demo/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/03_minimal/src/main.tsx b/examples/03_minimal/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/03_minimal/src/main.tsx +++ b/examples/03_minimal/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/04_promise/src/main.tsx b/examples/04_promise/src/main.tsx index dce443795..f213f62aa 100644 --- a/examples/04_promise/src/main.tsx +++ b/examples/04_promise/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/05_actions/src/main.tsx b/examples/05_actions/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/05_actions/src/main.tsx +++ b/examples/05_actions/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/06_nesting/src/components/AppWithoutSsr.tsx b/examples/06_nesting/src/components/AppWithoutSsr.tsx new file mode 100644 index 000000000..739bfd159 --- /dev/null +++ b/examples/06_nesting/src/components/AppWithoutSsr.tsx @@ -0,0 +1,14 @@ +import { CounterWithoutSsr } from './CounterWithoutSsr.js'; + +const AppWithoutSsr = () => { + return ( +
+ Waku +

Hello!!

+

This is a server component without SSR.

+ +
+ ); +}; + +export default AppWithoutSsr; diff --git a/examples/06_nesting/src/components/CounterWithoutSsr.tsx b/examples/06_nesting/src/components/CounterWithoutSsr.tsx new file mode 100644 index 000000000..8691f10ce --- /dev/null +++ b/examples/06_nesting/src/components/CounterWithoutSsr.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { useState } from 'react'; + +export const CounterWithoutSsr = () => { + if (typeof window === 'undefined') { + throw new Error('This component is for client only.'); + } + const [count, setCount] = useState(0); + const handleClick = () => { + setCount((c) => c + 1); + }; + return ( +
+

Count: {count}

+ +

This is a client component.

+
+ ); +}; diff --git a/examples/06_nesting/src/entries.tsx b/examples/06_nesting/src/entries.tsx index 8624b0612..0b0d31553 100644 --- a/examples/06_nesting/src/entries.tsx +++ b/examples/06_nesting/src/entries.tsx @@ -1,10 +1,10 @@ -import { lazy } from 'react'; import type { ReactNode } from 'react'; import { defineEntries } from 'waku/server'; import { Slot } from 'waku/client'; -const App = lazy(() => import('./components/App.js')); -const InnerApp = lazy(() => import('./components/InnerApp.js')); +import App from './components/App.js'; +import InnerApp from './components/InnerApp.js'; +import AppWithoutSsr from './components/AppWithoutSsr.js'; export default defineEntries( // renderEntries @@ -17,6 +17,9 @@ export default defineEntries( if (params.has('InnerApp')) { result.InnerApp = ; } + if (params.has('AppWithoutSsr')) { + result.AppWithoutSsr = ; + } return result; }, // getBuildConfig @@ -32,6 +35,11 @@ export default defineEntries( { input: 'InnerApp=5', skipPrefetch: true }, ], }, + { + pathname: '/no-ssr', + entries: [{ input: 'AppWithoutSsr' }], + isStatic: true, + }, ], // getSsrConfig async (pathname) => { @@ -41,6 +49,8 @@ export default defineEntries( input: '', body: , }; + case '/no-ssr': + return null; default: return null; } diff --git a/examples/06_nesting/src/main.tsx b/examples/06_nesting/src/main.tsx index bc436da93..dc03c864f 100644 --- a/examples/06_nesting/src/main.tsx +++ b/examples/06_nesting/src/main.tsx @@ -2,15 +2,25 @@ import { StrictMode } from 'react'; import { createRoot, hydrateRoot } from 'react-dom/client'; import { Root, Slot } from 'waku/client'; +const pathname = window.location.pathname; + const rootElement = ( - - - + {pathname === '/' ? ( + + + + ) : pathname === '/no-ssr' ? ( + + + + ) : ( +

Not Found

+ )}
); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/07_router/src/main.tsx b/examples/07_router/src/main.tsx index 93b72ef52..271f927b7 100644 --- a/examples/07_router/src/main.tsx +++ b/examples/07_router/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/08_cookies/src/main.tsx b/examples/08_cookies/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/08_cookies/src/main.tsx +++ b/examples/08_cookies/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/09_cssmodules/src/main.tsx b/examples/09_cssmodules/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/09_cssmodules/src/main.tsx +++ b/examples/09_cssmodules/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/10_fs-router/src/main.tsx b/examples/10_fs-router/src/main.tsx index 93b72ef52..271f927b7 100644 --- a/examples/10_fs-router/src/main.tsx +++ b/examples/10_fs-router/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/11_form/src/main.tsx b/examples/11_form/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/11_form/src/main.tsx +++ b/examples/11_form/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/12_css/src/main.tsx b/examples/12_css/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/12_css/src/main.tsx +++ b/examples/12_css/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/13_path-alias/src/main.tsx b/examples/13_path-alias/src/main.tsx index bc436da93..fe026c6b2 100644 --- a/examples/13_path-alias/src/main.tsx +++ b/examples/13_path-alias/src/main.tsx @@ -10,7 +10,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/examples/14_react-tweet/src/main.tsx b/examples/14_react-tweet/src/main.tsx index 2ac42e6ae..a17518496 100644 --- a/examples/14_react-tweet/src/main.tsx +++ b/examples/14_react-tweet/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement); diff --git a/packages/waku/src/cli.ts b/packages/waku/src/cli.ts index 92a43f409..4a82c0cd1 100644 --- a/packages/waku/src/cli.ts +++ b/packages/waku/src/cli.ts @@ -98,14 +98,6 @@ async function runDev(options: { ssr: boolean }) { '*', honoDevMiddleware({ ...options, config, env: process.env as any }), ); - app.notFound((c) => { - // TODO Will re-consider this later. Should not be hard-coded. - const file = 'public/404.html'; - if (existsSync(file)) { - return c.html(readFileSync(file, 'utf8'), 404); - } - return c.text('404 Not Found', 404); - }); const port = parseInt(process.env.PORT || '3000', 10); await startServer(app, port); } diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts index 1b88cfe8f..5f5a03b89 100644 --- a/packages/waku/src/lib/builder/build.ts +++ b/packages/waku/src/lib/builder/build.ts @@ -296,7 +296,7 @@ const buildSsrBundle = async ( base: config.basePath, plugins: [ rscIndexPlugin({ ...config, cssAssets }), - rscEnvPlugin({ config, hydrate: true }), + rscEnvPlugin({ config }), rscPrivatePlugin(config), ], ssr: isNodeCompatible @@ -349,7 +349,6 @@ const buildClientBundle = async ( config: ResolvedConfig, clientEntryFiles: Record, serverBuildOutput: Awaited>, - ssr: boolean, ) => { const mainJsFile = joinPath(rootDir, config.srcDir, config.mainJs); const nonJsAssets = serverBuildOutput.output.flatMap(({ type, fileName }) => @@ -361,7 +360,7 @@ const buildClientBundle = async ( plugins: [ viteReact(), rscIndexPlugin({ ...config, cssAssets }), - rscEnvPlugin({ config, hydrate: ssr }), + rscEnvPlugin({ config }), rscPrivatePlugin(config), ], build: { @@ -659,13 +658,7 @@ export async function build(options: { isNodeCompatible, ); } - await buildClientBundle( - rootDir, - config, - clientEntryFiles, - serverBuildOutput, - !!options.ssr, - ); + await buildClientBundle(rootDir, config, clientEntryFiles, serverBuildOutput); const distEntries = await import(filePathToFileURL(distEntriesFile)); const buildConfig = await getBuildConfig({ config, entries: distEntries }); diff --git a/packages/waku/src/lib/handlers/handler-dev.ts b/packages/waku/src/lib/handlers/handler-dev.ts index ad182016a..c84d2bfb9 100644 --- a/packages/waku/src/lib/handlers/handler-dev.ts +++ b/packages/waku/src/lib/handlers/handler-dev.ts @@ -56,7 +56,7 @@ export function createHandler< base: config.basePath, plugins: [ patchReactRefresh(viteReact()), - rscEnvPlugin({ config, hydrate: ssr }), + rscEnvPlugin({ config }), rscPrivatePlugin(config), rscIndexPlugin(config), rscHmrPlugin(), @@ -129,16 +129,6 @@ export function createHandler< }); }; - const willBeHandledByVite = async (pathname: string) => { - const vite = await vitePromise; - try { - const result = await vite.transformRequest(pathname); - return !!result; - } catch { - return false; - } - }; - return async (req, res, next) => { const [config, vite] = await Promise.all([configPromise, vitePromise]); const basePrefix = config.basePath + config.rscPath + '/'; @@ -181,7 +171,7 @@ export function createHandler< } return; } - if (ssr && !(await willBeHandledByVite(req.url.pathname))) { + if (ssr) { try { const readable = await renderHtml({ config, @@ -215,8 +205,6 @@ export function createHandler< .pipeTo(res.stream); return; } - next(); - return; } catch (e) { await handleError(e); return; diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-env.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-env.ts index ab394a1d3..325b60a81 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-env.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-env.ts @@ -2,13 +2,11 @@ import type { Plugin } from 'vite'; export function rscEnvPlugin({ config, - hydrate, }: { config?: { basePath: string; rscPath: string; }; - hydrate?: boolean | undefined; }): Plugin { return { name: 'rsc-env-plugin', @@ -28,9 +26,6 @@ export function rscEnvPlugin({ ], ] : []), - ...(hydrate - ? [['import.meta.env.WAKU_HYDRATE', JSON.stringify('true')]] - : []), ...Object.entries((globalThis as any).__WAKU_PRIVATE_ENV__).flatMap( ([k, v]) => k.startsWith('WAKU_PUBLIC_') diff --git a/packages/waku/src/lib/renderers/html-renderer.ts b/packages/waku/src/lib/renderers/html-renderer.ts index ac5520ee9..fdcaffd79 100644 --- a/packages/waku/src/lib/renderers/html-renderer.ts +++ b/packages/waku/src/lib/renderers/html-renderer.ts @@ -97,7 +97,7 @@ globalThis.__WAKU_PREFETCHED__ = { } data += decoder.decode(chunk); if (!headSent) { - if (!data.includes('')) { + if (!/<\/head>]*>/.test(data)) { return; } headSent = true; @@ -148,7 +148,7 @@ const buildHtml = ( 'html', null, createElement('head', { dangerouslySetInnerHTML: { __html: head } }), - createElement('body', null, body), + createElement('body', { 'data-hydrate': true }, body), ); export const renderHtml = async ( diff --git a/packages/website/src/main.tsx b/packages/website/src/main.tsx index 2ac42e6ae..a17518496 100644 --- a/packages/website/src/main.tsx +++ b/packages/website/src/main.tsx @@ -12,7 +12,7 @@ const rootElement = ( ); -if (import.meta.env.WAKU_HYDRATE) { +if (document.body.dataset.hydrate) { hydrateRoot(document.body, rootElement); } else { createRoot(document.body).render(rootElement);