diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..b18bd11 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,80 @@ +name: "CI/CD Pipeline" + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node v22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Installation pacakges + run: npm i + + - name: Check syntax typescript + run: npm run typecheck + + - name: Check syntax and rules of Eslint + run: npm run eslint + + - name: Start test + run: npm run test + + check-version: + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' && startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check version + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" + exit 1 + fi + build: + runs-on: ubuntu-latest + needs: check-version + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node v22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Start build + run: npm run build && mv package.json ./dist/ + + publish: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node v22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Publish Package + run: cd ./dist && npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index c6bba59..80f3484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### # Logs logs *.log @@ -102,7 +106,6 @@ dist # vuepress v2.x temp and cache directory .temp -.cache # Docusaurus cache and generated files .docusaurus @@ -128,3 +131,16 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +package-lock.json + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/changelog b/changelog new file mode 100644 index 0000000..3ca98a1 --- /dev/null +++ b/changelog @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 29-09-2024 \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..f49610d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,50 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import stylisticJs from "@stylistic/eslint-plugin-js"; +import parserTs from "@typescript-eslint/parser"; + +export default [ + { + files: ["**/*.{js,mjs,cjs,ts}"] + }, + { + languageOptions: { + globals: globals.node, + parser: parserTs + } + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { + "@stylistic": stylisticJs + }, + rules: { + //syntax code + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + + //syntax code + plugin + "@stylistic/quotes": ["error", "double"], + "@stylistic/array-bracket-newline": ["error", { "multiline": true }], + "@stylistic/array-bracket-spacing": ["error", "never"], + "@stylistic/array-element-newline": ["error", "consistent"], + "@stylistic/arrow-parens": ["error", "always"], + "@stylistic/arrow-spacing": "error", + "@stylistic/block-spacing": "error", + "@stylistic/brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "@stylistic/comma-dangle": ["error", "never"], + "@stylistic/comma-spacing": ["error", { "before": false, "after": true }], + "@stylistic/dot-location": ["error", "object"], + "@stylistic/keyword-spacing": ["error", { "before": true }], + "@stylistic/no-multi-spaces": "error", + "@stylistic/no-mixed-operators": "error", + "@stylistic/no-floating-decimal": "error", + "@stylistic/semi": "error", + "@stylistic/wrap-regex": "error" + } + } +]; \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..1011418 --- /dev/null +++ b/index.ts @@ -0,0 +1,105 @@ +import chalk from "chalk"; +import {DateTime} from "luxon"; +import _ from "lodash"; +import {config} from "dotenv"; +config(); + +type ILevel = "debug" | "info" | "warn" | "error" | "fatal"; + +class Logger { + nameService: string | null = null; + instanceLogger: Function = console.log; + level: ILevel = "info"; + + constructor(nameService: string, instanceLogger?: Function){ + this.nameService = nameService.toUpperCase(); + + if (!_.isNil(instanceLogger)){ + this.instanceLogger = instanceLogger; + } + + const level = (process.env.LOG_LEVEL).toLowerCase() as ILevel; + switch (level){ + case "debug": + case "error": + case "info": + case "warn": + case "fatal": + this.level = level; + break; + default: + if (!_.isNil(level) && level !== "null" && level !== "undefined" && level !== ""){ + this.#baseLog("fatal", "Not exist current level:", `"${level}"`, "select one of this: [debug|error|info|warn|fatal]"); + process.exit(1); + } + } + } + + debug(...args: Array){ + if (this.level === "debug"){ + this.#baseLog("debug", ...args); + } + } + + info(...args: Array){ + if ((["info", "debug"] as Array).includes(this.level)){ + this.#baseLog("info", ...args); + } + } + + warn(...args: Array){ + if ((["info", "debug", "warn"] as Array).includes(this.level)){ + this.#baseLog("warn", ...args); + } + } + + error(...args: Array){ + if ((["info", "debug", "warn", "error"] as Array).includes(this.level)){ + this.#baseLog("error", ...args); + } + } + + fatal(...args: Array){ + if ((["info", "debug", "warn", "error", "fatal"] as Array).includes(this.level)){ + this.#baseLog("fatal", ...args); + } + } + + #baseLog(type: ILevel, ...args: Array){ + let message = `[${DateTime.now().toFormat("dd-MM-yyyy hh:mm:ssZZ")}] [${type.toUpperCase()}] [${this.nameService}]`; + + args = args.map((message) => { + if (message instanceof Error){ + return message.stack; + } else { + return message; + } + }); + + switch (type){ + case "debug": + message = chalk.gray(message, ...args); + break; + case "info": + message = chalk.blue(message, ...args); + break; + case "warn": + message = chalk.yellow(message, ...args); + break; + case "error": + message = chalk.red(message, ...args); + break; + case "fatal": + message = chalk.redBright(message, ...args); + break; + } + + this.instanceLogger(message); + } +} + +export default Logger; + +export { + ILevel +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e0642b6 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "logger", + "version": "1.0.0", + "description": "Simplfy logger and user friendly", + "main": "index.ts", + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "eslint": "eslint", + "eslint-fix": "eslint --fix", + "test-dev": "cd ./test && vitest", + "test": "cd ./test && FORCE_COLOR=1 vitest run", + "build": "tsc" + }, + "keywords": [], + "author": "cesxhin", + "license": "MIT", + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.8.0", + "@types/capture-console": "^1.0.5", + "@types/lodash": "^4.17.9", + "@types/luxon": "^3.4.2", + "@types/node": "^22.7.4", + "chalk": "^5.3.0", + "dotenv": "^16.4.5", + "eslint": "^9.11.1", + "lodash": "^4.17.21", + "luxon": "^3.5.0", + "stream": "^0.0.3", + "typescript": "5.5.4", + "typescript-eslint": "^8.7.0", + "vitest": "^2.1.1" + } +} diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..d9333db --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect, assert, vi } from "vitest"; + +import Logger, { ILevel } from "../index"; +import {alternateCase, listLevel} from "./utils/utils"; + +let output: any = null; + +function captureLog(message: string){ + //@ts-ignore + output = JSON.stringify(message).replaceAll("\"", ""); +} + +process.env.LOG_LEVEL = "debug"; + +describe("Test format", () => { + for (const objectLevel of listLevel) { + it(`check format ${objectLevel.level}`, () => { + output = null; + const logRegex = `\\\\u001b\\[\\d{1,3}m\\[\\d{2}-\\d{2}-\\d{4} \\d{2}:\\d{2}:\\d{2}\\+\\d{2}:\\d{2}\\] \\[${objectLevel.level.toUpperCase()}\\] \\[TEST] .+?\\\\u001b\\[\\d{1,3}m`; + + const logger = new Logger("test", captureLog); + + logger[objectLevel.level]("Hi!"); + + assert.match(output, new RegExp(logRegex), `Format isn't correct for level --> ${objectLevel.level}`); + }); + } +}); + +describe("Test colors", () => { + for (const objectLevel of listLevel) { + it(`check color ${objectLevel.level}`, () => { + output = null; + + const logger = new Logger("test", captureLog); + logger[objectLevel.level]("Hi!"); + expect(output.startsWith(objectLevel.color) && output.endsWith("\\u001b[39m")).eq(true); + }); + } +}); + +describe("Test log level from ENV", () => { + it("Log level: (empty)", () => { + output = null; + process.env.LOG_LEVEL = ""; + + const logger = new Logger("test", captureLog); + output = ""; + logger.debug("Hi! custom"); + + return expect(output).empty; + }); + + it("Log level: null", () => { + output = null; + //@ts-ignore + process.env.LOG_LEVEL = null; + + const logger = new Logger("test", captureLog); + output = ""; + logger.debug("Hi! custom"); + + return expect(output).empty; + }); + + it("Log level: undefined", () => { + output = null; + process.env.LOG_LEVEL = undefined; + + const logger = new Logger("test", captureLog); + output = ""; + logger.debug("Hi! custom"); + + return expect(output).empty; + }); + + for (const objectLevel of listLevel) { + it(`Log level: ${objectLevel.level}`, () => { + output = null; + process.env.LOG_LEVEL = objectLevel.level; + + const logger = new Logger("test", captureLog); + logger[objectLevel.level]("Hi!"); + return expect(output).not.null; + }); + } + + for (const objectLevel of listLevel) { + it(`Log level: ${objectLevel.level.toUpperCase()}`, () => { + output = null; + process.env.LOG_LEVEL = objectLevel.level.toUpperCase(); + + const logger = new Logger("test", captureLog); + logger[objectLevel.level]("Hi!"); + return expect(output).not.null; + }); + } + + for (const objectLevel of listLevel) { + it(`Log level: ${alternateCase(objectLevel.level)}`, () => { + output = null; + process.env.LOG_LEVEL = alternateCase(objectLevel.level); + + const logger = new Logger("test", captureLog); + logger[objectLevel.level]("Hi!"); + return expect(output).not.null; + }); + } + + it("Log level: debrn (not exist)", () => { + process.env.LOG_LEVEL = "debrn"; + const exitCode = vi.spyOn(process, "exit"); + + try { + new Logger("test", captureLog); + } catch { + //ignore + } + + return expect(exitCode).toHaveBeenCalledWith(1); + }); + + it("Log level: 1234 (not exist)", () => { + //@ts-ignore + process.env.LOG_LEVEL = 1234; + const exitCode = vi.spyOn(process, "exit"); + + try { + new Logger("test", captureLog); + } catch { + //ignore + } + + return expect(exitCode).toHaveBeenCalledWith(1); + }); +}); + + +describe("Test print log level", () => { + for (const objectLevel of listLevel) { + it(`Log level: ${objectLevel.level}`, () => { + for (const singleLevel of listLevel.map((singleObjectlevel) => singleObjectlevel.level)) { + output = null; + process.env.LOG_LEVEL = singleLevel; + + const logger = new Logger("test", captureLog); + logger[singleLevel]("Hi!"); + + if (objectLevel.level === "debug"){ + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).not.null; + continue; + } + + if ((["debug", "info"] as Array)){ + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).not.null; + continue; + } + + if ((["debug", "info", "warn"] as Array)){ + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).not.null; + continue; + } + + if ((["debug", "info", "error"] as Array)){ + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).not.null; + continue; + } + + if ((["debug", "info", "error", "fatal"] as Array)){ + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).not.null; + continue; + } + + //eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(output).null; + } + }); + } +}); \ No newline at end of file diff --git a/test/utils/utils.ts b/test/utils/utils.ts new file mode 100644 index 0000000..c795cbc --- /dev/null +++ b/test/utils/utils.ts @@ -0,0 +1,34 @@ +import { ILevel } from "../.."; + +const listLevel: Array<{level: ILevel, color: string}> = [ +{ + level: "debug", + color: "\\u001b[90m" +}, { + level: "error", + color: "\\u001b[31m" +}, { + level: "fatal", + color: "\\u001b[91m" +}, { + level: "info", + color: "\\u001b[34m" +}, { + level: "warn", + color: "\\u001b[33m" +} +]; + +function alternateCase(text: string){ + return text. + split(""). + map((char, index) => + index % 2 === 0 ? char.toLowerCase() : char.toUpperCase() + ). + join(""); +} + +export { + alternateCase, + listLevel +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8c79b1a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "declaration": true, + "outDir": "./dist", + "skipLibCheck": true + }, + "exclude": ["test"] +} \ No newline at end of file