-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/mantis new cli #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
771e9d7
b94413f
879dc03
6daab03
7b58527
103f127
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import ora from 'ora'; | ||
| import { Action } from '../abstract.action'; | ||
| import { GenerateActionOptions } from './generate.types'; | ||
| import { execa } from 'execa'; | ||
|
|
||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| export class GenerateAction extends Action { | ||
| private options: GenerateActionOptions; | ||
|
|
||
| constructor(options: GenerateActionOptions) { | ||
| super('[GENERATE-ACTION]'); | ||
| this.options = options; | ||
| } | ||
|
|
||
| updateIndexTs = async (name: string) => { | ||
| const filePath = path.join('shared-ui/src', 'index.ts'); | ||
| let content = await fs.promises.readFile(filePath, 'utf8'); | ||
| // add new line to the end of the file | ||
| content += `\nexport * from './lib/${name}/${name}.component';`; | ||
| await fs.promises.writeFile(filePath, content, 'utf8'); | ||
| }; | ||
|
|
||
| addNewComponentToTsConfig = async (name: string) => { | ||
| const filePath = path.join('tsconfig.base.json'); | ||
| const content = await fs.promises.readFile(filePath, 'utf8'); | ||
| // Update tsconfig.base.json to add the new component in compilerOptions.paths array | ||
| const json = JSON.parse(content); | ||
| const COMPONENT_PATH = `shared-ui/src/lib/${name}/${name}.component`; | ||
| json.compilerOptions.paths[`@todo/${name}`] = [COMPONENT_PATH]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does |
||
| await fs.promises.writeFile( | ||
| filePath, | ||
| JSON.stringify(json, null, 2), | ||
| 'utf8', | ||
| ); | ||
| }; | ||
|
|
||
| generateComponent = async (name: string) => { | ||
| const spinner = ora(); | ||
| spinner.start('Generating component'); | ||
| // generate new shared ui component | ||
| await execa('npx', [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would need to be generic depending on the package manager used. |
||
| 'nx', | ||
| 'generate', | ||
| '@nx/angular:component', | ||
| name, | ||
| '--directory', | ||
| `shared-ui/src/lib/${name}`, | ||
| '--nameAndDirectoryFormat', | ||
| 'as-provided', | ||
| ]); | ||
| // export the component from the index.ts file | ||
| await this.updateIndexTs(name); | ||
| // add new component to tsconfig.base.json | ||
| await this.addNewComponentToTsConfig(name); | ||
| spinner.succeed(); | ||
| }; | ||
|
|
||
| async execute() { | ||
| try { | ||
| this.logger.info('Generating...'); | ||
| const { type, name, filePath } = this.options; | ||
| switch (type) { | ||
| case 'component': | ||
| await this.generateComponent(name); | ||
| break; | ||
| case 'service': | ||
| this.logger.warning('Unimplemented'); | ||
| break; | ||
| case 'mongo-schema': | ||
| if (!filePath) { | ||
| throw new Error('File path is required'); | ||
| } | ||
| this.logger.warning('Unimplemented'); | ||
| // await generateMongoSchema({ | ||
| // name, | ||
| // filePath, | ||
| // }); | ||
| break; | ||
| } | ||
| } catch (error) { | ||
| this.logger.error(error); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export type GeneratorType = 'component' | 'service' | 'mongo-schema'; | ||
|
|
||
| export type GenerateActionOptions = { | ||
| type: GeneratorType; | ||
| name: string; | ||
| filePath?: string; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| import { checkNodeVersion } from '../../utils/utilities.helper'; | ||
| import { Action } from '../abstract.action'; | ||
| import { InitActionOptions } from './init.types'; | ||
| import { MongoClient } from 'mongodb'; | ||
| import { PackageManager, PackageManagerName, installDependencies } from 'nypm'; | ||
|
|
||
| import fsExtra from 'fs-extra'; | ||
| import { $, execaCommand } from 'execa'; | ||
| import fs from 'fs'; | ||
| import inquirer from 'inquirer'; | ||
| import ora from 'ora'; | ||
| import path from 'path'; | ||
| import { changeDirectory } from '../../utils/files.helper'; | ||
| import { printWithMantisGradient } from '../../utils/prettyPrint.helper'; | ||
|
|
||
| type PMTypePromptResult = Pick<PackageManager, 'name'>; | ||
|
|
||
| export default class InitAction extends Action { | ||
| private options: InitActionOptions; | ||
|
|
||
| constructor(options: InitActionOptions) { | ||
| super('[INIT-ACTION]', { checkMantisProject: false }); | ||
| this.options = options; | ||
| } | ||
|
|
||
| promptForPackageManagerSelect = async (): Promise<PMTypePromptResult> => { | ||
| const temp = await inquirer.prompt<PMTypePromptResult>({ | ||
| type: 'list', | ||
| name: 'name' satisfies keyof PMTypePromptResult, | ||
| message: "Select the package manager you'd like to use:", | ||
| choices: ['npm', 'bun', 'pnpm', 'yarn'] satisfies PackageManagerName[], | ||
| }); | ||
| return temp; | ||
| }; | ||
|
|
||
| installDependenciesWithMessage = async (workspacePath: string) => { | ||
| const pm = await this.promptForPackageManagerSelect(); | ||
| const spinner = ora('Installing dependencies').start(); | ||
| await installDependencies({ | ||
| packageManager: pm.name, | ||
| cwd: workspacePath, | ||
| silent: true, | ||
| }); | ||
| spinner.succeed('Installed dependencies'); | ||
| return pm; | ||
| }; | ||
|
|
||
| async promptForMissingValues() { | ||
| type PromptResult = { | ||
| workspaceName: string; | ||
| mongoUrl: string; | ||
| createMobile: boolean; | ||
| }; | ||
| const answers = await inquirer.prompt<PromptResult>([ | ||
| { | ||
| type: 'input', | ||
| name: 'workspaceName', | ||
| message: 'Enter the name of the workspace to create:', | ||
| default: 'mantis', | ||
| when: () => !this.options.workspaceName, | ||
| validate: async (value) => { | ||
| if (value.trim().length <= 0) | ||
| return 'Workspace name must not be empty'; | ||
| if (await fsExtra.pathExists(value)) { | ||
| return `Workspace already exists: ${value}`; | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| }, | ||
| { | ||
| type: 'input', | ||
| name: 'mongoUrl', | ||
| message: | ||
| "Enter the MongoDB URI (default is 'mongodb://localhost:27017/mantis'):", | ||
| default: 'mongodb://localhost:27017/mantis', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few things to note. Could we update the prompt to be clear that we will set up the db for the user? The original prompt used the following options: Secondly, can we make the db unique to the project? We wouldn't want every mantis project the user creates to connect to the same db. |
||
| when: () => !this.options.mongodbUri, | ||
| validate: async (value) => { | ||
| if (value.trim().length <= 0) return 'Db url must not be empty'; | ||
| try { | ||
| await MongoClient.connect(value); | ||
| return true; | ||
| } catch (error) { | ||
| return `Db url seems to be invalid: ${error.message}`; | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| type: 'confirm', | ||
| name: 'createMobile', | ||
| message: 'Create mobile app?', | ||
| when: () => ![true, false].includes(this.options.createMobile), | ||
| }, | ||
| ]); | ||
|
|
||
| return { | ||
| workspaceName: answers.workspaceName || this.options.workspaceName, | ||
| mongoUrl: answers.mongoUrl || this.options.mongodbUri, | ||
| createMobile: [true, false].includes(answers.createMobile) | ||
| ? answers.createMobile | ||
| : this.options.createMobile, | ||
| }; | ||
| } | ||
|
|
||
| async createWorkspace(workspaceName: string) { | ||
| const spinner = ora(`Creating ${workspaceName} workspace...`).start(); | ||
| try { | ||
| const { stdout } = await execaCommand( | ||
| `yes | npx create-nx-workspace ${workspaceName} --preset=ts --nxCloud=skip --interactive=false`, | ||
| { shell: true }, | ||
| ); | ||
| console.log(stdout); | ||
| spinner.succeed(`Workspace ${workspaceName} created`); | ||
| } catch (error) { | ||
| spinner.fail(`Failed to create ${workspaceName} workspace`); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async setupEnvironment( | ||
| workspacePath: string, | ||
| mongoUrl: string, | ||
| frontendUrl: string = 'http://localhost:4200', | ||
| ) { | ||
| const spinner = ora('Setting up environment variables...').start(); | ||
| const envPath = path.join(workspacePath, '.env'); | ||
| const envContent = `MONGODB_URI=${mongoUrl}\nFRONTEND_URLS=${frontendUrl}`; | ||
| fs.writeFileSync(envPath, envContent); | ||
| spinner.succeed('Environment variables set'); | ||
| } | ||
|
|
||
| async cloneAndSetupTemplate(params: { | ||
| workspaceName: string; | ||
| workspacePath: string; | ||
| createMobile: boolean; | ||
| }) { | ||
| const { workspaceName, workspacePath } = params; | ||
| const spinner = ora('Setting up project template...').start(); | ||
| try { | ||
| await $`git clone --no-checkout https://github.com/mantis-apps/mantis-templates.git`; | ||
| changeDirectory('mantis-templates'); | ||
| await $`git sparse-checkout init`; | ||
| await $`git sparse-checkout set todo`; | ||
| await $`git checkout`; | ||
|
|
||
| await $`mv todo/app-web ${workspacePath}/app-web`; | ||
| if (params.createMobile) { | ||
| await $`mv todo/app-mobile ${workspacePath}/app-mobile`; | ||
| } | ||
| await $`mv todo/shared-ui ${workspacePath}/shared-ui`; | ||
| await $`mv todo/tsconfig.base.json ${workspacePath}/tsconfig.base.json`; | ||
| await $`mv todo/jest.config.ts ${workspacePath}/jest.config.ts`; | ||
| await $`mv todo/jest.preset.js ${workspacePath}/jest.preset.js`; | ||
| await $`mv todo/package.json ${workspacePath}/package.json`; | ||
| // await $`mv todo/package-lock.json ${workspacePath}/package-lock.json`; | ||
| await $`mv todo/nx.json ${workspacePath}/nx.json`; | ||
|
|
||
| changeDirectory(workspacePath); | ||
| await $`npm pkg set name=@${workspaceName}/source`; | ||
| await $`rm -rf mantis-templates`; | ||
| spinner.succeed('Project template setup complete.'); | ||
| } catch (error) { | ||
| spinner.fail('Failed to set up project template.'); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async execute() { | ||
| try { | ||
| printWithMantisGradient(`🛖 WORKSPACE CREATION 🛠️`); | ||
| await checkNodeVersion(); | ||
| const { workspaceName, mongoUrl, createMobile } = | ||
| await this.promptForMissingValues(); | ||
|
|
||
| const workspacePath = path.join(process.cwd(), workspaceName); | ||
| this.logger.info('Node version is valid'); | ||
| await this.createWorkspace(workspaceName); | ||
| await this.setupEnvironment(workspacePath, mongoUrl); | ||
|
|
||
| // navigate to the workspace | ||
| changeDirectory(workspacePath); | ||
|
|
||
| // remove node_modules, package-lock.json & package.json | ||
| await $`rm -rf node_modules package-lock.json package.json`; | ||
|
|
||
| // clone and setup template | ||
| await this.cloneAndSetupTemplate({ | ||
| workspaceName, | ||
| workspacePath, | ||
| createMobile, | ||
| }); | ||
|
|
||
| // install dependencies | ||
| const pm = await this.installDependenciesWithMessage(workspacePath); | ||
|
|
||
| // add mantis json | ||
| const mantisJsonPath = path.join(workspacePath, 'mantis.json'); | ||
| const mantisJsonContent = `{ | ||
| "name": "${workspaceName}", | ||
| "version": "0.0.1", | ||
| "description": "Mantis Workspace", | ||
| "workspace": ["@${workspaceName}/source"], | ||
| "packageManager": "${pm.name}" | ||
| }`; | ||
| fs.writeFileSync(mantisJsonPath, mantisJsonContent); | ||
| fs.writeFileSync(mantisJsonPath, mantisJsonContent); | ||
| this.logger.info('Workspace initialized'); | ||
| } catch (error) { | ||
| this.logger.error(error); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export type InitActionOptions = { | ||
| workspaceName: string; | ||
| mongodbUri: string; | ||
| createMobile: boolean; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: why use this when we already have
enquirer? I'm ok if we want to switch. We should just standardize on one though.