@vindo/react 0.0.1 → 0.0.3
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 +4 -4
- package/index.d.ts +19 -8
- package/index.js +2 -219
- package/lib/client.js +320 -0
- package/lib/request.js +127 -0
- package/lib/server.js +457 -0
- package/lib/util.js +105 -0
- package/package.json +20 -2
- package/client.js +0 -95
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
##
|
|
2
|
-
|
|
1
|
+
## React SSR
|
|
2
|
+
React server side rendering for @vindo/core framework
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
```
|
|
7
|
-
npm install @vindo/
|
|
7
|
+
npm install @vindo/react
|
|
8
8
|
```
|
|
9
9
|
Or
|
|
10
10
|
```
|
|
11
|
-
yarn add @vindo/
|
|
11
|
+
yarn add @vindo/react
|
|
12
12
|
```
|
package/index.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
declare module '@vindo/react' {
|
|
2
|
-
export const HTTPResponse: {
|
|
3
|
-
get(cb:Function): void
|
|
4
|
-
post(cb:Function): void
|
|
5
|
-
}
|
|
6
2
|
export function server(): Function;
|
|
7
3
|
}
|
|
8
4
|
|
|
5
|
+
declare module '@vindo/react/request'
|
|
9
6
|
declare module '@vindo/react/client' {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
type DataType = {
|
|
8
|
+
[key:string]: string | number | boolean
|
|
9
|
+
}
|
|
10
|
+
type HeaderType = {
|
|
11
|
+
[key:string]: string
|
|
12
|
+
}
|
|
13
|
+
export const http: {
|
|
14
|
+
set(args:{data?: DataType, path?: string}): Promise<object>
|
|
15
|
+
get(...args:any): Promise<object>
|
|
16
|
+
post(...args:any): Promise<object>
|
|
17
|
+
}
|
|
18
|
+
export function useStore(name?:string): any
|
|
19
|
+
export function useState(state?:{[key:string]: any}): any
|
|
20
|
+
export function useContext(): any
|
|
21
|
+
export function Link(props:any): any
|
|
22
|
+
export function View(props:any): any
|
|
23
|
+
export function Content(props:any): any
|
|
24
|
+
export function Provider(props:any): any
|
|
14
25
|
}
|
package/index.js
CHANGED
|
@@ -1,224 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @vindo/react
|
|
3
|
-
* Copyright(c)
|
|
3
|
+
* Copyright(c) 2023 Ruel Mindo
|
|
4
4
|
* MIT Licensed
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const path = require('node:path')
|
|
11
|
-
const React = require('node_modules/react')
|
|
12
|
-
const ReactDom = require('node_modules/react-dom/server')
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Shorthand
|
|
16
|
-
*/
|
|
17
|
-
const map = React.Children.map
|
|
18
|
-
const clone = React.cloneElement
|
|
19
|
-
const create = React.createElement
|
|
20
|
-
const isValid = React.isValidElement
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var data = {}
|
|
24
|
-
var index = require(path.resolve('src/react'))
|
|
25
|
-
if(index.default) {
|
|
26
|
-
index = index.default
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Set data
|
|
32
|
-
* @param {object} el
|
|
33
|
-
* @param {object} meta
|
|
34
|
-
*/
|
|
35
|
-
function set(el, meta) {
|
|
36
|
-
const {children: content, ...props} = el.props
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Use id if no name provided
|
|
40
|
-
*/
|
|
41
|
-
var name = props.name ?? props.id
|
|
42
|
-
var opt = {
|
|
43
|
-
name,
|
|
44
|
-
meta: {name, ...meta}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
switch(el.type.name) {
|
|
48
|
-
case 'View':
|
|
49
|
-
Object.assign(opt, {bundle: true, content: content ?? props})
|
|
50
|
-
break
|
|
51
|
-
default:
|
|
52
|
-
Object.assign(opt, {bundle: false, content: el})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function children(item) {
|
|
57
|
-
var children = item.props.children
|
|
58
|
-
|
|
59
|
-
if(item.type == 'head') {
|
|
60
|
-
children = head(children, opt)
|
|
61
|
-
}
|
|
62
|
-
if(item.type == 'body') {
|
|
63
|
-
children = body(children, opt)
|
|
64
|
-
}
|
|
65
|
-
return clone(item, item.props, children)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const app = index({name, content: opt.content, ...meta})
|
|
70
|
-
return {
|
|
71
|
-
name,
|
|
72
|
-
html: html(
|
|
73
|
-
clone(app, {}, map(app.props.children, children))
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Set data to first level of children function
|
|
81
|
-
* @param {array} children
|
|
82
|
-
* @param {object} data
|
|
83
|
-
*/
|
|
84
|
-
function inherit(children, data) {
|
|
85
|
-
|
|
86
|
-
return children.map((child, key) => {
|
|
87
|
-
if(typeof child.type == 'function') {
|
|
88
|
-
child = child.type({...child.props, data, meta: data.meta})
|
|
89
|
-
}
|
|
90
|
-
return clone(child, {key})
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Set DOCTYPE
|
|
97
|
-
* @param {object} html
|
|
98
|
-
*/
|
|
99
|
-
function html(obj) {
|
|
100
|
-
return '<!DOCTYPE html>'.concat(ReactDom.renderToString(obj))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Add a bundle script to head
|
|
106
|
-
* @param children
|
|
107
|
-
* @param args
|
|
108
|
-
*/
|
|
109
|
-
function head(children, args) {
|
|
110
|
-
const env = process.env
|
|
111
|
-
|
|
112
|
-
if(!args.bundle) {
|
|
113
|
-
return children
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const queries = new URLSearchParams({
|
|
117
|
-
hash: env.UUID,
|
|
118
|
-
name: args.name,
|
|
119
|
-
port: (env.NODE_ENV == 'dev' || env.NODE_ENV == 'develop' || env.NODE_ENV == 'development') && env.DEV_SERVER_PORT
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
return children.concat(
|
|
123
|
-
create('script', {
|
|
124
|
-
key: 0,
|
|
125
|
-
id: 'bundle',
|
|
126
|
-
type: 'module',
|
|
127
|
-
src: '/bundle.js?'.concat(queries.toString())
|
|
128
|
-
})
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Set meta data to children
|
|
135
|
-
* @param {array} children
|
|
136
|
-
* @param {object} args
|
|
137
|
-
*/
|
|
138
|
-
function body(children, args) {
|
|
139
|
-
|
|
140
|
-
if(typeof children.type == 'function') {
|
|
141
|
-
const type = children.type(args)
|
|
142
|
-
|
|
143
|
-
if(!type) {
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
if(type.props.children) {
|
|
147
|
-
children = type.props.children
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
data[args.name] = inherit(children, args)
|
|
151
|
-
|
|
152
|
-
return data[args.name]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Client fetch response
|
|
158
|
-
*
|
|
159
|
-
* @param {object} req
|
|
160
|
-
* @param {object} res
|
|
161
|
-
*/
|
|
162
|
-
function HTTPResponse(req, res) {
|
|
163
|
-
const token = req.get('x-fetch-request-token')
|
|
164
|
-
|
|
165
|
-
const response = function(data) {
|
|
166
|
-
if(token) {
|
|
167
|
-
res.json(data, 200, {'X-Fetch-Response': token})
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
get(cb) {
|
|
173
|
-
if(req.method == 'GET')
|
|
174
|
-
response(cb(req.query))
|
|
175
|
-
},
|
|
176
|
-
post(cb) {
|
|
177
|
-
if(req.method == 'POST')
|
|
178
|
-
response(cb(req.body))
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Middleware
|
|
186
|
-
*/
|
|
187
|
-
exports.server = function server() {
|
|
188
|
-
|
|
189
|
-
return function(req, res, next, {env, meta, events, exception}) {
|
|
190
|
-
exports.HTTPResponse = HTTPResponse(req, res)
|
|
191
|
-
/**
|
|
192
|
-
* JSX object
|
|
193
|
-
*/
|
|
194
|
-
if(req.is(env.UUID)) {
|
|
195
|
-
data = res.json(data[req.query.name]) ?? {}
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
events.on('render', function(component) {
|
|
200
|
-
if(req.get('x-fetch-request-token')) {
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if(isValid(component)) {
|
|
205
|
-
var data = set(component, meta)
|
|
206
|
-
if(!data) {
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Return nothing if the request not matched with the component.
|
|
211
|
-
*/
|
|
212
|
-
const errors = Object.keys(exception.statuses).concat('error')
|
|
213
|
-
if(data.name && req.name) {
|
|
214
|
-
if(req.route.back && data.name !== req.name && !errors.includes(data.name)) {
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return data
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
next()
|
|
223
|
-
}
|
|
224
|
-
}
|
|
7
|
+
module.exports = require('./lib/server')
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @vindo/react
|
|
3
|
+
* Copyright(c) 2025 Ruel Mindo
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import ReactDom from 'react-dom/client'
|
|
12
|
+
import {request, HTTPRequest} from '@vindo/react/request'
|
|
13
|
+
import {isObj, isFunc, merge, transform} from '@vindo/react/util'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const event = {
|
|
17
|
+
data: {
|
|
18
|
+
state: {}
|
|
19
|
+
},
|
|
20
|
+
updating: false
|
|
21
|
+
}
|
|
22
|
+
const _http = HTTPRequest()
|
|
23
|
+
const _context = React.createContext({})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Update DOM
|
|
28
|
+
*/
|
|
29
|
+
function update({path, type, ...data}, opts = {}) {
|
|
30
|
+
const args = {
|
|
31
|
+
type: type ?? 'update',
|
|
32
|
+
path,
|
|
33
|
+
data: merge(event.data.state, data)
|
|
34
|
+
}
|
|
35
|
+
request(args, opts).then((data) => event.render(data, type))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Use state
|
|
40
|
+
*/
|
|
41
|
+
export function useState(initialState = {}) {
|
|
42
|
+
if(!isObj(initialState)) {
|
|
43
|
+
throw Error('Custom state hook only allow object as parameter.')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ref = React.useRef({})
|
|
47
|
+
const [state, setState] = React.useState(initialState)
|
|
48
|
+
|
|
49
|
+
merge(ref.current, state)
|
|
50
|
+
|
|
51
|
+
return new Proxy({
|
|
52
|
+
async set(state = {}) {
|
|
53
|
+
event.updating = false
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge the value of its argument if the function (set) have more than 1 state as arguments
|
|
57
|
+
*/
|
|
58
|
+
if(arguments.length > 1) {
|
|
59
|
+
merge(state, ...arguments)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if(isObj(state)) {
|
|
63
|
+
setState(state)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if(isFunc(state)) {
|
|
67
|
+
const dataState = state(ref.current)
|
|
68
|
+
|
|
69
|
+
if(dataState) {
|
|
70
|
+
if(dataState instanceof Promise) {
|
|
71
|
+
setState(await dataState)
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
setState(dataState)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* Send GET request to the server
|
|
81
|
+
*/
|
|
82
|
+
get(data) {
|
|
83
|
+
if(isObj(data)) {
|
|
84
|
+
return _http.get({data})
|
|
85
|
+
}
|
|
86
|
+
if(isFunc(data)) {
|
|
87
|
+
return _http.get({}).then(data)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Send POST request to the server
|
|
92
|
+
*/
|
|
93
|
+
post(data) {
|
|
94
|
+
if(isObj(data)) {
|
|
95
|
+
return _http.post({data})
|
|
96
|
+
}
|
|
97
|
+
if(isFunc(data)) {
|
|
98
|
+
return _http.post({}).then(data)
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Re-render DOM with new global state
|
|
103
|
+
*/
|
|
104
|
+
update(data = {}) {
|
|
105
|
+
/**
|
|
106
|
+
* Merge the value of its argument if the function (set) have more than 1 state as arguments
|
|
107
|
+
*/
|
|
108
|
+
if(arguments.length > 1) {
|
|
109
|
+
merge(data, ...arguments)
|
|
110
|
+
}
|
|
111
|
+
event.update(data)
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
get(target, key) {
|
|
116
|
+
if(target[key]) {
|
|
117
|
+
return target[key]
|
|
118
|
+
}
|
|
119
|
+
if(event.updating) {
|
|
120
|
+
merge(ref.current, event.data.state)
|
|
121
|
+
}
|
|
122
|
+
return ref.current[key]
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Context
|
|
129
|
+
*/
|
|
130
|
+
export function useStore() {
|
|
131
|
+
const store = useContext('store')
|
|
132
|
+
|
|
133
|
+
function dispatch(data) {
|
|
134
|
+
const args = {
|
|
135
|
+
data,
|
|
136
|
+
type: 'store',
|
|
137
|
+
}
|
|
138
|
+
request(args, {method: 'POST'}).then((data) => event.render(data))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const proto = {
|
|
142
|
+
clear() {
|
|
143
|
+
dispatch({action: 'clear'})
|
|
144
|
+
},
|
|
145
|
+
remove(name) {
|
|
146
|
+
dispatch({action: 'remove', data: name})
|
|
147
|
+
},
|
|
148
|
+
dispatch(data) {
|
|
149
|
+
dispatch({action: 'add', data})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return merge(Object.create(proto), store)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Context
|
|
158
|
+
*/
|
|
159
|
+
export function useContext(name = null) {
|
|
160
|
+
const {meta, data, state, store} = React.useContext(_context)
|
|
161
|
+
|
|
162
|
+
switch(name) {
|
|
163
|
+
case 'store':
|
|
164
|
+
return store
|
|
165
|
+
case 'content':
|
|
166
|
+
return {data, name: meta.name}
|
|
167
|
+
default:
|
|
168
|
+
return {meta, state}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Wrapper
|
|
175
|
+
*/
|
|
176
|
+
export function Provider({children, ...value}) {
|
|
177
|
+
return React.createElement(_context, {value}, children)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Link
|
|
182
|
+
*/
|
|
183
|
+
export function Link({href, text, disabled, children}) {
|
|
184
|
+
if(!href) {
|
|
185
|
+
throw new ReferenceError(`Props 'href' is missing.`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const onClick = (e) => {
|
|
189
|
+
if(disabled || location.pathname == href) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
e.preventDefault()
|
|
193
|
+
|
|
194
|
+
update({
|
|
195
|
+
type: 'route',
|
|
196
|
+
path: new URL(href, location.origin)
|
|
197
|
+
})
|
|
198
|
+
history.pushState({}, text, href)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if(!children) {
|
|
202
|
+
children = text
|
|
203
|
+
}
|
|
204
|
+
return React.createElement('a', {href, onClick}, children)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Component Holder
|
|
210
|
+
*/
|
|
211
|
+
export function View() {}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find current route
|
|
216
|
+
*/
|
|
217
|
+
export function Content(props) {
|
|
218
|
+
const {data, name} = useContext('content')
|
|
219
|
+
/**
|
|
220
|
+
* View content coming from backend component (src/http)
|
|
221
|
+
*/
|
|
222
|
+
if(React.isValidElement(data.children)) {
|
|
223
|
+
return data.children
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* View content coming from react directory (src/react)
|
|
228
|
+
*/
|
|
229
|
+
return React.Children.map(props.children, (child) => {
|
|
230
|
+
if(!child.props.name) {
|
|
231
|
+
throw new ReferenceError(`Props 'name' is required for View component.`)
|
|
232
|
+
}
|
|
233
|
+
if(name == child.props.name) {
|
|
234
|
+
return child.props.component(data.props)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render react dom to root
|
|
242
|
+
* @param {object} document
|
|
243
|
+
* @param {object} chunk
|
|
244
|
+
*/
|
|
245
|
+
export function render({head, body}, chunk) {
|
|
246
|
+
var head = ReactDom.createRoot(head)
|
|
247
|
+
var body = ReactDom.createRoot(body)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Re-render DOM
|
|
252
|
+
*/
|
|
253
|
+
event.update = function update(state) {
|
|
254
|
+
merge(
|
|
255
|
+
event.data.state,
|
|
256
|
+
state
|
|
257
|
+
)
|
|
258
|
+
event.render(event.data)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Render content
|
|
263
|
+
*/
|
|
264
|
+
event.render = function render(args, type = null) {
|
|
265
|
+
/**
|
|
266
|
+
* Set data
|
|
267
|
+
*/
|
|
268
|
+
event.data = args
|
|
269
|
+
/**
|
|
270
|
+
* Updating content
|
|
271
|
+
*/
|
|
272
|
+
event.updating = true
|
|
273
|
+
/**
|
|
274
|
+
* Reference for mouse event functions
|
|
275
|
+
*/
|
|
276
|
+
chunk.refs = {
|
|
277
|
+
meta: args.meta,
|
|
278
|
+
state: new Proxy(event, {
|
|
279
|
+
get(target, key) {
|
|
280
|
+
switch(key) {
|
|
281
|
+
case 'set':
|
|
282
|
+
case 'update':
|
|
283
|
+
case 'render':
|
|
284
|
+
return update
|
|
285
|
+
}
|
|
286
|
+
return target.data[key]
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* For route request only
|
|
293
|
+
*/
|
|
294
|
+
if(type == 'route') {
|
|
295
|
+
head.render(chunk.head(args.meta))
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Set only for dynamic content coming from server
|
|
299
|
+
*/
|
|
300
|
+
if(args.data.children) {
|
|
301
|
+
args.data.children = transform(args.data.children, chunk)[0]
|
|
302
|
+
}
|
|
303
|
+
body.render(chunk.body(args))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Update when back/forward button is pressed
|
|
308
|
+
*/
|
|
309
|
+
window.onpopstate = function onpopstate() {
|
|
310
|
+
update({
|
|
311
|
+
type: 'route',
|
|
312
|
+
path: new URL(location.href)
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_http.get({type: 'hydrate'}).then(data => event.render(data))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
export default {render}
|
package/lib/request.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @vindo/react
|
|
3
|
+
* Copyright(c) 2025 Ruel Mindo
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const requestTypes = [
|
|
11
|
+
'store',
|
|
12
|
+
'fetch',
|
|
13
|
+
'route',
|
|
14
|
+
'update',
|
|
15
|
+
'hydrate',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get name
|
|
20
|
+
* @param {string} pathname
|
|
21
|
+
*/
|
|
22
|
+
function name(pathname) {
|
|
23
|
+
const name = pathname.split('/').at(-1)
|
|
24
|
+
if(name) {
|
|
25
|
+
return name
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get pagename
|
|
31
|
+
* @param {string} type
|
|
32
|
+
* @param {object} path
|
|
33
|
+
*/
|
|
34
|
+
function page(type, path) {
|
|
35
|
+
var base
|
|
36
|
+
switch(type) {
|
|
37
|
+
case 'route':
|
|
38
|
+
base = name(path.pathname)
|
|
39
|
+
break
|
|
40
|
+
default:
|
|
41
|
+
base = name(location.pathname)
|
|
42
|
+
}
|
|
43
|
+
return base ?? 'root'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get hash to create new url
|
|
48
|
+
*/
|
|
49
|
+
function getURL() {
|
|
50
|
+
var src = new URL(document.scripts.bundle.src)
|
|
51
|
+
|
|
52
|
+
var name = src.pathname.match(/^\/bundle-(.*)\.js$/)
|
|
53
|
+
if(name) {
|
|
54
|
+
return new URL(name[1], location.origin)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* HTTP Request
|
|
60
|
+
*/
|
|
61
|
+
export function HTTPRequest() {
|
|
62
|
+
const http = Object.defineProperties({}, {
|
|
63
|
+
get: {
|
|
64
|
+
value: function get(args = {}) {
|
|
65
|
+
return request({type: 'fetch', ...args, path: getURL()}, {method: 'GET'})
|
|
66
|
+
},
|
|
67
|
+
writable: false
|
|
68
|
+
},
|
|
69
|
+
post: {
|
|
70
|
+
value: function post(args = {}) {
|
|
71
|
+
return request({type: 'fetch', ...args, path: getURL()}, {method: 'POST'})
|
|
72
|
+
},
|
|
73
|
+
writable: false
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
return http
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* State request
|
|
82
|
+
*/
|
|
83
|
+
export function request({data, path, type, ...args}, opts = {}) {
|
|
84
|
+
|
|
85
|
+
if(data && typeof data !== 'object') {
|
|
86
|
+
throw new TypeError(`Invalid type of 'data'. Expected value of type 'object' but got ${typeof data}'.`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if(!requestTypes.includes(type)) {
|
|
90
|
+
throw new ReferenceError(`Invalid request type.`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var opts = {
|
|
94
|
+
method: 'GET',
|
|
95
|
+
...opts,
|
|
96
|
+
headers: Object.assign(opts.headers ?? {}, {
|
|
97
|
+
'X-State-Request': btoa(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
type,
|
|
100
|
+
page: page(type, path)
|
|
101
|
+
})
|
|
102
|
+
),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if(!data) {
|
|
107
|
+
data = args
|
|
108
|
+
}
|
|
109
|
+
if(!path) {
|
|
110
|
+
path = new URL(location.href)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if(opts.method == 'GET') {
|
|
114
|
+
if(data) {
|
|
115
|
+
for(var i in data) {
|
|
116
|
+
path.searchParams.append(i, data[i])
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if(opts.method == 'POST') {
|
|
122
|
+
opts.body = JSON.stringify(data)
|
|
123
|
+
opts.headers['Content-Type'] = 'application/json'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fetch(path, opts).then((res) => res.json())
|
|
127
|
+
}
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @vindo/react
|
|
3
|
+
* Copyright(c) 2025 Ruel Mindo
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const React = require('react')
|
|
12
|
+
const config = require('@vindo/core/config')
|
|
13
|
+
const ReactDom = require('react-dom/server')
|
|
14
|
+
const {isObj, isArr, isStr, isNum, isFunc, merge} = require('@vindo/react/util')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shorthand
|
|
18
|
+
*/
|
|
19
|
+
const clone = React.cloneElement
|
|
20
|
+
const create = React.createElement
|
|
21
|
+
const isValid = React.isValidElement
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var build = config.get('buildOption')
|
|
25
|
+
var manif = require(path.resolve(build.output, 'manifest.json'))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create HTTP state event ID
|
|
31
|
+
* @param {string} prefix
|
|
32
|
+
* @param {string} name
|
|
33
|
+
*/
|
|
34
|
+
function mkId(prefix, name) {
|
|
35
|
+
if(!name) {
|
|
36
|
+
name = 'root'
|
|
37
|
+
}
|
|
38
|
+
return encode(prefix.concat(name)).replace(/=/, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Encode string to base64
|
|
43
|
+
* @param {string} string
|
|
44
|
+
*/
|
|
45
|
+
function encode(string) {
|
|
46
|
+
return Buffer.from(string).toString('base64')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decode base64 string
|
|
51
|
+
* @param {string} string
|
|
52
|
+
*/
|
|
53
|
+
function decode(string) {
|
|
54
|
+
return JSON.parse(
|
|
55
|
+
Buffer.from(string, 'base64').toString('utf8')
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Require main component from react directory
|
|
61
|
+
* @param {string} point
|
|
62
|
+
* @param {object} data
|
|
63
|
+
*/
|
|
64
|
+
function entry(data) {
|
|
65
|
+
const app = require(path.resolve(path.dirname(build.entry)))
|
|
66
|
+
if(app) {
|
|
67
|
+
return app.default(data)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* React DOM
|
|
74
|
+
* @param {object} data
|
|
75
|
+
*/
|
|
76
|
+
function dom(data) {
|
|
77
|
+
return (item, key) => {
|
|
78
|
+
if(item.type == 'head') {
|
|
79
|
+
return clone(item, {key}, head(item.props.children, data))
|
|
80
|
+
}
|
|
81
|
+
if(item.type == 'body') {
|
|
82
|
+
return clone(item, {key}, body(item.props.children, data))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set DOCTYPE
|
|
89
|
+
* @param {object} html
|
|
90
|
+
*/
|
|
91
|
+
function html(obj) {
|
|
92
|
+
return '<!DOCTYPE html>'.concat(ReactDom.renderToString(obj))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Add a bundle script to head
|
|
98
|
+
* @param children
|
|
99
|
+
* @param args
|
|
100
|
+
*/
|
|
101
|
+
function head(children, {meta}) {
|
|
102
|
+
const env = process.env
|
|
103
|
+
|
|
104
|
+
if(env.NODE_ENV == 'dev' || env.NODE_ENV == 'develop' || env.NODE_ENV == 'development') {
|
|
105
|
+
children = children.concat(
|
|
106
|
+
create('script', {key: 0, src: env.DEV_SERVER}
|
|
107
|
+
))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return children.concat(
|
|
111
|
+
meta.bundle && create('script', {
|
|
112
|
+
key: 1,
|
|
113
|
+
id: 'bundle',
|
|
114
|
+
type: 'module',
|
|
115
|
+
src: manif.bundle
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set meta data to children
|
|
123
|
+
* @param {array} children
|
|
124
|
+
* @param {object} args
|
|
125
|
+
*/
|
|
126
|
+
function body(children, data) {
|
|
127
|
+
if(!isArr(children)) {
|
|
128
|
+
children = [children]
|
|
129
|
+
}
|
|
130
|
+
return children.map((child, key) => clone(child, {key, ...data}))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reduce object to necessary props
|
|
136
|
+
* @param {array|object} children
|
|
137
|
+
*/
|
|
138
|
+
function reducer(children) {
|
|
139
|
+
if(!children) {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
if(!isArr(children)) {
|
|
143
|
+
children = [children]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return children.map(({type, props}) => {
|
|
147
|
+
var p = {}
|
|
148
|
+
|
|
149
|
+
if(!props) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if(isFunc(type)) {
|
|
154
|
+
if(/^default_1/.test(type.name)) {
|
|
155
|
+
throw new ReferenceError(`Component function requires a name. Currently have a default name of '${type.name}'.`)
|
|
156
|
+
}
|
|
157
|
+
type = [type.name]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for(var i in props) {
|
|
161
|
+
var v = props[i]
|
|
162
|
+
|
|
163
|
+
if(isStr(v) || isNum(v)) {
|
|
164
|
+
p[i] = v
|
|
165
|
+
}
|
|
166
|
+
if(isArr(v)) {
|
|
167
|
+
p.children = v.map((v) => {
|
|
168
|
+
if(isObj(v)) {
|
|
169
|
+
return reducer(v)[0]
|
|
170
|
+
}
|
|
171
|
+
return v
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
if(isObj(v)) {
|
|
175
|
+
if(i == 'style') {
|
|
176
|
+
p.style = v
|
|
177
|
+
}
|
|
178
|
+
if(i == 'children') {
|
|
179
|
+
p.children = reducer(v)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if(isFunc(v)) {
|
|
183
|
+
var f = v.toString()
|
|
184
|
+
var m = [
|
|
185
|
+
...f.matchAll(/\((.*)\)(\s{|{)((.|\n)*)\}/g)
|
|
186
|
+
][0]
|
|
187
|
+
p[i] = {
|
|
188
|
+
name: i,
|
|
189
|
+
mouseevent: true,
|
|
190
|
+
code: m[3].trim(),
|
|
191
|
+
args: m[1].split(',').filter(v => v),
|
|
192
|
+
refs: ['state','meta']
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {type, props: p}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reduce to fewer object props before sending to client
|
|
204
|
+
*
|
|
205
|
+
* @param {string} name
|
|
206
|
+
*/
|
|
207
|
+
function reduce(args) {
|
|
208
|
+
if(!args) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
const {children} = args.data
|
|
212
|
+
if(children) {
|
|
213
|
+
args.data.children = reducer(children)[0]
|
|
214
|
+
}
|
|
215
|
+
return args
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get state
|
|
222
|
+
* @param {object} req
|
|
223
|
+
*/
|
|
224
|
+
function getState(req) {
|
|
225
|
+
const state = {
|
|
226
|
+
data: {},
|
|
227
|
+
type: 'initial',
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var token = req.get('x-state-request')
|
|
231
|
+
if(token) {
|
|
232
|
+
merge(state, decode(token))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if(state.type == 'fetch' || state.type == 'update') {
|
|
236
|
+
if(req.method == 'GET') {
|
|
237
|
+
merge(state.data, req.query)
|
|
238
|
+
}
|
|
239
|
+
if(req.method == 'POST') {
|
|
240
|
+
merge(state.data, req.body)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return state
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* State response
|
|
249
|
+
*
|
|
250
|
+
* @param {object} req
|
|
251
|
+
* @param {object} events
|
|
252
|
+
*/
|
|
253
|
+
function HTTPState(req, events) {
|
|
254
|
+
var exert = {}
|
|
255
|
+
var state = getState(req)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
state.on = function on(name, cb) {
|
|
259
|
+
events.on(mkId(name, req.name), async function(data) {
|
|
260
|
+
if(!cb) {
|
|
261
|
+
throw new ReferenceError('Callback function is required.')
|
|
262
|
+
}
|
|
263
|
+
return await cb(data)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
state.set = function set(data) {
|
|
268
|
+
merge(exert, data)
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Response for 'fetch' event from client
|
|
272
|
+
*/
|
|
273
|
+
state.get = function get(cb) {
|
|
274
|
+
state.on('GET', cb)
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Response for 'fetch' event from client
|
|
278
|
+
*/
|
|
279
|
+
state.post = function post(cb) {
|
|
280
|
+
state.on('POST', cb)
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Initial state
|
|
284
|
+
*/
|
|
285
|
+
state.use = async function use(initial = {}) {
|
|
286
|
+
if(isFunc(initial)) {
|
|
287
|
+
initial = await initial()
|
|
288
|
+
}
|
|
289
|
+
if(state.type == 'route' || state.type == 'initial') {
|
|
290
|
+
merge(state.data, initial)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Apply and update the current state
|
|
295
|
+
*/
|
|
296
|
+
state.apply = async function apply(cb) {
|
|
297
|
+
if(!isFunc(cb)) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
if(state.type == 'update') {
|
|
301
|
+
var data = await cb(state.data)
|
|
302
|
+
if(data) {
|
|
303
|
+
exert = data
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return new Proxy(state, {
|
|
309
|
+
get(target, key) {
|
|
310
|
+
if(target[key]) {
|
|
311
|
+
return target[key]
|
|
312
|
+
}
|
|
313
|
+
if(exert[key]) {
|
|
314
|
+
return exert[key]
|
|
315
|
+
}
|
|
316
|
+
return target.data[key]
|
|
317
|
+
},
|
|
318
|
+
set() {
|
|
319
|
+
throw new TypeError(`Cannot assign to read only object.`)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Prepare data
|
|
327
|
+
* @param {object} component
|
|
328
|
+
* @param {object} context
|
|
329
|
+
*/
|
|
330
|
+
function prepare(component, {meta, store, state}) {
|
|
331
|
+
const name = component.props.name ?? component.props.id
|
|
332
|
+
/**
|
|
333
|
+
* Metadata and component
|
|
334
|
+
*/
|
|
335
|
+
const {children, ...props} = component.props
|
|
336
|
+
const data = {
|
|
337
|
+
meta: {name, bundle: true, ...meta},
|
|
338
|
+
data: {
|
|
339
|
+
props,
|
|
340
|
+
children: isFunc(component.type) ? null : component
|
|
341
|
+
},
|
|
342
|
+
store
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get entry component
|
|
347
|
+
*/
|
|
348
|
+
var app = entry(data.meta)
|
|
349
|
+
return {
|
|
350
|
+
name,
|
|
351
|
+
data: merge(data, {state}),
|
|
352
|
+
html: html(
|
|
353
|
+
clone(app, {
|
|
354
|
+
children: app.props.children.map(dom(data))
|
|
355
|
+
})
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Middleware
|
|
363
|
+
*/
|
|
364
|
+
exports.server = function server() {
|
|
365
|
+
var data = {}
|
|
366
|
+
// TODO: Move to more reliable storage
|
|
367
|
+
var store = {}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Persist data
|
|
372
|
+
*/
|
|
373
|
+
function persist(type, {action, data}) {
|
|
374
|
+
if(type == 'store') {
|
|
375
|
+
switch(action) {
|
|
376
|
+
case 'clear':
|
|
377
|
+
return {}
|
|
378
|
+
case 'remove':
|
|
379
|
+
delete store[data]
|
|
380
|
+
default:
|
|
381
|
+
// TODO: Limit adding data
|
|
382
|
+
return merge(store, data)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return store
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
return function(req, res, next, {meta, events, exception}) {
|
|
390
|
+
const state = HTTPState(req, events)
|
|
391
|
+
const store = persist(state.type, req.body)
|
|
392
|
+
/**
|
|
393
|
+
* Emit state request event
|
|
394
|
+
*/
|
|
395
|
+
async function emit(name) {
|
|
396
|
+
return await events.emit(mkId(name, state.page), state.data)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Dispatch data and clear
|
|
401
|
+
*/
|
|
402
|
+
async function dispatch(obj = {}) {
|
|
403
|
+
data = {}
|
|
404
|
+
|
|
405
|
+
if(state.type == 'fetch') {
|
|
406
|
+
return res.json(await emit(req.method))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if(obj.data) {
|
|
410
|
+
return res.json(reduce(obj))
|
|
411
|
+
}
|
|
412
|
+
res.json(obj)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Render on first request
|
|
417
|
+
*/
|
|
418
|
+
events.on('__render', function(comp) {
|
|
419
|
+
if(isValid(comp)) {
|
|
420
|
+
var args = prepare(comp, {
|
|
421
|
+
meta,
|
|
422
|
+
store,
|
|
423
|
+
state: state.data,
|
|
424
|
+
})
|
|
425
|
+
/**
|
|
426
|
+
* Initial content
|
|
427
|
+
*/
|
|
428
|
+
if(state.type == 'initial') {
|
|
429
|
+
data[manif.hash] = args.data
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Update content
|
|
433
|
+
*/
|
|
434
|
+
if(state.type == 'route' || state.type == 'update' || state.type == 'store') {
|
|
435
|
+
return dispatch(args.data)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* If the request not matched.
|
|
440
|
+
*/
|
|
441
|
+
const errors = Object.keys(exception.statuses).concat('error')
|
|
442
|
+
if(args.name && req.name) {
|
|
443
|
+
if(req.route.back && args.name !== req.name && !errors.includes(args.name)) {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return args
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
if(req.is(manif.hash)) {
|
|
452
|
+
return dispatch(data[manif.hash])
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
next({state, store})
|
|
456
|
+
}
|
|
457
|
+
}
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import runtime from 'react/jsx-runtime'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const str = JSON.stringify
|
|
5
|
+
export const merge = Object.assign
|
|
6
|
+
export const isArr = Array.isArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export function isEmpty(obj) {
|
|
10
|
+
if(obj && Object.keys(obj).length == 0) {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isMore(val) {
|
|
17
|
+
return isObj(val) || val == undefined ? 'jsx' : 'jsxs'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isNum(val) {
|
|
21
|
+
return val && typeof val === 'number' && {}.toString.call(val) === '[object Number]'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isStr(val) {
|
|
25
|
+
return val && typeof val === 'string' && {}.toString.call(val) === '[object String]'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isFunc(val) {
|
|
29
|
+
return val && typeof val === 'function' && {}.toString.call(val) === '[object Function]'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isObj(val) {
|
|
33
|
+
return val && typeof val === 'object' && val.constructor === Object && Object.prototype === Object.getPrototypeOf(val)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toFunc(name, {code, args = [], refs}) {
|
|
37
|
+
var arr = ['return', 'function', name]
|
|
38
|
+
|
|
39
|
+
if(args) {
|
|
40
|
+
arr.push(
|
|
41
|
+
`(${args.length ? args.join(',') : ''})`
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
if(typeof code == 'string') {
|
|
45
|
+
arr.push(`{${code}}`)
|
|
46
|
+
}
|
|
47
|
+
return new Function(...Object.keys(refs), arr.join(' '))(...Object.values(refs))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Transform back to react object
|
|
53
|
+
*/
|
|
54
|
+
export function transform(children, data) {
|
|
55
|
+
if(!children) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if(!isArr(children)) {
|
|
59
|
+
children = [children]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return children.map(({type, props}, key) => {
|
|
63
|
+
var p = {}
|
|
64
|
+
|
|
65
|
+
if(!props) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if(isArr(type)) {
|
|
69
|
+
type = data[type[0]]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for(var i in props) {
|
|
73
|
+
var v = props[i]
|
|
74
|
+
|
|
75
|
+
if(isStr(v) || isNum(v)) {
|
|
76
|
+
p[i] = v
|
|
77
|
+
}
|
|
78
|
+
if(isObj(v)) {
|
|
79
|
+
if(i == 'style') {
|
|
80
|
+
p.style = v
|
|
81
|
+
}
|
|
82
|
+
if(i == 'children') {
|
|
83
|
+
p.children = transform(v, data)
|
|
84
|
+
}
|
|
85
|
+
if(v.mouseevent) {
|
|
86
|
+
p[i] = toFunc(v.name, {
|
|
87
|
+
args: v.args,
|
|
88
|
+
code: v.code,
|
|
89
|
+
refs: data.refs
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if(isArr(v)) {
|
|
94
|
+
p.children = v.map((i) => {
|
|
95
|
+
if(isObj(i)) {
|
|
96
|
+
return transform(i, data)
|
|
97
|
+
}
|
|
98
|
+
return i
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return runtime.jsx(type, p, key)
|
|
104
|
+
})
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vindo/react",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "React SSR",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "React SSR for @vindo/core framework.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -17,6 +17,24 @@
|
|
|
17
17
|
"keywords": [
|
|
18
18
|
"react"
|
|
19
19
|
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "*",
|
|
22
|
+
"react-dom": "*"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"default": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"./util": {
|
|
29
|
+
"default": "./lib/util.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"default": "./lib/client.js"
|
|
33
|
+
},
|
|
34
|
+
"./request": {
|
|
35
|
+
"default": "./lib/request.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
20
38
|
"author": "Ruel Mindo",
|
|
21
39
|
"license": "MIT",
|
|
22
40
|
"dependencies": {}
|
package/client.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @vindo/react
|
|
3
|
-
* Copyright(c) 2025 Ruel Mindo
|
|
4
|
-
* MIT Licensed
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use strict'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const React = require('node_modules/react')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Http Request
|
|
16
|
-
*/
|
|
17
|
-
function request({path, data}, opts = {}) {
|
|
18
|
-
if(data && typeof data !== 'object') {
|
|
19
|
-
throw TypeError(`Invalid type of 'data'. Expected value of type 'object' but got ${typeof data}'.`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
var uuid = window.crypto.randomUUID()
|
|
23
|
-
var opts = {
|
|
24
|
-
...opts,
|
|
25
|
-
headers: {
|
|
26
|
-
...opts.headers,
|
|
27
|
-
'X-Fetch-Request-Token': uuid
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
var url = new URL(location.href)
|
|
32
|
-
if(path) {
|
|
33
|
-
url = new URL(path, location.origin)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if(opts.method == 'GET') {
|
|
37
|
-
if(data) {
|
|
38
|
-
for(var i in data) {
|
|
39
|
-
url.searchParams.append(i, data[i])
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return fetch(url, opts).then((res) => {
|
|
45
|
-
return res.headers.get('X-Fetch-Response') == uuid && res.json()
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Http Request
|
|
53
|
-
*/
|
|
54
|
-
exports.http = {
|
|
55
|
-
get(args = {}) {
|
|
56
|
-
return request(args, {...args.headers, method: 'GET'})
|
|
57
|
-
},
|
|
58
|
-
post(args = {}) {
|
|
59
|
-
return request(args, {
|
|
60
|
-
body: JSON.stringify(args.data ?? {}),
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: {
|
|
63
|
-
...args.headers,
|
|
64
|
-
'Content-Type': 'application/json'
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* HTML Holder
|
|
73
|
-
*/
|
|
74
|
-
exports.View = function View({children}) {
|
|
75
|
-
return children
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Find current route
|
|
80
|
-
*/
|
|
81
|
-
exports.HydrateContent = function HydrateContent({data, meta, ...props}) {
|
|
82
|
-
|
|
83
|
-
if(data && React.isValidElement(data.content)) {
|
|
84
|
-
return React.createElement('main', props, data.content)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return React.createElement('main', props, React.Children.map(props.children, (child) => {
|
|
88
|
-
if(!data || !child.props.name) {
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
if(meta.name == child.props.name) {
|
|
92
|
-
return child.props.component(data.content)
|
|
93
|
-
}
|
|
94
|
-
}))
|
|
95
|
-
}
|