@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.
Files changed (182) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -2
  4. package/android/build.gradle +21 -0
  5. package/android/consumer-rules.pro +3 -0
  6. package/android/src/main/java/expo/modules/axonpackpreview/AxonpackPreviewModule.kt +72 -0
  7. package/android/src/main/java/expo/modules/axonpackpreview/BundleLoaderOverride.kt +41 -0
  8. package/android/src/main/java/expo/modules/axonpackpreview/HermesHeader.kt +24 -0
  9. package/android/src/main/java/expo/modules/axonpackpreview/PreviewPackage.kt +76 -0
  10. package/android/src/main/java/expo/modules/axonpackpreview/PreviewState.kt +295 -0
  11. package/app.plugin.js +112 -0
  12. package/bin/preview.js +2 -0
  13. package/build/cli.cjs +1465 -0
  14. package/build/cli.d.ts +3 -0
  15. package/build/cli.d.ts.map +1 -0
  16. package/build/cli.js +164 -0
  17. package/build/cli.js.map +1 -0
  18. package/build/components/preview-list/preview-list.component.d.ts +13 -0
  19. package/build/components/preview-list/preview-list.component.d.ts.map +1 -0
  20. package/build/components/preview-list/preview-list.component.js +316 -0
  21. package/build/components/preview-list/preview-list.component.js.map +1 -0
  22. package/build/components/preview-list/preview-list.types.d.ts +59 -0
  23. package/build/components/preview-list/preview-list.types.d.ts.map +1 -0
  24. package/build/components/preview-list/preview-list.types.js +2 -0
  25. package/build/components/preview-list/preview-list.types.js.map +1 -0
  26. package/build/components/preview-provider/preview-provider.component.d.ts +12 -0
  27. package/build/components/preview-provider/preview-provider.component.d.ts.map +1 -0
  28. package/build/components/preview-provider/preview-provider.component.js +19 -0
  29. package/build/components/preview-provider/preview-provider.component.js.map +1 -0
  30. package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts +26 -0
  31. package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts.map +1 -0
  32. package/build/constants/dev-preview-servers/dev-preview-servers.const.js +9 -0
  33. package/build/constants/dev-preview-servers/dev-preview-servers.const.js.map +1 -0
  34. package/build/constants/preview-list/theme.const.d.ts +5 -0
  35. package/build/constants/preview-list/theme.const.d.ts.map +1 -0
  36. package/build/constants/preview-list/theme.const.js +65 -0
  37. package/build/constants/preview-list/theme.const.js.map +1 -0
  38. package/build/index.d.ts +24 -0
  39. package/build/index.d.ts.map +1 -0
  40. package/build/index.js +16 -0
  41. package/build/index.js.map +1 -0
  42. package/build/services/cli/config.service.d.ts +13 -0
  43. package/build/services/cli/config.service.d.ts.map +1 -0
  44. package/build/services/cli/config.service.js +39 -0
  45. package/build/services/cli/config.service.js.map +1 -0
  46. package/build/services/cli/project.service.d.ts +25 -0
  47. package/build/services/cli/project.service.d.ts.map +1 -0
  48. package/build/services/cli/project.service.js +81 -0
  49. package/build/services/cli/project.service.js.map +1 -0
  50. package/build/services/cli/title.service.d.ts +15 -0
  51. package/build/services/cli/title.service.d.ts.map +1 -0
  52. package/build/services/cli/title.service.js +24 -0
  53. package/build/services/cli/title.service.js.map +1 -0
  54. package/build/services/dev-preview-servers/discover.service.d.ts +14 -0
  55. package/build/services/dev-preview-servers/discover.service.d.ts.map +1 -0
  56. package/build/services/dev-preview-servers/discover.service.js +106 -0
  57. package/build/services/dev-preview-servers/discover.service.js.map +1 -0
  58. package/build/services/native/build-info.service.d.ts +11 -0
  59. package/build/services/native/build-info.service.d.ts.map +1 -0
  60. package/build/services/native/build-info.service.js +18 -0
  61. package/build/services/native/build-info.service.js.map +1 -0
  62. package/build/services/native/native.service.d.ts +24 -0
  63. package/build/services/native/native.service.d.ts.map +1 -0
  64. package/build/services/native/native.service.js +13 -0
  65. package/build/services/native/native.service.js.map +1 -0
  66. package/build/services/previews/download.service.d.ts +14 -0
  67. package/build/services/previews/download.service.d.ts.map +1 -0
  68. package/build/services/previews/download.service.js +66 -0
  69. package/build/services/previews/download.service.js.map +1 -0
  70. package/build/services/previews/local-previews.service.d.ts +25 -0
  71. package/build/services/previews/local-previews.service.d.ts.map +1 -0
  72. package/build/services/previews/local-previews.service.js +50 -0
  73. package/build/services/previews/local-previews.service.js.map +1 -0
  74. package/build/services/previews/reload.service.d.ts +6 -0
  75. package/build/services/previews/reload.service.d.ts.map +1 -0
  76. package/build/services/previews/reload.service.js +13 -0
  77. package/build/services/previews/reload.service.js.map +1 -0
  78. package/build/services/previews/remote.service.d.ts +11 -0
  79. package/build/services/previews/remote.service.d.ts.map +1 -0
  80. package/build/services/previews/remote.service.js +34 -0
  81. package/build/services/previews/remote.service.js.map +1 -0
  82. package/build/services/previews/select.service.d.ts +11 -0
  83. package/build/services/previews/select.service.d.ts.map +1 -0
  84. package/build/services/previews/select.service.js +25 -0
  85. package/build/services/previews/select.service.js.map +1 -0
  86. package/build/services/previews/use-preview.service.d.ts +21 -0
  87. package/build/services/previews/use-preview.service.d.ts.map +1 -0
  88. package/build/services/previews/use-preview.service.js +24 -0
  89. package/build/services/previews/use-preview.service.js.map +1 -0
  90. package/build/services/previews/use-running.service.d.ts +4 -0
  91. package/build/services/previews/use-running.service.d.ts.map +1 -0
  92. package/build/services/previews/use-running.service.js +21 -0
  93. package/build/services/previews/use-running.service.js.map +1 -0
  94. package/build/services/publish/bundle.service.d.ts +11 -0
  95. package/build/services/publish/bundle.service.d.ts.map +1 -0
  96. package/build/services/publish/bundle.service.js +46 -0
  97. package/build/services/publish/bundle.service.js.map +1 -0
  98. package/build/services/publish/commands.service.d.ts +23 -0
  99. package/build/services/publish/commands.service.d.ts.map +1 -0
  100. package/build/services/publish/commands.service.js +130 -0
  101. package/build/services/publish/commands.service.js.map +1 -0
  102. package/build/services/publish/eas-env.service.d.ts +9 -0
  103. package/build/services/publish/eas-env.service.d.ts.map +1 -0
  104. package/build/services/publish/eas-env.service.js +49 -0
  105. package/build/services/publish/eas-env.service.js.map +1 -0
  106. package/build/services/registry/registry.service.d.ts +27 -0
  107. package/build/services/registry/registry.service.d.ts.map +1 -0
  108. package/build/services/registry/registry.service.js +89 -0
  109. package/build/services/registry/registry.service.js.map +1 -0
  110. package/build/services/s3/s3.service.d.ts +59 -0
  111. package/build/services/s3/s3.service.d.ts.map +1 -0
  112. package/build/services/s3/s3.service.js +154 -0
  113. package/build/services/s3/s3.service.js.map +1 -0
  114. package/build/services/start/start.service.d.ts +48 -0
  115. package/build/services/start/start.service.d.ts.map +1 -0
  116. package/build/services/start/start.service.js +172 -0
  117. package/build/services/start/start.service.js.map +1 -0
  118. package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts +21 -0
  119. package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts.map +1 -0
  120. package/build/stores/dev-preview-servers/dev-preview-servers.store.js +91 -0
  121. package/build/stores/dev-preview-servers/dev-preview-servers.store.js.map +1 -0
  122. package/build/stores/previews/previews.store.d.ts +36 -0
  123. package/build/stores/previews/previews.store.d.ts.map +1 -0
  124. package/build/stores/previews/previews.store.js +134 -0
  125. package/build/stores/previews/previews.store.js.map +1 -0
  126. package/build/utils/config/config.util.d.ts +39 -0
  127. package/build/utils/config/config.util.d.ts.map +1 -0
  128. package/build/utils/config/config.util.js +24 -0
  129. package/build/utils/config/config.util.js.map +1 -0
  130. package/build/utils/config/expo-config.util.d.ts +30 -0
  131. package/build/utils/config/expo-config.util.d.ts.map +1 -0
  132. package/build/utils/config/expo-config.util.js +43 -0
  133. package/build/utils/config/expo-config.util.js.map +1 -0
  134. package/build/utils/dev-preview-servers/merge-servers.util.d.ts +47 -0
  135. package/build/utils/dev-preview-servers/merge-servers.util.d.ts.map +1 -0
  136. package/build/utils/dev-preview-servers/merge-servers.util.js +37 -0
  137. package/build/utils/dev-preview-servers/merge-servers.util.js.map +1 -0
  138. package/build/utils/preview-list/theme.util.d.ts +5 -0
  139. package/build/utils/preview-list/theme.util.d.ts.map +1 -0
  140. package/build/utils/preview-list/theme.util.js +12 -0
  141. package/build/utils/preview-list/theme.util.js.map +1 -0
  142. package/build/utils/previews/layout.util.d.ts +89 -0
  143. package/build/utils/previews/layout.util.d.ts.map +1 -0
  144. package/build/utils/previews/layout.util.js +30 -0
  145. package/build/utils/previews/layout.util.js.map +1 -0
  146. package/build/utils/previews/preview-items.util.d.ts +23 -0
  147. package/build/utils/previews/preview-items.util.d.ts.map +1 -0
  148. package/build/utils/previews/preview-items.util.js +39 -0
  149. package/build/utils/previews/preview-items.util.js.map +1 -0
  150. package/build/utils/previews/preview-title.util.d.ts +2 -0
  151. package/build/utils/previews/preview-title.util.d.ts.map +1 -0
  152. package/build/utils/previews/preview-title.util.js +6 -0
  153. package/build/utils/previews/preview-title.util.js.map +1 -0
  154. package/build/utils/previews/running.util.d.ts +23 -0
  155. package/build/utils/previews/running.util.d.ts.map +1 -0
  156. package/build/utils/previews/running.util.js +23 -0
  157. package/build/utils/previews/running.util.js.map +1 -0
  158. package/build/utils/publish/changelog.util.d.ts +5 -0
  159. package/build/utils/publish/changelog.util.d.ts.map +1 -0
  160. package/build/utils/publish/changelog.util.js +19 -0
  161. package/build/utils/publish/changelog.util.js.map +1 -0
  162. package/build/utils/publish/indexes.util.d.ts +12 -0
  163. package/build/utils/publish/indexes.util.d.ts.map +1 -0
  164. package/build/utils/publish/indexes.util.js +52 -0
  165. package/build/utils/publish/indexes.util.js.map +1 -0
  166. package/build/utils/s3/sigv4.util.d.ts +24 -0
  167. package/build/utils/s3/sigv4.util.d.ts.map +1 -0
  168. package/build/utils/s3/sigv4.util.js +67 -0
  169. package/build/utils/s3/sigv4.util.js.map +1 -0
  170. package/expo-module.config.json +10 -0
  171. package/ios/AxonpackPreview.podspec +30 -0
  172. package/ios/AxonpackPreviewModule.swift +55 -0
  173. package/ios/PreviewReactDelegateHandler.swift +147 -0
  174. package/ios/PreviewState.swift +236 -0
  175. package/notes/README.md +36 -0
  176. package/notes/bundles.md +35 -0
  177. package/notes/dev-preview-servers.md +67 -0
  178. package/notes/distribution.md +138 -0
  179. package/notes/native-loader.md +56 -0
  180. package/notes/storage.md +145 -0
  181. package/notes/testing.md +97 -0
  182. package/package.json +107 -4
@@ -0,0 +1,295 @@
1
+ package expo.modules.axonpackpreview
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager
5
+ import android.preference.PreferenceManager
6
+ import android.util.Log
7
+ import com.facebook.react.ReactHost
8
+ import com.facebook.react.common.build.ReactBuildConfig
9
+ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
10
+ import com.facebook.react.packagerconnection.PackagerConnectionSettings
11
+ import java.io.File
12
+ import java.util.UUID
13
+ import org.json.JSONArray
14
+ import org.json.JSONObject
15
+
16
+ internal const val TAG = "AxonpackPreview"
17
+
18
+ /**
19
+ * Which bundle the next start should load. JS writes `state.json` under the folder expo-file-system
20
+ * calls the documents directory, and native only ever reads it. The bundle path in it is relative,
21
+ * so the state still holds if the app's files folder moves.
22
+ */
23
+ internal object PreviewState {
24
+ fun root(context: Context) = File(context.filesDir, "preview")
25
+
26
+ /**
27
+ * Previews need the New Architecture. Anything else, including a React Native too old to answer,
28
+ * counts as the old one, which only ever gets the base bundle.
29
+ */
30
+ fun newArchitecture(): Boolean =
31
+ try {
32
+ ReactNativeFeatureFlags.enableBridgelessArchitecture()
33
+ } catch (e: Throwable) {
34
+ false
35
+ }
36
+
37
+ fun warnOldArchitecture() =
38
+ Log.w(TAG, "@axonpack/preview needs the New Architecture, so the selected preview is ignored and the base bundle loads")
39
+
40
+ /** The selected preview if this app can run it, else null for the base bundle. */
41
+ fun bundleToLoad(context: Context): File? {
42
+ val bundle = selectedBundle(context) ?: return null
43
+ if (!madeForThisBuild(context)) return null
44
+ if (!newArchitecture()) {
45
+ warnOldArchitecture()
46
+ return null
47
+ }
48
+ val embedded = embeddedHermesVersion(context)
49
+ val preview = bundle.inputStream().use { HermesHeader.version(it) }
50
+ if (embedded != null && preview != null && embedded != preview) {
51
+ Log.w(TAG, "The preview is Hermes bytecode $preview but this build runs $embedded, so the base bundle loads")
52
+ return null
53
+ }
54
+ if (!claimStart(context)) return null
55
+ return bundle
56
+ }
57
+
58
+ // The bundle inside the app was compiled by the Hermes this build runs. A debug build has none,
59
+ // and then nothing is checked.
60
+ private var embeddedVersion: Int? = null
61
+ private var embeddedChecked = false
62
+
63
+ private fun embeddedHermesVersion(context: Context): Int? {
64
+ if (!embeddedChecked) {
65
+ embeddedVersion = try {
66
+ context.assets.open("index.android.bundle").use { HermesHeader.version(it) }
67
+ } catch (e: Exception) {
68
+ null
69
+ }
70
+ embeddedChecked = true
71
+ }
72
+ return embeddedVersion
73
+ }
74
+
75
+ /**
76
+ * A debug build asks Metro before the bundle loader, unless its dev support has a bundle file set.
77
+ * So in a debug build the selected preview goes there, and clearing it hands the start back to
78
+ * Metro. Release builds never ask Metro and are left alone.
79
+ */
80
+ fun pointDevSupport(context: Context, host: ReactHost?) {
81
+ if (!ReactBuildConfig.DEBUG || host == null) return
82
+ val devSupport = host.devSupportManager ?: return
83
+ val settings = devSupport.devSettings?.packagerConnectionSettings
84
+ val local = localServerHostPort(context)
85
+ if (local != null && settings != null) {
86
+ // A local Metro server wins over a downloaded preview and the default Metro (debug only).
87
+ saveDebugHost(context)
88
+ settings.debugServerHost = local
89
+ devSupport.bundleFilePath = null
90
+ return
91
+ }
92
+ if (settings != null) restoreDebugHost(context, settings)
93
+ devSupport.bundleFilePath = bundleToLoad(context)?.absolutePath
94
+ }
95
+
96
+ // Local Metro servers (debug builds only). `localServer` sits next to the preview fields and
97
+ // never changes them, so stopping a local server brings back whatever preview was chosen.
98
+
99
+ /** The chosen local server as `host:port`, or null; always null in a release build. */
100
+ fun localServerHostPort(context: Context): String? {
101
+ if (!ReactBuildConfig.DEBUG) return null
102
+ return readJson(context).optJSONObject("localServer")?.optString("hostPort")?.ifEmpty { null }
103
+ }
104
+
105
+ fun selectLocalServer(context: Context, url: String?, hostPort: String?) {
106
+ val json = readJson(context)
107
+ if (url == null || hostPort == null) {
108
+ json.remove("localServer")
109
+ } else {
110
+ json.put("localServer", JSONObject().put("url", url).put("hostPort", hostPort))
111
+ }
112
+ writeJson(context, json)
113
+ }
114
+
115
+ // The dev menu's "Change bundle location" lives in the same preference, so the developer's own
116
+ // value is kept aside while a local server is in use, and put back when it stops.
117
+ private const val DEBUG_HOST_PREF = "debug_http_host"
118
+
119
+ private fun saveDebugHost(context: Context) {
120
+ val json = readJson(context)
121
+ if (json.has("savedDebugHost")) return
122
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
123
+ json.put("savedDebugHost", prefs.getString(DEBUG_HOST_PREF, null) ?: "")
124
+ writeJson(context, json)
125
+ }
126
+
127
+ private fun restoreDebugHost(context: Context, settings: PackagerConnectionSettings) {
128
+ val json = readJson(context)
129
+ if (!json.has("savedDebugHost")) return
130
+ val saved = json.optString("savedDebugHost")
131
+ if (saved.isEmpty()) settings.resetDebugServerHost() else settings.debugServerHost = saved
132
+ json.remove("savedDebugHost")
133
+ writeJson(context, json)
134
+ }
135
+
136
+ // Crash fallback. Every JS start has its own token. A preview start writes it into the state as
137
+ // `pending`, and JS clears it once the app has mounted. Finding someone else's token means an
138
+ // earlier start of this preview never got that far, so the preview is marked broken and the base
139
+ // bundle loads instead. Several lookups in one start see their own token and change nothing.
140
+ private val processToken = UUID.randomUUID().toString()
141
+ private var starts = 0
142
+ private fun token() = "$processToken:$starts"
143
+
144
+ /** Called before a reload, so the next start has a token of its own. */
145
+ fun nextStart() {
146
+ starts++
147
+ }
148
+
149
+ private fun stateFile(context: Context) = File(root(context), "state.json")
150
+
151
+ private fun readJson(context: Context): JSONObject =
152
+ try {
153
+ val file = stateFile(context)
154
+ if (file.isFile) JSONObject(file.readText()) else JSONObject()
155
+ } catch (e: Exception) {
156
+ JSONObject()
157
+ }
158
+
159
+ private fun writeJson(context: Context, json: JSONObject) {
160
+ root(context).mkdirs()
161
+ stateFile(context).writeText(json.toString())
162
+ }
163
+
164
+ private fun brokenIds(json: JSONObject): List<String> {
165
+ val array = json.optJSONArray("broken") ?: return emptyList()
166
+ return (0 until array.length()).map { array.getString(it) }
167
+ }
168
+
169
+ private fun claimStart(context: Context): Boolean {
170
+ val json = readJson(context)
171
+ val pending = json.optString("pending")
172
+ if (pending == token()) return true
173
+ if (pending.isNotEmpty()) {
174
+ val id = json.optString("bundle").substringBefore("/")
175
+ Log.w(TAG, "The preview \"$id\" stopped before it finished starting, so it is marked broken and the base bundle loads")
176
+ json.put("broken", JSONArray(brokenIds(json) + id))
177
+ json.remove("bundle")
178
+ json.remove("pending")
179
+ writeJson(context, json)
180
+ return false
181
+ }
182
+ json.put("pending", token())
183
+ writeJson(context, json)
184
+ return true
185
+ }
186
+
187
+ /**
188
+ * The running start threw before JS confirmed it. True when that start was a preview: it is then
189
+ * marked broken and taken off the selection, so the next start is the base bundle.
190
+ */
191
+ fun failStart(context: Context): Boolean {
192
+ val json = readJson(context)
193
+ val bundle = json.optString("bundle")
194
+ if (bundle.isEmpty() || json.optString("pending") != token()) return false
195
+ val id = bundle.substringBefore("/")
196
+ Log.w(TAG, "The preview \"$id\" failed while starting, so it is marked broken and the base bundle loads")
197
+ json.put("broken", JSONArray(brokenIds(json) + id))
198
+ json.remove("bundle")
199
+ json.remove("pending")
200
+ writeJson(context, json)
201
+ return true
202
+ }
203
+
204
+ /** JS calls this once the app has mounted: this start worked. */
205
+ fun confirmStart(context: Context) {
206
+ val json = readJson(context)
207
+ if (json.optString("pending") == token()) {
208
+ json.remove("pending")
209
+ writeJson(context, json)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Records the choice for the next start or reload. Null clears it, so the base bundle loads. The
215
+ * path is relative to the preview folder and must stay inside it. Choosing a preview again takes
216
+ * it off the broken list, since it may have been fixed and downloaded again.
217
+ */
218
+ /** The key the config plugin baked into this build; null without the plugin. */
219
+ private fun buildKey(context: Context): String? =
220
+ try {
221
+ context.packageManager
222
+ .getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
223
+ .metaData
224
+ ?.getString("dev.axonpack.preview.KEY")
225
+ } catch (e: Exception) {
226
+ null
227
+ }
228
+
229
+ /**
230
+ * A preview chosen on an earlier build (the app was updated since) is not loaded: with app
231
+ * version matching, a new version is a new native build. The choice is cleared, so the app and
232
+ * the list both show the base bundle. Without a key on either side there is nothing to compare.
233
+ */
234
+ private fun madeForThisBuild(context: Context): Boolean {
235
+ val json = readJson(context)
236
+ val chosenFor = json.optString("key")
237
+ val build = buildKey(context)
238
+ if (chosenFor.isEmpty() || build == null || chosenFor == build) return true
239
+ Log.w(TAG, "The chosen preview was made for build $chosenFor but this is $build, so the base bundle loads")
240
+ json.remove("bundle")
241
+ json.remove("key")
242
+ json.remove("pending")
243
+ writeJson(context, json)
244
+ return false
245
+ }
246
+
247
+ fun select(context: Context, bundle: String?, key: String? = null) {
248
+ val root = root(context)
249
+ val json = readJson(context)
250
+ json.remove("pending")
251
+ if (bundle == null) {
252
+ json.remove("bundle")
253
+ json.remove("key")
254
+ } else {
255
+ require(File(root, bundle).canonicalPath.startsWith(root.canonicalPath + File.separator)) {
256
+ "The bundle path must stay inside the preview folder"
257
+ }
258
+ json.put("bundle", bundle)
259
+ if (key != null) json.put("key", key) else json.remove("key")
260
+ json.put("broken", JSONArray(brokenIds(json) - bundle.substringBefore("/")))
261
+ }
262
+ writeJson(context, json)
263
+ }
264
+
265
+ /** The raw choice and the broken list, for JS to show. */
266
+ fun read(context: Context): Map<String, Any?> {
267
+ val json = readJson(context)
268
+ return mapOf(
269
+ "bundle" to json.optString("bundle").ifEmpty { null },
270
+ "broken" to brokenIds(json),
271
+ "localServer" to json.optJSONObject("localServer")?.optString("url")?.ifEmpty { null },
272
+ )
273
+ }
274
+
275
+ /** Null means "load the base bundle", which is also the answer to anything unexpected. */
276
+ fun selectedBundle(context: Context): File? {
277
+ return try {
278
+ val root = root(context)
279
+ val state = File(root, "state.json")
280
+ if (!state.isFile) return null
281
+ val relative = JSONObject(state.readText()).optString("bundle")
282
+ if (relative.isEmpty()) return null
283
+ val bundle = File(root, relative)
284
+ // A path like "../x" must not let the state point outside our own folder.
285
+ if (!bundle.canonicalPath.startsWith(root.canonicalPath + File.separator) || !bundle.isFile) {
286
+ Log.w(TAG, "Selected bundle \"$relative\" is missing, loading the base bundle")
287
+ return null
288
+ }
289
+ bundle
290
+ } catch (e: Exception) {
291
+ Log.w(TAG, "Could not read the preview state, loading the base bundle", e)
292
+ null
293
+ }
294
+ }
295
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,112 @@
1
+ // Bakes the build's compatibility setting and key into the app at prebuild, so the app asks the
2
+ // bucket for exactly the previews made for its own native build. `preview publish` reads the
3
+ // same option from the same app config and builds the same key, so the two cannot disagree.
4
+ //
5
+ // plugins: [["@axonpack/preview", { compatibility: "appVersion" | "fingerprint" }]]
6
+ //
7
+ // appVersion (the default) keys on the app version; fingerprint on the native fingerprint.
8
+ const { createRequire } = require("node:module");
9
+ const { join } = require("node:path");
10
+ const {
11
+ AndroidConfig,
12
+ createRunOncePlugin,
13
+ withAndroidManifest,
14
+ withInfoPlist,
15
+ } = require("expo/config-plugins");
16
+
17
+ const pkg = require("./package.json");
18
+
19
+ const PLIST = {
20
+ compatibility: "AxonpackPreviewCompatibility",
21
+ key: "AxonpackPreviewKey",
22
+ slug: "AxonpackPreviewSlug",
23
+ };
24
+ const META = {
25
+ compatibility: "dev.axonpack.preview.COMPATIBILITY",
26
+ key: "dev.axonpack.preview.KEY",
27
+ slug: "dev.axonpack.preview.SLUG",
28
+ };
29
+
30
+ // Kept in step with src/utils/previews/layout.util.ts and src/utils/config/expo-config.util.ts by a test.
31
+ const compatibilityOf = (options) => {
32
+ const value = options?.compatibility ?? "appVersion";
33
+ if (value !== "appVersion" && value !== "fingerprint") {
34
+ throw new Error(
35
+ `${pkg.name}: compatibility must be "appVersion" or "fingerprint"`,
36
+ );
37
+ }
38
+ return value;
39
+ };
40
+ const appVersionOf = (config, platform) =>
41
+ String(config[platform]?.version ?? config.version ?? "");
42
+ const compatibilityKey = (compatibility, value) =>
43
+ `${compatibility === "appVersion" ? "version" : "fingerprint"}-${String(value).replace(/[^A-Za-z0-9._-]/g, "_")}`;
44
+
45
+ async function fingerprintOf(projectRoot, platform) {
46
+ const expo = createRequire(join(projectRoot, "package.json")).resolve(
47
+ "expo/package.json",
48
+ );
49
+ const { createFingerprintAsync } = createRequire(expo)("@expo/fingerprint");
50
+ const { hash } = await createFingerprintAsync(projectRoot, {
51
+ platforms: [platform],
52
+ silent: true,
53
+ });
54
+ return hash;
55
+ }
56
+
57
+ async function keyFor(config, projectRoot, platform, compatibility) {
58
+ const value =
59
+ compatibility === "appVersion"
60
+ ? appVersionOf(config, platform)
61
+ : await fingerprintOf(projectRoot, platform);
62
+ return compatibilityKey(compatibility, value);
63
+ }
64
+
65
+ const withPreview = (config, options = {}) => {
66
+ const compatibility = compatibilityOf(options);
67
+ config = withInfoPlist(config, async (mod) => {
68
+ mod.modResults[PLIST.compatibility] = compatibility;
69
+ mod.modResults[PLIST.key] = await keyFor(
70
+ mod,
71
+ mod.modRequest.projectRoot,
72
+ "ios",
73
+ compatibility,
74
+ );
75
+ // Tells this app's local Metro servers apart from other projects' (notes/dev-preview-servers.md).
76
+ mod.modResults[PLIST.slug] = mod.slug;
77
+ return mod;
78
+ });
79
+ config = withAndroidManifest(config, async (mod) => {
80
+ const application = AndroidConfig.Manifest.getMainApplicationOrThrow(
81
+ mod.modResults,
82
+ );
83
+ const key = await keyFor(
84
+ mod,
85
+ mod.modRequest.projectRoot,
86
+ "android",
87
+ compatibility,
88
+ );
89
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
90
+ application,
91
+ META.compatibility,
92
+ compatibility,
93
+ );
94
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
95
+ application,
96
+ META.key,
97
+ key,
98
+ );
99
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
100
+ application,
101
+ META.slug,
102
+ mod.slug,
103
+ );
104
+ return mod;
105
+ });
106
+ return config;
107
+ };
108
+
109
+ module.exports = createRunOncePlugin(withPreview, pkg.name, pkg.version);
110
+ module.exports.compatibilityOf = compatibilityOf;
111
+ module.exports.appVersionOf = appVersionOf;
112
+ module.exports.compatibilityKey = compatibilityKey;
package/bin/preview.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require("../build/cli.cjs");