linguist-js 2.7.1 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -2
- package/dist/helpers/convert-pcre.js +1 -1
- package/dist/helpers/load-data.d.ts +1 -1
- package/dist/helpers/load-data.js +4 -5
- package/dist/helpers/parse-gitattributes.d.ts +1 -0
- package/dist/helpers/parse-gitattributes.js +6 -4
- package/dist/helpers/parse-gitignore.js +1 -1
- package/dist/helpers/read-file.d.ts +1 -1
- package/dist/helpers/read-file.js +2 -2
- package/dist/helpers/walk-tree.js +7 -2
- package/dist/index.js +73 -32
- package/dist/package.json +56 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +146 -0
- package/dist/src/helpers/convert-pcre.d.ts +2 -0
- package/dist/src/helpers/convert-pcre.js +38 -0
- package/dist/src/helpers/load-data.d.ts +4 -0
- package/dist/src/helpers/load-data.js +37 -0
- package/dist/src/helpers/norm-path.d.ts +2 -0
- package/dist/src/helpers/norm-path.js +15 -0
- package/dist/src/helpers/parse-gitattributes.d.ts +17 -0
- package/dist/src/helpers/parse-gitattributes.js +37 -0
- package/dist/src/helpers/parse-gitignore.d.ts +1 -0
- package/dist/src/helpers/parse-gitignore.js +12 -0
- package/dist/src/helpers/read-file.d.ts +5 -0
- package/dist/src/helpers/read-file.js +23 -0
- package/dist/src/helpers/walk-tree.d.ts +20 -0
- package/dist/src/helpers/walk-tree.js +85 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +485 -0
- package/dist/src/schema.d.ts +37 -0
- package/dist/src/schema.js +2 -0
- package/dist/src/types.d.ts +74 -0
- package/dist/src/types.js +2 -0
- package/dist/types.d.ts +22 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/ext/documentation.yml +0 -3
- package/ext/generated.rb +5 -0
- package/ext/heuristics.yml +91 -16
- package/ext/languages.yml +317 -8
- package/ext/vendor.yml +0 -2
- package/package.json +6 -5
- package/readme.md +29 -5
package/dist/cli.js
CHANGED
|
@@ -24,11 +24,13 @@ commander_1.program
|
|
|
24
24
|
.option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
|
|
25
25
|
.option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
|
|
26
26
|
.option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
|
|
27
|
+
.option('-L|--calculateLines [bool]', 'Calculate lines of code totals', true)
|
|
27
28
|
.option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
|
|
28
29
|
.option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
|
|
29
30
|
.option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
|
|
30
31
|
.option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
|
|
31
32
|
.option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
|
|
33
|
+
.option('-D|--checkDetected [bool]', 'Force files marked with linguist-detectable to always appear in output', true)
|
|
32
34
|
.option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
|
|
33
35
|
.option('-S|--checkShebang [bool]', 'Check shebang lines for explicit classification', true)
|
|
34
36
|
.option('-M|--checkModeline [bool]', 'Check modelines for explicit classification', true)
|
|
@@ -79,16 +81,17 @@ if (args.analyze)
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
// List parsed results
|
|
82
|
-
for (const [lang, { bytes, color }] of sortedEntries) {
|
|
84
|
+
for (const [lang, { bytes, lines, color }] of sortedEntries) {
|
|
83
85
|
const percent = (bytes) => bytes / (totalBytes || 1) * 100;
|
|
84
86
|
const fmtd = {
|
|
85
87
|
index: (++count).toString().padStart(2, ' '),
|
|
86
88
|
lang: lang.padEnd(24, ' '),
|
|
87
89
|
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
88
90
|
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
91
|
+
loc: lines.code.toLocaleString().padStart(10, ' '),
|
|
89
92
|
icon: colouredMsg(hexToRgb(color !== null && color !== void 0 ? color : '#ededed'), '\u2588'),
|
|
90
93
|
};
|
|
91
|
-
console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B`);
|
|
94
|
+
console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B ${fmtd.loc} LOC`);
|
|
92
95
|
// If using `listFiles` option, list all files tagged as this language
|
|
93
96
|
if (args.listFiles) {
|
|
94
97
|
console.log(); // padding
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = pcre;
|
|
3
4
|
/** Convert a PCRE regex into JS. */
|
|
4
5
|
function pcre(regex) {
|
|
5
6
|
let finalRegex = regex;
|
|
@@ -35,4 +36,3 @@ function pcre(regex) {
|
|
|
35
36
|
// Return final regex
|
|
36
37
|
return RegExp(finalRegex, [...finalFlags].join(''));
|
|
37
38
|
}
|
|
38
|
-
exports.default = pcre;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Nukes unused `generated.rb` file content. */
|
|
2
|
-
export declare function parseGeneratedDataFile(fileContent: string):
|
|
2
|
+
export declare function parseGeneratedDataFile(fileContent: string): string[];
|
|
3
3
|
/** Load a data file from github-linguist. */
|
|
4
4
|
export default function loadFile(file: string, offline?: boolean): Promise<string>;
|
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.parseGeneratedDataFile =
|
|
6
|
+
exports.parseGeneratedDataFile = parseGeneratedDataFile;
|
|
7
|
+
exports.default = loadFile;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const cross_fetch_1 = __importDefault(require("cross-fetch"));
|
|
@@ -26,13 +27,11 @@ async function loadLocalFile(file) {
|
|
|
26
27
|
return fs_1.default.promises.readFile(filePath).then(buffer => buffer.toString());
|
|
27
28
|
}
|
|
28
29
|
/** Nukes unused `generated.rb` file content. */
|
|
29
|
-
|
|
30
|
+
function parseGeneratedDataFile(fileContent) {
|
|
30
31
|
var _a;
|
|
31
32
|
return [...(_a = fileContent.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []];
|
|
32
33
|
}
|
|
33
|
-
exports.parseGeneratedDataFile = parseGeneratedDataFile;
|
|
34
34
|
/** Load a data file from github-linguist. */
|
|
35
|
-
|
|
35
|
+
function loadFile(file, offline = false) {
|
|
36
36
|
return offline ? loadLocalFile(file) : loadWebFile(file);
|
|
37
37
|
}
|
|
38
|
-
exports.default = loadFile;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseAttributes;
|
|
3
4
|
const norm_path_1 = require("./norm-path");
|
|
4
5
|
/**
|
|
5
6
|
* Parses a gitattributes file.
|
|
@@ -21,10 +22,12 @@ function parseAttributes(content, folderRoot = '.') {
|
|
|
21
22
|
const falseParts = (str) => attrParts.filter(part => part.includes(str) && isFalse(part));
|
|
22
23
|
const hasTrueParts = (str) => trueParts(str).length > 0;
|
|
23
24
|
const hasFalseParts = (str) => falseParts(str).length > 0;
|
|
25
|
+
const boolOrNullVal = (str) => hasTrueParts(str) ? true : hasFalseParts(str) ? false : null;
|
|
24
26
|
const attrs = {
|
|
25
|
-
'generated':
|
|
26
|
-
'vendored':
|
|
27
|
-
'documentation':
|
|
27
|
+
'generated': boolOrNullVal('linguist-generated'),
|
|
28
|
+
'vendored': boolOrNullVal('linguist-vendored'),
|
|
29
|
+
'documentation': boolOrNullVal('linguist-documentation'),
|
|
30
|
+
'detectable': boolOrNullVal('linguist-detectable'),
|
|
28
31
|
'binary': hasTrueParts('binary') || hasFalseParts('text') ? true : hasFalseParts('binary') || hasTrueParts('text') ? false : null,
|
|
29
32
|
'language': (_b = (_a = trueParts('linguist-language').at(-1)) === null || _a === void 0 ? void 0 : _a.split('=')[1]) !== null && _b !== void 0 ? _b : null,
|
|
30
33
|
};
|
|
@@ -32,4 +35,3 @@ function parseAttributes(content, folderRoot = '.') {
|
|
|
32
35
|
}
|
|
33
36
|
return output;
|
|
34
37
|
}
|
|
35
|
-
exports.default = parseAttributes;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseGitignore;
|
|
3
4
|
function parseGitignore(content) {
|
|
4
5
|
const readableData = content
|
|
5
6
|
// Remove comments unless escaped
|
|
@@ -9,4 +10,3 @@ function parseGitignore(content) {
|
|
|
9
10
|
const arrayData = readableData.split(/\r?\n/).filter(data => data);
|
|
10
11
|
return arrayData;
|
|
11
12
|
}
|
|
12
|
-
exports.default = parseGitignore;
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Read part of a file on disc.
|
|
3
3
|
* @throws 'EPERM' if the file is not readable.
|
|
4
4
|
*/
|
|
5
|
-
export default function
|
|
5
|
+
export default function readFileChunk(filename: string, onlyFirstLine?: boolean): Promise<string>;
|
|
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = readFileChunk;
|
|
6
7
|
const fs_1 = __importDefault(require("fs"));
|
|
7
8
|
/**
|
|
8
9
|
* Read part of a file on disc.
|
|
9
10
|
* @throws 'EPERM' if the file is not readable.
|
|
10
11
|
*/
|
|
11
|
-
async function
|
|
12
|
+
async function readFileChunk(filename, onlyFirstLine = false) {
|
|
12
13
|
const chunkSize = 100;
|
|
13
14
|
const stream = fs_1.default.createReadStream(filename, { highWaterMark: chunkSize });
|
|
14
15
|
let content = '';
|
|
@@ -20,4 +21,3 @@ async function readFile(filename, onlyFirstLine = false) {
|
|
|
20
21
|
}
|
|
21
22
|
return content;
|
|
22
23
|
}
|
|
23
|
-
exports.default = readFile;
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = walk;
|
|
6
7
|
const fs_1 = __importDefault(require("fs"));
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const parse_gitignore_1 = __importDefault(require("./parse-gitignore"));
|
|
@@ -36,7 +37,12 @@ function walk(data) {
|
|
|
36
37
|
if (fs_1.default.existsSync(gitignoreFilename)) {
|
|
37
38
|
const gitignoreContents = fs_1.default.readFileSync(gitignoreFilename, 'utf-8');
|
|
38
39
|
const ignoredPaths = (0, parse_gitignore_1.default)(gitignoreContents);
|
|
39
|
-
|
|
40
|
+
const rootRelIgnoredPaths = ignoredPaths.map(ignorePath =>
|
|
41
|
+
// get absolute path of the ignore glob
|
|
42
|
+
(0, norm_path_1.normPath)(folder, ignorePath)
|
|
43
|
+
// convert abs ignore glob to be relative to the root folder
|
|
44
|
+
.replace(commonRoot + '/', ''));
|
|
45
|
+
ignored.add(rootRelIgnoredPaths);
|
|
40
46
|
}
|
|
41
47
|
// Add gitattributes if present
|
|
42
48
|
const gitattributesPath = (0, norm_path_1.normPath)(folder, '.gitattributes');
|
|
@@ -82,4 +88,3 @@ function walk(data) {
|
|
|
82
88
|
folders: [...allFolders],
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
|
-
exports.default = walk;
|
package/dist/index.js
CHANGED
|
@@ -39,14 +39,16 @@ const parse_gitattributes_1 = __importDefault(require("./helpers/parse-gitattrib
|
|
|
39
39
|
const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
|
|
40
40
|
const norm_path_1 = require("./helpers/norm-path");
|
|
41
41
|
async function analyse(rawPaths, opts = {}) {
|
|
42
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
43
|
-
var
|
|
42
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
43
|
+
var _u, _v;
|
|
44
44
|
const useRawContent = opts.fileContent !== undefined;
|
|
45
45
|
const input = [rawPaths !== null && rawPaths !== void 0 ? rawPaths : []].flat();
|
|
46
46
|
const manualFileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
|
|
47
47
|
// Normalise input option arguments
|
|
48
48
|
opts = {
|
|
49
|
+
calculateLines: (_b = opts.calculateLines) !== null && _b !== void 0 ? _b : true, // default to true if unset
|
|
49
50
|
checkIgnored: !opts.quick,
|
|
51
|
+
checkDetected: !opts.quick,
|
|
50
52
|
checkAttributes: !opts.quick,
|
|
51
53
|
checkHeuristics: !opts.quick,
|
|
52
54
|
checkShebang: !opts.quick,
|
|
@@ -65,9 +67,9 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
65
67
|
const extensions = {};
|
|
66
68
|
const globOverrides = {};
|
|
67
69
|
const results = {
|
|
68
|
-
files: { count: 0, bytes: 0, results: {}, alternatives: {} },
|
|
69
|
-
languages: { count: 0, bytes: 0, results: {} },
|
|
70
|
-
unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
|
|
70
|
+
files: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {}, alternatives: {} },
|
|
71
|
+
languages: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {} },
|
|
72
|
+
unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, extensions: {}, filenames: {} },
|
|
71
73
|
};
|
|
72
74
|
// Set a common root path so that vendor paths do not incorrectly match parent folders
|
|
73
75
|
const resolvedInput = input.map(path => (0, norm_path_1.normPath)(path_1.default.resolve(path)));
|
|
@@ -81,7 +83,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
81
83
|
// Prepare list of ignored files
|
|
82
84
|
const ignored = (0, ignore_1.default)();
|
|
83
85
|
ignored.add('.git/');
|
|
84
|
-
ignored.add((
|
|
86
|
+
ignored.add((_c = opts.ignoredFiles) !== null && _c !== void 0 ? _c : []);
|
|
85
87
|
const regexIgnores = opts.keepVendored ? [] : vendorPaths.map(path => RegExp(path, 'i'));
|
|
86
88
|
// Load file paths and folders
|
|
87
89
|
let files;
|
|
@@ -176,7 +178,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
176
178
|
files.push(...unignoredList);
|
|
177
179
|
}
|
|
178
180
|
// Ignore specific languages
|
|
179
|
-
for (const lang of (
|
|
181
|
+
for (const lang of (_d = opts.ignoredLanguages) !== null && _d !== void 0 ? _d : []) {
|
|
180
182
|
for (const key in langData) {
|
|
181
183
|
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
182
184
|
delete langData[key];
|
|
@@ -229,7 +231,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
229
231
|
// Check first line for readability
|
|
230
232
|
let firstLine;
|
|
231
233
|
if (useRawContent) {
|
|
232
|
-
firstLine = (
|
|
234
|
+
firstLine = (_f = (_e = manualFileContent[files.indexOf(file)]) === null || _e === void 0 ? void 0 : _e.split('\n')[0]) !== null && _f !== void 0 ? _f : null;
|
|
233
235
|
}
|
|
234
236
|
else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
|
|
235
237
|
firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
|
|
@@ -248,7 +250,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
248
250
|
const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
|
|
249
251
|
// Check for interpreter match
|
|
250
252
|
if (opts.checkShebang && hasShebang) {
|
|
251
|
-
const matchesInterpretor = (
|
|
253
|
+
const matchesInterpretor = (_g = data.interpreters) === null || _g === void 0 ? void 0 : _g.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
|
|
252
254
|
if (matchesInterpretor)
|
|
253
255
|
matches.push(lang);
|
|
254
256
|
}
|
|
@@ -256,7 +258,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
256
258
|
if (opts.checkModeline && hasModeline) {
|
|
257
259
|
const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
|
|
258
260
|
const matchesLang = modelineText.match(langMatcher(lang));
|
|
259
|
-
const matchesAlias = (
|
|
261
|
+
const matchesAlias = (_h = data.aliases) === null || _h === void 0 ? void 0 : _h.some(lang => modelineText.match(langMatcher(lang)));
|
|
260
262
|
if (matchesLang || matchesAlias)
|
|
261
263
|
matches.push(lang);
|
|
262
264
|
}
|
|
@@ -275,7 +277,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
275
277
|
let skipExts = false;
|
|
276
278
|
// Check if filename is a match
|
|
277
279
|
for (const lang in langData) {
|
|
278
|
-
const matchesName = (
|
|
280
|
+
const matchesName = (_j = langData[lang].filenames) === null || _j === void 0 ? void 0 : _j.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
|
|
279
281
|
if (matchesName) {
|
|
280
282
|
addResult(file, lang);
|
|
281
283
|
skipExts = true;
|
|
@@ -285,7 +287,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
285
287
|
const possibleExts = [];
|
|
286
288
|
if (!skipExts)
|
|
287
289
|
for (const lang in langData) {
|
|
288
|
-
const extMatches = (
|
|
290
|
+
const extMatches = (_k = langData[lang].extensions) === null || _k === void 0 ? void 0 : _k.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
|
|
289
291
|
if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
|
|
290
292
|
for (const ext of extMatches)
|
|
291
293
|
possibleExts.push({ ext, lang });
|
|
@@ -331,7 +333,7 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
331
333
|
heuristic.language = heuristic.language[0];
|
|
332
334
|
}
|
|
333
335
|
// Make sure the results includes this language
|
|
334
|
-
const languageGroup = (
|
|
336
|
+
const languageGroup = (_l = langData[heuristic.language]) === null || _l === void 0 ? void 0 : _l.group;
|
|
335
337
|
const matchesLang = fileAssociations[file].includes(heuristic.language);
|
|
336
338
|
const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
|
|
337
339
|
if (!matchesLang && !matchesParent)
|
|
@@ -376,17 +378,23 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
376
378
|
}
|
|
377
379
|
}
|
|
378
380
|
// Skip specified categories
|
|
379
|
-
if ((
|
|
381
|
+
if ((_m = opts.categories) === null || _m === void 0 ? void 0 : _m.length) {
|
|
380
382
|
const categories = ['data', 'markup', 'programming', 'prose'];
|
|
381
383
|
const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
|
|
382
384
|
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
383
|
-
|
|
385
|
+
// Skip if language is not hidden
|
|
386
|
+
if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; }))
|
|
384
387
|
continue;
|
|
388
|
+
// Skip if language is forced as detectable
|
|
389
|
+
if (opts.checkDetected) {
|
|
390
|
+
const detectable = (0, ignore_1.default)().add(getFlaggedGlobs('detectable', true));
|
|
391
|
+
if (detectable.ignores(relPath(file)))
|
|
392
|
+
continue;
|
|
385
393
|
}
|
|
394
|
+
// Delete result otherwise
|
|
386
395
|
delete results.files.results[file];
|
|
387
|
-
if (lang)
|
|
396
|
+
if (lang)
|
|
388
397
|
delete results.languages.results[lang];
|
|
389
|
-
}
|
|
390
398
|
}
|
|
391
399
|
for (const category of hiddenCategories) {
|
|
392
400
|
for (const [lang, { type }] of Object.entries(results.languages.results)) {
|
|
@@ -412,26 +420,59 @@ async function analyse(rawPaths, opts = {}) {
|
|
|
412
420
|
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
413
421
|
if (lang && !langData[lang])
|
|
414
422
|
continue;
|
|
415
|
-
|
|
423
|
+
// Calculate file size
|
|
424
|
+
const fileSize = (_p = (_o = manualFileContent[files.indexOf(file)]) === null || _o === void 0 ? void 0 : _o.length) !== null && _p !== void 0 ? _p : fs_1.default.statSync(file).size;
|
|
425
|
+
// Calculate lines of code
|
|
426
|
+
const loc = { total: 0, content: 0, code: 0 };
|
|
427
|
+
if (opts.calculateLines) {
|
|
428
|
+
const fileContent = (_r = ((_q = manualFileContent[files.indexOf(file)]) !== null && _q !== void 0 ? _q : fs_1.default.readFileSync(file).toString())) !== null && _r !== void 0 ? _r : '';
|
|
429
|
+
const allLines = fileContent.split(/\r?\n/gm);
|
|
430
|
+
loc.total = allLines.length;
|
|
431
|
+
loc.content = allLines.filter(line => line.trim().length > 0).length;
|
|
432
|
+
const codeLines = fileContent
|
|
433
|
+
.replace(/^\s*(\/\/|# |;|--).+/gm, '')
|
|
434
|
+
.replace(/\/\*.+\*\/|<!--.+-->/sg, '');
|
|
435
|
+
loc.code = codeLines.split(/\r?\n/gm).filter(line => line.trim().length > 0).length;
|
|
436
|
+
}
|
|
437
|
+
// Apply to files totals
|
|
416
438
|
results.files.bytes += fileSize;
|
|
417
|
-
|
|
418
|
-
|
|
439
|
+
results.files.lines.total += loc.total;
|
|
440
|
+
results.files.lines.content += loc.content;
|
|
441
|
+
results.files.lines.code += loc.code;
|
|
442
|
+
// Add results to 'languages' section if language match found, or 'unknown' section otherwise
|
|
443
|
+
if (lang) {
|
|
444
|
+
const { type } = langData[lang];
|
|
445
|
+
// set default if unset
|
|
446
|
+
(_s = (_u = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_u[lang] = { type, bytes: 0, lines: { total: 0, content: 0, code: 0 }, color: langData[lang].color });
|
|
447
|
+
// apply results to 'languages' section
|
|
448
|
+
if (opts.childLanguages) {
|
|
449
|
+
results.languages.results[lang].parent = langData[lang].group;
|
|
450
|
+
}
|
|
451
|
+
results.languages.results[lang].bytes += fileSize;
|
|
452
|
+
results.languages.bytes += fileSize;
|
|
453
|
+
results.languages.results[lang].lines.total += loc.total;
|
|
454
|
+
results.languages.results[lang].lines.content += loc.content;
|
|
455
|
+
results.languages.results[lang].lines.code += loc.code;
|
|
456
|
+
results.languages.lines.total += loc.total;
|
|
457
|
+
results.languages.lines.content += loc.content;
|
|
458
|
+
results.languages.lines.code += loc.code;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
419
461
|
const ext = path_1.default.extname(file);
|
|
420
|
-
const unknownType = ext
|
|
421
|
-
const name = ext
|
|
422
|
-
|
|
462
|
+
const unknownType = ext ? 'extensions' : 'filenames';
|
|
463
|
+
const name = ext || path_1.default.basename(file);
|
|
464
|
+
// apply results to 'unknown' section
|
|
465
|
+
(_t = (_v = results.unknown[unknownType])[name]) !== null && _t !== void 0 ? _t : (_v[name] = 0);
|
|
423
466
|
results.unknown[unknownType][name] += fileSize;
|
|
424
467
|
results.unknown.bytes += fileSize;
|
|
425
|
-
|
|
468
|
+
results.unknown.lines.total += loc.total;
|
|
469
|
+
results.unknown.lines.content += loc.content;
|
|
470
|
+
results.unknown.lines.code += loc.code;
|
|
426
471
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
results.languages.results[lang].parent = langData[lang].group;
|
|
432
|
-
}
|
|
433
|
-
results.languages.results[lang].bytes += fileSize;
|
|
434
|
-
results.languages.bytes += fileSize;
|
|
472
|
+
}
|
|
473
|
+
// Set lines output to NaN when line calculation is disabled
|
|
474
|
+
if (opts.calculateLines === false) {
|
|
475
|
+
results.files.lines = { total: NaN, content: NaN, code: NaN };
|
|
435
476
|
}
|
|
436
477
|
// Set counts
|
|
437
478
|
results.files.count = Object.keys(results.files.results).length;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linguist-js",
|
|
3
|
+
"version": "2.8.0",
|
|
4
|
+
"description": "Analyse languages used in a folder. Powered by GitHub Linguist, although it doesn't need to be installed.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linguist-js": "bin/index.js",
|
|
8
|
+
"linguist": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=12"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"download-files": "npx tsx@3 build/download-files",
|
|
15
|
+
"pre-publish": "npm run download-files && npm test && npm run perf",
|
|
16
|
+
"perf": "tsc && node test/perf",
|
|
17
|
+
"test": "tsc && node test/folder && node test/unit"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"dist/",
|
|
22
|
+
"ext/"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Nixinova/Linguist.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"linguist",
|
|
30
|
+
"languages",
|
|
31
|
+
"language-analysis",
|
|
32
|
+
"language-analyzer"
|
|
33
|
+
],
|
|
34
|
+
"author": "Nixinova (https://nixinova.com)",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Nixinova/Linguist/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/Nixinova/Linguist#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"binary-extensions": "^2.3.0 <3",
|
|
42
|
+
"commander": "^9.5.0 <10",
|
|
43
|
+
"common-path-prefix": "^3.0.0",
|
|
44
|
+
"cross-fetch": "^3.1.8 <4",
|
|
45
|
+
"ignore": "^5.3.2",
|
|
46
|
+
"isbinaryfile": "^4.0.10 <5",
|
|
47
|
+
"js-yaml": "^4.1.0",
|
|
48
|
+
"node-cache": "^5.1.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/js-yaml": "^4.0.9",
|
|
52
|
+
"@types/node": "ts5.0",
|
|
53
|
+
"deep-object-diff": "^1.1.9",
|
|
54
|
+
"typescript": "~5.0.4 <5.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
7
|
+
;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const index_1 = __importDefault(require("./index"));
|
|
12
|
+
const norm_path_1 = require("./helpers/norm-path");
|
|
13
|
+
const colouredMsg = ([r, g, b], msg) => `\u001B[${38};2;${r};${g};${b}m${msg}${'\u001b[0m'}`;
|
|
14
|
+
const hexToRgb = (hex) => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
|
|
15
|
+
commander_1.program
|
|
16
|
+
.name('linguist')
|
|
17
|
+
.usage('--analyze [<folders...>] [<options...>]')
|
|
18
|
+
.option('-a|--analyze|--analyse [folders...]', 'Analyse the languages of all files in a folder')
|
|
19
|
+
.option('-i|--ignoredFiles <files...>', `A list of file path globs to ignore`)
|
|
20
|
+
.option('-l|--ignoredLanguages <languages...>', `A list of languages to ignore`)
|
|
21
|
+
.option('-c|--categories <categories...>', 'Language categories to include in output')
|
|
22
|
+
.option('-C|--childLanguages [bool]', 'Display child languages instead of their parents', false)
|
|
23
|
+
.option('-j|--json [bool]', 'Display the output as JSON', false)
|
|
24
|
+
.option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
|
|
25
|
+
.option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
|
|
26
|
+
.option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
|
|
27
|
+
.option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
|
|
28
|
+
.option('-L|--calculateLines [bool]', 'Calculate lines of code totals', true)
|
|
29
|
+
.option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
|
|
30
|
+
.option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
|
|
31
|
+
.option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
|
|
32
|
+
.option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
|
|
33
|
+
.option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
|
|
34
|
+
.option('-D|--checkDetected [bool]', 'Force files marked with linguist-detectable to always appear in output', true)
|
|
35
|
+
.option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
|
|
36
|
+
.option('-S|--checkShebang [bool]', 'Check shebang lines for explicit classification', true)
|
|
37
|
+
.option('-M|--checkModeline [bool]', 'Check modelines for explicit classification', true)
|
|
38
|
+
.helpOption(`-h|--help`, 'Display this help message')
|
|
39
|
+
.version(package_json_1.default.version, '-v|--version', 'Display the installed version of linguist-js');
|
|
40
|
+
commander_1.program.parse(process.argv);
|
|
41
|
+
const args = commander_1.program.opts();
|
|
42
|
+
// Normalise arguments
|
|
43
|
+
for (const arg in args) {
|
|
44
|
+
const normalise = (val) => {
|
|
45
|
+
if (typeof val !== 'string')
|
|
46
|
+
return val;
|
|
47
|
+
val = val.replace(/^=/, '');
|
|
48
|
+
if (val.match(/true$|false$/))
|
|
49
|
+
val = val === 'true';
|
|
50
|
+
return val;
|
|
51
|
+
};
|
|
52
|
+
if (Array.isArray(args[arg]))
|
|
53
|
+
args[arg] = args[arg].map(normalise);
|
|
54
|
+
else
|
|
55
|
+
args[arg] = normalise(args[arg]);
|
|
56
|
+
}
|
|
57
|
+
// Run Linguist
|
|
58
|
+
if (args.analyze)
|
|
59
|
+
(async () => {
|
|
60
|
+
// Fetch language data
|
|
61
|
+
const root = args.analyze === true ? '.' : args.analyze;
|
|
62
|
+
const data = await (0, index_1.default)(root, args);
|
|
63
|
+
const { files, languages, unknown } = data;
|
|
64
|
+
// Print output
|
|
65
|
+
if (!args.json) {
|
|
66
|
+
const sortedEntries = Object.entries(languages.results).sort((a, b) => a[1].bytes < b[1].bytes ? +1 : -1);
|
|
67
|
+
const totalBytes = languages.bytes;
|
|
68
|
+
console.log(`\n Analysed ${files.bytes.toLocaleString()} B from ${files.count} files with linguist-js`);
|
|
69
|
+
console.log(`\n Language analysis results: \n`);
|
|
70
|
+
let count = 0;
|
|
71
|
+
if (sortedEntries.length === 0)
|
|
72
|
+
console.log(` None`);
|
|
73
|
+
// Collate files per language
|
|
74
|
+
const filesPerLanguage = {};
|
|
75
|
+
if (args.listFiles) {
|
|
76
|
+
for (const language of Object.keys(languages.results)) {
|
|
77
|
+
filesPerLanguage[language] = [];
|
|
78
|
+
}
|
|
79
|
+
for (const [file, lang] of Object.entries(files.results)) {
|
|
80
|
+
if (lang)
|
|
81
|
+
filesPerLanguage[lang].push(file);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// List parsed results
|
|
85
|
+
for (const [lang, { bytes, lines, color }] of sortedEntries) {
|
|
86
|
+
const percent = (bytes) => bytes / (totalBytes || 1) * 100;
|
|
87
|
+
const fmtd = {
|
|
88
|
+
index: (++count).toString().padStart(2, ' '),
|
|
89
|
+
lang: lang.padEnd(24, ' '),
|
|
90
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
91
|
+
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
92
|
+
loc: lines.code.toLocaleString().padStart(10, ' '),
|
|
93
|
+
icon: colouredMsg(hexToRgb(color !== null && color !== void 0 ? color : '#ededed'), '\u2588'),
|
|
94
|
+
};
|
|
95
|
+
console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B ${fmtd.loc} LOC`);
|
|
96
|
+
// If using `listFiles` option, list all files tagged as this language
|
|
97
|
+
if (args.listFiles) {
|
|
98
|
+
console.log(); // padding
|
|
99
|
+
for (const file of filesPerLanguage[lang]) {
|
|
100
|
+
let relFile = (0, norm_path_1.normPath)(node_path_1.default.relative(node_path_1.default.resolve('.'), file));
|
|
101
|
+
if (!relFile.startsWith('../'))
|
|
102
|
+
relFile = './' + relFile;
|
|
103
|
+
const bytes = (await node_fs_1.default.promises.stat(file)).size;
|
|
104
|
+
const fmtd2 = {
|
|
105
|
+
file: relFile.padEnd(42, ' '),
|
|
106
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
107
|
+
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
108
|
+
};
|
|
109
|
+
console.log(` ${fmtd.icon} ${fmtd2.file} ${fmtd2.percent}% ${fmtd2.bytes} B`);
|
|
110
|
+
}
|
|
111
|
+
console.log(); // padding
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!args.listFiles)
|
|
115
|
+
console.log(); // padding
|
|
116
|
+
console.log(` Total: ${totalBytes.toLocaleString()} B`);
|
|
117
|
+
// List unknown files/extensions
|
|
118
|
+
if (unknown.bytes > 0) {
|
|
119
|
+
console.log(`\n Unknown files and extensions:`);
|
|
120
|
+
for (const [name, bytes] of Object.entries(unknown.filenames)) {
|
|
121
|
+
console.log(` '${name}': ${bytes.toLocaleString()} B`);
|
|
122
|
+
}
|
|
123
|
+
for (const [ext, bytes] of Object.entries(unknown.extensions)) {
|
|
124
|
+
console.log(` '*${ext}': ${bytes.toLocaleString()} B`);
|
|
125
|
+
}
|
|
126
|
+
console.log(` Total: ${unknown.bytes.toLocaleString()} B`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (args.tree) {
|
|
130
|
+
const treeParts = args.tree.split('.');
|
|
131
|
+
let nestedData = data;
|
|
132
|
+
for (const part of treeParts) {
|
|
133
|
+
if (!nestedData[part])
|
|
134
|
+
throw Error(`TraversalError: Key '${part}' cannot be found on output object.`);
|
|
135
|
+
nestedData = nestedData[part];
|
|
136
|
+
}
|
|
137
|
+
console.log(nestedData);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.dir(data, { depth: null });
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
else {
|
|
144
|
+
console.log(`Welcome to linguist-js, a JavaScript port of GitHub's language analyzer.`);
|
|
145
|
+
console.log(`Type 'linguist --help' for a list of commands.`);
|
|
146
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = pcre;
|
|
4
|
+
/** Convert a PCRE regex into JS. */
|
|
5
|
+
function pcre(regex) {
|
|
6
|
+
let finalRegex = regex;
|
|
7
|
+
const replace = (search, replace) => finalRegex = finalRegex.replace(search, replace);
|
|
8
|
+
const finalFlags = new Set();
|
|
9
|
+
// Convert inline flag declarations
|
|
10
|
+
const inlineMatches = regex.matchAll(/\?(-)?([a-z]):/g);
|
|
11
|
+
const startMatches = regex.matchAll(/\(\?(-)?([a-z]+)\)/g);
|
|
12
|
+
for (const [match, isNegative, flags] of [...inlineMatches, ...startMatches]) {
|
|
13
|
+
replace(match, '');
|
|
14
|
+
const func = (flag) => isNegative ? finalFlags.delete(flag) : finalFlags.add(flag);
|
|
15
|
+
[...flags].forEach(func);
|
|
16
|
+
}
|
|
17
|
+
// Remove PCRE-only syntax
|
|
18
|
+
replace(/([*+]){2}/g, '$1');
|
|
19
|
+
replace(/\(\?>/g, '(?:');
|
|
20
|
+
// Remove start/end-of-file markers
|
|
21
|
+
if (/\\[AZ]/.test(finalRegex)) {
|
|
22
|
+
replace(/\\A/g, '^');
|
|
23
|
+
replace(/\\Z/g, '$');
|
|
24
|
+
finalFlags.delete('m');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
finalFlags.add('m');
|
|
28
|
+
}
|
|
29
|
+
// Reformat free-spacing mode
|
|
30
|
+
if (finalFlags.has('x')) {
|
|
31
|
+
finalFlags.delete('x');
|
|
32
|
+
replace(/#.+/g, '');
|
|
33
|
+
replace(/^\s+|\s+$|\n/gm, '');
|
|
34
|
+
replace(/\s+/g, ' ');
|
|
35
|
+
}
|
|
36
|
+
// Return final regex
|
|
37
|
+
return RegExp(finalRegex, [...finalFlags].join(''));
|
|
38
|
+
}
|