linguist-js 2.4.0 → 2.5.0

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
@@ -19,6 +19,7 @@ commander_1.program
19
19
  .option('-j|--json [bool]', 'Display the output as JSON', false)
20
20
  .option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
21
21
  .option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
22
+ .option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', 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)
24
25
  .option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
@@ -3,11 +3,12 @@ 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
+ const promises_1 = __importDefault(require("fs/promises"));
7
+ const path_1 = __importDefault(require("path"));
6
8
  const cross_fetch_1 = __importDefault(require("cross-fetch"));
7
9
  const node_cache_1 = __importDefault(require("node-cache"));
8
10
  const cache = new node_cache_1.default({});
9
- /** Load a data file from github-linguist. */
10
- async function loadFile(file) {
11
+ async function loadWebFile(file) {
11
12
  // Return cache if it exists
12
13
  const cachedContent = cache.get(file);
13
14
  if (cachedContent)
@@ -18,4 +19,12 @@ async function loadFile(file) {
18
19
  cache.set(file, fileContent);
19
20
  return fileContent;
20
21
  }
22
+ async function loadLocalFile(file) {
23
+ const filePath = path_1.default.resolve(__dirname, '../../ext', file);
24
+ return promises_1.default.readFile(filePath).then(buffer => buffer.toString());
25
+ }
26
+ /** Load a data file from github-linguist. */
27
+ async function loadFile(file, offline = false) {
28
+ return offline ? loadLocalFile(file) : loadWebFile(file);
29
+ }
21
30
  exports.default = loadFile;
@@ -5,12 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
- const allFiles = new Set();
9
- const allFolders = new Set();
8
+ let allFiles;
9
+ let allFolders;
10
10
  /** Generate list of files in a directory. */
11
- function walk(root, folders, ignored = []) {
12
- // Switch out paths that expect being in root
13
- ignored = ignored.map(match => match.replace(/^\^/, '^\\./'));
11
+ function walk(init, root, folders, gitignores, regexIgnores) {
12
+ // Initialise files and folders lists
13
+ if (init) {
14
+ allFiles = new Set();
15
+ allFolders = new Set();
16
+ }
14
17
  // Walk tree of a folder
15
18
  if (folders.length === 1) {
16
19
  const folder = folders[0];
@@ -27,7 +30,10 @@ function walk(root, folders, ignored = []) {
27
30
  // Create absolute path for disc operations
28
31
  const path = path_1.default.resolve(root, file).replace(/\\/g, '/');
29
32
  // Skip if nonexistant or ignored
30
- if (!fs_1.default.existsSync(path) || ignored.some(pattern => RegExp(pattern).test(file)))
33
+ const nonExistant = !fs_1.default.existsSync(path);
34
+ const isGitIgnored = gitignores.test(file.replace('./', '')).ignored;
35
+ const isRegexIgnored = regexIgnores.find(match => file.replace('./', '').match(match));
36
+ if (nonExistant || isGitIgnored || isRegexIgnored)
31
37
  continue;
32
38
  // Add absolute folder path to list
33
39
  allFolders.add(path_1.default.resolve(folder).replace(/\\/g, '/'));
@@ -35,7 +41,7 @@ function walk(root, folders, ignored = []) {
35
41
  if (file.endsWith('/')) {
36
42
  // Recurse into subfolders
37
43
  allFolders.add(path);
38
- walk(root, [path], ignored);
44
+ walk(false, root, [path], gitignores, regexIgnores);
39
45
  }
40
46
  else {
41
47
  // Add relative file path to list
@@ -46,7 +52,7 @@ function walk(root, folders, ignored = []) {
46
52
  // Recurse into all folders
47
53
  else {
48
54
  for (const path of folders) {
49
- walk(root, [path], ignored);
55
+ walk(false, root, [path], gitignores, regexIgnores);
50
56
  }
51
57
  }
52
58
  // Return absolute files and folders lists
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  const fs_1 = __importDefault(require("fs"));
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const js_yaml_1 = __importDefault(require("js-yaml"));
8
- const glob_to_regexp_1 = __importDefault(require("glob-to-regexp"));
8
+ const ignore_1 = __importDefault(require("ignore"));
9
9
  const common_path_prefix_1 = __importDefault(require("common-path-prefix"));
10
10
  const binary_extensions_1 = __importDefault(require("binary-extensions"));
11
11
  const isbinaryfile_1 = require("isbinaryfile");
@@ -13,7 +13,6 @@ const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
13
13
  const load_data_1 = __importDefault(require("./helpers/load-data"));
14
14
  const read_file_1 = __importDefault(require("./helpers/read-file"));
15
15
  const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
16
- const convert_glob_1 = __importDefault(require("./helpers/convert-glob"));
17
16
  async function analyse(input, opts = {}) {
18
17
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
19
18
  var _t, _u, _v;
@@ -21,11 +20,11 @@ async function analyse(input, opts = {}) {
21
20
  input = [input !== null && input !== void 0 ? input : []].flat();
22
21
  opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
23
22
  // Load data from github-linguist web repo
24
- const langData = await (0, load_data_1.default)('languages.yml').then(js_yaml_1.default.load);
25
- const vendorData = await (0, load_data_1.default)('vendor.yml').then(js_yaml_1.default.load);
26
- const docData = await (0, load_data_1.default)('documentation.yml').then(js_yaml_1.default.load);
27
- const heuristicsData = await (0, load_data_1.default)('heuristics.yml').then(js_yaml_1.default.load);
28
- const generatedData = await (0, load_data_1.default)('generated.rb').then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/\))/gm)) !== null && _a !== void 0 ? _a : []; });
23
+ const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
24
+ const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
25
+ const docData = await (0, load_data_1.default)('documentation.yml', opts.offline).then(js_yaml_1.default.load);
26
+ const heuristicsData = await (0, load_data_1.default)('heuristics.yml', opts.offline).then(js_yaml_1.default.load);
27
+ const generatedData = await (0, load_data_1.default)('generated.rb', opts.offline).then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []; });
29
28
  const vendorPaths = [...vendorData, ...docData, ...generatedData];
30
29
  // Setup main variables
31
30
  const fileAssociations = {};
@@ -38,13 +37,18 @@ async function analyse(input, opts = {}) {
38
37
  unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
39
38
  };
40
39
  // Prepare list of ignored files
41
- const ignoredFiles = ['(^|\\/)\\.git\\/'];
42
- if (!opts.keepVendored) {
43
- ignoredFiles.push(...vendorPaths);
44
- }
45
- if (opts.ignoredFiles) {
46
- ignoredFiles.push(...opts.ignoredFiles.map(path => (0, glob_to_regexp_1.default)('*' + path + '*', { extended: true }).source));
47
- }
40
+ const gitignores = (0, ignore_1.default)();
41
+ const regexIgnores = [];
42
+ gitignores.add('/.git');
43
+ if (!opts.keepVendored)
44
+ regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
45
+ if (opts.ignoredFiles)
46
+ gitignores.add(opts.ignoredFiles);
47
+ // Set a common root path so that vendor paths do not incorrectly match parent folders
48
+ const resolvedInput = input.map(path => path_1.default.resolve(path).replace(/\\/g, '/'));
49
+ const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
50
+ const relPath = (file) => path_1.default.relative(commonRoot, file).replace(/\\/g, '/');
51
+ const unRelPath = (file) => path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
48
52
  // Load file paths and folders
49
53
  let files, folders;
50
54
  if (useRawContent) {
@@ -54,10 +58,7 @@ async function analyse(input, opts = {}) {
54
58
  }
55
59
  else {
56
60
  // Uses directory on disc
57
- // Set a common root path so that vendor paths do not incorrectly match parent folders
58
- const resolvedInput = input.map(path => path_1.default.resolve(path).replace(/\\/g, '/'));
59
- const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
60
- const data = (0, walk_tree_1.default)(commonRoot, input, ignoredFiles);
61
+ const data = (0, walk_tree_1.default)(true, commonRoot, input, gitignores, regexIgnores);
61
62
  files = data.files;
62
63
  folders = data.folders;
63
64
  }
@@ -79,40 +80,39 @@ async function analyse(input, opts = {}) {
79
80
  }
80
81
  }
81
82
  }
82
- // Load gitattributes
83
- const customIgnored = [];
84
- const customBinary = [];
85
- const customText = [];
83
+ // Load gitignores and gitattributes
84
+ const customBinary = (0, ignore_1.default)();
85
+ const customText = (0, ignore_1.default)();
86
86
  if (!useRawContent && opts.checkAttributes) {
87
87
  for (const folder of folders) {
88
88
  // Skip if folder is marked in gitattributes
89
- if (customIgnored.some(path => (0, convert_glob_1.default)(path).test(folder)))
89
+ if (relPath(folder) && gitignores.ignores(relPath(folder))) {
90
90
  continue;
91
+ }
91
92
  // Parse gitignores
92
93
  const ignoresFile = path_1.default.join(folder, '.gitignore');
93
94
  if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
94
95
  const ignoresData = await (0, read_file_1.default)(ignoresFile);
95
- const ignoresList = ignoresData.split(/\r?\n/).filter(line => line.trim() && !line.startsWith('#'));
96
- const ignoredPaths = ignoresList.map(path => (0, convert_glob_1.default)(path).source);
97
- customIgnored.push(...ignoredPaths);
96
+ gitignores.add(ignoresData);
98
97
  }
99
98
  // Parse gitattributes
100
99
  const attributesFile = path_1.default.join(folder, '.gitattributes');
101
100
  if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
102
101
  const attributesData = await (0, read_file_1.default)(attributesFile);
103
- const relPathToRegex = (path) => (0, convert_glob_1.default)(path).source.substr(1).replace(folder, '');
104
102
  // Explicit text/binary associations
105
103
  const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
106
104
  for (const [_line, path, type] of contentTypeMatches) {
107
- if (['text', '-binary'].includes(type))
108
- customText.push(relPathToRegex(path));
109
- if (['-text', 'binary'].includes(type))
110
- customBinary.push(relPathToRegex(path));
105
+ if (['text', '-binary'].includes(type)) {
106
+ customText.add(path);
107
+ }
108
+ if (['-text', 'binary'].includes(type)) {
109
+ customBinary.add(path);
110
+ }
111
111
  }
112
112
  // Custom vendor options
113
113
  const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
114
114
  for (const [_line, path] of vendorMatches) {
115
- customIgnored.push(relPathToRegex(path));
115
+ gitignores.add(path);
116
116
  }
117
117
  // Custom file associations
118
118
  const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
@@ -120,20 +120,26 @@ async function analyse(input, opts = {}) {
120
120
  // If specified language is an alias, associate it with its full name
121
121
  if (!langData[forcedLang]) {
122
122
  const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
123
- if (overrideLang)
123
+ if (overrideLang) {
124
124
  forcedLang = overrideLang[0];
125
+ }
125
126
  }
126
- const fullPath = folder + (0, convert_glob_1.default)(path).source.substr(1);
127
+ const fullPath = relPath(folder) + '/' + path;
127
128
  overrides[fullPath] = forcedLang;
128
129
  }
129
130
  }
130
131
  }
131
132
  }
132
133
  // Check vendored files
133
- if (!useRawContent && !opts.keepVendored) {
134
+ if (!opts.keepVendored) {
134
135
  // Filter out any files that match a vendor file path
135
- const matcher = (match) => RegExp(match.replace(/\/$/, '/.+$').replace(/^\.\//, ''));
136
- files = files.filter(file => !customIgnored.some(pattern => matcher(pattern).test(file)));
136
+ if (useRawContent) {
137
+ files = gitignores.filter(files);
138
+ files = files.filter(file => !regexIgnores.find(match => match.test(file)));
139
+ }
140
+ else {
141
+ files = gitignores.filter(files.map(relPath)).map(unRelPath);
142
+ }
137
143
  }
138
144
  // Load all files and parse languages
139
145
  const addResult = (file, result) => {
@@ -152,11 +158,11 @@ async function analyse(input, opts = {}) {
152
158
  if (useRawContent) {
153
159
  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;
154
160
  }
155
- else {
156
- if (!fs_1.default.existsSync(file) || fs_1.default.lstatSync(file).isDirectory())
157
- continue;
161
+ else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
158
162
  firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
159
163
  }
164
+ else
165
+ continue;
160
166
  // Skip if file is unreadable
161
167
  if (firstLine === null)
162
168
  continue;
@@ -166,17 +172,18 @@ async function analyse(input, opts = {}) {
166
172
  if (!opts.quick && (hasShebang || hasModeline)) {
167
173
  const matches = [];
168
174
  for (const [lang, data] of Object.entries(langData)) {
169
- const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}\\b`;
175
+ const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
170
176
  // Check for interpreter match
171
177
  const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
172
178
  // Check modeline declaration
173
179
  const matchesLang = firstLine.toLowerCase().match(langMatcher(lang));
174
180
  const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => firstLine.toLowerCase().match(langMatcher(lang)));
175
181
  // Add language
176
- if (opts.checkShebang && matchesInterpretor)
177
- matches.push(lang);
178
- if (opts.checkModeline && (matchesLang || matchesAlias))
182
+ const interpretorCheck = opts.checkShebang && matchesInterpretor;
183
+ const modelineCheck = opts.checkModeline && (matchesLang || matchesAlias);
184
+ if (interpretorCheck || modelineCheck) {
179
185
  matches.push(lang);
186
+ }
180
187
  }
181
188
  if (matches.length) {
182
189
  // Add explicitly-identified language
@@ -188,7 +195,8 @@ async function analyse(input, opts = {}) {
188
195
  }
189
196
  // Check override for manual language classification
190
197
  if (!useRawContent && !opts.quick && opts.checkAttributes) {
191
- const match = overridesArray.find(item => RegExp(item[0]).test(file));
198
+ const isOverridden = (path) => (0, ignore_1.default)().add(path).ignores(relPath(file));
199
+ const match = overridesArray.find(item => isOverridden(item[0]));
192
200
  if (match) {
193
201
  const forcedLang = match[1];
194
202
  addResult(file, forcedLang);
@@ -210,12 +218,14 @@ async function analyse(input, opts = {}) {
210
218
  for (const lang in langData) {
211
219
  // Check if extension is a match
212
220
  const matchesExt = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.some(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
213
- if (matchesExt)
221
+ if (matchesExt) {
214
222
  addResult(file, lang);
223
+ }
215
224
  }
216
225
  // Fallback to null if no language matches
217
- if (!fileAssociations[file])
226
+ if (!fileAssociations[file]) {
218
227
  addResult(file, null);
228
+ }
219
229
  }
220
230
  // Narrow down file associations to the best fit
221
231
  for (const file in fileAssociations) {
@@ -226,8 +236,8 @@ async function analyse(input, opts = {}) {
226
236
  }
227
237
  // Skip binary files
228
238
  if (!useRawContent && !opts.keepBinary) {
229
- const isCustomText = customText.some(path => RegExp(path).test(file));
230
- const isCustomBinary = customBinary.some(path => RegExp(path).test(file));
239
+ const isCustomText = customText.ignores(relPath(file));
240
+ const isCustomBinary = customBinary.ignores(relPath(file));
231
241
  const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
232
242
  if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
233
243
  continue;
@@ -260,7 +270,7 @@ async function analyse(input, opts = {}) {
260
270
  if (heuristic.named_pattern)
261
271
  normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
262
272
  // Check file contents and apply heuristic patterns
263
- const fileContent = await (0, read_file_1.default)(file).catch(() => null);
273
+ const fileContent = opts.fileContent ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
264
274
  if (fileContent === null)
265
275
  continue;
266
276
  if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
@@ -277,16 +287,19 @@ async function analyse(input, opts = {}) {
277
287
  const categories = ['data', 'markup', 'programming', 'prose'];
278
288
  const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
279
289
  for (const [file, lang] of Object.entries(results.files.results)) {
280
- if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; }))
290
+ if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
281
291
  continue;
292
+ }
282
293
  delete results.files.results[file];
283
- if (lang)
294
+ if (lang) {
284
295
  delete results.languages.results[lang];
296
+ }
285
297
  }
286
298
  for (const category of hiddenCategories) {
287
299
  for (const [lang, { type }] of Object.entries(results.languages.results)) {
288
- if (type === category)
300
+ if (type === category) {
289
301
  delete results.languages.results[lang];
302
+ }
290
303
  }
291
304
  }
292
305
  }
@@ -295,8 +308,9 @@ async function analyse(input, opts = {}) {
295
308
  const newMap = {};
296
309
  for (const [file, lang] of Object.entries(results.files.results)) {
297
310
  let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
298
- if (!relPath.startsWith('../'))
311
+ if (!relPath.startsWith('../')) {
299
312
  relPath = './' + relPath;
313
+ }
300
314
  newMap[relPath] = lang;
301
315
  }
302
316
  results.files.results = newMap;
@@ -320,8 +334,9 @@ async function analyse(input, opts = {}) {
320
334
  // Add language and bytes data to corresponding section
321
335
  const { type } = langData[lang];
322
336
  (_s = (_v = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_v[lang] = { type, bytes: 0, color: langData[lang].color });
323
- if (opts.childLanguages)
337
+ if (opts.childLanguages) {
324
338
  results.languages.results[lang].parent = langData[lang].group;
339
+ }
325
340
  results.languages.results[lang].bytes += fileSize;
326
341
  results.languages.bytes += fileSize;
327
342
  }
package/dist/types.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface Options {
14
14
  relativePaths?: boolean;
15
15
  childLanguages?: boolean;
16
16
  quick?: boolean;
17
+ offline?: boolean;
17
18
  checkIgnored?: boolean;
18
19
  checkAttributes?: boolean;
19
20
  checkHeuristics?: boolean;
@@ -0,0 +1,17 @@
1
+ - ^[Dd]ocs?/
2
+ - (^|/)[Dd]ocumentation/
3
+ - (^|/)[Gg]roovydoc/
4
+ - (^|/)[Jj]avadoc/
5
+ - ^[Mm]an/
6
+ - ^[Ee]xamples/
7
+ - ^[Dd]emos?/
8
+ - (^|/)inst/doc/- (^|/)CITATION(\.cff|(S)?(\.(bib|md))?)$
9
+ - (^|/)CHANGE(S|LOG)?(\.|$)
10
+ - (^|/)CONTRIBUTING(\.|$)
11
+ - (^|/)COPYING(\.|$)
12
+ - (^|/)INSTALL(\.|$)
13
+ - (^|/)LICEN[CS]E(\.|$)
14
+ - (^|/)[Ll]icen[cs]e(\.|$)
15
+ - (^|/)README(\.|$)
16
+ - (^|/)[Rr]eadme(\.|$)
17
+ - ^[Ss]amples?/