linguist-js 2.6.1 → 2.7.0-pre
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 +36 -2
- package/dist/helpers/parse-gitattributes.d.ts +16 -0
- package/dist/helpers/parse-gitattributes.js +37 -0
- package/dist/helpers/walk-tree.d.ts +2 -2
- package/dist/helpers/walk-tree.js +9 -7
- package/dist/index.js +127 -116
- package/dist/types.d.ts +4 -0
- package/package.json +3 -3
- package/readme.md +8 -2
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const VERSION = require('../package.json').version;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
7
9
|
const commander_1 = require("commander");
|
|
8
10
|
const index_1 = __importDefault(require("./index"));
|
|
9
11
|
const colouredMsg = ([r, g, b], msg) => `\u001B[${38};2;${r};${g};${b}m${msg}${'\u001b[0m'}`;
|
|
@@ -18,6 +20,7 @@ commander_1.program
|
|
|
18
20
|
.option('-C|--childLanguages [bool]', 'Display child languages instead of their parents', false)
|
|
19
21
|
.option('-j|--json [bool]', 'Display the output as JSON', false)
|
|
20
22
|
.option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
|
|
23
|
+
.option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
|
|
21
24
|
.option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
|
|
22
25
|
.option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
|
|
23
26
|
.option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
|
|
@@ -59,21 +62,52 @@ if (args.analyze)
|
|
|
59
62
|
const sortedEntries = Object.entries(languages.results).sort((a, b) => a[1].bytes < b[1].bytes ? +1 : -1);
|
|
60
63
|
const totalBytes = languages.bytes;
|
|
61
64
|
console.log(`\n Analysed ${files.bytes.toLocaleString()} B from ${files.count} files with linguist-js`);
|
|
62
|
-
console.log(`\n Language analysis results
|
|
65
|
+
console.log(`\n Language analysis results: \n`);
|
|
63
66
|
let count = 0;
|
|
64
67
|
if (sortedEntries.length === 0)
|
|
65
68
|
console.log(` None`);
|
|
69
|
+
// Collate files per language
|
|
70
|
+
const filesPerLanguage = {};
|
|
71
|
+
if (args.listFiles) {
|
|
72
|
+
for (const language of Object.keys(languages.results)) {
|
|
73
|
+
filesPerLanguage[language] = [];
|
|
74
|
+
}
|
|
75
|
+
for (const [file, lang] of Object.entries(files.results)) {
|
|
76
|
+
if (lang)
|
|
77
|
+
filesPerLanguage[lang].push(file);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
66
80
|
// List parsed results
|
|
67
81
|
for (const [lang, { bytes, color }] of sortedEntries) {
|
|
82
|
+
const percent = (bytes) => bytes / (totalBytes || 1) * 100;
|
|
68
83
|
const fmtd = {
|
|
69
84
|
index: (++count).toString().padStart(2, ' '),
|
|
70
85
|
lang: lang.padEnd(24, ' '),
|
|
71
|
-
percent: (bytes
|
|
86
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
72
87
|
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
73
88
|
icon: colouredMsg(hexToRgb(color !== null && color !== void 0 ? color : '#ededed'), '\u2588'),
|
|
74
89
|
};
|
|
75
90
|
console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B`);
|
|
91
|
+
// If using `listFiles` option, list all files tagged as this language
|
|
92
|
+
if (args.listFiles) {
|
|
93
|
+
console.log(); // padding
|
|
94
|
+
for (const file of filesPerLanguage[lang]) {
|
|
95
|
+
let relFile = path_1.default.relative(path_1.default.resolve('.'), file).replace(/\\/g, '/');
|
|
96
|
+
if (!relFile.startsWith('../'))
|
|
97
|
+
relFile = './' + relFile;
|
|
98
|
+
const bytes = (await fs_1.default.promises.stat(file)).size;
|
|
99
|
+
const fmtd2 = {
|
|
100
|
+
file: relFile.padEnd(42, ' '),
|
|
101
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
102
|
+
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
103
|
+
};
|
|
104
|
+
console.log(` ${fmtd.icon} ${fmtd2.file} ${fmtd2.percent}% ${fmtd2.bytes} B`);
|
|
105
|
+
}
|
|
106
|
+
console.log(); // padding
|
|
107
|
+
}
|
|
76
108
|
}
|
|
109
|
+
if (!args.listFiles)
|
|
110
|
+
console.log(); // padding
|
|
77
111
|
console.log(` Total: ${totalBytes.toLocaleString()} B`);
|
|
78
112
|
// List unknown files/extensions
|
|
79
113
|
if (unknown.bytes > 0) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as T from '../types';
|
|
2
|
+
export type FlagAttributes = {
|
|
3
|
+
'vendored': boolean | null;
|
|
4
|
+
'generated': boolean | null;
|
|
5
|
+
'documentation': boolean | null;
|
|
6
|
+
'binary': boolean | null;
|
|
7
|
+
'language': T.LanguageResult;
|
|
8
|
+
};
|
|
9
|
+
export type ParsedGitattributes = Array<{
|
|
10
|
+
glob: T.FileGlob;
|
|
11
|
+
attrs: FlagAttributes;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Parses a gitattributes file.
|
|
15
|
+
*/
|
|
16
|
+
export default function parseAttributes(content: string, folderRoot?: string): ParsedGitattributes;
|
|
@@ -0,0 +1,37 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
7
|
+
/**
|
|
8
|
+
* Parses a gitattributes file.
|
|
9
|
+
*/
|
|
10
|
+
function parseAttributes(content, folderRoot = '.') {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const output = [];
|
|
13
|
+
for (const line of content.split('\n')) {
|
|
14
|
+
if (!line)
|
|
15
|
+
continue;
|
|
16
|
+
const parts = line.split(/\s+/g);
|
|
17
|
+
const fileGlob = parts[0];
|
|
18
|
+
const relFileGlob = path_1.default.join(folderRoot, fileGlob).replace(/\\/g, '/');
|
|
19
|
+
const attrParts = parts.slice(1);
|
|
20
|
+
const isTrue = (str) => !str.startsWith('-') && !str.endsWith('=false');
|
|
21
|
+
const isFalse = (str) => str.startsWith('-') || str.endsWith('=false');
|
|
22
|
+
const trueParts = (str) => attrParts.filter(part => part.includes(str) && isTrue(part));
|
|
23
|
+
const falseParts = (str) => attrParts.filter(part => part.includes(str) && isFalse(part));
|
|
24
|
+
const hasTrueParts = (str) => trueParts(str).length > 0;
|
|
25
|
+
const hasFalseParts = (str) => falseParts(str).length > 0;
|
|
26
|
+
const attrs = {
|
|
27
|
+
'generated': hasTrueParts('linguist-generated') ? true : hasFalseParts('linguist-generated') ? false : null,
|
|
28
|
+
'vendored': hasTrueParts('linguist-vendored') ? true : hasFalseParts('linguist-vendored') ? false : null,
|
|
29
|
+
'documentation': hasTrueParts('linguist-documentation') ? true : hasFalseParts('linguist-documentation') ? false : null,
|
|
30
|
+
'binary': hasTrueParts('binary') || hasFalseParts('text') ? true : hasFalseParts('binary') || hasTrueParts('text') ? false : null,
|
|
31
|
+
'language': (_b = (_a = trueParts('linguist-language').at(-1)) === null || _a === void 0 ? void 0 : _a.split('=')[1]) !== null && _b !== void 0 ? _b : null,
|
|
32
|
+
};
|
|
33
|
+
output.push({ glob: relFileGlob, attrs });
|
|
34
|
+
}
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
exports.default = parseAttributes;
|
|
@@ -8,8 +8,8 @@ interface WalkInput {
|
|
|
8
8
|
folderRoots: string[];
|
|
9
9
|
/** The absolute path of folders being checked */
|
|
10
10
|
folders: string[];
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** An instantiated Ignore object listing ignored files */
|
|
12
|
+
ignored: Ignore;
|
|
13
13
|
}
|
|
14
14
|
interface WalkOutput {
|
|
15
15
|
files: string[];
|
|
@@ -11,7 +11,7 @@ let allFolders;
|
|
|
11
11
|
;
|
|
12
12
|
/** Generate list of files in a directory. */
|
|
13
13
|
function walk(data) {
|
|
14
|
-
const { init, commonRoot, folderRoots, folders,
|
|
14
|
+
const { init, commonRoot, folderRoots, folders, ignored } = data;
|
|
15
15
|
// Initialise files and folders lists
|
|
16
16
|
if (init) {
|
|
17
17
|
allFiles = new Set();
|
|
@@ -34,11 +34,13 @@ function walk(data) {
|
|
|
34
34
|
// Create absolute path for disc operations
|
|
35
35
|
const path = path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
|
|
36
36
|
const localPath = localRoot ? file.replace(`./${localRoot}/`, '') : file.replace('./', '');
|
|
37
|
-
// Skip if nonexistant
|
|
37
|
+
// Skip if nonexistant
|
|
38
38
|
const nonExistant = !fs_1.default.existsSync(path);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if (nonExistant)
|
|
40
|
+
continue;
|
|
41
|
+
// Skip if marked as ignored
|
|
42
|
+
const isIgnored = ignored.test(localPath).ignored;
|
|
43
|
+
if (isIgnored)
|
|
42
44
|
continue;
|
|
43
45
|
// Add absolute folder path to list
|
|
44
46
|
allFolders.add(path_1.default.resolve(folder).replace(/\\/g, '/'));
|
|
@@ -46,7 +48,7 @@ function walk(data) {
|
|
|
46
48
|
if (file.endsWith('/')) {
|
|
47
49
|
// Recurse into subfolders
|
|
48
50
|
allFolders.add(path);
|
|
49
|
-
walk({ init: false, commonRoot
|
|
51
|
+
walk({ init: false, commonRoot, folderRoots, folders: [path], ignored });
|
|
50
52
|
}
|
|
51
53
|
else {
|
|
52
54
|
// Add file path to list
|
|
@@ -57,7 +59,7 @@ function walk(data) {
|
|
|
57
59
|
// Recurse into all folders
|
|
58
60
|
else {
|
|
59
61
|
for (const i in folders) {
|
|
60
|
-
walk({ init: false, commonRoot
|
|
62
|
+
walk({ init: false, commonRoot, folderRoots: [folderRoots[i]], folders: [folders[i]], ignored });
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
// Return absolute files and folders lists
|
package/dist/index.js
CHANGED
|
@@ -35,13 +35,23 @@ const isbinaryfile_1 = require("isbinaryfile");
|
|
|
35
35
|
const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
|
|
36
36
|
const load_data_1 = __importStar(require("./helpers/load-data"));
|
|
37
37
|
const read_file_1 = __importDefault(require("./helpers/read-file"));
|
|
38
|
+
const parse_gitattributes_1 = __importDefault(require("./helpers/parse-gitattributes"));
|
|
38
39
|
const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
|
|
39
|
-
async function analyse(
|
|
40
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q
|
|
41
|
-
var
|
|
40
|
+
async function analyse(rawInput, opts = {}) {
|
|
41
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
42
|
+
var _r, _s;
|
|
42
43
|
const useRawContent = opts.fileContent !== undefined;
|
|
43
|
-
input = [
|
|
44
|
-
|
|
44
|
+
const input = [rawInput !== null && rawInput !== void 0 ? rawInput : []].flat();
|
|
45
|
+
const manualFileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
|
|
46
|
+
// Normalise input option arguments
|
|
47
|
+
opts = {
|
|
48
|
+
checkIgnored: !opts.quick,
|
|
49
|
+
checkAttributes: !opts.quick,
|
|
50
|
+
checkHeuristics: !opts.quick,
|
|
51
|
+
checkShebang: !opts.quick,
|
|
52
|
+
checkModeline: !opts.quick,
|
|
53
|
+
...opts,
|
|
54
|
+
};
|
|
45
55
|
// Load data from github-linguist web repo
|
|
46
56
|
const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
|
|
47
57
|
const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
|
|
@@ -52,30 +62,33 @@ async function analyse(input, opts = {}) {
|
|
|
52
62
|
// Setup main variables
|
|
53
63
|
const fileAssociations = {};
|
|
54
64
|
const extensions = {};
|
|
55
|
-
const
|
|
65
|
+
const globOverrides = {};
|
|
56
66
|
const results = {
|
|
57
67
|
files: { count: 0, bytes: 0, results: {}, alternatives: {} },
|
|
58
68
|
languages: { count: 0, bytes: 0, results: {} },
|
|
59
69
|
unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
|
|
60
70
|
};
|
|
61
|
-
// Prepare list of ignored files
|
|
62
|
-
const gitignores = (0, ignore_1.default)();
|
|
63
|
-
const regexIgnores = [];
|
|
64
|
-
gitignores.add('.git');
|
|
65
|
-
if (!opts.keepVendored)
|
|
66
|
-
regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
|
|
67
|
-
if (opts.ignoredFiles)
|
|
68
|
-
gitignores.add(opts.ignoredFiles);
|
|
69
71
|
// Set a common root path so that vendor paths do not incorrectly match parent folders
|
|
70
72
|
const normPath = (file) => file.replace(/\\/g, '/');
|
|
71
73
|
const resolvedInput = input.map(path => normPath(path_1.default.resolve(path)));
|
|
72
74
|
const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
|
|
73
75
|
const localRoot = (folder) => folder.replace(commonRoot, '').replace(/^\//, '');
|
|
74
|
-
const relPath = (file) => normPath(path_1.default.relative(commonRoot, file));
|
|
75
|
-
const unRelPath = (file) => normPath(path_1.default.resolve(commonRoot, file));
|
|
76
|
-
|
|
76
|
+
const relPath = (file) => useRawContent ? file : normPath(path_1.default.relative(commonRoot, file));
|
|
77
|
+
const unRelPath = (file) => useRawContent ? file : normPath(path_1.default.resolve(commonRoot, file));
|
|
78
|
+
// Other helper functions
|
|
79
|
+
const fileMatchesGlobs = (file, ...globs) => (0, ignore_1.default)().add(globs).ignores(relPath(file));
|
|
80
|
+
const filterOutIgnored = (files, ignored) => ignored.filter(files.map(relPath)).map(unRelPath);
|
|
81
|
+
//*PREPARE FILES AND DATA*//
|
|
82
|
+
// Prepare list of ignored files
|
|
83
|
+
const ignored = (0, ignore_1.default)();
|
|
84
|
+
ignored.add('.git/');
|
|
85
|
+
ignored.add((_b = opts.ignoredFiles) !== null && _b !== void 0 ? _b : []);
|
|
86
|
+
const regexIgnores = [];
|
|
87
|
+
if (!opts.keepVendored)
|
|
88
|
+
regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
|
|
77
89
|
// Load file paths and folders
|
|
78
|
-
let files
|
|
90
|
+
let files;
|
|
91
|
+
let folders;
|
|
79
92
|
if (useRawContent) {
|
|
80
93
|
// Uses raw file content
|
|
81
94
|
files = input;
|
|
@@ -83,93 +96,96 @@ async function analyse(input, opts = {}) {
|
|
|
83
96
|
}
|
|
84
97
|
else {
|
|
85
98
|
// Uses directory on disc
|
|
86
|
-
const data = (0, walk_tree_1.default)({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput,
|
|
99
|
+
const data = (0, walk_tree_1.default)({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, ignored });
|
|
87
100
|
files = data.files;
|
|
88
101
|
folders = data.folders;
|
|
89
102
|
}
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
checkAttributes: !opts.quick,
|
|
94
|
-
checkHeuristics: !opts.quick,
|
|
95
|
-
checkShebang: !opts.quick,
|
|
96
|
-
checkModeline: !opts.quick,
|
|
97
|
-
...opts
|
|
98
|
-
};
|
|
99
|
-
// Ignore specific languages
|
|
100
|
-
for (const lang of (_b = opts.ignoredLanguages) !== null && _b !== void 0 ? _b : []) {
|
|
101
|
-
for (const key in langData) {
|
|
102
|
-
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
103
|
-
delete langData[key];
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Load gitignores and gitattributes
|
|
109
|
-
const customBinary = (0, ignore_1.default)();
|
|
110
|
-
const customText = (0, ignore_1.default)();
|
|
111
|
-
if (!useRawContent && opts.checkAttributes) {
|
|
103
|
+
// Load gitignore data and apply ignores rules
|
|
104
|
+
if (!useRawContent) {
|
|
105
|
+
// TODO switch to be like the gitattributes code
|
|
112
106
|
for (const folder of folders) {
|
|
113
|
-
// TODO FIX: this is absolute when only 1 path given
|
|
114
|
-
const localFilePath = (path) => localRoot(folder) ? localRoot(folder) + '/' + localPath(path) : path;
|
|
115
|
-
// Skip if folder is marked in gitattributes
|
|
116
|
-
if (relPath(folder) && gitignores.ignores(relPath(folder))) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
107
|
// Parse gitignores
|
|
120
108
|
const ignoresFile = path_1.default.join(folder, '.gitignore');
|
|
121
109
|
if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
|
|
122
110
|
const ignoresData = await (0, read_file_1.default)(ignoresFile);
|
|
123
111
|
const localIgnoresData = ignoresData.replace(/^[\/\\]/g, localRoot(folder) + '/');
|
|
124
|
-
|
|
112
|
+
ignored.add(localIgnoresData);
|
|
113
|
+
files = filterOutIgnored(files, ignored);
|
|
125
114
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const [_line, path] of vendorMatches) {
|
|
143
|
-
gitignores.add(localFilePath(path));
|
|
144
|
-
}
|
|
145
|
-
// Custom file associations
|
|
146
|
-
const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
|
|
147
|
-
for (let [_line, path, forcedLang] of customLangMatches) {
|
|
148
|
-
// If specified language is an alias, associate it with its full name
|
|
149
|
-
if (!langData[forcedLang]) {
|
|
150
|
-
const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
|
|
151
|
-
if (overrideLang) {
|
|
152
|
-
forcedLang = overrideLang[0];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const fullPath = path_1.default.join(relPath(folder), path);
|
|
156
|
-
overrides[fullPath] = forcedLang;
|
|
157
|
-
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Fetch and normalise gitattributes data of all subfolders and save to metadata
|
|
118
|
+
const manualAttributes = {}; // Maps file globs to gitattribute boolean flags
|
|
119
|
+
const getFlaggedGlobs = (attr, val) => {
|
|
120
|
+
return Object.entries(manualAttributes).filter(([, attrs]) => attrs[attr] === val).map(([glob,]) => glob);
|
|
121
|
+
};
|
|
122
|
+
if (!useRawContent && opts.checkAttributes) {
|
|
123
|
+
const nestedAttrFiles = files.filter(file => file.endsWith('.gitattributes'));
|
|
124
|
+
for (const attrFile of nestedAttrFiles) {
|
|
125
|
+
const relAttrFile = relPath(attrFile);
|
|
126
|
+
const relAttrFolder = path_1.default.dirname(relAttrFile);
|
|
127
|
+
const contents = await (0, read_file_1.default)(attrFile);
|
|
128
|
+
const parsed = (0, parse_gitattributes_1.default)(contents, relAttrFolder);
|
|
129
|
+
for (const { glob, attrs } of parsed) {
|
|
130
|
+
manualAttributes[glob] = attrs;
|
|
158
131
|
}
|
|
159
132
|
}
|
|
160
133
|
}
|
|
161
|
-
//
|
|
134
|
+
// Apply vendor file path matches and filter out vendored files
|
|
162
135
|
if (!opts.keepVendored) {
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
136
|
+
// Get data of files that have been manually marked with metadata
|
|
137
|
+
const vendorTrueGlobs = [...getFlaggedGlobs('vendored', true), ...getFlaggedGlobs('generated', true), ...getFlaggedGlobs('documentation', true)];
|
|
138
|
+
const vendorFalseGlobs = [...getFlaggedGlobs('vendored', false), ...getFlaggedGlobs('generated', false), ...getFlaggedGlobs('documentation', false)];
|
|
139
|
+
// Set up glob ignore object to use for expanding globs to match files
|
|
140
|
+
const vendorOverrides = (0, ignore_1.default)().add(vendorFalseGlobs);
|
|
141
|
+
// Remove all files marked as vendored by default
|
|
142
|
+
const excludedFiles = files.filter(file => vendorPaths.some(pathPtn => RegExp(pathPtn, 'i').test(relPath(file))));
|
|
143
|
+
files = files.filter(file => !excludedFiles.includes(file));
|
|
144
|
+
// Re-add removed files that are overridden manually in gitattributes
|
|
145
|
+
const overriddenExcludedFiles = excludedFiles.filter(file => vendorOverrides.ignores(relPath(file)));
|
|
146
|
+
files.push(...overriddenExcludedFiles);
|
|
147
|
+
// Remove files explicitly marked as vendored in gitattributes
|
|
148
|
+
// TODO change globs.includes(file) to parse the glob using ignore()
|
|
149
|
+
files = files.filter(file => !vendorTrueGlobs.includes(relPath(file)));
|
|
150
|
+
}
|
|
151
|
+
// Filter out binary files
|
|
152
|
+
if (!opts.keepBinary) {
|
|
153
|
+
// Filter out files that are binary by default
|
|
154
|
+
files = files.filter(file => !binary_extensions_1.default.some(ext => file.endsWith('.' + ext)));
|
|
155
|
+
// Filter out manually specified binary files
|
|
156
|
+
const binaryIgnored = (0, ignore_1.default)().add(getFlaggedGlobs('binary', true));
|
|
157
|
+
files = filterOutIgnored(files, binaryIgnored);
|
|
158
|
+
// Re-add files manually marked not as binary
|
|
159
|
+
const binaryUnignored = (0, ignore_1.default)().add(getFlaggedGlobs('binary', false));
|
|
160
|
+
// TODO parse the globs using ignore()
|
|
161
|
+
const unignoredList = filterOutIgnored(files, binaryUnignored);
|
|
162
|
+
files.push(...unignoredList);
|
|
163
|
+
}
|
|
164
|
+
// Ignore specific languages
|
|
165
|
+
for (const lang of (_c = opts.ignoredLanguages) !== null && _c !== void 0 ? _c : []) {
|
|
166
|
+
for (const key in langData) {
|
|
167
|
+
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
168
|
+
delete langData[key];
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
167
171
|
}
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
}
|
|
173
|
+
// Establish language overrides taken from gitattributes
|
|
174
|
+
const forcedLangs = Object.entries(manualAttributes).filter(([, attrs]) => attrs.language);
|
|
175
|
+
for (const [globPath, attrs] of forcedLangs) {
|
|
176
|
+
let forcedLang = attrs.language;
|
|
177
|
+
if (!forcedLang)
|
|
178
|
+
continue;
|
|
179
|
+
// If specified language is an alias, associate it with its full name
|
|
180
|
+
if (!langData[forcedLang]) {
|
|
181
|
+
const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
|
|
182
|
+
if (overrideLang) {
|
|
183
|
+
forcedLang = overrideLang[0];
|
|
184
|
+
}
|
|
170
185
|
}
|
|
186
|
+
globOverrides[globPath] = forcedLang;
|
|
171
187
|
}
|
|
172
|
-
|
|
188
|
+
//*PARSE LANGUAGES*//
|
|
173
189
|
const addResult = (file, result) => {
|
|
174
190
|
if (!fileAssociations[file]) {
|
|
175
191
|
fileAssociations[file] = [];
|
|
@@ -177,26 +193,36 @@ async function analyse(input, opts = {}) {
|
|
|
177
193
|
}
|
|
178
194
|
// Set parent to result group if it is present
|
|
179
195
|
// Is nullish if either `opts.childLanguages` is set or if there is no group
|
|
180
|
-
const finalResult = !opts.childLanguages && result && langData[result].group || result;
|
|
181
|
-
if (!fileAssociations[file].includes(finalResult))
|
|
196
|
+
const finalResult = !opts.childLanguages && result && langData[result] && langData[result].group || result;
|
|
197
|
+
if (!fileAssociations[file].includes(finalResult)) {
|
|
182
198
|
fileAssociations[file].push(finalResult);
|
|
199
|
+
}
|
|
183
200
|
extensions[file] = path_1.default.extname(file).toLowerCase();
|
|
184
201
|
};
|
|
185
|
-
const overridesArray = Object.entries(overrides);
|
|
186
|
-
// List all languages that could be associated with a given file
|
|
187
202
|
const definiteness = {};
|
|
188
203
|
const fromShebang = {};
|
|
189
|
-
for (const file of files) {
|
|
204
|
+
fileLoop: for (const file of files) {
|
|
205
|
+
// Check manual override
|
|
206
|
+
for (const globMatch in globOverrides) {
|
|
207
|
+
if (!fileMatchesGlobs(file, globMatch))
|
|
208
|
+
continue;
|
|
209
|
+
// If the given file matches the glob, apply the override to the file
|
|
210
|
+
const forcedLang = globOverrides[globMatch];
|
|
211
|
+
addResult(file, forcedLang);
|
|
212
|
+
definiteness[file] = true;
|
|
213
|
+
continue fileLoop; // no need to check other heuristics, the classified language has been found
|
|
214
|
+
}
|
|
215
|
+
// Check first line for readability
|
|
190
216
|
let firstLine;
|
|
191
217
|
if (useRawContent) {
|
|
192
|
-
firstLine = (_e = (_d =
|
|
218
|
+
firstLine = (_e = (_d = manualFileContent[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
|
|
193
219
|
}
|
|
194
220
|
else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
|
|
195
221
|
firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
|
|
196
222
|
}
|
|
197
223
|
else
|
|
198
224
|
continue;
|
|
199
|
-
// Skip if file is unreadable
|
|
225
|
+
// Skip if file is unreadable or blank
|
|
200
226
|
if (firstLine === null)
|
|
201
227
|
continue;
|
|
202
228
|
// Check first line for explicit classification
|
|
@@ -231,17 +257,6 @@ async function analyse(input, opts = {}) {
|
|
|
231
257
|
continue;
|
|
232
258
|
}
|
|
233
259
|
}
|
|
234
|
-
// Check override for manual language classification
|
|
235
|
-
if (!useRawContent && !opts.quick && opts.checkAttributes) {
|
|
236
|
-
const isOverridden = (path) => (0, ignore_1.default)().add(path).ignores(relPath(file));
|
|
237
|
-
const match = overridesArray.find(item => isOverridden(item[0]));
|
|
238
|
-
if (match) {
|
|
239
|
-
const forcedLang = match[1];
|
|
240
|
-
addResult(file, forcedLang);
|
|
241
|
-
definiteness[file] = true;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
260
|
// Search each language
|
|
246
261
|
let skipExts = false;
|
|
247
262
|
// Check if filename is a match
|
|
@@ -286,12 +301,8 @@ async function analyse(input, opts = {}) {
|
|
|
286
301
|
}
|
|
287
302
|
// Skip binary files
|
|
288
303
|
if (!useRawContent && !opts.keepBinary) {
|
|
289
|
-
|
|
290
|
-
const isCustomBinary = customBinary.ignores(relPath(file));
|
|
291
|
-
const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
|
|
292
|
-
if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
|
|
304
|
+
if (await (0, isbinaryfile_1.isBinaryFile)(file))
|
|
293
305
|
continue;
|
|
294
|
-
}
|
|
295
306
|
}
|
|
296
307
|
// Parse heuristics if applicable
|
|
297
308
|
if (opts.checkHeuristics)
|
|
@@ -327,7 +338,7 @@ async function analyse(input, opts = {}) {
|
|
|
327
338
|
}
|
|
328
339
|
}
|
|
329
340
|
// Check file contents and apply heuristic patterns
|
|
330
|
-
const fileContent =
|
|
341
|
+
const fileContent = opts.fileContent ? manualFileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
|
|
331
342
|
// Skip if file read errors
|
|
332
343
|
if (fileContent === null)
|
|
333
344
|
continue;
|
|
@@ -351,7 +362,7 @@ async function analyse(input, opts = {}) {
|
|
|
351
362
|
}
|
|
352
363
|
}
|
|
353
364
|
// Skip specified categories
|
|
354
|
-
if ((
|
|
365
|
+
if ((_l = opts.categories) === null || _l === void 0 ? void 0 : _l.length) {
|
|
355
366
|
const categories = ['data', 'markup', 'programming', 'prose'];
|
|
356
367
|
const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
|
|
357
368
|
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
@@ -387,21 +398,21 @@ async function analyse(input, opts = {}) {
|
|
|
387
398
|
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
388
399
|
if (lang && !langData[lang])
|
|
389
400
|
continue;
|
|
390
|
-
const fileSize = (
|
|
401
|
+
const fileSize = (_o = (_m = manualFileContent[files.indexOf(file)]) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : fs_1.default.statSync(file).size;
|
|
391
402
|
results.files.bytes += fileSize;
|
|
392
403
|
// If no language found, add extension in other section
|
|
393
404
|
if (!lang) {
|
|
394
405
|
const ext = path_1.default.extname(file);
|
|
395
406
|
const unknownType = ext === '' ? 'filenames' : 'extensions';
|
|
396
407
|
const name = ext === '' ? path_1.default.basename(file) : ext;
|
|
397
|
-
(
|
|
408
|
+
(_p = (_r = results.unknown[unknownType])[name]) !== null && _p !== void 0 ? _p : (_r[name] = 0);
|
|
398
409
|
results.unknown[unknownType][name] += fileSize;
|
|
399
410
|
results.unknown.bytes += fileSize;
|
|
400
411
|
continue;
|
|
401
412
|
}
|
|
402
413
|
// Add language and bytes data to corresponding section
|
|
403
414
|
const { type } = langData[lang];
|
|
404
|
-
(
|
|
415
|
+
(_q = (_s = results.languages.results)[lang]) !== null && _q !== void 0 ? _q : (_s[lang] = { type, bytes: 0, color: langData[lang].color });
|
|
405
416
|
if (opts.childLanguages) {
|
|
406
417
|
results.languages.results[lang].parent = langData[lang].group;
|
|
407
418
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export type Category = 'data' | 'markup' | 'programming' | 'prose';
|
|
|
4
4
|
export type FilePath = string;
|
|
5
5
|
export type Bytes = Integer;
|
|
6
6
|
export type Integer = number;
|
|
7
|
+
export type RelFile = string & {};
|
|
8
|
+
export type AbsFile = string & {};
|
|
9
|
+
export type AbsFolder = string & {};
|
|
10
|
+
export type FileGlob = string & {};
|
|
7
11
|
export interface Options {
|
|
8
12
|
fileContent?: string | string[];
|
|
9
13
|
ignoredFiles?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linguist-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0-pre",
|
|
4
4
|
"description": "Analyse languages used in a folder. Powered by GitHub Linguist, although it doesn't need to be installed.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"commander": "^9.5.0 <10",
|
|
43
43
|
"common-path-prefix": "^3.0.0",
|
|
44
44
|
"cross-fetch": "^3.1.8 <4",
|
|
45
|
-
"ignore": "^5.
|
|
45
|
+
"ignore": "^5.3.1",
|
|
46
46
|
"isbinaryfile": "^4.0.10 <5",
|
|
47
47
|
"js-yaml": "^4.1.0",
|
|
48
48
|
"node-cache": "^5.1.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/js-yaml": "^4.0.
|
|
51
|
+
"@types/js-yaml": "^4.0.9",
|
|
52
52
|
"@types/node": "ts5.0",
|
|
53
53
|
"deep-object-diff": "^1.1.9",
|
|
54
54
|
"typescript": "~5.0.4 <5.1"
|
package/readme.md
CHANGED
|
@@ -122,7 +122,7 @@ const { files, languages, unknown } = linguist(folder, options);
|
|
|
122
122
|
Alias for `checkAttributes:false, checkIgnored:false, checkHeuristics:false, checkShebang:false, checkModeline:false`.
|
|
123
123
|
- `offline` (boolean):
|
|
124
124
|
Whether to use pre-packaged metadata files instead of fetching them from GitHub at runtime (defaults to `false`).
|
|
125
|
-
|
|
125
|
+
- `keepVendored` (boolean):
|
|
126
126
|
Whether to keep vendored files (dependencies, etc) (defaults to `false`).
|
|
127
127
|
Does nothing when `fileContent` is set.
|
|
128
128
|
- `keepBinary` (boolean):
|
|
@@ -160,14 +160,20 @@ linguist --version
|
|
|
160
160
|
A list of languages to exclude from the output.
|
|
161
161
|
- `--categories <categories...>`:
|
|
162
162
|
A list of language categories that should be displayed in the output.
|
|
163
|
-
|
|
163
|
+
Must be one or more of `data`, `prose`, `programming`, `markup`.
|
|
164
164
|
- `--childLanguages`:
|
|
165
165
|
Display sub-languages instead of their parents, when possible.
|
|
166
166
|
- `--json`:
|
|
167
|
+
Only affects the CLI output.
|
|
167
168
|
Display the outputted language data as JSON.
|
|
168
169
|
- `--tree <traversal>`:
|
|
170
|
+
Only affects the CLI output.
|
|
169
171
|
A dot-delimited traversal to the nested object that should be logged to the console instead of the entire output.
|
|
170
172
|
Requires `--json` to be specified.
|
|
173
|
+
- `--listFiles`:
|
|
174
|
+
Only affects the visual CLI output.
|
|
175
|
+
List each matching file and its size under each outputted language result.
|
|
176
|
+
Does nothing if `--json` is specified.
|
|
171
177
|
- `--quick`:
|
|
172
178
|
Skip the checking of `.gitattributes` and `.gitignore` files for manual language classifications.
|
|
173
179
|
Alias for `--checkAttributes=false --checkIgnored=false --checkHeuristics=false --checkShebang=false --checkModeline=false`.
|