xhtmlx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/package.json +62 -0
- package/xhtmlx.d.ts +351 -0
- package/xhtmlx.js +2825 -0
- package/xhtmlx.min.js +2 -0
- package/xhtmlx.min.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dkropachev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# xhtmlx
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency JavaScript library for building dynamic UIs with REST APIs using declarative HTML attributes.
|
|
4
|
+
|
|
5
|
+
Like [htmx](https://htmx.org), but instead of receiving HTML from the server, xhtmlx receives **JSON** and renders UI **client-side** using templates.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script src="xhtmlx.js"></script>
|
|
11
|
+
|
|
12
|
+
<div xh-get="/api/users" xh-trigger="load" xh-template="/templates/user-list.html">
|
|
13
|
+
<span class="xh-indicator">Loading...</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div id="results"></div>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<!-- /templates/user-list.html -->
|
|
20
|
+
<div xh-each="users">
|
|
21
|
+
<div class="user">
|
|
22
|
+
<span xh-text="name"></span>
|
|
23
|
+
<span xh-text="email"></span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Server returns:
|
|
29
|
+
```json
|
|
30
|
+
{ "users": [{ "name": "Alice", "email": "alice@example.com" }] }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
xhtmlx fetches the JSON, loads the template, renders it with the data, and swaps it into the DOM. No JavaScript needed.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Drop the script into your page:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<script src="xhtmlx.js"></script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No build step. No dependencies.
|
|
44
|
+
|
|
45
|
+
## API Reference
|
|
46
|
+
|
|
47
|
+
### REST Verbs
|
|
48
|
+
|
|
49
|
+
| Attribute | Description |
|
|
50
|
+
|-----------|-------------|
|
|
51
|
+
| `xh-get` | Issue a GET request to the URL |
|
|
52
|
+
| `xh-post` | Issue a POST request |
|
|
53
|
+
| `xh-put` | Issue a PUT request |
|
|
54
|
+
| `xh-delete` | Issue a DELETE request |
|
|
55
|
+
| `xh-patch` | Issue a PATCH request |
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<button xh-get="/api/users">Load Users</button>
|
|
59
|
+
<button xh-post="/api/users" xh-vals='{"name": "Bob"}'>Create User</button>
|
|
60
|
+
<button xh-delete="/api/users/{{id}}">Delete</button>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Templates
|
|
64
|
+
|
|
65
|
+
**External template file:**
|
|
66
|
+
```html
|
|
67
|
+
<div xh-get="/api/users" xh-template="/templates/user-list.html"></div>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Inline template:**
|
|
71
|
+
```html
|
|
72
|
+
<div xh-get="/api/users">
|
|
73
|
+
<template>
|
|
74
|
+
<span xh-text="name"></span>
|
|
75
|
+
</template>
|
|
76
|
+
</div>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Templates can contain xhtmlx attributes, enabling nested API calls and template composition:
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<!-- /templates/user-card.html -->
|
|
83
|
+
<div class="card">
|
|
84
|
+
<h2 xh-text="name"></h2>
|
|
85
|
+
<div xh-get="/api/users/{{id}}/posts"
|
|
86
|
+
xh-trigger="load"
|
|
87
|
+
xh-template="/templates/post-list.html">
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Data Binding
|
|
93
|
+
|
|
94
|
+
| Attribute | Description |
|
|
95
|
+
|-----------|-------------|
|
|
96
|
+
| `xh-text` | Set element's textContent from data field |
|
|
97
|
+
| `xh-html` | Set element's innerHTML from data field (use with caution) |
|
|
98
|
+
| `xh-attr-*` | Set any attribute from data field |
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<span xh-text="user.name"></span>
|
|
102
|
+
<div xh-html="user.bio"></div>
|
|
103
|
+
<img xh-attr-src="user.avatar" xh-attr-alt="user.name">
|
|
104
|
+
<a xh-attr-href="user.profile_url" xh-text="user.name"></a>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Iteration
|
|
108
|
+
|
|
109
|
+
`xh-each` repeats the element for each item in an array:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<ul>
|
|
113
|
+
<li xh-each="items">
|
|
114
|
+
<span xh-text="name"></span> - <span xh-text="price"></span>
|
|
115
|
+
</li>
|
|
116
|
+
</ul>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For data `{ "items": [{ "name": "A", "price": 10 }, { "name": "B", "price": 20 }] }`, this renders two `<li>` elements.
|
|
120
|
+
|
|
121
|
+
Access the iteration index with `$index`:
|
|
122
|
+
```html
|
|
123
|
+
<li xh-each="items">
|
|
124
|
+
<span xh-text="$index"></span>. <span xh-text="name"></span>
|
|
125
|
+
</li>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Conditionals
|
|
129
|
+
|
|
130
|
+
| Attribute | Description |
|
|
131
|
+
|-----------|-------------|
|
|
132
|
+
| `xh-if` | Render element only if field is truthy |
|
|
133
|
+
| `xh-unless` | Render element only if field is falsy |
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<span xh-if="is_admin" class="badge">Admin</span>
|
|
137
|
+
<span xh-unless="verified" class="warning">Unverified</span>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Triggers
|
|
141
|
+
|
|
142
|
+
`xh-trigger` specifies what event fires the request:
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<div xh-get="/api/data" xh-trigger="load">Auto-load on page load</div>
|
|
146
|
+
<input xh-get="/api/search" xh-trigger="keyup changed delay:300ms">
|
|
147
|
+
<div xh-get="/api/feed" xh-trigger="every 5s">Polling</div>
|
|
148
|
+
<div xh-get="/api/more" xh-trigger="revealed">Load when scrolled into view</div>
|
|
149
|
+
<button xh-get="/api/data" xh-trigger="click once">Load once</button>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Default triggers:**
|
|
153
|
+
- `click` — buttons, links, and general elements
|
|
154
|
+
- `submit` — forms
|
|
155
|
+
- `change` — inputs, selects, textareas
|
|
156
|
+
|
|
157
|
+
**Modifiers:**
|
|
158
|
+
- `once` — fire only once
|
|
159
|
+
- `changed` — only fire if value changed
|
|
160
|
+
- `delay:Nms` — debounce before firing
|
|
161
|
+
- `throttle:Nms` — throttle firing rate
|
|
162
|
+
- `from:selector` — listen on a different element
|
|
163
|
+
|
|
164
|
+
### Targeting
|
|
165
|
+
|
|
166
|
+
| Attribute | Description |
|
|
167
|
+
|-----------|-------------|
|
|
168
|
+
| `xh-target` | CSS selector for where to place the rendered result |
|
|
169
|
+
| `xh-swap` | How to insert the content |
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<button xh-get="/api/users" xh-target="#user-list" xh-swap="innerHTML">
|
|
173
|
+
Load Users
|
|
174
|
+
</button>
|
|
175
|
+
<div id="user-list"></div>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Swap modes:**
|
|
179
|
+
|
|
180
|
+
| Mode | Behavior |
|
|
181
|
+
|------|----------|
|
|
182
|
+
| `innerHTML` | Replace target's children (default) |
|
|
183
|
+
| `outerHTML` | Replace target itself |
|
|
184
|
+
| `beforeend` | Append inside target |
|
|
185
|
+
| `afterbegin` | Prepend inside target |
|
|
186
|
+
| `beforebegin` | Insert before target |
|
|
187
|
+
| `afterend` | Insert after target |
|
|
188
|
+
| `delete` | Remove target |
|
|
189
|
+
| `none` | Don't swap (fire-and-forget) |
|
|
190
|
+
|
|
191
|
+
### URL Interpolation
|
|
192
|
+
|
|
193
|
+
Use `{{field}}` in URLs to insert values from the current data context:
|
|
194
|
+
|
|
195
|
+
```html
|
|
196
|
+
<div xh-each="users">
|
|
197
|
+
<button xh-get="/api/users/{{id}}/profile"
|
|
198
|
+
xh-template="/templates/profile.html">
|
|
199
|
+
View Profile
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Supports dot notation: `{{user.address.city}}`
|
|
205
|
+
|
|
206
|
+
### Indicators
|
|
207
|
+
|
|
208
|
+
Show a loading element while a request is in-flight:
|
|
209
|
+
|
|
210
|
+
```html
|
|
211
|
+
<button xh-get="/api/data" xh-indicator="#spinner">Load</button>
|
|
212
|
+
<span id="spinner" class="xh-indicator">Loading...</span>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The library adds the `xh-request` class to the indicator during requests. Default CSS is injected automatically to show/hide `.xh-indicator` elements.
|
|
216
|
+
|
|
217
|
+
### Request Data
|
|
218
|
+
|
|
219
|
+
| Attribute | Description |
|
|
220
|
+
|-----------|-------------|
|
|
221
|
+
| `xh-vals` | JSON string of values to send with the request |
|
|
222
|
+
| `xh-headers` | JSON string of custom headers |
|
|
223
|
+
|
|
224
|
+
```html
|
|
225
|
+
<button xh-post="/api/users"
|
|
226
|
+
xh-vals='{"name": "Alice", "role": "admin"}'
|
|
227
|
+
xh-headers='{"X-Custom": "value"}'>
|
|
228
|
+
Create User
|
|
229
|
+
</button>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For forms, form fields are automatically serialized:
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
<form xh-post="/api/users" xh-template="/templates/success.html">
|
|
236
|
+
<input name="name" type="text">
|
|
237
|
+
<input name="email" type="email">
|
|
238
|
+
<button type="submit">Create</button>
|
|
239
|
+
</form>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Error Handling
|
|
243
|
+
|
|
244
|
+
Specify templates for error responses:
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<div xh-get="/api/users"
|
|
248
|
+
xh-template="/templates/user-list.html"
|
|
249
|
+
xh-error-template="/templates/error.html"
|
|
250
|
+
xh-error-template-404="/templates/not-found.html"
|
|
251
|
+
xh-error-template-4xx="/templates/client-error.html"
|
|
252
|
+
xh-error-target="#error-area">
|
|
253
|
+
</div>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Resolution order:**
|
|
257
|
+
1. `xh-error-template-{exact code}` on the element (e.g. `xh-error-template-404`)
|
|
258
|
+
2. `xh-error-template-{class}` on the element (e.g. `xh-error-template-4xx`)
|
|
259
|
+
3. `xh-error-template` on the element (generic fallback)
|
|
260
|
+
4. Nearest ancestor `xh-error-boundary` (see below)
|
|
261
|
+
5. `xhtmlx.config.defaultErrorTemplate` (global fallback)
|
|
262
|
+
6. No template: adds `xh-error` CSS class and emits `xh:responseError` event
|
|
263
|
+
|
|
264
|
+
#### Error Boundaries
|
|
265
|
+
|
|
266
|
+
Wrap a section of your page with `xh-error-boundary` to catch errors from any child widget that doesn't have its own error template:
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<div xh-error-boundary
|
|
270
|
+
xh-error-template="/templates/error.html"
|
|
271
|
+
xh-error-target="#section-errors">
|
|
272
|
+
<div id="section-errors"></div>
|
|
273
|
+
|
|
274
|
+
<!-- If this fails and has no error template, the boundary catches it -->
|
|
275
|
+
<div xh-get="/api/widget-a" xh-trigger="load">
|
|
276
|
+
<template><span xh-text="data"></span></template>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<!-- This has its own error template, so the boundary is skipped -->
|
|
280
|
+
<div xh-get="/api/widget-b" xh-trigger="load"
|
|
281
|
+
xh-error-template="/templates/widget-error.html">
|
|
282
|
+
<template><span xh-text="data"></span></template>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Boundaries support the same status-specific attributes: `xh-error-template-404`, `xh-error-template-4xx`, etc.
|
|
288
|
+
|
|
289
|
+
Boundaries nest — the nearest ancestor boundary catches the error:
|
|
290
|
+
|
|
291
|
+
```html
|
|
292
|
+
<div xh-error-boundary xh-error-template="/templates/page-error.html">
|
|
293
|
+
<div xh-error-boundary xh-error-template="/templates/section-error.html">
|
|
294
|
+
<!-- Errors here go to section-error, not page-error -->
|
|
295
|
+
<div xh-get="/api/data" xh-trigger="load">...</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Global Error Config
|
|
301
|
+
|
|
302
|
+
Set a page-wide default for widgets without any error handling:
|
|
303
|
+
|
|
304
|
+
```html
|
|
305
|
+
<script>
|
|
306
|
+
xhtmlx.config.defaultErrorTemplate = "/templates/error.html";
|
|
307
|
+
xhtmlx.config.defaultErrorTarget = "#global-error";
|
|
308
|
+
</script>
|
|
309
|
+
<div id="global-error"></div>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Any widget that errors without an element-level template or boundary will use this global fallback.
|
|
313
|
+
|
|
314
|
+
**Error data context:**
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"status": 422,
|
|
318
|
+
"statusText": "Unprocessable Entity",
|
|
319
|
+
"body": { "error": "validation_failed", "fields": [...] }
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Use it in templates like any other data:
|
|
324
|
+
```html
|
|
325
|
+
<!-- /templates/validation-error.html -->
|
|
326
|
+
<div class="error">
|
|
327
|
+
<h3>Error <span xh-text="status"></span></h3>
|
|
328
|
+
<ul xh-each="body.fields">
|
|
329
|
+
<li><strong xh-text="name"></strong>: <span xh-text="message"></span></li>
|
|
330
|
+
</ul>
|
|
331
|
+
</div>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Events
|
|
335
|
+
|
|
336
|
+
xhtmlx emits custom DOM events for programmatic control:
|
|
337
|
+
|
|
338
|
+
| Event | When | Cancelable |
|
|
339
|
+
|-------|------|------------|
|
|
340
|
+
| `xh:beforeRequest` | Before fetch fires | Yes |
|
|
341
|
+
| `xh:afterRequest` | After fetch completes | No |
|
|
342
|
+
| `xh:beforeSwap` | Before DOM swap | Yes |
|
|
343
|
+
| `xh:afterSwap` | After DOM swap | No |
|
|
344
|
+
| `xh:responseError` | On HTTP error response | No |
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
document.body.addEventListener("xh:responseError", function(e) {
|
|
348
|
+
console.log(e.detail.status);
|
|
349
|
+
console.log(e.detail.body);
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Data Context
|
|
354
|
+
|
|
355
|
+
Data flows through nested templates via a context chain. Child templates can access parent data:
|
|
356
|
+
|
|
357
|
+
```html
|
|
358
|
+
<!-- Parent: fetches user -->
|
|
359
|
+
<div xh-get="/api/users/1" xh-trigger="load" xh-template="/templates/user.html"></div>
|
|
360
|
+
|
|
361
|
+
<!-- /templates/user.html: can access user fields, fetches posts -->
|
|
362
|
+
<h1 xh-text="name"></h1>
|
|
363
|
+
<div xh-get="/api/users/{{id}}/posts" xh-trigger="load">
|
|
364
|
+
<template>
|
|
365
|
+
<!-- Each post can access its own fields AND parent user fields via $parent -->
|
|
366
|
+
<div xh-each="posts">
|
|
367
|
+
<p><span xh-text="title"></span> by <span xh-text="$parent.name"></span></p>
|
|
368
|
+
</div>
|
|
369
|
+
</template>
|
|
370
|
+
</div>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Special variables:**
|
|
374
|
+
- `$index` — current iteration index (inside `xh-each`)
|
|
375
|
+
- `$parent` — parent data context
|
|
376
|
+
- `$root` — topmost data context
|
|
377
|
+
|
|
378
|
+
## Browser Support
|
|
379
|
+
|
|
380
|
+
xhtmlx uses `fetch()`, `Promise`, `WeakMap`, and `IntersectionObserver`. Works in all modern browsers (Chrome, Firefox, Safari, Edge). No IE support.
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xhtmlx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Declarative HTML attributes for REST API driven UIs. Like htmx, but with JSON APIs and client-side rendering.",
|
|
5
|
+
"main": "xhtmlx.js",
|
|
6
|
+
"types": "xhtmlx.d.ts",
|
|
7
|
+
"browser": "xhtmlx.js",
|
|
8
|
+
"unpkg": "xhtmlx.min.js",
|
|
9
|
+
"jsdelivr": "xhtmlx.min.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"xhtmlx.js",
|
|
12
|
+
"xhtmlx.d.ts",
|
|
13
|
+
"xhtmlx.min.js",
|
|
14
|
+
"xhtmlx.min.js.map",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "jest --verbose",
|
|
20
|
+
"test:unit": "jest tests/unit --verbose",
|
|
21
|
+
"test:integration": "jest tests/integration --verbose",
|
|
22
|
+
"lint": "eslint xhtmlx.js tests/ --ext .js",
|
|
23
|
+
"check": "node --check xhtmlx.js",
|
|
24
|
+
"build": "npx terser xhtmlx.js -o xhtmlx.min.js --compress --mangle --source-map \"filename='xhtmlx.min.js.map',url='xhtmlx.min.js.map'\"",
|
|
25
|
+
"prepublishOnly": "npm run check && npm run lint && npm test && npm run build",
|
|
26
|
+
"examples": "node examples/server.js"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"htmx",
|
|
30
|
+
"rest",
|
|
31
|
+
"api",
|
|
32
|
+
"declarative",
|
|
33
|
+
"html",
|
|
34
|
+
"attributes",
|
|
35
|
+
"template",
|
|
36
|
+
"client-side",
|
|
37
|
+
"rendering",
|
|
38
|
+
"json",
|
|
39
|
+
"ajax",
|
|
40
|
+
"fetch",
|
|
41
|
+
"spa",
|
|
42
|
+
"lightweight",
|
|
43
|
+
"no-build"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/teryxjs/xhtmlx.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/teryxjs/xhtmlx#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/teryxjs/xhtmlx/issues"
|
|
52
|
+
},
|
|
53
|
+
"author": "dkropachev",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"eslint": "^8.56.0",
|
|
57
|
+
"express": "^4.18.2",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
60
|
+
"terser": "^5.27.0"
|
|
61
|
+
}
|
|
62
|
+
}
|