arrmatura 5.0.1 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -15
- package/commons/components/viewport.xml +1 -1
- package/commons/pipes/showCounts.ts +1 -1
- package/core-web/WebPlatform.ts +25 -0
- package/core-web/attributes.ts +197 -0
- package/core-web/elements.ts +64 -0
- package/core-web/index.ts +19 -0
- package/core-web/reflow.ts +30 -0
- package/core-web/type.ts +19 -0
- package/package.json +5 -7
package/README.md
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
# Arrmatura
|
|
2
2
|
|
|
3
|
-
`Arrmatura` is a
|
|
3
|
+
`Arrmatura` is a mimimalistic component framework that designed to be:
|
|
4
4
|
|
|
5
5
|
- declarative and codeless
|
|
6
|
+
- reactive and functional
|
|
6
7
|
- non-obtrusive, platform-independent
|
|
7
|
-
- reactive and functional oriented
|
|
8
8
|
- easy to learn and get started.
|
|
9
9
|
|
|
10
10
|
# Get Started
|
|
11
11
|
|
|
12
12
|
npm i arrmatura
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
import { launchWeb } from 'arrmatura';
|
|
16
|
-
|
|
17
|
-
const components = `
|
|
14
|
+
./templates.xml
|
|
18
15
|
|
|
16
|
+
```xml
|
|
19
17
|
<component id="Application">
|
|
20
18
|
<Main name="Joe"/>
|
|
21
19
|
</component>
|
|
22
20
|
|
|
23
21
|
<component id="Main">
|
|
24
|
-
<Button title="{:greeting}, {name|upper}">
|
|
22
|
+
<Button title="{:greeting}, {name|upper}!">
|
|
25
23
|
</component>
|
|
26
|
-
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
index.ts
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import { arrmatura } from 'arrmatura';
|
|
30
|
+
import templates from './templates.xml';
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
types: [
|
|
32
|
+
arrmatura('<Application />', {
|
|
33
|
+
types: [templates],
|
|
30
34
|
resources: {
|
|
31
|
-
greeting: 'Hello
|
|
35
|
+
greeting: 'Hello',
|
|
32
36
|
},
|
|
33
|
-
|
|
37
|
+
functions: {
|
|
34
38
|
upper: (x) => x.toUpperCase(),
|
|
35
39
|
},
|
|
36
40
|
});
|
|
@@ -44,6 +48,7 @@ launchWeb('<Application />', {
|
|
|
44
48
|
|
|
45
49
|
# Samples
|
|
46
50
|
|
|
47
|
-
- [TodoMVC](https://alitskevich.github.io/arrmatura/todo.html)
|
|
48
|
-
- [Tasks](https://alitskevich.github.io/arrmatura/task.html)
|
|
49
|
-
- [
|
|
51
|
+
- [TodoMVC](https://alitskevich.github.io/arrmatura/todo.html) ([source](https://github.com/alitskevich/arrmatura/blob/master/docs/todo.html))
|
|
52
|
+
- [Tasks list](https://alitskevich.github.io/arrmatura/task.html) ([source](https://github.com/alitskevich/arrmatura/tree/master/app-task))
|
|
53
|
+
- [Game Solver](https://dlitskevich.github.io/solver/) ([source](https://github.com/dlitskevich/solver/tree/master/app))
|
|
54
|
+
- [Input Templates](https://alitskevich.github.io/templain/) ([source](https://github.com/alitskevich/templain/tree/master/app))
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<component id="Sidebar">
|
|
20
20
|
<div class="off-canvas off-canvas-sidebar-show">
|
|
21
21
|
<a class="off-canvas-toggle btn btn-primary btn-action show-lg" href="#sidebar">
|
|
22
|
-
<Icon type="
|
|
22
|
+
<Icon type="menu" />
|
|
23
23
|
</a>
|
|
24
24
|
<div id="sidebar" class="off-canvas-sidebar">
|
|
25
25
|
<Aside caption="{caption}">
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DomElement, DomTextElement } from './elements';
|
|
2
|
+
import { reflow } from './reflow';
|
|
3
|
+
|
|
4
|
+
export class WebPlatform implements IPlatform {
|
|
5
|
+
rootElement: HTMLElement;
|
|
6
|
+
|
|
7
|
+
constructor(root: HTMLElement | string = 'root') {
|
|
8
|
+
if (typeof root === 'string') {
|
|
9
|
+
root = document.getElementById(root) || window.document.body;
|
|
10
|
+
}
|
|
11
|
+
this.rootElement = root || window.document.body;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createTextElement(initials: Hash, _: IComponent): IComponentImplementation {
|
|
15
|
+
return new DomTextElement(initials);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createElement(tag: string, initials: Hash, owner: IComponent): IComponentImplementation {
|
|
19
|
+
return new DomElement(tag, initials, owner);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
reflow(root: IComponent) {
|
|
23
|
+
reflow(root, this.rootElement);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { DomNode, IElement } from './type';
|
|
2
|
+
|
|
3
|
+
const camelize = (key: string, sep = '_', jn = ' ') =>
|
|
4
|
+
('' + key)
|
|
5
|
+
.split(sep)
|
|
6
|
+
.map((s, i) => (i ? s[0].toUpperCase() + s.slice(1) : s))
|
|
7
|
+
.join(jn);
|
|
8
|
+
|
|
9
|
+
const setListener = ($: IElement, key: string, e: DomNode, v: unknown) => {
|
|
10
|
+
const fnKey = key.split(':')[0];
|
|
11
|
+
if (!v) {
|
|
12
|
+
setAttribute($, key, null);
|
|
13
|
+
} else {
|
|
14
|
+
setAttribute($, key, (ev: Event) => {
|
|
15
|
+
$.$attributes[fnKey]({ ...e.$dataset }, ev);
|
|
16
|
+
return false;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export function setAttribute($: IElement, key: string, value: null | string | Function) {
|
|
21
|
+
if (value != null) {
|
|
22
|
+
if (typeof value === 'function') {
|
|
23
|
+
const fnValue = (...args: unknown[]) => {
|
|
24
|
+
if (!$.isDone) {
|
|
25
|
+
value(...args);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
if (!$.listeners) {
|
|
29
|
+
$.listeners = new Map();
|
|
30
|
+
}
|
|
31
|
+
if (!$.listeners.has(key)) {
|
|
32
|
+
const [akey, ekey = akey] = key.split(':');
|
|
33
|
+
$.element.addEventListener(ekey, fnValue, false);
|
|
34
|
+
$.listeners.set(key, fnValue);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
$.element.setAttribute(key, value);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
if ($.listeners?.has(key)) {
|
|
41
|
+
const [akey, ekey = akey] = key.split(':');
|
|
42
|
+
$.element.removeEventListener(ekey, $.listeners.get(key));
|
|
43
|
+
$.listeners.delete(key);
|
|
44
|
+
} else {
|
|
45
|
+
$.element.removeAttribute(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// DOM
|
|
51
|
+
export const ATTR_SETTERS: Hash<($: IElement, e: DomNode, v: unknown) => void> = {
|
|
52
|
+
disabled: (_, e, v) => (e.disabled = v ? true : null),
|
|
53
|
+
class: (_, e, v) => {
|
|
54
|
+
e.className = String(v || '');
|
|
55
|
+
},
|
|
56
|
+
selected: (_, e, v) => (e.selected = v ? true : null),
|
|
57
|
+
value: (_, e, v) => (e.value = v == null ? '' : v),
|
|
58
|
+
checked: (_, e, v) => (e.checked = !!v),
|
|
59
|
+
|
|
60
|
+
init: ($, e, v) => {
|
|
61
|
+
const fn = v as Function;
|
|
62
|
+
$.init = () => fn(e, $);
|
|
63
|
+
},
|
|
64
|
+
data: function (_, e, u) {
|
|
65
|
+
const v = u as Hash;
|
|
66
|
+
if (v && e.$dataset) {
|
|
67
|
+
Object.keys(e.$dataset).forEach((k) => {
|
|
68
|
+
if (v[k] == null) {
|
|
69
|
+
e.dataset[camelize(k, '-', '')] = null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
e.$dataset = { ...e.$dataset, ...v };
|
|
73
|
+
} else {
|
|
74
|
+
e.$dataset = v ? { ...v } : e.$dataset || {};
|
|
75
|
+
}
|
|
76
|
+
if (v) {
|
|
77
|
+
Object.keys(v).forEach((k) => {
|
|
78
|
+
e.dataset[camelize(k, '-', '')] = v[k];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
click: ($, e, v) => setListener($, 'click:click', e, v),
|
|
84
|
+
['bubble-click']: ($, e, v) => setListener($, 'bubble-click:click', e, v),
|
|
85
|
+
blur: ($, e, v) => setListener($, 'blur:blur', e, v),
|
|
86
|
+
dblclick: ($, e, v) => setListener($, 'dblclick:dblclick', e, v),
|
|
87
|
+
scroll: ($, e, v) => setListener($, 'scroll:scroll', e, v),
|
|
88
|
+
|
|
89
|
+
touchstart: function ($, e, v) {
|
|
90
|
+
const h = !v
|
|
91
|
+
? null
|
|
92
|
+
: (ev: TouchEvent & { pageX: number; pageY: number }) => {
|
|
93
|
+
$.$attributes.touchstart(
|
|
94
|
+
{
|
|
95
|
+
...e.$dataset,
|
|
96
|
+
x: ev.pageX || ev.changedTouches[0].screenX,
|
|
97
|
+
y: ev.pageY || ev.changedTouches[0].screenY,
|
|
98
|
+
},
|
|
99
|
+
ev
|
|
100
|
+
);
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
setAttribute($, 'touchstart:touchstart', h);
|
|
104
|
+
setAttribute($, 'touchstart:mousedown', h);
|
|
105
|
+
},
|
|
106
|
+
touch: function ($, e, v) {
|
|
107
|
+
const data = { ...e.$dataset };
|
|
108
|
+
const hs = !v
|
|
109
|
+
? null
|
|
110
|
+
: (ev: TouchEvent & { pageX: number; pageY: number }) => {
|
|
111
|
+
data.active = true;
|
|
112
|
+
data.x = ev.pageX || ev.changedTouches[0].screenX;
|
|
113
|
+
data.y = ev.pageY || ev.changedTouches[0].screenY;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
setAttribute($, 'touch:touchstart', hs);
|
|
117
|
+
setAttribute($, 'touch:mousedown', hs);
|
|
118
|
+
const h = !v
|
|
119
|
+
? () => null
|
|
120
|
+
: (stop: boolean = false) =>
|
|
121
|
+
(ev: TouchEvent & { pageX: number; pageY: number }) => {
|
|
122
|
+
if (data.active) {
|
|
123
|
+
data.active = !stop;
|
|
124
|
+
|
|
125
|
+
data.xx = ev.pageX || ev.changedTouches[0].screenX;
|
|
126
|
+
data.yy = ev.pageY || ev.changedTouches[0].screenY;
|
|
127
|
+
data.dx = data.xx - data.x;
|
|
128
|
+
data.dy = data.yy - data.y;
|
|
129
|
+
$.$attributes.touch(data, ev);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
setAttribute($, 'touch:touchcancel', h(true));
|
|
135
|
+
setAttribute($, 'touch:touchend', h(true));
|
|
136
|
+
setAttribute($, 'touch:mouseup', h(true));
|
|
137
|
+
setAttribute($, 'touch:touchmove', h(false));
|
|
138
|
+
setAttribute($, 'touch:mousemove', h(false));
|
|
139
|
+
},
|
|
140
|
+
keypress: function ($, e, v) {
|
|
141
|
+
setAttribute(
|
|
142
|
+
$,
|
|
143
|
+
'keypress:keyup',
|
|
144
|
+
!v
|
|
145
|
+
? null
|
|
146
|
+
: (ev: KeyboardEvent) => {
|
|
147
|
+
if (ev.keyCode !== 13 && ev.keyCode !== 27) {
|
|
148
|
+
const fn = $.$attributes.keypress;
|
|
149
|
+
fn && fn({ value: e.value, ...e.$dataset }, ev);
|
|
150
|
+
setTimeout(() => e.focus(), 0);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
enter: function ($, e, v) {
|
|
157
|
+
setAttribute(
|
|
158
|
+
$,
|
|
159
|
+
'enter:keyup',
|
|
160
|
+
!v
|
|
161
|
+
? null
|
|
162
|
+
: (ev: KeyboardEvent) => {
|
|
163
|
+
if (ev.keyCode === 13) {
|
|
164
|
+
$.$attributes.enter({ value: e.value, ...e.$dataset }, ev);
|
|
165
|
+
}
|
|
166
|
+
if (ev.keyCode === 13 || ev.keyCode === 27) {
|
|
167
|
+
e.blur();
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
change: function ($, e, v) {
|
|
174
|
+
setAttribute(
|
|
175
|
+
$,
|
|
176
|
+
'change:change',
|
|
177
|
+
!v
|
|
178
|
+
? null
|
|
179
|
+
: (ev: Event) => {
|
|
180
|
+
$.$attributes.change({ value: e.value, ...e.$dataset }, ev);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
toggle: function ($, e, v) {
|
|
186
|
+
setAttribute(
|
|
187
|
+
$,
|
|
188
|
+
'toggle:change',
|
|
189
|
+
!v
|
|
190
|
+
? null
|
|
191
|
+
: (ev: Event) => {
|
|
192
|
+
$.$attributes.toggle({ value: e.checked, ...e.$dataset }, ev);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ATTR_SETTERS, setAttribute } from './attributes';
|
|
2
|
+
import { DomNode, IElement } from './type';
|
|
3
|
+
|
|
4
|
+
export class DomElement implements IComponentImplementation, IElement {
|
|
5
|
+
$: IComponent;
|
|
6
|
+
$attributes?: Hash;
|
|
7
|
+
element: DomNode;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
isDone: boolean = false;
|
|
10
|
+
listeners: any;
|
|
11
|
+
init?: () => any;
|
|
12
|
+
|
|
13
|
+
constructor(tag: string, _: Hash, $: IComponent) {
|
|
14
|
+
this.$ = $;
|
|
15
|
+
this.element = document.createElement(tag) as DomNode;
|
|
16
|
+
this.element.impl = this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
done() {
|
|
20
|
+
this.element?.parentElement?.removeChild(this.element);
|
|
21
|
+
this.$attributes = undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stateChanged(changes: Map<string, any>) {
|
|
25
|
+
const attrs: Hash = this.$attributes || (this.$attributes = {});
|
|
26
|
+
const e = this.element;
|
|
27
|
+
// this.$.log('changes', changes)
|
|
28
|
+
changes.forEach((value, key) => {
|
|
29
|
+
if (value !== attrs[key]) {
|
|
30
|
+
const setter = ATTR_SETTERS[key];
|
|
31
|
+
if (setter) {
|
|
32
|
+
setter(this, e, value);
|
|
33
|
+
} else if (key.slice(0, 5) === 'data-') {
|
|
34
|
+
ATTR_SETTERS.data(this, e, {
|
|
35
|
+
...e.$dataset,
|
|
36
|
+
[key.slice(5)]: value,
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
setAttribute(this, key, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
attrs[key] = value;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class DomTextElement implements IComponentImplementation {
|
|
48
|
+
element: Text;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
|
|
51
|
+
constructor(attrs: Hash) {
|
|
52
|
+
this.element = document.createTextNode(String(attrs.text || ''));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
done() {
|
|
56
|
+
this.element.parentElement?.removeChild(this.element);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stateChanged(changes: Map<string, any>) {
|
|
60
|
+
if (changes.has('text')) {
|
|
61
|
+
this.element.textContent = changes.get('text');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CArrmaturaNode, Platform } from '../core';
|
|
2
|
+
import { WebPlatform } from './WebPlatform';
|
|
3
|
+
|
|
4
|
+
export type ArrmaturaOptionsForWeb = ArrmaturaOptions & {
|
|
5
|
+
rootElement?: HTMLElement;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function arrmatura(
|
|
9
|
+
template: string,
|
|
10
|
+
{ rootElement, pipes, functions = pipes, types, resources }: ArrmaturaOptionsForWeb
|
|
11
|
+
) {
|
|
12
|
+
Platform.instance = new WebPlatform(rootElement);
|
|
13
|
+
|
|
14
|
+
const node = new CArrmaturaNode(template, { functions, types, resources });
|
|
15
|
+
|
|
16
|
+
node.launch();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Object.defineProperty(window, 'arrmatura', { value: arrmatura });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DomNode } from './type';
|
|
2
|
+
|
|
3
|
+
function appendElt(e: DomNode, p: DomNode | HTMLElement, cursor?: DomNode): DomNode {
|
|
4
|
+
const before = cursor ? cursor.nextSibling : p.firstChild;
|
|
5
|
+
if (!before) {
|
|
6
|
+
if (p !== e.parentElement) {
|
|
7
|
+
p.appendChild(e);
|
|
8
|
+
}
|
|
9
|
+
} else if (e !== before) {
|
|
10
|
+
p.insertBefore(e, before);
|
|
11
|
+
}
|
|
12
|
+
return e;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function reflow($: IComponent, parent: DomNode | HTMLElement, cursor?: DomNode) {
|
|
16
|
+
if ($.children) {
|
|
17
|
+
for (let p of $.children.values()) {
|
|
18
|
+
const e = p.impl.element as DomNode;
|
|
19
|
+
if (e) {
|
|
20
|
+
if (e) {
|
|
21
|
+
reflow(p, e);
|
|
22
|
+
cursor = appendElt(e, parent, cursor);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
cursor = reflow(p, parent, cursor);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cursor;
|
|
30
|
+
}
|
package/core-web/type.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface DomNode extends HTMLElement {
|
|
2
|
+
disabled: boolean | null;
|
|
3
|
+
selected: boolean | null;
|
|
4
|
+
value: unknown;
|
|
5
|
+
checked: boolean;
|
|
6
|
+
dataset: any;
|
|
7
|
+
focus(): void;
|
|
8
|
+
blur(): void;
|
|
9
|
+
$dataset: any;
|
|
10
|
+
impl?: IElement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IElement {
|
|
14
|
+
$attributes?: any;
|
|
15
|
+
init?: () => any;
|
|
16
|
+
isDone: boolean;
|
|
17
|
+
listeners: any;
|
|
18
|
+
element: DomNode;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arrmatura",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"description": "framework",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "npx webpack-dev-server --hot --contentBase docs",
|
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"lib",
|
|
20
20
|
"core",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"core-web",
|
|
22
|
+
"commons"
|
|
23
23
|
],
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@babel/cli": "^7.12.1",
|
|
26
26
|
"@babel/core": "^7.12.3",
|
|
27
27
|
"@babel/preset-env": "^7.12.1",
|
|
28
28
|
"babel-loader": "^8.2.1",
|
|
29
|
+
"babel-plugin-module-resolver": "^4.1.0",
|
|
29
30
|
"eslint": "^7.30.0",
|
|
30
31
|
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
|
31
32
|
"@typescript-eslint/parser": "^2.27.0",
|
|
@@ -36,14 +37,11 @@
|
|
|
36
37
|
"eslint-plugin-import": "^2.22.1",
|
|
37
38
|
"eslint-plugin-node": "^11.1.0",
|
|
38
39
|
"eslint-plugin-promise": "^4.2.1",
|
|
40
|
+
"metro-react-native-babel-preset": "^0.66.2",
|
|
39
41
|
"raw-loader": "^4.0.2",
|
|
40
42
|
"typescript": "^4.4.4",
|
|
41
43
|
"webpack": "^4.44.2",
|
|
42
44
|
"webpack-cli": "^3.3.12",
|
|
43
45
|
"webpack-dev-server": "^3.11.0"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"babel-plugin-module-resolver": "^4.1.0",
|
|
47
|
-
"metro-react-native-babel-preset": "^0.66.2"
|
|
48
46
|
}
|
|
49
47
|
}
|