Skip to content

Commit 1022e32

Browse files
committed
[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 <[email protected]>
1 parent 6353dde commit 1022e32

File tree

4 files changed

+290
-21
lines changed

4 files changed

+290
-21
lines changed

docs/get_the_code.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ npm start -- http://www.example.com
169169

170170
to automatically open `http://www.example.com` in the newly spawned Chrome tab.
171171

172+
173+
##### Controlling the feature set
174+
175+
By default `npm start` will enable a bunch of experimental features (related to DevTools) that are considered ready for teamfood.
176+
To also enable experimental features that aren't yet considered sufficiently stable to enable them by default for the team, run:
177+
178+
```bash
179+
# Long version
180+
npm start -- --unstable-features
181+
182+
# Short version
183+
npm start -- -u
184+
```
185+
186+
Just like with Chrome itself, you can also control the set of enabled and disabled features using
187+
188+
```bash
189+
npm start -- --enable-features=DevToolsAutomaticFileSystems
190+
npm start -- --disable-features=DevToolsWellKnown --enable-features=DevToolsFreestyler:multimodal/true
191+
```
192+
193+
which you can use to override the default feature set.
194+
172195
#### Running from file system
173196

174197
This works with Chromium 79 or later.

scripts/devtools_build.mjs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 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+
/**
6+
* Representation of the feature set that is configured for Chrome. This
7+
* keeps track of enabled and disabled features and generates the correct
8+
* combination of `--enable-features` / `--disable-features` command line
9+
* flags.
10+
*
11+
* There are unit tests for this in `./devtools_build.test.mjs`.
12+
*/
13+
export class FeatureSet {
14+
#disabled = new Set();
15+
#enabled = new Map();
16+
17+
/**
18+
* Disables the given `feature`.
19+
*
20+
* @param {string} feature the name of the feature to disable.
21+
*/
22+
disable(feature) {
23+
this.#disabled.add(feature);
24+
this.#enabled.delete(feature);
25+
}
26+
27+
/**
28+
* Enables the given `feature`, and optionally adds the `parameters` to it.
29+
* For example:
30+
* ```js
31+
* featureSet.enable('DevToolsFreestyler', {patching: true});
32+
* ```
33+
* The parameters are additive.
34+
*
35+
* @param {string} feature the name of the feature to enable.
36+
* @param {object} parameters the additional parameters to pass to it, in
37+
* the form of key/value pairs.
38+
*/
39+
enable(feature, parameters = {}) {
40+
this.#disabled.delete(feature);
41+
if (!this.#enabled.has(feature)) {
42+
this.#enabled.set(feature, Object.create(null));
43+
}
44+
for (const [key, value] of Object.entries(parameters)) {
45+
this.#enabled.get(feature)[key] = value;
46+
}
47+
}
48+
49+
/**
50+
* Merge the other `featureSet` into this.
51+
*
52+
* @param featureSet the other `FeatureSet` to apply.
53+
*/
54+
merge(featureSet) {
55+
for (const feature of featureSet.#disabled) {
56+
this.disable(feature);
57+
}
58+
for (const [feature, parameters] of featureSet.#enabled) {
59+
this.enable(feature, parameters);
60+
}
61+
}
62+
63+
/**
64+
* Yields the command line parameters to pass to the invocation of
65+
* a Chrome binary for achieving the state of the feature set.
66+
*/
67+
* [Symbol.iterator]() {
68+
const disabledFeatures = [...this.#disabled];
69+
if (disabledFeatures.length) {
70+
yield `--disable-features=${disabledFeatures.sort().join(',')}`;
71+
}
72+
const enabledFeatures = [...this.#enabled].map(([feature, parameters]) => {
73+
parameters = Object.entries(parameters);
74+
if (parameters.length) {
75+
parameters = parameters.map(([key, value]) => `${key}/${value}`);
76+
feature = `${feature}:${parameters.sort().join('/')}`;
77+
}
78+
return feature;
79+
});
80+
if (enabledFeatures.length) {
81+
yield `--enable-features=${enabledFeatures.sort().join(',')}`;
82+
}
83+
}
84+
85+
static parse(text) {
86+
const features = [];
87+
for (const str of text.split(',')) {
88+
const parts = str.split(':');
89+
if (parts.length < 1 || parts.length > 2) {
90+
throw new Error(`Invalid feature declaration '${str}'`);
91+
}
92+
const feature = parts[0];
93+
const parameters = Object.create(null);
94+
if (parts.length > 1) {
95+
const args = parts[1].split('/');
96+
if (args.length % 2 !== 0) {
97+
throw new Error(`Invalid parameters '${parts[1]}' for feature ${feature}`);
98+
}
99+
for (let i = 0; i < args.length; i += 2) {
100+
const key = args[i + 0];
101+
const value = args[i + 1];
102+
parameters[key] = value;
103+
}
104+
}
105+
features.push({feature, parameters});
106+
}
107+
return features;
108+
}
109+
}

scripts/devtools_build.test.mjs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2025 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+
// Run these tests with:
6+
//
7+
// npx mocha scripts/devtools_build.test.mjs
8+
9+
import {assert} from 'chai';
10+
11+
import {FeatureSet} from './devtools_build.mjs';
12+
13+
describe('FeatureSet', () => {
14+
it('yields an empty set of arguments by default', () => {
15+
const featureSet = new FeatureSet();
16+
17+
assert.isEmpty([...featureSet]);
18+
});
19+
20+
it('can enable features', () => {
21+
const featureSet = new FeatureSet();
22+
23+
featureSet.enable('DevToolsFreestyler');
24+
featureSet.enable('DevToolsWellKnown');
25+
26+
assert.deepEqual([...featureSet], [
27+
'--enable-features=DevToolsFreestyler,DevToolsWellKnown',
28+
]);
29+
});
30+
31+
it('can enable features with parameters', () => {
32+
const featureSet = new FeatureSet();
33+
34+
featureSet.enable('DevToolsFreestyler', {patching: true});
35+
featureSet.enable('DevToolsFreestyler', {user_tier: 'TESTERS', multimodal: true});
36+
featureSet.enable('DevToolsAiAssistancePerformanceAgent', {insights_enabled: true});
37+
38+
assert.deepEqual([...featureSet], [
39+
'--enable-features=DevToolsAiAssistancePerformanceAgent:insights_enabled/true,DevToolsFreestyler:multimodal/true/patching/true/user_tier/TESTERS',
40+
]);
41+
});
42+
43+
it('can disable features', () => {
44+
const featureSet = new FeatureSet();
45+
46+
featureSet.disable('MediaRouter');
47+
featureSet.disable('DevToolsAiGeneratedTimelineLabels');
48+
49+
assert.deepEqual([...featureSet], [
50+
'--disable-features=DevToolsAiGeneratedTimelineLabels,MediaRouter',
51+
]);
52+
});
53+
54+
it('can disable and enable unrelated features', () => {
55+
const featureSet = new FeatureSet();
56+
57+
featureSet.disable('MediaRouter');
58+
featureSet.enable('DevToolsAutomaticFileSystems');
59+
60+
assert.deepEqual([...featureSet], [
61+
'--disable-features=MediaRouter',
62+
'--enable-features=DevToolsAutomaticFileSystems',
63+
]);
64+
});
65+
66+
it('can disable previously enabled features', () => {
67+
const featureSet = new FeatureSet();
68+
69+
featureSet.enable('DevToolsFreestyler', {patching: true});
70+
featureSet.enable('DevToolsWellKnown');
71+
featureSet.disable('DevToolsFreestyler');
72+
73+
assert.deepEqual([...featureSet], [
74+
'--disable-features=DevToolsFreestyler',
75+
'--enable-features=DevToolsWellKnown',
76+
]);
77+
});
78+
79+
it('can merge feature sets', () => {
80+
const fs1 = new FeatureSet();
81+
fs1.enable('DevToolsFreestyler', {patching: true});
82+
fs1.enable('DevToolsWellKnown');
83+
fs1.disable('MediaRouter');
84+
const fs2 = new FeatureSet();
85+
fs2.disable('DevToolsWellKnown');
86+
fs2.enable('DevToolsFreestyler', {multimodal: true});
87+
88+
fs1.merge(fs2);
89+
90+
assert.deepEqual([...fs1], [
91+
'--disable-features=DevToolsWellKnown,MediaRouter',
92+
'--enable-features=DevToolsFreestyler:multimodal/true/patching/true',
93+
]);
94+
assert.deepEqual([...fs2], [
95+
'--disable-features=DevToolsWellKnown',
96+
'--enable-features=DevToolsFreestyler:multimodal/true',
97+
]);
98+
});
99+
100+
it('can parse --enable-features/--disable-features declarations', () => {
101+
assert.deepEqual(FeatureSet.parse('MediaRouter'), [
102+
{feature: 'MediaRouter', parameters: {}},
103+
]);
104+
assert.deepEqual(FeatureSet.parse('DevToolsFreestyler:multimodal/true/patching/true'), [
105+
{feature: 'DevToolsFreestyler', parameters: {multimodal: 'true', patching: 'true'}},
106+
]);
107+
assert.deepEqual(FeatureSet.parse('DevToolsFreestyler:multimodal/true,DevToolsWellKnown'), [
108+
{feature: 'DevToolsFreestyler', parameters: {multimodal: 'true'}},
109+
{feature: 'DevToolsWellKnown', parameters: {}},
110+
]);
111+
});
112+
});

scripts/run_start.mjs

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,26 @@ import path from 'node:path';
88
import yargs from 'yargs';
99
import {hideBin} from 'yargs/helpers';
1010

11+
import {FeatureSet} from './devtools_build.mjs';
1112
import {
1213
downloadedChromeBinaryPath,
1314
isInChromiumDirectory,
1415
rootPath,
1516
} from './devtools_paths.js';
1617

17-
// The list of features that are enabled by default.
18-
const ENABLE_FEATURES = [
19-
'DevToolsAiGeneratedTimelineLabels',
20-
'DevToolsAutomaticFileSystems',
21-
'DevToolsCssValueTracing',
22-
'DevToolsFreestyler:patching/true,user_tier/TESTERS',
23-
'DevToolsWellKnown',
24-
'DevToolsAiGeneratedTimelineLabels',
25-
'DevToolsAiAssistancePerformanceAgent:insights_enabled/true',
26-
];
27-
28-
// The list of features that are disabled by default.
29-
const DISABLE_FEATURES = [];
30-
if (process.platform === 'darwin') {
31-
DISABLE_FEATURES.push('MediaRouter');
32-
}
18+
// The default feature set.
19+
const DEFAULT_FEATURE_SET = new FeatureSet();
20+
process.platform === 'darwin' && DEFAULT_FEATURE_SET.disable('MediaRouter');
21+
DEFAULT_FEATURE_SET.enable('DevToolsAiAssistancePerformanceAgent', {insights_enabled: true});
22+
DEFAULT_FEATURE_SET.enable('DevToolsAiGeneratedTimelineLabels');
23+
DEFAULT_FEATURE_SET.enable('DevToolsAutomaticFileSystems');
24+
DEFAULT_FEATURE_SET.enable('DevToolsCssValueTracing');
25+
DEFAULT_FEATURE_SET.enable('DevToolsFreestyler', {patching: true, user_tier: 'TESTERS'});
26+
DEFAULT_FEATURE_SET.enable('DevToolsWellKnown');
27+
28+
// The unstable feature set (can be enabled via `--enable-unstable-features`).
29+
const UNSTABLE_FEATURE_SET = new FeatureSet();
30+
UNSTABLE_FEATURE_SET.enable('DevToolsFreestyler', {multimodal: true});
3331

3432
const argv = yargs(hideBin(process.argv))
3533
.option('browser', {
@@ -46,6 +44,22 @@ const argv = yargs(hideBin(process.argv))
4644
throw new Error(`Unsupported channel "${arg}"`);
4745
},
4846
})
47+
.option('unstable-features', {
48+
alias: 'u',
49+
type: 'boolean',
50+
default: false,
51+
description: 'Enable potentially unstable features',
52+
})
53+
.option('enable-features', {
54+
type: 'string',
55+
default: '',
56+
description: 'Enable specific features (just like with Chrome)',
57+
})
58+
.option('disable-features', {
59+
type: 'string',
60+
default: '',
61+
description: 'Disable specific features (just like with Chrome)',
62+
})
4963
.option('open', {
5064
type: 'boolean',
5165
default: true,
@@ -62,12 +76,13 @@ const argv = yargs(hideBin(process.argv))
6276
default: false,
6377
description: 'Enable verbose logging',
6478
})
79+
.group(['unstable-features', 'enable-features', 'disable-features'], 'Feature set:')
6580
.usage('npm start -- [options] [urls...]')
6681
.help('help')
6782
.version(false)
6883
.parseSync();
6984

70-
const {browser, target, open, verbose} = argv;
85+
const {browser, disableFeatures, enableFeatures, unstableFeatures, open, target, verbose} = argv;
7186
const cwd = process.cwd();
7287
const {env} = process;
7388
const runBuildPath = path.join(import.meta.dirname, 'run_build.mjs');
@@ -94,7 +109,7 @@ function findBrowserBinary() {
94109
}
95110

96111
if (verbose) {
97-
console.debug(`Launching custom binary at ${binary}.`);
112+
console.debug(`Launching custom binary at ${browser}.`);
98113
}
99114
return browser;
100115
}
@@ -121,9 +136,19 @@ function start() {
121136
args.push('--use-mock-keychain');
122137
}
123138

124-
// Disable/Enable experimental features.
125-
args.push(`--disable-features=${DISABLE_FEATURES.join(',')}`);
126-
args.push(`--enable-features=${ENABLE_FEATURES.join(',')}`);
139+
// Disable/Enable features.
140+
const featureSet = new FeatureSet();
141+
featureSet.merge(DEFAULT_FEATURE_SET);
142+
if (unstableFeatures) {
143+
featureSet.merge(UNSTABLE_FEATURE_SET);
144+
}
145+
for (const {feature} of FeatureSet.parse(disableFeatures)) {
146+
featureSet.disable(feature);
147+
}
148+
for (const {feature, parameters} of FeatureSet.parse(enableFeatures)) {
149+
featureSet.enable(feature, parameters);
150+
}
151+
args.push(...featureSet);
127152

128153
// Open with our freshly built DevTools front-end.
129154
const genDir = path.join(rootPath(), 'out', target, 'gen');

0 commit comments

Comments
 (0)