feat: add support for single-app mode (#817)

This commit is contained in:
Tommy Nguyen 2022-03-24 13:39:13 +01:00 коммит произвёл GitHub
Родитель 7426548911
Коммит a17eb12e80
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
31 изменённых файлов: 697 добавлений и 366 удалений

Просмотреть файл

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

Просмотреть файл

@ -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.&lt;platform&gt;.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>":