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.
Files changed (44) hide show
  1. package/dist/cli.js +5 -2
  2. package/dist/helpers/convert-pcre.js +1 -1
  3. package/dist/helpers/load-data.d.ts +1 -1
  4. package/dist/helpers/load-data.js +4 -5
  5. package/dist/helpers/parse-gitattributes.d.ts +1 -0
  6. package/dist/helpers/parse-gitattributes.js +6 -4
  7. package/dist/helpers/parse-gitignore.js +1 -1
  8. package/dist/helpers/read-file.d.ts +1 -1
  9. package/dist/helpers/read-file.js +2 -2
  10. package/dist/helpers/walk-tree.js +7 -2
  11. package/dist/index.js +73 -32
  12. package/dist/package.json +56 -0
  13. package/dist/src/cli.d.ts +1 -0
  14. package/dist/src/cli.js +146 -0
  15. package/dist/src/helpers/convert-pcre.d.ts +2 -0
  16. package/dist/src/helpers/convert-pcre.js +38 -0
  17. package/dist/src/helpers/load-data.d.ts +4 -0
  18. package/dist/src/helpers/load-data.js +37 -0
  19. package/dist/src/helpers/norm-path.d.ts +2 -0
  20. package/dist/src/helpers/norm-path.js +15 -0
  21. package/dist/src/helpers/parse-gitattributes.d.ts +17 -0
  22. package/dist/src/helpers/parse-gitattributes.js +37 -0
  23. package/dist/src/helpers/parse-gitignore.d.ts +1 -0
  24. package/dist/src/helpers/parse-gitignore.js +12 -0
  25. package/dist/src/helpers/read-file.d.ts +5 -0
  26. package/dist/src/helpers/read-file.js +23 -0
  27. package/dist/src/helpers/walk-tree.d.ts +20 -0
  28. package/dist/src/helpers/walk-tree.js +85 -0
  29. package/dist/src/index.d.ts +4 -0
  30. package/dist/src/index.js +485 -0
  31. package/dist/src/schema.d.ts +37 -0
  32. package/dist/src/schema.js +2 -0
  33. package/dist/src/types.d.ts +74 -0
  34. package/dist/src/types.js +2 -0
  35. package/dist/types.d.ts +22 -0
  36. package/dist/version.d.ts +2 -0
  37. package/dist/version.js +4 -0
  38. package/ext/documentation.yml +0 -3
  39. package/ext/generated.rb +5 -0
  40. package/ext/heuristics.yml +91 -16
  41. package/ext/languages.yml +317 -8
  42. package/ext/vendor.yml +0 -2
  43. package/package.json +6 -5
  44. 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): Promise<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 = void 0;
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
- async function parseGeneratedDataFile(fileContent) {
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
- async function loadFile(file, offline = false) {
35
+ function loadFile(file, offline = false) {
36
36
  return offline ? loadLocalFile(file) : loadWebFile(file);
37
37
  }
38
- exports.default = loadFile;
@@ -3,6 +3,7 @@ export type FlagAttributes = {
3
3
  'vendored': boolean | null;
4
4
  'generated': boolean | null;
5
5
  'documentation': boolean | null;
6
+ 'detectable': boolean | null;
6
7
  'binary': boolean | null;
7
8
  'language': T.LanguageResult;
8
9
  };
@@ -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': hasTrueParts('linguist-generated') ? true : hasFalseParts('linguist-generated') ? false : null,
26
- 'vendored': hasTrueParts('linguist-vendored') ? true : hasFalseParts('linguist-vendored') ? false : null,
27
- 'documentation': hasTrueParts('linguist-documentation') ? true : hasFalseParts('linguist-documentation') ? false : null,
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 readFile(filename: string, onlyFirstLine?: boolean): Promise<string>;
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 readFile(filename, onlyFirstLine = false) {
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
- ignored.add(ignoredPaths);
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 _r, _s;
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((_b = opts.ignoredFiles) !== null && _b !== void 0 ? _b : []);
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 (_c = opts.ignoredLanguages) !== null && _c !== void 0 ? _c : []) {
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 = (_e = (_d = manualFileContent[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
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 = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
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 = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
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 = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
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 = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
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 = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
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 ((_l = opts.categories) === null || _l === void 0 ? void 0 : _l.length) {
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
- if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
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
- 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;
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
- // If no language found, add extension in other section
418
- if (!lang) {
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 === '' ? 'filenames' : 'extensions';
421
- const name = ext === '' ? path_1.default.basename(file) : ext;
422
- (_p = (_r = results.unknown[unknownType])[name]) !== null && _p !== void 0 ? _p : (_r[name] = 0);
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
- continue;
468
+ results.unknown.lines.total += loc.total;
469
+ results.unknown.lines.content += loc.content;
470
+ results.unknown.lines.code += loc.code;
426
471
  }
427
- // Add language and bytes data to corresponding section
428
- const { type } = langData[lang];
429
- (_q = (_s = results.languages.results)[lang]) !== null && _q !== void 0 ? _q : (_s[lang] = { type, bytes: 0, color: langData[lang].color });
430
- if (opts.childLanguages) {
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 {};
@@ -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,2 @@
1
+ /** Convert a PCRE regex into JS. */
2
+ export default function pcre(regex: string): RegExp;
@@ -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
+ }