@fastify/react 0.3.0 → 0.4.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
@@ -16,7 +16,11 @@ import Head from 'unihead'
16
16
 
17
17
  // Helpers from the Node.js stream library to
18
18
  // make it easier to work with renderToPipeableStream()
19
- import { generateHtmlStream, onAllReady, onShellReady } from './server/stream.js'
19
+ import {
20
+ generateHtmlStream,
21
+ onAllReady,
22
+ onShellReady,
23
+ } from './server/stream.js'
20
24
 
21
25
  // Holds the universal route context
22
26
  import RouteContext from './server/context.js'
@@ -29,7 +33,7 @@ export default {
29
33
  createRoute,
30
34
  }
31
35
 
32
- export async function prepareClient ({
36
+ export async function prepareClient({
33
37
  routes: routesPromise,
34
38
  context: contextPromise,
35
39
  ...others
@@ -40,7 +44,7 @@ export async function prepareClient ({
40
44
  }
41
45
 
42
46
  // The return value of this function gets registered as reply.html()
43
- export function createHtmlFunction (source, scope, config) {
47
+ export function createHtmlFunction(source, scope, config) {
44
48
  // Templating functions for universal rendering (SSR+CSR)
45
49
  const [unHeadSource, unFooterSource] = source.split('<!-- element -->')
46
50
  const unHeadTemplate = createHtmlTemplateFunction(unHeadSource)
@@ -57,66 +61,75 @@ export function createHtmlFunction (source, scope, config) {
57
61
  return function ({ routes, context, body }) {
58
62
  // Decide which templating functions to use, with and without hydration
59
63
  const headTemplate = context.serverOnly ? soHeadTemplate : unHeadTemplate
60
- const footerTemplate = context.serverOnly ? soFooterTemplate : unFooterTemplate
64
+ const footerTemplate = context.serverOnly
65
+ ? soFooterTemplate
66
+ : unFooterTemplate
61
67
  // Render page-level <head> elements
62
68
  const head = new Head(context.head).render()
63
69
  // Create readable stream with prepended and appended chunks
64
- const readable = Readable.from(generateHtmlStream({
65
- body: body && (
66
- context.streaming
67
- ? onShellReady(body)
68
- : onAllReady(body)
69
- ),
70
- head: headTemplate({ ...context, head }),
71
- footer: () => footerTemplate({
72
- ...context,
73
- hydration: '',
74
- // Decide whether or not to include the hydration script
75
- ...!context.serverOnly && {
76
- hydration: (
77
- '<script>\n' +
78
- `window.route = ${devalue.uneval(context.toJSON())}\n` +
79
- `window.routes = ${devalue.uneval(routes.toJSON())}\n` +
80
- '</script>'
81
- ),
82
- },
70
+ const readable = Readable.from(
71
+ generateHtmlStream({
72
+ body:
73
+ body && (context.streaming ? onShellReady(body) : onAllReady(body)),
74
+ head: headTemplate({ ...context, head }),
75
+ footer: () =>
76
+ footerTemplate({
77
+ ...context,
78
+ hydration: '',
79
+ // Decide whether or not to include the hydration script
80
+ ...(!context.serverOnly && {
81
+ hydration: `<script>\nwindow.route = ${devalue.uneval(
82
+ context.toJSON(),
83
+ )}\nwindow.routes = ${devalue.uneval(
84
+ routes.toJSON(),
85
+ )}\n</script>`,
86
+ }),
87
+ }),
83
88
  }),
84
- }))
89
+ )
85
90
  // Send out header and readable stream with full response
86
91
  this.type('text/html')
87
92
  this.send(readable)
88
93
  }
89
94
  }
90
95
 
91
- export async function createRenderFunction ({ routes, create }) {
96
+ export async function createRenderFunction({ routes, create }) {
92
97
  // create is exported by client/index.js
93
- return function (req) {
98
+ return (req) => {
94
99
  // Create convenience-access routeMap
95
- const routeMap = Object.fromEntries(routes.toJSON().map((route) => {
96
- return [route.path, route]
97
- }))
100
+ const routeMap = Object.fromEntries(
101
+ routes.toJSON().map((route) => {
102
+ return [route.path, route]
103
+ }),
104
+ )
98
105
  // Creates main React component with all the SSR context it needs
99
- const app = !req.route.clientOnly && create({
100
- routes,
101
- routeMap,
102
- ctxHydration: req.route,
103
- url: req.url,
104
- })
106
+ const app =
107
+ !req.route.clientOnly &&
108
+ create({
109
+ routes,
110
+ routeMap,
111
+ ctxHydration: req.route,
112
+ url: req.url,
113
+ })
105
114
  // Perform SSR, i.e., turn app.instance into an HTML fragment
106
115
  // The SSR context data is passed along so it can be inlined for hydration
107
116
  return { routes, context: req.route, body: app }
108
117
  }
109
118
  }
110
119
 
111
- export function createRouteHandler (client, scope, config) {
112
- return function (req, reply) {
120
+ export function createRouteHandler({ client }, scope, config) {
121
+ return (req, reply) => {
113
122
  reply.html(reply.render(req))
114
123
  return reply
115
124
  }
116
125
  }
117
126
 
118
- export function createRoute ({ client, handler, errorHandler, route }, scope, config) {
119
- const onRequest = async function onRequest (req, reply) {
127
+ export function createRoute(
128
+ { client, handler, errorHandler, route },
129
+ scope,
130
+ config,
131
+ ) {
132
+ const onRequest = async function onRequest(req, reply) {
120
133
  req.route = await RouteContext.create(
121
134
  scope,
122
135
  req,
@@ -129,16 +142,14 @@ export function createRoute ({ client, handler, errorHandler, route }, scope, co
129
142
  // If getData is provided, register JSON endpoint for it
130
143
  scope.get(`/-/data${route.path}`, {
131
144
  onRequest,
132
- async handler (req, reply) {
145
+ async handler(req, reply) {
133
146
  reply.send(await route.getData(req.route))
134
147
  },
135
148
  })
136
149
  }
137
150
 
138
151
  // See https://github.com/fastify/fastify-dx/blob/main/URMA.md
139
- const hasURMAHooks = Boolean(
140
- route.getData || route.getMeta || route.onEnter,
141
- )
152
+ const hasURMAHooks = Boolean(route.getData || route.getMeta || route.onEnter)
142
153
 
143
154
  // Extend with route context initialization module
144
155
  RouteContext.extend(client.context)
@@ -149,8 +160,8 @@ export function createRoute ({ client, handler, errorHandler, route }, scope, co
149
160
  onRequest,
150
161
  // If either getData or onEnter are provided,
151
162
  // make sure they run before the SSR route handler
152
- ...hasURMAHooks && {
153
- async preHandler (req, reply) {
163
+ ...(hasURMAHooks && {
164
+ async preHandler(req, reply) {
154
165
  try {
155
166
  if (route.getData) {
156
167
  req.route.data = await route.getData(req.route)
@@ -172,7 +183,7 @@ export function createRoute ({ client, handler, errorHandler, route }, scope, co
172
183
  req.route.error = err
173
184
  }
174
185
  },
175
- },
186
+ }),
176
187
  handler,
177
188
  errorHandler,
178
189
  ...route,
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.3.0",
6
+ "version": "0.4.0",
7
7
  "files": [
8
8
  "virtual/create.jsx",
9
9
  "virtual/root.jsx",
@@ -29,28 +29,21 @@
29
29
  "history": "latest",
30
30
  "minipass": "latest",
31
31
  "react": "^18.2.0",
32
- "react-dom": "latest",
32
+ "react-dom": "^18.2.0",
33
33
  "react-router-dom": "latest",
34
34
  "unihead": "latest",
35
35
  "valtio": "latest"
36
36
  },
37
37
  "devDependencies": {
38
- "@babel/eslint-parser": "latest",
39
- "@babel/preset-react": "latest",
40
- "eslint": "latest",
41
- "eslint-config-standard": "latest",
42
- "eslint-plugin-import": "latest",
43
- "eslint-plugin-node": "latest",
44
- "eslint-plugin-promise": "latest",
45
- "eslint-plugin-react": "latest"
38
+ "@biomejs/biome": "^1.5.3"
46
39
  },
47
40
  "peerDependencies": {
48
- "@fastify/vite": "^5.0.5"
41
+ "@fastify/vite": "^6.0.2"
49
42
  },
50
43
  "publishConfig": {
51
44
  "access": "public"
52
45
  },
53
46
  "scripts": {
54
- "lint": "eslint . --ext .js,.jsx --fix"
47
+ "lint": "biome check --apply-unsafe ."
55
48
  }
56
49
  }
package/plugin.cjs CHANGED
@@ -2,15 +2,18 @@ const { readFileSync, existsSync } = require('fs')
2
2
  const { dirname, join, resolve } = require('path')
3
3
  const { fileURLToPath } = require('url')
4
4
 
5
- function viteReactFastifyDX (config = {}) {
5
+ function viteReactFastifyDX(config = {}) {
6
6
  const prefix = /^\/:/
7
- const routing = Object.assign({
8
- globPattern: '/pages/**/*.(jsx|tsx)',
9
- paramPattern: /\[(\w+)\]/,
10
- }, config)
7
+ const routing = Object.assign(
8
+ {
9
+ globPattern: '/pages/**/*.{jsx,tsx}',
10
+ paramPattern: /\[(\w+)\]/,
11
+ },
12
+ config,
13
+ )
11
14
  const virtualRoot = resolve(__dirname, 'virtual')
12
- const virtualModules = [
13
- 'mount.js',
15
+ const virtualModules = [
16
+ 'mount.js',
14
17
  'resource.js',
15
18
  'routes.js',
16
19
  'layouts.js',
@@ -18,7 +21,7 @@ function viteReactFastifyDX (config = {}) {
18
21
  'root.jsx',
19
22
  'layouts/',
20
23
  'context.js',
21
- 'core.jsx'
24
+ 'core.jsx',
22
25
  ]
23
26
  virtualModules.includes = function (virtual) {
24
27
  if (!virtual) {
@@ -35,12 +38,12 @@ function viteReactFastifyDX (config = {}) {
35
38
  'routes.js': {
36
39
  $globPattern: routing.globPattern,
37
40
  $paramPattern: routing.paramPattern,
38
- }
41
+ },
39
42
  }
40
43
 
41
44
  let viteProjectRoot
42
45
 
43
- function loadVirtualModuleOverride (virtual) {
46
+ function loadVirtualModuleOverride(virtual) {
44
47
  if (!virtualModules.includes(virtual)) {
45
48
  return
46
49
  }
@@ -50,13 +53,15 @@ function viteReactFastifyDX (config = {}) {
50
53
  }
51
54
  }
52
55
 
53
- function loadVirtualModule (virtual) {
56
+ function loadVirtualModule(virtual) {
54
57
  if (!virtualModules.includes(virtual)) {
55
58
  return
56
59
  }
57
60
  let code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
58
61
  if (virtualModuleInserts[virtual]) {
59
- for (const [key, value] of Object.entries(virtualModuleInserts[virtual])) {
62
+ for (const [key, value] of Object.entries(
63
+ virtualModuleInserts[virtual],
64
+ )) {
60
65
  code = code.replace(new RegExp(escapeRegExp(key), 'g'), value)
61
66
  }
62
67
  }
@@ -67,15 +72,13 @@ function viteReactFastifyDX (config = {}) {
67
72
  }
68
73
 
69
74
  // Thanks to https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
70
- function escapeRegExp (s) {
71
- return s
72
- .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
73
- .replace(/-/g, '\\x2d')
75
+ function escapeRegExp(s) {
76
+ return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
74
77
  }
75
78
 
76
79
  return {
77
80
  name: 'vite-plugin-fastify-react',
78
- config (config, { command }) {
81
+ config(config, { command }) {
79
82
  if (command === 'build' && config.build?.ssr) {
80
83
  config.build.rollupOptions = {
81
84
  output: {
@@ -84,10 +87,10 @@ function viteReactFastifyDX (config = {}) {
84
87
  }
85
88
  }
86
89
  },
87
- configResolved (config) {
90
+ configResolved(config) {
88
91
  viteProjectRoot = config.root
89
92
  },
90
- async resolveId (id) {
93
+ async resolveId(id) {
91
94
  const [, virtual] = id.split(prefix)
92
95
  if (virtual) {
93
96
  const override = await loadVirtualModuleOverride(virtual)
@@ -97,7 +100,7 @@ function viteReactFastifyDX (config = {}) {
97
100
  return id
98
101
  }
99
102
  },
100
- load (id) {
103
+ load(id) {
101
104
  const [, virtual] = id.split(prefix)
102
105
  return loadVirtualModule(virtual)
103
106
  },
package/server/context.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const routeContextInspect = Symbol.for('nodejs.util.inspect.custom')
2
2
 
3
3
  export default class RouteContext {
4
- static async create (server, req, reply, route, contextInit) {
4
+ static async create(server, req, reply, route, contextInit) {
5
5
  const routeContext = new RouteContext(server, req, reply, route)
6
6
  if (contextInit) {
7
7
  if (contextInit.state) {
@@ -14,7 +14,7 @@ export default class RouteContext {
14
14
  return routeContext
15
15
  }
16
16
 
17
- constructor (server, req, reply, route) {
17
+ constructor(server, req, reply, route) {
18
18
  this.server = server
19
19
  this.req = req
20
20
  this.reply = reply
@@ -31,7 +31,7 @@ export default class RouteContext {
31
31
  this.serverOnly = route.serverOnly
32
32
  }
33
33
 
34
- [routeContextInspect] () {
34
+ [routeContextInspect]() {
35
35
  return {
36
36
  ...this,
37
37
  server: { [routeContextInspect]: () => '[Server]' },
@@ -40,7 +40,7 @@ export default class RouteContext {
40
40
  }
41
41
  }
42
42
 
43
- toJSON () {
43
+ toJSON() {
44
44
  return {
45
45
  state: this.state,
46
46
  data: this.data,
@@ -54,7 +54,7 @@ export default class RouteContext {
54
54
  }
55
55
  }
56
56
 
57
- RouteContext.extend = function (initial) {
57
+ RouteContext.extend = (initial) => {
58
58
  const { default: _, ...extra } = initial
59
59
  for (const [prop, value] of Object.entries(extra)) {
60
60
  if (prop !== 'data' && prop !== 'state') {
package/server/stream.js CHANGED
@@ -7,24 +7,28 @@ import { Minipass } from 'minipass'
7
7
  import { renderToPipeableStream } from 'react-dom/server'
8
8
 
9
9
  // Helper function to prepend and append chunks the body stream
10
- export async function * generateHtmlStream ({ head, body, footer }) {
11
- yield head
10
+ export async function* generateHtmlStream({ head, body, footer }) {
11
+ for await (const chunk of await head) {
12
+ yield chunk
13
+ }
12
14
  if (body) {
13
15
  for await (const chunk of await body) {
14
16
  yield chunk
15
17
  }
16
18
  }
17
- yield footer()
19
+ for await (const chunk of await footer()) {
20
+ yield chunk
21
+ }
18
22
  }
19
23
 
20
24
  // Helper function to get an AsyncIterable (via PassThrough)
21
25
  // from the renderToPipeableStream() onShellReady event
22
- export function onShellReady (app) {
26
+ export function onShellReady(app) {
23
27
  const duplex = new Minipass()
24
28
  return new Promise((resolve, reject) => {
25
29
  try {
26
30
  const pipeable = renderToPipeableStream(app, {
27
- onShellReady () {
31
+ onShellReady() {
28
32
  resolve(pipeable.pipe(duplex))
29
33
  },
30
34
  })
@@ -36,12 +40,12 @@ export function onShellReady (app) {
36
40
 
37
41
  // Helper function to get an AsyncIterable (via Minipass)
38
42
  // from the renderToPipeableStream() onAllReady event
39
- export function onAllReady (app) {
43
+ export function onAllReady(app) {
40
44
  const duplex = new Minipass()
41
45
  return new Promise((resolve, reject) => {
42
46
  try {
43
47
  const pipeable = renderToPipeableStream(app, {
44
- onAllReady () {
48
+ onAllReady() {
45
49
  resolve(pipeable.pipe(duplex))
46
50
  },
47
51
  })
package/virtual/core.jsx CHANGED
@@ -1,16 +1,16 @@
1
+ import { createPath } from 'history'
1
2
  import { createContext, useContext, useEffect } from 'react'
2
- import { useLocation, BrowserRouter } from 'react-router-dom'
3
+ import { BrowserRouter, useLocation } from 'react-router-dom'
3
4
  import { StaticRouter } from 'react-router-dom/server.mjs'
4
- import { createPath } from 'history'
5
5
  import { proxy, useSnapshot } from 'valtio'
6
- import { waitResource, waitFetch } from '/:resource.js'
7
6
  import layouts from '/:layouts.js'
7
+ import { waitFetch, waitResource } from '/:resource.js'
8
8
 
9
9
  export const isServer = import.meta.env.SSR
10
10
  export const Router = isServer ? StaticRouter : BrowserRouter
11
11
  export const RouteContext = createContext({})
12
12
 
13
- export function useRouteContext () {
13
+ export function useRouteContext() {
14
14
  const routeContext = useContext(RouteContext)
15
15
  if (routeContext.state) {
16
16
  routeContext.snapshot = isServer
@@ -20,22 +20,22 @@ export function useRouteContext () {
20
20
  return routeContext
21
21
  }
22
22
 
23
- export function AppRoute ({ head, ctxHydration, ctx, children }) {
23
+ export function AppRoute({ head, ctxHydration, ctx, children }) {
24
24
  // If running on the server, assume all data
25
25
  // functions have already ran through the preHandler hook
26
26
  if (isServer) {
27
27
  const Layout = layouts[ctxHydration.layout ?? 'default']
28
28
  return (
29
- <RouteContext.Provider value={{
30
- ...ctx,
31
- ...ctxHydration,
32
- state: isServer
33
- ? ctxHydration.state ?? {}
34
- : proxy(ctxHydration.state ?? {}),
35
- }}>
36
- <Layout>
37
- {children}
38
- </Layout>
29
+ <RouteContext.Provider
30
+ value={{
31
+ ...ctx,
32
+ ...ctxHydration,
33
+ state: isServer
34
+ ? ctxHydration.state ?? {}
35
+ : proxy(ctxHydration.state ?? {}),
36
+ }}
37
+ >
38
+ <Layout>{children}</Layout>
39
39
  </RouteContext.Provider>
40
40
  )
41
41
  }
@@ -56,6 +56,7 @@ export function AppRoute ({ head, ctxHydration, ctx, children }) {
56
56
 
57
57
  // When the next route renders client-side,
58
58
  // force it to execute all URMA hooks again
59
+ // biome-ignore lint/correctness/useExhaustiveDependencies: I'm inclined to believe you, Biome, but I'm not risking it.
59
60
  useEffect(() => {
60
61
  window.route.firstRender = false
61
62
  }, [location])
@@ -102,16 +103,16 @@ export function AppRoute ({ head, ctxHydration, ctx, children }) {
102
103
  const Layout = layouts[ctx.layout ?? 'default']
103
104
 
104
105
  return (
105
- <RouteContext.Provider value={{
106
- ...ctxHydration,
107
- ...ctx,
108
- state: isServer
109
- ? ctxHydration.state ?? {}
110
- : proxy(ctxHydration.state ?? {}),
111
- }}>
112
- <Layout>
113
- {children}
114
- </Layout>
106
+ <RouteContext.Provider
107
+ value={{
108
+ ...ctxHydration,
109
+ ...ctx,
110
+ state: isServer
111
+ ? ctxHydration.state ?? {}
112
+ : proxy(ctxHydration.state ?? {}),
113
+ }}
114
+ >
115
+ <Layout>{children}</Layout>
115
116
  </RouteContext.Provider>
116
117
  )
117
118
  }
@@ -1,7 +1,5 @@
1
1
  import Root from '/:root.jsx'
2
2
 
3
- export default function create ({ url, ...serverInit }) {
4
- return (
5
- <Root url={url} {...serverInit} />
6
- )
3
+ export default function create({ url, ...serverInit }) {
4
+ return <Root url={url} {...serverInit} />
7
5
  }
@@ -1,12 +1,8 @@
1
1
  // This file serves as a placeholder
2
- // if no layout.jsx file is provided
2
+ // if no layouts/default.jsx file is provided
3
3
 
4
4
  import { Suspense } from 'react'
5
5
 
6
- export default function Layout ({ children }) {
7
- return (
8
- <Suspense>
9
- {children}
10
- </Suspense>
11
- )
6
+ export default function Layout({ children }) {
7
+ return <Suspense>{children}</Suspense>
12
8
  }
@@ -2,9 +2,15 @@ import { lazy } from 'react'
2
2
 
3
3
  const DefaultLayout = () => import('/:layouts/default.jsx')
4
4
 
5
- const appLayouts = import.meta.glob('/layouts/*.jsx')
5
+ const appLayouts = import.meta.glob('/layouts/*.{jsx,tsx}')
6
6
 
7
- appLayouts['/layouts/default.jsx'] ??= DefaultLayout
7
+ if (
8
+ !Object.keys(appLayouts).some((path) =>
9
+ path.match(/\/layouts\/default\.(j|t)sx/),
10
+ )
11
+ ) {
12
+ appLayouts['/layouts/default.jsx'] = DefaultLayout
13
+ }
8
14
 
9
15
  export default Object.fromEntries(
10
16
  Object.keys(appLayouts).map((path) => {
package/virtual/mount.js CHANGED
@@ -1,14 +1,15 @@
1
- import Head from 'unihead/client'
2
1
  import { createRoot, hydrateRoot } from 'react-dom/client'
2
+ import Head from 'unihead/client'
3
3
 
4
4
  import create from '/:create.jsx'
5
5
  import routesPromise from '/:routes.js'
6
6
 
7
- mount('main')
7
+ mount('root')
8
8
 
9
- async function mount (target) {
9
+ async function mount(targetInput) {
10
+ let target = targetInput
10
11
  if (typeof target === 'string') {
11
- target = document.querySelector(target)
12
+ target = document.getElementById(target)
12
13
  }
13
14
  const context = await import('/:context.js')
14
15
  const ctxHydration = await extendContext(window.route, context)
@@ -31,14 +32,17 @@ async function mount (target) {
31
32
  }
32
33
  }
33
34
 
34
- async function extendContext (ctx, {
35
- // The route context initialization function
36
- default: setter,
37
- // We destructure state here just to discard it from extra
38
- state,
39
- // Other named exports from context.js
40
- ...extra
41
- }) {
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
+ ) {
42
46
  Object.assign(ctx, extra)
43
47
  if (setter) {
44
48
  await setter(ctx)
@@ -1,67 +1,77 @@
1
1
  const fetchMap = new Map()
2
2
  const resourceMap = new Map()
3
3
 
4
- export function waitResource (path, id, promise) {
4
+ export function waitResource(path, id, promise) {
5
5
  const resourceId = `${path}:${id}`
6
- const loader = resourceMap.get(resourceId)
7
- if (loader) {
8
- if (loader.error) {
9
- throw loader.error
6
+ const loaderStatus = resourceMap.get(resourceId)
7
+ if (loaderStatus) {
8
+ if (loaderStatus.error) {
9
+ throw loaderStatus.error
10
10
  }
11
- if (loader.suspended) {
12
- throw loader.promise
11
+ if (loaderStatus.suspended) {
12
+ throw loaderStatus.promise
13
13
  }
14
14
  resourceMap.delete(resourceId)
15
15
 
16
- return loader.result
17
- } else {
18
- const loader = {
19
- suspended: true,
20
- error: null,
21
- result: null,
22
- promise: null,
23
- }
24
- loader.promise = promise()
25
- .then((result) => { loader.result = result })
26
- .catch((loaderError) => { loader.error = loaderError })
27
- .finally(() => { loader.suspended = false })
16
+ return loaderStatus.result
17
+ }
18
+ const loader = {
19
+ suspended: true,
20
+ error: null,
21
+ result: null,
22
+ promise: null,
23
+ }
24
+ loader.promise = promise()
25
+ .then((result) => {
26
+ loader.result = result
27
+ })
28
+ .catch((loaderError) => {
29
+ loader.error = loaderError
30
+ })
31
+ .finally(() => {
32
+ loader.suspended = false
33
+ })
28
34
 
29
- resourceMap.set(resourceId, loader)
35
+ resourceMap.set(resourceId, loader)
30
36
 
31
- return waitResource(path, id)
32
- }
37
+ return waitResource(path, id)
33
38
  }
34
39
 
35
- export function waitFetch (path) {
36
- const loader = fetchMap.get(path)
37
- if (loader) {
38
- if (loader.error || loader.data?.statusCode === 500) {
39
- if (loader.data?.statusCode === 500) {
40
- throw new Error(loader.data.message)
40
+ export function waitFetch(path) {
41
+ const loaderStatus = fetchMap.get(path)
42
+ if (loaderStatus) {
43
+ if (loaderStatus.error || loaderStatus.data?.statusCode === 500) {
44
+ if (loaderStatus.data?.statusCode === 500) {
45
+ throw new Error(loaderStatus.data.message)
41
46
  }
42
- throw loader.error
47
+ throw loaderStatus.error
43
48
  }
44
- if (loader.suspended) {
45
- throw loader.promise
49
+ if (loaderStatus.suspended) {
50
+ throw loaderStatus.promise
46
51
  }
47
52
  fetchMap.delete(path)
48
53
 
49
- return loader.data
50
- } else {
51
- const loader = {
52
- suspended: true,
53
- error: null,
54
- data: null,
55
- promise: null,
56
- }
57
- loader.promise = fetch(`/-/data${path}`)
58
- .then((response) => response.json())
59
- .then((loaderData) => { loader.data = loaderData })
60
- .catch((loaderError) => { loader.error = loaderError })
61
- .finally(() => { loader.suspended = false })
54
+ return loaderStatus.data
55
+ }
56
+ const loader = {
57
+ suspended: true,
58
+ error: null,
59
+ data: null,
60
+ promise: null,
61
+ }
62
+ loader.promise = fetch(`/-/data${path}`)
63
+ .then((response) => response.json())
64
+ .then((loaderData) => {
65
+ loader.data = loaderData
66
+ })
67
+ .catch((loaderError) => {
68
+ loader.error = loaderError
69
+ })
70
+ .finally(() => {
71
+ loader.suspended = false
72
+ })
62
73
 
63
- fetchMap.set(path, loader)
74
+ fetchMap.set(path, loader)
64
75
 
65
- return waitFetch(path)
66
- }
76
+ return waitFetch(path)
67
77
  }
package/virtual/root.jsx CHANGED
@@ -1,13 +1,13 @@
1
1
  import { Suspense } from 'react'
2
- import { Routes, Route } from 'react-router-dom'
3
- import { Router, AppRoute } from '/:core.jsx'
2
+ import { Route, Routes } from 'react-router-dom'
3
+ import { AppRoute, Router } from '/:core.jsx'
4
4
 
5
- export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
5
+ export default function Root({ url, routes, head, ctxHydration, routeMap }) {
6
6
  return (
7
7
  <Suspense>
8
8
  <Router location={url}>
9
- <Routes>{
10
- routes.map(({ path, component: Component }) =>
9
+ <Routes>
10
+ {routes.map(({ path, component: Component }) => (
11
11
  <Route
12
12
  key={path}
13
13
  path={path}
@@ -15,12 +15,14 @@ export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
15
15
  <AppRoute
16
16
  head={head}
17
17
  ctxHydration={ctxHydration}
18
- ctx={routeMap[path]}>
18
+ ctx={routeMap[path]}
19
+ >
19
20
  <Component />
20
21
  </AppRoute>
21
- } />,
22
- )
23
- }</Routes>
22
+ }
23
+ />
24
+ ))}
25
+ </Routes>
24
26
  </Router>
25
27
  </Suspense>
26
28
  )
package/virtual/routes.js CHANGED
@@ -6,11 +6,11 @@ export default import.meta.env.SSR
6
6
  ? createRoutes(import.meta.glob('$globPattern', { eager: true }))
7
7
  : hydrateRoutes(import.meta.glob('$globPattern'))
8
8
 
9
- async function createRoutes (from, { param } = { param: $paramPattern }) {
9
+ async function createRoutes(from, { param } = { param: $paramPattern }) {
10
10
  // Otherwise we get a ReferenceError, but since
11
11
  // this function is only ran once, there's no overhead
12
12
  class Routes extends Array {
13
- toJSON () {
13
+ toJSON() {
14
14
  return this.map((route) => {
15
15
  return {
16
16
  id: route.id,
@@ -28,26 +28,28 @@ async function createRoutes (from, { param } = { param: $paramPattern }) {
28
28
  if (Array.isArray(from)) {
29
29
  for (const routeDef of from) {
30
30
  promises.push(
31
- getRouteModule(routeDef.path, routeDef.component)
32
- .then((routeModule) => {
31
+ getRouteModule(routeDef.path, routeDef.component).then(
32
+ (routeModule) => {
33
33
  return {
34
34
  id: routeDef.path,
35
35
  path: routeDef.path ?? routeModule.path,
36
36
  ...routeModule,
37
37
  }
38
- }),
38
+ },
39
+ ),
39
40
  )
40
41
  }
41
42
  } else {
42
43
  // Ensure that static routes have precedence over the dynamic ones
43
- for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) {
44
+ for (const path of importPaths.sort((a, b) => (a > b ? -1 : 1))) {
44
45
  promises.push(
45
- getRouteModule(path, from[path])
46
- .then((routeModule) => {
47
- return {
48
- id: path,
49
- layout: routeModule.layout,
50
- path: routeModule.path ?? path
46
+ getRouteModule(path, from[path]).then((routeModule) => {
47
+ return {
48
+ id: path,
49
+ layout: routeModule.layout,
50
+ path:
51
+ routeModule.path ??
52
+ path
51
53
  // Remove /pages and .jsx extension
52
54
  .slice(6, -4)
53
55
  // Replace [id] with :id
@@ -56,20 +58,19 @@ async function createRoutes (from, { param } = { param: $paramPattern }) {
56
58
  .replace(/\/index$/, '/')
57
59
  // Remove trailing slashs
58
60
  .replace(/(.+)\/+$/, (...m) => m[1]),
59
- ...routeModule,
60
- }
61
- }),
61
+ ...routeModule,
62
+ }
63
+ }),
62
64
  )
63
65
  }
64
66
  }
65
- return new Routes(...await Promise.all(promises))
67
+ return new Routes(...(await Promise.all(promises)))
66
68
  }
67
69
 
68
- async function hydrateRoutes (from) {
70
+ async function hydrateRoutes(fromInput) {
71
+ let from = fromInput
69
72
  if (Array.isArray(from)) {
70
- from = Object.fromEntries(
71
- from.map((route) => [route.path, route]),
72
- )
73
+ from = Object.fromEntries(from.map((route) => [route.path, route]))
73
74
  }
74
75
  return window.routes.map((route) => {
75
76
  route.loader = memoImport(from[route.id])
@@ -78,7 +79,7 @@ async function hydrateRoutes (from) {
78
79
  })
79
80
  }
80
81
 
81
- function getRouteModuleExports (routeModule) {
82
+ function getRouteModuleExports(routeModule) {
82
83
  return {
83
84
  // The Route component (default export)
84
85
  component: routeModule.default,
@@ -95,23 +96,23 @@ function getRouteModuleExports (routeModule) {
95
96
  }
96
97
  }
97
98
 
98
- async function getRouteModule (path, routeModule) {
99
+ async function getRouteModule(path, routeModuleInput) {
100
+ let routeModule = routeModuleInput
99
101
  // const isServer = typeof process !== 'undefined'
100
102
  if (typeof routeModule === 'function') {
101
103
  routeModule = await routeModule()
102
104
  return getRouteModuleExports(routeModule)
103
- } else {
104
- return getRouteModuleExports(routeModule)
105
105
  }
106
+ return getRouteModuleExports(routeModule)
106
107
  }
107
108
 
108
- function memoImport (func) {
109
+ function memoImport(func) {
109
110
  // Otherwise we get a ReferenceError, but since this function
110
111
  // is only ran once for each route, there's no overhead
111
112
  const kFuncExecuted = Symbol('kFuncExecuted')
112
113
  const kFuncValue = Symbol('kFuncValue')
113
114
  func[kFuncExecuted] = false
114
- return async function () {
115
+ return async () => {
115
116
  if (!func[kFuncExecuted]) {
116
117
  func[kFuncValue] = await func()
117
118
  func[kFuncExecuted] = true