diff --git a/change/react-native-windows-2020-05-28-13-42-40-autolink.json b/change/react-native-windows-2020-05-28-13-42-40-autolink.json new file mode 100644 index 00000000000..e449a4b55dc --- /dev/null +++ b/change/react-native-windows-2020-05-28-13-42-40-autolink.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Auto-linking for native modules (source-based)", + "packageName": "react-native-windows", + "email": "jthysell@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-05-28T20:42:40.417Z" +} diff --git a/packages/E2ETest/windows/ReactUWPTestApp/App.xaml.cs b/packages/E2ETest/windows/ReactUWPTestApp/App.xaml.cs index 03fbb136dde..5ef955d0c10 100644 --- a/packages/E2ETest/windows/ReactUWPTestApp/App.xaml.cs +++ b/packages/E2ETest/windows/ReactUWPTestApp/App.xaml.cs @@ -41,6 +41,8 @@ public App() InstanceSettings.UseDeveloperSupport = false; #endif + Microsoft.ReactNative.Managed.AutolinkedNativeModules.RegisterAutolinkedNativeModulePackages(PackageProviders); // Includes any autolinked modules + PackageProviders.Add(new Microsoft.ReactNative.Managed.ReactPackageProvider()); PackageProviders.Add(new ReflectionReactPackageProvider()); PackageProviders.Add(new TreeDumpLibrary.ReactPackageProvider()); diff --git a/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.cs b/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.cs new file mode 100644 index 00000000000..72a24f0937d --- /dev/null +++ b/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.cs @@ -0,0 +1,13 @@ +// AutolinkedNativeModules.g.cs contents generated by "react-native autolink-windows" + +using System.Collections.Generic; + +namespace Microsoft.ReactNative.Managed +{ + internal static class AutolinkedNativeModules + { + internal static void RegisterAutolinkedNativeModulePackages(IList packageProviders) + { + } + } +} diff --git a/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.targets b/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..85bc2375663 --- /dev/null +++ b/packages/E2ETest/windows/ReactUWPTestApp/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/E2ETest/windows/ReactUWPTestApp/ReactUWPTestApp.csproj b/packages/E2ETest/windows/ReactUWPTestApp/ReactUWPTestApp.csproj index 8d576e3807e..25ad2cfb125 100644 --- a/packages/E2ETest/windows/ReactUWPTestApp/ReactUWPTestApp.csproj +++ b/packages/E2ETest/windows/ReactUWPTestApp/ReactUWPTestApp.csproj @@ -1,10 +1,9 @@  - + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug x86 @@ -125,6 +124,7 @@ App.xaml + MainPage.xaml @@ -159,9 +159,6 @@ 6.2.9 - - 2.3.191129002 - 1.0.3 @@ -182,5 +179,17 @@ dist/app/index.js - + + + + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + \ No newline at end of file diff --git a/packages/microsoft-reactnative-sampleapps/react-native.config.js b/packages/microsoft-reactnative-sampleapps/react-native.config.js index 25b1f17a2da..24587b25a88 100644 --- a/packages/microsoft-reactnative-sampleapps/react-native.config.js +++ b/packages/microsoft-reactnative-sampleapps/react-native.config.js @@ -1,3 +1,16 @@ +// Change the below to true for autolink to target SampleAppCS instead of SampleAppCPP. +// Then run `npx react-native autolink-windows` to actually run the autolink. +const targetCS = false; + module.exports = { reactNativePath: '../../vnext', + project: { + windows: { + sourceDir: 'windows', + solutionFile: 'SampleApps.sln', + project: { + projectFile: targetCS ? 'SampleAppCS\\SampleAppCS.csproj' : 'SampleAppCPP\\SampleAppCPP.vcxproj', + }, + }, + }, }; diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/App.cpp b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/App.cpp index f5330572651..87401b3a1e0 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/App.cpp +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/App.cpp @@ -2,7 +2,10 @@ // Licensed under the MIT License. #include "pch.h" + #include "App.h" + +#include "AutolinkedNativeModules.g.h" #include #include #include @@ -42,6 +45,8 @@ App::App() noexcept { ReactPropertyBag::Set(InstanceSettings().Properties(), ReactPropertyId{L"Prop1"}, 42); ReactPropertyBag::Set(InstanceSettings().Properties(), ReactPropertyId{L"Prop2"}, L"Hello World!"); + RegisterAutolinkedNativeModulePackages(PackageProviders()); // Includes any autolinked modules + PackageProviders().Append(make()); // Includes all modules in this project PackageProviders().Append(winrt::SampleLibraryCpp::ReactPackageProvider()); PackageProviders().Append(winrt::SampleLibraryCS::ReactPackageProvider()); diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.cpp b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.cpp new file mode 100644 index 00000000000..f3b94db59f9 --- /dev/null +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.cpp @@ -0,0 +1,13 @@ +// AutolinkedNativeModules.g.cpp contents generated by "react-native autolink-windows" +// clang-format off +#include "pch.h" +#include "AutolinkedNativeModules.g.h" + +namespace winrt::Microsoft::ReactNative +{ + +void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) +{ +} + +} diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.h b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.h new file mode 100644 index 00000000000..99c3efc78bd --- /dev/null +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.h @@ -0,0 +1,10 @@ +// AutolinkedNativeModules.g.h contents generated by "react-native autolink-windows" +#pragma once + +// clang-format off +namespace winrt::Microsoft::ReactNative +{ + +void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders); + +} diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.targets b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..85bc2375663 --- /dev/null +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj index 05eb188fcce..b5b1e1ed2b7 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj @@ -42,7 +42,7 @@ - + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ @@ -102,7 +102,7 @@ - + @@ -139,6 +139,7 @@ Code + App.xaml @@ -170,6 +171,7 @@ Code + Create @@ -193,9 +195,6 @@ false - - - {47eec7f3-40d3-49ba-82c1-eaf103b54215} @@ -210,11 +209,20 @@ + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj.filters b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj.filters index de9839ae11c..d29aa524fc1 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj.filters +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCPP/SampleAppCpp.vcxproj.filters @@ -11,12 +11,14 @@ + + diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/App.xaml.cs b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/App.xaml.cs index 6a1eee65006..4c8238db475 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/App.xaml.cs +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/App.xaml.cs @@ -42,7 +42,9 @@ public App() InstanceSettings.Properties.Set(ReactPropertyBagHelper.GetName(null, "Prop1"), 43); InstanceSettings.Properties.Set(ReactPropertyBagHelper.GetName(null, "Prop2"), "Hello RNW!"); - PackageProviders.Add(new ReactPackageProvider()); // Includes any modules in this project + Microsoft.ReactNative.Managed.AutolinkedNativeModules.RegisterAutolinkedNativeModulePackages(PackageProviders); // Includes any autolinked modules + + PackageProviders.Add(new Microsoft.ReactNative.Managed.ReactPackageProvider()); PackageProviders.Add(new ReflectionReactPackageProvider()); PackageProviders.Add(new SampleLibraryCS.ReactPackageProvider()); PackageProviders.Add(new SampleLibraryCpp.ReactPackageProvider()); diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.cs b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.cs new file mode 100644 index 00000000000..72a24f0937d --- /dev/null +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.cs @@ -0,0 +1,13 @@ +// AutolinkedNativeModules.g.cs contents generated by "react-native autolink-windows" + +using System.Collections.Generic; + +namespace Microsoft.ReactNative.Managed +{ + internal static class AutolinkedNativeModules + { + internal static void RegisterAutolinkedNativeModulePackages(IList packageProviders) + { + } + } +} diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.targets b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..85bc2375663 --- /dev/null +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/SampleAppCS.csproj b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/SampleAppCS.csproj index 8203bbb347c..9eb52232235 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/SampleAppCS.csproj +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleAppCS/SampleAppCS.csproj @@ -1,10 +1,9 @@  - + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug x86 @@ -125,6 +124,7 @@ App.xaml + MainPage.xaml @@ -177,5 +177,17 @@ 16.0 - - \ No newline at end of file + + + + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + + diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCPP/SampleLibraryCPP.vcxproj b/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCPP/SampleLibraryCPP.vcxproj index 06945cd170c..e6a94ce7b73 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCPP/SampleLibraryCPP.vcxproj +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCPP/SampleLibraryCPP.vcxproj @@ -17,7 +17,7 @@ - + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ @@ -78,7 +78,7 @@ - + @@ -159,7 +159,16 @@ - + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + diff --git a/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCS/SampleLibraryCS.csproj b/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCS/SampleLibraryCS.csproj index f0e6c05940f..f753df564a5 100644 --- a/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCS/SampleLibraryCS.csproj +++ b/packages/microsoft-reactnative-sampleapps/windows/SampleLibraryCS/SampleLibraryCS.csproj @@ -1,10 +1,9 @@  - + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug AnyCPU @@ -132,5 +131,17 @@ 16.0 - - \ No newline at end of file + + + + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + + diff --git a/packages/playground/react-native.config.js b/packages/playground/react-native.config.js new file mode 100644 index 00000000000..5720987bf24 --- /dev/null +++ b/packages/playground/react-native.config.js @@ -0,0 +1,15 @@ +// Change the below to true for autolink to target playground-win32 instead of playground. +// Then run `npx react-native autolink-windows` to actually run the autolink. +const targetWin32 = false; + +module.exports = { + project: { + windows: { + sourceDir: 'windows', + solutionFile: targetWin32 ? 'playground-win32.sln' : 'playground.sln', + project: { + projectFile: targetWin32 ? 'playground-win32\\playground-win32.vcxproj' : 'playground\\playground.vcxproj', + }, + }, + }, +}; diff --git a/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.cpp b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.cpp new file mode 100644 index 00000000000..f3b94db59f9 --- /dev/null +++ b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.cpp @@ -0,0 +1,13 @@ +// AutolinkedNativeModules.g.cpp contents generated by "react-native autolink-windows" +// clang-format off +#include "pch.h" +#include "AutolinkedNativeModules.g.h" + +namespace winrt::Microsoft::ReactNative +{ + +void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) +{ +} + +} diff --git a/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.h b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.h new file mode 100644 index 00000000000..6b826dd0b86 --- /dev/null +++ b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.h @@ -0,0 +1,10 @@ +// AutolinkedNativeModules.g.h contents generated by "react-native autolink-windows" +#pragma once + +namespace winrt::Microsoft::ReactNative { + +void RegisterAutolinkedNativeModulePackages( + winrt::Windows::Foundation::Collections::IVector const + &packageProviders); + +} diff --git a/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.targets b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..85bc2375663 --- /dev/null +++ b/packages/playground/windows/playground-win32/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/playground/windows/playground-win32/Playground-win32.vcxproj b/packages/playground/windows/playground-win32/Playground-win32.vcxproj index 1c5c772a106..0e448cfb75f 100644 --- a/packages/playground/windows/playground-win32/Playground-win32.vcxproj +++ b/packages/playground/windows/playground-win32/Playground-win32.vcxproj @@ -11,6 +11,9 @@ false + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ + Debug @@ -57,7 +60,7 @@ - + False @@ -118,7 +121,16 @@ resources.pri - + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + diff --git a/packages/playground/windows/playground/AutolinkedNativeModules.g.cpp b/packages/playground/windows/playground/AutolinkedNativeModules.g.cpp new file mode 100644 index 00000000000..f3b94db59f9 --- /dev/null +++ b/packages/playground/windows/playground/AutolinkedNativeModules.g.cpp @@ -0,0 +1,13 @@ +// AutolinkedNativeModules.g.cpp contents generated by "react-native autolink-windows" +// clang-format off +#include "pch.h" +#include "AutolinkedNativeModules.g.h" + +namespace winrt::Microsoft::ReactNative +{ + +void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) +{ +} + +} diff --git a/packages/playground/windows/playground/AutolinkedNativeModules.g.h b/packages/playground/windows/playground/AutolinkedNativeModules.g.h new file mode 100644 index 00000000000..99c3efc78bd --- /dev/null +++ b/packages/playground/windows/playground/AutolinkedNativeModules.g.h @@ -0,0 +1,10 @@ +// AutolinkedNativeModules.g.h contents generated by "react-native autolink-windows" +#pragma once + +// clang-format off +namespace winrt::Microsoft::ReactNative +{ + +void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders); + +} diff --git a/packages/playground/windows/playground/AutolinkedNativeModules.g.targets b/packages/playground/windows/playground/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..85bc2375663 --- /dev/null +++ b/packages/playground/windows/playground/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/playground/windows/playground/Playground.vcxproj b/packages/playground/windows/playground/Playground.vcxproj index 9c1d183a70c..b8f7e96fc79 100644 --- a/packages/playground/windows/playground/Playground.vcxproj +++ b/packages/playground/windows/playground/Playground.vcxproj @@ -20,6 +20,9 @@ Playground_TemporaryKey.pfx + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ + Debug @@ -99,7 +102,7 @@ - + @@ -132,6 +135,7 @@ + App.xaml @@ -164,6 +168,7 @@ + Create @@ -198,7 +203,16 @@ Samples/rntester.tsx - + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + diff --git a/packages/playground/windows/playground/Playground.vcxproj.filters b/packages/playground/windows/playground/Playground.vcxproj.filters index e0fa9658ed5..52c1ce6bbf9 100644 --- a/packages/playground/windows/playground/Playground.vcxproj.filters +++ b/packages/playground/windows/playground/Playground.vcxproj.filters @@ -11,11 +11,13 @@ + + diff --git a/packages/playground/windows/playground/nativeModules.g.h b/packages/playground/windows/playground/nativeModules.g.h deleted file mode 100644 index 049a8c9e57c..00000000000 --- a/packages/playground/windows/playground/nativeModules.g.h +++ /dev/null @@ -1,2 +0,0 @@ -// NativeModules.g.h -- contents generated by "react-native run-windows" -#define REACT_REGISTER_NATIVE_MODULE_PACKAGES() \ No newline at end of file diff --git a/packages/playground/windows/playground/pch.h b/packages/playground/windows/playground/pch.h index 6781809faaa..0cad8c87048 100644 --- a/packages/playground/windows/playground/pch.h +++ b/packages/playground/windows/playground/pch.h @@ -31,5 +31,3 @@ #include #include #include - -#include "nativeModules.g.h" diff --git a/vnext/PropertySheets/Autolink.props b/vnext/PropertySheets/Autolink.props new file mode 100644 index 00000000000..719ba31315f --- /dev/null +++ b/vnext/PropertySheets/Autolink.props @@ -0,0 +1,16 @@ + + + + + + true + npx react-native autolink-windows + $([MSBuild]::GetDirectoryNameOfFileAbove($(ProjectDir), 'package.json')) + --check --sln $([MSBuild]::MakeRelative($(AutolinkCommandWorkingDir), $(SolutionPath))) --proj $([MSBuild]::MakeRelative($(AutolinkCommandWorkingDir), $(ProjectPath))) + --check + + + diff --git a/vnext/PropertySheets/Autolink.targets b/vnext/PropertySheets/Autolink.targets new file mode 100644 index 00000000000..6a17e61ba4e --- /dev/null +++ b/vnext/PropertySheets/Autolink.targets @@ -0,0 +1,10 @@ + + + + + + + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Common.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Common.props index 01cae9efadf..332307dd203 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Common.props +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Common.props @@ -2,6 +2,11 @@ diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.props index c8c79d89f1e..f21a9c6ea97 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.props +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.props @@ -2,7 +2,12 @@ - + + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets index d809fa577e9..010f48faf46 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets @@ -2,14 +2,18 @@ - + {f7d32bd0-2749-483e-9a0d-1635ef7e3136} Microsoft.ReactNative - + {F2824844-CE15-4242-9420-308923CD76C3} Microsoft.ReactNative.Managed @@ -21,12 +25,13 @@ - - - + + + - + + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.props index c8c79d89f1e..ac5ee6edbe0 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.props +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.props @@ -2,7 +2,11 @@ - + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.targets b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.targets index 8a4459082e0..6adc8f1aaca 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.targets +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpLib.targets @@ -2,22 +2,25 @@ - + {f7d32bd0-2749-483e-9a0d-1635ef7e3136} Microsoft.ReactNative false - - + {F2824844-CE15-4242-9420-308923CD76C3} Microsoft.ReactNative.Managed false - - + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.Common.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.Common.props new file mode 100644 index 00000000000..9d9cdf86d6a --- /dev/null +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.Common.props @@ -0,0 +1,12 @@ + + + + + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.props index c8c79d89f1e..39e1556ec38 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.props +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.props @@ -2,7 +2,12 @@ - + + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.targets b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.targets index cb42c2bc956..46901df229f 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.targets +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppApp.targets @@ -2,25 +2,29 @@ - - - + {f7d32bd0-2749-483e-9a0d-1635ef7e3136} - - - + + + + + Condition="Exists('$(ProjectDir)\AutolinkedNativeModules.g.targets')" /> diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.props b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.props index c8c79d89f1e..b5ad5d3f7c5 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.props +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.props @@ -2,7 +2,11 @@ - + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.targets b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.targets index ec40ec8c41e..2fb80035b37 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.targets +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CppLib.targets @@ -2,16 +2,20 @@ - {f7d32bd0-2749-483e-9a0d-1635ef7e3136} false + diff --git a/vnext/PropertySheets/ManagedCodeGen/Microsoft.ReactNative.Managed.CodeGen.targets b/vnext/PropertySheets/ManagedCodeGen/Microsoft.ReactNative.Managed.CodeGen.targets index aa71b0786f8..1b7745dd5e3 100644 --- a/vnext/PropertySheets/ManagedCodeGen/Microsoft.ReactNative.Managed.CodeGen.targets +++ b/vnext/PropertySheets/ManagedCodeGen/Microsoft.ReactNative.Managed.CodeGen.targets @@ -16,7 +16,7 @@ $(_ReactNativeCodeGenOutFolder)$(_ReactNativeName).g.cs <_ReactNativeCodeGenResponseFile>$(_ReactNativeCodeGenOutFolder)$(_ReactNativeName).rsp - <_ReactNativeCodeGenProjectPath>$(ReactNativeWindowsDir)Microsoft.ReactNative.Managed.CodeGen\Microsoft.ReactNative.Managed.CodeGen.csproj + <_ReactNativeCodeGenProjectPath>$(ReactNativeWindowsDir)\Microsoft.ReactNative.Managed.CodeGen\Microsoft.ReactNative.Managed.CodeGen.csproj <_ReactNativeCodeGenProjectProperties>Configuration=$(Configuration);Platform=$(Platform);DeployOnBuild=true;PublishProfile=DeployAsTool-$(Configuration);NoWarn=1023 @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/vnext/local-cli/runWindows/utils/VSProjectUtils.ps1 b/vnext/Scripts/VSProjectUtils.ps1 similarity index 99% rename from vnext/local-cli/runWindows/utils/VSProjectUtils.ps1 rename to vnext/Scripts/VSProjectUtils.ps1 index 1c97ad8d59a..3758d272580 100644 --- a/vnext/local-cli/runWindows/utils/VSProjectUtils.ps1 +++ b/vnext/Scripts/VSProjectUtils.ps1 @@ -124,7 +124,7 @@ function Add-PackagesConfigXml { [bool] $CheckConfig = $True ) - $packageXml = Load-PackagesConfigXml -PackagesConfigPath $PackagesConfigPath + $packageXml = Load-PackagesConfigXml -PackagesConfigPath $PackagesConfigPath -CheckConfig $CheckConfig $existingPackageNode = $packageXml.selectSingleNode("packages/package[@id=""$PackageName""]") diff --git a/vnext/local-cli/config/configUtils.js b/vnext/local-cli/config/configUtils.js new file mode 100644 index 00000000000..b82bff8dd18 --- /dev/null +++ b/vnext/local-cli/config/configUtils.js @@ -0,0 +1,309 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * @format + */ +// @ts-check + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +const xmldom = require('xmldom').DOMParser; +const xpath = require('xpath'); + +const msbuildSelect = xpath.useNamespaces({ + msbuild: 'http://schemas.microsoft.com/developer/msbuild/2003', +}); + +/** + * Search for files matching the pattern under the target folder. + * @param {string} folder The absolute path to target folder. + * @param {string} filenamePattern The pattern to search for. + * @return {array} Return the array of relative file paths. + */ +function findFiles(folder, filenamePattern) { + const files = glob.sync(path.join('**', filenamePattern), { + cwd: folder, + ignore: [ + 'node_modules/**', + '**/Debug/**', + '**/Release/**', + '**/WinUI3/**', + '**/Generated Files/**', + '**/packages/**', + ], + }); + + return files; +} + +/** + * Search for the windows sub-folder under the target folder. + * @param {string} folder The absolute path to the target folder. + * @return The absolute path to the windows folder, if it exists. + */ +function findWindowsFolder(folder) { + const winDir = 'windows'; + const joinedDir = path.join(folder, winDir); + if (fs.existsSync(joinedDir)) { + return joinedDir; + } + + return null; +} + +/** + * Checks if the target file path is a RNW solution file by checking if it contains the string "ReactNative". + * @param {string} filePath The absolute file path to check. + * @return {boolean} Whether the path is to a RNW solution file. + */ +function isRnwSolution(filePath) { + return ( + fs + .readFileSync(filePath) + .toString() + .search(/ReactNative/) > 0 + ); +} + +/** + * Search for the RNW solution files under the target folder. + * @param {string} winFolder The absolute path to target folder. + * @return {array} Return the array of relative file paths. + */ +function findSolutionFiles(winFolder) { + // First search for all potential solution files + const allSolutions = findFiles(winFolder, '*.sln'); + + if (allSolutions.length === 0) { + // If there're no solution files, return 0 + return []; + } else if (allSolutions.length === 1) { + // If there is exactly one solution file, assume it's it + return [allSolutions[0]]; + } + + var solutionFiles = []; + + // Try to find any solution file that appears to be a react native solution + for (const solutionFile of allSolutions) { + if (isRnwSolution(path.join(winFolder, solutionFile))) { + solutionFiles.push(solutionFile); + } + } + + return solutionFiles; +} + +/** + * Checks if the target file path is a RNW lib project file. + * @param {string} filePath The absolute file path to check. + * @return {boolean} Whether the path is to a RNW lib project file. + */ +function isRnwDependencyProject(filePath) { + const projectContents = readProjectFile(filePath); + + const projectLang = getProjectLanguage(filePath); + if (projectLang === 'cs') { + return importProjectExists( + projectContents, + 'Microsoft.ReactNative.Uwp.CSharpLib.targets', + ); + } else if (projectLang === 'cpp') { + return importProjectExists( + projectContents, + 'Microsoft.ReactNative.Uwp.CppLib.targets', + ); + } + + return false; +} + +/** + * Search for the RNW lib project files under the target folder. + * @param {string} winFolder The absolute path to target folder. + * @return {array} Return the array of relative file paths. + */ +function findDependencyProjectFiles(winFolder) { + // First, search for all potential project files + const allCppProj = findFiles(winFolder, '*.vcxproj'); + const allCsProj = findFiles(winFolder, '*.csproj'); + + const allProjects = allCppProj.concat(allCsProj); + + if (allProjects.length === 0) { + // If there're no project files, return 0 + return []; + } + + var dependencyProjectFiles = []; + + // Try to find any project file that appears to be a dependency project + for (const projectFile of allProjects) { + if (isRnwDependencyProject(path.join(winFolder, projectFile))) { + dependencyProjectFiles.push(projectFile); + } + } + + return dependencyProjectFiles; +} + +/** + * Checks if the target file path is a RNW app project file. + * @param {string} filePath The absolute file path to check. + * @return {boolean} Whether the path is to a RNW app project file. + */ +function isRnwAppProject(filePath) { + const projectContents = readProjectFile(filePath); + + const projectLang = getProjectLanguage(filePath); + if (projectLang === 'cs') { + return importProjectExists( + projectContents, + 'Microsoft.ReactNative.Uwp.CSharpApp.targets', + ); + } else if (projectLang === 'cpp') { + return importProjectExists( + projectContents, + 'Microsoft.ReactNative.Uwp.CppApp.targets', + ); + } + + return false; +} + +/** + * Search for the RNW app project files under the target folder. + * @param {string} winFolder The absolute path to target folder. + * @return {array} Return the array of relative file paths. + */ +function findAppProjectFiles(winFolder) { + // First, search for all potential project files + const allCppProj = findFiles(winFolder, '*.vcxproj'); + const allCsProj = findFiles(winFolder, '*.csproj'); + + const allProjects = allCppProj.concat(allCsProj); + + if (allProjects.length === 0) { + // If there're no project files, return 0 + return []; + } + + var appProjectFiles = []; + + // Try to find any project file that appears to be an app project + for (const projectFile of allProjects) { + if (isRnwAppProject(path.join(winFolder, projectFile))) { + appProjectFiles.push(projectFile); + } + } + + return appProjectFiles; +} + +/** + * Returns the programming language of the project file. + * @param {string} projectPath The project file path to check. + * @return {string} The language string: cpp, cs, or null if unknown. + */ +function getProjectLanguage(projectPath) { + if (projectPath.endsWith('.vcxproj')) { + return 'cpp'; + } else if (projectPath.endsWith('.csproj')) { + return 'cs'; + } + return null; +} + +/** + * Reads in the contents of the target project file. + * @param {string} projectPath The target project file path. + * @return {object} The project file contents. + */ +function readProjectFile(projectPath) { + const projectContents = fs.readFileSync(projectPath, 'utf8').toString(); + return new xmldom().parseFromString(projectContents, 'application/xml'); +} + +/** + * Search for the given property in the project contents and return its value. + * @param {object} projectContents The XML project contents. + * @param {string} propertyName The property to look for. + * @return {string} The value of the tag if it exists. + */ +function findPropertyValue(projectContents, propertyName) { + var nodes = msbuildSelect( + `//msbuild:PropertyGroup/msbuild:${propertyName}`, + projectContents, + ); + + if (nodes.length > 0) { + // Take the last one + return nodes[nodes.length - 1].textContent; + } + + return null; +} + +/** + * Search for the given import project in the project contents and return if it exists. + * @param {object} projectContents The XML project contents. + * @param {string} projectName The project to look for. + * @return {boolean} If the target exists. + */ +function importProjectExists(projectContents, projectName) { + var nodes = msbuildSelect( + `//msbuild:Import[contains(@Project,'${projectName}')]`, + projectContents, + ); + + return nodes.length > 0; +} + +/** + * Gets the name of the project from the project contents. + * @param {object} projectContents The XML project contents. + * @return {string} The project name. + */ +function getProjectName(projectContents) { + return ( + findPropertyValue(projectContents, 'ProjectName') || + findPropertyValue(projectContents, 'AssemblyName') || + '' + ); +} + +/** + * Gets the namespace of the project from the project contents. + * @param {object} projectContents The XML project contents. + * @return {string} The project namespace. + */ +function getProjectNamespace(projectContents) { + return findPropertyValue(projectContents, 'RootNamespace'); +} + +/** + * Gets the guid of the project from the project contents. + * @param {object} projectContents The XML project contents. + * @return {string} The project guid. + */ +function getProjectGuid(projectContents) { + return findPropertyValue(projectContents, 'ProjectGuid'); +} + +module.exports = { + findFiles: findFiles, + findWindowsFolder: findWindowsFolder, + isRnwSolution: isRnwSolution, + findSolutionFiles: findSolutionFiles, + isRnwDependencyProject: isRnwDependencyProject, + findDependencyProjectFiles: findDependencyProjectFiles, + isRnwAppProject: isRnwAppProject, + findAppProjectFiles: findAppProjectFiles, + getProjectLanguage: getProjectLanguage, + readProjectFile: readProjectFile, + getProjectName: getProjectName, + getProjectNamespace: getProjectNamespace, + getProjectGuid: getProjectGuid, +}; diff --git a/vnext/local-cli/config/dependencyConfig.js b/vnext/local-cli/config/dependencyConfig.js index 40e4d5a8a9c..81230ff5cd4 100644 --- a/vnext/local-cli/config/dependencyConfig.js +++ b/vnext/local-cli/config/dependencyConfig.js @@ -1,102 +1,244 @@ -const fs = require('fs'); +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * @format + */ +// @ts-check + const path = require('path'); -const glob = require('glob'); -const xmldoc = require('xmldoc'); -function dependencyConfigWindows(folder, userConfig = {}) { - const sourceDir = userConfig.sourceDir || findWindowsAppFolder(folder); +const configUtils = require('./configUtils.js'); + +/* + +react-native config will generate the following JSON for each native module dependency +under node_modules that has a windows implementation, in order to support auto-linking. +This is done heurestically, so if the result isn't quite correct, native module developers +can provide a manual override file: react-native.config.js. + +Schema for dependencies: + +Tags: +auto - Item is always calculated by config. An override file should NEVER provide it. +req - Item is required. If an override file exists, it MUST provide it. If no override file exists, config will try to calculate it. +opt - Item is optional. If an override file exists, it MAY provide it. If no override file exists, config may try to calculate it. + +{ + folder: string, // (auto) Absolute path to the module root folder, determined by react-native config, ex: 'c:\path\to\app-name\node_modules\my-module' + sourceDir: string, // (opt, req if projects defined) Relative path to the windows implementation under folder, ex: 'windows' + solutionFile: string, // (opt) Relative path to the module's VS solution file under sourceDir, ex: 'MyModule.sln' + projects: [ // (opt) Array of VS projects that must be added to the consuming app's solution file, so they are built + { + projectFile: string, // (req) Relative path to the VS project file under sourceDir, ex: 'MyModule\MyModule.vcxproj' for 'c:\path\to\app-name\node_modules\my-module\windows\MyModule\MyModule.vcxproj' + directDependency: bool, // (req) Whether to add the project file as a dependency to the consuming app's project file. true for projects that provide native modules + projectName: string, // (auto) Name of the project, determined from projectFile, ex: 'MyModule' + projectLang: string, // (auto) Language of the project, cpp or cs, determined from projectFile + projectGuid: string, // (auto) Project identifier, determined from projectFile + cppHeaders: [], // (opt) Array of cpp header include lines, ie: 'winrt/MyModule.h', to be transformed into '#include ' + cppPackageProviders: [], // (opt) Array of fully qualified cpp IReactPackageProviders, ie: 'MyModule::ReactPackageProvider' + csNamespaces: [], // (opt) Array of cs namespaces, ie: 'MyModule', to be transformed into 'using MyModule;' + csPackageProviders: [], // (opt) Array of fully qualified cs IReactPackageProviders, ie: 'MyModule.ReactPackageProvider' + }, + ], + nugetPackages: [ // (opt) Array of nuget packages including native modules that must be added as a dependency to the consuming app. It can be empty, but by its nature it can't be calculated + { + packageName: string, // (req) Name of the nuget package to install + packageVersion: string, // (req) Version of the nuget package to install + cppHeaders: [], // (req) Array of cpp header include lines, ie: 'winrt/NugetModule.h', to be transformed into '#include ' + cppPackageProviders: [], // (req) Array of fully qualified cpp IReactPackageProviders, ie: 'NugetModule::ReactPackageProvider' + csNamespaces: [], // (req) Array of cs namespaces, ie: 'NugetModule', to be transformed into 'using NugetModule;' + csPackageProviders: [], // (req) Array of fully qualified cs IReactPackageProviders, ie: 'NugetModule.ReactPackageProvider' + }, + ], +} - if (!sourceDir) { +Example react-native.config.js for a 'MyModule': + +module.exports = { + dependency: { + platforms: { + windows: { + sourceDir: 'windows', + solutionFile: 'MyModule.sln', + projects: [ + { + projectFile: 'MyModule\\MyModule.vcxproj', + directDependency: true, + } + ], + }, + }, + }, +}; + +*/ + +/** + * Gets the config of any RNW native modules under the target folder. + * @param {string} folder The absolute path to the target folder. + * @param {object} userConfig A manually specified override config. + * @return {object} The config if any RNW native modules exist. + */ +function dependencyConfigWindows(folder, userConfig = {}) { + if (userConfig === null) { return null; } - var packageName = null; - const packageIDL = findPackageProviderIDL(sourceDir); - if (packageIDL){ - packageName = parsePackageIDLFile(packageIDL); + const usingManualProjectsOverride = + 'projects' in userConfig && Array.isArray(userConfig.projects); + + const usingManualNugetPackagesOverride = + 'nugetPackages' in userConfig && Array.isArray(userConfig.nugetPackages); + + var result = { + folder, + projects: usingManualProjectsOverride ? userConfig.projects : [], + nugetPackages: usingManualNugetPackagesOverride + ? userConfig.nugetPackages + : [], + }; + + var sourceDir = null; + if (usingManualProjectsOverride && result.projects.length > 0) { + // Manaully provided projects, so extract the sourceDir + if (!('sourceDir' in userConfig)) { + sourceDir = + 'Error: Source dir is required if projects are specified, but it is not specified in react-native.config.'; + } else if (userConfig.sourceDir === null) { + sourceDir = + 'Error: Source dir is required if projects are specified, but it is null in react-native.config.'; + } else { + sourceDir = path.join(folder, userConfig.sourceDir); + } + } else if (!usingManualProjectsOverride) { + // No manually provided projects, try to find sourceDir + sourceDir = configUtils.findWindowsFolder(folder); } - var cppProjFile = null; - var csProjectFile = null; - if (packageName) - { - cppProjFile = findCppProject(sourceDir, packageName); - csProjectFile = findCSProject(sourceDir,packageName); + + if ( + sourceDir === null && + result.projects.length === 0 && + result.nugetPackages.length === 0 + ) { + // Nothing to look for here, bail + return null; + } else if (sourceDir !== null && sourceDir.startsWith('Error: ')) { + // Source dir error, bail with error + result.sourceDir = sourceDir; + return result; } - var projGUID = null; - if (cppProjFile) - { - const proj = readProject(cppProjFile); - var groupNode = proj.childNamed('PropertyGroup'); - if (groupNode){ - var nameNode = groupNode.childNamed('ProjectGuid'); - if (nameNode){ - projGUID = nameNode.val; - } + result.sourceDir = sourceDir.substr(folder.length + 1); + + const usingManualSolutionFile = 'solutionFile' in userConfig; + + var solutionFile = null; + if (usingManualSolutionFile && userConfig.solutionFile !== null) { + // Manually provided solutionFile, so extract it + solutionFile = path.join(sourceDir, userConfig.solutionFile); + } else if (!usingManualSolutionFile) { + // No manually provided solutionFile, try to find it + const foundSolutions = configUtils.findSolutionFiles(sourceDir); + if (foundSolutions.length === 1) { + solutionFile = path.join(sourceDir, foundSolutions[0]); } } - return { - sourceDir, - packageIDL, - packageName, - cppProjFile, - csProjectFile, - projGUID, - }; -} + result.solutionFile = + solutionFile !== null ? solutionFile.substr(sourceDir.length + 1) : null; -function findWindowsAppFolder(folder) { - const winDir = 'windows'; - const joinedDir = path.join(folder, winDir); - if (fs.existsSync(joinedDir)) { - return joinedDir; - } + if (usingManualProjectsOverride) { + // react-native.config used, fill out (auto) items for each provided project, verify (req) items are present - return null; -} + const alwaysRequired = ['projectFile', 'directDependency']; -// assumption is every cpp native module will have a ReactPackageProvider.idl defined -function findPackageProviderIDL(folder) { - const PackageIDLPath = glob.sync(path.join('**', 'ReactPackageProvider.idl'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', 'Generated Files'], - })[0]; + for (let project of result.projects) { + // Verifying (req) items + var errorFound = false; + alwaysRequired.forEach(item => { + if (!(item in project)) { + project[ + item + ] = `Error: ${item} is required for each project in react-native.config`; + errorFound = true; + } + }); - return PackageIDLPath ? path.join(folder, PackageIDLPath) : null; -} + if (errorFound) { + break; + } -// look for packagename 'XYZ' in string 'namesapce XYZ {' -function parsePackageIDLFile(packageIDL) { - const buf = fs.readFileSync(packageIDL, 'utf8'); - const indexofNameSpace = buf.indexOf('namespace') + 9; - const indexofBracket = buf.indexOf('{'); - const packageName = buf.substring(indexofNameSpace, indexofBracket).replace(/\s+/g, ' ').trim(); + const projectFile = path.join(sourceDir, project.projectFile); - return packageName; -} + const projectContents = configUtils.readProjectFile(projectFile); -// read visual studio project file which is actually a XML doc -function readProject(projectPath) { - return new (xmldoc.XmlDocument)(fs.readFileSync(projectPath, 'utf8')); -} + // Calculating (auto) items + project.projectName = configUtils.getProjectName(projectContents); + project.projectLang = configUtils.getProjectLanguage(projectFile); + project.projectGuid = configUtils.getProjectGuid(projectContents); -function findCppProject(folder, projectName) { - const cppProj = glob.sync(path.join('**', projectName + '.vcxproj'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', '**/Generated Files/**', '**/packages/**'], - })[0]; + if (project.directDependency) { + // Calculating more (auto) items - return cppProj ? path.join(folder, cppProj) : null; -} + const projectNamespace = configUtils.getProjectNamespace( + projectContents, + ); + const cppNamespace = projectNamespace.replace(/\./g, '::'); + const csNamespace = projectNamespace.replace(/::/g, '.'); + + project.cppHeaders = project.cppHeaders || [`winrt/${csNamespace}.h`]; + project.cppPackageProviders = project.cppPackageProviders || [ + `${cppNamespace}::ReactPackageProvider`, + ]; + project.csNamespaces = project.csNamespaces || [`${csNamespace}`]; + project.csPackageProviders = project.csPackageProviders || [ + `${csNamespace}.ReactPackageProvider`, + ]; + } + } + } else { + // No react-native.config, try to heurestically find any projects + + const foundProjects = configUtils.findDependencyProjectFiles(sourceDir); + + for (const foundProject of foundProjects) { + const projectFile = path.join(sourceDir, foundProject); -function findCSProject(folder, projectName) { - const cppProj = glob.sync(path.join('**', projectName + '.csproj'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', '**/Generated Files/**', '**/packages/**'], - })[0]; + const projectLang = configUtils.getProjectLanguage(projectFile); + + const projectContents = configUtils.readProjectFile(projectFile); + + const projectName = configUtils.getProjectName(projectContents); + + const projectGuid = configUtils.getProjectGuid(projectContents); + + const projectNamespace = configUtils.getProjectNamespace(projectContents); + + const directDependency = true; + + const cppNamespace = projectNamespace.replace(/\./g, '::'); + const csNamespace = projectNamespace.replace(/::/g, '.'); + + const cppHeaders = [`winrt/${csNamespace}.h`]; + const cppPackageProviders = [`${cppNamespace}::ReactPackageProvider`]; + const csNamespaces = [`${csNamespace}`]; + const csPackageProviders = [`${csNamespace}.ReactPackageProvider`]; + + result.projects.push({ + projectFile: projectFile.substr(sourceDir.length + 1), + projectName, + projectLang, + projectGuid, + directDependency, + cppHeaders, + cppPackageProviders, + csNamespaces, + csPackageProviders, + }); + } + } - return cppProj ? path.join(folder, cppProj) : null; + return result; } module.exports = { diff --git a/vnext/local-cli/config/projectConfig.js b/vnext/local-cli/config/projectConfig.js index 4569b84c414..c1fea11cdd3 100644 --- a/vnext/local-cli/config/projectConfig.js +++ b/vnext/local-cli/config/projectConfig.js @@ -1,66 +1,164 @@ -const fs = require('fs'); +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * @format + */ +// @ts-check + const path = require('path'); -const glob = require('glob'); -function projectConfigWindows(folder, userConfig = {}) { - const sourceDir = userConfig.sourceDir || findWindowsAppFolder(folder); +const configUtils = require('./configUtils.js'); + +/* + +react-native config will generate the following JSON for app projects that have a +windows implementation, as a target for auto-linking. This is done heurestically, +so if the result isn't quite correct, app developers can provide a manual override +file: react-native.config.js. + +Schema for app projects: + +Tags: +auto - Item is always calculated by config. An override file should NEVER provide it. +req - Item is required. If an override file exists, it MUST provide it. If no override file exists, config will try to calculate it. +opt - Item is optional. If an override file exists, it MAY provide it. If no override file exists, config may try to calculate it. + +{ + folder: string, // (auto) Absolute path to the app root folder, determined by react-native config, ex: 'c:\path\to\my-app' + sourceDir: string, // (req) Relative path to the windows implementation under folder, ex: 'windows' + solutionFile: string, // (req) Relative path to the app's VS solution file under sourceDir, ex: 'MyApp.sln' + project: { // (req) + projectFile: string, // (req) Relative path to the VS project file under sourceDir, ex: 'MyApp\MyApp.vcxproj' for 'c:\path\to\my-app\windows\MyApp\MyApp.vcxproj' + projectName: string, // (auto) Name of the project, determined from projectFile, ex: 'MyApp' + projectLang: string, // (auto) Language of the project, cpp or cs, determined from projectFile + projectGuid: string, // (auto) Project identifier, determined from projectFile + }, +} + +Example react-native.config.js for a 'MyApp': + +module.exports = { + project: { + windows: { + sourceDir: 'windows', + solutionFile: 'MyApp.sln', + project: { + projectFile: 'MyApp\\MyApp.vcxproj', + }, + }, + }, +}; + +*/ - if (!sourceDir) { +/** + * Gets the config of any RNW apps under the target folder. + * @param {string} folder The absolute path to the target folder. + * @param {object} userConfig A manually specified override config. + * @return {object} The config if any RNW apps exist. + */ +function projectConfigWindows(folder, userConfig = {}) { + if (userConfig === null) { return null; } - const projectSolution = findSolution(sourceDir); - if (projectSolution){ - var extension = path.extname(projectSolution); - var projectName = path.basename(projectSolution, extension); - } - const cppProjFile = findCppProject(sourceDir, projectName); - const csProjectFile = findCSProject(sourceDir,projectName); - - return { - sourceDir, - projectSolution, - projectName, - cppProjFile, - csProjectFile, - }; -} + const usingManualOverride = 'sourceDir' in userConfig; -function findWindowsAppFolder(folder) { - const winDir = 'windows'; - const joinedDir = path.join(folder, winDir); - if (fs.existsSync(joinedDir)) { - return joinedDir; + const sourceDir = usingManualOverride + ? path.join(folder, userConfig.sourceDir) + : configUtils.findWindowsFolder(folder); + + if (sourceDir === null) { + // Nothing to look for here, bail + return null; } - return null; -} + var result = { + folder: folder, + sourceDir: sourceDir.substr(folder.length + 1), + }; -function findSolution(folder) { - const solutionPath = glob.sync(path.join('**', '*.sln'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', 'Generated Files'], - })[0]; + var validProject = false; - return solutionPath ? path.join(folder, solutionPath) : null; -} + if (usingManualOverride) { + // Manual override, try to use it for solutionFile + if (!('solutionFile' in userConfig)) { + result.solutionFile = + 'Error: Solution file is required but not specified in react-native.config.'; + } else if (userConfig.solutionFile === null) { + result.solutionFile = + 'Error: Solution file is null in react-native.config.'; + } else { + result.solutionFile = userConfig.solutionFile; + } -function findCppProject(folder, projectName) { - const cppProj = glob.sync(path.join('**', projectName + '.vcxproj'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', '**/Generated Files/**', '**/packages/**'], - })[0]; + // Manual override, try to use it for project + if (!('project' in userConfig)) { + result.project = + 'Error: Project is required but not specified in react-native.config.'; + } else if (userConfig.project === null) { + result.project = 'Error: Project is null in react-native.config.'; + } else { + if (!('projectFile' in userConfig.project)) { + result.project = { + projectFile: + 'Error: Project file is required for project in react-native.config.', + }; + } else if (userConfig.project.projectFile === null) { + result.project = { + projectFile: 'Error: Project file is null in react-native.config.', + }; + } else { + result.project = { + projectFile: userConfig.project.projectFile, + }; + validProject = true; + } + } + } else { + // No manually provided solutionFile, try to find it + const foundSolutions = configUtils.findSolutionFiles(sourceDir); + if (foundSolutions.length === 0) { + result.solutionFile = + 'Error: No app solution file found, please specify in react-native.config.'; + } else if (foundSolutions.length > 1) { + result.solutionFile = + 'Error: Too many app solution files found, please specify in react-native.config.'; + } else { + result.solutionFile = foundSolutions[0]; + } - return cppProj ? path.join(folder, cppProj) : null; -} + // No manually provided project, try to find it + const foundProjects = configUtils.findAppProjectFiles(sourceDir); + if (foundProjects.length === 0) { + result.project = { + projectFile: + 'Error: No app project file found, please specify in react-native.config.', + }; + } else if (foundProjects.length > 1) { + result.project = { + projectFile: + 'Error: Too many app project files found, please specify in react-native.config.', + }; + } else { + result.project = { + projectFile: foundProjects[0], + }; + validProject = true; + } + } + + if (validProject) { + const projectFile = path.join(sourceDir, result.project.projectFile); + const projectContents = configUtils.readProjectFile(projectFile); -function findCSProject(folder, projectName) { - const cppProj = glob.sync(path.join('**', projectName + '.csproj'), { - cwd: folder, - ignore: ['node_modules/**', '**/Debug/**', '**/Release/**', '**/Generated Files/**', '**/packages/**'], - })[0]; + // Add missing (auto) items + result.project.projectName = configUtils.getProjectName(projectContents); + result.project.projectLang = configUtils.getProjectLanguage(projectFile); + result.project.projectGuid = configUtils.getProjectGuid(projectContents); + } - return cppProj ? path.join(folder, cppProj) : null; + return result; } module.exports = { diff --git a/vnext/local-cli/generator-common/index.js b/vnext/local-cli/generator-common/index.js index 7b953c826ea..f2d7a001596 100644 --- a/vnext/local-cli/generator-common/index.js +++ b/vnext/local-cli/generator-common/index.js @@ -156,6 +156,28 @@ function walk(current/*: string*/)/*: string[]*/ { return result.concat.apply([current], files); } +/** + * Get a source file and replace parts of its contents. + * @param {string} srcPath Path to the source file. + * @param {object} replacements e.g. {'TextToBeReplaced': 'Replacement'} + * @return The contents of the file with the replacements applied. + */ +function resolveContents(srcPath, replacements) { + let content = fs.readFileSync(srcPath, 'utf8'); + + if (replacements.useMustache) { + content = mustache.render(content, replacements); + (replacements.regExpPatternsToRemove || []).forEach(regexPattern => { + content = content.replace(new RegExp(regexPattern, 'g'), ''); + }); + } else { + Object.keys(replacements).forEach(regex => { + content = content.replace(new RegExp(regex, 'g'), replacements[regex]); + }); + } + return content; +} + // Binary files, don't process these (avoid decoding as utf8) const binaryExtensions = ['.png', '.jar', '.keystore']; @@ -224,18 +246,7 @@ function copyAndReplace( } else { // Text file const srcPermissions = fs.statSync(srcPath).mode; - let content = fs.readFileSync(srcPath, 'utf8'); - - if (replacements.useMustache) { - content = mustache.render(content, replacements); - (replacements.regExpPatternsToRemove || []).forEach(regexPattern => { - content = content.replace(new RegExp(regexPattern, 'g'), ''); - }); - } else { - Object.keys(replacements).forEach(regex => { - content = content.replace(new RegExp(regex, 'g'), replacements[regex]); - }); - } + let content = resolveContents(srcPath, replacements); let shouldOverwrite = 'overwrite'; if (contentChangedCallback) { @@ -394,5 +405,5 @@ function upgradeFileContentChangedCallback( } module.exports = { - createDir, copyAndReplaceWithChangedCallback, copyAndReplaceAll, + createDir, resolveContents, copyAndReplaceWithChangedCallback, copyAndReplaceAll, }; diff --git a/vnext/local-cli/generator-windows/index.js b/vnext/local-cli/generator-windows/index.js index 0e70dd6b3f0..ec828e8281e 100644 --- a/vnext/local-cli/generator-windows/index.js +++ b/vnext/local-cli/generator-windows/index.js @@ -127,8 +127,8 @@ function copyProjectTemplateAndReplace( // Visual Studio is very picky about the casing of the guids for projects, project references and the solution // https://www.bing.com/search?q=visual+studio+project+guid+casing&cvid=311a5ad7f9fc41089507b24600d23ee7&FORM=ANAB01&PC=U531 // we therefore have to precariously use the right casing in the right place or risk building in VS breaking. - projectGuidLower: `{${projectGuid}}`, - projectGuidUpper: `{${projectGuid}}`, + projectGuidLower: `{${projectGuid.toLowerCase()}}`, + projectGuidUpper: `{${projectGuid.toUpperCase()}}`, // packaging and signing variables: packageGuid: packageGuid, @@ -142,6 +142,13 @@ function copyProjectTemplateAndReplace( xamlNamespace: xamlNamespace, xamlNamespaceCpp: xamlNamespaceCpp, cppNugetPackages: cppNugetPackages, + + // autolinking template variables + autolinkProjectReferencesForTargets: '', + autolinkCsUsingNamespaces: '', + autolinkCsReactPacakgeProviders: '', + autolinkCppIncludes: '', + autolinkCppPackageProviders: '', }; [ diff --git a/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.sln b/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.sln index 21ac2e0edd8..112303ec1bb 100644 --- a/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.sln +++ b/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.sln @@ -51,6 +51,7 @@ Global ..\node_modules\react-native-windows\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.vcxitems*{da8b35b3-da00-4b02-bde6-6a397b3fd46b}*SharedItemsImports = 9 {{^useExperimentalNuget}} ..\node_modules\react-native-windows\Chakra\Chakra.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 + ..\node_modules\react-native-windows\JSI\Shared\JSI.Shared.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 {{/useExperimentalNuget}} ..\node_modules\react-native-windows\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 {{^useExperimentalNuget}} diff --git a/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.vcxproj b/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.vcxproj index 17d71777407..5f15eab3b05 100644 --- a/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.vcxproj +++ b/vnext/local-cli/generator-windows/templates/cpp/proj/MyApp.vcxproj @@ -100,7 +100,7 @@ - + @@ -217,4 +217,4 @@ {{/cppNugetPackages}} - \ No newline at end of file + diff --git a/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.cpp b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.cpp index 7d268b12944..aa6e5db813d 100644 --- a/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.cpp +++ b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.cpp @@ -1,13 +1,13 @@ // AutolinkedNativeModules.g.cpp contents generated by "react-native autolink-windows" +// clang-format off #include "pch.h" -#include "AutolinkedNativeModules.g.h" +#include "AutolinkedNativeModules.g.h"{{ &autolinkCppIncludes }} -// clang-format off namespace winrt::Microsoft::ReactNative { void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) -{ +{ {{ &autolinkCppPackageProviders }} } } diff --git a/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.h b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.h index 99c3efc78bd..f28bb8be361 100644 --- a/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.h +++ b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.h @@ -1,7 +1,7 @@ // AutolinkedNativeModules.g.h contents generated by "react-native autolink-windows" +// clang-format off #pragma once -// clang-format off namespace winrt::Microsoft::ReactNative { diff --git a/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.targets b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..392bde77a0f --- /dev/null +++ b/vnext/local-cli/generator-windows/templates/cpp/src/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + {{ &autolinkProjectReferencesForTargets }} + + diff --git a/vnext/local-cli/generator-windows/templates/cs/proj/MyApp.csproj b/vnext/local-cli/generator-windows/templates/cs/proj/MyApp.csproj index d3d9df56a95..50279ea0c35 100644 --- a/vnext/local-cli/generator-windows/templates/cs/proj/MyApp.csproj +++ b/vnext/local-cli/generator-windows/templates/cs/proj/MyApp.csproj @@ -1,10 +1,9 @@  - + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug x86 @@ -169,7 +168,12 @@ 16.0 - + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. diff --git a/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.cs b/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.cs index 4f311206b86..d8e0da259a3 100644 --- a/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.cs +++ b/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.cs @@ -1,14 +1,13 @@ -// AutolinkedNativeModules.g.cs -- contents generated by "react-native run-windows" +// AutolinkedNativeModules.g.cs contents generated by "react-native autolink-windows" -using System.Collections.Generic; +using System.Collections.Generic;{{ &autolinkCsUsingNamespaces }} namespace Microsoft.ReactNative.Managed { internal static class AutolinkedNativeModules { internal static void RegisterAutolinkedNativeModulePackages(IList packageProviders) - { - + { {{ &autolinkCsReactPacakgeProviders }} } } } diff --git a/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.targets b/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.targets new file mode 100644 index 00000000000..392bde77a0f --- /dev/null +++ b/vnext/local-cli/generator-windows/templates/cs/src/AutolinkedNativeModules.g.targets @@ -0,0 +1,6 @@ + + + + {{ &autolinkProjectReferencesForTargets }} + + diff --git a/vnext/local-cli/runWindows/runWindows.js b/vnext/local-cli/runWindows/runWindows.js index 8157677d7d1..c5684a2aaf7 100644 --- a/vnext/local-cli/runWindows/runWindows.js +++ b/vnext/local-cli/runWindows/runWindows.js @@ -12,6 +12,7 @@ const {newError, newInfo} = require('./utils/commandWithProgress'); const info = require('./utils/info'); const msbuildtools = require('./utils/msbuildtools'); const autolink = require('./utils/autolink'); + const chalk = require('chalk'); function ExitProcessWithError(loggingWasEnabled) { @@ -23,7 +24,13 @@ function ExitProcessWithError(loggingWasEnabled) { process.exit(1); } -async function runWindows(config, args, options) { +/** + * Performs build deploy and launch of RNW apps. + * @param {array} args Unprocessed args passed from react-native CLI. + * @param {object} config Config passed from react-native CLI. + * @param {object} options Options passed from react-native CLI. + */ +async function runWindows(args, config, options) { const verbose = options.logging; if (verbose) { @@ -46,13 +53,25 @@ async function runWindows(config, args, options) { } } - // Fix up options - options.root = options.root || process.cwd(); - const slnFile = options.sln || build.getSolutionFile(options); + // Either use the specified root or get the default one + options.root = options.root || config.root; + + // Get the solution file + const slnFile = build.getAppSolutionFile(options, config); if (options.autolink) { - autolink.updateAutoLink(verbose); + const autolinkArgs = []; + const autolinkConfig = config; + const autoLinkOptions = { + logging: options.logging, + proj: options.proj, + sln: options.sln, + }; + await autolink.func(autolinkArgs, autolinkConfig, autoLinkOptions); + } else { + newInfo('Autolink step is skipped'); } + if (options.build) { if (!slnFile) { newError( @@ -70,7 +89,12 @@ async function runWindows(config, args, options) { // Get build/deploy options const buildType = deploy.getBuildConfiguration(options); - const msBuildProps = build.parseMsBuildProps(options); + var msBuildProps = build.parseMsBuildProps(options); + + if (!options.autolink) { + // Disable the autolink check if --no-autolink was passed + msBuildProps.RunAutolinkCheck = 'false'; + } try { await build.buildSolution( @@ -100,6 +124,13 @@ async function runWindows(config, args, options) { await deploy.startServerInNewWindow(options, verbose); if (options.deploy) { + if (!slnFile) { + newError( + 'Visual Studio Solution file not found. Maybe run "react-native windows" first?', + ); + ExitProcessWithError(options.logging); + } + try { if (options.device || options.emulator || options.target) { await deploy.deployToDevice(options, verbose); @@ -201,6 +232,11 @@ module.exports = { description: 'Do not launch the app after deployment', default: false, }, + { + command: '--no-autolink', + description: 'Do not run autolinking', + default: false, + }, { command: '--no-build', description: 'Do not build the solution', @@ -213,7 +249,14 @@ module.exports = { }, { command: '--sln [string]', - description: 'Solution file to build, e.g. windows\\myApp.sln', + description: + "Override the app solution file determined by 'react-native config', e.g. windows\\myApp.sln", + default: undefined, + }, + { + command: '--proj [string]', + description: + "Override the app project file determined by 'react-native config', e.g. windows\\myApp\\myApp.vcxproj", default: undefined, }, { @@ -231,11 +274,6 @@ module.exports = { description: 'Dump environment information', default: false, }, - { - command: '--autolink', - description: 'Auto link native modules', - default: false, - }, { command: '--direct-debugging [number]', description: 'Enable direct debugging on specified port', diff --git a/vnext/local-cli/runWindows/utils/autolink.js b/vnext/local-cli/runWindows/utils/autolink.js index 6e614de36b9..37f41b80966 100644 --- a/vnext/local-cli/runWindows/utils/autolink.js +++ b/vnext/local-cli/runWindows/utils/autolink.js @@ -5,96 +5,503 @@ */ // @ts-check -const execSync = require('child_process').execSync; -const path = require('path'); const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const performance = require('perf_hooks').performance; + +const {newSpinner} = require('./commandWithProgress'); +const vstools = require('./vstools'); +const generatorCommon = require('../../generator-common'); + +const configUtils = require('../../config/configUtils'); + +const templateRoot = path.join(__dirname, '../../generator-windows/templates'); + +/** + * Logs the given message if verbose is True. + * @param {string} message The message to log. + * @param {boolean} verbose Whether or not verbose logging is enabled. + */ +function verboseMessage(message, verbose) { + if (verbose) { + console.log(message); + } +} + +/** + * Loads a source template file and performs the given replacements, normalizing CRLF. + * @param {string} srcFile Path to the source file. + * @param {object} replacements e.g. {'TextToBeReplaced': 'Replacement'} + * @return The contents of the file with the replacements applied. + */ +function getNormalizedContents(srcFile, replacements) { + // Template files are CRLF, JS-generated replacements are LF, normalize replacements to CRLF + for (var key in replacements) { + replacements[key] = replacements[key].replace(/\n/g, '\r\n'); + } + + replacements.useMustache = true; + + return generatorCommon.resolveContents(srcFile, replacements); +} + +/** + * Updates the target file with the expected contents if it's different. + * @param {string} filePath Path to the target file to update. + * @param {string} expectedContents The expected contents of the file. + * @param {boolean} verbose If true, enable verbose logging. + * @param {boolean} checkMode It true, don't make any changes. + * @return {boolean} Whether any changes were necessary. + */ +function updateFile(filePath, expectedContents, verbose, checkMode) { + const fileName = chalk.bold(path.basename(filePath)); + verboseMessage(`Reading ${fileName}...`, verbose); + const actualContents = fs.existsSync(filePath) + ? fs.readFileSync(filePath).toString() + : ''; + + const contentsChanged = expectedContents !== actualContents; + + if (contentsChanged) { + verboseMessage(chalk.yellow(`${fileName} needs to be updated.`), verbose); + if (!checkMode) { + verboseMessage(`Writing ${fileName}...`, verbose); + fs.writeFileSync(filePath, expectedContents, { + encoding: 'utf8', + flag: 'w', + }); + } + } else { + verboseMessage(`No changes to ${fileName}.`, verbose); + } + + return contentsChanged; +} + +/** + * Exits the script with the given status code. + * @param {number} statusCode The status code. + * @param {boolean} loggingWasEnabled Whether or not verbose lossing was enabled. + */ +function exitProcessWithStatusCode(statusCode, loggingWasEnabled) { + if (!loggingWasEnabled && statusCode !== 0) { + console.log( + `Error: Re-run the command with ${chalk.bold( + '--logging', + )} for more information.`, + ); + } + process.exit(statusCode); +} + +/** + * Performs auto-linking for RNW native modules and apps. + * @param {array} args Unprocessed args passed from react-native CLI. + * @param {object} config Config passed from react-native CLI. + * @param {object} options Options passed from react-native CLI. + */ +async function updateAutoLink(args, config, options) { + const startTime = performance.now(); + + const verbose = options.logging; + + const checkMode = options.check; + + var changesNecessary = false; + + const spinner = newSpinner( + checkMode ? 'Checking auto-linked files...' : 'Auto-linking...', + ); + + verboseMessage('', verbose); -function updateAutoLink(verbose) { - const execString = 'react-native config'; - let output; try { - console.log('Running react-native config...'); - output = execSync(execString).toString(); - if (verbose) { - console.log(output); + verboseMessage('Parsing project...', verbose); + + const projectConfig = config.project; + + if (!('windows' in projectConfig) || projectConfig.windows === null) { + throw new Error( + 'Windows auto-link only supported on windows app projects', + ); } - const config = JSON.parse(output); - const windowsPlatformConfig = config.project.windows; - const cppProjFile = windowsPlatformConfig - ? windowsPlatformConfig.cppProjFile - : null; - if (cppProjFile == null) { - console.log( - 'AutoLink currently is only supported on C++/WinRT main project.', + var windowsAppConfig = projectConfig.windows; + + if (options.sln) { + const slnFile = path.join(windowsAppConfig.folder, options.sln); + + windowsAppConfig.solutionFile = path.relative( + path.join(windowsAppConfig.folder, windowsAppConfig.sourceDir), + slnFile, ); - return; } - console.log(`cppProjFile: ${cppProjFile}`); - const sourceDir = config.project.windows.sourceDir; - console.log(`sourceDir: ${sourceDir}`); + if (options.proj) { + const projFile = path.join(windowsAppConfig.folder, options.proj); + + const projectContents = configUtils.readProjectFile(projFile); + + windowsAppConfig.project = { + projectFile: path.relative( + path.join(windowsAppConfig.folder, windowsAppConfig.sourceDir), + projFile, + ), + projectName: configUtils.getProjectName(projectContents), + projectLang: configUtils.getProjectLanguage(projFile), + projectGuid: configUtils.getProjectGuid(projectContents), + }; + } - const projectName = config.project.windows.projectName; - console.log(`projectName: ${projectName}`); + verboseMessage('Found windows app project, config:', verbose); + verboseMessage(windowsAppConfig, verbose); + + const alwaysRequired = ['folder', 'sourceDir', 'solutionFile', 'project']; + + alwaysRequired.forEach(item => { + if (!(item in windowsAppConfig) || windowsAppConfig[item] === null) { + throw new Error( + `${item} is required but not specified by react-native config`, + ); + } else if ( + typeof windowsAppConfig[item] === 'string' && + windowsAppConfig[item].startsWith('Error: ') + ) { + throw new Error(`${item} invalid. ${windowsAppConfig[item]}`); + } + }); - //#1. update nativeModules.g.h - const generatedHeader = path.join( - sourceDir, - projectName, - 'AutolinkedNativeModules.g.h', + const solutionFile = path.join( + windowsAppConfig.folder, + windowsAppConfig.sourceDir, + windowsAppConfig.solutionFile, ); - if (!fs.existsSync(generatedHeader)) { - console.log( - 'AutoLink can not locate generated header file: {generatedHeader}', - ); - return; + + const windowsAppProjectConfig = windowsAppConfig.project; + + const projectRequired = [ + 'projectFile', + 'projectName', + 'projectLang', + 'projectGuid', + ]; + + projectRequired.forEach(item => { + if ( + !(item in windowsAppProjectConfig) || + windowsAppProjectConfig[item] === null + ) { + throw new Error( + `project.${item} is required but not specified by react-native config`, + ); + } else if ( + typeof windowsAppProjectConfig[item] === 'string' && + windowsAppProjectConfig[item].startsWith('Error: ') + ) { + throw new Error( + `project.${item} invalid. ${windowsAppProjectConfig[item]}`, + ); + } + }); + + const projectFile = path.join( + windowsAppConfig.folder, + windowsAppConfig.sourceDir, + windowsAppConfig.project.projectFile, + ); + + const projectDir = path.dirname(projectFile); + const projectLang = windowsAppConfig.project.projectLang; + + verboseMessage('Parsing dependencies...', verbose); + + const dependenciesConfig = config.dependencies; + + let windowsDependencies = {}; + + for (const dependencyName in dependenciesConfig) { + const windowsDependency = + dependenciesConfig[dependencyName].platforms.windows; + + if (windowsDependency) { + verboseMessage( + `${chalk.bold(dependencyName)} has windows implementation, config:`, + verbose, + ); + verboseMessage(windowsDependency, verbose); + + var dependencyIsValid = true; + + dependencyIsValid = + dependencyIsValid && + 'sourceDir' in windowsDependency && + windowsDependency.sourceDir !== null && + !windowsDependency.sourceDir.startsWith('Error: '); + + if ( + 'projects' in windowsDependency && + Array.isArray(windowsDependency.projects) + ) { + windowsDependency.projects.forEach(project => { + const itemsToCheck = ['projectFile', 'directDependency']; + itemsToCheck.forEach(item => { + dependencyIsValid = + dependencyIsValid && + item in project && + project[item] !== null && + !project[item].toString().startsWith('Error: '); + }); + }); + } + + if (dependencyIsValid) { + verboseMessage(`Adding ${chalk.bold(dependencyName)}.`, verbose); + windowsDependencies[dependencyName] = windowsDependency; + } + } } - //TODO: Update to the new RegisterAutolinkedNativeModulePackages method - - let generatedIncludes = - '// AutolinkedNativeModules.g.h -- contents generated by "react-native run-windows"\\\r\n\\\r\n#pragma once\\\r\n\\\r\n'; - const dependencies = config.dependencies; - let packageRegistrations = - '#define REGISTER_AUTOLINKED_NATIVE_MODULE_PACKAGES()'; - for (const dependency in dependencies) { - const windowDependency = dependencies[dependency].platforms.windows; - const cppProjFile = windowDependency - ? windowDependency.cppProjFile - : null; - if (cppProjFile == null) { - console.log('No C++/WinRT project found for ' + dependency); - continue; + // Generating cs/h files for app code consumption + if (projectLang === 'cs') { + let csUsingNamespaces = ''; + let csReactPacakgeProviders = ''; + + for (const dependencyName in windowsDependencies) { + windowsDependencies[dependencyName].projects.forEach(project => { + if (project.directDependency) { + csUsingNamespaces += `\n\n// Namespaces from ${dependencyName}`; + project.csNamespaces.forEach(namespace => { + csUsingNamespaces += `\nusing ${namespace};`; + }); + + csReactPacakgeProviders += `\n // IReactPackageProviders from ${dependencyName}`; + project.csPackageProviders.forEach(packageProvider => { + csReactPacakgeProviders += `\n packageProviders.Add(new ${packageProvider}());`; + }); + } + }); } - console.log( - `Adding include and package provider statement for ${dependency}`, + + const csFileName = 'AutolinkedNativeModules.g.cs'; + + const srcCsFile = path.join(templateRoot, projectLang, 'src', csFileName); + + const destCsFile = path.join(projectDir, csFileName); + + verboseMessage( + `Calculating ${chalk.bold(path.basename(destCsFile))}...`, + verbose, + ); + + const csContents = getNormalizedContents(srcCsFile, { + autolinkCsUsingNamespaces: csUsingNamespaces, + autolinkCsReactPacakgeProviders: csReactPacakgeProviders, + }); + + changesNecessary = + updateFile(destCsFile, csContents, verbose, checkMode) || + changesNecessary; + } else if (projectLang === 'cpp') { + let cppIncludes = ''; + let cppPackageProviders = ''; + + for (const dependencyName in windowsDependencies) { + windowsDependencies[dependencyName].projects.forEach(project => { + if (project.directDependency) { + cppIncludes += `\n\n// Includes from ${dependencyName}`; + project.cppHeaders.forEach(header => { + cppIncludes += `\n#include <${header}>`; + }); + + cppPackageProviders += `\n // IReactPackageProviders from ${dependencyName}`; + project.cppPackageProviders.forEach(packageProvider => { + cppPackageProviders += `\n packageProviders.Append(winrt::${packageProvider}());`; + }); + } + }); + } + + const cppFileName = 'AutolinkedNativeModules.g.cpp'; + + const srcCppFile = path.join( + templateRoot, + projectLang, + 'src', + cppFileName, + ); + + const destCppFile = path.join(projectDir, cppFileName); + + verboseMessage( + `Calculating ${chalk.bold(path.basename(destCppFile))}...`, + verbose, ); - const trimmedPackageName = windowDependency.packageName.trim(); - const includeStatement = `\r\n#include `; - generatedIncludes += includeStatement; - const packageRegistration = ` \\\r\n PackageProviders().Append(winrt::${trimmedPackageName}::ReactPackageProvider());`; - packageRegistrations += packageRegistration; + + const cppContents = getNormalizedContents(srcCppFile, { + autolinkCppIncludes: cppIncludes, + autolinkCppPackageProviders: cppPackageProviders, + }); + + changesNecessary = + updateFile(destCppFile, cppContents, verbose, checkMode) || + changesNecessary; + } + + // Generating targets for app project consumption + let projectReferencesForTargets = ''; + + for (const dependencyName in windowsDependencies) { + windowsDependencies[dependencyName].projects.forEach(project => { + if (project.directDependency) { + const dependencyProjectFile = path.join( + windowsDependencies[dependencyName].folder, + windowsDependencies[dependencyName].sourceDir, + project.projectFile, + ); + + const relDependencyProjectFile = path.relative( + projectDir, + dependencyProjectFile, + ); + + projectReferencesForTargets += `\n `; + projectReferencesForTargets += `\n + ${project.projectGuid} + `; + } + }); } - console.log('Updating AutolinkedNativeModules.g.h...'); - const contents = generatedIncludes + '\r\n' + packageRegistrations; - fs.writeFileSync(generatedHeader, contents, {encoding: 'utf8', flag: 'w'}); + const targetFileName = 'AutolinkedNativeModules.g.targets'; - //TODO: - //#2. Update project file to add references to native module packages + const srcTargetFile = path.join( + templateRoot, + projectLang, + 'src', + targetFileName, + ); - //TODO: - //#3. Update solution file to include native module project files + const destTargetFile = path.join(projectDir, targetFileName); - return; + verboseMessage( + `Calculating ${chalk.bold(path.basename(destTargetFile))}...`, + verbose, + ); + + const targetContents = getNormalizedContents(srcTargetFile, { + autolinkProjectReferencesForTargets: projectReferencesForTargets, + }); + + changesNecessary = + updateFile(destTargetFile, targetContents, verbose, checkMode) || + changesNecessary; + + // Generating project entries for solution + let projectsForSolution = []; + + for (const dependencyName in windowsDependencies) { + // Process projects + windowsDependencies[dependencyName].projects.forEach(project => { + const dependencyProjectFile = path.join( + windowsDependencies[dependencyName].folder, + windowsDependencies[dependencyName].sourceDir, + project.projectFile, + ); + + projectsForSolution.push({ + projectFile: dependencyProjectFile, + projectName: project.projectName, + projectLang: project.projectLang, + projectGuid: project.projectGuid, + }); + }); + } + + verboseMessage( + `Calculating ${chalk.bold(path.basename(solutionFile))} changes...`, + verbose, + ); + + projectsForSolution.forEach(project => { + const contentsChanged = vstools.addProjectToSolution( + solutionFile, + project, + verbose, + checkMode, + ); + changesNecessary = changesNecessary || contentsChanged; + }); + + spinner.succeed(); + var endTime = performance.now(); + + if (!changesNecessary) { + console.log( + `${chalk.green( + 'Success:', + )} No auto-linking changes necessary. (${Math.round( + endTime - startTime, + )}ms)`, + ); + } else if (checkMode) { + console.log( + `${chalk.yellow( + 'Warning:', + )} Auto-linking changes were necessary but ${chalk.bold( + '--check', + )} specified. Run ${chalk.bold( + "'npx react-native autolink-windows'", + )} to apply the changes. (${Math.round(endTime - startTime)}ms)`, + ); + exitProcessWithStatusCode(0, verbose); + } else { + console.log( + `${chalk.green( + 'Success:', + )} Auto-linking changes completed. (${Math.round( + endTime - startTime, + )}ms)`, + ); + } } catch (e) { - console.error('Parsing react-native config failed!'); - console.error(e); - return; + spinner.fail(); + var endTime = performance.now(); + console.log( + `${chalk.red('Error:')} ${e.toString()}. (${Math.round( + endTime - startTime, + )}ms)`, + ); + exitProcessWithStatusCode(1, verbose); } } module.exports = { - updateAutoLink, + name: 'autolink-windows', + description: 'performs autolinking', + func: updateAutoLink, + options: [ + { + command: '--logging', + description: 'Verbose output logging', + default: false, + }, + { + command: '--check', + description: 'Only check whether any autolinked files need to change', + default: false, + }, + { + command: '--sln [string]', + description: + "Override the app solution file determined by 'react-native config', e.g. windows\\myApp.sln", + default: undefined, + }, + { + command: '--proj [string]', + description: + "Override the app project file determined by 'react-native config', e.g. windows\\myApp\\myApp.vcxproj", + default: undefined, + }, + ], }; diff --git a/vnext/local-cli/runWindows/utils/build.js b/vnext/local-cli/runWindows/utils/build.js index ff08341df4a..237320a6757 100644 --- a/vnext/local-cli/runWindows/utils/build.js +++ b/vnext/local-cli/runWindows/utils/build.js @@ -10,12 +10,16 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const {execSync} = require('child_process'); -const glob = require('glob'); + const MSBuildTools = require('./msbuildtools'); const Version = require('./version'); -const {commandWithProgress, newSpinner} = require('./commandWithProgress'); +const { + commandWithProgress, + newSpinner, + newError, +} = require('./commandWithProgress'); const util = require('util'); -const chalk = require('chalk'); + const existsAsync = util.promisify(fs.exists); async function buildSolution( @@ -120,17 +124,66 @@ async function restoreNuGetPackages(options, slnFile, verbose) { } } -function getSolutionFile(options) { - const solutions = glob.sync(path.join(options.root, 'windows/*.sln')); - if (solutions.length === 0) { +const configErrorString = 'Error: '; + +function getAppSolutionFile(options, config) { + // Use the solution file if specified + if (options.sln) { + return path.join(options.root, options.sln); + } + + // Check the answer from react-native config + const windowsAppConfig = config.project.windows; + const configSolutionFile = windowsAppConfig.solutionFile; + + if (configSolutionFile.startsWith(configErrorString)) { + newError( + configSolutionFile.substr(configErrorString.length) + + ' Optionally, use --sln {slnFile}.', + ); return null; - } else if (solutions.length === 1) { - return solutions[0]; } else { - console.log(chalk.red('More than one solution file found:')); - console.log(chalk.bold(solutions.map(x => fs.realpathSync(x)).join('\n'))); - console.log('Use --sln {slnFile} to specify which one to build'); + return path.join( + windowsAppConfig.folder, + windowsAppConfig.sourceDir, + configSolutionFile, + ); + } +} + +function getAppProjectFile(options, config) { + // Use the project file if specified + if (options.proj) { + return path.join(options.root, options.proj); + } + + // Check the answer from react-native config + const windowsAppConfig = config.project.windows; + const configProject = windowsAppConfig.project; + + if ( + typeof configProject === 'string' && + configProject.startsWith(configErrorString) + ) { + newError( + configProject.substr(configErrorString.length) + + ' Optionally, use --proj {projFile}.', + ); return null; + } else { + const configProjectFile = configProject.projectFile; + if (configProjectFile.startsWith(configErrorString)) { + newError( + configProjectFile.substr(configErrorString.length) + + ' Optionally, use --proj {projFile}.', + ); + return null; + } + return path.join( + windowsAppConfig.folder, + windowsAppConfig.sourceDir, + configProjectFile, + ); } } @@ -148,7 +201,8 @@ function parseMsBuildProps(options) { module.exports = { buildSolution, - getSolutionFile, + getAppSolutionFile, + getAppProjectFile, restoreNuGetPackages, parseMsBuildProps, }; diff --git a/vnext/local-cli/runWindows/utils/vstools.js b/vnext/local-cli/runWindows/utils/vstools.js new file mode 100644 index 00000000000..b59fb4a1a88 --- /dev/null +++ b/vnext/local-cli/runWindows/utils/vstools.js @@ -0,0 +1,213 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * @format + */ +// @ts-check + +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +const projectTypeGuidsByLanguage = { + cpp: '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}', + cs: '{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}', +}; + +/** + * Checks is the given block of lines exists within an array of lines. + * @param {array} lines The array of lines to search. + * @param {array} block The block of lines to search for. + * @return {boolean} True if the block of lines does exist within lines. + */ +function linesContainsBlock(lines, block) { + if (block.length > 0) { + var startIndex = lines.indexOf(block[0]); + + if (startIndex >= 0) { + for (let i = 1; i < block.length; i++) { + if (lines[startIndex + i] !== block[i]) { + return false; + } + } + return true; + } + } + + return false; +} + +/** + * Insert the given block of lines into an array of lines. + * @param {array} lines The array of lines to insert into. + * @param {array} block The block of lines to insert. + * @param {number} index The index to perform the insert. + */ +function insertBlockIntoLines(lines, block, index) { + for (let i = 0; i < block.length; i++) { + lines.splice(index + i, 0, block[i]); + } +} + +/** + * Search through an array of lines for a block of lines starting with startLine and ending with endLine. + * @param {array} lines The array of lines to search. + * @param {string} startLine The first line of the block. + * @param {string} endLine The last line of the block. + * @param {boolean} includeStartEnd Include the start and end lines in the result. + * @return {array} The found block of lines, if found. + */ +function getBlockContentsFromLines( + lines, + startLine, + endLine, + includeStartEnd = true, +) { + const startIndex = lines.indexOf(startLine); + const endIndex = lines.indexOf(endLine, startIndex); + + if (startIndex >= 0 && startIndex < endIndex) { + if (includeStartEnd) { + return lines.slice(startIndex, endIndex + 1); + } else if (startIndex + 1 < endIndex) { + return lines.slice(startIndex + 1, endIndex); + } + } + + return []; +} + +/** + * Adds the necessary info from a VS project into a VS solution file so that it will build. + * @param {string} slnFile The Absolute path to the target VS solution file. + * @param {object} project The object representing the project info. + * @param {boolean} verbose If true, enable verbose logging. + * @param {boolean} checkMode It true, don't make any changes. + * @return {boolean} Whether any changes were necessary. + */ +function addProjectToSolution( + slnFile, + project, + verbose = false, + checkMode = false, +) { + if (verbose) { + console.log( + `Processing ${chalk.bold(path.basename(project.projectFile))}...`, + ); + } + + let slnLines = fs + .readFileSync(slnFile) + .toString() + .split('\r\n'); + + let contentsChanged = false; + + // Check for the project entry block + + const slnDir = path.dirname(slnFile); + const relProjectFile = path.relative(slnDir, project.projectFile); + + const projectTypeGuid = projectTypeGuidsByLanguage[project.projectLang]; + + const projectGuid = project.projectGuid.toUpperCase(); + + const projectEntryBlock = [ + `Project("${projectTypeGuid}") = "${ + project.projectName + }", "${relProjectFile}", "${projectGuid}"`, + 'EndProject', + ]; + + if (!linesContainsBlock(slnLines, projectEntryBlock)) { + if (verbose) { + console.log(chalk.yellow('Missing project entry block.')); + } + + const globalIndex = slnLines.indexOf('Global'); + insertBlockIntoLines(slnLines, projectEntryBlock, globalIndex); + contentsChanged = true; + } + + // Check for the project configuration platforms + + const slnConfigs = getBlockContentsFromLines( + slnLines, + '\tGlobalSection(SolutionConfigurationPlatforms) = preSolution', + '\tEndGlobalSection', + false, + ).map(line => line.match(/\s+([\w|]+)\s=/)[1]); + + let projectConfigLines = []; + + slnConfigs.forEach(slnConfig => { + projectConfigLines.push( + `\t\t${projectGuid}.${slnConfig}.ActiveCfg = ${slnConfig.replace( + 'x86', + 'Win32', + )}`, + ); + projectConfigLines.push( + `\t\t${projectGuid}.${slnConfig}.Build.0 = ${slnConfig.replace( + 'x86', + 'Win32', + )}`, + ); + }); + + const projectConfigStartIndex = slnLines.indexOf( + '\tGlobalSection(ProjectConfigurationPlatforms) = postSolution', + ); + + projectConfigLines.forEach(projectConfigLine => { + if (slnLines.indexOf(projectConfigLine) < 0) { + if (verbose) { + console.log(chalk.yellow('Missing project config block.')); + } + + const projectConfigEndIndex = slnLines.indexOf( + '\tEndGlobalSection', + projectConfigStartIndex, + ); + + slnLines.splice(projectConfigEndIndex, 0, projectConfigLine); + contentsChanged = true; + } + }); + + // Write out new solution file if there were changes + if (contentsChanged) { + if (verbose) { + console.log( + chalk.yellow( + `${chalk.bold(path.basename(slnFile))} needs to be updated.`, + ), + ); + } + + if (!checkMode) { + if (verbose) { + console.log( + `Writing changes to ${chalk.bold(path.basename(slnFile))}...`, + ); + } + + const slnContents = slnLines.join('\r\n'); + fs.writeFileSync(slnFile, slnContents, { + encoding: 'utf8', + flag: 'w', + }); + } + } else { + if (verbose) { + console.log(`No changes to ${chalk.bold(path.basename(slnFile))}.`); + } + } + + return contentsChanged; +} + +module.exports = { + addProjectToSolution: addProjectToSolution, +}; diff --git a/vnext/package.json b/vnext/package.json index b44bd729b16..f6e8158895b 100644 --- a/vnext/package.json +++ b/vnext/package.json @@ -35,7 +35,9 @@ "shelljs": "^0.7.8", "username": "^5.1.0", "uuid": "^3.3.2", - "xml-parser": "^1.2.1" + "xml-parser": "^1.2.1", + "xmldom": "^0.3.0", + "xpath": "^0.0.27" }, "devDependencies": { "@microsoft/api-documenter": "^7.3.8", @@ -53,9 +55,9 @@ "just-scripts": "^0.36.1", "prettier": "1.17.0", "react": "16.11.0", - "react-native-windows-codegen": "0.0.6", - "react-native-platform-override": "^0.0.4", "react-native": "0.62.2", + "react-native-platform-override": "^0.0.4", + "react-native-windows-codegen": "0.0.6", "typescript": "^3.8.3" }, "peerDependencies": { diff --git a/vnext/react-native.config.js b/vnext/react-native.config.js index 9a81411678b..0dcc82c393d 100644 --- a/vnext/react-native.config.js +++ b/vnext/react-native.config.js @@ -6,6 +6,7 @@ module.exports = { // **** This section defined commands and options on how to provide the windows platform to external applications commands: [ require('./local-cli/runWindows/runWindows'), + require('./local-cli/runWindows/utils/autolink'), ], platforms: { windows: { diff --git a/yarn.lock b/yarn.lock index 0c7dcde7442..230be92c4b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14095,6 +14095,11 @@ xmldom@0.1.x, xmldom@^0.1.19, xmldom@^0.1.22, xmldom@^0.1.27: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= +xmldom@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" + integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g== + xpath@0.0.27, xpath@^0.0.27: version "0.0.27" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"