Skip to content

Commit 9901bf0

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[ui] Add scaffold widget script
Adds a new script to scaffold UI widgets, automating the initial boilerplate and structural setup. This would help us build widgets faster and more consistently, in line with the UI engineering vision Bug: none Change-Id: I79d61be5b4ab3ad6d899346fded9425e816a79a0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6556183 Reviewed-by: Danil Somsikov <dsv@chromium.org> Commit-Queue: Ergün Erdoğmuş <ergunsh@chromium.org>
1 parent a684598 commit 9901bf0

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed

scripts/scaffold/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Scaffold script
2+
3+
This directory contains scripts designed to automate the creation of boilerplate code for various components and
4+
modules with Chrome DevTools.

scripts/scaffold/scaffold-widget.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env node
2+
3+
// Copyright 2025 The Chromium Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style license that can be
5+
// found in the LICENSE file.
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
/**
11+
* Converts a string to camelCase.
12+
* e.g., "my-component-name" -> "myComponentName"
13+
* e.g., "MyComponentName" -> "myComponentName"
14+
* @param {string} str
15+
* @returns {string}
16+
*/
17+
function toCamelCase(str) {
18+
if (!str) {
19+
return '';
20+
}
21+
// Normalize to handle various inputs (kebab, snake, space, Pascal)
22+
const s = str.replace(/[-_.\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, m => m.toLowerCase());
23+
return s.charAt(0).toLowerCase() + s.substring(1);
24+
}
25+
26+
/**
27+
* Converts a string to PascalCase.
28+
* e.g., "my-component-name" -> "MyComponentName"
29+
* e.g., "myComponentName" -> "MyComponentName"
30+
* @param {string} str
31+
* @returns {string}
32+
*/
33+
function toPascalCase(str) {
34+
if (!str) {
35+
return '';
36+
}
37+
// Normalize to handle various inputs (kebab, snake, space, camel)
38+
const s = str.replace(/[-_.\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, m => m.toUpperCase());
39+
return s.charAt(0).toUpperCase() + s.substring(1);
40+
}
41+
42+
/**
43+
* Converts a string to kebab-case.
44+
* e.g., "MyComponentName" -> "my-component-name"
45+
* e.g., "myComponentName" -> "my-component-name"
46+
* @param {string} str
47+
* @returns {string}
48+
*/
49+
function toKebabCase(str) {
50+
if (!str) {
51+
return '';
52+
}
53+
return str
54+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2') // Add hyphen before capital in camelCase or PascalCase
55+
.replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
56+
.toLowerCase();
57+
}
58+
59+
// --- Main Script Logic ---
60+
61+
function main() {
62+
const args = process.argv.slice(2);
63+
if (args.length !== 2) {
64+
console.error('Usage: node scaffold-widget.js <path-to-create-component> <ComponentName>');
65+
console.error('Example: node scaffold-widget.js front_end/panels/ai_assistance ChatInputWidget');
66+
process.exit(1);
67+
}
68+
69+
const componentDestPath = args[0];
70+
const componentBaseName = args[1];
71+
72+
// --- Derive Name Variations ---
73+
const pascalCaseName = toPascalCase(componentBaseName); // e.g., MyNewWidget
74+
const camelCaseName = toCamelCase(componentBaseName); // e.g., myNewWidget
75+
const kebabCaseName = toKebabCase(componentBaseName); // e.g., my-new-widget
76+
77+
// --- Define Replacements ---
78+
const currentYear = new Date().getFullYear().toString();
79+
const componentPathAbsolute = path.resolve(componentDestPath);
80+
const frontEndPathAbsolute = path.resolve(process.cwd(), 'front_end');
81+
const frontEndPathForImports =
82+
path.relative(componentPathAbsolute, frontEndPathAbsolute).replace(/\\/g, '/'); // Normalize to forward slashes
83+
84+
const tsReplacements = {
85+
'{{DATE}}': currentYear,
86+
'{{FRONT_END_PATH_PREFIX}}': frontEndPathForImports,
87+
'{{COMPONENT_PATH_PREFIX}}': componentDestPath.replace(/\\/g, '/'), // Use forward slashes for paths in code
88+
'{{COMPONENT_NAME_PASCAL_CASE}}': pascalCaseName,
89+
'{{COMPONENT_NAME_CAMEL_CASE}}': camelCaseName, // Used for style var and import path
90+
'{{COMPONENT_NAME_KEBAP_CASE}}': kebabCaseName,
91+
};
92+
93+
const cssReplacements = {
94+
'{{DATE}}': currentYear,
95+
'{{COMPONENT_NAME_KEBAP_CASE}}': kebabCaseName,
96+
};
97+
98+
// --- Read Template Files ---
99+
let tsTemplateContent;
100+
let cssTemplateContent;
101+
102+
try {
103+
tsTemplateContent = fs.readFileSync(path.resolve(__dirname, 'templates', 'WidgetTemplate.ts.txt'), 'utf8');
104+
cssTemplateContent = fs.readFileSync(path.resolve(__dirname, 'templates', 'WidgetTemplate.css.txt'), 'utf8');
105+
} catch (error) {
106+
console.error('Error reading template files (WidgetTemplate.ts.txt or WidgetTemplate.css.txt):', error.message);
107+
console.error('Make sure these files are in a "templates" subdirectory where the script is run.');
108+
process.exit(1);
109+
}
110+
111+
// --- Process Templates ---
112+
let processedTsContent = tsTemplateContent;
113+
for (const placeholder in tsReplacements) {
114+
processedTsContent = processedTsContent.replace(
115+
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), tsReplacements[placeholder]);
116+
}
117+
118+
let processedCssContent = cssTemplateContent;
119+
for (const placeholder in cssReplacements) {
120+
processedCssContent = processedCssContent.replace(
121+
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), cssReplacements[placeholder]);
122+
}
123+
124+
// --- Write Output Files ---
125+
const outputTsFilename = `${pascalCaseName}.ts`;
126+
const outputCssFilename = `${camelCaseName}.css`;
127+
128+
const outputTsFilePath = path.join(componentDestPath, outputTsFilename);
129+
const outputCssFilePath = path.join(componentDestPath, outputCssFilename);
130+
131+
try {
132+
// Ensure the destination directory exists
133+
fs.mkdirSync(componentDestPath, {recursive: true});
134+
135+
// Write the processed TypeScript file
136+
fs.writeFileSync(outputTsFilePath, processedTsContent);
137+
console.log(`Successfully created: ${outputTsFilePath}`);
138+
139+
// Write the processed CSS file
140+
fs.writeFileSync(outputCssFilePath, processedCssContent);
141+
console.log(`Successfully created: ${outputCssFilePath}\n`);
142+
143+
// --- Post-creation instructions ---
144+
// Paths for build system (relative, forward slashes)
145+
const grdTsPath = path.join(componentDestPath, `${pascalCaseName}.js`).replace(/\\/g, '/');
146+
const grdCssPath = path.join(componentDestPath, `${camelCaseName}.css.js`).replace(/\\/g, '/');
147+
console.log('1. Update \'grd_files_debug_sources\' in \'devtools_grd_files.gni\':');
148+
console.log(' Add the following generated JavaScript files:');
149+
console.log(` "${grdTsPath}",`);
150+
console.log(` "${grdCssPath}",`);
151+
console.log(' (Note: The .ts file becomes .js, and .css becomes .css.js in GRD entries)');
152+
153+
console.log('\n2. Update \'devtools_module("<module-name>")\' in the relevant \'BUILD.gn\' file:');
154+
console.log(' Add the source TypeScript file to the \'sources\' list:');
155+
console.log(` "${outputTsFilename}",`);
156+
157+
console.log('\n3. Update \'generate_css("css_files")\' in the relevant \'BUILD.gn\' file:');
158+
console.log(' Add the source CSS file to the \'sources\' list:');
159+
console.log(` "${outputCssFilename}",`);
160+
161+
} catch (error) {
162+
console.error(`Error writing output files to ${componentDestPath}:`, error.message);
163+
process.exit(1);
164+
}
165+
}
166+
167+
main();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright {{DATE}} The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
7+
.{{COMPONENT_NAME_KEBAP_CASE}} {
8+
9+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright {{DATE}} 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+
import * as i18n from '{{FRONT_END_PATH_PREFIX}}/core/i18n/i18n.js';
6+
import * as UI from '{{FRONT_END_PATH_PREFIX}}/ui/legacy/legacy.js';
7+
import * as Lit from '{{FRONT_END_PATH_PREFIX}}/ui/lit/lit.js';
8+
9+
import {{COMPONENT_NAME_CAMEL_CASE}}Styles from './{{COMPONENT_NAME_CAMEL_CASE}}.css.js';
10+
11+
const {render, html} = Lit;
12+
13+
const UIStrings = {
14+
/**
15+
*@description Example string description
16+
*/
17+
exampleI18nString: 'Example string, please remove',
18+
} as const;
19+
const str_ = i18n.i18n.registerUIStrings('{{COMPONENT_PATH_PREFIX}}/{{COMPONENT_NAME_PASCAL_CASE}}.ts', UIStrings);
20+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
21+
22+
export interface ViewInput {
23+
}
24+
25+
export interface ViewOutput {
26+
}
27+
28+
export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => {
29+
// clang-format off
30+
render(html`
31+
<style>${{{COMPONENT_NAME_CAMEL_CASE}}Styles}</style>
32+
<div class="{{COMPONENT_NAME_KEBAP_CASE}}">
33+
${i18nString(UIStrings.exampleI18nString)}
34+
</div>
35+
`, target, {host: target});
36+
// clang-format on
37+
};
38+
export type View = typeof DEFAULT_VIEW;
39+
40+
export class {{COMPONENT_NAME_PASCAL_CASE}} extends UI.Widget.Widget {
41+
#view: View;
42+
#viewOutput: ViewOutput = {};
43+
44+
constructor(element?: HTMLElement, view?: View) {
45+
super(false, false, element);
46+
this.#view = view ?? DEFAULT_VIEW;
47+
}
48+
49+
override wasShown(): void {
50+
super.wasShown();
51+
}
52+
53+
override willHide(): void {
54+
super.willHide();
55+
}
56+
57+
override performUpdate(): Promise<void>|void {
58+
this.#view({}, this.#viewOutput, this.contentElement);
59+
}
60+
}

0 commit comments

Comments
 (0)