@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,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