main
js 600 lines 15.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7 'use strict';
8
9 const babel = require('@babel/register');
10 const {transformSync} = require('@babel/core');
11 const Module = require('module');
12 const path = require('path');
13 const fs = require('fs');
14 babel({
15 plugins: [
16 'babel-plugin-syntax-hermes-parser',
17 '@babel/plugin-transform-modules-commonjs',
18 ],
19 });
20
21 const yargs = require('yargs');
22 const argv = yargs
23 .parserConfiguration({
24 // Important: This option tells yargs to move all other options not
25 // specified here into the `_` key. We use this to send all of the
26 // Jest options that we don't use through to Jest (like --watch).
27 'unknown-options-as-args': true,
28 })
29 .wrap(yargs.terminalWidth())
30 .options({
31 csv: {
32 alias: 'c',
33 describe: 'output cvs.',
34 requiresArg: false,
35 type: 'boolean',
36 default: false,
37 },
38 diff: {
39 alias: 'd',
40 describe: 'output diff of two or more flags.',
41 requiresArg: false,
42 type: 'array',
43 choices: [
44 'www',
45 'www-modern',
46 'rn',
47 'rn-fb',
48 'rn-next',
49 'canary',
50 'next',
51 'experimental',
52 null,
53 ],
54 default: null,
55 },
56 sort: {
57 alias: 's',
58 describe: 'sort diff by one or more flags.',
59 requiresArg: false,
60 type: 'string',
61 default: 'flag',
62 choices: [
63 'flag',
64 'www',
65 'www-modern',
66 'rn',
67 'rn-fb',
68 'rn-next',
69 'canary',
70 'next',
71 'experimental',
72 ],
73 },
74 cleanup: {
75 describe: 'output flags by cleanup category.',
76 requiresArg: false,
77 type: 'boolean',
78 default: false,
79 },
80 }).argv;
81
82 // Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'.
83 // We need to do string replace, since the __NEXT_MAJOR__ is assigned to __EXPERIMENTAL__.
84 function getReactFeatureFlagsMajor() {
85 const virtualName = 'ReactFeatureFlagsMajor.js';
86 const file = fs.readFileSync(
87 path.join(__dirname, '../../packages/shared/ReactFeatureFlags.js'),
88 'utf8'
89 );
90 const fileContent = transformSync(
91 file.replace(
92 'const __NEXT_MAJOR__ = __EXPERIMENTAL__;',
93 'const __NEXT_MAJOR__ = "next";'
94 ),
95 {
96 plugins: [
97 'babel-plugin-syntax-hermes-parser',
98 '@babel/plugin-transform-modules-commonjs',
99 ],
100 }
101 ).code;
102
103 const parent = module.parent;
104 const m = new Module(virtualName, parent);
105 m.filename = virtualName;
106
107 m._compile(fileContent, virtualName);
108
109 return m.exports;
110 }
111
112 // Load RN ReactFeatureFlags with __NEXT_RN_MAJOR__ replaced with 'next'.
113 // We need to do string replace, since the __NEXT_RN_MAJOR__ is assigned to false.
114 function getReactNativeFeatureFlagsMajor() {
115 const virtualName = 'ReactNativeFeatureFlagsMajor.js';
116 const file = fs.readFileSync(
117 path.join(
118 __dirname,
119 '../../packages/shared/forks/ReactFeatureFlags.native-oss.js'
120 ),
121 'utf8'
122 );
123 const fileContent = transformSync(
124 file
125 .replace(
126 'const __NEXT_RN_MAJOR__ = true;',
127 'const __NEXT_RN_MAJOR__ = "next";'
128 )
129 .replace(
130 'const __TODO_NEXT_RN_MAJOR__ = false;',
131 'const __TODO_NEXT_RN_MAJOR__ = "next-todo";'
132 ),
133 {
134 plugins: [
135 'babel-plugin-syntax-hermes-parser',
136 '@babel/plugin-transform-modules-commonjs',
137 ],
138 }
139 ).code;
140
141 const parent = module.parent;
142 const m = new Module(virtualName, parent);
143 m.filename = virtualName;
144
145 m._compile(fileContent, virtualName);
146
147 return m.exports;
148 }
149
150 // The RN and www Feature flag files import files that don't exist.
151 // Mock the imports with the dynamic flag values.
152 function mockDynamicallyFeatureFlags() {
153 // Mock the ReactNativeInternalFeatureFlags and ReactFeatureFlags modules
154 const DynamicFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www-dynamic.js');
155 const DynamicFeatureFlagsNative = require('../../packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js');
156
157 const originalLoad = Module._load;
158
159 Module._load = function (request, parent) {
160 if (request === 'ReactNativeInternalFeatureFlags') {
161 return DynamicFeatureFlagsNative;
162 } else if (request === 'ReactFeatureFlags') {
163 return DynamicFeatureFlagsWWW;
164 }
165
166 return originalLoad.apply(this, arguments);
167 };
168 }
169 // Set the globals to string values to output them to the table.
170 global.__VARIANT__ = 'gk';
171 global.__PROFILE__ = 'profile';
172 global.__DEV__ = 'dev';
173 global.__EXPERIMENTAL__ = 'experimental';
174
175 // Load all the feature flag files.
176 mockDynamicallyFeatureFlags();
177 const ReactFeatureFlags = require('../../packages/shared/ReactFeatureFlags.js');
178 const ReactFeatureFlagsWWW = require('../../packages/shared/forks/ReactFeatureFlags.www.js');
179 const ReactFeatureFlagsNativeFB = require('../../packages/shared/forks/ReactFeatureFlags.native-fb.js');
180 const ReactFeatureFlagsMajor = getReactFeatureFlagsMajor();
181 const ReactNativeFeatureFlagsMajor = getReactNativeFeatureFlagsMajor();
182
183 const allFlagsUniqueFlags = Array.from(
184 new Set([
185 ...Object.keys(ReactFeatureFlags),
186 ...Object.keys(ReactFeatureFlagsWWW),
187 ...Object.keys(ReactFeatureFlagsNativeFB),
188 ])
189 ).sort();
190
191 // These functions are the rules for what each value means in each channel.
192 function getNextMajorFlagValue(flag) {
193 const value = ReactFeatureFlagsMajor[flag];
194 if (value === true || value === 'next') {
195 return '';
196 } else if (value === false || value === null || value === 'experimental') {
197 return '';
198 } else if (value === 'profile') {
199 return '📊';
200 } else if (value === 'dev') {
201 return '💻';
202 } else if (typeof value === 'number' || typeof value === 'string') {
203 return value;
204 } else {
205 throw new Error(`Unexpected OSS Stable value ${value} for flag ${flag}`);
206 }
207 }
208
209 function getOSSCanaryFlagValue(flag) {
210 const value = ReactFeatureFlags[flag];
211 if (value === true) {
212 return '';
213 } else if (
214 value === false ||
215 value === null ||
216 value === 'experimental' ||
217 value === 'next'
218 ) {
219 return '';
220 } else if (value === 'profile') {
221 return '📊';
222 } else if (value === 'dev') {
223 return '💻';
224 } else if (typeof value === 'number' || typeof value === 'string') {
225 return value;
226 } else {
227 throw new Error(`Unexpected OSS Canary value ${value} for flag ${flag}`);
228 }
229 }
230
231 function getOSSExperimentalFlagValue(flag) {
232 const value = ReactFeatureFlags[flag];
233 if (value === true || value === 'experimental') {
234 return '';
235 } else if (value === false || value === null || value === 'next') {
236 return '';
237 } else if (value === 'profile') {
238 return '📊';
239 } else if (value === 'dev') {
240 return '💻';
241 } else if (typeof value === 'number' || typeof value === 'string') {
242 return value;
243 } else {
244 throw new Error(
245 `Unexpected OSS Experimental value ${value} for flag ${flag}`
246 );
247 }
248 }
249
250 function getWWWModernFlagValue(flag) {
251 const value = ReactFeatureFlagsWWW[flag];
252 if (value === true || value === 'experimental') {
253 return '';
254 } else if (value === false || value === null || value === 'next') {
255 return '';
256 } else if (value === 'profile') {
257 return '📊';
258 } else if (value === 'dev') {
259 return '💻';
260 } else if (value === 'gk') {
261 return '🧪';
262 } else if (typeof value === 'number' || typeof value === 'string') {
263 return value;
264 } else {
265 throw new Error(`Unexpected WWW Modern value ${value} for flag ${flag}`);
266 }
267 }
268
269 function getWWWClassicFlagValue(flag) {
270 const value = ReactFeatureFlagsWWW[flag];
271 if (value === true) {
272 return '';
273 } else if (
274 value === false ||
275 value === null ||
276 value === 'experimental' ||
277 value === 'next'
278 ) {
279 return '';
280 } else if (value === 'profile') {
281 return '📊';
282 } else if (value === 'dev') {
283 return '💻';
284 } else if (value === 'gk') {
285 return '🧪';
286 } else if (typeof value === 'number' || typeof value === 'string') {
287 return value;
288 } else {
289 throw new Error(`Unexpected WWW Classic value ${value} for flag ${flag}`);
290 }
291 }
292
293 function getRNNextMajorFlagValue(flag) {
294 const value = ReactNativeFeatureFlagsMajor[flag];
295 if (value === true || value === 'next') {
296 return '';
297 } else if (value === 'next-todo') {
298 return '📋';
299 } else if (value === false || value === null || value === 'experimental') {
300 return '';
301 } else if (value === 'profile') {
302 return '📊';
303 } else if (value === 'dev') {
304 return '💻';
305 } else if (value === 'gk') {
306 return '🧪';
307 } else if (typeof value === 'number' || typeof value === 'string') {
308 return value;
309 } else {
310 throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
311 }
312 }
313
314 function getRNOSSFlagValue(flag) {
315 const value = ReactNativeFeatureFlagsMajor[flag];
316 if (value === true) {
317 return '';
318 } else if (
319 value === false ||
320 value === null ||
321 value === 'experimental' ||
322 value === 'next' ||
323 value === 'next-todo'
324 ) {
325 return '';
326 } else if (value === 'profile') {
327 return '📊';
328 } else if (value === 'dev') {
329 return '💻';
330 } else if (value === 'gk') {
331 return '🧪';
332 } else if (typeof value === 'number' || typeof value === 'string') {
333 return value;
334 } else {
335 throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
336 }
337 }
338
339 function getRNFBFlagValue(flag) {
340 const value = ReactFeatureFlagsNativeFB[flag];
341 if (value === true) {
342 return '';
343 } else if (
344 value === false ||
345 value === null ||
346 value === 'experimental' ||
347 value === 'next'
348 ) {
349 return '';
350 } else if (value === 'profile') {
351 return '📊';
352 } else if (value === 'dev') {
353 return '💻';
354 } else if (value === 'gk') {
355 return '🧪';
356 } else if (typeof value === 'number' || typeof value === 'string') {
357 return value;
358 } else {
359 throw new Error(`Unexpected RN FB value ${value} for flag ${flag}`);
360 }
361 }
362
363 function argToHeader(arg) {
364 switch (arg) {
365 case 'www':
366 return 'WWW Classic';
367 case 'www-modern':
368 return 'WWW Modern';
369 case 'rn':
370 return 'RN OSS';
371 case 'rn-fb':
372 return 'RN FB';
373 case 'rn-next':
374 return 'RN Next Major';
375 case 'canary':
376 return 'OSS Canary';
377 case 'next':
378 return 'OSS Next Major';
379 case 'experimental':
380 return 'OSS Experimental';
381 default:
382 return arg;
383 }
384 }
385
386 const FLAG_CONFIG = {
387 'OSS Next Major': getNextMajorFlagValue,
388 'OSS Canary': getOSSCanaryFlagValue,
389 'OSS Experimental': getOSSExperimentalFlagValue,
390 'WWW Classic': getWWWClassicFlagValue,
391 'WWW Modern': getWWWModernFlagValue,
392 'RN FB': getRNFBFlagValue,
393 'RN OSS': getRNOSSFlagValue,
394 'RN Next Major': getRNNextMajorFlagValue,
395 };
396
397 const FLAG_COLUMNS = Object.keys(FLAG_CONFIG);
398
399 const INTERNAL_VARIANTS = ['WWW Classic', 'WWW Modern', 'RN FB'];
400 const OSS_VARIANTS = [
401 'OSS Next Major',
402 'OSS Canary',
403 'OSS Experimental',
404 'RN OSS',
405 'RN Next Major',
406 ];
407
408 // Build the table with the value for each flag.
409 function buildTable(filterFn) {
410 const isDiff = argv.diff != null && argv.diff.length > 1;
411 const table = {};
412 const filteredFlags = filterFn
413 ? allFlagsUniqueFlags.filter(filterFn)
414 : allFlagsUniqueFlags;
415 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
416 for (const flag of filteredFlags) {
417 const values = FLAG_COLUMNS.reduce((acc, key) => {
418 acc[key] = FLAG_CONFIG[key](flag);
419 return acc;
420 }, {});
421
422 if (!isDiff) {
423 table[flag] = values;
424 continue;
425 }
426
427 const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
428 if (key in values) {
429 acc[key] = values[key];
430 }
431 return acc;
432 }, {});
433
434 if (new Set(Object.values(subset)).size !== 1) {
435 table[flag] = subset;
436 }
437 }
438
439 // Sort the table
440 let sorted = table;
441 if (isDiff || argv.sort) {
442 const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
443 const sortBy =
444 sortChannel === 'flag'
445 ? ([flagA], [flagB]) => {
446 return flagA.localeCompare(flagB);
447 }
448 : ([, rowA], [, rowB]) => {
449 return rowB[sortChannel]
450 .toString()
451 .localeCompare(rowA[sortChannel]);
452 };
453 sorted = Object.fromEntries(Object.entries(table).sort(sortBy));
454 }
455
456 return sorted;
457 }
458
459 function formatTable(tableData) {
460 // left align the flag names.
461 const maxLength = Math.max(
462 ...Object.keys(tableData).map(item => item.length)
463 );
464 const padded = {};
465 Object.keys(tableData).forEach(key => {
466 const newKey = key.padEnd(maxLength, ' ');
467 padded[newKey] = tableData[key];
468 });
469
470 return padded;
471 }
472
473 if (argv.csv) {
474 const table = buildTable();
475 const csvRows = [
476 `Flag name, ${FLAG_COLUMNS.join(', ')}`,
477 ...Object.keys(table).map(flag => {
478 const row = table[flag];
479 return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`;
480 }),
481 ];
482 fs.writeFile('./flags.csv', csvRows.join('\n'), function (err) {
483 if (err) {
484 return console.log(err);
485 }
486 console.log('The file was saved to ./flags.csv');
487 });
488 }
489
490 if (argv.cleanup) {
491 const allPassingFlags = [];
492 const allFailingFlags = [];
493 const needsShippedExperimentFlags = [];
494 const earlyExperimentationFlags = [];
495 const internalOnlyFlags = [];
496
497 const diffedFlagColumns =
498 argv.diff[0] != null ? argv.diff.map(argToHeader) : FLAG_COLUMNS;
499
500 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
501 for (const flag of allFlagsUniqueFlags) {
502 const values = diffedFlagColumns.reduce((acc, key) => {
503 acc[key] = FLAG_CONFIG[key](flag);
504 return acc;
505 }, {});
506
507 const uniqueValues = new Set(Object.values(values));
508
509 if (
510 uniqueValues.size === 1 &&
511 (uniqueValues.has('') ||
512 typeof uniqueValues.values().next().value === 'number')
513 ) {
514 allPassingFlags.push(flag);
515 }
516
517 if (uniqueValues.size === 1 && uniqueValues.has('')) {
518 allFailingFlags.push(flag);
519 }
520
521 const internalVariantValues = INTERNAL_VARIANTS.filter(value =>
522 diffedFlagColumns.includes(value)
523 ).map(v => values[v]);
524 const ossVariantValues = OSS_VARIANTS.filter(value =>
525 diffedFlagColumns.includes(value)
526 ).map(v => values[v]);
527
528 if (
529 internalVariantValues.some(v => v === '') &&
530 ossVariantValues.every(v => v === '')
531 ) {
532 internalOnlyFlags.push(flag);
533 }
534
535 if (
536 internalVariantValues.some(v => v === '🧪') &&
537 (ossVariantValues.every(v => v === '') ||
538 (ossVariantValues.some(v => v === '') &&
539 values['OSS Experimental'] === ''))
540 ) {
541 earlyExperimentationFlags.push(flag);
542 }
543
544 if (
545 internalVariantValues.some(v => v === '🧪' || v === '') &&
546 ossVariantValues.every(v => v === '')
547 ) {
548 needsShippedExperimentFlags.push(flag);
549 }
550 }
551
552 if (allPassingFlags.length > 0) {
553 console.log('ALL VARIANTS PASS (✅)');
554 console.table(
555 formatTable(buildTable(flag => allPassingFlags.includes(flag)))
556 );
557 }
558
559 if (allFailingFlags.length > 0) {
560 console.log('ALL VARIANTS FAIL (❌)');
561 console.table(
562 formatTable(buildTable(flag => allFailingFlags.includes(flag)))
563 );
564 }
565
566 if (internalOnlyFlags.length > 0) {
567 console.log('INTERNAL ONLY (✅)');
568 console.table(
569 formatTable(buildTable(flag => internalOnlyFlags.includes(flag)))
570 );
571 }
572
573 if (earlyExperimentationFlags.length > 0) {
574 console.log('WAITING ON RESULTS (🧪)');
575 console.table(
576 formatTable(buildTable(flag => earlyExperimentationFlags.includes(flag)))
577 );
578 }
579
580 if (needsShippedExperimentFlags.length > 0) {
581 console.log('WAITING ON ROLLOUT (🧪)');
582 console.table(
583 formatTable(
584 buildTable(flag => needsShippedExperimentFlags.includes(flag))
585 )
586 );
587 }
588 } else {
589 console.table(formatTable(buildTable()));
590 }
591
592 console.log(`
593 Legend:
594 ✅ On
595 ❌ Off
596 💻 DEV
597 📋 TODO
598 📊 Profiling
599 🧪 Experiment
600 `);