'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', 'rn-next', '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', 'rn-next', 'canary', 'next', 'experimental', ], }, cleanup: { describe: 'output flags by cleanup category.', requiresArg: false, type: 'boolean', default: false, }, }).argv; // Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'. // We need to do string replace, since the __NEXT_MAJOR__ is assigned to __EXPERIMENTAL__. function getReactFeatureFlagsMajor() { const virtualName = 'ReactFeatureFlagsMajor.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; } // 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() { // 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 ReactFeatureFlagsMajor = getReactFeatureFlagsMajor(); const ReactNativeFeatureFlagsMajor = getReactNativeFeatureFlagsMajor(); const allFlagsUniqueFlags = Array.from( new Set([ ...Object.keys(ReactFeatureFlags), ...Object.keys(ReactFeatureFlagsWWW), ...Object.keys(ReactFeatureFlagsNativeFB), ]) ).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 === null || 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 === null || 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 === null || 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 === null || 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 === null || 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 getRNNextMajorFlagValue(flag) { const value = ReactNativeFeatureFlagsMajor[flag]; if (value === true || value === 'next') { return 'โœ…'; } else if (value === 'next-todo') { return '๐Ÿ“‹'; } else if (value === false || value === null || 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 = ReactNativeFeatureFlagsMajor[flag]; if (value === true) { return 'โœ…'; } else if ( value === false || value === null || value === 'experimental' || value === 'next' || value === 'next-todo' ) { 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 === null || 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 'rn-next': return 'RN Next Major'; case 'canary': return 'OSS Canary'; case 'next': return 'OSS Next Major'; case 'experimental': return 'OSS Experimental'; default: return 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); const INTERNAL_VARIANTS = ['WWW Classic', 'WWW Modern', 'RN FB']; const OSS_VARIANTS = [ 'OSS Next Major', 'OSS Canary', 'OSS Experimental', 'RN OSS', 'RN Next Major', ]; // Build the table with the value for each flag. function buildTable(filterFn) { const isDiff = argv.diff != null && argv.diff.length > 1; const table = {}; const filteredFlags = filterFn ? allFlagsUniqueFlags.filter(filterFn) : allFlagsUniqueFlags; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const flag of filteredFlags) { const values = FLAG_COLUMNS.reduce((acc, key) => { acc[key] = FLAG_CONFIG[key](flag); return acc; }, {}); 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); const sortBy = sortChannel === 'flag' ? ([flagA], [flagB]) => { return flagA.localeCompare(flagB); } : ([, rowA], [, rowB]) => { return rowB[sortChannel] .toString() .localeCompare(rowA[sortChannel]); }; sorted = Object.fromEntries(Object.entries(table).sort(sortBy)); } return sorted; } function formatTable(tableData) { // left align the flag names. const maxLength = Math.max( ...Object.keys(tableData).map(item => item.length) ); const padded = {}; Object.keys(tableData).forEach(key => { const newKey = key.padEnd(maxLength, ' '); padded[newKey] = tableData[key]; }); return padded; } if (argv.csv) { const table = buildTable(); const csvRows = [ `Flag name, ${FLAG_COLUMNS.join(', ')}`, ...Object.keys(table).map(flag => { const row = table[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 to ./flags.csv'); }); } if (argv.cleanup) { const allPassingFlags = []; const allFailingFlags = []; const needsShippedExperimentFlags = []; const earlyExperimentationFlags = []; const internalOnlyFlags = []; const diffedFlagColumns = argv.diff[0] != null ? argv.diff.map(argToHeader) : FLAG_COLUMNS; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const flag of allFlagsUniqueFlags) { const values = diffedFlagColumns.reduce((acc, key) => { acc[key] = FLAG_CONFIG[key](flag); return acc; }, {}); const uniqueValues = new Set(Object.values(values)); if ( uniqueValues.size === 1 && (uniqueValues.has('โœ…') || typeof uniqueValues.values().next().value === 'number') ) { allPassingFlags.push(flag); } if (uniqueValues.size === 1 && uniqueValues.has('โŒ')) { allFailingFlags.push(flag); } const internalVariantValues = INTERNAL_VARIANTS.filter(value => diffedFlagColumns.includes(value) ).map(v => values[v]); const ossVariantValues = OSS_VARIANTS.filter(value => diffedFlagColumns.includes(value) ).map(v => values[v]); if ( internalVariantValues.some(v => v === 'โœ…') && ossVariantValues.every(v => v === 'โŒ') ) { internalOnlyFlags.push(flag); } if ( internalVariantValues.some(v => v === '๐Ÿงช') && (ossVariantValues.every(v => v === 'โŒ') || (ossVariantValues.some(v => v === 'โŒ') && values['OSS Experimental'] === 'โœ…')) ) { earlyExperimentationFlags.push(flag); } if ( internalVariantValues.some(v => v === '๐Ÿงช' || v === 'โŒ') && ossVariantValues.every(v => v === 'โœ…') ) { needsShippedExperimentFlags.push(flag); } } if (allPassingFlags.length > 0) { console.log('ALL VARIANTS PASS (โœ…)'); console.table( formatTable(buildTable(flag => allPassingFlags.includes(flag))) ); } if (allFailingFlags.length > 0) { console.log('ALL VARIANTS FAIL (โŒ)'); console.table( formatTable(buildTable(flag => allFailingFlags.includes(flag))) ); } if (internalOnlyFlags.length > 0) { console.log('INTERNAL ONLY (โœ…)'); console.table( formatTable(buildTable(flag => internalOnlyFlags.includes(flag))) ); } if (earlyExperimentationFlags.length > 0) { console.log('WAITING ON RESULTS (๐Ÿงช)'); console.table( formatTable(buildTable(flag => earlyExperimentationFlags.includes(flag))) ); } if (needsShippedExperimentFlags.length > 0) { console.log('WAITING ON ROLLOUT (๐Ÿงช)'); console.table( formatTable( buildTable(flag => needsShippedExperimentFlags.includes(flag)) ) ); } } else { console.table(formatTable(buildTable())); } console.log(` Legend: โœ… On โŒ Off ๐Ÿ’ป DEV ๐Ÿ“‹ TODO ๐Ÿ“Š Profiling ๐Ÿงช Experiment `);