@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 CHANGED
@@ -1,12 +1,12 @@
1
- ## Cross-origin (CORS)
2
- Resource sharing restriction for Web and API
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/cors
7
+ npm install @vindo/react
8
8
  ```
9
9
  Or
10
10
  ```
11
- yarn add @vindo/cors
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
- export const http:any;
11
- export function View(props): any;
12
- export function Hydrate(props): any;
13
- export function HydrateContent(props): any;
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) 2025 Ruel Mindo
3
+ * Copyright(c) 2023 Ruel Mindo
4
4
  * MIT Licensed
5
5
  */
6
6
 
7
- 'use strict'
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.1",
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
- }