@fastify/react 0.4.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 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
- export async function prepareClient({
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
- export function createHtmlFunction(source, scope, config) {
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
- // Unsafe if dealing with user-input, but safe here
55
- // where we control the index.html source
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
- export async function createRenderFunction({ routes, create }) {
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
- export function createRouteHandler({ client }, scope, config) {
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
- export function createRoute(
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.4.0",
6
+ "version": "0.6.0",
7
7
  "files": [
8
8
  "virtual/create.jsx",
9
9
  "virtual/root.jsx",
@@ -25,21 +25,21 @@
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": "latest",
36
+ "react-router-dom": "^6",
34
37
  "unihead": "latest",
35
38
  "valtio": "latest"
36
39
  },
37
40
  "devDependencies": {
38
41
  "@biomejs/biome": "^1.5.3"
39
42
  },
40
- "peerDependencies": {
41
- "@fastify/vite": "^6.0.2"
42
- },
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
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 viteReactFastifyDX(config = {}) {
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 = viteReactFastifyDX
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
@@ -49,6 +73,9 @@ export function AppRoute({ head, ctxHydration, ctx, children }) {
49
73
  if (ctx.firstRender) {
50
74
  ctx.data = window.route.data
51
75
  ctx.head = window.route.head
76
+ } else {
77
+ ctx.data = undefined
78
+ ctx.head = undefined
52
79
  }
53
80
 
54
81
  const location = useLocation()
@@ -59,6 +86,7 @@ export function AppRoute({ head, ctxHydration, ctx, children }) {
59
86
  // biome-ignore lint/correctness/useExhaustiveDependencies: I'm inclined to believe you, Biome, but I'm not risking it.
60
87
  useEffect(() => {
61
88
  window.route.firstRender = false
89
+ window.route.actionData = {}
62
90
  }, [location])
63
91
 
64
92
  // If we have a getData function registered for this route
@@ -66,7 +94,7 @@ export function AppRoute({ head, ctxHydration, ctx, children }) {
66
94
  try {
67
95
  const { pathname, search } = location
68
96
  // If not, fetch data from the JSON endpoint
69
- ctx.data = waitFetch(`${pathname}${search}`)
97
+ ctx.data = waitFetch(`/-/data${pathname}${search}`)
70
98
  } catch (status) {
71
99
  // If it's an actual error...
72
100
  if (status instanceof Error) {
@@ -1,7 +1,12 @@
1
- const fetchMap = new Map()
2
- const resourceMap = new Map()
1
+ const clientFetchMap = new Map()
2
+ const clientResourceMap = new Map()
3
3
 
4
- export function waitResource(path, id, promise) {
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(`/-/data${path}`)
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/routes.js CHANGED
@@ -93,6 +93,7 @@ function getRouteModuleExports(routeModule) {
93
93
  streaming: routeModule.streaming,
94
94
  clientOnly: routeModule.clientOnly,
95
95
  serverOnly: routeModule.serverOnly,
96
+ ...routeModule,
96
97
  }
97
98
  }
98
99