Files
Samuel Susla 0f45d332d4 move Fantom's API to regular functions (#50035)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50035

changelog: [internal]

to make it easier to write JSDocs, let's export functions directly from index.js instead of using proxy object.

Reviewed By: rubennorte

Differential Revision: D71200977

fbshipit-source-id: 0b53c0d3f73577c19253537b9e884459a4920643
2025-03-14 11:12:15 -07:00

227 lines
6.8 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {FeatureFlagValue} from '../../../packages/react-native/scripts/featureflags/types';
import ReactNativeFeatureFlags from '../../../packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config';
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {extract, parse} from 'jest-docblock';
type CommonFeatureFlags = (typeof ReactNativeFeatureFlags)['common'];
type JsOnlyFeatureFlags = (typeof ReactNativeFeatureFlags)['jsOnly'];
type DocblockPragmas = {[key: string]: string | string[]};
export enum FantomTestConfigMode {
DevelopmentWithBytecode,
DevelopmentWithSource,
Optimized,
}
export type FantomTestConfigCommonFeatureFlags = Partial<{
[key in keyof CommonFeatureFlags]: CommonFeatureFlags[key]['defaultValue'],
}>;
export type FantomTestConfigJsOnlyFeatureFlags = Partial<{
[key in keyof JsOnlyFeatureFlags]: JsOnlyFeatureFlags[key]['defaultValue'],
}>;
export type FantomTestConfigReactInternalFeatureFlags = {
[key: string]: FeatureFlagValue,
};
export type FantomTestConfig = {
mode: FantomTestConfigMode,
flags: {
common: FantomTestConfigCommonFeatureFlags,
jsOnly: FantomTestConfigJsOnlyFeatureFlags,
reactInternal: FantomTestConfigReactInternalFeatureFlags,
},
};
const DEFAULT_MODE: FantomTestConfigMode =
FantomTestConfigMode.DevelopmentWithSource;
const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
const FANTOM_BENCHMARK_FILENAME_RE = /[Bb]enchmark-itest\./g;
const FANTOM_BENCHMARK_SUITE_RE =
/\n(Fantom\.)?unstable_benchmark(\s*)\.suite\(/g;
/**
* Extracts the Fantom configuration from the test file, specified as part of
* the docblock comment. E.g.:
*
* ```
* /**
* * @flow strict-local
* * @fantom_mode opt
* * @fantom_flags commonTestFlag:true
* * @fantom_flags jsOnlyTestFlag:true
* * @fantom_react_fb_flags reactInternalFlag:true
* *
* ```
*
* The supported options are:
* - `fantom_mode`: specifies the level of optimization to compile the test
* with. Valid values are `dev` and `opt`.
* - `fantom_flags`: specifies the configuration for common and JS-only feature
* flags. They can be specified in the same pragma or in different ones, and
* the format is `<flag_name>:<value>`.
*/
export default function getFantomTestConfig(
testPath: string,
): FantomTestConfig {
const testContents = fs.readFileSync(testPath, 'utf8');
const docblock = extract(testContents);
const pragmas = parse(docblock) as DocblockPragmas;
const config: FantomTestConfig = {
mode: DEFAULT_MODE,
flags: {
common: {},
jsOnly: {
enableAccessToHostTreeInFabric: true,
},
reactInternal: {},
},
};
const maybeMode = pragmas.fantom_mode;
if (maybeMode != null) {
if (Array.isArray(maybeMode)) {
throw new Error('Expected a single value for @fantom_mode');
}
const mode = maybeMode;
switch (mode) {
case 'dev':
config.mode = FantomTestConfigMode.DevelopmentWithSource;
break;
case 'dev-bytecode':
config.mode = FantomTestConfigMode.DevelopmentWithBytecode;
break;
case 'opt':
config.mode = FantomTestConfigMode.Optimized;
break;
default:
throw new Error(`Invalid Fantom mode: ${mode}`);
}
} else {
if (
FANTOM_BENCHMARK_FILENAME_RE.test(testPath) ||
FANTOM_BENCHMARK_SUITE_RE.test(testContents)
) {
config.mode = FantomTestConfigMode.Optimized;
}
}
const maybeRawFlagConfig = pragmas.fantom_flags;
if (maybeRawFlagConfig != null) {
const rawFlagConfigs = (
Array.isArray(maybeRawFlagConfig)
? maybeRawFlagConfig
: [maybeRawFlagConfig]
).flatMap(value => value.split(/\s+/g));
for (const rawFlagConfig of rawFlagConfigs) {
const matches = FANTOM_FLAG_FORMAT.exec(rawFlagConfig);
if (matches == null) {
throw new Error(
`Invalid format for Fantom feature flag: ${rawFlagConfig}. Expected <flag_name>:<value>`,
);
}
const [, name, rawValue] = matches;
if (ReactNativeFeatureFlags.common[name]) {
const flagConfig = ReactNativeFeatureFlags.common[name];
const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue);
config.flags.common[name] = value;
} else if (ReactNativeFeatureFlags.jsOnly[name]) {
const flagConfig = ReactNativeFeatureFlags.jsOnly[name];
const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue);
config.flags.jsOnly[name] = value;
} else {
const validKeys = Object.keys(ReactNativeFeatureFlags.common)
.concat(Object.keys(ReactNativeFeatureFlags.jsOnly))
.join(', ');
throw new Error(
`Invalid Fantom feature flag: ${name}. Valid flags are: ${validKeys}`,
);
}
}
}
const maybeReactInternalRawFlagConfig = pragmas.fantom_react_fb_flags;
if (maybeReactInternalRawFlagConfig != null) {
const reactInternalRawFlagConfigs = (
Array.isArray(maybeReactInternalRawFlagConfig)
? maybeReactInternalRawFlagConfig
: [maybeReactInternalRawFlagConfig]
).flatMap(value => value.split(/\s+/g));
for (const reactInternalRawFlagConfig of reactInternalRawFlagConfigs) {
const matches = FANTOM_FLAG_FORMAT.exec(reactInternalRawFlagConfig);
if (matches == null) {
throw new Error(
`Invalid format for Fantom React fb feature flag: ${reactInternalRawFlagConfig}. Expected <flag_name>:<value>`,
);
}
const [, name, rawValue] = matches;
const value = parseFeatureFlagValue(false, rawValue);
config.flags.reactInternal[name] = value;
}
}
return config;
}
function parseFeatureFlagValue<T: boolean | number | string>(
defaultValue: T,
value: string,
): T {
switch (typeof defaultValue) {
case 'boolean':
if (value === 'true') {
// $FlowExpectedError[incompatible-return] at this point we know T is a boolean
return true;
} else if (value === 'false') {
// $FlowExpectedError[incompatible-return] at this point we know T is a boolean
return false;
} else {
throw new Error(`Invalid value for boolean flag: ${value}`);
}
case 'number':
const parsed = Number(value);
if (Number.isNaN(parsed)) {
throw new Error(`Invalid value for number flag: ${value}`);
}
// $FlowExpectedError[incompatible-return] at this point we know T is a number
return parsed;
case 'string':
// $FlowExpectedError[incompatible-return] at this point we know T is a string
return value;
default:
throw new Error(`Unsupported feature flag type: ${typeof defaultValue}`);
}
}