├── 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 |
{props.children}
4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/layouts/default.jsx: -------------------------------------------------------------------------------- 1 | export default function Default (props) { 2 | return ( 3 |
{props.children}
4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **Fastify DX** has been deprecated in favor of [**@fastify/vite**](https://github.com/fastify/fastify-vite). 3 | 4 | Read all about it in [this blog post](./blog-post.pdf). 5 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/create.jsx: -------------------------------------------------------------------------------- 1 | import Root from '/dx:root.jsx' 2 | 3 | export default function create ({ url, payload }) { 4 | return () => 5 | } 6 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/create.tsx: -------------------------------------------------------------------------------- 1 | import Root from '/dx:root.tsx' 2 | 3 | export default function create ({ url, payload }) { 4 | return () => 5 | } 6 | -------------------------------------------------------------------------------- /starters/solid/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env') 2 | 3 | module.exports = { 4 | plugins: [ 5 | postcssPresetEnv({ 6 | stage: 1, 7 | }), 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /starters/svelte/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env') 2 | 3 | module.exports = { 4 | plugins: [ 5 | postcssPresetEnv({ 6 | stage: 1, 7 | }), 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /starters/svelte/client/layouts/default.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/layouts/default.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /starters/svelte/client/index.js: -------------------------------------------------------------------------------- 1 | import Root from '/dx:root.svelte' 2 | import routes from '/dx:routes.js' 3 | import * as context from '/dx:context.js' 4 | 5 | export default { 6 | Root, 7 | routes, 8 | context, 9 | } 10 | -------------------------------------------------------------------------------- /packages/fastify-svelte/server/stream.js: -------------------------------------------------------------------------------- 1 | 2 | // Helper function to prepend and append chunks the body stream 3 | export async function * generateHtmlStream ({ head, body, footer }) { 4 | yield head 5 | yield body 6 | yield footer() 7 | } 8 | -------------------------------------------------------------------------------- /starters/svelte/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /starters/solid/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/fastify-solid/server/stream.js: -------------------------------------------------------------------------------- 1 | 2 | // Helper function to prepend and append chunks the body stream 3 | export async function * generateHtmlStream ({ head, body, stream, footer }) { 4 | yield head 5 | if (body) { 6 | yield body 7 | } 8 | if (stream) { 9 | for await (const chunk of await stream) { 10 | yield chunk 11 | } 12 | } 13 | yield footer() 14 | } 15 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/layouts.js: -------------------------------------------------------------------------------- 1 | import DefaultLayout from '/dx:layouts/default.svelte' 2 | 3 | const appLayouts = import.meta.globEager('/layouts/*.svelte') 4 | 5 | appLayouts['/layouts/default.svelte'] ??= DefaultLayout 6 | 7 | export default Object.fromEntries( 8 | Object.keys(appLayouts).map((path) => { 9 | const name = path.slice(9, -7) 10 | return [name, appLayouts[path]] 11 | }), 12 | ) 13 | -------------------------------------------------------------------------------- /starters/solid/client/fetch.js: -------------------------------------------------------------------------------- 1 | // This method is only used on the client, assuming the 2 | // requests are made to the current location, so no base 3 | // URL configuration is needed in this example. 4 | 5 | export function sendJSON (path, options) { 6 | return fetch(path, { 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | ...options.json && { 11 | body: JSON.stringify(options.json), 12 | }, 13 | ...options 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /starters/svelte/client/fetch.js: -------------------------------------------------------------------------------- 1 | // This method is only used on the client, assuming the 2 | // requests are made to the current location, so no base 3 | // URL configuration is needed in this example. 4 | 5 | export function sendJSON (path, options) { 6 | return fetch(path, { 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | ...options.json && { 11 | body: JSON.stringify(options.json), 12 | }, 13 | ...options 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /starters/svelte/client/layouts/auth.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | {#if !$snapshot.user.authenticated} 8 |

This route needs authentication.

9 | 12 | {:else} 13 | 14 | {/if} 15 |
16 | -------------------------------------------------------------------------------- /starters/svelte/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | parser: '@babel/eslint-parser', 3 | parserOptions: { 4 | requireConfigFile: false, 5 | ecmaVersion: 2021, 6 | sourceType: 'module', 7 | }, 8 | plugins: [ 9 | 'svelte3' 10 | ], 11 | overrides: [ 12 | { 13 | files: ['*.svelte'], 14 | processor: 'svelte3/svelte3' 15 | } 16 | ], 17 | rules: { 18 | 'comma-dangle': ['error', 'always-multiline'], 19 | 'import/no-absolute-path': 'off', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/fastify-svelte/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | parser: '@babel/eslint-parser', 3 | parserOptions: { 4 | requireConfigFile: false, 5 | ecmaVersion: 2021, 6 | sourceType: 'module', 7 | }, 8 | plugins: [ 9 | 'svelte3' 10 | ], 11 | overrides: [ 12 | { 13 | files: ['*.svelte'], 14 | processor: 'svelte3/svelte3' 15 | } 16 | ], 17 | rules: { 18 | 'comma-dangle': ['error', 'always-multiline'], 19 | 'import/no-absolute-path': 'off', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/layouts.js: -------------------------------------------------------------------------------- 1 | import DefaultLayout from '/dx:layouts/default.jsx' 2 | 3 | const appLayouts = import.meta.globEager('/layouts/*.(jsx|tsx)') 4 | 5 | if (!appLayouts['/layouts/default.jsx'] && !appLayouts['/layouts/default.tsx']) { 6 | appLayouts['/layouts/default.jsx'] = DefaultLayout 7 | } 8 | 9 | export default Object.fromEntries( 10 | Object.keys(appLayouts).map((path) => { 11 | const name = path.slice(9, -4) 12 | return [name, appLayouts[path]] 13 | }), 14 | ) 15 | -------------------------------------------------------------------------------- /starters/solid/client/index.js: -------------------------------------------------------------------------------- 1 | // SSR functions from Solid 2 | import { renderToStream, renderToString } from 'solid-js/web' 3 | 4 | import create from '/dx:create.jsx' 5 | import routes from '/dx:routes.js' 6 | import * as context from '/dx:context.js' 7 | 8 | export default { 9 | // Solid requires SSR functions to be imported 10 | // from the same module environment where all 11 | // application-level code runs 12 | renderToStream, 13 | renderToString, 14 | // Exports required by Fastify DX itself 15 | create, 16 | routes, 17 | context, 18 | } 19 | -------------------------------------------------------------------------------- /starters/solid/vite.config.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | import viteSolid from 'vite-plugin-solid' 5 | import fastifySolid from '@fastify/solid/plugin' 6 | import unocss from 'unocss/vite' 7 | 8 | const path = fileURLToPath(import.meta.url) 9 | const root = join(dirname(path), 'client') 10 | 11 | const plugins = [ 12 | viteSolid({ ssr: true }), 13 | unocss(), 14 | fastifySolid(), 15 | ] 16 | 17 | const ssr = { 18 | noExternal: ['solid-app-router'], 19 | } 20 | 21 | export default { root, plugins, ssr } 22 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/root.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {#each payload.routes as { path, component }} 12 | 13 | 18 | 19 | {/each} 20 | 21 | -------------------------------------------------------------------------------- /starters/solid/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | parser: '@babel/eslint-parser', 3 | parserOptions: { 4 | babelOptions: { 5 | presets: ['babel-preset-solid'], 6 | }, 7 | requireConfigFile: false, 8 | ecmaVersion: 2021, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | plugins: ['solid'], 15 | extends: ['plugin:solid/recommended', 'standard'], 16 | rules: { 17 | 'solid/reactivity': 'off', 18 | 'comma-dangle': ['error', 'always-multiline'], 19 | 'import/no-absolute-path': 'off', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/fastify-solid/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | parser: '@babel/eslint-parser', 3 | parserOptions: { 4 | babelOptions: { 5 | presets: ['babel-preset-solid'], 6 | }, 7 | requireConfigFile: false, 8 | ecmaVersion: 2021, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | plugins: ['solid'], 15 | extends: ['plugin:solid/recommended', 'standard'], 16 | rules: { 17 | 'solid/reactivity': 'off', 18 | 'comma-dangle': ['error', 'always-multiline'], 19 | 'import/no-absolute-path': 'off', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /starters/svelte/client/pages/client-only.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 |

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.

22 | -------------------------------------------------------------------------------- /starters/svelte/client/pages/server-only.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 |

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.

22 | -------------------------------------------------------------------------------- /starters/solid/client/pages/client-only.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'solid-app-router' 2 | 3 | export const clientOnly = true 4 | 5 | export function getMeta () { 6 | return { 7 | title: 'Client Only Page' 8 | } 9 | } 10 | 11 | export default function ClientOnly () { 12 | return ( 13 | <> 14 |

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.

21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /starters/solid/client/pages/server-only.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'solid-app-router' 2 | 3 | export const serverOnly = true 4 | 5 | export function getMeta () { 6 | return { 7 | title: 'Server Only Page' 8 | } 9 | } 10 | 11 | export default function ServerOnly () { 12 | return ( 13 | <> 14 |

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.

21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /starters/svelte/vite.config.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | import { svelte as viteSvelte } from '@sveltejs/vite-plugin-svelte' 5 | import fastifySvelte from '@fastify/svelte/plugin' 6 | import unocss from 'unocss/vite' 7 | import { extractorSvelte } from '@unocss/core' 8 | 9 | const path = fileURLToPath(import.meta.url) 10 | 11 | const root = join(dirname(path), 'client') 12 | const plugins = [ 13 | unocss({ extractors: [extractorSvelte] }), 14 | viteSvelte({ 15 | compilerOptions: { 16 | hydratable: true, 17 | }, 18 | }), 19 | fastifySvelte(), 20 | ] 21 | 22 | export default { 23 | root, 24 | plugins, 25 | } 26 | -------------------------------------------------------------------------------- /starters/svelte/client/root.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {#each payload.routes as { path, component }} 18 | 19 | 25 | 26 | {/each} 27 | 28 | -------------------------------------------------------------------------------- /starters/solid/client/layouts/auth.jsx: -------------------------------------------------------------------------------- 1 | import { children } from 'solid-js' 2 | import { useRouteContext } from '/dx:core.js' 3 | 4 | export default function Auth (props) { 5 | const c = children(() => props.children) 6 | const { actions, state } = useRouteContext() 7 | const authenticate = () => actions.authenticate(state) 8 | return ( 9 |
10 | {state.user.authenticated 11 | ? c() 12 | : authenticate()} /> } 13 |
14 | ) 15 | } 16 | 17 | function Login (props) { 18 | return ( 19 | <> 20 |

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 |

Todo List — Using Store

22 | 27 |
28 | 29 | 30 |
31 |

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 |

Todo List — Using Store

24 | 29 |
30 | 31 | 32 |
33 |

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 | Waiting for content

}> 21 | 22 |
23 | ) 24 | } 25 | 26 | function Message (props) { 27 | return ( 28 |

{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 | 14 | { 15 | // eslint-disable-next-line solid/prefer-for 16 | props.payload.routes.map(route => 17 | 23 | } />, 24 | ) 25 | } 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/root.tsx: -------------------------------------------------------------------------------- 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 | 14 | { 15 | // eslint-disable-next-line solid/prefer-for 16 | props.payload.routes.map(route => 17 | 23 | } />, 24 | ) 25 | } 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /starters/solid/client/root.jsx: -------------------------------------------------------------------------------- 1 | import 'uno.css' 2 | import { createMutable } from 'solid-js/store' 3 | import { Router, Routes, Route } from 'solid-app-router' 4 | import DXRoute from '/dx:route.jsx' 5 | import { isServer } from '/dx:core.js' 6 | 7 | export default function Root (props) { 8 | // eslint-disable-next-line solid/reactivity 9 | props.payload.serverRoute.state = isServer 10 | ? props.payload.serverRoute.state 11 | : createMutable(props.payload.serverRoute.state) 12 | // This is so we can serialize state into the hydration payload after SSR is done 13 | return ( 14 | 15 | { 16 | // eslint-disable-next-line solid/prefer-for 17 | props.payload.routes.map(route => 18 | 24 | } />, 25 | ) 26 | } 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /starters/solid/client/pages/using-store.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'solid-app-router' 2 | import { useRouteContext } from '/dx:core.js' 3 | 4 | export function getMeta () { 5 | return { title: 'Todo List — Using Store' } 6 | } 7 | 8 | export default function UsingStore (props) { 9 | let input 10 | const {state, actions} = useRouteContext() 11 | const addItem = (value) => { 12 | actions.todoList.add(state, value) 13 | input.value = '' 14 | } 15 | return ( 16 | <> 17 |

Todo List — Using Store

18 | 23 |
24 | 25 | 26 |
27 |

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 |

Todo List — Using Custom Layout

20 | 25 |
26 | 27 | 28 |
29 |

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 |

Todo List — Using Data

29 | 34 |
35 | 36 | 37 |
38 |

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)
10 | 👉 [**Documentation**](https://github.com/fastify/fastify-dx/tree/main/packages/fastify-solid) 11 | 12 | 13 | ## @fastify/svelte 14 | 15 | [**@fastify/svelte**](https://github.com/fastify/fastify-dx/tree/main/packages/fastify-svelte) features **Svelte v3** support via [**@sveltejs/vite-plugin-svelte**](https://github.com/sveltejs/vite-plugin-svelte) with: 16 | 17 | - [**Svelte Navigator**](https://github.com/mefechoel/svelte-navigator) for **universal routing**; 18 | - [**Sveltio**](https://github.com/wobsoriano/sveltio) for global state management. 19 | 20 | 👉 [**Starter template**](https://github.com/fastify/fastify-dx/tree/main/starters/svelte)
21 | 👉 [**Documentation**](https://github.com/fastify/fastify-dx/tree/main/packages/fastify-svelte) 22 | -------------------------------------------------------------------------------- /packages/fastify-svelte/mount.ts: -------------------------------------------------------------------------------- 1 | import Head from 'unihead/client' 2 | import Root from '/dx:root.svelte' 3 | import routesPromise from '/dx:routes.js' 4 | 5 | mount('main') 6 | 7 | async function mount (target) { 8 | if (typeof target === 'string') { 9 | target = document.querySelector(target) 10 | } 11 | const context = await import('/dx:context.js') 12 | const serverRoute = await extendContext(window.route, context) 13 | const head = new Head(window.route.head, window.document) 14 | const resolvedRoutes = await routesPromise 15 | const routeMap = Object.fromEntries( 16 | resolvedRoutes.map((route) => [route.path, route]), 17 | ) 18 | new Root({ 19 | target: document.querySelector('main'), 20 | props: { 21 | payload: { 22 | head, 23 | serverRoute, 24 | routes: window.routes, 25 | routeMap, 26 | }, 27 | }, 28 | hydrate: !serverRoute.clientOnly, 29 | }) 30 | } 31 | 32 | async function extendContext (ctx, { 33 | // The route context initialization function 34 | default: setter, 35 | // We destructure state here just to discard it from extra 36 | state, 37 | // Other named exports from context.js 38 | ...extra 39 | }) { 40 | Object.assign(ctx, extra) 41 | if (setter) { 42 | await setter(ctx) 43 | } 44 | return ctx 45 | } 46 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/mount.js: -------------------------------------------------------------------------------- 1 | import Head from 'unihead/client' 2 | import Root from '/dx:root.svelte' 3 | import routesPromise from '/dx:routes.js' 4 | 5 | mount('main') 6 | 7 | async function mount (target) { 8 | if (typeof target === 'string') { 9 | target = document.querySelector(target) 10 | } 11 | const context = await import('/dx:context.js') 12 | const serverRoute = await extendContext(window.route, context) 13 | const head = new Head(window.route.head, window.document) 14 | const resolvedRoutes = await routesPromise 15 | const routeMap = Object.fromEntries( 16 | resolvedRoutes.map((route) => [route.path, route]), 17 | ) 18 | new Root({ 19 | target: document.querySelector('main'), 20 | props: { 21 | payload: { 22 | head, 23 | serverRoute, 24 | routes: window.routes, 25 | routeMap, 26 | }, 27 | }, 28 | hydrate: !serverRoute.clientOnly, 29 | }) 30 | } 31 | 32 | async function extendContext (ctx, { 33 | // The route context initialization function 34 | default: setter, 35 | // We destructure state here just to discard it from extra 36 | state, 37 | // Other named exports from context.js 38 | ...extra 39 | }) { 40 | Object.assign(ctx, extra) 41 | if (setter) { 42 | await setter(ctx) 43 | } 44 | return ctx 45 | } 46 | -------------------------------------------------------------------------------- /starters/solid/client/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-base: #f1f1f1; 3 | --color-highlight: #ff80ff; 4 | } 5 | html { 6 | background: #222; 7 | } 8 | main { 9 | width: 800px; 10 | margin: 0 auto; 11 | padding: 2em; 12 | box-shadow: 5px 5px 30px rgba(0,0,0,0.4); 13 | border-radius: 10px; 14 | background-color: rgba(255, 255, 255, 0.1); 15 | font-family: Avenir, Helvetica, Arial, sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | color: var(--color-base); 19 | margin-top: 60px; 20 | & a { 21 | color: var(--color-highlight); 22 | text-decoration: none; 23 | font-weight: bold; 24 | border-bottom: 1px solid var(--color-highlight); 25 | &:hover { 26 | color: #ffde00; 27 | } 28 | &:active { 29 | color: #eecf00 30 | } 31 | } 32 | & p { 33 | font-size: 1.2em; 34 | } 35 | & ul { 36 | & li { 37 | &:not(:last-child) { 38 | margin-bottom: 0.5em; 39 | } 40 | break-inside: avoid; 41 | font-size: 1em; 42 | } 43 | } 44 | & code { 45 | color: #ffde00; 46 | font-weight: bold; 47 | font-family: 'Consolas', 'Andale Mono', monospace; 48 | font-size: 0.9em; 49 | } 50 | & img { 51 | width: 14em; 52 | } 53 | & button { 54 | margin: 0 0.5em; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /starters/svelte/client/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-base: #f1f1f1; 3 | --color-highlight: #ff80ff; 4 | } 5 | html { 6 | background: #222; 7 | } 8 | main { 9 | width: 800px; 10 | margin: 0 auto; 11 | padding: 2em; 12 | box-shadow: 5px 5px 30px rgba(0,0,0,0.4); 13 | border-radius: 10px; 14 | background-color: rgba(255, 255, 255, 0.1); 15 | font-family: Avenir, Helvetica, Arial, sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | color: var(--color-base); 19 | margin-top: 60px; 20 | & a { 21 | color: var(--color-highlight); 22 | text-decoration: none; 23 | font-weight: bold; 24 | border-bottom: 1px solid var(--color-highlight); 25 | &:hover { 26 | color: #ffde00; 27 | } 28 | &:active { 29 | color: #eecf00 30 | } 31 | } 32 | & p { 33 | font-size: 1.2em; 34 | } 35 | & ul { 36 | & li { 37 | &:not(:last-child) { 38 | margin-bottom: 0.5em; 39 | } 40 | break-inside: avoid; 41 | font-size: 1em; 42 | } 43 | } 44 | & code { 45 | color: #ffde00; 46 | font-weight: bold; 47 | font-family: 'Consolas', 'Andale Mono', monospace; 48 | font-size: 0.9em; 49 | } 50 | & img { 51 | width: 14em; 52 | } 53 | & button { 54 | margin: 0 0.5em; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/mount.js: -------------------------------------------------------------------------------- 1 | import Head from 'unihead/client' 2 | import { render, hydrate } from 'solid-js/web' 3 | 4 | import create from '/dx:create.jsx' 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.js') 14 | const serverRoute = 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 | const app = create({ 21 | payload: { 22 | head, 23 | serverRoute, 24 | routes: window.routes, 25 | routeMap, 26 | }, 27 | }) 28 | if (serverRoute.clientOnly) { 29 | render(() => app, target) 30 | } else { 31 | hydrate(() => app, target) 32 | } 33 | } 34 | 35 | async function extendContext (ctx, { 36 | // The route context initialization function 37 | default: setter, 38 | // We destructure state here just to discard it from extra 39 | state, 40 | // Other named exports from context.js 41 | ...extra 42 | }) { 43 | Object.assign(ctx, extra) 44 | if (setter) { 45 | await setter(ctx) 46 | } 47 | return ctx 48 | } 49 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/mount.ts: -------------------------------------------------------------------------------- 1 | import Head from 'unihead/client' 2 | import { render, hydrate } from 'solid-js/web' 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.js') 14 | const serverRoute = 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 | const app = create({ 21 | payload: { 22 | head, 23 | serverRoute, 24 | routes: window.routes, 25 | routeMap, 26 | }, 27 | }) 28 | if (serverRoute.clientOnly) { 29 | render(() => app, target) 30 | } else { 31 | hydrate(() => app, target) 32 | } 33 | } 34 | 35 | async function extendContext (ctx, { 36 | // The route context initialization function 37 | default: setter, 38 | // We destructure state here just to discard it from extra 39 | state, 40 | // Other named exports from context.js 41 | ...extra 42 | }) { 43 | Object.assign(ctx, extra) 44 | if (setter) { 45 | await setter(ctx) 46 | } 47 | return ctx 48 | } 49 | -------------------------------------------------------------------------------- /starters/svelte/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 svelte -- 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,.svelte --fix" 11 | }, 12 | "dependencies": { 13 | "fastify": "^4.9.2", 14 | "@fastify/vite": "^3.0.1", 15 | "devalue": "^2.0.1", 16 | "svelte-loadable": "^2.0.1", 17 | "svelte-navigator": "^3.2.2", 18 | "sveltio": "^1.0.5", 19 | "unihead": "^0.0.6" 20 | }, 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.16.0", 23 | "@sveltejs/vite-plugin-svelte": "^1.0.0-next.49", 24 | "eslint": "^8.18.0", 25 | "eslint-config-standard": "^17.0.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-promise": "^6.0.0", 29 | "eslint-plugin-svelte3": "^4.0.0", 30 | "postcss-preset-env": "^7.7.1", 31 | "unocss": "^0.37.4" 32 | }, 33 | "devInstall": { 34 | "local": { 35 | "@fastify/svelte": "^0.1.0" 36 | }, 37 | "external": { 38 | "fastify": "^4.9.2", 39 | "@fastify/vite": "^3.0.1" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /starters/solid/client/context.js: -------------------------------------------------------------------------------- 1 | // Thin layer on top of fetch() 2 | // to automatically perform JSON requests 3 | import { sendJSON } from '/fetch.js' 4 | 5 | // The default export function runs exactly once on 6 | // the server and once on the client during the 7 | // first render, that is, it's not executed again 8 | // in subsquent client-side navigation via React Router. 9 | export default (ctx) => { 10 | if (ctx.server) { 11 | ctx.state.todoList = ctx.server.db.todoList 12 | } 13 | } 14 | 15 | // State initializer, must be a function called state 16 | // as this is a special context.js export and needs 17 | // special processing, e.g., serialization and hydration 18 | export function state () { 19 | return { 20 | user: { 21 | authenticated: false, 22 | }, 23 | todoList: null, 24 | } 25 | } 26 | 27 | // Grouped actions that operate on the state. This export 28 | // could be named anything, no special processing involved. 29 | export const actions = { 30 | authenticate (state) { 31 | state.user.authenticated = true 32 | }, 33 | todoList: { 34 | async add (state, item) { 35 | await sendJSON('/api/todo/items', { method: 'put', json: item }) 36 | state.todoList.push(item) 37 | }, 38 | async remove (state, index) { 39 | await sendJSON('/api/todo/items', { method: 'delete', json: index }) 40 | state.todoList.splice(index, 1) 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /starters/svelte/client/context.js: -------------------------------------------------------------------------------- 1 | // Thin layer on top of fetch() 2 | // to automatically perform JSON requests 3 | import { sendJSON } from '/fetch.js' 4 | 5 | // The default export function runs exactly once on 6 | // the server and once on the client during the 7 | // first render, that is, it's not executed again 8 | // in subsquent client-side navigation via React Router. 9 | export default (ctx) => { 10 | if (ctx.server) { 11 | ctx.state.todoList = ctx.server.db.todoList 12 | } 13 | } 14 | 15 | // State initializer, must be a function called state 16 | // as this is a special context.js export and needs 17 | // special processing, e.g., serialization and hydration 18 | export function state () { 19 | return { 20 | user: { 21 | authenticated: false, 22 | }, 23 | todoList: null, 24 | } 25 | } 26 | 27 | // Grouped actions that operate on the state. This export 28 | // could be named anything, no special processing involved. 29 | export const actions = { 30 | authenticate (state) { 31 | state.user.authenticated = true 32 | }, 33 | todoList: { 34 | async add (state, item) { 35 | await sendJSON('/api/todo/items', { method: 'put', json: item }) 36 | state.todoList.push(item) 37 | }, 38 | async remove (state, index) { 39 | await sendJSON('/api/todo/items', { method: 'delete', json: index }) 40 | state.todoList.splice(index, 1) 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/fastify-solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint": "eslint . --ext .js,.jsx --fix" 4 | }, 5 | "type": "module", 6 | "main": "index.js", 7 | "name": "@fastify/solid", 8 | "version": "0.1.0", 9 | "files": [ 10 | "virtual/create.jsx", 11 | "virtual/create.tsx", 12 | "virtual/root.jsx", 13 | "virtual/root.tsx", 14 | "virtual/route.jsx", 15 | "virtual/layouts.js", 16 | "virtual/layouts/default.jsx", 17 | "virtual/context.js", 18 | "virtual/context.ts", 19 | "virtual/mount.js", 20 | "virtual/mount.ts", 21 | "virtual/core.js", 22 | "virtual/routes.js", 23 | "index.js", 24 | "plugin.cjs", 25 | "server/context.js", 26 | "server/stream.js" 27 | ], 28 | "license": "MIT", 29 | "exports": { 30 | ".": "./index.js", 31 | "./plugin": "./plugin.cjs" 32 | }, 33 | "dependencies": { 34 | "devalue": "^2.0.1", 35 | "minipass": "^3.3.4", 36 | "solid-app-router": "^0.4.1", 37 | "solid-js": "^1.4.4", 38 | "unihead": "^0.0.6" 39 | }, 40 | "devDependencies": { 41 | "@babel/eslint-parser": "^7.16.0", 42 | "babel-preset-solid": "^1.4.4", 43 | "eslint": "^7.32.0", 44 | "eslint-config-standard": "^16.0.2", 45 | "eslint-plugin-import": "^2.22.1", 46 | "eslint-plugin-node": "^11.1.0", 47 | "eslint-plugin-promise": "^4.3.1", 48 | "eslint-plugin-solid": "^0.4.7", 49 | "vite-plugin-solid": "^2.2.6" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /starters/solid/client/pages/using-data.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js' 2 | import { Link } from 'solid-app-router' 3 | import { useRouteContext } from '/dx:core.js' 4 | 5 | export function getMeta () { 6 | return { title: 'Todo List — Using Data' } 7 | } 8 | 9 | export function getData ({ server }) { 10 | return { 11 | todoList: server.db.todoList 12 | } 13 | } 14 | 15 | export default function UsingData (props) { 16 | let input 17 | const {data} = useRouteContext() 18 | const [todoList, updateTodoList] = createSignal(data.todoList) 19 | const addItem = (value) => { 20 | updateTodoList(list => [...list, value]) 21 | input.value = '' 22 | } 23 | return ( 24 | <> 25 |

Todo List — Using Data

26 | 31 |
32 | 33 | 34 |
35 |

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 | Fastify DX 22 |

{state.message}

23 | 39 | -------------------------------------------------------------------------------- /starters/solid/client/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import logo from '/assets/logo.svg' 2 | import { Link } from 'solid-app-router' 3 | import { isServer, useRouteContext } from '/dx:core.js' 4 | 5 | export function getMeta () { 6 | return { 7 | title: 'Welcome to Fastify DX!' 8 | } 9 | } 10 | 11 | export default function Index () { 12 | const { state } = useRouteContext() 13 | if (isServer) { 14 | // State is automatically hydrated on the client 15 | state.message = 'Welcome to Fastify DX for Solid!' 16 | } 17 | return ( 18 | <> 19 | Fastify DX 20 |

{state.message}

21 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/fastify-svelte/server/context.js: -------------------------------------------------------------------------------- 1 | const routeContextInspect = Symbol.for('nodejs.util.inspect.custom') 2 | 3 | export default class RouteContext { 4 | static async create (server, req, reply, route, contextInit) { 5 | const routeContext = new RouteContext(server, req, reply, route) 6 | if (contextInit) { 7 | if (contextInit.state) { 8 | routeContext.state = contextInit.state() 9 | } 10 | if (contextInit.default) { 11 | await contextInit.default(routeContext) 12 | } 13 | } 14 | return routeContext 15 | } 16 | 17 | constructor (server, req, reply, route) { 18 | this.server = server 19 | this.req = req 20 | this.reply = reply 21 | this.head = {} 22 | this.state = null 23 | this.stateProxy = null 24 | this.data = route.data 25 | this.firstRender = true 26 | this.layout = route.layout 27 | this.getMeta = !!route.getMeta 28 | this.getData = !!route.getData 29 | this.onEnter = !!route.onEnter 30 | this.streaming = route.streaming 31 | this.clientOnly = route.clientOnly 32 | this.serverOnly = route.serverOnly 33 | } 34 | 35 | [routeContextInspect] () { 36 | return { 37 | ...this, 38 | server: { [routeContextInspect]: () => '[Server]' }, 39 | req: { [routeContextInspect]: () => '[Request]' }, 40 | reply: { [routeContextInspect]: () => '[Reply]' }, 41 | } 42 | } 43 | 44 | toJSON () { 45 | return { 46 | state: this.stateProxy, 47 | data: this.data, 48 | layout: this.layout, 49 | getMeta: this.getMeta, 50 | getData: this.getData, 51 | onEnter: this.onEnter, 52 | firstRender: this.firstRender, 53 | clientOnly: this.clientOnly, 54 | } 55 | } 56 | } 57 | 58 | RouteContext.extend = function (initial) { 59 | const { default: _, ...extra } = initial 60 | for (const [prop, value] of Object.entries(extra)) { 61 | if (prop !== 'data' && prop !== 'state') { 62 | Object.defineProperty(RouteContext.prototype, prop, value) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/fastify-solid/server/context.js: -------------------------------------------------------------------------------- 1 | 2 | const routeContextInspect = Symbol.for('nodejs.util.inspect.custom') 3 | 4 | export default class RouteContext { 5 | static async create (server, req, reply, route, contextInit) { 6 | const routeContext = new RouteContext(server, req, reply, route) 7 | if (contextInit) { 8 | if (contextInit.state) { 9 | routeContext.state = contextInit.state() 10 | } 11 | if (contextInit.default) { 12 | await contextInit.default(routeContext) 13 | } 14 | } 15 | return routeContext 16 | } 17 | 18 | constructor (server, req, reply, route) { 19 | this.server = server 20 | this.req = req 21 | this.reply = reply 22 | this.head = {} 23 | this.state = null 24 | this.data = route.data 25 | this.hydration = {} 26 | this.firstRender = true 27 | this.layout = route.layout 28 | this.getMeta = !!route.getMeta 29 | this.getData = !!route.getData 30 | this.onEnter = !!route.onEnter 31 | this.streaming = route.streaming 32 | this.clientOnly = route.clientOnly 33 | this.serverOnly = route.serverOnly 34 | } 35 | 36 | [routeContextInspect] () { 37 | return { 38 | ...this, 39 | server: { [routeContextInspect]: () => '[Server]' }, 40 | req: { [routeContextInspect]: () => '[Request]' }, 41 | reply: { [routeContextInspect]: () => '[Reply]' }, 42 | } 43 | } 44 | 45 | toJSON () { 46 | return { 47 | state: this.state, 48 | data: this.data, 49 | hydration: this.hydration, 50 | layout: this.layout, 51 | getMeta: this.getMeta, 52 | getData: this.getData, 53 | onEnter: this.onEnter, 54 | firstRender: this.firstRender, 55 | clientOnly: this.clientOnly, 56 | } 57 | } 58 | } 59 | 60 | RouteContext.extend = function (initial) { 61 | const { default: _, ...extra } = initial 62 | for (const [prop, value] of Object.entries(extra)) { 63 | if (prop !== 'data' && prop !== 'state') { 64 | Object.defineProperty(RouteContext.prototype, prop, value) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /devinstall.mjs: -------------------------------------------------------------------------------- 1 | 2 | /* globals $,path,fs */ 3 | /* eslint-disable node/no-path-concat */ 4 | 5 | const { name: example } = path.parse(process.cwd()) 6 | const exRoot = path.resolve(__dirname, 'starters', example) 7 | const command = process.argv.slice(process.argv.findIndex(_ => _ === '--') + 1) 8 | 9 | if (!fs.existsSync(exRoot)) { 10 | console.log('Must be called from a directory under starters/.') 11 | process.exit() 12 | } 13 | 14 | await $`rm -rf ${exRoot}/node_modules/vite` 15 | await $`rm -rf ${exRoot}/node_modules/.vite` 16 | 17 | const template = require(path.join(exRoot, 'package.json')) 18 | 19 | const { external, local } = template.devInstall 20 | const dependencies = { ...external } 21 | 22 | const localDepMap = { 23 | '@fastify/react': 'fastify-react', 24 | '@fastify/solid': 'fastify-solid', 25 | '@fastify/svelte': 'fastify-svelte', 26 | '@fastify/vue': 'fastify-vue', 27 | } 28 | 29 | for (const localDep of Object.keys(local)) { 30 | for (const [dep, version] of Object.entries( 31 | require(path.join(__dirname, 'packages', localDepMap[localDep], 'package.json')).dependencies || []) 32 | ) { 33 | if (!Object.keys(local).includes(dep)) { 34 | dependencies[dep] = version 35 | } 36 | } 37 | } 38 | 39 | await createPackageFile(exRoot, dependencies) 40 | await $`npm install -f` 41 | 42 | for (const localDep of Object.keys(local)) { 43 | await $`rm -rf ${exRoot}/node_modules/${localDep}` 44 | await $`cp -r ${__dirname}/packages/${localDepMap[localDep]} ${exRoot}/node_modules/${localDep}` 45 | // if (localDep === 'fastify-dx') { 46 | // await $`ln -s ${exRoot}/node_modules/${localDep}/index.mjs ${exRoot}/node_modules/.bin/dx` 47 | // } 48 | } 49 | 50 | try { 51 | await $`${command}` 52 | } finally { 53 | setImmediate(() => process.exit(0)) 54 | } 55 | 56 | async function createPackageFile (exRoot, dependencies) { 57 | const { type, scripts, devDependencies, devInstall } = template 58 | await fs.writeFile( 59 | path.join(exRoot, 'package.json'), 60 | JSON.stringify({ type, scripts, dependencies, devDependencies, devInstall }, null, 2), 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/route.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 | {#if isServer} 80 | 81 | 82 | 83 | {:else} 84 | {#await setupClientRouteContext then} 85 | 86 | 87 | 88 | {/await} 89 | {/if} 90 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/route.jsx: -------------------------------------------------------------------------------- 1 | import { createResource } from 'solid-js' 2 | import { isServer, Suspense } from 'solid-js/web' 3 | import { useLocation } from 'solid-app-router' 4 | import { RouteContext, jsonDataFetch } from '/dx:core.js' 5 | import layouts from '/dx:layouts.js' 6 | 7 | export default function DXRoute (props) { 8 | const ctx = props.payload.routeMap[props.path] 9 | const location = useLocation() 10 | 11 | ctx.state = props.state 12 | ctx.actions = props.payload.serverRoute.actions 13 | 14 | if (isServer) { 15 | ctx.layout = props.payload.serverRoute.layout ?? 'default' 16 | ctx.data = props.payload.serverRoute.data 17 | } 18 | 19 | async function setup () { 20 | if (props.payload.serverRoute.firstRender) { 21 | // ctx.hydration = payload.serverRoute.hydration 22 | ctx.data = props.payload.serverRoute.data 23 | ctx.layout = props.payload.serverRoute.layout ?? 'default' 24 | props.payload.serverRoute.firstRender = false 25 | return ctx 26 | } 27 | ctx.layout = ctx.layout ?? 'default' 28 | const { getMeta, getData, onEnter } = await ctx.loader() 29 | if (getData) { 30 | try { 31 | const fullPath = `${location.pathname}${location.search}` 32 | const updatedData = await jsonDataFetch(fullPath) 33 | if (!ctx.data) { 34 | ctx.data = {} 35 | } 36 | if (updatedData) { 37 | Object.assign(ctx.data, updatedData) 38 | } 39 | ctx.error = null 40 | } catch (error) { 41 | ctx.error = error 42 | } 43 | } 44 | if (getMeta) { 45 | const updatedMeta = await getMeta(ctx) 46 | if (updatedMeta) { 47 | props.payload.head.update(updatedMeta) 48 | } 49 | } 50 | if (onEnter) { 51 | const updatedData = await onEnter(ctx) 52 | if (updatedData) { 53 | Object.assign(ctx.data, updatedData) 54 | } 55 | } 56 | return ctx 57 | } 58 | 59 | let element 60 | if (isServer) { 61 | element = ( 62 | 63 | 64 | 65 | 66 | 67 | ) 68 | } else { 69 | const [routeContext] = createResource(setup) 70 | element = ( 71 | 72 | {!routeContext.loading && 73 | 74 | 75 | 76 | 77 | 78 | } 79 | 80 | ) 81 | } 82 | return element 83 | } 84 | 85 | function Layout (props) { 86 | const Component = layouts[props.id].default 87 | return {props.children} 88 | } 89 | -------------------------------------------------------------------------------- /packages/fastify-svelte/plugin.cjs: -------------------------------------------------------------------------------- 1 | const { readFileSync, existsSync } = require('fs') 2 | const { dirname, join, resolve } = require('path') 3 | const { fileURLToPath } = require('url') 4 | 5 | function viteSvelteFastifyDX (config = {}) { 6 | const prefix = /^\/?dx:/ 7 | const routing = Object.assign({ 8 | globPattern: '/pages/**/*.svelte', 9 | paramPattern: /\[(\w+)\]/, 10 | }, config) 11 | const virtualRoot = resolve(__dirname, 'virtual') 12 | const virtualModules = [ 13 | 'mount.js', 14 | 'mount.ts', 15 | 'routes.js', 16 | 'layouts.js', 17 | 'root.svelte', 18 | 'route.svelte', 19 | 'layouts/', 20 | 'context.js', 21 | 'context.ts', 22 | 'core.js' 23 | ] 24 | virtualModules.includes = function (virtual) { 25 | if (!virtual) { 26 | return false 27 | } 28 | for (const entry of this) { 29 | if (virtual.startsWith(entry)) { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | const virtualModuleInserts = { 36 | 'routes.js': { 37 | $globPattern: routing.globPattern, 38 | $paramPattern: routing.paramPattern, 39 | } 40 | } 41 | 42 | let viteProjectRoot 43 | 44 | function loadVirtualModuleOverride (virtual) { 45 | if (!virtualModules.includes(virtual)) { 46 | return 47 | } 48 | const overridePath = resolve(viteProjectRoot, virtual) 49 | if (existsSync(overridePath)) { 50 | return overridePath 51 | } 52 | } 53 | 54 | function loadVirtualModule (virtual) { 55 | if (!virtualModules.includes(virtual)) { 56 | return 57 | } 58 | let code = readFileSync(resolve(virtualRoot, virtual), 'utf8') 59 | if (virtualModuleInserts[virtual]) { 60 | for (const [key, value] of Object.entries(virtualModuleInserts[virtual])) { 61 | code = code.replace(new RegExp(escapeRegExp(key), 'g'), value) 62 | } 63 | } 64 | return { 65 | code, 66 | map: null, 67 | } 68 | } 69 | 70 | // Thanks to https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js 71 | function escapeRegExp (s) { 72 | return s 73 | .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') 74 | .replace(/-/g, '\\x2d') 75 | } 76 | 77 | return { 78 | name: 'vite-plugin-react-fastify-dx', 79 | config (config, { command }) { 80 | if (command === 'build' && config.build?.ssr) { 81 | config.build.rollupOptions = { 82 | output: { 83 | format: 'es', 84 | }, 85 | } 86 | } 87 | }, 88 | configResolved (config) { 89 | viteProjectRoot = config.root 90 | }, 91 | async resolveId (id) { 92 | const [, virtual] = id.split(prefix) 93 | if (virtual) { 94 | const override = await loadVirtualModuleOverride(virtual) 95 | if (override) { 96 | return override 97 | } 98 | return id 99 | } 100 | }, 101 | load (id) { 102 | const [, virtual] = id.split(prefix) 103 | return loadVirtualModule(virtual) 104 | }, 105 | } 106 | } 107 | 108 | module.exports = viteSvelteFastifyDX 109 | -------------------------------------------------------------------------------- /packages/fastify-solid/plugin.cjs: -------------------------------------------------------------------------------- 1 | const { readFileSync, existsSync } = require('fs') 2 | const { dirname, join, resolve } = require('path') 3 | const { fileURLToPath } = require('url') 4 | 5 | function viteSolidFastifyDX (config = {}) { 6 | const prefix = /^\/?dx:/ 7 | const routing = Object.assign({ 8 | globPattern: '/pages/**/*.(jsx|tsx)', 9 | paramPattern: /\[(\w+)\]/, 10 | }, config) 11 | const virtualRoot = resolve(__dirname, 'virtual') 12 | const virtualModules = [ 13 | 'mount.js', 14 | 'mount.ts', 15 | 'resource.js', 16 | 'routes.js', 17 | 'layouts.js', 18 | 'create.jsx', 19 | 'create.tsx', 20 | 'root.jsx', 21 | 'root.tsx', 22 | 'layouts/', 23 | 'context.js', 24 | 'context.ts', 25 | 'route.jsx', 26 | 'core.js' 27 | ] 28 | virtualModules.includes = function (virtual) { 29 | if (!virtual) { 30 | return false 31 | } 32 | for (const entry of this) { 33 | if (virtual.startsWith(entry)) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | const virtualModuleInserts = { 40 | 'routes.js': { 41 | $globPattern: routing.globPattern, 42 | $paramPattern: routing.paramPattern, 43 | } 44 | } 45 | 46 | let viteProjectRoot 47 | 48 | function loadVirtualModuleOverride (virtual) { 49 | if (!virtualModules.includes(virtual)) { 50 | return 51 | } 52 | const overridePath = resolve(viteProjectRoot, virtual) 53 | if (existsSync(overridePath)) { 54 | return overridePath 55 | } 56 | } 57 | 58 | function loadVirtualModule (virtual) { 59 | if (!virtualModules.includes(virtual)) { 60 | return 61 | } 62 | let code = readFileSync(resolve(virtualRoot, virtual), 'utf8') 63 | if (virtualModuleInserts[virtual]) { 64 | for (const [key, value] of Object.entries(virtualModuleInserts[virtual])) { 65 | code = code.replace(new RegExp(escapeRegExp(key), 'g'), value) 66 | } 67 | } 68 | return { 69 | code, 70 | map: null, 71 | } 72 | } 73 | 74 | // Thanks to https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js 75 | function escapeRegExp (s) { 76 | return s 77 | .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') 78 | .replace(/-/g, '\\x2d') 79 | } 80 | 81 | return { 82 | name: 'vite-plugin-react-fastify-dx', 83 | config (config, { command }) { 84 | if (command === 'build' && config.build?.ssr) { 85 | config.build.rollupOptions = { 86 | output: { 87 | format: 'es', 88 | }, 89 | } 90 | } 91 | }, 92 | configResolved (config) { 93 | viteProjectRoot = config.root 94 | }, 95 | async resolveId (id) { 96 | const [, virtual] = id.split(prefix) 97 | if (virtual) { 98 | const override = await loadVirtualModuleOverride(virtual) 99 | if (override) { 100 | return override 101 | } 102 | return id 103 | } 104 | }, 105 | load (id) { 106 | const [, virtual] = id.split(prefix) 107 | return loadVirtualModule(virtual) 108 | }, 109 | } 110 | } 111 | 112 | module.exports = viteSolidFastifyDX 113 | -------------------------------------------------------------------------------- /packages/fastify-svelte/virtual/routes.js: -------------------------------------------------------------------------------- 1 | /* global $paramPattern */ 2 | 3 | export default import.meta.env.SSR 4 | ? createRoutes(import.meta.globEager('$globPattern')) 5 | : hydrateRoutes(import.meta.glob('$globPattern')) 6 | 7 | async function createRoutes (from, { param } = { param: $paramPattern }) { 8 | // Otherwise we get a ReferenceError, but since 9 | // this function is only ran once, there's no overhead 10 | class Routes extends Array { 11 | toJSON () { 12 | return this.map((route) => { 13 | return { 14 | id: route.id, 15 | path: route.path, 16 | layout: route.layout, 17 | getData: !!route.getData, 18 | getMeta: !!route.getMeta, 19 | onEnter: !!route.onEnter, 20 | } 21 | }) 22 | } 23 | } 24 | const importPaths = Object.keys(from) 25 | const promises = [] 26 | if (Array.isArray(from)) { 27 | for (const routeDef of from) { 28 | promises.push( 29 | getRouteModule(routeDef.path, routeDef.component) 30 | .then((routeModule) => { 31 | return { 32 | id: routeDef.path, 33 | path: routeDef.path ?? routeModule.path, 34 | ...routeModule, 35 | } 36 | }), 37 | ) 38 | } 39 | } else { 40 | // Ensure that static routes have precedence over the dynamic ones 41 | for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) { 42 | promises.push( 43 | getRouteModule(path, from[path]) 44 | .then((routeModule) => { 45 | return { 46 | id: path, 47 | layout: routeModule.layout, 48 | path: routeModule.path ?? path 49 | // Remove /pages and .jsx extension 50 | .slice(6, -7) 51 | // Replace [id] with :id 52 | .replace(param, (_, m) => `:${m}`) 53 | // Replace '/index' with '/' 54 | .replace(/\/index$/, '/') 55 | // Remove trailing slashs 56 | .replace(/(.+)\/+$/, (...m) => m[1]), 57 | ...routeModule, 58 | } 59 | }), 60 | ) 61 | } 62 | } 63 | return new Routes(...await Promise.all(promises)) 64 | } 65 | 66 | async function hydrateRoutes (from) { 67 | if (Array.isArray(from)) { 68 | from = Object.fromEntries( 69 | from.map((route) => [route.path, route]), 70 | ) 71 | } 72 | return window.routes.map((route) => { 73 | route.loader = memoImport(from[route.id]) 74 | route.component = () => route.loader() 75 | return route 76 | }) 77 | } 78 | 79 | function getRouteModuleExports (routeModule) { 80 | return { 81 | // The Route component (default export) 82 | component: routeModule.default, 83 | // The Layout Route component 84 | layout: routeModule.layout, 85 | // Route-level hooks 86 | getData: routeModule.getData, 87 | getMeta: routeModule.getMeta, 88 | onEnter: routeModule.onEnter, 89 | // Other Route-level settings 90 | streaming: routeModule.streaming, 91 | clientOnly: routeModule.clientOnly, 92 | serverOnly: routeModule.serverOnly, 93 | } 94 | } 95 | 96 | async function getRouteModule (path, routeModule) { 97 | // const isServer = typeof process !== 'undefined' 98 | if (typeof routeModule === 'function') { 99 | routeModule = await routeModule() 100 | return getRouteModuleExports(routeModule) 101 | } else { 102 | return getRouteModuleExports(routeModule) 103 | } 104 | } 105 | 106 | function memoImport (func) { 107 | // Otherwise we get a ReferenceError, but since this function 108 | // is only ran once for each route, there's no overhead 109 | const kFuncExecuted = Symbol('kFuncExecuted') 110 | const kFuncValue = Symbol('kFuncValue') 111 | func[kFuncExecuted] = false 112 | return async function () { 113 | if (!func[kFuncExecuted]) { 114 | func[kFuncValue] = await func() 115 | func[kFuncExecuted] = true 116 | } 117 | return func[kFuncValue] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/fastify-solid/virtual/routes.js: -------------------------------------------------------------------------------- 1 | /* global $paramPattern */ 2 | 3 | import { lazy } from 'solid-js' 4 | 5 | export default import.meta.env.SSR 6 | ? createRoutes(import.meta.globEager('$globPattern')) 7 | : hydrateRoutes(import.meta.glob('$globPattern')) 8 | 9 | async function createRoutes (from, { param } = { param: $paramPattern }) { 10 | // Otherwise we get a ReferenceError, but since 11 | // this function is only ran once, there's no overhead 12 | class Routes extends Array { 13 | toJSON () { 14 | return this.map((route) => { 15 | return { 16 | id: route.id, 17 | path: route.path, 18 | layout: route.layout, 19 | getData: !!route.getData, 20 | getMeta: !!route.getMeta, 21 | onEnter: !!route.onEnter, 22 | } 23 | }) 24 | } 25 | } 26 | const importPaths = Object.keys(from) 27 | const promises = [] 28 | if (Array.isArray(from)) { 29 | for (const routeDef of from) { 30 | promises.push( 31 | getRouteModule(routeDef.path, routeDef.component) 32 | .then((routeModule) => { 33 | return { 34 | id: routeDef.path, 35 | path: routeDef.path ?? routeModule.path, 36 | ...routeModule, 37 | } 38 | }), 39 | ) 40 | } 41 | } else { 42 | // Ensure that static routes have precedence over the dynamic ones 43 | for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) { 44 | promises.push( 45 | getRouteModule(path, from[path]) 46 | .then((routeModule) => { 47 | return { 48 | id: path, 49 | layout: routeModule.layout, 50 | path: routeModule.path ?? path 51 | // Remove /pages and .jsx extension 52 | .slice(6, -4) 53 | // Replace [id] with :id 54 | .replace(param, (_, m) => `:${m}`) 55 | // Replace '/index' with '/' 56 | .replace(/\/index$/, '/') 57 | // Remove trailing slashs 58 | .replace(/(.+)\/+$/, (...m) => m[1]), 59 | ...routeModule, 60 | } 61 | }), 62 | ) 63 | } 64 | } 65 | return new Routes(...await Promise.all(promises)) 66 | } 67 | 68 | async function hydrateRoutes (from) { 69 | if (Array.isArray(from)) { 70 | from = Object.fromEntries( 71 | from.map((route) => [route.path, route]), 72 | ) 73 | } 74 | return window.routes.map((route) => { 75 | route.loader = memoImport(from[route.id]) 76 | route.component = lazy(() => route.loader()) 77 | return route 78 | }) 79 | } 80 | 81 | function getRouteModuleExports (routeModule) { 82 | return { 83 | // The Route component (default export) 84 | component: routeModule.default, 85 | // The Layout Route component 86 | layout: routeModule.layout, 87 | // Route-level hooks 88 | getData: routeModule.getData, 89 | getMeta: routeModule.getMeta, 90 | onEnter: routeModule.onEnter, 91 | // Other Route-level settings 92 | streaming: routeModule.streaming, 93 | clientOnly: routeModule.clientOnly, 94 | serverOnly: routeModule.serverOnly, 95 | } 96 | } 97 | 98 | async function getRouteModule (path, routeModule) { 99 | // const isServer = typeof process !== 'undefined' 100 | if (typeof routeModule === 'function') { 101 | routeModule = await routeModule() 102 | return getRouteModuleExports(routeModule) 103 | } else { 104 | return getRouteModuleExports(routeModule) 105 | } 106 | } 107 | 108 | function memoImport (func) { 109 | // Otherwise we get a ReferenceError, but since this function 110 | // is only ran once for each route, there's no overhead 111 | const kFuncExecuted = Symbol('kFuncExecuted') 112 | const kFuncValue = Symbol('kFuncValue') 113 | func[kFuncExecuted] = false 114 | return async function () { 115 | if (!func[kFuncExecuted]) { 116 | func[kFuncValue] = await func() 117 | func[kFuncExecuted] = true 118 | } 119 | return func[kFuncValue] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /starters/solid/client/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 42 | 47 | 49 | 52 | 57 | 58 | 61 | 66 | 67 | 70 | 75 | 76 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /starters/svelte/client/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 42 | 47 | 49 | 52 | 57 | 58 | 61 | 66 | 67 | 70 | 75 | 76 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /packages/fastify-svelte/index.js: -------------------------------------------------------------------------------- 1 | // Used to send a readable stream to reply.send() 2 | import { Readable } from 'stream' 3 | 4 | // fastify-vite's minimal HTML templating function, 5 | // which extracts interpolation variables from comments 6 | // and returns a function with the generated code 7 | import { createHtmlTemplateFunction } from '@fastify/vite' 8 | 9 | // Used to safely serialize JavaScript into 10 | // ' 87 | ) 88 | } 89 | }), 90 | })) 91 | // Send out header and readable stream with full response 92 | this.type('text/html') 93 | this.send(readable) 94 | } 95 | } 96 | 97 | export async function createRenderFunction ({ routes, Root }) { 98 | // create is exported by client/index.js 99 | return function (req) { 100 | // Create convenience-access routeMap 101 | const routeMap = Object.fromEntries(routes.toJSON().map((route) => { 102 | return [route.path, route] 103 | })) 104 | // Creates main React component with all the SSR context it needs 105 | const app = !req.route.clientOnly && Root.render({ 106 | url: req.url, 107 | payload: { 108 | routes, 109 | routeMap, 110 | serverRoute: req.route, 111 | }, 112 | }) 113 | // Perform SSR, i.e., turn app.instance into an HTML fragment 114 | // The SSR context data is passed along so it can be inlined for hydration 115 | return { routes, context: req.route, app } 116 | } 117 | } 118 | 119 | export function createRouteHandler (client, scope, config) { 120 | return function (req, reply) { 121 | reply.html(reply.render(req)) 122 | return reply 123 | } 124 | } 125 | 126 | export function createRoute ({ client, handler, errorHandler, route }, scope, config) { 127 | const onRequest = async function onRequest (req, reply) { 128 | req.route = await RouteContext.create( 129 | scope, 130 | req, 131 | reply, 132 | route, 133 | client.context, 134 | ) 135 | } 136 | if (route.getData) { 137 | // If getData is provided, register JSON endpoint for it 138 | scope.get(`/-/data${route.path}`, { 139 | onRequest, 140 | async handler (req, reply) { 141 | reply.send(await route.getData(req.route)) 142 | }, 143 | }) 144 | } 145 | 146 | // See https://github.com/fastify/fastify-dx/blob/main/URMA.md 147 | const hasURMAHooks = Boolean( 148 | route.getData || route.getMeta || route.onEnter, 149 | ) 150 | 151 | // Extend with route context initialization module 152 | RouteContext.extend(client.context) 153 | 154 | scope.get(route.path, { 155 | onRequest, 156 | // If either getData or onEnter are provided, 157 | // make sure they run before the SSR route handler 158 | ...hasURMAHooks && { 159 | async preHandler (req, reply) { 160 | try { 161 | if (route.getData) { 162 | req.route.data = await route.getData(req.route) 163 | } 164 | if (route.getMeta) { 165 | req.route.head = await route.getMeta(req.route) 166 | } 167 | if (route.onEnter) { 168 | if (!req.route.data) { 169 | req.route.data = {} 170 | } 171 | const result = await route.onEnter(req.route) 172 | Object.assign(req.route.data, result) 173 | } 174 | } catch (err) { 175 | if (config.dev) { 176 | console.error(err) 177 | } 178 | req.route.error = err 179 | } 180 | }, 181 | }, 182 | handler, 183 | errorHandler, 184 | ...route, 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /packages/fastify-solid/index.js: -------------------------------------------------------------------------------- 1 | // Used to send a readable stream to reply.send() 2 | import { Readable } from 'stream' 3 | 4 | // Helper to make the stream returned renderToPipeableStream() 5 | // behave like an event emitter and facilitate error handling in Fastify 6 | import Minipass from 'minipass' 7 | 8 | // fastify-vite's minimal HTML templating function, 9 | // which extracts interpolation variables from comments 10 | // and returns a function with the generated code 11 | import { createHtmlTemplateFunction } from '@fastify/vite' 12 | 13 | // Used to safely serialize JavaScript into 14 | // ' 85 | ), 86 | }, 87 | }), 88 | })) 89 | // Send out header and readable stream with full response 90 | this.type('text/html') 91 | this.send(readable) 92 | } 93 | } 94 | 95 | export async function createRenderFunction ({ 96 | renderToString, 97 | renderToStream, 98 | routes, 99 | create, 100 | }) { 101 | // Convenience-access routeMap 102 | const routeMap = Object.fromEntries( 103 | routes.toJSON().map(route => [route.path, route]), 104 | ) 105 | return async function (req) { 106 | let stream = null 107 | let body = null 108 | if (!req.route.clientOnly) { 109 | // Creates main Solid component with all the SSR context it needs 110 | const app = create({ 111 | url: req.url, 112 | payload: { 113 | routes, 114 | routeMap, 115 | serverRoute: req.route, 116 | }, 117 | }) 118 | if (req.route.streaming) { 119 | const duplex = new Minipass() 120 | renderToStream(app).pipe(duplex) 121 | stream = duplex 122 | } else { 123 | body = await renderToString(app) 124 | } 125 | } 126 | // Perform SSR, i.e., turn app.instance into an HTML fragment 127 | // The SSR context data is passed along so it can be inlined for hydration 128 | return { 129 | routes, 130 | context: req.route, 131 | stream, 132 | body, 133 | } 134 | } 135 | } 136 | 137 | export function createRouteHandler (client, scope, config) { 138 | return async function (req, reply) { 139 | reply.html(await reply.render(req)) 140 | return reply 141 | } 142 | } 143 | 144 | export function createRoute ({ client, handler, errorHandler, route }, scope, config) { 145 | const onRequest = async function onRequest (req, reply) { 146 | req.route = await RouteContext.create( 147 | scope, 148 | req, 149 | reply, 150 | route, 151 | client.context, 152 | ) 153 | } 154 | if (route.getData) { 155 | // If getData is provided, register JSON endpoint for it 156 | scope.get(`/-/data${route.path}`, { 157 | onRequest, 158 | async handler (req, reply) { 159 | reply.send(await route.getData(req.route)) 160 | }, 161 | }) 162 | } 163 | 164 | // See https://github.com/fastify/fastify-dx/blob/main/URMA.md 165 | const hasURMAHooks = Boolean( 166 | route.getData || route.getMeta || route.onEnter, 167 | ) 168 | 169 | // Extend with route context initialization module 170 | RouteContext.extend(client.context) 171 | 172 | scope.get(route.path, { 173 | onRequest, 174 | // If either getData or onEnter are provided, 175 | // make sure they run before the SSR route handler 176 | ...hasURMAHooks && { 177 | async preHandler (req, reply) { 178 | try { 179 | if (route.getData) { 180 | req.route.data = await route.getData(req.route) 181 | } 182 | if (route.getMeta) { 183 | req.route.head = await route.getMeta(req.route) 184 | } 185 | if (route.onEnter) { 186 | if (!req.route.data) { 187 | req.route.data = {} 188 | } 189 | const result = await route.onEnter(req.route) 190 | Object.assign(req.route.data, result) 191 | } 192 | } catch (err) { 193 | if (config.dev) { 194 | console.error(err) 195 | } 196 | req.route.error = err 197 | } 198 | }, 199 | }, 200 | handler, 201 | errorHandler, 202 | ...route, 203 | }) 204 | } 205 | -------------------------------------------------------------------------------- /URMA.md: -------------------------------------------------------------------------------- 1 | # The Universal Route Module API Specification 2 | 3 | This document contains the proposed specification for a standard **Route Module API** frameworks could adopt to improve code portability and reduce vendor lock-in. This specification is heavily inspired by Remix's [**Route Module API**](https://remix.run/docs/en/v1/api/conventions#route-module-api). 4 | 5 | ## Status 6 | 7 | This is a **living document** in its earliest stage — a lot of things might still change as we develop **Fastify DX** and following the feedback of all interested parties (e.g., other framework authors willing to collaborate). 8 | 9 | As it stands, this API can be described with the following TypeScript interfaces: 10 | 11 | ```ts 12 | interface RouteMeta { 13 | title: string | null, 14 | html: Record | null 15 | body: Record | null 16 | meta: Record[] | null, 17 | link: Record[] | null, 18 | } 19 | 20 | interface RouteContext { 21 | readonly url: string 22 | readonly static: boolean 23 | readonly server: boolean 24 | readonly client: boolean 25 | fetch: () => Promise 26 | reload: () => Promise 27 | meta?: RouteMeta 28 | data?: any 29 | } 30 | 31 | interface RouteModule { 32 | generated?: boolean 33 | streaming?: boolean 34 | clientOnly?: boolean 35 | serverOnly?: boolean 36 | onRequest?: (context: RouteContext) => void | Promise 37 | onEnter?: (context: RouteContext) => void | Promise 38 | onLeave?: (context: RouteContext) => void | Promise 39 | getData?: (context: RouteContext) => any | Promise 40 | getMeta?: (context: RouteContext) => RouteMeta | Promise 41 | } 42 | ``` 43 | 44 | ## Problem 45 | 46 | The problem this specification tries to solve is how to determine the behaviour of web pages that can be both **server-side rendered** and (continuosly) **client-side rendered** in an uniform way. It tries to answer these questions: 47 | 48 | - How to determine the **rendering mode** and **rendering settings** of a web page. 49 | - How to implement `` tags, HTML `` and `` attributes of a web page, both for **SSR** and **CSR route navigation**. 50 | - How to implement a **data loading function** for a web page, both for **SSR** and **CSR route navigation**. 51 | - How to implement **static data payloads** for web pages being **statically generated**. 52 | 53 | All existing frameworks have different answers to these questions. There's a great opportunity for standardization in this area that would improve **code portability** across frameworks, help make underlying patterns **more transparent** and let framework authors focus on enhancing developer experience in upward layers where **more value** can be provided. 54 | 55 | ## Solution 56 | 57 | Framework route components are typically loaded as JavaScript modules, where the actual component instance is conventionally exposed through the `default` export. Frameworks can leverage route component JavaScript modules to collect other properties, as has been made widely popular by [Next.js](https://nextjs.org/) and its [data fetching function exports](https://nextjs.org/docs/basic-features/data-fetching/overview). 58 | 59 | I belive Remix laid substantial groundwork for a generic API specifying several route module core functionalities. This specification builds on top of it, expanding on it and trying to fill in the gaps, and offering some subtle modifications as well. 60 | 61 | 62 | 63 |
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 `` of the page and other page-level tags. Finally the route component function is executed, which in this case, has a data property prepopulated by the underlying hypothetical framework. 74 | 75 | </td> 76 | <td width="600px"><br> 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 | <h1>{data.title}</h1> 100 | <p>{data.body}</p> 101 | </> 102 | ) 103 | } 104 | ``` 105 | 106 | </td> 107 | </tr> 108 | </table> 109 | 110 | ## Route Context Object 111 | 112 | <table> 113 | <tr> 114 | <td width="400px" valign="top"> 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 | </td> 125 | <td width="600px"><br> 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<Response> 136 | reload: () => Promise<Response> 137 | meta?: RouteMeta 138 | data?: any 139 | } 140 | ``` 141 | 142 | </td> 143 | </tr> 144 | </table> 145 | 146 | ## Named Exports: Rendering Options 147 | 148 | By default, route modules **should** run universally, both on the server and on the client (with client-side hydration), but it **should** be possible specify different rendering modes or settings for a route module. 149 | 150 | <table> 151 | <tr> 152 | <td width="400px" valign="top"> 153 | 154 | ### `streaming` 155 | 156 | Determines if **server-side rendering** should take place in **streaming mode** if the underlying framework supports it. 157 | 158 | </td> 159 | <td width="600px"><br> 160 | 161 | It must be set with a `boolean`: 162 | 163 | ```js 164 | export const streaming = true 165 | ``` 166 | 167 | </td> 168 | </tr> 169 | </table> 170 | 171 | <table> 172 | <tr> 173 | <td width="400px" valign="top"> 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 | </td> 180 | <td width="600px"><br> 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 | </td> 197 | </tr> 198 | </table> 199 | 200 | <table> 201 | <tr> 202 | <td width="400px" valign="top"> 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 | </td> 209 | <td width="600px"><br> 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 | </td> 226 | </tr> 227 | </table> 228 | 229 | ## Named Exports: Route Navigation and Hydration 230 | 231 | <table> 232 | <tr> 233 | <td width="400px" valign="top"> 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 | </td> 242 | <td width="600px"><br> 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 | </td> 259 | </tr> 260 | </table> 261 | 262 | <table> 263 | <tr> 264 | <td width="400px" valign="top"> 265 | 266 | ## `getData()` 267 | 268 | Determines the **server data function** for the route. It must be implemented in way that it can run both on the server prior to **server-side rendering** and **through an endpoint that can be fetched** prior to **client-side route navigation**. This function is expected to return an object, whose properties should be merged into the passed route context under a `data` property, which should be also seamlessly hydrated on first-render if populated during SSR. In the case of **static generation**, its return value should be embedded within each page as an inline `<script>`. 269 | 270 | </td> 271 | <td width="600px"><br> 272 | 273 | ```js 274 | export async function getData (context) { 275 | const url = context.req 276 | const data = await context.dataReturningFunction() 277 | return { data } 278 | } 279 | ``` 280 | 281 | > This function could be used to reimplement `gerServerSideProps()` in Next.js, `load()` in SvelteKit and `loader()` in Remix. 282 | 283 | </td> 284 | </tr> 285 | </table> 286 | 287 | ## Named Exports: Page Metadata 288 | 289 | This specification recommends that `<head>` serialization (and other page-level tags) take place indepedently from SSR, for optimal performance and opening the possibility of delivering an application shell for CSR-only rendering. SSR may still yield additional `<link>` preload tags for dynamic component assets, but that should happen isolatedly from the main page metadata so the ability to stream it right away to the client is preserved. 290 | 291 | <table width="100%"> 292 | <tr> 293 | <td width="400px" valign="top"> 294 | 295 | ## `getMeta()` 296 | 297 | Determines HTML tags such as `<title>`, `<meta>` and `<link>`, as well as `<html>` and `<body>` tag attributes. It must be set either with an object or a function that returns an object described by the `RouteMeta` interface. 298 | 299 | </td> 300 | <td width="600px"><br> 301 | 302 | ```js 303 | export async function getMeta (context) { 304 | const title = context.post.title 305 | return { 306 | title: 'Page title', 307 | html: { lang: 'en' }, 308 | meta: [ 309 | { name: 'twitter:title', content: title } 310 | ] 311 | } 312 | } 313 | ``` 314 | 315 | </td> 316 | </tr> 317 | </table> 318 | 319 | 320 | ## Acknowledgements 321 | 322 | Special thanks to my employeer, [NearForm](https://nearform.com), for sponsoring this project, and [Matteo Collina](https://github.com/mcollina) and [Simone Busoli](https://github.com/simoneb) for their honest feedback and guidance. Also a big shout out to [Sébastien Chopin](https://github.com/Atinux), [Pooya Parsa](https://github.com/pi0) and [Xin Du](https://github.com/clarkdo) from Nuxt.js — who I learned a lot from. 323 | 324 | This specification owes a lot to [Ryan Florence](https://github.com/ryanflorence) and [Michael Jackson](https://github.com/mjackson) for their time spent designing Remix and coming up with an excellent Route Module API for their framework. [Guillermo Rauch](https://github.com/rauchg) and [Tim Neutkens](https://github.com/timneutkens) also need to be ackowledged for their work in Next.js which helped shape up a lot of the developer experience we've come to expect from modern frameworks. 325 | -------------------------------------------------------------------------------- /packages/fastify-svelte/README.md: -------------------------------------------------------------------------------- 1 | <br> 2 | 3 | # @fastify/svelte [![NPM version](https://img.shields.io/npm/v/@fastify/svelte.svg?style=flat)](https://www.npmjs.com/package/@fastify/svelte) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) 4 | 5 | **Fastify DX for Svelte** (**`@fastify/svelte`**) is a renderer for [**@fastify/vite**](https://github.com/fastify/fastify-vite). 6 | 7 | It lets you run and SSR (server-side render) **Svelte applications built with Vite** on [Fastify](https://fastify.io/), with a minimal and transparent **server-first approach** — everything starts with `server.js`, your actual Fastify server). 8 | 9 | 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/), [Valtio](https://github.com/pmndrs/valtio) and [Svelte Navigator](https://github.com/EmilTholin/svelte-navigator). 10 | 11 | ## Quick Start 12 | 13 | Ensure you have **Node v16+**. 14 | 15 | Make a copy of [**starters/svelte**](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte). If you have [`degit`](https://github.com/Rich-Harris/degit), run the following from a new directory: 16 | 17 | ```bash 18 | degit fastify/fastify-dx/starters/svelte 19 | ``` 20 | 21 | > **If you're starting a project from scratch**, you'll need these packages installed. 22 | > 23 | > ```bash 24 | > npm i fastify fastify-vite fastify-dx-svelte -P 25 | > npm i @sveltejs/vite-plugin-svelte -D 26 | > ``` 27 | 28 | 29 | Run `npm install`. 30 | 31 | Run `npm run dev`. 32 | 33 | Visit `http://localhost:3000/`. 34 | 35 | ## What's Included 36 | 37 | That will get you a **starter template** with: 38 | 39 | - A minimal [Fastify](https://github.com/fastify/fastify) server. 40 | - Some dummy API routes. 41 | - A `pages/` folder with some [demo routes](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte/client/pages). 42 | - All configuration files. 43 | 44 | It also includes some _**opinionated**_ essentials: 45 | 46 | - [**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/). 47 | 48 | - [**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). 49 | 50 | - [**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. <br>Svelte support is provided via [Sveltio](https://github.com/wobsoriano/sveltio) by [Robert Soriano](https://robsoriano.com/). 51 | 52 | 53 | ## Package Scripts 54 | 55 | - `npm run dev` boots the development server. 56 | - `npm run build` creates the production bundle. 57 | - `npm run serve` serves the production bundle. 58 | 59 | 60 | ## Basic setup 61 | 62 | The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte) 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. 63 | 64 | If you want 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. 65 | 66 | 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/): 67 | 68 | ```js 69 | const server = Fastify({ logger: true }) 70 | ``` 71 | 72 | The starter template's `server.js` file: 73 | 74 | ```js 75 | import Fastify from 'fastify' 76 | import FastifyVite from 'fastify-vite' 77 | import FastifyDXSvelte from 'fastify-dx-svelte' 78 | 79 | const server = Fastify() 80 | 81 | await server.register(FastifyVite, { 82 | root: import.meta.url, 83 | renderer: FastifyDXSvelte, 84 | }) 85 | 86 | await server.vite.ready() 87 | await server.listen(3000) 88 | ``` 89 | 90 | The starter template's [`vite.config.js`](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/vite.config.js) file: 91 | 92 | ```js 93 | import { join, dirname } from 'path' 94 | import { fileURLToPath } from 'url' 95 | 96 | import { svelte as viteSvelte } from '@sveltejs/vite-plugin-svelte' 97 | import viteSvelteFastifyDX from 'fastify-dx-svelte/plugin' 98 | import unocss from 'unocss/vite' 99 | import { extractorSvelte } from '@unocss/core' 100 | 101 | const path = fileURLToPath(import.meta.url) 102 | 103 | const root = join(dirname(path), 'client') 104 | const plugins = [ 105 | unocss({ extractors: [extractorSvelte] }), 106 | viteSvelte({ 107 | compilerOptions: { 108 | hydratable: true, 109 | } 110 | }), 111 | viteSvelteFastifyDX(), 112 | ] 113 | 114 | export default { root, plugins } 115 | ``` 116 | 117 | 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. 118 | 119 | </td> 120 | </tr> 121 | </table> 122 | 123 | ### Route exports 124 | 125 | Fastify DX picks up exports from route modules to determine route behavior and functionality, as per the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md). 126 | 127 | To add those exports, you must use `<script context="module">` (Svelte-specific syntax) which determines the script that runs in the general module namespace for a Svelte component. So in Fastify DX Svelte applications, it's commonplace to have two code blocks, a regular one and another with `context` set to `module`: 128 | 129 | ```html 130 | <script context="module"> 131 | export function getData () { 132 | return { message: 'Hello from getData!' } 133 | } 134 | <script> 135 | 136 | <script> 137 | import { useRouteContext } = '/dx:core.js' 138 | const { data } = useRouteContext() 139 | </script> 140 | 141 | <p>{data.message}</p> 142 | ``` 143 | 144 | 145 | ## Project Structure 146 | 147 | The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte) looks like this: 148 | 149 | ``` 150 | ├── server.js 151 | ├── client/ 152 | │ ├── index.js 153 | │ ├── context.js 154 | │ ├── root.svelte 155 | │ ├── index.html 156 | │ ├── layouts/ 157 | │ │ ├── default.svelte 158 | │ │ └── auth.svelte 159 | │ └── pages/ 160 | │ ├── index.svelte 161 | │ ├── client-only.svelte 162 | │ ├── server-only.svelte 163 | │ ├── using-data.svelte 164 | │ └── using-store.svelte 165 | ├── vite.config.js 166 | └── package.json 167 | ``` 168 | 169 | Several internal files are provided as virtual modules by Fastify DX. They are located inside the `fastify-dx-svelte` package in `node_modules`, and dynamically loaded so you don't have to worry about them unless you want them overriden. 170 | 171 | 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]. 172 | 173 | [virtual-modules]: https://github.com/fastify/fastify-dx/blob/main/docs/svelte/virtual-modules.md 174 | 175 | 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 Svelte** as a renderer adapter to **fastify-vite**. 176 | 177 | 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. 178 | 179 | > Right now, it's mostly a **boilerplate file** because it must exist but it will also probably never need to be changed. 180 | 181 | It exports your application's factory function (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/svelte/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). 182 | 183 | 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. 184 | 185 | > You can expand this file with additional `<meta>` and `<link>` tags if you wish, provided you don't remove any of the placeholders. 186 | 187 | This files links to `/dx:mount.js`, which is a virtual module provided by Fastify DX. 188 | 189 | Virtual modules are covered [here][virtual-modules]. 190 | 191 | 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]. 192 | 193 | [routing-config]: https://github.com/fastify/fastify-dx/blob/main/docs/svelte/routing-config.md 194 | 195 | The `client/layouts/` directory contains your route layout modules, which can be associated to any route. By default, `layouts/default.svelte` 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.svelte`, to demonstrate a more advanced use of layouts. 196 | 197 | [routing-config]: https://github.com/fastify/fastify-dx/blob/main/docs/svelte/routing-config.md 198 | 199 | 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. 200 | 201 | [route-context]: https://github.com/fastify/fastify-dx/blob/main/docs/svelte/route-context.md 202 | 203 | # Rendering modes 204 | 205 | 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). Fastify DX for Svelte supports all of these modes minus streaming, which is currently not yet supported by Svelte itself. 206 | 207 | ## `serverOnly` 208 | 209 | 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. 210 | 211 | 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. 212 | 213 | ```html 214 | <script context="module"> 215 | export const serverOnly = true 216 | </script> 217 | 218 | <p>This route is rendered on the server only!</p> 219 | ``` 220 | 221 | [This example](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/client/pages/server-only.svelte) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte). 222 | 223 | ## `clientOnly` 224 | 225 | 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. 226 | 227 | 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. 228 | 229 | ```html 230 | <script context="module"> 231 | export const clientOnly = true 232 | </script> 233 | 234 | <p>This route is rendered on the client only!</p> 235 | ``` 236 | 237 | [This example](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/client/pages/client-only.svelte) is part of the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/svelte). 238 | 239 | ## Routing Configuration 240 | 241 | 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. 242 | 243 | ### Dynamic parameters 244 | 245 | 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`). 246 | 247 | ```js 248 | // ... 249 | const plugins = [ 250 | // ... 251 | viteSvelteFastifyDX({ paramPattern: /\$(\w+)/ }), 252 | ] 253 | ``` 254 | 255 | ### Routes location 256 | 257 | You can also change the glob pattern used to determine where to route modules from. 258 | 259 | 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: 260 | 261 | ```js 262 | // ... 263 | const plugins = [ 264 | // ... 265 | viteSvelteFastifyDX({ globPattern: '/views/**/*.svelte' }), 266 | ] 267 | ``` 268 | 269 | ### View modules 270 | 271 | 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**. 272 | 273 | Additionally, [**you can provide your own routes**](https://github.com/fastify/fastify-dx/tree/dev/packages/fastify-dx-svelte#dxroutesjs). 274 | 275 | ```html 276 | <script context="module"> 277 | export const path = '/my-page' 278 | </script> 279 | 280 | <p>Route with path export</p> 281 | ``` 282 | 283 | ## Isomorphic data prefetching 284 | 285 | 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. 286 | 287 | ### `getData(ctx)` 288 | 289 | 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. 290 | 291 | The objet returned by `getData()` gets automatically assigned as `data` in the [universal route context](https://github.com/fastify/fastify-dx/blob/main/docs/svelte/route-context.md) object and is accessible from `getMeta()` and `onEnter()` hooks and also via the `useRouteContext()` hook. 292 | 293 | ```html 294 | <script context="module"> 295 | export function getData (ctx) { 296 | return { 297 | message: 'Hello from getData!', 298 | } 299 | } 300 | </script> 301 | 302 | <script> 303 | import { useRouteContext } from '/dx:core.js' 304 | const { data } = useRouteContext() 305 | </script> 306 | 307 | <p>{data.message}</p> 308 | ``` 309 | 310 | ## Route Context 311 | 312 | ### Initialization module 313 | 314 | 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/). 315 | 316 | **Consuming the route context:** 317 | 318 | ```js 319 | import { 320 | useRouteContext 321 | } from '/dx:core.js' 322 | 323 | // ... 324 | const { 325 | state, 326 | actions 327 | } = useRouteContext() 328 | 329 | // ... 330 | actions.addTodoItem(state, value) 331 | ``` 332 | 333 | See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/client/pages/using-store.vue) in the starter template. 334 | 335 | 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. 336 | 337 | ```js 338 | import ky from 'ky-universal' 339 | 340 | export default (ctx) => { 341 | if (ctx.server) { 342 | // Populate state.todoList on the server 343 | ctx.state.todoList = ctx.server.db.todoList 344 | // It'll get automatically serialized to the client on first render! 345 | } 346 | } 347 | 348 | export const $fetch = ky.extend({ 349 | prefixUrl: 'http://localhost:3000' 350 | }) 351 | 352 | // Must be a function so each request can have its own state 353 | export const state = () => ({ 354 | todoList: null, 355 | }) 356 | 357 | export const actions = { 358 | async addTodoItem (state, item) { 359 | await $fetch.put('api/todo/items', { 360 | json: { item }, 361 | }) 362 | state.todoList.push(item) 363 | }, 364 | } 365 | ``` 366 | 367 | See the [full example](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/client/context.js) in the starter template. 368 | 369 | ### The `useRouteContext()` hook 370 | 371 | 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**. 372 | 373 | 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. 374 | 375 | It automatically causes the component to be suspended if the `getData()`, `getMeta()` and `onEnter()` functions are asynchronous. 376 | 377 | ```html 378 | <script> 379 | import { useRouteContext } from '/dx:core.js' 380 | const { data } = useRouteContext() 381 | </script> 382 | 383 | <p>{data.message}</p> 384 | ``` 385 | 386 | ### Execution order 387 | 388 | This graph illustrates the execution order to expect from route context initialization. 389 | 390 | ``` 391 | context.js default function export 392 | └─ getData() function export 393 | └─ getMeta() function export 394 | └─ onEnter() function export 395 | └─ Route module 396 | ``` 397 | 398 | 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. 399 | 400 | 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. 401 | 402 | 403 | ## Universal Route Enter Event 404 | 405 | ### `onEnter(ctx)` 406 | 407 | 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. 408 | 409 | It receives the [universal route context][route-context] as first parameter, so you can make changes to `data`, `meta` and `state` if needed. 410 | 411 | [route-context]: https://github.com/fastify/fastify-dx/blob/main/docs/svelte/route-context.md 412 | 413 | ```html 414 | <script context="module"> 415 | export function onEnter (ctx) { 416 | if (ctx.server?.underPressure) { 417 | ctx.clientOnly = true 418 | } 419 | } 420 | </script> 421 | 422 | <p>No pre-rendered HTML sent to the browser.</p> 423 | ``` 424 | 425 | 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. 426 | 427 | 428 | ## Route Layouts 429 | 430 | Fastify DX will automatically load layouts from the `layouts/` folder. By default, `/dx:layouts/default.svelte` is used — that is, if a project is missing a `layouts/defaults.svelte` file, the one provided by Fastify DX is used instead. 431 | 432 | See the section on [Virtual Modules](https://github.com/fastify/fastify-dx/blob/main/docs/svelte/virtual-modules.md) to learn more about this. 433 | 434 | You assign a layout to a route by exporting `layout`. 435 | 436 | See [`pages/using-auth.svelte`](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/pages/using-auth.svelte) in the starter template: 437 | 438 | ```js 439 | export const layout = 'auth' 440 | ``` 441 | 442 | That'll will cause the route to be wrapped in the layout component exported by [`layouts/auth.svelte`](https://github.com/fastify/fastify-dx/blob/main/starters/svelte/layouts/auth.svelte): 443 | 444 | ```html 445 | <script> 446 | import { useRouteContext } from '/dx:core.js' 447 | const { snapshot, actions, state } = useRouteContext() 448 | </script> 449 | 450 | <div class="contents"> 451 | {#if !$snapshot.user.authenticated} 452 | <p>This route needs authentication.</p> 453 | <button on:click={() => actions.authenticate(state)}> 454 | Click this button to authenticate. 455 | </button> 456 | {:else} 457 | <slot /> 458 | {/if} 459 | </div> 460 | ``` 461 | 462 | Note that like routes, it has access to `useRouteContext()`. 463 | 464 | 465 | ## Meta Tags 466 | 467 | 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. 468 | 469 | > 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-dx-svelte/index.js#L57) to [fastify-vite](https://github.com/fastify/fastify-vite). 470 | 471 | ### `getMeta()` 472 | 473 | 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. 474 | 475 | It receives the [route context](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/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. 476 | 477 | ```html 478 | <script context="module"> 479 | export function getMeta (ctx) { 480 | return { 481 | title: 'Route Title', 482 | meta: [ 483 | { name: 'twitter:title', value: 'Route Title' }, 484 | ] 485 | } 486 | } 487 | </script> 488 | 489 | <p>Route with meta tags.</p> 490 | ``` 491 | 492 | 493 | ## Virtual Modules 494 | 495 | **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. 496 | 497 | Fastify DX virtual modules are **fully ejectable**. For instance, the starter template relies on the `/dx:root.svelte` virtual module to provide the Vue shell of your application. If you copy the `root.svelte` file [from the fastify-dx-svelte package](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/root.svelte) 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.svelte` of its own to include UnoCSS. 498 | 499 | Aside from `root.svelte`, the starter template comes with two other virtual modules already ejected and part of the local project — `context.js` and `layouts/default.svelte`. If you don't need to customize them, you can safely removed them from your project. 500 | 501 | ### `/dx:root.svelte` 502 | 503 | This is the root Svelte 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. 504 | 505 | ```html 506 | <script> 507 | import 'uno.css' 508 | import { proxy } from 'sveltio' 509 | import { Router, Route } from 'svelte-routing' 510 | import DXRoute from '/dx:route.svelte' 511 | 512 | export let url = null 513 | export let payload 514 | 515 | let state = proxy(payload.serverRoute.state) 516 | </script> 517 | 518 | <Router url="{url}"> 519 | {#each payload.routes as { path, component }} 520 | <Route path="{path}" let:location> 521 | <DXRoute 522 | path={path} 523 | location={location} 524 | state={state} 525 | payload={payload} 526 | component={component} /> 527 | </Route> 528 | {/each} 529 | </Router> 530 | ``` 531 | 532 | ### `/dx:route.svelte` 533 | 534 | This is used by `root.svelte` to enhance your route modules with the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md). 535 | 536 | <b>You'll rarely need to customize this file.</b> 537 | 538 | ```html 539 | <script> 540 | import { setContext } from 'svelte' 541 | import Loadable from 'svelte-loadable' 542 | import { routeContext, jsonDataFetch } from '/dx:core.js' 543 | import layouts from '/dx:layouts.js' 544 | 545 | const isServer = import.meta.env.SSR 546 | 547 | setContext(routeContext, { 548 | get routeContext () { 549 | return ctx 550 | } 551 | }) 552 | 553 | export let path 554 | export let component 555 | export let payload 556 | export let state 557 | export let location 558 | 559 | let ctx = payload.routeMap[path] 560 | 561 | ctx.state = state 562 | ctx.actions = payload.serverRoute.actions 563 | 564 | if (isServer) { 565 | ctx.layout = payload.serverRoute.layout ?? 'default' 566 | ctx.data = payload.serverRoute.data 567 | ctx.state = state 568 | } 569 | 570 | async function setup () { 571 | if (payload.serverRoute.firstRender) { 572 | ctx.data = payload.serverRoute.data 573 | ctx.layout = payload.serverRoute.layout ?? 'default' 574 | payload.serverRoute.firstRender = false 575 | return 576 | } 577 | ctx.layout = ctx.layout ?? 'default' 578 | const { getMeta, getData, onEnter } = await ctx.loader() 579 | if (getData) { 580 | try { 581 | const fullPath = `${location.pathname}${location.search}` 582 | const updatedData = await jsonDataFetch(fullPath) 583 | if (!ctx.data) { 584 | ctx.data = {} 585 | } 586 | if (updatedData) { 587 | Object.assign(ctx.data, updatedData) 588 | } 589 | ctx.error = null 590 | } catch (error) { 591 | ctx.error = error 592 | } 593 | } 594 | if (getMeta) { 595 | const updatedMeta = await getMeta(ctx) 596 | if (updatedMeta) { 597 | payload.head.update(updatedMeta) 598 | } 599 | } 600 | if (onEnter) { 601 | const updatedData = await onEnter(ctx) 602 | if (updatedData) { 603 | Object.assign(ctx.data, updatedData) 604 | } 605 | } 606 | } 607 | 608 | let setupClientRouteContext = !isServer && setup() 609 | </script> 610 | 611 | {#if isServer} 612 | <svelte:component this={layouts[ctx.layout].default}> 613 | <svelte:component this={component} /> 614 | </svelte:component> 615 | {:else} 616 | {#await setupClientRouteContext}{:then} 617 | <svelte:component this={layouts[ctx.layout].default}> 618 | <Loadable loader={component} /> 619 | </svelte:component> 620 | {/await} 621 | {/if} 622 | ``` 623 | 624 | What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/route.svelte). 625 | 626 | 627 | ### `/dx:routes.js` 628 | 629 | 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. 630 | 631 | ```js 632 | export default import.meta.env.SSR 633 | ? createRoutes(import.meta.globEager('$globPattern')) 634 | : hydrateRoutes(import.meta.glob('$globPattern')) 635 | ``` 636 | 637 | See [the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/routes.js) for the `createRoutes()` and `hydrateRoutes()` definitions. 638 | 639 | 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: 640 | 641 | ```js 642 | const routes = [ 643 | { 644 | path: '/', 645 | component: () => import('/custom/index.svelte'), 646 | } 647 | ] 648 | 649 | export default import.meta.env.SSR 650 | ? createRoutes(routes) 651 | : hydrateRoutes(routes) 652 | ```` 653 | 654 | **Nested routes aren't supported yet.** 655 | 656 | 657 | ### `/dx:core.js` 658 | 659 | Implements `useRouteContext()`. 660 | 661 | See its full definition [here](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/core.js). 662 | 663 | ### `/dx:layouts.js` 664 | 665 | This is responsible for loading **layout components**. It's part of `route.svelte` by default. If a project has no `layouts/default.svelte` 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. 666 | 667 | <b>You'll rarely need to customize this file.</b> 668 | 669 | ```js 670 | import DefaultLayout from '/dx:layouts/default.svelte' 671 | 672 | const appLayouts = import.meta.globEager('/layouts/*.svelte') 673 | 674 | appLayouts['/layouts/default.svelte'] ??= DefaultLayout 675 | 676 | export default Object.fromEntries( 677 | Object.keys(appLayouts).map((path) => { 678 | const name = path.slice(9, -7) 679 | return [name, appLayouts[path]] 680 | }), 681 | ) 682 | 683 | ``` 684 | 685 | What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/layouts.js). 686 | 687 | ### `/dx:mount.js` 688 | 689 | 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. 690 | 691 | <b>You'll rarely need to customize this file.</b> 692 | 693 | [See the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-svelte/virtual/mount.js) for the `mount()` function definition. 694 | 695 | 696 | 697 | ## Maintainance 698 | 699 | Created and maintained by [Jonas Galvez](https://github.com/sponsors/galvez), **Principal Engineer** and **Open Sourcerer** at [NodeSource](https://nodesource.com). 700 | 701 | New contributors are extremely welcome to look for [good first issues](https://github.com/fastify/fastify-dx/labels/good%20first%20issue). 702 | 703 | ## Gold Sponsors 704 | 705 | <a href="https://nodesource.com"><img width="200px" src="https://user-images.githubusercontent.com/12291/206885948-3fa742a2-1057-4db2-8648-46f5cb673461.svg"></a> 706 | 707 | [Contact me](mailto:jonasgalvez@gmail.com) to add your company's logo here. 708 | 709 | ## GitHub Sponsors 710 | 711 | - [**Duc-Thien Bui**](https://github.com/aecea) 712 | - [**Tom Preston-Werner**](https://github.com/mojombo) 713 | - [**Clifford Fajardo**](https://github.com/cliffordfajardo) 714 | - [**David Adam Coffey**](https://github.com/dacoffey) 715 | - [**Mezereon**](https://github.com/mezereon-co) 716 | - [**A-J Roos**](https://github.com/Asjas) 717 | - [**James Isaacs**](https://github.com/jamesisaacs2) 718 | 719 | [**Click here**](https://github.com/sponsors/galvez) to add your name to this list. 720 | 721 | _Thank you!_ 722 | -------------------------------------------------------------------------------- /packages/fastify-solid/README.md: -------------------------------------------------------------------------------- 1 | <br> 2 | 3 | # @fastify/solid [![NPM version](https://img.shields.io/npm/v/@fastify/solid.svg?style=flat)](https://www.npmjs.com/package/@fastify/solid) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) 4 | 5 | **Fastify DX for Solid** (**`@fastify/solid`**) is a renderer for [**@fastify/vite**](https://github.com/fastify/fastify-vite). 6 | 7 | It lets you run and SSR (server-side render) **Solid applications built with Vite** on [Fastify](https://fastify.io/), with a minimal and transparent **server-first approach** — everything starts with `server.js`, your actual Fastify server. 8 | 9 | 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/) and [Solid Router](https://github.com/solidjs/solid-router). 10 | 11 | ## Quick Start 12 | 13 | Ensure you have **Node v16+**. 14 | 15 | Make a copy of [**starters/solid**](https://github.com/fastify/fastify-dx/tree/dev/starters/solid). If you have [`degit`](https://github.com/Rich-Harris/degit), run the following from a new directory: 16 | 17 | ```bash 18 | degit fastify/fastify-dx/starters/solid 19 | ``` 20 | 21 | > **If you're starting a project from scratch**, you'll need these packages installed. 22 | > 23 | > ```bash 24 | > npm i fastify fastify-vite fastify-dx-solid -P 25 | > npm i vite-plugin-solid -D 26 | > ``` 27 | 28 | 29 | Run `npm install`. 30 | 31 | Run `npm run dev`. 32 | 33 | Visit `http://localhost:3000/`. 34 | 35 | ## What's Included 36 | 37 | That will get you a **starter template** with: 38 | 39 | - A minimal [Fastify](https://github.com/fastify/fastify) server. 40 | - Some dummy API routes. 41 | - A `pages/` folder with some [demo routes](https://github.com/fastify/fastify-dx/tree/dev/starters/solid/client/pages). 42 | - All configuration files. 43 | 44 | It also includes some _**opinionated**_ essentials: 45 | 46 | - [**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/). 47 | 48 | - [**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). 49 | 50 | ## Package Scripts 51 | 52 | - `npm run dev` boots the development server. 53 | - `npm run build` creates the production bundle. 54 | - `npm run serve` serves the production bundle. 55 | 56 | ## Basic setup 57 | 58 | The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/solid) 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. 59 | 60 | If you want 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. 61 | 62 | 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/): 63 | 64 | ```js 65 | const server = Fastify({ logger: true }) 66 | ``` 67 | 68 | The starter template's `server.js` file: 69 | 70 | ```js 71 | import Fastify from 'fastify' 72 | import FastifyVite from 'fastify-vite' 73 | import FastifyDXSolid from 'fastify-dx-solid' 74 | 75 | const server = Fastify() 76 | 77 | await server.register(FastifyVite, { 78 | root: import.meta.url, 79 | renderer: FastifyDXSolid, 80 | }) 81 | 82 | await server.vite.ready() 83 | await server.listen(3000) 84 | ``` 85 | 86 | The starter template's [`vite.config.js`](https://github.com/fastify/fastify-dx/blob/main/starters/solid/vite.config.js) file: 87 | 88 | ```js 89 | import { join, dirname } from 'path' 90 | import { fileURLToPath } from 'url' 91 | 92 | import viteSolid from 'vite-plugin-solid' 93 | import viteSolidFastifyDX from 'fastify-dx-solid/plugin' 94 | import unocss from 'unocss/vite' 95 | 96 | const path = fileURLToPath(import.meta.url) 97 | const root = join(dirname(path), 'client') 98 | 99 | const plugins = [ 100 | viteSolid({ ssr: true }), 101 | viteSolidFastifyDX(), 102 | unocss() 103 | ] 104 | 105 | const ssr = { 106 | noExternal: ['solid-app-router'], 107 | } 108 | 109 | export default { root, plugins, ssr } 110 | ``` 111 | 112 | 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. 113 | 114 | </td> 115 | </tr> 116 | </table> 117 | 118 | ## Project Structure 119 | 120 | The [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/solid) looks like this: 121 | 122 | ``` 123 | ├── server.js 124 | ├── client/ 125 | │ ├── index.js 126 | │ ├── context.js 127 | │ ├── root.jsx 128 | │ ├── index.html 129 | │ ├── layouts/ 130 | │ │ ├── default.jsx 131 | │ │ └── auth.jsx 132 | │ └── pages/ 133 | │ ├── index.jsx 134 | │ ├── client-only.jsx 135 | │ ├── server-only.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-dx-solid` 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/solid/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 Solid** 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 factory function (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/solid/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/solid/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/solid/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/solid/route-context.md 175 | 176 | # Rendering modes 177 | 178 | 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). Fastify DX for Svelte supports all of these modes minus streaming, which is currently not yet supported by Svelte itself. 179 | 180 | ## `streaming` 181 | 182 | 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. 183 | 184 | ```jsx 185 | import { createResource } from 'solid-js' 186 | import { useRouteContext } from '/dx:core.js' 187 | 188 | export const streaming = true 189 | 190 | export default function Streaming () { 191 | const {state} = useRouteContext() 192 | const [message] = createResource(async () => { 193 | // If already retrieved on the server, no need 194 | // to wait for it on the client 195 | if (state.message) { 196 | return state.message 197 | } 198 | // Note: assignments to useRouteContext().state on the server 199 | // are automatically relayed to the client in the hydration payload 200 | const message = await afterSeconds({ 201 | message: 'Delayed by Resource API', 202 | seconds: 5, 203 | }) 204 | state.message = message 205 | return message 206 | }) 207 | return ( 208 | <Suspense fallback={<p>Waiting for content</p>}> 209 | <Message message={message()} /> 210 | </Suspense> 211 | ) 212 | } 213 | ``` 214 | 215 | [See the full example](https://github.com/fastify/fastify-dx/blob/main/starters/solid/client/pages/streaming.jsx) in the [starter template](https://github.com/fastify/fastify-dx/tree/dev/starters/solid). 216 | 217 | ## `serverOnly` 218 | 219 | 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. 220 | 221 | 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. 222 | 223 | ```jsx 224 | export const serverOnly = true 225 | 226 | export default function ServerOnly (props) { 227 | return <p>This route is rendered on the server only!</p> 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 `<!-- element -->` 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 | return <p>This route is rendered on the client only!</p> 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 `<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. 252 | 253 | ### Dynamic parameters 254 | 255 | 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`). 256 | 257 | ```js 258 | // ... 259 | const plugins = [ 260 | // ... 261 | viteSvelteFastifyDX({ paramPattern: /\$(\w+)/ }), 262 | ] 263 | ``` 264 | 265 | ### Routes location 266 | 267 | You can also change the glob pattern used to determine where to route modules from. 268 | 269 | 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: 270 | 271 | ```js 272 | // ... 273 | const plugins = [ 274 | // ... 275 | viteSvelteFastifyDX({ globPattern: '/views/**/*.svelte' }), 276 | ] 277 | ``` 278 | 279 | ### View modules 280 | 281 | 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**. 282 | 283 | Additionally, [**you can provide your own routes**](https://github.com/fastify/fastify-dx/tree/dev/packages/fastify-dx-svelte#dxroutesjs). 284 | 285 | ```html 286 | <script context="module"> 287 | export const path = '/my-page' 288 | </script> 289 | 290 | <p>Route with path export</p> 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 <p>{data.message}</p> 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 | <div class="contents"> 342 | {state.user.authenticated 343 | ? props.children 344 | : <Login onClick={() => authenticate()} /> } 345 | </div> 346 | ) 347 | } 348 | 349 | function Login (props) { 350 | return ( 351 | <> 352 | <p>This route needs authentication.</p> 353 | <button onClick={props.onClick}> 354 | Click this button to authenticate. 355 | </button> 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 <p>{data.message}</p> 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 `<Link>`, 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 | <p>No pre-rendered HTML sent to the browser.</p> 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 `<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. 483 | 484 | > 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-dx-solid/index.js#L57) to [fastify-vite](https://github.com/fastify/fastify-vite). 485 | 486 | ### `getMeta()` 487 | 488 | 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. 489 | 490 | It receives the [route context](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/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. 491 | 492 | ```jsx 493 | export function getMeta (ctx) { 494 | return { 495 | title: 'Route Title', 496 | meta: [ 497 | { name: 'twitter:title', value: 'Route Title' }, 498 | ] 499 | } 500 | } 501 | 502 | export default function Route (props) { 503 | return <p>Route with meta tags.</p> 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 | <Router url={props.url}> 530 | <Routes>{ 531 | // eslint-disable-next-line solid/prefer-for 532 | props.payload.routes.map(route => 533 | <Route path={route.path} element={ 534 | <DXRoute 535 | state={props.payload.serverRoute.state} 536 | path={route.path} 537 | payload={props.payload} 538 | component={route.component} /> 539 | } /> 540 | ) 541 | }</Routes> 542 | </Router> 543 | ) 544 | } 545 | ``` 546 | 547 | ### `/dx:route.jsx` 548 | 549 | This is used by `root.jsx` to enhance your route modules with the [URMA specification](https://github.com/fastify/fastify-dx/blob/main/URMA.md). 550 | 551 | <b>You'll rarely need to customize this file.</b> 552 | 553 | ```jsx 554 | import { createContext, createSignal, createResource, children } from 'solid-js' 555 | import { isServer, Suspense } from 'solid-js/web' 556 | import { Router, Routes, Route, useLocation } from 'solid-app-router' 557 | import { RouteContext, jsonDataFetch } from '/dx:core.js' 558 | import layouts from '/dx:layouts.js' 559 | 560 | export default function DXRoute (props) { 561 | const ctx = props.payload.routeMap[props.path] 562 | const location = useLocation() 563 | 564 | ctx.state = props.state 565 | ctx.actions = props.payload.serverRoute.actions 566 | 567 | if (isServer) { 568 | ctx.layout = props.payload.serverRoute.layout ?? 'default' 569 | ctx.data = props.payload.serverRoute.data 570 | } 571 | 572 | async function setup () { 573 | if (props.payload.serverRoute.firstRender) { 574 | // ctx.hydration = props.payload.serverRoute.hydration 575 | ctx.data = props.payload.serverRoute.data 576 | ctx.layout = props.payload.serverRoute.layout ?? 'default' 577 | props.payload.serverRoute.firstRender = false 578 | return ctx 579 | } 580 | ctx.layout = ctx.layout ?? 'default' 581 | const { getMeta, getData, onEnter } = await ctx.loader() 582 | if (getData) { 583 | try { 584 | const fullPath = `${location.pathname}${location.search}` 585 | const updatedData = await jsonDataFetch(fullPath) 586 | if (!ctx.data) { 587 | ctx.data = {} 588 | } 589 | if (updatedData) { 590 | Object.assign(ctx.data, updatedData) 591 | } 592 | ctx.error = null 593 | } catch (error) { 594 | ctx.error = error 595 | } 596 | } 597 | if (getMeta) { 598 | const updatedMeta = await getMeta(ctx) 599 | if (updatedMeta) { 600 | props.payload.head.update(updatedMeta) 601 | } 602 | } 603 | if (onEnter) { 604 | const updatedData = await onEnter(ctx) 605 | if (updatedData) { 606 | Object.assign(ctx.data, updatedData) 607 | } 608 | } 609 | return ctx 610 | } 611 | 612 | let element 613 | if (isServer) { 614 | element = ( 615 | <RouteContext.Provider value={ctx}> 616 | <Layout id={ctx.layout}> 617 | <props.component /> 618 | </Layout> 619 | </RouteContext.Provider> 620 | ) 621 | } else { 622 | const [routeContext] = createResource(setup) 623 | element = ( 624 | <Suspense> 625 | {!routeContext.loading && 626 | <RouteContext.Provider value={routeContext()}> 627 | <Layout id={routeContext().layout}> 628 | <props.component /> 629 | </Layout> 630 | </RouteContext.Provider> 631 | } 632 | </Suspense> 633 | ) 634 | } 635 | return element 636 | } 637 | 638 | function Layout (props) { 639 | const Component = layouts[props.id].default 640 | return <Component>{props.children}</Component> 641 | } 642 | ``` 643 | 644 | What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/virtual/route.jsx). 645 | 646 | 647 | ### `/dx:routes.js` 648 | 649 | 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. 650 | 651 | ```js 652 | export default import.meta.env.SSR 653 | ? createRoutes(import.meta.globEager('$globPattern')) 654 | : hydrateRoutes(import.meta.glob('$globPattern')) 655 | ``` 656 | 657 | See [the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/virtual/routes.js) for the `createRoutes()` and `hydrateRoutes()` definitions. 658 | 659 | 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: 660 | 661 | ```js 662 | const routes = [ 663 | { 664 | path: '/', 665 | component: () => import('/custom/index.jsx'), 666 | } 667 | ] 668 | 669 | export default import.meta.env.SSR 670 | ? createRoutes(routes) 671 | : hydrateRoutes(routes) 672 | ```` 673 | 674 | **Nested routes aren't supported yet.** 675 | 676 | 677 | ### `/dx:core.js` 678 | 679 | Implements `useRouteContext()`. 680 | 681 | See its full definition [here](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/virtual/core.js). 682 | 683 | ### `/dx:layouts.js` 684 | 685 | This is responsible for loading **layout components**. It's part of `route.jsx` by default. 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. 686 | 687 | <b>You'll rarely need to customize this file.</b> 688 | 689 | ```js 690 | import DefaultLayout from '/dx:layouts/default.jsx' 691 | 692 | const appLayouts = import.meta.globEager('/layouts/*.jsx') 693 | 694 | appLayouts['/layouts/default.jsx'] ??= DefaultLayout 695 | 696 | export default Object.fromEntries( 697 | Object.keys(appLayouts).map((path) => { 698 | const name = path.slice(9, -4) 699 | return [name, appLayouts[path]] 700 | }), 701 | ) 702 | 703 | ``` 704 | 705 | What you see above is its [full definition](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/virtual/layouts.js). 706 | 707 | ### `/dx:mount.js` 708 | 709 | 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. 710 | 711 | <b>You'll rarely need to customize this file.</b> 712 | 713 | [See the full file](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-solid/virtual/mount.js) for the `mount()` function definition. 714 | 715 | 716 | ## Maintainance 717 | 718 | Created and maintained by [Jonas Galvez](https://github.com/sponsors/galvez), **Principal Engineer** and **Open Sourcerer** at [NodeSource](https://nodesource.com). 719 | 720 | New contributors are extremely welcome to look for [good first issues](https://github.com/fastify/fastify-dx/labels/good%20first%20issue). 721 | 722 | ## Gold Sponsors 723 | 724 | <a href="https://nodesource.com"><img width="200px" src="https://user-images.githubusercontent.com/12291/206885948-3fa742a2-1057-4db2-8648-46f5cb673461.svg"></a> 725 | 726 | [Contact me](mailto:jonasgalvez@gmail.com) to add your company's logo here. 727 | 728 | ## GitHub Sponsors 729 | 730 | - [**Duc-Thien Bui**](https://github.com/aecea) 731 | - [**Tom Preston-Werner**](https://github.com/mojombo) 732 | - [**Clifford Fajardo**](https://github.com/cliffordfajardo) 733 | - [**David Adam Coffey**](https://github.com/dacoffey) 734 | - [**Mezereon**](https://github.com/mezereon-co) 735 | - [**A-J Roos**](https://github.com/Asjas) 736 | - [**James Isaacs**](https://github.com/jamesisaacs2) 737 | 738 | [**Click here**](https://github.com/sponsors/galvez) to add your name to this list. 739 | 740 | _Thank you!_ 741 | --------------------------------------------------------------------------------