@vibecodeapp/sdk 0.1.0 → 0.2.1
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 +25 -11
- package/dist/dev/VibeDevWrapper.d.ts +9 -0
- package/dist/dev/VibeDevWrapper.d.ts.map +1 -0
- package/dist/dev/VibeDevWrapper.js +6 -0
- package/dist/dev/VibeDevWrapper.js.map +1 -0
- package/dist/dev/safe-insets.d.ts +9 -0
- package/dist/dev/safe-insets.d.ts.map +1 -0
- package/dist/dev/safe-insets.js +5 -0
- package/dist/dev/safe-insets.js.map +1 -0
- package/dist/dev/safe-insets.web.d.ts +9 -0
- package/dist/dev/safe-insets.web.d.ts.map +1 -0
- package/dist/dev/safe-insets.web.js +40 -0
- package/dist/dev/safe-insets.web.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/polyfills/alert.web.d.ts +1 -1
- package/dist/polyfills/alert.web.d.ts.map +1 -1
- package/dist/polyfills/alert.web.js +1 -1
- package/dist/polyfills/alert.web.js.map +1 -1
- package/dist/polyfills/fetch.web.d.ts +14 -0
- package/dist/polyfills/fetch.web.d.ts.map +1 -0
- package/dist/polyfills/fetch.web.js +160 -0
- package/dist/polyfills/fetch.web.js.map +1 -0
- package/dist/polyfills/haptics.web.d.ts.map +1 -1
- package/dist/polyfills/haptics.web.js +3 -3
- package/dist/polyfills/haptics.web.js.map +1 -1
- package/dist/polyfills/maps.web.d.ts +1 -1
- package/dist/polyfills/maps.web.d.ts.map +1 -1
- package/dist/polyfills/maps.web.js +4 -4
- package/dist/polyfills/maps.web.js.map +1 -1
- package/dist/polyfills/refresh-control-component.d.ts +1 -1
- package/dist/polyfills/refresh-control-component.d.ts.map +1 -1
- package/dist/polyfills/refresh-control-component.js +13 -9
- package/dist/polyfills/refresh-control-component.js.map +1 -1
- package/dist/polyfills/secure-store.web.d.ts.map +1 -1
- package/dist/polyfills/secure-store.web.js +6 -6
- package/dist/polyfills/secure-store.web.js.map +1 -1
- package/metro/index.cjs +121 -108
- package/metro/index.d.ts +18 -18
- package/metro/metro-http-store.cjs +254 -254
- package/metro/transformer.cjs +69 -0
- package/package.json +59 -57
|
@@ -11,264 +11,264 @@
|
|
|
11
11
|
* Requires an HTTP/2 server - will fail on HTTP/1.1.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const http2 = require(
|
|
15
|
-
const zlib = require(
|
|
16
|
-
const { Buffer } = require(
|
|
17
|
-
const pLimit = require(
|
|
14
|
+
const http2 = require('node:http2')
|
|
15
|
+
const zlib = require('node:zlib')
|
|
16
|
+
const { Buffer } = require('node:buffer')
|
|
17
|
+
const pLimit = require('p-limit')
|
|
18
18
|
|
|
19
|
-
const NULL_BYTE = 0x00
|
|
20
|
-
const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE])
|
|
19
|
+
const NULL_BYTE = 0x00
|
|
20
|
+
const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE])
|
|
21
21
|
|
|
22
22
|
const ZLIB_OPTIONS = {
|
|
23
|
-
|
|
24
|
-
}
|
|
23
|
+
level: 9,
|
|
24
|
+
}
|
|
25
25
|
|
|
26
26
|
class HttpStore {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
27
|
+
#origin
|
|
28
|
+
#path
|
|
29
|
+
#timeout
|
|
30
|
+
#sessions = []
|
|
31
|
+
#maxConnections
|
|
32
|
+
#nextSession = 0
|
|
33
|
+
#limit
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} options
|
|
37
|
+
* @param {string} options.endpoint - Full URL to the cache server (e.g., "https://cache.example.com/metro")
|
|
38
|
+
* @param {number} [options.timeout=10000] - Request timeout in milliseconds
|
|
39
|
+
* @param {number} [options.maxConnections=8] - Number of HTTP/2 connections in pool
|
|
40
|
+
* @param {number} [options.maxConcurrent=64] - Max concurrent requests in flight
|
|
41
|
+
*/
|
|
42
|
+
constructor(options) {
|
|
43
|
+
const url = new URL(options.endpoint)
|
|
44
|
+
this.#origin = url.origin
|
|
45
|
+
this.#path = url.pathname.replace(/\/$/, '') // Remove trailing slash
|
|
46
|
+
this.#timeout = options.timeout ?? 10000
|
|
47
|
+
this.#maxConnections = options.maxConnections ?? 8
|
|
48
|
+
this.#limit = pLimit(options.maxConcurrent ?? 64)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get an HTTP/2 session from the pool using round-robin.
|
|
53
|
+
* Lazily creates connections on first use.
|
|
54
|
+
* Never throws - returns null if connection fails.
|
|
55
|
+
*/
|
|
56
|
+
#getSession() {
|
|
57
|
+
// Round-robin through connections
|
|
58
|
+
const index = this.#nextSession++ % this.#maxConnections
|
|
59
|
+
|
|
60
|
+
// Check if we have a valid session at this index
|
|
61
|
+
const existing = this.#sessions[index]
|
|
62
|
+
if (existing && !existing.closed && !existing.destroyed) {
|
|
63
|
+
return existing
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create new session for this slot
|
|
67
|
+
try {
|
|
68
|
+
const session = http2.connect(this.#origin)
|
|
69
|
+
|
|
70
|
+
session.on('error', (err) => {
|
|
71
|
+
console.warn('[HttpStore] Session error:', err.message)
|
|
72
|
+
if (this.#sessions[index] === session) {
|
|
73
|
+
this.#sessions[index] = null
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
session.on('close', () => {
|
|
78
|
+
if (this.#sessions[index] === session) {
|
|
79
|
+
this.#sessions[index] = null
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
this.#sessions[index] = session
|
|
84
|
+
return session
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn('[HttpStore] Failed to connect:', err.message)
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GET - Blocking read from cache.
|
|
93
|
+
* Returns null on any error (network, timeout, parse) - never throws.
|
|
94
|
+
* @param {Buffer} key
|
|
95
|
+
* @returns {Promise<any|null>}
|
|
96
|
+
*/
|
|
97
|
+
async get(key) {
|
|
98
|
+
try {
|
|
99
|
+
return await this.#limit(() => this.#doGet(key))
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn('[HttpStore] Get failed:', err.message)
|
|
102
|
+
return null // Fall back to next cache store (FileStore)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#doGet(key) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const session = this.#getSession()
|
|
109
|
+
if (!session) {
|
|
110
|
+
reject(new Error('No session available'))
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const path = `${this.#path}/${key.toString('hex')}`
|
|
115
|
+
const req = session.request({
|
|
116
|
+
':method': 'GET',
|
|
117
|
+
':path': path,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const timeoutId = setTimeout(() => {
|
|
121
|
+
req.close(http2.constants.NGHTTP2_CANCEL)
|
|
122
|
+
reject(new Error('Request timed out'))
|
|
123
|
+
}, this.#timeout)
|
|
124
|
+
|
|
125
|
+
const chunks = []
|
|
126
|
+
|
|
127
|
+
req.on('response', (headers) => {
|
|
128
|
+
const status = headers[':status']
|
|
129
|
+
|
|
130
|
+
// 404 = cache miss
|
|
131
|
+
if (status === 404) {
|
|
132
|
+
clearTimeout(timeoutId)
|
|
133
|
+
req.close()
|
|
134
|
+
resolve(null)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Non-2xx = error
|
|
139
|
+
if (status < 200 || status >= 300) {
|
|
140
|
+
clearTimeout(timeoutId)
|
|
141
|
+
req.close()
|
|
142
|
+
reject(new Error(`HTTP error: ${status}`))
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
req.on('data', (chunk) => {
|
|
148
|
+
chunks.push(chunk)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
req.on('end', () => {
|
|
152
|
+
clearTimeout(timeoutId)
|
|
153
|
+
|
|
154
|
+
const compressed = Buffer.concat(chunks)
|
|
155
|
+
if (compressed.length === 0) {
|
|
156
|
+
resolve(null)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Decompress the response
|
|
161
|
+
zlib.gunzip(compressed, (err, buffer) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
reject(err)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// If first byte is NULL_BYTE, it's a raw Buffer
|
|
169
|
+
if (buffer.length > 0 && buffer[0] === NULL_BYTE) {
|
|
170
|
+
resolve(buffer.subarray(1))
|
|
171
|
+
} else {
|
|
172
|
+
resolve(JSON.parse(buffer.toString('utf8')))
|
|
173
|
+
}
|
|
174
|
+
} catch (parseErr) {
|
|
175
|
+
reject(parseErr)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
req.on('error', (err) => {
|
|
181
|
+
clearTimeout(timeoutId)
|
|
182
|
+
reject(err)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
req.end()
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* SET - Non-blocking write to cache.
|
|
191
|
+
* Returns immediately, HTTP request happens in background.
|
|
192
|
+
* @param {Buffer} key
|
|
193
|
+
* @param {any} value
|
|
194
|
+
* @returns {Promise<void>}
|
|
195
|
+
*/
|
|
196
|
+
set(key, value) {
|
|
197
|
+
// Fire and forget - queue the work but return immediately
|
|
198
|
+
this.#limit(() => this.#doSet(key, value)).catch((err) => {
|
|
199
|
+
console.warn('[HttpStore] Background write failed:', err.message)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return Promise.resolve()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#doSet(key, value) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const session = this.#getSession()
|
|
208
|
+
if (!session) {
|
|
209
|
+
reject(new Error('No session available'))
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const path = `${this.#path}/${key.toString('hex')}`
|
|
214
|
+
|
|
215
|
+
// Prepare data to compress
|
|
216
|
+
let data
|
|
217
|
+
if (Buffer.isBuffer(value)) {
|
|
218
|
+
// Prepend NULL_BYTE marker for Buffer values
|
|
219
|
+
data = Buffer.concat([NULL_BYTE_BUFFER, value])
|
|
220
|
+
} else {
|
|
221
|
+
data = Buffer.from(JSON.stringify(value) ?? 'null', 'utf8')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Compress the data
|
|
225
|
+
zlib.gzip(data, ZLIB_OPTIONS, (err, compressed) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
reject(err)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const req = session.request({
|
|
232
|
+
':method': 'PUT',
|
|
233
|
+
':path': path,
|
|
234
|
+
'content-type': 'application/octet-stream',
|
|
235
|
+
'content-length': compressed.length,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const timeoutId = setTimeout(() => {
|
|
239
|
+
req.close(http2.constants.NGHTTP2_CANCEL)
|
|
240
|
+
reject(new Error('Request timed out'))
|
|
241
|
+
}, this.#timeout)
|
|
242
|
+
|
|
243
|
+
req.on('response', (headers) => {
|
|
244
|
+
const status = headers[':status']
|
|
245
|
+
|
|
246
|
+
clearTimeout(timeoutId)
|
|
247
|
+
|
|
248
|
+
if (status >= 200 && status < 300) {
|
|
249
|
+
resolve()
|
|
250
|
+
} else {
|
|
251
|
+
reject(new Error(`HTTP error: ${status}`))
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
req.on('error', (err) => {
|
|
256
|
+
clearTimeout(timeoutId)
|
|
257
|
+
reject(err)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
req.write(compressed)
|
|
261
|
+
req.end()
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* CLEAR - Not implemented.
|
|
268
|
+
*/
|
|
269
|
+
clear() {
|
|
270
|
+
// No-op - Metro's HTTP cache protocol doesn't define a clear operation
|
|
271
|
+
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
module.exports = { HttpStore }
|
|
274
|
+
module.exports = { HttpStore }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro transformer that automatically wraps _layout.tsx with Vibecode providers
|
|
3
|
+
* Uses simple string transformations to inject wrappers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Chain to SVG transformer, which then chains to the default babel transformer
|
|
7
|
+
// Cant use @expo/metro-config/babel-transformer because it doesnt chain to the SVG transformer.
|
|
8
|
+
const upstreamTransformer = require('react-native-svg-transformer/expo')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if the file is the root _layout.tsx
|
|
12
|
+
*/
|
|
13
|
+
function isRootLayout(filename) {
|
|
14
|
+
return (
|
|
15
|
+
(filename.includes('app/_layout.tsx') ||
|
|
16
|
+
filename.includes('app/_layout.js')) &&
|
|
17
|
+
!filename.includes('app/(') // Exclude grouped layouts
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wraps the layout component with Vibecode providers
|
|
23
|
+
*/
|
|
24
|
+
function wrapLayoutWithProviders(src) {
|
|
25
|
+
// Find the default export function
|
|
26
|
+
const defaultExportPattern = /export\s+default\s+function\s+(\w+)/
|
|
27
|
+
const match = src.match(defaultExportPattern)
|
|
28
|
+
|
|
29
|
+
if (!match) {
|
|
30
|
+
return src // Can't transform, return as-is
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const originalFunctionName = match[1]
|
|
34
|
+
|
|
35
|
+
// Remove the default keyword from the original function
|
|
36
|
+
let transformed = src.replace(
|
|
37
|
+
defaultExportPattern,
|
|
38
|
+
`function ${originalFunctionName}`,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Add wrapper import at the top
|
|
42
|
+
transformed = `import { VibeDevWrapper } from '@vibecodeapp/sdk';\n${transformed}`
|
|
43
|
+
|
|
44
|
+
// Add new default export that wraps the original function
|
|
45
|
+
const newDefaultExport = `
|
|
46
|
+
export default function VibeRootLayoutWrapper() {
|
|
47
|
+
return (
|
|
48
|
+
<VibeDevWrapper>
|
|
49
|
+
<${originalFunctionName} />
|
|
50
|
+
</VibeDevWrapper>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
transformed = transformed + '\n' + newDefaultExport
|
|
56
|
+
|
|
57
|
+
return transformed
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function transform(props) {
|
|
61
|
+
if (isRootLayout(props.filename)) {
|
|
62
|
+
const transformedSrc = wrapLayoutWithProviders(props.src)
|
|
63
|
+
return upstreamTransformer.transform({ ...props, src: transformedSrc })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return upstreamTransformer.transform(props)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { transform }
|