diff --git a/.eslintignore b/.eslintignore index 9495ed7c48c5..d745b4b718d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -38,3 +38,6 @@ packages/components/node_modules # React **/storybook-static/** + +# Templates +packages/cli/src/component/templates/** 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 8a1b459e5c1c..cd7fb4dc9a2f 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", "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..0aa179569e5f --- /dev/null +++ b/packages/cli/src/commands/component.js @@ -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, +}; diff --git a/packages/cli/src/component/index.js b/packages/cli/src/component/index.js new file mode 100644 index 000000000000..1cb35359d9f1 --- /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 { 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>; 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>); + // TODO + }); + + describe('automated accessibility testing', () => { + it('should have no axe violations', async () => { + render(<<%= name %>>test>); + await expect(screen.getByText('test')).toHaveNoAxeViolations(); + }); + + it('should have no accessibility checker violations', async () => { + render(<<%= name %>>test>); + 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..2cdc9b6495d3 100644 --- a/packages/cli/src/logger.js +++ b/packages/cli/src/logger.js @@ -16,7 +16,8 @@ const chalk = require('chalk'); * @returns {object} */ function createLogger(command) { - let start; + const timers = []; + let indentLevel = 0; /** * Display the given message with a box character. This also includes @@ -29,24 +30,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 +86,6 @@ function displayBanner() { / __/ _\` | '__| '_ \\ / _ \\| '_ \\ | (_| (_| | | | |_) | (_) | | | | \\___\\__,_|_| |_.__/ \\___/|_| |_| - `); } diff --git a/yarn.lock b/yarn.lock index 7ed0b4572924..5688c8e109fb 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"