diff --git a/.ado/compliance.yml b/.ado/compliance.yml index f6f80e27130..3831667368b 100644 --- a/.ado/compliance.yml +++ b/.ado/compliance.yml @@ -6,10 +6,10 @@ parameters: default: Medium: name: rnw-pool-4-microsoft - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 Large: name: rnw-pool-8-microsoft - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 - name: forceCodeQL displayName: Force CodeQL to rebuild databases type: boolean diff --git a/.ado/continuous.yml b/.ado/continuous.yml index c5b97e77de9..9d916c4ddf9 100644 --- a/.ado/continuous.yml +++ b/.ado/continuous.yml @@ -13,13 +13,13 @@ parameters: default: Small: name: rnw-pool-2 - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 Medium: name: rnw-pool-4 - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 Large: name: rnw-pool-8 - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 stages: - template: stages.yml diff --git a/.ado/image/rnw-img-vs2022-node18.json b/.ado/image/rnw-img-vs2022-node18.json new file mode 100644 index 00000000000..8901268d8f0 --- /dev/null +++ b/.ado/image/rnw-img-vs2022-node18.json @@ -0,0 +1,66 @@ +{ + "imageType": "Managed", + "baseImage": "/MicrosoftWindowsServer/WindowsServer/2022-datacenter/latest", + "artifacts": [ + { + "name": "windows-EnableDeveloperMode" + }, + { + "name": "windows-enable-long-paths" + }, + { + "name": "windows-gitinstall" + }, + { + "name": "windows-AzPipeline-ImageHelpers" + }, + { + "name": "windows-AzPipeline-InitializeVM" + }, + { + "name": "windows-AzPipeline-powershellCore" + }, + { + "name": "windows-AzPipeline-7zip" + }, + { + "name": "windows-visualstudio-bootstrapper", + "parameters": { + "Workloads": "--add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.Universal --add Microsoft.Component.MSBuild --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.ComponentGroup.UWP.Support --add Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core --add Microsoft.VisualStudio.Component.Windows10SDK.19041 --add Microsoft.VisualStudio.ComponentGroup.UWP.VC --includeRecommended --includeOptional", + "SKU": "Enterprise", + "VSBootstrapperURL": "https://aka.ms/vs/17/release/vs_Enterprise.exe" + } + }, + { + "name": "Windows-NodeJS", + "parameters": { + "Version": "18.16.1" + } + }, + { + "name": "windows-npm-global", + "parameters": { + "packages": "yarn@1.22.19, midgard-yarn@1.23.34, verdaccio@5.2.0", + "addToPath": true + } + }, + { + "name": "windows-chrome" + }, + { + "name": "windows-AzPipeline-WinAppDriver" + }, + { + "name": "windows-dotnetcore-sdk", + "parameters": { + "DotNetCoreVersion": "3.1.425" + } + }, + { + "name": "windows-dotnetcore-sdk", + "parameters": { + "DotNetCoreVersion": "6.0.403" + } + } + ] +} \ No newline at end of file diff --git a/.ado/jobs/cli-init.yml b/.ado/jobs/cli-init.yml index 83e7569f21c..58e73539da2 100644 --- a/.ado/jobs/cli-init.yml +++ b/.ado/jobs/cli-init.yml @@ -434,6 +434,12 @@ jobs: configuration: ${{ parameters.configuration }} buildEnvironment: ${{ parameters.buildEnvironment }} + - task: CmdLine@2 + displayName: Create npm directory + name: createNpmDirectory + inputs: + script: mkdir %APPDATA%\npm + - template: ../templates/react-native-init.yml parameters: language: ${{ matrix.language }} diff --git a/.ado/jobs/e2e-test.yml b/.ado/jobs/e2e-test.yml index 72d6140d90d..c947d7a9eb0 100644 --- a/.ado/jobs/e2e-test.yml +++ b/.ado/jobs/e2e-test.yml @@ -45,7 +45,7 @@ jobs: displayName: E2E Test App ${{ matrix.Name }} variables: [template: ../variables/windows.yml] - pool: ${{ parameters.AgentPool.Medium }} + pool: ${{ parameters.AgentPool.MediumNode16 }} timeoutInMinutes: 60 # how long to run the job before automatically cancelling cancelTimeoutInMinutes: 5 # how much time to give 'run always even if cancelled tasks' before killing them @@ -153,7 +153,7 @@ jobs: displayName: E2E Test App Fabric ${{ matrix.Name }} variables: [template: ../variables/windows.yml] - pool: ${{ parameters.AgentPool.Medium }} + pool: ${{ parameters.AgentPool.MediumNode16 }} timeoutInMinutes: 60 # how long to run the job before automatically cancelling cancelTimeoutInMinutes: 5 # how much time to give 'run always even if cancelled tasks' before killing them diff --git a/.ado/jobs/node-tests.yml b/.ado/jobs/node-tests.yml index 7a9f0878c6f..38fc59b9e83 100644 --- a/.ado/jobs/node-tests.yml +++ b/.ado/jobs/node-tests.yml @@ -11,7 +11,7 @@ parameters: - name: versions type: object - default: [16] + default: [18] jobs: - ${{ each nodeVersion in parameters.versions }}: diff --git a/.ado/jobs/playground.yml b/.ado/jobs/playground.yml index 5f57d9c7809..1f250428471 100644 --- a/.ado/jobs/playground.yml +++ b/.ado/jobs/playground.yml @@ -103,7 +103,7 @@ jobs: displayName: Playground ${{ matrix.Name }} variables: [template: ../variables/windows.yml] - pool: ${{ parameters.AgentPool.Medium }} + pool: ${{ parameters.AgentPool.MediumNode16 }} timeoutInMinutes: 60 cancelTimeoutInMinutes: 5 diff --git a/.ado/publish.yml b/.ado/publish.yml index eb44c06d0da..3467423b638 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -18,10 +18,10 @@ parameters: default: Medium: name: rnw-pool-4-microsoft - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 Large: name: rnw-pool-8-microsoft - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 - name: desktopBuildMatrix type: object diff --git a/.ado/templates/prepare-js-env.yml b/.ado/templates/prepare-js-env.yml index 8c8a706926d..46dd12a4cf4 100644 --- a/.ado/templates/prepare-js-env.yml +++ b/.ado/templates/prepare-js-env.yml @@ -11,7 +11,7 @@ steps: - task: NodeTool@0 displayName: Set Node Version inputs: - versionSpec: '16.x' + versionSpec: '18.x' - template: yarn-install.yml parameters: diff --git a/.ado/windows-vs-pr.yml b/.ado/windows-vs-pr.yml index faf6c2e09ed..1b8e84448df 100644 --- a/.ado/windows-vs-pr.yml +++ b/.ado/windows-vs-pr.yml @@ -16,13 +16,16 @@ parameters: default: Small: name: rnw-pool-2 - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 Medium: + name: rnw-pool-4 + demands: ImageOverride -equals rnw-img-vs2022-node18 + MediumNode16: name: rnw-pool-4 demands: ImageOverride -equals rnw-img-vs2022 Large: name: rnw-pool-8 - demands: ImageOverride -equals rnw-img-vs2022 + demands: ImageOverride -equals rnw-img-vs2022-node18 stages: - template: stages.yml diff --git a/.unbroken_exclusions b/.unbroken_exclusions index c45218154c0..fc3b366a363 100644 --- a/.unbroken_exclusions +++ b/.unbroken_exclusions @@ -21,3 +21,4 @@ URL not found https://docs.github.com/get-started/quickstart while parsing CONTR !packages/@rnw-scripts/format-files/node_modules !packages/@rnw-scripts/promote-release/node_modules !.github/ISSUE_TEMPLATE +!.github/security.md \ No newline at end of file diff --git a/change/@office-iss-react-native-win32-2a5de4b2-a121-42e6-8388-1f71a7c705af.json b/change/@office-iss-react-native-win32-2a5de4b2-a121-42e6-8388-1f71a7c705af.json new file mode 100644 index 00000000000..be71cff759a --- /dev/null +++ b/change/@office-iss-react-native-win32-2a5de4b2-a121-42e6-8388-1f71a7c705af.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "[Win32] view.focus sometimes skips setting focus", + "packageName": "@office-iss/react-native-win32", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-1d2f797d-2e27-460b-acbb-450fe696a40b.json b/change/react-native-windows-1d2f797d-2e27-460b-acbb-450fe696a40b.json new file mode 100644 index 00000000000..9c10aacd364 --- /dev/null +++ b/change/react-native-windows-1d2f797d-2e27-460b-acbb-450fe696a40b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add SelfContained to MS.RN.Manged.Codegen.csproj", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/@office-iss/react-native-win32/overrides.json b/packages/@office-iss/react-native-win32/overrides.json index 46e72311c30..214841109d8 100644 --- a/packages/@office-iss/react-native-win32/overrides.json +++ b/packages/@office-iss/react-native-win32/overrides.json @@ -142,6 +142,10 @@ "baseFile": "packages/react-native/Libraries/Components/TextInput/TextInputState.js", "baseHash": "60655baaca427e1c7c1b8884833b848335c4033b" }, + { + "type": "platform", + "file": "src/Libraries/Components/TextInput/Win32TextInputNativeComponent.js" + }, { "type": "copy", "file": "src/Libraries/Components/ToastAndroid/ToastAndroid.win32.js", diff --git a/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInput.win32.js b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInput.win32.js index b3d2937ca33..a1777ea8739 100644 --- a/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInput.win32.js +++ b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInput.win32.js @@ -1,44 +1,38 @@ /** - * This is a forked and slightly modified version of React Native's TextInput. - * The fork is necessary as platform checks in the base implementation made the - * control unusable on win32. In addition to cleaning up some of the code, this - * fork also uses Typescript rather than Flow for type safety. + * Copyright (c) Meta Platforms, Inc. and affiliates. * - * More general documentation on this control can be found at - * https://facebook.github.io/react-native/docs/textinput.html - * - * The original implementation can be found at - * https://github.com/facebook/react-native/blob/1013a010492a7bef5ff58073a088ac924a986e9e/Libraries/Components/TextInput/TextInput.js - * - * This control does not support the full React Native TextInput interface yet. - * Most of the work necessary to make that happen needs to happen on the native side. - * Future work on the JS side may include: - * 1. Expanded typings for some of the events - * 2. Additional work to manage selection - * 3. Any base/default styling work + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import {findNodeHandle} from '../../ReactNative/RendererProxy'; -import UIManager from '../../ReactNative/UIManager'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; -import * as React from 'react'; -import TextAncestor from '../../Text/TextAncestor'; -import TextInputState from './TextInputState'; -import type {ViewProps} from '../View/ViewPropTypes'; import type { PressEvent, ScrollEvent, SyntheticEvent, } from '../../Types/CoreEventTypes'; +import type {ViewProps} from '../View/ViewPropTypes'; +import type {TextInputType} from './TextInput.flow'; + +import usePressability from '../../Pressability/usePressability'; +import flattenStyle from '../../StyleSheet/flattenStyle'; import StyleSheet, { type ColorValue, type TextStyleProp, type ViewStyleProp, } from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import TextAncestor from '../../Text/TextAncestor'; +import Platform from '../../Utilities/Platform'; +import useMergeRefs from '../../Utilities/useMergeRefs'; +import TextInputState from './TextInputState'; +import invariant from 'invariant'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {useCallback, useLayoutEffect, useRef, useState} from 'react'; type ReactRefSetter = {current: null | T, ...} | ((ref: null | T) => mixed); type TextInputInstance = React.ElementRef> & { @@ -48,6 +42,39 @@ type TextInputInstance = React.ElementRef> & { +setSelection: (start: number, end: number) => void, }; +let AndroidTextInput; +let AndroidTextInputCommands; +let RCTSinglelineTextInputView; +let RCTSinglelineTextInputNativeCommands; +let RCTMultilineTextInputView; +let RCTMultilineTextInputNativeCommands; +let WindowsTextInput; // [Windows] +let WindowsTextInputCommands; // [Windows] +import type {KeyEvent} from '../../Types/CoreEventTypes'; // [Windows] + +// [Windows +if (Platform.OS === 'android') { + AndroidTextInput = require('./AndroidTextInputNativeComponent').default; + AndroidTextInputCommands = + require('./AndroidTextInputNativeComponent').Commands; +} else if (Platform.OS === 'ios') { + RCTSinglelineTextInputView = + require('./RCTSingelineTextInputNativeComponent').default; + RCTSinglelineTextInputNativeCommands = + require('./RCTSingelineTextInputNativeComponent').Commands; + RCTMultilineTextInputView = + require('./RCTMultilineTextInputNativeComponent').default; + RCTMultilineTextInputNativeCommands = + require('./RCTMultilineTextInputNativeComponent').Commands; +} +// [Windows +else if (Platform.OS === 'win32') { + WindowsTextInput = require('./Win32TextInputNativeComponent').default; + WindowsTextInputCommands = + require('./Win32TextInputNativeComponent').Commands; +} +// Windows] + export type ChangeEvent = SyntheticEvent< $ReadOnly<{| eventCount: number, @@ -184,6 +211,15 @@ export type TextContentType = | 'addressState' | 'countryName' | 'creditCardNumber' + | 'creditCardExpiration' + | 'creditCardExpirationMonth' + | 'creditCardExpirationYear' + | 'creditCardSecurityCode' + | 'creditCardType' + | 'creditCardName' + | 'creditCardGivenName' + | 'creditCardMiddleName' + | 'creditCardFamilyName' | 'emailAddress' | 'familyName' | 'fullStreetAddress' @@ -204,7 +240,11 @@ export type TextContentType = | 'username' | 'password' | 'newPassword' - | 'oneTimeCode'; + | 'oneTimeCode' + | 'birthdate' + | 'birthdateDay' + | 'birthdateMonth' + | 'birthdateYear'; export type enterKeyHintType = // Cross Platform @@ -320,6 +360,16 @@ type IOSProps = $ReadOnly<{| * @platform ios */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + + /** + * If `false`, the iOS system will not insert an extra space after a paste operation + * neither delete one or two spaces after a cut or delete operation. + * + * The default value is `true`. + * + * @platform ios + */ + smartInsertDelete?: ?boolean, |}>; type AndroidProps = $ReadOnly<{| @@ -408,10 +458,37 @@ type AndroidProps = $ReadOnly<{| underlineColorAndroid?: ?ColorValue, |}>; +// [Windows + +type SubmitKeyEvent = $ReadOnly<{| + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + code: string, +|}>; + +type WindowsProps = $ReadOnly<{| + /** + * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. + * @platform windows + */ + clearTextOnSubmit?: ?boolean, + + /** + * Configures keys that can be used to submit editing for the TextInput. + * @platform windows + */ + submitKeyEvents?: ?$ReadOnlyArray, +|}>; + +// Windows] + export type Props = $ReadOnly<{| ...$Diff>, ...IOSProps, ...AndroidProps, + ...WindowsProps, // [Windows] /** * String to be read by screenreaders to indicate an error state. The acceptable parameters @@ -441,7 +518,16 @@ export type Props = $ReadOnly<{| * - `additional-name` * - `address-line1` * - `address-line2` + * - `birthdate-day` (iOS 17+) + * - `birthdate-full` (iOS 17+) + * - `birthdate-month` (iOS 17+) + * - `birthdate-year` (iOS 17+) * - `cc-number` + * - `cc-csc` (iOS 17+) + * - `cc-exp` (iOS 17+) + * - `cc-exp-day` (iOS 17+) + * - `cc-exp-month` (iOS 17+) + * - `cc-exp-year` (iOS 17+) * - `country` * - `current-password` * - `email` @@ -460,6 +546,11 @@ export type Props = $ReadOnly<{| * * The following values work on iOS only: * + * - `cc-name` (iOS 17+) + * - `cc-given-name` (iOS 17+) + * - `cc-middle-name` (iOS 17+) + * - `cc-family-name` (iOS 17+) + * - `cc-type` (iOS 17+) * - `nickname` * - `organization` * - `organization-title` @@ -467,15 +558,6 @@ export type Props = $ReadOnly<{| * * The following values work on Android only: * - * - `birthdate-day` - * - `birthdate-full` - * - `birthdate-month` - * - `birthdate-year` - * - `cc-csc` - * - `cc-exp` - * - `cc-exp-day` - * - `cc-exp-month` - * - `cc-exp-year` * - `gender` * - `name-family` * - `name-given` @@ -511,6 +593,11 @@ export type Props = $ReadOnly<{| | 'cc-exp-month' | 'cc-exp-year' | 'cc-number' + | 'cc-name' + | 'cc-given-name' + | 'cc-middle-name' + | 'cc-family-name' + | 'cc-type' | 'country' | 'current-password' | 'email' @@ -575,7 +662,7 @@ export type Props = $ReadOnly<{| * On Android devices manufactured by Xiaomi with Android Q, * when keyboardType equals 'email-address'this will be set * in native to 'true' to prevent a system related crash. This - * will cause cursor to be diabled as a side-effect. + * will cause cursor to be disabled as a side-effect. * */ caretHidden?: ?boolean, @@ -933,215 +1020,875 @@ export type Props = $ReadOnly<{| * unwanted edits without flicker. */ value?: ?Stringish, - - text?: ?Stringish, // Win32 |}>; -/* -import { - TextInputProps, - NativeMethods, -} from 'react-native'; -import { - IBlurEvent, - IChangeEvent, - IFocusEvent, -} from './TextInput.Types.win32'; -import React from 'react' - -type RCTTextInputProps = TextInputProps & { - text: string; -}; -*/ - -// RCTTextInput is the native component that win32 understands -const RCTTextInput = requireNativeComponent('RCTTextInput'); +const emptyFunctionThatReturnsTrue = () => true; -// Adding typings on ViewManagers is problematic as available functionality is not known until -// registration at runtime and would require native and js to always be in sync. -const TextInputViewManager = UIManager.getViewManagerConfig('RCTTextInput'); +/** + * A foundational component for inputting text into the app via a + * keyboard. Props provide configurability for several features, such as + * auto-correction, auto-capitalization, placeholder text, and different keyboard + * types, such as a numeric keypad. + * + * The simplest use case is to plop down a `TextInput` and subscribe to the + * `onChangeText` events to read the user input. There are also other events, + * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple + * example: + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, TextInput } from 'react-native'; + * + * export default class UselessTextInput extends Component { + * constructor(props) { + * super(props); + * this.state = { text: 'Useless Placeholder' }; + * } + * + * render() { + * return ( + * this.setState({text})} + * value={this.state.text} + * /> + * ); + * } + * } + * + * // skip this line if using Create React Native App + * AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput); + * ``` + * + * Two methods exposed via the native element are .focus() and .blur() that + * will focus or blur the TextInput programmatically. + * + * Note that some props are only available with `multiline={true/false}`. + * Additionally, border styles that apply to only one side of the element + * (e.g., `borderBottomColor`, `borderLeftWidth`, etc.) will not be applied if + * `multiline=false`. To achieve the same effect, you can wrap your `TextInput` + * in a `View`: + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, View, TextInput } from 'react-native'; + * + * class UselessTextInput extends Component { + * render() { + * return ( + * + * ); + * } + * } + * + * export default class UselessTextInputMultiline extends Component { + * constructor(props) { + * super(props); + * this.state = { + * text: 'Useless Multiline Placeholder', + * }; + * } + * + * // If you type something in the text box that is a color, the background will change to that + * // color. + * render() { + * return ( + * + * this.setState({text})} + * value={this.state.text} + * /> + * + * ); + * } + * } + * + * // skip these lines if using Create React Native App + * AppRegistry.registerComponent( + * 'AwesomeProject', + * () => UselessTextInputMultiline + * ); + * ``` + * + * `TextInput` has by default a border at the bottom of its view. This border + * has its padding set by the background image provided by the system, and it + * cannot be changed. Solutions to avoid this is to either not set height + * explicitly, case in which the system will take care of displaying the border + * in the correct position, or to not display the border by setting + * `underlineColorAndroid` to transparent. + * + * Note that on Android performing text selection in input can change + * app's activity `windowSoftInputMode` param to `adjustResize`. + * This may cause issues with components that have position: 'absolute' + * while keyboard is active. To avoid this behavior either specify `windowSoftInputMode` + * in AndroidManifest.xml ( https://developer.android.com/guide/topics/manifest/activity-element.html ) + * or control this param programmatically with native code. + * + */ +function InternalTextInput(props: Props): React.Node { + const { + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, + 'aria-multiselectable': ariaMultiselectable, // Win32 + 'aria-required': ariaRequired, // Win32 + accessibilityState, + id, + tabIndex, + selection: propsSelection, + ...otherProps + } = props; + + const inputRef = useRef>>(null); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const selection: ?Selection = + propsSelection == null + ? null + : { + start: propsSelection.start, + end: propsSelection.end ?? propsSelection.start, + }; + + const [mostRecentEventCount, setMostRecentEventCount] = useState(0); + + const [lastNativeText, setLastNativeText] = useState(props.value); + const [lastNativeSelectionState, setLastNativeSelection] = useState<{| + selection: ?Selection, + mostRecentEventCount: number, + |}>({selection, mostRecentEventCount}); + + const lastNativeSelection = lastNativeSelectionState.selection; + + let viewCommands; + if (AndroidTextInputCommands) { + viewCommands = AndroidTextInputCommands; + } + // [Windows + else if (WindowsTextInputCommands) { + viewCommands = WindowsTextInputCommands; + } + // Windows] + else { + viewCommands = + props.multiline === true + ? RCTMultilineTextInputNativeCommands + : RCTSinglelineTextInputNativeCommands; + } -class TextInput extends React.Component { - // TODO: Once the native side begins supporting programmatic selection - // this will become important for selection management - // private _lastNativeTextSelection: any; + const text = + typeof props.value === 'string' + ? props.value + : typeof props.defaultValue === 'string' + ? props.defaultValue + : ''; + + // This is necessary in case native updates the text and JS decides + // that the update should be ignored and we should stick with the value + // that we have in JS. + useLayoutEffect(() => { + const nativeUpdate: {text?: string, selection?: Selection} = {}; + + if (lastNativeText !== props.value && typeof props.value === 'string') { + nativeUpdate.text = props.value; + setLastNativeText(props.value); + } - _rafID: AnimationFrameID; + if ( + selection && + lastNativeSelection && + (lastNativeSelection.start !== selection.start || + lastNativeSelection.end !== selection.end) + ) { + nativeUpdate.selection = selection; + setLastNativeSelection({selection, mostRecentEventCount}); + } - _lastNativeText: ?Stringish; - _eventCount: number = 0; + if (Object.keys(nativeUpdate).length === 0) { + return; + } - constructor(props: Props) { - super(props); - } + if (inputRef.current != null) { + viewCommands.setTextAndSelection( + inputRef.current, + mostRecentEventCount, + text, + selection?.start ?? -1, + selection?.end ?? -1, + ); + } + }, [ + mostRecentEventCount, + inputRef, + props.value, + props.defaultValue, + lastNativeText, + selection, + lastNativeSelection, + text, + viewCommands, + ]); + + useLayoutEffect(() => { + const inputRefValue = inputRef.current; + + if (inputRefValue != null) { + TextInputState.registerInput(inputRefValue); + + return () => { + TextInputState.unregisterInput(inputRefValue); + + if (TextInputState.currentlyFocusedInput() === inputRefValue) { + nullthrows(inputRefValue).blur(); + } + }; + } + }, [inputRef]); + + const setLocalRef = useCallback( + (instance: TextInputInstance | null) => { + inputRef.current = instance; + + /* + Hi reader from the future. I'm sorry for this. + + This is a hack. Ideally we would forwardRef to the underlying + host component. However, since TextInput has it's own methods that can be + called as well, if we used the standard forwardRef then these + methods wouldn't be accessible and thus be a breaking change. + + We have a couple of options of how to handle this: + - Return a new ref with everything we methods from both. This is problematic + because we need React to also know it is a host component which requires + internals of the class implementation of the ref. + - Break the API and have some other way to call one set of the methods or + the other. This is our long term approach as we want to eventually + get the methods on host components off the ref. So instead of calling + ref.measure() you might call ReactNative.measure(ref). This would hopefully + let the ref for TextInput then have the methods like `.clear`. Or we do it + the other way and make it TextInput.clear(textInputRef) which would be fine + too. Either way though is a breaking change that is longer term. + - Mutate this ref. :( Gross, but accomplishes what we need in the meantime + before we can get to the long term breaking change. + */ + if (instance != null) { + // $FlowFixMe[incompatible-use] - See the explanation above. + Object.assign(instance, { + clear(): void { + if (inputRef.current != null) { + viewCommands.setTextAndSelection( + inputRef.current, + mostRecentEventCount, + '', + 0, + 0, + ); + } + }, + // TODO: Fix this returning true on null === null, when no input is focused + isFocused(): boolean { + return TextInputState.currentlyFocusedInput() === inputRef.current; + }, + getNativeRef(): ?React.ElementRef> { + return inputRef.current; + }, + setSelection(start: number, end: number): void { + if (inputRef.current != null) { + viewCommands.setTextAndSelection( + inputRef.current, + mostRecentEventCount, + null, + start, + end, + ); + } + }, + }); + } + }, + [mostRecentEventCount, viewCommands], + ); + + const ref = useMergeRefs( + setLocalRef, + props.forwardedRef, + ); + + const _onChange = (event: ChangeEvent) => { + const currentText = event.nativeEvent.text; + props.onChange && props.onChange(event); + props.onChangeText && props.onChangeText(currentText); + + if (inputRef.current == null) { + // calling `props.onChange` or `props.onChangeText` + // may clean up the input itself. Exits here. + return; + } - /** - * On mount TextInput needs to register itself with TextInputState - * and conditionally request an animation frame for focus. - */ - componentDidMount() { - this._lastNativeText = this.props.value; + setLastNativeText(currentText); + // This must happen last, after we call setLastNativeText. + // Different ordering can cause bugs when editing AndroidTextInputs + // with multiple Fragments. + // We must update this so that controlled input updates work. + setMostRecentEventCount(event.nativeEvent.eventCount); + }; - // $FlowFixMe - // Win32 - TextInputState.registerInput(this); + const _onChangeSync = (event: ChangeEvent) => { + const currentText = event.nativeEvent.text; + props.unstable_onChangeSync && props.unstable_onChangeSync(event); + props.unstable_onChangeTextSync && + props.unstable_onChangeTextSync(currentText); - if (this.props.autoFocus === true) { - this._rafID = requestAnimationFrame(this.focus); + if (inputRef.current == null) { + // calling `props.onChange` or `props.onChangeText` + // may clean up the input itself. Exits here. + return; } - } - /** - * This is an unfortunate consequence of having controlled TextInputs. - * Tree diffing reconciliation will not always send down text values - * This sets text explicitly. - */ - componentDidUpdate() { - if (this._lastNativeText !== this._getText()) { - this._getText() && this.setNativeText(this._getText()); - } + setLastNativeText(currentText); + // This must happen last, after we call setLastNativeText. + // Different ordering can cause bugs when editing AndroidTextInputs + // with multiple Fragments. + // We must update this so that controlled input updates work. + setMostRecentEventCount(event.nativeEvent.eventCount); + }; - return; - } + const _onSelectionChange = (event: SelectionChangeEvent) => { + props.onSelectionChange && props.onSelectionChange(event); - /** - * Pre-unmoun the TextInput should blur, unregister and clean up - * the animation frame for focus (edge cases) - */ - componentWillUnmount() { - // blur - if (this.isFocused()) { - this.blur(); + if (inputRef.current == null) { + // calling `props.onSelectionChange` + // may clean up the input itself. Exits here. + return; } - // unregister - // $FlowFixMe - // Win32 - TextInputState.unregisterInput(this); + setLastNativeSelection({ + selection: event.nativeEvent.selection, + mostRecentEventCount, + }); + }; - // cancel animationFrame - if (this._rafID !== null) { - cancelAnimationFrame(this._rafID); + const _onFocus = (event: FocusEvent) => { + TextInputState.focusInput(inputRef.current); + if (props.onFocus) { + props.onFocus(event); } - if (this._rafID) { - return; + }; + + const _onBlur = (event: BlurEvent) => { + TextInputState.blurInput(inputRef.current); + if (props.onBlur) { + props.onBlur(event); } - return; - } + }; - render(): React.Node { - let {allowFontScaling, ...otherProps} = this.props; - - allowFontScaling = - this.props.allowFontScaling === null || - this.props.allowFontScaling === undefined - ? true - : this.props.allowFontScaling; - - return ( - - - - ); - } + const _onScroll = (event: ScrollEvent) => { + props.onScroll && props.onScroll(event); + }; - /** - * Returns true if the TextInput is focused - */ - isFocused(): boolean { - return TextInputState.currentlyFocusedInput() === this; + let textInput = null; + + const multiline = props.multiline ?? false; + + let submitBehavior: SubmitBehavior; + if (props.submitBehavior != null) { + // `submitBehavior` is set explicitly + if (!multiline && props.submitBehavior === 'newline') { + // For single line text inputs, `'newline'` is not a valid option + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = props.submitBehavior; + } + } else if (multiline) { + if (props.blurOnSubmit === true) { + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = 'newline'; + } + } else { + // Single line + if (props.blurOnSubmit !== false) { + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = 'submit'; + } } - /** - * Focuses the TextInput - */ - focus: () => void = (): void => { - // $FlowFixMe - // Win32 - TextInputState.setFocusedTextInput(this); - UIManager.dispatchViewManagerCommand( - findNodeHandle(this), - TextInputViewManager.Commands.focus, - null, - ); - }; + const accessible = props.accessible !== false; + + const accessibilityErrorMessage = + props.accessibilityInvalid === true + ? props.accessibilityErrorMessage + : null; + + const focusable = props.focusable !== false; + + const config = React.useMemo( + () => ({ + hitSlop: props.hitSlop, + onPress: (event: PressEvent) => { + if (props.editable !== false) { + if (inputRef.current != null) { + inputRef.current.focus(); + } + } + }, + onPressIn: props.onPressIn, + onPressOut: props.onPressOut, + cancelable: + Platform.OS === 'ios' ? !props.rejectResponderTermination : null, + }), + [ + props.editable, + props.hitSlop, + props.onPressIn, + props.onPressOut, + props.rejectResponderTermination, + ], + ); + + // Hide caret during test runs due to a flashing caret + // makes screenshot tests flakey + let caretHidden = props.caretHidden; + if (Platform.isTesting) { + caretHidden = true; + } - /** - * Blurs the TextInput - */ - blur: () => void = (): void => { - // $FlowFixMe - // Win32 - TextInputState.blurTextInput(this); - UIManager.dispatchViewManagerCommand( - findNodeHandle(this), - TextInputViewManager.Commands.blur, - null, - ); + // TextInput handles onBlur and onFocus events + // so omitting onBlur and onFocus pressability handlers here. + const {onBlur, onFocus, ...eventHandlers} = usePressability(config) || {}; + const eventPhase = Object.freeze({Capturing: 1, Bubbling: 3}); + const _keyDown = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + // $FlowFixMe - keyDownEvents was already checked to not be undefined + for (const el of props.keyDownEvents) { + if ( + event.nativeEvent.code == el.code && + el.handledEventPhase == eventPhase.Bubbling + ) { + event.stopPropagation(); + } + } + } + props.onKeyDown && props.onKeyDown(event); }; - /** - * Use clear for programmatic clearing of the text - */ - clear: () => void = (): void => { - this.setNativeText(''); + const _keyUp = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + // $FlowFixMe - keyDownEvents was already checked to not be undefined + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 3) { + event.stopPropagation(); + } + } + } + props.onKeyUp && props.onKeyUp(event); }; - setEventCount: () => void = (): void => { - UIManager.dispatchViewManagerCommand( - findNodeHandle(this), - TextInputViewManager.Commands.setEventCount, - [this._eventCount], - ); + const _keyDownCapture = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + // $FlowFixMe - keyDownEvents was already checked to not be undefined + for (const el of props.keyDownEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } + } + props.onKeyDownCapture && props.onKeyDownCapture(event); }; - setNativeText: (val: ?Stringish) => void = (val: ?Stringish): void => { - if (this._lastNativeText !== val) { - UIManager.dispatchViewManagerCommand( - findNodeHandle(this), - TextInputViewManager.Commands.setNativeText, - [val], - ); + const _keyUpCapture = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + // $FlowFixMe - keyDownEvents was already checked to not be undefined + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } } + props.onKeyUpCapture && props.onKeyUpCapture(event); }; - _getText = (): string | null => { - if (this.props.value != null && this.props.value != undefined) { - return this.props.value; - } - if ( - this.props.defaultValue != null && - this.props.defaultValue != undefined - ) { - return this.props.defaultValue; + let _accessibilityState; + if ( + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaMultiselectable != null || + ariaRequired != null || + ariaSelected != null + ) { + _accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + multiselectable: + ariaMultiselectable ?? accessibilityState?.multiselectable, // Win32 + required: ariaRequired ?? accessibilityState?.required, // Win32 + selected: ariaSelected ?? accessibilityState?.selected, + }; + } + + // $FlowFixMe[underconstrained-implicit-instantiation] + let style = flattenStyle(props.style); + + if (Platform.OS === 'ios') { + const RCTTextInputView = + props.multiline === true + ? RCTMultilineTextInputView + : RCTSinglelineTextInputView; + + style = props.multiline === true ? [styles.multilineInput, style] : style; + + const useOnChangeSync = + (props.unstable_onChangeSync || props.unstable_onChangeTextSync) && + !(props.onChange || props.onChangeText); + + textInput = ( + + ); + } else if (Platform.OS === 'android') { + const autoCapitalize = props.autoCapitalize || 'sentences'; + const _accessibilityLabelledBy = + props?.['aria-labelledby'] ?? props?.accessibilityLabelledBy; + const placeholder = props.placeholder ?? ''; + let children = props.children; + const childCount = React.Children.count(children); + invariant( + !(props.value != null && childCount), + 'Cannot specify both value and children.', + ); + if (childCount > 1) { + children = {children}; } - return null; - }; - _onChange = (e: ChangeEvent): void => { - const text = e.nativeEvent.text; - this._eventCount = e.nativeEvent.eventCount; - this.setEventCount(); + textInput = ( + /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up + * exactly with the props for TextInput. This will need to get fixed */ + /* $FlowFixMe[incompatible-type] the types for AndroidTextInput don't + * match up exactly with the props for TextInput. This will need to get + * fixed */ + /* $FlowFixMe[incompatible-type-arg] the types for AndroidTextInput don't + * match up exactly with the props for TextInput. This will need to get + * fixed */ + + ); + } // [Windows + else if (Platform.OS === 'win32') { + textInput = ( + + ); + } // Windows] + return ( + {textInput} + ); +} - this.props.onChange && this.props.onChange(e); - this.props.onChangeText && this.props.onChangeText(text); - this._lastNativeText = text; +const enterKeyHintToReturnTypeMap = { + enter: 'default', + done: 'done', + go: 'go', + next: 'next', + previous: 'previous', + search: 'search', + send: 'send', +}; - this.forceUpdate(); - return; - }; +const inputModeToKeyboardTypeMap = { + none: 'default', + text: 'default', + decimal: 'decimal-pad', + numeric: 'number-pad', + tel: 'phone-pad', + search: Platform.OS === 'ios' ? 'web-search' : 'default', + email: 'email-address', + url: 'url', +}; - _onFocus = (e: FocusEvent): void => { - this.focus(); - this.props.onFocus && this.props.onFocus(e); - }; +// Map HTML autocomplete values to Android autoComplete values +const autoCompleteWebToAutoCompleteAndroidMap = { + 'address-line1': 'postal-address-region', + 'address-line2': 'postal-address-locality', + bday: 'birthdate-full', + 'bday-day': 'birthdate-day', + 'bday-month': 'birthdate-month', + 'bday-year': 'birthdate-year', + 'cc-csc': 'cc-csc', + 'cc-exp': 'cc-exp', + 'cc-exp-month': 'cc-exp-month', + 'cc-exp-year': 'cc-exp-year', + 'cc-number': 'cc-number', + country: 'postal-address-country', + 'current-password': 'password', + email: 'email', + 'honorific-prefix': 'name-prefix', + 'honorific-suffix': 'name-suffix', + name: 'name', + 'additional-name': 'name-middle', + 'family-name': 'name-family', + 'given-name': 'name-given', + 'new-password': 'password-new', + off: 'off', + 'one-time-code': 'sms-otp', + 'postal-code': 'postal-code', + sex: 'gender', + 'street-address': 'street-address', + tel: 'tel', + 'tel-country-code': 'tel-country-code', + 'tel-national': 'tel-national', + username: 'username', +}; - _onBlur = (e: BlurEvent): void => { - this.props.onBlur && this.props.onBlur(e); - }; -} +// Map HTML autocomplete values to iOS textContentType values +const autoCompleteWebToTextContentTypeMap = { + 'address-line1': 'streetAddressLine1', + 'address-line2': 'streetAddressLine2', + bday: 'birthdate', + 'bday-day': 'birthdateDay', + 'bday-month': 'birthdateMonth', + 'bday-year': 'birthdateYear', + 'cc-csc': 'creditCardSecurityCode', + 'cc-exp-month': 'creditCardExpirationMonth', + 'cc-exp-year': 'creditCardExpirationYear', + 'cc-exp': 'creditCardExpiration', + 'cc-given-name': 'creditCardGivenName', + 'cc-additional-name': 'creditCardMiddleName', + 'cc-family-name': 'creditCardFamilyName', + 'cc-name': 'creditCardName', + 'cc-number': 'creditCardNumber', + 'cc-type': 'creditCardType', + 'current-password': 'password', + country: 'countryName', + email: 'emailAddress', + name: 'name', + 'additional-name': 'middleName', + 'family-name': 'familyName', + 'given-name': 'givenName', + nickname: 'nickname', + 'honorific-prefix': 'namePrefix', + 'honorific-suffix': 'nameSuffix', + 'new-password': 'newPassword', + off: 'none', + 'one-time-code': 'oneTimeCode', + organization: 'organizationName', + 'organization-title': 'jobTitle', + 'postal-code': 'postalCode', + 'street-address': 'fullStreetAddress', + tel: 'telephoneNumber', + url: 'URL', + username: 'username', +}; + +const ExportedForwardRef: React.AbstractComponent< + React.ElementConfig, + TextInputInstance, + // $FlowFixMe[incompatible-call] +> = React.forwardRef(function TextInput( + { + allowFontScaling = true, + rejectResponderTermination = true, + underlineColorAndroid = 'transparent', + autoComplete, + textContentType, + readOnly, + editable, + enterKeyHint, + returnKeyType, + inputMode, + showSoftInputOnFocus, + keyboardType, + ...restProps + }, + forwardedRef: ReactRefSetter, +) { + // $FlowFixMe[underconstrained-implicit-instantiation] + let style = flattenStyle(restProps.style); + + if (style?.verticalAlign != null) { + // $FlowFixMe[prop-missing] + // $FlowFixMe[cannot-write] + style.textAlignVertical = + // $FlowFixMe[invalid-computed-prop] + verticalAlignToTextAlignVerticalMap[style.verticalAlign]; + // $FlowFixMe[prop-missing] + // $FlowFixMe[cannot-write] + delete style.verticalAlign; + } + + return ( + + ); +}); + +ExportedForwardRef.displayName = 'TextInput'; + +/** + * Switch to `deprecated-react-native-prop-types` for compatibility with future + * releases. This is deprecated and will be removed in the future. + */ +ExportedForwardRef.propTypes = + require('deprecated-react-native-prop-types').TextInputPropTypes; + +// $FlowFixMe[prop-missing] +ExportedForwardRef.State = { + currentlyFocusedInput: TextInputState.currentlyFocusedInput, + + currentlyFocusedField: TextInputState.currentlyFocusedField, + focusTextInput: TextInputState.focusTextInput, + blurTextInput: TextInputState.blurTextInput, +}; + +export type TextInputComponentStatics = $ReadOnly<{| + State: $ReadOnly<{| + currentlyFocusedInput: typeof TextInputState.currentlyFocusedInput, + currentlyFocusedField: typeof TextInputState.currentlyFocusedField, + focusTextInput: typeof TextInputState.focusTextInput, + blurTextInput: typeof TextInputState.blurTextInput, + |}>, +|}>; + +const styles = StyleSheet.create({ + multilineInput: { + // This default top inset makes RCTMultilineTextInputView seem as close as possible + // to single-line RCTSinglelineTextInputView defaults, using the system defaults + // of font size 17 and a height of 31 points. + paddingTop: 5, + }, +}); + +const verticalAlignToTextAlignVerticalMap = { + auto: 'auto', + top: 'top', + bottom: 'bottom', + middle: 'center', +}; -module.exports = TextInput; +// $FlowFixMe[unclear-type] Unclear type. Using `any` type is not safe. +module.exports = ((ExportedForwardRef: any): TextInputType); diff --git a/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInputState.win32.js b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInputState.win32.js index 73e68522f2d..79373c4b650 100644 --- a/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInputState.win32.js +++ b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/TextInputState.win32.js @@ -21,8 +21,7 @@ import type { import {Commands as AndroidTextInputCommands} from '../../Components/TextInput/AndroidTextInputNativeComponent'; import {Commands as iOSTextInputCommands} from '../../Components/TextInput/RCTSingelineTextInputNativeComponent'; - -import {UIManager} from 'react-native'; +import {Commands as Win32TextInputCommands} from '../../Components/TextInput/Win32TextInputNativeComponent'; const {findNodeHandle} = require('../../ReactNative/RendererProxy'); const Platform = require('../../Utilities/Platform'); @@ -106,7 +105,16 @@ function focusTextInput(textField: ?ComponentRef) { return; } - if (textField != null) { + // [Win32 + if (Platform.OS === 'win32' && textField != null) { + // On Windows, we cannot test if the currentlyFocusedInputRef equals the + // target ref because the call to focus on the target ref may occur before + // an onBlur event for the target ref has been dispatched to JS but after + // the target ref has lost native focus. + focusInput(textField); + Win32TextInputCommands.focus(textField); + // Win32] + } else if (textField != null) { const fieldCanBeFocused = currentlyFocusedInputRef !== textField && // $FlowFixMe - `currentProps` is missing in `NativeMethods` @@ -126,11 +134,6 @@ function focusTextInput(textField: ?ComponentRef) { } else if (Platform.OS === 'android') { AndroidTextInputCommands.focus(textField); } - // [Win32 - else if (Platform.OS === 'win32') { - UIManager.focus(findNodeHandle(textField)); - } - // Win32] } } @@ -164,7 +167,7 @@ function blurTextInput(textField: ?ComponentRef) { } // [Win32 else if (Platform.OS === 'win32') { - UIManager.blur(findNodeHandle(textField)); + Win32TextInputCommands.blur(textField); } // Win32] } diff --git a/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/Win32TextInputNativeComponent.js b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/Win32TextInputNativeComponent.js new file mode 100644 index 00000000000..8966c333b26 --- /dev/null +++ b/packages/@office-iss/react-native-win32/src/Libraries/Components/TextInput/Win32TextInputNativeComponent.js @@ -0,0 +1,23 @@ +/** + * @flow + * @format + */ + +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; + +import requireNativeComponent from '../../ReactNative/requireNativeComponent'; +import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; +import type {TextInputNativeCommands} from './TextInputNativeCommands'; +type NativeType = HostComponent; + +type NativeCommands = TextInputNativeCommands; + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['focus', 'blur', 'setTextAndSelection'], +}); + +const WindowsTextInputComponent: NativeType = + requireNativeComponent('RCTTextInput'); + +export default WindowsTextInputComponent; +// [Windows] diff --git a/packages/react-native-windows-init/src/Cli.ts b/packages/react-native-windows-init/src/Cli.ts index 06b376c605e..b00a4d798ec 100644 --- a/packages/react-native-windows-init/src/Cli.ts +++ b/packages/react-native-windows-init/src/Cli.ts @@ -39,10 +39,19 @@ import { import requireGenerateWindows from './requireGenerateWindows'; -const npmConfReg = execSync('npm config get registry').toString().trim(); -const NPM_REGISTRY_URL = validUrl.isUri(npmConfReg) - ? npmConfReg - : 'http://registry.npmjs.org'; +let NPM_REGISTRY_URL = 'http://registry.npmjs.org'; +try { + const npmConfReg = execSync('npm config get registry').toString().trim(); + if (validUrl.isUri(npmConfReg)) { + NPM_REGISTRY_URL = npmConfReg; + } +} catch (error: any) { + // Ignore workspace errors as `npm config` does not support it + const stderr = error?.stderr?.toString() || ''; + if (!stderr.includes('ENOWORKSPACES')) { + throw error; + } +} // Causes the type-checker to ensure the options object is a valid yargs options object function initOptions>(options: T): T { diff --git a/vnext/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj b/vnext/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj index 3e1a1fee543..47a02c0f43f 100644 --- a/vnext/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj +++ b/vnext/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj @@ -11,7 +11,7 @@ x64 win-x64 - + true enable 8.0