feat: add support for single-app mode (#817)
This commit is contained in:
Родитель
7426548911
Коммит
a17eb12e80
|
@ -93,6 +93,9 @@ android {
|
|||
"ReactTestApp_recommendedFlipperVersion",
|
||||
recommendedFlipperVersion ? "\"${recommendedFlipperVersion}\"" : "\"0\""
|
||||
|
||||
def singleApp = getSingleAppMode()
|
||||
buildConfigField "String", "ReactTestApp_singleApp", singleApp ? "\"${singleApp}\"" : "null"
|
||||
|
||||
resValue "string", "app_name", project.ext.react.appName
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
@ -64,30 +64,51 @@ class MainActivity : ReactActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
didInitialNavigation = savedInstanceState?.getBoolean("didInitialNavigation", false) == true
|
||||
|
||||
val (manifest, checksum) = testApp.manifestProvider.fromResources()
|
||||
val components = manifest.components ?: listOf()
|
||||
if (components.count() > 0) {
|
||||
val index = if (components.count() == 1) 0 else session.lastOpenedComponent(checksum)
|
||||
index?.let {
|
||||
val component = newComponentViewModel(components[it])
|
||||
val startInitialComponent = { _: ReactContext ->
|
||||
if (!didInitialNavigation) {
|
||||
startComponent(component)
|
||||
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
when {
|
||||
BuildConfig.ReactTestApp_singleApp === null -> {
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
didInitialNavigation =
|
||||
savedInstanceState?.getBoolean("didInitialNavigation", false) == true
|
||||
|
||||
if (components.count() > 0) {
|
||||
val index =
|
||||
if (components.count() == 1) 0 else session.lastOpenedComponent(checksum)
|
||||
index?.let {
|
||||
val component = newComponentViewModel(components[it])
|
||||
val startInitialComponent = { _: ReactContext ->
|
||||
if (!didInitialNavigation) {
|
||||
startComponent(component)
|
||||
}
|
||||
}
|
||||
testApp.reactNativeHost.apply {
|
||||
addReactInstanceEventListener(startInitialComponent)
|
||||
reactInstanceManager.currentReactContext?.let(startInitialComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
testApp.reactNativeHost.apply {
|
||||
addReactInstanceEventListener(startInitialComponent)
|
||||
reactInstanceManager.currentReactContext?.let(startInitialComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupToolbar(manifest.displayName)
|
||||
setupRecyclerView(components, checksum)
|
||||
setupToolbar(manifest.displayName)
|
||||
setupRecyclerView(components, checksum)
|
||||
}
|
||||
|
||||
components.count() > 0 -> {
|
||||
val slug = BuildConfig.ReactTestApp_singleApp
|
||||
val component = components.find { it.slug == slug }
|
||||
?: throw IllegalArgumentException("No component with slug: $slug")
|
||||
val intent = ComponentActivity.newIntent(this, newComponentViewModel(component))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("At least one component must be declared")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
|
|
@ -51,11 +51,14 @@ class ComponentActivity : ReactActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (BuildConfig.ReactTestApp_singleApp === null) {
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val componentName = intent.extras?.getString(COMPONENT_NAME, null)
|
||||
?: throw IllegalArgumentException("Class name has to be provided.")
|
||||
?: throw IllegalArgumentException("Component name must be provided.")
|
||||
title = intent.extras?.getString(COMPONENT_DISPLAY_NAME, componentName)
|
||||
|
||||
findClass(componentName)?.let {
|
||||
|
|
|
@ -12,6 +12,7 @@ data class Component(
|
|||
val displayName: String?,
|
||||
val initialProperties: Bundle?,
|
||||
val presentationStyle: String?,
|
||||
val slug: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
@ -19,5 +20,6 @@ data class Manifest(
|
|||
val name: String,
|
||||
val displayName: String,
|
||||
val bundleRoot: String?,
|
||||
val singleApp: String?,
|
||||
val components: List<Component>?,
|
||||
)
|
||||
|
|
|
@ -2,6 +2,8 @@ import groovy.json.JsonSlurper
|
|||
|
||||
import java.nio.file.Paths
|
||||
|
||||
ext.manifest = null
|
||||
|
||||
ext.buildReactNativeFromSource = { baseDir ->
|
||||
def reactNativePath = findNodeModulesPath(baseDir, "react-native")
|
||||
return !file("${reactNativePath}/android").exists()
|
||||
|
@ -62,11 +64,9 @@ ext.findNodeModulesPath = { baseDir, packageName ->
|
|||
return null
|
||||
}
|
||||
|
||||
ext.getAppName = { baseDir ->
|
||||
def manifestFile = findFile("app.json")
|
||||
if (manifestFile != null) {
|
||||
def manifest = new JsonSlurper().parseText(manifestFile.text)
|
||||
|
||||
ext.getAppName = {
|
||||
def manifest = getManifest()
|
||||
if (manifest != null) {
|
||||
def displayName = manifest["displayName"]
|
||||
if (displayName instanceof String) {
|
||||
return displayName
|
||||
|
@ -81,10 +81,9 @@ ext.getAppName = { baseDir ->
|
|||
return "ReactTestApp"
|
||||
}
|
||||
|
||||
ext.getApplicationId = { baseDir ->
|
||||
def manifestFile = findFile("app.json")
|
||||
if (manifestFile != null) {
|
||||
def manifest = new JsonSlurper().parseText(manifestFile.text)
|
||||
ext.getApplicationId = {
|
||||
def manifest = getManifest()
|
||||
if (manifest != null) {
|
||||
def config = manifest["android"]
|
||||
if (config instanceof Object && config.containsKey("package")) {
|
||||
return config["package"]
|
||||
|
@ -121,6 +120,18 @@ ext.getFlipperVersion = { baseDir ->
|
|||
return recommendedFlipperVersion
|
||||
}
|
||||
|
||||
ext.getManifest = {
|
||||
if (manifest == null) {
|
||||
def manifestFile = findFile("app.json")
|
||||
if (manifestFile == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
manifest = new JsonSlurper().parseText(manifestFile.text)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
ext.getReactNativeVersionNumber = { baseDir ->
|
||||
def reactNativePath = findNodeModulesPath(baseDir, "react-native")
|
||||
def packageJson = file("${reactNativePath}/package.json")
|
||||
|
@ -129,7 +140,7 @@ ext.getReactNativeVersionNumber = { baseDir ->
|
|||
return (major as int) * 10000 + (minor as int) * 100 + (patch as int)
|
||||
}
|
||||
|
||||
ext.getSigningConfigs = { baseDir ->
|
||||
ext.getSigningConfigs = {
|
||||
def safeSetMap = { varName, map, prop, defaultVal ->
|
||||
map[varName] = prop.containsKey(varName) ? prop.get(varName) : defaultVal
|
||||
}
|
||||
|
@ -168,6 +179,18 @@ ext.getSigningConfigs = { baseDir ->
|
|||
return definedConfigs
|
||||
}
|
||||
|
||||
ext.getSingleAppMode = {
|
||||
def manifest = getManifest()
|
||||
if (manifest != null) {
|
||||
def singleApp = manifest["singleApp"]
|
||||
if (singleApp instanceof String) {
|
||||
return singleApp
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
ext.isFabricEnabled = { baseDir ->
|
||||
return project.hasProperty("USE_FABRIC") &&
|
||||
project.getProperty("USE_FABRIC") == "1" &&
|
||||
|
|
|
@ -41,19 +41,22 @@ class ManifestTests: XCTestCase {
|
|||
appKey: "0",
|
||||
displayName: nil,
|
||||
initialProperties: ["key": "value"],
|
||||
presentationStyle: nil
|
||||
presentationStyle: nil,
|
||||
slug: nil
|
||||
),
|
||||
Component(
|
||||
appKey: "1",
|
||||
displayName: "1",
|
||||
initialProperties: nil,
|
||||
presentationStyle: nil
|
||||
presentationStyle: nil,
|
||||
slug: nil
|
||||
),
|
||||
]
|
||||
let expected = Manifest(
|
||||
name: "Name",
|
||||
displayName: "Display Name",
|
||||
bundleRoot: nil,
|
||||
singleApp: nil,
|
||||
components: expectedComponents
|
||||
)
|
||||
|
||||
|
|
|
@ -544,6 +544,6 @@ SPEC CHECKSUMS:
|
|||
Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
PODFILE CHECKSUM: 3a904fc37e625c7d2f8f76787663057cf8ad41d8
|
||||
PODFILE CHECKSUM: bcdb8c6a896b4276dbcf172c76964cd24abde5ce
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -37,8 +37,8 @@ final class ContentViewController: UITableViewController {
|
|||
private let reactInstance: ReactInstance
|
||||
private var sections: [SectionData]
|
||||
|
||||
public init() {
|
||||
reactInstance = ReactInstance()
|
||||
public init(reactInstance: ReactInstance) {
|
||||
self.reactInstance = reactInstance
|
||||
sections = []
|
||||
|
||||
super.init(style: .grouped)
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>msauth.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
|
|
@ -8,10 +8,17 @@ extension Component {
|
|||
case displayName
|
||||
case initialProperties
|
||||
case presentationStyle
|
||||
case slug
|
||||
}
|
||||
|
||||
init(appKey: String) {
|
||||
self.init(appKey: appKey, displayName: nil, initialProperties: nil, presentationStyle: nil)
|
||||
self.init(
|
||||
appKey: appKey,
|
||||
displayName: nil,
|
||||
initialProperties: nil,
|
||||
presentationStyle: nil,
|
||||
slug: nil
|
||||
)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
|
@ -25,6 +32,7 @@ extension Component {
|
|||
return try? [AnyHashable: Any].decode(from: decoder)
|
||||
}()
|
||||
presentationStyle = try container.decodeIfPresent(String.self, forKey: .presentationStyle)
|
||||
slug = try container.decodeIfPresent(String.self, forKey: .slug)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,11 +47,13 @@ extension Manifest {
|
|||
}
|
||||
|
||||
static func from(data: Data) -> (Manifest, String)? {
|
||||
guard let manifest = try? JSONDecoder().decode(self, from: data) else {
|
||||
do {
|
||||
let manifest = try JSONDecoder().decode(self, from: data)
|
||||
return (manifest, data.sha256)
|
||||
} catch {
|
||||
assertionFailure("Failed to load manifest: \(error)")
|
||||
return nil
|
||||
}
|
||||
|
||||
return (manifest, data.sha256)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@ struct Component: Decodable {
|
|||
let displayName: String?
|
||||
let initialProperties: [AnyHashable: Any]?
|
||||
let presentationStyle: String?
|
||||
let slug: String?
|
||||
}
|
||||
|
||||
struct Manifest: Decodable {
|
||||
let name: String
|
||||
let displayName: String
|
||||
let bundleRoot: String?
|
||||
let singleApp: String?
|
||||
let components: [Component]?
|
||||
}
|
||||
|
|
|
@ -10,8 +10,12 @@
|
|||
|
||||
@class RCTBridge;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
FOUNDATION_EXTERN RTAView *RTACreateReactRootView(RCTBridge *,
|
||||
NSString *moduleName,
|
||||
NSDictionary *initialProperties);
|
||||
NSDictionary *_Nullable initialProperties);
|
||||
|
||||
FOUNDATION_EXTERN NSObject *RTACreateSurfacePresenterBridgeAdapter(RCTBridge *);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -238,3 +238,36 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
|
|||
remoteBundleURL = urlComponents.url
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
typealias RTAView = UIView
|
||||
#else
|
||||
typealias RTAView = NSView
|
||||
#endif
|
||||
|
||||
func createReactRootView(_ reactInstance: ReactInstance) -> (RTAView, String)? {
|
||||
guard let (manifest, _) = Manifest.fromFile(),
|
||||
let slug = manifest.singleApp
|
||||
else {
|
||||
assertionFailure("Failed to load manifest")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let component = manifest.components?.first(where: { $0.slug == slug }) else {
|
||||
assertionFailure("Failed to find component with slug: \(slug)")
|
||||
return nil
|
||||
}
|
||||
|
||||
reactInstance.initReact(bundleRoot: manifest.bundleRoot) {}
|
||||
guard let bridge = reactInstance.bridge else {
|
||||
assertionFailure("Failed to initialize React")
|
||||
return nil
|
||||
}
|
||||
|
||||
let view = RTACreateReactRootView(
|
||||
bridge,
|
||||
component.appKey,
|
||||
component.initialProperties
|
||||
)
|
||||
return (view, component.displayName ?? component.appKey)
|
||||
}
|
||||
|
|
|
@ -1,35 +1,10 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var isRunningTests: Bool {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
return environment["XCInjectBundleInto"] != nil
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene,
|
||||
willConnectTo _: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions)
|
||||
{
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene
|
||||
// `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see
|
||||
// `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
guard !isRunningTests else {
|
||||
return
|
||||
}
|
||||
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UINavigationController(rootViewController: ContentViewController())
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
private lazy var reactInstance = ReactInstance()
|
||||
|
||||
func sceneDidDisconnect(_: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
|
@ -79,6 +54,79 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Multi-app extensions
|
||||
|
||||
#if !ENABLE_SINGLE_APP_MODE
|
||||
|
||||
extension SceneDelegate {
|
||||
var isRunningTests: Bool {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
return environment["XCInjectBundleInto"] != nil
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene,
|
||||
willConnectTo _: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions)
|
||||
{
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene
|
||||
// `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see
|
||||
// `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
guard !isRunningTests else {
|
||||
return
|
||||
}
|
||||
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UINavigationController(
|
||||
rootViewController: ContentViewController(reactInstance: reactInstance)
|
||||
)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !ENABLE_SINGLE_APP_MODE
|
||||
|
||||
// MARK: - Single-app extensions
|
||||
|
||||
#if ENABLE_SINGLE_APP_MODE
|
||||
|
||||
extension SceneDelegate {
|
||||
func scene(_ scene: UIScene,
|
||||
willConnectTo _: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions)
|
||||
{
|
||||
guard let windowScene = scene as? UIWindowScene else {
|
||||
assertionFailure("Default scene configuration should have been loaded by now")
|
||||
return
|
||||
}
|
||||
|
||||
guard let (rootView, _) = createReactRootView(reactInstance) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
rootView.backgroundColor = UIColor.systemBackground
|
||||
|
||||
let viewController = UIViewController(nibName: nil, bundle: nil)
|
||||
viewController.view = rootView
|
||||
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = viewController
|
||||
self.window = window
|
||||
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ENABLE_SINGLE_APP_MODE
|
||||
|
||||
// MARK: - UIScene.OpenURLOptions extensions
|
||||
|
||||
extension UIScene.OpenURLOptions {
|
||||
func dictionary() -> [UIApplication.OpenURLOptionsKey: Any] {
|
||||
var options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
|
|
|
@ -18,11 +18,11 @@ def app_manifest(project_root)
|
|||
@app_manifest[project_root] = JSON.parse(File.read(manifest_path))
|
||||
end
|
||||
|
||||
def app_name(project_root)
|
||||
def app_config(project_root)
|
||||
manifest = app_manifest(project_root)
|
||||
return [nil, nil] if manifest.nil?
|
||||
return [nil, nil, nil] if manifest.nil?
|
||||
|
||||
[manifest['name'], manifest['displayName']]
|
||||
[manifest['name'], manifest['displayName'], manifest['singleApp']]
|
||||
end
|
||||
|
||||
def autolink_script_path
|
||||
|
@ -156,16 +156,16 @@ def use_react_native!(project_root, target_platform, options)
|
|||
end
|
||||
|
||||
def make_project!(xcodeproj, project_root, target_platform, options)
|
||||
src_xcodeproj = project_path(xcodeproj, target_platform)
|
||||
xcodeproj_src = project_path(xcodeproj, target_platform)
|
||||
destination = File.join(nearest_node_modules(project_root), '.generated', target_platform.to_s)
|
||||
dst_xcodeproj = File.join(destination, xcodeproj)
|
||||
xcodeproj_dst = File.join(destination, xcodeproj)
|
||||
|
||||
# Copy Xcode project files
|
||||
FileUtils.mkdir_p(destination)
|
||||
FileUtils.cp_r(src_xcodeproj, destination)
|
||||
name, display_name = app_name(project_root)
|
||||
FileUtils.cp_r(xcodeproj_src, destination)
|
||||
name, display_name, single_app = app_config(project_root)
|
||||
unless name.nil?
|
||||
xcschemes_path = File.join(dst_xcodeproj, 'xcshareddata', 'xcschemes')
|
||||
xcschemes_path = File.join(xcodeproj_dst, 'xcshareddata', 'xcschemes')
|
||||
FileUtils.cp(File.join(xcschemes_path, 'ReactTestApp.xcscheme'),
|
||||
File.join(xcschemes_path, "#{name}.xcscheme"))
|
||||
end
|
||||
|
@ -182,6 +182,32 @@ def make_project!(xcodeproj, project_root, target_platform, options)
|
|||
FileUtils.ln_sf(source, shared_path) unless File.exist?(shared_path)
|
||||
end
|
||||
|
||||
# Copy localization files and replace instances of `ReactTestApp` with app display name
|
||||
product_name = display_name || name
|
||||
product_name = if product_name.is_a? String
|
||||
product_name
|
||||
else
|
||||
target.name
|
||||
end
|
||||
localizations_src = project_path('Localizations', target_platform)
|
||||
if File.exist?(localizations_src)
|
||||
main_strings = 'Main.strings'
|
||||
localizations_dst = File.join(destination, 'Localizations')
|
||||
|
||||
Dir.entries(localizations_src).each do |entry|
|
||||
next if entry.start_with?('.')
|
||||
|
||||
lproj = File.join(localizations_dst, entry)
|
||||
FileUtils.mkdir_p(lproj)
|
||||
|
||||
File.open(File.join(lproj, main_strings), 'w') do |f|
|
||||
File.foreach(File.join(localizations_src, entry, main_strings)) do |line|
|
||||
f.write(line.sub('ReactTestApp', product_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
react_native = react_native_path(project_root, target_platform)
|
||||
version = package_version(react_native.to_s).segments
|
||||
version = (version[0] * 10_000) + (version[1] * 100) + version[2]
|
||||
|
@ -207,17 +233,12 @@ def make_project!(xcodeproj, project_root, target_platform, options)
|
|||
build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = product_bundle_identifier
|
||||
end
|
||||
|
||||
product_name = display_name || name
|
||||
build_settings['PRODUCT_DISPLAY_NAME'] = if product_name.is_a? String
|
||||
product_name
|
||||
else
|
||||
target.name
|
||||
end
|
||||
build_settings['PRODUCT_DISPLAY_NAME'] = display_name
|
||||
|
||||
supports_flipper = target_platform == :ios && flipper_enabled?
|
||||
use_fabric = options[:fabric_enabled] && version >= 6800
|
||||
|
||||
app_project = Xcodeproj::Project.open(dst_xcodeproj)
|
||||
app_project = Xcodeproj::Project.open(xcodeproj_dst)
|
||||
app_project.native_targets.each do |target|
|
||||
next if target.name != 'ReactTestApp'
|
||||
|
||||
|
@ -242,13 +263,16 @@ def make_project!(xcodeproj, project_root, target_platform, options)
|
|||
config.build_settings['OTHER_SWIFT_FLAGS'] << '-DFB_SONARKIT_ENABLED'
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] << '-DUSE_FLIPPER'
|
||||
end
|
||||
if single_app.is_a? String
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] << '-DENABLE_SINGLE_APP_MODE'
|
||||
end
|
||||
end
|
||||
end
|
||||
app_project.save
|
||||
|
||||
config = app_project.build_configurations[0]
|
||||
{
|
||||
:xcodeproj_path => dst_xcodeproj,
|
||||
:xcodeproj_path => xcodeproj_dst,
|
||||
:platforms => {
|
||||
:ios => config.resolve_build_setting('IPHONEOS_DEPLOYMENT_TARGET'),
|
||||
:macos => config.resolve_build_setting('MACOSX_DEPLOYMENT_TARGET'),
|
||||
|
@ -262,7 +286,7 @@ def use_test_app_internal!(target_platform, options)
|
|||
xcodeproj = 'ReactTestApp.xcodeproj'
|
||||
project_root = Pod::Config.instance.installation_root
|
||||
project_target = make_project!(xcodeproj, project_root, target_platform, options)
|
||||
dst_xcodeproj, platforms = project_target.values_at(:xcodeproj_path, :platforms)
|
||||
xcodeproj_dst, platforms = project_target.values_at(:xcodeproj_path, :platforms)
|
||||
|
||||
require_relative(autolink_script_path)
|
||||
|
||||
|
@ -273,7 +297,7 @@ def use_test_app_internal!(target_platform, options)
|
|||
# Allow platform deployment target to be overridden
|
||||
end
|
||||
|
||||
project dst_xcodeproj
|
||||
project xcodeproj_dst
|
||||
|
||||
react_native_post_install = nil
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/* Class = "NSMenuItem"; title = "ReactTestApp"; ObjectID = "1Xt-HY-uBw"; */
|
||||
"1Xt-HY-uBw.title" = "ReactTestApp";
|
||||
|
||||
/* Class = "NSMenuItem"; title = "Quit ReactTestApp"; ObjectID = "4sb-4s-VLi"; */
|
||||
"4sb-4s-VLi.title" = "Quit ReactTestApp";
|
||||
|
||||
/* Class = "NSMenuItem"; title = "About ReactTestApp"; ObjectID = "5kV-Vb-QxS"; */
|
||||
"5kV-Vb-QxS.title" = "About ReactTestApp";
|
||||
|
||||
/* Class = "NSMenuItem"; title = "ReactTestApp Help"; ObjectID = "FKE-Sm-Kum"; */
|
||||
"FKE-Sm-Kum.title" = "ReactTestApp Help";
|
||||
|
||||
/* Class = "NSMenuItem"; title = "Hide ReactTestApp"; ObjectID = "Olw-nP-bQN"; */
|
||||
"Olw-nP-bQN.title" = "Hide ReactTestApp";
|
||||
|
||||
/* Class = "NSMenu"; title = "ReactTestApp"; ObjectID = "uQy-DD-JDr"; */
|
||||
"uQy-DD-JDr.title" = "ReactTestApp";
|
|
@ -65,6 +65,7 @@
|
|||
19B368BD24B12C24002CCEFF /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
|
||||
19B368BE24B12C24002CCEFF /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
|
||||
19C4C89227710D8500157870 /* Manifest+Decoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Manifest+Decoder.swift"; path = "../ReactTestAppShared/Manifest+Decoder.swift"; sourceTree = "<group>"; };
|
||||
19E0B90C27E9F8FD006FD558 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = ../Localizations/en.lproj/Main.strings; sourceTree = "<group>"; };
|
||||
19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppTests.swift; sourceTree = "<group>"; };
|
||||
19E791C224B08E4D00FA6468 /* ReactTestAppUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppUITests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -381,6 +382,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
193EF069247A736300BE8C79 /* Base */,
|
||||
19E0B90C27E9F8FD006FD558 /* en */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
@ -7,107 +7,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
|
||||
private(set) lazy var reactInstance = ReactInstance()
|
||||
|
||||
private var isPresenting: Bool {
|
||||
!(mainWindow?.contentViewController is ViewController)
|
||||
}
|
||||
private lazy var mainWindow: NSWindow? = {
|
||||
// `keyWindow` might be `nil` while loading or when the window is not
|
||||
// active. Use `identifier` to find our main window.
|
||||
let windows = NSApplication.shared.windows
|
||||
return windows.first { $0.identifier?.rawValue == "MainWindow" }
|
||||
}()
|
||||
|
||||
private weak var mainWindow: NSWindow?
|
||||
private var manifestChecksum: String?
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
defer {
|
||||
NotificationCenter.default.post(
|
||||
name: .ReactTestAppDidInitialize,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
// `keyWindow` might be `nil` while loading or when the window is not
|
||||
// active. Use `identifier` to find our main window.
|
||||
let windows = NSApplication.shared.windows
|
||||
mainWindow = windows.first { $0.identifier?.rawValue == "MainWindow" }
|
||||
|
||||
if Session.shouldRememberLastComponent {
|
||||
rememberLastComponentMenuItem.state = .on
|
||||
}
|
||||
|
||||
guard let (manifest, checksum) = Manifest.fromFile() else {
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: "Could not load 'app.json'",
|
||||
action: nil,
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
let components = manifest.components ?? []
|
||||
if components.isEmpty {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .ReactTestAppDidRegisterApps,
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { [weak self] note in
|
||||
guard let strongSelf = self,
|
||||
let appKeys = note.userInfo?["appKeys"] as? [String]
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let components = appKeys.map { Component(appKey: $0) }
|
||||
strongSelf.onComponentsRegistered(components, enable: true)
|
||||
if components.count == 1, !strongSelf.isPresenting {
|
||||
strongSelf.present(components[0])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onComponentsRegistered(components, enable: false)
|
||||
|
||||
let bundleRoot = manifest.bundleRoot
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
self?.reactInstance.initReact(bundleRoot: bundleRoot) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let strongSelf = self, !components.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
|
||||
strongSelf.present(components[index])
|
||||
}
|
||||
|
||||
strongSelf.reactMenu.items.forEach { $0.isEnabled = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifestChecksum = checksum
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_: Notification) {
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
|
||||
// MARK: - User interaction
|
||||
|
||||
@objc
|
||||
private func onComponentSelected(menuItem: NSMenuItem) {
|
||||
guard let component = menuItem.representedObject as? Component else {
|
||||
return
|
||||
}
|
||||
|
||||
present(component)
|
||||
|
||||
if let checksum = manifestChecksum {
|
||||
Session.storeComponent(index: menuItem.tag, checksum: checksum)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction
|
||||
func onLoadEmbeddedBundleSelected(_: NSMenuItem) {
|
||||
reactInstance.remoteBundleURL = nil
|
||||
|
@ -139,92 +57,242 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
static let modalSize = CGSize(width: 586, height: 326)
|
||||
}
|
||||
|
||||
private func onComponentsRegistered(_ components: [Component], enable: Bool) {
|
||||
removeAllComponentsFromMenu()
|
||||
components.enumerated().forEach { index, component in
|
||||
let title = component.displayName ?? component.appKey
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: title,
|
||||
action: #selector(onComponentSelected),
|
||||
keyEquivalent: index < 9 ? String(index + 1) : ""
|
||||
)
|
||||
item.tag = index
|
||||
item.keyEquivalentModifierMask = [.shift, .command]
|
||||
item.isEnabled = enable
|
||||
item.representedObject = component
|
||||
}
|
||||
}
|
||||
|
||||
private func present(_ component: Component) {
|
||||
guard let window = mainWindow,
|
||||
let bridge = reactInstance.bridge
|
||||
else {
|
||||
private func showReactMenu() {
|
||||
guard let mainMenu = reactMenu.supermenu else {
|
||||
return
|
||||
}
|
||||
|
||||
let title = component.displayName ?? component.appKey
|
||||
|
||||
let viewController: NSViewController = {
|
||||
if let viewController = RTAViewControllerFromString(component.appKey, bridge) {
|
||||
return viewController
|
||||
}
|
||||
|
||||
let viewController = NSViewController(nibName: nil, bundle: nil)
|
||||
viewController.title = title
|
||||
viewController.view = RTACreateReactRootView(
|
||||
bridge,
|
||||
component.appKey,
|
||||
component.initialProperties
|
||||
)
|
||||
return viewController
|
||||
}()
|
||||
|
||||
switch component.presentationStyle {
|
||||
case "modal":
|
||||
let rootView = viewController.view
|
||||
let modalFrame = NSRect(size: WindowSize.modalSize)
|
||||
rootView.frame = modalFrame
|
||||
|
||||
var token: NSObjectProtocol?
|
||||
token = NotificationCenter.default.addObserver(
|
||||
forName: .RCTContentDidAppear,
|
||||
object: rootView,
|
||||
queue: nil,
|
||||
using: { _ in
|
||||
#if USE_FABRIC
|
||||
rootView.frame = modalFrame
|
||||
#else
|
||||
(rootView as? RCTRootView)?.contentView.frame = modalFrame
|
||||
#endif
|
||||
NotificationCenter.default.removeObserver(token!)
|
||||
}
|
||||
)
|
||||
|
||||
window.contentViewController?.presentAsModalWindow(viewController)
|
||||
|
||||
default:
|
||||
window.title = title
|
||||
let frame = window.contentViewController?.view.frame
|
||||
viewController.view.frame = frame ?? NSRect(size: WindowSize.defaultSize)
|
||||
window.contentViewController = viewController
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAllComponentsFromMenu() {
|
||||
let numberOfItems = reactMenu.numberOfItems
|
||||
for reverseIndex in 1 ... numberOfItems {
|
||||
let index = numberOfItems - reverseIndex
|
||||
guard let item = reactMenu.item(at: index) else {
|
||||
preconditionFailure()
|
||||
}
|
||||
if item.isSeparatorItem == true {
|
||||
break
|
||||
}
|
||||
reactMenu.removeItem(at: index)
|
||||
}
|
||||
let index = mainMenu.indexOfItem(withSubmenu: reactMenu)
|
||||
mainMenu.item(at: index)?.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Multi-app extensions
|
||||
|
||||
#if !ENABLE_SINGLE_APP_MODE
|
||||
|
||||
extension AppDelegate {
|
||||
private var isPresenting: Bool {
|
||||
!(mainWindow?.contentViewController is ViewController)
|
||||
}
|
||||
|
||||
func applicationWillFinishLaunching(_: Notification) {
|
||||
if Session.shouldRememberLastComponent {
|
||||
rememberLastComponentMenuItem.state = .on
|
||||
}
|
||||
|
||||
showReactMenu()
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
defer {
|
||||
NotificationCenter.default.post(
|
||||
name: .ReactTestAppDidInitialize,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
guard let (manifest, checksum) = Manifest.fromFile() else {
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: "Could not load 'app.json'",
|
||||
action: nil,
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
let components = manifest.components ?? []
|
||||
if components.isEmpty {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .ReactTestAppDidRegisterApps,
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { [weak self] note in
|
||||
guard let strongSelf = self,
|
||||
let appKeys = note.userInfo?["appKeys"] as? [String]
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let components = appKeys.map { Component(appKey: $0) }
|
||||
strongSelf.onComponentsRegistered(components, enable: true)
|
||||
if components.count == 1, !strongSelf.isPresenting {
|
||||
strongSelf.present(components[0])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onComponentsRegistered(components, enable: false)
|
||||
|
||||
let bundleRoot = manifest.bundleRoot
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
self?.reactInstance.initReact(bundleRoot: bundleRoot) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let strongSelf = self, !components.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
|
||||
strongSelf.present(components[index])
|
||||
}
|
||||
|
||||
strongSelf.reactMenu.items.forEach { $0.isEnabled = true }
|
||||
strongSelf.rememberLastComponentMenuItem.isEnabled = components.count > 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifestChecksum = checksum
|
||||
}
|
||||
|
||||
@objc
|
||||
private func onComponentSelected(menuItem: NSMenuItem) {
|
||||
guard let component = menuItem.representedObject as? Component else {
|
||||
return
|
||||
}
|
||||
|
||||
present(component)
|
||||
|
||||
if let checksum = manifestChecksum {
|
||||
Session.storeComponent(index: menuItem.tag, checksum: checksum)
|
||||
}
|
||||
}
|
||||
|
||||
private func onComponentsRegistered(_ components: [Component], enable: Bool) {
|
||||
removeAllComponentsFromMenu()
|
||||
components.enumerated().forEach { index, component in
|
||||
let title = component.displayName ?? component.appKey
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: title,
|
||||
action: #selector(onComponentSelected),
|
||||
keyEquivalent: index < 9 ? String(index + 1) : ""
|
||||
)
|
||||
item.tag = index
|
||||
item.keyEquivalentModifierMask = [.shift, .command]
|
||||
item.isEnabled = enable
|
||||
item.representedObject = component
|
||||
}
|
||||
|
||||
rememberLastComponentMenuItem.isEnabled = components.count > 1
|
||||
}
|
||||
|
||||
private func present(_ component: Component) {
|
||||
guard let window = mainWindow,
|
||||
let bridge = reactInstance.bridge
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let title = component.displayName ?? component.appKey
|
||||
|
||||
let viewController: NSViewController = {
|
||||
if let viewController = RTAViewControllerFromString(component.appKey, bridge) {
|
||||
return viewController
|
||||
}
|
||||
|
||||
let viewController = NSViewController(nibName: nil, bundle: nil)
|
||||
viewController.title = title
|
||||
viewController.view = RTACreateReactRootView(
|
||||
bridge,
|
||||
component.appKey,
|
||||
component.initialProperties
|
||||
)
|
||||
return viewController
|
||||
}()
|
||||
|
||||
switch component.presentationStyle {
|
||||
case "modal":
|
||||
let rootView = viewController.view
|
||||
let modalFrame = NSRect(size: WindowSize.modalSize)
|
||||
rootView.frame = modalFrame
|
||||
|
||||
var token: NSObjectProtocol?
|
||||
token = NotificationCenter.default.addObserver(
|
||||
forName: .RCTContentDidAppear,
|
||||
object: rootView,
|
||||
queue: nil,
|
||||
using: { _ in
|
||||
#if USE_FABRIC
|
||||
rootView.frame = modalFrame
|
||||
#else
|
||||
(rootView as? RCTRootView)?.contentView.frame = modalFrame
|
||||
#endif
|
||||
NotificationCenter.default.removeObserver(token!)
|
||||
}
|
||||
)
|
||||
|
||||
window.contentViewController?.presentAsModalWindow(viewController)
|
||||
|
||||
default:
|
||||
window.title = title
|
||||
let frame = window.contentViewController?.view.frame
|
||||
viewController.view.frame = frame ?? NSRect(size: WindowSize.defaultSize)
|
||||
window.contentViewController = viewController
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAllComponentsFromMenu() {
|
||||
let numberOfItems = reactMenu.numberOfItems
|
||||
for reverseIndex in 1 ... numberOfItems {
|
||||
let index = numberOfItems - reverseIndex
|
||||
guard let item = reactMenu.item(at: index) else {
|
||||
preconditionFailure()
|
||||
}
|
||||
if item.isSeparatorItem == true {
|
||||
break
|
||||
}
|
||||
reactMenu.removeItem(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !ENABLE_SINGLE_APP_MODE
|
||||
|
||||
// MARK: - Single-app extensions
|
||||
|
||||
#if ENABLE_SINGLE_APP_MODE
|
||||
|
||||
extension AppDelegate {
|
||||
func applicationWillFinishLaunching(_: Notification) {
|
||||
guard let window = mainWindow else {
|
||||
assertionFailure("Main window should have been instantiated by now")
|
||||
return
|
||||
}
|
||||
|
||||
guard let (rootView, title) = createReactRootView(reactInstance) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
window.title = title
|
||||
|
||||
let frame = window.contentViewController?.view.frame
|
||||
rootView.frame = frame ?? NSRect(size: WindowSize.defaultSize)
|
||||
window.contentViewController?.view = rootView
|
||||
|
||||
#if DEBUG
|
||||
if Session.shouldRememberLastComponent {
|
||||
rememberLastComponentMenuItem.state = .on
|
||||
}
|
||||
|
||||
showReactMenu()
|
||||
#endif // DEBUG
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
NotificationCenter.default.post(
|
||||
name: .ReactTestAppDidInitialize,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ENABLE_SINGLE_APP_MODE
|
||||
|
||||
// MARK: - NSRect extensions
|
||||
|
||||
extension NSRect {
|
||||
init(size: CGSize) {
|
||||
self.init(x: 0, y: 0, width: size.width, height: size.height)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -370,7 +369,7 @@
|
|||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="React" id="f0Z-7n-fql">
|
||||
<menuItem title="React" hidden="YES" id="f0Z-7n-fql">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="React" autoenablesItems="NO" id="nwF-AT-VY9">
|
||||
<items>
|
||||
|
@ -386,7 +385,7 @@
|
|||
<action selector="onLoadFromDevServerSelected:" target="Voe-Tx-rLC" id="FOl-Nq-cWc"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Remember Last Opened Component" id="lzS-xk-P1D">
|
||||
<menuItem title="Remember Last Opened Component" enabled="NO" id="lzS-xk-P1D">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="onRememberLastComponentSelected:" target="Voe-Tx-rLC" id="TFw-gh-YQX"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<string>$(PRODUCT_DISPLAY_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
|
75
schema.json
75
schema.json
|
@ -19,6 +19,10 @@
|
|||
"description": "The style in which to present your component.",
|
||||
"type": "string",
|
||||
"enum": ["default", "modal"]
|
||||
},
|
||||
"slug": {
|
||||
"description": "URL slug that uniquely identifies this component. Used for deep linking.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["appKey"]
|
||||
|
@ -36,6 +40,10 @@
|
|||
"description": "Specifies the root of the bundle file name. E.g., if the bundle file is `index.<platform>.bundle`, `index` is the bundle root. Defaults to `index` and `main`.",
|
||||
"type": "string"
|
||||
},
|
||||
"singleApp": {
|
||||
"description": "In single-app mode, the component with the specified slug gets launched automatically, essentially behaving as a normal app. Defaults to multi-app mode.",
|
||||
"type": "string"
|
||||
},
|
||||
"components": {
|
||||
"description": "All components that should be accessible from the home screen should be declared under this property. Each component must have `appKey` set, i.e. the name that you passed to `AppRegistry.registerComponent`.",
|
||||
"type": "array",
|
||||
|
@ -43,6 +51,29 @@
|
|||
}
|
||||
},
|
||||
"required": ["name", "displayName"]
|
||||
},
|
||||
"signingConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyAlias": {
|
||||
"description": "Use this property to specify the alias of key to use in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"keyPassword": {
|
||||
"description": "Use this property to specify the password of key in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"storeFile": {
|
||||
"description": "Use this property to specify the relative file path to the key store file",
|
||||
"type": "string"
|
||||
},
|
||||
"storePassword": {
|
||||
"description": "Use this property to specify the password of the key store",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["storeFile"],
|
||||
"exclude-from-codegen": true
|
||||
}
|
||||
},
|
||||
"allOf": [{ "$ref": "#/$defs/manifest" }],
|
||||
|
@ -97,49 +128,13 @@
|
|||
"properties": {
|
||||
"debug": {
|
||||
"description": "Use this property for the debug signing config for the app. The value `storeFile` is required. Android defaults will be provided for other properties.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyAlias": {
|
||||
"description": "Use this property to specify the alias of key to use in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"keyPassword": {
|
||||
"description": "Use this property to specify the password of key in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"storeFile": {
|
||||
"description": "Use this property to specify the relative file path to the key store file",
|
||||
"type": "string"
|
||||
},
|
||||
"storePassword": {
|
||||
"description": "Use this property to specify the password of the key store",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["storeFile"]
|
||||
"allOf": [{ "$ref": "#/$defs/signingConfig" }],
|
||||
"type": "object"
|
||||
},
|
||||
"release": {
|
||||
"description": "Use this property for the release signing config for the app. The value `storeFile` is required. Android defaults will be provided for other properties.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyAlias": {
|
||||
"description": "Use this property to specify the alias of key to use in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"keyPassword": {
|
||||
"description": "Use this property to specify the password of key in the store",
|
||||
"type": "string"
|
||||
},
|
||||
"storeFile": {
|
||||
"description": "Use this property to specify the relative file path to the key store file",
|
||||
"type": "string"
|
||||
},
|
||||
"storePassword": {
|
||||
"description": "Use this property to specify the password of the key store",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["storeFile"]
|
||||
"allOf": [{ "$ref": "#/$defs/signingConfig" }],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,14 +211,16 @@ async function generate(output) {
|
|||
}
|
||||
|
||||
Object.entries(schema.$defs).forEach(([key, definition]) => {
|
||||
lines.push(
|
||||
...generateType(
|
||||
typename(key),
|
||||
/** @type {SchemaObjectProperty} */ (definition),
|
||||
lang
|
||||
),
|
||||
""
|
||||
);
|
||||
if (!("exclude-from-codegen" in definition)) {
|
||||
lines.push(
|
||||
...generateType(
|
||||
typename(key),
|
||||
/** @type {SchemaObjectProperty} */ (definition),
|
||||
lang
|
||||
),
|
||||
""
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
|
||||
|
|
|
@ -27,8 +27,18 @@ function findAppManifest(cwd = process.cwd()) {
|
|||
}
|
||||
|
||||
function makeValidator() {
|
||||
const { default: Ajv } = require("ajv");
|
||||
const { default: Ajv, _ } = require("ajv");
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
ajv.addKeyword({
|
||||
keyword: "exclude-from-codegen",
|
||||
type: "object",
|
||||
schemaType: "boolean",
|
||||
code: (cxt) => {
|
||||
const { data, schema } = cxt;
|
||||
const op = schema ? _`!==` : _`===`;
|
||||
cxt.fail(_`${data} %2 ${op} 0`);
|
||||
},
|
||||
});
|
||||
return ajv.compile(require(`${__dirname}/../schema.json`));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "TestFixture",
|
||||
"displayName": "Test Fixture",
|
||||
"singleApp": "test-fixture"
|
||||
}
|
|
@ -17,10 +17,18 @@ def fixture_path(*args)
|
|||
end
|
||||
|
||||
class TestTestApp < Minitest::Test
|
||||
def test_app_name
|
||||
name, display_name = app_name(fixture_path('with_resources'))
|
||||
def test_app_config
|
||||
name, display_name, single_app = app_config(fixture_path('with_resources'))
|
||||
assert_equal(name, 'TestFixture')
|
||||
assert_equal(display_name, 'Test Fixture')
|
||||
assert_nil(single_app)
|
||||
end
|
||||
|
||||
def test_app_config_single_app
|
||||
name, display_name, single_app = app_config(fixture_path('single_app_mode'))
|
||||
assert_equal(name, 'TestFixture')
|
||||
assert_equal(display_name, 'Test Fixture')
|
||||
assert_equal(single_app, 'test-fixture')
|
||||
end
|
||||
|
||||
def test_autolink_script_path
|
||||
|
|
|
@ -17,6 +17,7 @@ using ReactTestApp::Component;
|
|||
using ReactTestApp::JSBundleSource;
|
||||
using ReactTestApp::Session;
|
||||
using winrt::Microsoft::ReactNative::IJSValueWriter;
|
||||
using winrt::Microsoft::ReactNative::ReactNativeHost;
|
||||
using winrt::Microsoft::ReactNative::ReactRootView;
|
||||
using winrt::ReactTestApp::implementation::MainPage;
|
||||
using winrt::Windows::ApplicationModel::Core::CoreApplication;
|
||||
|
@ -30,6 +31,7 @@ using winrt::Windows::UI::Core::CoreDispatcherPriority;
|
|||
using winrt::Windows::UI::Popups::MessageDialog;
|
||||
using winrt::Windows::UI::ViewManagement::ApplicationView;
|
||||
using winrt::Windows::UI::Xaml::RoutedEventArgs;
|
||||
using winrt::Windows::UI::Xaml::Visibility;
|
||||
using winrt::Windows::UI::Xaml::Window;
|
||||
using winrt::Windows::UI::Xaml::Automation::Peers::MenuBarItemAutomationPeer;
|
||||
using winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem;
|
||||
|
@ -40,6 +42,13 @@ using winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs;
|
|||
|
||||
namespace
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
constexpr bool kDebug = true;
|
||||
#else
|
||||
constexpr bool kDebug = false;
|
||||
#endif // _DEBUG
|
||||
constexpr bool kSingleAppMode = static_cast<bool>(ENABLE_SINGLE_APP_MODE);
|
||||
|
||||
void SetMenuItemText(IInspectable const &sender,
|
||||
bool const isEnabled,
|
||||
winrt::hstring const &enableText,
|
||||
|
@ -103,8 +112,14 @@ namespace
|
|||
}
|
||||
}
|
||||
|
||||
void InitializeReactRootView(ReactRootView reactRootView, Component const &component)
|
||||
void InitializeReactRootView(ReactNativeHost const &reactNativeHost,
|
||||
ReactRootView reactRootView,
|
||||
Component const &component)
|
||||
{
|
||||
if (reactRootView.ReactNativeHost() == nullptr) {
|
||||
reactRootView.ReactNativeHost(reactNativeHost);
|
||||
}
|
||||
|
||||
reactRootView.ComponentName(winrt::to_hstring(component.appKey));
|
||||
reactRootView.InitialProps(
|
||||
[initialProps = component.initialProperties](IJSValueWriter const &writer) {
|
||||
|
@ -140,10 +155,27 @@ MainPage::MainPage()
|
|||
auto result = ::ReactTestApp::GetManifest("app.json");
|
||||
if (result.has_value()) {
|
||||
auto &[manifest, checksum] = result.value();
|
||||
|
||||
manifestChecksum_ = std::move(checksum);
|
||||
|
||||
AppTitle().Text(to_hstring(manifest.displayName));
|
||||
reactInstance_.BundleRoot(manifest.bundleRoot.has_value()
|
||||
? std::make_optional(to_hstring(manifest.bundleRoot.value()))
|
||||
: std::nullopt);
|
||||
manifestChecksum_ = std::move(checksum);
|
||||
|
||||
if constexpr (kSingleAppMode) {
|
||||
assert(manifest.singleApp.has_value() ||
|
||||
!"`ENABLE_SINGLE_APP_MODE` shouldn't have been true");
|
||||
assert(manifest.components.has_value() || !"At least one component must be declared");
|
||||
|
||||
for (auto &component : *manifest.components) {
|
||||
if (component.slug == *manifest.singleApp) {
|
||||
InitializeReactRootView(reactInstance_.ReactHost(), ReactRootView(), component);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InitializeReactMenu(std::move(manifest));
|
||||
} else {
|
||||
InitializeReactMenu(std::nullopt);
|
||||
|
@ -249,73 +281,71 @@ void MainPage::LoadReactComponent(Component const &component)
|
|||
auto title = to_hstring(component.displayName.value_or(component.appKey));
|
||||
auto &&presentationStyle = component.presentationStyle.value_or("");
|
||||
if (presentationStyle == "modal") {
|
||||
if (DialogReactRootView().ReactNativeHost() == nullptr) {
|
||||
DialogReactRootView().ReactNativeHost(reactInstance_.ReactHost());
|
||||
}
|
||||
|
||||
InitializeReactRootView(DialogReactRootView(), component);
|
||||
InitializeReactRootView(reactInstance_.ReactHost(), DialogReactRootView(), component);
|
||||
ContentDialog().Title(box_value(title));
|
||||
ContentDialog().ShowAsync();
|
||||
} else {
|
||||
if (ReactRootView().ReactNativeHost() == nullptr) {
|
||||
ReactRootView().ReactNativeHost(reactInstance_.ReactHost());
|
||||
}
|
||||
|
||||
InitializeReactRootView(ReactRootView(), component);
|
||||
InitializeReactRootView(reactInstance_.ReactHost(), ReactRootView(), component);
|
||||
AppTitle().Text(title);
|
||||
}
|
||||
}
|
||||
|
||||
void MainPage::InitializeDebugMenu()
|
||||
{
|
||||
if (!reactInstance_.UseCustomDeveloperMenu()) {
|
||||
return;
|
||||
if constexpr (kDebug || !kSingleAppMode) {
|
||||
if (!reactInstance_.UseCustomDeveloperMenu()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetWebDebuggerMenuItem(WebDebuggerMenuItem(), reactInstance_.UseWebDebugger());
|
||||
WebDebuggerMenuItem().IsEnabled(reactInstance_.IsWebDebuggerAvailable());
|
||||
|
||||
SetDirectDebuggerMenuItem(DirectDebuggingMenuItem(), reactInstance_.UseDirectDebugger());
|
||||
SetBreakOnFirstLineMenuItem(BreakOnFirstLineMenuItem(), reactInstance_.BreakOnFirstLine());
|
||||
|
||||
SetFastRefreshMenuItem(FastRefreshMenuItem(), reactInstance_.UseFastRefresh());
|
||||
FastRefreshMenuItem().IsEnabled(reactInstance_.IsFastRefreshAvailable());
|
||||
|
||||
DebugMenuBarItem().IsEnabled(true);
|
||||
}
|
||||
|
||||
SetWebDebuggerMenuItem(WebDebuggerMenuItem(), reactInstance_.UseWebDebugger());
|
||||
WebDebuggerMenuItem().IsEnabled(reactInstance_.IsWebDebuggerAvailable());
|
||||
|
||||
SetDirectDebuggerMenuItem(DirectDebuggingMenuItem(), reactInstance_.UseDirectDebugger());
|
||||
SetBreakOnFirstLineMenuItem(BreakOnFirstLineMenuItem(), reactInstance_.BreakOnFirstLine());
|
||||
|
||||
SetFastRefreshMenuItem(FastRefreshMenuItem(), reactInstance_.UseFastRefresh());
|
||||
FastRefreshMenuItem().IsEnabled(reactInstance_.IsFastRefreshAvailable());
|
||||
|
||||
DebugMenuBarItem().IsEnabled(true);
|
||||
}
|
||||
|
||||
void MainPage::InitializeReactMenu(std::optional<::ReactTestApp::Manifest> manifest)
|
||||
{
|
||||
RememberLastComponentMenuItem().IsChecked(Session::ShouldRememberLastComponent());
|
||||
if constexpr (kDebug || !kSingleAppMode) {
|
||||
AppMenuBar().Visibility(Visibility::Visible);
|
||||
|
||||
auto menuItems = ReactMenuBarItem().Items();
|
||||
if (!manifest.has_value()) {
|
||||
MenuFlyoutItem newMenuItem;
|
||||
newMenuItem.Text(L"Couldn't parse 'app.json'");
|
||||
newMenuItem.IsEnabled(false);
|
||||
menuItems.Append(newMenuItem);
|
||||
return;
|
||||
}
|
||||
RememberLastComponentMenuItem().IsChecked(Session::ShouldRememberLastComponent());
|
||||
|
||||
AppTitle().Text(to_hstring(manifest->displayName));
|
||||
auto menuItems = ReactMenuBarItem().Items();
|
||||
if (!manifest.has_value()) {
|
||||
MenuFlyoutItem newMenuItem;
|
||||
newMenuItem.Text(L"Couldn't parse 'app.json'");
|
||||
newMenuItem.IsEnabled(false);
|
||||
menuItems.Append(newMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &components = manifest->components;
|
||||
if (!components.has_value() || components->empty()) {
|
||||
reactInstance_.SetComponentsRegisteredDelegate(
|
||||
[this](std::vector<std::string> const &appKeys) {
|
||||
std::vector<Component> components;
|
||||
components.reserve(appKeys.size());
|
||||
std::transform(std::begin(appKeys),
|
||||
std::end(appKeys),
|
||||
std::back_inserter(components),
|
||||
[](std::string const &appKey) { return Component{appKey}; });
|
||||
OnComponentsRegistered(std::move(components));
|
||||
PresentReactMenu();
|
||||
});
|
||||
} else {
|
||||
OnComponentsRegistered(std::move(components.value()));
|
||||
reactInstance_.SetComponentsRegisteredDelegate(
|
||||
[this](std::vector<std::string> const &) { PresentReactMenu(); });
|
||||
if constexpr (!kSingleAppMode) {
|
||||
auto &components = manifest->components;
|
||||
if (!components.has_value() || components->empty()) {
|
||||
reactInstance_.SetComponentsRegisteredDelegate(
|
||||
[this](std::vector<std::string> const &appKeys) {
|
||||
std::vector<Component> components;
|
||||
components.reserve(appKeys.size());
|
||||
std::transform(std::begin(appKeys),
|
||||
std::end(appKeys),
|
||||
std::back_inserter(components),
|
||||
[](std::string const &appKey) { return Component{appKey}; });
|
||||
OnComponentsRegistered(std::move(components));
|
||||
PresentReactMenu();
|
||||
});
|
||||
} else {
|
||||
OnComponentsRegistered(std::move(components.value()));
|
||||
reactInstance_.SetComponentsRegisteredDelegate(
|
||||
[this](std::vector<std::string> const &) { PresentReactMenu(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,6 +441,8 @@ void MainPage::OnComponentsRegistered(std::vector<Component> components)
|
|||
|
||||
menuItems.Append(newMenuItem);
|
||||
}
|
||||
|
||||
RememberLastComponentMenuItem().IsEnabled(components.size() > 1);
|
||||
}
|
||||
|
||||
// Adjust height of custom title bar to match close, minimize and maximize icons
|
||||
|
|
|
@ -17,12 +17,18 @@
|
|||
<Grid x:Name="AppTitleBar" Background="Transparent">
|
||||
<TextBlock x:Name="AppTitle" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<MenuBar x:Name="AppMenuBar" HorizontalAlignment="Left" VerticalContentAlignment="Stretch" Width="Auto">
|
||||
<MenuBar
|
||||
x:Name="AppMenuBar"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Width="Auto"
|
||||
Visibility="Collapsed">
|
||||
<MenuBarItem x:Name="ReactMenuBarItem" Title="React" AccessKey="R">
|
||||
<MenuFlyoutItem Text="Load from JS bundle" Click="LoadFromJSBundle" AccessKey="E"/>
|
||||
<MenuFlyoutItem Text="Load from dev server" Click="LoadFromDevServer" AccessKey="D"/>
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="RememberLastComponentMenuItem"
|
||||
IsEnabled="false"
|
||||
Text="Remember last opened component"
|
||||
Click="ToggleRememberLastComponent"
|
||||
AccessKey="R"
|
||||
|
|
|
@ -97,6 +97,7 @@ namespace ReactTestApp
|
|||
c.displayName = get_optional<std::string>(j, "displayName");
|
||||
c.initialProperties = parseInitialProps(j);
|
||||
c.presentationStyle = get_optional<std::string>(j, "presentationStyle");
|
||||
c.slug = get_optional<std::string>(j, "slug");
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, Manifest &m)
|
||||
|
@ -104,6 +105,7 @@ namespace ReactTestApp
|
|||
m.name = j.at("name");
|
||||
m.displayName = j.at("displayName");
|
||||
m.bundleRoot = get_optional<std::string>(j, "bundleRoot");
|
||||
m.singleApp = get_optional<std::string>(j, "singleApp");
|
||||
m.components = get_optional<std::vector<Component>>(j, "components")
|
||||
.value_or(std::vector<Component>{});
|
||||
}
|
||||
|
|
|
@ -18,12 +18,14 @@ namespace ReactTestApp
|
|||
std::optional<std::string> displayName;
|
||||
std::optional<std::map<std::string, std::any>> initialProperties;
|
||||
std::optional<std::string> presentationStyle;
|
||||
std::optional<std::string> slug;
|
||||
};
|
||||
|
||||
struct Manifest {
|
||||
std::string name;
|
||||
std::string displayName;
|
||||
std::optional<std::string> bundleRoot;
|
||||
std::optional<std::string> singleApp;
|
||||
std::optional<std::vector<Component>> components;
|
||||
};
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
<!--Temporarily disable cppwinrt heap enforcement to work around xaml compiler generated std::shared_ptr use -->
|
||||
<AdditionalOptions Condition="'$(CppWinRTHeapEnforcement)'==''">/DWINRT_NO_MAKE_DETECTION %(AdditionalOptions)</AdditionalOptions>
|
||||
<DisableSpecificWarnings></DisableSpecificWarnings>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;REACT_NATIVE_VERSION=10000000;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;ENABLE_SINGLE_APP_MODE=0;REACT_NATIVE_VERSION=10000000;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
|
|
|
@ -402,6 +402,7 @@ function copyAndReplace(srcPath, destPath, replacements, callback = rethrow) {
|
|||
* assetItemFilters: string;
|
||||
* assetFilters: string;
|
||||
* packageCertificate: string;
|
||||
* singleApp?: string;
|
||||
* }} Application name, and paths to directories and files to include.
|
||||
*/
|
||||
function getBundleResources(manifestFilePath, projectFilesDestPath) {
|
||||
|
@ -415,10 +416,11 @@ function getBundleResources(manifestFilePath, projectFilesDestPath) {
|
|||
if (manifestFilePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(manifestFilePath, textFileReadOptions);
|
||||
const { name, resources, windows } = JSON.parse(content);
|
||||
const { name, singleApp, resources, windows } = JSON.parse(content);
|
||||
const projectPath = path.dirname(manifestFilePath);
|
||||
return {
|
||||
appName: name || defaultName,
|
||||
singleApp,
|
||||
appxManifest: projectRelativePath(
|
||||
projectPath,
|
||||
(windows && windows.appxManifest) || defaultAppxManifest
|
||||
|
@ -547,6 +549,7 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
|
|||
assetItemFilters,
|
||||
assetFilters,
|
||||
packageCertificate,
|
||||
singleApp,
|
||||
} = getBundleResources(manifestFilePath, projectFilesDestPath);
|
||||
|
||||
const rnWindowsVersion = getPackageVersion(rnWindowsPath);
|
||||
|
@ -569,6 +572,9 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
|
|||
"REACT_NATIVE_VERSION=10000000;": `REACT_NATIVE_VERSION=${rnWindowsVersionNumber};`,
|
||||
"<!-- ReactTestApp asset items -->": assetItems,
|
||||
"\\$\\(ReactTestAppPackageManifest\\)": appxManifest,
|
||||
...(typeof singleApp === "string"
|
||||
? { "ENABLE_SINGLE_APP_MODE=0;": "ENABLE_SINGLE_APP_MODE=1;" }
|
||||
: undefined),
|
||||
...(useNuGet
|
||||
? {
|
||||
"<UseExperimentalNuget>false</UseExperimentalNuget>":
|
||||
|
|
Загрузка…
Ссылка в новой задаче