drupal-radix-cli
Version:
CLI for managing Radix components in a Drupal theme
324 lines (272 loc) • 10.8 kB
JavaScript
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('js-yaml');
const prompts = require('@clack/prompts');
const { isCancel, cancel, select, text, intro, outro, confirm } = prompts;
const color = require('picocolors');
let radixComponentsPath = '../../contrib/radix/components';
const activeThemePath = process.cwd();
async function listComponents() {
try {
intro(color.magenta(`Available ${color.bold('Radix')} Components:`));
const components = await fs.readdirSync(radixComponentsPath);
for (const componentDir of components) {
const ymlPath = `${radixComponentsPath}/${componentDir}/${componentDir}.component.yml`;
if (fs.existsSync(ymlPath)) {
const fileContents = await fs.readFile(ymlPath, 'utf8');
const doc = yaml.load(fileContents);
if (doc?.name) {
const description = doc.description || 'No description available';
console.log(`- ${color.magenta(doc.name)}: ${description}`);
}
}
}
outro(
color.magenta(
`That's all. To add a component to your theme, run ${color.inverse('drupal-radix-cli add')} or to generate a new one ${color.inverse('drupal-radix-cli generate')}.`,
),
);
} catch (error) {
console.error('Error listing components:', error);
}
}
async function updateTemplateReferences(componentName, themeName) {
const updatedTemplates = [];
const templatesPath = path.join(activeThemePath, 'templates');
if (!fs.existsSync(templatesPath)) {
return updatedTemplates;
}
try {
const templateFiles = await findTwigFiles(templatesPath);
for (const templateFile of templateFiles) {
const fileContent = await fs.readFile(templateFile, 'utf8');
const radixPattern = new RegExp(`radix:${componentName}`, 'g');
if (radixPattern.test(fileContent)) {
const updatedContent = fileContent.replace(radixPattern, `${themeName}:${componentName}`);
await fs.writeFile(templateFile, updatedContent, 'utf8');
updatedTemplates.push(path.relative(activeThemePath, templateFile));
}
}
} catch (error) {
console.error('Error updating template references:', error);
}
return updatedTemplates;
}
async function findTwigFiles(dir) {
const twigFiles = [];
async function scanDirectory(currentDir) {
const items = await fs.readdir(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await scanDirectory(fullPath);
} else if (item.endsWith('.html.twig')) {
twigFiles.push(fullPath);
}
}
}
await scanDirectory(dir);
return twigFiles;
}
async function addComponent(skipTemplateUpdate = false) {
try {
const components = fs.readdirSync(radixComponentsPath);
const options = components
.map((component) => {
const ymlPath = `${radixComponentsPath}/${component}/${component}.component.yml`;
if (fs.existsSync(ymlPath)) {
const fileContents = fs.readFileSync(ymlPath, 'utf8');
const doc = yaml.load(fileContents);
if (doc?.name) {
return { value: component, label: doc.name };
}
}
return null;
})
.filter(Boolean);
if (options.length === 0) {
outro(color.yellow('No components available to add.'));
return;
}
const maxItems = 8;
const componentName = await select({
message: 'Pick a Radix component to add to your theme.',
options: options,
maxItems: maxItems,
onCancel: () => {
cancel('Operation cancelled.');
process.exit(0);
},
});
if (isCancel(componentName)) {
cancel('Operation cancelled.');
process.exit(0);
}
const sourcePath = `${radixComponentsPath}/${componentName}`;
const targetPath = `${activeThemePath}/components/${componentName}`;
if (fs.existsSync(targetPath)) {
// Ask user if they want to overwrite the existing component
const overwrite = await confirm({
message: `${componentName} already exists. Do you want to overwrite it?`,
initial: false,
});
if (isCancel(overwrite) || !overwrite) {
outro(color.yellow(`Operation cancelled. ${componentName} was not overwritten.`));
process.exit(0);
}
fs.removeSync(targetPath);
}
fs.copySync(sourcePath, targetPath);
let outputMessage = color.magenta(`Component ${componentName} has been added at: ${color.bold(targetPath)}`);
if (!skipTemplateUpdate) {
// Get theme name from the current working directory
const themeName = path.basename(activeThemePath);
// Check and update templates
const updatedTemplates = await updateTemplateReferences(componentName, themeName);
if (updatedTemplates.length > 0) {
outputMessage += `\n\n${color.green('Updated template references:')}`;
for (const templatePath of updatedTemplates) {
outputMessage += `\n${color.cyan(`- ${templatePath}`)}`;
}
}
} else {
outputMessage += `\n${color.yellow('Template update skipped (--no-template flag used), update your twig templates manually.')}`;
}
outro(outputMessage);
} catch (error) {
console.error('Error during the add component process:', error);
}
}
async function generateComponent() {
const response = await text({
message: 'What is the name of your component?',
placeholder: 'eg. card',
onCancel: () => {
cancel(outro(color.yellow(`"Operation cancelled."`)));
process.exit(0);
},
});
if (isCancel(response)) {
cancel(outro(color.yellow(`"Operation cancelled."`)));
process.exit(0);
}
const componentName = response;
const componentsDirPath = path.join(activeThemePath, 'components');
const componentDirPath = path.join(componentsDirPath, componentName);
try {
if (await fs.pathExists(componentDirPath)) {
outro(color.yellow(`The ${color.italic(componentName)} component already exists! maybe try another name?`));
process.exit(0);
}
await fs.ensureDir(componentsDirPath);
await fs.ensureDir(componentDirPath);
const filesToCreate = [
`${componentName}.twig`,
`${componentName}.scss`,
`_${componentName}.js`,
'README.mdx',
`${componentName}.component.yml`,
];
for (const file of filesToCreate) {
const filePath = path.join(componentDirPath, file);
await fs.ensureFile(filePath);
}
// Initial content for the SCSS file
const scssFilePath = path.join(componentDirPath, `${componentName}.scss`);
await fs.appendFile(scssFilePath, '@import "../../src/scss/init";\n');
// Initial content for the component YML file
const componentYmlPath = path.join(componentDirPath, `${componentName}.component.yml`);
const componentYmlContent = `$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
name: ${componentName}
status: experimental
description: 'The ${componentName} component auto-generated by drupal-radix-cli'
`;
await fs.writeFile(componentYmlPath, componentYmlContent);
// Initial content for the component README file
const readmeMdPath = path.join(componentDirPath, 'README.md');
const readmeMdContent = `# ${componentName} Component
This component was generated by the \`drupal-radix-cli\` tool. Feel free to update this README to provide more information about your component and how to use it.`;
await fs.writeFile(readmeMdPath, readmeMdContent);
// Initial content for the twig file
const twigFilePath = path.join(componentDirPath, `${componentName}.twig`);
const twigCommentBlock = `{#
/**
* @file
* Template for ${componentName} component.
*/
#}`;
await fs.writeFile(twigFilePath, twigCommentBlock);
console.log(
`Component ${componentName} generated successfully. Make sure to remove anything that you don't need and update your ${componentName}.component.yml file.`,
);
} catch (error) {
console.error('Error generating component:', error);
process.exit(1);
}
}
function showHelp() {
intro(`
${color.magenta('Usage: drupal-radix-cli [command] [--radix-path <path>] [--no-template]')}
Commands:
${color.green('list')} - Displays a list of available Radix components.
${color.green('add')} - Adds a new component to your theme, replacing the existing one if it's already there.
${color.green('generate')} - Generates a new component structure within the 'components' directory.
${color.green('help')} - Shows this help message.
Flags:
${color.green('--radix-path')} - Optional. Specify the path to the Radix components directory. If not provided, defaults to "../../../contrib/radix/components".
${color.green('--no-template')} - Optional. Skip updating template references when adding components.
`);
outro(color.magenta('You can also use the --help flag to show this help message.'));
}
(async () => {
const args = process.argv.slice(2);
const radixPathFlagIndex = args.findIndex((arg) => arg === '--radix-path');
const noTemplateFlagIndex = args.findIndex((arg) => arg === '--no-template');
if (radixPathFlagIndex > -1) {
let providedPath = args[radixPathFlagIndex + 1];
if (!providedPath) {
try {
providedPath = await text({
message: 'Enter the path to the Radix components directory:',
placeholder: '../../../contrib/radix/components',
onCancel: () => {
cancel(outro(color.yellow(`"Operation cancelled."`)));
process.exit(0);
},
});
radixComponentsPath = path.resolve(providedPath);
} catch (error) {
if (isCancel(error)) {
cancel(outro(color.yellow(`"Operation cancelled."`)));
process.exit(1);
} else {
console.error('An unexpected error occurred:', error);
process.exit(1);
}
}
} else {
radixComponentsPath = path.resolve(providedPath);
}
}
const command = args[0] || 'help';
const skipTemplateUpdate = noTemplateFlagIndex > -1;
try {
if (command === 'list') {
await listComponents();
} else if (command === 'add') {
await addComponent(skipTemplateUpdate);
} else if (command === 'generate') {
await generateComponent();
} else if (command === 'help') {
showHelp();
} else {
console.log(`Unknown command: ${command}`);
showHelp();
}
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}
})();