@fastify/react 0.5.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/client.js +44 -0
- package/{server/context.js → context.js} +2 -0
- package/index.js +11 -186
- package/package.json +47 -14
- package/plugin/index.js +102 -0
- package/plugin/parsers.js +42 -0
- package/plugin/parsers.test.js +28 -0
- package/plugin/preload.js +75 -0
- package/plugin/stores.js +39 -0
- package/plugin/virtual.js +100 -0
- package/rendering.js +139 -0
- package/routing.js +142 -0
- package/server.js +129 -0
- package/templating.js +51 -0
- package/virtual/core.jsx +29 -15
- package/virtual/create.jsx +1 -1
- package/virtual/layouts.js +1 -1
- package/virtual/mount.js +31 -28
- package/virtual/resource.js +11 -6
- package/virtual/root.jsx +1 -1
- package/virtual/routes.js +1 -122
- package/plugin.cjs +0 -110
- package/server/stream.js +0 -56
package/virtual/mount.js
CHANGED
|
@@ -1,48 +1,51 @@
|
|
|
1
1
|
import { createRoot, hydrateRoot } from 'react-dom/client'
|
|
2
2
|
import Head from 'unihead/client'
|
|
3
|
+
import { hydrateRoutes } from '@fastify/react/client'
|
|
4
|
+
import routes from '$app/routes.js'
|
|
5
|
+
import create from '$app/create.jsx'
|
|
6
|
+
import * as context from '$app/context.js'
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
import routesPromise from '/:routes.js'
|
|
6
|
-
|
|
7
|
-
mount('root')
|
|
8
|
-
|
|
9
|
-
async function mount(targetInput) {
|
|
10
|
-
let target = targetInput
|
|
11
|
-
if (typeof target === 'string') {
|
|
12
|
-
target = document.getElementById(target)
|
|
13
|
-
}
|
|
14
|
-
const context = await import('/:context.js')
|
|
8
|
+
async function mountApp (...targets) {
|
|
15
9
|
const ctxHydration = await extendContext(window.route, context)
|
|
16
10
|
const head = new Head(window.route.head, window.document)
|
|
17
|
-
const resolvedRoutes = await
|
|
11
|
+
const resolvedRoutes = await hydrateRoutes(routes)
|
|
18
12
|
const routeMap = Object.fromEntries(
|
|
19
13
|
resolvedRoutes.map((route) => [route.path, route]),
|
|
20
14
|
)
|
|
21
|
-
|
|
22
15
|
const app = create({
|
|
23
16
|
head,
|
|
24
17
|
ctxHydration,
|
|
25
18
|
routes: window.routes,
|
|
26
19
|
routeMap,
|
|
27
20
|
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
let mountTargetFound = false
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
const targetElem = document.querySelector(target)
|
|
24
|
+
if (targetElem) {
|
|
25
|
+
mountTargetFound = true
|
|
26
|
+
if (ctxHydration.clientOnly) {
|
|
27
|
+
createRoot(targetElem).render(app)
|
|
28
|
+
} else {
|
|
29
|
+
hydrateRoot(targetElem, app)
|
|
30
|
+
}
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!mountTargetFound) {
|
|
35
|
+
throw new Error(`No mount element found from provided list of targets: ${targets}`)
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
) {
|
|
39
|
+
mountApp('#root', 'main')
|
|
40
|
+
|
|
41
|
+
async function extendContext (ctx, {
|
|
42
|
+
// The route context initialization function
|
|
43
|
+
default: setter,
|
|
44
|
+
// We destructure state here just to discard it from extra
|
|
45
|
+
state,
|
|
46
|
+
// Other named exports from context.js
|
|
47
|
+
...extra
|
|
48
|
+
}) {
|
|
46
49
|
Object.assign(ctx, extra)
|
|
47
50
|
if (setter) {
|
|
48
51
|
await setter(ctx)
|
package/virtual/resource.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const clientFetchMap = new Map()
|
|
2
|
+
const clientResourceMap = new Map()
|
|
3
3
|
|
|
4
|
-
export function waitResource(
|
|
4
|
+
export function waitResource(
|
|
5
|
+
path,
|
|
6
|
+
id,
|
|
7
|
+
promise,
|
|
8
|
+
resourceMap = clientResourceMap,
|
|
9
|
+
) {
|
|
5
10
|
const resourceId = `${path}:${id}`
|
|
6
11
|
const loaderStatus = resourceMap.get(resourceId)
|
|
7
12
|
if (loaderStatus) {
|
|
@@ -37,7 +42,7 @@ export function waitResource(path, id, promise) {
|
|
|
37
42
|
return waitResource(path, id)
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
export function waitFetch(path) {
|
|
45
|
+
export function waitFetch(path, options = {}, fetchMap = clientFetchMap) {
|
|
41
46
|
const loaderStatus = fetchMap.get(path)
|
|
42
47
|
if (loaderStatus) {
|
|
43
48
|
if (loaderStatus.error || loaderStatus.data?.statusCode === 500) {
|
|
@@ -59,7 +64,7 @@ export function waitFetch(path) {
|
|
|
59
64
|
data: null,
|
|
60
65
|
promise: null,
|
|
61
66
|
}
|
|
62
|
-
loader.promise = fetch(
|
|
67
|
+
loader.promise = fetch(path, options)
|
|
63
68
|
.then((response) => response.json())
|
|
64
69
|
.then((loaderData) => {
|
|
65
70
|
loader.data = loaderData
|
|
@@ -73,5 +78,5 @@ export function waitFetch(path) {
|
|
|
73
78
|
|
|
74
79
|
fetchMap.set(path, loader)
|
|
75
80
|
|
|
76
|
-
return waitFetch(path)
|
|
81
|
+
return waitFetch(path, options, fetchMap)
|
|
77
82
|
}
|
package/virtual/root.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Suspense } from 'react'
|
|
2
2
|
import { Route, Routes } from 'react-router-dom'
|
|
3
|
-
import { AppRoute, Router } from '
|
|
3
|
+
import { AppRoute, Router } from '$app/core.jsx'
|
|
4
4
|
|
|
5
5
|
export default function Root({ url, routes, head, ctxHydration, routeMap }) {
|
|
6
6
|
return (
|
package/virtual/routes.js
CHANGED
|
@@ -1,122 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { lazy } from 'react'
|
|
4
|
-
|
|
5
|
-
export default import.meta.env.SSR
|
|
6
|
-
? createRoutes(import.meta.glob('$globPattern', { eager: true }))
|
|
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).then(
|
|
32
|
-
(routeModule) => {
|
|
33
|
-
return {
|
|
34
|
-
id: routeDef.path,
|
|
35
|
-
path: routeDef.path ?? routeModule.path,
|
|
36
|
-
...routeModule,
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
// Ensure that static routes have precedence over the dynamic ones
|
|
44
|
-
for (const path of importPaths.sort((a, b) => (a > b ? -1 : 1))) {
|
|
45
|
-
promises.push(
|
|
46
|
-
getRouteModule(path, from[path]).then((routeModule) => {
|
|
47
|
-
return {
|
|
48
|
-
id: path,
|
|
49
|
-
layout: routeModule.layout,
|
|
50
|
-
path:
|
|
51
|
-
routeModule.path ??
|
|
52
|
-
path
|
|
53
|
-
// Remove /pages and .jsx extension
|
|
54
|
-
.slice(6, -4)
|
|
55
|
-
// Replace [id] with :id
|
|
56
|
-
.replace(param, (_, m) => `:${m}`)
|
|
57
|
-
// Replace '/index' with '/'
|
|
58
|
-
.replace(/\/index$/, '/')
|
|
59
|
-
// Remove trailing slashs
|
|
60
|
-
.replace(/(.+)\/+$/, (...m) => m[1]),
|
|
61
|
-
...routeModule,
|
|
62
|
-
}
|
|
63
|
-
}),
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return new Routes(...(await Promise.all(promises)))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function hydrateRoutes(fromInput) {
|
|
71
|
-
let from = fromInput
|
|
72
|
-
if (Array.isArray(from)) {
|
|
73
|
-
from = Object.fromEntries(from.map((route) => [route.path, route]))
|
|
74
|
-
}
|
|
75
|
-
return window.routes.map((route) => {
|
|
76
|
-
route.loader = memoImport(from[route.id])
|
|
77
|
-
route.component = lazy(() => route.loader())
|
|
78
|
-
return route
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getRouteModuleExports(routeModule) {
|
|
83
|
-
return {
|
|
84
|
-
// The Route component (default export)
|
|
85
|
-
component: routeModule.default,
|
|
86
|
-
// The Layout Route component
|
|
87
|
-
layout: routeModule.layout,
|
|
88
|
-
// Route-level hooks
|
|
89
|
-
getData: routeModule.getData,
|
|
90
|
-
getMeta: routeModule.getMeta,
|
|
91
|
-
onEnter: routeModule.onEnter,
|
|
92
|
-
// Other Route-level settings
|
|
93
|
-
streaming: routeModule.streaming,
|
|
94
|
-
clientOnly: routeModule.clientOnly,
|
|
95
|
-
serverOnly: routeModule.serverOnly,
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function getRouteModule(path, routeModuleInput) {
|
|
100
|
-
let routeModule = routeModuleInput
|
|
101
|
-
// const isServer = typeof process !== 'undefined'
|
|
102
|
-
if (typeof routeModule === 'function') {
|
|
103
|
-
routeModule = await routeModule()
|
|
104
|
-
return getRouteModuleExports(routeModule)
|
|
105
|
-
}
|
|
106
|
-
return getRouteModuleExports(routeModule)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function memoImport(func) {
|
|
110
|
-
// Otherwise we get a ReferenceError, but since this function
|
|
111
|
-
// is only ran once for each route, there's no overhead
|
|
112
|
-
const kFuncExecuted = Symbol('kFuncExecuted')
|
|
113
|
-
const kFuncValue = Symbol('kFuncValue')
|
|
114
|
-
func[kFuncExecuted] = false
|
|
115
|
-
return async () => {
|
|
116
|
-
if (!func[kFuncExecuted]) {
|
|
117
|
-
func[kFuncValue] = await func()
|
|
118
|
-
func[kFuncExecuted] = true
|
|
119
|
-
}
|
|
120
|
-
return func[kFuncValue]
|
|
121
|
-
}
|
|
122
|
-
}
|
|
1
|
+
export default import.meta.glob('/pages/**/*.{jsx,tsx}')
|
package/plugin.cjs
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
const { readFileSync, existsSync } = require('fs')
|
|
2
|
-
const { dirname, join, resolve } = require('path')
|
|
3
|
-
const { fileURLToPath } = require('url')
|
|
4
|
-
|
|
5
|
-
function viteReactFastifyDX(config = {}) {
|
|
6
|
-
const prefix = /^\/:/
|
|
7
|
-
const routing = Object.assign(
|
|
8
|
-
{
|
|
9
|
-
globPattern: '/pages/**/*.{jsx,tsx}',
|
|
10
|
-
paramPattern: /\[(\w+)\]/,
|
|
11
|
-
},
|
|
12
|
-
config,
|
|
13
|
-
)
|
|
14
|
-
const virtualRoot = resolve(__dirname, 'virtual')
|
|
15
|
-
const virtualModules = [
|
|
16
|
-
'mount.js',
|
|
17
|
-
'resource.js',
|
|
18
|
-
'routes.js',
|
|
19
|
-
'layouts.js',
|
|
20
|
-
'create.jsx',
|
|
21
|
-
'root.jsx',
|
|
22
|
-
'layouts/',
|
|
23
|
-
'context.js',
|
|
24
|
-
'core.jsx',
|
|
25
|
-
]
|
|
26
|
-
virtualModules.includes = function (virtual) {
|
|
27
|
-
if (!virtual) {
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
for (const entry of this) {
|
|
31
|
-
if (virtual.startsWith(entry)) {
|
|
32
|
-
return true
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return false
|
|
36
|
-
}
|
|
37
|
-
const virtualModuleInserts = {
|
|
38
|
-
'routes.js': {
|
|
39
|
-
$globPattern: routing.globPattern,
|
|
40
|
-
$paramPattern: routing.paramPattern,
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let viteProjectRoot
|
|
45
|
-
|
|
46
|
-
function loadVirtualModuleOverride(virtual) {
|
|
47
|
-
if (!virtualModules.includes(virtual)) {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
const overridePath = resolve(viteProjectRoot, virtual)
|
|
51
|
-
if (existsSync(overridePath)) {
|
|
52
|
-
return overridePath
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function loadVirtualModule(virtual) {
|
|
57
|
-
if (!virtualModules.includes(virtual)) {
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
let code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
|
|
61
|
-
if (virtualModuleInserts[virtual]) {
|
|
62
|
-
for (const [key, value] of Object.entries(
|
|
63
|
-
virtualModuleInserts[virtual],
|
|
64
|
-
)) {
|
|
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.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
name: 'vite-plugin-fastify-react',
|
|
81
|
-
config(config, { command }) {
|
|
82
|
-
if (command === 'build' && config.build?.ssr) {
|
|
83
|
-
config.build.rollupOptions = {
|
|
84
|
-
output: {
|
|
85
|
-
format: 'es',
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
configResolved(config) {
|
|
91
|
-
viteProjectRoot = config.root
|
|
92
|
-
},
|
|
93
|
-
async resolveId(id) {
|
|
94
|
-
const [, virtual] = id.split(prefix)
|
|
95
|
-
if (virtual) {
|
|
96
|
-
const override = await loadVirtualModuleOverride(virtual)
|
|
97
|
-
if (override) {
|
|
98
|
-
return override
|
|
99
|
-
}
|
|
100
|
-
return id
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
load(id) {
|
|
104
|
-
const [, virtual] = id.split(prefix)
|
|
105
|
-
return loadVirtualModule(virtual)
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
module.exports = viteReactFastifyDX
|
package/server/stream.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Helper to make the stream returned renderToPipeableStream()
|
|
2
|
-
// behave like an event emitter and facilitate error handling in Fastify
|
|
3
|
-
import { Minipass } from 'minipass'
|
|
4
|
-
|
|
5
|
-
// React 18's preferred server-side rendering function,
|
|
6
|
-
// which enables the combination of React.lazy() and Suspense
|
|
7
|
-
import { renderToPipeableStream } from 'react-dom/server'
|
|
8
|
-
|
|
9
|
-
// Helper function to prepend and append chunks the body stream
|
|
10
|
-
export async function* generateHtmlStream({ head, body, footer }) {
|
|
11
|
-
for await (const chunk of await head) {
|
|
12
|
-
yield chunk
|
|
13
|
-
}
|
|
14
|
-
if (body) {
|
|
15
|
-
for await (const chunk of await body) {
|
|
16
|
-
yield chunk
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
for await (const chunk of await footer()) {
|
|
20
|
-
yield chunk
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Helper function to get an AsyncIterable (via PassThrough)
|
|
25
|
-
// from the renderToPipeableStream() onShellReady event
|
|
26
|
-
export function onShellReady(app) {
|
|
27
|
-
const duplex = new Minipass()
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
try {
|
|
30
|
-
const pipeable = renderToPipeableStream(app, {
|
|
31
|
-
onShellReady() {
|
|
32
|
-
resolve(pipeable.pipe(duplex))
|
|
33
|
-
},
|
|
34
|
-
})
|
|
35
|
-
} catch (error) {
|
|
36
|
-
resolve(error)
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Helper function to get an AsyncIterable (via Minipass)
|
|
42
|
-
// from the renderToPipeableStream() onAllReady event
|
|
43
|
-
export function onAllReady(app) {
|
|
44
|
-
const duplex = new Minipass()
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
try {
|
|
47
|
-
const pipeable = renderToPipeableStream(app, {
|
|
48
|
-
onAllReady() {
|
|
49
|
-
resolve(pipeable.pipe(duplex))
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
} catch (error) {
|
|
53
|
-
resolve(error)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
}
|