Skip to content
Open
8 changes: 8 additions & 0 deletions scopes/generator/generator/generator.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export type TemplateDescriptor = {
name: string;
description?: string;
hidden?: boolean;
/**
* the env that will be used for components created with this template.
* only relevant for component templates (not workspace templates).
* expected format is an aspect ID string, e.g., "bitdev.react/react-env".
*/
env?: string;
};

type TemplateWithId = { id: string; envName?: string };
Expand Down Expand Up @@ -167,11 +173,13 @@ export class GeneratorMain {
if (this.config.hideCoreTemplates && this.bitApi.isCoreAspect(id)) return true;
return false;
};
const componentTemplate = template as ComponentTemplate;
return {
aspectId: id,
name: template.name,
description: template.description,
hidden: shouldBeHidden(),
env: componentTemplate.env,
};
};

Expand Down
43 changes: 38 additions & 5 deletions scopes/generator/generator/templates.cmd.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
import type { Command, CommandOptions } from '@teambit/cli';
import chalk from 'chalk';
import { groupBy } from 'lodash';
import { groupBy, countBy } from 'lodash';
import type { GeneratorMain, TemplateDescriptor } from './generator.main.runtime';

/**
* Extracts a friendly display name from an aspect ID.
* e.g., "teambit.react/react" → "React", "teambit.harmony/node" → "Node"
*/
function getFriendlyName(aspectId: string): string {
const lastSegment = aspectId.split('/').pop() || aspectId;
// Handle special cases
if (lastSegment.toLowerCase() === 'mdx') return 'MDX';
// Handle kebab-case by capitalizing each word
const parts = lastSegment.split('-').filter(Boolean);
if (parts.length > 1) {
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
}
// Capitalize first letter
return lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
}

/**
* Gets unique envs from templates, sorted by frequency (most common first).
*/
function getUniqueEnvs(templates: TemplateDescriptor[]): string[] {
const envs = templates.map((t) => t.env).filter(Boolean) as string[];
if (envs.length === 0) return [];
const counts = countBy(envs);
return Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.map(([env]) => env);
}

export type TemplatesOptions = {
showAll?: boolean;
aspect?: string;
Expand Down Expand Up @@ -45,10 +74,14 @@ export class TemplatesCmd implements Command {
};
const output = Object.keys(grouped)
.map((aspectId) => {
const names = grouped[aspectId].map(templateOutput).join('\n');
const groupTitle = grouped[aspectId][0].titlePrefix
? `${grouped[aspectId][0].titlePrefix} (${aspectId})`
: aspectId;
const templates = grouped[aspectId];
const names = templates.map(templateOutput).join('\n');
// Get unique envs from templates in this group
const uniqueEnvs = getUniqueEnvs(templates);
const envSuffix = uniqueEnvs.length > 0 ? chalk.dim(` (env: ${uniqueEnvs.join(', ')})`) : '';
// Use titlePrefix if available, otherwise generate friendly name from aspectId
const friendlyName = templates[0].titlePrefix || getFriendlyName(aspectId);
const groupTitle = `${friendlyName}${envSuffix}`;
return `${chalk.blue.bold(groupTitle)}\n${names}\n`;
})
.join('\n');
Expand Down