@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/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
- import create from '/:create.jsx'
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 routesPromise
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
- if (ctxHydration.clientOnly) {
29
- createRoot(target).render(app)
30
- } else {
31
- hydrateRoot(target, app)
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
- async function extendContext(
36
- ctx,
37
- {
38
- // The route context initialization function
39
- default: setter,
40
- // We destructure state here just to discard it from extra
41
- state,
42
- // Other named exports from context.js
43
- ...extra
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)
@@ -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/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 '/:core.jsx'
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
- /* global $paramPattern */
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
- }