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 +483 -223
- package/dist/router/client.d.ts +1 -1
- package/dist/router/client.js +9 -5
- package/package.json +1 -4
- package/src/router/client.ts +13 -6
package/README.md
CHANGED
|
@@ -2,329 +2,589 @@
|
|
|
2
2
|
|
|
3
3
|
⛩️ The minimal React framework
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
[](https://www.npmjs.com/package/waku)
|
|
7
|
-
[](https://waku.gg/discord)
|
|
5
|
+
visit [waku.gg](https://waku.gg) or `npm create waku@latest`
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
[](https://github.com/pmndrs/jotai/actions?query=workflow%3ALint)
|
|
8
|
+
[](https://www.npmjs.com/package/waku)
|
|
9
|
+
[](https://www.npmjs.com/package/waku)
|
|
10
|
+
[](https://discord.gg/MrQdmzd)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
https://github.com/dai-shi/waku/discussions/260
|
|
12
|
+
<br>
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Introduction
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
22
|
+
## Getting started
|
|
23
23
|
|
|
24
|
-
Waku
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
router. This flexibility makes it easier to build new features.
|
|
26
|
+
```
|
|
27
|
+
npm create waku@latest
|
|
28
|
+
```
|
|
35
29
|
|
|
36
|
-
|
|
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
|
-
|
|
32
|
+
Let's face it: React is getting complicated. But not without good reason!
|
|
43
33
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
42
|
+
```tsx
|
|
43
|
+
// server component
|
|
44
|
+
import db from 'some-db';
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
starting point for your project.
|
|
119
|
+
### Pages
|
|
78
120
|
|
|
79
|
-
|
|
121
|
+
#### Single routes
|
|
80
122
|
|
|
81
|
-
|
|
123
|
+
Pages can be rendered as a single route (e.g., `/about`).
|
|
82
124
|
|
|
83
|
-
|
|
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
|
-
####
|
|
149
|
+
#### Segment routes
|
|
86
150
|
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
import {
|
|
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
|
-
|
|
180
|
+
Static paths (or other values) could also be generated programatically.
|
|
97
181
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
import {
|
|
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
|
-
|
|
123
|
-
<StrictMode>
|
|
124
|
-
<Root>
|
|
125
|
-
<Slot id="App" />
|
|
126
|
-
</Root>
|
|
127
|
-
</StrictMode>
|
|
128
|
-
);
|
|
245
|
+
#### Catch-all routes
|
|
129
246
|
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
####
|
|
324
|
+
#### Other layouts
|
|
151
325
|
|
|
152
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
376
|
+
## Static assets
|
|
189
377
|
|
|
190
|
-
|
|
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
|
-
|
|
380
|
+
For example an image added to `./public/images/logo.svg` can be rendered via `<img src="/images/logo.svg" />`.
|
|
194
381
|
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
import { createRoot } from 'react-dom/client';
|
|
202
|
-
import { Router } from 'waku/router/client';
|
|
384
|
+
### Server
|
|
203
385
|
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
410
|
+
## State management
|
|
213
411
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
import
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
the following commands:
|
|
494
|
+
#### Public
|
|
284
495
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
513
|
+
```tsx
|
|
514
|
+
// client components can only access public variables
|
|
515
|
+
'use client';
|
|
295
516
|
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
+
#### Pure SSG
|
|
305
559
|
|
|
306
|
-
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
##
|
|
584
|
+
## Community
|
|
325
585
|
|
|
326
|
-
|
|
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
|
-
##
|
|
588
|
+
## Roadmap
|
|
329
589
|
|
|
330
|
-
|
|
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.
|
package/dist/router/client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ declare global {
|
|
|
5
5
|
readonly env: Record<string, string>;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
-
type ChangeLocation = (path?: string, searchParams?: URLSearchParams,
|
|
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 = {
|
package/dist/router/client.js
CHANGED
|
@@ -114,7 +114,10 @@ function InnerRouter() {
|
|
|
114
114
|
}, [
|
|
115
115
|
cached
|
|
116
116
|
]);
|
|
117
|
-
const changeLocation = useCallback((path, searchParams,
|
|
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 (
|
|
126
|
-
window.history
|
|
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
package/src/router/client.ts
CHANGED
|
@@ -47,7 +47,8 @@ const parseLocation = (): RouteProps => {
|
|
|
47
47
|
type ChangeLocation = (
|
|
48
48
|
path?: string,
|
|
49
49
|
searchParams?: URLSearchParams,
|
|
50
|
-
|
|
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
|
-
(
|
|
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 (
|
|
202
|
-
window.history
|
|
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))) {
|