@fastify/react 0.6.0 → 1.0.0-beta.2

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)
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,123 +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
- ...routeModule,
97
- }
98
- }
99
-
100
- async function getRouteModule(path, routeModuleInput) {
101
- let routeModule = routeModuleInput
102
- // const isServer = typeof process !== 'undefined'
103
- if (typeof routeModule === 'function') {
104
- routeModule = await routeModule()
105
- return getRouteModuleExports(routeModule)
106
- }
107
- return getRouteModuleExports(routeModule)
108
- }
109
-
110
- function memoImport(func) {
111
- // Otherwise we get a ReferenceError, but since this function
112
- // is only ran once for each route, there's no overhead
113
- const kFuncExecuted = Symbol('kFuncExecuted')
114
- const kFuncValue = Symbol('kFuncValue')
115
- func[kFuncExecuted] = false
116
- return async () => {
117
- if (!func[kFuncExecuted]) {
118
- func[kFuncValue] = await func()
119
- func[kFuncExecuted] = true
120
- }
121
- return func[kFuncValue]
122
- }
123
- }
1
+ export default import.meta.glob('/pages/**/*.{jsx,tsx}')
package/plugin.cjs DELETED
@@ -1,115 +0,0 @@
1
- const { readFileSync, existsSync } = require('fs')
2
- const { dirname, join, resolve } = require('path')
3
- const { fileURLToPath } = require('url')
4
- const stripFunction = require('acorn-strip-function')
5
-
6
- function viteFastifyReact(config = {}) {
7
- const prefix = /^\/:/
8
- const routing = Object.assign(
9
- {
10
- globPattern: '/pages/**/*.{jsx,tsx}',
11
- paramPattern: /\[(\w+)\]/,
12
- },
13
- config,
14
- )
15
- const virtualRoot = resolve(__dirname, 'virtual')
16
- const virtualModules = [
17
- 'mount.js',
18
- 'resource.js',
19
- 'routes.js',
20
- 'layouts.js',
21
- 'create.jsx',
22
- 'root.jsx',
23
- 'layouts/',
24
- 'context.js',
25
- 'core.jsx',
26
- ]
27
- virtualModules.includes = function (virtual) {
28
- if (!virtual) {
29
- return false
30
- }
31
- for (const entry of this) {
32
- if (virtual.startsWith(entry)) {
33
- return true
34
- }
35
- }
36
- return false
37
- }
38
- const virtualModuleInserts = {
39
- 'routes.js': {
40
- $globPattern: routing.globPattern,
41
- $paramPattern: routing.paramPattern,
42
- },
43
- }
44
-
45
- let viteProjectRoot
46
-
47
- function loadVirtualModuleOverride(virtual) {
48
- if (!virtualModules.includes(virtual)) {
49
- return
50
- }
51
- const overridePath = resolve(viteProjectRoot, virtual)
52
- if (existsSync(overridePath)) {
53
- return overridePath
54
- }
55
- }
56
-
57
- function loadVirtualModule(virtual) {
58
- if (!virtualModules.includes(virtual)) {
59
- return
60
- }
61
- let code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
62
- if (virtualModuleInserts[virtual]) {
63
- for (const [key, value] of Object.entries(
64
- virtualModuleInserts[virtual],
65
- )) {
66
- code = code.replace(new RegExp(escapeRegExp(key), 'g'), value)
67
- }
68
- }
69
- return {
70
- code,
71
- map: null,
72
- }
73
- }
74
-
75
- // Thanks to https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
76
- function escapeRegExp(s) {
77
- return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
78
- }
79
-
80
- return {
81
- name: 'vite-plugin-fastify-react',
82
- config(config, { command }) {
83
- if (command === 'build' && config.build?.ssr) {
84
- config.build.rollupOptions = {
85
- output: {
86
- format: 'es',
87
- },
88
- }
89
- }
90
- },
91
- configResolved(config) {
92
- viteProjectRoot = config.root
93
- },
94
- async resolveId(id) {
95
- const [, virtual] = id.split(prefix)
96
- if (virtual) {
97
- const override = await loadVirtualModuleOverride(virtual)
98
- if (override) {
99
- return override
100
- }
101
- return id
102
- }
103
- },
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
- }
109
- const [, virtual] = id.split(prefix)
110
- return loadVirtualModule(virtual)
111
- },
112
- }
113
- }
114
-
115
- module.exports = viteFastifyReact
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
- }
File without changes