From 1022e326455ab47f171797c4b7f66bce3d1697f5 Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Fri, 28 Mar 2025 14:39:58 +0100 Subject: [PATCH] [npm] Allow to select (unstable) features with `npm start`. This adds an option `--enable-unstable-features`, which now also turns on multimodal support for Freestyler. It also adds `--enable-features` and `--disable-features`, to allow more fine-grained control over the exact feature set when necessary. Fixed: 406941932 Change-Id: I7fcecbba833a2e0a66166890aa5af218ae2a8f7e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6404318 Reviewed-by: Nikolay Vitkov --- docs/get_the_code.md | 23 +++++++ scripts/devtools_build.mjs | 109 +++++++++++++++++++++++++++++++ scripts/devtools_build.test.mjs | 112 ++++++++++++++++++++++++++++++++ scripts/run_start.mjs | 65 ++++++++++++------ 4 files changed, 289 insertions(+), 20 deletions(-) create mode 100644 scripts/devtools_build.mjs create mode 100644 scripts/devtools_build.test.mjs diff --git a/docs/get_the_code.md b/docs/get_the_code.md index 04db549691..e4048aa232 100644 --- a/docs/get_the_code.md +++ b/docs/get_the_code.md @@ -169,6 +169,29 @@ npm start -- http://www.example.com to automatically open `http://www.example.com` in the newly spawned Chrome tab. + +##### Controlling the feature set + +By default `npm start` will enable a bunch of experimental features (related to DevTools) that are considered ready for teamfood. +To also enable experimental features that aren't yet considered sufficiently stable to enable them by default for the team, run: + +```bash +# Long version +npm start -- --unstable-features + +# Short version +npm start -- -u +``` + +Just like with Chrome itself, you can also control the set of enabled and disabled features using + +```bash +npm start -- --enable-features=DevToolsAutomaticFileSystems +npm start -- --disable-features=DevToolsWellKnown --enable-features=DevToolsFreestyler:multimodal/true +``` + +which you can use to override the default feature set. + #### Running from file system This works with Chromium 79 or later. diff --git a/scripts/devtools_build.mjs b/scripts/devtools_build.mjs new file mode 100644 index 0000000000..473b1970d1 --- /dev/null +++ b/scripts/devtools_build.mjs @@ -0,0 +1,109 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Representation of the feature set that is configured for Chrome. This + * keeps track of enabled and disabled features and generates the correct + * combination of `--enable-features` / `--disable-features` command line + * flags. + * + * There are unit tests for this in `./devtools_build.test.mjs`. + */ +export class FeatureSet { + #disabled = new Set(); + #enabled = new Map(); + + /** + * Disables the given `feature`. + * + * @param {string} feature the name of the feature to disable. + */ + disable(feature) { + this.#disabled.add(feature); + this.#enabled.delete(feature); + } + + /** + * Enables the given `feature`, and optionally adds the `parameters` to it. + * For example: + * ```js + * featureSet.enable('DevToolsFreestyler', {patching: true}); + * ``` + * The parameters are additive. + * + * @param {string} feature the name of the feature to enable. + * @param {object} parameters the additional parameters to pass to it, in + * the form of key/value pairs. + */ + enable(feature, parameters = {}) { + this.#disabled.delete(feature); + if (!this.#enabled.has(feature)) { + this.#enabled.set(feature, Object.create(null)); + } + for (const [key, value] of Object.entries(parameters)) { + this.#enabled.get(feature)[key] = value; + } + } + + /** + * Merge the other `featureSet` into this. + * + * @param featureSet the other `FeatureSet` to apply. + */ + merge(featureSet) { + for (const feature of featureSet.#disabled) { + this.disable(feature); + } + for (const [feature, parameters] of featureSet.#enabled) { + this.enable(feature, parameters); + } + } + + /** + * Yields the command line parameters to pass to the invocation of + * a Chrome binary for achieving the state of the feature set. + */ + * [Symbol.iterator]() { + const disabledFeatures = [...this.#disabled]; + if (disabledFeatures.length) { + yield `--disable-features=${disabledFeatures.sort().join(',')}`; + } + const enabledFeatures = [...this.#enabled].map(([feature, parameters]) => { + parameters = Object.entries(parameters); + if (parameters.length) { + parameters = parameters.map(([key, value]) => `${key}/${value}`); + feature = `${feature}:${parameters.sort().join('/')}`; + } + return feature; + }); + if (enabledFeatures.length) { + yield `--enable-features=${enabledFeatures.sort().join(',')}`; + } + } + + static parse(text) { + const features = []; + for (const str of text.split(',')) { + const parts = str.split(':'); + if (parts.length < 1 || parts.length > 2) { + throw new Error(`Invalid feature declaration '${str}'`); + } + const feature = parts[0]; + const parameters = Object.create(null); + if (parts.length > 1) { + const args = parts[1].split('/'); + if (args.length % 2 !== 0) { + throw new Error(`Invalid parameters '${parts[1]}' for feature ${feature}`); + } + for (let i = 0; i < args.length; i += 2) { + const key = args[i + 0]; + const value = args[i + 1]; + parameters[key] = value; + } + } + features.push({feature, parameters}); + } + return features; + } +} diff --git a/scripts/devtools_build.test.mjs b/scripts/devtools_build.test.mjs new file mode 100644 index 0000000000..649e2cb2ae --- /dev/null +++ b/scripts/devtools_build.test.mjs @@ -0,0 +1,112 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Run these tests with: +// +// npx mocha scripts/devtools_build.test.mjs + +import {assert} from 'chai'; + +import {FeatureSet} from './devtools_build.mjs'; + +describe('FeatureSet', () => { + it('yields an empty set of arguments by default', () => { + const featureSet = new FeatureSet(); + + assert.isEmpty([...featureSet]); + }); + + it('can enable features', () => { + const featureSet = new FeatureSet(); + + featureSet.enable('DevToolsFreestyler'); + featureSet.enable('DevToolsWellKnown'); + + assert.deepEqual([...featureSet], [ + '--enable-features=DevToolsFreestyler,DevToolsWellKnown', + ]); + }); + + it('can enable features with parameters', () => { + const featureSet = new FeatureSet(); + + featureSet.enable('DevToolsFreestyler', {patching: true}); + featureSet.enable('DevToolsFreestyler', {user_tier: 'TESTERS', multimodal: true}); + featureSet.enable('DevToolsAiAssistancePerformanceAgent', {insights_enabled: true}); + + assert.deepEqual([...featureSet], [ + '--enable-features=DevToolsAiAssistancePerformanceAgent:insights_enabled/true,DevToolsFreestyler:multimodal/true/patching/true/user_tier/TESTERS', + ]); + }); + + it('can disable features', () => { + const featureSet = new FeatureSet(); + + featureSet.disable('MediaRouter'); + featureSet.disable('DevToolsAiGeneratedTimelineLabels'); + + assert.deepEqual([...featureSet], [ + '--disable-features=DevToolsAiGeneratedTimelineLabels,MediaRouter', + ]); + }); + + it('can disable and enable unrelated features', () => { + const featureSet = new FeatureSet(); + + featureSet.disable('MediaRouter'); + featureSet.enable('DevToolsAutomaticFileSystems'); + + assert.deepEqual([...featureSet], [ + '--disable-features=MediaRouter', + '--enable-features=DevToolsAutomaticFileSystems', + ]); + }); + + it('can disable previously enabled features', () => { + const featureSet = new FeatureSet(); + + featureSet.enable('DevToolsFreestyler', {patching: true}); + featureSet.enable('DevToolsWellKnown'); + featureSet.disable('DevToolsFreestyler'); + + assert.deepEqual([...featureSet], [ + '--disable-features=DevToolsFreestyler', + '--enable-features=DevToolsWellKnown', + ]); + }); + + it('can merge feature sets', () => { + const fs1 = new FeatureSet(); + fs1.enable('DevToolsFreestyler', {patching: true}); + fs1.enable('DevToolsWellKnown'); + fs1.disable('MediaRouter'); + const fs2 = new FeatureSet(); + fs2.disable('DevToolsWellKnown'); + fs2.enable('DevToolsFreestyler', {multimodal: true}); + + fs1.merge(fs2); + + assert.deepEqual([...fs1], [ + '--disable-features=DevToolsWellKnown,MediaRouter', + '--enable-features=DevToolsFreestyler:multimodal/true/patching/true', + ]); + assert.deepEqual([...fs2], [ + '--disable-features=DevToolsWellKnown', + '--enable-features=DevToolsFreestyler:multimodal/true', + ]); + }); + + it('can parse --enable-features/--disable-features declarations', () => { + assert.deepEqual(FeatureSet.parse('MediaRouter'), [ + {feature: 'MediaRouter', parameters: {}}, + ]); + assert.deepEqual(FeatureSet.parse('DevToolsFreestyler:multimodal/true/patching/true'), [ + {feature: 'DevToolsFreestyler', parameters: {multimodal: 'true', patching: 'true'}}, + ]); + assert.deepEqual(FeatureSet.parse('DevToolsFreestyler:multimodal/true,DevToolsWellKnown'), [ + {feature: 'DevToolsFreestyler', parameters: {multimodal: 'true'}}, + {feature: 'DevToolsWellKnown', parameters: {}}, + ]); + }); +}); diff --git a/scripts/run_start.mjs b/scripts/run_start.mjs index 9eda032cdd..a50607595a 100644 --- a/scripts/run_start.mjs +++ b/scripts/run_start.mjs @@ -8,28 +8,26 @@ import path from 'node:path'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; +import {FeatureSet} from './devtools_build.mjs'; import { downloadedChromeBinaryPath, isInChromiumDirectory, rootPath, } from './devtools_paths.js'; -// The list of features that are enabled by default. -const ENABLE_FEATURES = [ - 'DevToolsAiGeneratedTimelineLabels', - 'DevToolsAutomaticFileSystems', - 'DevToolsCssValueTracing', - 'DevToolsFreestyler:patching/true,user_tier/TESTERS', - 'DevToolsWellKnown', - 'DevToolsAiGeneratedTimelineLabels', - 'DevToolsAiAssistancePerformanceAgent:insights_enabled/true', -]; +// The default feature set. +const DEFAULT_FEATURE_SET = new FeatureSet(); +process.platform === 'darwin' && DEFAULT_FEATURE_SET.disable('MediaRouter'); +DEFAULT_FEATURE_SET.enable('DevToolsAiAssistancePerformanceAgent', {insights_enabled: true}); +DEFAULT_FEATURE_SET.enable('DevToolsAiGeneratedTimelineLabels'); +DEFAULT_FEATURE_SET.enable('DevToolsAutomaticFileSystems'); +DEFAULT_FEATURE_SET.enable('DevToolsCssValueTracing'); +DEFAULT_FEATURE_SET.enable('DevToolsFreestyler', {patching: true, user_tier: 'TESTERS'}); +DEFAULT_FEATURE_SET.enable('DevToolsWellKnown'); -// The list of features that are disabled by default. -const DISABLE_FEATURES = []; -if (process.platform === 'darwin') { - DISABLE_FEATURES.push('MediaRouter'); -} +// The unstable feature set (can be enabled via `--enable-unstable-features`). +const UNSTABLE_FEATURE_SET = new FeatureSet(); +UNSTABLE_FEATURE_SET.enable('DevToolsFreestyler', {multimodal: true}); const argv = yargs(hideBin(process.argv)) .option('browser', { @@ -46,6 +44,22 @@ const argv = yargs(hideBin(process.argv)) throw new Error(`Unsupported channel "${arg}"`); }, }) + .option('unstable-features', { + alias: 'u', + type: 'boolean', + default: false, + description: 'Enable potentially unstable features', + }) + .option('enable-features', { + type: 'string', + default: '', + description: 'Enable specific features (just like with Chrome)', + }) + .option('disable-features', { + type: 'string', + default: '', + description: 'Disable specific features (just like with Chrome)', + }) .option('open', { type: 'boolean', default: true, @@ -62,12 +76,13 @@ const argv = yargs(hideBin(process.argv)) default: false, description: 'Enable verbose logging', }) + .group(['unstable-features', 'enable-features', 'disable-features'], 'Feature set:') .usage('npm start -- [options] [urls...]') .help('help') .version(false) .parseSync(); -const {browser, target, open, verbose} = argv; +const {browser, disableFeatures, enableFeatures, unstableFeatures, open, target, verbose} = argv; const cwd = process.cwd(); const {env} = process; const runBuildPath = path.join(import.meta.dirname, 'run_build.mjs'); @@ -94,7 +109,7 @@ function findBrowserBinary() { } if (verbose) { - console.debug(`Launching custom binary at ${binary}.`); + console.debug(`Launching custom binary at ${browser}.`); } return browser; } @@ -121,9 +136,19 @@ function start() { args.push('--use-mock-keychain'); } - // Disable/Enable experimental features. - args.push(`--disable-features=${DISABLE_FEATURES.join(',')}`); - args.push(`--enable-features=${ENABLE_FEATURES.join(',')}`); + // Disable/Enable features. + const featureSet = new FeatureSet(); + featureSet.merge(DEFAULT_FEATURE_SET); + if (unstableFeatures) { + featureSet.merge(UNSTABLE_FEATURE_SET); + } + for (const {feature} of FeatureSet.parse(disableFeatures)) { + featureSet.disable(feature); + } + for (const {feature, parameters} of FeatureSet.parse(enableFeatures)) { + featureSet.enable(feature, parameters); + } + args.push(...featureSet); // Open with our freshly built DevTools front-end. const genDir = path.join(rootPath(), 'out', target, 'gen');