Add --cleanup option to flags script to show groups of flags by status (#31762)
`yarn flags --cleanup` will categorize flags to make it more clear which ones may need to be cleaned up, experiments checked on, or are blocked by internal rollouts. Alternative to #31760 <img width="787" alt="Screenshot 2024-12-13 at 2 31 30 PM" src="https://github.com/user-attachments/assets/452aee7e-9caf-4210-a621-53941d59cb2b" />
Jack Pope committed
Dec 13, 2024 at 15:49 UTC
982cf95c8b1914f87c5b1ea4ed73528e9d5dfb66
1 file changed
+174
-43
scripts/flags/flags.js
+174
-43
@@ -62,6 +62,12 @@ const argv = yargs
62
'experimental',
63
],
64
},
65
+ cleanup: {
66
+ describe: 'output flags by cleanup category.',
67
+ requiresArg: false,
68
+ type: 'boolean',
69
+ default: false,
70
+ },
71
}).argv;
72
73
// Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'.
@@ -375,53 +381,86 @@ const FLAG_CONFIG = {
381
382
const FLAG_COLUMNS = Object.keys(FLAG_CONFIG);
383
384
+const INTERNAL_VARIANTS = ['WWW Classic', 'WWW Modern', 'RN FB'];
385
+const OSS_VARIANTS = [
386
+ 'OSS Next Major',
387
+ 'OSS Canary',
388
+ 'OSS Experimental',
389
+ 'RN OSS',
390
+ 'RN Next Major',
391
+];
392
+
393
// Build the table with the value for each flag.
379
-const isDiff = argv.diff != null && argv.diff.length > 1;
380
-const table = {};
381
-// eslint-disable-next-line no-for-of-loops/no-for-of-loops
382
-for (const flag of allFlagsUniqueFlags) {
383
- const values = FLAG_COLUMNS.reduce((acc, key) => {
384
- acc[key] = FLAG_CONFIG[key](flag);
385
- return acc;
386
- }, {});
387
-
388
- if (!isDiff) {
389
- table[flag] = values;
390
- continue;
391
- }
394
+function buildTable(filterFn) {
395
+ const isDiff = argv.diff != null && argv.diff.length > 1;
396
+ const table = {};
397
+ const filteredFlags = filterFn
398
+ ? allFlagsUniqueFlags.filter(filterFn)
399
+ : allFlagsUniqueFlags;
400
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
401
+ for (const flag of filteredFlags) {
402
+ const values = FLAG_COLUMNS.reduce((acc, key) => {
403
+ acc[key] = FLAG_CONFIG[key](flag);
404
+ return acc;
405
+ }, {});
406
+
407
+ if (!isDiff) {
408
+ table[flag] = values;
409
+ continue;
410
+ }
411
+
412
+ const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
413
+ if (key in values) {
414
+ acc[key] = values[key];
415
+ }
416
+ return acc;
417
+ }, {});
418
393
- const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
394
- if (key in values) {
395
- acc[key] = values[key];
419
+ if (new Set(Object.values(subset)).size !== 1) {
420
+ table[flag] = subset;
421
}
397
- return acc;
398
- }, {});
422
+ }
423
400
- if (new Set(Object.values(subset)).size !== 1) {
401
- table[flag] = subset;
424
+ // Sort the table
425
+ let sorted = table;
426
+ if (isDiff || argv.sort) {
427
+ const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
428
+ const sortBy =
429
+ sortChannel === 'flag'
430
+ ? ([flagA], [flagB]) => {
431
+ return flagA.localeCompare(flagB);
432
+ }
433
+ : ([, rowA], [, rowB]) => {
434
+ return rowB[sortChannel]
435
+ .toString()
436
+ .localeCompare(rowA[sortChannel]);
437
+ };
438
+ sorted = Object.fromEntries(Object.entries(table).sort(sortBy));
439
}
440
+
441
+ return sorted;
442
}
443
405
-// Sort the table
406
-let sorted = table;
407
-if (isDiff || argv.sort) {
408
- const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
409
- const sortBy =
410
- sortChannel === 'flag'
411
- ? ([flagA], [flagB]) => {
412
- return flagA.localeCompare(flagB);
413
- }
414
- : ([, rowA], [, rowB]) => {
415
- return rowB[sortChannel].toString().localeCompare(rowA[sortChannel]);
416
- };
417
- sorted = Object.fromEntries(Object.entries(table).sort(sortBy));
444
+function formatTable(tableData) {
445
+ // left align the flag names.
446
+ const maxLength = Math.max(
447
+ ...Object.keys(tableData).map(item => item.length)
448
+ );
449
+ const padded = {};
450
+ Object.keys(tableData).forEach(key => {
451
+ const newKey = key.padEnd(maxLength, ' ');
452
+ padded[newKey] = tableData[key];
453
+ });
454
+
455
+ return padded;
456
}
457
458
if (argv.csv) {
459
+ const table = buildTable();
460
const csvRows = [
461
`Flag name, ${FLAG_COLUMNS.join(', ')}`,
462
...Object.keys(table).map(flag => {
424
- const row = sorted[flag];
463
+ const row = table[flag];
464
return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`;
465
}),
466
];
@@ -433,16 +472,108 @@ if (argv.csv) {
472
});
473
}
474
436
-// left align the flag names.
437
-const maxLength = Math.max(...Object.keys(sorted).map(item => item.length));
438
-const padded = {};
439
-Object.keys(sorted).forEach(key => {
440
- const newKey = key.padEnd(maxLength, ' ');
441
- padded[newKey] = sorted[key];
442
-});
475
+if (argv.cleanup) {
476
+ const allPassingFlags = [];
477
+ const allFailingFlags = [];
478
+ const needsShippedExperimentFlags = [];
479
+ const earlyExperimentationFlags = [];
480
+ const internalOnlyFlags = [];
481
+
482
+ const diffedFlagColumns =
483
+ argv.diff[0] != null ? argv.diff.map(argToHeader) : FLAG_COLUMNS;
484
+
485
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
486
+ for (const flag of allFlagsUniqueFlags) {
487
+ const values = diffedFlagColumns.reduce((acc, key) => {
488
+ acc[key] = FLAG_CONFIG[key](flag);
489
+ return acc;
490
+ }, {});
491
+
492
+ const uniqueValues = new Set(Object.values(values));
493
+
494
+ if (
495
+ uniqueValues.size === 1 &&
496
+ (uniqueValues.has('✅') ||
497
+ typeof uniqueValues.values().next().value === 'number')
498
+ ) {
499
+ allPassingFlags.push(flag);
500
+ }
501
+
502
+ if (uniqueValues.size === 1 && uniqueValues.has('❌')) {
503
+ allFailingFlags.push(flag);
504
+ }
505
+
506
+ const internalVariantValues = INTERNAL_VARIANTS.filter(value =>
507
+ diffedFlagColumns.includes(value)
508
+ ).map(v => values[v]);
509
+ const ossVariantValues = OSS_VARIANTS.filter(value =>
510
+ diffedFlagColumns.includes(value)
511
+ ).map(v => values[v]);
512
+
513
+ if (
514
+ internalVariantValues.some(v => v === '✅') &&
515
+ ossVariantValues.every(v => v === '❌')
516
+ ) {
517
+ internalOnlyFlags.push(flag);
518
+ }
519
+
520
+ if (
521
+ internalVariantValues.some(v => v === '🧪') &&
522
+ (ossVariantValues.every(v => v === '❌') ||
523
+ (ossVariantValues.some(v => v === '❌') &&
524
+ values['OSS Experimental'] === '✅'))
525
+ ) {
526
+ earlyExperimentationFlags.push(flag);
527
+ }
528
+
529
+ if (
530
+ internalVariantValues.some(v => v === '🧪' || v === '❌') &&
531
+ ossVariantValues.every(v => v === '✅')
532
+ ) {
533
+ needsShippedExperimentFlags.push(flag);
534
+ }
535
+ }
536
+
537
+ if (allPassingFlags.length > 0) {
538
+ console.log('ALL VARIANTS PASS (✅)');
539
+ console.table(
540
+ formatTable(buildTable(flag => allPassingFlags.includes(flag)))
541
+ );
542
+ }
543
+
544
+ if (allFailingFlags.length > 0) {
545
+ console.log('ALL VARIANTS FAIL (❌)');
546
+ console.table(
547
+ formatTable(buildTable(flag => allFailingFlags.includes(flag)))
548
+ );
549
+ }
550
+
551
+ if (internalOnlyFlags.length > 0) {
552
+ console.log('INTERNAL ONLY (✅)');
553
+ console.table(
554
+ formatTable(buildTable(flag => internalOnlyFlags.includes(flag)))
555
+ );
556
+ }
557
+
558
+ if (earlyExperimentationFlags.length > 0) {
559
+ console.log('WAITING ON RESULTS (🧪)');
560
+ console.table(
561
+ formatTable(buildTable(flag => earlyExperimentationFlags.includes(flag)))
562
+ );
563
+ }
564
+
565
+ if (needsShippedExperimentFlags.length > 0) {
566
+ console.log('WAITING ON ROLLOUT (🧪)');
567
+ console.table(
568
+ formatTable(
569
+ buildTable(flag => needsShippedExperimentFlags.includes(flag))
570
+ )
571
+ );
572
+ }
573
+} else {
574
+ console.table(formatTable(buildTable()));
575
+}
576
444
-// print table with formatting
445
-console.table(padded);
577
console.log(`
578
Legend:
579
✅ On