diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd641034e9..405321ea7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,12 @@ concurrency: # Hermes, sanitizer, and macOS-hosted Android builds run in the Azure pipeline nightly.yml. jobs: + # TEMPORARY: remove before merge; exercise React Native unconditionally on every PR. + ReactNative: + uses: ./.github/workflows/reactnative-pr.yml + with: + force-run: true + # ── Apple: macOS ────────────────────────────────────────────── MacOS: uses: ./.github/workflows/build-macos.yml @@ -164,6 +170,8 @@ jobs: # Run after unsuccessful dependencies too, so they cannot skip the required check. if: always() needs: + # TEMPORARY: remove before merge. + - ReactNative - MacOS - MacOS_Ninja - iOS_Xcode26 diff --git a/.github/workflows/reactnative-android.yml b/.github/workflows/reactnative-android.yml new file mode 100644 index 0000000000..2e3ab9b9c7 --- /dev/null +++ b/.github/workflows/reactnative-android.yml @@ -0,0 +1,29 @@ +name: React Native Build Android +on: workflow_call +jobs: + build-android: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - name: Setting up developer environment + run: node setup_dev.js + working-directory: Embedding/ReactNative/Package + - name: List installed packages + run: npm ls + working-directory: Embedding/ReactNative/Apps/Playground + - name: Gulp (Android) + run: npx gulp buildAndroid + working-directory: Embedding/ReactNative/Package + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-ios.yml b/.github/workflows/reactnative-ios.yml new file mode 100644 index 0000000000..9a5fb1951e --- /dev/null +++ b/.github/workflows/reactnative-ios.yml @@ -0,0 +1,27 @@ +name: React Native Build iOS +on: workflow_call +jobs: + build-iOS: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Setting up developer environment + run: node setup_dev.js + working-directory: Embedding/ReactNative/Package + - name: Clean and Install Pods + run: | + cd ios + rm -rf Pods Podfile.lock + pod install + working-directory: Embedding/ReactNative/Apps/Playground + - name: Gulp (iOS) + run: npx gulp buildIOS + working-directory: Embedding/ReactNative/Package + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-package.yml b/.github/workflows/reactnative-package.yml new file mode 100644 index 0000000000..2101506fcf --- /dev/null +++ b/.github/workflows/reactnative-package.yml @@ -0,0 +1,33 @@ +name: React Native Package +on: workflow_call +jobs: + package: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: NPM Install + run: npm install + working-directory: Embedding/ReactNative/Package + - name: Download Assembled Folder + uses: actions/download-artifact@v4 + with: + name: Assembled + path: Embedding/ReactNative/Package/Assembled + - name: Publish Package dry run + run: | + npm publish --access public --dry-run + npm pack + working-directory: Embedding/ReactNative/Package/Assembled + - name: Upload .tgz tarball + uses: actions/upload-artifact@v4 + with: + name: BabylonReactNative + path: Embedding/ReactNative/Package/Assembled/*.tgz + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-pr.yml b/.github/workflows/reactnative-pr.yml new file mode 100644 index 0000000000..9d7562c09a --- /dev/null +++ b/.github/workflows/reactnative-pr.yml @@ -0,0 +1,68 @@ +name: React Native PR Build +on: + pull_request: + branches: + - master + paths: + - Embedding/ReactNative/** + - .github/workflows/reactnative-*.yml + workflow_call: + inputs: + force-run: + description: Run the graph even while ci.yml also calls it + type: boolean + default: true +jobs: + entry: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + run: ${{ steps.select.outputs.run }} + steps: + - uses: actions/checkout@v4.2.2 + - name: Avoid duplicate PR builds + id: select + env: + FORCE_RUN: ${{ inputs.force-run }} + # TEMPORARY: remove before merge with the unconditional ci.yml call. + # Path-triggered builds automatically resume as soon as that call is removed. + run: | + if [ "$FORCE_RUN" = "true" ] || ! grep -Eq '^[[:space:]]+uses: \./\.github/workflows/reactnative-pr\.yml[[:space:]]*$' .github/workflows/ci.yml; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + build-android: + needs: [entry] + uses: ./.github/workflows/reactnative-android.yml + if: ${{ needs.entry.outputs.run == 'true' }} + build-iOS: + needs: [entry] + uses: ./.github/workflows/reactnative-ios.yml + if: ${{ needs.entry.outputs.run == 'true' }} + build-windows: + needs: [entry] + uses: ./.github/workflows/reactnative-windows.yml + if: ${{ needs.entry.outputs.run == 'true' }} + build-typescript: + needs: [entry] + uses: ./.github/workflows/reactnative-typescript.yml + if: ${{ needs.entry.outputs.run == 'true' }} + package: + needs: + - build-typescript + uses: ./.github/workflows/reactnative-package.yml + test-package-ios: + needs: + - package + uses: ./.github/workflows/reactnative-test-ios.yml + test-package-android: + needs: + - package + uses: ./.github/workflows/reactnative-test-android.yml + test-package-windows: + needs: + - package + uses: ./.github/workflows/reactnative-test-windows.yml +permissions: + contents: read diff --git a/.github/workflows/reactnative-publish-preview.yml b/.github/workflows/reactnative-publish-preview.yml new file mode 100644 index 0000000000..4ebe9c6c3f --- /dev/null +++ b/.github/workflows/reactnative-publish-preview.yml @@ -0,0 +1,60 @@ +name: React Native Publish Package For Preview +on: + workflow_dispatch: + inputs: + release_version: + description: NPM Release Version + required: true + type: string + default: 1.0.0 + NPM_tag: + description: NPM Tag + required: true + type: string + default: preview +jobs: + build-typescript: + uses: ./.github/workflows/reactnative-typescript.yml + package: + needs: + - build-typescript + uses: ./.github/workflows/reactnative-package.yml + publish: + runs-on: macos-15 + needs: + - package + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: https://registry.npmjs.org + - name: Download Assembled Folder + uses: actions/download-artifact@v4 + with: + name: Assembled + path: Embedding/ReactNative/Package/Assembled + - name: Diagnose NPM Registry Connectivity + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + echo "--- Node & NPM versions ---" + node --version + npm --version + echo "--- NPM config registry ---" + npm config get registry + echo "--- NPM ping ---" + npm ping + echo "--- NPM whoami ---" + npm whoami + - name: Version & Publish Package + run: | + npm version --no-git-tag-version "$RELEASE_VERSION" + npm publish --access public --tag "$NPM_TAG" + working-directory: Embedding/ReactNative/Package/Assembled + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + RELEASE_VERSION: ${{ inputs.release_version }} + NPM_TAG: ${{ inputs.NPM_tag }} +permissions: + contents: read diff --git a/.github/workflows/reactnative-publish.yml b/.github/workflows/reactnative-publish.yml new file mode 100644 index 0000000000..50b4237ed5 --- /dev/null +++ b/.github/workflows/reactnative-publish.yml @@ -0,0 +1,54 @@ +name: React Native Publish Package +on: + release: + types: + - published +jobs: + build-typescript: + uses: ./.github/workflows/reactnative-typescript.yml + if: ${{ startsWith(github.event.release.tag_name, 'react-native/v') }} + package: + needs: + - build-typescript + uses: ./.github/workflows/reactnative-package.yml + if: ${{ startsWith(github.event.release.tag_name, 'react-native/v') }} + publish: + runs-on: macos-15 + needs: + - package + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: https://registry.npmjs.org + - name: Download Assembled Folder + uses: actions/download-artifact@v4 + with: + name: Assembled + path: Embedding/ReactNative/Package/Assembled + - name: Diagnose NPM Registry Connectivity + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + echo "--- Node & NPM versions ---" + node --version + npm --version + echo "--- NPM config registry ---" + npm config get registry + echo "--- NPM ping ---" + npm ping + echo "--- NPM whoami ---" + npm whoami + - name: Version & Publish Package + run: | + version="${RELEASE_TAG#react-native/v}" + npm version --no-git-tag-version "$version" + npm publish --access public + working-directory: Embedding/ReactNative/Package/Assembled + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + if: ${{ startsWith(github.event.release.tag_name, 'react-native/v') }} +permissions: + contents: read diff --git a/.github/workflows/reactnative-test-android.yml b/.github/workflows/reactnative-test-android.yml new file mode 100644 index 0000000000..36e2e95424 --- /dev/null +++ b/.github/workflows/reactnative-test-android.yml @@ -0,0 +1,37 @@ +name: React Native Test Android Package +on: workflow_call +jobs: + build-test-android: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: BabylonReactNative + path: Embedding/ReactNative/artifact + - name: Install build package + run: | + package=$(find ../../artifact -maxdepth 1 -name 'babylonjs-react-native-*.tgz' -print -quit) + test -n "$package" + npm install "$package" + npm install + working-directory: Embedding/ReactNative/Apps/Playground + - name: Gulp (Android) + run: | + npm install + npx gulp buildAndroid + working-directory: Embedding/ReactNative/Package + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-test-ios.yml b/.github/workflows/reactnative-test-ios.yml new file mode 100644 index 0000000000..45f4f4e019 --- /dev/null +++ b/.github/workflows/reactnative-test-ios.yml @@ -0,0 +1,38 @@ +name: React Native Test iOS Package +on: workflow_call +jobs: + build-test-iOS: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: BabylonReactNative + path: Embedding/ReactNative/artifact + - name: Install build package + run: | + package=$(find ../../artifact -maxdepth 1 -name 'babylonjs-react-native-*.tgz' -print -quit) + test -n "$package" + npm install "$package" + npm install + working-directory: Embedding/ReactNative/Apps/Playground + - name: Clean and Install Pods + run: | + cd ios + rm -rf Pods Podfile.lock + pod install + working-directory: Embedding/ReactNative/Apps/Playground + - name: Gulp (iOS) + run: | + npm install + npx gulp buildIOS + working-directory: Embedding/ReactNative/Package + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-test-windows.yml b/.github/workflows/reactnative-test-windows.yml new file mode 100644 index 0000000000..01233a04ce --- /dev/null +++ b/.github/workflows/reactnative-test-windows.yml @@ -0,0 +1,152 @@ +name: React Native Test Windows Package +on: + workflow_call: + inputs: + WindowsTargetPlatformVersion: + description: Windows Target Platform Version + type: string + default: 10.0.22621.0 + required: false + WindowsTargetPlatformMinVersion: + description: Windows Minimum Target Platform Version + type: string + default: 10.0.17763.0 + required: false +jobs: + build-test-windows: + runs-on: windows-2022 + steps: + - name: Support longpaths + run: git config --system core.longpaths true + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + - name: Download package + uses: actions/download-artifact@v4 + with: + name: BabylonReactNative + path: Embedding/ReactNative/artifact + - name: Install packaged sources + shell: pwsh + run: | + Copy-Item package-windows.json package.json -Force + Copy-Item package-lock-windows.json package-lock.json -Force + $package = Get-ChildItem ../../artifact/babylonjs-react-native-*.tgz -File | Select-Object -First 1 + if (-not $package) { + throw "BabylonReactNative package artifact was not found." + } + npm ci + npm install --no-save --package-lock=false $package.FullName + working-directory: Embedding/ReactNative/Apps/Playground + - name: Generate Windows project + run: node generate-windows-test-app.mjs + working-directory: Embedding/ReactNative/Apps/Playground + - name: Build packaged Windows consumer + shell: pwsh + run: | + $solutionPath = (Resolve-Path Playground.sln).Path + $solutionDir = "$(Split-Path $solutionPath)\" + $solutionFileName = Split-Path $solutionPath -Leaf + $projectPath = (Resolve-Path ../node_modules/.generated/windows/UWP/ReactTestApp.vcxproj).Path + $reactNativeNuGetTargetsPath = (Resolve-Path ReactNativeNuGet.targets).Path + $buildProperties = @( + "/p:Platform=x64", + "/p:Configuration=Release", + "/p:SolutionPath=$solutionPath", + "/p:SolutionDir=$solutionDir", + "/p:SolutionFileName=$solutionFileName", + "/p:WindowsTargetPlatformVersion=${{ inputs.WindowsTargetPlatformVersion }}", + "/p:WindowsTargetPlatformMinVersion=${{ inputs.WindowsTargetPlatformMinVersion }}", + "/p:ForceImportAfterCppTargets=$reactNativeNuGetTargetsPath", + "/p:WindowsAppSDKVerifyTransitiveDependencies=false", + "/p:AppxBundle=Never", + "/p:AppxPackage=false", + "/p:UseBundle=false", + "/v:minimal" + ) + + function Build-Project([string]$path) { + [xml]$projectXml = Get-Content $path + $sourceItemImports = Select-Xml -Xml $projectXml -XPath "//*[local-name()='Import' and contains(@Project, 'Microsoft.ReactNative.Cxx.vcxitems')]" + if ($sourceItemImports) { + foreach ($sourceItemImport in $sourceItemImports) { + $sourceItemImport.Node.ParentNode.RemoveChild($sourceItemImport.Node) | Out-Null + } + $projectXml.Save($path) + } + + $evaluatedProperties = MSBuild $path /p:UseExperimentalNuget=true /getProperty:UseExperimentalNuget,MSBuildProjectFullPath,LanguageStandard /getItem:ProjectReference,ClCompile @buildProperties | ConvertFrom-Json + if ($evaluatedProperties.Properties.UseExperimentalNuget -ne "true") { + throw "RNW NuGet mode was not enabled for $path." + } + $rnwSourceReference = $evaluatedProperties.Items.ProjectReference | Where-Object Identity -Like "*\react-native-windows\Microsoft.ReactNative\Microsoft.ReactNative.vcxproj" + if ($rnwSourceReference) { + throw "RNW source project reference was enabled for $path." + } + $rnwSourceCompile = $evaluatedProperties.Items.ClCompile | Where-Object Identity -Like "*\react-native-windows\Microsoft.ReactNative.Cxx\*" + if ($rnwSourceCompile) { + throw "RNW source compile items were enabled for $path." + } + if ($path -eq $projectPath -and $evaluatedProperties.Properties.LanguageStandard -ne "stdcpp20") { + throw "C++20 was not enabled for the generated Windows app." + } + $mainPageCompile = $evaluatedProperties.Items.ClCompile | Where-Object Identity -Like "*\react-native-test-app\windows\UWP\MainPage.cpp" + if ($path -eq $projectPath -and ($mainPageCompile.LanguageStandard -ne "stdcpp20" -or $mainPageCompile.AdditionalOptions -match "(^|\s)/await(\s|$)")) { + throw "The generated Windows app was not configured for standard C++20 coroutines." + } + + MSBuild $path /p:UseExperimentalNuget=true /t:Restore /m /p:RestoreRecursive=false @buildProperties + if ($LASTEXITCODE -ne 0) { + throw "Restore failed for $path." + } + + MSBuild $path /p:UseExperimentalNuget=true /m /p:BuildProjectReferences=false @buildProperties + if ($LASTEXITCODE -ne 0) { + throw "Build failed for $path." + } + } + + [xml]$autolinkedProjects = Get-Content ../node_modules/.generated/windows/UWP/AutolinkedNativeModules.g.targets + $projectDirectory = Split-Path $projectPath + $projectReferences = Select-Xml -Xml $autolinkedProjects -XPath "//*[local-name()='ProjectReference']" + foreach ($projectReference in $projectReferences) { + $relativePath = $projectReference.Node.Include.Replace('$(ProjectDir)', '') + Build-Project (Resolve-Path (Join-Path $projectDirectory $relativePath)).Path + } + + $mainPagePath = (Resolve-Path ../node_modules/react-native-test-app/windows/UWP/MainPage.cpp).Path + $mainPageContent = [System.IO.File]::ReadAllText($mainPagePath) + $onNavigatedToPattern = "(?s)IAsyncAction MainPage::OnNavigatedTo\(NavigationEventArgs const &e\)\s*\{.*?\}\s*(?=bool MainPage::LoadJSBundleFrom)" + if (-not [System.Text.RegularExpressions.Regex]::IsMatch($mainPageContent, $onNavigatedToPattern)) { + throw "The expected react-native-test-app OnNavigatedTo implementation was not found." + } + $onNavigatedToReplacement = @( + "IAsyncAction MainPage::OnNavigatedTo(NavigationEventArgs const &e)", + "{", + " Base::OnNavigatedTo(e);", + " LoadFromDevServer({}, {});", + " co_return;", + "}", + "" + ) -join [System.Environment]::NewLine + $updatedMainPageContent = [System.Text.RegularExpressions.Regex]::Replace( + $mainPageContent, + $onNavigatedToPattern, + $onNavigatedToReplacement + ) + [System.IO.File]::WriteAllText($mainPagePath, $updatedMainPageContent) + + $buildProperties += @( + "/p:LanguageStandard=stdcpp20", + "/p:CppWinRTEnableLegacyCoroutines=false" + ) + Build-Project $projectPath + working-directory: Embedding/ReactNative/Apps/Playground/windows + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-typescript.yml b/.github/workflows/reactnative-typescript.yml new file mode 100644 index 0000000000..3fb532680f --- /dev/null +++ b/.github/workflows/reactnative-typescript.yml @@ -0,0 +1,39 @@ +name: React Native Assemble +on: workflow_call +jobs: + Build: + runs-on: macos-15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: NPM Install + run: npm install + working-directory: Embedding/ReactNative/Package + - name: NPM Install (Modules) + run: | + export BABYLON_NO_CMAKE_POSTINSTALL=1 + npm install + working-directory: Embedding/ReactNative/Modules/@babylonjs/react-native + - name: NPM Install (Playground) + run: | + export BABYLON_NO_CMAKE_POSTINSTALL=1 + npm install + working-directory: Embedding/ReactNative/Apps/Playground + - name: Playground unit tests + run: npm test -- --runInBand + working-directory: Embedding/ReactNative/Apps/Playground + - name: Gulp + run: npx gulp buildAssembled + working-directory: Embedding/ReactNative/Package + - name: Upload Assembled Folder + uses: actions/upload-artifact@v4 + with: + name: Assembled + path: Embedding/ReactNative/Package/Assembled + timeout-minutes: 90 +permissions: + contents: read diff --git a/.github/workflows/reactnative-windows.yml b/.github/workflows/reactnative-windows.yml new file mode 100644 index 0000000000..2c6f575b70 --- /dev/null +++ b/.github/workflows/reactnative-windows.yml @@ -0,0 +1,142 @@ +name: React Native Build Windows +on: + workflow_call: + inputs: + WindowsTargetPlatformVersion: + description: Windows Target Platform Version + type: string + default: 10.0.22621.0 + required: false + WindowsTargetPlatformMinVersion: + description: Windows Minimum Target Platform Version + type: string + default: 10.0.17763.0 + required: false +jobs: + build-windows: + runs-on: windows-2022 + steps: + - name: Support longpaths + run: git config --system core.longpaths true + - name: Checkout Repo + uses: actions/checkout@v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + - name: Setting up developer environment + run: | + Copy-Item -Path "Apps/Playground/package-windows.json" -Destination "Apps/Playground/package.json" -Force + Copy-Item -Path "Apps/Playground/package-lock-windows.json" -Destination "Apps/Playground/package-lock.json" -Force + cd Package + node setup_dev.js + working-directory: Embedding/ReactNative + - name: NPM Generate Windows Project + run: node generate-windows-test-app.mjs + working-directory: Embedding/ReactNative/Apps/Playground + - name: Build Windows consumer + shell: pwsh + run: | + $solutionPath = (Resolve-Path Playground.sln).Path + $solutionDir = "$(Split-Path $solutionPath)\" + $solutionFileName = Split-Path $solutionPath -Leaf + $projectPath = (Resolve-Path ../node_modules/.generated/windows/UWP/ReactTestApp.vcxproj).Path + $reactNativeNuGetTargetsPath = (Resolve-Path ReactNativeNuGet.targets).Path + $buildProperties = @( + "/p:Platform=x64", + "/p:Configuration=Release", + "/p:SolutionPath=$solutionPath", + "/p:SolutionDir=$solutionDir", + "/p:SolutionFileName=$solutionFileName", + "/p:WindowsTargetPlatformVersion=${{ inputs.WindowsTargetPlatformVersion }}", + "/p:WindowsTargetPlatformMinVersion=${{ inputs.WindowsTargetPlatformMinVersion }}", + "/p:ForceImportAfterCppTargets=$reactNativeNuGetTargetsPath", + "/p:WindowsAppSDKVerifyTransitiveDependencies=false", + "/p:AppxBundle=Never", + "/p:AppxPackage=false", + "/p:UseBundle=false", + "/v:minimal" + ) + + function Build-Project([string]$path) { + [xml]$projectXml = Get-Content $path + $sourceItemImports = Select-Xml -Xml $projectXml -XPath "//*[local-name()='Import' and contains(@Project, 'Microsoft.ReactNative.Cxx.vcxitems')]" + if ($sourceItemImports) { + foreach ($sourceItemImport in $sourceItemImports) { + $sourceItemImport.Node.ParentNode.RemoveChild($sourceItemImport.Node) | Out-Null + } + $projectXml.Save($path) + } + + $evaluatedProperties = MSBuild $path /p:UseExperimentalNuget=true /getProperty:UseExperimentalNuget,MSBuildProjectFullPath,LanguageStandard /getItem:ProjectReference,ClCompile @buildProperties | ConvertFrom-Json + if ($evaluatedProperties.Properties.UseExperimentalNuget -ne "true") { + throw "RNW NuGet mode was not enabled for $path." + } + $rnwSourceReference = $evaluatedProperties.Items.ProjectReference | Where-Object Identity -Like "*\react-native-windows\Microsoft.ReactNative\Microsoft.ReactNative.vcxproj" + if ($rnwSourceReference) { + throw "RNW source project reference was enabled for $path." + } + $rnwSourceCompile = $evaluatedProperties.Items.ClCompile | Where-Object Identity -Like "*\react-native-windows\Microsoft.ReactNative.Cxx\*" + if ($rnwSourceCompile) { + throw "RNW source compile items were enabled for $path." + } + if ($path -eq $projectPath -and $evaluatedProperties.Properties.LanguageStandard -ne "stdcpp20") { + throw "C++20 was not enabled for the generated Windows app." + } + $mainPageCompile = $evaluatedProperties.Items.ClCompile | Where-Object Identity -Like "*\react-native-test-app\windows\UWP\MainPage.cpp" + if ($path -eq $projectPath -and ($mainPageCompile.LanguageStandard -ne "stdcpp20" -or $mainPageCompile.AdditionalOptions -match "(^|\s)/await(\s|$)")) { + throw "The generated Windows app was not configured for standard C++20 coroutines." + } + + MSBuild $path /p:UseExperimentalNuget=true /t:Restore /m /p:RestoreRecursive=false @buildProperties + if ($LASTEXITCODE -ne 0) { + throw "Restore failed for $path." + } + + MSBuild $path /p:UseExperimentalNuget=true /m /p:BuildProjectReferences=false @buildProperties + if ($LASTEXITCODE -ne 0) { + throw "Build failed for $path." + } + } + + [xml]$autolinkedProjects = Get-Content ../node_modules/.generated/windows/UWP/AutolinkedNativeModules.g.targets + $projectDirectory = Split-Path $projectPath + $projectReferences = Select-Xml -Xml $autolinkedProjects -XPath "//*[local-name()='ProjectReference']" + foreach ($projectReference in $projectReferences) { + $relativePath = $projectReference.Node.Include.Replace('$(ProjectDir)', '') + Build-Project (Resolve-Path (Join-Path $projectDirectory $relativePath)).Path + } + + $mainPagePath = (Resolve-Path ../node_modules/react-native-test-app/windows/UWP/MainPage.cpp).Path + $mainPageContent = [System.IO.File]::ReadAllText($mainPagePath) + $onNavigatedToPattern = "(?s)IAsyncAction MainPage::OnNavigatedTo\(NavigationEventArgs const &e\)\s*\{.*?\}\s*(?=bool MainPage::LoadJSBundleFrom)" + if (-not [System.Text.RegularExpressions.Regex]::IsMatch($mainPageContent, $onNavigatedToPattern)) { + throw "The expected react-native-test-app OnNavigatedTo implementation was not found." + } + $onNavigatedToReplacement = @( + "IAsyncAction MainPage::OnNavigatedTo(NavigationEventArgs const &e)", + "{", + " Base::OnNavigatedTo(e);", + " LoadFromDevServer({}, {});", + " co_return;", + "}", + "" + ) -join [System.Environment]::NewLine + $updatedMainPageContent = [System.Text.RegularExpressions.Regex]::Replace( + $mainPageContent, + $onNavigatedToPattern, + $onNavigatedToReplacement + ) + [System.IO.File]::WriteAllText($mainPagePath, $updatedMainPageContent) + + $buildProperties += @( + "/p:LanguageStandard=stdcpp20", + "/p:CppWinRTEnableLegacyCoroutines=false" + ) + Build-Project $projectPath + working-directory: Embedding/ReactNative/Apps/Playground/windows + timeout-minutes: 90 +permissions: + contents: read diff --git a/Embedding/ReactNative/.gitignore b/Embedding/ReactNative/.gitignore new file mode 100644 index 0000000000..1911a4bfbd --- /dev/null +++ b/Embedding/ReactNative/.gitignore @@ -0,0 +1,17 @@ +# OSX +.DS_Store + +# Android Studio +.project +.classpath +org.eclipse.buildship.core.prefs + +# XCode +IDEWorkspaceChecks.plist +WorkspaceSettings.xcsettings + +# npm +node_modules + +# Build output, largely Babylon Native +Build \ No newline at end of file diff --git a/Embedding/ReactNative/Apps/Playground/.gitignore b/Embedding/ReactNative/Apps/Playground/.gitignore new file mode 100644 index 0000000000..5fbd50620e --- /dev/null +++ b/Embedding/ReactNative/Apps/Playground/.gitignore @@ -0,0 +1,14 @@ +*.binlog +*.hprof +*.zip +.DS_Store +.gradle/ +.idea/ +.vs/ +.xcode.env +Pods/ +build/ +dist/ +local.properties +msbuild.binlog +node_modules/ diff --git a/Embedding/ReactNative/Apps/Playground/.watchmanconfig b/Embedding/ReactNative/Apps/Playground/.watchmanconfig new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Embedding/ReactNative/Apps/Playground/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/Embedding/ReactNative/Apps/Playground/App.tsx b/Embedding/ReactNative/Apps/Playground/App.tsx new file mode 100644 index 0000000000..c417ee8196 --- /dev/null +++ b/Embedding/ReactNative/Apps/Playground/App.tsx @@ -0,0 +1,345 @@ +/** + * Generated with the TypeScript template + * https://github.com/react-native-community/react-native-template-typescript + * + * @format + */ + +import React, { + useState, + FunctionComponent, + useEffect, + useCallback, +} from "react"; +import { + SafeAreaView, + StatusBar, + Button, + View, + Text, + ViewProps, + Image, +} from "react-native"; + +import { + EngineView, + useEngine, + EngineViewCallbacks, +} from "@babylonjs/react-native"; +import { + Scene, + Vector3, + ArcRotateCamera, + Camera, + WebXRSessionManager, + SceneLoader, + TransformNode, + DeviceSourceManager, + DeviceType, + PointerInput, + WebXRTrackingState, + IMouseEvent, +} from "@babylonjs/core"; +import "@babylonjs/loaders"; +import Slider from "@react-native-community/slider"; + +const EngineScreen: FunctionComponent = (props: ViewProps) => { + const defaultScale = 1; + const enableSnapshots = false; + + const engine = useEngine(); + const [toggleView, setToggleView] = useState(false); + const [camera, setCamera] = useState(); + const [rootNode, setRootNode] = useState(); + const [scene, setScene] = useState(); + const [xrSession, setXrSession] = useState(); + const [scale, setScale] = useState(defaultScale); + const [snapshotData, setSnapshotData] = useState(); + const [engineViewCallbacks, setEngineViewCallbacks] = + useState(); + const [trackingState, setTrackingState] = useState(); + const [xrError, setXrError] = useState(); + const [controlsHeight, setControlsHeight] = useState(0); + + useEffect(() => { + if (engine) { + const scene = new Scene(engine); + setScene(scene); + scene.createDefaultCamera(true); + (scene.activeCamera as ArcRotateCamera).beta -= Math.PI / 8; + setCamera(scene.activeCamera!); + scene.createDefaultLight(true); + const rootNode = new TransformNode("Root Container", scene); + setRootNode(rootNode); + + const deviceSourceManager = new DeviceSourceManager(engine); + const handlePointerInput = (event: IMouseEvent) => { + if (event.inputIndex === PointerInput.Move && event.movementX) { + rootNode.rotate(Vector3.Down(), event.movementX * 0.005); + } + }; + + deviceSourceManager.onDeviceConnectedObservable.add((device) => { + if (device.deviceType === DeviceType.Touch) { + const touch = deviceSourceManager.getDeviceSource( + device.deviceType, + device.deviceSlot + )!; + touch.onInputChangedObservable.add((touchEvent) => { + handlePointerInput(touchEvent); + }); + } else if (device.deviceType === DeviceType.Mouse) { + const mouse = deviceSourceManager.getDeviceSource( + device.deviceType, + device.deviceSlot + )!; + mouse.onInputChangedObservable.add((mouseEvent) => { + if (mouse.getInput(PointerInput.LeftClick)) { + handlePointerInput(mouseEvent); + } + }); + } + }); + + const transformContainer = new TransformNode( + "Transform Container", + scene + ); + transformContainer.parent = rootNode; + transformContainer.scaling.scaleInPlace(0.2); + transformContainer.position.y -= 0.2; + + scene.beforeRender = function () { + transformContainer.rotate( + Vector3.Up(), + 0.005 * scene.getAnimationRatio() + ); + }; + + SceneLoader.ImportMeshAsync( + "", + "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxAnimated/glTF-Binary/BoxAnimated.glb" + ).then((result) => { + const mesh = result.meshes[0]; + mesh.parent = transformContainer; + }); + } + }, [engine]); + + useEffect(() => { + if (rootNode) { + rootNode.scaling = new Vector3(scale, scale, scale); + } + }, [rootNode, scale]); + + const trackingStateToString = ( + trackingState: WebXRTrackingState | undefined + ): string => { + return trackingState === undefined ? "" : WebXRTrackingState[trackingState]; + }; + + const onToggleXr = useCallback(async () => { + setXrError(undefined); + const action = xrSession ? "stop" : "start"; + + try { + if (xrSession) { + await xrSession.exitXRAsync(); + return; + } + + if (rootNode === undefined || scene === undefined) { + throw new Error("The scene is still initializing."); + } + + const xr = await scene.createDefaultXRExperienceAsync({ + disableDefaultUI: true, + disableTeleportation: true, + }); + const session = await xr.baseExperience.enterXRAsync( + "immersive-ar", + "unbounded", + xr.renderTarget + ); + setXrSession(session); + session.onXRSessionEnded.add(() => { + setXrSession(undefined); + setTrackingState(undefined); + }); + + setTrackingState(xr.baseExperience.camera.trackingState); + xr.baseExperience.camera.onTrackingStateChanged.add( + (newTrackingState) => { + setTrackingState(newTrackingState); + } + ); + + // TODO: Figure out why getFrontPosition stopped working + //box.position = (scene.activeCamera as TargetCamera).getFrontPosition(2); + const cameraRay = scene.activeCamera!.getForwardRay(1); + rootNode.position = cameraRay.origin.add( + cameraRay.direction.scale(cameraRay.length) + ); + rootNode.rotate(Vector3.Up(), 3.14159); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setXrError(`Unable to ${action} XR: ${message}`); + } + }, [rootNode, scene, xrSession]); + + const onInitialized = useCallback( + async (engineViewCallbacks: EngineViewCallbacks) => { + setEngineViewCallbacks(engineViewCallbacks); + }, + [engine] + ); + + const onSnapshot = useCallback(async () => { + if (engineViewCallbacks) { + setSnapshotData( + "data:image/jpeg;base64," + (await engineViewCallbacks.takeSnapshot()) + ); + } + }, [engineViewCallbacks]); + + return ( + <> + + {!toggleView && ( + + {enableSnapshots && ( + +