@fastify/react 0.1.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/README.md +90 -0
- package/index.js +178 -0
- package/package.json +57 -0
- package/plugin.cjs +112 -0
- package/server/context.js +65 -0
- package/server/stream.js +53 -0
- package/virtual/context.js +4 -0
- package/virtual/context.ts +4 -0
- package/virtual/core.jsx +145 -0
- package/virtual/create.jsx +7 -0
- package/virtual/create.tsx +7 -0
- package/virtual/layouts/default.jsx +12 -0
- package/virtual/layouts.js +14 -0
- package/virtual/mount.js +47 -0
- package/virtual/mount.ts +47 -0
- package/virtual/resource.js +68 -0
- package/virtual/root.jsx +27 -0
- package/virtual/root.tsx +27 -0
- package/virtual/routes.js +121 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# fastify-dx-react [](https://www.npmjs.com/package/fastify-dx-react) [](https://standardjs.com/)
|
|
2
|
+
|
|
3
|
+
- [**Introduction**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#introduction)
|
|
4
|
+
- [**Quick Start**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#quick-start)
|
|
5
|
+
- [**Package Scripts**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#package-scripts)
|
|
6
|
+
- [**Basic Setup**](https://github.com/fastify/fastify-dx/blob/main/docs/react/basic-setup.md)
|
|
7
|
+
- [**Project Structure**](https://github.com/fastify/fastify-dx/blob/main/docs/react/project-structure.md)
|
|
8
|
+
- [**Rendering Modes**](https://github.com/fastify/fastify-dx/blob/main/docs/react/rendering-modes.md)
|
|
9
|
+
- [**Routing Configuration**](https://github.com/fastify/fastify-dx/blob/main/docs/react/routing-config.md)
|
|
10
|
+
- [**Data Prefetching**](https://github.com/fastify/fastify-dx/blob/main/docs/react/data-prefetching.md)
|
|
11
|
+
- [**Route Layouts**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-layouts.md)
|
|
12
|
+
- [**Route Context**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md)
|
|
13
|
+
- [**Route Enter Event**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-enter.md)
|
|
14
|
+
- [**Virtual Modules**](https://github.com/fastify/fastify-dx/blob/main/docs/react/virtual-modules.md)
|
|
15
|
+
|
|
16
|
+
## Introduction
|
|
17
|
+
|
|
18
|
+
**Fastify DX for React** is a renderer adapter for [**fastify-vite**](https://github.com/fastify/fastify-vite).
|
|
19
|
+
|
|
20
|
+
It is a **fast**, **lightweight** alternative to Next.js and Remix packed with **Developer Experience** features.
|
|
21
|
+
|
|
22
|
+
It has an extremely small core (~1k LOC total) and is built on top of [Fastify](https://github.com/fastify/fastify), [Vite](https://vitejs.dev/), [React Router](https://reactrouter.com/docs/en/v6) and [Valtio](https://github.com/pmndrs/valtio).
|
|
23
|
+
|
|
24
|
+
[**See the release notes for the 0.0.1 alpha release**](https://github.com/fastify/fastify-dx/releases/tag/v0.0.1).
|
|
25
|
+
|
|
26
|
+
> At this stage this project is mostly a [**one-man show**](https://github.com/sponsors/galvez), who's devoting all his free time to its completion. Contributions are extremely welcome, as well as bug reports for any issues you may find.
|
|
27
|
+
|
|
28
|
+
In this first alpha release it's still missing a test suite. The same is true for [**fastify-vite**]().
|
|
29
|
+
|
|
30
|
+
It'll move into **beta** status when test suites are added to both packages.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Ensure you have **Node v16+**.
|
|
35
|
+
|
|
36
|
+
Make a copy of [**starters/react**](https://github.com/fastify/fastify-dx/tree/dev/starters/react). If you have [`degit`](https://github.com/Rich-Harris/degit), run the following from a new directory:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
degit fastify/fastify-dx/starters/react
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **If you're starting a project from scratch**, you'll need these packages installed.
|
|
43
|
+
>
|
|
44
|
+
> ```bash
|
|
45
|
+
> npm i fastify fastify-vite fastify-dx-react -P
|
|
46
|
+
> npm i @vitejs/plugin-react -D
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Run `npm install`.
|
|
51
|
+
|
|
52
|
+
Run `npm run dev`.
|
|
53
|
+
|
|
54
|
+
Visit `http://localhost:3000/`.
|
|
55
|
+
|
|
56
|
+
## What's Included
|
|
57
|
+
|
|
58
|
+
That will get you a **starter template** with:
|
|
59
|
+
|
|
60
|
+
- A minimal [Fastify](https://github.com/fastify/fastify) server.
|
|
61
|
+
- Some dummy API routes.
|
|
62
|
+
- A `pages/` folder with some [demo routes](https://github.com/fastify/fastify-dx/tree/dev/starters/react/client/pages).
|
|
63
|
+
- All configuration files.
|
|
64
|
+
|
|
65
|
+
It also includes some _**opinionated**_ essentials:
|
|
66
|
+
|
|
67
|
+
- [**PostCSS Preset Env**](https://www.npmjs.com/package/postcss-preset-env) by [**Jonathan Neal**](https://github.com/jonathantneal), which enables [several modern CSS features](https://preset-env.cssdb.org/), such as [**CSS Nesting**](https://www.w3.org/TR/css-nesting-1/).
|
|
68
|
+
|
|
69
|
+
- [**UnoCSS**](https://github.com/unocss/unocss) by [**Anthony Fu**](https://antfu.me/), which supports all [Tailwind utilities](https://uno.antfu.me/) and many other goodies through its [default preset](https://github.com/unocss/unocss/tree/main/packages/preset-uno).
|
|
70
|
+
|
|
71
|
+
- [**Valtio**](https://github.com/pmndrs/valtio) by [**Daishi Kato**](https://blog.axlight.com/), with a global and SSR-ready store which you can use anywhere.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Package Scripts
|
|
75
|
+
|
|
76
|
+
`npm run dev` boots the development server.
|
|
77
|
+
|
|
78
|
+
`npm run build` creates the production bundle.
|
|
79
|
+
|
|
80
|
+
`npm run serve` serves the production bundle.
|
|
81
|
+
|
|
82
|
+
## Meta
|
|
83
|
+
|
|
84
|
+
Created by [Jonas Galvez](https://github.com/sponsors/galvez), **Engineering Manager** and **Open Sourcerer** at [NearForm](https://nearform.com).
|
|
85
|
+
|
|
86
|
+
## Sponsors
|
|
87
|
+
|
|
88
|
+
<a href="https://nearform.com"><img width="200px" src="https://user-images.githubusercontent.com/12291/172310344-594669fd-da4c-466b-a250-a898569dfea3.svg"></a>
|
|
89
|
+
|
|
90
|
+
Also [**Duc-Thien Bui**](https://github.com/aecea) and [**Tom Preston-Werner**](https://github.com/mojombo) [via GitHub Sponsors](https://github.com/sponsors/galvez). _Thank you!_
|
package/index.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Used to send a readable stream to reply.send()
|
|
2
|
+
import { Readable } from 'stream'
|
|
3
|
+
|
|
4
|
+
// fastify-vite's minimal HTML templating function,
|
|
5
|
+
// which extracts interpolation variables from comments
|
|
6
|
+
// and returns a function with the generated code
|
|
7
|
+
import { createHtmlTemplateFunction } from '@fastify/vite'
|
|
8
|
+
|
|
9
|
+
// Used to safely serialize JavaScript into
|
|
10
|
+
// <script> tags, preventing a few types of attack
|
|
11
|
+
import devalue from 'devalue'
|
|
12
|
+
|
|
13
|
+
// Small SSR-ready library used to generate
|
|
14
|
+
// <title>, <meta> and <link> elements
|
|
15
|
+
import Head from 'unihead'
|
|
16
|
+
|
|
17
|
+
// Helpers from the Node.js stream library to
|
|
18
|
+
// make it easier to work with renderToPipeableStream()
|
|
19
|
+
import { generateHtmlStream, onAllReady, onShellReady } from './server/stream.js'
|
|
20
|
+
|
|
21
|
+
// Holds the universal route context
|
|
22
|
+
import RouteContext from './server/context.js'
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
prepareClient,
|
|
26
|
+
createHtmlFunction,
|
|
27
|
+
createRenderFunction,
|
|
28
|
+
createRouteHandler,
|
|
29
|
+
createRoute,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function prepareClient ({
|
|
33
|
+
routes: routesPromise,
|
|
34
|
+
context: contextPromise,
|
|
35
|
+
...others
|
|
36
|
+
}) {
|
|
37
|
+
const context = await contextPromise
|
|
38
|
+
const resolvedRoutes = await routesPromise
|
|
39
|
+
return { context, routes: resolvedRoutes, ...others }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The return value of this function gets registered as reply.html()
|
|
43
|
+
export function createHtmlFunction (source, scope, config) {
|
|
44
|
+
// Templating functions for universal rendering (SSR+CSR)
|
|
45
|
+
const [unHeadSource, unFooterSource] = source.split('<!-- element -->')
|
|
46
|
+
const unHeadTemplate = createHtmlTemplateFunction(unHeadSource)
|
|
47
|
+
const unFooterTemplate = createHtmlTemplateFunction(unFooterSource)
|
|
48
|
+
// Templating functions for server-only rendering (SSR only)
|
|
49
|
+
const [soHeadSource, soFooterSource] = source
|
|
50
|
+
// Unsafe if dealing with user-input, but safe here
|
|
51
|
+
// where we control the index.html source
|
|
52
|
+
.replace(/<script[^>]+type="module"[^>]+>.*?<\/script>/g, '')
|
|
53
|
+
.split('<!-- element -->')
|
|
54
|
+
const soHeadTemplate = createHtmlTemplateFunction(soHeadSource)
|
|
55
|
+
const soFooterTemplate = createHtmlTemplateFunction(soFooterSource)
|
|
56
|
+
// This function gets registered as reply.html()
|
|
57
|
+
return function ({ routes, context, body }) {
|
|
58
|
+
// Decide which templating functions to use, with and without hydration
|
|
59
|
+
const headTemplate = context.serverOnly ? soHeadTemplate : unHeadTemplate
|
|
60
|
+
const footerTemplate = context.serverOnly ? soFooterTemplate : unFooterTemplate
|
|
61
|
+
// Render page-level <head> elements
|
|
62
|
+
const head = new Head(context.head).render()
|
|
63
|
+
// 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(context.toJSON())}\n` +
|
|
79
|
+
`window.routes = ${devalue(routes.toJSON())}\n` +
|
|
80
|
+
'</script>'
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
}))
|
|
85
|
+
// Send out header and readable stream with full response
|
|
86
|
+
this.type('text/html')
|
|
87
|
+
this.send(readable)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createRenderFunction ({ routes, create }) {
|
|
92
|
+
// create is exported by client/index.js
|
|
93
|
+
return function (req) {
|
|
94
|
+
// Create convenience-access routeMap
|
|
95
|
+
const routeMap = Object.fromEntries(routes.toJSON().map((route) => {
|
|
96
|
+
return [route.path, route]
|
|
97
|
+
}))
|
|
98
|
+
// 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
|
+
})
|
|
105
|
+
// Perform SSR, i.e., turn app.instance into an HTML fragment
|
|
106
|
+
// The SSR context data is passed along so it can be inlined for hydration
|
|
107
|
+
return { routes, context: req.route, body: app }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createRouteHandler (client, scope, config) {
|
|
112
|
+
return function (req, reply) {
|
|
113
|
+
reply.html(reply.render(req))
|
|
114
|
+
return reply
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createRoute ({ client, handler, errorHandler, route }, scope, config) {
|
|
119
|
+
const onRequest = async function onRequest (req, reply) {
|
|
120
|
+
req.route = await RouteContext.create(
|
|
121
|
+
scope,
|
|
122
|
+
req,
|
|
123
|
+
reply,
|
|
124
|
+
route,
|
|
125
|
+
client.context,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
if (route.getData) {
|
|
129
|
+
// If getData is provided, register JSON endpoint for it
|
|
130
|
+
scope.get(`/-/data${route.path}`, {
|
|
131
|
+
onRequest,
|
|
132
|
+
async handler (req, reply) {
|
|
133
|
+
reply.send(await route.getData(req.route))
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// See https://github.com/fastify/fastify-dx/blob/main/URMA.md
|
|
139
|
+
const hasURMAHooks = Boolean(
|
|
140
|
+
route.getData || route.getMeta || route.onEnter,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Extend with route context initialization module
|
|
144
|
+
RouteContext.extend(client.context)
|
|
145
|
+
|
|
146
|
+
scope.get(route.path, {
|
|
147
|
+
onRequest,
|
|
148
|
+
// If either getData or onEnter are provided,
|
|
149
|
+
// make sure they run before the SSR route handler
|
|
150
|
+
...hasURMAHooks && {
|
|
151
|
+
async preHandler (req, reply) {
|
|
152
|
+
try {
|
|
153
|
+
if (route.getData) {
|
|
154
|
+
req.route.data = await route.getData(req.route)
|
|
155
|
+
}
|
|
156
|
+
if (route.getMeta) {
|
|
157
|
+
req.route.head = await route.getMeta(req.route)
|
|
158
|
+
}
|
|
159
|
+
if (route.onEnter) {
|
|
160
|
+
if (!req.route.data) {
|
|
161
|
+
req.route.data = {}
|
|
162
|
+
}
|
|
163
|
+
const result = await route.onEnter(req.route)
|
|
164
|
+
Object.assign(req.route.data, result)
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (config.dev) {
|
|
168
|
+
console.error(err)
|
|
169
|
+
}
|
|
170
|
+
req.route.error = err
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
handler,
|
|
175
|
+
errorHandler,
|
|
176
|
+
...route,
|
|
177
|
+
})
|
|
178
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"lint": "eslint . --ext .js,.jsx --fix"
|
|
4
|
+
},
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"name": "@fastify/react",
|
|
8
|
+
"version": "0.1.0",
|
|
9
|
+
"files": [
|
|
10
|
+
"virtual/create.jsx",
|
|
11
|
+
"virtual/create.tsx",
|
|
12
|
+
"virtual/root.jsx",
|
|
13
|
+
"virtual/root.tsx",
|
|
14
|
+
"virtual/layouts.js",
|
|
15
|
+
"virtual/layouts/default.jsx",
|
|
16
|
+
"virtual/context.js",
|
|
17
|
+
"virtual/context.ts",
|
|
18
|
+
"virtual/mount.js",
|
|
19
|
+
"virtual/mount.ts",
|
|
20
|
+
"virtual/resource.js",
|
|
21
|
+
"virtual/core.jsx",
|
|
22
|
+
"virtual/routes.js",
|
|
23
|
+
"index.js",
|
|
24
|
+
"plugin.cjs",
|
|
25
|
+
"server/context.js",
|
|
26
|
+
"server/stream.js"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./index.js",
|
|
31
|
+
"./plugin": "./plugin.cjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"devalue": "^2.0.1",
|
|
35
|
+
"history": "^5.3.0",
|
|
36
|
+
"minipass": "^3.3.4",
|
|
37
|
+
"react": "^18.2.0",
|
|
38
|
+
"react-dom": "^18.2.0",
|
|
39
|
+
"react-router-dom": "^6.4.3",
|
|
40
|
+
"unihead": "^0.0.6",
|
|
41
|
+
"valtio": "^1.7.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@babel/eslint-parser": "^7.16.0",
|
|
45
|
+
"@babel/preset-react": "^7.16.0",
|
|
46
|
+
"@vitejs/plugin-react": "^2.2.0",
|
|
47
|
+
"eslint": "^7.32.0",
|
|
48
|
+
"eslint-config-standard": "^16.0.2",
|
|
49
|
+
"eslint-plugin-import": "^2.22.1",
|
|
50
|
+
"eslint-plugin-node": "^11.1.0",
|
|
51
|
+
"eslint-plugin-promise": "^4.3.1",
|
|
52
|
+
"eslint-plugin-react": "^7.29.4"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/plugin.cjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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 = /^\/?dx:/
|
|
7
|
+
const routing = Object.assign({
|
|
8
|
+
globPattern: '/pages/**/*.(jsx|tsx)',
|
|
9
|
+
paramPattern: /\[(\w+)\]/,
|
|
10
|
+
}, config)
|
|
11
|
+
const virtualRoot = resolve(__dirname, 'virtual')
|
|
12
|
+
const virtualModules = [
|
|
13
|
+
'mount.js',
|
|
14
|
+
'mount.ts',
|
|
15
|
+
'resource.js',
|
|
16
|
+
'resource.ts',
|
|
17
|
+
'routes.js',
|
|
18
|
+
'layouts.js',
|
|
19
|
+
'create.jsx',
|
|
20
|
+
'create.tsx',
|
|
21
|
+
'root.jsx',
|
|
22
|
+
'root.tsx',
|
|
23
|
+
'layouts/',
|
|
24
|
+
'context.js',
|
|
25
|
+
'context.ts',
|
|
26
|
+
'core.jsx'
|
|
27
|
+
]
|
|
28
|
+
virtualModules.includes = function (virtual) {
|
|
29
|
+
if (!virtual) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
for (const entry of this) {
|
|
33
|
+
if (virtual.startsWith(entry)) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
const virtualModuleInserts = {
|
|
40
|
+
'routes.js': {
|
|
41
|
+
$globPattern: routing.globPattern,
|
|
42
|
+
$paramPattern: routing.paramPattern,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let viteProjectRoot
|
|
47
|
+
|
|
48
|
+
function loadVirtualModuleOverride (virtual) {
|
|
49
|
+
if (!virtualModules.includes(virtual)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const overridePath = resolve(viteProjectRoot, virtual)
|
|
53
|
+
if (existsSync(overridePath)) {
|
|
54
|
+
return overridePath
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadVirtualModule (virtual) {
|
|
59
|
+
if (!virtualModules.includes(virtual)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
let code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
|
|
63
|
+
if (virtualModuleInserts[virtual]) {
|
|
64
|
+
for (const [key, value] of Object.entries(virtualModuleInserts[virtual])) {
|
|
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
|
|
77
|
+
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
|
|
78
|
+
.replace(/-/g, '\\x2d')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: 'vite-plugin-react-fastify-dx',
|
|
83
|
+
config (config, { command }) {
|
|
84
|
+
if (command === 'build' && config.build?.ssr) {
|
|
85
|
+
config.build.rollupOptions = {
|
|
86
|
+
output: {
|
|
87
|
+
format: 'es',
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
configResolved (config) {
|
|
93
|
+
viteProjectRoot = config.root
|
|
94
|
+
},
|
|
95
|
+
async resolveId (id) {
|
|
96
|
+
const [, virtual] = id.split(prefix)
|
|
97
|
+
if (virtual) {
|
|
98
|
+
const override = await loadVirtualModuleOverride(virtual)
|
|
99
|
+
if (override) {
|
|
100
|
+
return override
|
|
101
|
+
}
|
|
102
|
+
return id
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
load (id) {
|
|
106
|
+
const [, virtual] = id.split(prefix)
|
|
107
|
+
return loadVirtualModule(virtual)
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = viteReactFastifyDX
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
const routeContextInspect = Symbol.for('nodejs.util.inspect.custom')
|
|
3
|
+
|
|
4
|
+
export default class RouteContext {
|
|
5
|
+
static async create (server, req, reply, route, contextInit) {
|
|
6
|
+
const routeContext = new RouteContext(server, req, reply, route)
|
|
7
|
+
if (contextInit) {
|
|
8
|
+
if (contextInit.state) {
|
|
9
|
+
routeContext.state = contextInit.state()
|
|
10
|
+
}
|
|
11
|
+
if (contextInit.default) {
|
|
12
|
+
await contextInit.default(routeContext)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return routeContext
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor (server, req, reply, route) {
|
|
19
|
+
this.server = server
|
|
20
|
+
this.req = req
|
|
21
|
+
this.reply = reply
|
|
22
|
+
this.head = {}
|
|
23
|
+
this.state = null
|
|
24
|
+
this.data = route.data
|
|
25
|
+
this.firstRender = true
|
|
26
|
+
this.layout = route.layout
|
|
27
|
+
this.getMeta = !!route.getMeta
|
|
28
|
+
this.getData = !!route.getData
|
|
29
|
+
this.onEnter = !!route.onEnter
|
|
30
|
+
this.streaming = route.streaming
|
|
31
|
+
this.clientOnly = route.clientOnly
|
|
32
|
+
this.serverOnly = route.serverOnly
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[routeContextInspect] () {
|
|
36
|
+
return {
|
|
37
|
+
...this,
|
|
38
|
+
server: { [routeContextInspect]: () => '[Server]' },
|
|
39
|
+
req: { [routeContextInspect]: () => '[Request]' },
|
|
40
|
+
reply: { [routeContextInspect]: () => '[Reply]' },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toJSON () {
|
|
45
|
+
return {
|
|
46
|
+
state: this.state,
|
|
47
|
+
data: this.data,
|
|
48
|
+
layout: this.layout,
|
|
49
|
+
getMeta: this.getMeta,
|
|
50
|
+
getData: this.getData,
|
|
51
|
+
onEnter: this.onEnter,
|
|
52
|
+
firstRender: this.firstRender,
|
|
53
|
+
clientOnly: this.clientOnly,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
RouteContext.extend = function (initial) {
|
|
59
|
+
const { default: _, ...extra } = initial
|
|
60
|
+
for (const [prop, value] of Object.entries(extra)) {
|
|
61
|
+
if (prop !== 'data' && prop !== 'state') {
|
|
62
|
+
Object.defineProperty(RouteContext.prototype, prop, value)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/server/stream.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
// Helper to make the stream returned renderToPipeableStream()
|
|
3
|
+
// behave like an event emitter and facilitate error handling in Fastify
|
|
4
|
+
import Minipass from 'minipass'
|
|
5
|
+
|
|
6
|
+
// React 18's preferred server-side rendering function,
|
|
7
|
+
// which enables the combination of React.lazy() and Suspense
|
|
8
|
+
import { renderToPipeableStream } from 'react-dom/server'
|
|
9
|
+
|
|
10
|
+
// Helper function to prepend and append chunks the body stream
|
|
11
|
+
export async function * generateHtmlStream ({ head, body, footer }) {
|
|
12
|
+
yield head
|
|
13
|
+
if (body) {
|
|
14
|
+
for await (const chunk of await body) {
|
|
15
|
+
yield chunk
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
yield footer()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper function to get an AsyncIterable (via PassThrough)
|
|
22
|
+
// from the renderToPipeableStream() onShellReady event
|
|
23
|
+
export function onShellReady (app) {
|
|
24
|
+
const duplex = new Minipass()
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
try {
|
|
27
|
+
const pipeable = renderToPipeableStream(app, {
|
|
28
|
+
onShellReady () {
|
|
29
|
+
resolve(pipeable.pipe(duplex))
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
} catch (error) {
|
|
33
|
+
resolve(error)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper function to get an AsyncIterable (via Minipass)
|
|
39
|
+
// from the renderToPipeableStream() onAllReady event
|
|
40
|
+
export function onAllReady (app) {
|
|
41
|
+
const duplex = new Minipass()
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
try {
|
|
44
|
+
const pipeable = renderToPipeableStream(app, {
|
|
45
|
+
onAllReady () {
|
|
46
|
+
resolve(pipeable.pipe(duplex))
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
} catch (error) {
|
|
50
|
+
resolve(error)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
package/virtual/core.jsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect } from 'react'
|
|
2
|
+
import { useLocation, BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
3
|
+
import { StaticRouter } from 'react-router-dom/server.mjs'
|
|
4
|
+
import { createPath } from 'history'
|
|
5
|
+
import { proxy, useSnapshot } from 'valtio'
|
|
6
|
+
import { waitResource, waitFetch } from '/dx:resource.js'
|
|
7
|
+
import layouts from '/dx:layouts.js'
|
|
8
|
+
|
|
9
|
+
export const isServer = import.meta.env.SSR
|
|
10
|
+
export const Router = isServer ? StaticRouter : BrowserRouter
|
|
11
|
+
export const RouteContext = createContext({})
|
|
12
|
+
|
|
13
|
+
export function useRouteContext () {
|
|
14
|
+
const routeContext = useContext(RouteContext)
|
|
15
|
+
if (routeContext.state) {
|
|
16
|
+
routeContext.snapshot = isServer
|
|
17
|
+
? routeContext.state
|
|
18
|
+
: useSnapshot(routeContext.state)
|
|
19
|
+
}
|
|
20
|
+
return routeContext
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DXApp ({
|
|
24
|
+
url,
|
|
25
|
+
routes,
|
|
26
|
+
head,
|
|
27
|
+
routeMap,
|
|
28
|
+
ctxHydration,
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<Router location={url}>
|
|
32
|
+
<Routes>{
|
|
33
|
+
routes.map(({ path, component: Component }) =>
|
|
34
|
+
<Route
|
|
35
|
+
key={path}
|
|
36
|
+
path={path}
|
|
37
|
+
element={
|
|
38
|
+
<DXRoute
|
|
39
|
+
head={head}
|
|
40
|
+
ctxHydration={ctxHydration}
|
|
41
|
+
ctx={routeMap[path]}>
|
|
42
|
+
<Component />
|
|
43
|
+
</DXRoute>
|
|
44
|
+
} />,
|
|
45
|
+
)
|
|
46
|
+
}</Routes>
|
|
47
|
+
</Router>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function DXRoute ({ head, ctxHydration, ctx, children }) {
|
|
52
|
+
// If running on the server, assume all data
|
|
53
|
+
// functions have already ran through the preHandler hook
|
|
54
|
+
if (isServer) {
|
|
55
|
+
const Layout = layouts[ctxHydration.layout ?? 'default']
|
|
56
|
+
return (
|
|
57
|
+
<RouteContext.Provider value={{
|
|
58
|
+
...ctx,
|
|
59
|
+
...ctxHydration,
|
|
60
|
+
state: isServer
|
|
61
|
+
? ctxHydration.state
|
|
62
|
+
: proxy(ctxHydration.state),
|
|
63
|
+
}}>
|
|
64
|
+
<Layout>
|
|
65
|
+
{children}
|
|
66
|
+
</Layout>
|
|
67
|
+
</RouteContext.Provider>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
// Note that on the client, window.route === ctxHydration
|
|
71
|
+
|
|
72
|
+
// Indicates whether or not this is a first render on the client
|
|
73
|
+
ctx.firstRender = window.route.firstRender
|
|
74
|
+
|
|
75
|
+
// If running on the client, the server context data
|
|
76
|
+
// is still available, hydrated from window.route
|
|
77
|
+
if (ctx.firstRender) {
|
|
78
|
+
ctx.data = window.route.data
|
|
79
|
+
ctx.head = window.route.head
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const location = useLocation()
|
|
83
|
+
const path = createPath(location)
|
|
84
|
+
|
|
85
|
+
// When the next route renders client-side,
|
|
86
|
+
// force it to execute all URMA hooks again
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
window.route.firstRender = false
|
|
89
|
+
}, [location])
|
|
90
|
+
|
|
91
|
+
// If we have a getData function registered for this route
|
|
92
|
+
if (!ctx.data && ctx.getData) {
|
|
93
|
+
try {
|
|
94
|
+
const { pathname, search } = location
|
|
95
|
+
// If not, fetch data from the JSON endpoint
|
|
96
|
+
ctx.data = waitFetch(`${pathname}${search}`)
|
|
97
|
+
} catch (status) {
|
|
98
|
+
// If it's an actual error...
|
|
99
|
+
if (status instanceof Error) {
|
|
100
|
+
ctx.error = status
|
|
101
|
+
}
|
|
102
|
+
// If it's just a promise (suspended state)
|
|
103
|
+
throw status
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Note that ctx.loader() at this point will resolve the
|
|
108
|
+
// memoized module, so there's barely any overhead
|
|
109
|
+
|
|
110
|
+
if (!ctx.firstRender && ctx.getMeta) {
|
|
111
|
+
const updateMeta = async () => {
|
|
112
|
+
const { getMeta } = await ctx.loader()
|
|
113
|
+
head.update(await getMeta(ctx))
|
|
114
|
+
}
|
|
115
|
+
waitResource(path, 'updateMeta', updateMeta)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!ctx.firstRender && ctx.onEnter) {
|
|
119
|
+
const runOnEnter = async () => {
|
|
120
|
+
const { onEnter } = await ctx.loader()
|
|
121
|
+
const updatedData = await onEnter(ctx)
|
|
122
|
+
if (!ctx.data) {
|
|
123
|
+
ctx.data = {}
|
|
124
|
+
}
|
|
125
|
+
Object.assign(ctx.data, updatedData)
|
|
126
|
+
}
|
|
127
|
+
waitResource(path, 'onEnter', runOnEnter)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Layout = layouts[ctx.layout ?? 'default']
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<RouteContext.Provider value={{
|
|
134
|
+
...ctxHydration,
|
|
135
|
+
...ctx,
|
|
136
|
+
state: isServer
|
|
137
|
+
? ctxHydration.state
|
|
138
|
+
: proxy(ctxHydration.state),
|
|
139
|
+
}}>
|
|
140
|
+
<Layout>
|
|
141
|
+
{children}
|
|
142
|
+
</Layout>
|
|
143
|
+
</RouteContext.Provider>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { lazy } from 'react'
|
|
2
|
+
|
|
3
|
+
const DefaultLayout = () => import('/dx:layouts/default.jsx')
|
|
4
|
+
|
|
5
|
+
const appLayouts = import.meta.glob('/layouts/*.jsx')
|
|
6
|
+
|
|
7
|
+
appLayouts['/layouts/default.jsx'] ??= DefaultLayout
|
|
8
|
+
|
|
9
|
+
export default Object.fromEntries(
|
|
10
|
+
Object.keys(appLayouts).map((path) => {
|
|
11
|
+
const name = path.slice(9, -4)
|
|
12
|
+
return [name, lazy(appLayouts[path])]
|
|
13
|
+
}),
|
|
14
|
+
)
|
package/virtual/mount.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Head from 'unihead/client'
|
|
2
|
+
import { createRoot, hydrateRoot } from 'react-dom/client'
|
|
3
|
+
|
|
4
|
+
import create from '/dx:create.jsx'
|
|
5
|
+
import routesPromise from '/dx:routes.js'
|
|
6
|
+
|
|
7
|
+
mount('main')
|
|
8
|
+
|
|
9
|
+
async function mount (target) {
|
|
10
|
+
if (typeof target === 'string') {
|
|
11
|
+
target = document.querySelector(target)
|
|
12
|
+
}
|
|
13
|
+
const context = await import('/dx:context.js')
|
|
14
|
+
const ctxHydration = await extendContext(window.route, context)
|
|
15
|
+
const head = new Head(window.route.head, window.document)
|
|
16
|
+
const resolvedRoutes = await routesPromise
|
|
17
|
+
const routeMap = Object.fromEntries(
|
|
18
|
+
resolvedRoutes.map((route) => [route.path, route]),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const app = create({
|
|
22
|
+
head,
|
|
23
|
+
ctxHydration,
|
|
24
|
+
routes: window.routes,
|
|
25
|
+
routeMap,
|
|
26
|
+
})
|
|
27
|
+
if (ctxHydration.clientOnly) {
|
|
28
|
+
createRoot(target).render(app)
|
|
29
|
+
} else {
|
|
30
|
+
hydrateRoot(target, app)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
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
|
+
}) {
|
|
42
|
+
Object.assign(ctx, extra)
|
|
43
|
+
if (setter) {
|
|
44
|
+
await setter(ctx)
|
|
45
|
+
}
|
|
46
|
+
return ctx
|
|
47
|
+
}
|
package/virtual/mount.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Head from 'unihead/client'
|
|
2
|
+
import { createRoot, hydrateRoot } from 'react-dom/client'
|
|
3
|
+
|
|
4
|
+
import create from '/dx:create.tsx'
|
|
5
|
+
import routesPromise from '/dx:routes.js'
|
|
6
|
+
|
|
7
|
+
mount('main')
|
|
8
|
+
|
|
9
|
+
async function mount (target) {
|
|
10
|
+
if (typeof target === 'string') {
|
|
11
|
+
target = document.querySelector(target)
|
|
12
|
+
}
|
|
13
|
+
const context = await import('/dx:context.ts')
|
|
14
|
+
const ctxHydration = await extendContext(window.route, context)
|
|
15
|
+
const head = new Head(window.route.head, window.document)
|
|
16
|
+
const resolvedRoutes = await routesPromise
|
|
17
|
+
const routeMap = Object.fromEntries(
|
|
18
|
+
resolvedRoutes.map((route) => [route.path, route]),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const app = create({
|
|
22
|
+
head,
|
|
23
|
+
ctxHydration,
|
|
24
|
+
routes: window.routes,
|
|
25
|
+
routeMap,
|
|
26
|
+
})
|
|
27
|
+
if (ctxHydration.clientOnly) {
|
|
28
|
+
createRoot(target).render(app)
|
|
29
|
+
} else {
|
|
30
|
+
hydrateRoot(target, app)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
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
|
+
}) {
|
|
42
|
+
Object.assign(ctx, extra)
|
|
43
|
+
if (setter) {
|
|
44
|
+
await setter(ctx)
|
|
45
|
+
}
|
|
46
|
+
return ctx
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
const fetchMap = new Map()
|
|
3
|
+
const resourceMap = new Map()
|
|
4
|
+
|
|
5
|
+
export function waitResource (path, id, promise) {
|
|
6
|
+
const resourceId = `${path}:${id}`
|
|
7
|
+
const loader = resourceMap.get(resourceId)
|
|
8
|
+
if (loader) {
|
|
9
|
+
if (loader.error) {
|
|
10
|
+
throw loader.error
|
|
11
|
+
}
|
|
12
|
+
if (loader.suspended) {
|
|
13
|
+
throw loader.promise
|
|
14
|
+
}
|
|
15
|
+
resourceMap.delete(resourceId)
|
|
16
|
+
|
|
17
|
+
return loader.result
|
|
18
|
+
} else {
|
|
19
|
+
const loader = {
|
|
20
|
+
suspended: true,
|
|
21
|
+
error: null,
|
|
22
|
+
result: null,
|
|
23
|
+
promise: null,
|
|
24
|
+
}
|
|
25
|
+
loader.promise = promise()
|
|
26
|
+
.then((result) => { loader.result = result })
|
|
27
|
+
.catch((loaderError) => { loader.error = loaderError })
|
|
28
|
+
.finally(() => { loader.suspended = false })
|
|
29
|
+
|
|
30
|
+
resourceMap.set(resourceId, loader)
|
|
31
|
+
|
|
32
|
+
return waitResource(path, id)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function waitFetch (path) {
|
|
37
|
+
const loader = fetchMap.get(path)
|
|
38
|
+
if (loader) {
|
|
39
|
+
if (loader.error || loader.data?.statusCode === 500) {
|
|
40
|
+
if (loader.data?.statusCode === 500) {
|
|
41
|
+
throw new Error(loader.data.message)
|
|
42
|
+
}
|
|
43
|
+
throw loader.error
|
|
44
|
+
}
|
|
45
|
+
if (loader.suspended) {
|
|
46
|
+
throw loader.promise
|
|
47
|
+
}
|
|
48
|
+
fetchMap.delete(path)
|
|
49
|
+
|
|
50
|
+
return loader.data
|
|
51
|
+
} else {
|
|
52
|
+
const loader = {
|
|
53
|
+
suspended: true,
|
|
54
|
+
error: null,
|
|
55
|
+
data: null,
|
|
56
|
+
promise: null,
|
|
57
|
+
}
|
|
58
|
+
loader.promise = fetch(`/-/data${path}`)
|
|
59
|
+
.then((response) => response.json())
|
|
60
|
+
.then((loaderData) => { loader.data = loaderData })
|
|
61
|
+
.catch((loaderError) => { loader.error = loaderError })
|
|
62
|
+
.finally(() => { loader.suspended = false })
|
|
63
|
+
|
|
64
|
+
fetchMap.set(path, loader)
|
|
65
|
+
|
|
66
|
+
return waitFetch(path)
|
|
67
|
+
}
|
|
68
|
+
}
|
package/virtual/root.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Suspense } from 'react'
|
|
2
|
+
import { Routes, Route } from 'react-router-dom'
|
|
3
|
+
import { Router, DXRoute } from '/dx:core.jsx'
|
|
4
|
+
|
|
5
|
+
export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
|
|
6
|
+
return (
|
|
7
|
+
<Suspense>
|
|
8
|
+
<Router location={url}>
|
|
9
|
+
<Routes>{
|
|
10
|
+
routes.map(({ path, component: Component }) =>
|
|
11
|
+
<Route
|
|
12
|
+
key={path}
|
|
13
|
+
path={path}
|
|
14
|
+
element={
|
|
15
|
+
<DXRoute
|
|
16
|
+
head={head}
|
|
17
|
+
ctxHydration={ctxHydration}
|
|
18
|
+
ctx={routeMap[path]}>
|
|
19
|
+
<Component />
|
|
20
|
+
</DXRoute>
|
|
21
|
+
} />,
|
|
22
|
+
)
|
|
23
|
+
}</Routes>
|
|
24
|
+
</Router>
|
|
25
|
+
</Suspense>
|
|
26
|
+
)
|
|
27
|
+
}
|
package/virtual/root.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Suspense } from 'react'
|
|
2
|
+
import { Routes, Route } from 'react-router-dom'
|
|
3
|
+
import { Router, DXRoute } from '/dx:core.jsx'
|
|
4
|
+
|
|
5
|
+
export default function Root ({ url, routes, head, ctxHydration, routeMap }) {
|
|
6
|
+
return (
|
|
7
|
+
<Suspense>
|
|
8
|
+
<Router location={url}>
|
|
9
|
+
<Routes>{
|
|
10
|
+
routes.map(({ path, component: Component }) =>
|
|
11
|
+
<Route
|
|
12
|
+
key={path}
|
|
13
|
+
path={path}
|
|
14
|
+
element={
|
|
15
|
+
<DXRoute
|
|
16
|
+
head={head}
|
|
17
|
+
ctxHydration={ctxHydration}
|
|
18
|
+
ctx={routeMap[path]}>
|
|
19
|
+
<Component />
|
|
20
|
+
</DXRoute>
|
|
21
|
+
} />,
|
|
22
|
+
)
|
|
23
|
+
}</Routes>
|
|
24
|
+
</Router>
|
|
25
|
+
</Suspense>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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)
|
|
32
|
+
.then((routeModule) => {
|
|
33
|
+
return {
|
|
34
|
+
id: routeDef.path,
|
|
35
|
+
path: routeDef.path ?? routeModule.path,
|
|
36
|
+
...routeModule,
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Ensure that static routes have precedence over the dynamic ones
|
|
43
|
+
for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) {
|
|
44
|
+
promises.push(
|
|
45
|
+
getRouteModule(path, from[path])
|
|
46
|
+
.then((routeModule) => {
|
|
47
|
+
return {
|
|
48
|
+
id: path,
|
|
49
|
+
layout: routeModule.layout,
|
|
50
|
+
path: routeModule.path ?? path
|
|
51
|
+
// Remove /pages and .jsx extension
|
|
52
|
+
.slice(6, -4)
|
|
53
|
+
// Replace [id] with :id
|
|
54
|
+
.replace(param, (_, m) => `:${m}`)
|
|
55
|
+
// Replace '/index' with '/'
|
|
56
|
+
.replace(/\/index$/, '/')
|
|
57
|
+
// Remove trailing slashs
|
|
58
|
+
.replace(/(.+)\/+$/, (...m) => m[1]),
|
|
59
|
+
...routeModule,
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return new Routes(...await Promise.all(promises))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function hydrateRoutes (from) {
|
|
69
|
+
if (Array.isArray(from)) {
|
|
70
|
+
from = Object.fromEntries(
|
|
71
|
+
from.map((route) => [route.path, route]),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return window.routes.map((route) => {
|
|
75
|
+
route.loader = memoImport(from[route.id])
|
|
76
|
+
route.component = lazy(() => route.loader())
|
|
77
|
+
return route
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getRouteModuleExports (routeModule) {
|
|
82
|
+
return {
|
|
83
|
+
// The Route component (default export)
|
|
84
|
+
component: routeModule.default,
|
|
85
|
+
// The Layout Route component
|
|
86
|
+
layout: routeModule.layout,
|
|
87
|
+
// Route-level hooks
|
|
88
|
+
getData: routeModule.getData,
|
|
89
|
+
getMeta: routeModule.getMeta,
|
|
90
|
+
onEnter: routeModule.onEnter,
|
|
91
|
+
// Other Route-level settings
|
|
92
|
+
streaming: routeModule.streaming,
|
|
93
|
+
clientOnly: routeModule.clientOnly,
|
|
94
|
+
serverOnly: routeModule.serverOnly,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getRouteModule (path, routeModule) {
|
|
99
|
+
// const isServer = typeof process !== 'undefined'
|
|
100
|
+
if (typeof routeModule === 'function') {
|
|
101
|
+
routeModule = await routeModule()
|
|
102
|
+
return getRouteModuleExports(routeModule)
|
|
103
|
+
} else {
|
|
104
|
+
return getRouteModuleExports(routeModule)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function memoImport (func) {
|
|
109
|
+
// Otherwise we get a ReferenceError, but since this function
|
|
110
|
+
// is only ran once for each route, there's no overhead
|
|
111
|
+
const kFuncExecuted = Symbol('kFuncExecuted')
|
|
112
|
+
const kFuncValue = Symbol('kFuncValue')
|
|
113
|
+
func[kFuncExecuted] = false
|
|
114
|
+
return async function () {
|
|
115
|
+
if (!func[kFuncExecuted]) {
|
|
116
|
+
func[kFuncValue] = await func()
|
|
117
|
+
func[kFuncExecuted] = true
|
|
118
|
+
}
|
|
119
|
+
return func[kFuncValue]
|
|
120
|
+
}
|
|
121
|
+
}
|