@keepsnappy/react-native 0.0.0-stage → 0.1.0-alpha.11
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 +260 -2
- package/android/build.gradle +35 -0
- package/android/src/main/java/expo/modules/perflab/PerfLabModule.kt +445 -0
- package/android/src/main/java/expo/modules/perflab/PerfLabSampler.kt +515 -0
- package/app.plugin.js +105 -0
- package/expo-module.config.json +9 -0
- package/ios/PerfLab.podspec +29 -0
- package/ios/PerfLabHermes.h +18 -0
- package/ios/PerfLabHermes.mm +64 -0
- package/ios/PerfLabModule.swift +371 -0
- package/ios/PerfLabSampler.swift +364 -0
- package/js/babel-transformer.js +71 -0
- package/js/hook.polyfill.js +333 -0
- package/js/renderer-switch.js +16 -0
- package/js/sdk/activation.ts +375 -0
- package/js/sdk/active.ts +62 -0
- package/js/sdk/artifacts.ts +632 -0
- package/js/sdk/boot.ts +21 -0
- package/js/sdk/causes.ts +932 -0
- package/js/sdk/core.ts +2403 -0
- package/js/sdk/cpuprof.ts +133 -0
- package/js/sdk/dormant.ts +77 -0
- package/js/sdk/env.d.ts +2 -0
- package/js/sdk/fiber.ts +591 -0
- package/js/sdk/interactions.ts +531 -0
- package/js/sdk/links.ts +201 -0
- package/js/sdk/nav.ts +383 -0
- package/js/sdk/net-install.ts +49 -0
- package/js/sdk/net.ts +684 -0
- package/js/sdk/profile-export.ts +257 -0
- package/js/sdk/profile.ts +539 -0
- package/js/sdk/push-install.ts +158 -0
- package/js/sdk/push.ts +819 -0
- package/js/sdk/retain-floor.ts +11 -0
- package/js/sdk/sampler.ts +236 -0
- package/js/sdk/screens.ts +586 -0
- package/js/sdk/stream.ts +125 -0
- package/js/zustand-vanilla.js +31 -0
- package/metro.js +238 -0
- package/package.json +26 -4
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import Foundation
|
|
3
|
+
import QuartzCore
|
|
4
|
+
|
|
5
|
+
// Fallback until the SDK configures the sampler; same value as SAMPLER_MS_DEFAULT in js/sdk/sampler.ts.
|
|
6
|
+
private let samplerMsDefault = 4000.0
|
|
7
|
+
import UIKit
|
|
8
|
+
|
|
9
|
+
private let jsThreadMarkers = ["com.facebook.react.runtime.JavaScript", "com.facebook.react.JavaScript", "mqt_js", "mqt_v_js"]
|
|
10
|
+
|
|
11
|
+
private func isJsThreadName(_ name: String) -> Bool {
|
|
12
|
+
jsThreadMarkers.contains { name == $0 || name.hasPrefix($0) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private let timebase: mach_timebase_info_data_t = {
|
|
16
|
+
var info = mach_timebase_info_data_t()
|
|
17
|
+
mach_timebase_info(&info)
|
|
18
|
+
return info
|
|
19
|
+
}()
|
|
20
|
+
|
|
21
|
+
// performance.now() in Hermes on iOS is steady_clock = mach_absolute_time.
|
|
22
|
+
func perfLabMonotonicMs() -> Double {
|
|
23
|
+
Double(mach_absolute_time()) * Double(timebase.numer) / Double(timebase.denom) / 1_000_000
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private func machCount<T>(_ type: T.Type) -> mach_msg_type_number_t {
|
|
27
|
+
mach_msg_type_number_t(MemoryLayout<T>.size / MemoryLayout<natural_t>.size)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func threadCpuMs(_ thread: thread_act_t) -> Double? {
|
|
31
|
+
var info = thread_basic_info()
|
|
32
|
+
var count = machCount(thread_basic_info.self)
|
|
33
|
+
let kr = withUnsafeMutablePointer(to: &info) {
|
|
34
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
35
|
+
thread_info(thread, thread_flavor_t(THREAD_BASIC_INFO), $0, &count)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
guard kr == KERN_SUCCESS else { return nil }
|
|
39
|
+
let seconds = Double(info.user_time.seconds) + Double(info.system_time.seconds)
|
|
40
|
+
let micros = Double(info.user_time.microseconds) + Double(info.system_time.microseconds)
|
|
41
|
+
return seconds * 1000 + micros / 1000
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func threadName(_ thread: thread_act_t) -> String? {
|
|
45
|
+
var info = thread_extended_info()
|
|
46
|
+
var count = machCount(thread_extended_info.self)
|
|
47
|
+
let kr = withUnsafeMutablePointer(to: &info) {
|
|
48
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
49
|
+
thread_info(thread, thread_flavor_t(THREAD_EXTENDED_INFO), $0, &count)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
guard kr == KERN_SUCCESS else { return nil }
|
|
53
|
+
let name = withUnsafeBytes(of: info.pth_name) { raw -> String in
|
|
54
|
+
String(decoding: raw.prefix { $0 != 0 }, as: UTF8.self)
|
|
55
|
+
}
|
|
56
|
+
return name.isEmpty ? nil : name
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func processCpuMs() -> Double {
|
|
60
|
+
var usage = rusage()
|
|
61
|
+
getrusage(RUSAGE_SELF, &usage)
|
|
62
|
+
let seconds = Double(usage.ru_utime.tv_sec) + Double(usage.ru_stime.tv_sec)
|
|
63
|
+
let micros = Double(usage.ru_utime.tv_usec) + Double(usage.ru_stime.tv_usec)
|
|
64
|
+
return seconds * 1000 + micros / 1000
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func memoryMb() -> (footprint: Double, resident: Double)? {
|
|
68
|
+
var info = task_vm_info_data_t()
|
|
69
|
+
var count = machCount(task_vm_info_data_t.self)
|
|
70
|
+
let kr = withUnsafeMutablePointer(to: &info) {
|
|
71
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
72
|
+
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
guard kr == KERN_SUCCESS else { return nil }
|
|
76
|
+
let mb = 1024.0 * 1024.0
|
|
77
|
+
return (Double(info.phys_footprint) / mb, Double(info.resident_size) / mb)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Counts presented and missed frames on the main run loop. A frame's budget is the link's own
|
|
81
|
+
// scheduled cadence (targetTimestamp - timestamp of the previous callback), not link.duration:
|
|
82
|
+
// on ProMotion screens the link runs at 60 Hz while duration reports the 120 Hz budget.
|
|
83
|
+
// The link asks for no frame rate, so it never raises the screen's refresh rate; a single
|
|
84
|
+
// 120 Hz drop between two 60 Hz callbacks is invisible.
|
|
85
|
+
final class PerfLabFrameCounter: NSObject {
|
|
86
|
+
private var link: CADisplayLink?
|
|
87
|
+
private var lastTimestamp: CFTimeInterval = 0
|
|
88
|
+
private var lastBudget: CFTimeInterval = 0
|
|
89
|
+
|
|
90
|
+
private let lock: UnsafeMutablePointer<os_unfair_lock> = {
|
|
91
|
+
let pointer = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
|
|
92
|
+
pointer.initialize(to: os_unfair_lock())
|
|
93
|
+
return pointer
|
|
94
|
+
}()
|
|
95
|
+
private var presentedTotal = 0
|
|
96
|
+
private var missedTotal = 0
|
|
97
|
+
private var longestGapMs = 0.0
|
|
98
|
+
private var budgetMs = 0.0
|
|
99
|
+
|
|
100
|
+
private(set) var maxHz = 0
|
|
101
|
+
// Main thread's port, taken on the main thread when the counter first starts.
|
|
102
|
+
private var mainPort: thread_act_t = 0
|
|
103
|
+
|
|
104
|
+
var mainThread: thread_act_t {
|
|
105
|
+
os_unfair_lock_lock(lock)
|
|
106
|
+
defer { os_unfair_lock_unlock(lock) }
|
|
107
|
+
return mainPort
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
deinit {
|
|
111
|
+
lock.deinitialize(count: 1)
|
|
112
|
+
lock.deallocate()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func start() {
|
|
116
|
+
DispatchQueue.main.async {
|
|
117
|
+
guard self.link == nil else { return }
|
|
118
|
+
os_unfair_lock_lock(self.lock)
|
|
119
|
+
if self.mainPort == 0 {
|
|
120
|
+
self.mainPort = mach_thread_self()
|
|
121
|
+
}
|
|
122
|
+
os_unfair_lock_unlock(self.lock)
|
|
123
|
+
self.maxHz = UIScreen.main.maximumFramesPerSecond
|
|
124
|
+
self.lastTimestamp = 0
|
|
125
|
+
let link = CADisplayLink(target: self, selector: #selector(self.tick(_:)))
|
|
126
|
+
link.add(to: .main, forMode: .common)
|
|
127
|
+
self.link = link
|
|
128
|
+
let center = NotificationCenter.default
|
|
129
|
+
center.addObserver(self, selector: #selector(self.pause), name: UIApplication.willResignActiveNotification, object: nil)
|
|
130
|
+
center.addObserver(self, selector: #selector(self.pause), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func stop() {
|
|
135
|
+
DispatchQueue.main.async {
|
|
136
|
+
self.link?.invalidate()
|
|
137
|
+
self.link = nil
|
|
138
|
+
NotificationCenter.default.removeObserver(self)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The link stops while the app is inactive; the gap across it is not a stall.
|
|
143
|
+
@objc private func pause() {
|
|
144
|
+
lastTimestamp = 0
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@objc private func tick(_ link: CADisplayLink) {
|
|
148
|
+
let timestamp = link.timestamp
|
|
149
|
+
let budget = link.targetTimestamp - timestamp
|
|
150
|
+
if lastTimestamp > 0 {
|
|
151
|
+
let gap = timestamp - lastTimestamp
|
|
152
|
+
let expected = lastBudget > 0 ? lastBudget : budget
|
|
153
|
+
let missed = expected > 0 ? max(0, Int((gap / expected).rounded()) - 1) : 0
|
|
154
|
+
os_unfair_lock_lock(lock)
|
|
155
|
+
presentedTotal += 1
|
|
156
|
+
missedTotal += missed
|
|
157
|
+
if gap * 1000 > longestGapMs {
|
|
158
|
+
longestGapMs = gap * 1000
|
|
159
|
+
}
|
|
160
|
+
budgetMs = expected * 1000
|
|
161
|
+
os_unfair_lock_unlock(lock)
|
|
162
|
+
}
|
|
163
|
+
lastTimestamp = timestamp
|
|
164
|
+
lastBudget = budget
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Totals are cumulative; the longest gap restarts with every read.
|
|
168
|
+
func read() -> (presentedTotal: Int, missedTotal: Int, longestGapMs: Double, budgetMs: Double) {
|
|
169
|
+
os_unfair_lock_lock(lock)
|
|
170
|
+
defer { os_unfair_lock_unlock(lock) }
|
|
171
|
+
let out = (presentedTotal, missedTotal, longestGapMs, budgetMs)
|
|
172
|
+
longestGapMs = 0
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Off unless configured. `auto` samples on a utility queue every intervalMs and needs no JS;
|
|
178
|
+
// `manual` only keeps the frame counter running and samples when asked. Every sample is logged
|
|
179
|
+
// as `PERF_LAB sample {json}` through the module's native log and kept in a short ring for windows.
|
|
180
|
+
final class PerfLabSampler {
|
|
181
|
+
private let queue = DispatchQueue(label: "com.gigaverse.perflab.sampler", qos: .utility)
|
|
182
|
+
private let log: (String) -> Void
|
|
183
|
+
private let frames = PerfLabFrameCounter()
|
|
184
|
+
|
|
185
|
+
private var timer: DispatchSourceTimer?
|
|
186
|
+
private var mode = "off"
|
|
187
|
+
private var intervalMs = samplerMsDefault
|
|
188
|
+
private var seq = 0
|
|
189
|
+
private var previous: (t: Double, processMs: Double, mainMs: Double?, jsMs: Double?, presented: Int, missed: Int)?
|
|
190
|
+
private var ring: [[String: Any]] = []
|
|
191
|
+
private let ringSize = 64
|
|
192
|
+
// Records of samplerRequest, kept outside the ring until taken, so a long window still finds its open sample.
|
|
193
|
+
private var pinned: [(reason: String, record: [String: Any])] = []
|
|
194
|
+
private let pinnedMax = 32
|
|
195
|
+
private var jsThread: thread_act_t = 0
|
|
196
|
+
private var jsThreadName: String?
|
|
197
|
+
private var scans = 0
|
|
198
|
+
|
|
199
|
+
init(log: @escaping (String) -> Void) {
|
|
200
|
+
self.log = log
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
deinit {
|
|
204
|
+
timer?.cancel()
|
|
205
|
+
frames.stop()
|
|
206
|
+
releaseJsThread()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func configure(mode newMode: String, intervalMs newInterval: Double) -> [String: Any] {
|
|
210
|
+
queue.sync {
|
|
211
|
+
mode = ["auto", "manual"].contains(newMode) ? newMode : "off"
|
|
212
|
+
intervalMs = max(250, newInterval)
|
|
213
|
+
timer?.cancel()
|
|
214
|
+
timer = nil
|
|
215
|
+
if mode == "off" {
|
|
216
|
+
frames.stop()
|
|
217
|
+
previous = nil
|
|
218
|
+
ring.removeAll()
|
|
219
|
+
pinned.removeAll()
|
|
220
|
+
releaseJsThread()
|
|
221
|
+
} else {
|
|
222
|
+
frames.start()
|
|
223
|
+
if mode == "auto" {
|
|
224
|
+
let source = DispatchSource.makeTimerSource(queue: queue)
|
|
225
|
+
let interval = DispatchTimeInterval.milliseconds(Int(intervalMs))
|
|
226
|
+
source.schedule(deadline: .now() + interval, repeating: interval, leeway: .milliseconds(Int(intervalMs / 10)))
|
|
227
|
+
source.setEventHandler { [weak self] in
|
|
228
|
+
_ = self?.collect(reason: "auto")
|
|
229
|
+
}
|
|
230
|
+
source.resume()
|
|
231
|
+
timer = source
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return ["mode": mode, "intervalMs": intervalMs, "nowMs": perfLabMonotonicMs()]
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func sample(reason: String) -> [String: Any]? {
|
|
239
|
+
queue.sync { mode == "off" ? nil : collect(reason: reason) }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Returns at once; the sample is taken on the sampler queue (record time is taken there) and lands in the ring.
|
|
243
|
+
func request(reason: String) {
|
|
244
|
+
queue.async { [weak self] in
|
|
245
|
+
guard let self, self.mode != "off" else { return }
|
|
246
|
+
let record = self.collect(reason: reason)
|
|
247
|
+
self.pinned.append((reason, record))
|
|
248
|
+
if self.pinned.count > self.pinnedMax { self.pinned.removeFirst() }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func take(reason: String) -> [String: Any]? {
|
|
253
|
+
queue.sync {
|
|
254
|
+
guard let index = pinned.firstIndex(where: { $0.reason == reason }) else { return nil }
|
|
255
|
+
return pinned.remove(at: index).record
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func recent(since t: Double) -> [[String: Any]] {
|
|
260
|
+
queue.sync { ring.filter { (($0["t"] as? Double) ?? 0) >= t } }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private func releaseJsThread() {
|
|
264
|
+
if jsThread != 0 {
|
|
265
|
+
mach_port_deallocate(mach_task_self_, jsThread)
|
|
266
|
+
jsThread = 0
|
|
267
|
+
jsThreadName = nil
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// task_threads hands out a send right per thread; the JS thread's right is kept so its port
|
|
272
|
+
// stays valid between samples, every other right is released.
|
|
273
|
+
private func findJsThread() {
|
|
274
|
+
var list: thread_act_array_t?
|
|
275
|
+
var count: mach_msg_type_number_t = 0
|
|
276
|
+
guard task_threads(mach_task_self_, &list, &count) == KERN_SUCCESS, let list else { return }
|
|
277
|
+
for i in 0..<Int(count) {
|
|
278
|
+
let thread = list[i]
|
|
279
|
+
if jsThread == 0, let name = threadName(thread), isJsThreadName(name) {
|
|
280
|
+
jsThread = thread
|
|
281
|
+
jsThreadName = name
|
|
282
|
+
} else {
|
|
283
|
+
mach_port_deallocate(mach_task_self_, thread)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
vm_deallocate(mach_task_self_, vm_address_t(UInt(bitPattern: list)), vm_size_t(Int(count) * MemoryLayout<thread_t>.stride))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private func jsCpuMs() -> Double? {
|
|
290
|
+
if jsThread != 0, let ms = threadCpuMs(jsThread) {
|
|
291
|
+
return ms
|
|
292
|
+
}
|
|
293
|
+
releaseJsThread()
|
|
294
|
+
// A missing JS thread is looked for again every 5th sample, not on every one.
|
|
295
|
+
if scans % 5 == 0 {
|
|
296
|
+
findJsThread()
|
|
297
|
+
}
|
|
298
|
+
scans += 1
|
|
299
|
+
return jsThread == 0 ? nil : threadCpuMs(jsThread)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func collect(reason: String) -> [String: Any] {
|
|
303
|
+
let started = perfLabMonotonicMs()
|
|
304
|
+
let t = started
|
|
305
|
+
let processMs = processCpuMs()
|
|
306
|
+
let main = frames.mainThread
|
|
307
|
+
let mainMs = main == 0 ? nil : threadCpuMs(main)
|
|
308
|
+
let jsMs = jsCpuMs()
|
|
309
|
+
let memory = memoryMb()
|
|
310
|
+
let frame = frames.read()
|
|
311
|
+
seq += 1
|
|
312
|
+
|
|
313
|
+
var cpu: [String: Any] = ["processMs": processMs]
|
|
314
|
+
if let mainMs { cpu["mainMs"] = mainMs }
|
|
315
|
+
if let jsMs { cpu["jsMs"] = jsMs }
|
|
316
|
+
if let jsThreadName { cpu["jsThread"] = jsThreadName }
|
|
317
|
+
var framesOut: [String: Any] = [
|
|
318
|
+
"presentedTotal": frame.presentedTotal,
|
|
319
|
+
"missedTotal": frame.missedTotal,
|
|
320
|
+
"longestGapMs": frame.longestGapMs,
|
|
321
|
+
"budgetMs": frame.budgetMs,
|
|
322
|
+
"maxHz": frames.maxHz,
|
|
323
|
+
]
|
|
324
|
+
var record: [String: Any] = [
|
|
325
|
+
"v": 1,
|
|
326
|
+
"platform": "ios",
|
|
327
|
+
"seq": seq,
|
|
328
|
+
"reason": reason,
|
|
329
|
+
"t": t,
|
|
330
|
+
"wallMs": Date().timeIntervalSince1970 * 1000,
|
|
331
|
+
"thermal": ProcessInfo.processInfo.thermalState.rawValue,
|
|
332
|
+
"intervalMs": intervalMs,
|
|
333
|
+
"mode": mode,
|
|
334
|
+
]
|
|
335
|
+
if let memory {
|
|
336
|
+
record["mem"] = ["footprintMb": memory.footprint, "residentMb": memory.resident]
|
|
337
|
+
} else {
|
|
338
|
+
record["mem"] = [String: Any]()
|
|
339
|
+
}
|
|
340
|
+
if let previous, t > previous.t {
|
|
341
|
+
let dt = t - previous.t
|
|
342
|
+
record["dtMs"] = dt
|
|
343
|
+
cpu["processCores"] = (processMs - previous.processMs) / dt
|
|
344
|
+
if let mainMs, let before = previous.mainMs { cpu["mainCores"] = (mainMs - before) / dt }
|
|
345
|
+
if let jsMs, let before = previous.jsMs, jsMs >= before { cpu["jsCores"] = (jsMs - before) / dt }
|
|
346
|
+
framesOut["presented"] = frame.presentedTotal - previous.presented
|
|
347
|
+
framesOut["missed"] = frame.missedTotal - previous.missed
|
|
348
|
+
}
|
|
349
|
+
previous = (t, processMs, mainMs, jsMs, frame.presentedTotal, frame.missedTotal)
|
|
350
|
+
record["cpu"] = cpu
|
|
351
|
+
record["frames"] = framesOut
|
|
352
|
+
record["collectMs"] = perfLabMonotonicMs() - started
|
|
353
|
+
|
|
354
|
+
ring.append(record)
|
|
355
|
+
if ring.count > ringSize {
|
|
356
|
+
ring.removeFirst(ring.count - ringSize)
|
|
357
|
+
}
|
|
358
|
+
if let data = try? JSONSerialization.data(withJSONObject: record),
|
|
359
|
+
let json = String(data: data, encoding: .utf8) {
|
|
360
|
+
log("PERF_LAB sample \(json)")
|
|
361
|
+
}
|
|
362
|
+
return record
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Wraps the app's Babel transformer (set by withPerfLab) and adds one import to
|
|
2
|
+
// the entry module: the perf-lab boot module. That import is what puts the SDK
|
|
3
|
+
// in the graph; getModulesRunBeforeMainModule then runs it before the entry.
|
|
4
|
+
//
|
|
5
|
+
// babel-preset-expo inlines EXPO_PUBLIC_* only outside node_modules, and the
|
|
6
|
+
// SDK is installed into node_modules, so this wrapper inlines the SDK's own
|
|
7
|
+
// EXPO_PUBLIC_* reads with the build-time values.
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const upstreamPath = (process.env.KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER ?? process.env.PERFLAB_UPSTREAM_BABEL_TRANSFORMER);
|
|
12
|
+
const entryFile = (process.env.KEEPSNAPPY_ENTRY_FILE ?? process.env.PERFLAB_ENTRY_FILE);
|
|
13
|
+
const bootFile = (process.env.KEEPSNAPPY_BOOT_FILE ?? process.env.PERFLAB_BOOT_FILE);
|
|
14
|
+
|
|
15
|
+
if (!upstreamPath || !entryFile || !bootFile) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'KeepSnappy Babel transformer loaded without withKeepSnappy env (KEEPSNAPPY_*)',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const upstream = require(upstreamPath);
|
|
22
|
+
|
|
23
|
+
const packageRoot = path.resolve(__dirname, '..') + path.sep;
|
|
24
|
+
const ENV_REF = /\bprocess\.env\.(EXPO_PUBLIC_[A-Z0-9_]+)\b/g;
|
|
25
|
+
|
|
26
|
+
const inlineEnv = src =>
|
|
27
|
+
src.replace(ENV_REF, (_match, name) =>
|
|
28
|
+
process.env[name] === undefined ? 'undefined' : JSON.stringify(process.env[name]),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const envSignature = () =>
|
|
32
|
+
Object.keys(process.env)
|
|
33
|
+
.filter(name => (name.startsWith('EXPO_PUBLIC_PERF') || name.startsWith('EXPO_PUBLIC_KEEPSNAPPY')))
|
|
34
|
+
.sort()
|
|
35
|
+
.map(name => `${name}=${process.env[name]}`)
|
|
36
|
+
.join('&');
|
|
37
|
+
|
|
38
|
+
const bootImport = () => {
|
|
39
|
+
let rel = path
|
|
40
|
+
.relative(path.dirname(entryFile), bootFile)
|
|
41
|
+
.replace(/\\/g, '/')
|
|
42
|
+
.replace(/\.tsx?$/, '');
|
|
43
|
+
if (!rel.startsWith('.')) {
|
|
44
|
+
rel = `./${rel}`;
|
|
45
|
+
}
|
|
46
|
+
return `import ${JSON.stringify(rel)};\n`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const absolute = (filename, options) =>
|
|
50
|
+
path.resolve(options?.projectRoot ?? process.cwd(), filename);
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
...upstream,
|
|
54
|
+
transform(args) {
|
|
55
|
+
const file = absolute(args.filename, args.options);
|
|
56
|
+
if (file === entryFile) {
|
|
57
|
+
return upstream.transform({ ...args, src: bootImport() + args.src });
|
|
58
|
+
}
|
|
59
|
+
if (file.startsWith(packageRoot)) {
|
|
60
|
+
return upstream.transform({ ...args, src: inlineEnv(args.src) });
|
|
61
|
+
}
|
|
62
|
+
return upstream.transform(args);
|
|
63
|
+
},
|
|
64
|
+
getCacheKey(...args) {
|
|
65
|
+
const base =
|
|
66
|
+
typeof upstream.getCacheKey === 'function'
|
|
67
|
+
? upstream.getCacheKey(...args)
|
|
68
|
+
: '';
|
|
69
|
+
return `${base}$perflab:${entryFile}:${bootFile}:${envSignature()}`;
|
|
70
|
+
},
|
|
71
|
+
};
|