@fastify/react 0.1.1 → 0.2.0-rc.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 CHANGED
@@ -1,663 +1,5 @@
1
1
  <br>
2
2
 
3
- # @fastify/react [![NPM version](https://img.shields.io/npm/v/@fastify/react.svg?style=flat)](https://www.npmjs.com/package/@fastify/react) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
3
+ **`@fastify/react`** is the official [**`@fastify/vite`**](https://fastify-vite.dev) renderer for React.
4
4
 
5
- **Fastify DX for React** (**`@fastify/react`**) is a renderer for [**@fastify/vite**](https://github.com/fastify/fastify-vite).
6
-
7
- It has an extremely small core (~1k LOC total) and is built on top of [Fastify](https://github.com/fastify/fastify), [Vite](https://vitejs.dev/), [React Router](https://reactrouter.com/docs/en/v6) and [Valtio](https://github.com/pmndrs/valtio).
8
-
9
- ## Quick Start
10
-
11
- Ensure you have **Node v16+**.
12
-
13
- Make a copy of [**starters/react**](https://github.com/fastify/fastify-dx/tree/dev/starters/react). If you have [`degit`](https://github.com/Rich-Harris/degit), run the following from a new directory:
14
-
15
- ```bash
16
- degit fastify/fastify-dx/starters/react
17
- ```
18
-
19
- > **If you're starting a project from scratch**, you'll need these packages installed.
20
- >
21
- > ```bash
22
- > npm i fastify @fastify/vite @fastify/react -P
23
- > npm i @vitejs/plugin-react -D
24
- > ```
25
-
26
-
27
- Run `npm install`.
28
-
29
- Run `npm run dev`.
30
-
31
- Visit `http://localhost:3000/`.
32
-
33
- ## What's Included
34
-
35
- That will get you a **starter template** with:
36
-
37
- - A minimal [Fastify](https://github.com/fastify/fastify) server.
38
- - Some dummy API routes.
39
- - A `pages/` folder with some [demo routes](https://github.com/fastify/fastify-dx/tree/dev/starters/react/client/pages).
40
- - All configuration files.
41
-
42
- It also includes some _**opinionated**_ essentials:
43
-
44
- - [**PostCSS Preset Env**](https://www.npmjs.com/package/postcss-preset-env) by [**Jonathan Neal**](https://github.com/jonathantneal), which enables [several modern CSS features](https://preset-env.cssdb.org/), such as [**CSS Nesting**](https://www.w3.org/TR/css-nesting-1/).
45
-
46
- - [**UnoCSS**](https://github.com/unocss/unocss) by [**Anthony Fu**](https://antfu.me/), which supports all [Tailwind utilities](https://uno.antfu.me/) and many other goodies through its [default preset](https://github.com/unocss/unocss/tree/main/packages/preset-uno).
47
-
48
- - [**Valtio**](https://github.com/pmndrs/valtio) by [**Daishi Kato**](https://blog.axlight.com/), with a global and SSR-ready store which you can use anywhere.
49
-
50
-
51
- ## Package Scripts
52
-
53
- `npm run dev` boots the development server.
54
-
55
- `npm run build` creates the production bundle.
56
-
57
- `npm run serve` serves the production bundle.
58
-
59
-
60
-
61
- ## Basic setup
62
-
63
- The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/react) follows [@fastify/vite](https://github.com/fastify/fastify-vite)'s convention of having a `client` folder with an `index.js` file, which is automatically resolved as your `clientModule` setting.
64
-
65
- If you want a flat directory setup, where server and client files are mixed together, you can manually set `clientModule` to something else. Note that in this case you'll also need to update `root` in your `vite.config.js` file.
66
-
67
- When deploying to production, bear in mind the `client/dist` directory, generated when you run `npm run build`, needs to be included. You'll also want to enable Fastify's [built-in logging](https://www.fastify.io/docs/latest/Reference/Logging/):
68
-
69
- ```js
70
- const server = Fastify({ logger: true })
71
- ```
72
-
73
- The starter template's `server.js` file:
74
-
75
- ```js
76
- import Fastify from 'fastify'
77
- import FastifyVite from '@fastify/vite'
78
- import FastifyReact from '@fastify/react'
79
-
80
- const server = Fastify()
81
-
82
- await server.register(FastifyVite, {
83
- root: import.meta.url,
84
- renderer: FastifyReact,
85
- })
86
-
87
- await server.vite.ready()
88
- await server.listen(3000)
89
- ```
90
-
91
- The starter template's [`vite.config.js`](https://github.com/fastify/fastify-dx/blob/main/starters/react/vite.config.js) file:
92
-
93
- ```js
94
- import viteReact from '@vitejs/plugin-react'
95
- import viteReactFastifyDX from '@fastify/react/plugin'
96
- import unocss from 'unocss/vite'
97
-
98
- const path = fileURLToPath(import.meta.url)
99
-
100
- const root = join(dirname(path), 'client')
101
- const plugins = [
102
- viteReact(),
103
- viteReactFastifyDX(),
104
- unocss()
105
- ]
106
-
107
- export default { root, plugins }
108
- ```
109
-
110
- Note that you only need to use Fastify DX's Vite plugin, which includes all functionality from [@fastify/vite](https://github.com/fastify/fastify-vite)'s Vite plugin.
111
-
112
- </td>
113
- </tr>
114
- </table>
115
-
116
-
117
- ## Project Structure
118
-
119
- The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/react) looks like this:
120
-
121
- ```
122
- ├── server.js
123
- ├── client/
124
- │ ├── index.js
125
- │ ├── context.js
126
- │ ├── root.jsx
127
- │ ├── index.html
128
- │ ├── layouts/
129
- │ │ ├── default.jsx
130
- │ │ └── auth.jsx
131
- │ └── pages/
132
- │ ├── index.jsx
133
- │ ├── client-only.jsx
134
- │ ├── server-only.jsx
135
- │ ├── streaming.jsx
136
- │ ├── using-data.jsx
137
- │ └── using-store.jsx
138
- ├── vite.config.js
139
- └── package.json
140
- ```
141
-
142
- Several internal files are provided as virtual modules by Fastify DX. They are located inside the `@fastify/react` package in `node_modules`, and dynamically loaded so you don't have to worry about them unless you want them overriden.
143
-
144
- In this case, placing a file with the same name as the registered virtual module in your Vite project root will override it. Find the detailed rundown of all virtual modules [here][virtual-modules].
145
-
146
- [virtual-modules]: https://github.com/fastify/fastify-dx/blob/main/docs/react/virtual-modules.md
147
-
148
- The `server.js` file is your application entry point. It's the file that runs everything. It boots a Fastify server configured with [**fastify-vite**](https://github.com/fastify/fastify-vite) and **Fastify DX for React** as a renderer adapter to **fastify-vite**.
149
-
150
- The `client/index.js` file is your Vite server entry point, it's the file that provides your client bundle (which runs in the Vite-enriched environment) to the Node.js environment where Fastify runs.
151
-
152
- > Right now, it's mostly a **boilerplate file** because it must exist but it will also probably never need to be changed.
153
-
154
- It exports your application's root React component (must be named `create`), the application routes (must be named `routes`) and the universal route context [initialization module](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md#initialization-module) (must be named `context` and have a dynamic module import so Fastify DX can pick up `default` and named exports).
155
-
156
- The `client/index.html` file is the [root HTML template of the application](https://vitejs.dev/guide/#index-html-and-project-root), which Vite uses as the client bundling entry point.
157
-
158
- > You can expand this file with additional `<meta>` and `<link>` tags if you wish, provided you don't remove any of the placeholders.
159
-
160
- This files links to `/dx:mount.js`, which is a virtual module provided by Fastify DX.
161
-
162
- Virtual modules are covered [here][virtual-modules].
163
-
164
- The `client/pages/` directory contains your route modules, whose paths are dynamically inferred from the directory structure itself. You can change this behavior easily. More on this [here][routing-config].
165
-
166
- [routing-config]: https://github.com/fastify/fastify-dx/blob/main/docs/react/routing-config.md
167
-
168
- The `client/layouts/` directory contains your route layout modules, which can be associated to any route. By default, `layouts/default.jsx` is used, but if you don't need to do any modifications on that file, you can safely removed as it's provided by Fastify DX in that case. The starter template also comes with `layouts/auth.jsx`, to demonstrate a more advanced use of layouts.
169
-
170
- [routing-config]: https://github.com/fastify/fastify-dx/blob/main/docs/react/routing-config.md
171
-
172
- The `client/context.js` file is the universal [route context][route-context] initialization module. Any named exports from this file are attached to the `RouteContext` class prototype on the server, preventing them from being reassigned on every request. The `default` export from this file, however, runs for every request so you can attach any request-specific data to it.
173
-
174
- [route-context]: https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md
175
-
176
-
177
- # Rendering modes
178
-
179
- Following the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md), Fastify DX's route modules can be set for universal rendering (SSR + CSR hydration, the default behavior), SSR in streaming mode, SSR only (client gets no JavaScript) or CSR only (SSR fully disabled).
180
-
181
- ## `streaming`
182
-
183
- If a route module exports `streaming` set to `true`, SSR will take place in **streaming mode**. That means if you have components depending on asynchronous resources and `<Suspense>` sections with defined fallback components, they will be streamed right way while the resources finish processing.
184
-
185
- ```jsx
186
- import React, { Suspense } from 'react'
187
-
188
- export const streaming = true
189
-
190
- export default function Index () {
191
- return (
192
- <Suspense fallback={<p>Waiting for content</p>}>
193
- <Message />
194
- </Suspense>
195
- )
196
- }
197
-
198
- function Message () {
199
- const message = afterSeconds({
200
- id: 'index',
201
- message: 'Delayed by Suspense API',
202
- seconds: 5
203
- })
204
- return <p>{message}</p>
205
- }
206
- ```
207
-
208
- [See the full example](https://github.com/fastify/fastify-dx/blob/main/starters/react/client/pages/streaming.jsx) in the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/react).
209
-
210
-
211
- ## `serverOnly`
212
-
213
- If a route module exports `serverOnly` set to `true`, only SSR will take place. The client gets the server-side rendered markup without any accompanying JavaScript or data hydration.
214
-
215
- You should use this setting to deliver lighter pages when there's no need to run any code on them, such as statically generated content sites.
216
-
217
- This differs from [React Server Components](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md), which are also supported, but whose server-only rendering is more granular (available for any route child component) and fully controlled by React.
218
-
219
- ```jsx
220
- export const serverOnly = true
221
-
222
- export function Index () {
223
- return <p>No JavaScript sent to the browser.</p>
224
- }
225
- ```
226
-
227
- [This example](https://github.com/fastify/fastify-dx/blob/main/starters/react/client/pages/server-only.jsx) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/react).
228
-
229
- ## `clientOnly`
230
-
231
- If a route module exports `clientOnly` set to `true`, no SSR will take place, only data fetching and data hydration. The client gets the empty container element (the one that wraps `<!-- element -->` in `index.html`) and all rendering takes place on the client only.
232
-
233
- You can use this setting to save server resources on internal pages where SSR makes no significant diference for search engines or UX in general, such as a password-protected admin section.
234
-
235
- This differs from [React Client Components](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md), which are also supported, but rendering is more granular (available for any route child component) and fully controlled by React.
236
-
237
- ```jsx
238
- export const clientOnly = true
239
-
240
- export function Index () {
241
- return <p>No pre-rendered HTML sent to the browser.</p>
242
- }
243
- ```
244
-
245
- [This example](https://github.com/fastify/fastify-dx/blob/main/starters/react/client/pages/client-only.jsx) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/react).
246
-
247
-
248
- ## Routing Configuration
249
-
250
- By default, routes are loaded from the `<project-root>/pages` folder, where `<project-root>` refers to the `root` setting in `vite.config.js`. The route paths are **dynamically inferred from the directory structure**, very much like Next.js and Nuxt.js.
251
-
252
- ### Dynamic parameters
253
-
254
- Dynamic route parameters follow the [Next.js convention](https://nextjs.org/docs/basic-features/pages#pages-with-dynamic-routes) (`[param]`), but that can be overriden by using the `paramPattern` plugin option. For example, this configuration switches the param pattern to the [Remix convention](https://remix.run/docs/en/v1/guides/routing#dynamic-segments) (`$param`).
255
-
256
- ```js
257
- // ...
258
- const plugins = [
259
- // ...
260
- viteReactFastifyDX({ paramPattern: /\$(\w+)/ }),
261
- ]
262
- ```
263
-
264
- ### Routes location
265
-
266
- You can also change the glob pattern used to determine where to route modules from.
267
-
268
- Since this setting is passed to [Vite's glob importers](https://vitejs.dev/guide/features.html#glob-import), the value needs to be a string:
269
-
270
- ```js
271
- // ...
272
- const plugins = [
273
- // ...
274
- viteReactFastifyDX({ globPattern: '/views/**/*.jsx' }),
275
- ]
276
- ```
277
-
278
- ### View modules
279
-
280
- You also can export a `path` constant from your route modules, in which case its value will be used to **override the dynamically inferred paths from the directory structure**.
281
-
282
- Additionally, [**you can provide your own routes**](https://github.com/fastify/fastify-dx/tree/dev/packages/fastify-react#dxroutesjs).
283
-
284
- ```jsx
285
- export const path = '/my-page'
286
-
287
- export defaut function MyPage () {
288
- return <p>Route with path export</p>
289
- }
290
- ```
291
-
292
-
293
- ## Isomorphic data prefetching
294
-
295
- The only way for the React runtime to execute asynchronous operations prior the rendering of a component is through the Suspense API. Fastify DX implements the `getData()` hook from the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md) to solve this problem.
296
-
297
- ### `getData(ctx)`
298
-
299
- This hook is set up in a way that it runs server-side before any SSR takes place, so any data fetched is made available to the route component before it starts rendering. During first render, any data retrieved on the server is automatically sent to be hydrated on the client so no new requests are made. Then, during client-side navigation (post first-render), a JSON request is fired to an endpoint automatically registered for running the `getData()` function for that route on the server.
300
-
301
- The objet returned by `getData()` gets automatically assigned as `data` in the [universal route context](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md) object and is accessible from `getMeta()` and `onEnter()` hooks and also via the `useRouteContext()` hook.
302
-
303
- ```jsx
304
- import { useRouteContext } from '/dx:core.jsx'
305
-
306
- export function getData (ctx) {
307
- return {
308
- message: 'Hello from getData!',
309
- }
310
- }
311
-
312
- export function Index () {
313
- const { data } = useRouteContext()
314
- return <p>{data.message}</p>
315
- }
316
- ```
317
-
318
-
319
- ## Route Layouts
320
-
321
- Fastify DX will automatically load layouts from the `layouts/` folder. By default, `/dx:layouts/default.jsx` is used — that is, if a project is missing a `layouts/defaults.jsx` file, the one provided by Fastify DX is used instead.
322
-
323
- See the section on [Virtual Modules](https://github.com/fastify/fastify-dx/blob/main/docs/react/virtual-modules.md) to learn more about this.
324
-
325
- You assign a layout to a route by exporting `layout`.
326
-
327
- See [`pages/using-auth.jsx`](https://github.com/fastify/fastify-dx/blob/main/starters/react/pages/using-auth.jsx) in the starter template:
328
-
329
- ```js
330
- export const layout = 'auth'
331
- ```
332
-
333
- That'll will cause the route to be wrapped in the layout component exported by [`layouts/auth.jsx`](https://github.com/fastify/fastify-dx/blob/main/starters/react/layouts/auth.jsx):
334
-
335
- ```jsx
336
- import { Suspense } from 'react'
337
- import { useRouteContext } from '/dx:core.jsx'
338
-
339
- export default function Auth ({ children }) {
340
- const { actions, state, snapshot } = useRouteContext()
341
- const authenticate = () => actions.authenticate(state)
342
- return (
343
- <Suspense>
344
- {snapshot.user.authenticated
345
- ? children
346
- : <Login onClick={() => authenticate()} /> }
347
- </Suspense>
348
- )
349
- }
350
-
351
- function Login ({ onClick }) {
352
- return (
353
- <>
354
- <p>This route needs authentication.</p>
355
- <button onClick={onClick}>
356
- Click this button to authenticate.
357
- </button>
358
- </>
359
- )
360
- }
361
- ```
362
-
363
- Note that like routes, it has access to `useRouteContext()`.
364
-
365
- ## Route Context
366
-
367
- ### Initialization module
368
-
369
- The starter template includes a sample `context.js` file. This file is optional and can be safely removed. If it's present, Fastify DX automatically loads it and uses it to do any RouteContext extensions or data injections you might need. If you're familiar with [Nuxt.js](https://nuxtjs.org/), you can think of it as a [Nuxt.js plugin](https://nuxtjs.org/docs/directory-structure/plugins/).
370
-
371
- **Consuming the route context:**
372
-
373
- ```jsx
374
- import {
375
- useRouteContext
376
- } from '/dx:core.jsx'
377
-
378
- // ...
379
- const {
380
- state,
381
- actions
382
- } = useRouteContext()
383
-
384
- // ...
385
- actions.addTodoItem(state, value)
386
- ```
387
-
388
- See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/react/client/pages/using-store.jsx) in the starter template.
389
-
390
- This example demonstrates how to use it to set up an universally available (SSR and CSR) `$fetch` function (using [`ky-universal`](https://www.npmjs.com/package/ky-universal)) and also export some store actions. They're all made available by `useRouteContext()`, covered next.
391
-
392
- ```js
393
- import ky from 'ky-universal'
394
-
395
- export default (ctx) => {
396
- if (ctx.server) {
397
- // Populate state.todoList on the server
398
- ctx.state.todoList = ctx.server.db.todoList
399
- // It'll get automatically serialized to the client on first render!
400
- }
401
- }
402
-
403
- export const $fetch = ky.extend({
404
- prefixUrl: 'http://localhost:3000'
405
- })
406
-
407
- // Must be a function so each request can have its own state
408
- export const state = () => ({
409
- todoList: null,
410
- })
411
-
412
- export const actions = {
413
- async addTodoItem (state, item) {
414
- await $fetch.put('api/todo/items', {
415
- json: { item },
416
- })
417
- state.todoList.push(item)
418
- },
419
- }
420
- ```
421
-
422
- See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/react/client/context.js) in the starter template.
423
-
424
- ### The `useRouteContext()` hook
425
-
426
- This hook can be used in any React component to retrieve a reference to the current route context. It's modelled after the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md), with still some rough differences and missing properties in this **alpha release**.
427
-
428
- By default, It includes reference to `data` — which is automatically populated if you use the `getData()` function, and `state` and `snapshot` — which hold references to the global [Valtio](https://github.com/pmndrs/valtio) state proxy and state snapshot object (returned by Valtio's `useSnapshot()`).
429
-
430
- It automatically causes the component to be [suspended](https://17.reactjs.org/docs/concurrent-mode-suspense.html) if the `getData()`, `getMeta()` and `onEnter()` functions are asynchronous.
431
-
432
- ```jsx
433
- import { useRouteContext } from '/dx:core.jsx'
434
-
435
- export function Index () {
436
- const { data } = useRouteContext()
437
- return <p>{data.message}</p>
438
- }
439
- ```
440
-
441
- ### Execution order
442
-
443
- This graph illustrates the execution order to expect from route context initialization.
444
-
445
- ```
446
- context.js default function export
447
- └─ getData() function export
448
- └─ getMeta() function export
449
- └─ onEnter() function export
450
- └─ Route module
451
- ```
452
-
453
- First the `default` function export from `context.js` (if present) is executed. This is where you can manually feed global server data into your application by populating the global Valtio state (the route context's `state` property, which is automatically hydrated on the client.
454
-
455
- Then `getData()` runs — which populates the route context's `data` property, and is also automatically hydrated on the client. Then `getMeta()`, which populates the route context's `head` property. Then `onEnter()`, and finally your route component.
456
-
457
-
458
- ## Universal Route Enter Event
459
-
460
- ### `onEnter(ctx)`
461
-
462
- If a route module exports a `onEnter()` function, it's executed before the route renders, both in SSR and client-side navigation. That is, the first time a route render on the server, onEnter() runs on the server. Then, since it already ran on the server, it doesn't run again on the client for that first route. But if you navigate to another route on the client using `<Link>`, it runs normally as you'd expect.
463
-
464
- It receives the [universal route context][route-context] as first parameter, so you can make changes to `data`, `meta` and `state` if needed.
465
-
466
- [route-context]: https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md
467
-
468
- ```jsx
469
- export function onEnter (ctx) {
470
- if (ctx.server?.underPressure) {
471
- ctx.clientOnly = true
472
- }
473
- }
474
-
475
- export function Index () {
476
- return <p>No pre-rendered HTML sent to the browser.</p>
477
- }
478
- ```
479
-
480
- The example demonstrates how to turn off SSR and downgrade to CSR-only, assuming you have a `pressureHandler` configured in [`underpressure`](https://github.com/fastify/under-pressure) to set a `underPressure` flag on your server instance.
481
-
482
-
483
- ## Meta Tags
484
-
485
- Following the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md), Fastify DX renders `<head>` elements independently from the SSR phase. This allows you to fetch data for populating the first `<meta>` tags and stream them right away to the client, and only then perform SSR.
486
-
487
- > Additional `<link>` preload tags can be produced from the SSR phase. This is **not currently implemented** in this **alpha release** but is a planned feature. If you can't wait for it, you can roll out your own (and perhaps contribute your solution) by providing your own [`createHtmlFunction()`](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/index.js#L57) to [@fastify/vite](https://github.com/fastify/fastify-vite).
488
-
489
- ### `getMeta()`
490
-
491
- To populate `<title>`, `<meta>` and `<link>` elements, export a `getMeta()` function that returns an object matching the format expected by [unihead](https://github.com/galvez/unihead), the underlying library used by Fastify DX.
492
-
493
- It receives the [route context](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/README.md#route-context) as first parameter and runs after `getData()`, allowing you to access any `data` populated by these other functions to generate your tags.
494
-
495
- ```jsx
496
- export function getMeta (ctx) {
497
- return {
498
- title: 'Route Title',
499
- meta: [
500
- { name: 'twitter:title', value: 'Route Title' },
501
- ]
502
- }
503
- }
504
-
505
- export function Index () {
506
- return <p>Route with meta tags.</p>
507
- }
508
- ```
509
-
510
-
511
- ## Virtual Modules
512
-
513
- **Fastify DX** relies on [virtual modules](https://github.com/rollup/plugins/tree/master/packages/virtual) to save your project from having too many boilerplate files. Virtual modules are a [Rollup](https://rollupjs.org/guide/en/) feature exposed and fully supported by [Vite](https://vitejs.dev/). When you see imports that start with `/dx:`, you know a Fastify DX virtual module is being used.
514
-
515
- Fastify DX virtual modules are **fully ejectable**. For instance, the starter template relies on the `/dx:root.jsx` virtual module to provide the React Router shell of your application. If you copy the `root.jsx` file [from the @fastify/react package](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/root.jsx) and place it your Vite project root, **that copy of the file is used instead**. In fact, the starter template already comes with a custom `root.jsx` of its own to include UnoCSS.
516
-
517
- Aside from `root.jsx`, the starter template comes with two other virtual modules already ejected and part of the local project — `context.js` and `layouts/default.jsx`. If you don't need to customize them, you can safely removed them from your project.
518
-
519
- ### `/dx:root.jsx`
520
-
521
- This is the root React component. It's used internally by `/dx:create.jsx` and provided as part of the starter template. You can use this file to add a common layout to all routes, or just use it to add additional, custom context providers.
522
-
523
- The version provided as part of the starter template includes [UnoCSS](https://github.com/unocss/unocss)'s own virtual module import, necessary to enable its CSS engine.
524
-
525
- ```jsx
526
- import 'virtual:uno.css'
527
- import { Suspense } from 'react'
528
- import { DXApp } from '/dx:core.jsx'
529
-
530
- export default function Root ({ url, serverInit }) {
531
- return (
532
- <Suspense>
533
- <DXApp url={url} {...serverInit} />
534
- </Suspense>
535
- )
536
- }
537
- ```
538
-
539
- Note that a top-level `<Suspense>` wrapper is necessary because Fastify DX has code-splitting enabled at the route-level. You can opt out of code-splitting by providing your own `routes.js` file, but that's very unlikely to be ever required for any reason.
540
-
541
- ### `/dx:routes.js`
542
-
543
- Fastify DX has **code-splitting** out of the box. It does that by eagerly loading all route data on the server, and then hydrating any missing metadata on the client. That's why the routes module default export is conditioned to `import.meta.env.SSR`, and different helper functions are called for each rendering environment.
544
-
545
- ```js
546
- export default import.meta.env.SSR
547
- ? createRoutes(import.meta.globEager('$globPattern'))
548
- : hydrateRoutes(import.meta.glob('$globPattern'))
549
- ```
550
-
551
- See [the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/routes.js) for the `createRoutes()` and `hydrateRoutes()` definitions.
552
-
553
- If you want to use your own custom routes list, you must eject this file as-is and replace the glob imports with your own routes list:
554
-
555
- ```js
556
- const routes = [
557
- {
558
- path: '/',
559
- component: () => import('/custom/index.jsx'),
560
- }
561
- ]
562
-
563
- export default import.meta.env.SSR
564
- ? createRoutes(routes)
565
- : hydrateRoutes(routes)
566
- ````
567
-
568
- ### `/dx:core.jsx`
569
-
570
- Implements `useRouteContext()`, `DXApp` and `DXRoute`.
571
-
572
- `DXApp` is imported by `root.jsx` and encapsulates Fastify DX's route component API.
573
-
574
- > React Router's [nested routes](https://reactrouter.com/docs/en/v6/getting-started/concepts#nested-routes) aren't supported yet.
575
-
576
- See its full definition [here](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/core.jsx).
577
-
578
- ### `/dx:create.jsx`
579
-
580
- This virtual module creates your root React component.
581
-
582
- This is where `root.jsx` is imported.
583
-
584
- <b>You'll rarely need to customize this file.</b>
585
-
586
- ```jsx
587
- import Root from '/dx:root.jsx'
588
-
589
- export default function create ({ url, ...serverInit }) {
590
- return (
591
- <Root url={url} serverInit={serverInit} />
592
- )
593
- }
594
- ```
595
-
596
- What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/create.jsx).
597
-
598
- ### `/dx:layouts.js`
599
-
600
- This is responsible for loading **layout components**. It's used internally by `/dx:core.jsx`. If a project has no `layouts/default.jsx` file, the default one from Fastify DX is used. This virtual module works in conjunction with the `/dx:layouts/` virtual module which provides exports from the `/layouts` folder.
601
-
602
- <b>You'll rarely need to customize this file.</b>
603
-
604
- ```jsx
605
- import { lazy } from 'react'
606
-
607
- const DefaultLayout = () => import('/dx:layouts/default.jsx')
608
-
609
- const appLayouts = import.meta.glob('/layouts/*.jsx')
610
-
611
- appLayouts['/layouts/default.jsx'] ??= DefaultLayout
612
-
613
- export default Object.fromEntries(
614
- Object.keys(appLayouts).map((path) => {
615
- const name = path.slice(9, -4)
616
- return [name, lazy(appLayouts[path])]
617
- })
618
- )
619
- ```
620
-
621
- What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/layouts.js).
622
-
623
- ### `/dx:mount.js`
624
-
625
- This is the file `index.html` links to by default. It sets up the application with an `unihead` instance for head management, the initial route context, and provides the conditional mounting logic to defer to CSR-only if `clientOnly` is enabled.
626
-
627
- <b>You'll rarely need to customize this file.</b>
628
-
629
- [See the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/mount.js) for the `mount()` function definition.
630
-
631
- ### `/dx:resource.js`
632
-
633
- Provides the `waitResource()` and `waitFetch()` data fetching helpers implementing the [Suspense API](https://17.reactjs.org/docs/concurrent-mode-suspense.html). They're used by `/dx:core.jsx`.
634
-
635
- <b>You'll rarely need to customize this file.</b>
636
-
637
- See its full definition [here](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-react/virtual/resource.js).
638
-
639
- ## Maintainance
640
-
641
- Created and maintained by [Jonas Galvez](https://github.com/sponsors/galvez), **Principal Engineer** and **Open Sourcerer** at [NodeSource](https://nodesource.com).
642
-
643
- New contributors are extremely welcome to look for [good first issues](https://github.com/fastify/fastify-dx/labels/good%20first%20issue).
644
-
645
- ## Gold Sponsors
646
-
647
- <a href="https://nodesource.com"><img width="200px" src="https://user-images.githubusercontent.com/12291/206885948-3fa742a2-1057-4db2-8648-46f5cb673461.svg"></a>
648
-
649
- [Contact me](mailto:jonasgalvez@gmail.com) to add your company's logo here.
650
-
651
- ## GitHub Sponsors
652
-
653
- - [**Duc-Thien Bui**](https://github.com/aecea)
654
- - [**Tom Preston-Werner**](https://github.com/mojombo)
655
- - [**Clifford Fajardo**](https://github.com/cliffordfajardo)
656
- - [**David Adam Coffey**](https://github.com/dacoffey)
657
- - [**Mezereon**](https://github.com/mezereon-co)
658
- - [**A-J Roos**](https://github.com/Asjas)
659
- - [**James Isaacs**](https://github.com/jamesisaacs2)
660
-
661
- [**Click here**](https://github.com/sponsors/galvez) to add your name to this list.
662
-
663
- _Thank you!_
5
+ See the [documentation suite](https://fastify-vite.dev) to learn more.
package/index.js CHANGED
@@ -4,11 +4,11 @@ import { Readable } from 'stream'
4
4
  // fastify-vite's minimal HTML templating function,
5
5
  // which extracts interpolation variables from comments
6
6
  // and returns a function with the generated code
7
- import { createHtmlTemplateFunction } from '@fastify/vite'
7
+ import { createHtmlTemplateFunction } from '@fastify/vite/utils'
8
8
 
9
9
  // Used to safely serialize JavaScript into
10
10
  // <script> tags, preventing a few types of attack
11
- import devalue from 'devalue'
11
+ import * as devalue from 'devalue'
12
12
 
13
13
  // Small SSR-ready library used to generate
14
14
  // <title>, <meta> and <link> elements
@@ -75,10 +75,10 @@ export function createHtmlFunction (source, scope, config) {
75
75
  ...!context.serverOnly && {
76
76
  hydration: (
77
77
  '<script>\n' +
78
- `window.route = ${devalue(context.toJSON())}\n` +
79
- `window.routes = ${devalue(routes.toJSON())}\n` +
78
+ `window.route = ${devalue.uneval(context.toJSON())}\n` +
79
+ `window.routes = ${devalue.uneval(routes.toJSON())}\n` +
80
80
  '</script>'
81
- )
81
+ ),
82
82
  },
83
83
  }),
84
84
  }))
package/package.json CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
- "scripts": {
3
- "lint": "eslint . --ext .js,.jsx --fix"
4
- },
5
2
  "type": "module",
6
3
  "main": "index.js",
7
4
  "name": "@fastify/react",
8
- "version": "0.1.1",
5
+ "description": "The official @fastify/vite renderer for React",
6
+ "version": "0.2.0-rc.1",
9
7
  "files": [
10
8
  "virtual/create.jsx",
11
9
  "virtual/create.tsx",
@@ -31,27 +29,32 @@
31
29
  "./plugin": "./plugin.cjs"
32
30
  },
33
31
  "dependencies": {
34
- "devalue": "^2.0.1",
35
- "history": "^5.3.0",
36
- "minipass": "^3.3.4",
32
+ "devalue": "latest",
33
+ "history": "latest",
34
+ "minipass": "latest",
37
35
  "react": "^18.2.0",
38
- "react-dom": "^18.2.0",
39
- "react-router-dom": "^6.4.3",
40
- "unihead": "^0.0.6",
41
- "valtio": "^1.7.2"
36
+ "react-dom": "latest",
37
+ "react-router-dom": "latest",
38
+ "unihead": "latest",
39
+ "valtio": "latest"
42
40
  },
43
41
  "devDependencies": {
44
- "@babel/eslint-parser": "^7.16.0",
45
- "@babel/preset-react": "^7.16.0",
46
- "@vitejs/plugin-react": "^2.2.0",
47
- "eslint": "^7.32.0",
48
- "eslint-config-standard": "^16.0.2",
49
- "eslint-plugin-import": "^2.22.1",
50
- "eslint-plugin-node": "^11.1.0",
51
- "eslint-plugin-promise": "^4.3.1",
52
- "eslint-plugin-react": "^7.29.4"
42
+ "@babel/eslint-parser": "latest",
43
+ "@babel/preset-react": "latest",
44
+ "eslint": "latest",
45
+ "eslint-config-standard": "latest",
46
+ "eslint-plugin-import": "latest",
47
+ "eslint-plugin-node": "latest",
48
+ "eslint-plugin-promise": "latest",
49
+ "eslint-plugin-react": "latest"
50
+ },
51
+ "peerDependencies": {
52
+ "@fastify/vite": "^5.0.2"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public"
56
+ },
57
+ "scripts": {
58
+ "lint": "eslint . --ext .js,.jsx --fix"
56
59
  }
57
- }
60
+ }
package/plugin.cjs CHANGED
@@ -3,7 +3,7 @@ const { dirname, join, resolve } = require('path')
3
3
  const { fileURLToPath } = require('url')
4
4
 
5
5
  function viteReactFastifyDX (config = {}) {
6
- const prefix = /^\/?dx:/
6
+ const prefix = /^\/:/
7
7
  const routing = Object.assign({
8
8
  globPattern: '/pages/**/*.(jsx|tsx)',
9
9
  paramPattern: /\[(\w+)\]/,
@@ -11,18 +11,13 @@ function viteReactFastifyDX (config = {}) {
11
11
  const virtualRoot = resolve(__dirname, 'virtual')
12
12
  const virtualModules = [
13
13
  'mount.js',
14
- 'mount.ts',
15
14
  'resource.js',
16
- 'resource.ts',
17
15
  'routes.js',
18
16
  'layouts.js',
19
17
  'create.jsx',
20
- 'create.tsx',
21
18
  'root.jsx',
22
- 'root.tsx',
23
19
  'layouts/',
24
20
  'context.js',
25
- 'context.ts',
26
21
  'core.jsx'
27
22
  ]
28
23
  virtualModules.includes = function (virtual) {
@@ -79,7 +74,7 @@ function viteReactFastifyDX (config = {}) {
79
74
  }
80
75
 
81
76
  return {
82
- name: 'vite-plugin-react-fastify-dx',
77
+ name: 'vite-plugin-fastify-react',
83
78
  config (config, { command }) {
84
79
  if (command === 'build' && config.build?.ssr) {
85
80
  config.build.rollupOptions = {
package/server/context.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  const routeContextInspect = Symbol.for('nodejs.util.inspect.custom')
3
2
 
4
3
  export default class RouteContext {
package/server/stream.js CHANGED
@@ -1,7 +1,6 @@
1
-
2
1
  // Helper to make the stream returned renderToPipeableStream()
3
2
  // behave like an event emitter and facilitate error handling in Fastify
4
- import Minipass from 'minipass'
3
+ import { Minipass } from 'minipass'
5
4
 
6
5
  // React 18's preferred server-side rendering function,
7
6
  // which enables the combination of React.lazy() and Suspense
package/virtual/core.jsx CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createContext, useContext, useEffect } from 'react'
2
- import { useLocation, BrowserRouter, Routes, Route } from 'react-router-dom'
2
+ import { useLocation, BrowserRouter } from 'react-router-dom'
3
3
  import { StaticRouter } from 'react-router-dom/server.mjs'
4
4
  import { createPath } from 'history'
5
5
  import { proxy, useSnapshot } from 'valtio'
6
- import { waitResource, waitFetch } from '/dx:resource.js'
7
- import layouts from '/dx:layouts.js'
6
+ import { waitResource, waitFetch } from '/:resource.js'
7
+ import layouts from '/:layouts.js'
8
8
 
9
9
  export const isServer = import.meta.env.SSR
10
10
  export const Router = isServer ? StaticRouter : BrowserRouter
@@ -14,41 +14,13 @@ export function useRouteContext () {
14
14
  const routeContext = useContext(RouteContext)
15
15
  if (routeContext.state) {
16
16
  routeContext.snapshot = isServer
17
- ? routeContext.state
18
- : useSnapshot(routeContext.state)
17
+ ? routeContext.state ?? {}
18
+ : useSnapshot(routeContext.state ?? {})
19
19
  }
20
20
  return routeContext
21
21
  }
22
22
 
23
- export function DXApp ({
24
- url,
25
- routes,
26
- head,
27
- routeMap,
28
- ctxHydration,
29
- }) {
30
- return (
31
- <Router location={url}>
32
- <Routes>{
33
- routes.map(({ path, component: Component }) =>
34
- <Route
35
- key={path}
36
- path={path}
37
- element={
38
- <DXRoute
39
- head={head}
40
- ctxHydration={ctxHydration}
41
- ctx={routeMap[path]}>
42
- <Component />
43
- </DXRoute>
44
- } />,
45
- )
46
- }</Routes>
47
- </Router>
48
- )
49
- }
50
-
51
- export function DXRoute ({ head, ctxHydration, ctx, children }) {
23
+ export function AppRoute ({ head, ctxHydration, ctx, children }) {
52
24
  // If running on the server, assume all data
53
25
  // functions have already ran through the preHandler hook
54
26
  if (isServer) {
@@ -58,8 +30,8 @@ export function DXRoute ({ head, ctxHydration, ctx, children }) {
58
30
  ...ctx,
59
31
  ...ctxHydration,
60
32
  state: isServer
61
- ? ctxHydration.state
62
- : proxy(ctxHydration.state),
33
+ ? ctxHydration.state ?? {}
34
+ : proxy(ctxHydration.state ?? {}),
63
35
  }}>
64
36
  <Layout>
65
37
  {children}
@@ -134,8 +106,8 @@ export function DXRoute ({ head, ctxHydration, ctx, children }) {
134
106
  ...ctxHydration,
135
107
  ...ctx,
136
108
  state: isServer
137
- ? ctxHydration.state
138
- : proxy(ctxHydration.state),
109
+ ? ctxHydration.state ?? {}
110
+ : proxy(ctxHydration.state ?? {}),
139
111
  }}>
140
112
  <Layout>
141
113
  {children}
@@ -1,4 +1,4 @@
1
- import Root from '/dx:root.jsx'
1
+ import Root from '/:root.jsx'
2
2
 
3
3
  export default function create ({ url, ...serverInit }) {
4
4
  return (
@@ -1,6 +1,6 @@
1
1
  import { lazy } from 'react'
2
2
 
3
- const DefaultLayout = () => import('/dx:layouts/default.jsx')
3
+ const DefaultLayout = () => import('/:layouts/default.jsx')
4
4
 
5
5
  const appLayouts = import.meta.glob('/layouts/*.jsx')
6
6
 
package/virtual/mount.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import Head from 'unihead/client'
2
2
  import { createRoot, hydrateRoot } from 'react-dom/client'
3
3
 
4
- import create from '/dx:create.jsx'
5
- import routesPromise from '/dx:routes.js'
4
+ import create from '/:create.jsx'
5
+ import routesPromise from '/:routes.js'
6
6
 
7
7
  mount('main')
8
8
 
@@ -10,7 +10,7 @@ async function mount (target) {
10
10
  if (typeof target === 'string') {
11
11
  target = document.querySelector(target)
12
12
  }
13
- const context = await import('/dx:context.js')
13
+ const context = await import('/:context.js')
14
14
  const ctxHydration = await extendContext(window.route, context)
15
15
  const head = new Head(window.route.head, window.document)
16
16
  const resolvedRoutes = await routesPromise
@@ -1,4 +1,3 @@
1
-
2
1
  const fetchMap = new Map()
3
2
  const resourceMap = new Map()
4
3
 
package/virtual/root.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Suspense } from 'react'
2
2
  import { Routes, Route } from 'react-router-dom'
3
- import { Router, DXRoute } from '/dx:core.jsx'
3
+ import { Router, AppRoute } from '/:core.jsx'
4
4
 
5
5
  export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
6
6
  return (
@@ -12,16 +12,16 @@ export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
12
12
  key={path}
13
13
  path={path}
14
14
  element={
15
- <DXRoute
15
+ <AppRoute
16
16
  head={head}
17
17
  ctxHydration={ctxHydration}
18
18
  ctx={routeMap[path]}>
19
19
  <Component />
20
- </DXRoute>
20
+ </AppRoute>
21
21
  } />,
22
22
  )
23
23
  }</Routes>
24
24
  </Router>
25
25
  </Suspense>
26
26
  )
27
- }
27
+ }
@@ -1,4 +0,0 @@
1
- // This file serves as a placeholder
2
- // if no context.js file is provided
3
-
4
- export default () => {}
@@ -1,7 +0,0 @@
1
- import Root from '/dx:root.tsx'
2
-
3
- export default function create ({ url, ...serverInit }) {
4
- return (
5
- <Root url={url} {...serverInit} />
6
- )
7
- }
package/virtual/mount.ts DELETED
@@ -1,47 +0,0 @@
1
- import Head from 'unihead/client'
2
- import { createRoot, hydrateRoot } from 'react-dom/client'
3
-
4
- import create from '/dx:create.tsx'
5
- import routesPromise from '/dx:routes.js'
6
-
7
- mount('main')
8
-
9
- async function mount (target) {
10
- if (typeof target === 'string') {
11
- target = document.querySelector(target)
12
- }
13
- const context = await import('/dx:context.ts')
14
- const ctxHydration = await extendContext(window.route, context)
15
- const head = new Head(window.route.head, window.document)
16
- const resolvedRoutes = await routesPromise
17
- const routeMap = Object.fromEntries(
18
- resolvedRoutes.map((route) => [route.path, route]),
19
- )
20
-
21
- const app = create({
22
- head,
23
- ctxHydration,
24
- routes: window.routes,
25
- routeMap,
26
- })
27
- if (ctxHydration.clientOnly) {
28
- createRoot(target).render(app)
29
- } else {
30
- hydrateRoot(target, app)
31
- }
32
- }
33
-
34
- async function extendContext (ctx, {
35
- // The route context initialization function
36
- default: setter,
37
- // We destructure state here just to discard it from extra
38
- state,
39
- // Other named exports from context.js
40
- ...extra
41
- }) {
42
- Object.assign(ctx, extra)
43
- if (setter) {
44
- await setter(ctx)
45
- }
46
- return ctx
47
- }
package/virtual/root.tsx DELETED
@@ -1,27 +0,0 @@
1
- import { Suspense } from 'react'
2
- import { Routes, Route } from 'react-router-dom'
3
- import { Router, DXRoute } from '/dx:core.jsx'
4
-
5
- export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
6
- return (
7
- <Suspense>
8
- <Router location={url}>
9
- <Routes>{
10
- routes.map(({ path, component: Component }) =>
11
- <Route
12
- key={path}
13
- path={path}
14
- element={
15
- <DXRoute
16
- head={head}
17
- ctxHydration={ctxHydration}
18
- ctx={routeMap[path]}>
19
- <Component />
20
- </DXRoute>
21
- } />,
22
- )
23
- }</Routes>
24
- </Router>
25
- </Suspense>
26
- )
27
- }