Skip to content

Commit

Permalink
Added ability to generate a component from the CLI (blackbaud#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blackbaud-PaulCrowder authored and Blackbaud-MikitaYankouski committed May 3, 2019
1 parent 61c9f0a commit e4a000b
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 0 deletions.
165 changes: 165 additions & 0 deletions cli/generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*jshint node: true*/
'use strict';

const fs = require('fs-extra');
const path = require('path');

function resolveFilePath(pathParts, fileName) {
fs.ensureDirSync(path.resolve('src', 'app', ...pathParts));

return path.resolve('src', 'app', ...pathParts, fileName);
}

function properCase(name) {
let nameProper = '';

for (let i = 0, n = name.length; i < n; i++) {
let c = name.charAt(i);

if (c !== '-') {
if (nameProper.length === 0 || name.charAt(i - 1) === '-') {
c = c.toUpperCase();
}

nameProper += c;
}
}

return nameProper;
}

function snakeCase(name) {
let nameSnake = '';

for (let i = 0, n = name.length; i < n; i++) {
const c = name.charAt(i);
const cLower = c.toLowerCase();

if (i > 0 && c !== cLower) {
nameSnake += '-';
}

nameSnake += cLower;
}

return nameSnake;
}

function generateComponentTs(pathParts, fileName, name, nameSnakeCase) {
fs.writeFileSync(
resolveFilePath(pathParts, fileName + '.ts'),
`import {
Component
} from '@angular/core';
@Component({
selector: 'app-${nameSnakeCase}',
templateUrl: './${fileName}.html',
styleUrls: ['./${fileName}.scss']
})
export class ${name} {
}
`
);
}

function generateComponentSpec(pathParts, fileName, name, nameSnakeCase) {
let nameWithSpaces = properCase(nameSnakeCase.replace(/\-/g, ' '));

fs.writeFileSync(
resolveFilePath(pathParts, fileName + '.spec.ts'),
`import {
TestBed
} from '@angular/core/testing';
import {
expect,
SkyAppTestModule
} from '@blackbaud/skyux-builder/runtime/testing/browser';
import {
${name}
} from './${fileName}';
describe('${nameWithSpaces} component', () => {
/**
* This configureTestingModule function imports SkyAppTestModule, which brings in all of
* the SKY UX modules and components in your application for testing convenience. If this has
* an adverse effect on your test performance, you can individually bring in each of your app
* components and the SKY UX modules that those components rely upon.
*/
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SkyAppTestModule]
});
});
it('should do something', () => {
const fixture = TestBed.createComponent(${name});
fixture.detectChanges();
expect(true).toBe(false);
});
});
`
);
}

function generateComponentHtml(pathParts, fileName) {
fs.writeFileSync(
resolveFilePath(pathParts, fileName + '.html'),
''
);
}

function generateComponentScss(pathParts, fileName) {
fs.writeFileSync(
resolveFilePath(pathParts, fileName + '.scss'),
''
);
}

function getPathParts(name) {
return name.replace(/\\/g, '/').split('/');
}

function generateComponent(name) {
const pathParts = getPathParts(name);

const classNameWithoutComponent = properCase(pathParts.pop());

const className = `${classNameWithoutComponent}Component`;

const nameSnakeCase = snakeCase(classNameWithoutComponent);

const fileName = `${nameSnakeCase}.component`;

generateComponentTs(pathParts, fileName, className, nameSnakeCase);
generateComponentSpec(pathParts, fileName, className, nameSnakeCase);
generateComponentHtml(pathParts, fileName);
generateComponentScss(pathParts, fileName);
}

function generate(argv) {
try {
let type = argv._[1];
let name = argv._[2];

switch (type) {
case 'component':
case 'c':
generateComponent(name);
break;
}

process.exit(0);
} catch (err) {
process.exit(1);
}
}

module.exports = generate;
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ module.exports = {
case 'version':
require('./cli/version')();
break;
case 'generate':
require('./cli/generate')(argv);
break;
default:
return false;
}
Expand Down
154 changes: 154 additions & 0 deletions test/cli-generate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*jshint jasmine: true, node: true */
'use strict';

const mock = require('mock-require');
const path = require('path');

function escapeRegExp(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

describe('cli generate', () => {
function validateComponent(
nameArg,
expectedPathParts,
expectedFileName,
expectedClassName,
expectedSelector,
expectedDescribe,
expectedExitCode
) {
let fsMock;

function resolvePath(ext) {
return path.resolve('src', 'app', ...expectedPathParts, `${expectedFileName}.${ext}`);
}

function validateTSFile(stringMatch) {
expect(fsMock.writeFileSync).toHaveBeenCalledWith(
resolvePath('ts'),
jasmine.stringMatching(escapeRegExp(stringMatch))
);
}

function validateScssFile() {
expect(fsMock.writeFileSync).toHaveBeenCalledWith(
resolvePath('scss'),
''
);
}

function validateHtmlFile() {
expect(fsMock.writeFileSync).toHaveBeenCalledWith(
resolvePath('html'),
''
);
}

function validateSpecFile(stringMatch) {
expect(fsMock.writeFileSync).toHaveBeenCalledWith(
resolvePath('spec.ts'),
jasmine.stringMatching(escapeRegExp(stringMatch))
);
}

expectedExitCode = expectedExitCode || 0;

fsMock = {
ensureDirSync: jasmine.createSpy('ensureDirSync'),
writeFileSync: jasmine.createSpy('writeFileSync')
};

mock('fs-extra', fsMock);

spyOn(process, 'exit').and.returnValue();

const generate = mock.reRequire('../cli/generate');

generate({
_: [
'generate',
'component',
nameArg
]
});

if (expectedExitCode === 0) {
expect(fsMock.ensureDirSync).toHaveBeenCalledWith(
path.resolve('src', 'app', ...expectedPathParts)
);

validateTSFile(
`@Component({
selector: '${expectedSelector}',
templateUrl: './${expectedFileName}.html',
styleUrls: ['./${expectedFileName}.scss']
})
export class ${expectedClassName}`
);

validateScssFile();
validateHtmlFile();

validateSpecFile(
`import {
${expectedClassName}
} from './${expectedFileName}';`
);

validateSpecFile(`describe('${expectedDescribe}', () => {`);
validateSpecFile(`const fixture = TestBed.createComponent(${expectedClassName});`);
}

expect(process.exit).toHaveBeenCalledWith(expectedExitCode);
}

afterEach(() => {
mock.stopAll();
});

it('should generate a component', () => {
validateComponent(
'my-test',
[],
'my-test.component',
'MyTestComponent',
'app-my-test',
'My test component'
);
});

it('should generate a component in a sub-directory', () => {
validateComponent(
'/subdir1/subdir2/my-test',
['subdir1', 'subdir2'],
'my-test.component',
'MyTestComponent',
'app-my-test',
'My test component'
);
});

it('should handle proper-case names', () => {
validateComponent(
'MyTest',
[],
'my-test.component',
'MyTestComponent',
'app-my-test',
'My test component'
);
});

it('should handle invalid input', () => {
validateComponent(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
1
);
});
});
4 changes: 4 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ describe('@blackbaud/skyux-builder', () => {
'version': {
cmd: 'version',
lib: 'version'
},
'generate': {
cmd: 'generate',
lib: 'generate'
}
};

Expand Down

0 comments on commit e4a000b

Please sign in to comment.