├── starters ├── solid │ ├── .eslintignore │ ├── client │ │ ├── layouts │ │ │ ├── default.jsx │ │ │ └── auth.jsx │ │ ├── index.html │ │ ├── fetch.js │ │ ├── index.js │ │ ├── pages │ │ │ ├── client-only.jsx │ │ │ ├── server-only.jsx │ │ │ ├── streaming.jsx │ │ │ ├── using-store.jsx │ │ │ ├── using-auth.jsx │ │ │ ├── using-data.jsx │ │ │ └── index.jsx │ │ ├── root.jsx │ │ ├── base.css │ │ ├── context.js │ │ └── assets │ │ │ └── logo.svg │ ├── postcss.config.cjs │ ├── vite.config.js │ ├── .eslintrc │ ├── server.js │ └── package.json └── svelte │ ├── .eslintignore │ ├── postcss.config.cjs │ ├── client │ ├── layouts │ │ ├── default.svelte │ │ └── auth.svelte │ ├── index.js │ ├── index.html │ ├── fetch.js │ ├── pages │ │ ├── client-only.svelte │ │ ├── server-only.svelte │ │ ├── using-store.svelte │ │ ├── using-auth.svelte │ │ ├── using-data.svelte │ │ └── index.svelte │ ├── root.svelte │ ├── base.css │ ├── context.js │ └── assets │ │ └── logo.svg │ ├── .eslintrc │ ├── vite.config.js │ ├── server.js │ └── package.json ├── .gitignore ├── blog-post.pdf ├── .gitattributes ├── package.json ├── packages ├── fastify-solid │ ├── virtual │ │ ├── context.js │ │ ├── context.ts │ │ ├── layouts │ │ │ └── default.jsx │ │ ├── create.jsx │ │ ├── create.tsx │ │ ├── layouts.js │ │ ├── core.js │ │ ├── root.jsx │ │ ├── root.tsx │ │ ├── mount.js │ │ ├── mount.ts │ │ ├── route.jsx │ │ └── routes.js │ ├── server │ │ ├── stream.js │ │ └── context.js │ ├── .eslintrc │ ├── package.json │ ├── plugin.cjs │ ├── index.js │ └── README.md └── fastify-svelte │ ├── virtual │ ├── context.js │ ├── context.ts │ ├── layouts │ │ └── default.svelte │ ├── layouts.js │ ├── root.svelte │ ├── core.js │ ├── mount.js │ ├── route.svelte │ └── routes.js │ ├── server │ ├── stream.js │ └── context.js │ ├── .eslintrc │ ├── package.json │ ├── mount.ts │ ├── plugin.cjs │ ├── index.js │ └── README.md ├── README.md ├── prerelease.mjs ├── ALPHA.md ├── devinstall.mjs └── URMA.md /starters/solid/.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /starters/svelte/.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist 4 | .DS_Store -------------------------------------------------------------------------------- /blog-post.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastify/fastify-dx/HEAD/blog-post.pdf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^4.7.2", 4 | "zx": "^6.1.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/context.js: -------------------------------------------------------------------------------- 1 | // This file serves as a placeholder 2 | // if no context.js file is provided 3 | 4 | export default () => {} 5 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/context.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a placeholder 2 | // if no context.js file is provided 3 | 4 | export default () => {} 5 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/context.js: -------------------------------------------------------------------------------- 1 | // This file serves as a placeholder 2 | // if no context.js file is provided 3 | 4 | export default () => {} 5 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/context.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a placeholder 2 | // if no context.js file is provided 3 | 4 | export default () => {} 5 | -------------------------------------------------------------------------------- /starters/solid/client/layouts/default.jsx: -------------------------------------------------------------------------------- 1 | export default function Default (props) { 2 | return ( 3 |
This route needs authentication.
9 | 12 | {:else} 13 |This route is rendered on the client only!
16 |17 | Go back to the index 18 |
19 |⁂
20 |When this route is rendered on the server, no SSR takes place.
21 |See the output of curl http://localhost:3000/client-only.
This route is rendered on the server only!
16 |17 | Go back to the index 18 |
19 |⁂
20 |When this route is rendered on the server, no JavaScript is sent to the client.
21 |See the output of curl http://localhost:3000/server-only.
This route is rendered on the client only!
15 |16 | Go back to the index 17 |
18 |⁂
19 |When this route is rendered on the server, no SSR takes place.
20 |See the output of curl http://localhost:3000/client-only.
This route is rendered on the server only!
15 |16 | Go back to the index 17 |
18 |⁂
19 |When this route is rendered on the server, no JavaScript is sent to the client.
20 |See the output of curl http://localhost:3000/server-only.
This route needs authentication.
21 | 24 | > 25 | ) 26 | } -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/core.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'solid-js' 2 | 3 | // Solid already exports isServer, but this 4 | // is for consistency with the other implementations 5 | export const isServer = import.meta.env.SSR 6 | export const RouteContext = createContext() 7 | 8 | export function useRouteContext () { 9 | return useContext(RouteContext) 10 | } 11 | 12 | export async function jsonDataFetch (path) { 13 | const response = await fetch(`/-/data${path}`) 14 | let data 15 | let error 16 | try { 17 | data = await response.json() 18 | } catch (err) { 19 | error = err 20 | } 21 | if (data?.statusCode === 500) { 22 | throw new Error(data.message) 23 | } 24 | if (error) { 25 | throw error 26 | } 27 | return data 28 | } 29 | -------------------------------------------------------------------------------- /starters/solid/server.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import FastifyVite from '@fastify/vite' 3 | import FastifySolid from '@fastify/solid' 4 | 5 | const server = Fastify() 6 | 7 | await server.register(FastifyVite, { 8 | root: import.meta.url, 9 | renderer: FastifySolid, 10 | }) 11 | 12 | await server.vite.ready() 13 | 14 | server.decorate('db', { 15 | todoList: [ 16 | 'Do laundry', 17 | 'Respond to emails', 18 | 'Write report', 19 | ] 20 | }) 21 | 22 | server.put('/api/todo/items', (req, reply) => { 23 | server.db.todoList.push(req.body) 24 | reply.send({ ok: true }) 25 | }) 26 | 27 | server.delete('/api/todo/items', (req, reply) => { 28 | server.db.todoList.splice(req.body, 1) 29 | reply.send({ ok: true }) 30 | }) 31 | 32 | await server.listen({ port: 3000 }) 33 | -------------------------------------------------------------------------------- /starters/svelte/server.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import FastifyVite from '@fastify/vite' 3 | import FastifySvelte from '@fastify/svelte' 4 | 5 | const server = Fastify() 6 | 7 | await server.register(FastifyVite, { 8 | root: import.meta.url, 9 | renderer: FastifySvelte, 10 | }) 11 | 12 | await server.vite.ready() 13 | 14 | server.decorate('db', { 15 | todoList: [ 16 | 'Do laundry', 17 | 'Respond to emails', 18 | 'Write report', 19 | ], 20 | }) 21 | 22 | server.put('/api/todo/items', (req, reply) => { 23 | console.log('!') 24 | server.db.todoList.push(req.body) 25 | reply.send({ ok: true }) 26 | }) 27 | 28 | server.delete('/api/todo/items', (req, reply) => { 29 | server.db.todoList.splice(req.body, 1) 30 | reply.send({ ok: true }) 31 | }) 32 | 33 | await server.listen({ port: 3000 }) 34 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/core.js: -------------------------------------------------------------------------------- 1 | import { getContext } from 'svelte' 2 | import { useSnapshot } from 'sveltio' 3 | 4 | export const isServer = import.meta.env.SSR 5 | export const routeContext = Symbol('routeContext') 6 | 7 | export function useRouteContext () { 8 | const { 9 | routeContext: ctx, 10 | state, 11 | actions 12 | } = getContext(routeContext) 13 | ctx.state = state 14 | ctx.actions = actions 15 | ctx.snapshot = useSnapshot(state) 16 | return ctx 17 | } 18 | 19 | export async function jsonDataFetch (path) { 20 | const response = await fetch(`/-/data${path}`) 21 | let data 22 | let error 23 | try { 24 | data = await response.json() 25 | } catch (err) { 26 | error = err 27 | } 28 | if (data?.statusCode === 500) { 29 | throw new Error(data.message) 30 | } 31 | if (error) { 32 | throw error 33 | } 34 | return data 35 | } 36 | -------------------------------------------------------------------------------- /starters/svelte/client/pages/using-store.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 |32 | Go back to the index 33 |
34 |⁂
35 |When you navigate away from this route, any additions to the to-do 36 | list are not lost, because they're bound to the global application state.
37 | -------------------------------------------------------------------------------- /starters/svelte/client/pages/using-auth.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 |34 | Go back to the index 35 |
36 |⁂
37 |When you navigate away from this route, any additions to the to-do 38 | list are not lost, because they're bound to the global application state.
39 | -------------------------------------------------------------------------------- /starters/solid/client/pages/streaming.jsx: -------------------------------------------------------------------------------- 1 | import { createResource } from 'solid-js' 2 | import { useRouteContext } from '/dx:core.js' 3 | 4 | export const streaming = true 5 | 6 | export default function Streaming () { 7 | const {state} = useRouteContext() 8 | const [message] = createResource(async () => { 9 | if (state.delayedMessage) { 10 | return state.delayedMessage 11 | } 12 | const message = await afterSeconds({ 13 | message: 'Delayed by Resource API', 14 | seconds: 5, 15 | }) 16 | state.delayedMessage = message 17 | return message 18 | }) 19 | return ( 20 |{props.message}
29 | ) 30 | } 31 | 32 | function afterSeconds ({ message, seconds }) { 33 | return new Promise((resolve) => { 34 | setTimeout(() => { 35 | resolve(message) 36 | }, seconds * 1000) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/root.jsx: -------------------------------------------------------------------------------- 1 | import { createMutable } from 'solid-js/store' 2 | import { Router, Routes, Route } from 'solid-app-router' 3 | import DXRoute from '/dx:route.jsx' 4 | import { isServer } from '/dx:core.js' 5 | 6 | export default function Root (props) { 7 | // eslint-disable-next-line solid/reactivity 8 | props.payload.serverRoute.state = isServer 9 | ? props.payload.serverRoute.state 10 | : createMutable(props.payload.serverRoute.state) 11 | // This is so we can serialize state into the hydration payload after SSR is done 12 | return ( 13 |28 | Go back to the index 29 |
30 |⁂
31 |When you navigate away from this route, any additions to the to-do 32 | list are not lost, because they're bound to the global application state.
33 | > 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /starters/solid/client/pages/using-auth.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'solid-app-router' 2 | import { useRouteContext } from '/dx:core.js' 3 | 4 | export const layout = 'auth' 5 | 6 | export function getMeta () { 7 | return { title: 'Todo List — Using Custom Layout' } 8 | } 9 | 10 | export default function UsingCustomLayout (props) { 11 | let input 12 | const {state, actions} = useRouteContext() 13 | const addItem = (value) => { 14 | actions.todolist.add(state, value) 15 | input.value = '' 16 | } 17 | return ( 18 | <> 19 |30 | Go back to the index 31 |
32 |⁂
33 |When you navigate away from this route, any additions to the to-do 34 | list are not lost, because they're bound to the global application state.
35 | > 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /starters/svelte/client/pages/using-data.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 |39 | Go back to the index 40 |
41 |⁂
42 |When you navigate away from this route, any additions to the to-do 43 | list will be lost, because they're bound to this route component only.
44 |See the /using-store example to learn 45 | how to use the application global state for it. 46 |
47 | -------------------------------------------------------------------------------- /packages/fastify-svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint": "eslint . --ext .js,.svelte --fix" 4 | }, 5 | "type": "module", 6 | "main": "index.js", 7 | "name": "@fastify/svelte", 8 | "version": "0.1.0", 9 | "files": [ 10 | "virtual/root.svelte", 11 | "virtual/route.svelte", 12 | "virtual/layouts.js", 13 | "virtual/layouts/default.svelte", 14 | "virtual/context.js", 15 | "virtual/mount.js", 16 | "virtual/core.js", 17 | "virtual/routes.js", 18 | "index.js", 19 | "plugin.cjs", 20 | "server/context.js", 21 | "server/stream.js" 22 | ], 23 | "license": "MIT", 24 | "exports": { 25 | ".": "./index.js", 26 | "./plugin": "./plugin.cjs" 27 | }, 28 | "dependencies": { 29 | "devalue": "^2.0.1", 30 | "svelte-loadable": "^2.0.1", 31 | "svelte-navigator": "^3.2.2", 32 | "sveltio": "^1.0.5", 33 | "unihead": "^0.0.6" 34 | }, 35 | "devDependencies": { 36 | "@babel/eslint-parser": "^7.18.2", 37 | "eslint": "^8.18.0", 38 | "eslint-config-standard": "^16.0.2", 39 | "eslint-plugin-import": "^2.22.1", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-promise": "^4.3.1", 42 | "eslint-plugin-svelte3": "^4.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /prerelease.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { join } from 'path' 3 | import { writeFile, readdir } from 'fs/promises' 4 | 5 | const starters = await readdir(join(__dirname, 'starters')) 6 | 7 | for (const starter of starters) { 8 | if (starter.startsWith('.')) { 9 | continue 10 | } 11 | const oldAdapter = `fastify-dx-${starter.split('-')[0]}` 12 | const newAdapter = `@fastify/${starter.split('-')[0]}` 13 | const pkgPath = join(__dirname, 'starters', starter, 'package.json') 14 | const pkg = require(pkgPath) 15 | pkg.devInstall.external.ky = '^0.31.4' 16 | delete pkg.devInstall.local[oldAdapter] 17 | delete pkg.devInstall.external['fastify-vite'] 18 | pkg.devInstall.external['@fastify/vite'] = '^3.0.1' 19 | delete pkg.devInstall.local[newAdapter] 20 | delete pkg.devInstall.external[newAdapter] 21 | pkg.devInstall.local[newAdapter] = '^0.1.0' 22 | pkg.devInstall.external.fastify = '^4.9.2' 23 | pkg.dependencies = {} 24 | for (const [ext, version] of Object.entries(pkg.devInstall.external)) { 25 | pkg.dependencies[ext] = version 26 | } 27 | // for (const [local, version] of Object.entries(pkg.devInstall.local)) { 28 | // pkg.dependencies[local] = version 29 | // } 30 | await writeFile(pkgPath, JSON.stringify(pkg, null, 2)) 31 | } -------------------------------------------------------------------------------- /ALPHA.md: -------------------------------------------------------------------------------- 1 | 2 | ## @fastify/solid 3 | 4 | [**@fastify/solid**](https://github.com/fastify/fastify-dx/tree/main/packages/fastify-solid) features **Solid v1.6** support via [**vite-plugin-solid**](https://github.com/solidjs/vite-plugin-solid) with: 5 | 6 | - [**Solid Router**](https://github.com/solidjs/solid-router) for **universal routing**; 7 | - Solid's own [**`createMutable()`**](https://www.solidjs.com/docs/latest/api#createmutable) primitive for global state management. 8 | 9 | 👉 [**Starter template**](https://github.com/fastify/fastify-dx/tree/main/starters/solid)36 | Go back to the index 37 |
38 |⁂
39 |When you navigate away from this route, any additions to the to-do 40 | list will be lost, because they're bound to this route component only.
41 |See the /using-store example to learn 42 | how to use the application global state for it. 43 |
44 | > 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /starters/solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "node server.js --dev", 5 | "build": "npm run build:client && npm run build:server", 6 | "serve": "node server.js", 7 | "devinstall": "zx ../../devinstall.mjs -- node server.js --dev", 8 | "build:client": "vite build --outDir dist/client --ssrManifest", 9 | "build:server": "vite build --outDir dist/server --ssr /index.js", 10 | "lint": "eslint . --ext .js,.jsx --fix" 11 | }, 12 | "dependencies": { 13 | "ky-universal": "^0.10.1", 14 | "ky": "^0.31.4", 15 | "fastify": "^4.9.2", 16 | "@fastify/vite": "^3.0.1", 17 | "devalue": "^2.0.1", 18 | "minipass": "^3.3.4", 19 | "solid-app-router": "^0.4.1", 20 | "solid-js": "^1.4.4", 21 | "unihead": "^0.0.6" 22 | }, 23 | "devDependencies": { 24 | "vite-plugin-solid": "^2.5.0", 25 | "@babel/eslint-parser": "^7.16.0", 26 | "babel-preset-solid": "^1.4.4", 27 | "eslint": "^7.32.0", 28 | "eslint-config-standard": "^16.0.2", 29 | "eslint-plugin-import": "^2.22.1", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint-plugin-promise": "^4.3.1", 32 | "eslint-plugin-solid": "^0.4.7", 33 | "postcss-preset-env": "^7.7.1", 34 | "unocss": "^0.37.4" 35 | }, 36 | "devInstall": { 37 | "local": { 38 | "@fastify/solid": "^0.1.0" 39 | }, 40 | "external": { 41 | "ky-universal": "^0.10.1", 42 | "ky": "^0.31.4", 43 | "fastify": "^4.9.2", 44 | "@fastify/vite": "^3.0.1" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /starters/svelte/client/pages/index.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 |getData() function
26 | and useRouteContext() to retrieve server data for a route.getData() function
24 | and useRouteContext() to retrieve server data for a route.|
64 |
65 | ### An hypothetical React component
66 |
67 | For a React route component running in an hypothetical framework that implements this specification, here's what it might look like.
68 |
69 | At the top, we enable `stream` to determine that this route should be server-side rendered in streaming mode.
70 |
71 | Then the `getData()` function, which should run both in SSR and CSR route navigation. It is assumed this function received a **route context object**, and in this case, a generic `route` object is also provided by the framework to identify the current route.
72 |
73 | The `getMeta()` function runs after `getData()` and has access to the data returned by it via the `data` property. It's used to define the ` |
76 | 77 | 78 | ```jsx 79 | export const streaming = true 80 | 81 | export async function getData ({ route, loadPageData }) { 82 | const pageData = await loadPageData(route) 83 | return pageData 84 | } 85 | 86 | export async function getMeta ({ data }) { 87 | return { 88 | html: { lang: data.lang }, 89 | title: data.title, 90 | meta: [ 91 | { name: 'twitter:title', value: data.title } 92 | ] 93 | } 94 | } 95 | 96 | export default function Route ({ data }) { 97 | return ( 98 | <> 99 | {data.title}100 |{data.body} 101 | > 102 | ) 103 | } 104 | ``` 105 | 106 | |
107 |
| 115 | 116 | ### `context` 117 | 118 | In the case of **named function exports**, a `context` object must be passed to the function, providing universal access to route information, data and methods. See the TypeScript interface to the right for a minimal implementation. 119 | 120 | If the route context contains a `data` object, it must be made available during SSR (seamlessly hydrated on first-render) and CSR. 121 | 122 | It may contain references to the Request and Response objects following the convention and semantics of the underlying server used. In the case of Fastify, those would be `req` and `reply`. 123 | 124 | | 125 |126 | 127 | It must implement at least the following TypeScript interface: 128 | 129 | ```ts 130 | interface RouteContext { 131 | readonly url: string 132 | readonly static: boolean 133 | readonly server: boolean 134 | readonly client: boolean 135 | fetch: () => Promise |
143 |
| 153 | 154 | ### `streaming` 155 | 156 | Determines if **server-side rendering** should take place in **streaming mode** if the underlying framework supports it. 157 | 158 | | 159 |160 | 161 | It must be set with a `boolean`: 162 | 163 | ```js 164 | export const streaming = true 165 | ``` 166 | 167 | |
168 |
| 174 | 175 | ### `serverOnly` 176 | 177 | Determines that the component must only run on the server and no JavaScript code should run on the client, i.e., the client should receive the **static markup** produced by running the component on the server, making it effectively [SSR](https://web.dev/rendering-on-the-web/#server-rendering)-only). 178 | 179 | | 180 |181 | 182 | It must be either set with a `boolean`: 183 | 184 | ```js 185 | export const serverOnly = true 186 | ``` 187 | 188 | Or with a function that returns a `boolean`: 189 | 190 | ```js 191 | export function serverOnly (context) { 192 | return context.shouldRunOnlyOnTheServer 193 | } 194 | ``` 195 | 196 | |
197 |
| 203 | 204 | ### `clientOnly` 205 | 206 | Determines that the component must only run on the client and no **server-side rendering** is to take splace, i.e., the client should perform rendering entirely on its own ([CSR](https://web.dev/rendering-on-the-web/#client-side-rendering-(csr))-only). It may either be a `boolean` or a function that returns a `boolean`. 207 | 208 | | 209 |210 | 211 | It must be either set with a `boolean`: 212 | 213 | ```js 214 | export const clientOnly = true 215 | ``` 216 | 217 | Or with a function that returns a `boolean`: 218 | 219 | ```js 220 | export function clientOnly (context) { 221 | return context.shouldRunOnlyOnTheClient 222 | } 223 | ``` 224 | 225 | |
226 |
| 234 | 235 | ## `onEnter()` 236 | 237 | Determines the universal **route handler** for the component. It must be implemented in way that it can run both on the server prior to **server-side rendering** and on the client prior to **client-side route navigation** (via **History API**). 238 | 239 | It **must** receive a route context object that **should** receive server request and response references during SSR and a client-side router reference during CSR, or hybrid APIs that can work in both environments. 240 | 241 | | 242 |243 | 244 | ```js 245 | export async function onEnter ({ reply, isServer }) { 246 | if (isServer) { 247 | // This runs on the server where you have access, 248 | // for instance, to the Fastify Reply object 249 | reply.header('X-Custom-Header', 'value') 250 | } else { 251 | console.log('This should only appear in the browser') 252 | } 253 | } 254 | ``` 255 | 256 | > This function could be used to reimplement `useAsyncData()` in Nuxt.js and `action()` in Remix.js. 257 | 258 | |
259 |
This route is rendered on the server only!
228 | } 229 | ``` 230 | 231 | [This example](https://github.com/fastify/fastify-dx/blob/main/starters/solid/client/pages/server-only.jsx) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/solid). 232 | 233 | ## `clientOnly` 234 | 235 | 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 `` in `index.html`) and all rendering takes place on the client only. 236 | 237 | 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. 238 | 239 | ```jsx 240 | export const clientOnly = true 241 | 242 | export default function Route (props) { 243 | returnThis route is rendered on the client only!
244 | } 245 | ``` 246 | 247 | [This example](https://github.com/fastify/fastify-dx/blob/main/starters/solid/client/pages/client-only.jsx) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/solid). 248 | 249 | ## Routing Configuration 250 | 251 | By default, routes are loaded from the `Route with path export
291 | ``` 292 | 293 | ## Isomorphic data prefetching 294 | 295 | Fastify DX for Svelte 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/solid/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.js' 305 | 306 | export function getData (ctx) { 307 | return { 308 | message: 'Hello from getData!', 309 | } 310 | } 311 | 312 | export default function Route (props) { 313 | const { data } = useRouteContext() 314 | return{data.message}
315 | } 316 | ``` 317 | 318 | ## Route Layouts 319 | 320 | 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. 321 | 322 | See the section on [Virtual Modules](https://github.com/fastify/fastify-dx/blob/main/docs/solid/virtual-modules.md) to learn more about this. 323 | 324 | You assign a layout to a route by exporting `layout`. 325 | 326 | See [`pages/using-auth.jsx`](https://github.com/fastify/fastify-dx/blob/main/starters/solid/pages/using-auth.jsx) in the starter template: 327 | 328 | ```js 329 | export const layout = 'auth' 330 | ``` 331 | 332 | 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/solid/layouts/auth.jsx): 333 | 334 | ```jsx 335 | import { useRouteContext } from '/dx:core.js' 336 | 337 | export default function Auth (props) { 338 | const { actions, state } = useRouteContext() 339 | const authenticate = () => actions.authenticate(state) 340 | return ( 341 |This route needs authentication.
353 | 356 | > 357 | ) 358 | } 359 | ``` 360 | 361 | Note that like routes, it has access to `useRouteContext()`. 362 | 363 | ## Route Context 364 | 365 | ### Initialization module 366 | 367 | 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/). 368 | 369 | **Consuming the route context:** 370 | 371 | ```js 372 | import { 373 | useRouteContext 374 | } from '/dx:core.js' 375 | 376 | // ... 377 | const { 378 | state, 379 | actions 380 | } = useRouteContext() 381 | 382 | // ... 383 | actions.addTodoItem(state, value) 384 | ``` 385 | 386 | See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/solid/client/pages/using-store.vue) in the starter template. 387 | 388 | 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. 389 | 390 | ```js 391 | import ky from 'ky-universal' 392 | 393 | export default (ctx) => { 394 | if (ctx.server) { 395 | // Populate state.todoList on the server 396 | ctx.state.todoList = ctx.server.db.todoList 397 | // It'll get automatically serialized to the client on first render! 398 | } 399 | } 400 | 401 | export const $fetch = ky.extend({ 402 | prefixUrl: 'http://localhost:3000' 403 | }) 404 | 405 | // Must be a function so each request can have its own state 406 | export const state = () => ({ 407 | todoList: null, 408 | }) 409 | 410 | export const actions = { 411 | async addTodoItem (state, item) { 412 | await $fetch.put('api/todo/items', { 413 | json: { item }, 414 | }) 415 | state.todoList.push(item) 416 | }, 417 | } 418 | ``` 419 | 420 | See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/solid/client/context.js) in the starter template. 421 | 422 | ### The `useRouteContext()` hook 423 | 424 | This hook can be used in any Vue 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**. 425 | 426 | By default, It includes reference to `data` — which is automatically populated if you use the `getData()` function, and `state` which hold references to the global [`reactive()`](https://vuejs.org/api/reactivity-core.html#reactive) object. 427 | 428 | It automatically causes the component to be suspended if the `getData()`, `getMeta()` and `onEnter()` functions are asynchronous. 429 | 430 | ```jsx 431 | import { useRouteContext } from '/dx:core.js' 432 | 433 | export default function Route (props) { 434 | const { data } = useRouteContext() 435 | return{data.message}
436 | } 437 | ``` 438 | 439 | ### Execution order 440 | 441 | This graph illustrates the execution order to expect from route context initialization. 442 | 443 | ``` 444 | context.js default function export 445 | └─ getData() function export 446 | └─ getMeta() function export 447 | └─ onEnter() function export 448 | └─ Route module 449 | ``` 450 | 451 | 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 state (the route context's `state` property, which is automatically hydrated on the client. 452 | 453 | 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. 454 | 455 | ## Universal Route Enter Event 456 | 457 | ### `onEnter(ctx)` 458 | 459 | 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 ``, it runs normally as you'd expect. 460 | 461 | It receives the [universal route context][route-context] as first parameter, so you can make changes to `data`, `meta` and `state` if needed. 462 | 463 | [route-context]: https://github.com/fastify/fastify-dx/blob/main/docs/solid/route-context.md 464 | 465 | ```jsx 466 | export function onEnter (ctx) { 467 | if (ctx.server?.underPressure) { 468 | ctx.clientOnly = true 469 | } 470 | } 471 | 472 | export default function Route (props) { 473 |No pre-rendered HTML sent to the browser.
474 | } 475 | ``` 476 | 477 | 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. 478 | 479 | 480 | ## Meta Tags 481 | 482 | Following the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md), Fastify DX renders `` elements independently from the SSR phase. This allows you to fetch data for populating the first `` tags and stream them right away to the client, and only then perform SSR. 483 | 484 | > Additional `` 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-dx-solid/index.js#L57) to [fastify-vite](https://github.com/fastify/fastify-vite). 485 | 486 | ### `getMeta()` 487 | 488 | To populate `Route with meta tags.
504 | } 505 | ``` 506 | 507 | 508 | ## Virtual Modules 509 | 510 | **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. 511 | 512 | Fastify DX virtual modules are **fully ejectable**. For instance, the starter template relies on the `/dx:root.jsx` virtual module to provide the Vue shell of your application. If you copy the `root.jsx` file [from the fastify-dx-solid package](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/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. 513 | 514 | 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. 515 | 516 | ### `/dx:root.jsx` 517 | 518 | This is the root Solid component. It's provided as part of the starter template. You can use this file to add a common layout to all routes. 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. 519 | 520 | ```jsx 521 | import 'uno.css' 522 | import { createMutable } from 'solid-js/store' 523 | import { Router, Routes, Route } from 'solid-app-router' 524 | import DXRoute from '/dx:route.jsx' 525 | 526 | export default function Root (props) { 527 | props.payload.serverRoute.state = createMutable(props.payload.serverRoute.state) 528 | return ( 529 |