chrome-devtools-frontend
Version:
Chrome DevTools UI
214 lines (209 loc) • 7.83 kB
text/typescript
// 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.
/**
* @fileoverview A library to identify and templatize UI.UIUtils and related calls.
*/
import type {TSESTree} from '@typescript-eslint/utils';
import {isIdentifier, isIdentifierChain, isMemberExpression} from './ast.ts';
import {DomFragment} from './dom-fragment.ts';
type CallExpression = TSESTree.CallExpression;
type Node = TSESTree.Node;
export const uiUtils = {
create(context) {
const sourceCode = context.getSourceCode();
return {
CallExpression(node: CallExpression) {
let func = isMemberExpression(
node.callee, n => isIdentifierChain(n, ['UI', 'UIUtils', 'CheckboxLabel']),
n => isIdentifier(n, ['create', 'createWithStringLiteral']));
if (func) {
const domFragment = DomFragment.getOrCreate(node, sourceCode);
domFragment.tagName = 'devtools-checkbox';
const args = node.arguments;
const title = args.shift();
if (title && !isIdentifier(title, 'undefined')) {
domFragment.textContent = title;
}
const checked = args.shift();
if (checked && !isIdentifier(checked, 'undefined')) {
domFragment.booleanAttributes.push({
key: 'checked',
value: checked,
});
}
if (isIdentifier(func, 'create')) {
args.shift(); // TODO(b/348173254): Support subtitle
}
const jslogContext = args.shift();
if (jslogContext && !isIdentifier(jslogContext, 'undefined')) {
domFragment.bindings.push({
key: 'jslogContext',
value: jslogContext,
});
}
const small = args.shift();
if (small && !isIdentifier(small, 'undefined')) {
domFragment.classList.push('small');
}
}
func =
isMemberExpression(node.callee, n => isIdentifierChain(n, ['UI', 'UIUtils']), n => n.type === 'Identifier');
if (!func) {
return;
}
if (isIdentifier(func, 'createLabel')) {
const domFragment = DomFragment.getOrCreate(node, sourceCode);
domFragment.tagName = 'label';
const title = node.arguments[0];
if (title) {
domFragment.textContent = title;
}
const className = node.arguments[1];
if (className) {
domFragment.classList.push(className);
}
const associatedControl = node.arguments[2];
if (associatedControl) {
domFragment.appendChild(associatedControl, sourceCode);
}
}
if (isIdentifier(func, 'createTextButton')) {
const opts = node.arguments[2];
if (opts && opts.type !== 'ObjectExpression') {
return;
}
const domFragment = DomFragment.getOrCreate(node, sourceCode);
domFragment.tagName = 'devtools-button';
const text = node.arguments[0];
if (text) {
domFragment.textContent = text;
}
const clickHandler = node.arguments[1];
if (clickHandler) {
domFragment.eventListeners.push({
key: 'click',
value: clickHandler,
});
}
let variant: string|Node = 'Buttons.Button.Variant.OUTLINED';
if (opts) {
for (const property of opts.properties) {
if (property.type !== 'Property') {
continue;
}
if (isIdentifier(property.key, 'className')) {
domFragment.classList.push(property.value);
}
if (isIdentifier(property.key, 'jslogContext')) {
domFragment.bindings.push({
key: 'jslogContext',
value: property.value,
});
}
if (isIdentifier(property.key, 'variant')) {
variant = property.value;
}
if (isIdentifier(property.key, 'title')) {
domFragment.attributes.push({
key: 'title',
value: property.value,
});
}
if (isIdentifier(property.key, 'icon')) {
domFragment.bindings.push({
key: 'iconName',
value: property.value,
});
}
}
}
domFragment.bindings.push({
key: 'variant',
value: variant,
});
}
if (isIdentifier(func, 'createInput')) {
const domFragment = DomFragment.getOrCreate(node, sourceCode);
domFragment.tagName = 'input';
domFragment.attributes.push({
key: 'spellcheck',
value: 'false',
});
domFragment.classList.push('harmony-input');
const className = node.arguments[0];
if (className && !isIdentifier(className, 'undefined')) {
domFragment.classList.push(className);
}
const type = node.arguments[1];
if (type && !isIdentifier(type, 'undefined')) {
domFragment.attributes.push({
key: 'type',
value: type,
});
}
const jslogContext = node.arguments[2];
if (jslogContext && !isIdentifier(jslogContext, 'undefined')) {
domFragment.attributes.push({
key: 'jslog',
value: '${VisualLogging.textField(' + sourceCode.getText(jslogContext) +
').track({keydown: \'Enter\', change: true})}'
});
}
}
if (isIdentifier(func, 'createOption')) {
const domFragment = DomFragment.getOrCreate(node, sourceCode);
domFragment.tagName = 'option';
const title = node.arguments[0];
if (title) {
domFragment.textContent = title;
}
const value = node.arguments[1];
if (value) {
domFragment.attributes.push({
key: 'value',
value,
});
}
const jslogContext = node.arguments[1];
if (jslogContext) {
domFragment.attributes.push({
key: 'jslog',
value: '${VisualLogging.dropDown(' + sourceCode.getText(jslogContext) + ').track({click: true})}',
});
}
}
},
functionCall(call: CallExpression, _firstArg: Node, secondArg: Node, domFragment: DomFragment): boolean {
if (isIdentifierChain(call.callee, ['UI', 'SettingsUI', 'bindCheckbox'])) {
let setting = secondArg;
if (setting.type === 'CallExpression' &&
isMemberExpression(
setting.callee,
n => n.type === 'CallExpression' &&
isIdentifierChain(n.callee, ['Common', 'Settings', 'Settings', 'instance']),
n => isIdentifier(n, 'moduleSetting'))) {
setting = setting.arguments[0];
}
domFragment.directives.push({
name: 'bindToSetting',
arguments: [setting],
});
return true;
}
if (isIdentifierChain(call.callee, ['UI', 'UIUtils', 'createTextChild'])) {
domFragment.textContent = secondArg;
return true;
}
if (isIdentifierChain(call.callee, ['UI', 'Tooltip', 'Tooltip', 'install'])) {
domFragment.attributes.push({
key: 'title',
value: secondArg,
});
return true;
}
return false;
},
};
}
};