diff --git a/packages/app/scripts/types.ts b/packages/app/scripts/types.ts index 07f0ecb4b..4e04824dc 100644 --- a/packages/app/scripts/types.ts +++ b/packages/app/scripts/types.ts @@ -221,6 +221,7 @@ export type Assets = { assetItems: string; assetItemFilters: string; assetFilters: string; + contentItems: string; }; export type AppManifest = { @@ -245,6 +246,7 @@ export type AppxBundle = { assetItems: string; assetItemFilters: string; assetFilters: string; + contentItems: string; packageCertificate: string; singleApp?: string; }; diff --git a/packages/app/test/windows/getBundleResources.test.mts b/packages/app/test/windows/getBundleResources.test.mts index b3e382d19..ae4fb8614 100644 --- a/packages/app/test/windows/getBundleResources.test.mts +++ b/packages/app/test/windows/getBundleResources.test.mts @@ -5,12 +5,100 @@ import { getBundleResources as getBundleResourcesActual } from "../../windows/pr import { fs, setMockFiles } from "../fs.mock.mts"; describe("getBundleResources()", () => { - const getBundleResources: typeof getBundleResourcesActual = (p) => - getBundleResourcesActual(p, fs); + const getBundleResources: typeof getBundleResourcesActual = (p, opts) => + getBundleResourcesActual(p, opts, fs); + + const legacyOpts = { useFabric: false }; + const newArchOpts = { useFabric: true }; afterEach(() => setMockFiles()); - it("returns app name and bundle resources", () => { + for (const opts of [legacyOpts, newArchOpts]) { + const arch = opts.useFabric ? "new" : "old"; + + it(`returns package manifest (${arch} arch)`, () => { + setMockFiles({ + "app.json": JSON.stringify({ + windows: { + appxManifest: "windows/Example/Package.appxmanifest", + }, + }), + }); + + deepEqual(getBundleResources("app.json", opts), { + appName: "ReactTestApp", + singleApp: undefined, + appxManifest: "windows\\Example\\Package.appxmanifest", + assetItems: "", + assetItemFilters: "", + assetFilters: "", + contentItems: "", + packageCertificate: "", + }); + }); + + it(`handles missing manifest (${arch} arch)`, (t) => { + const warnMock = t.mock.method(console, "warn", () => null); + + deepEqual(getBundleResources("", opts), { + appName: "ReactTestApp", + appxManifest: "windows/Package.appxmanifest", + assetItems: "", + assetItemFilters: "", + assetFilters: "", + contentItems: "", + packageCertificate: "", + }); + + equal( + warnMock.mock.calls[0].arguments[1], + "Could not find 'app.json' file." + ); + }); + + it(`handles invalid manifest (${arch} arch)`, (t) => { + const warnMock = t.mock.method(console, "warn", () => null); + setMockFiles({ "app.json": "-" }); + + deepEqual(getBundleResources("app.json", opts), { + appName: "ReactTestApp", + appxManifest: "windows/Package.appxmanifest", + assetItems: "", + assetItemFilters: "", + assetFilters: "", + contentItems: "", + packageCertificate: "", + }); + + match( + warnMock.mock.calls[0].arguments[1], + /^Could not parse 'app.json':\n/ + ); + }); + + it(`returns package certificate (${arch} arch)`, () => { + setMockFiles({ + "app.json": JSON.stringify({ + windows: { + certificateKeyFile: "windows/ReactTestApp_TemporaryKey.pfx", + certificateThumbprint: "thumbprint", + certificatePassword: "password", + }, + }), + }); + + const { packageCertificate } = getBundleResources("app.json", opts); + equal( + packageCertificate, + `true + $(ProjectRootDir)\\windows\\ReactTestApp_TemporaryKey.pfx + thumbprint + password` + ); + }); + } + + it("returns app name and bundle resources (old arch)", () => { const assets = path.join("dist", "assets"); const bundle = path.join("dist", "main.bundle"); setMockFiles({ @@ -28,7 +116,8 @@ describe("getBundleResources()", () => { assetItems, assetItemFilters, assetFilters, - } = getBundleResources("app.json"); + contentItems, + } = getBundleResources("app.json", legacyOpts); equal(appName, "Example"); equal(appxManifest, "windows\\Package.appxmanifest"); @@ -54,83 +143,45 @@ describe("getBundleResources()", () => { assetFilters, /^\s+{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}<\/UniqueIdentifier>\s+<\/Filter>$/ ); + equal(contentItems, ""); }); - it("returns package manifest", () => { + it("returns app name and bundle resources (new arch)", () => { + const assets = path.join("dist", "assets"); + const bundle = path.join("dist", "main.bundle"); setMockFiles({ "app.json": JSON.stringify({ - windows: { - appxManifest: "windows/Example/Package.appxmanifest", - }, + name: "Example", + resources: [assets, bundle], }), + [path.join(assets, "app.json")]: "{}", + [bundle]: "'use strict';", }); - deepEqual(getBundleResources("app.json"), { - appName: "ReactTestApp", - singleApp: undefined, - appxManifest: "windows\\Example\\Package.appxmanifest", - assetItems: "", - assetItemFilters: "", - assetFilters: "", - packageCertificate: "", - }); - }); - - it("handles missing manifest", (t) => { - const warnMock = t.mock.method(console, "warn", () => null); - - deepEqual(getBundleResources(""), { - appName: "ReactTestApp", - appxManifest: "windows/Package.appxmanifest", - assetItems: "", - assetItemFilters: "", - assetFilters: "", - packageCertificate: "", - }); - - equal( - warnMock.mock.calls[0].arguments[1], - "Could not find 'app.json' file." - ); - }); - - it("handles invalid manifest", (t) => { - const warnMock = t.mock.method(console, "warn", () => null); - setMockFiles({ "app.json": "-" }); - - deepEqual(getBundleResources("app.json"), { - appName: "ReactTestApp", - appxManifest: "windows/Package.appxmanifest", - assetItems: "", - assetItemFilters: "", - assetFilters: "", - packageCertificate: "", - }); - - match( - warnMock.mock.calls[0].arguments[1], - /^Could not parse 'app.json':\n/ - ); - }); - - it("returns package certificate", () => { - setMockFiles({ - "app.json": JSON.stringify({ - windows: { - certificateKeyFile: "windows/ReactTestApp_TemporaryKey.pfx", - certificateThumbprint: "thumbprint", - certificatePassword: "password", - }, - }), - }); + const { + appName, + appxManifest, + assetItems, + assetItemFilters, + assetFilters, + contentItems, + } = getBundleResources("app.json", newArchOpts); - const { packageCertificate } = getBundleResources("app.json"); + equal(appName, "Example"); + equal(appxManifest, "windows\\Package.appxmanifest"); + equal(assetItems, ""); + equal(assetItemFilters, ""); + equal(assetFilters, ""); equal( - packageCertificate, - `true - $(ProjectRootDir)\\windows\\ReactTestApp_TemporaryKey.pfx - thumbprint - password` + contentItems, + ` + Bundle\\assets\\app.json + PreserveNewest + + + Bundle\\main.bundle + PreserveNewest + ` ); }); }); diff --git a/packages/app/test/windows/parseResources.test.mts b/packages/app/test/windows/parseResources.test.mts index bc1b438f4..96cc91271 100644 --- a/packages/app/test/windows/parseResources.test.mts +++ b/packages/app/test/windows/parseResources.test.mts @@ -4,31 +4,59 @@ import { parseResources as parseResourcesActual } from "../../windows/project.mj import { fs, setMockFiles } from "../fs.mock.mts"; describe("parseResources()", () => { - const parseResources: typeof parseResourcesActual = (r, p) => - parseResourcesActual(r, p, fs); + const parseResources: typeof parseResourcesActual = (r, p, opts) => + parseResourcesActual(r, p, opts, fs); - const empty = { assetFilters: "", assetItemFilters: "", assetItems: "" }; + const empty = { + assetFilters: "", + assetItemFilters: "", + assetItems: "", + contentItems: "", + }; + + const legacyOpts = { useFabric: false }; + const newArchOpts = { useFabric: true }; afterEach(() => setMockFiles()); - it("returns empty strings for no resources", () => { - deepEqual(parseResources(undefined, ""), empty); - deepEqual(parseResources([], ""), empty); - deepEqual(parseResources({}, ""), empty); - deepEqual(parseResources({ windows: [] }, ""), empty); - }); + for (const opts of [legacyOpts, newArchOpts]) { + const arch = opts.useFabric ? "new" : "old"; + + it(`returns empty strings for no resources (${arch} arch)`, () => { + deepEqual(parseResources(undefined, "", opts), empty); + deepEqual(parseResources([], "", opts), empty); + deepEqual(parseResources({}, "", opts), empty); + deepEqual(parseResources({ windows: [] }, "", opts), empty); + }); + + it(`skips missing assets (${arch} arch)`, (t) => { + const warnMock = t.mock.method(console, "warn", () => null); + + const resources = ["dist/assets", "dist/main.bundle"]; + + deepEqual(parseResources(resources, ".", opts), empty); + + equal( + warnMock.mock.calls[0].arguments[1], + "Resource not found: dist/assets" + ); + equal( + warnMock.mock.calls[1].arguments[1], + "Resource not found: dist/main.bundle" + ); + }); + } - it("returns references to existing assets", () => { + it("returns references to existing assets (old arch)", () => { setMockFiles({ "dist/assets/node_modules/arnold/portrait.png": "{}", "dist/assets/splash.png": "{}", "dist/main.jsbundle": "'use strict';", }); - const { assetItems, assetItemFilters, assetFilters } = parseResources( - ["dist/assets", "dist/main.jsbundle"], - "." - ); + const { assetItems, assetItemFilters, assetFilters, contentItems } = + parseResources(["dist/assets", "dist/main.jsbundle"], ".", legacyOpts); + equal( assetItems, ` @@ -57,20 +85,36 @@ describe("parseResources()", () => { assetFilters, /^\s+{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}<\/UniqueIdentifier>\s+<\/Filter>\s+\s+{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}<\/UniqueIdentifier>\s+<\/Filter>\s+\s+{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}<\/UniqueIdentifier>\s+<\/Filter>$/ ); + equal(contentItems, ""); }); - it("skips missing assets", (t) => { - const warnMock = t.mock.method(console, "warn", () => null); + it("returns references to existing assets (new arch)", () => { + setMockFiles({ + "dist/assets/node_modules/arnold/portrait.png": "{}", + "dist/assets/splash.png": "{}", + "dist/main.jsbundle": "'use strict';", + }); - deepEqual(parseResources(["dist/assets", "dist/main.bundle"], "."), empty); + const { assetItems, assetItemFilters, assetFilters, contentItems } = + parseResources(["dist/assets", "dist/main.jsbundle"], ".", newArchOpts); + equal(assetItems, ""); + equal(assetItemFilters, ""); + equal(assetFilters, ""); equal( - warnMock.mock.calls[0].arguments[1], - "Resource not found: dist/assets" - ); - equal( - warnMock.mock.calls[1].arguments[1], - "Resource not found: dist/main.bundle" + contentItems, + ` + Bundle\\assets\\node_modules\\arnold\\portrait.png + PreserveNewest + + + Bundle\\assets\\splash.png + PreserveNewest + + + Bundle\\main.jsbundle + PreserveNewest + ` ); }); }); diff --git a/packages/app/windows/Shared/ReactInstance.cpp b/packages/app/windows/Shared/ReactInstance.cpp index bf5fa74ae..60418c4db 100644 --- a/packages/app/windows/Shared/ReactInstance.cpp +++ b/packages/app/windows/Shared/ReactInstance.cpp @@ -46,11 +46,23 @@ namespace winrt::hstring const kUseWebDebugger = L"useWebDebugger"; #endif // USE_WEB_DEBUGGER - std::optional GetBundleName(std::optional const &bundleRoot) + std::filesystem::path GetBundleRootPath() + { +#if USE_FABRIC + WCHAR modulePath[MAX_PATH]; + GetModuleFileNameW(nullptr, modulePath, MAX_PATH); + PathCchRemoveFileSpec(modulePath, MAX_PATH); + return std::filesystem::path{modulePath}.replace_filename(L"Bundle") / L""; +#else // USE_FABRIC + return std::filesystem::path{L"Bundle\\"}; +#endif // USE_FABRIC + } + + std::optional GetBundleName(std::filesystem::path bundlePath, + std::optional const &bundleRoot) { constexpr std::wstring_view const bundleExtension = L".bundle"; - std::filesystem::path bundlePath{L"Bundle\\"}; if (bundleRoot.has_value()) { std::wstring_view root = bundleRoot.value(); for (auto &&ext : {L".windows", L".native", L""}) { @@ -104,77 +116,40 @@ std::vector const ReactTestApp::JSBundleNames = { L"main", }; -ReactInstance::ReactInstance() -{ - reactNativeHost_.PackageProviders().Append(winrt::make()); - winrt::Microsoft::ReactNative::RegisterAutolinkedNativeModulePackages( - reactNativeHost_.PackageProviders()); - - reactNativeHost_.InstanceSettings().InstanceLoaded( - [this](winrt::IInspectable const & /*sender*/, winrt::InstanceLoadedEventArgs const &args) { - context_ = args.Context(); - -#if __has_include("AppRegistry.h") && __has_include() - if (!onComponentsRegistered_) { - return; - } - - winrt::Microsoft::ReactNative::ExecuteJsi(context_, [this](Runtime &runtime) noexcept { - try { - onComponentsRegistered_(ReactTestApp::GetAppKeys(runtime)); - } catch ([[maybe_unused]] std::exception const &e) { -#if defined(_DEBUG) && !defined(DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION) - if (IsDebuggerPresent()) { - __debugbreak(); - } -#endif // defined(_DEBUG) && !defined(DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION) - } - }); -#endif // __has_include("AppRegistry.h") && __has_include() - }); -} - -#if __has_include() -ReactInstance::ReactInstance(HWND hwnd, - winrt::Microsoft::UI::Composition::Compositor const &compositor) - : ReactInstance() -{ - winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( - reactNativeHost_.InstanceSettings().Properties(), reinterpret_cast(hwnd)); - - // By using the MicrosoftCompositionContextHelper here, React Native Windows - // will use Lifted Visuals for its tree. - winrt::Microsoft::ReactNative::Composition::CompositionUIService::SetCompositor( - reactNativeHost_.InstanceSettings(), compositor); -} -#endif // __has_include() - -bool ReactInstance::LoadJSBundleFrom(JSBundleSource source) +bool ReactInstance::LoadJSBundleFrom(JSBundleSource source, bool reloadHost) { source_ = source; - auto instanceSettings = reactNativeHost_.InstanceSettings(); switch (source) { case JSBundleSource::DevServer: - instanceSettings.JavaScriptBundleFile(L"index"); + // "Fast Refresh" determines whether the bundle is loaded from the + // dev server (since at least 0.76) + // https://github.com/microsoft/react-native-windows/blob/react-native-windows_v0.76.17/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp#L641 + UseFastRefresh(true, reloadHost); break; case JSBundleSource::Embedded: - auto const &bundleName = GetBundleName(bundleRoot_); - if (!bundleName.has_value()) { - return false; - } - instanceSettings.JavaScriptBundleFile(bundleName.value()); + UseFastRefresh(false, reloadHost); break; } - Reload(); return true; } -void ReactInstance::Reload() +void ReactInstance::Reload(bool reloadHost) { auto instanceSettings = reactNativeHost_.InstanceSettings(); + instanceSettings.DebugBundlePath(L"index"); + + auto const bundleRootPath = GetBundleRootPath(); +#if USE_FABRIC + instanceSettings.BundleRootPath(L"file://" + bundleRootPath.wstring()); +#endif // USE_FABRIC + auto const &bundleName = GetBundleName(bundleRootPath, bundleRoot_); + if (bundleName.has_value()) { + instanceSettings.JavaScriptBundleFile(bundleName.value()); + } + #if USE_WEB_DEBUGGER instanceSettings.UseWebDebugger(UseWebDebugger()); #endif // USE_WEB_DEBUGGER @@ -196,7 +171,9 @@ void ReactInstance::Reload() instanceSettings.SourceBundleHost(host); instanceSettings.SourceBundlePort(static_cast(port)); - reactNativeHost_.ReloadInstance(); + if (reloadHost) { + reactNativeHost_.ReloadInstance(); + } } bool ReactInstance::BreakOnFirstLine() const @@ -270,10 +247,10 @@ bool ReactInstance::UseFastRefresh() const return IsFastRefreshAvailable() && RetrieveLocalSetting(kUseFastRefresh, true); } -void ReactInstance::UseFastRefresh(bool useFastRefresh) +void ReactInstance::UseFastRefresh(bool useFastRefresh, bool reloadHost) { StoreLocalSetting(kUseFastRefresh, useFastRefresh); - Reload(); + Reload(reloadHost); } bool ReactInstance::UseWebDebugger() const @@ -297,6 +274,35 @@ void ReactInstance::UseWebDebugger(bool useWebDebugger) #endif // USE_WEB_DEBUGGER } +void ReactInstance::InitializeHost(winrt::Microsoft::ReactNative::ReactNativeHost host) +{ + host.PackageProviders().Append(winrt::make()); + winrt::Microsoft::ReactNative::RegisterAutolinkedNativeModulePackages(host.PackageProviders()); + + host.InstanceSettings().InstanceLoaded( + [this](winrt::IInspectable const & /*sender*/, winrt::InstanceLoadedEventArgs const &args) { + context_ = args.Context(); + +#if __has_include("AppRegistry.h") && __has_include() + if (!onComponentsRegistered_) { + return; + } + + winrt::Microsoft::ReactNative::ExecuteJsi(context_, [this](Runtime &runtime) noexcept { + try { + onComponentsRegistered_(ReactTestApp::GetAppKeys(runtime)); + } catch ([[maybe_unused]] std::exception const &e) { +#if defined(_DEBUG) && !defined(DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION) + if (IsDebuggerPresent()) { + __debugbreak(); + } +#endif // defined(_DEBUG) && !defined(DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION) + } + }); +#endif // __has_include("AppRegistry.h") && __has_include() + }); +} + winrt::IAsyncOperation ReactTestApp::IsDevServerRunning() { winrt::Uri uri(L"http://localhost:8081/status"); diff --git a/packages/app/windows/Shared/ReactInstance.h b/packages/app/windows/Shared/ReactInstance.h index 2353d4759..edbf5d736 100644 --- a/packages/app/windows/Shared/ReactInstance.h +++ b/packages/app/windows/Shared/ReactInstance.h @@ -32,19 +32,24 @@ namespace ReactTestApp public: static constexpr uint32_t Version = REACT_NATIVE_VERSION; - ReactInstance(); + ReactInstance() + { + InitializeHost(reactNativeHost_); + } -#if __has_include() - ReactInstance(HWND hwnd, winrt::Microsoft::UI::Composition::Compositor const &); -#endif // __has_include() + ReactInstance(winrt::Microsoft::ReactNative::ReactNativeHost reactNativeHost) + : reactNativeHost_(reactNativeHost) + { + InitializeHost(reactNativeHost); + } auto const &ReactHost() const { return reactNativeHost_; } - bool LoadJSBundleFrom(JSBundleSource); - void Reload(); + bool LoadJSBundleFrom(JSBundleSource, bool reloadHost = true); + void Reload(bool reloadHost = true); bool BreakOnFirstLine() const; void BreakOnFirstLine(bool); @@ -89,7 +94,7 @@ namespace ReactTestApp void UseDirectDebugger(bool); bool UseFastRefresh() const; - void UseFastRefresh(bool); + void UseFastRefresh(bool, bool reloadHost = true); bool UseWebDebugger() const; void UseWebDebugger(bool); @@ -100,6 +105,8 @@ namespace ReactTestApp std::optional bundleRoot_; JSBundleSource source_ = JSBundleSource::DevServer; OnComponentsRegistered onComponentsRegistered_; + + void InitializeHost(winrt::Microsoft::ReactNative::ReactNativeHost); }; winrt::Windows::Foundation::IAsyncOperation IsDevServerRunning(); diff --git a/packages/app/windows/Win32/Main.cpp b/packages/app/windows/Win32/Main.cpp index 91da5f7ea..102d6826c 100644 --- a/packages/app/windows/Win32/Main.cpp +++ b/packages/app/windows/Win32/Main.cpp @@ -9,20 +9,7 @@ namespace winrt { using winrt::Microsoft::ReactNative::IJSValueWriter; - using winrt::Microsoft::ReactNative::LayoutDirection; - using winrt::Microsoft::ReactNative::ReactCoreInjection; - using winrt::Microsoft::ReactNative::ReactNativeIsland; using winrt::Microsoft::ReactNative::ReactViewOptions; - using winrt::Microsoft::UI::Composition::Compositor; - using winrt::Microsoft::UI::Content::ContentSizePolicy; - using winrt::Microsoft::UI::Content::DesktopChildSiteBridge; - using winrt::Microsoft::UI::Dispatching::DispatcherQueueController; - using winrt::Microsoft::UI::Windowing::AppWindow; - using winrt::Microsoft::UI::Windowing::AppWindowChangedEventArgs; - using winrt::Microsoft::UI::Windowing::OverlappedPresenter; - using winrt::Microsoft::UI::Windowing::OverlappedPresenterState; - using winrt::Windows::Foundation::AsyncStatus; - using winrt::Windows::Foundation::Size; } // namespace winrt namespace @@ -34,30 +21,9 @@ namespace #endif constexpr bool kSingleAppMode = static_cast(ENABLE_SINGLE_APP_MODE); - float ScaleFactor(HWND hwnd) noexcept + void ConfigureReactViewOptions(winrt::ReactViewOptions viewOptions, + ReactApp::Component const &component) { - return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); - } - - void UpdateRootViewSizeToAppWindow(winrt::ReactNativeIsland const &rootView, - winrt::AppWindow const &window) - { - // Do not relayout when minimized - auto windowState = window.Presenter().as().State(); - if (windowState == winrt::OverlappedPresenterState::Minimized) { - return; - } - - auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); - auto scaleFactor = ScaleFactor(hwnd); - winrt::Size size{window.ClientSize().Width / scaleFactor, - window.ClientSize().Height / scaleFactor}; - rootView.Arrange({size, size, winrt::LayoutDirection::Undefined}, {0, 0}); - } - - winrt::ReactViewOptions MakeReactViewOptions(ReactApp::Component const &component) - { - winrt::ReactViewOptions viewOptions; viewOptions.ComponentName(winrt::to_hstring(component.appKey)); auto initialProps = component.initialProperties.value_or({}); @@ -71,8 +37,6 @@ namespace } writer.WriteObjectEnd(); }); - - return viewOptions; } } // namespace @@ -91,22 +55,8 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* instance */, // Enable per monitor DPI scaling SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - // Create a DispatcherQueue for this thread. This is needed for Composition, Content, and - // Input APIs. - auto dispatcherQueueController = winrt::DispatcherQueueController::CreateOnCurrentThread(); - - // Create a Compositor for all Content on this thread. - auto compositor = winrt::Compositor{}; - - // Create a top-level window. - auto window = winrt::AppWindow::Create(); - window.Title(winrt::to_hstring(manifest.displayName)); - window.Resize({600, 800}); - window.Show(); - auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); - auto scaleFactor = ScaleFactor(hwnd); - - auto instance = ReactTestApp::ReactInstance{hwnd, compositor}; + auto app = winrt::Microsoft::ReactNative::ReactNativeAppBuilder().Build(); + auto instance = ReactTestApp::ReactInstance{app.ReactNativeHost()}; if (manifest.bundleRoot.has_value()) { auto &bundleRoot = *manifest.bundleRoot; instance.BundleRoot(std::make_optional(winrt::to_hstring(bundleRoot))); @@ -115,81 +65,31 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* instance */, // Start the react-native instance, which will create a JavaScript runtime and load the // applications bundle if constexpr (kDebug) { - instance.LoadJSBundleFrom(ReactTestApp::JSBundleSource::DevServer); + instance.LoadJSBundleFrom(ReactTestApp::JSBundleSource::DevServer, false); } else { - instance.LoadJSBundleFrom(ReactTestApp::JSBundleSource::Embedded); + instance.LoadJSBundleFrom(ReactTestApp::JSBundleSource::Embedded, false); } - // Create a RootView which will present a react-native component - winrt::ReactViewOptions viewOptions; + // Configure ReactViewOptions to load the initial component if constexpr (kSingleAppMode) { assert(manifest.singleApp.has_value() || !"`ENABLE_SINGLE_APP_MODE` shouldn't have been true"); for (auto &component : *manifest.components) { if (component.slug == *manifest.singleApp) { - viewOptions = MakeReactViewOptions(component); + ConfigureReactViewOptions(app.ReactViewOptions(), component); break; } } } else { // TODO: Implement session restoration auto &component = (*manifest.components)[0]; - viewOptions = MakeReactViewOptions(component); + ConfigureReactViewOptions(app.ReactViewOptions(), component); } - auto rootView = winrt::ReactNativeIsland{compositor}; - rootView.ReactViewHost( - winrt::ReactCoreInjection::MakeViewHost(instance.ReactHost(), viewOptions)); - - // Update the size of the RootView when the AppWindow changes size - window.Changed( - [wkRootView = winrt::make_weak(rootView)](winrt::AppWindow const &window, - winrt::AppWindowChangedEventArgs const &args) { - if (args.DidSizeChange() || args.DidVisibilityChange()) { - if (auto rootView = wkRootView.get()) { - UpdateRootViewSizeToAppWindow(rootView, window); - } - } - }); - - // Quit application when main window is closed - window.Destroying([&host = instance.ReactHost()](winrt::AppWindow const & /* window */, - winrt::IInspectable const & /* args */) { - // Before we shutdown the application - unload the ReactNativeHost to give the javascript a - // chance to save any state - auto async = host.UnloadInstance(); - async.Completed([host](auto asyncInfo, winrt::AsyncStatus asyncStatus) { - assert(asyncStatus == winrt::AsyncStatus::Completed); - host.InstanceSettings().UIDispatcher().Post([]() { PostQuitMessage(0); }); - }); - }); - - // DesktopChildSiteBridge create a ContentSite that can host the RootView ContentIsland - auto bridge = winrt::DesktopChildSiteBridge::Create(compositor, window.Id()); - bridge.Connect(rootView.Island()); - bridge.ResizePolicy(winrt::ContentSizePolicy::ResizeContentToParentWindow); - - auto invScale = 1.0f / scaleFactor; - rootView.RootVisual().Scale({invScale, invScale, invScale}); - rootView.ScaleFactor(scaleFactor); - - // Set the intialSize of the root view - UpdateRootViewSizeToAppWindow(rootView, window); - - bridge.Show(); - - // Run the main application event loop - dispatcherQueueController.DispatcherQueue().RunEventLoop(); - - // Rundown the DispatcherQueue. This drains the queue and raises events to let components - // know the message loop has finished. - dispatcherQueueController.ShutdownQueue(); - - bridge.Close(); - bridge = nullptr; + auto window = app.AppWindow(); + window.Title(winrt::to_hstring(manifest.displayName)); + window.Resize({600, 800}); - // Destroy all Composition objects - compositor.Close(); - compositor = nullptr; + app.Start(); } diff --git a/packages/app/windows/Win32/ReactApp.Package.wapproj b/packages/app/windows/Win32/ReactApp.Package.wapproj index 02a30f8a5..ea9bc070f 100644 --- a/packages/app/windows/Win32/ReactApp.Package.wapproj +++ b/packages/app/windows/Win32/ReactApp.Package.wapproj @@ -25,6 +25,7 @@ --> $([MSBuild]::MakeRelative($(MSBuildThisFileDirectory), $(SolutionDir)))\$(MSBuildProjectName)\bin\ $(BaseOutputPath)\$(Platform)\$(Configuration)\ + $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'app.json')) $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ @@ -83,6 +84,7 @@ + diff --git a/packages/app/windows/Win32/ReactApp.vcxproj b/packages/app/windows/Win32/ReactApp.vcxproj index 12cc2dc8d..c0c267ad5 100644 --- a/packages/app/windows/Win32/ReactApp.vcxproj +++ b/packages/app/windows/Win32/ReactApp.vcxproj @@ -135,7 +135,6 @@ - diff --git a/packages/app/windows/project.mjs b/packages/app/windows/project.mjs index 6ee2d617b..bc56b0515 100644 --- a/packages/app/windows/project.mjs +++ b/packages/app/windows/project.mjs @@ -126,7 +126,7 @@ function warn(message) { * @param {string} source * @returns {AssetItems} */ -function generateContentItems( +function generateAssetItems( resources, projectPath, assets = { assetFilters: [], assetItemFilters: [], assetItems: [] }, @@ -159,7 +159,7 @@ function generateContentItems( const files = fs .readdirSync(resourcePath) .map((file) => path.join(resource, file)); - generateContentItems( + generateAssetItems( files, projectPath, assets, @@ -168,7 +168,7 @@ function generateContentItems( fs ); } else { - const assetPath = normalizePath(path.relative(projectPath, resourcePath)); + const assetPath = normalizePath(resourcePath); /** * When a resources folder is included in the manifest, the directory * structure within the folder must be maintained. For example, given @@ -199,6 +199,53 @@ function generateContentItems( return assets; } +/** + * @param {string[]} resources + * @param {string} projectPath + * @param {string=} currentDir + * @param {string=} destination + * @param {string[]=} result + * @returns {string[]} + */ +function generateContentItems( + resources, + projectPath, + currentDir = ".", + destination = "Bundle", + result = [], + fs = nodefs +) { + for (const res of resources) { + const assetPath = path.isAbsolute(res) + ? path.relative(projectPath, res) + : path.join(currentDir, res); + if (!fs.existsSync(assetPath)) { + warn(`Resource not found: ${res}`); + continue; + } + + const link = `${destination}\\${path.basename(assetPath)}`; + if (fs.statSync(assetPath).isDirectory()) { + generateContentItems( + fs.readdirSync(assetPath), + projectPath, + assetPath, + link, + result, + fs + ); + } else { + result.push( + ``, + ` ${link}`, + ` PreserveNewest`, + `` + ); + } + } + return result; +} + /** * Finds NuGet dependencies. * @@ -318,38 +365,60 @@ export function importTargets(refs) { /** * @param {string[] | { windows?: string[] } | undefined} resources * @param {string} projectPath + * @param {Pick} options * @returns {Assets} */ -export function parseResources(resources, projectPath, fs = nodefs) { +export function parseResources(resources, projectPath, options, fs = nodefs) { if (!Array.isArray(resources)) { if (resources?.windows) { - return parseResources(resources.windows, projectPath, fs); + return parseResources(resources.windows, projectPath, options, fs); } - return { assetItems: "", assetItemFilters: "", assetFilters: "" }; - } + return { + assetItems: "", + assetItemFilters: "", + assetFilters: "", + contentItems: "", + }; + } else if (!options.useFabric) { + const { assetItems, assetItemFilters, assetFilters } = generateAssetItems( + resources, + projectPath, + /* assets */ undefined, + /* currentFilter */ undefined, + /* source */ undefined, + fs + ); - const { assetItems, assetItemFilters, assetFilters } = generateContentItems( - resources, - projectPath, - /* assets */ undefined, - /* currentFilter */ undefined, - /* source */ undefined, - fs - ); + return { + assetItems: assetItems.join("\n "), + assetItemFilters: assetItemFilters.join("\n "), + assetFilters: assetFilters.join("\n "), + contentItems: "", + }; + } return { - assetItems: assetItems.join("\n "), - assetItemFilters: assetItemFilters.join("\n "), - assetFilters: assetFilters.join("\n "), + assetItems: "", + assetItemFilters: "", + assetFilters: "", + contentItems: generateContentItems( + resources, + projectPath, + /** currentDir */ undefined, + /** destination */ undefined, + /** result */ undefined, + fs + ).join("\n "), }; } /** * Reads manifest file and and resolves paths to bundle resources. * @param {string | null} manifestFilePath Path to the closest manifest file. + * @param {Pick} options * @returns {AppxBundle} Application name, and paths to directories and files to include. */ -export function getBundleResources(manifestFilePath, fs = nodefs) { +export function getBundleResources(manifestFilePath, options, fs = nodefs) { // Default value if manifest or 'name' field don't exist. const defaultName = "ReactTestApp"; @@ -374,7 +443,7 @@ export function getBundleResources(manifestFilePath, fs = nodefs) { windows || {}, projectPath ), - ...parseResources(resources, projectPath, fs), + ...parseResources(resources, projectPath, options, fs), }; } catch (e) { if (isErrorLike(e)) { @@ -393,6 +462,7 @@ export function getBundleResources(manifestFilePath, fs = nodefs) { assetItems: "", assetItemFilters: "", assetFilters: "", + contentItems: "", packageCertificate: "", }; } @@ -404,21 +474,22 @@ export function getBundleResources(manifestFilePath, fs = nodefs) { * @returns {Promise} */ export async function projectInfo( - { useFabric, useNuGet }, + options, rnWindowsPath, destPath, fs = nodefs ) { const version = getPackageVersion("react-native-windows", rnWindowsPath, fs); const versionNumber = toVersionNumber(version); - const newArch = useFabric ?? versionNumber >= v(0, 80, 0); + const manifestFilePath = findNearest("app.json", destPath, fs); + const useFabric = options.useFabric ?? versionNumber >= v(0, 80, 0); return { version, versionNumber, - bundle: getBundleResources(findNearest("app.json", destPath, fs), fs), + bundle: getBundleResources(manifestFilePath, { useFabric }, fs), nugetDependencies: await getNuGetDependencies(rnWindowsPath), - useExperimentalNuGet: newArch || useNuGet, - useFabric: newArch, + useExperimentalNuGet: useFabric || options.useNuGet, + useFabric, }; } diff --git a/packages/app/windows/win32.mjs b/packages/app/windows/win32.mjs index 96130083e..b97ac352e 100644 --- a/packages/app/windows/win32.mjs +++ b/packages/app/windows/win32.mjs @@ -18,12 +18,16 @@ export function configureForWin32({ ["Main.rc"], ["Main.small.ico"], ["Package.appxmanifest"], - ["ReactApp.Package.wapproj"], + [ + "ReactApp.Package.wapproj", + { + "": bundle.contentItems, + }, + ], [ "ReactApp.vcxproj", { "REACT_NATIVE_VERSION=1000000000;": `REACT_NATIVE_VERSION=${versionNumber};`, - "": bundle.assetItems, "": importTargets(nugetDependencies), ...(typeof bundle.singleApp === "string" @@ -31,13 +35,7 @@ export function configureForWin32({ : undefined), }, ], - [ - "ReactApp.vcxproj.filters", - { - "": bundle.assetItemFilters, - "": bundle.assetFilters, - }, - ], + ["ReactApp.vcxproj.filters"], ["resource.h"], ], solutionTemplatePath: path.join(