|
| 1 | +// Copyright 2024 The Chromium Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +// Copyright (c) Meta Platforms, Inc. and affiliates. |
| 6 | + |
| 7 | +// Chrome DevTools has an experiment system integrated deeply with its panel |
| 8 | +// framework and settings UI. We add some React Native-specific experiments, |
| 9 | +// some of which control new RN-specific UI and some of which add gating to |
| 10 | +// *existing* features. |
| 11 | +// |
| 12 | +// The goals are: |
| 13 | +// 1. To allow the core, non-RN entry points (like `inspector.ts`) to continue |
| 14 | +// to work, largely unmodified, for ease of testing. |
| 15 | +// 2. To allow users of each entry point to enable or disable experiments as |
| 16 | +// needed through the UI. |
| 17 | +// 3. To only show experiments in Settings if they are relevant to the current |
| 18 | +// entry point. |
| 19 | +// 4. To minimise RN-specific changes to core code, for ease of rebasing onto |
| 20 | +// new versions of Chrome DevTools. |
| 21 | +// 5. To allow RN entry points to enable/configure *core* experiments before |
| 22 | +// they are registered (in MainImpl). |
| 23 | +// |
| 24 | +// To add a new React Native-specific experiment: |
| 25 | +// - define it in the RNExperiments enum and Experiments enum (in Runtime.ts) |
| 26 | +// - register it in this file (rn_experiments.ts) |
| 27 | +// - set `enabledByDefault` and `configurable` as appropriate |
| 28 | +// - optionally, configure it further in each RN-specific entry point |
| 29 | +// (rn_inspector.ts, rn_fusebox.ts) |
| 30 | +// |
| 31 | +// React Native-specific experiments are merged into the main ExperimentsSupport |
| 32 | +// object in MainImpl and can't be configured further afterwards (except |
| 33 | +// through the UI). |
| 34 | + |
| 35 | +import * as Root from '../../core/root/root.js'; |
| 36 | + |
| 37 | +export const RNExperimentName = Root.Runtime.RNExperimentName; |
| 38 | +export type RNExperimentName = Root.Runtime.RNExperimentName; |
| 39 | + |
| 40 | +const state = { |
| 41 | + didInitializeExperiments: false, |
| 42 | + isReactNativeEntryPoint: false, |
| 43 | +}; |
| 44 | + |
| 45 | +/** |
| 46 | + * Set whether the current entry point is a React Native entry point. |
| 47 | + * This must be called before constructing MainImpl. |
| 48 | + */ |
| 49 | +export function setIsReactNativeEntryPoint(value: boolean) { |
| 50 | + if (state.didInitializeExperiments) { |
| 51 | + throw new Error( |
| 52 | + 'setIsReactNativeEntryPoint must be called before constructing MainImpl' |
| 53 | + ); |
| 54 | + } |
| 55 | + state.isReactNativeEntryPoint = value; |
| 56 | +} |
| 57 | + |
| 58 | +type RNExperimentPredicate = ({ |
| 59 | + isReactNativeEntryPoint, |
| 60 | +}: { |
| 61 | + isReactNativeEntryPoint: boolean; |
| 62 | +}) => boolean; |
| 63 | +type RNExperimentSpec = { |
| 64 | + name: RNExperimentName; |
| 65 | + title: string; |
| 66 | + unstable: boolean; |
| 67 | + docLink?: string; |
| 68 | + feedbackLink?: string; |
| 69 | + enabledByDefault?: boolean | RNExperimentPredicate; |
| 70 | + configurable?: boolean | RNExperimentPredicate; |
| 71 | +}; |
| 72 | + |
| 73 | +class RNExperiment { |
| 74 | + readonly name: RNExperimentName; |
| 75 | + readonly title: string; |
| 76 | + readonly unstable: boolean; |
| 77 | + readonly docLink?: string; |
| 78 | + readonly feedbackLink?: string; |
| 79 | + enabledByDefault: RNExperimentPredicate; |
| 80 | + configurable: RNExperimentPredicate; |
| 81 | + |
| 82 | + constructor(spec: RNExperimentSpec) { |
| 83 | + this.name = spec.name; |
| 84 | + this.title = spec.title; |
| 85 | + this.unstable = spec.unstable; |
| 86 | + this.docLink = spec.docLink; |
| 87 | + this.feedbackLink = spec.feedbackLink; |
| 88 | + this.enabledByDefault = normalizePredicate(spec.enabledByDefault, false); |
| 89 | + this.configurable = normalizePredicate(spec.configurable, true); |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +function normalizePredicate( |
| 94 | + pred: boolean | null | undefined | RNExperimentPredicate, |
| 95 | + defaultValue: boolean |
| 96 | +): RNExperimentPredicate { |
| 97 | + if (pred == null) { |
| 98 | + return () => defaultValue; |
| 99 | + } |
| 100 | + if (typeof pred === 'boolean') { |
| 101 | + return () => pred; |
| 102 | + } |
| 103 | + return pred; |
| 104 | +} |
| 105 | + |
| 106 | +class RNExperimentsSupport { |
| 107 | + #experiments: Map<Root.Runtime.RNExperimentName, RNExperiment> = new Map(); |
| 108 | + #defaultEnabledCoreExperiments = new Set<Root.Runtime.ExperimentName>(); |
| 109 | + #nonConfigurableCoreExperiments = new Set<Root.Runtime.ExperimentName>(); |
| 110 | + |
| 111 | + register(spec: RNExperimentSpec): void { |
| 112 | + if (state.didInitializeExperiments) { |
| 113 | + throw new Error( |
| 114 | + 'Experiments must be registered before constructing MainImpl' |
| 115 | + ); |
| 116 | + } |
| 117 | + const { name } = spec; |
| 118 | + if (this.#experiments.has(name)) { |
| 119 | + throw new Error(`React Native Experiment ${name} is already registered`); |
| 120 | + } |
| 121 | + this.#experiments.set(name, new RNExperiment(spec)); |
| 122 | + } |
| 123 | + |
| 124 | + /** |
| 125 | + * Enable the given (RN-specific or core) experiments by default. |
| 126 | + */ |
| 127 | + enableExperimentsByDefault(names: Root.Runtime.ExperimentName[]) { |
| 128 | + if (state.didInitializeExperiments) { |
| 129 | + throw new Error( |
| 130 | + 'Experiments must be configured before constructing MainImpl' |
| 131 | + ); |
| 132 | + } |
| 133 | + for (const name of names) { |
| 134 | + if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) { |
| 135 | + const experiment = this.#experiments.get( |
| 136 | + name as unknown as RNExperimentName |
| 137 | + ); |
| 138 | + if (!experiment) { |
| 139 | + throw new Error(`React Native Experiment ${name} is not registered`); |
| 140 | + } |
| 141 | + experiment.enabledByDefault = () => true; |
| 142 | + } else { |
| 143 | + this.#defaultEnabledCoreExperiments.add( |
| 144 | + name as Root.Runtime.ExperimentName |
| 145 | + ); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + /** |
| 151 | + * Set the given (RN-specific or core) experiments to be non-configurable. |
| 152 | + */ |
| 153 | + setNonConfigurableExperiments(names: Root.Runtime.ExperimentName[]) { |
| 154 | + if (state.didInitializeExperiments) { |
| 155 | + throw new Error( |
| 156 | + 'Experiments must be configured before constructing MainImpl' |
| 157 | + ); |
| 158 | + } |
| 159 | + for (const name of names) { |
| 160 | + if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) { |
| 161 | + const experiment = this.#experiments.get( |
| 162 | + name as unknown as RNExperimentName |
| 163 | + ); |
| 164 | + if (!experiment) { |
| 165 | + throw new Error(`React Native Experiment ${name} is not registered`); |
| 166 | + } |
| 167 | + experiment.configurable = () => false; |
| 168 | + } else { |
| 169 | + this.#nonConfigurableCoreExperiments.add( |
| 170 | + name as Root.Runtime.ExperimentName |
| 171 | + ); |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + copyInto(other: Root.Runtime.ExperimentsSupport, titlePrefix: string = ''): void { |
| 177 | + for (const [name, spec] of this.#experiments) { |
| 178 | + other.register( |
| 179 | + name, |
| 180 | + titlePrefix + spec.title, |
| 181 | + spec.unstable, |
| 182 | + spec.docLink, |
| 183 | + spec.feedbackLink |
| 184 | + ); |
| 185 | + if ( |
| 186 | + spec.enabledByDefault({ |
| 187 | + isReactNativeEntryPoint: state.isReactNativeEntryPoint, |
| 188 | + }) |
| 189 | + ) { |
| 190 | + other.enableExperimentsByDefault([name]); |
| 191 | + } |
| 192 | + if ( |
| 193 | + !spec.configurable({ |
| 194 | + isReactNativeEntryPoint: state.isReactNativeEntryPoint, |
| 195 | + }) |
| 196 | + ) { |
| 197 | + other.setNonConfigurableExperiments([name]); |
| 198 | + } |
| 199 | + } |
| 200 | + for (const name of this.#defaultEnabledCoreExperiments) { |
| 201 | + other.enableExperimentsByDefault([name]); |
| 202 | + } |
| 203 | + for (const name of this.#nonConfigurableCoreExperiments) { |
| 204 | + other.setNonConfigurableExperiments([name]); |
| 205 | + } |
| 206 | + state.didInitializeExperiments = true; |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +// Early registration for React Native-specific experiments. Only use this |
| 211 | +// *before* constructing MainImpl; afterwards read from Root.Runtime.experiments |
| 212 | +// as normal. |
| 213 | +export const RNExperiments = new RNExperimentsSupport(); |
| 214 | + |
| 215 | +RNExperiments.register({ |
| 216 | + name: RNExperimentName.JS_HEAP_PROFILER_ENABLE, |
| 217 | + title: 'Enable Heap Profiler', |
| 218 | + unstable: false, |
| 219 | + enabledByDefault: ({ isReactNativeEntryPoint }) => !isReactNativeEntryPoint, |
| 220 | + configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, |
| 221 | +}); |
| 222 | + |
| 223 | +RNExperiments.register({ |
| 224 | + name: RNExperimentName.ENABLE_REACT_DEVTOOLS_PANEL, |
| 225 | + title: 'Enable React DevTools panel', |
| 226 | + unstable: true, |
| 227 | + enabledByDefault: false, |
| 228 | + configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, |
| 229 | +}); |
| 230 | + |
| 231 | +RNExperiments.register({ |
| 232 | + name: RNExperimentName.REACT_NATIVE_SPECIFIC_UI, |
| 233 | + title: 'Show React Native-specific UI', |
| 234 | + unstable: false, |
| 235 | + enabledByDefault: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, |
| 236 | + configurable: false, |
| 237 | +}); |
0 commit comments