UNPKG

@department-of-veterans-affairs/generator-vets-website

Version:

Generate a React app for vets-website

387 lines (350 loc) 13 kB
import BaseStrategy from './base-strategy.js'; import chalk from 'chalk'; import { TEMPLATE_TYPES } from '../../../utils/constants.js'; import { handleExpirationDateAlias, computeFormProperties, } from '../../../lib/form-helpers.js'; import { isDryRunMode } from '../../../lib/dry-run-helpers.js'; import { calculateSubFolder } from '../../../utils/generator-helpers.js'; import { generateFormIdConst } from '../../../utils/filters.js'; import { generatePrompts, getFieldDefinitions } from '../../../lib/prompts.js'; function capitalizeFirstLetter(str) { if (!str || typeof str !== 'string') return str; return str.charAt(0).toUpperCase() + str.slice(1); } /** * Strategy for generating form-based applications */ export default class FormStrategy extends BaseStrategy { getRequiredFields() { return [ 'appName', 'folderName', 'entryName', 'rootUrl', 'formNumber', 'benefitDescription', 'ombNumber', 'expirationDate', 'templateType', ]; } getAdditionalPrompts(generator, _store) { const formFields = getFieldDefinitions('form'); return generatePrompts(generator, formFields); } processPromptResults(_generator, _store) { handleExpirationDateAlias(); computeFormProperties(); } generateFiles(generator, store) { const props = store.getAllProps(); const templateType = store.getValue('templateType'); const folderName = store.getValue('folderName'); props.subFolder = folderName ? calculateSubFolder(folderName) : ''; const formNumber = store.getValue('formNumber'); if (formNumber && !props.formIdConst) { props.formIdConst = generateFormIdConst(formNumber); } // For Form Engine, use special path structure const appPath = templateType === TEMPLATE_TYPES.FORM_ENGINE ? `src/applications/simple-forms-form-engine/${store.getValue('folderName')}` : this.getAppPath(store); if ( templateType === TEMPLATE_TYPES.WITH_1_PAGE || templateType === TEMPLATE_TYPES.WITH_4_PAGES ) { this._generateStandardFormFiles(generator, appPath, props); } if (templateType === TEMPLATE_TYPES.WITH_4_PAGES) { this._generateAdditionalPages(generator, appPath, props); } if (templateType === TEMPLATE_TYPES.FORM_ENGINE) { this._generateFormEngineFiles(generator, appPath, props); } } updateExternalFiles(generator, store) { this._updateMissingJsonSchema(generator, store); this._updatePlatformConstants(generator, store); } getCompletionMessage(store) { const folderName = store.getValue('folderName'); const entryName = store.getValue('entryName'); const rootUrl = store.getValue('rootUrl'); const templateType = store.getValue('templateType'); const usesVetsJsonSchema = store.getValue('usesVetsJsonSchema'); if (templateType === 'FORM_ENGINE') { return [ '------------------------------------', chalk.bold.yellow('⚠️ FORM ENGINE (EXPERIMENTAL)'), '', chalk.yellow('This form uses the experimental Form Engine flow.'), chalk.yellow('To edit this form, you must use Drupal CMS.'), '', chalk.cyan('To login to the form builder in Drupal:'), chalk.cyan('1. Ensure you have a Drupal account'), chalk.cyan('2. Connect to the VA VPN network'), chalk.cyan('3. Navigate to prod.cms.va.gov'), chalk.cyan('4. Login with your PIV card'), chalk.cyan('5. Go to prod.cms.va.gov/form-builder'), '', chalk.bold('Next Steps for Deployment:'), 'Create and merge PRs in the following order:', [ chalk.bold('1.') + ' ' + chalk.cyan('vets-website'), chalk.bold('2.') + ' ' + chalk.cyan('../content-build'), ].join('\n'), '', chalk.bold('README: ') + chalk.cyan(`src/applications/simple-forms-form-engine/${folderName}/README.md`), '------------------------------------', ].join('\n'); } return [ '------------------------------------', chalk.bold('Next Steps for Deployment:'), 'Create and merge PRs in the following order:', usesVetsJsonSchema ? [ chalk.bold('1.') + ' ' + chalk.cyan('../vets-json-schema'), chalk.bold('2.') + ' ' + chalk.cyan('vets-website'), chalk.bold('3.') + ' ' + chalk.cyan('../content-build'), ].join('\n') : [ chalk.bold('1.') + ' ' + chalk.cyan('vets-website'), chalk.bold('2.') + ' ' + chalk.cyan('../content-build'), ].join('\n'), '', chalk.yellow('Note:') + ' Cypress tests disabled in CI. Re-enable after content-build is deployed', '', '------------------------------------', chalk.bold('Development Commands:'), chalk.bold('Site: ') + chalk.cyan(`http://localhost:3001${rootUrl}`), chalk.bold('Watch: ') + chalk.cyan(`yarn watch --env entry=${entryName}`), chalk.bold('Mock API: ') + chalk.cyan( `yarn mock-api --responses src/applications/${folderName}/tests/fixtures/mocks/local-mock-responses.js`, ), chalk.bold('Unit test: ') + chalk.cyan(`yarn test:unit --app-folder ${folderName} --log-level all`), chalk.bold('Cypress: ') + chalk.cyan( `yarn cy:run --spec "src/applications/${folderName}/tests/e2e/${entryName}.cypress.spec.js"`, ), '', chalk.bold('README: ') + chalk.cyan(`src/applications/${folderName}/README.md`), '------------------------------------', ].join('\n'); } _generateStandardFormFiles(generator, appPath, props) { // Core form files this.copyTemplate( generator, 'form/entry.scss.ejs', `${appPath}/sass/${props.entryName}.scss`, props, ); this.copyTemplate( generator, 'form/reducer.js.ejs', `${appPath}/reducers/index.js`, props, ); this.copyTemplate( generator, 'form/App.jsx.ejs', `${appPath}/containers/App.jsx`, props, ); this.copyTemplate(generator, 'form/routes.jsx.ejs', `${appPath}/routes.jsx`, props); // Form-specific containers this.copyTemplate( generator, 'form/IntroductionPage.jsx.ejs', `${appPath}/containers/IntroductionPage.jsx`, props, ); this.copyTemplate( generator, 'form/ConfirmationPage.jsx.ejs', `${appPath}/containers/ConfirmationPage.jsx`, props, ); // Form configuration this.copyTemplate( generator, 'form/constants.js.ejs', `${appPath}/constants.js`, props, ); this.copyTemplate(generator, 'form/form.js.ejs', `${appPath}/config/form.js`, props); // Basic page this.copyTemplate( generator, 'form/pages/nameAndDateOfBirth.js.ejs', `${appPath}/pages/nameAndDateOfBirth.js`, props, ); // Test files this.copyTemplate( generator, 'form/tests/containers/ConfirmationPage.unit.spec.jsx', `${appPath}/tests/containers/ConfirmationPage.unit.spec.jsx`, props, ); this.copyTemplate( generator, 'form/tests/containers/IntroductionPage.unit.spec.jsx', `${appPath}/tests/containers/IntroductionPage.unit.spec.jsx`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/data/minimal-test.json.ejs', `${appPath}/tests/fixtures/data/minimal-test.json`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/data/maximal-test.json.ejs', `${appPath}/tests/fixtures/data/maximal-test.json`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/mocks/local-mock-responses.js', `${appPath}/tests/fixtures/mocks/local-mock-responses.js`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/mocks/user.json', `${appPath}/tests/fixtures/mocks/user.json`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/mocks/application-submit.json', `${appPath}/tests/fixtures/mocks/application-submit.json`, props, ); this.copyTemplate( generator, 'form/tests/fixtures/mocks/feature-toggles.json.ejs', `${appPath}/tests/fixtures/mocks/feature-toggles.json`, props, ); // Cypress test this.copyTemplate( generator, 'form/cypress.spec.js.ejs', `${appPath}/tests/e2e/${props.entryName}.cypress.spec.js`, props, ); } _generateAdditionalPages(generator, appPath, props) { this.copyTemplate( generator, 'form/pages/identificationInformation.js.ejs', `${appPath}/pages/identificationInformation.js`, props, ); this.copyTemplate( generator, 'form/pages/mailingAddress.js.ejs', `${appPath}/pages/mailingAddress.js`, props, ); this.copyTemplate( generator, 'form/pages/phoneAndEmailAddress.js.ejs', `${appPath}/pages/phoneAndEmailAddress.js`, props, ); } _generateFormEngineFiles(generator, appPath, props) { this.copyTemplate( generator, 'form/formEngine.js.ejs', `${appPath}/app-entry.jsx`, props, ); } _updateMissingJsonSchema(generator, store) { if (!store.getValue('usesVetsJsonSchema')) { const filePath = './src/platform/forms/tests/forms-config-validator.unit.spec.jsx'; const regex = /(const missingFromVetsJsonSchema = \[)([\s\S]*?)(\];)/; const newEntry = ` VA_FORM_IDS.${store.getValue('formIdConst')},`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); } } _updatePlatformConstants(generator, store) { if (store.getValue('formNumber')) { const filePath = './src/platform/forms/constants.js'; const formIdConst = store.getValue('formIdConst'); const formNumber = store.getValue('formNumber'); const entryName = store.getValue('entryName'); const benefitDescription = store.getValue('benefitDescription'); const trackingPrefix = store.getValue('trackingPrefix'); const appName = store.getValue('appName'); const addToMyVaSip = store.getValue('addToMyVaSip'); // Update VA_FORM_IDS let regex = /(export const VA_FORM_IDS = Object\.freeze\({)([\s\S]*?)(}\))/; let newEntry = ` ${formIdConst}: '${formNumber}',`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); // Update FORM_BENEFITS regex = /(export const FORM_BENEFITS = {)([\s\S]*?)(};)/; newEntry = ` [VA_FORM_IDS.${formIdConst}]: '${benefitDescription}',`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); // Update TRACKING_PREFIXES regex = /(export const TRACKING_PREFIXES = {)([\s\S]*?)(};)/; newEntry = ` [VA_FORM_IDS.${formIdConst}]: '${trackingPrefix}',`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); // Update SIP_ENABLED_FORMS regex = /(export const SIP_ENABLED_FORMS = new Set\(\[)([\s\S]*?)(]\);)/; newEntry = ` VA_FORM_IDS.${formIdConst},`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); // Update getAllFormLinks regex = /(export const getAllFormLinks = [\s\S]*?return {)([\s\S]*?)( {2}};)/; newEntry = ` [VA_FORM_IDS.${formIdConst}]: \`\${tryGetAppUrl('${entryName}')}/\`,`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); // Update MY_VA_SIP_FORMS if (addToMyVaSip === true || addToMyVaSip === undefined) { regex = /(export const MY_VA_SIP_FORMS = \[)([\s\S]*?)(];)/; newEntry = ` {\n` + ` id: VA_FORM_IDS.${formIdConst},\n` + ` benefit: '${capitalizeFirstLetter(benefitDescription)}',\n` + ` title: '${capitalizeFirstLetter(appName)}',\n` + ` description: '${capitalizeFirstLetter(benefitDescription)}',\n` + ` trackingPrefix: '${trackingPrefix}',\n` + ` },`; this._tryUpdateRegexInFile(generator, filePath, regex, newEntry); } } } _tryUpdateRegexInFile(generator, filePath, regex, newEntry) { // In dry run mode, just track the file without trying to read/write if (isDryRunMode(generator.options)) { generator.fs.write(filePath, ''); // This will be mocked and only track the file return; } try { const content = generator.fs.read(filePath); const updatedContent = content.replace(regex, (match, start, arrayContent, end) => { if (arrayContent.includes(newEntry)) { return match; } return `${start}${arrayContent.trimEnd()}\n${newEntry}\n${end}`; }); generator.fs.write(filePath, updatedContent); } catch (error) { generator.log( chalk.yellow(`Could not write to ${filePath}. Error: ${error.message}`), ); } } }