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 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 / (totalBytes || 1) * 100).toFixed(2).padStart(5, ' '),
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
- gitignores: Ignore;
12
- regexIgnores: RegExp[];
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, gitignores, regexIgnores } = data;
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 or ignored
37
+ // Skip if nonexistant
38
38
  const nonExistant = !fs_1.default.existsSync(path);
39
- const isGitIgnored = gitignores.test(localPath).ignored;
40
- const isRegexIgnored = regexIgnores.find(match => localPath.match(match));
41
- if (nonExistant || isGitIgnored || isRegexIgnored)
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: commonRoot, folderRoots, folders: [path], gitignores, regexIgnores });
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: commonRoot, folderRoots: [folderRoots[i]], folders: [folders[i]], gitignores, regexIgnores });
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(input, opts = {}) {
40
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
41
- var _t, _u;
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 = [input !== null && input !== void 0 ? input : []].flat();
44
- opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
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 overrides = {};
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
- const localPath = (file) => localRoot(unRelPath(file));
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, folders;
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, gitignores, regexIgnores });
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
- // Apply aliases
91
- opts = {
92
- checkIgnored: !opts.quick,
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
- gitignores.add(localIgnoresData);
112
+ ignored.add(localIgnoresData);
113
+ files = filterOutIgnored(files, ignored);
125
114
  }
126
- // Parse gitattributes
127
- const attributesFile = path_1.default.join(folder, '.gitattributes');
128
- if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
129
- const attributesData = await (0, read_file_1.default)(attributesFile);
130
- // Explicit text/binary associations
131
- const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
132
- for (const [_line, path, type] of contentTypeMatches) {
133
- if (['text', '-binary'].includes(type)) {
134
- customText.add(localFilePath(path));
135
- }
136
- if (['-text', 'binary'].includes(type)) {
137
- customBinary.add(localFilePath(path));
138
- }
139
- }
140
- // Custom vendor options
141
- const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
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
- // Check vendored files
134
+ // Apply vendor file path matches and filter out vendored files
162
135
  if (!opts.keepVendored) {
163
- // Filter out any files that match a vendor file path
164
- if (useRawContent) {
165
- files = gitignores.filter(files);
166
- files = files.filter(file => !regexIgnores.find(match => match.test(file)));
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
- else {
169
- files = gitignores.filter(files.map(localPath)).map(unRelPath);
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
- // Load all files and parse languages
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 = (_c = opts.fileContent) === null || _c === void 0 ? void 0 : _c[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
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
- const isCustomText = customText.ignores(relPath(file));
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 = ((_l = opts.fileContent) === null || _l === void 0 ? void 0 : _l.length) ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
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 ((_m = opts.categories) === null || _m === void 0 ? void 0 : _m.length) {
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 = (_q = (_p = (_o = opts.fileContent) === null || _o === void 0 ? void 0 : _o[files.indexOf(file)]) === null || _p === void 0 ? void 0 : _p.length) !== null && _q !== void 0 ? _q : fs_1.default.statSync(file).size;
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
- (_r = (_t = results.unknown[unknownType])[name]) !== null && _r !== void 0 ? _r : (_t[name] = 0);
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
- (_s = (_u = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_u[lang] = { type, bytes: 0, color: langData[lang].color });
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.6.1",
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.2.4",
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.5",
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
- - `keepVendored` (boolean):
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
- Must be one or more of `data`, `prose`, `programming`, `markup`.
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`.