linguist-js 2.1.2 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const VERSION = require('../package.json').version;
7
7
  const commander_1 = require("commander");
8
8
  const index_1 = __importDefault(require("./index"));
9
+ const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
9
10
  const colouredMsg = ([r, g, b], msg) => `\u001B[${38};2;${r};${g};${b}m${msg}${'\u001b[0m'}`;
10
11
  const hexToRgb = (hex) => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
11
12
  commander_1.program
@@ -21,6 +22,7 @@ commander_1.program
21
22
  .option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
22
23
  .option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
23
24
  .option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
25
+ .option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
24
26
  .option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
25
27
  .option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
26
28
  .option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
@@ -49,17 +51,23 @@ if (args.analyze)
49
51
  (async () => {
50
52
  // Fetch language data
51
53
  const root = args.analyze === true ? '.' : args.analyze;
52
- const { files, languages, unknown } = await (0, index_1.default)(root, args);
54
+ const data = await (0, index_1.default)(root, args);
55
+ const { files, languages, unknown } = data;
56
+ // Get file count
57
+ let totalFiles = (0, walk_tree_1.default)(root).files.length;
53
58
  // Print output
54
59
  if (!args.json) {
55
60
  const sortedEntries = Object.entries(languages.results).sort((a, b) => a[1].bytes < b[1].bytes ? +1 : -1);
56
61
  const totalBytes = languages.bytes;
57
- console.log(`\n Analysed ${files.bytes.toLocaleString()} B from ${files.count} files with linguist-js`);
62
+ console.log(`\n Analysed ${files.bytes.toLocaleString()} B from ${totalFiles} files (${totalFiles - files.count} ignored) with linguist-js`);
58
63
  console.log(`\n Language analysis results:`);
59
- let i = 0;
64
+ let count = 0;
65
+ if (sortedEntries.length === 0)
66
+ console.log(` None`);
67
+ // List parsed results
60
68
  for (const [lang, { bytes, color }] of sortedEntries) {
61
69
  const fmtd = {
62
- index: (++i).toString().padStart(2, ' '),
70
+ index: (++count).toString().padStart(2, ' '),
63
71
  lang: lang.padEnd(24, ' '),
64
72
  percent: (bytes / (totalBytes || 1) * 100).toFixed(2).padStart(5, ' '),
65
73
  bytes: bytes.toLocaleString().padStart(10, ' '),
@@ -68,35 +76,33 @@ if (args.analyze)
68
76
  console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B`);
69
77
  }
70
78
  console.log(` Total: ${totalBytes.toLocaleString()} B`);
79
+ // List unknown files/extensions
71
80
  if (unknown.bytes > 0) {
72
81
  console.log(`\n Unknown files and extensions:`);
73
82
  for (const [name, bytes] of Object.entries(unknown.filenames)) {
74
83
  console.log(` '${name}': ${bytes.toLocaleString()} B`);
75
84
  }
76
85
  for (const [ext, bytes] of Object.entries(unknown.extensions)) {
77
- console.log(` '${ext}': ${bytes.toLocaleString()} B`);
86
+ console.log(` '*${ext}': ${bytes.toLocaleString()} B`);
78
87
  }
79
88
  console.log(` Total: ${unknown.bytes.toLocaleString()} B`);
80
89
  }
81
90
  }
82
- else {
83
- const data = { files, languages, unknown };
84
- if (args.tree) {
85
- const treeParts = args.tree.split('.');
86
- let nestedData = data;
87
- for (const part of treeParts) {
88
- if (!nestedData[part])
89
- throw Error(`TraversalError: Key '${part}' cannot be found on output object.`);
90
- nestedData = nestedData[part];
91
- }
92
- console.log(nestedData);
93
- }
94
- else {
95
- console.log(JSON.stringify(data, null, 2).replace(/{\s+"type".+?}/sg, obj => obj.replace(/\n\s+/g, ' ')));
91
+ else if (args.tree) {
92
+ const treeParts = args.tree.split('.');
93
+ let nestedData = data;
94
+ for (const part of treeParts) {
95
+ if (!nestedData[part])
96
+ throw Error(`TraversalError: Key '${part}' cannot be found on output object.`);
97
+ nestedData = nestedData[part];
96
98
  }
99
+ console.log(nestedData);
100
+ }
101
+ else {
102
+ console.dir(data, { depth: null });
97
103
  }
98
104
  })();
99
105
  else {
100
- console.log(`Welcome to linguist-js, the JavaScript port of GitHub's language analyzer.`);
106
+ console.log(`Welcome to linguist-js, a JavaScript port of GitHub's language analyzer.`);
101
107
  console.log(`Type 'linguist --help' for a list of commands.`);
102
108
  }
@@ -0,0 +1,11 @@
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 glob_to_regexp_1 = __importDefault(require("glob-to-regexp"));
7
+ function parseGitGlob(path) {
8
+ const globPath = `**/${path}`.replace(/\\/g, '/').replace(/\[:(space|digit):\]/g, (_, val) => `\\${val[0]}`);
9
+ return (0, glob_to_regexp_1.default)(globPath, { globstar: true, extended: true });
10
+ }
11
+ exports.default = parseGitGlob;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /** Convert a PCRE regex into JS. */
4
4
  function pcre(regex) {
5
5
  let finalRegex = regex;
6
- let finalFlags = new Set();
6
+ const finalFlags = new Set();
7
7
  // Convert inline flag declarations
8
8
  const inlineMatches = regex.matchAll(/\?([a-z]):/g);
9
9
  const startMatches = regex.matchAll(/\(\?([a-z]+)\)/g);
@@ -4,12 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
- /** Read part of a file on disc. */
7
+ /**
8
+ * Read part of a file on disc.
9
+ * @throws 'EPERM' if the file is not readable.
10
+ */
8
11
  async function readFile(filename, onlyFirstLine = false) {
9
12
  const chunkSize = 100;
10
13
  const stream = fs_1.default.createReadStream(filename, { highWaterMark: chunkSize });
11
14
  let content = '';
12
- for await (const data of stream) {
15
+ for await (const data of stream) { // may throw
13
16
  content += data.toString();
14
17
  if (onlyFirstLine && content.includes('\n')) {
15
18
  return content.split(/\r?\n/)[0];
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
12
12
  const load_data_1 = __importDefault(require("./helpers/load-data"));
13
13
  const read_file_1 = __importDefault(require("./helpers/read-file"));
14
14
  const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
15
- const convertToRegex = (path) => (0, glob_to_regexp_1.default)('**/' + path, { globstar: true, extended: true });
15
+ const convert_glob_1 = __importDefault(require("./helpers/convert-glob"));
16
16
  async function analyse(input, opts = {}) {
17
17
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
18
18
  var _k, _l, _m;
@@ -31,7 +31,7 @@ async function analyse(input, opts = {}) {
31
31
  };
32
32
  const ignoredFiles = [
33
33
  /\/\.git\//,
34
- opts.keepVendored ? [] : vendorData.map(path => (0, convert_pcre_1.default)(path)),
34
+ opts.keepVendored ? [] : vendorData.map(path => RegExp(path)),
35
35
  (_b = (_a = opts.ignoredFiles) === null || _a === void 0 ? void 0 : _a.map(path => (0, glob_to_regexp_1.default)('*' + path + '*', { extended: true }))) !== null && _b !== void 0 ? _b : [],
36
36
  ].flat();
37
37
  let { files, folders } = (0, walk_tree_1.default)(input !== null && input !== void 0 ? input : '.', ignoredFiles);
@@ -48,27 +48,38 @@ async function analyse(input, opts = {}) {
48
48
  }
49
49
  // Load gitattributes
50
50
  const customIgnored = [];
51
+ const customBinary = [];
52
+ const customText = [];
51
53
  if (!opts.quick) {
52
54
  for (const folder of folders) {
53
55
  // Skip if folder is marked in gitattributes
54
- if (customIgnored.some(path => (0, convert_pcre_1.default)(path).test(folder)))
56
+ if (customIgnored.some(path => (0, convert_glob_1.default)(path).test(folder)))
55
57
  continue;
56
58
  // Parse gitignores
57
59
  const ignoresFile = path_1.default.join(folder, '.gitignore');
58
60
  if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
59
61
  const ignoresData = await (0, read_file_1.default)(ignoresFile);
60
62
  const ignoresList = ignoresData.split(/\r?\n/).filter(line => line.trim() && !line.startsWith('#'));
61
- const ignoredPaths = ignoresList.map(path => convertToRegex(path).source);
62
- customIgnored.push(...ignoredPaths.map(file => file.replace(folder, '')));
63
+ const ignoredPaths = ignoresList.map(path => (0, convert_glob_1.default)(path).source);
64
+ customIgnored.push(...ignoredPaths);
63
65
  }
64
66
  // Parse gitattributes
65
67
  const attributesFile = path_1.default.join(folder, '.gitattributes');
66
68
  if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
67
69
  const attributesData = await (0, read_file_1.default)(attributesFile);
70
+ const relPathToRegex = (path) => (0, convert_glob_1.default)(path).source.substr(1).replace(folder, '');
71
+ // Explicit text/binary associations
72
+ const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
73
+ for (const [_line, path, type] of contentTypeMatches) {
74
+ if (['text', '-binary'].includes(type))
75
+ customText.push(relPathToRegex(path));
76
+ if (['-text', 'binary'].includes(type))
77
+ customBinary.push(relPathToRegex(path));
78
+ }
68
79
  // Custom vendor options
69
80
  const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
70
81
  for (const [_line, path] of vendorMatches) {
71
- customIgnored.push(convertToRegex(path).source.substr(1).replace(folder, ''));
82
+ customIgnored.push(relPathToRegex(path));
72
83
  }
73
84
  // Custom file associations
74
85
  const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
@@ -79,7 +90,7 @@ async function analyse(input, opts = {}) {
79
90
  if (overrideLang)
80
91
  forcedLang = overrideLang[0];
81
92
  }
82
- const fullPath = folder + convertToRegex(path).source.substr(1);
93
+ const fullPath = folder + (0, convert_glob_1.default)(path).source.substr(1);
83
94
  overrides[fullPath] = forcedLang;
84
95
  }
85
96
  }
@@ -88,7 +99,7 @@ async function analyse(input, opts = {}) {
88
99
  // Check vendored files
89
100
  if (!opts.keepVendored) {
90
101
  // Filter out any files that match a vendor file path
91
- const matcher = (match) => (0, convert_pcre_1.default)(match.replace(/\/$/, '/.+$').replace(/^\.\//, ''));
102
+ const matcher = (match) => RegExp(match.replace(/\/$/, '/.+$').replace(/^\.\//, ''));
92
103
  files = files.filter(file => !customIgnored.some(pattern => matcher(pattern).test(file)));
93
104
  }
94
105
  // Load all files and parse languages
@@ -106,16 +117,18 @@ async function analyse(input, opts = {}) {
106
117
  for (const file of files) {
107
118
  if (!fs_1.default.existsSync(file) || fs_1.default.lstatSync(file).isDirectory())
108
119
  continue;
120
+ const firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
121
+ // Skip if file is unreadable
122
+ if (firstLine === null)
123
+ continue;
109
124
  // Check shebang line for explicit classification
110
- if (!opts.quick && opts.checkShebang) {
111
- const firstLine = await (0, read_file_1.default)(file, true);
112
- if (firstLine.startsWith('#!')) {
113
- const matches = Object.entries(langData).filter(([, data]) => { var _a; return (_a = data.interpreters) === null || _a === void 0 ? void 0 : _a.some(interpreter => firstLine.match('\\b' + interpreter + '\\b')); });
114
- if (matches.length) {
115
- const forcedLang = matches[0][0];
116
- addResult(file, forcedLang);
117
- continue;
118
- }
125
+ if (!opts.quick && opts.checkShebang && firstLine.startsWith('#!')) {
126
+ // Find matching interpreters
127
+ const matches = Object.entries(langData).filter(([, data]) => { var _a; return (_a = data.interpreters) === null || _a === void 0 ? void 0 : _a.some(interpreter => firstLine.match('\\b' + interpreter + '\\b')); });
128
+ if (matches.length) {
129
+ // Add explicitly-identified language
130
+ const forcedLang = matches[0][0];
131
+ addResult(file, forcedLang);
119
132
  }
120
133
  }
121
134
  // Check override for manual language classification
@@ -151,8 +164,13 @@ async function analyse(input, opts = {}) {
151
164
  // Narrow down file associations to the best fit
152
165
  for (const file in fileAssociations) {
153
166
  // Skip binary files
154
- if (!opts.keepBinary && (binary_extensions_1.default.some(ext => file.endsWith('.' + ext)) || await (0, isbinaryfile_1.isBinaryFile)(file))) {
155
- continue;
167
+ if (!opts.keepBinary) {
168
+ const isCustomText = customText.some(path => RegExp(path).test(file));
169
+ const isCustomBinary = customBinary.some(path => RegExp(path).test(file));
170
+ const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
171
+ if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
172
+ continue;
173
+ }
156
174
  }
157
175
  // Parse heuristics if applicable
158
176
  if (opts.checkHeuristics)
@@ -180,7 +198,9 @@ async function analyse(input, opts = {}) {
180
198
  if (heuristic.named_pattern)
181
199
  normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
182
200
  // Check file contents and apply heuristic patterns
183
- const fileContent = await (0, read_file_1.default)(file);
201
+ const fileContent = await (0, read_file_1.default)(file).catch(() => null);
202
+ if (fileContent === null)
203
+ continue;
184
204
  if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
185
205
  results.files.results[file] = heuristic.language;
186
206
  break;
@@ -208,6 +228,17 @@ async function analyse(input, opts = {}) {
208
228
  }
209
229
  }
210
230
  }
231
+ // Convert paths to relative
232
+ if (opts.relativePaths) {
233
+ const newMap = {};
234
+ for (const [file, lang] of Object.entries(results.files.results)) {
235
+ let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
236
+ if (!relPath.startsWith('../'))
237
+ relPath = './' + relPath;
238
+ newMap[relPath] = lang;
239
+ }
240
+ results.files.results = newMap;
241
+ }
211
242
  // Load language bytes size
212
243
  for (const [file, lang] of Object.entries(results.files.results)) {
213
244
  if (lang && !langData[lang])
package/dist/types.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface Options {
10
10
  categories?: Category[];
11
11
  keepVendored?: boolean;
12
12
  keepBinary?: boolean;
13
+ relativePaths?: boolean;
13
14
  childLanguages?: boolean;
14
15
  quick?: boolean;
15
16
  checkIgnored?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linguist-js",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
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": {
@@ -10,7 +10,6 @@
10
10
  "node": ">=12"
11
11
  },
12
12
  "scripts": {
13
- "prepublish": "tsc",
14
13
  "prepare": "npm test && npm run perf",
15
14
  "perf": "tsc && node test/perf",
16
15
  "test": "tsc && node test/test"
@@ -38,9 +37,9 @@
38
37
  "homepage": "https://github.com/Nixinova/Linguist#readme",
39
38
  "dependencies": {
40
39
  "binary-extensions": "^2.2.0",
41
- "commander": "^8.2.0",
42
- "cross-fetch": "^3.1.4",
43
- "glob-to-regexp": "^0.4.1",
40
+ "commander": "^9.0.0",
41
+ "cross-fetch": "^3.1.5",
42
+ "glob-to-regexp": "~0.4.1",
44
43
  "isbinaryfile": "^4.0.8",
45
44
  "js-yaml": "^4.1.0",
46
45
  "node-cache": "^5.1.2"
@@ -49,7 +48,7 @@
49
48
  "@types/glob-to-regexp": "ts4.4",
50
49
  "@types/js-yaml": "ts4.4",
51
50
  "@types/node": "ts4.4",
52
- "deep-object-diff": "^1.1.0",
53
- "typescript": "~4.4.3"
51
+ "deep-object-diff": "^1.1.7",
52
+ "typescript": "~4.5.5"
54
53
  }
55
54
  }
package/readme.md CHANGED
@@ -10,6 +10,7 @@ Powered by [github-linguist](https://github.com/github/linguist), although it do
10
10
 
11
11
  ## Install
12
12
 
13
+ [Node.js](https://nodejs.org) must be installed to be able to use this.
13
14
  Linguist is available [on npm](https://npmjs.com/package/linguist-js) as `linguist-js`.
14
15
 
15
16
  Install locally using `npm install linguist-js` and import it into your code like so:
@@ -114,6 +115,8 @@ const { files, languages, unknown } = linguist(folder, options);
114
115
  Whether to keep vendored files (dependencies, etc) (defaults to `false`).
115
116
  - `keepBinary` (boolean):
116
117
  Whether binary files should be included in the output (defaults to `false`).
118
+ - `relativePaths` (boolean):
119
+ Change the absolute file paths in the output to be relative to the current working directory (defaults to `false`).
117
120
  - `checkAttributes` (boolean):
118
121
  Force the checking of `.gitattributes` files (defaults to `true` unless `quick` is set).
119
122
  - `checkIgnored` (boolean):
@@ -132,35 +135,37 @@ linguist --help
132
135
 
133
136
  - `--analyze`:
134
137
  Analyse the language of all files found in a folder.
135
- - `<folders...>` (optional):
138
+ - `<folders...>`:
136
139
  The folders to analyse (defaults to `./`).
137
- - `--ignoredFiles <paths...>` (optional):
140
+ - `--ignoredFiles <paths...>`:
138
141
  A list of space-delimited file path globs to ignore.
139
- - `--ignoredLanguages` (optional):
142
+ - `--ignoredLanguages`:
140
143
  A list of languages to ignore.
141
- - `--categories <categories...>` (optional):
144
+ - `--categories <categories...>`:
142
145
  A list of space-delimited categories that should be displayed in the output.
143
- - `--childLanguages` (optional):
146
+ - `--childLanguages`:
144
147
  Whether to display sub-languages instead of their parents, when possible.
145
- - `--json` (optional):
148
+ - `--json`:
146
149
  Display the outputted language data as JSON.
147
- - `--tree <traversal>` (optional):
150
+ - `--tree <traversal>`:
148
151
  A dot-delimited traversal to the nested object that should be logged to the console instead of the entire output.
149
152
  Requires `--json` to be specified.
150
- - `--quick` (optional):
153
+ - `--quick`:
151
154
  Whether to skip the checking of `.gitattributes` and `.gitignore` files for manual language classifications.
152
155
  Alias for `--checkAttributes=false --checkIgnored=false --checkHeuristics=false --checkShebang=false`.
153
- - `--keepVendored` (optional):
156
+ - `--keepVendored`:
154
157
  Whether to include vendored files (auto-generated files, dependencies folder, etc).
155
- - `--keepBinary` (optional):
158
+ - `--keepBinary`:
156
159
  Whether binary files should be excluded from the output.
157
- - `--checkAttributes` (optional):
160
+ - `--relativePaths`:
161
+ Change the absolute file paths in the output to be relative to the current working directory.
162
+ - `--checkAttributes`:
158
163
  Force the checking of `.gitatributes` files (use alongside `--quick` to overwrite).
159
- - `--checkIgnored` (optional):
164
+ - `--checkIgnored`:
160
165
  Force the checking of `.gitignore` files (use alongside `--quick` to overwrite).
161
- - `--checkHeuristics` (optional):
166
+ - `--checkHeuristics`:
162
167
  Apply heuristics to ambiguous languages (use alongside `--quick` to overwrite).
163
- - `--checkShebang` (optional):
168
+ - `--checkShebang`:
164
169
  Check shebang (`#!`) lines for explicit classification (use alongside `--quick` to overwrite).
165
170
  - `--help`:
166
171
  Display a help message.