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.
Files changed (51) hide show
  1. package/README.md +77 -54
  2. package/dist/cli.js +7 -2
  3. package/dist/client.d.ts +1 -1
  4. package/dist/client.js +11 -1
  5. package/dist/config.d.ts +4 -2
  6. package/dist/lib/builder/build.js +31 -25
  7. package/dist/lib/config.d.ts +1 -0
  8. package/dist/lib/config.js +9 -1
  9. package/dist/lib/middleware/dev-server.js +7 -12
  10. package/dist/lib/plugins/vite-plugin-nonjs-resolve.js +4 -13
  11. package/dist/lib/plugins/vite-plugin-rsc-analyze.js +6 -11
  12. package/dist/lib/plugins/vite-plugin-rsc-delegate.js +10 -19
  13. package/dist/lib/plugins/vite-plugin-rsc-entries.js +6 -1
  14. package/dist/lib/plugins/vite-plugin-rsc-hmr.js +15 -30
  15. package/dist/lib/plugins/vite-plugin-rsc-index.d.ts +1 -1
  16. package/dist/lib/plugins/vite-plugin-rsc-index.js +10 -10
  17. package/dist/lib/plugins/vite-plugin-rsc-managed.js +19 -36
  18. package/dist/lib/plugins/vite-plugin-rsc-transform.js +1 -1
  19. package/dist/lib/renderers/dev-worker-impl.js +23 -14
  20. package/dist/lib/renderers/html-renderer.js +6 -5
  21. package/dist/lib/utils/merge-vite-config.js +3 -1
  22. package/dist/main.d.ts +3 -2
  23. package/dist/main.js +6 -2
  24. package/dist/main.react-server.d.ts +3 -0
  25. package/dist/main.react-server.js +3 -0
  26. package/dist/router/client.d.ts +19 -7
  27. package/dist/router/client.js +40 -5
  28. package/dist/router/fs-router.d.ts +1 -1
  29. package/dist/router/fs-router.js +23 -15
  30. package/package.json +11 -10
  31. package/src/cli.ts +7 -2
  32. package/src/client.ts +11 -1
  33. package/src/config.ts +4 -2
  34. package/src/lib/builder/build.ts +47 -8
  35. package/src/lib/config.ts +3 -1
  36. package/src/lib/middleware/dev-server.ts +5 -4
  37. package/src/lib/plugins/vite-plugin-nonjs-resolve.ts +5 -6
  38. package/src/lib/plugins/vite-plugin-rsc-analyze.ts +6 -5
  39. package/src/lib/plugins/vite-plugin-rsc-delegate.ts +10 -12
  40. package/src/lib/plugins/vite-plugin-rsc-entries.ts +8 -1
  41. package/src/lib/plugins/vite-plugin-rsc-hmr.ts +16 -24
  42. package/src/lib/plugins/vite-plugin-rsc-index.ts +10 -10
  43. package/src/lib/plugins/vite-plugin-rsc-managed.ts +24 -41
  44. package/src/lib/plugins/vite-plugin-rsc-transform.ts +2 -2
  45. package/src/lib/renderers/dev-worker-impl.ts +34 -10
  46. package/src/lib/renderers/html-renderer.ts +12 -6
  47. package/src/lib/utils/merge-vite-config.ts +3 -2
  48. package/src/main.react-server.ts +5 -0
  49. package/src/main.ts +13 -2
  50. package/src/router/client.ts +52 -5
  51. 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 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.
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 designed to bring a fun developer experience to the modern React server components era. Yes, let’s make React development fun!
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@next
27
+ npm create waku@latest
28
28
  ```
29
29
 
30
- **Node.js version requirement:** `^20.8.0 || ^18.17.0`
30
+ **Node.js version requirement:** `^20.8.0` or `^18.17.0`
31
31
 
32
32
  ## Rendering
33
33
 
34
- While there's a 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).
34
+ While theres 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'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.
36
+ So please dont be intimidated by the `'use client'` directive! Once you get the hang of it, youll 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. Its way simpler than maintaining separate codebases for your backend and frontend.
37
37
 
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.
38
+ And please dont 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't included in the client bundle. They have no state, interactivity, or access to browser APIs since they run _exclusively_ on the server.
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 arent 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.js';
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 the module is 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.
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.js';
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'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 in the component hierarchy, all imported components are hydrated and will run in the browser as well.
134
+ It begins with a server component at the top of the tree. Then at points down the hierarchy, youll 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 "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.
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 youre 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 a familiar file-based “pages router” experience built for the modern React server components era.
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 please feel free to try both and see which you prefer.
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`. 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 including the render method and other options.
152
+ The directory for file-based routing in Waku projects is `./src/pages`.
153
153
 
154
- Waku currently supports two rendering options: `'static'` for static prerendering (SSG) or `'dynamic'` for server-side rendering (SSR).
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 { Header } from '../components/header.js';
162
- import { Footer } from '../components/footer.js';
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
- <div>
191
- <h1>{data.someDynamicTitle}</h1>
192
- <div>{data.someDynamicContent}</div>
193
- </div>
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., `/about` or `/blog`).
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
- 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 segment (e.g., `'introducing-waku'`). If statically prerendering a segment route at build time, a `staticPaths` array must also be provided.
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-create-pages'],
279
+ staticPaths: ['introducing-waku', 'introducing-pages-router'],
266
280
  };
267
281
  };
268
282
  ```
269
283
 
270
284
  ```tsx
271
- // ./src/pages/shop/[category]/index.tsx
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) could also be generated programmatically.
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
- ['some-category', 'some-product'],
355
- ['some-category', 'another-product'],
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 "wildcard" routes (e.g., `/app/[...catchAll]`) have indefinite segments. Wildcard routes receive a prop with segment values as an ordered array.
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.js';
395
- import { Header } from '../components/header.js';
396
- import { Footer } from '../components/footer.js';
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 further down the tree. For example, you could add a layout at `./pages/blog/_layout.tsx` to add a sidebar to both the blog index and all blog article pages.
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.js';
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
- 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.
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 a `value` property with two additional properties related to the current route: `path` (string) and `searchParams` ([URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)).
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 router = useRouter();
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 could also be generated programmatically.
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 <main>{children}</main>;
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
- For example, an image added to `./public/images/logo.svg` can be rendered via `<img src="/images/logo.svg" />`.
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.js';
690
- import { getArticle, getStaticPaths } from '../../lib/blog.js';
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's performance and scalability, but Waku should be compatible with all React state management libraries such as Zustand and Valtio.
743
+ We recommend [Jotai](https://jotai.org) for global React state management based on the atomic models performance and scalability, but Waku should be compatible with all React state management libraries such as Zustand and Valtio.
721
744
 
722
- 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.
745
+ Were exploring a deeper integration of atomic state management into Waku to achieve the performance and developer experience of signals while preserving Reacts declarative programming model.
723
746
 
724
747
  ## Environment variables
725
748
 
726
- It's important to distinguish environment variables that must be kept secret from those that can be made public.
749
+ Its 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' browsers.
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're seeking additional contributors. Check out our [roadmap](https://github.com/dai-shi/waku/issues/24) for more information.
887
+ Waku is in active development and were 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(process.env.PORT || '3000', 10);
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(process.env.PORT || '8080', 10);
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?: ((data: unknown) => void) | undefined) => Elements;
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
- const elements = use(elementsPromise);
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
- * Defaults to "main.tsx".
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 `.ts`, `.tsx` and `.jsx` in the development mode.
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-')) {
@@ -22,4 +22,5 @@ export declare function resolveConfig(config: Config): Promise<{
22
22
  default: import("./middleware/types.js").Middleware;
23
23
  }>[];
24
24
  }>;
25
+ export declare const EXTENSIONS: string[];
25
26
  export {};
@@ -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.tsx',
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
+ ];