forked from blackbaud/skyux-builder
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added ability to generate a component from the CLI (blackbaud#330)
- Loading branch information
1 parent
61c9f0a
commit e4a000b
Showing
4 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters