Skip to content

Commit b0323be

Browse files
rickhanloniigaearon
authored andcommitted
Add script to output flag values (#28115)
## Overview Depends on: #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">
1 parent 61c2448 commit b0323be

File tree

2 files changed

+354
-1
lines changed

2 files changed

+354
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@
130130
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
131131
"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",
132132
"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"
134135
},
135136
"resolutions": {
136137
"react-is": "npm:react-is"

scripts/flags/flags.js

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

0 commit comments

Comments
 (0)