@fastify/react 0.6.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/client.js +44 -0
- package/index.js +11 -244
- package/package.json +45 -15
- package/plugin/index.js +102 -0
- package/plugin/parsers.js +42 -0
- package/plugin/parsers.test.js +28 -0
- package/plugin/preload.js +75 -0
- package/plugin/stores.js +39 -0
- package/plugin/virtual.js +100 -0
- package/rendering.js +139 -0
- package/routing.js +142 -0
- package/server.js +129 -0
- package/templating.js +51 -0
- package/virtual/core.jsx +6 -17
- package/virtual/create.jsx +1 -1
- package/virtual/index.js +7 -0
- package/virtual/layouts.js +1 -1
- package/virtual/mount.js +31 -28
- package/virtual/root.jsx +1 -1
- package/virtual/routes.js +1 -123
- package/plugin.cjs +0 -115
- package/server/stream.js +0 -56
- /package/{server/context.js → context.js} +0 -0
package/plugin/stores.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
export function generateStores(keys) {
|
|
3
|
+
let code = `
|
|
4
|
+
import { useRouteContext } from '@fastify/react/client'
|
|
5
|
+
|
|
6
|
+
function storeGetter (proxy, prop) {
|
|
7
|
+
if (!proxy.context) {
|
|
8
|
+
proxy.context = useRouteContext()
|
|
9
|
+
}
|
|
10
|
+
if (prop === 'state') {
|
|
11
|
+
return proxy.context.state[proxy.key]
|
|
12
|
+
}
|
|
13
|
+
let method
|
|
14
|
+
if (method = proxy.context.actions[proxy.key][prop]) {
|
|
15
|
+
if (!proxy.wrappers[prop]) {
|
|
16
|
+
proxy.wrappers[prop] = (...args) => {
|
|
17
|
+
return method(proxy.context.state, ...args)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return proxy.wrappers[prop]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
code += `
|
|
26
|
+
export const ${key} = new Proxy({
|
|
27
|
+
key: '${key}',
|
|
28
|
+
wrappers: {},
|
|
29
|
+
context: null,
|
|
30
|
+
}, {
|
|
31
|
+
get: storeGetter
|
|
32
|
+
})
|
|
33
|
+
`
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
code,
|
|
37
|
+
map: null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { findExports } from 'mlly'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
const virtualRoot = resolve(__dirname, '..', 'virtual')
|
|
9
|
+
|
|
10
|
+
const virtualModules = [
|
|
11
|
+
'mount.js',
|
|
12
|
+
'resource.js',
|
|
13
|
+
'routes.js',
|
|
14
|
+
'layouts.js',
|
|
15
|
+
'create.jsx',
|
|
16
|
+
'root.jsx',
|
|
17
|
+
'layouts/',
|
|
18
|
+
'context.js',
|
|
19
|
+
'core.jsx',
|
|
20
|
+
'index.js',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export const prefix = /^\/?\$app\//
|
|
24
|
+
|
|
25
|
+
export async function resolveId (id) {
|
|
26
|
+
if (prefix.test(id)) {
|
|
27
|
+
const [, virtual] = id.split(prefix)
|
|
28
|
+
if (virtual) {
|
|
29
|
+
const override = loadVirtualModuleOverride(this.root, virtual)
|
|
30
|
+
if (override) {
|
|
31
|
+
return override
|
|
32
|
+
}
|
|
33
|
+
return id
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadVirtualModule (virtualInput) {
|
|
39
|
+
let virtual = virtualInput
|
|
40
|
+
if (!/\.((mc)?ts)|((mc)?js)|(jsx)$/.test(virtual)) {
|
|
41
|
+
virtual += '.js'
|
|
42
|
+
}
|
|
43
|
+
if (!virtualModules.includes(virtual)) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
|
|
47
|
+
return {
|
|
48
|
+
code,
|
|
49
|
+
map: null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
virtualModules.includes = function (virtual) {
|
|
54
|
+
if (!virtual) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
for (const entry of this) {
|
|
58
|
+
if (virtual.startsWith(entry)) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadVirtualModuleOverride (viteProjectRoot, virtual) {
|
|
66
|
+
if (!virtualModules.includes(virtual)) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const overridePath = resolve(viteProjectRoot, virtual)
|
|
70
|
+
if (existsSync(overridePath)) {
|
|
71
|
+
return overridePath
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadSource (id) {
|
|
76
|
+
const filePath = id
|
|
77
|
+
.replace(/\?client$/, '')
|
|
78
|
+
.replace(/\?server$/, '')
|
|
79
|
+
return readFileSync(filePath, 'utf8')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createPlaceholderExports (source) {
|
|
83
|
+
let pExports = ''
|
|
84
|
+
for (const exp of findExports(source)) {
|
|
85
|
+
switch (exp.type) {
|
|
86
|
+
case 'named':
|
|
87
|
+
for (const name of exp.names) {
|
|
88
|
+
pExports += `export const ${name} = {}\n`
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
case 'default':
|
|
92
|
+
pExports += `export default {}\n`
|
|
93
|
+
break
|
|
94
|
+
case 'declaration':
|
|
95
|
+
pExports += `export const ${exp.name} = {}\n`
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return pExports
|
|
100
|
+
}
|
package/rendering.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
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
|
+
// React 18's preferred server-side rendering function,
|
|
6
|
+
// which enables the combination of React.lazy() and Suspense
|
|
7
|
+
import { renderToPipeableStream } from 'react-dom/server'
|
|
8
|
+
import * as devalue from 'devalue'
|
|
9
|
+
import Head from 'unihead'
|
|
10
|
+
import { createHtmlTemplates } from './templating.js'
|
|
11
|
+
|
|
12
|
+
// Helper function to get an AsyncIterable (via PassThrough)
|
|
13
|
+
// from the renderToPipeableStream() onShellReady event
|
|
14
|
+
export function onShellReady(app) {
|
|
15
|
+
const duplex = new Minipass()
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
try {
|
|
18
|
+
const pipeable = renderToPipeableStream(app, {
|
|
19
|
+
onShellReady() {
|
|
20
|
+
resolve(pipeable.pipe(duplex))
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
} catch (error) {
|
|
24
|
+
resolve(error)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper function to get an AsyncIterable (via Minipass)
|
|
30
|
+
// from the renderToPipeableStream() onAllReady event
|
|
31
|
+
export function onAllReady(app) {
|
|
32
|
+
const duplex = new Minipass()
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
try {
|
|
35
|
+
const pipeable = renderToPipeableStream(app, {
|
|
36
|
+
onAllReady() {
|
|
37
|
+
resolve(pipeable.pipe(duplex))
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
} catch (error) {
|
|
41
|
+
resolve(error)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createRenderFunction ({ routes, create }) {
|
|
47
|
+
// Used when hydrating React Router on the client
|
|
48
|
+
const routeMap = Object.fromEntries(routes.map(_ => [_.path, _]))
|
|
49
|
+
// Registered as reply.render()
|
|
50
|
+
return function () {
|
|
51
|
+
if (this.request.route.streaming) {
|
|
52
|
+
return createStreamingResponse(this.request, routes, routeMap, create)
|
|
53
|
+
}
|
|
54
|
+
return createResponse(this.request, routes, routeMap, create)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createStreamingResponse (req, routes) {
|
|
59
|
+
// SSR stream
|
|
60
|
+
const body = await onShellReady(req.route.app)
|
|
61
|
+
return { routes, context: req.route, body }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function createResponse (req, routes) {
|
|
65
|
+
let body
|
|
66
|
+
if (!req.route.clientOnly) {
|
|
67
|
+
// SSR string
|
|
68
|
+
body = await onAllReady(req.route.app)
|
|
69
|
+
}
|
|
70
|
+
return { routes, context: req.route, body }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The return value of this function gets registered as reply.html()
|
|
74
|
+
export async function createHtmlFunction (source, _, config) {
|
|
75
|
+
// Creates `universal` and `serverOnly` sets of
|
|
76
|
+
// HTML `beforeElement` and `afterElement` templates
|
|
77
|
+
const templates = await createHtmlTemplates(source, config)
|
|
78
|
+
|
|
79
|
+
// Registered as reply.html()
|
|
80
|
+
return async function () {
|
|
81
|
+
const { routes, context, body } = await this.render()
|
|
82
|
+
|
|
83
|
+
this.type('text/html')
|
|
84
|
+
|
|
85
|
+
// Use template with client module import removed
|
|
86
|
+
if (context.serverOnly) {
|
|
87
|
+
// Turn off hydration
|
|
88
|
+
context.hydration = ''
|
|
89
|
+
|
|
90
|
+
return streamShell(
|
|
91
|
+
templates.serverOnly,
|
|
92
|
+
context,
|
|
93
|
+
body,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Embed full hydration script
|
|
98
|
+
context.hydration = (
|
|
99
|
+
`<script>\nwindow.route = ${
|
|
100
|
+
// Server data payload
|
|
101
|
+
devalue.uneval(context.toJSON())
|
|
102
|
+
}\nwindow.routes = ${
|
|
103
|
+
// Universal router payload
|
|
104
|
+
devalue.uneval(routes.toJSON())
|
|
105
|
+
}\n</script>`
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// In all other cases use universal,
|
|
109
|
+
// template which works the same for SSR and CSR.
|
|
110
|
+
|
|
111
|
+
if (context.clientOnly) {
|
|
112
|
+
return sendClientOnlyShell(templates.universal, context)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return streamShell(templates.universal, context, body)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function sendClientOnlyShell (templates, context, body) {
|
|
120
|
+
context.head = new Head(context.head).render()
|
|
121
|
+
return `${
|
|
122
|
+
templates.beforeElement(context)
|
|
123
|
+
}${
|
|
124
|
+
templates.afterElement(context)
|
|
125
|
+
}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function streamShell (templates, context, body) {
|
|
129
|
+
context.head = new Head(context.head).render()
|
|
130
|
+
return Readable.from(createShellStream(templates, context, body))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function * createShellStream (templates, context, body) {
|
|
134
|
+
yield templates.beforeElement(context)
|
|
135
|
+
for await (const chunk of body) {
|
|
136
|
+
yield chunk
|
|
137
|
+
}
|
|
138
|
+
yield templates.afterElement(context)
|
|
139
|
+
}
|
package/routing.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import Youch from 'youch'
|
|
4
|
+
import RouteContext from './context.js'
|
|
5
|
+
import { createHtmlFunction } from './rendering.js'
|
|
6
|
+
|
|
7
|
+
export async function prepareClient (entries, _) {
|
|
8
|
+
const client = entries.ssr
|
|
9
|
+
if (client.context instanceof Promise) {
|
|
10
|
+
client.context = await client.context
|
|
11
|
+
}
|
|
12
|
+
if (client.routes instanceof Promise) {
|
|
13
|
+
client.routes = await client.routes
|
|
14
|
+
}
|
|
15
|
+
if (client.create instanceof Promise) {
|
|
16
|
+
const { default: create } = await client.create
|
|
17
|
+
client.create = create
|
|
18
|
+
}
|
|
19
|
+
return client
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createErrorHandler (_, scope, config) {
|
|
23
|
+
return async (error, req, reply) => {
|
|
24
|
+
req.log.error(error)
|
|
25
|
+
if (config.dev) {
|
|
26
|
+
const youch = new Youch(error, req.raw)
|
|
27
|
+
reply.code(500)
|
|
28
|
+
reply.type('text/html')
|
|
29
|
+
reply.send(await youch.toHTML())
|
|
30
|
+
return reply
|
|
31
|
+
}
|
|
32
|
+
reply.code(500)
|
|
33
|
+
reply.send('')
|
|
34
|
+
return reply
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function createRoute ({ client, errorHandler, route }, scope, config) {
|
|
39
|
+
if (route.configure) {
|
|
40
|
+
await route.configure(scope)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Used when hydrating Vue Router on the client
|
|
44
|
+
const routeMap = Object.fromEntries(client.routes.map(_ => [_.path, _]))
|
|
45
|
+
|
|
46
|
+
// Extend with route context initialization module
|
|
47
|
+
RouteContext.extend(client.context)
|
|
48
|
+
|
|
49
|
+
const onRequest = async (req, reply) => {
|
|
50
|
+
req.route = await RouteContext.create(
|
|
51
|
+
scope,
|
|
52
|
+
req,
|
|
53
|
+
reply,
|
|
54
|
+
route,
|
|
55
|
+
client.context,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const preHandler = [
|
|
60
|
+
async (req) => {
|
|
61
|
+
if (!req.route.clientOnly) {
|
|
62
|
+
const app = client.create({
|
|
63
|
+
routes: client.routes,
|
|
64
|
+
routeMap,
|
|
65
|
+
ctxHydration: req.route,
|
|
66
|
+
url: req.url,
|
|
67
|
+
})
|
|
68
|
+
req.route.app = app
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
if (route.getData) {
|
|
74
|
+
preHandler.push(async (req) => {
|
|
75
|
+
if (!req.route.data) {
|
|
76
|
+
req.route.data = {}
|
|
77
|
+
}
|
|
78
|
+
const result = await route.getData(req.route)
|
|
79
|
+
Object.assign(req.route.data, result)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (route.getMeta) {
|
|
84
|
+
preHandler.push(async (req) => {
|
|
85
|
+
req.route.head = await route.getMeta(req.route)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (route.onEnter) {
|
|
90
|
+
preHandler.push(async (req) => {
|
|
91
|
+
try {
|
|
92
|
+
if (route.onEnter) {
|
|
93
|
+
if (!req.route.data) {
|
|
94
|
+
req.route.data = {}
|
|
95
|
+
}
|
|
96
|
+
const result = await route.onEnter(req.route)
|
|
97
|
+
Object.assign(req.route.data, result)
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (config.dev) {
|
|
101
|
+
console.error(err)
|
|
102
|
+
}
|
|
103
|
+
req.route.error = err
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Route handler
|
|
109
|
+
let handler
|
|
110
|
+
if (config.dev) {
|
|
111
|
+
handler = (_, reply) => reply.html()
|
|
112
|
+
} else {
|
|
113
|
+
const { id } = route
|
|
114
|
+
const htmlPath = id.replace(/pages\/(.*?)\.jsx/, 'html/$1.html')
|
|
115
|
+
const htmlSource = readFileSync(join(config.vite.root, config.vite.build.outDir, htmlPath), 'utf8')
|
|
116
|
+
const htmlFunction = await createHtmlFunction(htmlSource, scope, config)
|
|
117
|
+
handler = (_, reply) => htmlFunction.call(reply)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Replace wildcard routes with Fastify compatible syntax
|
|
121
|
+
const routePath = route.path.replace(/:[^+]+\+/, '*')
|
|
122
|
+
|
|
123
|
+
scope.route({
|
|
124
|
+
url: routePath,
|
|
125
|
+
method: route.method ?? ['GET', 'POST', 'PUT', 'DELETE'],
|
|
126
|
+
errorHandler,
|
|
127
|
+
onRequest,
|
|
128
|
+
preHandler,
|
|
129
|
+
handler,
|
|
130
|
+
...route,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (route.getData) {
|
|
134
|
+
// If getData is provided, register JSON endpoint for it
|
|
135
|
+
scope.get(`/-/data${routePath}`, {
|
|
136
|
+
onRequest,
|
|
137
|
+
async handler (req, reply) {
|
|
138
|
+
reply.send(await route.getData(req.route))
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Otherwise we get a ReferenceError, but since
|
|
2
|
+
// this function is only ran once, there's no overhead
|
|
3
|
+
class Routes extends Array {
|
|
4
|
+
toJSON () {
|
|
5
|
+
return this.map((route) => {
|
|
6
|
+
return {
|
|
7
|
+
id: route.id,
|
|
8
|
+
path: route.path,
|
|
9
|
+
name: route.name,
|
|
10
|
+
layout: route.layout,
|
|
11
|
+
getData: !!route.getData,
|
|
12
|
+
getMeta: !!route.getMeta,
|
|
13
|
+
onEnter: !!route.onEnter,
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function prepareServer(server) {
|
|
20
|
+
let url
|
|
21
|
+
server.decorate('serverURL', { getter: () => url })
|
|
22
|
+
server.addHook('onListen', () => {
|
|
23
|
+
const { port, address, family } = server.server.address()
|
|
24
|
+
const protocol = server.https ? 'https' : 'http'
|
|
25
|
+
if (family === 'IPv6') {
|
|
26
|
+
url = `${protocol}://[${address}]:${port}`
|
|
27
|
+
} else {
|
|
28
|
+
url = `${protocol}://${address}:${port}`
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
server.decorateRequest('fetchMap', null)
|
|
32
|
+
server.addHook('onRequest', (req, _, done) => {
|
|
33
|
+
req.fetchMap = new Map()
|
|
34
|
+
done()
|
|
35
|
+
})
|
|
36
|
+
server.addHook('onResponse', (req, _, done) => {
|
|
37
|
+
req.fetchMap = undefined
|
|
38
|
+
done()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createRoutes (fromPromise, { param } = { param: /\[([\.\w]+\+?)\]/ }) {
|
|
43
|
+
const { default: from } = await fromPromise
|
|
44
|
+
const importPaths = Object.keys(from)
|
|
45
|
+
const promises = []
|
|
46
|
+
if (Array.isArray(from)) {
|
|
47
|
+
for (const routeDef of from) {
|
|
48
|
+
promises.push(
|
|
49
|
+
getRouteModule(routeDef.path, routeDef.component)
|
|
50
|
+
.then((routeModule) => {
|
|
51
|
+
return {
|
|
52
|
+
id: routeDef.path,
|
|
53
|
+
name: routeDef.path ?? routeModule.path,
|
|
54
|
+
path: routeDef.path ?? routeModule.path,
|
|
55
|
+
...routeModule,
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Ensure that static routes have precedence over the dynamic ones
|
|
62
|
+
for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) {
|
|
63
|
+
promises.push(
|
|
64
|
+
getRouteModule(path, from[path])
|
|
65
|
+
.then((routeModule) => {
|
|
66
|
+
const route = {
|
|
67
|
+
id: path,
|
|
68
|
+
layout: routeModule.layout,
|
|
69
|
+
name: path
|
|
70
|
+
// Remove /pages and .vue extension
|
|
71
|
+
.slice(6, -4)
|
|
72
|
+
// Remove params
|
|
73
|
+
.replace(param, '')
|
|
74
|
+
// Remove leading and trailing slashes
|
|
75
|
+
.replace(/^\/*|\/*$/g, '')
|
|
76
|
+
// Replace slashes with underscores
|
|
77
|
+
.replace(/\//g, '_'),
|
|
78
|
+
path:
|
|
79
|
+
routeModule.path ??
|
|
80
|
+
path
|
|
81
|
+
// Remove /pages and .vue extension
|
|
82
|
+
.slice(6, -4)
|
|
83
|
+
// Replace [id] with :id and [slug+] with :slug+
|
|
84
|
+
.replace(param, (_, m) => `:${m}`)
|
|
85
|
+
.replace(/:\w+\+/, (_, m) => `*`)
|
|
86
|
+
// Replace '/index' with '/'
|
|
87
|
+
.replace(/\/index$/, '/')
|
|
88
|
+
// Remove trailing slashs
|
|
89
|
+
.replace(/(.+)\/+$/, (...m) => m[1]),
|
|
90
|
+
...routeModule,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (route.name === '') {
|
|
94
|
+
route.name = 'catch-all'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return route
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return new Routes(...await Promise.all(promises))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function getRouteModuleExports (routeModule) {
|
|
107
|
+
return {
|
|
108
|
+
// The Route component (default export)
|
|
109
|
+
component: routeModule.default,
|
|
110
|
+
// The Layout Route component
|
|
111
|
+
layout: routeModule.layout,
|
|
112
|
+
// Route-level hooks
|
|
113
|
+
getData: routeModule.getData,
|
|
114
|
+
getMeta: routeModule.getMeta,
|
|
115
|
+
onEnter: routeModule.onEnter,
|
|
116
|
+
// Other Route-level settings
|
|
117
|
+
streaming: routeModule.streaming,
|
|
118
|
+
clientOnly: routeModule.clientOnly,
|
|
119
|
+
serverOnly: routeModule.serverOnly,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getRouteModule (path, routeModuleInput) {
|
|
124
|
+
if (typeof routeModuleInput === 'function') {
|
|
125
|
+
const routeModule = await routeModuleInput()
|
|
126
|
+
return getRouteModuleExports(routeModule)
|
|
127
|
+
}
|
|
128
|
+
return getRouteModuleExports(routeModuleInput)
|
|
129
|
+
}
|
package/templating.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHtmlTemplateFunction } from '@fastify/vite/utils'
|
|
2
|
+
import { HTMLRewriter } from 'html-rewriter-wasm'
|
|
3
|
+
|
|
4
|
+
export async function createHtmlTemplates (source, config) {
|
|
5
|
+
const el = '<!-- element -->'
|
|
6
|
+
|
|
7
|
+
const universal = source.split(el)
|
|
8
|
+
const serverOnlyRaw = await removeClientModule(source, config)
|
|
9
|
+
const serverOnly = serverOnlyRaw.split(el)
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
// Templates for client-only and universal rendering
|
|
13
|
+
universal: {
|
|
14
|
+
beforeElement: await createHtmlTemplateFunction(universal[0]),
|
|
15
|
+
afterElement: await createHtmlTemplateFunction(universal[1]),
|
|
16
|
+
},
|
|
17
|
+
// Templates for server-only rendering
|
|
18
|
+
serverOnly: {
|
|
19
|
+
beforeElement: await createHtmlTemplateFunction(serverOnly[0]),
|
|
20
|
+
afterElement: await createHtmlTemplateFunction(serverOnly[1]),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function removeClientModule (html, config) {
|
|
26
|
+
const decoder = new TextDecoder()
|
|
27
|
+
|
|
28
|
+
let output = ''
|
|
29
|
+
const rewriter = new HTMLRewriter((outputChunk) => {
|
|
30
|
+
output += decoder.decode(outputChunk)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
rewriter.on('script', {
|
|
34
|
+
element (element) {
|
|
35
|
+
for (const [attr, value] of element.attributes) {
|
|
36
|
+
if (attr === 'type' && value === 'module') {
|
|
37
|
+
element.replace('')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const encoder = new TextEncoder()
|
|
45
|
+
await rewriter.write(encoder.encode(html))
|
|
46
|
+
await rewriter.end()
|
|
47
|
+
return output
|
|
48
|
+
} finally {
|
|
49
|
+
rewriter.free()
|
|
50
|
+
}
|
|
51
|
+
}
|
package/virtual/core.jsx
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { createPath } from 'history'
|
|
2
|
-
import {
|
|
3
|
-
import { BrowserRouter, useLocation } from 'react-router
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import layouts from '
|
|
7
|
-
import { waitFetch, waitResource } from '
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
import { BrowserRouter, StaticRouter, useLocation } from 'react-router'
|
|
4
|
+
import { proxy } from 'valtio'
|
|
5
|
+
import { RouteContext, useRouteContext } from '@fastify/react/client'
|
|
6
|
+
import layouts from '$app/layouts.js'
|
|
7
|
+
import { waitFetch, waitResource } from '$app/resource.js'
|
|
8
8
|
|
|
9
9
|
export const isServer = import.meta.env.SSR
|
|
10
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
11
|
|
|
23
12
|
let serverActionCounter = 0
|
|
24
13
|
|
package/virtual/create.jsx
CHANGED
package/virtual/index.js
ADDED