diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e38d54..5f416d2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,11 +47,11 @@ jobs: with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 + uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 + uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -78,6 +78,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 + uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 68e1c9e..9628c5a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,18 +13,18 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [18.x, 20.x] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: false steps: - name: Harden Runner uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: - egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + egress-policy: audit - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ba5298f..5488450 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: SARIF file path: results.sarif @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/upload-sarif@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: sarif_file: results.sarif diff --git a/README.md b/README.md index 1983e60..50327b3 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,18 @@ Available script arguments are: ### tree(options?: lstree.options): lstree Create a new lstree clojure function. Available options are: ```ts -interface options { +interface LStreeOptions { ignore?: string[]; description?: Map; depth?: number; showFilesDescriptor?: boolean; + showTitle?: boolean; + title?: string; + margin?: { + top?: number; + left?: number; + bottom?: number; + }; } ``` diff --git a/index.d.ts b/index.d.ts index dd632ae..c32c12a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,13 +1,15 @@ -declare namespace lstree { - interface options { - ignore?: string[]; - description?: Map; - depth?: number; - showFilesDescriptor?: boolean; - } - - export function tree(options?: options): (dir: string, pRootPath?: number) => void; +export interface LSTreeOptions { + ignore?: string[]; + description?: Map; + depth?: number; + showFilesDescriptor?: boolean; + showTitle?: boolean; + title?: string; + margin?: { + top?: number; + left?: number; + bottom?: number; + }; } - -export as namespace lstree; -export = lstree; + +export default function tree(options?: LSTreeOptions): (dir: string, pRootPath?: number) => Promise; diff --git a/index.js b/index.js index 4d318f8..40d38e2 100644 --- a/index.js +++ b/index.js @@ -1,25 +1,27 @@ // Import Node.js Dependencies import fs from "fs/promises"; import path from "path"; +import os from "node:os"; // Import Third-party Dependencies import kleur from "kleur"; import is from "@slimio/is"; -// CONSTANTS const { yellow, white, cyan, gray } = kleur; + /** - * @version 0.1.0 - * @function tree - * @description desc clojure - * @memberof lstree * @param {object} [options] object representing the options for customizing the tree view. * @param {string[]} [options.ignore] allows you to exclude files or folders from the tree. * @param {Map} [options.description] allows you to add a description for files to their right. + * The key is the name of file, value is the description of the file. * @param {number} [options.depth=1] Wanted depth * @param {boolean} [options.showFilesDescriptor=false] view file description - * The key is the name of file, value is the description of the file. - * @returns {Promise} + * @param {boolean} [options.showTitle=true] view lstree title + * @param {string} [options.title="project tree"] allow to add a custom title, default "project tree" + * @param {number} [options.margin.top] allow to add a margin top (one new line per number) + * @param {number} [options.margin.left] allow to add a margin left (two spaces per number) + * @param {number} [options.margin.bottom] allow to add a margin bottom (one new line per number) + * @returns {(path: string) => Promise} * * @example * const options = { @@ -34,17 +36,56 @@ const { yellow, white, cyan, gray } = kleur; export default function tree(options = Object.create(null)) { const IGNORE_FILE = new Set(["node_modules", "coverage", "docs", ".nyc_output", ".git"]); const DESC_FILE = new Map([ - [".eslintrc", "ESLint configuration"], - [".editorconfig", "Configuration for the code editor"], - [".gitignore", "Files to ignore on GIT"], - [".npmignore", "Files to ignore when publishing the NPM package"], - [".npmrc", "Local configuration of NPM"], - ["LICENCE", "Legal license of the project"], - ["CONTRIBUTING.md", "Code of Conduct & Contribution Rules for the SlimIO Project"], - ["commitlint.config.json", "Configuration of the convention to respect for GIT commits"], - ["jsdoc.json", "Configuration to generate the JSDoc with the command npm run doc"], + // Configuration files + [".babelrc", "Babel configuration for JavaScript transpilation"], + ["tsconfig.json", "TypeScript configuration for TypeScript projects"], + ["jest.config.js", "Jest configuration for testing"], + ["webpack.config.js", "Webpack configuration for bundling assets"], + [".all-contributorsrc", "All Contributors configuration for managing project contributors"], + [".editorconfig", "EditorConfig configuration for maintaining consistent coding styles between different editors and IDEs"], + [".gitignore", " Specifies files and directories to be ignored by Git"], + [".npmrc", "Configuration file for npm"], + + // Dependency management + ["package-lock.json", "npm package lock file for managing package dependencies with npm"], + ["yarn.lock", "Yarn lock file for managing package dependencies with Yarn"], + ["pnpm-lock.yaml", "pnpm lock file for managing package dependencies with pnpm"], + + // Scripts and automation + ["prettier.config.js", "Prettier configuration for code formatting"], + ["husky.config.js", "Husky configuration for Git hooks"], + ["lint-staged.config.js", "Lint-staged configuration for running linters on staged files"], + + // Documentation + ["CHANGELOG.md", "Changelog to document project changes and version history"], + ["CODE_OF_CONDUCT.md", "Code of Conduct for the project contributors"], + ["CONTRIBUTING.md", "Guidelines for contributing to the project"], + + // Testing + [".env.example", "Example environment variables file for local development"], + + // Continuous Integration + [".github", "GitHub Actions configuration for automating workflows"], + ["dependabot.yml", "Dependabot configuration for automating dependency updates"], + ["scorecard.yml", "OSSF Scorecard analysis configuration for evaluating open source projects"], + ["codeql.yml", "CodeQL configuration for analyzing code quality"], + ["node.js.yml", "Node.js pipeline configuration for running tests, linting..."], + + // Docker + ["Dockerfile", "Docker configuration for containerizing the application"], + ["docker-compose.yml", "Docker Compose configuration for managing containers"], + + // Environment Variables + [".env", "Environment variables file for local development (contains sensitive data, not committed to VCS)"], + + // Node.js runtime + ["index.js", "Main application file"], + + // Other project-related files + ["LICENSE", "Legal license of the project"], + ["jsdoc.json", "Configuration to generate JSDoc with the command npm run doc"], ["package.json", "Manifest of the project"], - ["README.md", "Documentation of the projet (start, use...)"] + ["README.md", "Documentation of the project (start, use...)"] ]); // Retrieve and apply arguments @@ -71,7 +112,7 @@ export default function tree(options = Object.create(null)) { * @memberof lstree * @param {!string} dir directory path. Path can handle "/" and "\" separator and not end with a separator * @param {number} [pRootPath] path of the root folder if there is recursivity - * @returns {void} + * @returns {Promise} * @example * tree("C:/path/to/your/directory/newProject"); * output expected : @@ -96,21 +137,19 @@ export default function tree(options = Object.create(null)) { if (is.nullOrUndefined(dir)) { throw new Error("Current working directory path is missing"); } - if (dir.match(/\//gi)) { - // eslint-disable-next-line - dir = dir.replace(/\//gi, "\\"); - } - if (dir.match(/\\$/gi)) { - // eslint-disable-next-line - dir = dir.replace(/\\$/gi, ""); + + let marginTop = options.margin?.top; + if (pRootPath === null && marginTop) { + while (marginTop-- > 0) { + console.log(""); + } } const rootPath = pRootPath === null ? dir : pRootPath; // Calculate Depth with root folder and number of separators "\" - const depth = is.nullOrUndefined(dir.replace(rootPath, "").match(/\\\w+/g)) ? - 0 : dir.replace(rootPath, "").match(/\\\w+/g).length; + const depth = dir.replace(rootPath, "").match(/[/\\]/g, "")?.length ?? 0; - let strAddDepth = ""; + let strAddDepth = "".padStart(2 * options.margin?.left ?? 0, " "); if (depth > 0) { for (let index = 0; index < depth; index++) { strAddDepth += yellow("│ "); @@ -126,8 +165,9 @@ export default function tree(options = Object.create(null)) { let nbFolder = 0; // Print only one time at the begginning - if (depth === 0) { - console.log(gray("\n > project tree\n")); + if (depth === 0 && (is.bool(options.showTitle) ? options.showTitle : true)) { + const title = options.title ?? "project tree"; + console.log(gray(`${os.EOL} > ${title}${os.EOL}`)); } // eslint-disable-next-line @@ -156,16 +196,35 @@ export default function tree(options = Object.create(null)) { } } - const last = files.length - 1; + function getPrefix(index) { + if (nbFolder === 0 && depth === 0 && files.length === 1) { + return "──"; + } + + if (index === files.length - 1) { + return "└─"; + } + + return nbFolder === 0 && depth === 0 && index === 0 ? "┌─" : "├─"; + } + // Print all files after folders - for (const [ind, val] of files.entries()) { - if (viewFileDescription && DESC_FILE.has(val)) { + for (const [index, fileName] of files.entries()) { + const prefix = getPrefix(index); + if (viewFileDescription && DESC_FILE.has(fileName)) { // ajouter la desc a la droite - const desc = DESC_FILE.get(val); - console.log(yellow(`${strAddDepth}${ind === last ? "└" : "├"} ${white(`${val}`)} ${cyan(`(${desc})`)}`)); + const desc = DESC_FILE.get(fileName); + console.log(yellow(`${strAddDepth}${prefix} ${white(`${fileName}`)} ${cyan(`(${desc})`)}`)); } else { - console.log(yellow(`${strAddDepth}${ind === last ? "└" : "├"} ${white(`${val}`)}`)); + console.log(yellow(`${strAddDepth}${prefix} ${white(`${fileName}`)}`)); + } + } + + let marginBottom = options.margin?.bottom; + if (depth === 0 && options.margin?.bottom) { + while (marginBottom-- > 0) { + console.log(""); } } }; diff --git a/package.json b/package.json index 5b3a3cf..fb26c80 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,11 @@ "sade": "^1.8.1" }, "devDependencies": { - "@nodesecure/eslint-config": "^1.7.0", - "c8": "^8.0.0", - "eslint": "^8.39.0", - "pkg-ok": "^3.0.0" + "@nodesecure/eslint-config": "^1.8.0", + "c8": "^8.0.1", + "eslint": "^8.50.0", + "pkg-ok": "^3.0.0", + "strip-ansi": "^7.1.0" }, "engines": { "node": ">=14" diff --git a/test/ut/test.js b/test/ut/test.js index 8c17e7d..700f8af 100644 --- a/test/ut/test.js +++ b/test/ut/test.js @@ -1,9 +1,11 @@ // Import Node.js Dependencies import assert from "node:assert"; import { test } from "node:test"; -import { EOL } from "node:os"; import { join } from "node:path"; +// Import Third-party Dependencies +import stripAnsi from "strip-ansi"; + // Import Internal Dependencies import tree from "../../index.js"; @@ -16,15 +18,52 @@ test("it should throw an error when directory path is missing", async() => { } }); -// TODO: package is completely broken -// test("it should print the list of the entire folder and file in the directory", async() => { -// const logs = []; -// console.log = (data) => logs.push(data); - -// await tree({ depth: 3 })(join(process.cwd(), "/test/fixtures")); -// assert.equal(logs.shift(), "┌─📁 folderA"); -// assert.equal(logs.shift(), "│ ├─📁 folderB"); -// assert.equal(logs.shift(), "│ │ └─📄 fileB.js"); -// assert.equal(logs.shift(), "│ └─📄 fileA.txt"); -// assert.equal(logs.shift(), "└─📄 index.js"); -// }); +test("it should print the list of the entire folder and file in the directory", async() => { + const logs = []; + console.log = (data) => logs.push(stripAnsi(data).trim()); + + await tree({ depth: 3 })(join(process.cwd(), "/test/fixtures")); + assert.equal(logs.shift(), "> project tree"); + assert.equal(logs.shift(), "┌─📁 folderA"); + assert.equal(logs.shift(), "│ ├─📁 folderB"); + assert.equal(logs.shift(), "│ │ └─ b.js"); + assert.equal(logs.shift(), "│ └─ fileA.txt"); + assert.equal(logs.shift(), "└─ index.js"); +}); + +test("it should add and show custom description", async() => { + const logs = []; + console.log = (data) => logs.push(stripAnsi(data).trim()); + + await tree({ + depth: 3, + description: new Map([["fileA.txt", "foo"]]), + showFilesDescriptor: true + })(join(process.cwd(), "/test/fixtures")); + + assert.equal(logs.shift(), "> project tree"); + assert.equal(logs.shift(), "┌─📁 folderA"); + assert.equal(logs.shift(), "│ ├─📁 folderB"); + assert.equal(logs.shift(), "│ │ └─ b.js"); + assert.equal(logs.shift(), "│ └─ fileA.txt (foo)"); + assert.equal(logs.shift(), "└─ index.js (Main application file)"); +}); + +test("it should ignore folder", async() => { + const logs = []; + console.log = (data) => logs.push(stripAnsi(data).trim()); + + await tree({ depth: 3, ignore: ["folderA"] })(join(process.cwd(), "/test/fixtures")); + assert.equal(logs.shift(), "> project tree"); + assert.equal(logs.shift(), "── index.js"); +}); + +test("it show custom title", async() => { + const logs = []; + console.log = (data) => logs.push(stripAnsi(data).trim()); + + await tree({ depth: 3, ignore: ["folderA"], title: "foo" })(join(process.cwd(), "/test/fixtures")); + assert.equal(logs.shift(), "> foo"); + assert.equal(logs.shift(), "── index.js"); +}); +