waku 0.19.0-beta.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,329 +2,589 @@
2
2
 
3
3
  ⛩️ The minimal React framework
4
4
 
5
- [![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/waku/ci.yml?branch=main)](https://github.com/dai-shi/waku/actions?query=workflow%3ACI)
6
- [![npm](https://img.shields.io/npm/v/waku)](https://www.npmjs.com/package/waku)
7
- [![discord](https://img.shields.io/discord/627656437971288081)](https://waku.gg/discord)
5
+ visit [waku.gg](https://waku.gg) or `npm create waku@latest`
8
6
 
9
- <!-- [![size](https://img.shields.io/bundlephobia/minzip/waku)](https://bundlephobia.com/result?p=waku) -->
7
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/dai-shi/waku/ci.yml?branch=main&style=flat&colorA=000000&colorB=000000)](https://github.com/pmndrs/jotai/actions?query=workflow%3ALint)
8
+ [![Version](https://img.shields.io/npm/v/waku?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/waku)
9
+ [![Downloads](https://img.shields.io/npm/dt/waku.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/waku)
10
+ [![Discord Shield](https://img.shields.io/discord/627656437971288081?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/MrQdmzd)
10
11
 
11
- Waku means "frame" in Japanese. Waku-Waku means being excited.
12
- https://github.com/dai-shi/waku/discussions/260
12
+ <br>
13
13
 
14
- ## Project status
14
+ ## Introduction
15
15
 
16
- Roadmap: See https://github.com/dai-shi/waku/issues/24 (working towards v1-alpha)
16
+ **Waku** _(wah-ku)_ or **わく** means “framework” in Japanese. As the minimal React framework, it aims 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.
17
17
 
18
- Feel free to try it _seriously_ with non-production projects and give us feedback.
18
+ We recommend other frameworks for heavy ecommerce or enterprise applications. Waku is a lightweight alternative designed to bring a fun developer experience to the modern React server components era. Yes, let’s make React development fun again!
19
19
 
20
- Playground: https://codesandbox.io/p/sandbox/waku-example-counter-mdc1yb
20
+ > 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.
21
21
 
22
- ## Introduction
22
+ ## Getting started
23
23
 
24
- Waku is a React framework that supports React Server Components
25
- (RSCs), a new feature that will be available in a future version of
26
- React. RSCs allow developers to render UI components on the server,
27
- improving performance and enabling server-side features. To use RSCs,
28
- a framework is necessary for bundling, optionally server, router and
29
- so on.
24
+ 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](https://github.com/dai-shi/waku/tree/main/examples/01_template).
30
25
 
31
- Waku takes a minimalistic approach, providing a minimal API that
32
- allows for multiple feature implementations and encourages growth in
33
- the ecosystem. For example, the minimal API is not tied to a specific
34
- router. This flexibility makes it easier to build new features.
26
+ ```
27
+ npm create waku@latest
28
+ ```
35
29
 
36
- Waku uses Vite internally, and while it is still a work in progress,
37
- it will eventually support all of Vite's features. It can even
38
- work as a replacement for Vite + React client components. While using
39
- RSCs is optional, it is highly recommended for improved user and
40
- developer experiences.
30
+ ## Rendering
41
31
 
42
- ## Why develop a React framework?
32
+ Let's face it: React is getting complicated. But not without good reason!
43
33
 
44
- We believe that React Server Components (RSCs) are the future of React.
45
- The challenge is that we can't utilize RSCs with the React library alone.
46
- Instead, they require a React framework for bundling, at the very least.
34
+ While there's a bit of a learning curve to modern React rendering, it introduces powerful new patterns of composability that are only made possible with the advent of React server components. So stick with us.
47
35
 
48
- Currently, only a few React frameworks support RSCs, and
49
- they often come with more features than RSCs.
50
- It would be nice to have a minimal framework that implements RSCs,
51
- which should help learning how RSCs work.
36
+ Future versions of Waku may provide additional APIs to abstract away some of the complexity for an improved developer experience.
52
37
 
53
- Learning is the start, but it's not what we aim at.
54
- Our assumption is that RSC best practices are still to explore.
55
- The minimal implementation should clarify the fundamentals of RSCs
56
- and enable the creation of additional features.
57
- Our goal is to establish an ecosystem that covers a broader range of use cases.
38
+ #### Server components
58
39
 
59
- ## How to create a new project
40
+ Waku follows React conventions including support for [server components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) and [server actions](https://react.dev/reference/react/use-server). Server components can be made async to securely perform server-side logic and data fetching, but have no interactivity.
60
41
 
61
- To start a new Waku project, you can use any of the following
62
- commands, depending on your preferred package manager:
42
+ ```tsx
43
+ // server component
44
+ import db from 'some-db';
63
45
 
64
- ```sh
65
- npm create waku@latest
46
+ import { Gallery } from '../components/gallery.js';
47
+
48
+ export const StorePage = async () => {
49
+ const products = await db.query('SELECT * FROM products');
50
+
51
+ return <Gallery products={products} />;
52
+ };
66
53
  ```
67
54
 
68
- ```sh
69
- yarn create waku
55
+ #### Client components
56
+
57
+ Client components are specified with the `'use client'` directive at the top of the file. They can use all traditional React features such as state, effects, and event handlers.
58
+
59
+ ```tsx
60
+ // client component
61
+ 'use client';
62
+
63
+ import { useState } from 'react';
64
+
65
+ export const Counter = () => {
66
+ const [count, setCount] = useState(0);
67
+
68
+ return (
69
+ <>
70
+ <div>Count: {count}</div>
71
+ <button onClick={() => setCount((c) => c + 1)}>Increment</button>
72
+ </>
73
+ );
74
+ };
70
75
  ```
71
76
 
72
- ```sh
73
- pnpm create waku
77
+ #### Weaving patterns
78
+
79
+ 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`.
80
+
81
+ #### Server-side rendering
82
+
83
+ Waku provides static prerendering (SSG) or server-side rendering (SSR) options for layouts and pages including their server _and_ client components. Client components are then hydrated in the browser to support events, effects, and so on.
84
+
85
+ #### Further reading
86
+
87
+ To learn more about the modern React architecture, we recommend [Making Sense of React Server Components](https://www.joshwcomeau.com/react/server-components/) and [The Two Reacts: Part 1](https://overreacted.io/the-two-reacts/).
88
+
89
+ ## Routing (low-level API)
90
+
91
+ The entry point for routing in Waku projects is `./src/entries.tsx`. Export the `createPages` function to create your layouts and pages programatically.
92
+
93
+ Both `createLayout` and `createPage` accept a configuration object to specify the route path, React component, and render method (`'static'` for SSG or `'dynamic'` for SSR). Layout components must accept a `children` prop.
94
+
95
+ ```tsx
96
+ // ./src/entries.tsx
97
+ import { createPages } from 'waku';
98
+
99
+ import { RootLayout } from './templates/root-layout.js';
100
+ import { HomePage } from './templates/home-page.js';
101
+
102
+ export default createPages(async ({ createPage, createLayout }) => {
103
+ // Create root layout
104
+ createLayout({
105
+ render: 'static',
106
+ path: '/',
107
+ component: RootLayout,
108
+ });
109
+
110
+ // Create home page
111
+ createPage({
112
+ render: 'dynamic',
113
+ path: '/',
114
+ component: HomePage,
115
+ });
116
+ });
74
117
  ```
75
118
 
76
- These commands will create an example app that you can use as a
77
- starting point for your project.
119
+ ### Pages
78
120
 
79
- Minimum requirement: Node.js 18.16.0
121
+ #### Single routes
80
122
 
81
- ## Practices
123
+ Pages can be rendered as a single route (e.g., `/about`).
82
124
 
83
- ### Minimal
125
+ ```tsx
126
+ // ./src/entries.tsx
127
+ import { createPages } from 'waku';
128
+
129
+ import { AboutPage } from './templates/about-page.js';
130
+ import { BlogIndexPage } from './templates/blog-index-page.js';
131
+
132
+ export default createPages(async ({ createPage }) => {
133
+ // Create about page
134
+ createPage({
135
+ render: 'static',
136
+ path: '/about',
137
+ component: AboutPage,
138
+ });
139
+
140
+ // Create blog index page
141
+ createPage({
142
+ render: 'static',
143
+ path: '/blog',
144
+ component: BlogIndexPage,
145
+ });
146
+ });
147
+ ```
84
148
 
85
- #### Server API
149
+ #### Segment routes
86
150
 
87
- To use React Server Components in Waku, you need to create an
88
- `entries.ts` file in the project root directory with a
89
- `renderEntries` function that returns a server component module.
90
- Here's an example:
151
+ Pages can also render a segment route (e.g., `/blog/[slug]`). The rendered React component automatically receives a prop named by the segment (e.g, `slug`) with the value of the rendered route (e.g., `'introducing-waku'`). If statically prerendering a segment route at build time, a `staticPaths` array must also be provided.
91
152
 
92
153
  ```tsx
93
- import { lazy } from 'react';
94
- import { defineEntries } from 'waku/server';
154
+ // ./src/entries.tsx
155
+ import { createPages } from 'waku';
156
+
157
+ import { BlogArticlePage } from './templates/blog-article-page.js';
158
+ import { ProductCategoryPage } from './templates/product-category-page.js';
159
+
160
+ export default createPages(async ({ createPage }) => {
161
+ // Create blog article pages
162
+ // `<BlogArticlePage>` receives `slug` prop
163
+ createPage({
164
+ render: 'static',
165
+ path: '/blog/[slug]',
166
+ staticPaths: ['introducing-waku', 'introducing-create-pages'],
167
+ component: BlogArticlePage,
168
+ });
169
+
170
+ // Create product category pages
171
+ // `<ProductCategoryPage>` receives `category` prop
172
+ createPage({
173
+ render: 'dynamic',
174
+ path: '/shop/[category]',
175
+ component: ProductCategoryPage,
176
+ });
177
+ });
178
+ ```
95
179
 
96
- const App = lazy(() => import('./components/App.js'));
180
+ Static paths (or other values) could also be generated programatically.
97
181
 
98
- export default defineEntries(
99
- // renderEntries
100
- async (input) => {
101
- return {
102
- App: <App name={input || 'Waku'} />,
103
- };
104
- },
105
- );
182
+ ```tsx
183
+ // ./src/entries.tsx
184
+ import { createPages } from 'waku';
185
+
186
+ import { getBlogPaths } from './lib/get-blog-paths.js';
187
+ import { BlogArticlePage } from './templates/blog-article-page.js';
188
+
189
+ export default createPages(async ({ createPage }) => {
190
+ const blogPaths = await getBlogPaths();
191
+
192
+ createPage({
193
+ render: 'static',
194
+ path: '/blog/[slug]',
195
+ staticPaths: blogPaths,
196
+ component: BlogArticlePage,
197
+ });
198
+ });
106
199
  ```
107
200
 
108
- The `id` parameter is the ID of the React Server Component
109
- that you want to load on the server. You specify the RSC ID from the
110
- client.
201
+ #### Nested segment routes
111
202
 
112
- #### Client API
203
+ Routes can contain multiple segments (e.g., `/shop/[category]/[product]`).
204
+
205
+ ```tsx
206
+ // ./src/entries.tsx
207
+ import { createPages } from 'waku';
208
+
209
+ import { ProductDetailPage } from './templates/product-detail-page.js';
210
+
211
+ export default createPages(async ({ createPage }) => {
212
+ // Create product detail pages
213
+ // `<ProductDetailPage>` receives `category` and `product` props
214
+ createPage({
215
+ render: 'dynamic',
216
+ path: '/shop/[category]/[product]',
217
+ component: ProductDetailPage,
218
+ });
219
+ });
220
+ ```
113
221
 
114
- To render a React Server Component on the client, you can use the
115
- `Root` and `Slot` components from `waku/client` with the RSC
116
- ID to create a wrapper component. Here's an example:
222
+ For static prerendering of nested segment routes, the `staticPaths` array is instead comprised of ordered arrays.
117
223
 
118
224
  ```tsx
119
- import { createRoot } from 'react-dom/client';
120
- import { Root, Slot } from 'waku/client';
225
+ // ./src/entries.tsx
226
+ import { createPages } from 'waku';
227
+
228
+ import { ProductDetailPage } from './templates/product-detail-page.js';
229
+
230
+ export default createPages(async ({ createPage }) => {
231
+ // Create product detail pages
232
+ // `<ProductDetailPage>` receives `category` and `product` props
233
+ createPage({
234
+ render: 'static',
235
+ path: '/shop/[category]/[product]',
236
+ staticPaths: [
237
+ ['someCategory', 'someProduct'],
238
+ ['someCategory', 'anotherProduct'],
239
+ ],
240
+ component: ProductDetailPage,
241
+ });
242
+ });
243
+ ```
121
244
 
122
- const rootElement = (
123
- <StrictMode>
124
- <Root>
125
- <Slot id="App" />
126
- </Root>
127
- </StrictMode>
128
- );
245
+ #### Catch-all routes
129
246
 
130
- createRoot(document.getElementById('root')!).render(rootElement);
247
+ Catch-all or "wildcard" routes (e.g., `/app/[...catchAll]`) 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.
248
+
249
+ ```tsx
250
+ // ./src/entries.tsx
251
+ import { createPages } from 'waku';
252
+
253
+ import { DashboardPage } from './templates/dashboard-page.js';
254
+
255
+ export default createPages(async ({ createPage }) => {
256
+ // Create account dashboard
257
+ // `<DashboardPage>` receives `catchAll` prop (string[])
258
+ createPage({
259
+ render: 'dynamic',
260
+ path: '/app/[...catchAll]',
261
+ component: DashboardPage,
262
+ });
263
+ });
131
264
  ```
132
265
 
133
- The `initialInput` prop can be passed to the `Root` Component,
134
- overriding the default input which is `""`.
135
- You can also re-render a React Server Component with new input.
136
- Here's an example just to illustrate the idea:
266
+ ### Layouts
267
+
268
+ Layouts wrap an 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.
269
+
270
+ #### Root layout
271
+
272
+ The root layout rendered at `path: '/'` 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.
137
273
 
138
274
  ```tsx
139
- import { useRefetch } from 'waku/client';
140
-
141
- const Component = () => {
142
- const refetch = useRefetch();
143
- const handleClick = () => {
144
- refetch('...');
145
- };
146
- // ...
275
+ // ./src/entries.tsx
276
+ import { createPages } from 'waku';
277
+
278
+ import { RootLayout } from './templates/root-layout.js';
279
+
280
+ export default createPages(async ({ createLayout }) => {
281
+ // Add a global header and footer
282
+ createLayout({
283
+ render: 'static',
284
+ path: '/',
285
+ component: RootLayout,
286
+ });
287
+ });
288
+ ```
289
+
290
+ ```tsx
291
+ // ./src/templates/root-layout.tsx
292
+ import '../styles.css';
293
+
294
+ import { Providers } from '../components/providers.js';
295
+ import { Header } from '../components/header.js';
296
+ import { Footer } from '../components/footer.js';
297
+
298
+ export const RootLayout = async ({ children }) => {
299
+ return (
300
+ <Providers>
301
+ <meta property="og:image" content="/images/preview.png" />
302
+ <link rel="icon" type="image/png" href="/images/favicon.png" />
303
+ <Header />
304
+ <main>{children}</main>
305
+ <Footer />
306
+ </Providers>
307
+ );
308
+ };
309
+ ```
310
+
311
+ ```tsx
312
+ // ./src/components/providers.tsx
313
+ 'use client';
314
+
315
+ import { createStore, Provider } from 'jotai';
316
+
317
+ const store = createStore();
318
+
319
+ export const Providers = ({ children }) => {
320
+ return <Provider store={store}>{children}</Provider>;
147
321
  };
148
322
  ```
149
323
 
150
- #### Additional Server API
324
+ #### Other layouts
151
325
 
152
- In addition to the `renderEntries` function, you can also
153
- optionally specify `getBuildConfig` function in
154
- `entries.ts`. Here's an example:
326
+ Layouts are also helpful further down the tree. For example you could add a layout at `path: '/blog` to add a sidebar to both the blog index and all blog article pages.
155
327
 
156
328
  ```tsx
157
- import { defineEntries } from 'waku/server';
158
-
159
- export default defineEntries(
160
- // renderEntries
161
- async (input) => {
162
- return {
163
- App: <App name={input || 'Waku'} />,
164
- };
165
- },
166
- // getBuildConfig
167
- async () => {
168
- return {
169
- '/': {
170
- entries: [['']],
171
- },
172
- };
173
- },
174
- );
329
+ // ./src/entries.tsx
330
+ import { createPages } from 'waku';
331
+
332
+ import { BlogLayout } from './templates/blog-layout.js';
333
+
334
+ export default createPages(async ({ createLayout }) => {
335
+ // Add a sidebar to the blog index and blog article pages
336
+ createLayout({
337
+ render: 'static',
338
+ path: '/blog',
339
+ component: BlogLayout,
340
+ });
341
+ });
342
+ ```
343
+
344
+ ```tsx
345
+ // ./src/templates/blog-layout.tsx
346
+ import { Sidebar } from '../components/sidebar.js';
347
+
348
+ export const BlogLayout = async ({ children }) => {
349
+ return (
350
+ <>
351
+ <div>{children}</div>
352
+ <Sidebar />
353
+ </>
354
+ );
355
+ };
175
356
  ```
176
357
 
177
- The `getBuildConfig` function is used for build-time
178
- optimization. It renders React Server Components during the build
179
- process to produce the output that will be sent to the client. Note
180
- that rendering here means to produce RSC payload not HTML content.
358
+ ## Navigation
181
359
 
182
- #### How to try it
360
+ Internal links should be made with the Waku `<Link />` component. It accepts a `to` prop for the destination, which is automatically prefetched ahead of the navigation.
183
361
 
184
- If you create a project with something like
185
- `npm create waku@latest`, it will create the minimal
186
- example app.
362
+ ```tsx
363
+ // ./src/templates/home-page.tsx
364
+ import { Link } from 'waku';
365
+
366
+ export const HomePage = async () => {
367
+ return (
368
+ <>
369
+ <h1>Home</h1>
370
+ <Link to="/about">About</Link>
371
+ </>
372
+ );
373
+ };
374
+ ```
187
375
 
188
- ### Router
376
+ ## Static assets
189
377
 
190
- Waku provides a router built on top of the minimal API, and it serves
191
- as a reference implementation.
378
+ 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.
192
379
 
193
- #### Client API
380
+ For example an image added to `./public/images/logo.svg` can be rendered via `<img src="/images/logo.svg" />`.
194
381
 
195
- To use the router, it is required to use the `Router`
196
- component instead of using `Root` and `Slot` directly.
197
- The following code demonstrates how to use
198
- the `Router` component as the root component:
382
+ ## Data fetching
199
383
 
200
- ```tsx
201
- import { createRoot } from 'react-dom/client';
202
- import { Router } from 'waku/router/client';
384
+ ### Server
203
385
 
204
- const root = createRoot(document.getElementById('root')!);
386
+ 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.
205
387
 
206
- root.render(<Router />);
388
+ ```tsx
389
+ // ./src/templates/blog-article-page.tsx
390
+ import { MDX } from '../components/mdx.js';
391
+ import { getArticle } from '../lib/get-article.js';
392
+
393
+ export const BlogArticlePage = async ({ slug }) => {
394
+ const article = await getArticle(slug);
395
+
396
+ return (
397
+ <>
398
+ <title>{article.frontmatter.title}</h3>
399
+ <h1>{article.frontmatter.title}</h3>
400
+ <MDX>{article.content}</MDX>
401
+ </>
402
+ );
403
+ };
207
404
  ```
208
405
 
209
- The `Router` component internally uses `Root` and `Slot`
210
- and handles nested routes.
406
+ ### Client
407
+
408
+ 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.
211
409
 
212
- #### Server API
410
+ ## State management
213
411
 
214
- In `entries.ts`, we use `defineRouter` to export
215
- `getEntry` and `getBuildConfig` at once.
216
- Here's a simple example code without builder:
412
+ We recommend [Jotai](https://jotai.org) 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.
413
+
414
+ 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.
415
+
416
+ ## Metadata
417
+
418
+ 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.
217
419
 
218
420
  ```tsx
219
- import { defineRouter } from 'waku/router/server';
220
-
221
- export default defineRouter((id) => {
222
- switch (id) {
223
- case 'index/page':
224
- return import('./routes/index.tsx');
225
- case 'foo/page':
226
- return import('./routes/foo.tsx');
227
- default:
228
- return null;
229
- }
230
- });
421
+ // ./src/templates/root-layout.tsx
422
+ export const RootLayout = async ({ children }) => {
423
+ return (
424
+ <>
425
+ <meta property="og:image" content="/images/preview.png" />
426
+ <link rel="icon" type="image/png" href="/images/favicon.png" />
427
+ {children}
428
+ </>
429
+ );
430
+ };
431
+ ```
432
+
433
+ ```tsx
434
+ // ./src/templates/home-page.tsx
435
+ export const HomePage = async () => {
436
+ return (
437
+ <>
438
+ <title>Waku</title>
439
+ <meta property="description" content="The minimal React framework" />
440
+ <h1>Waku</h1>
441
+ <div>Hello world!</div>
442
+ </>
443
+ );
444
+ };
231
445
  ```
232
446
 
233
- The implementation of the `defineRouter` is config-based.
234
- However, it isn't too difficult to make a file-based router.
235
- Here's a file-based example code with builder:
447
+ ## Styling
448
+
449
+ ### Global styles
450
+
451
+ 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.
236
452
 
237
453
  ```tsx
238
- import { fileURLtoPath } from 'node:url';
239
- import path from 'node:path';
240
- import { glob } from 'glob';
241
- import { defineRouter } from 'waku/router/server';
242
-
243
- const routesDir = path.join(
244
- path.dirname(fileURLToPath(import.meta.url)),
245
- 'routes',
246
- );
247
-
248
- export default defineRouter(
249
- // getComponent (id is '**/layout' or '**/page')
250
- async (id) => {
251
- const files = await glob(`${id}.{tsx,js}`, { cwd: routesDir });
252
- if (files.length === 0) {
253
- return null;
254
- }
255
- const items = id.split('/');
256
- switch (items.length) {
257
- case 1:
258
- return import(`./routes/${items[0]}.tsx`);
259
- case 2:
260
- return import(`./routes/${items[0]}/${items[1]}.tsx`);
261
- case 3:
262
- return import(`./routes/${items[0]}/${items[1]}/${items[2]}.tsx`);
263
- default:
264
- throw new Error('too deep route');
265
- }
266
- },
267
- // getPathsForBuild
268
- async () => {
269
- const files = await glob('**/page.{tsx,js}', { cwd: routesDir });
270
- return files.map(
271
- (file) => '/' + file.slice(0, Math.max(0, file.lastIndexOf('/'))),
272
- );
454
+ // ./src/templates/root-layout.tsx
455
+ import '../styles.css';
456
+
457
+ export const RootLayout = async ({ children }) => {
458
+ return <main>{children}</main>;
459
+ };
460
+ ```
461
+
462
+ ```css
463
+ /* ./src/styles.css */
464
+ @tailwind base;
465
+ @tailwind components;
466
+ @tailwind utilities;
467
+ ```
468
+
469
+ ```js
470
+ // ./tailwind.config.js
471
+ export default {
472
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
473
+ };
474
+ ```
475
+
476
+ ```js
477
+ // ./postcss.config.js
478
+ export default {
479
+ plugins: {
480
+ tailwindcss: {},
481
+ autoprefixer: {},
273
482
  },
274
- );
483
+ };
275
484
  ```
276
485
 
277
- Due to the limitation of bundler, we cannot automatically allow
278
- infinite depth of routes.
486
+ ## Environment variables
487
+
488
+ It's important to distinguish environment variables that must be kept secret from those that can be made public.
489
+
490
+ #### Private
279
491
 
280
- #### How to try it
492
+ 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.
281
493
 
282
- You can try an example app in the repository by cloning it and running
283
- the following commands:
494
+ #### Public
284
495
 
285
- ```sh
286
- git clone https://github.com/dai-shi/waku.git
287
- cd waku
288
- pnpm install
289
- npm run examples:dev:07_router
496
+ 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.
497
+
498
+ ### Runtime agnostic (recommended)
499
+
500
+ Environment variables are available on the server via the Waku `getEnv` function and on the client via `import.meta.env`.
501
+
502
+ ```tsx
503
+ // server components can access both private and public variables
504
+ import { getEnv } from 'waku';
505
+
506
+ export const ServerComponent = async () => {
507
+ const secretKey = getEnv('SECRET_KEY');
508
+
509
+ return <>{/* ...*/}</>;
510
+ };
290
511
  ```
291
512
 
292
- Alternatively, you could create a project with something like
293
- `npm create waku@latest` and copy files from the example
294
- folder in the repository.
513
+ ```tsx
514
+ // client components can only access public variables
515
+ 'use client';
295
516
 
296
- ## Deploy
517
+ export const ClientComponent = () => {
518
+ const publicStatement = import.meta.env.WAKU_PUBLIC_HELLO;
519
+
520
+ return <>{/* ...*/}</>;
521
+ };
522
+ ```
523
+
524
+ ### Node.js
525
+
526
+ In Node.js environments, `process.env` may be used for compatibility.
527
+
528
+ ```tsx
529
+ // server components can access both private and public variables
530
+ export const ServerComponent = async () => {
531
+ const secretKey = process.env.SECRET_KEY;
532
+
533
+ return <>{/* ...*/}</>;
534
+ };
535
+ ```
536
+
537
+ ```tsx
538
+ // client components can only access public variables
539
+ 'use client';
540
+
541
+ export const ClientComponent = () => {
542
+ const publicStatement = process.env.WAKU_PUBLIC_HELLO;
543
+
544
+ return <>{/* ...*/}</>;
545
+ };
546
+ ```
547
+
548
+ ## Deployment
297
549
 
298
550
  ### Vercel
299
551
 
300
- ```sh
552
+ Waku projects can be deployed to Vercel with the [Vercel CLI](https://vercel.com/docs/cli) automatically.
553
+
554
+ ```
301
555
  vercel
302
556
  ```
303
557
 
304
- Then change the setting as follows (needs redeploy for the first time):
558
+ #### Pure SSG
305
559
 
306
- ![vercel](https://github.com/dai-shi/waku/assets/490574/6bd317a8-2772-42f4-92d4-b508af7d7460)
560
+ Adding the `--with-vercel-static` flag to the build script will produce static sites without serverless functions.
561
+
562
+ ```
563
+ {
564
+ "scripts": {
565
+ "build": "waku build --with-ssr --with-vercel-static"
566
+ }
567
+ }
568
+ ```
307
569
 
308
570
  ### Cloudflare (experimental)
309
571
 
310
- ```sh
572
+ ```
311
573
  npm run build -- --with-cloudflare
312
- rm -r node_modules
313
- npm install --omit=dev --omit=peer
314
574
  npx wrangler dev # or deploy
315
575
  ```
316
576
 
317
577
  ### Deno Deploy (experimental)
318
578
 
319
- ```sh
579
+ ```
320
580
  npm run build -- --with-deno
321
581
  DENO_DEPLOY_TOKEN=... deployctl deploy --project=... --prod serve.ts --exclude node_modules
322
582
  ```
323
583
 
324
- ## Tweets
584
+ ## Community
325
585
 
326
- <https://github.com/dai-shi/waku/discussions/150>
586
+ Please join our friendly [GitHub discussions](https://github.com/dai-shi/waku/discussions) or [Discord server](https://discord.gg/MrQdmzd) to participate in the Waku community. Hope to see you there!
327
587
 
328
- ## Diagrams
588
+ ## Roadmap
329
589
 
330
- <https://github.com/dai-shi/waku/discussions/151>
590
+ Waku is in active development and we're seeking additional contributors. Check out our [roadmap](https://github.com/dai-shi/waku/issues/24) for more information.
@@ -5,7 +5,7 @@ declare global {
5
5
  readonly env: Record<string, string>;
6
6
  }
7
7
  }
8
- type ChangeLocation = (path?: string, searchParams?: URLSearchParams, mode?: 'push' | 'replace' | false) => void;
8
+ type ChangeLocation = (path?: string, searchParams?: URLSearchParams, method?: 'pushState' | 'replaceState' | false, scrollTo?: ScrollToOptions | false) => void;
9
9
  export declare function useChangeLocation(): ChangeLocation;
10
10
  export declare function useLocation(): RouteProps;
11
11
  export type LinkProps = {
@@ -114,7 +114,10 @@ function InnerRouter() {
114
114
  }, [
115
115
  cached
116
116
  ]);
117
- const changeLocation = useCallback((path, searchParams, mode = 'push')=>{
117
+ const changeLocation = useCallback((path, searchParams, method = 'pushState', scrollTo = {
118
+ top: 0,
119
+ left: 0
120
+ })=>{
118
121
  const url = new URL(window.location.href);
119
122
  if (path) {
120
123
  url.pathname = path;
@@ -122,13 +125,14 @@ function InnerRouter() {
122
125
  if (searchParams) {
123
126
  url.search = '?' + searchParams.toString();
124
127
  }
125
- if (mode === 'replace') {
126
- window.history.replaceState(window.history.state, '', url);
127
- } else if (mode === 'push') {
128
- window.history.pushState(window.history.state, '', url);
128
+ if (method) {
129
+ window.history[method](window.history.state, '', url);
129
130
  }
130
131
  const loc = parseLocation();
131
132
  setLoc(loc);
133
+ if (scrollTo) {
134
+ window.scrollTo(scrollTo);
135
+ }
132
136
  const componentIds = getComponentIds(loc.path);
133
137
  const skip = getSkipList(componentIds, loc, cachedRef.current);
134
138
  if (componentIds.every((id)=>skip.includes(id))) {
package/package.json CHANGED
@@ -1,10 +1,7 @@
1
1
  {
2
2
  "name": "waku",
3
3
  "description": "⛩️ The minimal React framework",
4
- "version": "0.19.0-beta.2",
5
- "publishConfig": {
6
- "tag": "next"
7
- },
4
+ "version": "0.19.0",
8
5
  "type": "module",
9
6
  "author": "Daishi Kato",
10
7
  "repository": {
@@ -47,7 +47,8 @@ const parseLocation = (): RouteProps => {
47
47
  type ChangeLocation = (
48
48
  path?: string,
49
49
  searchParams?: URLSearchParams,
50
- mode?: 'push' | 'replace' | false,
50
+ method?: 'pushState' | 'replaceState' | false,
51
+ scrollTo?: ScrollToOptions | false,
51
52
  ) => void;
52
53
 
53
54
  type PrefetchLocation = (path: string, searchParams: URLSearchParams) => void;
@@ -190,7 +191,12 @@ function InnerRouter() {
190
191
  }, [cached]);
191
192
 
192
193
  const changeLocation: ChangeLocation = useCallback(
193
- (path, searchParams, mode = 'push') => {
194
+ (
195
+ path,
196
+ searchParams,
197
+ method = 'pushState',
198
+ scrollTo = { top: 0, left: 0 },
199
+ ) => {
194
200
  const url = new URL(window.location.href);
195
201
  if (path) {
196
202
  url.pathname = path;
@@ -198,13 +204,14 @@ function InnerRouter() {
198
204
  if (searchParams) {
199
205
  url.search = '?' + searchParams.toString();
200
206
  }
201
- if (mode === 'replace') {
202
- window.history.replaceState(window.history.state, '', url);
203
- } else if (mode === 'push') {
204
- window.history.pushState(window.history.state, '', url);
207
+ if (method) {
208
+ window.history[method](window.history.state, '', url);
205
209
  }
206
210
  const loc = parseLocation();
207
211
  setLoc(loc);
212
+ if (scrollTo) {
213
+ window.scrollTo(scrollTo);
214
+ }
208
215
  const componentIds = getComponentIds(loc.path);
209
216
  const skip = getSkipList(componentIds, loc, cachedRef.current);
210
217
  if (componentIds.every((id) => skip.includes(id))) {