Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.

Commit f40758a

Browse files
authored
Merge pull request #45 from fastify/react-ts
feat(react): TypeScript support, starter template
2 parents 2e2b689 + 6bd34ac commit f40758a

35 files changed

+891
-9
lines changed

devinstall.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
const { name: example } = path.parse(process.cwd())
66
const exRoot = path.resolve(__dirname, 'starters', example)
7-
const command = process.argv.slice(5)
7+
const command = process.argv.slice(process.argv.findIndex(_ => _ === '--') + 1)
88

99
if (!fs.existsSync(exRoot)) {
1010
console.log('Must be called from a directory under starters/.')

packages/fastify-dx-react/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export function createHtmlFunction (source, scope, config) {
7070
head: headTemplate({ ...context, head }),
7171
footer: () => footerTemplate({
7272
...context,
73+
hydration: '',
7374
// Decide whether or not to include the hydration script
7475
...!context.serverOnly && {
7576
hydration: (

packages/fastify-dx-react/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
"type": "module",
66
"main": "index.js",
77
"name": "fastify-dx-react",
8-
"version": "0.0.4",
8+
"version": "0.0.5",
99
"files": [
1010
"virtual/create.jsx",
11+
"virtual/create.tsx",
1112
"virtual/root.jsx",
13+
"virtual/root.tsx",
1214
"virtual/layouts.js",
1315
"virtual/layouts/default.jsx",
1416
"virtual/context.js",
17+
"virtual/context.ts",
1518
"virtual/mount.js",
19+
"virtual/mount.ts",
1620
"virtual/resource.js",
1721
"virtual/core.jsx",
1822
"virtual/routes.js",

packages/fastify-dx-react/plugin.cjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@ const { fileURLToPath } = require('url')
55
function viteReactFastifyDX (config = {}) {
66
const prefix = /^\/?dx:/
77
const routing = Object.assign({
8-
globPattern: '/pages/**/*.jsx',
8+
globPattern: '/pages/**/*.(jsx|tsx)',
99
paramPattern: /\[(\w+)\]/,
1010
}, config)
1111
const virtualRoot = resolve(__dirname, 'virtual')
1212
const virtualModules = [
1313
'mount.js',
14+
'mount.ts',
1415
'resource.js',
16+
'resource.ts',
1517
'routes.js',
1618
'layouts.js',
1719
'create.jsx',
20+
'create.tsx',
1821
'root.jsx',
22+
'root.tsx',
1923
'layouts/',
2024
'context.js',
25+
'context.ts',
2126
'core.jsx'
2227
]
2328
virtualModules.includes = function (virtual) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This file serves as a placeholder
2+
// if no context.js file is provided
3+
4+
export default () => {}

packages/fastify-dx-react/virtual/create.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import Root from '/dx:root.jsx'
22

33
export default function create ({ url, ...serverInit }) {
44
return (
5-
<Root url={url} serverInit={serverInit} />
5+
<Root url={url} {...serverInit} />
66
)
77
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Root from '/dx:root.tsx'
2+
3+
export default function create ({ url, ...serverInit }) {
4+
return (
5+
<Root url={url} {...serverInit} />
6+
)
7+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Head from 'unihead/client'
2+
import { createRoot, hydrateRoot } from 'react-dom/client'
3+
4+
import create from '/dx:create.tsx'
5+
import routesPromise from '/dx:routes.js'
6+
7+
mount('main')
8+
9+
async function mount (target) {
10+
if (typeof target === 'string') {
11+
target = document.querySelector(target)
12+
}
13+
const context = await import('/dx:context.ts')
14+
const ctxHydration = await extendContext(window.route, context)
15+
const head = new Head(window.route.head, window.document)
16+
const resolvedRoutes = await routesPromise
17+
const routeMap = Object.fromEntries(
18+
resolvedRoutes.map((route) => [route.path, route]),
19+
)
20+
21+
const app = create({
22+
head,
23+
ctxHydration,
24+
routes: window.routes,
25+
routeMap,
26+
})
27+
if (ctxHydration.clientOnly) {
28+
createRoot(target).render(app)
29+
} else {
30+
hydrateRoot(target, app)
31+
}
32+
}
33+
34+
async function extendContext (ctx, {
35+
// The route context initialization function
36+
default: setter,
37+
// We destructure state here just to discard it from extra
38+
state,
39+
// Other named exports from context.js
40+
...extra
41+
}) {
42+
Object.assign(ctx, extra)
43+
if (setter) {
44+
await setter(ctx)
45+
}
46+
return ctx
47+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
2+
const fetchMap = new Map()
3+
const resourceMap = new Map()
4+
5+
export function waitResource (path, id, promise) {
6+
const resourceId = `${path}:${id}`
7+
const loader = resourceMap.get(resourceId)
8+
if (loader) {
9+
if (loader.error) {
10+
throw loader.error
11+
}
12+
if (loader.suspended) {
13+
throw loader.promise
14+
}
15+
resourceMap.delete(resourceId)
16+
17+
return loader.result
18+
} else {
19+
const loader = {
20+
suspended: true,
21+
error: null,
22+
result: null,
23+
promise: null,
24+
}
25+
loader.promise = promise()
26+
.then((result) => { loader.result = result })
27+
.catch((loaderError) => { loader.error = loaderError })
28+
.finally(() => { loader.suspended = false })
29+
30+
resourceMap.set(resourceId, loader)
31+
32+
return waitResource(path, id)
33+
}
34+
}
35+
36+
export function waitFetch (path) {
37+
const loader = fetchMap.get(path)
38+
if (loader) {
39+
if (loader.error || loader.data?.statusCode === 500) {
40+
if (loader.data?.statusCode === 500) {
41+
throw new Error(loader.data.message)
42+
}
43+
throw loader.error
44+
}
45+
if (loader.suspended) {
46+
throw loader.promise
47+
}
48+
fetchMap.delete(path)
49+
50+
return loader.data
51+
} else {
52+
const loader = {
53+
suspended: true,
54+
error: null,
55+
data: null,
56+
promise: null,
57+
}
58+
loader.promise = fetch(`/-/data${path}`)
59+
.then((response) => response.json())
60+
.then((loaderData) => { loader.data = loaderData })
61+
.catch((loaderError) => { loader.error = loaderError })
62+
.finally(() => { loader.suspended = false })
63+
64+
fetchMap.set(path, loader)
65+
66+
return waitFetch(path)
67+
}
68+
}
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
import { Suspense } from 'react'
2-
import { DXApp } from '/dx:core.jsx'
2+
import { Routes, Route } from 'react-router-dom'
3+
import { Router, DXRoute } from '/dx:core.jsx'
34

4-
export default function Root ({ url, serverInit }) {
5+
export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
56
return (
67
<Suspense>
7-
<DXApp url={url} {...serverInit} />
8+
<Router location={url}>
9+
<Routes>{
10+
routes.map(({ path, component: Component }) =>
11+
<Route
12+
key={path}
13+
path={path}
14+
element={
15+
<DXRoute
16+
head={head}
17+
ctxHydration={ctxHydration}
18+
ctx={routeMap[path]}>
19+
<Component />
20+
</DXRoute>
21+
} />,
22+
)
23+
}</Routes>
24+
</Router>
825
</Suspense>
926
)
10-
}
27+
}

0 commit comments

Comments
 (0)