@axonpack/preview 0.0.0-stage → 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/android/build.gradle +21 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/java/expo/modules/axonpackpreview/AxonpackPreviewModule.kt +72 -0
- package/android/src/main/java/expo/modules/axonpackpreview/BundleLoaderOverride.kt +41 -0
- package/android/src/main/java/expo/modules/axonpackpreview/HermesHeader.kt +24 -0
- package/android/src/main/java/expo/modules/axonpackpreview/PreviewPackage.kt +76 -0
- package/android/src/main/java/expo/modules/axonpackpreview/PreviewState.kt +295 -0
- package/app.plugin.js +112 -0
- package/bin/preview.js +2 -0
- package/build/cli.cjs +1465 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.d.ts.map +1 -0
- package/build/cli.js +164 -0
- package/build/cli.js.map +1 -0
- package/build/components/preview-list/preview-list.component.d.ts +13 -0
- package/build/components/preview-list/preview-list.component.d.ts.map +1 -0
- package/build/components/preview-list/preview-list.component.js +316 -0
- package/build/components/preview-list/preview-list.component.js.map +1 -0
- package/build/components/preview-list/preview-list.types.d.ts +59 -0
- package/build/components/preview-list/preview-list.types.d.ts.map +1 -0
- package/build/components/preview-list/preview-list.types.js +2 -0
- package/build/components/preview-list/preview-list.types.js.map +1 -0
- package/build/components/preview-provider/preview-provider.component.d.ts +12 -0
- package/build/components/preview-provider/preview-provider.component.d.ts.map +1 -0
- package/build/components/preview-provider/preview-provider.component.js +19 -0
- package/build/components/preview-provider/preview-provider.component.js.map +1 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts +26 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts.map +1 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.js +9 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.js.map +1 -0
- package/build/constants/preview-list/theme.const.d.ts +5 -0
- package/build/constants/preview-list/theme.const.d.ts.map +1 -0
- package/build/constants/preview-list/theme.const.js +65 -0
- package/build/constants/preview-list/theme.const.js.map +1 -0
- package/build/index.d.ts +24 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +16 -0
- package/build/index.js.map +1 -0
- package/build/services/cli/config.service.d.ts +13 -0
- package/build/services/cli/config.service.d.ts.map +1 -0
- package/build/services/cli/config.service.js +39 -0
- package/build/services/cli/config.service.js.map +1 -0
- package/build/services/cli/project.service.d.ts +25 -0
- package/build/services/cli/project.service.d.ts.map +1 -0
- package/build/services/cli/project.service.js +81 -0
- package/build/services/cli/project.service.js.map +1 -0
- package/build/services/cli/title.service.d.ts +15 -0
- package/build/services/cli/title.service.d.ts.map +1 -0
- package/build/services/cli/title.service.js +24 -0
- package/build/services/cli/title.service.js.map +1 -0
- package/build/services/dev-preview-servers/discover.service.d.ts +14 -0
- package/build/services/dev-preview-servers/discover.service.d.ts.map +1 -0
- package/build/services/dev-preview-servers/discover.service.js +106 -0
- package/build/services/dev-preview-servers/discover.service.js.map +1 -0
- package/build/services/native/build-info.service.d.ts +11 -0
- package/build/services/native/build-info.service.d.ts.map +1 -0
- package/build/services/native/build-info.service.js +18 -0
- package/build/services/native/build-info.service.js.map +1 -0
- package/build/services/native/native.service.d.ts +24 -0
- package/build/services/native/native.service.d.ts.map +1 -0
- package/build/services/native/native.service.js +13 -0
- package/build/services/native/native.service.js.map +1 -0
- package/build/services/previews/download.service.d.ts +14 -0
- package/build/services/previews/download.service.d.ts.map +1 -0
- package/build/services/previews/download.service.js +66 -0
- package/build/services/previews/download.service.js.map +1 -0
- package/build/services/previews/local-previews.service.d.ts +25 -0
- package/build/services/previews/local-previews.service.d.ts.map +1 -0
- package/build/services/previews/local-previews.service.js +50 -0
- package/build/services/previews/local-previews.service.js.map +1 -0
- package/build/services/previews/reload.service.d.ts +6 -0
- package/build/services/previews/reload.service.d.ts.map +1 -0
- package/build/services/previews/reload.service.js +13 -0
- package/build/services/previews/reload.service.js.map +1 -0
- package/build/services/previews/remote.service.d.ts +11 -0
- package/build/services/previews/remote.service.d.ts.map +1 -0
- package/build/services/previews/remote.service.js +34 -0
- package/build/services/previews/remote.service.js.map +1 -0
- package/build/services/previews/select.service.d.ts +11 -0
- package/build/services/previews/select.service.d.ts.map +1 -0
- package/build/services/previews/select.service.js +25 -0
- package/build/services/previews/select.service.js.map +1 -0
- package/build/services/previews/use-preview.service.d.ts +21 -0
- package/build/services/previews/use-preview.service.d.ts.map +1 -0
- package/build/services/previews/use-preview.service.js +24 -0
- package/build/services/previews/use-preview.service.js.map +1 -0
- package/build/services/previews/use-running.service.d.ts +4 -0
- package/build/services/previews/use-running.service.d.ts.map +1 -0
- package/build/services/previews/use-running.service.js +21 -0
- package/build/services/previews/use-running.service.js.map +1 -0
- package/build/services/publish/bundle.service.d.ts +11 -0
- package/build/services/publish/bundle.service.d.ts.map +1 -0
- package/build/services/publish/bundle.service.js +46 -0
- package/build/services/publish/bundle.service.js.map +1 -0
- package/build/services/publish/commands.service.d.ts +23 -0
- package/build/services/publish/commands.service.d.ts.map +1 -0
- package/build/services/publish/commands.service.js +130 -0
- package/build/services/publish/commands.service.js.map +1 -0
- package/build/services/publish/eas-env.service.d.ts +9 -0
- package/build/services/publish/eas-env.service.d.ts.map +1 -0
- package/build/services/publish/eas-env.service.js +49 -0
- package/build/services/publish/eas-env.service.js.map +1 -0
- package/build/services/registry/registry.service.d.ts +27 -0
- package/build/services/registry/registry.service.d.ts.map +1 -0
- package/build/services/registry/registry.service.js +89 -0
- package/build/services/registry/registry.service.js.map +1 -0
- package/build/services/s3/s3.service.d.ts +59 -0
- package/build/services/s3/s3.service.d.ts.map +1 -0
- package/build/services/s3/s3.service.js +154 -0
- package/build/services/s3/s3.service.js.map +1 -0
- package/build/services/start/start.service.d.ts +48 -0
- package/build/services/start/start.service.d.ts.map +1 -0
- package/build/services/start/start.service.js +172 -0
- package/build/services/start/start.service.js.map +1 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts +21 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts.map +1 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.js +91 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.js.map +1 -0
- package/build/stores/previews/previews.store.d.ts +36 -0
- package/build/stores/previews/previews.store.d.ts.map +1 -0
- package/build/stores/previews/previews.store.js +134 -0
- package/build/stores/previews/previews.store.js.map +1 -0
- package/build/utils/config/config.util.d.ts +39 -0
- package/build/utils/config/config.util.d.ts.map +1 -0
- package/build/utils/config/config.util.js +24 -0
- package/build/utils/config/config.util.js.map +1 -0
- package/build/utils/config/expo-config.util.d.ts +30 -0
- package/build/utils/config/expo-config.util.d.ts.map +1 -0
- package/build/utils/config/expo-config.util.js +43 -0
- package/build/utils/config/expo-config.util.js.map +1 -0
- package/build/utils/dev-preview-servers/merge-servers.util.d.ts +47 -0
- package/build/utils/dev-preview-servers/merge-servers.util.d.ts.map +1 -0
- package/build/utils/dev-preview-servers/merge-servers.util.js +37 -0
- package/build/utils/dev-preview-servers/merge-servers.util.js.map +1 -0
- package/build/utils/preview-list/theme.util.d.ts +5 -0
- package/build/utils/preview-list/theme.util.d.ts.map +1 -0
- package/build/utils/preview-list/theme.util.js +12 -0
- package/build/utils/preview-list/theme.util.js.map +1 -0
- package/build/utils/previews/layout.util.d.ts +89 -0
- package/build/utils/previews/layout.util.d.ts.map +1 -0
- package/build/utils/previews/layout.util.js +30 -0
- package/build/utils/previews/layout.util.js.map +1 -0
- package/build/utils/previews/preview-items.util.d.ts +23 -0
- package/build/utils/previews/preview-items.util.d.ts.map +1 -0
- package/build/utils/previews/preview-items.util.js +39 -0
- package/build/utils/previews/preview-items.util.js.map +1 -0
- package/build/utils/previews/preview-title.util.d.ts +2 -0
- package/build/utils/previews/preview-title.util.d.ts.map +1 -0
- package/build/utils/previews/preview-title.util.js +6 -0
- package/build/utils/previews/preview-title.util.js.map +1 -0
- package/build/utils/previews/running.util.d.ts +23 -0
- package/build/utils/previews/running.util.d.ts.map +1 -0
- package/build/utils/previews/running.util.js +23 -0
- package/build/utils/previews/running.util.js.map +1 -0
- package/build/utils/publish/changelog.util.d.ts +5 -0
- package/build/utils/publish/changelog.util.d.ts.map +1 -0
- package/build/utils/publish/changelog.util.js +19 -0
- package/build/utils/publish/changelog.util.js.map +1 -0
- package/build/utils/publish/indexes.util.d.ts +12 -0
- package/build/utils/publish/indexes.util.d.ts.map +1 -0
- package/build/utils/publish/indexes.util.js +52 -0
- package/build/utils/publish/indexes.util.js.map +1 -0
- package/build/utils/s3/sigv4.util.d.ts +24 -0
- package/build/utils/s3/sigv4.util.d.ts.map +1 -0
- package/build/utils/s3/sigv4.util.js +67 -0
- package/build/utils/s3/sigv4.util.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/AxonpackPreview.podspec +30 -0
- package/ios/AxonpackPreviewModule.swift +55 -0
- package/ios/PreviewReactDelegateHandler.swift +147 -0
- package/ios/PreviewState.swift +236 -0
- package/notes/README.md +36 -0
- package/notes/bundles.md +35 -0
- package/notes/dev-preview-servers.md +67 -0
- package/notes/distribution.md +138 -0
- package/notes/native-loader.md +56 -0
- package/notes/storage.md +145 -0
- package/notes/testing.md +97 -0
- package/package.json +107 -4
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
let previewLog = Logger(subsystem: "dev.axonpack.preview", category: "AxonpackPreview")
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
Which bundle the next start should load. JS writes `state.json` under the app's Documents folder
|
|
9
|
+
(what expo-file-system calls the documents directory), and native only ever reads it. The bundle
|
|
10
|
+
path in it is relative, because the app's container path changes when the app is updated.
|
|
11
|
+
*/
|
|
12
|
+
enum PreviewState {
|
|
13
|
+
static var root: URL? {
|
|
14
|
+
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?
|
|
15
|
+
.appendingPathComponent("preview", isDirectory: true)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Previews need the New Architecture; under the old one only the base bundle ever loads.
|
|
19
|
+
static var newArchitecture: Bool { RCTIsNewArchEnabled() }
|
|
20
|
+
|
|
21
|
+
static func warnOldArchitecture() {
|
|
22
|
+
previewLog.warning("@axonpack/preview needs the New Architecture, so the selected preview is ignored and the base bundle loads")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The selected preview if this app can run it, else nil for the base bundle.
|
|
26
|
+
static func bundleToLoad() -> URL? {
|
|
27
|
+
guard let preview = selectedBundle() else {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
guard madeForThisBuild() else {
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
33
|
+
guard newArchitecture else {
|
|
34
|
+
warnOldArchitecture()
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
if let embedded = embeddedHermesVersion, let version = hermesVersion(of: preview), embedded != version {
|
|
38
|
+
previewLog.warning("The preview is Hermes bytecode \(version) but this build runs \(embedded), so the base bundle loads")
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
guard claimStart() else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
return preview
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The bundle inside the app was compiled by the Hermes this build runs. A debug build has none,
|
|
48
|
+
/// and then nothing is checked.
|
|
49
|
+
static let embeddedHermesVersion: UInt32? = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
50
|
+
.flatMap { hermesVersion(of: $0) }
|
|
51
|
+
|
|
52
|
+
/// Hermes bytecode starts with an 8-byte magic number, then its format version as a 32-bit int.
|
|
53
|
+
/// Nil for anything else, such as a plain JS bundle.
|
|
54
|
+
static func hermesVersion(of url: URL) -> UInt32? {
|
|
55
|
+
guard let handle = try? FileHandle(forReadingFrom: url) else {
|
|
56
|
+
return nil
|
|
57
|
+
}
|
|
58
|
+
defer { try? handle.close() }
|
|
59
|
+
guard let header = try? handle.read(upToCount: 12), header.count == 12 else {
|
|
60
|
+
return nil
|
|
61
|
+
}
|
|
62
|
+
let magic: [UInt8] = [0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f]
|
|
63
|
+
guard Array(header.prefix(8)) == magic else {
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
return header.suffix(4).enumerated().reduce(UInt32(0)) { $0 | UInt32($1.element) << (8 * UInt32($1.offset)) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Crash fallback. Every JS start has its own token. A preview start writes it into the state as
|
|
70
|
+
// `pending`, and JS clears it once the app has mounted. Finding someone else's token means an
|
|
71
|
+
// earlier start of this preview never got that far, so the preview is marked broken and the base
|
|
72
|
+
// bundle loads instead. Several lookups in one start see their own token and change nothing.
|
|
73
|
+
private static let processToken = UUID().uuidString
|
|
74
|
+
private static var starts = 0
|
|
75
|
+
private static var token: String { "\(processToken):\(starts)" }
|
|
76
|
+
|
|
77
|
+
/// Called before a reload, so the next start has a token of its own.
|
|
78
|
+
static func nextStart() {
|
|
79
|
+
starts += 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static var stateURL: URL? { root?.appendingPathComponent("state.json") }
|
|
83
|
+
|
|
84
|
+
private static func readJson() -> [String: Any] {
|
|
85
|
+
guard
|
|
86
|
+
let stateURL,
|
|
87
|
+
let data = try? Data(contentsOf: stateURL),
|
|
88
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
89
|
+
else {
|
|
90
|
+
return [:]
|
|
91
|
+
}
|
|
92
|
+
return json
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static func writeJson(_ json: [String: Any]) {
|
|
96
|
+
guard let root, let stateURL else {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
100
|
+
try? JSONSerialization.data(withJSONObject: json).write(to: stateURL, options: .atomic)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static func claimStart() -> Bool {
|
|
104
|
+
var json = readJson()
|
|
105
|
+
let pending = json["pending"] as? String ?? ""
|
|
106
|
+
if pending == token {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
if !pending.isEmpty {
|
|
110
|
+
let id = (json["bundle"] as? String ?? "").components(separatedBy: "/").first ?? ""
|
|
111
|
+
previewLog.warning("The preview \(id, privacy: .public) stopped before it finished starting, so it is marked broken and the base bundle loads")
|
|
112
|
+
json["broken"] = (json["broken"] as? [String] ?? []) + [id]
|
|
113
|
+
json["bundle"] = nil
|
|
114
|
+
json["pending"] = nil
|
|
115
|
+
writeJson(json)
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
json["pending"] = token
|
|
119
|
+
writeJson(json)
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// The key the config plugin baked into this build; nil without the plugin.
|
|
124
|
+
private static var buildKey: String? {
|
|
125
|
+
Bundle.main.object(forInfoDictionaryKey: "AxonpackPreviewKey") as? String
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// A preview chosen on an earlier build (the app was updated since) is not loaded: with app
|
|
129
|
+
/// version matching, a new version is a new native build. The choice is cleared, so the app and
|
|
130
|
+
/// the list both show the base bundle. Without a key on either side there is nothing to compare.
|
|
131
|
+
private static func madeForThisBuild() -> Bool {
|
|
132
|
+
var json = readJson()
|
|
133
|
+
guard let chosenFor = json["key"] as? String, let build = buildKey, chosenFor != build else {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
previewLog.warning("The chosen preview was made for build \(chosenFor, privacy: .public) but this is \(build, privacy: .public), so the base bundle loads")
|
|
137
|
+
json["bundle"] = nil
|
|
138
|
+
json["key"] = nil
|
|
139
|
+
json["pending"] = nil
|
|
140
|
+
writeJson(json)
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// JS calls this once the app has mounted: this start worked.
|
|
145
|
+
static func confirmStart() {
|
|
146
|
+
var json = readJson()
|
|
147
|
+
if json["pending"] as? String == token {
|
|
148
|
+
json["pending"] = nil
|
|
149
|
+
writeJson(json)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Records the choice for the next start or reload. Nil clears it, so the base bundle loads. The
|
|
154
|
+
/// path is relative to the preview folder and must stay inside it. Choosing a preview again takes
|
|
155
|
+
/// it off the broken list, since it may have been fixed and downloaded again.
|
|
156
|
+
static func select(_ bundle: String?, key: String? = nil) throws {
|
|
157
|
+
guard let root else {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
var json = readJson()
|
|
161
|
+
json["pending"] = nil
|
|
162
|
+
if let bundle {
|
|
163
|
+
guard root.appendingPathComponent(bundle).standardizedFileURL.path.hasPrefix(root.standardizedFileURL.path + "/") else {
|
|
164
|
+
throw PreviewPathOutsideException()
|
|
165
|
+
}
|
|
166
|
+
let id = bundle.components(separatedBy: "/").first ?? ""
|
|
167
|
+
json["bundle"] = bundle
|
|
168
|
+
json["key"] = key
|
|
169
|
+
json["broken"] = (json["broken"] as? [String] ?? []).filter { $0 != id }
|
|
170
|
+
} else {
|
|
171
|
+
json["bundle"] = nil
|
|
172
|
+
json["key"] = nil
|
|
173
|
+
}
|
|
174
|
+
writeJson(json)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Local Metro servers (debug builds only). `localServer` sits next to the preview fields and
|
|
178
|
+
// never changes them, so stopping a local server brings back whatever preview was chosen.
|
|
179
|
+
|
|
180
|
+
/// The chosen local server's bundle URL, or nil; always nil in a release build.
|
|
181
|
+
static var localServerURL: URL? {
|
|
182
|
+
#if DEBUG
|
|
183
|
+
guard
|
|
184
|
+
let server = readJson()["localServer"] as? [String: Any],
|
|
185
|
+
let url = server["url"] as? String
|
|
186
|
+
else {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
return URL(string: url)
|
|
190
|
+
#else
|
|
191
|
+
return nil
|
|
192
|
+
#endif
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static func selectLocalServer(_ url: String?) {
|
|
196
|
+
var json = readJson()
|
|
197
|
+
if let url {
|
|
198
|
+
json["localServer"] = ["url": url]
|
|
199
|
+
} else {
|
|
200
|
+
json["localServer"] = nil
|
|
201
|
+
}
|
|
202
|
+
writeJson(json)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// The raw choice and the broken list, for JS to show.
|
|
206
|
+
static func read() -> [String: Any?] {
|
|
207
|
+
let json = readJson()
|
|
208
|
+
return [
|
|
209
|
+
"bundle": json["bundle"] as? String,
|
|
210
|
+
"broken": json["broken"] as? [String] ?? [],
|
|
211
|
+
"localServer": (json["localServer"] as? [String: Any])?["url"] as? String,
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Nil means "load the base bundle", which is also the answer to anything unexpected.
|
|
216
|
+
static func selectedBundle() -> URL? {
|
|
217
|
+
guard let root, let data = try? Data(contentsOf: root.appendingPathComponent("state.json")) else {
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
220
|
+
guard
|
|
221
|
+
let state = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
222
|
+
let relative = state["bundle"] as? String, !relative.isEmpty
|
|
223
|
+
else {
|
|
224
|
+
previewLog.warning("Could not read the preview state, loading the base bundle")
|
|
225
|
+
return nil
|
|
226
|
+
}
|
|
227
|
+
let bundle = root.appendingPathComponent(relative).standardizedFileURL
|
|
228
|
+
// A path like "../x" must not let the state point outside our own folder.
|
|
229
|
+
let inside = bundle.path.hasPrefix(root.standardizedFileURL.path + "/")
|
|
230
|
+
guard inside, FileManager.default.fileExists(atPath: bundle.path) else {
|
|
231
|
+
previewLog.warning("Selected bundle \(relative, privacy: .public) is missing, loading the base bundle")
|
|
232
|
+
return nil
|
|
233
|
+
}
|
|
234
|
+
return bundle
|
|
235
|
+
}
|
|
236
|
+
}
|
package/notes/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @axonpack/preview notes
|
|
2
|
+
|
|
3
|
+
Scope: this package only. One note per area, and **each one owns its own feature status**: what is
|
|
4
|
+
built, what is not, and what never will be. There is no combined list anywhere; a second copy would
|
|
5
|
+
only disagree with these.
|
|
6
|
+
|
|
7
|
+
| Note | Covers |
|
|
8
|
+
| -------------------------------------------------- | ----------------------------------------------------------------- |
|
|
9
|
+
| [bundles.md](./bundles.md) | Building a preview bundle, and which native build it can run on |
|
|
10
|
+
| [native-loader.md](./native-loader.md) | Starting the app from a downloaded bundle, and getting back again |
|
|
11
|
+
| [distribution.md](./distribution.md) | Publishing a preview so installed apps can find it |
|
|
12
|
+
| [storage.md](./storage.md) | Where previews are kept, and running a local S3 to test with |
|
|
13
|
+
| [testing.md](./testing.md) | Running the example end to end on your own machine |
|
|
14
|
+
| [dev-preview-servers.md](./dev-preview-servers.md) | Several local Metro servers for one app, switched live (debug) |
|
|
15
|
+
|
|
16
|
+
To run the example end to end without a cloud account, follow [testing.md](./testing.md).
|
|
17
|
+
|
|
18
|
+
## How to edit an area note
|
|
19
|
+
|
|
20
|
+
- **Features first**, as a checklist: `- [x]` for shipped, `- [ ]` for not built yet. One
|
|
21
|
+
plain-English line each, no sub-lists, no paragraphs.
|
|
22
|
+
- **Describe a feature from the outside**: what someone using the package gets. No file names, no
|
|
23
|
+
function or option names, and no explanation of how it works. That belongs in the prose below the
|
|
24
|
+
checklist and in the code comments.
|
|
25
|
+
- **Prose after the checklist** carries the design: how it works, and the decisions that constrain
|
|
26
|
+
it. Why, not what.
|
|
27
|
+
- **Won't do goes last**, for what the platform makes impossible. No checkboxes there, because nothing in
|
|
28
|
+
it is waiting to be built. Move a line there instead of deleting it, so the reason survives. If
|
|
29
|
+
something becomes possible, move it back up as `- [ ]`.
|
|
30
|
+
- **A feature belongs to exactly one note.** If two notes would list it, one of them is wrong.
|
|
31
|
+
- New work goes in the note for its area; only a genuinely new surface earns a new note, and a new
|
|
32
|
+
note earns a row in the table above.
|
|
33
|
+
|
|
34
|
+
The writing rules that apply to every note in the repo, and how the symlinks under
|
|
35
|
+
`notes/@axonpack/` work, are in
|
|
36
|
+
[the repo's notes guide](https://github.com/axonpack/axonpack/blob/main/notes/README.md).
|
package/notes/bundles.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Bundles
|
|
2
|
+
|
|
3
|
+
Building a preview bundle, and which native build it can run on.
|
|
4
|
+
|
|
5
|
+
- [x] Builds the same release bundle and images the native build would embed, for Android and iOS
|
|
6
|
+
- [x] Names a preview after its branch, with a readable title QA sees
|
|
7
|
+
- [x] Marks every preview with the native build it was made for, so an app only ever offers ones it can run
|
|
8
|
+
- [x] Lets the developer match previews to builds by app version (the default) or by native fingerprint
|
|
9
|
+
|
|
10
|
+
A preview is the output of `expo export:embed --bytecode`: Hermes bytecode plus the images next to
|
|
11
|
+
it, in the folders the native build uses. React Native finds images beside a bundle loaded from a
|
|
12
|
+
file, so nothing in JS has to map them.
|
|
13
|
+
|
|
14
|
+
Bytecode runs only on a build with the same native code and the same Hermes version. Each build
|
|
15
|
+
gets a key, and the app lists only previews published under its own key. The developer picks how
|
|
16
|
+
the key is made, with one option in the app config that the build and `publish` both read:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
"plugins": [["@axonpack/preview", { "compatibility": "appVersion" }]]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| `compatibility` | Key | Pick it when |
|
|
23
|
+
| ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `appVersion` (default) | `version-<app version>` | You bump the version for every native change. Simple, and readable in the bucket |
|
|
25
|
+
| `fingerprint` | `fingerprint-<hash>` | Native changes can land without a version bump. Exact, the same hash expo-updates' fingerprint policy uses |
|
|
26
|
+
|
|
27
|
+
The trade-off of `appVersion`: a native change without a version bump is not detected, so a preview
|
|
28
|
+
built on the old native code is still offered. Two things catch most of the damage: a preview whose
|
|
29
|
+
Hermes bytecode version differs from the build's is not loaded, and a preview that fails while
|
|
30
|
+
starting falls back to the base bundle and is marked broken. `publish` prints a one-line warning in
|
|
31
|
+
this mode. With `fingerprint`, any native change (a new native dependency, a config plugin change,
|
|
32
|
+
a prebuild with different settings) makes older previews incompatible.
|
|
33
|
+
|
|
34
|
+
A preview chosen on one build is not started by a later build with another key; the app starts the
|
|
35
|
+
base bundle and the download moves to Incompatible.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Dev preview servers
|
|
2
|
+
|
|
3
|
+
Several Metro servers for the same app, one per task or worktree, and one debug build that switches
|
|
4
|
+
between them live.
|
|
5
|
+
|
|
6
|
+
- [x] Lists every Metro server for this app on the developer's computer, in a debug build
|
|
7
|
+
- [x] Finds plain `expo start` servers, and servers started with `preview start` with their branch
|
|
8
|
+
- [x] Shows a new server within about two seconds, and drops one that stopped
|
|
9
|
+
- [x] Leaves out other apps' servers; keeps the same app built for other native code, marked
|
|
10
|
+
- [x] Switches the running app to a server, and back, with no rebuild
|
|
11
|
+
- [x] Starts Metro on a free port and announces it with one command, `preview start`
|
|
12
|
+
- [x] Asks for the name the app shows, prefilled from the branch (`--title` or no terminal skips it)
|
|
13
|
+
- [x] Off unless the app turns it on, and never in a release build
|
|
14
|
+
- [x] Only this app's servers, by the slug the config plugin bakes in; a build without it says so
|
|
15
|
+
|
|
16
|
+
## Turning it on
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
<PreviewProvider config={{ ...config, devPreviewServers: true }}>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It is one switch, off by default, and only shows in a debug build. Nothing else is set: the app probes
|
|
23
|
+
ports 8081 to 8099, asks the registry on port 19150 (the same constant `preview start` uses, in
|
|
24
|
+
`src/constants/dev-preview-servers/dev-preview-servers.const.ts`), looks on the host its own Metro is on (plus `10.0.2.2` on the
|
|
25
|
+
Android emulator), and lists only servers for the slug the config plugin baked into the build. A
|
|
26
|
+
build made without the plugin has no slug, and the section says so instead of listing anything.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
**Running it.** In the example, `bun run start:preview` (or `bunx preview start`).
|
|
31
|
+
|
|
32
|
+
**The name.** In a terminal, `preview start` asks for the name the app's list shows, prefilled with
|
|
33
|
+
the branch's readable title (`task-1-login-fix` becomes "Task 1 login fix"). Enter keeps it, or edit
|
|
34
|
+
it first. `--title <name>` skips the question, and so does a run with no terminal (CI, an AI agent),
|
|
35
|
+
which takes the branch title. `publish` asks the same way for the title QA sees.
|
|
36
|
+
|
|
37
|
+
**Two sources.** `preview start` runs `expo start` on a free port and announces it to a small
|
|
38
|
+
registry on the computer (started by the first `start`, gone once no server is left), with the
|
|
39
|
+
branch, worktree and the native key `publish` would use. Heartbeats every 5 s keep it listed; it
|
|
40
|
+
drops out 15 s after the last, so a killed process goes too. Plain `expo start` servers are found
|
|
41
|
+
by probing the port range for Metro's `/status`; each one's Expo manifest then gives its slug,
|
|
42
|
+
name, version and bundle URL. A server found both ways uses the registry's details. While the list
|
|
43
|
+
is on screen the app looks every 2 s, and never starts a look while the last one runs. A probe gives
|
|
44
|
+
up after 2 s: Android sends 5 requests per host at a time and the emulator takes about 400 ms to
|
|
45
|
+
refuse a closed port, so a tighter limit dropped live servers there. A closed port on the computer
|
|
46
|
+
itself fails at once, so the scan stays cheap.
|
|
47
|
+
|
|
48
|
+
**Which servers.** Only the app's own slug, which the config plugin bakes into the build. A server
|
|
49
|
+
for the same app but another native key (another app version, or another fingerprint) is listed and
|
|
50
|
+
marked, since a developer may still want it. A plain server in fingerprint mode shows "native build
|
|
51
|
+
unknown", because its manifest does not carry a fingerprint.
|
|
52
|
+
|
|
53
|
+
**Reaching it.** The app probes the host its own Metro is on. On the Android emulator it also tries
|
|
54
|
+
`10.0.2.2`, which reaches the computer with no `adb reverse`. A phone on USB needs `adb reverse`,
|
|
55
|
+
which `preview start` runs for its port and the registry's when `adb` is on the PATH. A phone
|
|
56
|
+
on Wi-Fi was started from the computer's LAN address, so the same rule finds the others.
|
|
57
|
+
|
|
58
|
+
**Switching.** The choice is stored next to the preview choice and never changes it. On every start
|
|
59
|
+
of a debug build the dev preview server wins, then a downloaded preview, then the default Metro. Android
|
|
60
|
+
points React Native's dev support at the server (and puts the developer's own dev-menu host back
|
|
61
|
+
afterwards); iOS starts from the server's bundle URL. "Stop using dev preview server" brings back what ran
|
|
62
|
+
before, a chosen preview included; "Back to base" stops both.
|
|
63
|
+
|
|
64
|
+
## Won't do
|
|
65
|
+
|
|
66
|
+
- Run in a release build. There is no Metro there, and a release build ignores the setting even if it
|
|
67
|
+
is in the saved state.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Distribution
|
|
2
|
+
|
|
3
|
+
Publishing a preview so installed apps can find it.
|
|
4
|
+
|
|
5
|
+
- [x] Publishes a preview from the developer's machine with one command
|
|
6
|
+
- [x] Replaces a preview when the same branch is published again
|
|
7
|
+
- [x] Gives every preview a changelog, from the branch's commits unless one is passed in
|
|
8
|
+
- [x] Keeps one list per native build for the app, and one list of everything for people
|
|
9
|
+
- [x] Removes a preview, and cleans up files nothing uses any more
|
|
10
|
+
- [x] Runs against a local S3 server while developing
|
|
11
|
+
|
|
12
|
+
Previews go to an S3-compatible bucket: S3, R2, MinIO or any other host with the S3 API. The app
|
|
13
|
+
gets the endpoint, the bucket and, for a private bucket, a read-only key from the config it passes
|
|
14
|
+
in.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
Run from the app's root (`--project <dir>` otherwise). `preview --help` lists every option.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx preview publish # id = git branch, both platforms; asks for the title
|
|
22
|
+
npx preview publish --id task-1 --changelog="- Fixes the login"
|
|
23
|
+
npx preview unpublish task-1
|
|
24
|
+
npx preview prune
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`publish` builds with `expo export:embed --dev false --bytecode`, the same step a release build
|
|
28
|
+
runs, so the bundle is Hermes bytecode with its images beside it. It reads the `compatibility`
|
|
29
|
+
option from the app config (see [bundles.md](./bundles.md)), builds the same key the app was built
|
|
30
|
+
with, records the app version and the native fingerprint either way, uploads each file once under
|
|
31
|
+
its sha256, writes the preview's manifest, and only then adds it to the lists. `--compatibility`
|
|
32
|
+
overrides the option, with a warning, since a build of the app would then not list the preview. An app never sees a preview whose files
|
|
33
|
+
are not all there. The changelog is the commit subjects since the branch left the base branch
|
|
34
|
+
(`--base`, default: the repo's default branch); `--changelog=<text>` or `--changelog-file` replace it.
|
|
35
|
+
|
|
36
|
+
### EAS environment
|
|
37
|
+
|
|
38
|
+
`--environment <name>` builds with an EAS environment's variables, like `eas update --environment`:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx preview publish --environment preview
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It runs `eas env:pull --environment <name> --non-interactive` into a temp folder outside the app,
|
|
45
|
+
reads the file and deletes it. The values go into the environment of everything publish runs that
|
|
46
|
+
reads the app config: the config itself (app version, slug), the fingerprint and `expo
|
|
47
|
+
export:embed`, so the key matches a build made in that environment. A variable already set in the
|
|
48
|
+
shell wins over the EAS one. Without the flag nothing changes and eas-cli is never called.
|
|
49
|
+
|
|
50
|
+
It needs eas-cli installed, a login (`eas login`, or `EXPO_TOKEN` in CI) and the app linked to an
|
|
51
|
+
EAS project (`eas init`); publish says which one is missing. Variables with **secret** visibility
|
|
52
|
+
are never readable outside EAS servers: `eas env:pull` leaves them out (as a comment), so set them
|
|
53
|
+
in the shell if the bundle needs them. Plain text and sensitive ones come through. A file-type
|
|
54
|
+
variable is written by `eas env:pull` itself into the app's `.eas/.env/` folder, and its value is
|
|
55
|
+
that file's path.
|
|
56
|
+
|
|
57
|
+
## CI
|
|
58
|
+
|
|
59
|
+
[`ci/github-actions/`](../ci/github-actions/) has a sample GitHub Actions workflow for each
|
|
60
|
+
package manager. Copy the one that matches your app's lockfile into `.github/workflows/`; GitHub
|
|
61
|
+
does not run them from this package.
|
|
62
|
+
|
|
63
|
+
| Lockfile | File to copy |
|
|
64
|
+
| ------------------- | ----------------------------------------------------------- |
|
|
65
|
+
| `bun.lock` | [`preview.bun.yml`](../ci/github-actions/preview.bun.yml) |
|
|
66
|
+
| `package-lock.json` | [`preview.npm.yml`](../ci/github-actions/preview.npm.yml) |
|
|
67
|
+
| `pnpm-lock.yaml` | [`preview.pnpm.yml`](../ci/github-actions/preview.pnpm.yml) |
|
|
68
|
+
| `yarn.lock` | [`preview.yarn.yml`](../ci/github-actions/preview.yarn.yml) |
|
|
69
|
+
|
|
70
|
+
All four have the same jobs; only the setup, install and run steps differ. The yarn file uses
|
|
71
|
+
`yarn install --immutable` (Yarn 2 and later); for Yarn 1 change it to `--frozen-lockfile`. It
|
|
72
|
+
publishes each pull request when it is opened, pushed to or reopened, with the branch name as the
|
|
73
|
+
id and the pull request's base branch for the changelog. Pull requests from forks get no secrets,
|
|
74
|
+
so it only works for branches in the repo.
|
|
75
|
+
|
|
76
|
+
Repository secrets it reads:
|
|
77
|
+
|
|
78
|
+
| Secret | Needed |
|
|
79
|
+
| ------------------------------ | ----------------------------------------------- |
|
|
80
|
+
| `PREVIEW_S3_URL` | yes |
|
|
81
|
+
| `PREVIEW_S3_ACCESS_KEY_ID` | yes |
|
|
82
|
+
| `PREVIEW_S3_SECRET_ACCESS_KEY` | yes |
|
|
83
|
+
| `PREVIEW_S3_REGION` | no, default `us-east-1` |
|
|
84
|
+
| `EXPO_TOKEN` | only for `--environment` (an Expo access token) |
|
|
85
|
+
|
|
86
|
+
Without EAS variables, drop the `expo/expo-github-action` step, `EXPO_TOKEN` and `EAS_ENVIRONMENT`.
|
|
87
|
+
|
|
88
|
+
### Run by hand
|
|
89
|
+
|
|
90
|
+
Each workflow also has a `workflow_dispatch` trigger, for publishing any branch, or removing a
|
|
91
|
+
preview, without a pull request. Run it from the Actions tab
|
|
92
|
+
([GitHub's guide](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow)). Its inputs:
|
|
93
|
+
|
|
94
|
+
| Input | Values | Meaning |
|
|
95
|
+
| ------------- | ------------------------------------------- | ------------------------------------------------------------- |
|
|
96
|
+
| `action` | `publish` (default), `unpublish` | Which job runs |
|
|
97
|
+
| `id` | any, optional | The preview id. Empty means the branch the run was started on |
|
|
98
|
+
| `platform` | `all` (default), `android`, `ios` | Publish only |
|
|
99
|
+
| `environment` | `preview` (default), or any EAS environment | Passed as `--environment`. Empty means no `--environment` |
|
|
100
|
+
|
|
101
|
+
A dispatched publish counts the changelog from the repository's default branch. A pull request
|
|
102
|
+
run always uses `--environment preview`.
|
|
103
|
+
|
|
104
|
+
### Unpublish on close (optional)
|
|
105
|
+
|
|
106
|
+
When a pull request closes, merged or not, the `unpublish` job removes its preview from every
|
|
107
|
+
build's list. The job is optional. To keep previews after close, do one of these in your copy of
|
|
108
|
+
the workflow:
|
|
109
|
+
|
|
110
|
+
- remove `closed` from the trigger's `types` (a dispatch can still unpublish)
|
|
111
|
+
- delete the `unpublish` job
|
|
112
|
+
- change the job's condition to `if: false`
|
|
113
|
+
|
|
114
|
+
The last two also turn off unpublishing by dispatch.
|
|
115
|
+
|
|
116
|
+
Then remove previews by hand with `preview unpublish <id>`, and delete the files no preview uses
|
|
117
|
+
any more with `preview prune`.
|
|
118
|
+
|
|
119
|
+
## Layout
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
index.json every native build and its previews, for people
|
|
123
|
+
index/<platform>/<key>.json what one native build can run; the app reads this
|
|
124
|
+
previews/<platform>/<key>/<id>.json one preview's manifest: files, changelog, versions
|
|
125
|
+
files/<sha256> bundle and image blobs, shared between previews
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`<key>` is `version-1.0.0` or `fingerprint-<hash>`. Every manifest and index entry also records the
|
|
129
|
+
compatibility setting, the app version and the native fingerprint, so `index.json` can group builds
|
|
130
|
+
by version whichever setting made them.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Both lists are read, changed and written back only if nobody wrote in between (`If-Match` on the
|
|
137
|
+
ETag, or `If-None-Match: *` for a new file). A clash reads again and retries, so two publishes at
|
|
138
|
+
once both end up listed. `unpublish` leaves files in place; `prune` deletes the ones no manifest uses.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Native loader
|
|
2
|
+
|
|
3
|
+
Starting the app from a downloaded bundle, and getting back again.
|
|
4
|
+
|
|
5
|
+
- [ ] Starts the app from the chosen preview, with no change to the app's native code
|
|
6
|
+
- [ ] Switches to another preview or back to the base build with one restart
|
|
7
|
+
- [ ] Works in debug builds too: a chosen preview loads instead of Metro, and going back to the base build returns to Metro
|
|
8
|
+
- [x] Falls back to the base build when a preview crashes before it has started
|
|
9
|
+
- [ ] Works next to expo-updates, which keeps managing the base build
|
|
10
|
+
- [ ] Needs the New Architecture; under the old one it logs a warning and always loads the base build
|
|
11
|
+
|
|
12
|
+
Previews need React Native's New Architecture, the default from Expo SDK 53. Under the old one the
|
|
13
|
+
package never loads a preview: it logs one warning and the base bundle loads, so the app still
|
|
14
|
+
starts.
|
|
15
|
+
|
|
16
|
+
In a debug build the base is whatever Metro serves. While a preview is chosen it loads from the
|
|
17
|
+
device and Metro is not asked; going back to the base reloads from Metro.
|
|
18
|
+
|
|
19
|
+
If a preview fails before the app has mounted, it is marked broken and the base bundle loads. On
|
|
20
|
+
Android that happens straight away: the app restarts into the base bundle. On iOS the failed preview
|
|
21
|
+
leaves a blank screen until the app is closed and opened again, and that next start is the base
|
|
22
|
+
bundle. Errors that do not come from a starting preview are thrown on as usual, so the app behaves as
|
|
23
|
+
it would without this package.
|
|
24
|
+
|
|
25
|
+
With no preview chosen, the package changes nothing at startup: expo-updates, or the bundle inside
|
|
26
|
+
the app, loads as usual.
|
|
27
|
+
|
|
28
|
+
expo-updates is placed ahead of every other module on both platforms, so this package is never
|
|
29
|
+
asked first. On Android a chosen preview is also put straight into React's host, in the private
|
|
30
|
+
field expo-dev-launcher uses, so it wins even after expo-updates has applied a downloaded update; if
|
|
31
|
+
that field is missing, the base loads and a warning says why. On iOS, while expo-updates is active,
|
|
32
|
+
the base bundle starts and the app reloads once straight into the preview. Going back to the base
|
|
33
|
+
hands the start back to expo-updates.
|
|
34
|
+
|
|
35
|
+
## Testing next to expo-updates
|
|
36
|
+
|
|
37
|
+
The example leaves expo-updates out, so its default setup stays small. To try the two together:
|
|
38
|
+
|
|
39
|
+
1. In `example/`, run `npx expo install expo-updates`.
|
|
40
|
+
2. Add to `app.json` under `expo`, so expo-updates is on in release builds but never asks the
|
|
41
|
+
network and always starts its embedded update:
|
|
42
|
+
```json
|
|
43
|
+
"runtimeVersion": "1.0.0",
|
|
44
|
+
"updates": {
|
|
45
|
+
"url": "https://localhost.invalid/preview-example",
|
|
46
|
+
"checkAutomatically": "NEVER",
|
|
47
|
+
"fallbackToCacheTimeout": 0
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
3. `npx expo prebuild`, then a release build: `npx expo run:android --variant release` or
|
|
51
|
+
`npx expo run:ios --configuration Release`.
|
|
52
|
+
4. With no preview chosen, the app shows the embedded bundle. Choose a preview: it loads, and again
|
|
53
|
+
after closing and reopening the app. Go back to the base: the embedded bundle loads.
|
|
54
|
+
5. To check the case where expo-updates has applied a downloaded update, point `updates.url` at a
|
|
55
|
+
real update server (EAS Update, or your own), publish an update, let the app apply it, then
|
|
56
|
+
repeat step 4. Going back to the base should load that update.
|