waku 0.20.0 → 0.20.1
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 +77 -54
- package/dist/cli.js +7 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +11 -1
- package/dist/config.d.ts +4 -2
- package/dist/lib/builder/build.js +31 -25
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +9 -1
- package/dist/lib/middleware/dev-server.js +7 -12
- package/dist/lib/plugins/vite-plugin-nonjs-resolve.js +4 -13
- package/dist/lib/plugins/vite-plugin-rsc-analyze.js +6 -11
- package/dist/lib/plugins/vite-plugin-rsc-delegate.js +10 -19
- package/dist/lib/plugins/vite-plugin-rsc-entries.js +6 -1
- package/dist/lib/plugins/vite-plugin-rsc-hmr.js +15 -30
- package/dist/lib/plugins/vite-plugin-rsc-index.d.ts +1 -1
- package/dist/lib/plugins/vite-plugin-rsc-index.js +10 -10
- package/dist/lib/plugins/vite-plugin-rsc-managed.js +19 -36
- package/dist/lib/plugins/vite-plugin-rsc-transform.js +1 -1
- package/dist/lib/renderers/dev-worker-impl.js +23 -14
- package/dist/lib/renderers/html-renderer.js +6 -5
- package/dist/lib/utils/merge-vite-config.js +3 -1
- package/dist/main.d.ts +3 -2
- package/dist/main.js +6 -2
- package/dist/main.react-server.d.ts +3 -0
- package/dist/main.react-server.js +3 -0
- package/dist/router/client.d.ts +19 -7
- package/dist/router/client.js +40 -5
- package/dist/router/fs-router.d.ts +1 -1
- package/dist/router/fs-router.js +23 -15
- package/package.json +11 -10
- package/src/cli.ts +7 -2
- package/src/client.ts +11 -1
- package/src/config.ts +4 -2
- package/src/lib/builder/build.ts +47 -8
- package/src/lib/config.ts +3 -1
- package/src/lib/middleware/dev-server.ts +5 -4
- package/src/lib/plugins/vite-plugin-nonjs-resolve.ts +5 -6
- package/src/lib/plugins/vite-plugin-rsc-analyze.ts +6 -5
- package/src/lib/plugins/vite-plugin-rsc-delegate.ts +10 -12
- package/src/lib/plugins/vite-plugin-rsc-entries.ts +8 -1
- package/src/lib/plugins/vite-plugin-rsc-hmr.ts +16 -24
- package/src/lib/plugins/vite-plugin-rsc-index.ts +10 -10
- package/src/lib/plugins/vite-plugin-rsc-managed.ts +24 -41
- package/src/lib/plugins/vite-plugin-rsc-transform.ts +2 -2
- package/src/lib/renderers/dev-worker-impl.ts +34 -10
- package/src/lib/renderers/html-renderer.ts +12 -6
- package/src/lib/utils/merge-vite-config.ts +3 -2
- package/src/main.react-server.ts +5 -0
- package/src/main.ts +13 -2
- package/src/router/client.ts +52 -5
- package/src/router/fs-router.ts +20 -9
package/README.md
CHANGED
|
@@ -13,9 +13,9 @@ visit [waku.gg](https://waku.gg) or `npm create waku@latest`
|
|
|
13
13
|
|
|
14
14
|
## Introduction
|
|
15
15
|
|
|
16
|
-
**Waku** _(wah-ku)_ or **わく** means “framework” in Japanese. As the minimal React framework, it
|
|
16
|
+
**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.
|
|
17
17
|
|
|
18
|
-
We recommend other frameworks for heavy ecommerce or enterprise applications. Waku is a lightweight alternative
|
|
18
|
+
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!
|
|
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
|
|
|
@@ -24,30 +24,30 @@ We recommend other frameworks for heavy ecommerce or enterprise applications. Wa
|
|
|
24
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).
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
npm create waku@
|
|
27
|
+
npm create waku@latest
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
**Node.js version requirement:** `^20.8.0
|
|
30
|
+
**Node.js version requirement:** `^20.8.0` or `^18.17.0`
|
|
31
31
|
|
|
32
32
|
## Rendering
|
|
33
33
|
|
|
34
|
-
While there
|
|
34
|
+
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](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md).
|
|
35
35
|
|
|
36
|
-
So please don
|
|
36
|
+
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.
|
|
37
37
|
|
|
38
|
-
And please don
|
|
38
|
+
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.
|
|
39
39
|
|
|
40
40
|
> Future versions of Waku may provide additional opt-in APIs to abstract some of the complexity away for an improved developer experience.
|
|
41
41
|
|
|
42
42
|
#### Server components
|
|
43
43
|
|
|
44
|
-
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
|
|
44
|
+
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.
|
|
45
45
|
|
|
46
46
|
```tsx
|
|
47
47
|
// server component
|
|
48
48
|
import db from 'some-db';
|
|
49
49
|
|
|
50
|
-
import { Gallery } from '../components/gallery
|
|
50
|
+
import { Gallery } from '../components/gallery';
|
|
51
51
|
|
|
52
52
|
export const Store = async () => {
|
|
53
53
|
const products = await db.query('SELECT * FROM products');
|
|
@@ -58,7 +58,7 @@ export const Store = async () => {
|
|
|
58
58
|
|
|
59
59
|
#### Client components
|
|
60
60
|
|
|
61
|
-
A `'use client'` directive placed at the top of a file will create a server-client boundary when
|
|
61
|
+
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.
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
64
|
// client component
|
|
@@ -95,7 +95,7 @@ Server components can import client components and doing so will create a server
|
|
|
95
95
|
|
|
96
96
|
```tsx
|
|
97
97
|
// ./src/pages/_layout.tsx
|
|
98
|
-
import { Providers } from '../components/providers
|
|
98
|
+
import { Providers } from '../components/providers';
|
|
99
99
|
|
|
100
100
|
export default async function RootLayout({ children }) {
|
|
101
101
|
return (
|
|
@@ -131,9 +131,9 @@ Waku provides static prerendering (SSG) and server-side rendering (SSR) options
|
|
|
131
131
|
|
|
132
132
|
Each layout and page in Waku is composed of a React component hierarchy.
|
|
133
133
|
|
|
134
|
-
It begins with a server component at the top of the tree. Then at points down the hierarchy, you
|
|
134
|
+
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.
|
|
135
135
|
|
|
136
|
-
Server components can be rendered below this boundary, but only via composition (e.g., `children` props). Together they form [a new
|
|
136
|
+
Server components can be rendered below this boundary, but only via composition (e.g., `children` props). Together they form [a new “React server” layer](https://github.com/reactwg/server-components/discussions/4) that runs _before_ the traditional “React client” layer with which you’re already familiar.
|
|
137
137
|
|
|
138
138
|
Client components are server-side rendered as SSR is separate from RSC. See the [linked diagrams](https://github.com/reactwg/server-components/discussions/4) for a helpful visual.
|
|
139
139
|
|
|
@@ -143,32 +143,40 @@ To learn more about the modern React architecture, we recommend [Making Sense of
|
|
|
143
143
|
|
|
144
144
|
## Routing
|
|
145
145
|
|
|
146
|
-
Waku provides
|
|
146
|
+
Waku provides minimal file-based “pages router” experience built for the modern React server components era.
|
|
147
147
|
|
|
148
|
-
Its underlying [low-level API](https://github.com/dai-shi/waku/blob/main/docs/create-pages.mdx) is also available for those that prefer programmatic routing. This documentation covers file-based routing since many React developers prefer it, but
|
|
148
|
+
Its underlying [low-level API](https://github.com/dai-shi/waku/blob/main/docs/create-pages.mdx) 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.
|
|
149
149
|
|
|
150
150
|
### Overview
|
|
151
151
|
|
|
152
|
-
The directory for file-based routing in Waku projects is `./src/pages`.
|
|
152
|
+
The directory for file-based routing in Waku projects is `./src/pages`.
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
Waku currently supports two rendering options:
|
|
157
|
+
|
|
158
|
+
- `'static'` for static prerendering (SSG)
|
|
159
|
+
|
|
160
|
+
- `'dynamic'` for server-side rendering (SSR)
|
|
155
161
|
|
|
156
162
|
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.
|
|
157
163
|
|
|
158
164
|
```tsx
|
|
159
165
|
// ./src/pages/_layout.tsx
|
|
166
|
+
import '../styles.css';
|
|
160
167
|
|
|
161
|
-
import {
|
|
162
|
-
import {
|
|
168
|
+
import { Providers } from '../components/providers';
|
|
169
|
+
import { Header } from '../components/header';
|
|
170
|
+
import { Footer } from '../components/footer';
|
|
163
171
|
|
|
164
172
|
// Create root layout
|
|
165
173
|
export default async function RootLayout({ children }) {
|
|
166
174
|
return (
|
|
167
|
-
|
|
175
|
+
<Providers>
|
|
168
176
|
<Header />
|
|
169
177
|
<main>{children}</main>
|
|
170
178
|
<Footer />
|
|
171
|
-
|
|
179
|
+
</Providers>
|
|
172
180
|
);
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -187,10 +195,10 @@ export default async function HomePage() {
|
|
|
187
195
|
const data = await getData();
|
|
188
196
|
|
|
189
197
|
return (
|
|
190
|
-
|
|
191
|
-
<h1>{data.
|
|
192
|
-
<div>{data.
|
|
193
|
-
|
|
198
|
+
<>
|
|
199
|
+
<h1>{data.title}</h1>
|
|
200
|
+
<div>{data.content}</div>
|
|
201
|
+
</>
|
|
194
202
|
);
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -207,9 +215,11 @@ export const getConfig = async () => {
|
|
|
207
215
|
|
|
208
216
|
### Pages
|
|
209
217
|
|
|
218
|
+
Pages render a single route, segment route, or catch-all route based on the file system path (conventions below). All page components automatically receive two props related to the rendered route: `path` (string) and `searchParams` ([URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)).
|
|
219
|
+
|
|
210
220
|
#### Single routes
|
|
211
221
|
|
|
212
|
-
Pages can be rendered as a single route (e.g.,
|
|
222
|
+
Pages can be rendered as a single route (e.g., `about.tsx` or `blog/index.tsx`).
|
|
213
223
|
|
|
214
224
|
```tsx
|
|
215
225
|
// ./src/pages/about.tsx
|
|
@@ -243,7 +253,11 @@ export const getConfig = async () => {
|
|
|
243
253
|
|
|
244
254
|
#### Segment routes
|
|
245
255
|
|
|
246
|
-
|
|
256
|
+
Segment routes (e.g., `[slug].tsx` or `[slug]/index.tsx`) are marked with brackets.
|
|
257
|
+
|
|
258
|
+
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'`).
|
|
259
|
+
|
|
260
|
+
If statically prerendering a segment route at build time, a `staticPaths` array must also be provided.
|
|
247
261
|
|
|
248
262
|
```tsx
|
|
249
263
|
// ./src/pages/blog/[slug].tsx
|
|
@@ -262,13 +276,13 @@ const getData = async (slug) => {
|
|
|
262
276
|
export const getConfig = async () => {
|
|
263
277
|
return {
|
|
264
278
|
render: 'static',
|
|
265
|
-
staticPaths: ['introducing-waku', 'introducing-
|
|
279
|
+
staticPaths: ['introducing-waku', 'introducing-pages-router'],
|
|
266
280
|
};
|
|
267
281
|
};
|
|
268
282
|
```
|
|
269
283
|
|
|
270
284
|
```tsx
|
|
271
|
-
// ./src/pages/shop/[category]
|
|
285
|
+
// ./src/pages/shop/[category].tsx
|
|
272
286
|
|
|
273
287
|
// Create product category pages
|
|
274
288
|
export default async function ProductCategoryPage({ category }) {
|
|
@@ -288,7 +302,7 @@ export const getConfig = async () => {
|
|
|
288
302
|
};
|
|
289
303
|
```
|
|
290
304
|
|
|
291
|
-
Static paths (or other values)
|
|
305
|
+
Static paths (or other config values) can also be generated programmatically.
|
|
292
306
|
|
|
293
307
|
```tsx
|
|
294
308
|
// ./src/pages/blog/[slug].tsx
|
|
@@ -320,7 +334,7 @@ const getStaticPaths = async () => {
|
|
|
320
334
|
|
|
321
335
|
#### Nested segment routes
|
|
322
336
|
|
|
323
|
-
Routes can contain multiple segments (e.g., `/shop/[category]/[product]`).
|
|
337
|
+
Routes can contain multiple segments (e.g., `/shop/[category]/[product]`) by creating folders with brackets as well.
|
|
324
338
|
|
|
325
339
|
```tsx
|
|
326
340
|
// ./src/pages/shop/[category]/[product].tsx
|
|
@@ -351,8 +365,8 @@ export const getConfig = async () => {
|
|
|
351
365
|
return {
|
|
352
366
|
render: 'static',
|
|
353
367
|
staticPaths: [
|
|
354
|
-
['
|
|
355
|
-
['
|
|
368
|
+
['same-category', 'some-product'],
|
|
369
|
+
['same-category', 'another-product'],
|
|
356
370
|
],
|
|
357
371
|
};
|
|
358
372
|
};
|
|
@@ -360,9 +374,9 @@ export const getConfig = async () => {
|
|
|
360
374
|
|
|
361
375
|
#### Catch-all routes
|
|
362
376
|
|
|
363
|
-
Catch-all or
|
|
377
|
+
Catch-all or “wildcard” segment routes (e.g., `/app/[...catchAll]`) are marked with an ellipsis before the name and have indefinite segments.
|
|
364
378
|
|
|
365
|
-
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.
|
|
379
|
+
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.
|
|
366
380
|
|
|
367
381
|
```tsx
|
|
368
382
|
// ./src/pages/app/[...catchAll].tsx
|
|
@@ -391,9 +405,9 @@ The root layout placed at `./pages/_layout.tsx` is especially useful. It can be
|
|
|
391
405
|
// ./src/pages/_layout.tsx
|
|
392
406
|
import '../styles.css';
|
|
393
407
|
|
|
394
|
-
import { Providers } from '../components/providers
|
|
395
|
-
import { Header } from '../components/header
|
|
396
|
-
import { Footer } from '../components/footer
|
|
408
|
+
import { Providers } from '../components/providers';
|
|
409
|
+
import { Header } from '../components/header';
|
|
410
|
+
import { Footer } from '../components/footer';
|
|
397
411
|
|
|
398
412
|
// Create root layout
|
|
399
413
|
export default async function RootLayout({ children }) {
|
|
@@ -430,11 +444,11 @@ export const Providers = ({ children }) => {
|
|
|
430
444
|
|
|
431
445
|
#### Other layouts
|
|
432
446
|
|
|
433
|
-
Layouts are also helpful
|
|
447
|
+
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.
|
|
434
448
|
|
|
435
449
|
```tsx
|
|
436
450
|
// ./src/pages/blog/_layout.tsx
|
|
437
|
-
import { Sidebar } from '../../components/sidebar
|
|
451
|
+
import { Sidebar } from '../../components/sidebar';
|
|
438
452
|
|
|
439
453
|
// Create blog layout
|
|
440
454
|
export default async function BlogLayout({ children }) {
|
|
@@ -457,7 +471,7 @@ export const getConfig = async () => {
|
|
|
457
471
|
|
|
458
472
|
### Link
|
|
459
473
|
|
|
460
|
-
|
|
474
|
+
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.
|
|
461
475
|
|
|
462
476
|
```tsx
|
|
463
477
|
// ./src/pages/index.tsx
|
|
@@ -479,7 +493,7 @@ The `useRouter` hook can be used to inspect the current route or perform program
|
|
|
479
493
|
|
|
480
494
|
#### router properties
|
|
481
495
|
|
|
482
|
-
The `router` object has
|
|
496
|
+
The `router` object has two properties related to the current route: `path` (string) and `searchParams` ([URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)).
|
|
483
497
|
|
|
484
498
|
```tsx
|
|
485
499
|
'use client';
|
|
@@ -487,8 +501,7 @@ The `router` object has a `value` property with two additional properties relate
|
|
|
487
501
|
import { useRouter_UNSTABLE as useRouter } from 'waku';
|
|
488
502
|
|
|
489
503
|
export const Component = () => {
|
|
490
|
-
const
|
|
491
|
-
const { path, searchParams } = router.value;
|
|
504
|
+
const { path, searchParams } = useRouter();
|
|
492
505
|
|
|
493
506
|
return (
|
|
494
507
|
<>
|
|
@@ -575,7 +588,7 @@ export const getConfig = async () => {
|
|
|
575
588
|
};
|
|
576
589
|
```
|
|
577
590
|
|
|
578
|
-
Metadata
|
|
591
|
+
Metadata can also be generated programmatically.
|
|
579
592
|
|
|
580
593
|
```tsx
|
|
581
594
|
// ./src/pages/index.tsx
|
|
@@ -621,7 +634,7 @@ Install any required dev dependencies (e.g., `npm i -D tailwindcss autoprefixer`
|
|
|
621
634
|
import '../styles.css';
|
|
622
635
|
|
|
623
636
|
export default async function RootLayout({ children }) {
|
|
624
|
-
return
|
|
637
|
+
return <>{children}</>;
|
|
625
638
|
}
|
|
626
639
|
|
|
627
640
|
export const getConfig = async () => {
|
|
@@ -659,7 +672,17 @@ export default {
|
|
|
659
672
|
|
|
660
673
|
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.
|
|
661
674
|
|
|
662
|
-
|
|
675
|
+
```tsx
|
|
676
|
+
// assuming image is saved at `/public/images/logo.svg`
|
|
677
|
+
|
|
678
|
+
export const Logo = () => {
|
|
679
|
+
return (
|
|
680
|
+
<>
|
|
681
|
+
<img src="/images/logo.svg" />
|
|
682
|
+
</>
|
|
683
|
+
);
|
|
684
|
+
};
|
|
685
|
+
```
|
|
663
686
|
|
|
664
687
|
## File system
|
|
665
688
|
|
|
@@ -686,8 +709,8 @@ All of the wonderful patterns of React server components are supported. For exam
|
|
|
686
709
|
|
|
687
710
|
```tsx
|
|
688
711
|
// ./src/pages/blog/[slug].tsx
|
|
689
|
-
import { MDX } from '../../components/mdx
|
|
690
|
-
import { getArticle, getStaticPaths } from '../../lib/blog
|
|
712
|
+
import { MDX } from '../../components/mdx';
|
|
713
|
+
import { getArticle, getStaticPaths } from '../../lib/blog';
|
|
691
714
|
|
|
692
715
|
export default async function BlogArticlePage({ slug }) {
|
|
693
716
|
const article = await getArticle(slug);
|
|
@@ -717,13 +740,13 @@ Data should be fetched on the server when possible for the best user experience,
|
|
|
717
740
|
|
|
718
741
|
## State management
|
|
719
742
|
|
|
720
|
-
We recommend [Jotai](https://jotai.org) for global React state management based on the atomic model
|
|
743
|
+
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.
|
|
721
744
|
|
|
722
|
-
We
|
|
745
|
+
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.
|
|
723
746
|
|
|
724
747
|
## Environment variables
|
|
725
748
|
|
|
726
|
-
It
|
|
749
|
+
It’s important to distinguish environment variables that must be kept secret from those that can be made public.
|
|
727
750
|
|
|
728
751
|
#### Private
|
|
729
752
|
|
|
@@ -731,7 +754,7 @@ By default all environment variables are considered private and accessible only
|
|
|
731
754
|
|
|
732
755
|
#### Public
|
|
733
756
|
|
|
734
|
-
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
|
|
757
|
+
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.
|
|
735
758
|
|
|
736
759
|
### Runtime agnostic (recommended)
|
|
737
760
|
|
|
@@ -861,4 +884,4 @@ Please join our friendly [GitHub discussions](https://github.com/dai-shi/waku/di
|
|
|
861
884
|
|
|
862
885
|
## Roadmap
|
|
863
886
|
|
|
864
|
-
Waku is in active development and we
|
|
887
|
+
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/cli.js
CHANGED
|
@@ -45,6 +45,10 @@ const { values, positionals } = parseArgs({
|
|
|
45
45
|
'with-aws-lambda': {
|
|
46
46
|
type: 'boolean'
|
|
47
47
|
},
|
|
48
|
+
port: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
short: 'p'
|
|
51
|
+
},
|
|
48
52
|
version: {
|
|
49
53
|
type: 'boolean',
|
|
50
54
|
short: 'v'
|
|
@@ -88,7 +92,7 @@ async function runDev() {
|
|
|
88
92
|
config,
|
|
89
93
|
env: process.env
|
|
90
94
|
}));
|
|
91
|
-
const port = parseInt(
|
|
95
|
+
const port = parseInt(values.port || '3000', 10);
|
|
92
96
|
await startServer(app, port);
|
|
93
97
|
}
|
|
94
98
|
async function runBuild() {
|
|
@@ -119,7 +123,7 @@ async function runStart({ distDir = 'dist', entriesJs = 'entries.js', publicDir
|
|
|
119
123
|
}
|
|
120
124
|
return c.text('404 Not Found', 404);
|
|
121
125
|
});
|
|
122
|
-
const port = parseInt(
|
|
126
|
+
const port = parseInt(values.port || '8080', 10);
|
|
123
127
|
await startServer(app, port);
|
|
124
128
|
}
|
|
125
129
|
function startServer(app, port) {
|
|
@@ -157,6 +161,7 @@ Options:
|
|
|
157
161
|
--with-partykit Output for PartyKit on build
|
|
158
162
|
--with-deno Output for Deno on build
|
|
159
163
|
--with-aws-lambda Output for AWS Lambda on build
|
|
164
|
+
-p, --port Port number for the server
|
|
160
165
|
-v, --version Display the version number
|
|
161
166
|
-h, --help Display this help message
|
|
162
167
|
`);
|
package/dist/client.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ type CacheEntry = [
|
|
|
13
13
|
elements: Elements
|
|
14
14
|
];
|
|
15
15
|
declare const fetchCache: [CacheEntry?];
|
|
16
|
-
export declare const fetchRSC: (input: string, searchParamsString: string, setElements: SetElements, cache?: [CacheEntry?], unstable_onFetchData?: (
|
|
16
|
+
export declare const fetchRSC: (input: string, searchParamsString: string, setElements: SetElements, cache?: [CacheEntry?], unstable_onFetchData?: (data: unknown) => void) => Elements;
|
|
17
17
|
export declare const prefetchRSC: (input: string, searchParamsString: string) => void;
|
|
18
18
|
export declare const Root: ({ initialInput, initialSearchParamsString, cache, unstable_onFetchData, children, }: {
|
|
19
19
|
initialInput?: string;
|
package/dist/client.js
CHANGED
|
@@ -101,7 +101,17 @@ export const Slot = ({ id, children, fallback })=>{
|
|
|
101
101
|
if (!elementsPromise) {
|
|
102
102
|
throw new Error('Missing Root component');
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
let elements;
|
|
105
|
+
try {
|
|
106
|
+
elements = use(elementsPromise);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e instanceof Error) {
|
|
109
|
+
// HACK we assume any error as Not Found,
|
|
110
|
+
// probably caused by history api fallback
|
|
111
|
+
e.statusCode = 404;
|
|
112
|
+
}
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
105
115
|
if (!(id in elements)) {
|
|
106
116
|
if (fallback) {
|
|
107
117
|
return fallback;
|
package/dist/config.d.ts
CHANGED
|
@@ -41,13 +41,15 @@ export interface Config {
|
|
|
41
41
|
indexHtml?: string;
|
|
42
42
|
/**
|
|
43
43
|
* The client main file relative to srcDir.
|
|
44
|
-
*
|
|
44
|
+
* The extension should be `.js`,
|
|
45
|
+
* but resolved with other extensions in the development mode.
|
|
46
|
+
* Defaults to "main.js".
|
|
45
47
|
*/
|
|
46
48
|
mainJs?: string;
|
|
47
49
|
/**
|
|
48
50
|
* The entries.js file relative to srcDir or distDir.
|
|
49
51
|
* The extension should be `.js`,
|
|
50
|
-
* but resolved with
|
|
52
|
+
* but resolved with other extensions in the development mode.
|
|
51
53
|
* Defaults to "entries.js".
|
|
52
54
|
*/
|
|
53
55
|
entriesJs?: string;
|
|
@@ -2,7 +2,7 @@ import { Readable } from 'node:stream';
|
|
|
2
2
|
import { pipeline } from 'node:stream/promises';
|
|
3
3
|
import { build as buildVite, resolveConfig as resolveViteConfig } from 'vite';
|
|
4
4
|
import viteReact from '@vitejs/plugin-react';
|
|
5
|
-
import { resolveConfig } from '../config.js';
|
|
5
|
+
import { resolveConfig, EXTENSIONS } from '../config.js';
|
|
6
6
|
import { decodeFilePathFromAbsolute, extname, filePathToFileURL, fileURLToFilePath, joinPath } from '../utils/path.js';
|
|
7
7
|
import { appendFile, createWriteStream, existsSync, mkdir, readdir, readFile, rename, unlink, writeFile } from '../utils/node-fs.js';
|
|
8
8
|
import { encodeInput, generatePrefetchCode } from '../renderers/utils.js';
|
|
@@ -51,14 +51,7 @@ const analyzeEntries = async (rootDir, config, entriesFile)=>{
|
|
|
51
51
|
});
|
|
52
52
|
for (const file of files){
|
|
53
53
|
const ext = extname(file);
|
|
54
|
-
if (
|
|
55
|
-
'.js',
|
|
56
|
-
'.ts',
|
|
57
|
-
'.tsx',
|
|
58
|
-
'.jsx',
|
|
59
|
-
'.mjs',
|
|
60
|
-
'.cjs'
|
|
61
|
-
].includes(ext)) {
|
|
54
|
+
if (EXTENSIONS.includes(ext)) {
|
|
62
55
|
moduleFileMap.set(joinPath(preserveModuleDir, file.slice(0, -ext.length)), joinPath(dir, file));
|
|
63
56
|
}
|
|
64
57
|
}
|
|
@@ -183,6 +176,9 @@ const buildServerBundle = async (rootDir, config, entriesFile, clientEntryFiles,
|
|
|
183
176
|
},
|
|
184
177
|
noExternal: /^(?!node:)/
|
|
185
178
|
},
|
|
179
|
+
esbuild: {
|
|
180
|
+
jsx: 'automatic'
|
|
181
|
+
},
|
|
186
182
|
define: {
|
|
187
183
|
'process.env.NODE_ENV': JSON.stringify('production')
|
|
188
184
|
},
|
|
@@ -210,8 +206,7 @@ const buildServerBundle = async (rootDir, config, entriesFile, clientEntryFiles,
|
|
|
210
206
|
return serverBuildOutput;
|
|
211
207
|
};
|
|
212
208
|
// For SSR (render client components on server to generate HTML)
|
|
213
|
-
const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible)=>{
|
|
214
|
-
const mainJsFile = joinPath(rootDir, config.srcDir, config.mainJs);
|
|
209
|
+
const buildSsrBundle = async (rootDir, config, mainJsFile, clientEntryFiles, serverBuildOutput, isNodeCompatible)=>{
|
|
215
210
|
const cssAssets = serverBuildOutput.output.flatMap(({ type, fileName })=>type === 'asset' && fileName.endsWith('.css') ? [
|
|
216
211
|
fileName
|
|
217
212
|
] : []);
|
|
@@ -220,7 +215,8 @@ const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutp
|
|
|
220
215
|
plugins: [
|
|
221
216
|
rscIndexPlugin({
|
|
222
217
|
...config,
|
|
223
|
-
cssAssets
|
|
218
|
+
cssAssets,
|
|
219
|
+
mainJs: mainJsFile.split('/').pop()
|
|
224
220
|
}),
|
|
225
221
|
rscEnvPlugin({
|
|
226
222
|
config
|
|
@@ -242,6 +238,9 @@ const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutp
|
|
|
242
238
|
},
|
|
243
239
|
noExternal: /^(?!node:)/
|
|
244
240
|
},
|
|
241
|
+
esbuild: {
|
|
242
|
+
jsx: 'automatic'
|
|
243
|
+
},
|
|
245
244
|
define: {
|
|
246
245
|
'process.env.NODE_ENV': JSON.stringify('production')
|
|
247
246
|
},
|
|
@@ -270,8 +269,7 @@ const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutp
|
|
|
270
269
|
});
|
|
271
270
|
};
|
|
272
271
|
// For Browsers
|
|
273
|
-
const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput)=>{
|
|
274
|
-
const mainJsFile = joinPath(rootDir, config.srcDir, config.mainJs);
|
|
272
|
+
const buildClientBundle = async (rootDir, config, mainJsFile, clientEntryFiles, serverBuildOutput)=>{
|
|
275
273
|
const nonJsAssets = serverBuildOutput.output.flatMap(({ type, fileName })=>type === 'asset' && !fileName.endsWith('.js') ? [
|
|
276
274
|
fileName
|
|
277
275
|
] : []);
|
|
@@ -282,7 +280,8 @@ const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildO
|
|
|
282
280
|
viteReact(),
|
|
283
281
|
rscIndexPlugin({
|
|
284
282
|
...config,
|
|
285
|
-
cssAssets
|
|
283
|
+
cssAssets,
|
|
284
|
+
mainJs: mainJsFile.split('/').pop()
|
|
286
285
|
}),
|
|
287
286
|
rscEnvPlugin({
|
|
288
287
|
config
|
|
@@ -296,6 +295,7 @@ const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildO
|
|
|
296
295
|
onwarn,
|
|
297
296
|
input: {
|
|
298
297
|
main: mainJsFile,
|
|
298
|
+
// rollup will ouput the style files related to clientEntryFiles, but since it does not find any link to them in the index.html file, it will not inject them. They are only mentioned by the standalone `clientEntryFiles`
|
|
299
299
|
...clientEntryFiles
|
|
300
300
|
},
|
|
301
301
|
preserveEntrySignatures: 'exports-only',
|
|
@@ -376,7 +376,11 @@ const pathSpec2pathname = (pathSpec)=>{
|
|
|
376
376
|
}
|
|
377
377
|
return '/' + pathSpec.map(({ name })=>name).join('/');
|
|
378
378
|
};
|
|
379
|
-
const emitHtmlFiles = async (rootDir, config, distEntriesFile, distEntries, buildConfig, getClientModules)=>{
|
|
379
|
+
const emitHtmlFiles = async (rootDir, config, distEntriesFile, distEntries, buildConfig, getClientModules, clientBuildOutput)=>{
|
|
380
|
+
const nonJsAssets = clientBuildOutput.output.flatMap(({ type, fileName })=>type === 'asset' && !fileName.endsWith('.js') ? [
|
|
381
|
+
fileName
|
|
382
|
+
] : []);
|
|
383
|
+
const cssAssets = nonJsAssets.filter((asset)=>asset.endsWith('.css'));
|
|
380
384
|
const basePrefix = config.basePath + config.rscPath + '/';
|
|
381
385
|
const publicIndexHtmlFile = joinPath(rootDir, config.distDir, config.publicDir, config.indexHtml);
|
|
382
386
|
const publicIndexHtml = await readFile(publicIndexHtmlFile, {
|
|
@@ -389,6 +393,12 @@ const emitHtmlFiles = async (rootDir, config, distEntriesFile, distEntries, buil
|
|
|
389
393
|
const pathSpec = typeof pathname === 'string' ? pathname2pathSpec(pathname) : pathname;
|
|
390
394
|
let htmlStr = publicIndexHtml;
|
|
391
395
|
let htmlHead = publicIndexHtmlHead;
|
|
396
|
+
if (cssAssets.length) {
|
|
397
|
+
const cssStr = cssAssets.map((asset)=>`<link rel="stylesheet" href="${asset}">`).join('\n');
|
|
398
|
+
// HACK is this too naive to inject style code?
|
|
399
|
+
htmlStr = htmlStr.replace(/<\/head>/, cssStr);
|
|
400
|
+
htmlHead += cssStr;
|
|
401
|
+
}
|
|
392
402
|
const inputsForPrefetch = new Set();
|
|
393
403
|
const moduleIdsForPrefetch = new Set();
|
|
394
404
|
for (const { input, skipPrefetch } of entries || []){
|
|
@@ -455,12 +465,7 @@ export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)};
|
|
|
455
465
|
await appendFile(distEntriesFile, code);
|
|
456
466
|
};
|
|
457
467
|
const resolveFileName = (fname)=>{
|
|
458
|
-
for (const ext of
|
|
459
|
-
'.js',
|
|
460
|
-
'.ts',
|
|
461
|
-
'.tsx',
|
|
462
|
-
'.jsx'
|
|
463
|
-
]){
|
|
468
|
+
for (const ext of EXTENSIONS){
|
|
464
469
|
const resolvedName = fname.slice(0, -extname(fname).length) + ext;
|
|
465
470
|
if (existsSync(resolvedName)) {
|
|
466
471
|
return resolvedName;
|
|
@@ -474,11 +479,12 @@ export async function build(options) {
|
|
|
474
479
|
const rootDir = (await resolveViteConfig({}, 'build', 'production', 'production')).root;
|
|
475
480
|
const entriesFile = resolveFileName(joinPath(rootDir, config.srcDir, config.entriesJs));
|
|
476
481
|
const distEntriesFile = resolveFileName(joinPath(rootDir, config.distDir, config.entriesJs));
|
|
482
|
+
const mainJsFile = resolveFileName(joinPath(rootDir, config.srcDir, config.mainJs));
|
|
477
483
|
const isNodeCompatible = options.deploy !== 'cloudflare' && options.deploy !== 'partykit' && options.deploy !== 'deno';
|
|
478
484
|
const { clientEntryFiles, serverEntryFiles, serverModuleFiles } = await analyzeEntries(rootDir, config, entriesFile);
|
|
479
485
|
const serverBuildOutput = await buildServerBundle(rootDir, config, entriesFile, clientEntryFiles, serverEntryFiles, serverModuleFiles, (options.deploy === 'vercel-serverless' ? 'vercel' : false) || (options.deploy === 'netlify-functions' ? 'netlify' : false) || (options.deploy === 'cloudflare' ? 'cloudflare' : false) || (options.deploy === 'partykit' ? 'partykit' : false) || (options.deploy === 'deno' ? 'deno' : false) || (options.deploy === 'aws-lambda' ? 'aws-lambda' : false), isNodeCompatible);
|
|
480
|
-
await buildSsrBundle(rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible);
|
|
481
|
-
await buildClientBundle(rootDir, config, clientEntryFiles, serverBuildOutput);
|
|
486
|
+
await buildSsrBundle(rootDir, config, mainJsFile, clientEntryFiles, serverBuildOutput, isNodeCompatible);
|
|
487
|
+
const clientBuildOutput = await buildClientBundle(rootDir, config, mainJsFile, clientEntryFiles, serverBuildOutput);
|
|
482
488
|
const distEntries = await import(filePathToFileURL(distEntriesFile));
|
|
483
489
|
const buildConfig = await getBuildConfig({
|
|
484
490
|
config,
|
|
@@ -486,7 +492,7 @@ export async function build(options) {
|
|
|
486
492
|
});
|
|
487
493
|
await appendFile(distEntriesFile, `export const buildConfig = ${JSON.stringify(buildConfig)};`);
|
|
488
494
|
const { getClientModules } = await emitRscFiles(rootDir, config, distEntries, buildConfig);
|
|
489
|
-
await emitHtmlFiles(rootDir, config, distEntriesFile, distEntries, buildConfig, getClientModules);
|
|
495
|
+
await emitHtmlFiles(rootDir, config, distEntriesFile, distEntries, buildConfig, getClientModules, clientBuildOutput);
|
|
490
496
|
if (options.deploy?.startsWith('vercel-')) {
|
|
491
497
|
await emitVercelOutput(rootDir, config, options.deploy.slice('vercel-'.length));
|
|
492
498
|
} else if (options.deploy?.startsWith('netlify-')) {
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/config.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function resolveConfig(config) {
|
|
|
23
23
|
assetsDir: 'assets',
|
|
24
24
|
ssrDir: 'ssr',
|
|
25
25
|
indexHtml: 'index.html',
|
|
26
|
-
mainJs: 'main.
|
|
26
|
+
mainJs: 'main.js',
|
|
27
27
|
entriesJs: 'entries.js',
|
|
28
28
|
preserveModuleDirs: [
|
|
29
29
|
'pages',
|
|
@@ -43,3 +43,11 @@ export async function resolveConfig(config) {
|
|
|
43
43
|
}
|
|
44
44
|
return resolvedConfig;
|
|
45
45
|
}
|
|
46
|
+
export const EXTENSIONS = [
|
|
47
|
+
'.js',
|
|
48
|
+
'.ts',
|
|
49
|
+
'.tsx',
|
|
50
|
+
'.jsx',
|
|
51
|
+
'.mjs',
|
|
52
|
+
'.cjs'
|
|
53
|
+
];
|