[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 <nvitkov@chromium.org>
This commit is contained in:
Benedikt Meurer
2025-03-28 06:40:16 -07:00
parent 6353dded50
commit 1022e32645
4 changed files with 289 additions and 20 deletions
+23
View File
@@ -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.
+109
View File
@@ -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;
}
}
+112
View File
@@ -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: {}},
]);
});
});
+45 -20
View File
@@ -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');