Skip to content

Commit

Permalink
feat: Generator CLI support (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmuzina authored Dec 13, 2024
1 parent 062a8ba commit 294460e
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 63 deletions.
8 changes: 4 additions & 4 deletions apps/generator-canonical-ds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
},
"repository": {
"type": "git",
"url": "https://github.com/canonical/vanilla-monorepo"
"url": "https://github.com/canonical/ds25"
},
"bugs": {
"url": "https://github.com/canonical/vanilla-monorepo/issues"
"url": "https://github.com/canonical/ds25/issues"
},
"homepage": "https://github.com/canonical/vanilla-monorepo#readme",
"homepage": "https://github.com/canonical/ds25#readme",
"files": ["generators"],
"type": "module",
"main": "generators/index.js",
Expand All @@ -28,7 +28,7 @@
"check:ts": "tsc --noEmit"
},
"dependencies": {
"yeoman-generator": "7.3.3"
"yeoman-generator": "^7.4.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand Down
5 changes: 5 additions & 0 deletions apps/generator-canonical-ds/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export default class BaseGenerator extends Generator {
]);
return this.composeWith(
`${globalContext.generatorScriptIdentifer}:${answers.subgenerator || this.subgenerators[0].name}`,
// Pass CLI args to the subgenerator
{
generatorArgs: this.args,
generatorOptions: this.options,
},
);
}

Expand Down
7 changes: 5 additions & 2 deletions apps/generator-canonical-ds/src/component/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ This generator creates a new React component with a basic structure.

### Getting Started
1. Install [Yeoman](https://yeoman.io/): `bun add -g yo`
2. Run the generator: `yo @canonical/canonical-ds:component`
3. Follow the prompts to create a new React component
2. Run the generator: `yo @canonical/canonical-ds:component path/to/component`

The following optional flags can be used:
- `--withStyles`: Include a CSS file for the component.
- `--withStories`: Include a Storybook file for the component.
114 changes: 59 additions & 55 deletions apps/generator-canonical-ds/src/component/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,61 @@
import path from "node:path";
import Generator from "yeoman-generator";
import Generator, { type BaseOptions } from "yeoman-generator";
import globalContext from "../app/global-context.js";
import casing from "../utils/casing.js";

interface ComponentGeneratorAnswers {
/** The path to the component's root directory */
componentAbsolutePath: string;
componentPath: string;
/** Whether to include styles in the component */
includeStyles: boolean;
withStyles: boolean;
/** Whether to include a storybook story in the component */
includeStorybook: boolean;
withStories: boolean;
}

export default class ComponentGenerator extends Generator {
answers?: ComponentGeneratorAnswers;
type ComponentGeneratorOptions = BaseOptions & ComponentGeneratorAnswers;

initializing() {
this.log("Welcome to the component generator!");
this.log(
"This generator should be run from the root directory of all your application's components (ex: src/components).",
);
}
export default class ComponentGenerator extends Generator<ComponentGeneratorOptions> {
answers!: ComponentGeneratorAnswers;

constructor(args: string | string[], options: ComponentGeneratorOptions) {
super(args, options);

async prompting() {
this.answers = await this.prompt([
{
type: "input",
name: "componentAbsolutePath",
message:
"Enter the component's name, including its path (ex: `form/input/Checkbox`):",
default: ".",
filter: (input: string) => {
const resolvedInput = path.resolve(this.env.cwd, input);
// Force the directory name (the component name) to be PascalCased
const dirName = path.basename(resolvedInput);
return path.resolve(
resolvedInput,
"..",
casing.toPascalCase(dirName),
);
},
},
{
type: "confirm",
name: "includeStyles",
message: "Do you want to include styles?",
default: true,
},
{
type: "confirm",
name: "includeStorybook",
message: "Would you like to include a story file?",
default: true,
},
]);
// Output introductory logging if help is not requested
// If help is requested, the `log()` function is not defined, so this block is skipped
if (!this.options.help) {
this.log("Welcome to the component generator!");
this.log(
"This generator should be run from the root directory of all your application's components (ex: src/components).",
);
this.log(
`This generator supports CLI input only. Use yo ${globalContext.generatorScriptIdentifer}:component --help for more information.`,
);
}

this.argument("componentPath", {
type: String,
description: "The path to the component's root directory",
required: true,
default: this.env.cwd,
});

this.option("withStyles", {
type: Boolean,
description: "Include styles in the component",
default: false,
});

this.option("withStories", {
type: Boolean,
description: "Include a storybook story in the component",
default: false,
});

this.answers = {
componentPath: path.resolve(this.env.cwd, this.options.componentPath),
withStyles: this.options.withStyles,
withStories: this.options.withStories,
};
}

/**
Expand Down Expand Up @@ -84,7 +86,9 @@ export default class ComponentGenerator extends Generator {
writing(): void {
if (!this.answers) return;

const componentName = path.basename(this.answers.componentAbsolutePath);
const componentName = casing.toPascalCase(
path.basename(this.answers.componentPath),
);

const templateData = {
...globalContext,
Expand All @@ -93,55 +97,55 @@ export default class ComponentGenerator extends Generator {
/** The path to the component's directory relative to the current working directory */
componentRelativePath: path.relative(
this.env.cwd,
this.answers.componentAbsolutePath,
this.answers.componentPath,
),
componentCssClassName:
this.answers.includeStyles && casing.toKebabCase(componentName),
this.answers.withStyles && casing.toKebabCase(componentName),
};

this.fs.copyTpl(
this.templatePath("Component.tsx.ejs"),
this.destinationPath(
`${this.answers.componentAbsolutePath}/${templateData.componentName}.tsx`,
`${this.answers.componentPath}/${templateData.componentName}.tsx`,
),
templateData,
);

this.fs.copyTpl(
this.templatePath("index.ts.ejs"),
this.destinationPath(`${this.answers.componentAbsolutePath}/index.ts`),
this.destinationPath(`${this.answers.componentPath}/index.ts`),
templateData,
);

this.fs.copyTpl(
this.templatePath("types.ts.ejs"),
this.destinationPath(`${this.answers.componentAbsolutePath}/types.ts`),
this.destinationPath(`${this.answers.componentPath}/types.ts`),
templateData,
);

this.fs.copyTpl(
this.templatePath("Component.test.tsx.ejs"),
this.destinationPath(
`${this.answers.componentAbsolutePath}/${templateData.componentName}.test.tsx`,
`${this.answers.componentPath}/${templateData.componentName}.test.tsx`,
),
templateData,
);

if (this.answers.includeStyles) {
if (this.answers.withStyles) {
this.fs.copyTpl(
this.templatePath("Component.css.ejs"),
this.destinationPath(
`${this.answers.componentAbsolutePath}/${templateData.componentName}.css`,
`${this.answers.componentPath}/${templateData.componentName}.css`,
),
templateData,
);
}

if (this.answers.includeStorybook) {
if (this.answers.withStories) {
this.fs.copyTpl(
this.templatePath("Component.stories.tsx.ejs"),
this.destinationPath(
`${this.answers.componentAbsolutePath}/${templateData.componentName}.stories.tsx`,
`${this.answers.componentPath}/${templateData.componentName}.stories.tsx`,
),
templateData,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* <%= generatorPackageName %> <%= generatorPackageVersion %> */
import type React from 'react';
import type { <%= componentName %>Props } from './types.js';
<% if (includeStyles) { _%>
<% if (withStyles) { _%>
import './<%= componentName %>.css';
const componentCssClassName = "<%= cssNamespace %> <%= componentCssClassName %>";
Expand All @@ -23,7 +23,7 @@ const <%= componentName %> = ({
id={id}
style={style}
className={[
<% if (includeStyles) { _%>
<% if (withStyles) { _%>
componentCssClassName,
<%_ } _%>
className
Expand Down
Binary file modified bun.lockb
Binary file not shown.

0 comments on commit 294460e

Please sign in to comment.