Skip to content

Commit

Permalink
Add script to output flag values (#28115)
Browse files Browse the repository at this point in the history
## Overview

Depends on: #28116

Add `yarn flags` to output at table of all feature flags. 

Provides options to output a csv file, diff two or more builds, and
sort.

### Options
<img width="1154" alt="Screenshot 2024-01-26 at 4 06 53 PM"
src="https://github.com/facebook/react/assets/2440089/c3dbd632-adb9-4416-9488-1c603ee4e789">

### `yarn flags --diff next canary`
<img width="637" alt="Screenshot 2024-01-26 at 4 15 03 PM"
src="https://github.com/facebook/react/assets/2440089/1a681ae8-ce33-42d0-9d1f-3f415a8e1c3d">


### `yarn flags --diff canary experimental`
<img width="637" alt="Screenshot 2024-01-26 at 4 14 51 PM"
src="https://github.com/facebook/react/assets/2440089/c66f66cb-3cee-4df6-a1d1-b24600ebd4b3">


### `yarn flags` (all flags)

<img width="1054" alt="Screenshot 2024-01-26 at 4 16 30 PM"
src="https://github.com/facebook/react/assets/2440089/4ce99c7c-825e-4bca-9b83-ca5d6e2bc1a9">
  • Loading branch information
rickhanlonii authored Jan 31, 2024
1 parent f3ce87a commit 45582c6
Show file tree
Hide file tree
Showing 2 changed files with 354 additions and 1 deletion.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
"download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build --type=node react/index react-dom/index react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime",
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js"
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
"flags": "node ./scripts/flags/flags.js"
},
"resolutions": {
"react-is": "npm:react-is"
Expand Down
352 changes: 352 additions & 0 deletions scripts/flags/flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
'use strict';

const babel = require('@babel/register');
const {transformSync} = require('@babel/core');
const Module = require('module');
const path = require('path');
const fs = require('fs');
babel({
plugins: ['@babel/plugin-transform-modules-commonjs'],
});

const yargs = require('yargs');
const argv = yargs
.parserConfiguration({
// Important: This option tells yargs to move all other options not
// specified here into the `_` key. We use this to send all of the
// Jest options that we don't use through to Jest (like --watch).
'unknown-options-as-args': true,
})
.wrap(yargs.terminalWidth())
.options({
csv: {
alias: 'c',
describe: 'output cvs.',
requiresArg: false,
type: 'boolean',
default: false,
},
diff: {
alias: 'd',
describe: 'output diff of two or more flags.',
requiresArg: false,
type: 'array',
choices: [
'www',
'www-modern',
'rn',
'rn-fb',
'canary',
'next',
'experimental',
null,
],
default: null,
},
sort: {
alias: 's',
describe: 'sort diff by one or more flags.',
requiresArg: false,
type: 'string',
default: 'flag',
choices: [
'flag',
'www',
'www-modern',
'rn',
'rn-fb',
'canary',
'next',
'experimental',
],
},
}).argv;

// Load ReactNativeFeatureFlags with __NEXT_MAJOR__ replace with 'next'.
// We need to do string replace, since the __NEXT_MAJOR__ is assigned to __EXPERIMENTAL__.
function getReactNativeFeatureFlagsMajor() {
const virtualName = 'ReactNativeFeatureFlagsMajor.js';
const file = fs.readFileSync(
path.join(__dirname, '../../packages/shared/ReactFeatureFlags.js'),
'utf8'
);
const fileContent = transformSync(
file.replace(
'const __NEXT_MAJOR__ = __EXPERIMENTAL__;',
'const __NEXT_MAJOR__ = "next";'
),
{
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() {
// Mock the ReactNativeInternalFeatureFlags and ReactFeatureFlags modules
const DynamicFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www-dynamic.js');
const DynamicFeatureFlagsNative = require('../../packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js');

const originalLoad = Module._load;

Module._load = function (request, parent) {
if (request === 'ReactNativeInternalFeatureFlags') {
return DynamicFeatureFlagsNative;
} else if (request === 'ReactFeatureFlags') {
return DynamicFeatureFlagsWWW;
}

return originalLoad.apply(this, arguments);
};
}
// Set the globals to string values to output them to the table.
global.__VARIANT__ = 'gk';
global.__PROFILE__ = 'profile';
global.__DEV__ = 'dev';
global.__EXPERIMENTAL__ = 'experimental';

// Load all the feature flag files.
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 allFlagsUniqueFlags = Array.from(
new Set([
...Object.keys(ReactFeatureFlags),
...Object.keys(ReactFeatureFlagsWWW),
...Object.keys(ReactFeatureFlagsNativeFB),
...Object.keys(ReactFeatureFlagsNativeOSS),
])
).sort();

// These functions are the rules for what each value means in each channel.
function getNextMajorFlagValue(flag) {
const value = ReactFeatureFlagsMajor[flag];
if (value === true || value === 'next') {
return '✅';
} else if (value === false || value === 'experimental') {
return '❌';
} else if (value === 'profile') {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
return value;
} else {
throw new Error(`Unexpected OSS Stable value ${value} for flag ${flag}`);
}
}

function getOSSCanaryFlagValue(flag) {
const value = ReactFeatureFlags[flag];
if (value === true) {
return '✅';
} else if (value === false || value === 'experimental' || value === 'next') {
return '❌';
} else if (value === 'profile') {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
return value;
} else {
throw new Error(`Unexpected OSS Canary value ${value} for flag ${flag}`);
}
}

function getOSSExperimentalFlagValue(flag) {
const value = ReactFeatureFlags[flag];
if (value === true || value === 'experimental') {
return '✅';
} else if (value === false || value === 'next') {
return '❌';
} else if (value === 'profile') {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
return value;
} else {
throw new Error(
`Unexpected OSS Experimental value ${value} for flag ${flag}`
);
}
}

function getWWWModernFlagValue(flag) {
const value = ReactFeatureFlagsWWW[flag];
if (value === true || value === 'experimental') {
return '✅';
} else if (value === false || value === 'next') {
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 WWW Modern value ${value} for flag ${flag}`);
}
}

function getWWWClassicFlagValue(flag) {
const value = ReactFeatureFlagsWWW[flag];
if (value === true) {
return '✅';
} else if (value === false || value === 'experimental' || value === 'next') {
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 WWW Classic value ${value} for flag ${flag}`);
}
}

function getRNOSSFlagValue(flag) {
const value = ReactFeatureFlagsNativeOSS[flag];
if (value === true) {
return '✅';
} else if (value === false || value === 'experimental' || value === 'next') {
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 getRNFBFlagValue(flag) {
const value = ReactFeatureFlagsNativeFB[flag];
if (value === true) {
return '✅';
} else if (value === false || value === 'experimental' || value === 'next') {
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 FB value ${value} for flag ${flag}`);
}
}

function argToHeader(arg) {
switch (arg) {
case 'www':
return 'WWW Classic';
case 'www-modern':
return 'WWW Modern';
case 'rn':
return 'RN OSS';
case 'rn-fb':
return 'RN FB';
case 'canary':
return 'OSS Canary';
case 'next':
return 'OSS Next Major';
case 'experimental':
return 'OSS Experimental';
default:
return arg;
}
}

// 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),
};

if (!isDiff) {
table[flag] = values;
continue;
}

const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
if (key in values) {
acc[key] = values[key];
}
return acc;
}, {});

if (new Set(Object.values(subset)).size !== 1) {
table[flag] = subset;
}
}

// Sort the table
let sorted = table;
if (isDiff || argv.sort) {
const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
sorted = Object.fromEntries(
Object.entries(table).sort(([, rowA], [, rowB]) =>
rowB[sortChannel].toString().localeCompare(rowA[sortChannel])
)
);
}

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) {
if (err) {
return console.log(err);
}
console.log('The file was saved!');
});
}

// left align the flag names.
const maxLength = Math.max(...Object.keys(sorted).map(item => item.length));
const padded = {};
Object.keys(sorted).forEach(key => {
const newKey = key.padEnd(maxLength, ' ');
padded[newKey] = sorted[key];
});

// print table with formatting
console.table(padded);

0 comments on commit 45582c6

Please sign in to comment.