diff --git a/.prettierignore b/.prettierignore
index d973e3d317c4..4c9d4a9c5a40 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -51,3 +51,6 @@ packages/components/docs/js
# Generated files
**/generated/**
+
+# Templates
+**/*.template.*
diff --git a/.yarn/offline-mirror/ansi-colors-4.1.1.tgz b/.yarn/offline-mirror/ansi-colors-4.1.1.tgz
new file mode 100644
index 000000000000..d18e121b0fec
Binary files /dev/null and b/.yarn/offline-mirror/ansi-colors-4.1.1.tgz differ
diff --git a/.yarn/offline-mirror/enquirer-2.3.6.tgz b/.yarn/offline-mirror/enquirer-2.3.6.tgz
new file mode 100644
index 000000000000..920d6e31f503
Binary files /dev/null and b/.yarn/offline-mirror/enquirer-2.3.6.tgz differ
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 27784dea0924..f227f6517be7 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -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-rc.0",
"progress-estimator": "^0.2.2",
diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js
index 3c4ebfaba776..5db408955de8 100644
--- a/packages/cli/src/cli.js
+++ b/packages/cli/src/cli.js
@@ -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());
diff --git a/packages/cli/src/commands/component.js b/packages/cli/src/commands/component.js
new file mode 100644
index 000000000000..680c00f2b109
--- /dev/null
+++ b/packages/cli/src/commands/component.js
@@ -0,0 +1,131 @@
+/**
+ * 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 template = require('lodash.template');
+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,
+};
diff --git a/packages/cli/src/component/index.js b/packages/cli/src/component/index.js
new file mode 100644
index 000000000000..3ebbddf18c73
--- /dev/null
+++ b/packages/cli/src/component/index.js
@@ -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 { extension, name, filepath } of files) {
+ const contents = await fs.readFile(filepath, 'utf8');
+ const compile = template(contents);
+ templates[name] = {
+ compile,
+ };
+ }
+
+ return templates;
+}
+
+module.exports = {
+ loadTemplates,
+};
diff --git a/packages/cli/src/component/templates/component.template.js b/packages/cli/src/component/templates/component.template.js
new file mode 100644
index 000000000000..efcd9231556e
--- /dev/null
+++ b/packages/cli/src/component/templates/component.template.js
@@ -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
{children}
;
+}
+
+<%= name %>.propTypes = {
+ children: PropTypes.node,
+};
+
+export default <%= name %>;
diff --git a/packages/cli/src/component/templates/index.template.js b/packages/cli/src/component/templates/index.template.js
new file mode 100644
index 000000000000..376a6359cb18
--- /dev/null
+++ b/packages/cli/src/component/templates/index.template.js
@@ -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 %> };
diff --git a/packages/cli/src/component/templates/mdx.template.mdx b/packages/cli/src/component/templates/mdx.template.mdx
new file mode 100644
index 000000000000..cf7402301845
--- /dev/null
+++ b/packages/cli/src/component/templates/mdx.template.mdx
@@ -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 %>) | [Usage
+guidelines](https://www.carbondesignsystem.com/components/<%= name %>/usage)
+ | [Accessibility](https://www.carbondesignsystem.com/components/<%=
+url %>/accessibility)
+
+
+
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Component API](#component-api)
+- [Feedback](#feedback)
+
+
+
+## Overview
+
+TODO
+
+## Component API
+
+
+
+## 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).
diff --git a/packages/cli/src/component/templates/story.template.js b/packages/cli/src/component/templates/story.template.js
new file mode 100644
index 000000000000..021cf57c7236
--- /dev/null
+++ b/packages/cli/src/component/templates/story.template.js
@@ -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 %>>;
diff --git a/packages/cli/src/component/templates/test.template.js b/packages/cli/src/component/templates/test.template.js
new file mode 100644
index 000000000000..11c3574eeb0e
--- /dev/null
+++ b/packages/cli/src/component/templates/test.template.js
@@ -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
+ });
+});
diff --git a/packages/cli/src/logger.js b/packages/cli/src/logger.js
index 431dfdf8d7c8..c0d9ad1ad85c 100644
--- a/packages/cli/src/logger.js
+++ b/packages/cli/src/logger.js
@@ -16,7 +16,9 @@ const chalk = require('chalk');
* @returns {object}
*/
function createLogger(command) {
+ const timers = [];
let start;
+ let indentLevel = 0;
/**
* Display the given message with a box character. This also includes
@@ -29,24 +31,46 @@ function createLogger(command) {
console.log(chalk`{yellow ${command} ▐} {gray ${boxCharacter}} ${message}`);
}
+ function getLinePrefix() {
+ let prefix = '';
+ for (let i = 0; i < indentLevel; i++) {
+ prefix += '┃ ';
+ }
+ return prefix;
+ }
+
return {
info(message) {
- log('┣', chalk.gray(message));
+ indentLevel -= 1;
+ const prefix = getLinePrefix();
+ indentLevel += 1;
+
+ log(prefix + '┣', chalk.gray(message));
},
start(message) {
- start = Date.now();
- log('┏', message);
+ const start = Date.now();
+ timers.push(start);
+
+ const prefix = getLinePrefix();
+ log(prefix + '┏', message);
+
+ indentLevel += 1;
},
stop(message) {
- const duration = ((Date.now() - start) / 1000).toFixed(2);
+ indentLevel -= 1;
+
+ const duration = ((Date.now() - timers.pop()) / 1000).toFixed(2);
+ const prefix = getLinePrefix();
+
if (message) {
- log('┗', message);
+ log(prefix + '┗', message);
} else {
- log('┗', chalk`{gray Done in {italic ${duration}s}}`);
+ log(prefix + '┗', chalk`{gray Done in {italic ${duration}s}}`);
}
},
newline() {
- log('┃');
+ const prefix = getLinePrefix();
+ log(prefix + '┃');
},
};
}
@@ -63,7 +87,6 @@ function displayBanner() {
/ __/ _\` | '__| '_ \\ / _ \\| '_ \\
| (_| (_| | | | |_) | (_) | | | |
\\___\\__,_|_| |_.__/ \\___/|_| |_|
-
`);
}
diff --git a/packages/react/src/components/TestComponent/TestComponent-story.js b/packages/react/src/components/TestComponent/TestComponent-story.js
new file mode 100644
index 000000000000..b1d7f52a7387
--- /dev/null
+++ b/packages/react/src/components/TestComponent/TestComponent-story.js
@@ -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 { TestComponent } from './';
+import mdx from './TestComponent.mdx';
+
+export default {
+ title: 'TestComponent',
+ component: TestComponent,
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+};
+
+export const example = () => Story Example;
diff --git a/packages/react/src/components/TestComponent/TestComponent.js b/packages/react/src/components/TestComponent/TestComponent.js
new file mode 100644
index 000000000000..711c8b30a487
--- /dev/null
+++ b/packages/react/src/components/TestComponent/TestComponent.js
@@ -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 TestComponent({ children, ...rest }) {
+ return {children}
;
+}
+
+TestComponent.propTypes = {
+ // TODO
+};
+
+export default TestComponent;
diff --git a/packages/react/src/components/TestComponent/TestComponent.mdx b/packages/react/src/components/TestComponent/TestComponent.mdx
new file mode 100644
index 000000000000..ce6014579259
--- /dev/null
+++ b/packages/react/src/components/TestComponent/TestComponent.mdx
@@ -0,0 +1,34 @@
+import { Props } from '@storybook/addon-docs/blocks';
+import { TestComponent } from './';
+
+# TestComponent
+
+[Source
+code](https://github.com/carbon-design-system/carbon/tree/master/packages/react/src/components/TestComponent) | [Usage
+guidelines](https://www.carbondesignsystem.com/components/TestComponent/usage)
+ | [Accessibility](https://www.carbondesignsystem.com/components/test-component/accessibility)
+
+
+
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Component API](#component-api)
+- [Feedback](#feedback)
+
+
+
+## Overview
+
+TODO
+
+## Component API
+
+
+
+## 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/TestComponent/TestComponent.mdx).
diff --git a/packages/react/src/components/TestComponent/__tests__/TestComponent-test.js b/packages/react/src/components/TestComponent/__tests__/TestComponent-test.js
new file mode 100644
index 000000000000..3bec8b4a14ba
--- /dev/null
+++ b/packages/react/src/components/TestComponent/__tests__/TestComponent-test.js
@@ -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 { TestComponent } from '../';
+
+describe('TestComponent', () => {
+ afterEach(cleanup);
+
+ it('should work', () => {
+ render(test);
+ // TODO
+ });
+
+ describe('automated accessibility testing', () => {
+ it('should have no axe violations', async () => {
+ render(test);
+ await expect(screen.getByText('test')).toHaveNoAxeViolations();
+ });
+
+ it('should have no accessibility checker violations', async () => {
+ render(test);
+ await expect(screen.getByText('test')).toHaveNoACViolations('TestComponent');
+ });
+ });
+
+ describe('Component API', () => {
+ // TODO
+ });
+});
diff --git a/packages/react/src/components/TestComponent/index.js b/packages/react/src/components/TestComponent/index.js
new file mode 100644
index 000000000000..c6e38779301a
--- /dev/null
+++ b/packages/react/src/components/TestComponent/index.js
@@ -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 TestComponent from './TestComponent';
+ export { TestComponent };
diff --git a/yarn.lock b/yarn.lock
index 4a78c2cbb939..a5635cfe5bbf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5435,6 +5435,11 @@ ansi-colors@^3.0.0:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+ansi-colors@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+ integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
ansi-cyan@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873"
@@ -10172,6 +10177,13 @@ enhanced-resolve@^4.1.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
+enquirer@^2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+ integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+ dependencies:
+ ansi-colors "^4.1.1"
+
ent@^2.2.0, ent@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"