@samitouri / QOS-React / commits / 45582c6c4d

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">

Ricky committed Jan 30, 2024 at 23:15 UTC 45582c6c4d8b73229c71febbba43ee60fbab81fb
2 files changed +354 -1
package.json
+2 -1
@@ -130,7 +130,8 @@
130 "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
131 "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",
132 "check-release-dependencies": "node ./scripts/release/check-release-dependencies",
133 - "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js"
133 + "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
134 + "flags": "node ./scripts/flags/flags.js"
135 },
136 "resolutions": {
137 "react-is": "npm:react-is"
scripts/flags/flags.js new
+352
@@ -0,0 +1,352 @@
1 +'use strict';
2 +
3 +const babel = require('@babel/register');
4 +const {transformSync} = require('@babel/core');
5 +const Module = require('module');
6 +const path = require('path');
7 +const fs = require('fs');
8 +babel({
9 + plugins: ['@babel/plugin-transform-modules-commonjs'],
10 +});
11 +
12 +const yargs = require('yargs');
13 +const argv = yargs
14 + .parserConfiguration({
15 + // Important: This option tells yargs to move all other options not
16 + // specified here into the `_` key. We use this to send all of the
17 + // Jest options that we don't use through to Jest (like --watch).
18 + 'unknown-options-as-args': true,
19 + })
20 + .wrap(yargs.terminalWidth())
21 + .options({
22 + csv: {
23 + alias: 'c',
24 + describe: 'output cvs.',
25 + requiresArg: false,
26 + type: 'boolean',
27 + default: false,
28 + },
29 + diff: {
30 + alias: 'd',
31 + describe: 'output diff of two or more flags.',
32 + requiresArg: false,
33 + type: 'array',
34 + choices: [
35 + 'www',
36 + 'www-modern',
37 + 'rn',
38 + 'rn-fb',
39 + 'canary',
40 + 'next',
41 + 'experimental',
42 + null,
43 + ],
44 + default: null,
45 + },
46 + sort: {
47 + alias: 's',
48 + describe: 'sort diff by one or more flags.',
49 + requiresArg: false,
50 + type: 'string',
51 + default: 'flag',
52 + choices: [
53 + 'flag',
54 + 'www',
55 + 'www-modern',
56 + 'rn',
57 + 'rn-fb',
58 + 'canary',
59 + 'next',
60 + 'experimental',
61 + ],
62 + },
63 + }).argv;
64 +
65 +// Load ReactNativeFeatureFlags with __NEXT_MAJOR__ replace with 'next'.
66 +// We need to do string replace, since the __NEXT_MAJOR__ is assigned to __EXPERIMENTAL__.
67 +function getReactNativeFeatureFlagsMajor() {
68 + const virtualName = 'ReactNativeFeatureFlagsMajor.js';
69 + const file = fs.readFileSync(
70 + path.join(__dirname, '../../packages/shared/ReactFeatureFlags.js'),
71 + 'utf8'
72 + );
73 + const fileContent = transformSync(
74 + file.replace(
75 + 'const __NEXT_MAJOR__ = __EXPERIMENTAL__;',
76 + 'const __NEXT_MAJOR__ = "next";'
77 + ),
78 + {
79 + plugins: ['@babel/plugin-transform-modules-commonjs'],
80 + }
81 + ).code;
82 +
83 + const parent = module.parent;
84 + const m = new Module(virtualName, parent);
85 + m.filename = virtualName;
86 +
87 + m._compile(fileContent, virtualName);
88 +
89 + return m.exports;
90 +}
91 +
92 +// The RN and www Feature flag files import files that don't exist.
93 +// Mock the imports with the dynamic flag values.
94 +function mockDynamicallyFeatureFlags() {
95 + // Mock the ReactNativeInternalFeatureFlags and ReactFeatureFlags modules
96 + const DynamicFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www-dynamic.js');
97 + const DynamicFeatureFlagsNative = require('../../packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js');
98 +
99 + const originalLoad = Module._load;
100 +
101 + Module._load = function (request, parent) {
102 + if (request === 'ReactNativeInternalFeatureFlags') {
103 + return DynamicFeatureFlagsNative;
104 + } else if (request === 'ReactFeatureFlags') {
105 + return DynamicFeatureFlagsWWW;
106 + }
107 +
108 + return originalLoad.apply(this, arguments);
109 + };
110 +}
111 +// Set the globals to string values to output them to the table.
112 +global.__VARIANT__ = 'gk';
113 +global.__PROFILE__ = 'profile';
114 +global.__DEV__ = 'dev';
115 +global.__EXPERIMENTAL__ = 'experimental';
116 +
117 +// Load all the feature flag files.
118 +mockDynamicallyFeatureFlags();
119 +const ReactFeatureFlags = require('../../packages/shared/ReactFeatureFlags.js');
120 +const ReactFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www.js');
121 +const ReactFeatureFlagsNativeFB = require('../../packages/shared/forks/ReactFeatureFlags.native-fb.js');
122 +const ReactFeatureFlagsNativeOSS = require('../../packages/shared/forks/ReactFeatureFlags.native-oss.js');
123 +const ReactFeatureFlagsMajor = getReactNativeFeatureFlagsMajor();
124 +
125 +const allFlagsUniqueFlags = Array.from(
126 + new Set([
127 + ...Object.keys(ReactFeatureFlags),
128 + ...Object.keys(ReactFeatureFlagsWWW),
129 + ...Object.keys(ReactFeatureFlagsNativeFB),
130 + ...Object.keys(ReactFeatureFlagsNativeOSS),
131 + ])
132 +).sort();
133 +
134 +// These functions are the rules for what each value means in each channel.
135 +function getNextMajorFlagValue(flag) {
136 + const value = ReactFeatureFlagsMajor[flag];
137 + if (value === true || value === 'next') {
138 + return '✅';
139 + } else if (value === false || value === 'experimental') {
140 + return '❌';
141 + } else if (value === 'profile') {
142 + return '📊';
143 + } else if (value === 'dev') {
144 + return '💻';
145 + } else if (typeof value === 'number') {
146 + return value;
147 + } else {
148 + throw new Error(`Unexpected OSS Stable value ${value} for flag ${flag}`);
149 + }
150 +}
151 +
152 +function getOSSCanaryFlagValue(flag) {
153 + const value = ReactFeatureFlags[flag];
154 + if (value === true) {
155 + return '✅';
156 + } else if (value === false || value === 'experimental' || value === 'next') {
157 + return '❌';
158 + } else if (value === 'profile') {
159 + return '📊';
160 + } else if (value === 'dev') {
161 + return '💻';
162 + } else if (typeof value === 'number') {
163 + return value;
164 + } else {
165 + throw new Error(`Unexpected OSS Canary value ${value} for flag ${flag}`);
166 + }
167 +}
168 +
169 +function getOSSExperimentalFlagValue(flag) {
170 + const value = ReactFeatureFlags[flag];
171 + if (value === true || value === 'experimental') {
172 + return '✅';
173 + } else if (value === false || value === 'next') {
174 + return '❌';
175 + } else if (value === 'profile') {
176 + return '📊';
177 + } else if (value === 'dev') {
178 + return '💻';
179 + } else if (typeof value === 'number') {
180 + return value;
181 + } else {
182 + throw new Error(
183 + `Unexpected OSS Experimental value ${value} for flag ${flag}`
184 + );
185 + }
186 +}
187 +
188 +function getWWWModernFlagValue(flag) {
189 + const value = ReactFeatureFlagsWWW[flag];
190 + if (value === true || value === 'experimental') {
191 + return '✅';
192 + } else if (value === false || value === 'next') {
193 + return '❌';
194 + } else if (value === 'profile') {
195 + return '📊';
196 + } else if (value === 'dev') {
197 + return '💻';
198 + } else if (value === 'gk') {
199 + return '🧪';
200 + } else if (typeof value === 'number') {
201 + return value;
202 + } else {
203 + throw new Error(`Unexpected WWW Modern value ${value} for flag ${flag}`);
204 + }
205 +}
206 +
207 +function getWWWClassicFlagValue(flag) {
208 + const value = ReactFeatureFlagsWWW[flag];
209 + if (value === true) {
210 + return '✅';
211 + } else if (value === false || value === 'experimental' || value === 'next') {
212 + return '❌';
213 + } else if (value === 'profile') {
214 + return '📊';
215 + } else if (value === 'dev') {
216 + return '💻';
217 + } else if (value === 'gk') {
218 + return '🧪';
219 + } else if (typeof value === 'number') {
220 + return value;
221 + } else {
222 + throw new Error(`Unexpected WWW Classic value ${value} for flag ${flag}`);
223 + }
224 +}
225 +
226 +function getRNOSSFlagValue(flag) {
227 + const value = ReactFeatureFlagsNativeOSS[flag];
228 + if (value === true) {
229 + return '✅';
230 + } else if (value === false || value === 'experimental' || value === 'next') {
231 + return '❌';
232 + } else if (value === 'profile') {
233 + return '📊';
234 + } else if (value === 'dev') {
235 + return '💻';
236 + } else if (value === 'gk') {
237 + return '🧪';
238 + } else if (typeof value === 'number') {
239 + return value;
240 + } else {
241 + throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
242 + }
243 +}
244 +
245 +function getRNFBFlagValue(flag) {
246 + const value = ReactFeatureFlagsNativeFB[flag];
247 + if (value === true) {
248 + return '✅';
249 + } else if (value === false || value === 'experimental' || value === 'next') {
250 + return '❌';
251 + } else if (value === 'profile') {
252 + return '📊';
253 + } else if (value === 'dev') {
254 + return '💻';
255 + } else if (value === 'gk') {
256 + return '🧪';
257 + } else if (typeof value === 'number') {
258 + return value;
259 + } else {
260 + throw new Error(`Unexpected RN FB value ${value} for flag ${flag}`);
261 + }
262 +}
263 +
264 +function argToHeader(arg) {
265 + switch (arg) {
266 + case 'www':
267 + return 'WWW Classic';
268 + case 'www-modern':
269 + return 'WWW Modern';
270 + case 'rn':
271 + return 'RN OSS';
272 + case 'rn-fb':
273 + return 'RN FB';
274 + case 'canary':
275 + return 'OSS Canary';
276 + case 'next':
277 + return 'OSS Next Major';
278 + case 'experimental':
279 + return 'OSS Experimental';
280 + default:
281 + return arg;
282 + }
283 +}
284 +
285 +// Build the table with the value for each flag.
286 +const isDiff = argv.diff != null && argv.diff.length > 1;
287 +const table = {};
288 +// eslint-disable-next-line no-for-of-loops/no-for-of-loops
289 +for (const flag of allFlagsUniqueFlags) {
290 + const values = {
291 + 'OSS Next Major': getNextMajorFlagValue(flag),
292 + 'OSS Canary': getOSSCanaryFlagValue(flag),
293 + 'OSS Experimental': getOSSExperimentalFlagValue(flag),
294 + 'WWW Classic': getWWWClassicFlagValue(flag),
295 + 'WWW Modern': getWWWModernFlagValue(flag),
296 + 'RN FB': getRNFBFlagValue(flag),
297 + 'RN OSS': getRNOSSFlagValue(flag),
298 + };
299 +
300 + if (!isDiff) {
301 + table[flag] = values;
302 + continue;
303 + }
304 +
305 + const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
306 + if (key in values) {
307 + acc[key] = values[key];
308 + }
309 + return acc;
310 + }, {});
311 +
312 + if (new Set(Object.values(subset)).size !== 1) {
313 + table[flag] = subset;
314 + }
315 +}
316 +
317 +// Sort the table
318 +let sorted = table;
319 +if (isDiff || argv.sort) {
320 + const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
321 + sorted = Object.fromEntries(
322 + Object.entries(table).sort(([, rowA], [, rowB]) =>
323 + rowB[sortChannel].toString().localeCompare(rowA[sortChannel])
324 + )
325 + );
326 +}
327 +
328 +if (argv.csv) {
329 + const csvHeader =
330 + 'Flag name, WWW Classic, RN FB, OSS Canary, OSS Experimental, WWW Modern, RN OSS\n';
331 + const csvRows = Object.keys(sorted).map(flag => {
332 + const row = sorted[flag];
333 + return `${flag}, ${row['WWW Classic']}, ${row['RN FB']}, ${row['OSS Canary']}, ${row['OSS Experimental']}, ${row['WWW Modern']}, ${row['RN OSS']}`;
334 + });
335 + fs.writeFile('./flags.csv', csvHeader + csvRows.join('\n'), function (err) {
336 + if (err) {
337 + return console.log(err);
338 + }
339 + console.log('The file was saved!');
340 + });
341 +}
342 +
343 +// left align the flag names.
344 +const maxLength = Math.max(...Object.keys(sorted).map(item => item.length));
345 +const padded = {};
346 +Object.keys(sorted).forEach(key => {
347 + const newKey = key.padEnd(maxLength, ' ');
348 + padded[newKey] = sorted[key];
349 +});
350 +
351 +// print table with formatting
352 +console.table(padded);