import path from 'path'; import Generator from 'yeoman-generator'; import validateProjectName from 'validate-npm-package-name'; type FrontendOptions = 'react-ts' | 'react-js'; type BackendOptions = ExpressOptions | 'flask'; type ExpressOptions = 'express-ts' | 'express-js'; const backendCommand: Record = { 'express-js': 'Navigate to `server` folder and run `yarn start:dev`', 'express-ts': 'Navigate to `server` folder and run `yarn start:dev`', flask: 'Navigate to `server` folder and run `chmod +x ./bin/setup.sh && ./bin/setup.sh` to setup. Then, run `flask run` to start the server', }; class DuffelAppGenerator extends Generator { userInputs?: { projectName: string; frontend: FrontendOptions; backend: BackendOptions; accessToken: string; }; usingYarn: boolean; constructor(args: string | string[], opts: Generator.GeneratorOptions) { super(args, opts); this.sourceRoot(path.resolve(__dirname, 'templates')); this.usingYarn = process.argv[0].includes('yarn'); } _exit_if_not_exists(command: string, message: string) { try { this.spawnCommandSync(command, ['-v'], { stdio: 'ignore', }); } catch (e) { this.log(message); process.exit(); } } projectPath = (path: string) => { if (!this.userInputs) { throw new Error('no user input found'); } return this.destinationPath(`${this.userInputs.projectName}/${path}`); }; async prompting() { const { projectName, backend } = await this.prompt([ { type: 'input', name: 'projectName', message: "What's your project name?", default: 'my-app', validate: (val: string) => { if (!validateProjectName(val).validForNewPackages) { return 'Invalid project name'; } return true; }, }, { type: 'list', name: 'backend', message: 'Which backend framework would you like to use?', choices: [ // disabling non-js backend for now as it hasn't been updated to match the latest version { name: 'Express (TypeScript)', value: 'express-ts' }, { name: 'Express (JavaScript)', value: 'express-js' }, { name: 'Flask (Python)', value: 'flask' }, ], }, ]); // Ensure that python3 is installed if (backend === 'flask') { this._exit_if_not_exists('python3', 'Python3 is required to use Flask'); } const { frontend, accessToken } = await this.prompt([ { type: 'list', name: 'frontend', message: 'Which frontend framework would you like to use?', choices: [ // disabling typescript frontend for now as it hasn't been updated to match the latest version { name: 'React (TypeScript)', value: 'react-ts' }, { name: 'React (JavaScript)', value: 'react-js' }, ], }, { type: 'input', name: 'accessToken', message: "What's your Duffel test token? (a list of them can be found here: https://app.duffel.com/tokens)", validate: (val: string) => val.length > 0 && val.startsWith('duffel_test') ? true : 'Please use the test token (starting with `duffel_test`) for safety. You can update this later once you want to make real API requests to flights', }, ]); this.userInputs = { projectName, frontend, backend, accessToken, }; } async writing() { if (!this.userInputs) { // this shouldn't be reachable unless we break something throw new Error('Expected user inputs, got none'); } const { accessToken, frontend, backend } = this.userInputs; if (!frontend.includes('react')) { throw new Error('Unimplemented'); } // copy the server code and put the access token in the env files switch (backend) { case 'express-js': case 'express-ts': this._copy_express_template(backend, accessToken); break; case 'flask': this._copy_flask_template(accessToken); break; } // copy the client side code switch (frontend) { case 'react-ts': this.fs.copy(this.templatePath('react-ts'), this.projectPath('client')); break; case 'react-js': this.fs.copy(this.templatePath('react-js'), this.projectPath('client')); break; } // copy gitignore file this.fs.copy( this.templatePath('common/gitignore'), this.projectPath('client/.gitignore') ); } _copy_express_template(backend: ExpressOptions, accessToken: string) { // copy the template this.fs.copy(this.templatePath(backend), this.projectPath('server')); // copy env variables this.fs.copyTpl( this.templatePath('common/env'), this.projectPath('server/env'), { duffelAccessToken: accessToken, } ); // copy gitignore file this.fs.copy( this.templatePath('common/gitignore'), this.projectPath('server/.gitignore') ); } _copy_flask_template(accessToken: string) { // copy the template this.fs.copyTpl( this.templatePath('flask'), this.projectPath('server'), { duffelAccessToken: accessToken, }, undefined, { globOptions: { dot: true, }, } ); // copy gitignore file this.fs.copy( this.templatePath('common/gitignore-flask'), this.projectPath('server/.gitignore') ); } install() { this.log('Installing dependencies...'); if (this.usingYarn) { this.spawnCommandSync('yarn', [], { cwd: this.projectPath('client') }); if (['express-ts', 'express-js'].includes(this.userInputs.backend)) { this.spawnCommandSync('yarn', [], { cwd: this.projectPath('server') }); } return; } // otherwise, use npm install this.spawnCommandSync('npm', ['install'], { cwd: this.projectPath('client'), }); if (['express-ts', 'express-js'].includes(this.userInputs.backend)) { this.spawnCommandSync('npm', ['install'], { cwd: this.projectPath('server'), }); } } end() { this.log(` You are ready to go! 🎉 Now you can run the app by following these steps: 1. ${backendCommand[this.userInputs.backend]}. 2. (On a separate terminal) Navigate to \`client\` folder and run \`yarn start\` 3. Your app should be ready for development at localhost:3000 with the server running on localhost:3001. `); } } export default DuffelAppGenerator;