GithubHelp home page GithubHelp logo

dai-shi / waku Goto Github PK

View Code? Open in Web Editor NEW
3.9K 3.9K 103.0 2.78 MB

⛩️ The minimal React framework

Home Page: https://waku.gg

License: MIT License

JavaScript 0.62% TypeScript 91.54% MDX 7.34% CSS 0.50%

waku's Introduction

Waku

⛩️ The minimal React framework

visit waku.gg or npm create waku@latest

Build Status Version Downloads Discord Shield


Introduction

Waku (wah-ku) or わく means “framework” in Japanese. As the minimal React framework, it’s designed to accelerate the work of developers at startups and agencies building small to medium-sized React projects. These include marketing websites, light ecommerce, and web applications.

We recommend other frameworks for heavy ecommerce or enterprise applications. Waku is a lightweight alternative bringing a fast developer experience to the modern React server components era. Yes, let’s make React development fast again!

Waku is in rapid development and some features are currently missing. Please try it on non-production projects and report any issues you may encounter. Expect that there will be some breaking changes on the road towards a stable v1 release. Contributors are welcome.

Getting started

Start a new Waku project with the create command for your preferred package manager. It will scaffold a new project with our default Waku starter.

npm create waku@latest

Node.js version requirement: ^20.8.0 or ^18.17.0

Rendering

While there’s a little bit of a learning curve to modern React rendering, it introduces powerful new patterns of full-stack composability that are only possible with the advent of server components.

So please don’t be intimidated by the 'use client' directive! Once you get the hang of it, you’ll appreciate how awesome it is to flexibly move server-client boundaries with a single line of code as your full-stack React codebase evolves over time. It’s way simpler than maintaining separate codebases for your backend and frontend.

And please don’t fret about client components! Even if you only lightly optimize towards server components, your client bundle size will be smaller than traditional React frameworks, which are always 100% client components.

Future versions of Waku may provide additional opt-in APIs to abstract some of the complexity away for an improved developer experience.

Server components

Server components can be made async and can securely perform server-side logic and data fetching. Feel free to access the local file-system and import heavy dependencies since they aren’t included in the client bundle. They have no state, interactivity, or access to browser APIs since they run exclusively on the server.

// server component
import db from 'some-db';

import { Gallery } from '../components/gallery.js';

export const Store = async () => {
  const products = await db.query('SELECT * FROM products');

  return <Gallery products={products} />;
};

Client components

A 'use client' directive placed at the top of a file will create a server-client boundary when imported into a server component. All components imported below the boundary will be hydrated to run in the browser as well. They can use all traditional React features such as state, effects, and event handlers.

// client component
'use client';

import { useState } from 'react';

export const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <div>Count: {count}</div>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </>
  );
};

Shared components

Simple React components that meet all of the rules of both server and client components can be imported into either server or client components without affecting the server-client boundary.

// shared component
export const Headline = ({ children }) => {
  return <h3>{children}</h3>;
};

Weaving patterns

Server components can import client components and doing so will create a server-client boundary. Client components cannot import server components, but they can accept server components as props such as children. For example, you may want to add global context providers this way.

// ./src/pages/_layout.tsx
import { Providers } from '../components/providers.js';

export default async function RootLayout({ children }) {
  return (
    <Providers>
      <main>{children}</main>
    </Providers>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
// ./src/components/providers.tsx
'use client';

import { Provider } from 'jotai';

export const Providers = ({ children }) => {
  return <Provider>{children}</Provider>;
};

Server-side rendering

Waku provides static prerendering (SSG) and server-side rendering (SSR) options for both layouts and pages including all of their server and client components. SSR is a distinct concept from RSC.

tl;dr:

Each layout and page in Waku is composed of a React component hierarchy.

It begins with a server component at the top of the tree. Then at points down the hierarchy, you’ll eventually import a component that needs client component APIs. Mark this file with a 'use client' directive at the top. When imported into a server component, it will create a server-client boundary. Below this point, all imported components are hydrated and will run in the browser as well.

Server components can be rendered below this boundary, but only via composition (e.g., children props). Together they form a new “React server” layer that runs before the traditional “React client” layer with which you’re already familiar.

Client components are server-side rendered as SSR is separate from RSC. See the linked diagrams for a helpful visual.

Further reading

To learn more about the modern React architecture, we recommend Making Sense of React Server Components and The Two Reacts: Part 1.

Routing

Waku provides minimal file-based “pages router” experience built for the modern React server components era.

Its underlying low-level API is also available for those that prefer programmatic routing. This documentation covers file-based routing since many React developers prefer it, but feel free to try both and see which you prefer.

Overview

The directory for file-based routing in Waku projects is ./src/pages.

Layouts and pages can be created by making a new file with two exports: a default function for the React component and a named getConfig function that returns a configuration object to specify the render method and other options.

Waku currently supports two rendering options:

  • 'static' for static prerendering (SSG)

  • 'dynamic' for server-side rendering (SSR)

For example, you can statically prerender a global header and footer in the root layout at build time, but dynamically render the rest of a home page at request time for personalized user experiences.

// ./src/pages/_layout.tsx
import '../styles.css';

import { Providers } from '../components/providers.js';
import { Header } from '../components/header.js';
import { Footer } from '../components/footer.js';

// Create root layout
export default async function RootLayout({ children }) {
  return (
    <Providers>
      <Header />
      <main>{children}</main>
      <Footer />
    </Providers>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
// ./src/pages/index.tsx

// Create home page
export default async function HomePage() {
  const data = await getData();

  return (
    <>
      <h1>{data.title}</h1>
      <div>{data.content}</div>
    </>
  );
}

const getData = async () => {
  /* ... */
};

export const getConfig = async () => {
  return {
    render: 'dynamic',
  };
};

Pages

Single routes

Pages can be rendered as a single route (e.g., about.tsx or blog.tsx).

// ./src/pages/about.tsx

// Create about page
export default async function AboutPage() {
  return <>{/* ...*/}</>;
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
// ./src/pages/blog.tsx

// Create blog index page
export default async function BlogIndexPage() {
  return <>{/* ...*/}</>;
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};

Segment routes

Pages can also render a segment route (e.g., [slug].tsx) marked with brackets.

The rendered React component automatically receives a prop named by the segment (e.g, slug) with the value of the rendered segment (e.g., 'introducing-waku').

If statically prerendering a segment route at build time, a staticPaths array must also be provided.

// ./src/pages/blog/[slug].tsx

// Create blog article pages
export default async function BlogArticlePage({ slug }) {
  const data = await getData(slug);

  return <>{/* ...*/}</>;
}

const getData = async (slug) => {
  /* ... */
};

export const getConfig = async () => {
  return {
    render: 'static',
    staticPaths: ['introducing-waku', 'introducing-pages-router'],
  };
};
// ./src/pages/shop/[category].tsx

// Create product category pages
export default async function ProductCategoryPage({ category }) {
  const data = await getData(category);

  return <>{/* ...*/}</>;
}

const getData = async (category) => {
  /* ... */
};

export const getConfig = async () => {
  return {
    render: 'dynamic',
  };
};

Static paths (or other config values) can also be generated programmatically.

// ./src/pages/blog/[slug].tsx

// Create blog article pages
export default async function BlogArticlePage({ slug }) {
  const data = await getData(slug);

  return <>{/* ...*/}</>;
}

const getData = async (slug) => {
  /* ... */
};

export const getConfig = async () => {
  const staticPaths = await getStaticPaths();

  return {
    render: 'static',
    staticPaths,
  };
};

const getStaticPaths = async () => {
  /* ... */
};

Nested segment routes

Routes can contain multiple segments (e.g., /shop/[category]/[product]) by creating folders with brackets as well.

// ./src/pages/shop/[category]/[product].tsx

// Create product category pages
export default async function ProductDetailPage({ category, product }) {
  return <>{/* ...*/}</>;
}

export const getConfig = async () => {
  return {
    render: 'dynamic',
  };
};

For static prerendering of nested segment routes, the staticPaths array is instead composed of ordered arrays.

// ./src/pages/shop/[category]/[product].tsx

// Create product detail pages
export default async function ProductDetailPage({ category, product }) {
  return <>{/* ...*/}</>;
}

export const getConfig = async () => {
  return {
    render: 'static',
    staticPaths: [
      ['same-category', 'some-product'],
      ['same-category', 'another-product'],
    ],
  };
};

Catch-all routes

Catch-all or “wildcard” segment routes (e.g., /app/[...catchAll]) are marked with an ellipsis before the name and have indefinite segments.

Wildcard routes receive a prop with segment values as an ordered array. For example, the /app/profile/settings route would receive a catchAll prop with the value ['profile', 'settings']. These values can then be used to determine what to render in the component.

// ./src/pages/app/[...catchAll].tsx

// Create dashboard page
export default async function DashboardPage({ catchAll }) {
  return <>{/* ...*/}</>;
}

export const getConfig = async () => {
  return {
    render: 'dynamic',
  };
};

Layouts

Layouts are created with a special _layout.tsx file name and wrap the entire route and its descendents. They must accept a children prop of type ReactNode. While not required, you will typically want at least a root layout.

Root layout

The root layout placed at ./pages/_layout.tsx is especially useful. It can be used for setting global styles, global metadata, global providers, global data, and global components, such as a header and footer.

// ./src/pages/_layout.tsx
import '../styles.css';

import { Providers } from '../components/providers.js';
import { Header } from '../components/header.js';
import { Footer } from '../components/footer.js';

// Create root layout
export default async function RootLayout({ children }) {
  return (
    <Providers>
      <link rel="icon" type="image/png" href="/images/favicon.png" />
      <meta property="og:image" content="/images/opengraph.png" />
      <Header />
      <main>{children}</main>
      <Footer />
    </Providers>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
// ./src/components/providers.tsx
'use client';

import { createStore, Provider } from 'jotai';

const store = createStore();

export const Providers = ({ children }) => {
  return <Provider store={store}>{children}</Provider>;
};

Other layouts

Layouts are also helpful in nested routes. For example, you can add a layout at ./pages/blog/_layout.tsx to add a sidebar to both the blog index and all blog article pages.

// ./src/pages/blog/_layout.tsx
import { Sidebar } from '../../components/sidebar.js';

// Create blog layout
export default async function BlogLayout({ children }) {
  return (
    <div className="flex">
      <div>{children}</div>
      <Sidebar />
    </div>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};

Navigation

Link

The<Link /> component should be used for internal links. It accepts a to prop for the destination, which is automatically prefetched ahead of the navigation.

// ./src/pages/index.tsx
import { Link } from 'waku';

export default async function HomePage() {
  return (
    <>
      <h1>Home</h1>
      <Link to="/about">About</Link>
    </>
  );
}

useRouter

The useRouter hook can be used to inspect the current route or perform programmatic navigation.

router properties

The router object has a value property with two additional properties related to the current route: path (string) and searchParams (URLSearchParams).

'use client';

import { useRouter_UNSTABLE as useRouter } from 'waku';

export const Component = () => {
  const router = useRouter();
  const { path, searchParams } = router.value;

  return (
    <>
      <div>current path: {path}</div>
      <div>current searchParams: {searchParams.toString()}</div>
    </>
  );
};

router methods

The router object also contains several methods for programmatic navigation:

  • router.push(to: string) - navigate to the provided route

  • router.prefetch(to: string) - prefetch the provided route

  • router.replace(to: string) - replace the current history entry

  • router.reload() - reload the current route

  • router.back() - navigate to the previous entry in the session history

  • router.forward() - navigate to the next entry in the session history

'use client';

import { useRouter_UNSTABLE as useRouter } from 'waku';

export const Component = () => {
  const router = useRouter();

  return (
    <>
      <button onClick={() => router.push('/')}>Home</button>
      <button onClick={() => router.back()}>Back</button>
    </>
  );
};

Metadata

Waku automatically hoists any title, meta, and link tags to the document head. So adding meta tags is as simple as adding it to any of your layout or page components.

// ./src/pages/_layout.tsx
export default async function RootLayout({ children }) {
  return (
    <>
      <link rel="icon" type="image/png" href="/images/favicon.png" />
      <meta property="og:image" content="/images/opengraph.png" />
      {children}
    </>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
// ./src/pages/index.tsx
export default async function HomePage() {
  return (
    <>
      <title>Waku</title>
      <meta property="description" content="The minimal React framework" />
      <h1>Waku</h1>
      <div>Hello world!</div>
    </>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};

Metadata can also be generated programmatically.

// ./src/pages/index.tsx
export default async function HomePage() {
  return (
    <>
      <Head />
      <div>{/* ...*/}</div>
    </>
  );
}

const Head = async () => {
  const metadata = await getMetadata();

  return (
    <>
      <title>{metadata.title}</title>
      <meta property="description" content={metadata.description} />
    </>
  );
};

const getMetadata = async () => {
  /* ... */
};

export const getConfig = async () => {
  return {
    render: 'static',
  };
};

Styling

Global styles

Install any required dev dependencies (e.g., npm i -D tailwindcss autoprefixer) and set up any required configuration (e.g., postcss.config.js). Then create your global stylesheet (e.g., ./src/styles.css) and import it into the root layout.

// ./src/pages/_layout.tsx
import '../styles.css';

export default async function RootLayout({ children }) {
  return <>{children}</>;
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};
/* ./src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// ./tailwind.config.js
export default {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
};
// ./postcss.config.js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

Static assets

Static assets such as images, fonts, stylesheets, and scripts can be placed in a special ./public folder of the Waku project root directory. The public directory structure is served relative to the / base path.

// assuming image is saved at `/public/images/logo.svg`

export const Component = () => {
  return (
    <>
      <img src="/images/logo.svg" />
    </>
  );
};

File system

Files placed in a special ./private folder of the Waku project root directory can be securely accessed in React server components.

export default async function HomePage() {
  const file = readFileSync('./private/README.md', 'utf8');
  /* ... */
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};

Data fetching

Server

All of the wonderful patterns of React server components are supported. For example, you can compile MDX files or perform code syntax highlighting on the server with zero impact on the client bundle size.

// ./src/pages/blog/[slug].tsx
import { MDX } from '../../components/mdx.js';
import { getArticle, getStaticPaths } from '../../lib/blog.js';

export default async function BlogArticlePage({ slug }) {
  const article = await getArticle(slug);

  return (
    <>
      <title>{article.frontmatter.title}</title>
      <h1>{article.frontmatter.title}</h1>
      <MDX>{article.content}</MDX>
    </>
  );
}

export const getConfig = async () => {
  const staticPaths = await getStaticPaths();

  return {
    render: 'static',
    staticPaths,
  };
};

Client

Data should be fetched on the server when possible for the best user experience, but all data fetching libraries such as React Query should be compatible with Waku.

State management

We recommend Jotai for global React state management based on the atomic model’s performance and scalability, but Waku should be compatible with all React state management libraries such as Zustand and Valtio.

We’re exploring a deeper integration of atomic state management into Waku to achieve the performance and developer experience of signals while preserving React’s declarative programming model.

Environment variables

It’s important to distinguish environment variables that must be kept secret from those that can be made public.

Private

By default all environment variables are considered private and accessible only in server components, which can be rendered exclusively in a secure environment. You must still take care not to inadvertently pass the variable as props to any client components.

Public

A special WAKU_PUBLIC_ prefix is required to make an environment variable public and accessible in client components. They will be present as cleartext in the production JavaScript bundle sent to users’ browsers.

Runtime agnostic (recommended)

Environment variables are available on the server via the Waku getEnv function and on the client via import.meta.env.

// server components can access both private and public variables
import { getEnv } from 'waku';

export const ServerComponent = async () => {
  const secretKey = getEnv('SECRET_KEY');

  return <>{/* ...*/}</>;
};
// client components can only access public variables
'use client';

export const ClientComponent = () => {
  const publicStatement = import.meta.env.WAKU_PUBLIC_HELLO;

  return <>{/* ...*/}</>;
};

Node.js

In Node.js environments, process.env may be used for compatibility.

// server components can access both private and public variables
export const ServerComponent = async () => {
  const secretKey = process.env.SECRET_KEY;

  return <>{/* ...*/}</>;
};
// client components can only access public variables
'use client';

export const ClientComponent = () => {
  const publicStatement = process.env.WAKU_PUBLIC_HELLO;

  return <>{/* ...*/}</>;
};

Deployment

Vercel

Waku projects can be deployed to Vercel with the Vercel CLI automatically.

vercel

Pure SSG

Adding the --with-vercel-static flag to the build script will produce static sites without serverless functions.

{
  "scripts": {
    "build": "waku build --with-vercel-static"
  }
}

Netlify

Waku projects can be deployed to Netlify with the Netlify CLI.

npm run build -- --with-netlify
netlify deploy

Pure SSG

Adding the --with-netlify-static flag to the build script will produce static sites without Netlify functions.

{
  "scripts": {
    "build": "waku build --with-netlify-static"
  }
}

Cloudflare (experimental)

npm run build -- --with-cloudflare
npx wrangler dev # or deploy

PartyKit (experimental)

npm run build -- --with-partykit
npx partykit dev # or deploy

Deno Deploy (experimental)

npm run build -- --with-deno
DENO_DEPLOY_TOKEN=... deployctl deploy --project=... --prod dist/serve.js --exclude node_modules

AWS Lambda (experimental)

npm run build -- --with-aws-lambda

The handler entrypoint is dist/serve.js: see Hono AWS Lambda Deploy Docs.

Community

Please join our friendly GitHub discussions or Discord server to participate in the Waku community. Hope to see you there!

Roadmap

Waku is in active development and we’re seeking additional contributors. Check out our roadmap for more information.

waku's People

Contributors

agnar-nomad avatar aheissenberger avatar ahme-dev avatar aslemammad avatar dai-shi avatar easonyou avatar gregberge avatar himself65 avatar hyf0 avatar idiglove avatar leniwen avatar lesenelir avatar masterlambaster avatar mjh316 avatar nonzzz avatar ojj1123 avatar otofu-square avatar perfectpan avatar sandren avatar threepointone avatar tobbe avatar vasucp1207 avatar yeskunall avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

waku's Issues

v1 Roadmap

Here are tasks that I would like to finish before v1.

Previous Versions

-v0.14.0

  • Server bundling #21
  • New API as middleware #23
  • RSC-only SSR (or opt-in SSR) #66
  • Improve HTTP errors (404 instead of 500, especially with router) #76
  • Support interacting with server (req and res) #86
  • CSS Modules on server #98 #105
  • Server bundling issues #79

v0.15.0

  • fix waku/router #85 #104
  • Revisit entries.ts API (requires to revisit waku/router) #84
  • Revisit mutate() api and maybe serve() api #119

v0.16.0

  • waku/router [slug] support #133
  • SWR #96

v0.17.0

  • Reconsider SSR (give up RSC-only SSR and go with full SSR) #147
  • Node 20 #159
  • Refactor rscPrefix config to rscPath #160

v0.18.0

  • Support running on the edge
  • Vite 5 #207

v0.18.1

v0.19.0

  • Utilize hoistable elements #289
  • Renew waku/router API #246
  • Environment variables #292
  • Improve Vercel deployment
  • Config file waku.config.ts support in cli.ts #368
  • Improve CLI and templates

v0.19.1

v0.19.2

v0.19.3

v0.19.4

v0.20.0

  • #513
  • Enable SSR by default (no --with-ssr option)
  • getContext from waku/server
  • Conditional SSR
    #534
    #569
  • File-based router
    #572
    #575
  • Managed mode (no main.tsx and entries.tsx)
    #580
  • new waku/router apis
    #593
    #613

v0.20.1

  • Support omitting file extensions in import statements
  • Support .js and .jsx extensions

v0.21.0

  • Consider supporting importing server files directly from client files (and maybe inline "use server" too)
    #403
    #554 (tentative)

v1-alpha

  • #329
  • support progressive enhancement for forms
  • Migrate to react-server-dom-esm
    facebook/react#27197
  • More compatibilities with third-party libraries
    #423

v1-beta

  • Explore support for third-party routers
    #272
  • Explore integration of client state management

v1-final

  • Wait for React 19

Unable to run the example

I tried:

npm create wakuwork
cd wakuwork-example

Then installed packages with:

npm i --legacy-peer-deps

And tried to run it:

npm run dev

But got this error:

➜  wakuwork-example npm run dev             

> [email protected] dev
> NODE_OPTIONS='--loader tsx' wakuwork dev

(node:22229) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
file:///Users/USER/Desktop/projects/RSC/wakuwork-example/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js:386
      throw new Error('Expected source to have been transformed to a string.');
            ^

Error: Expected source to have been transformed to a string.
    at transformSource (file:///Users/USER/Desktop/projects/RSC/wakuwork-example/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js:386:13)
    at async Loader.moduleStrategy (node:internal/modules/esm/translators:142:17)
    at async link (node:internal/modules/esm/module_job:64:21)

Env:

➜  wakuwork-example node -v                 v16.4.0
➜  wakuwork-example npm -v                  7.18.1

M1 mac

ExpressJS example ?

We do have express middleware, could you add an example on how to use with existing express server ? Thanks.

is api router needed?

In waku, we have server action. I wonder whether we still need something like api router in Nextjs in this case. Is it a good practice to replace traditional requests with server actions?

unexpected cached result

export default defineRouter(
  async id => {
    switch (id) {
      case 'index': {
        const { default: AppCreator } = await import('./src/app.js');
        return AppCreator({
          name: 'this is index'
        });
      }
      case 'about': {
        const { default: AppCreator } = await import('./src/app.js');
        return AppCreator({
          name: 'this is about'
        });
      }
      default:
        return null;
    }
  },
  async () => {
    return ['index', 'about'];
  }
);
const App = ({ name = "Anonymous" }) => () => {
  return (
    <div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
      <h1>Hello {name}!!</h1>
      <h3>This is a server component.</h3>
    </div>
  );
};

export default App;

image

image

Ssr doesn't work if use getContext in entries.ts 's defineRouter/getComponent

What's the issue

I use ssr with routes and getContext

As the code show, it doesn't work if use getContext in defineRouter/getComponent.

https://github.com/EasonYou/waku-example/blob/71308df89396c7fa6b5c9ce54ff46d8fafcdf7aa/src/entries.ts#L1-L21

image

What do I find

The error tells that ctx was empty, it cause by the getComponent so that waku can't get element by getSsrConfig because of the component import error. Then the ssr will not work. But no error on the client side, just like a csr app.

waku/src/router/server.ts

Lines 155 to 171 in 5af1c34

const getSsrConfig: GetSsrConfig = async (pathStr) => {
const url = new URL(pathStr, "http://localhost");
// We need to keep the logic in sync with waku/router/client
// FIXME We should probably create a common function
const pathItems = url.pathname.split("/").filter(Boolean);
const rscId = pathItems.join("/") || "index";
try {
await getComponent(rscId);
} catch (e) {
// FIXME Is there a better way to check if the path exists?
return null;
}
return { element: [SSR_PREFIX + url.pathname, { search: url.search }] };
};
return { getEntry, getBuildConfig, getSsrConfig };
}

I don't know if it is a purposeful design that user can't use getSsrConfig or it is a bug.

Use process.env

I tried to use pg package for node postgres and it shows me process undefined, I think somehow there should be a way to pass and allow env variables.

Uncaught ReferenceError: process is not defined
    at node_modules/pg/lib/defaults.js (defaults.js:8:9)
    at __require (chunk-DFKQJ226.js?v=99f0aa57:8:50)
    at node_modules/pg/lib/utils.js (utils.js:5:18)
    at __require (chunk-DFKQJ226.js?v=99f0aa57:8:50)
    at node_modules/pg/lib/client.js (client.js:4:13)
    at __require (chunk-DFKQJ226.js?v=99f0aa57:8:50)
    at node_modules/pg/lib/index.js (index.js:3:14)
    at __require (chunk-DFKQJ226.js?v=99f0aa57:8:50)
    at index.js:55:1

Node version 18+ required

Hey, wanted to try it but it seems it does not work with node16

❯ yarn
yarn install v1.22.19
info No lockfile found.
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
error [email protected]: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.14.0"
error Found incompatible module.
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

installing with npm or pnpm doesn't throw like yarn but it throws when running the dev command:

dev/open-source/waku-example 4s 
❯ pnpm dev

> [email protected] dev /Users/astahmer/dev/open-source/waku-example
> waku dev

file:///Users/astahmer/dev/open-source/waku-example/node_modules/.pnpm/[email protected][email protected][email protected]_react@18_f2jmasybwugzd65qzb5uz2pgtu/node_modules/waku/dist/cli.js:4
import { parseArgs } from "node:util";
         ^^^^^^^^^
SyntaxError: The requested module 'node:util' does not provide an export named 'parseArgs'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:127:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:191:5)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:337:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:61:12)
 ELIFECYCLE  Command failed with exit code 1.

just thought it could be nice to document on the website or in the README cause it does not seem obvious at first (when not installing from yarn)

nodenext complications

I have noticed this library has its module set to nodenext. Does this affect the consumer of the library?

`yarn create waku` example doesn't work on windows filesystem

Fully reproduction video
note: webpage at 00:31 is just a CodeSandbox demo

Environment

system version
node v18.13.0
package-management [email protected]
os windows 10

What's happened

Waku's counter example doesn't work on yarn create waku, but works on the CodeSandbox demo.

with yarn dev

  • yarn dev calls waku dev -with--ssr

like the above video, the server component is rendered but suddenly disappears with the below error log.
image

after several refreshing(F5), the server component now appears but the client component never seems like the below screenshot. the spinner is still spinning...
image

with yarn build

not only waku dev, but also waku build has failed.
It tried to open C:\Users\user\Documents\GitHub\waku-example\dist\public\RSC\App\props={"name":"Waku"}, but there is no such file.

build result directory looks like this.
image

open full log
$ yarn build
yarn run v1.22.19
$ waku build
vite v4.4.4 building SSR bundle for production...
✓ 4 modules transformed.
✓ built in 126ms
vite v4.4.4 building SSR bundle for production...
✓ 3 modules transformed.
dist/entries.js                0.59 kB
dist/assets/App-6a21f0cf.js    1.18 kB
dist/assets/rsc0-6c7571303.js  1.25 kB
✓ built in 54ms
vite v4.4.4 building for production...
✓ 37 modules transformed.
../dist/public/index.html                            1.16 kB │ gzip:   0.57 kB
../dist/public/assets/rsc0-6c7571303.js              0.90 kB │ gzip:   0.38 kB
../dist/public/assets/jsx-dev-runtime-4f08db3d.js   42.59 kB │ gzip:  13.54 kB
../dist/public/assets/main-0ebf788c.js             358.79 kB │ gzip: 108.74 kB
✓ built in 2.01s
node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);        
            ^

[Error: ENOENT: no such file or directory, open 'C:\Users\user\Documents\GitHub\waku-example\dist\public\RSC\App\props={"name":"Waku"}'] {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: 'C:\\Users\\user\\Documents\\GitHub\\waku-example\\dist\\public\\RSC\\App\\props={"name":"Waku"}'
}

Node.js v18.13.0
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

I've never touched any file or code, just install dependencies and run dev or build directly... am I missing something wrong?

Can Client components render Server components (yet)?

I created a test app with npm create waku@latest to play around with RSC without the baggage of next.js. I ran into an issue trying to render server components inside client components, but it's quite possible I'm missing something, too. Here's the code:

App.tsx

import * as fs from 'node:fs';
import { FilePicker } from './FilePicker.js';

const App = () => {
  const entries = fs.readdirSync('assets', { withFileTypes: true });
  const files = entries.filter(e => e.isFile()).map(e => e.name).sort();
  return (
    <div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
      <p>(Server component)</p>
      <FilePicker files={files} />
    </div>
  );
};

export default App;

FilePicker.tsx

'use client';
import React from 'react';
import { FileRenderer } from './FileRenderer.js';

export function FilePicker({ files }: { files: string[] }) {
    'use client';
    const initial = files[0]!;
    const [selected, setSelected] = React.useState(initial)
    return <>
        <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}>
            <p>(Client component)</p>
            Pick a file: <br />
            <select value={selected} onChange={e => setSelected(e.target.value)}>
                {files.map(f => <option key={f} value={f}>{f}</option>)}
            </select>
            <FileRenderer path={selected} />
        </div>
    </>
}

FileRenderer.tsx

'use server';
import * as fs from 'fs';

export function FileRenderer({ path }: { path: string }) {
    // 💥
    const file = path //  fs.readFileSync(`assets/${path}`);

    return <div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
        <p>(Server component)</p>
        <p>Content of {path}:</p>
        <p>{file.toString()}</p>
    </div>
}

That code renders like this:
image

However, if I uncomment the fs reference in FileRenderer to actually use "server" code inside FileRenderer, the UI fails with this error:

Uncaught TypeError: fs.readFileSync is not a function
FileRenderer@http://localhost:3000/src/FileRenderer.tsx?t=1685134414661:21:19
renderWithHooks@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:8179:35
mountIndeterminateComponent@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:11861:21
beginWork$1@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:12863:22
callCallback2@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14207:22
invokeGuardedCallbackImpl@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14232:24
invokeGuardedCallback@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14272:37
beginWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:18290:36
performUnitOfWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17579:20
workLoopSync@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17405:30
renderRootSync@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17382:17
performConcurrentWorkOnRoot@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:16861:83
workLoop@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:193:50
flushWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:172:22
performWorkUntilDeadline@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:382:29
EventHandlerNonNull*node_modules/scheduler/cjs/scheduler.development.js/<@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:403:11
node_modules/scheduler/cjs/scheduler.development.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:450:9
__require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50
node_modules/scheduler/index.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:462:24
__require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50
node_modules/react-dom/cjs/react-dom.development.js/<@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:478:25
node_modules/react-dom/cjs/react-dom.development.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:25920:9
__require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50
node_modules/react-dom/index.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:25933:24
__require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50
node_modules/react-dom/client.js@http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=1f8b7987:11:13
__require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50
@http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=1f8b7987:37:16

As a sanity check, some Next.js docs for server components mention this kind of interleaving explicitly:

On the client, React renders Client Components and slots in the rendered result of Server Components, merging the work done on the server and client.
If any Server Components are nested inside a Client Component, their rendered content will be placed correctly within the Client Component.

not compatible with prisma

Trying to add prisma to an waku project, but seems that they are not compatible. @prisma/client doesn't have esm export. When the server component render, error shows:
Cannot render RSC ReferenceError: module is not defined
Here is the repo link which reproduces the error.
I am not familiar with vite, but I have followed Server-Side Rendering section on vite official doc and add prisma to it, and this kind of error doesn't show, not sure it's a bug from vite or the waku behavior.
Here is the repo link in which prisma works with vite.

examples can't run with `The React Server Writer cannot be used outside a react-server environment`

step

yarn run v1.22.17
$ waku dev --with-ssr
Listening on 3000
(node:59454) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:59454) DeprecationWarning: Obsolete loader hook(s) supplied and will be ignored: getSource, transformSource
17:07:51 [vite] Error when evaluating SSR module /src/components/Counter.tsx: failed to import "react-server-dom-webpack/server"
|- Error: The React Server Writer cannot be used outside a react-server environment. You must configure Node.js using the `--conditions react-server` flag.
    at Object.<anonymous> (/Users/bamcop/StartKit/SSR/waku/examples/01_counter/node_modules/react-server-dom-webpack/server.js:3:7)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

build error in SWR 2.2.0

"use client";

import { useState } from "react";
import useSWR from 'swr'

export const Counter = () => {
  // @ts-ignore
  const { data } = useSWR('hello', () => 'world', {
    suspense: true
  })
  const [count, setCount] = useState(0);
  return (
    <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <h3>This is a client component.</h3>
      <div>
        {data}
      </div>
    </div>
  );
};
/Users/himself65/.nvm/versions/node/v18.16.1/bin/npm run build

> [email protected] build
> waku build

vite v4.3.9 building SSR bundle for production...
✓ 6 modules transformed.
✓ built in 42ms
"default" is not exported by "node_modules/swr/core/dist/react-server.mjs", imported by "src/Counter.tsx".
file: /Users/himself65/Code/affine-waku/src/Counter.tsx:4:7
2: 
3: import { useState } from "react";
4: import useSWR from 'swr'
          ^
5: 
6: export const Counter = () => {
file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:2124
        base = Object.assign(new Error(base.message), base);
                             ^

Error [RollupError]: "default" is not exported by "node_modules/swr/core/dist/react-server.mjs", imported by "src/Counter.tsx".
    at error (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:2124:30)
    at Module.error (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:13463:16)
    at Module.traceVariable (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:13884:29)
    at ModuleScope.findVariable (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:12429:39)
    at ReturnValueScope.findVariable (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:6965:38)
    at ChildScope.findVariable (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:6965:38)
    at Identifier.bind (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:8127:40)
    at CallExpression.bind (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:5738:23)
    at CallExpression.bind (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:9680:15)
    at VariableDeclarator.bind (file:///Users/himself65/Code/affine-waku/node_modules/rollup/dist/es/shared/node-entry.js:5738:23) {
  binding: 'default',
  code: 'MISSING_EXPORT',
  exporter: '/Users/himself65/Code/affine-waku/node_modules/swr/core/dist/react-server.mjs',
  id: '/Users/himself65/Code/affine-waku/src/Counter.tsx',
  url: 'https://rollupjs.org/troubleshooting/#error-name-is-not-exported-by-module',
  pos: 103,
  loc: {
    column: 7,
    file: '/Users/himself65/Code/affine-waku/src/Counter.tsx',
    line: 4
  },
  frame: '2: \n' +
    '3: import { useState } from "react";\n' +
    "4: import useSWR from 'swr'\n" +
    '          ^\n' +
    '5: \n' +
    '6: export const Counter = () => {',
  watchFiles: [
    '/Users/himself65/Code/affine-waku/entries.ts',
    '/Users/himself65/Code/affine-waku/package.json',
    '/Users/himself65/Code/affine-waku/node_modules/waku/dist/server.js',
    '/Users/himself65/Code/affine-waku/src/App.tsx',
    '/Users/himself65/Code/affine-waku/src/Counter.tsx',
    '/Users/himself65/Code/affine-waku/node_modules/swr/core/dist/react-server.mjs',
    '/Users/himself65/Code/affine-waku/node_modules/swr/_internal/dist/react-server.mjs'
  ]
}

Node.js v18.16.1

Process finished with exit code 1

Upstream: vercel/swr#2700


repo: https://github.com/himself65/affine-waku/blob/issue-swr/src/Counter.tsx

yarn install
yarn build

Feature: support reading request "context" in server actions

My goal here is to support session-based authentication for server actions in RSC.

Authentication for server actions seems like an open question at this point, but step one is making some kind "current context" from the caller available to the action. For a client holding access tokens, it's probably trivial to wire up helpers to invoke actions with. However, for battle tested session cookie-based authentication, there seems to be some work to do.

The same problem could surface for any other data sourced from the server request that needs to be supplied to actions.

As far as prior art, the next.js examples imply a backend like AsyncLocalStorage, given this code:

import { cookies } from 'next/headers';
 
export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';
 
    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }
 
  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}

To start, I briefly explored adding support for reading request headers in server actions. Two things got in the way:

  1. The action handlers are run in a worker, separately from the express handler. This means even on the server, anything originating from the request cannot share module state with an action, and instead must cross a thread boundary via postMessage. This stopped a simple AsyncLocalStorage approach from working. I'm curious if RSC prescribes the worker in some way
  2. The server code is bundled and loaded by Vite. This is what stopped me (for now) from simply importing a module from the server code and seeding AsyncLocalStorage with something around this spot: https://github.com/dai-shi/waku/blob/0848fa64eac2a578f602d7ab460889a8124bbbb0/src/lib/rsc-handler-worker.ts#L228C1-L229 The bundled action code gets its own module graph, and I couldn't easily figure out how to access a known module in that graph from the worker source to seed it with a value.

That fiddling can be found here, if interested: https://github.com/dai-shi/waku/compare/main...jwbay:request-headers?expand=1

The interesting output is:

✓ built in 1.15s
clientEntries { 'assets/rsc0.js': 'assets/rsc0-1b0f0351.js' }
Creating header store @     at eval (/home/admin/source/waku/examples/05_mutation/dist/assets/headers-storage-b5646013.js:5:40)
3:44:00 PM [vite] page reload dist/public/index.html
Listening on 8080
Creating header store @     at file:///home/admin/source/waku/dist/lib/headers-storage.js:2:40
Creating header store @     at eval (/home/admin/source/waku/examples/05_mutation/dist/assets/headers-storage-b5646013.js:5:40)
user-agent in worker src:  Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
user-agent in action:  undefined

Possibly the simplest alternative is to .call/.apply the server action with a this value containing request context. Making it a first/last parameter is also possible, but would require some TS trickery.

Cannot import commonjs module from server component

When trying to import a CJS module from a server component I get an error in dev & production

Error when evaluating SSR module /src/components/App.tsx: failed to import "/src/components/foo.cjs"
|- ReferenceError: require is not defined
    at eval (/Users/tom.sherman/code/waku-example/src/components/foo.cjs:3:31)
    at instantiateModule (file:///Users/tom.sherman/code/waku-example/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3b8eb186.js:55920:15)

Cannot render RSC ReferenceError: require is not defined
    at eval (/Users/tom.sherman/code/waku-example/src/components/foo.cjs:3:31)
    at instantiateModule (file:///Users/tom.sherman/code/waku-example/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3b8eb186.js:55920:15)

Minimal reproduction: tom-sherman/waku-example@77cf350

Using the `promise-template` fails with an error

Using the promise-template fails with the following error:

❯ npm run dev

> [email protected] dev
> waku dev --with-ssr

(node:21449) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:21449) DeprecationWarning: Obsolete loader hook(s) supplied and will be ignored: getSource, transformSource
Listening on 3000
1:55:21 PM [vite] Error when evaluating SSR module /private/tmp/waku-project/src/entries.js: failed to import "waku/client"
|- file:///private/tmp/waku-project/node_modules/waku/dist/client.js:202
import { createFromFetch, encodeReply } from "react-server-dom-webpack/client";
         ^^^^^^^^^^^^^^^
SyntaxError: Named export 'createFromFetch' not found. The requested module 'react-server-dom-webpack/client' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'react-server-dom-webpack/client';
const { createFromFetch, encodeReply } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:124:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:190:5)

Steps to recreate

  1. npm create waku@latest
  2. Choose the promise-template
  3. npm install
  4. npm run dev

[Discuss]: Compare with Other RSC frameworks.

Thanks for awesome works, I usually use Vite most of the time, and I am very happy to have a Vite-based server-side framework.

In early stage, Shopify https://github.com/Shopify/hydrogen-v1 implement a SSR framework(with RSC) based on Vite. It contains:

  • some vite rsc plugins
  • react-server-dom-vite (like react-server-dom-webpack)

Looks like we can build on top of those packages

[bug] Missing `file://` protocol while running examples in Windows

error output

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file and data are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
    at __node_internal_captureLargerStackTrace (node:internal/errors:490:5)
    at new NodeError (node:internal/errors:399:5)
    at throwIfUnsupportedURLScheme (node:internal/modules/esm/resolve:1059:11)
    at defaultResolve (node:internal/modules/esm/resolve:1135:3)
    at nextResolve (node:internal/modules/esm/loader:163:28)
    at u (file:///C:/Users/iheyu/Documents/GitHub/wakuwork/examples/01_counter.5.5/node_modules/@esbuild-kit/esm-loader/dist/index.js:1:2406)                   at nextResolve (node:internal/modules/esm/loader:163:28)
    at resolve (file:///C:/Users/iheyu/Documents/GitHub/wakuwork/dist/node-loader.js:1:238)                                                                     at async nextResolve (node:internal/modules/esm/loader:163:22)
    at async resolve (file:///C:/Users/iheyu/Documents/GitHub/wakuwork/examples/01_counter/node_modules/.pnpm/registry.npmmirror.com+react-server-dom-webpack@18.3.0-canary-aef7ce554-20230503_react-dom@18_4enxzw6ksfg63hwdnna6ipu25e/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js:34:10)                                            at async nextResolve (node:internal/modules/esm/loader:163:22)
    at async ESMLoader.resolve (node:internal/modules/esm/loader:838:24)      
    at async ESMLoader.getModuleJob (node:internal/modules/esm/loader:424:7) {

  code: 'ERR_UNSUPPORTED_ESM_URL_SCHEME'
}

Reproduce

  1. clone https://github.com/dai-shi/wakuwork
  2. in examples, run pnpm dev

@dai-shi I have found the cause and come out with a fix locally. But it's a Windows-specific problem, not sure if you would accept the fix.

hooks cannot be used in server component

I try to use hooks in server component, but it reported that 「Cannot read properties of null (reading 'useContext')」

This bug is also similar with #103 (comment) since react-tweet use 「useMemo」 in the server component

codesandbox: https://codesandbox.io/p/sandbox/waku-example-counter-forked-4rg8fz?file=%2Fsrc%2Fcomponents%2FApp.tsx%3A7%2C38

I figured it out the reason, but I do not know how to fix it.

vite.ssrLoadModule will resolve package with override conditions https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/ssrModuleLoader.ts#L132. So when the user code bundled in ssrLoadModule, the react package will be resolved as index.js instead of react.shared-subset.js because of lacking react-server condition.

And the react-dom-server package resolve react as react.shared-subset.js, so there will appear two React instance.

After I forcely changed the vite code by adding react-server condition, it worked.
CleanShot 2023-08-01 at 00 03 47@2x

Obviously it is not a correct way to solve this problem and I have no idea how to fix it, so I leave a message about what I found here.

path changes between `dev` and `build`

import { Counter } from "./Counter.js";
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'url'

const __dirname = fileURLToPath(new URL('.', import.meta.url));

const App = ({ name = "Anonymous" }) => {
  const text = readFileSync(resolve(__dirname, './input.txt'), {
    encoding: 'utf-8'
  })
  return (
    <div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
      <h1>Hello {name}!!</h1>
      {text}
      <h3>This is a server component.</h3>
      <Counter />
    </div>
  );
};

export default App;

image

➜  affine-waku git:(main) ✗ yarn dev  
yarn run v1.22.19
$ waku dev
Listening on 3000
^C
➜  affine-waku git:(main) ✗ yarn build
yarn run v1.22.19
$ waku build
vite v4.3.9 building SSR bundle for production...
transforming (1) entries.tsError when using sourcemap for reporting an error: Can't resolve original location of error.
Module level directives cause errors when bundled, "use client" in "src/Counter.tsx" was ignored.
✓ 4 modules transformed.
✓ built in 56ms
vite v4.3.9 building SSR bundle for production...
transforming (1) entries.tsError when using sourcemap for reporting an error: Can't resolve original location of error.
Module level directives cause errors when bundled, "use client" in "src/Counter.tsx" was ignored.
✓ 3 modules transformed.
dist/entries.js              0.40 kB
dist/assets/rsc0.js          1.17 kB
dist/assets/App-cde87dad.js  1.37 kB
✓ built in 25ms
vite v4.3.9 building for production...
transforming (1) src/Counter.tsxError when using sourcemap for reporting an error: Can't resolve original location of error.
✓ 37 modules transformed.
dist/public/index.html                            1.09 kB │ gzip:   0.54 kB
dist/public/assets/rsc0-102901cd.js               0.82 kB │ gzip:   0.38 kB
dist/public/assets/jsx-dev-runtime-06c801c5.js   42.46 kB │ gzip:  13.51 kB
dist/public/assets/main-9c2c75fb.js             359.06 kB │ gzip: 108.57 kB
✓ built in 760ms
clientEntries { 'assets/rsc0.js': 'assets/rsc0-102901cd.js' }
Error: ENOENT: no such file or directory, open '/Users/himself65/Code/affine-waku/dist/assets/input.txt'
    at Object.openSync (node:fs:601:3)
    at Proxy.readFileSync (node:fs:469:35)
    at App (/Users/himself65/Code/affine-waku/dist/assets/App-cde87dad.js:17:38)
    at attemptResolveElement (/Users/himself65/Code/affine-waku/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js:1462:18)
    at retryTask (/Users/himself65/Code/affine-waku/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js:2063:15)
    at performWork (/Users/himself65/Code/affine-waku/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js:2125:7)
    at AsyncLocalStorage.run (node:async_hooks:338:14)
    at Immediate._onImmediate (/Users/himself65/Code/affine-waku/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js:2245:29)
    at process.processImmediate (node:internal/timers:476:21) {
  errno: -2,
  syscall: 'open',
  code: 'ENOENT',
  path: '/Users/himself65/Code/affine-waku/dist/assets/input.txt'
}
4:51:37 PM [vite] page reload dist/public/index.html
4:51:37 PM [vite] page reload dist/entries.js
✨  Done in 1.24s.

`npm create waku@latest` fails

For me creating a new example app fails with the following:

Error: ENOENT: no such file or directory, scandir 'template'
    at Object.readdirSync (node:fs:1452:3)
    at init (/Users/ilakovac/Library/pnpm/store/v3/tmp/dlx-17047/node_modules/.pnpm/[email protected]/node_modules/create-waku/dist/cli.js:7633:42)
    at Object.<anonymous> (/Users/ilakovac/Library/pnpm/store/v3/tmp/dlx-17047/node_modules/.pnpm/[email protected]/node_modules/create-waku/dist/cli.js:7742:1)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47 {
  errno: -2,
  syscall: 'scandir',
  code: 'ENOENT',
  path: 'template'
}

Same thing happens with:

  • pnpm create waku
  • yarn create waku

I supposed this is the line at fault: https://github.com/dai-shi/waku/blob/main/packages/create-waku/src/cli.ts#L38

Error with latest react builds

18.3.0-canary-e91142dd6-20230705 works, but 18.3.0-canary-fdc8c81e0-20230707 fails.

[vite] Error when evaluating SSR module /src/components/App.tsx: failed to import "/src/components/Counter.tsx"
|- Error: The React Server Writer cannot be used outside a react-server environment. You must configure Node.js using the --conditions react-server flag.

It seems like facebook/react#27033 changes to use import {registerServerReference} from "react-server-dom-webpack/server"; in the transformed file with packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js, which might cause the error.

Not yet sure how to fix it.

Custom preloaders?

Right now Waku displays its own spinner:
image

I tried wrapping a client component into <Suspense fallback={...}>
But it shows Waku's spinner first and then switches to suspense fallback for a split second

Is there a way to use your own preloader?

  • If so, how do we make it?
  • If not, is it planned?

Thanks!

Creating a new project fails

Try to create a new project using npm create waku@latest.

Observed result:

PS C:\demos> npm create waku@latest
'create-waku' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! code 1
npm ERR! path C:\demos
npm ERR! command failed
npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c create-waku

npm ERR! A complete log of this run can be found in: C:\Users\mauri\AppData\Local\npm-cache\_logs\2023-11-16T12_43_09_734Z-debug-0.log
PS C:\demos>

Content of the log file created:

0 verbose cli C:\Program Files\nodejs\node.exe C:\Users\mauri\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js
1 info using [email protected]
2 info using [email protected]
3 timing npm:load:whichnode Completed in 2ms
4 timing config:load:defaults Completed in 2ms
5 timing config:load:file:C:\Users\mauri\AppData\Roaming\npm\node_modules\npm\npmrc Completed in 3ms
6 timing config:load:builtin Completed in 3ms
7 timing config:load:cli Completed in 1ms
8 timing config:load:env Completed in 0ms
9 timing config:load:file:C:\demos\.npmrc Completed in 1ms
10 timing config:load:project Completed in 2ms
11 timing config:load:file:C:\Users\mauri\.npmrc Completed in 0ms
12 timing config:load:user Completed in 0ms
13 timing config:load:file:C:\Users\mauri\AppData\Roaming\npm\etc\npmrc Completed in 0ms
14 timing config:load:global Completed in 0ms
15 timing config:load:setEnvs Completed in 1ms
16 timing config:load Completed in 10ms
17 timing npm:load:configload Completed in 10ms
18 timing config:load:flatten Completed in 2ms
19 timing npm:load:mkdirpcache Completed in 0ms
20 timing npm:load:mkdirplogs Completed in 1ms
21 verbose title npm create waku@latest
22 verbose argv "create" "waku@latest"
23 timing npm:load:setTitle Completed in 0ms
24 timing npm:load:display Completed in 1ms
25 verbose logfile logs-max:10 dir:C:\Users\mauri\AppData\Local\npm-cache\_logs\2023-11-16T12_43_09_734Z-
26 verbose logfile C:\Users\mauri\AppData\Local\npm-cache\_logs\2023-11-16T12_43_09_734Z-debug-0.log
27 timing npm:load:logFile Completed in 21ms
28 timing npm:load:timers Completed in 0ms
29 timing npm:load:configScope Completed in 0ms
30 timing npm:load Completed in 57ms
31 silly logfile start cleaning logs, removing 2 files
32 timing arborist:ctor Completed in 0ms
33 silly logfile done cleaning log files
34 http fetch GET 200 https://registry.npmjs.org/create-waku 420ms (cache revalidated)
35 timing arborist:ctor Completed in 1ms
36 timing arborist:ctor Completed in 0ms
37 timing command:create Completed in 484ms
38 verbose stack Error: command failed
38 verbose stack     at ChildProcess.<anonymous> (C:\Users\mauri\AppData\Roaming\npm\node_modules\npm\node_modules\@npmcli\promise-spawn\lib\index.js:53:27)
38 verbose stack     at ChildProcess.emit (node:events:514:28)
38 verbose stack     at maybeClose (node:internal/child_process:1105:16)
38 verbose stack     at ChildProcess._handle.onexit (node:internal/child_process:305:5)
39 verbose cwd C:\demos
40 verbose Windows_NT 10.0.22621
41 verbose node v20.9.0
42 verbose npm  v10.2.4
43 error code 1
44 error path C:\demos
45 error command failed
46 error command C:\WINDOWS\system32\cmd.exe /d /s /c create-waku
47 verbose exit 1
48 timing npm Completed in 788ms
49 verbose code 1
50 error A complete log of this run can be found in: C:\Users\mauri\AppData\Local\npm-cache\_logs\2023-11-16T12_43_09_734Z-debug-0.log

Environment:

PS C:\demos> node -v
v20.9.0
PS C:\demos> npm -v
10.2.4
PS C:\demos> winver
PS C:\demos> systeminfo

Host Name:                 DESKTOP-GVQ1HCA
OS Name:                   Microsoft Windows 11 Pro
OS Version:                10.0.22621 N/A Build 22621
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free
Registered Owner:          [email protected]
Registered Organization:   N/A
Product ID:                00330-80137-56147-AA921
Original Install Date:     12-4-2023, 21:01:18
System Boot Time:          16-11-2023, 10:35:56
System Manufacturer:       MEDION
System Model:              X87047
System Type:               x64-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: Intel64 Family 6 Model 158 Stepping 13 GenuineIntel ~3000 Mhz
BIOS Version:              American Megatrends Inc. 360H4W0X.115, 17-6-2019
Windows Directory:         C:\WINDOWS
System Directory:          C:\WINDOWS\system32
Boot Device:               \Device\HarddiskVolume3
System Locale:             en-gb;English (United Kingdom)
Input Locale:              en-us;English (United States)
Time Zone:                 (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
Total Physical Memory:     16.308 MB
Available Physical Memory: 7.385 MB
Virtual Memory: Max Size:  22.196 MB
Virtual Memory: Available: 8.907 MB
Virtual Memory: In Use:    13.289 MB
Page File Location(s):     C:\pagefile.sys
Domain:                    WORKGROUP
Logon Server:              \\DESKTOP-GVQ1HCA
Hotfix(s):                 5 Hotfix(s) Installed.
                           [01]: KB5032007
                           [02]: KB5012170
                           [03]: KB5032190
                           [04]: KB5031592
                           [05]: KB5032393
Network Card(s):           2 NIC(s) Installed.
                           [01]: TAP-Windows Adapter V9
                                 Connection Name: Ethernet 2
                                 Status:          Media disconnected
                           [02]: Realtek PCIe GbE Family Controller
                                 Connection Name: Ethernet
                                 DHCP Enabled:    Yes
                                 DHCP Server:     192.168.178.1
                                 IP address(es)
                                 [01]: 192.168.178.222
                                 [02]: fe80::cb6:d9e2:cc42:e3fd
                                 [03]: 2001:1c00:201b:a900:398f:5f81:5ccb:c599
                                 [04]: 2001:1c00:201b:a900:ebb6:8f1a:d197:dcd7
Hyper-V Requirements:      A hypervisor has been detected. Features required for Hyper-V will not be displayed.

Suggestion for website

I see the waku website looks like it was built from scratch, Isn't a good idea to use an existing SSG framework to power the website as it is easy to maintain and provides a complete solution overall?

create waku error

yarn create waku
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: Done in 0s 69ms

node:events:491
      throw er; // Unhandled 'error' event
      ^

Error: connect ECONNREFUSED 0.0.0.0:443
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)
Emitted 'error' event on ClientRequest instance at:
    at TLSSocket.socketErrorListener (node:_http_client:502:9)
    at TLSSocket.emit (node:events:513:28)
    at emitErrorNT (node:internal/streams/destroy:151:8)
    at emitErrorCloseNT (node:internal/streams/destroy:116:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  errno: -61,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '0.0.0.0',
  port: 443
}

Node.js v18.16.0

Support typescript path alias

with the below paths compiler option on tsconfig,

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
see full tsconfig - may not be related on this context
{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "downlevelIteration": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "exclude": ["node_modules"],
  "include": ["**/*.d.ts", "**/*.ts", "**/*.tsx"]
}

the Typescript doesn't detect path issues because I and TS believed waku (or vite) would map the path, but the Counter path in the below code doesn't work.

import { Counter } from "@/components/Counter.js";

const App = () => {
  return (
    <div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
      <h1>Hello!!</h1>
      <h3>This is a server component.</h3>
      <Counter />
    </div>
  );
};

export default App;

waku will throw error on the terminal below. and cannot show entire elements.
this may be expected behavior because App component is the root component, which doesn't have any more error boundaries from the parent.
image

[vite] Error when evaluating SSR module /src/components/App.tsx: failed to import "@/components/Counter.js"
|- Error: Cannot find module '@/components/Counter.js' imported from 'C:/Users/user/Documents/GitHub/waku-example/src/components/App.tsx'
    at nodeImport (file:///C:/Users/user/Documents/GitHub/waku-example/node_modules/vite/dist/node/chunks/dep-abb4f102.js:55911:25)
    at ssrImport (file:///C:/Users/user/Documents/GitHub/waku-example/node_modules/vite/dist/node/chunks/dep-abb4f102.js:55813:30)
    at eval (C:/Users/user/Documents/GitHub/waku-example/src/components/App.tsx:4:37)
    at async instantiateModule (file:///C:/Users/user/Documents/GitHub/waku-example/node_modules/vite/dist/node/chunks/dep-abb4f102.js:55875:9)   

note: source code is Waku's Counter example. just run yarn create waku to get full source.

of course, if i change "@/components/Counter.js" to "./Counter.js", it works fine. but i hope waku supports typescript path.

Editing server components requires restarting dev server

I can edit the entry point...

const App = serve<{ name: string }>("App");
root.render(
  <StrictMode>
    <App name="THIS EDIT WORKS" />
  </StrictMode>
);

and I can edit client components

export const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}>
      <p>This edit works...</p>
      <p>Count: {count}</p>
    </div>
  );
};

but any edits to the server component in App.tsx require stopping the dev server and restarting it. Is this the expected behavior?

Using "wakuwork": "~0.9.0" on OSX

Support server bundling with vite

Currently, wakuwork uses react-server-dom-webpack/node-loader for handing "use client" and "use server".
We use vite for client bundling, but for server bundling we use a naive approach.
It's limited. For example, we now don't support "use client" in node_modules.

I'm hoping we could use vite for server bundling. Wish vite-rsc does that.

@nksaraf What's the current status?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.