From 923e29aac3eaa0a9bc6c13872c457c6b64feb793 Mon Sep 17 00:00:00 2001
From: Jonas Galvez
Date: Wed, 3 Aug 2022 22:34:09 -0300
Subject: [PATCH 1/3] feat: basic TypeScript support for React
---
packages/fastify-dx-react/plugin.cjs | 7 +-
packages/fastify-dx-react/virtual/create.jsx | 2 +-
packages/fastify-dx-react/virtual/create.tsx | 7 ++
packages/fastify-dx-react/virtual/mount.ts | 47 +++++++++++
packages/fastify-dx-react/virtual/root.jsx | 25 +++++-
packages/fastify-dx-react/virtual/root.tsx | 27 ++++++
starters/react-ts/.eslintignore | 1 +
starters/react-ts/.eslintrc | 34 ++++++++
starters/react-ts/client/assets/logo.svg | 84 +++++++++++++++++++
starters/react-ts/client/base.css | 56 +++++++++++++
starters/react-ts/client/context.ts | 57 +++++++++++++
starters/react-ts/client/index.html | 13 +++
starters/react-ts/client/index.ts | 9 ++
starters/react-ts/client/layouts/auth.jsx | 25 ++++++
starters/react-ts/client/layouts/default.jsx | 9 ++
.../react-ts/client/pages/client-only.jsx | 23 +++++
starters/react-ts/client/pages/index.jsx | 41 +++++++++
.../react-ts/client/pages/server-only.jsx | 17 ++++
starters/react-ts/client/pages/streaming.jsx | 46 ++++++++++
starters/react-ts/client/pages/using-auth.jsx | 39 +++++++++
starters/react-ts/client/pages/using-data.jsx | 46 ++++++++++
.../react-ts/client/pages/using-store.jsx | 36 ++++++++
starters/react-ts/client/root.tsx | 28 +++++++
starters/react-ts/package.json | 50 +++++++++++
starters/react-ts/postcss.config.cjs | 9 ++
starters/react-ts/server.d.ts | 6 ++
starters/react-ts/server.ts | 32 +++++++
starters/react-ts/tsconfig.json | 12 +++
starters/react-ts/vite.config.ts | 29 +++++++
29 files changed, 811 insertions(+), 6 deletions(-)
create mode 100644 packages/fastify-dx-react/virtual/create.tsx
create mode 100644 packages/fastify-dx-react/virtual/mount.ts
create mode 100644 packages/fastify-dx-react/virtual/root.tsx
create mode 100644 starters/react-ts/.eslintignore
create mode 100644 starters/react-ts/.eslintrc
create mode 100644 starters/react-ts/client/assets/logo.svg
create mode 100644 starters/react-ts/client/base.css
create mode 100644 starters/react-ts/client/context.ts
create mode 100644 starters/react-ts/client/index.html
create mode 100644 starters/react-ts/client/index.ts
create mode 100644 starters/react-ts/client/layouts/auth.jsx
create mode 100644 starters/react-ts/client/layouts/default.jsx
create mode 100644 starters/react-ts/client/pages/client-only.jsx
create mode 100644 starters/react-ts/client/pages/index.jsx
create mode 100644 starters/react-ts/client/pages/server-only.jsx
create mode 100644 starters/react-ts/client/pages/streaming.jsx
create mode 100644 starters/react-ts/client/pages/using-auth.jsx
create mode 100644 starters/react-ts/client/pages/using-data.jsx
create mode 100644 starters/react-ts/client/pages/using-store.jsx
create mode 100644 starters/react-ts/client/root.tsx
create mode 100644 starters/react-ts/package.json
create mode 100644 starters/react-ts/postcss.config.cjs
create mode 100644 starters/react-ts/server.d.ts
create mode 100644 starters/react-ts/server.ts
create mode 100644 starters/react-ts/tsconfig.json
create mode 100644 starters/react-ts/vite.config.ts
diff --git a/packages/fastify-dx-react/plugin.cjs b/packages/fastify-dx-react/plugin.cjs
index 2ec2063..f615341 100644
--- a/packages/fastify-dx-react/plugin.cjs
+++ b/packages/fastify-dx-react/plugin.cjs
@@ -5,19 +5,24 @@ const { fileURLToPath } = require('url')
function viteReactFastifyDX (config = {}) {
const prefix = /^\/?dx:/
const routing = Object.assign({
- globPattern: '/pages/**/*.jsx',
+ globPattern: '/pages/**/*.(jsx|tsx)',
paramPattern: /\[(\w+)\]/,
}, config)
const virtualRoot = resolve(__dirname, 'virtual')
const virtualModules = [
'mount.js',
+ 'mount.ts',
'resource.js',
+ 'resource.ts',
'routes.js',
'layouts.js',
'create.jsx',
+ 'create.tsx',
'root.jsx',
+ 'root.tsx',
'layouts/',
'context.js',
+ 'context.ts',
'core.jsx'
]
virtualModules.includes = function (virtual) {
diff --git a/packages/fastify-dx-react/virtual/create.jsx b/packages/fastify-dx-react/virtual/create.jsx
index cf222b2..1f9ece7 100644
--- a/packages/fastify-dx-react/virtual/create.jsx
+++ b/packages/fastify-dx-react/virtual/create.jsx
@@ -2,6 +2,6 @@ import Root from '/dx:root.jsx'
export default function create ({ url, ...serverInit }) {
return (
-
+
)
}
diff --git a/packages/fastify-dx-react/virtual/create.tsx b/packages/fastify-dx-react/virtual/create.tsx
new file mode 100644
index 0000000..399609b
--- /dev/null
+++ b/packages/fastify-dx-react/virtual/create.tsx
@@ -0,0 +1,7 @@
+import Root from '/dx:root.tsx'
+
+export default function create ({ url, ...serverInit }) {
+ return (
+
+ )
+}
diff --git a/packages/fastify-dx-react/virtual/mount.ts b/packages/fastify-dx-react/virtual/mount.ts
new file mode 100644
index 0000000..54e06bc
--- /dev/null
+++ b/packages/fastify-dx-react/virtual/mount.ts
@@ -0,0 +1,47 @@
+import Head from 'unihead/client'
+import { createRoot, hydrateRoot } from 'react-dom/client'
+
+import create from '/dx:create.tsx'
+import routesPromise from '/dx:routes.js'
+
+mount('main')
+
+async function mount (target) {
+ if (typeof target === 'string') {
+ target = document.querySelector(target)
+ }
+ const context = await import('/dx:context.ts')
+ const ctxHydration = await extendContext(window.route, context)
+ const head = new Head(window.route.head, window.document)
+ const resolvedRoutes = await routesPromise
+ const routeMap = Object.fromEntries(
+ resolvedRoutes.map((route) => [route.path, route]),
+ )
+
+ const app = create({
+ head,
+ ctxHydration,
+ routes: window.routes,
+ routeMap,
+ })
+ if (ctxHydration.clientOnly) {
+ createRoot(target).render(app)
+ } else {
+ hydrateRoot(target, app)
+ }
+}
+
+async function extendContext (ctx, {
+ // The route context initialization function
+ default: setter,
+ // We destructure state here just to discard it from extra
+ state,
+ // Other named exports from context.js
+ ...extra
+}) {
+ Object.assign(ctx, extra)
+ if (setter) {
+ await setter(ctx)
+ }
+ return ctx
+}
diff --git a/packages/fastify-dx-react/virtual/root.jsx b/packages/fastify-dx-react/virtual/root.jsx
index f3e51a3..c493947 100644
--- a/packages/fastify-dx-react/virtual/root.jsx
+++ b/packages/fastify-dx-react/virtual/root.jsx
@@ -1,10 +1,27 @@
import { Suspense } from 'react'
-import { DXApp } from '/dx:core.jsx'
+import { Routes, Route } from 'react-router-dom'
+import { Router, DXRoute } from '/dx:core.jsx'
-export default function Root ({ url, serverInit }) {
+export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
return (
-
+
+ {
+ routes.map(({ path, component: Component }) =>
+
+
+
+ } />,
+ )
+ }
+
)
-}
+}
\ No newline at end of file
diff --git a/packages/fastify-dx-react/virtual/root.tsx b/packages/fastify-dx-react/virtual/root.tsx
new file mode 100644
index 0000000..c493947
--- /dev/null
+++ b/packages/fastify-dx-react/virtual/root.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react'
+import { Routes, Route } from 'react-router-dom'
+import { Router, DXRoute } from '/dx:core.jsx'
+
+export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
+ return (
+
+
+ {
+ routes.map(({ path, component: Component }) =>
+
+
+
+ } />,
+ )
+ }
+
+
+ )
+}
\ No newline at end of file
diff --git a/starters/react-ts/.eslintignore b/starters/react-ts/.eslintignore
new file mode 100644
index 0000000..53c37a1
--- /dev/null
+++ b/starters/react-ts/.eslintignore
@@ -0,0 +1 @@
+dist
\ No newline at end of file
diff --git a/starters/react-ts/.eslintrc b/starters/react-ts/.eslintrc
new file mode 100644
index 0000000..15c7b7c
--- /dev/null
+++ b/starters/react-ts/.eslintrc
@@ -0,0 +1,34 @@
+{
+ parser: '@typescript-eslint/parser',
+ plugins: ['react'],
+ extends: [
+ 'eslint:recommended',
+ 'standard-with-typescript',
+ 'plugin:react/recommended',
+ ],
+ parserOptions: {
+ project: './tsconfig.json',
+ requireConfigFile: false,
+ ecmaVersion: 2021,
+ sourceType: 'module',
+ babelOptions: {
+ presets: ['@babel/preset-react'],
+ },
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ rules: {
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/strict-boolean-expressions': 'off',
+ 'comma-dangle': ['error', 'always-multiline'],
+ 'react/prop-types': 'off',
+ 'import/no-absolute-path': 'off',
+ 'react/react-in-jsx-scope': 'off',
+ },
+ settings: {
+ react: {
+ version: '18.0',
+ },
+ },
+}
diff --git a/starters/react-ts/client/assets/logo.svg b/starters/react-ts/client/assets/logo.svg
new file mode 100644
index 0000000..9f2f2fd
--- /dev/null
+++ b/starters/react-ts/client/assets/logo.svg
@@ -0,0 +1,84 @@
+
+image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/starters/react-ts/client/base.css b/starters/react-ts/client/base.css
new file mode 100644
index 0000000..fde0cfc
--- /dev/null
+++ b/starters/react-ts/client/base.css
@@ -0,0 +1,56 @@
+:root {
+ --color-base: #f1f1f1;
+ --color-highlight: #ff80ff;
+}
+html {
+ background: #222;
+}
+main {
+ width: 800px;
+ margin: 0 auto;
+ padding: 2em;
+ box-shadow: 5px 5px 30px rgba(0,0,0,0.4);
+ border-radius: 10px;
+ background-color: rgba(255, 255, 255, 0.1);
+ font-family: Avenir, Helvetica, Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: var(--color-base);
+ margin-top: 60px;
+ & a {
+ color: var(--color-highlight);
+ text-decoration: none;
+ font-weight: bold;
+ border-bottom: 1px solid var(--color-highlight);
+ &:hover {
+ color: #ffde00;
+ }
+ &:active {
+ color: #eecf00
+ }
+ }
+ & p {
+ font-size: 1.2em;
+ }
+ & ul {
+ & li {
+ &:not(:last-child) {
+ margin-bottom: 0.5em;
+ }
+ break-inside: avoid;
+ font-size: 1em;
+ }
+ }
+ & code {
+ color: #ffde00;
+ font-weight: bold;
+ font-family: 'Consolas', 'Andale Mono', monospace;
+ font-size: 0.9em;
+ }
+ & img {
+ width: 14em;
+ }
+ & button {
+ margin: 0 0.5em;
+ }
+}
diff --git a/starters/react-ts/client/context.ts b/starters/react-ts/client/context.ts
new file mode 100644
index 0000000..50c1262
--- /dev/null
+++ b/starters/react-ts/client/context.ts
@@ -0,0 +1,57 @@
+import ky from 'ky-universal'
+
+interface User {
+ authenticated: boolean | null
+}
+
+interface State {
+ message: string | null
+ user: User | null
+ todoList: string[] | null
+}
+
+// This will eventually be provided by fastify-dx's core package
+interface RouteContext {
+ server?: any
+ req?: any
+ reply?: any
+ actions: object
+ data: any
+ state: State
+}
+
+export default (ctx: RouteContext): void => {
+ if (ctx.server) {
+ ctx.state.todoList = ctx.server.db.todoList
+ }
+}
+
+export const $fetch = ky.extend({
+ prefixUrl: 'http://localhost:3000',
+})
+
+export const state = (): State => ({
+ message: null,
+ user: {
+ authenticated: false,
+ },
+ todoList: null,
+})
+
+export const actions = {
+ authenticate (state: State) {
+ state.user.authenticated = true
+ },
+ async addTodoItem (state: State, item) {
+ await $fetch.put('api/todo/items', {
+ json: { item },
+ })
+ state.todoList.push(item)
+ },
+ async removeTodoItem (state: State, index) {
+ await $fetch.delete('api/todo/items', {
+ json: { index },
+ })
+ state.todoList.splice(index, 1)
+ },
+}
diff --git a/starters/react-ts/client/index.html b/starters/react-ts/client/index.html
new file mode 100644
index 0000000..9c739b4
--- /dev/null
+++ b/starters/react-ts/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/starters/react-ts/client/index.ts b/starters/react-ts/client/index.ts
new file mode 100644
index 0000000..d2d4629
--- /dev/null
+++ b/starters/react-ts/client/index.ts
@@ -0,0 +1,9 @@
+import routes from '/dx:routes.js'
+import create from '/dx:create.tsx'
+import * as context from '/dx:context.ts'
+
+export default {
+ context,
+ routes,
+ create,
+}
diff --git a/starters/react-ts/client/layouts/auth.jsx b/starters/react-ts/client/layouts/auth.jsx
new file mode 100644
index 0000000..517989a
--- /dev/null
+++ b/starters/react-ts/client/layouts/auth.jsx
@@ -0,0 +1,25 @@
+import { Suspense } from 'react'
+import { useRouteContext } from '/dx:core.jsx'
+
+export default function Auth ({ children }) {
+ const { actions, state, snapshot } = useRouteContext()
+ const authenticate = () => actions.authenticate(state)
+ return (
+
+ {snapshot.user.authenticated
+ ? children
+ : authenticate()} /> }
+
+ )
+}
+
+function Login ({ onClick }) {
+ return (
+ <>
+ This route needs authentication.
+
+ Click this button to authenticate.
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/starters/react-ts/client/layouts/default.jsx b/starters/react-ts/client/layouts/default.jsx
new file mode 100644
index 0000000..604b629
--- /dev/null
+++ b/starters/react-ts/client/layouts/default.jsx
@@ -0,0 +1,9 @@
+import { Suspense } from 'react'
+
+export default function Default ({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/starters/react-ts/client/pages/client-only.jsx b/starters/react-ts/client/pages/client-only.jsx
new file mode 100644
index 0000000..2e72f69
--- /dev/null
+++ b/starters/react-ts/client/pages/client-only.jsx
@@ -0,0 +1,23 @@
+import { Link } from 'react-router-dom'
+
+export const clientOnly = true
+
+export function getMeta () {
+ return {
+ title: 'Client Only Page'
+ }
+}
+
+export default function ClientOnly () {
+ return (
+ <>
+ This route is rendered on the client only!
+
+ Go back to the index
+
+ ⁂
+ When this route is rendered on the server, no SSR takes place.
+ See the output of curl http://localhost:3000/client-only
.
+ >
+ )
+}
diff --git a/starters/react-ts/client/pages/index.jsx b/starters/react-ts/client/pages/index.jsx
new file mode 100644
index 0000000..1d9e575
--- /dev/null
+++ b/starters/react-ts/client/pages/index.jsx
@@ -0,0 +1,41 @@
+import logo from '/assets/logo.svg'
+import { Link } from 'react-router-dom'
+import { isServer, useRouteContext } from '/dx:core.jsx'
+
+export function getMeta () {
+ return {
+ title: 'Welcome to Fastify DX!'
+ }
+}
+
+export default function Index () {
+ const { snapshot, state } = useRouteContext()
+ if (isServer) {
+ // State is automatically hydrated on the client
+ state.message = 'Welcome to Fastify DX for React!'
+ }
+ return (
+ <>
+
+ {snapshot.message}
+
+ /using-data demonstrates how to
+ leverage the getData()
function
+ and useRouteContext()
to retrieve server data for a route.
+ /using-store demonstrates how to
+ leverage the
+ automated Valtio store
+ to retrieve server data for a route and maintain it in a global
+ state even after navigating to another route.
+ /using-auth demonstrates how to
+ wrap a route in a custom layout component.
+ /client-only demonstrates how to set
+ up a route for rendering on the client only (disables SSR).
+ /server-only demonstrates how to set
+ up a route for rendering on the server only (sends no JavaScript).
+ /streaming demonstrates how to set
+ up a route for SSR in streaming mode.
+
+ >
+ )
+}
diff --git a/starters/react-ts/client/pages/server-only.jsx b/starters/react-ts/client/pages/server-only.jsx
new file mode 100644
index 0000000..bb65b74
--- /dev/null
+++ b/starters/react-ts/client/pages/server-only.jsx
@@ -0,0 +1,17 @@
+import { Link } from 'react-router-dom'
+
+export const serverOnly = true
+
+export default function ServerOnly () {
+ return (
+ <>
+ This route is rendered on the server only!
+
+ Go back to the index
+
+ ⁂
+ When this route is rendered on the server, no JavaScript is sent to the client.
+ See the output of curl http://localhost:3000/server-only
.
+ >
+ )
+}
diff --git a/starters/react-ts/client/pages/streaming.jsx b/starters/react-ts/client/pages/streaming.jsx
new file mode 100644
index 0000000..99da054
--- /dev/null
+++ b/starters/react-ts/client/pages/streaming.jsx
@@ -0,0 +1,46 @@
+import { Suspense } from 'react'
+
+export const streaming = true
+
+export default function Index () {
+ return (
+ Waiting for content
}>
+
+
+ )
+}
+
+function Message () {
+ const message = afterSeconds({
+ id: 'index',
+ message: 'Delayed by Suspense API',
+ seconds: 5
+ })
+ return {message}
+}
+
+const delays = new Map()
+
+function afterSeconds ({ id, message, seconds }) {
+ const delay = delays.get(id)
+ if (delay) {
+ if (delay.message) {
+ delays.delete(id)
+ return delay.message
+ }
+ if (delay.promise) {
+ throw delay.promise
+ }
+ } else {
+ delays.set(id, {
+ message: null,
+ promise: new Promise((resolve) => {
+ setTimeout(() => {
+ delays.get(id).message = message
+ resolve()
+ }, seconds * 1000)
+ })
+ })
+ return afterSeconds({ id, message })
+ }
+}
diff --git a/starters/react-ts/client/pages/using-auth.jsx b/starters/react-ts/client/pages/using-auth.jsx
new file mode 100644
index 0000000..75e7412
--- /dev/null
+++ b/starters/react-ts/client/pages/using-auth.jsx
@@ -0,0 +1,39 @@
+import { useState } from 'react'
+import { Link } from 'react-router-dom'
+import { useRouteContext } from '/dx:core.jsx'
+
+export const layout = 'auth'
+
+export function getMeta () {
+ return { title: 'Using Custom Layout' }
+}
+
+export default function Index (props) {
+ const {snapshot, state, actions} = useRouteContext()
+ const [input, setInput] = useState(null)
+ const addItem = async (value) => {
+ await actions.addTodoItem(state, value)
+ input.value = ''
+ }
+ return (
+ <>
+ Todo List — Using Custom Layout
+ {
+ snapshot.todoList.map((item, i) => {
+ return {item}
+ })
+ }
+
+
+ addItem(input.value)}>Add
+
+
+ Go back to the index
+
+ ⁂
+ This example is exactly the same as /using-store,
+ except it's wrapped in a custom layout which blocks it until
+ user.authenticated
is true
in the global state.
+ >
+ )
+}
diff --git a/starters/react-ts/client/pages/using-data.jsx b/starters/react-ts/client/pages/using-data.jsx
new file mode 100644
index 0000000..b380e85
--- /dev/null
+++ b/starters/react-ts/client/pages/using-data.jsx
@@ -0,0 +1,46 @@
+import { useState } from 'react'
+import { Link } from 'react-router-dom'
+import { useRouteContext } from '/dx:core.jsx'
+
+export function getMeta () {
+ return { title: 'Todo List — Using Data' }
+}
+
+export function getData ({ server }) {
+ return {
+ todoList: server.db.todoList
+ }
+}
+
+export default function Index (props) {
+ const {data} = useRouteContext()
+ const [todoList, updateTodoList] = useState(data.todoList)
+ const [input, setInput] = useState(null)
+ const addItem = (value) => {
+ updateTodoList(list => [...list, value])
+ input.value = ''
+ }
+ return (
+ <>
+ Todo List — Using Data
+ {
+ todoList.map((item, i) => {
+ return {item}
+ })
+ }
+
+
+ addItem(input.value)}>Add
+
+
+ Go back to the index
+
+ ⁂
+ When you navigate away from this route, any additions to the to-do
+ list will be lost, because they're bound to this route component only.
+ See the /using-store example to learn
+ how to use the application global state for it.
+
+ >
+ )
+}
diff --git a/starters/react-ts/client/pages/using-store.jsx b/starters/react-ts/client/pages/using-store.jsx
new file mode 100644
index 0000000..b530216
--- /dev/null
+++ b/starters/react-ts/client/pages/using-store.jsx
@@ -0,0 +1,36 @@
+import { useState } from 'react'
+import { Link } from 'react-router-dom'
+import { useRouteContext } from '/dx:core.jsx'
+
+export function getMeta () {
+ return { title: 'Todo List — Using Store' }
+}
+
+export default function Index (props) {
+ const {snapshot, state, actions} = useRouteContext()
+ const [input, setInput] = useState(null)
+ const addItem = async (value) => {
+ await actions.addTodoItem(state, value)
+ input.value = ''
+ }
+ return (
+ <>
+ Todo List — Using Store
+ {
+ snapshot.todoList.map((item, i) => {
+ return {item}
+ })
+ }
+
+
+ addItem(input.value)}>Add
+
+
+ Go back to the index
+
+ ⁂
+ When you navigate away from this route, any additions to the to-do
+ list are not lost, because they're bound to the global application state.
+ >
+ )
+}
diff --git a/starters/react-ts/client/root.tsx b/starters/react-ts/client/root.tsx
new file mode 100644
index 0000000..66845bd
--- /dev/null
+++ b/starters/react-ts/client/root.tsx
@@ -0,0 +1,28 @@
+import 'virtual:uno.css'
+import { Suspense } from 'react'
+import { Routes, Route } from 'react-router-dom'
+import { Router, DXRoute } from '/dx:core.jsx'
+
+export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
+ return (
+
+
+ {
+ routes.map(({ path, component: Component }) =>
+
+
+
+ } />,
+ )
+ }
+
+
+ )
+}
diff --git a/starters/react-ts/package.json b/starters/react-ts/package.json
new file mode 100644
index 0000000..6494040
--- /dev/null
+++ b/starters/react-ts/package.json
@@ -0,0 +1,50 @@
+{
+ "type": "module",
+ "scripts": {
+ "dev": "tsx server.ts --dev",
+ "build": "npm run build:client && npm run build:server",
+ "serve": "tsx server.ts",
+ "devinstall": "zx ../../devinstall.mjs -- tsx server.ts --dev",
+ "build:client": "vite build --outDir dist/client --ssrManifest",
+ "build:server": "vite build --outDir dist/server --ssr /index.ts",
+ "lint": "eslint . --ext .js,.ts,.tsx --fix"
+ },
+ "dependencies": {
+ "tsx": "^3.7.1",
+ "fastify-vite": "^3.0.0-beta.23",
+ "ky-universal": "^0.10.1",
+ "devalue": "^2.0.1",
+ "minipass": "^3.3.4",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0",
+ "react-router-dom": "^6.3.0",
+ "unihead": "^0.0.6",
+ "valtio": "^1.6.1"
+ },
+ "devDependencies": {
+ "@babel/eslint-parser": "^7.16.0",
+ "@babel/preset-react": "^7.16.0",
+ "@vitejs/plugin-react": "^1.3.2",
+ "@typescript-eslint/eslint-plugin": "^5.30.5",
+ "@typescript-eslint/parser": "^5.30.5",
+ "eslint": "^8.19.0",
+ "eslint-config-standard-with-typescript": "^22.0.0",
+ "eslint-plugin-import": "^2.22.1",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^4.3.1",
+ "eslint-plugin-react": "^7.29.4",
+ "postcss-preset-env": "^7.7.1",
+ "unocss": "^0.37.4",
+ "vite-plugin-blueprint": "^0.0.4"
+ },
+ "devInstall": {
+ "local": {
+ "fastify-dx-react": "^0.0.1-alpha.0"
+ },
+ "external": {
+ "tsx": "^3.7.1",
+ "fastify-vite": "^3.0.0-beta.23",
+ "ky-universal": "^0.10.1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/starters/react-ts/postcss.config.cjs b/starters/react-ts/postcss.config.cjs
new file mode 100644
index 0000000..8b78078
--- /dev/null
+++ b/starters/react-ts/postcss.config.cjs
@@ -0,0 +1,9 @@
+const postcssPresetEnv = require('postcss-preset-env')
+
+module.exports = {
+ plugins: [
+ postcssPresetEnv({
+ stage: 1,
+ }),
+ ]
+}
diff --git a/starters/react-ts/server.d.ts b/starters/react-ts/server.d.ts
new file mode 100644
index 0000000..bd3b6a2
--- /dev/null
+++ b/starters/react-ts/server.d.ts
@@ -0,0 +1,6 @@
+declare module 'fastify' {
+ export interface FastifyInstance {
+ vite: object
+ db: object
+ }
+}
diff --git a/starters/react-ts/server.ts b/starters/react-ts/server.ts
new file mode 100644
index 0000000..ab93432
--- /dev/null
+++ b/starters/react-ts/server.ts
@@ -0,0 +1,32 @@
+import Fastify from 'fastify'
+import FastifyVite from 'fastify-vite'
+import FastifyDXReact from 'fastify-dx-react'
+
+const server = Fastify()
+
+server.decorate('db', {
+ todoList: [
+ 'Do laundry',
+ 'Respond to emails',
+ 'Write report',
+ ],
+})
+
+server.put('/api/todo/items', (req, reply) => {
+ server.db.todoList.push(req.body.item)
+ reply.send({ ok: true })
+})
+
+server.delete('/api/todo/items', (req, reply) => {
+ server.db.todoList.splice(req.body.index, 1)
+ reply.send({ ok: true })
+})
+
+await server.register(FastifyVite, {
+ root: import.meta.url,
+ renderer: FastifyDXReact,
+})
+
+await server.vite.ready()
+
+await server.listen(3000)
diff --git a/starters/react-ts/tsconfig.json b/starters/react-ts/tsconfig.json
new file mode 100644
index 0000000..de018f6
--- /dev/null
+++ b/starters/react-ts/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "strictNullChecks": true
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "files": [
+ "server.d.ts"
+ ]
+}
\ No newline at end of file
diff --git a/starters/react-ts/vite.config.ts b/starters/react-ts/vite.config.ts
new file mode 100644
index 0000000..86b44a7
--- /dev/null
+++ b/starters/react-ts/vite.config.ts
@@ -0,0 +1,29 @@
+import { join, dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+import viteReact from '@vitejs/plugin-react'
+import viteReactFastifyDX from 'fastify-dx-react/plugin'
+import unocss from 'unocss/vite'
+
+const path = fileURLToPath(import.meta.url)
+
+const root = join(dirname(path), 'client')
+const plugins = [
+ viteReact({
+ // Necessary until this Vite issue is resolved:
+ // https://github.com/vitejs/vite/issues/3301#issuecomment-1080292430
+ fastRefresh: false,
+ }),
+ viteReactFastifyDX(),
+ unocss(),
+]
+
+export default {
+ root,
+ plugins,
+ ssr: {
+ external: [
+ 'use-sync-external-store',
+ ],
+ },
+}
From 527ec85044d55423208180fad0a6c9981dcaf66c Mon Sep 17 00:00:00 2001
From: Jonas Galvez
Date: Thu, 4 Aug 2022 18:20:52 -0300
Subject: [PATCH 2/3] fix(react): hydration undefined when serverOnly
---
devinstall.mjs | 2 +-
packages/fastify-dx-react/index.js | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/devinstall.mjs b/devinstall.mjs
index 3756684..9827d67 100644
--- a/devinstall.mjs
+++ b/devinstall.mjs
@@ -4,7 +4,7 @@
const { name: example } = path.parse(process.cwd())
const exRoot = path.resolve(__dirname, 'starters', example)
-const command = process.argv.slice(5)
+const command = process.argv.slice(process.argv.findIndex(_ => _ === '--') + 1)
if (!fs.existsSync(exRoot)) {
console.log('Must be called from a directory under starters/.')
diff --git a/packages/fastify-dx-react/index.js b/packages/fastify-dx-react/index.js
index 3be8f71..804b723 100644
--- a/packages/fastify-dx-react/index.js
+++ b/packages/fastify-dx-react/index.js
@@ -70,6 +70,7 @@ export function createHtmlFunction (source, scope, config) {
head: headTemplate({ ...context, head }),
footer: () => footerTemplate({
...context,
+ hydration: '',
// Decide whether or not to include the hydration script
...!context.serverOnly && {
hydration: (
From 6bd34acd527e26001117d5f0573af457090d64b3 Mon Sep 17 00:00:00 2001
From: Jonas Galvez
Date: Thu, 4 Aug 2022 18:25:02 -0300
Subject: [PATCH 3/3] fix
---
packages/fastify-dx-react/package.json | 6 +-
packages/fastify-dx-react/virtual/context.ts | 4 ++
packages/fastify-dx-react/virtual/resource.ts | 68 +++++++++++++++++++
starters/react/package.json | 2 +-
4 files changed, 78 insertions(+), 2 deletions(-)
create mode 100644 packages/fastify-dx-react/virtual/context.ts
create mode 100644 packages/fastify-dx-react/virtual/resource.ts
diff --git a/packages/fastify-dx-react/package.json b/packages/fastify-dx-react/package.json
index a9505f6..9d922b1 100644
--- a/packages/fastify-dx-react/package.json
+++ b/packages/fastify-dx-react/package.json
@@ -5,14 +5,18 @@
"type": "module",
"main": "index.js",
"name": "fastify-dx-react",
- "version": "0.0.4",
+ "version": "0.0.5",
"files": [
"virtual/create.jsx",
+ "virtual/create.tsx",
"virtual/root.jsx",
+ "virtual/root.tsx",
"virtual/layouts.js",
"virtual/layouts/default.jsx",
"virtual/context.js",
+ "virtual/context.ts",
"virtual/mount.js",
+ "virtual/mount.ts",
"virtual/resource.js",
"virtual/core.jsx",
"virtual/routes.js",
diff --git a/packages/fastify-dx-react/virtual/context.ts b/packages/fastify-dx-react/virtual/context.ts
new file mode 100644
index 0000000..1e605f5
--- /dev/null
+++ b/packages/fastify-dx-react/virtual/context.ts
@@ -0,0 +1,4 @@
+// This file serves as a placeholder
+// if no context.js file is provided
+
+export default () => {}
diff --git a/packages/fastify-dx-react/virtual/resource.ts b/packages/fastify-dx-react/virtual/resource.ts
new file mode 100644
index 0000000..69d7310
--- /dev/null
+++ b/packages/fastify-dx-react/virtual/resource.ts
@@ -0,0 +1,68 @@
+
+const fetchMap = new Map()
+const resourceMap = new Map()
+
+export function waitResource (path, id, promise) {
+ const resourceId = `${path}:${id}`
+ const loader = resourceMap.get(resourceId)
+ if (loader) {
+ if (loader.error) {
+ throw loader.error
+ }
+ if (loader.suspended) {
+ throw loader.promise
+ }
+ resourceMap.delete(resourceId)
+
+ return loader.result
+ } else {
+ const loader = {
+ suspended: true,
+ error: null,
+ result: null,
+ promise: null,
+ }
+ loader.promise = promise()
+ .then((result) => { loader.result = result })
+ .catch((loaderError) => { loader.error = loaderError })
+ .finally(() => { loader.suspended = false })
+
+ resourceMap.set(resourceId, loader)
+
+ return waitResource(path, id)
+ }
+}
+
+export function waitFetch (path) {
+ const loader = fetchMap.get(path)
+ if (loader) {
+ if (loader.error || loader.data?.statusCode === 500) {
+ if (loader.data?.statusCode === 500) {
+ throw new Error(loader.data.message)
+ }
+ throw loader.error
+ }
+ if (loader.suspended) {
+ throw loader.promise
+ }
+ fetchMap.delete(path)
+
+ return loader.data
+ } else {
+ const loader = {
+ suspended: true,
+ error: null,
+ data: null,
+ promise: null,
+ }
+ loader.promise = fetch(`/-/data${path}`)
+ .then((response) => response.json())
+ .then((loaderData) => { loader.data = loaderData })
+ .catch((loaderError) => { loader.error = loaderError })
+ .finally(() => { loader.suspended = false })
+
+ fetchMap.set(path, loader)
+
+ return waitFetch(path)
+ }
+}
diff --git a/starters/react/package.json b/starters/react/package.json
index 786ac70..fc61807 100644
--- a/starters/react/package.json
+++ b/starters/react/package.json
@@ -10,7 +10,7 @@
"lint": "eslint . --ext .js,.jsx --fix"
},
"dependencies": {
- "fastify-dx-react": "^0.0.3",
+ "fastify-dx-react": "^0.0.4",
"fastify-vite": "^3.0.0-beta.23",
"ky-universal": "^0.10.1"
},