@fastify/react 0.5.0 → 0.6.0
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/index.js +68 -10
- package/package.json +5 -2
- package/plugin.cjs +8 -3
- package/server/context.js +2 -0
- package/virtual/core.jsx +26 -1
- package/virtual/resource.js +11 -6
- package/virtual/routes.js +1 -0
package/index.js
CHANGED
|
@@ -14,6 +14,9 @@ import * as devalue from 'devalue'
|
|
|
14
14
|
// <title>, <meta> and <link> elements
|
|
15
15
|
import Head from 'unihead'
|
|
16
16
|
|
|
17
|
+
// Used for removing <script> tags when serverOnly is enabled
|
|
18
|
+
import { HTMLRewriter } from 'html-rewriter-wasm'
|
|
19
|
+
|
|
17
20
|
// Helpers from the Node.js stream library to
|
|
18
21
|
// make it easier to work with renderToPipeableStream()
|
|
19
22
|
import {
|
|
@@ -27,13 +30,14 @@ import RouteContext from './server/context.js'
|
|
|
27
30
|
|
|
28
31
|
export default {
|
|
29
32
|
prepareClient,
|
|
33
|
+
prepareServer,
|
|
30
34
|
createHtmlFunction,
|
|
31
35
|
createRenderFunction,
|
|
32
36
|
createRouteHandler,
|
|
33
37
|
createRoute,
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
async function prepareClient({
|
|
37
41
|
routes: routesPromise,
|
|
38
42
|
context: contextPromise,
|
|
39
43
|
...others
|
|
@@ -44,17 +48,15 @@ export async function prepareClient({
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// The return value of this function gets registered as reply.html()
|
|
47
|
-
|
|
51
|
+
async function createHtmlFunction(source, scope, config) {
|
|
48
52
|
// Templating functions for universal rendering (SSR+CSR)
|
|
49
53
|
const [unHeadSource, unFooterSource] = source.split('<!-- element -->')
|
|
50
54
|
const unHeadTemplate = createHtmlTemplateFunction(unHeadSource)
|
|
51
55
|
const unFooterTemplate = createHtmlTemplateFunction(unFooterSource)
|
|
52
56
|
// Templating functions for server-only rendering (SSR only)
|
|
53
|
-
const [soHeadSource, soFooterSource] = source
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.replace(/<script[^>]+type="module"[^>]+>.*?<\/script>/g, '')
|
|
57
|
-
.split('<!-- element -->')
|
|
57
|
+
const [soHeadSource, soFooterSource] = (await removeModules(source)).split(
|
|
58
|
+
'<!-- element -->',
|
|
59
|
+
)
|
|
58
60
|
const soHeadTemplate = createHtmlTemplateFunction(soHeadSource)
|
|
59
61
|
const soFooterTemplate = createHtmlTemplateFunction(soFooterSource)
|
|
60
62
|
// This function gets registered as reply.html()
|
|
@@ -93,7 +95,7 @@ export function createHtmlFunction(source, scope, config) {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
async function createRenderFunction({ routes, create }) {
|
|
97
99
|
// create is exported by client/index.js
|
|
98
100
|
return (req) => {
|
|
99
101
|
// Create convenience-access routeMap
|
|
@@ -117,14 +119,37 @@ export async function createRenderFunction({ routes, create }) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
function createRouteHandler({ client }, scope, config) {
|
|
121
123
|
return (req, reply) => {
|
|
122
124
|
reply.html(reply.render(req))
|
|
123
125
|
return reply
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
function prepareServer(server) {
|
|
130
|
+
let url
|
|
131
|
+
server.decorate('serverURL', { getter: () => url })
|
|
132
|
+
server.addHook('onListen', () => {
|
|
133
|
+
const { port, address, family } = server.server.address()
|
|
134
|
+
const protocol = server.https ? 'https' : 'http'
|
|
135
|
+
if (family === 'IPv6') {
|
|
136
|
+
url = `${protocol}://[${address}]:${port}`
|
|
137
|
+
} else {
|
|
138
|
+
url = `${protocol}://${address}:${port}`
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
server.decorateRequest('fetchMap', null)
|
|
142
|
+
server.addHook('onRequest', (req, _, done) => {
|
|
143
|
+
req.fetchMap = new Map()
|
|
144
|
+
done()
|
|
145
|
+
})
|
|
146
|
+
server.addHook('onResponse', (req, _, done) => {
|
|
147
|
+
req.fetchMap = undefined
|
|
148
|
+
done()
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function createRoute(
|
|
128
153
|
{ client, handler, errorHandler, route },
|
|
129
154
|
scope,
|
|
130
155
|
config,
|
|
@@ -138,6 +163,11 @@ export function createRoute(
|
|
|
138
163
|
client.context,
|
|
139
164
|
)
|
|
140
165
|
}
|
|
166
|
+
|
|
167
|
+
if (route.configure) {
|
|
168
|
+
await route.configure(scope)
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
if (route.getData) {
|
|
142
172
|
// If getData is provided, register JSON endpoint for it
|
|
143
173
|
scope.get(`/-/data${route.path}`, {
|
|
@@ -189,3 +219,31 @@ export function createRoute(
|
|
|
189
219
|
...route,
|
|
190
220
|
})
|
|
191
221
|
}
|
|
222
|
+
|
|
223
|
+
async function removeModules(html) {
|
|
224
|
+
const decoder = new TextDecoder()
|
|
225
|
+
|
|
226
|
+
let output = ''
|
|
227
|
+
const rewriter = new HTMLRewriter((outputChunk) => {
|
|
228
|
+
output += decoder.decode(outputChunk)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
rewriter.on('script', {
|
|
232
|
+
element(element) {
|
|
233
|
+
for (const [attr, value] of element.attributes) {
|
|
234
|
+
if (attr === 'type' && value === 'module') {
|
|
235
|
+
element.replace('')
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const encoder = new TextEncoder()
|
|
243
|
+
await rewriter.write(encoder.encode(html))
|
|
244
|
+
await rewriter.end()
|
|
245
|
+
return output
|
|
246
|
+
} finally {
|
|
247
|
+
rewriter.free()
|
|
248
|
+
}
|
|
249
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"name": "@fastify/react",
|
|
5
5
|
"description": "The official @fastify/vite renderer for React",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.0",
|
|
7
7
|
"files": [
|
|
8
8
|
"virtual/create.jsx",
|
|
9
9
|
"virtual/root.jsx",
|
|
@@ -25,12 +25,15 @@
|
|
|
25
25
|
"./plugin": "./plugin.cjs"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@fastify/vite": "^6.0.5",
|
|
29
|
+
"acorn-strip-function": "^1.2.0",
|
|
28
30
|
"devalue": "latest",
|
|
29
31
|
"history": "latest",
|
|
32
|
+
"html-rewriter-wasm": "^0.4.1",
|
|
30
33
|
"minipass": "latest",
|
|
31
34
|
"react": "^18.2.0",
|
|
32
35
|
"react-dom": "^18.2.0",
|
|
33
|
-
"react-router-dom": "
|
|
36
|
+
"react-router-dom": "^6",
|
|
34
37
|
"unihead": "latest",
|
|
35
38
|
"valtio": "latest"
|
|
36
39
|
},
|
package/plugin.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const { readFileSync, existsSync } = require('fs')
|
|
2
2
|
const { dirname, join, resolve } = require('path')
|
|
3
3
|
const { fileURLToPath } = require('url')
|
|
4
|
+
const stripFunction = require('acorn-strip-function')
|
|
4
5
|
|
|
5
|
-
function
|
|
6
|
+
function viteFastifyReact(config = {}) {
|
|
6
7
|
const prefix = /^\/:/
|
|
7
8
|
const routing = Object.assign(
|
|
8
9
|
{
|
|
@@ -100,11 +101,15 @@ function viteReactFastifyDX(config = {}) {
|
|
|
100
101
|
return id
|
|
101
102
|
}
|
|
102
103
|
},
|
|
103
|
-
load(id) {
|
|
104
|
+
load(id, options) {
|
|
105
|
+
if (!options?.ssr && !id.startsWith('/:') && id.match(/.(j|t)sx$/)) {
|
|
106
|
+
const source = readFileSync(id, 'utf8')
|
|
107
|
+
return stripFunction(stripFunction(source, 'configure'), 'getData')
|
|
108
|
+
}
|
|
104
109
|
const [, virtual] = id.split(prefix)
|
|
105
110
|
return loadVirtualModule(virtual)
|
|
106
111
|
},
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
module.exports =
|
|
115
|
+
module.exports = viteFastifyReact
|
package/server/context.js
CHANGED
|
@@ -19,6 +19,7 @@ export default class RouteContext {
|
|
|
19
19
|
this.req = req
|
|
20
20
|
this.reply = reply
|
|
21
21
|
this.head = {}
|
|
22
|
+
this.actionData = {}
|
|
22
23
|
this.state = null
|
|
23
24
|
this.data = route.data
|
|
24
25
|
this.firstRender = true
|
|
@@ -42,6 +43,7 @@ export default class RouteContext {
|
|
|
42
43
|
|
|
43
44
|
toJSON() {
|
|
44
45
|
return {
|
|
46
|
+
actionData: this.actionData,
|
|
45
47
|
state: this.state,
|
|
46
48
|
data: this.data,
|
|
47
49
|
layout: this.layout,
|
package/virtual/core.jsx
CHANGED
|
@@ -20,6 +20,30 @@ export function useRouteContext() {
|
|
|
20
20
|
return routeContext
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
let serverActionCounter = 0
|
|
24
|
+
|
|
25
|
+
export function createServerAction(name) {
|
|
26
|
+
return `/-/action/${name ?? serverActionCounter++}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useServerAction(action, options = {}) {
|
|
30
|
+
if (import.meta.env.SSR) {
|
|
31
|
+
const { req, server } = useRouteContext()
|
|
32
|
+
req.route.actionData[action] = waitFetch(
|
|
33
|
+
`${server.serverURL}${action}`,
|
|
34
|
+
options,
|
|
35
|
+
req.fetchMap,
|
|
36
|
+
)
|
|
37
|
+
return req.route.actionData[action]
|
|
38
|
+
}
|
|
39
|
+
const { actionData } = useRouteContext()
|
|
40
|
+
if (actionData[action]) {
|
|
41
|
+
return actionData[action]
|
|
42
|
+
}
|
|
43
|
+
actionData[action] = waitFetch(action, options)
|
|
44
|
+
return actionData[action]
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
export function AppRoute({ head, ctxHydration, ctx, children }) {
|
|
24
48
|
// If running on the server, assume all data
|
|
25
49
|
// functions have already ran through the preHandler hook
|
|
@@ -62,6 +86,7 @@ export function AppRoute({ head, ctxHydration, ctx, children }) {
|
|
|
62
86
|
// biome-ignore lint/correctness/useExhaustiveDependencies: I'm inclined to believe you, Biome, but I'm not risking it.
|
|
63
87
|
useEffect(() => {
|
|
64
88
|
window.route.firstRender = false
|
|
89
|
+
window.route.actionData = {}
|
|
65
90
|
}, [location])
|
|
66
91
|
|
|
67
92
|
// If we have a getData function registered for this route
|
|
@@ -69,7 +94,7 @@ export function AppRoute({ head, ctxHydration, ctx, children }) {
|
|
|
69
94
|
try {
|
|
70
95
|
const { pathname, search } = location
|
|
71
96
|
// If not, fetch data from the JSON endpoint
|
|
72
|
-
ctx.data = waitFetch(
|
|
97
|
+
ctx.data = waitFetch(`/-/data${pathname}${search}`)
|
|
73
98
|
} catch (status) {
|
|
74
99
|
// If it's an actual error...
|
|
75
100
|
if (status instanceof Error) {
|
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
|
}
|