import {inject} from 'aurelia-dependency-injection'; import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; import mkdirp from 'mkdirp'; @inject(Project, CLIOptions, UI) export default class ReduxComponentGenerator { constructor(project, options, ui) { this.project = project; this.options = options; this.ui = ui; } execute() { return this.ui .ensureAnswer(this.options.args[0], 'What is the name of the redux component? (in hyphen format. ft- will be prefixed automatically)') .then(name => { let area = 'products'; this.ui.ensureAnswer(this.options.args[1], 'Which sub area? (blank sub area will add it at the area level)', 'portfolio') .then(subarea => { this.ui.ensureAnswer(this.options.args[2], 'Do you want a site specific data reducer? (Y/N)', 'Y') .then(siteSpecificReducer => { this.ui.ensureAnswer(this.options.args[3], 'Do you want an app state reducer? (Y/N)', 'N') .then(appStateReducer => { let fileName = this.project.makeFileName(name); let className = this.project.makeClassName(name); let funcName = this.project.makeFunctionName(name); let makeSiteSpecificReducer = (siteSpecificReducer.toLowerCase() === 'y'); let makeAppStateReducer = (appStateReducer.toLowerCase() === 'y'); let subDir = (subarea) ? area + '/' + subarea : area; let relDir = 'components/' + subDir + '/ft-' + fileName; let dir = 'src/' + relDir; mkdirp.sync(dir);// quick fix incase parent folder doesn't exist. this.project.locations.push(this.project[dir] = ProjectItem.directory(dir)); this.project[dir].add( ProjectItem.text(`ft-${fileName}.js`, this.generateComponentJsSource(className)), ProjectItem.text(`ft-${fileName}.spec.js`, this.generateComponentTestJsSource(className, fileName, relDir, funcName)), ProjectItem.text(`ft-${fileName}.html`, this.generateComponentHtmlSource(fileName)), ProjectItem.text(`ft-${fileName}.md`, this.generateComponentMdSource('ft-' + fileName)) // ProjectItem.text(`_ft-${fileName}.scss`, this.generateComponentSassSource('ft-' + fileName, name)) ); if (makeSiteSpecificReducer) { let reducerDir = dir + '/lib/en-us-retail'; mkdirp.sync(reducerDir);// quick fix incase parent folder doesn't exist. this.project.locations.push(this.project[reducerDir] = ProjectItem.directory(reducerDir)); this.project[reducerDir].add( ProjectItem.text(`${fileName}.reducer.js`, this.generateReducerJsSource(funcName, fileName)), ProjectItem.text(`${fileName}.spec.js`, this.generateReducerTestJsSource(funcName, fileName)) ); } if (makeAppStateReducer) { let reducerDirAppState = dir + '/lib'; mkdirp.sync(reducerDirAppState);// quick fix incase parent folder doesn't exist. this.project.locations.push(this.project[reducerDirAppState] = ProjectItem.directory(reducerDirAppState)); this.project[reducerDirAppState].add( ProjectItem.text(`${fileName}.reducer.js`, this.generateAppStateReducerJsSource(funcName, fileName)), ProjectItem.text(`${fileName}.spec.js`, this.generateAppStateReducerTestJsSource(funcName, fileName)) ); } return this.project.commitChanges() .then(() => this.ui.log(`Created ${fileName}.`)); }); }); }); }); } /** * source for top level component js * @param className * @returns {string} */ generateComponentJsSource(className) { return `import {inject} from 'aurelia-framework'; import {ReduxComponentBase} from '../../../../lib/redux-component-base'; //import {getFundState} from '../../lib/map-state-utils'; @inject('Store', 'Beans', Element) export class Ft${className} extends ReduxComponentBase { populated = false; constructor(store, beans, element) { // add logging and integration with the data store super(store); this.fundId = element.getAttribute('fund-id'); // This is a top level component so we need to initiate the population this.dispatch({ type: 'POPULATE_CHANGE_ME_STATE', // beans: beans..., // beans set in configuration for site e.g. configuration/en-us-retail/beans.js fundId: this.fundId }); } /** * Called when the state changes. */ mapState(newState) { try { this.logger.debug('mapState()'); //let fundState = getFundState(newState, this.fundId); // set component properties from state as appropriate //this.label = newState.products.distributions.label; //this.proximity = fundState.distributions.caveats.proximity; // if we get to here without erroring, data has been populated successfully this.populated = true; } catch (e) { // state not populated yet this.populated = false; } } } `; } /** * source for top level component js unit test * @param className * @param fileName * @returns {string} */ generateComponentTestJsSource(className, fileName, relDir, funcName) { return `import {StageComponent} from 'aurelia-testing'; import {bootstrap} from 'aurelia-bootstrapper'; import {wait} from '../../../../lib/test-utils.js'; describe('${className}', () => { let delay = 10; let component; let mockStore = { dispatch: function(action) { expect(action.type).toBeDefined(); }, subscribe: function(callback) { } }; let mockBeans = { // see configuration/en-us-retail/beans.js for example component: { ${funcName}: [] }, bean: { } }; let mockAttributes; let mockState; beforeEach(() => { spyOn(mockStore, 'dispatch'); mockAttributes = {}; mockState = {}; component = StageComponent .withResources('${relDir}/ft-${fileName}') .inView(\`\`); component.configure = (aurelia) => { aurelia.container.registerInstance('Store', mockStore); aurelia.container.registerInstance('Beans', mockBeans); aurelia.use.standardConfiguration(); }; }); it('should render something', done => { component.boundTo(mockAttributes); component.create(bootstrap) .then(() => { if (component.viewModel.mapState) { component.viewModel.mapState(mockState); } }) .then(wait(delay)) .then(() => { const testElement = document.getElementById('test1'); expect(testElement.innerText.trim()).toBe('ft-${fileName}: replace me'); }) .then(done); }); afterEach(() => { component.dispose(); }); }); `; } /** * source for top level component html * @returns {string} */ generateComponentHtmlSource(fileName) { return ` `; } /** * source for reducer * @param funcName, fileName */ generateReducerJsSource(funcName, fileName) { return `/** * Data Reducer for ${funcName} * Takes site specific json data and creates model for components * after applying business and presentation logic and data mapping */ import {LogManager} from 'aurelia-framework'; const logger = LogManager.getLogger('${fileName}'); export function ${funcName}(state, action) { switch (action.type) { case 'CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS': logger.debug('Reducing: CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS'); return Object.assign({}, state, { ${funcName}: action.data }); default: return state; } } `; } /** * source for reducer unit test * @param funcName */ generateReducerTestJsSource(funcName, fileName) { return `import {${funcName}} from './${fileName}.reducer'; describe('${funcName}', () => { it('should return unchanged state if action does not apply', done => { let action = { type: 'ANOTHER_ACTION' }; let oldState = {}; let newState = ${funcName}(oldState, action, {}); expect(newState).toBe(oldState); done(); }); it('should return some stuff', done => { let action = { type: 'CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS', data: { stuff: 'some stuff' } }; let newState = ${funcName}({}, action, {}); expect(newState.${funcName}.stuff).toBe('some stuff'); done(); }); }); `; } /** * source for reducer * @param funcName, fileName */ generateAppStateReducerJsSource(funcName, fileName) { return `/** * App State Reducer for ${funcName} * Takes application state data and creates model for components */ import {LogManager} from 'aurelia-framework'; const logger = LogManager.getLogger('${fileName}'); export function ${funcName}(state, action) { switch (action.type) { case 'SOME_ACTION': logger.debug('Reducing: SOME_ACTION'); return Object.assign({}, state, { ${funcName}: action.data }); default: return state; } } `; } /** * source for reducer unit test * @param funcName */ generateAppStateReducerTestJsSource(funcName, fileName) { return `import {${funcName}} from './${fileName}.reducer'; describe('${funcName}', () => { it('should return unchanged state if action does not apply', done => { let action = { type: 'ANOTHER_ACTION' }; let oldState = {}; let newState = ${funcName}(oldState, action, {}); expect(newState).toBe(oldState); done(); }); it('should return some stuff', done => { let action = { type: 'SOME_ACTION', data: { stuff: 'some stuff' } }; let newState = ${funcName}({}, action, {}); expect(newState.${funcName}.stuff).toBe('some stuff'); done(); }); }); `; } /** * generate markdown stub * @param fileName * @returns {string} */ generateComponentMdSource(fileName) { return `# ${ fileName } ## Usage ${'```'}html <${ fileName} fund-id="817" cid="uniqueId"> ${'```'} *The cid is guaranteed to be unique to this page even if multiple instances of the component are added to the same page.* ## Developer notes `; } /** * generate sass partial stub * @param fileName * @returns {string} */ // generateComponentSassSource(fileName, name) { // return ` // // CSS specific to the ${ fileName } component goes here // [data-fti-component="${ name }"] { // } // `; // } }