@rettangoli/fe 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 +324 -0
- package/package.json +37 -0
- package/src/common.js +203 -0
- package/src/commonBuild.js +22 -0
- package/src/createComponent.js +486 -0
- package/src/createWebPatch.js +18 -0
- package/src/index.js +7 -0
- package/src/parser.js +399 -0
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
|
|
2
|
+
# Rettangoli Frontend
|
|
3
|
+
|
|
4
|
+
## Development
|
|
5
|
+
|
|
6
|
+
Bundle the code under `example` folder using `rettangoli-fe`
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bun run ../rettangoli-cli/cli.js fe build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Visit the example project that shows the components in action
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bunx serve ./viz/static
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Instead of running `fe build` each time, we can also watch for file changes and bundle the code automatically.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun run ../rettangoli-cli/cli.js fe watch
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Note: rettangoli-vt is not setup for this project yet. We just use static files under `viz/static` folder.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Introduction
|
|
29
|
+
|
|
30
|
+
A frontend framework with just 3 type of files to scale from a single page to a full fledged complex application.
|
|
31
|
+
|
|
32
|
+
Implemented using:
|
|
33
|
+
|
|
34
|
+
Browser native:
|
|
35
|
+
* web components for components
|
|
36
|
+
|
|
37
|
+
Runtime:
|
|
38
|
+
* [snabbdom](https://github.com/snabbdom/snabbdom) for virtual dom
|
|
39
|
+
* [immer](https://github.com/immerjs/immer) for data immutability for state management
|
|
40
|
+
* [json-e](https://github.com/json-e/json-e) for templating using json
|
|
41
|
+
* [rxjs](https://github.com/ReactiveX/rxjs) for reactive programming
|
|
42
|
+
|
|
43
|
+
Build & Development:
|
|
44
|
+
* [esbuild](https://esbuild.github.io/) for bundling
|
|
45
|
+
* [vite](https://vite.dev/) for development
|
|
46
|
+
|
|
47
|
+
## View Layer
|
|
48
|
+
|
|
49
|
+
The view layer is unique in that it uses yaml.
|
|
50
|
+
|
|
51
|
+
* The yaml will be converted into json at build time. The json will then be consumed by snabbdom to be transformed into html through a virtual dom.
|
|
52
|
+
|
|
53
|
+
### Yaml to write html
|
|
54
|
+
|
|
55
|
+
Standard html can be totally written in yaml.
|
|
56
|
+
|
|
57
|
+
All child element are arrays. Except for things like actual content of text
|
|
58
|
+
|
|
59
|
+
Use `#` and `.` selectors to represent `id` and `class`.
|
|
60
|
+
|
|
61
|
+
`div#myid.class1.class2 custorm-attribute=abcd`
|
|
62
|
+
|
|
63
|
+
will become
|
|
64
|
+
|
|
65
|
+
`<div id="myid" class="class1 class2" custom-attribute="abcd"></div>`
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### Templating using json-e
|
|
69
|
+
|
|
70
|
+
`json-e` templating language allows us to write conditions and loops in our yaml. For example:
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Loop
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
template:
|
|
77
|
+
- rtgl-view w=f g=m:
|
|
78
|
+
- $map: { $eval: projects }
|
|
79
|
+
each(v,k):
|
|
80
|
+
- rtgl-view#project-${v.id} h=64 w=f bw=xs p=m cur=p:
|
|
81
|
+
- rtgl-text s=lg: "${v.name}"
|
|
82
|
+
- rtgl-text s=sm: "${v.description}"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Conditional. We usually use `$switch` more as it tends to be more flexible than `$if`.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
template:
|
|
89
|
+
- rtgl-view d=h w=f h=f:
|
|
90
|
+
- $switch:
|
|
91
|
+
'showSidebar':
|
|
92
|
+
- sidebar-component: []
|
|
93
|
+
- rtgl-view w=f h=f:
|
|
94
|
+
- $switch:
|
|
95
|
+
'currentRoute== "/projects"':
|
|
96
|
+
- projects-component: []
|
|
97
|
+
'currentRoute== "/profile"':
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`json-e` has many more features but we want to keep it simple and in most cases loops and conditionals are enough.
|
|
101
|
+
|
|
102
|
+
The actual data used in the template is passed in as `viewData` from the state store which we will cover later.
|
|
103
|
+
|
|
104
|
+
### Define a elementName
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
elementName: custom-projects
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This will be the web component name that will be used for the component.
|
|
111
|
+
|
|
112
|
+
The component can later be used as `<custom-projects></custom-projects>`.
|
|
113
|
+
|
|
114
|
+
### Styles
|
|
115
|
+
|
|
116
|
+
Styles can also be completely written in yaml.
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
styles:
|
|
120
|
+
'#title':
|
|
121
|
+
font-size: 24px
|
|
122
|
+
'@media (min-width: 768px)':
|
|
123
|
+
'#title':
|
|
124
|
+
font-size: 32px
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
TODO better support nesting and issue with some global selectors.
|
|
128
|
+
|
|
129
|
+
### Event listeners
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
refs:
|
|
133
|
+
createButton:
|
|
134
|
+
eventListeners:
|
|
135
|
+
click:
|
|
136
|
+
handler: handleCreateButtonClick
|
|
137
|
+
project-*:
|
|
138
|
+
eventListeners:
|
|
139
|
+
click:
|
|
140
|
+
handler: handleProjectsClick
|
|
141
|
+
|
|
142
|
+
template:
|
|
143
|
+
- rtgl-button#createButton: Create Project
|
|
144
|
+
- rtgl-view w=f g=m:
|
|
145
|
+
- $map: { $eval: projects }
|
|
146
|
+
each(v,k):
|
|
147
|
+
- rtgl-view#project-${v.id} h=64 w=f bw=xs p=m cur=p:
|
|
148
|
+
- rtgl-text s=lg: "${v.name}"
|
|
149
|
+
- rtgl-text s=sm: "${v.description}"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The above example, will attach event listenrs to `#createButton` and all `#project-*` (wild card support) elements. And bind them to the handlers `handleCreateButtonClick` and `handleProjectsClick`.
|
|
153
|
+
|
|
154
|
+
### Defining data schema
|
|
155
|
+
|
|
156
|
+
Component have a few types of data that can be defined using a JSON schema:
|
|
157
|
+
|
|
158
|
+
* `viewDataSchema` - The data that will used for the template.
|
|
159
|
+
* `propsSchema` - The data that will be passed to the component via javascript, those can be objects.
|
|
160
|
+
* `attrsSchema` - The data that will be passed to the component via html attributes, this is raw strings.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
viewDataSchema:
|
|
165
|
+
type: object
|
|
166
|
+
properties:
|
|
167
|
+
title:
|
|
168
|
+
type: string
|
|
169
|
+
default: Projects
|
|
170
|
+
createButtonText:
|
|
171
|
+
type: string
|
|
172
|
+
default: Create Project
|
|
173
|
+
projects:
|
|
174
|
+
type: array
|
|
175
|
+
items:
|
|
176
|
+
type: object
|
|
177
|
+
properties:
|
|
178
|
+
id:
|
|
179
|
+
type: string
|
|
180
|
+
name:
|
|
181
|
+
type: string
|
|
182
|
+
default: Project 1
|
|
183
|
+
description:
|
|
184
|
+
type: string
|
|
185
|
+
default: Project 1 description
|
|
186
|
+
propsSchema:
|
|
187
|
+
type: object
|
|
188
|
+
properties: {}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## State Store
|
|
193
|
+
|
|
194
|
+
* Define `initial state`
|
|
195
|
+
* `toViewData` will take current `state`, `props` and `attrs` and return the `viewData` to be used by the view template
|
|
196
|
+
* Any exported function that starts with `select` will beceme selectors and are used by handlers to access state data
|
|
197
|
+
* `actions` are all other exported functions that are used to mutate the state.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
export const INITIAL_STATE = Object.freeze({
|
|
201
|
+
title: "Projects",
|
|
202
|
+
createButtonText: "Create Project",
|
|
203
|
+
projects: [
|
|
204
|
+
{
|
|
205
|
+
id: "1",
|
|
206
|
+
name: "Project 1",
|
|
207
|
+
description: "Project 1 description",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: '2',
|
|
211
|
+
name: 'Project 2',
|
|
212
|
+
description: 'Project 2 description'
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export const toViewData = ({ state, props }, payload) => {
|
|
218
|
+
return state;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const selectProjects = (state, props, payload) => {
|
|
222
|
+
return state.projects;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const setProjects = (state, payload) => {
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Note that this is just a dump store, it is not reactive. Components will need to call `deps.render()` from handlers to re-render the component.
|
|
231
|
+
|
|
232
|
+
## Handlers
|
|
233
|
+
|
|
234
|
+
`handleOnMount` is a special handler that is called when the component is mounted. It returns a promise that resolves when the component is mounted.
|
|
235
|
+
|
|
236
|
+
All other exported functions will automatically become handlers and can be used in the view layers's `eventListeners`
|
|
237
|
+
|
|
238
|
+
A special object called `deps` is injected into all handlers. It has the following properties:
|
|
239
|
+
|
|
240
|
+
* `deps.render()` will re-render the component. We call this function each time we have chaged state and want to re-render the component.
|
|
241
|
+
* `deps.store` is the store instance. Use selectors to select state and actions to mutate state.
|
|
242
|
+
* `deps.transformedHandlers` can be used to call other handlers.
|
|
243
|
+
* `deps.attrs` is the html attributes that are passed to the component.
|
|
244
|
+
* `deps.props` is the javascript properties that are passed to the component.
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
export const handleOnMount = (deps) => {
|
|
249
|
+
() => {
|
|
250
|
+
// unsubscribe
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const handleCreateButtonClick = async (e, deps) => {
|
|
255
|
+
const { store, deps, render } = deps;
|
|
256
|
+
const formIsVisible = store.selectFormIsVisible();
|
|
257
|
+
|
|
258
|
+
if (!formIsVisible) {
|
|
259
|
+
store.setFormIsVisible(true);
|
|
260
|
+
}
|
|
261
|
+
deps.render();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const handleProjectsClick = (e, deps) => {
|
|
265
|
+
const id = e.target.id
|
|
266
|
+
console.log('handleProjectsClick', id);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
* `deps.dispatchEvent` can be used to dispatch custom dom events.
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
export const handleProjectsClick = (e, deps) => {
|
|
276
|
+
deps.dispatchEvent(new CustomEvent('project-clicked', {
|
|
277
|
+
projectId: '1',
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
### Adding additional dependencies
|
|
284
|
+
|
|
285
|
+
This is a simple yet powerful way to do dependency injection. Those are all global singleton dependencies. Technically anything can be injected and be made accessible to all components.
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
const componentDependencies = {
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const pageDependencies = {
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const deps = {
|
|
295
|
+
components: componentDependencies,
|
|
296
|
+
pages: pageDependencies,
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
## Testing
|
|
302
|
+
|
|
303
|
+
This framework is written with testability in mind.
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## View
|
|
307
|
+
|
|
308
|
+
Visual testing `rettangoli-vt`
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
## State Store
|
|
312
|
+
|
|
313
|
+
Those are all pure functions and it is straighforward to test them. Actions can be turned into pure functions using immer produce.
|
|
314
|
+
|
|
315
|
+
Example
|
|
316
|
+
|
|
317
|
+
```yaml
|
|
318
|
+
...
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Handlers
|
|
322
|
+
|
|
323
|
+
Test them as normal functions.
|
|
324
|
+
They are not always pure per se due to calling of dependencies.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rettangoli/fe",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Frontend framework for building reactive web components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"keywords": ["frontend", "reactive", "components", "web", "framework"],
|
|
8
|
+
"files": [
|
|
9
|
+
"src/*.js",
|
|
10
|
+
"src/!(cli)",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/yuusoft-org/rettangoli.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.js",
|
|
21
|
+
"./cli": "./src/cli/index.js"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"js-yaml": "^4.1.0",
|
|
25
|
+
"esbuild": "^0.25.4",
|
|
26
|
+
"vite": "^6.3.5"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"immer": "^10.1.1",
|
|
30
|
+
"jempl": "^0.0.6",
|
|
31
|
+
"rxjs": "^7.8.2",
|
|
32
|
+
"snabbdom": "^3.6.2"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "node watch.js --watch"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/common.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Subject } from "rxjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A custom subject that can be used to dispatch actions and subscribe to them
|
|
5
|
+
* You can think of this as a bus for all frontend events and communication
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* const subject = new CustomSubject();
|
|
9
|
+
*
|
|
10
|
+
* const subscription = subject.subscribe(({ action, payload }) => {
|
|
11
|
+
* console.log(action, payload);
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* subject.dispatch("action", { payload: "payload" });
|
|
15
|
+
*
|
|
16
|
+
* subscription.unsubscribe();
|
|
17
|
+
*/
|
|
18
|
+
export class CustomSubject {
|
|
19
|
+
_subject = new Subject();
|
|
20
|
+
pipe = (...args) => {
|
|
21
|
+
return this._subject.pipe(...args);
|
|
22
|
+
};
|
|
23
|
+
dispatch = (action, payload) => {
|
|
24
|
+
this._subject.next({
|
|
25
|
+
action,
|
|
26
|
+
payload: payload || {},
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
dispatchCall = (action, payload) => {
|
|
30
|
+
return () => this.dispatch(action, payload || {});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getQueryParamsObject = () => {
|
|
35
|
+
const queryParams = new URLSearchParams(window.location.search + "");
|
|
36
|
+
const paramsObject = {};
|
|
37
|
+
|
|
38
|
+
for (const [key, value] of queryParams.entries()) {
|
|
39
|
+
paramsObject[key] = value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return paramsObject;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class LayoutOptions {
|
|
46
|
+
constructor(params) {
|
|
47
|
+
this._isTouchLayout = params?.isTouchLayout || false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get isTouchLayout() {
|
|
51
|
+
return this._isTouchLayout;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setIsTouchLayout = (isTouchLayout) => {
|
|
55
|
+
this._isTouchLayout = isTouchLayout;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const matchPaths = (path, pattern) => {
|
|
60
|
+
// Normalize paths by removing trailing slashes
|
|
61
|
+
const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
62
|
+
const normalizedPattern = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
63
|
+
|
|
64
|
+
// Convert pattern segments with parameters like [id] to regex patterns
|
|
65
|
+
const regexPattern = normalizedPattern
|
|
66
|
+
.split("/")
|
|
67
|
+
.map((segment) => {
|
|
68
|
+
// Check if segment is a parameter (enclosed in square brackets)
|
|
69
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
70
|
+
// Extract parameter name and create a capturing group
|
|
71
|
+
return "([^/]+)";
|
|
72
|
+
}
|
|
73
|
+
// Regular segment, match exactly
|
|
74
|
+
return segment;
|
|
75
|
+
})
|
|
76
|
+
.join("/");
|
|
77
|
+
|
|
78
|
+
// Create regex with start and end anchors
|
|
79
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
80
|
+
|
|
81
|
+
// Test if path matches the pattern
|
|
82
|
+
return regex.test(normalizedPath);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Request {
|
|
87
|
+
_authToken;
|
|
88
|
+
|
|
89
|
+
constructor(baseUrl, authToken, headers) {
|
|
90
|
+
this._baseUrl = baseUrl;
|
|
91
|
+
this._authToken = authToken;
|
|
92
|
+
this._headers = headers;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setAuthToken(token) {
|
|
96
|
+
this._authToken = token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async request(name, payload, options) {
|
|
100
|
+
const headers = {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
...this._headers,
|
|
103
|
+
};
|
|
104
|
+
if (this._authToken) {
|
|
105
|
+
headers.Authorization = `Bearer ${this._authToken}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await fetch(`${this._baseUrl}/${name}`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: JSON.stringify(payload),
|
|
111
|
+
headers,
|
|
112
|
+
credentials: options?.includeCredentials ? "include" : undefined,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return response.json();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {Object} ApiEndpointConfig
|
|
121
|
+
* @property {boolean} [includeCredentials] - Whether to include credentials in the request
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates an HTTP client with a flexible JSON configuration.
|
|
126
|
+
* The generated client provides typed methods for each API endpoint based on the configuration.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} config - The client configuration
|
|
129
|
+
* @param {string} config.baseUrl - Base URL for all API requests
|
|
130
|
+
* @param {Object} [config.headers={}] - Default headers for all requests
|
|
131
|
+
* @param {Object.<string, Object.<string, ApiEndpointConfig>>} [config.apis={}] - API configuration object
|
|
132
|
+
* @returns {Object} The configured HTTP client with API methods
|
|
133
|
+
*/
|
|
134
|
+
export function createHttpClient(config) {
|
|
135
|
+
const { baseUrl, apis = {}, headers = {} } = config;
|
|
136
|
+
const requests = new Map();
|
|
137
|
+
|
|
138
|
+
const httpClient = {};
|
|
139
|
+
|
|
140
|
+
// Create request instances and client structure from configuration
|
|
141
|
+
Object.entries(apis).forEach(([apiName, endpoints]) => {
|
|
142
|
+
const apiBaseUrl = `${baseUrl}/${apiName}`;
|
|
143
|
+
const request = new Request(apiBaseUrl, undefined, headers);
|
|
144
|
+
requests.set(apiName, request);
|
|
145
|
+
|
|
146
|
+
// Create API namespace on the client
|
|
147
|
+
httpClient[apiName] = {};
|
|
148
|
+
|
|
149
|
+
// Create methods for each endpoint
|
|
150
|
+
Object.entries(endpoints).forEach(([endpointName, options]) => {
|
|
151
|
+
httpClient[apiName][endpointName] = (body) => {
|
|
152
|
+
return request.request(endpointName, body, options);
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Add setAuthToken method to the client
|
|
158
|
+
httpClient.setAuthToken = (token) => {
|
|
159
|
+
for (const request of requests.values()) {
|
|
160
|
+
request.setAuthToken(token);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return httpClient;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
export const extractCategoryAndComponent = (filePath) => {
|
|
169
|
+
const parts = filePath.split("/");
|
|
170
|
+
const component = parts[parts.length - 1].split(".")[0];
|
|
171
|
+
const category = parts[parts.length - 3];
|
|
172
|
+
const fileType = parts[parts.length - 1].split(".")[1];
|
|
173
|
+
return { category, component, fileType };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// Helper function to flatten arrays while preserving object structure
|
|
179
|
+
export const flattenArrays = (items) => {
|
|
180
|
+
if (!Array.isArray(items)) {
|
|
181
|
+
return items;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return items.reduce((acc, item) => {
|
|
185
|
+
if (Array.isArray(item)) {
|
|
186
|
+
// Recursively flatten nested arrays
|
|
187
|
+
acc.push(...flattenArrays(item));
|
|
188
|
+
} else {
|
|
189
|
+
// If it's an object with nested arrays, process those too
|
|
190
|
+
if (item && typeof item === "object") {
|
|
191
|
+
const entries = Object.entries(item);
|
|
192
|
+
if (entries.length > 0) {
|
|
193
|
+
const [key, value] = entries[0];
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
item = { [key]: flattenArrays(value) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
acc.push(item);
|
|
200
|
+
}
|
|
201
|
+
return acc;
|
|
202
|
+
}, []);
|
|
203
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Function to recursively get all files in a directory
|
|
7
|
+
export function getAllFiles(dirPaths, arrayOfFiles = []) {
|
|
8
|
+
dirPaths.forEach((dirPath) => {
|
|
9
|
+
const files = readdirSync(dirPath);
|
|
10
|
+
|
|
11
|
+
files.forEach((file) => {
|
|
12
|
+
const fullPath = join(dirPath, file);
|
|
13
|
+
if (statSync(fullPath).isDirectory()) {
|
|
14
|
+
arrayOfFiles = getAllFiles([fullPath], arrayOfFiles);
|
|
15
|
+
} else {
|
|
16
|
+
arrayOfFiles.push(fullPath);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return arrayOfFiles;
|
|
22
|
+
}
|