Repository: facebook/react
Pull request: Better NEXT_MAJOR support for RN flags

The `__NEXT_MAJOR__` value in the RN flags doesn't make sense because:

a) The flags are for the next RN major, since it only impacts the renderers
b) The flags are off, so they're not currently in the next major, they need enabled
c) the flag script didn't support it

This PR adds two aliases to the RN file:
- `__TODO_NEXT_RN_MAJOR__`: flags that need enabled before the next RN major.
- `__NEXT_RN_MAJOR__`: flags that have been enabled since the last RN major. 

These values will need to be manually kept up to date when we cut a RN version, but once RN switches to the canary build and aligns all the flags, this entire file can be deleted.

## Script screen
Notably, I added a TODO value and a legend that prints at the end of the script:

<img width="1078" alt="Screenshot 2024-03-18 at 8 11 27 PM" src="https://github.com/facebook/react/assets/2440089/14da9066-f77d-437f-8188-830a31a843c5">


Changed files:
filename: packages/shared/forks/ReactFeatureFlags.native-oss.js
patch: @@ -10,98 +10,109 @@
 import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
 import typeof * as ExportsType from './ReactFeatureFlags.native-oss';
 
+// TODO: Align these flags with canary and delete this file once RN ships from Canary.
+
 // -----------------------------------------------------------------------------
-// Ready for next major.
+// TODO for next React Native major.
 //
-// Alias __NEXT_MAJOR__ to false for easier skimming.
+// Alias __TODO_NEXT_RN_MAJOR__ to false for easier skimming.
 // -----------------------------------------------------------------------------
-const __NEXT_MAJOR__ = false;
+const __TODO_NEXT_RN_MAJOR__ = false;
+export const enableRefAsProp = __TODO_NEXT_RN_MAJOR__;
+export const disableStringRefs = __TODO_NEXT_RN_MAJOR__;
+export const disableLegacyMode = __TODO_NEXT_RN_MAJOR__;
+export const enableBigIntSupport = __TODO_NEXT_RN_MAJOR__;
+export const useModernStrictMode = __TODO_NEXT_RN_MAJOR__;
+export const enableReactTestRendererWarning = __TODO_NEXT_RN_MAJOR__;
+export const enableAsyncActions = __TODO_NEXT_RN_MAJOR__;
+export const consoleManagedByDevToolsDuringStrictMode = __TODO_NEXT_RN_MAJOR__;
+export const enableDeferRootSchedulingToMicrotask = __TODO_NEXT_RN_MAJOR__;
+export const alwaysThrottleDisappearingFallbacks = __TODO_NEXT_RN_MAJOR__;
+export const alwaysThrottleRetries = __TODO_NEXT_RN_MAJOR__;
+export const enableInfiniteRenderLoopDetection = __TODO_NEXT_RN_MAJOR__;
+export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__;
+export const disableModulePatternComponents = __TODO_NEXT_RN_MAJOR__;
 
+// -----------------------------------------------------------------------------
+// These are ready to flip after the next React npm release (or RN switches to
+// Canary, but can't flip before then because of react/renderer mismatches.
+// -----------------------------------------------------------------------------
+export const enableCache = __TODO_NEXT_RN_MAJOR__;
+export const enableRenderableContext = __TODO_NEXT_RN_MAJOR__;
+
+// -----------------------------------------------------------------------------
+// Already enabled for next React Native major.
+// Hardcode these to true after the next RN major.
+//
+// Alias __NEXT_RN_MAJOR__ to true for easier skimming.
+// -----------------------------------------------------------------------------
+const __NEXT_RN_MAJOR__ = true;
+export const disableClientCache = __NEXT_RN_MAJOR__;
+export const disableLegacyContext = __NEXT_RN_MAJOR__;
+export const enableCacheElement = __NEXT_RN_MAJOR__;
+export const enableTaint = __NEXT_RN_MAJOR__;
+export const enableUnifiedSyncLane = __NEXT_RN_MAJOR__;
+export const enableFizzExternalRuntime = __NEXT_RN_MAJOR__; // DOM-only
+export const disableJavaScriptURLs = __NEXT_RN_MAJOR__; // DOM-only
+export const enableFormActions = __NEXT_RN_MAJOR__; // DOM-only
+export const enableBinaryFlight = __NEXT_RN_MAJOR__; // DOM-only
+export const enableCustomElementPropertySupport = __NEXT_RN_MAJOR__; // DOM-only
+export const enableServerComponentKeys = __NEXT_RN_MAJOR__;
+export const enableServerComponentLogs = __NEXT_RN_MAJOR__;
+
+// DEV-only but enabled in the next RN Major.
+// Not supported by flag script to avoid the special case.
 export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
+
+// TODO: decide on React 19
+export const enableUseMemoCacheHook = false;
+export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
+
+// -----------------------------------------------------------------------------
+// All other flags
+// -----------------------------------------------------------------------------
+export const enableCPUSuspense = false;
 export const enableDebugTracing = false;
 export const enableAsyncDebugInfo = false;
 export const enableSchedulingProfiler = false;
-export const enableProfilerTimer = __PROFILE__;
-export const enableProfilerCommitHooks = __PROFILE__;
-export const enableProfilerNestedUpdatePhase = __PROFILE__;
-export const enableUpdaterTracking = __PROFILE__;
-export const enableCache = __NEXT_MAJOR__;
 export const enableLegacyCache = false;
-export const enableCacheElement = true;
 export const enableFetchInstrumentation = false;
-export const enableFormActions = true; // Doesn't affect Native
-export const enableBinaryFlight = true;
-export const enableTaint = true;
 export const enablePostpone = false;
-export const disableJavaScriptURLs = true;
 export const disableCommentsAsDOMContainers = true;
 export const disableInputAttributeSyncing = false;
 export const disableIEWorkarounds = true;
 export const enableScopeAPI = false;
 export const enableCreateEventHandleAPI = false;
 export const enableSuspenseCallback = false;
-export const disableLegacyContext = true;
 export const enableTrustedTypesIntegration = false;
 export const disableTextareaChildren = false;
-export const disableModulePatternComponents = false;
 export const enableSuspenseAvoidThisFallback = false;
 export const enableSuspenseAvoidThisFallbackFizz = false;
-export const enableCPUSuspense = false;
-export const enableUseMemoCacheHook = false;
 export const enableUseEffectEventHook = false;
 export const enableClientRenderFallbackOnTextMismatch = true;
-export const enableComponentStackLocations = false;
 export const enableLegacyFBSupport = false;
 export const enableFilterEmptyStringAttributesDOM = true;
 export const enableGetInspectorDataForInstanceInProduction = false;
-export const enableRenderableContext = false;
-
 export const enableRetryLaneExpiration = false;
 export const retryLaneExpirationMs = 5000;
 export const syncLaneExpirationMs = 250;
 export const transitionLaneExpirationMs = 5000;
-
 export const enableUseRefAccessWarning = false;
-
 export const disableSchedulerTimeoutInWorkLoop = false;
 export const enableLazyContextPropagation = false;
 export const enableLegacyHidden = false;
 export const forceConcurrentByDefaultForTesting = false;
-export const enableUnifiedSyncLane = true;
 export const allowConcurrentByDefault = false;
-export const enableCustomElementPropertySupport = true;
 export const enableNewBooleanProps = true;
-
-export const consoleManagedByDevToolsDuringStrictMode = false;
-
 export const enableTransitionTracing = false;
-
-export const useModernStrictMode = false;
 export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
-export const enableFizzExternalRuntime = true;
-export const enableDeferRootSchedulingToMicrotask = false;
-export const enableInfiniteRenderLoopDetection = false;
-
-export const enableAsyncActions = false;
-
-export const alwaysThrottleDisappearingFallbacks = false;
-export const alwaysThrottleRetries = false;
-
 export const passChildrenWhenCloningPersistedNodes = false;
-export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
-export const disableClientCache = true;
-
-export const enableServerComponentKeys = true;
-export const enableServerComponentLogs = true;
-
-// TODO: Should turn this on in next "major" RN release.
-export const enableRefAsProp = false;
-export const disableStringRefs = false;
-
-export const enableReactTestRendererWarning = false;
 
-export const enableBigIntSupport = false;
-export const disableLegacyMode = false;
+// Profiling Only
+export const enableProfilerTimer = __PROFILE__;
+export const enableProfilerCommitHooks = __PROFILE__;
+export const enableProfilerNestedUpdatePhase = __PROFILE__;
+export const enableUpdaterTracking = __PROFILE__;
 
 // Flow magic to verify the exports of this file match the original version.
 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);

filename: scripts/flags/flags.js
patch: @@ -36,6 +36,7 @@ const argv = yargs
         'www-modern',
         'rn',
         'rn-fb',
+        'rn-next',
         'canary',
         'next',
         'experimental',
@@ -55,17 +56,18 @@ const argv = yargs
         'www-modern',
         'rn',
         'rn-fb',
+        'rn-next',
         'canary',
         'next',
         'experimental',
       ],
     },
   }).argv;
 
-// Load ReactNativeFeatureFlags with __NEXT_MAJOR__ replace with 'next'.
+// Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'.
 // We need to do string replace, since the __NEXT_MAJOR__ is assigned to __EXPERIMENTAL__.
-function getReactNativeFeatureFlagsMajor() {
-  const virtualName = 'ReactNativeFeatureFlagsMajor.js';
+function getReactFeatureFlagsMajor() {
+  const virtualName = 'ReactFeatureFlagsMajor.js';
   const file = fs.readFileSync(
     path.join(__dirname, '../../packages/shared/ReactFeatureFlags.js'),
     'utf8'
@@ -89,6 +91,41 @@ function getReactNativeFeatureFlagsMajor() {
   return m.exports;
 }
 
+// Load RN ReactFeatureFlags with __NEXT_RN_MAJOR__ replaced with 'next'.
+// We need to do string replace, since the __NEXT_RN_MAJOR__ is assigned to false.
+function getReactNativeFeatureFlagsMajor() {
+  const virtualName = 'ReactNativeFeatureFlagsMajor.js';
+  const file = fs.readFileSync(
+    path.join(
+      __dirname,
+      '../../packages/shared/forks/ReactFeatureFlags.native-oss.js'
+    ),
+    'utf8'
+  );
+  const fileContent = transformSync(
+    file
+      .replace(
+        'const __NEXT_RN_MAJOR__ = true;',
+        'const __NEXT_RN_MAJOR__ = "next";'
+      )
+      .replace(
+        'const __TODO_NEXT_RN_MAJOR__ = false;',
+        'const __TODO_NEXT_RN_MAJOR__ = "next-todo";'
+      ),
+    {
+      plugins: ['@babel/plugin-transform-modules-commonjs'],
+    }
+  ).code;
+
+  const parent = module.parent;
+  const m = new Module(virtualName, parent);
+  m.filename = virtualName;
+
+  m._compile(fileContent, virtualName);
+
+  return m.exports;
+}
+
 // The RN and www Feature flag files import files that don't exist.
 // Mock the imports with the dynamic flag values.
 function mockDynamicallyFeatureFlags() {
@@ -119,15 +156,14 @@ mockDynamicallyFeatureFlags();
 const ReactFeatureFlags = require('../../packages/shared/ReactFeatureFlags.js');
 const ReactFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www.js');
 const ReactFeatureFlagsNativeFB = require('../../packages/shared/forks/ReactFeatureFlags.native-fb.js');
-const ReactFeatureFlagsNativeOSS = require('../../packages/shared/forks/ReactFeatureFlags.native-oss.js');
-const ReactFeatureFlagsMajor = getReactNativeFeatureFlagsMajor();
+const ReactFeatureFlagsMajor = getReactFeatureFlagsMajor();
+const ReactNativeFeatureFlagsMajor = getReactNativeFeatureFlagsMajor();
 
 const allFlagsUniqueFlags = Array.from(
   new Set([
     ...Object.keys(ReactFeatureFlags),
     ...Object.keys(ReactFeatureFlagsWWW),
     ...Object.keys(ReactFeatureFlagsNativeFB),
-    ...Object.keys(ReactFeatureFlagsNativeOSS),
   ])
 ).sort();
 
@@ -223,11 +259,37 @@ function getWWWClassicFlagValue(flag) {
   }
 }
 
+function getRNNextMajorFlagValue(flag) {
+  const value = ReactNativeFeatureFlagsMajor[flag];
+  if (value === true || value === 'next') {
+    return '✅';
+  } else if (value === 'next-todo') {
+    return '📋';
+  } else if (value === false || value === 'experimental') {
+    return '❌';
+  } else if (value === 'profile') {
+    return '📊';
+  } else if (value === 'dev') {
+    return '💻';
+  } else if (value === 'gk') {
+    return '🧪';
+  } else if (typeof value === 'number') {
+    return value;
+  } else {
+    throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
+  }
+}
+
 function getRNOSSFlagValue(flag) {
-  const value = ReactFeatureFlagsNativeOSS[flag];
+  const value = ReactNativeFeatureFlagsMajor[flag];
   if (value === true) {
     return '✅';
-  } else if (value === false || value === 'experimental' || value === 'next') {
+  } else if (
+    value === false ||
+    value === 'experimental' ||
+    value === 'next' ||
+    value === 'next-todo'
+  ) {
     return '❌';
   } else if (value === 'profile') {
     return '📊';
@@ -271,6 +333,8 @@ function argToHeader(arg) {
       return 'RN OSS';
     case 'rn-fb':
       return 'RN FB';
+    case 'rn-next':
+      return 'RN Next Major';
     case 'canary':
       return 'OSS Canary';
     case 'next':
@@ -282,20 +346,28 @@ function argToHeader(arg) {
   }
 }
 
+const FLAG_CONFIG = {
+  'OSS Next Major': getNextMajorFlagValue,
+  'OSS Canary': getOSSCanaryFlagValue,
+  'OSS Experimental': getOSSExperimentalFlagValue,
+  'WWW Classic': getWWWClassicFlagValue,
+  'WWW Modern': getWWWModernFlagValue,
+  'RN FB': getRNFBFlagValue,
+  'RN OSS': getRNOSSFlagValue,
+  'RN Next Major': getRNNextMajorFlagValue,
+};
+
+const FLAG_COLUMNS = Object.keys(FLAG_CONFIG);
+
 // Build the table with the value for each flag.
 const isDiff = argv.diff != null && argv.diff.length > 1;
 const table = {};
 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
 for (const flag of allFlagsUniqueFlags) {
-  const values = {
-    'OSS Next Major': getNextMajorFlagValue(flag),
-    'OSS Canary': getOSSCanaryFlagValue(flag),
-    'OSS Experimental': getOSSExperimentalFlagValue(flag),
-    'WWW Classic': getWWWClassicFlagValue(flag),
-    'WWW Modern': getWWWModernFlagValue(flag),
-    'RN FB': getRNFBFlagValue(flag),
-    'RN OSS': getRNOSSFlagValue(flag),
-  };
+  const values = FLAG_COLUMNS.reduce((acc, key) => {
+    acc[key] = FLAG_CONFIG[key](flag);
+    return acc;
+  }, {});
 
   if (!isDiff) {
     table[flag] = values;
@@ -330,17 +402,18 @@ if (isDiff || argv.sort) {
 }
 
 if (argv.csv) {
-  const csvHeader =
-    'Flag name, WWW Classic, RN FB, OSS Canary, OSS Experimental, WWW Modern, RN OSS\n';
-  const csvRows = Object.keys(sorted).map(flag => {
-    const row = sorted[flag];
-    return `${flag}, ${row['WWW Classic']}, ${row['RN FB']}, ${row['OSS Canary']}, ${row['OSS Experimental']}, ${row['WWW Modern']}, ${row['RN OSS']}`;
-  });
-  fs.writeFile('./flags.csv', csvHeader + csvRows.join('\n'), function (err) {
+  const csvRows = [
+    `Flag name, ${FLAG_COLUMNS.join(', ')}`,
+    ...Object.keys(table).map(flag => {
+      const row = sorted[flag];
+      return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`;
+    }),
+  ];
+  fs.writeFile('./flags.csv', csvRows.join('\n'), function (err) {
     if (err) {
       return console.log(err);
     }
-    console.log('The file was saved!');
+    console.log('The file was saved to ./flags.csv');
   });
 }
 
@@ -354,3 +427,12 @@ Object.keys(sorted).forEach(key => {
 
 // print table with formatting
 console.table(padded);
+console.log(`
+Legend:
+  ✅ On
+  ❌ Off
+  💻 DEV
+  📋 TODO
+  📊 Profiling
+  🧪 Experiment
+`);