@fastify/react 0.2.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 +60 -47
- package/package.json +5 -12
- package/plugin.cjs +23 -20
- package/server/context.js +5 -5
- package/server/stream.js +11 -7
- package/virtual/core.jsx +26 -25
- package/virtual/create.jsx +2 -4
- package/virtual/layouts/default.jsx +3 -7
- package/virtual/layouts.js +8 -2
- package/virtual/mount.js +16 -12
- package/virtual/resource.js +57 -47
- package/virtual/root.jsx +11 -9
- package/virtual/routes.js +27 -26
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 {
|
|
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
|
|
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
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
? onShellReady(body)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
96
|
+
export async function createRenderFunction({ routes, create }) {
|
|
92
97
|
// create is exported by client/index.js
|
|
93
|
-
return
|
|
98
|
+
return (req) => {
|
|
94
99
|
// Create convenience-access routeMap
|
|
95
|
-
const routeMap = Object.fromEntries(
|
|
96
|
-
|
|
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 =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
112
|
-
return
|
|
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
|
|
119
|
-
|
|
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,26 +142,26 @@ 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
|
|
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)
|
|
145
156
|
|
|
146
|
-
scope.
|
|
157
|
+
scope.route({
|
|
158
|
+
url: route.path,
|
|
159
|
+
method: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
147
160
|
onRequest,
|
|
148
161
|
// If either getData or onEnter are provided,
|
|
149
162
|
// make sure they run before the SSR route handler
|
|
150
|
-
...hasURMAHooks && {
|
|
151
|
-
async preHandler
|
|
163
|
+
...(hasURMAHooks && {
|
|
164
|
+
async preHandler(req, reply) {
|
|
152
165
|
try {
|
|
153
166
|
if (route.getData) {
|
|
154
167
|
req.route.data = await route.getData(req.route)
|
|
@@ -170,7 +183,7 @@ export function createRoute ({ client, handler, errorHandler, route }, scope, co
|
|
|
170
183
|
req.route.error = err
|
|
171
184
|
}
|
|
172
185
|
},
|
|
173
|
-
},
|
|
186
|
+
}),
|
|
174
187
|
handler,
|
|
175
188
|
errorHandler,
|
|
176
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.
|
|
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": "
|
|
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
|
-
"@
|
|
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": "^
|
|
41
|
+
"@fastify/vite": "^6.0.2"
|
|
49
42
|
},
|
|
50
43
|
"publishConfig": {
|
|
51
44
|
"access": "public"
|
|
52
45
|
},
|
|
53
46
|
"scripts": {
|
|
54
|
-
"lint": "
|
|
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
|
|
5
|
+
function viteReactFastifyDX(config = {}) {
|
|
6
6
|
const prefix = /^\/:/
|
|
7
|
-
const routing = Object.assign(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
90
|
+
configResolved(config) {
|
|
88
91
|
viteProjectRoot = config.root
|
|
89
92
|
},
|
|
90
|
-
async resolveId
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
}
|
package/virtual/create.jsx
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
// This file serves as a placeholder
|
|
2
|
-
// if no
|
|
2
|
+
// if no layouts/default.jsx file is provided
|
|
3
3
|
|
|
4
4
|
import { Suspense } from 'react'
|
|
5
5
|
|
|
6
|
-
export default function Layout
|
|
7
|
-
return
|
|
8
|
-
<Suspense>
|
|
9
|
-
{children}
|
|
10
|
-
</Suspense>
|
|
11
|
-
)
|
|
6
|
+
export default function Layout({ children }) {
|
|
7
|
+
return <Suspense>{children}</Suspense>
|
|
12
8
|
}
|
package/virtual/layouts.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
7
|
+
mount('root')
|
|
8
8
|
|
|
9
|
-
async function mount
|
|
9
|
+
async function mount(targetInput) {
|
|
10
|
+
let target = targetInput
|
|
10
11
|
if (typeof target === 'string') {
|
|
11
|
-
target = document.
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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)
|
package/virtual/resource.js
CHANGED
|
@@ -1,67 +1,77 @@
|
|
|
1
1
|
const fetchMap = new Map()
|
|
2
2
|
const resourceMap = new Map()
|
|
3
3
|
|
|
4
|
-
export function waitResource
|
|
4
|
+
export function waitResource(path, id, promise) {
|
|
5
5
|
const resourceId = `${path}:${id}`
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
if (
|
|
9
|
-
throw
|
|
6
|
+
const loaderStatus = resourceMap.get(resourceId)
|
|
7
|
+
if (loaderStatus) {
|
|
8
|
+
if (loaderStatus.error) {
|
|
9
|
+
throw loaderStatus.error
|
|
10
10
|
}
|
|
11
|
-
if (
|
|
12
|
-
throw
|
|
11
|
+
if (loaderStatus.suspended) {
|
|
12
|
+
throw loaderStatus.promise
|
|
13
13
|
}
|
|
14
14
|
resourceMap.delete(resourceId)
|
|
15
15
|
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
35
|
+
resourceMap.set(resourceId, loader)
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
}
|
|
37
|
+
return waitResource(path, id)
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export function waitFetch
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
40
|
-
throw new Error(
|
|
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
|
|
47
|
+
throw loaderStatus.error
|
|
43
48
|
}
|
|
44
|
-
if (
|
|
45
|
-
throw
|
|
49
|
+
if (loaderStatus.suspended) {
|
|
50
|
+
throw loaderStatus.promise
|
|
46
51
|
}
|
|
47
52
|
fetchMap.delete(path)
|
|
48
53
|
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
+
fetchMap.set(path, loader)
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
}
|
|
76
|
+
return waitFetch(path)
|
|
67
77
|
}
|
package/virtual/root.jsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Suspense } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Route, Routes } from 'react-router-dom'
|
|
3
|
+
import { AppRoute, Router } from '/:core.jsx'
|
|
4
4
|
|
|
5
|
-
export default function Root
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115
|
+
return async () => {
|
|
115
116
|
if (!func[kFuncExecuted]) {
|
|
116
117
|
func[kFuncValue] = await func()
|
|
117
118
|
func[kFuncExecuted] = true
|