diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 64dec25..7aa6b3e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -20,9 +20,11 @@ module.exports = { "node_modules/*", "expressots.config.ts", "commitlint.config.ts", - "vitest.config.ts", + "jest.config.ts", ".eslintrc.cjs", "coverage/*", + "scripts/*", + "test/**/*.spec.ts", ], rules: { "@typescript-eslint/interface-name-prefix": "off", diff --git a/.gitignore b/.gitignore index 7ef4f15..fe87823 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ expressots.config.ts *.tgz -*.container.ts +app.ts -coverage/ \ No newline at end of file +coverage/ +.early.coverage diff --git a/expressots.config.ts b/expressots.config.ts index a8f1b4a..7b564e5 100644 --- a/expressots.config.ts +++ b/expressots.config.ts @@ -1,9 +1,13 @@ -import { ExpressoConfig, Pattern } from "./src/types"; +import { ExpressoConfig, Pattern } from "@expressots/shared"; const config: ExpressoConfig = { sourceRoot: "src", scaffoldPattern: Pattern.KEBAB_CASE, opinionated: false, + env: { + development: ".env.development", + production: ".env.production", + }, /* scaffoldSchematics: { entity: "model", provider: "adapter", diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..206eaad --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,24 @@ +import type { JestConfigWithTsJest } from "ts-jest"; + +const config: JestConfigWithTsJest = { + testEnvironment: "node", + roots: ["/src", "/test"], + testRegex: ".*\\.spec\\.ts$", + testPathIgnorePatterns: ["/node_modules/", "/bin/"], + collectCoverageFrom: ["src/**/*.ts", "!**/*.spec.ts", "src/**/index.ts"], + moduleNameMapper: { + "^@src/(.*)$": "/src/$1", + }, + setupFiles: ["reflect-metadata"], + transform: { + "^.+\\.ts$": [ + "ts-jest", + { + tsconfig: "tsconfig.json", + // Add any ts-jest specific options here + }, + ], + }, +}; + +export default config; diff --git a/package.json b/package.json index 7891adb..2a6e80a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expressots/cli", - "version": "1.12.0", + "version": "3.0.0-beta.1", "description": "Expressots CLI - modern, fast, lightweight nodejs web framework (@cli)", "author": "Richard Zampieri", "license": "MIT", @@ -8,10 +8,11 @@ "url": "https://github.com/expressots/expressots-cli/issues" }, "bin": { - "expressots": "bin/cli.js" + "expressots": "bin/cli.js", + "ex": "bin/cli.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.18.0" }, "funding": { "type": "github", @@ -34,23 +35,22 @@ "prepare": "husky", "start:build": "npm run build && npm run start", "start": "node ./bin/cli.js", - "start:dev": "tsnd ./src/cli.ts", - "build": "npm run clean && tsc -p tsconfig.json && yarn cp:templates && chmod +x ./bin/cli.js", - "cp:templates": "cp -r ./src/generate/templates ./bin/generate/templates", - "clean": "rimraf ./bin", + "start:dev": "tsx ./src/cli.ts", + "build": "npm run clean && tsc -p tsconfig.json && npm run cp:templates && node scripts/chmod.js ./bin/cli.js", + "cp:templates": "node scripts/cp.js ./src/generate/templates ./bin/generate/", + "clean": "node scripts/rm.js bin", "prepublish": "npm run build && npm pack", "publish": "npm publish --tag latest", "format": "prettier --write \"./src/**/*.ts\" --cache", "lint": "eslint \"./src/**/*.ts\"", "lint:fix": "eslint \"./src/**/*.ts\" --fix", "release": "release-it", - "test": "vitest run --reporter default", - "test:watch": "vitest", - "coverage": "vitest run --coverage" + "test": "jest", + "coverage": "jest --coverage", + "test:watch": "jest --watch" }, "dependencies": { - "@expressots/boost-ts": "1.3.0", - "axios": "^1.7.3", + "axios": "1.7.7", "chalk-animation": "2.0.3", "cli-progress": "3.12.0", "cli-table3": "0.6.5", @@ -66,31 +66,30 @@ "@codecov/vite-plugin": "^0.0.1-beta.9", "@commitlint/cli": "19.2.1", "@commitlint/config-conventional": "19.1.0", + "@expressots/shared": "0.1.0", "@release-it/conventional-changelog": "7.0.2", "@types/chalk-animation": "1.6.1", "@types/cli-progress": "3.11.0", "@types/degit": "2.8.3", "@types/inquirer": "9.0.3", + "@types/jest": "^29.5.14", "@types/mustache": "4.2.2", "@types/node": "20.12.7", "@types/yargs": "17.0.22", "@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/parser": "7.6.0", - "@vitest/coverage-v8": "1.4.0", "chalk": "4.1.2", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "husky": "9.0.11", + "jest": "^29.7.0", "prettier": "3.2.5", "reflect-metadata": "0.2.2", "release-it": "16.3.0", - "rimraf": "5.0.5", "shx": "0.3.4", - "ts-node-dev": "2.0.0", - "typescript": "5.2.2", - "vite": "5.2.8", - "vite-tsconfig-paths": "4.3.2", - "vitest": "1.4.0" + "ts-jest": "^29.2.5", + "tsx": "^4.19.2", + "typescript": "5.2.2" }, "release-it": { "git": { diff --git a/scripts/chmod.js b/scripts/chmod.js new file mode 100644 index 0000000..80e0d05 --- /dev/null +++ b/scripts/chmod.js @@ -0,0 +1,23 @@ +const { execSync } = require("child_process"); + +function makeExecutable(targetFile) { + if (process.platform !== "win32") { // Check if the OS is not Windows + try { + execSync(`chmod +x ${targetFile}`); + console.log(`Made ${targetFile} executable.`); + } catch (error) { + console.error(`Error making ${targetFile} executable:`, error.message); + process.exit(1); + } + } else { + console.log(`Skipping chmod on Windows for ${targetFile}`); + } +} + +if (process.argv.length !== 3) { + console.error("Usage: node chmod.js "); + process.exit(1); +} + +const targetFile = process.argv[2]; +makeExecutable(targetFile); \ No newline at end of file diff --git a/scripts/cp.js b/scripts/cp.js new file mode 100644 index 0000000..3d2089c --- /dev/null +++ b/scripts/cp.js @@ -0,0 +1,35 @@ +const fs = require("fs"); +const path = require("path"); + +function copyRecursiveSync(src, dest) { + const exists = fs.existsSync(src); + const stats = exists && fs.statSync(src); + const isDirectory = exists && stats.isDirectory(); + + if (isDirectory) { + fs.mkdirSync(dest, { recursive: true }); + fs.readdirSync(src).forEach(function (childItemName) { + copyRecursiveSync( + path.join(src, childItemName), + path.join(dest, childItemName), + ); + }); + } else { + fs.copyFileSync(src, dest); + } +} + +if (process.argv.length < 4) { + process.stderr.write( + "Usage: node copy.js ... \n", + ); + process.exit(1); +} + +const destination = process.argv[process.argv.length - 1]; + +for (let i = 2; i < process.argv.length - 1; i++) { + const origin = process.argv[i]; + copyRecursiveSync(origin, path.join(destination, path.basename(origin))); + process.stdout.write(`Copied: ${origin} to ${destination}\n`); +} \ No newline at end of file diff --git a/scripts/rm.js b/scripts/rm.js new file mode 100644 index 0000000..6ee4423 --- /dev/null +++ b/scripts/rm.js @@ -0,0 +1,24 @@ +const fs = require("fs").promises; + +const removeTarget = async (target) => { + try { + const targetExists = await fs.stat(target).catch(() => null); + if (!targetExists) { + process.stdout.write(`Directory '${target}' does not exist.\n`); + return; + } + await fs.rm(target, { recursive: true, force: true }); + process.stdout.write(`Removed: ${target}\n`); + } catch (error) { + process.stderr.write(`Error: Unable to remove '${target}'\n`); + process.exit(1); + } +}; + +if (process.argv.length !== 3) { + process.stderr.write("Usage: node rm.js \n"); + process.exit(1); +} + +const target = process.argv[2]; +removeTarget(target); diff --git a/src/@types/config.ts b/src/@types/config.ts deleted file mode 100644 index 9d788c4..0000000 --- a/src/@types/config.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const enum Pattern { - LOWER_CASE = "lowercase", - KEBAB_CASE = "kebab-case", - PASCAL_CASE = "PascalCase", - CAMEL_CASE = "camelCase", -} - -interface IProviders { - prisma?: { - schemaName: string; - schemaPath: string; - entitiesPath: string; - entityNamePattern: string; - }; -} - -/** - * The configuration object for the Expresso CLI. - * - * @property {Pattern} scaffoldPattern - The pattern to use when scaffolding files. - * @property {string} sourceRoot - The root directory for the source files. - * @property {boolean} opinionated - Whether or not to use the opinionated configuration. - * - * @see [ExpressoConfig](https://expresso-ts.com/docs) - */ -export interface ExpressoConfig { - scaffoldPattern: Pattern; - sourceRoot: string; - opinionated: boolean; - providers?: IProviders; - scaffoldSchematics?: { - entity?: string; - controller?: string; - usecase?: string; - dto?: string; - module?: string; - provider?: string; - middleware?: string; - }; -} diff --git a/src/@types/index.ts b/src/@types/index.ts deleted file mode 100644 index 5c62e04..0000000 --- a/src/@types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./config"; diff --git a/src/cli.ts b/src/cli.ts index 5af8b34..03c941b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,11 +13,17 @@ import { generateProject } from "./generate"; import { helpCommand } from "./help/cli"; import { infoProject } from "./info"; import { createProject } from "./new"; -import { addProviderCMD } from "./providers"; +import { addProviderCMD, removeProviderCMD } from "./providers"; import { createExternalProviderCMD } from "./providers/create/cli"; import { printError } from "./utils/cli-ui"; import { scriptsCommand } from "./scripts"; +/** + * The current version of the ExpressoTS Bundle. + * core, adapters, and cli. + */ +export const BUNDLE_VERSION = "3.0.0-beta.1"; + stdout.write(`\n${[chalk.bold.green("🐎 Expressots")]}\n\n`); yargs(hideBin(process.argv)) @@ -28,6 +34,7 @@ yargs(hideBin(process.argv)) .command(prodCommand) .command(createExternalProviderCMD()) .command(addProviderCMD()) + .command(removeProviderCMD()) .command(generateProject()) .command(scriptsCommand()) .command(infoProject()) diff --git a/src/commands/__tests__/project.commands.spec.ts b/src/commands/__tests__/project.commands.spec.ts deleted file mode 100644 index e331cb5..0000000 --- a/src/commands/__tests__/project.commands.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from "vitest"; - -describe("project.commands", () => { - it("should return a list of commands", () => { - expect(true).toBe(true); - }); -}); diff --git a/src/commands/project.commands.ts b/src/commands/project.commands.ts index 500d109..41c001d 100644 --- a/src/commands/project.commands.ts +++ b/src/commands/project.commands.ts @@ -45,22 +45,13 @@ function getOutDir(): string { * @returns The configuration */ const opinionatedConfig: Array = [ - "--transpile-only", - "--clear", - "-r", - "dotenv/config", + "--watch", "-r", "tsconfig-paths/register", "./src/main.ts", ]; -const nonOpinionatedConfig: Array = [ - "--transpile-only", - "--clear", - "-r", - "dotenv/config", - "./src/main.ts", -]; +const nonOpinionatedConfig: Array = ["--watch", "./src/main.ts"]; /** * Dev command module @@ -191,7 +182,7 @@ export const runCommand = async ({ switch (command) { case "dev": execCmd( - "tsnd", + "tsx", opinionated ? opinionatedConfig : nonOpinionatedConfig, ); break; @@ -219,14 +210,12 @@ export const runCommand = async ({ let config: Array = []; if (opinionated) { config = [ - "-r", - "dotenv/config", "-r", `./${outDir}/register-path.js`, `./${outDir}/src/main.js`, ]; } else { - config = ["-r", "dotenv/config", `./${outDir}/main.js`]; + config = [`./${outDir}/main.js`]; } clearScreen(); execCmd("node", config); diff --git a/src/generate/templates/nonopinionated/module.tpl b/src/generate/templates/nonopinionated/module.tpl index beef305..7f7de62 100644 --- a/src/generate/templates/nonopinionated/module.tpl +++ b/src/generate/templates/nonopinionated/module.tpl @@ -1,4 +1,3 @@ -import { ContainerModule } from "inversify"; -import { CreateModule } from "@expressots/core"; +import { ContainerModule, CreateModule } from "@expressots/core"; export const {{moduleName}}{{schematic}}: ContainerModule = CreateModule([]); diff --git a/src/generate/templates/opinionated/controller-service-delete.tpl b/src/generate/templates/opinionated/controller-service-delete.tpl index 7d6c0e2..3764b47 100644 --- a/src/generate/templates/opinionated/controller-service-delete.tpl +++ b/src/generate/templates/opinionated/controller-service-delete.tpl @@ -1,21 +1,13 @@ -import { BaseController, StatusCode } from "@expressots/core"; -import { controller, Delete, param, response } from "@expressots/adapter-express"; -import { Response } from "express"; +import { controller, Delete, param } from "@expressots/adapter-express"; +import { inject } from "@expressots/core"; import { {{className}}UseCase } from "./{{fileName}}.usecase"; -import { I{{className}}RequestDTO, I{{className}}ResponseDTO } from "./{{fileName}}.dto"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { - constructor(private {{useCase}}UseCase: {{className}}UseCase) { - super(); - } +export class {{className}}Controller { + @inject({{className}}UseCase) private {{useCase}}UseCase: {{className}}UseCase; @Delete("/:id") - execute(@param("id") id: string, @response() res: Response): I{{className}}ResponseDTO { - return this.callUseCase( - this.{{useCase}}UseCase.execute(id), - res, - StatusCode.OK, - ); + execute(@param("id") id: string) { + return this.{{useCase}}UseCase.execute(id); } } diff --git a/src/generate/templates/opinionated/controller-service-get.tpl b/src/generate/templates/opinionated/controller-service-get.tpl index ed8ad1f..1d87d32 100644 --- a/src/generate/templates/opinionated/controller-service-get.tpl +++ b/src/generate/templates/opinionated/controller-service-get.tpl @@ -1,21 +1,13 @@ -import { BaseController, StatusCode } from "@expressots/core"; -import { controller, Get, response } from "@expressots/adapter-express"; -import { Response } from "express"; +import { controller, Get } from "@expressots/adapter-express"; +import { inject } from "@expressots/core"; import { {{className}}UseCase } from "./{{fileName}}.usecase"; -import { I{{className}}ResponseDTO } from "./{{fileName}}.dto"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { - constructor(private {{useCase}}UseCase: {{className}}UseCase) { - super(); - } - +export class {{className}}Controller { + @inject({{className}}UseCase) private {{useCase}}UseCase: {{className}}UseCase; + @Get("/") - execute(@response() res: Response): I{{className}}ResponseDTO { - return this.callUseCase( - this.{{useCase}}UseCase.execute(), - res, - StatusCode.OK, - ); + execute() { + return this.{{useCase}}UseCase.execute(); } } diff --git a/src/generate/templates/opinionated/controller-service-patch.tpl b/src/generate/templates/opinionated/controller-service-patch.tpl index c7dd160..8ebdc36 100644 --- a/src/generate/templates/opinionated/controller-service-patch.tpl +++ b/src/generate/templates/opinionated/controller-service-patch.tpl @@ -1,24 +1,14 @@ -import { BaseController, StatusCode } from "@expressots/core"; -import { controller, Patch, body, param, response } from "@expressots/adapter-express"; -import { Response } from "express"; +import { controller, Patch, body } from "@expressots/adapter-express"; +import { inject } from "@expressots/core"; import { {{className}}UseCase } from "./{{fileName}}.usecase"; -import { I{{className}}RequestDTO, I{{className}}ResponseDTO } from "./{{fileName}}.dto"; +import { I{{className}}RequestDTO } from "./{{fileName}}.dto"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { - constructor(private {{useCase}}UseCase: {{className}}UseCase) { - super(); - } +export class {{className}}Controller { + @inject({{className}}UseCase) private {{useCase}}UseCase: {{className}}UseCase; @Patch("/") - execute( - @body() payload: I{{className}}RequestDTO, - @response() res: Response, - ): I{{className}}ResponseDTO { - return this.callUseCase( - this.{{useCase}}UseCase.execute(payload), - res, - StatusCode.OK, - ); + execute(@body() payload: I{{className}}RequestDTO) { + return this.{{useCase}}UseCase.execute(payload); } } diff --git a/src/generate/templates/opinionated/controller-service-post.tpl b/src/generate/templates/opinionated/controller-service-post.tpl index ac8481d..f9be0f0 100644 --- a/src/generate/templates/opinionated/controller-service-post.tpl +++ b/src/generate/templates/opinionated/controller-service-post.tpl @@ -1,21 +1,14 @@ -import { BaseController, StatusCode } from "@expressots/core"; -import { controller, Post, body, response } from "@expressots/adapter-express"; -import { Response } from "express"; +import { body, controller, Post } from "@expressots/adapter-express"; +import { inject } from "@expressots/core"; import { {{className}}UseCase } from "./{{fileName}}.usecase"; -import { I{{className}}RequestDTO, I{{className}}ResponseDTO } from "./{{fileName}}.dto"; +import { I{{className}}RequestDTO } from "./{{fileName}}.dto"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { - constructor(private {{useCase}}UseCase: {{className}}UseCase) { - super(); - } +export class {{className}}Controller { + @inject({{className}}UseCase) private {{useCase}}UseCase: {{className}}UseCase; @Post("/") - execute(@body() payload: I{{className}}RequestDTO, @response() res: Response): I{{className}}ResponseDTO { - return this.callUseCase( - this.{{useCase}}UseCase.execute(payload), - res, - StatusCode.Created, - ); + execute(@body() payload: I{{className}}RequestDTO) { + return this.{{useCase}}UseCase.execute(payload); } } diff --git a/src/generate/templates/opinionated/controller-service-put.tpl b/src/generate/templates/opinionated/controller-service-put.tpl index 806c782..153662c 100644 --- a/src/generate/templates/opinionated/controller-service-put.tpl +++ b/src/generate/templates/opinionated/controller-service-put.tpl @@ -1,24 +1,14 @@ -import { BaseController, StatusCode } from "@expressots/core"; -import { controller, Put, body, param, response } from "@expressots/adapter-express"; -import { Response } from "express"; +import { body, controller, Put } from "@expressots/adapter-express"; +import { inject } from "@expressots/core"; import { {{className}}UseCase } from "./{{fileName}}.usecase"; -import { I{{className}}RequestDTO, I{{className}}ResponseDTO } from "./{{fileName}}.dto"; +import { I{{className}}RequestDTO } from "./{{fileName}}.dto"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { - constructor(private {{useCase}}UseCase: {{className}}UseCase) { - super(); - } +export class {{className}}Controller { + @inject({{className}}UseCase) private {{useCase}}UseCase: {{className}}UseCase; @Put("/") - execute( - @body() payload: I{{className}}RequestDTO, - @response() res: Response, - ): I{{className}}ResponseDTO { - return this.callUseCase( - this.{{useCase}}UseCase.execute(payload), - res, - StatusCode.OK, - ); + execute(@body() payload: I{{className}}RequestDTO) { + return this.{{useCase}}UseCase.execute(payload); } } diff --git a/src/generate/templates/opinionated/controller-service.tpl b/src/generate/templates/opinionated/controller-service.tpl index cad0b78..6969e5a 100644 --- a/src/generate/templates/opinionated/controller-service.tpl +++ b/src/generate/templates/opinionated/controller-service.tpl @@ -1,8 +1,7 @@ -import { BaseController } from "@expressots/core"; import { controller, {{method}} } from "@expressots/adapter-express"; @controller("/{{{route}}}") -export class {{className}}Controller extends BaseController { +export class {{className}}Controller { @{{method}}("/") execute() { return "Ok"; diff --git a/src/generate/templates/opinionated/module-service.tpl b/src/generate/templates/opinionated/module-service.tpl index 9ef58e7..255eeec 100644 --- a/src/generate/templates/opinionated/module-service.tpl +++ b/src/generate/templates/opinionated/module-service.tpl @@ -1,5 +1,4 @@ -import { ContainerModule } from "inversify"; -import { CreateModule } from "@expressots/core"; +import { CreateModule, ContainerModule } from "@expressots/core"; import { {{className}}Controller } from "{{{path}}}"; export const {{moduleName}}Module: ContainerModule = CreateModule([{{className}}Controller]); diff --git a/src/generate/templates/opinionated/module.tpl b/src/generate/templates/opinionated/module.tpl index 45df72d..e32c88f 100644 --- a/src/generate/templates/opinionated/module.tpl +++ b/src/generate/templates/opinionated/module.tpl @@ -1,4 +1,3 @@ -import { ContainerModule } from "inversify"; -import { CreateModule } from "@expressots/core"; +import { CreateModule, ContainerModule } from "@expressots/core"; export const {{moduleName}}Module: ContainerModule = CreateModule([]); diff --git a/src/generate/utils/command-utils.ts b/src/generate/utils/command-utils.ts index 9dd1dae..4f73f9d 100644 --- a/src/generate/utils/command-utils.ts +++ b/src/generate/utils/command-utils.ts @@ -6,12 +6,12 @@ import { anyCaseToKebabCase, anyCaseToPascalCase, anyCaseToLowerCase, -} from "@expressots/boost-ts"; +} from "./string-utils"; import { printError } from "../../utils/cli-ui"; import { verifyIfFileExists } from "../../utils/verify-file-exists"; import Compiler from "../../utils/compiler"; -import { ExpressoConfig, Pattern } from "../../types"; +import { ExpressoConfig, Pattern } from "@expressots/shared"; export const enum PathStyle { None = "none", diff --git a/src/generate/utils/nonopininated-cmd.ts b/src/generate/utils/nonopininated-cmd.ts index a7b8a4b..e1ee281 100644 --- a/src/generate/utils/nonopininated-cmd.ts +++ b/src/generate/utils/nonopininated-cmd.ts @@ -2,8 +2,8 @@ import { anyCaseToCamelCase, anyCaseToKebabCase, anyCaseToPascalCase, -} from "@expressots/boost-ts"; -import { ExpressoConfig } from "../../@types"; +} from "./string-utils"; +import { ExpressoConfig } from "@expressots/shared"; import { printGenerateSuccess } from "../../utils/cli-ui"; import { diff --git a/src/generate/utils/opinionated-cmd.ts b/src/generate/utils/opinionated-cmd.ts index 3c48006..e4663d7 100644 --- a/src/generate/utils/opinionated-cmd.ts +++ b/src/generate/utils/opinionated-cmd.ts @@ -2,7 +2,7 @@ import { anyCaseToCamelCase, anyCaseToKebabCase, anyCaseToPascalCase, -} from "@expressots/boost-ts"; +} from "./string-utils"; import * as nodePath from "node:path"; import fs from "fs"; import { printGenerateSuccess } from "../../utils/cli-ui"; @@ -20,7 +20,7 @@ import { addModuleToContainer, addModuleToContainerNestedPath, } from "../../utils/add-module-to-container"; -import { ExpressoConfig } from "../../@types"; +import { ExpressoConfig } from "@expressots/shared"; /** * Process commands for opinionated service scaffolding diff --git a/src/generate/utils/string-utils.ts b/src/generate/utils/string-utils.ts new file mode 100644 index 0000000..3b7532a --- /dev/null +++ b/src/generate/utils/string-utils.ts @@ -0,0 +1,67 @@ +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to camelCase. + * @param str - The input string to be converted. + * @returns The converted string in camelCase. + */ +export function anyCaseToCamelCase(str: string): string { + return str + .replace(/[-_]+(.)?/g, (_, char) => (char ? char.toUpperCase() : "")) + .replace(/^[A-Z]/, (char) => char.toLowerCase()); +} + +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to kebab-case. + * @param str - The input string to be converted. + * @returns The converted string in kebab-case. + */ +export function anyCaseToKebabCase(str: string): string { + return str + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") // Convert camelCase and PascalCase to kebab-case + .replace(/_/g, "-") // Convert snake_case to kebab-case + .toLowerCase(); // Ensure all characters are lowercase +} + +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to PascalCase. + * @param str - The input string to be converted. + * @returns The converted string in PascalCase. + */ +export function anyCaseToPascalCase(str: string): string { + return str + .replace(/[-_]+(.)?/g, (_, char) => (char ? char.toUpperCase() : "")) + .replace(/^[a-z]/, (char) => char.toUpperCase()); +} + +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to snake_case. + * @param str - The input string to be converted. + * @returns The converted string in snake_case. + */ +export function anyCaseToSnakeCase(str: string): string { + return str + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/[-]+/g, "_") + .toLowerCase(); +} + +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to UPPER CASE. + * @param str - The input string to be converted. + * @returns The converted string in UPPER CASE. + */ +export function anyCaseToUpperCase(str: string): string { + return str + .replace(/[-_]+(.)?/g, (_, char) => (char ? char.toUpperCase() : "")) + .toUpperCase(); +} + +/** + * Converts a string from any case (camelCase, PascalCase, kebab-case, snake_case) to lower case. + * @param str - The input string to be converted. + * @returns The converted string in lower case. + */ +export function anyCaseToLowerCase(str: string): string { + return str + .replace(/[-_]+(.)?/g, (_, char) => (char ? char.toLowerCase() : "")) + .toLowerCase(); +} diff --git a/src/help/form.ts b/src/help/form.ts index a027853..812641a 100644 --- a/src/help/form.ts +++ b/src/help/form.ts @@ -27,7 +27,12 @@ const helpForm = async (): Promise => { ["dto", "g d", "Generate a dto"], ["entity", "g e", "Generate an entity"], ["provider", "g p", "Generate internal provider"], - ["provider", "add", "Add external provider to the project"], + [ + "provider", + "add", + "Add provider to the project. Use -d to add as dev dependency", + ], + ["provider", "remove", "Remove provider from the project"], ["provider", "create", "Create external provider"], ["module", "g mo", "Generate a module"], ["middleware", "g mi", "Generate a middleware"], diff --git a/src/index.ts b/src/index.ts index 814fd72..5656364 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -export * from "./types"; export * from "./generate"; export * from "./utils"; export * from "./new"; diff --git a/src/info/form.ts b/src/info/form.ts index 37a9f54..9aa310a 100644 --- a/src/info/form.ts +++ b/src/info/form.ts @@ -1,9 +1,9 @@ import chalk from "chalk"; -import path from "path"; import fs from "fs"; import os from "os"; +import path from "path"; +import { BUNDLE_VERSION } from "../cli"; import { printError, printSuccess } from "../utils/cli-ui"; -import axios from "axios"; function getInfosFromPackage() { try { @@ -34,17 +34,5 @@ export const infoForm = (): void => { console.log(chalk.green("System information:")); console.log(chalk.white(`\tOS Version: ${os.version()}`)); console.log(chalk.white(`\tNodeJS version: ${process.version}`)); - currentCLIVersion(); + printSuccess("CLI version:", BUNDLE_VERSION); }; - -async function currentCLIVersion(): Promise { - try { - const response = await axios.get( - "https://api.github.com/repos/expressots/expressots-cli/releases", - ); - const latestRelease = `v${response.data[0].tag_name}`; - printSuccess("CLI version:", latestRelease); - } catch (error: Error | any) { - printError("Error:", error.message); - } -} diff --git a/src/new/form.ts b/src/new/form.ts index 982c837..596eabb 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -8,6 +8,7 @@ import path from "node:path"; import { centerText } from "../utils/center-text"; import { printError } from "../utils/cli-ui"; import { changePackageName } from "../utils/change-package-info"; +import { BUNDLE_VERSION } from "../cli"; async function packageManagerInstall({ packageManager, @@ -18,49 +19,70 @@ async function packageManagerInstall({ directory: string; progressBar: SingleBar; }) { - return new Promise((resolve, reject) => { - const isWindows: boolean = process.platform === "win32"; - const command: string = isWindows - ? `${packageManager}.cmd` - : packageManager; + const command: string = + process.platform === "win32" ? `${packageManager}.cmd` : packageManager; - const installProcess = spawn(command, ["install", "--prefer-offline"], { + const args = ["install", "--prefer-offline", "--silent"]; + if (packageManager === "yarn") { + args.push("--ignore-engines"); + args.splice(args.indexOf("--prefer-offline"), 1); + } + return new Promise((resolve, reject) => { + const installProcess = spawn(command, args, { cwd: directory, shell: true, timeout: 600000, }); - // eslint-disable-next-line prefer-const - let installTimeout: NodeJS.Timeout; - - installProcess.on("error", (error) => { - clearTimeout(installTimeout); - reject(new Error(`Failed to start subprocess: ${error.message}`)); - }); + // Simulate incremental progress + let progress = 0; + const interval = setInterval(() => { + if (progress < 90) { + progress += 5; + progressBar.update(progress); + } + }, 1000); + // Handle stdout for meaningful output or progress feedback installProcess.stdout?.on("data", (data: Buffer) => { const output = data.toString().trim(); - const npmProgressMatch = output.match( + // Remove all data from || to the end of the line + const cleanedOutput = output.replace(/\|\|.*$/g, ""); + + // Match and handle npm-specific progress + const npmProgressMatch = cleanedOutput.match( /\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/, ); if (npmProgressMatch) { const [, current, total, task] = npmProgressMatch; - const progress = Math.round( + progress = Math.round( (parseInt(current) / parseInt(total)) * 100, ); progressBar.update(progress, { doing: task }); } else { - progressBar.increment(5, { doing: output }); + // Update "task" without changing the progress + progressBar.update(progress, { doing: cleanedOutput }); } }); + // Handle errors + installProcess.on("error", (error) => { + clearInterval(interval); // Stop interval on error + progressBar.stop(); + reject(new Error(`Failed to start subprocess: ${error.message}`)); + }); + + // Finalize progress on close installProcess.on("close", (code) => { - clearTimeout(installTimeout); + clearInterval(interval); // Stop interval when the process ends if (code === 0) { + progressBar.update(100, { doing: "Complete!" }); // Finalize progress + progressBar.stop(); resolve("Installation Done!"); } else { + progressBar.stop(); reject( new Error( `${packageManager} install exited with code ${code}`, @@ -68,11 +90,6 @@ async function packageManagerInstall({ ); } }); - - installTimeout = setTimeout(() => { - installProcess.kill("SIGKILL"); - reject(new Error("Installation took too long. Aborted!")); - }, 600000); }); } @@ -218,9 +235,11 @@ const projectForm = async ( const [_, template] = answer.template.match(/(.*) ::/) as Array; + const repo: string = `expressots/templates/${templates[template]}#${BUNDLE_VERSION}`; + try { const emitter = degit( - `expressots/expressots/templates/${templates[template]}`, + `expressots/templates/${templates[template]}`, ); await emitter.clone(answer.name); @@ -250,8 +269,6 @@ const projectForm = async ( name: projectName, }); - renameEnvFile(answer.name); - progressBar.update(100); progressBar.stop(); diff --git a/src/providers/add/cli.ts b/src/providers/add/cli.ts index 7d92fe6..55bc610 100644 --- a/src/providers/add/cli.ts +++ b/src/providers/add/cli.ts @@ -1,5 +1,5 @@ import { Argv, CommandModule } from "yargs"; -import { addExternalProvider } from "./form"; +import { addProvider, removeProvider } from "./form"; // eslint-disable-next-line @typescript-eslint/ban-types type CommandModuleArgs = {}; @@ -17,13 +17,36 @@ export const addProviderCMD = (): CommandModule => { .option("version", { describe: "The provider version to be installed", type: "string", - default: "latest", + default: false, alias: "v", + }) + .option("dev", { + describe: "Add provider as a dev dependency", + type: "boolean", + default: false, + alias: "d", }); return yargs; }, - handler: async ({ provider, version }) => { - await addExternalProvider(provider, version); + handler: async ({ provider, version, dev }) => { + await addProvider(provider, version, dev); + }, + }; +}; + +export const removeProviderCMD = (): CommandModule => { + return { + command: "remove ", + describe: "Remove provider from the project.", + builder: (yargs: Argv): Argv => { + yargs.positional("provider", { + describe: "The provider to be removed from the project", + type: "string", + }); + return yargs; + }, + handler: async ({ provider: packageName }) => { + await removeProvider(packageName); }, }; }; diff --git a/src/providers/add/form.ts b/src/providers/add/form.ts index 88e99e1..8d53299 100644 --- a/src/providers/add/form.ts +++ b/src/providers/add/form.ts @@ -4,103 +4,141 @@ import fs from "node:fs"; import { exit } from "node:process"; import { printError } from "../../utils/cli-ui"; -export const addExternalProvider = async ( - provider: string, - version: string, -): Promise => { - await installProvider(provider, version); +type PackageManagerConfig = { + install: string; + addDev: string; + remove: string; }; -async function installProvider(provider: string, version: string) { - const packageManager = fs.existsSync( - "package-lock.json" || "yarn.lock" || "pnpm-lock.yaml", - ) - ? "npm" - : fs.existsSync("yarn.lock") - ? "yarn" - : fs.existsSync("pnpm-lock.yaml") - ? "pnpm" - : null; - - if (packageManager) { - console.log(`Installing ${provider} provider ...`); - const currentVersion = version === "latest" ? "" : `@${version}`; - await execProcess({ - commandArg: packageManager, - args: ["add", `${provider}${currentVersion}`, "--prefer-offline"], - directory: process.cwd(), - }); - } else { - printError( - "No package manager found in the project", - "install-provider", - ); - return; +type PackageManager = { + npm: PackageManagerConfig; + yarn: PackageManagerConfig; + pnpm: PackageManagerConfig; +}; + +const PACKAGE_MANAGERS: PackageManager = { + npm: { + install: "install", + addDev: "install --save-dev", + remove: "uninstall", + }, + yarn: { + install: "add", + addDev: "add --dev", + remove: "remove", + }, + pnpm: { + install: "add", + addDev: "add --save-dev", + remove: "remove", + }, +}; + +function detectPackageManager(): string | null { + const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]; + const managers = Object.keys(PACKAGE_MANAGERS); + + for (let i = 0; i < lockFiles.length; i++) { + if (fs.existsSync(lockFiles[i])) { + return managers[i]; + } } + return null; } async function execProcess({ - commandArg, + command, args, directory, }: { - commandArg: string; + command: string; args: string[]; directory: string; -}) { +}): Promise { return new Promise((resolve, reject) => { - const isWindows: boolean = process.platform === "win32"; - const command: string = isWindows ? `${commandArg}.cmd` : commandArg; + const isWindows = process.platform === "win32"; + const execCommand = isWindows ? `${command}.cmd` : command; - const installProcess = spawn(command, args, { + const processRunner = spawn(execCommand, args, { cwd: directory, shell: true, }); - console.log( - chalk.bold.blue(`Executing: ${commandArg} ${args.join(" ")}`), - ); + console.log(chalk.bold.blue(`Executing: ${command} ${args.join(" ")}`)); console.log( chalk.yellow("-------------------------------------------------"), ); - installProcess.stdout.on("data", (data) => { - console.log(chalk.green(data.toString().trim())); // Display regular messages in green + processRunner.stdout.on("data", (data) => { + console.log(chalk.green(data.toString().trim())); }); - installProcess.stderr.on("data", (data) => { - console.error(chalk.red(data.toString().trim())); // Display error messages in red + processRunner.stderr.on("data", (data) => { + console.error(chalk.red(data.toString().trim())); }); - installProcess.on("close", (code) => { + processRunner.on("close", (code) => { if (code === 0) { console.log( - chalk.bold.green( - "-------------------------------------------------", - ), + chalk.bold.green("Operation completed successfully!\n"), ); - console.log(chalk.bold.green("Installation Done!\n")); - resolve("Installation Done!"); + resolve(); } else { console.error( - chalk.bold.red("---------------------------------------"), - ); - console.error( - chalk.bold.red( - `Command ${command} ${args.join( - " ", - )} exited with code ${code}`, - ), - ); - reject( - new Error( - `Command ${command} ${args.join( - " ", - )} exited with code ${code}`, - ), + chalk.bold.red(`Command failed with exit code ${code}`), ); + reject(new Error(`Command failed with exit code ${code}`)); exit(1); } }); }); } + +export async function addProvider( + packageName: string, + version?: string, + isDevDependency = false, +): Promise { + const packageManager = detectPackageManager(); + + if (!packageManager) { + printError("No package manager found in the project", "add-package"); + return; + } + + const pkgManagerConfig: PackageManagerConfig = + PACKAGE_MANAGERS[packageManager as keyof PackageManager]; + + const command = isDevDependency + ? pkgManagerConfig.addDev + : pkgManagerConfig.install; + const versionSuffix = version && version !== "latest" ? `@${version}` : ""; + + console.log( + `${isDevDependency ? "Adding devDependency" : "Installing"} ${packageName}...`, + ); + await execProcess({ + command: packageManager, + args: [...command.split(" "), `${packageName}${versionSuffix}`], + directory: process.cwd(), + }); +} + +export async function removeProvider(packageName: string): Promise { + const packageManager = detectPackageManager(); + + if (!packageManager) { + printError("No package manager found in the project", "remove-package"); + return; + } + + const command = + PACKAGE_MANAGERS[packageManager as keyof PackageManager].remove; + + console.log(`Removing ${packageName}...`); + await execProcess({ + command: packageManager, + args: [...command.split(" "), packageName], + directory: process.cwd(), + }); +} diff --git a/src/providers/create/form.ts b/src/providers/create/form.ts index 0554f48..0494989 100644 --- a/src/providers/create/form.ts +++ b/src/providers/create/form.ts @@ -57,7 +57,7 @@ export const createExternalProvider = async ( } try { - const emitter = degit(`expressots/expressots-provider-template`); + const emitter = degit(`expressots/templates/provider`); await emitter.clone(providerInfo.providerName); changePackageName({ diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 0303157..0000000 --- a/src/types.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./@types"; diff --git a/src/utils/add-module-to-container.ts b/src/utils/add-module-to-container.ts index 0700757..34ef530 100644 --- a/src/utils/add-module-to-container.ts +++ b/src/utils/add-module-to-container.ts @@ -4,12 +4,12 @@ import fs from "node:fs"; import { printError } from "./cli-ui"; import Compiler from "./compiler"; -const APP_CONTAINER = "app.container.ts"; +const APP_CONTAINER = "app.ts"; type AppContainerType = { regex: RegExp; path: string; - content: RegExpMatchArray; + content: string; modules: string[]; imports: string[]; notImports: string[]; @@ -20,6 +20,7 @@ async function validateAppContainer(): Promise { const imports: string[] = []; const notImports: string[] = []; + // Locate the container file const path = globSync(`./${sourceRoot}/${APP_CONTAINER}`, { absolute: true, ignore: "**/node_modules/**", @@ -33,8 +34,10 @@ async function validateAppContainer(): Promise { process.exit(1); } + // Read the container file const fileContent = await fs.promises.readFile(path[0], "utf8"); + // Collect imports and other lines fileContent.split("\n").forEach((line: string) => { if (line.startsWith("import")) { imports.push(line); @@ -43,25 +46,30 @@ async function validateAppContainer(): Promise { } }); - // Validate the file content - const moduleDeclarationRegex = /.create\(\s*\[([\s\S]*?)]/; - const moduleDeclarationMatch = fileContent.match(moduleDeclarationRegex); + // Regex to detect and extract modules from configContainer + const moduleRegex = /this\.configContainer\(\s*\[\s*([\s\S]*?)\s*]\s*\)/; - if (!moduleDeclarationMatch) { - printError("Container format incorrect!", APP_CONTAINER); + const moduleMatch = fileContent.match(moduleRegex); + + if (!moduleMatch) { + printError( + "The App class does not contain a valid configContainer([]) declaration!", + APP_CONTAINER, + ); process.exit(1); } - const modules = moduleDeclarationMatch[1] + // Extract modules if present + const modules = moduleMatch[1] .trim() .split(",") .filter((m) => m.trim() !== "") .map((m) => m.trim()); return { - regex: moduleDeclarationRegex, + regex: moduleRegex, path: path[0], - content: moduleDeclarationMatch, + content: fileContent, modules, imports, notImports, @@ -73,6 +81,7 @@ async function addModuleToContainer( modulePath?: string, path?: string, ) { + console.log("To chamando esse cara"); const containerData: AppContainerType = await validateAppContainer(); const moduleName = (name[0].toUpperCase() + name.slice(1)).trimStart(); @@ -109,7 +118,7 @@ async function addModuleToContainer( containerData.modules.push(`${moduleName}Module`); const newModule = containerData.modules.join(", "); - const newModuleDeclaration = `.create([${newModule}]`; + const newModuleDeclaration = `this.configContainer([${newModule}])`; const newFileContent = [ ...containerData.imports, @@ -157,7 +166,7 @@ async function addModuleToContainerNestedPath(name: string, path?: string) { containerData.modules.push(`${moduleName}Module`); const newModule = containerData.modules.join(", "); - const newModuleDeclaration = `.create([${newModule}]`; + const newModuleDeclaration = `this.configContainer([${newModule}])`; const newFileContent = [ ...containerData.imports, diff --git a/src/utils/compiler.ts b/src/utils/compiler.ts index 37e2035..14d4c26 100644 --- a/src/utils/compiler.ts +++ b/src/utils/compiler.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import path from "path"; import { RegisterOptions, Service } from "ts-node"; -import { ExpressoConfig } from "../types"; +import { ExpressoConfig } from "@expressots/shared"; import { printError } from "./cli-ui"; /** diff --git a/test/test.spec.ts b/test/test.spec.ts index 5628bdb..a673e4c 100644 --- a/test/test.spec.ts +++ b/test/test.spec.ts @@ -1,3 +1,5 @@ -it("pass", () => { - expect(true).toBe(true); +describe("Test", () => { + it("pass", () => { + expect(true).toBe(true); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 91d0322..dcf9ee3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "types": ["node", "reflect-metadata", "vitest/globals"] + "types": ["node", "reflect-metadata", "jest"] }, "include": ["./src/**/*.ts"], "exclude": ["node_modules", "test/**/*.spec.ts", "./bin/**/*"] diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index aa2776a..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineConfig } from "vitest/config"; -import { codecovVitePlugin } from "@codecov/vite-plugin"; -import tsconfigPaths from "vite-tsconfig-paths"; - -/** - * @see {@link https://vitejs.dev/config/} - * @see {@link https://vitest.dev/config/} - */ -export default defineConfig({ - plugins: [ - tsconfigPaths(), - codecovVitePlugin({ - enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined, - bundleName: "expresso-ts-cli-coverage", - uploadToken: process.env.CODECOV_TOKEN, - }), - ], - test: { - globals: true, - environment: "node", - setupFiles: ["reflect-metadata"], - exclude: ["**/node_modules/**", "**/benchmark/**", "**/bin/**"], - coverage: { - all: true, - include: ["./src/**"], - exclude: ["**/node_modules/**", "**/bin/**", "**/index.ts/**"], - thresholds: { - global: { - statements: 85, - branches: 85, - functions: 85, - lines: 85, - }, - }, - reporter: ["text", "html", "json"], - provider: "v8", - }, - // ref: https://vitest.dev/config/#testtimeout - testTimeout: 10000, - }, -});