Skip to content

Commit

Permalink
feat(cli): add component generator (#1814)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonhaenisch authored and adamdbradley committed Aug 17, 2019
1 parent b402999 commit 9ab0637
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
"@types/graceful-fs": "^4.1.2",
"@types/inquirer": "6.5.0",
"@types/is-glob": "^4.0.0",
"@types/jest": "^24.0.13",
"@types/mime-types": "^2.1.0",
Expand Down
12 changes: 11 additions & 1 deletion scripts/build-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const rollup = require('rollup');
const rollupResolve = require('rollup-plugin-node-resolve');
const rollupCommonjs = require('rollup-plugin-commonjs');
const rollupPluginJson = require('rollup-plugin-json');
const { run, transpile, relativeResolve } = require('./script-utils');

const TRANSPILED_DIR = path.join(__dirname, '..', 'dist', 'transpiled-cli');
Expand All @@ -14,12 +15,20 @@ async function buildCli() {
const rollupBuild = await rollup.rollup({
input: ENTRY_FILE,
external: [
'assert',
'buffer',
'child_process',
'crypto',
'events',
'fs',
'https',
'os',
'path',
'readline',
'stream',
'string_decoder',
'tty',
'util',
],
plugins: [
(() => {
Expand All @@ -35,7 +44,8 @@ async function buildCli() {
rollupResolve({
preferBuiltins: true
}),
rollupCommonjs()
rollupCommonjs(),
rollupPluginJson(),
],
onwarn: (message) => {
if (message.code === 'CIRCULAR_DEPENDENCY') return;
Expand Down
1 change: 1 addition & 0 deletions scripts/build-prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fs.emptyDirSync(DIST_LICENSES);
'exit',
'glob',
'graceful-fs',
'inquirer',
'is-glob',
'minimatch',
'node-fetch',
Expand Down
6 changes: 6 additions & 0 deletions src/cli/run-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { taskDocs } from './task-docs';
import { taskHelp } from './task-help';
import { taskServe } from './task-serve';
import { taskTest } from './task-test';
import { taskGenerate } from './task-generate';
import { taskCheckVersion, taskVersion } from './task-version';
import exit from 'exit';

Expand Down Expand Up @@ -36,6 +37,11 @@ export async function runTask(process: NodeJS.Process, config: d.Config, flags:
await taskTest(config);
break;

case 'g':
case 'generate':
await taskGenerate(config, flags);
break;

default:
config.logger.error(`Invalid stencil command, please see the options below:`);
taskHelp(process, config.logger);
Expand Down
158 changes: 158 additions & 0 deletions src/cli/task-generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import * as d from '../declarations';
import fs from 'fs';
import { join, parse, relative } from 'path';
import { promisify } from 'util';
import inquirer from 'inquirer';
import exit from 'exit';

const writeFile = promisify(fs.writeFile);
const mkdir = promisify(fs.mkdir);

/**
* Task to generate component boilerplate.
*/
export async function taskGenerate(config: d.Config, flags: d.ConfigFlags) {
if (!config.configPath) {
config.logger.error('Please run this command in your root directory (i. e. the one containing stencil.config.ts).');
exit(1);
}

const baseDir = parse(config.configPath).dir;
const srcDir = config.srcDir || 'src';

const input =
flags.unknownArgs.find(arg => !arg.startsWith('-')) ||
(await inquirer.prompt([{ name: 'name', message: 'Component name (dash-case):' }])).name;

const { dir, base: componentName } = parse(input);

if (!componentName.includes('-')) {
config.logger.error('The name needs to be in dash case.');
return exit(1);
}

const extensionsToGenerate: GeneratableExtension[] = ['tsx', ...(await chooseFilesToGenerate())];

const outDir = join(baseDir, srcDir, 'components', dir, componentName);
await mkdir(outDir, { recursive: true });

const writtenFiles = await Promise.all(
extensionsToGenerate.map(extension =>
writeFileByExtension(outDir, componentName, extension, extensionsToGenerate.includes('css')),
),
).catch(error => config.logger.error(error));

if (!writtenFiles) {
return exit(1);
}

config.logger.info();
config.logger.info(`${config.logger.gray('$')} stencil generate ${input}`);
config.logger.info();
config.logger.info('The following files have been generated:');
writtenFiles.map(file => config.logger.info(`- ${relative(baseDir, file)}`));
}

/**
* Show a checkbox prompt to select the files to be generated.
*/
const chooseFilesToGenerate = async () =>
(await inquirer.prompt([
{
name: 'filesToGenerate',
type: 'checkbox',
message: 'Which additional files do you want to generate?',
choices: [
{ value: 'css', name: 'Stylesheet', checked: true },
{ value: 'spec.ts', name: 'Spec Test', checked: true },
{ value: 'e2e.ts', name: 'E2E Test', checked: true },
],
},
])).filesToGenerate as GeneratableExtension[];

/**
* Get a file's boilerplate by its extension and write it to disk.
*/
const writeFileByExtension = async (path: string, name: string, extension: GeneratableExtension, withCss: boolean) => {
const outFile = join(path, `${name}.${extension}`);
const boilerplate = getBoilerplateByExtension(name, extension, withCss);

await writeFile(outFile, boilerplate, { flag: 'wx' });

return outFile;
};

/**
* Get the boilerplate for a file by its extension.
*/
const getBoilerplateByExtension = (name: string, extension: GeneratableExtension, withCss: boolean) => {
switch (extension) {
case 'tsx':
return getComponentBoilerplate(name, withCss);

case 'css':
return '';

case 'spec.ts':
return getSpecTestBoilerplate(name);

case 'e2e.ts':
return getE2eTestBoilerplate(name);
}
};

/**
* Get the boilerplate for a component.
*/
const getComponentBoilerplate = (name: string, style = false) => `import { h, Component, Host } from '@stencil/core';
@Component({ tag: '${name}'${style ? `, styleUrl: '${name}.css'` : ''}, shadow: true })
export class ${toPascalCase(name)} {
render() {
return (
<Host>
<slot></slot>
</Host>
);
}
}
`;

/**
* Get the boilerplate for a spec test.
*/
const getSpecTestBoilerplate = (name: string) => `import { ${toPascalCase(name)} } from './${name}';
describe('${name}', () => {
it('builds', () => {
expect(new ${toPascalCase(name)}()).toBeTruthy();
});
});
`;

/**
* Get the boilerplate for an E2E test.
*/
const getE2eTestBoilerplate = (name: string) => `import { newE2EPage } from '@stencil/core/testing';
describe('${name}', () => {
it('renders', async () => {
const page = await newE2EPage();
await page.setContent('<${name}></${name}>');
const element = await page.find('${name}');
expect(element).toHaveClass('hydrated');
});
});
`;

/**
* Convert a dash case string to pascal case.
*/
const toPascalCase = (str: string) =>
str.split('-').reduce((res, part) => res + part[0].toUpperCase() + part.substr(1), '');

/**
* Extensions available to generate.
*/
type GeneratableExtension = 'tsx' | 'css' | 'spec.ts' | 'e2e.ts';
2 changes: 1 addition & 1 deletion src/declarations/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export interface NodeResolveConfig {


export interface ConfigFlags {
task?: 'build' | 'docs' | 'help' | 'serve' | 'test';
task?: 'build' | 'docs' | 'help' | 'serve' | 'test' | 'g' | 'generate';
args?: string[];
knownArgs?: string[];
unknownArgs?: string[];
Expand Down

0 comments on commit 9ab0637

Please sign in to comment.