Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 280 additions & 82 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"figlet": "^1.7.0",
"fs-extra": "^11.2.0",
"gradient-string": "^2.0.2",
"inquirer": "^9.2.22",
Copy link
Collaborator

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.

"joi": "^17.13.3",
"log-symbols": "^6.0.0",
"mongodb": "^6.3.0",
"node-emoji": "^2.1.3",
Expand Down
11 changes: 10 additions & 1 deletion src/actions/abstract.action.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { verifyMantisProject } from '../utils/verify-mantis-project';
import { OrbitLogger } from '../utils/orbitLogger.helper';

/**
Expand All @@ -7,8 +8,16 @@ import { OrbitLogger } from '../utils/orbitLogger.helper';
export abstract class Action {
protected logger: OrbitLogger;

constructor(loggerContext: string) {
constructor(
loggerContext: string,
options: { checkMantisProject: boolean } = {
checkMantisProject: true,
},
) {
this.logger = new OrbitLogger(loggerContext);
if (options?.checkMantisProject) {
verifyMantisProject();
}
}

/**
Expand Down
86 changes: 86 additions & 0 deletions src/actions/generate/generate.actions.ts
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];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @todo need to be updated to be generic to the project name?

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', [
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
}
}
}
7 changes: 7 additions & 0 deletions src/actions/generate/generate.types.ts
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;
};
212 changes: 212 additions & 0 deletions src/actions/init/init.actions.ts
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',
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:

      {
        name: 'Create a local development mongo db for me (default)',
        value: 'local',
      },
      {
        name: 'Provide a mongo connection string',
        value: 'dbUrl',
      },

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);
}
}
}
5 changes: 5 additions & 0 deletions src/actions/init/init.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type InitActionOptions = {
workspaceName: string;
mongodbUri: string;
createMobile: boolean;
};
Loading