Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add component command #7384

Merged
Merged
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ packages/components/node_modules

# React
**/storybook-static/**

# Templates
packages/cli/src/component/templates/**
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ packages/components/docs/js

# Generated files
**/generated/**

# Templates
**/*.template.*
Binary file added .yarn/offline-mirror/ansi-colors-4.1.1.tgz
Binary file not shown.
Binary file added .yarn/offline-mirror/enquirer-2.3.6.tgz
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
"chalk": "^2.4.2",
"child-process-promise": "^2.2.1",
"clipboardy": "^2.1.0",
"enquirer": "^2.3.6",
"fast-glob": "^3.2.2",
"fs-extra": "^8.0.1",
"inquirer": "^6.4.1",
"lodash.template": "^4.5.0",
"prettier": "^2.1.0",
"prettier-config-carbon": "^0.5.0",
"progress-estimator": "^0.2.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ async function main({ argv }) {
console.error(error.stderr);
process.exit(1);
}
throw error;
console.error(error);
process.exit(1);
return;
}
console.log(message);
console.log(yargs.help());
Expand Down
130 changes: 130 additions & 0 deletions packages/cli/src/commands/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Copyright IBM Corp. 2019, 2019
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const { paramCase } = require('change-case');
const fs = require('fs-extra');
const { prompt } = require('enquirer');
const path = require('path');
const { loadTemplates } = require('../component');
const { createLogger } = require('../logger');

const logger = createLogger('component');

function clearConsole() {
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
}

async function component() {
const templates = await loadTemplates();
const questions = [
{
type: 'input',
name: 'name',
message: 'What is the name of this component?',
validate(value) {
if (value === '') {
return 'A name is required for the component';
}
return true;
},
},
{
type: 'input',
name: 'directory',
message: 'Specify the path for this component',
initial: '.',
},
{
type: 'multiselect',
name: 'options',
message: 'What else should we scaffold out for you?',
initial: ['tests', 'stories'],
choices: [
{
name: 'tests',
value: true,
},
{
name: 'stories',
value: true,
},
],
result(names) {
return this.map(names);
},
},
];

clearConsole();
const answers = await prompt(questions);

logger.start('Generating component...');

const directory = path.resolve(
process.cwd(),
answers.directory,
answers.name
);

logger.info(`Writing component directory to ${directory}`);

if (await fs.exists(directory)) {
throw new Error(`A directory already exists at ${directory}`);
}

logger.info('Scaffolding out default files...');

await fs.ensureDir(directory);
await fs.writeFile(
path.join(directory, 'index.js'),
templates.index.compile({ name: answers.name })
);
await fs.writeFile(
path.join(directory, `${answers.name}.js`),
templates.component.compile({ name: answers.name })
);

if (answers.options.tests) {
logger.start('Scaffolding out test files...');
await fs.ensureDir(path.join(directory, '__tests__'));
await fs.writeFile(
path.join(directory, '__tests__', `${answers.name}-test.js`),
templates.test.compile({ name: answers.name })
);
logger.stop();
}

if (answers.options.stories) {
logger.start('Scaffolding out story files...');
await fs.writeFile(
path.join(directory, `${answers.name}-story.js`),
templates.story.compile({
name: answers.name,
})
);
await fs.writeFile(
path.join(directory, `${answers.name}.mdx`),
templates.mdx.compile({
name: answers.name,
url: paramCase(answers.name),
})
);
logger.stop();
}

logger.stop();
}

module.exports = {
command: 'component',
desc: '[EXPERIMENTAL] Scaffold a component in React',
handler: component,
};
47 changes: 47 additions & 0 deletions packages/cli/src/component/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright IBM Corp. 2019, 2019
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const fs = require('fs-extra');
const path = require('path');
const template = require('lodash.template');

const TEMPLATES_DIR = path.join(__dirname, 'templates');
const blocklist = new Set(['.DS_Store']);

async function loadTemplates() {
const files = await fs.readdir(TEMPLATES_DIR).then((names) => {
return names
.filter((name) => {
return !blocklist.has(name);
})
.map((name) => {
const extension = path.extname(name);
return {
name: path.basename(name, `.template${extension}`),
filepath: path.join(TEMPLATES_DIR, name),
};
});
});

const templates = {};

for (const { name, filepath } of files) {
const contents = await fs.readFile(filepath, 'utf8');
const compile = template(contents);
templates[name] = {
compile,
};
}

return templates;
}

module.exports = {
loadTemplates,
};
19 changes: 19 additions & 0 deletions packages/cli/src/component/templates/component.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2016, 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import PropTypes from 'prop-types';
import React from 'react';

function <%= name %>({ children, ...rest }) {
return <div {...rest}>{children}</div>;
}

<%= name %>.propTypes = {
children: PropTypes.node,
};

export default <%= name %>;
9 changes: 9 additions & 0 deletions packages/cli/src/component/templates/index.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Copyright IBM Corp. 2016, 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import <%= name %> from './<%= name %>';
export { <%= name %> };
37 changes: 37 additions & 0 deletions packages/cli/src/component/templates/mdx.template.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Props } from '@storybook/addon-docs/blocks';
import { <%= name %> } from './';

# <%= name %>

[Source
code](https://github.com/carbon-design-system/carbon/tree/master/packages/react/src/components/<%=
name %>) &nbsp;|&nbsp; [Usage
guidelines](https://www.carbondesignsystem.com/components/<%= name %>/usage)
&nbsp;|&nbsp; [Accessibility](https://www.carbondesignsystem.com/components/<%=
url %>/accessibility)

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

## Table of Contents

- [Overview](#overview)
- [Component API](#component-api)
- [Feedback](#feedback)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Overview

TODO

## Component API

<Props />

## Feedback

Help us improve this component by providing feedback, asking questions on Slack,
or updating this file on
[GitHub](https://github.com/carbon-design-system/carbon/edit/master/packages/react/src/components/<%=
name %>/<%= name %>.mdx).
22 changes: 22 additions & 0 deletions packages/cli/src/component/templates/story.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import { <%= name %> } from './';
import mdx from './<%= name %>.mdx';

export default {
title: '<%= name %>',
component: <%= name %>,
parameters: {
docs: {
page: mdx,
},
},
};

export const example = () => <<%= name %>>Story Example</<%= name %>>;
35 changes: 35 additions & 0 deletions packages/cli/src/component/templates/test.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright IBM Corp. 2016, 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { cleanup, render, screen } from '@testing-library/react';
import React from 'react';
import { <%= name %> } from '../';

describe('<%= name %>', () => {
afterEach(cleanup);

it('should work', () => {
render(<<%= name %>>test</<%= name %>>);
// TODO
});

describe('automated accessibility testing', () => {
it('should have no axe violations', async () => {
render(<<%= name %>>test</<%= name %>>);
await expect(screen.getByText('test')).toHaveNoAxeViolations();
});

it('should have no accessibility checker violations', async () => {
render(<<%= name %>>test</<%= name %>>);
await expect(screen.getByText('test')).toHaveNoACViolations('<%= name %>');
});
});

describe('Component API', () => {
// TODO
});
});
Loading