diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b56031..5a277e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [23.x, 22.x, 20.x, 18.x, 16.x, 14.x, 12.x] + node-version: [23.x, 22.x, 21.x, 20.x] steps: - uses: actions/checkout@v2 diff --git a/bin/index.js b/bin/index.js index e8ee9bf..7c3d154 100644 --- a/bin/index.js +++ b/bin/index.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../dist/cli.js'); +import '../dist/cli.js'; diff --git a/package-lock.json b/package-lock.json index a4a2d46..6e78c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,19 @@ { "name": "linguist-js", - "version": "2.9.0", - "lockfileVersion": 2, + "version": "3.0.0-dev", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linguist-js", - "version": "2.9.0", + "version": "3.0.0-dev", "license": "ISC", "dependencies": { - "binary-extensions": "^2.3.0 <3", - "commander": "^9.5.0 <10", + "binary-extensions": "^3.0.0", + "commander": "^13.1.0", "common-path-prefix": "^3.0.0", - "cross-fetch": "^3.2.0 <4", "ignore": "^7.0.3", - "isbinaryfile": "^4.0.10 <5", + "isbinaryfile": "^5.0.4", "js-yaml": "^4.1.0", "node-cache": "^5.1.2" }, @@ -24,13 +23,13 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", - "@types/node": "ts5.0", + "@types/node": "ts5.7", "deep-object-diff": "^1.1.9", - "typescript": "~5.0.4 <5.1" + "typescript": "~5.7.3" }, "engines": { - "node": ">=12", - "npm": "<9" + "node": ">=20", + "npm": ">=10" } }, "node_modules/@types/js-yaml": { @@ -54,11 +53,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-3.0.0.tgz", + "integrity": "sha512-X0RfwMgXPEesg6PCXzytQZt9Unh9gtc4SfeTNJvKifUL//Oegcc/Yf31z6hThNZ8dnD3Ir3wkHVN0eWrTvP5ww==", "engines": { - "node": ">=8" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -73,11 +72,11 @@ } }, "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=18" } }, "node_modules/common-path-prefix": { @@ -85,14 +84,6 @@ "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/deep-object-diff": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", @@ -108,11 +99,11 @@ } }, "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz", + "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==", "engines": { - "node": ">= 8.0.0" + "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" @@ -140,41 +131,17 @@ "node": ">= 8.0.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/undici-types": { @@ -182,141 +149,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - }, - "dependencies": { - "@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true - }, - "@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", - "dev": true, - "requires": { - "undici-types": "~6.20.0" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" - }, - "commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" - }, - "common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" - }, - "cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "requires": { - "node-fetch": "^2.7.0" - } - }, - "deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", - "dev": true - }, - "ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==" - }, - "isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "requires": { - "clone": "2.x" - } - }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true - }, - "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } } } } diff --git a/package.json b/package.json index 732f954..14c75d4 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { "name": "linguist-js", - "version": "2.9.0", + "version": "3.0.0-dev", "description": "Analyse languages used in a folder. Powered by GitHub Linguist, although it doesn't need to be installed.", "main": "dist/index.js", + "type": "module", "bin": { "linguist-js": "bin/index.js", "linguist": "bin/index.js" }, "engines": { - "node": ">=12", - "npm": "<9" + "node": ">=20", + "npm": ">=10" }, "scripts": { "download-files": "npx tsx@3 build/download-files", @@ -39,19 +40,18 @@ }, "homepage": "https://github.com/Nixinova/Linguist#readme", "dependencies": { - "binary-extensions": "^2.3.0 <3", - "commander": "^9.5.0 <10", + "binary-extensions": "^3.0.0", + "commander": "^13.1.0", "common-path-prefix": "^3.0.0", - "cross-fetch": "^3.2.0 <4", "ignore": "^7.0.3", - "isbinaryfile": "^4.0.10 <5", + "isbinaryfile": "^5.0.4", "js-yaml": "^4.1.0", "node-cache": "^5.1.2" }, "devDependencies": { "@types/js-yaml": "^4.0.9", - "@types/node": "ts5.0", + "@types/node": "ts5.7", "deep-object-diff": "^1.1.9", - "typescript": "~5.0.4 <5.1" + "typescript": "~5.7.3" } } diff --git a/readme.md b/readme.md index 5ab96ba..65dd03d 100644 --- a/readme.md +++ b/readme.md @@ -51,9 +51,8 @@ Running LinguistJS on this folder will return the following JSON: "count": 5, "bytes": 6020, "lines": { - "total": 100, - "content": 90, - "code": 80, + "total": 100, + "content": 90, }, "results": { "/src/index.ts": "TypeScript", @@ -63,57 +62,41 @@ Running LinguistJS on this folder will return the following JSON: "/x.pluginspec": "Ruby", }, "alternatives": { - "/x.pluginspec": ["XML"], + "/x.pluginspec": ["XML"], }, }, "languages": { "count": 3, "bytes": 6010, "lines": { - "total": 90, - "content": 80, - "code": 70, + "total": 90, + "content": 80, }, "results": { - "JavaScript": { - "type": "programming", - "bytes": 1000, - "lines": { "total": 49, "content": 49, "code": 44 }, - "color": "#f1e05a" - }, - "Markdown": { - "type": "prose", - "bytes": 3000, - "lines": { "total": 10, "content": 5, "code": 5 }, - "color": "#083fa1" - }, - "Ruby": { - "type": "programming", - "bytes": 10, - "lines": { "total": 1, "content": 1, "code": 1 }, - "color": "#701516" - }, - "TypeScript": { - "type": "programming", - "bytes": 2000, - "lines": { "total": 30, "content": 25, "code": 20 }, - "color": "#2b7489" - }, + "JavaScript": { "bytes": 1000, "lines": { "total": 49, "content": 49 }, }, + "Markdown": { "bytes": 3000, "lines": { "total": 10, "content": 5 }, }, + "Ruby": { "bytes": 10, "lines": { "total": 1, "content": 1 }, }, + "TypeScript": { "bytes": 2000, "lines": { "total": 30, "content": 25 }, }, }, }, "unknown": { "count": 1, "bytes": 10, "lines": { - "total": 10, - "content": 10, - "code": 10, + "total": 10, + "content": 10, }, "filenames": { "no-lang": 10, }, "extensions": {}, }, + "repository": { + "JavaScript": { "type": "programming", "color": "#f1e05a" }, + "Markdown": { "type": "prose", "color": "#083fa1" }, + "Ruby": { "type": "programming", "color": "#701516" }, + "TypeScript": { "type": "programming", "color": "#2b7489" }, + } } ``` @@ -134,13 +117,13 @@ const linguist = require('linguist-js'); // Analyse folder on disc const folder = './src'; const options = { keepVendored: false, quick: false }; -const { files, languages, unknown } = await linguist(folder, options); +const { files, languages, unknown, repository } = await linguist(folder, options); // Analyse file content from raw input const fileNames = ['file1.ts', 'file2.ts', 'ignoreme.js']; const fileContent = ['#!/usr/bin/env node', 'console.log("Example");', '"ignored"']; const options = { ignoredFiles: ['ignore*'] }; -const { files, languages, unknown } = await linguist(fileNames, { fileContent, ...options }); +const { files, languages, unknown, repository } = await linguist(fileNames, { fileContent, ...options }); ``` - `linguist(entry?, opts?)` (default export): diff --git a/src/cli.ts b/src/cli.ts index e479e13..bd87f71 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,11 +1,11 @@ -const VERSION = require('../package.json').version; - -import FS from 'fs'; -import Path from 'path'; +import FS from 'node:fs'; +import Path from 'node:path'; import { program } from 'commander'; +import linguist from './index.js'; +import { normPath } from './helpers/norm-path.js'; -import linguist from './index'; -import { normPath } from './helpers/norm-path'; +const packageJson = JSON.parse(FS.readFileSync(new URL('../package.json', import.meta.url), "utf-8")); +const VERSION = packageJson.version; const colouredMsg = ([r, g, b]: number[], msg: string): string => `\u001B[${38};2;${r};${g};${b}m${msg}${'\u001b[0m'}`; const hexToRgb = (hex: string): number[] => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)]; @@ -14,7 +14,7 @@ program .name('linguist') .usage('--analyze [] []') - .option('-a|--analyze|--analyse [folders...]', 'Analyse the languages of all files in a folder') + .option('-a|--analyze [folders...]', 'Analyse the languages of all files in a folder') .option('-i|--ignoredFiles ', `A list of file path globs to ignore`) .option('-l|--ignoredLanguages ', `A list of languages to ignore`) .option('-c|--categories ', 'Language categories to include in output') @@ -66,7 +66,7 @@ if (args.analyze) (async () => { // Fetch language data const root = args.analyze === true ? '.' : args.analyze; const data = await linguist(root, args); - const { files, languages, unknown } = data; + const { files, languages, unknown, repository } = data; // Print output if (!args.json) { // Ignore languages with a bytes/% size less than the declared min size @@ -83,22 +83,22 @@ if (args.analyze) (async () => { 'loc': n => n, }; const minBytesSize = conversionFactors[minSizeUnit](+minSizeAmt); - const other = { bytes: 0, lines: { total: 0, content: 0, code: 0 } }; + const other = { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 } }; // Apply specified minimums: delete language results that do not reach the threshold for (const [lang, data] of Object.entries(languages.results)) { - const checkUnit = checkBytes ? data.bytes : data.lines.code; + const checkUnit = checkBytes ? data.bytes : data.lines.content; if (checkUnit < minBytesSize) { // Add to 'other' count + other.count++; other.bytes += data.bytes; other.lines.total += data.lines.total; other.lines.content += data.lines.content; - other.lines.code += data.lines.code; // Remove language result delete languages.results[lang]; } } if (other.bytes) { - languages.results["Other"] = { ...other, type: null! }; + languages.results["Other"] = other; } } @@ -121,15 +121,16 @@ if (args.analyze) (async () => { } } // List parsed results - for (const [lang, { bytes, lines, color }] of sortedEntries) { + for (const [lang, { bytes, lines }] of sortedEntries) { + const colour = hexToRgb(repository[lang].color ?? '#ededed'); const percent = (bytes: number) => bytes / (totalBytes || 1) * 100; const fmtd = { index: (++count).toString().padStart(2, ' '), lang: lang.padEnd(24, ' '), percent: percent(bytes).toFixed(2).padStart(5, ' '), bytes: bytes.toLocaleString().padStart(10, ' '), - loc: lines.code.toLocaleString().padStart(10, ' '), - icon: colouredMsg(hexToRgb(color ?? '#ededed'), '\u2588'), + loc: lines.content.toLocaleString().padStart(10, ' '), + icon: colouredMsg(colour, '\u2588'), }; console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B ${fmtd.loc} LOC`); diff --git a/src/helpers/load-data.ts b/src/helpers/load-data.ts index 8b27a1a..a8e4e89 100644 --- a/src/helpers/load-data.ts +++ b/src/helpers/load-data.ts @@ -1,6 +1,5 @@ -import FS from 'fs'; -import Path from 'path'; -import fetch from 'cross-fetch'; +import FS from 'node:fs'; +import Path from 'node:path'; import Cache from 'node-cache'; const cache = new Cache({}); diff --git a/src/helpers/norm-path.ts b/src/helpers/norm-path.ts index 1fa9efb..a2eb30a 100644 --- a/src/helpers/norm-path.ts +++ b/src/helpers/norm-path.ts @@ -1,4 +1,4 @@ -import Path from 'path'; +import Path from 'node:path'; export const normPath = function normalisedPath(...inputPaths: string[]) { return Path.join(...inputPaths).replace(/\\/g, '/'); diff --git a/src/helpers/parse-gitattributes.ts b/src/helpers/parse-gitattributes.ts index d243daf..06655b0 100644 --- a/src/helpers/parse-gitattributes.ts +++ b/src/helpers/parse-gitattributes.ts @@ -1,5 +1,5 @@ -import * as T from '../types'; -import { normPath } from './norm-path'; +import * as T from '../types.js'; +import { normPath } from './norm-path.js'; export type FlagAttributes = { 'vendored': boolean | null, diff --git a/src/helpers/read-file.ts b/src/helpers/read-file.ts index fbd4246..1c3b55b 100644 --- a/src/helpers/read-file.ts +++ b/src/helpers/read-file.ts @@ -1,4 +1,4 @@ -import FS from 'fs'; +import FS from 'node:fs'; /** * Read part of a file on disc. diff --git a/src/helpers/walk-tree.ts b/src/helpers/walk-tree.ts index 2f2d305..e9e14ab 100644 --- a/src/helpers/walk-tree.ts +++ b/src/helpers/walk-tree.ts @@ -1,8 +1,8 @@ -import FS from 'fs'; -import Path from 'path'; +import FS from 'node:fs'; +import Path from 'node:path'; import { Ignore } from 'ignore'; -import parseGitignore from './parse-gitignore'; -import { normPath, normAbsPath } from './norm-path'; +import parseGitignore from './parse-gitignore.js'; +import { normPath, normAbsPath } from './norm-path.js'; let allFiles: Set; let allFolders: Set; diff --git a/src/index.ts b/src/index.ts index 2513141..e43ff12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,22 @@ -import FS from 'fs'; -import Path from 'path'; +import FS from 'node:fs'; +import Path from 'node:path'; import YAML from 'js-yaml'; import ignore, { Ignore } from 'ignore'; import commonPrefix from 'common-path-prefix'; -import binaryData from 'binary-extensions'; import { isBinaryFile } from 'isbinaryfile'; -import walk from './helpers/walk-tree'; -import loadFile, { parseGeneratedDataFile } from './helpers/load-data'; -import readFileChunk from './helpers/read-file'; -import parseAttributes, { FlagAttributes } from './helpers/parse-gitattributes'; -import pcre from './helpers/convert-pcre'; -import { normPath } from './helpers/norm-path'; -import * as T from './types'; -import * as S from './schema'; +import walk from './helpers/walk-tree.js'; +import loadFile, { parseGeneratedDataFile } from './helpers/load-data.js'; +import readFileChunk from './helpers/read-file.js'; +import parseAttributes, { FlagAttributes } from './helpers/parse-gitattributes.js'; +import pcre from './helpers/convert-pcre.js'; +import { normPath } from './helpers/norm-path.js'; +import * as T from './types.js'; +import * as S from './schema.js'; + +const binaryData = JSON.parse( + FS.readFileSync(new URL('../node_modules/binary-extensions/binary-extensions.json', import.meta.url), "utf-8") +) as string[]; async function analyse(path?: string, opts?: T.Options): Promise async function analyse(paths?: string[], opts?: T.Options): Promise @@ -47,9 +50,10 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom const extensions: Record = {}; const globOverrides: Record = {}; const results: T.Results = { - files: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {}, alternatives: {} }, - languages: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {} }, - unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, extensions: {}, filenames: {} }, + files: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {}, alternatives: {} }, + languages: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {} }, + unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, extensions: {}, filenames: {} }, + repository: {}, }; // Set a common root path so that vendor paths do not incorrectly match parent folders @@ -240,21 +244,22 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom if (firstLine === null) continue; // Check first line for explicit classification + const modelineRegex = /-\*-|(?:syntax|filetype|ft)\s*=/; const hasShebang = opts.checkShebang && /^#!/.test(firstLine); - const hasModeline = opts.checkModeline && /-\*-|(syntax|filetype|ft)\s*=/.test(firstLine); + const hasModeline = opts.checkModeline && modelineRegex.test(firstLine); if (!opts.quick && (hasShebang || hasModeline)) { const matches = []; for (const [lang, data] of Object.entries(langData)) { const langMatcher = (lang: string) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`; // Check for interpreter match if (opts.checkShebang && hasShebang) { - const matchesInterpretor = data.interpreters?.some(interpreter => firstLine!.match(`\\b${interpreter}\\b`)); + const matchesInterpretor = data.interpreters?.some(interpreter => firstLine.match(`\\b${interpreter}\\b`)); if (matchesInterpretor) matches.push(lang); } // Check modeline declaration if (opts.checkModeline && hasModeline) { - const modelineText = firstLine!.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1'); + const modelineText = firstLine.toLowerCase().split(modelineRegex)[1]; const matchesLang = modelineText.match(langMatcher(lang)); const matchesAlias = data.aliases?.some(lang => modelineText.match(langMatcher(lang))); if (matchesLang || matchesAlias) @@ -393,7 +398,7 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom delete results.languages.results[lang]; } for (const category of hiddenCategories) { - for (const [lang, { type }] of Object.entries(results.languages.results)) { + for (const [lang, { type }] of Object.entries(results.repository)) { if (type === category) { delete results.languages.results[lang]; } @@ -420,39 +425,37 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom // Calculate file size const fileSize = manualFileContent[files.indexOf(file)]?.length ?? FS.statSync(file).size; // Calculate lines of code - const loc = { total: 0, content: 0, code: 0 }; + const loc = { total: 0, content: 0 }; if (opts.calculateLines) { const fileContent = (manualFileContent[files.indexOf(file)] ?? FS.readFileSync(file).toString()) ?? ''; const allLines = fileContent.split(/\r?\n/gm); loc.total = allLines.length; loc.content = allLines.filter(line => line.trim().length > 0).length; - const codeLines = fileContent - .replace(/^\s*(\/\/|# |;|--).+/gm, '') - .replace(/\/\*.+\*\/|/sg, '') - loc.code = codeLines.split(/\r?\n/gm).filter(line => line.trim().length > 0).length; } // Apply to files totals results.files.bytes += fileSize; results.files.lines.total += loc.total; results.files.lines.content += loc.content; - results.files.lines.code += loc.code; // Add results to 'languages' section if language match found, or 'unknown' section otherwise if (lang) { - const { type } = langData[lang]; + // update language in repository if not yet present + if (!results.repository[lang]) { + const { type, color } = langData[lang]; + results.repository[lang] = { type, color }; + if (opts.childLanguages) { + results.repository[lang].parent = langData[lang].group; + } + } // set default if unset - results.languages.results[lang] ??= { type, bytes: 0, lines: { total: 0, content: 0, code: 0 }, color: langData[lang].color }; + results.languages.results[lang] ??= { count: 0, bytes: 0, lines: { total: 0, content: 0 } }; // apply results to 'languages' section - if (opts.childLanguages) { - results.languages.results[lang].parent = langData[lang].group; - } + results.languages.results[lang].count++; results.languages.results[lang].bytes += fileSize; results.languages.bytes += fileSize; results.languages.results[lang].lines.total += loc.total; results.languages.results[lang].lines.content += loc.content; - results.languages.results[lang].lines.code += loc.code; results.languages.lines.total += loc.total; results.languages.lines.content += loc.content; - results.languages.lines.code += loc.code; } else { const ext = Path.extname(file); @@ -464,13 +467,12 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom results.unknown.bytes += fileSize; results.unknown.lines.total += loc.total; results.unknown.lines.content += loc.content; - results.unknown.lines.code += loc.code; } } // Set lines output to NaN when line calculation is disabled if (opts.calculateLines === false) { - results.files.lines = { total: NaN, content: NaN, code: NaN } + results.files.lines = { total: NaN, content: NaN } } // Set counts @@ -481,4 +483,4 @@ async function analyse(rawPaths?: string | string[], opts: T.Options = {}): Prom // Return return results; } -export = analyse; +export default analyse; diff --git a/src/schema.ts b/src/schema.ts index 8617ebb..76781ea 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import { Category, Language } from './types' +import { Category, Language } from './types.js' export interface LanguagesScema { [name: string]: { diff --git a/src/types.ts b/src/types.ts index 077e9d2..e9938d6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,15 +30,16 @@ export interface Options { checkModeline?: boolean } +type LinesOfCode = { + total: Integer + content: Integer +} + export interface Results { files: { count: Integer bytes: Bytes - lines: { - total: Integer - content: Integer - code: Integer - } + lines: LinesOfCode /** Note: Results use slashes as delimiters even on Windows. */ results: Record alternatives: Record @@ -46,32 +47,23 @@ export interface Results { languages: { count: Integer bytes: Bytes - lines: { - total: Integer - content: Integer - code: Integer - } + lines: LinesOfCode results: Record } unknown: { count: Integer bytes: Bytes - lines: { - total: Integer - content: Integer - code: Integer - } + lines: LinesOfCode extensions: Record filenames: Record } + repository: Record } diff --git a/test/expected.json b/test/expected.json index 90c7971..27c7604 100644 --- a/test/expected.json +++ b/test/expected.json @@ -2,7 +2,7 @@ "files": { "count": 12, "bytes": 199, - "lines": { "total": 27, "content": 16, "code": 11 }, + "lines": { "total": 27, "content": 16 }, "results": { "~/al.al": "Perl", "~/alternatives.asc": "AGS Script", @@ -18,27 +18,74 @@ "~/unknown": null }, "alternatives": { - "~/alternatives.asc": [ "AsciiDoc", "Public Key" ] + "~/alternatives.asc": ["AsciiDoc", "Public Key"] } }, "languages": { "count": 8, "bytes": 190, "results": { - "Perl": { "type": "programming", "bytes": 0, "lines": { "total": 1, "content": 0, "code": 0 },"color": "#0298c3" }, - "AGS Script": { "type": "programming", "bytes": 14, "lines": { "total": 2, "content": 1, "code": 1 },"color": "#B9D9FF" }, - "JSON": { "type": "data", "bytes": 8, "lines": { "total": 4, "content": 2, "code": 2 },"color": "#292929"}, - "JavaScript": { "type": "programming", "bytes": 23, "lines": { "total": 4, "content": 3, "code": 3 },"color": "#f1e05a" }, - "Text": { "type": "prose", "bytes": 0, "lines": { "total": 1, "content": 0, "code": 0 } }, - "C": { "type": "programming", "bytes": 130, "lines": { "total": 10, "content": 8, "code": 4 }, "color": "#555555"}, - "C++": { "type": "programming", "bytes": 15, "lines": { "total": 2, "content": 1, "code": 0 }, "color": "#f34b7d" }, - "TOML": { "type": "data", "bytes": 0, "lines": { "total": 1, "content": 0, "code": 0 }, "color": "#9c4221" } + "Perl": { + "count": 1, + "type": "programming", + "bytes": 0, + "lines": { "total": 1, "content": 0 }, + "color": "#0298c3" + }, + "AGS Script": { + "count": 1, + "type": "programming", + "bytes": 14, + "lines": { "total": 2, "content": 1 }, + "color": "#B9D9FF" + }, + "JSON": { + "count": 2, + "type": "data", + "bytes": 8, + "lines": { "total": 4, "content": 2 }, + "color": "#292929" + }, + "JavaScript": { + "count": 3, + "type": "programming", + "bytes": 23, + "lines": { "total": 4, "content": 3 }, + "color": "#f1e05a" + }, + "Text": { + "count": 1, + "type": "prose", + "bytes": 0, + "lines": { "total": 1, "content": 0 } + }, + "C": { + "count": 1, + "type": "programming", + "bytes": 130, + "lines": { "total": 10, "content": 8 }, + "color": "#555555" + }, + "C++": { + "count": 1, + "type": "programming", + "bytes": 15, + "lines": { "total": 2, "content": 1 }, + "color": "#f34b7d" + }, + "TOML": { + "count": 1, + "type": "data", + "bytes": 0, + "lines": { "total": 1, "content": 0 }, + "color": "#9c4221" + } } }, "unknown": { "count": 1, "bytes": 9, - "lines": { "total": 2, "content": 1, "code": 1 }, + "lines": { "total": 2, "content": 1 }, "extensions": {}, "filenames": { "unknown": 9 diff --git a/test/folder.js b/test/folder.js index 349c9f2..36919eb 100644 --- a/test/folder.js +++ b/test/folder.js @@ -1,11 +1,14 @@ -const fs = require('fs'); -const linguist = require('..'); -const { updatedDiff } = require('deep-object-diff'); +import FS from 'node:fs'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { updatedDiff } from 'deep-object-diff'; +import linguist from '../dist/index.js'; async function testFolder() { console.info('-'.repeat(11) + '\nFolder test\n' + '-'.repeat(11)); - const samplesFolder = __dirname.replace(/\\/g, '/') + '/samples'; - const expectedJson = fs.readFileSync(__dirname + '/expected.json', { encoding: 'utf8' }); + const curFolder = dirname(fileURLToPath(import.meta.url)); + const samplesFolder = curFolder.replace(/\\/g, '/') + '/samples'; + const expectedJson = FS.readFileSync(curFolder + '/expected.json', { encoding: 'utf8' }); const expected = JSON.parse(expectedJson.replace(/~/g, samplesFolder)); const actual = await linguist(samplesFolder); diff --git a/test/perf.js b/test/perf.js index 5d8d134..b75ff20 100644 --- a/test/perf.js +++ b/test/perf.js @@ -1,4 +1,4 @@ -const linguist = require('..'); +import linguist from '../dist/index.js'; async function perfTest() { let time = 0; diff --git a/test/unit.js b/test/unit.js index 0264e1b..c902ec1 100644 --- a/test/unit.js +++ b/test/unit.js @@ -1,4 +1,4 @@ -const linguist = require('..'); +import linguist from '../dist/index.js'; let i = 0; let errors = 0; diff --git a/tsconfig.json b/tsconfig.json index 5dbba49..91264ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,9 @@ /* Examples: https://github.com/tsconfig/bases */ /* Basic Options */ - "target": "es2019", // Node 12 - "module": "commonjs", - "lib": ["es2020"], + "target": "ES2023", + "module": "NodeNext", + "lib": ["ESNext"], //"allowJs": true, //"checkJs": true, //"jsx": "preserve", @@ -46,7 +46,7 @@ //"noPropertyAccessFromIndexSignature": true, /* Module Resolution Options */ - "moduleResolution": "node", + "moduleResolution": "nodenext", "resolveJsonModule": true, //"baseUrl": "./", //"paths": {},