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.

+ + + ) +} \ 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}
  • + }) + }
+
+ + +
+

+ 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}
  • + }) + }
+
+ + +
+

+ 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}
  • + }) + }
+
+ + +
+

+ 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" },