UNPKG

chrome-devtools-frontend

Version:
168 lines (141 loc) • 6.43 kB
#!/usr/bin/env node // 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. const fs = require('fs'); const path = require('path'); /** * Converts a string to camelCase. * e.g., "my-component-name" -> "myComponentName" * e.g., "MyComponentName" -> "myComponentName" * @param {string} str * @returns {string} */ function toCamelCase(str) { if (!str) { return ''; } // Normalize to handle various inputs (kebab, snake, space, Pascal) const s = str.replace(/[-_.\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, m => m.toLowerCase()); return s.charAt(0).toLowerCase() + s.substring(1); } /** * Converts a string to PascalCase. * e.g., "my-component-name" -> "MyComponentName" * e.g., "myComponentName" -> "MyComponentName" * @param {string} str * @returns {string} */ function toPascalCase(str) { if (!str) { return ''; } // Normalize to handle various inputs (kebab, snake, space, camel) const s = str.replace(/[-_.\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, m => m.toUpperCase()); return s.charAt(0).toUpperCase() + s.substring(1); } /** * Converts a string to kebab-case. * e.g., "MyComponentName" -> "my-component-name" * e.g., "myComponentName" -> "my-component-name" * @param {string} str * @returns {string} */ function toKebabCase(str) { if (!str) { return ''; } return str .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // Add hyphen before capital in camelCase or PascalCase .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens .toLowerCase(); } // --- Main Script Logic --- function main() { const args = process.argv.slice(2); if (args.length !== 2) { console.error('Usage: node scaffold-widget.js <path-to-create-component> <ComponentName>'); console.error('Example: node scaffold-widget.js front_end/panels/ai_assistance ChatInputWidget'); process.exit(1); } const componentDestPath = args[0]; const componentBaseName = args[1]; // --- Derive Name Variations --- const pascalCaseName = toPascalCase(componentBaseName); // e.g., MyNewWidget const camelCaseName = toCamelCase(componentBaseName); // e.g., myNewWidget const kebabCaseName = toKebabCase(componentBaseName); // e.g., my-new-widget // --- Define Replacements --- const currentYear = new Date().getFullYear().toString(); const componentPathAbsolute = path.resolve(componentDestPath); const frontEndPathAbsolute = path.resolve(process.cwd(), 'front_end'); const frontEndPathForImports = path.relative(componentPathAbsolute, frontEndPathAbsolute).replace(/\\/g, '/'); // Normalize to forward slashes const tsReplacements = { '{{DATE}}': currentYear, '{{FRONT_END_PATH_PREFIX}}': frontEndPathForImports, '{{COMPONENT_PATH_PREFIX}}': componentDestPath.replace(/\\/g, '/'), // Use forward slashes for paths in code '{{COMPONENT_NAME_PASCAL_CASE}}': pascalCaseName, '{{COMPONENT_NAME_CAMEL_CASE}}': camelCaseName, // Used for style var and import path '{{COMPONENT_NAME_KEBAP_CASE}}': kebabCaseName, }; const cssReplacements = { '{{DATE}}': currentYear, '{{COMPONENT_NAME_KEBAP_CASE}}': kebabCaseName, }; // --- Read Template Files --- let tsTemplateContent; let cssTemplateContent; try { tsTemplateContent = fs.readFileSync(path.resolve(__dirname, 'templates', 'WidgetTemplate.ts.txt'), 'utf8'); cssTemplateContent = fs.readFileSync(path.resolve(__dirname, 'templates', 'WidgetTemplate.css.txt'), 'utf8'); } catch (error) { console.error('Error reading template files (WidgetTemplate.ts.txt or WidgetTemplate.css.txt):', error.message); console.error('Make sure these files are in a "templates" subdirectory where the script is run.'); process.exit(1); } // --- Process Templates --- let processedTsContent = tsTemplateContent; for (const placeholder in tsReplacements) { processedTsContent = processedTsContent.replace( new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), tsReplacements[placeholder]); } let processedCssContent = cssTemplateContent; for (const placeholder in cssReplacements) { processedCssContent = processedCssContent.replace( new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), cssReplacements[placeholder]); } // --- Write Output Files --- const outputTsFilename = `${pascalCaseName}.ts`; const outputCssFilename = `${camelCaseName}.css`; const outputTsFilePath = path.join(componentDestPath, outputTsFilename); const outputCssFilePath = path.join(componentDestPath, outputCssFilename); try { // Ensure the destination directory exists fs.mkdirSync(componentDestPath, {recursive: true}); // Write the processed TypeScript file fs.writeFileSync(outputTsFilePath, processedTsContent); console.log(`Successfully created: ${outputTsFilePath}`); // Write the processed CSS file fs.writeFileSync(outputCssFilePath, processedCssContent); console.log(`Successfully created: ${outputCssFilePath}\n`); // --- Post-creation instructions --- // Paths for build system (relative, forward slashes) const grdTsPath = path.join(componentDestPath, `${pascalCaseName}.js`).replace(/\\/g, '/'); const grdCssPath = path.join(componentDestPath, `${camelCaseName}.css.js`).replace(/\\/g, '/'); console.log('1. Update \'grd_files_debug_sources\' in \'devtools_grd_files.gni\':'); console.log(' Add the following generated JavaScript files:'); console.log(` "${grdTsPath}",`); console.log(` "${grdCssPath}",`); console.log(' (Note: The .ts file becomes .js, and .css becomes .css.js in GRD entries)'); console.log('\n2. Update \'devtools_module("<module-name>")\' in the relevant \'BUILD.gn\' file:'); console.log(' Add the source TypeScript file to the \'sources\' list:'); console.log(` "${outputTsFilename}",`); console.log('\n3. Update \'generate_css("css_files")\' in the relevant \'BUILD.gn\' file:'); console.log(' Add the source CSS file to the \'sources\' list:'); console.log(` "${outputCssFilename}",`); } catch (error) { console.error(`Error writing output files to ${componentDestPath}:`, error.message); process.exit(1); } } main();