mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Add script to output flag values (#28115)
## Overview Depends on: https://github.com/facebook/react/pull/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">
This commit is contained in:
@@ -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"
|
||||
|
||||
352
scripts/flags/flags.js
Normal file
352
scripts/flags/flags.js
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user