linguist-js 3.0.0-pre → 3.0.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/analyser/pipeline/aggregate.d.ts +3 -1
- package/dist/analyser/pipeline/aggregate.js +8 -8
- package/dist/analyser/pipeline/classify.js +2 -1
- package/dist/program/data/loadDataFiles.js +9 -1
- package/dist/program/data/retrieveData.d.ts +1 -4
- package/dist/program/data/retrieveData.js +3 -6
- package/dist/program/utils/pcre.js +1 -0
- package/dist/src/cli/output/default.d.ts +3 -0
- package/dist/src/cli/output/default.js +106 -0
- package/dist/src/cli/output/tree.d.ts +3 -0
- package/dist/src/cli/output/tree.js +11 -0
- package/dist/src/cli/runCliAnalysis.d.ts +2 -0
- package/dist/src/cli/runCliAnalysis.js +25 -0
- package/dist/src/cli/utils.d.ts +2 -0
- package/dist/src/cli/utils.js +9 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +56 -0
- package/dist/src/helpers/convert-pcre.d.ts +2 -0
- package/dist/src/helpers/convert-pcre.js +38 -0
- package/dist/src/helpers/load-data.d.ts +4 -0
- package/dist/src/helpers/load-data.js +37 -0
- package/dist/src/helpers/norm-path.d.ts +2 -0
- package/dist/src/helpers/norm-path.js +15 -0
- package/dist/src/helpers/parse-gitattributes.d.ts +17 -0
- package/dist/src/helpers/parse-gitattributes.js +37 -0
- package/dist/src/helpers/parse-gitignore.d.ts +1 -0
- package/dist/src/helpers/parse-gitignore.js +12 -0
- package/dist/src/helpers/read-file.d.ts +5 -0
- package/dist/src/helpers/read-file.js +23 -0
- package/dist/src/helpers/walk-tree.d.ts +20 -0
- package/dist/src/helpers/walk-tree.js +90 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +470 -0
- package/dist/src/program/data/loadData.d.ts +4 -0
- package/dist/src/program/data/loadData.js +32 -0
- package/dist/src/program/fs/normalisedPath.d.ts +2 -0
- package/dist/src/program/fs/normalisedPath.js +7 -0
- package/dist/src/program/fs/readFile.d.ts +5 -0
- package/dist/src/program/fs/readFile.js +17 -0
- package/dist/src/program/fs/walkTree.d.ts +20 -0
- package/dist/src/program/fs/walkTree.js +82 -0
- package/dist/src/program/parsing/parseGitattributes.d.ts +17 -0
- package/dist/src/program/parsing/parseGitattributes.js +33 -0
- package/dist/src/program/parsing/parseGitignore.d.ts +1 -0
- package/dist/src/program/parsing/parseGitignore.js +9 -0
- package/dist/src/program/utils/pcre.d.ts +2 -0
- package/dist/src/program/utils/pcre.js +35 -0
- package/dist/src/schema.d.ts +37 -0
- package/dist/src/schema.js +2 -0
- package/dist/src/types/schema.d.ts +37 -0
- package/dist/src/types/schema.js +1 -0
- package/dist/src/types/types.d.ts +65 -0
- package/dist/src/types/types.js +1 -0
- package/dist/src/types.d.ts +74 -0
- package/dist/src/types.js +2 -0
- package/dist/types/types.d.ts +0 -1
- package/ext/languages.yml +8 -0
- package/package.json +2 -2
- package/readme.md +20 -37
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import * as T from '../../types/types.js';
|
|
2
|
-
export declare function aggregateResults(files: T.VirtualFile[], classifications: Record<string, string[]>, heuristicResolutions: Record<string, string | undefined>, langData:
|
|
2
|
+
export declare function aggregateResults(files: T.VirtualFile[], classifications: Record<string, string[]>, heuristicResolutions: Record<string, string | undefined>, langData: {
|
|
3
|
+
[language: string]: T.LanguageMetadata;
|
|
4
|
+
}, opts: T.Options): T.Results;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Path from 'node:path';
|
|
2
2
|
const categoryKeys = ['data', 'markup', 'programming', 'prose'];
|
|
3
3
|
function pickBestLanguage(classifications) {
|
|
4
|
+
// Assign first language as a default option
|
|
4
5
|
return classifications[0] ?? null;
|
|
5
6
|
}
|
|
6
7
|
function makeRelPath(path) {
|
|
@@ -15,7 +16,7 @@ function normPath(filePath) {
|
|
|
15
16
|
}
|
|
16
17
|
export function aggregateResults(files, classifications, heuristicResolutions, langData, opts) {
|
|
17
18
|
const results = {
|
|
18
|
-
files: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {}
|
|
19
|
+
files: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {} },
|
|
19
20
|
languages: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {} },
|
|
20
21
|
unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, extensions: {}, filenames: {} },
|
|
21
22
|
repository: {},
|
|
@@ -27,10 +28,12 @@ export function aggregateResults(files, classifications, heuristicResolutions, l
|
|
|
27
28
|
// Narrow down file associations to the best fit
|
|
28
29
|
const candidates = classifications[file.path] ?? [];
|
|
29
30
|
// If no heuristics, assign a language
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const bestLanguage = heuristicResolutions[file.path] ?? pickBestLanguage(candidates);
|
|
32
|
+
const selectedLanguage = bestLanguage
|
|
33
|
+
? opts.childLanguages
|
|
34
|
+
? bestLanguage // use the child language
|
|
35
|
+
: (langData[bestLanguage]?.group ?? bestLanguage) // use the parent language, if it exists
|
|
36
|
+
: null;
|
|
34
37
|
// Load language bytes size
|
|
35
38
|
const size = file.size ?? file.content?.length ?? 0;
|
|
36
39
|
// Calculate lines of code
|
|
@@ -72,9 +75,6 @@ export function aggregateResults(files, classifications, heuristicResolutions, l
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
results.files.results[outputPath] = selectedLanguage;
|
|
75
|
-
if (alternativeLanguages.length) {
|
|
76
|
-
results.files.alternatives[outputPath] = alternativeLanguages;
|
|
77
|
-
}
|
|
78
78
|
// Apply to files totals
|
|
79
79
|
results.files.bytes += size;
|
|
80
80
|
results.files.lines.total += Number.isNaN(loc.total) ? 0 : loc.total;
|
|
@@ -18,11 +18,12 @@ export function classifyFiles(files, langData, opts) {
|
|
|
18
18
|
const classifications = {};
|
|
19
19
|
for (const file of files) {
|
|
20
20
|
// Search each language
|
|
21
|
+
// Note: order here is important; the first match will be the one chosen
|
|
21
22
|
const candidates = [
|
|
22
23
|
...byAttributes(file, langData),
|
|
24
|
+
...byModeline(file, langData, opts),
|
|
23
25
|
...byFilename(file, langData),
|
|
24
26
|
...byShebang(file, langData, opts),
|
|
25
|
-
...byModeline(file, langData, opts),
|
|
26
27
|
...byExtension(file, langData),
|
|
27
28
|
];
|
|
28
29
|
classifications[file.path] = dedupeClassifications(candidates);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import YAML from 'js-yaml';
|
|
1
2
|
import Cache from 'node-cache';
|
|
2
3
|
import FS from 'node:fs';
|
|
3
4
|
import Path from 'node:path';
|
|
@@ -14,8 +15,15 @@ async function loadWebFile(file) {
|
|
|
14
15
|
// Load file content, falling back to the local file if the request fails
|
|
15
16
|
const fileContent = await fetch(dataUrl(file))
|
|
16
17
|
.then((data) => data.text())
|
|
17
|
-
.catch(async () =>
|
|
18
|
+
.catch(async (x) => void x);
|
|
19
|
+
if (!fileContent) {
|
|
20
|
+
return await loadLocalFile(file);
|
|
21
|
+
}
|
|
18
22
|
cache.set(file, fileContent);
|
|
23
|
+
// Clean up lengthy files
|
|
24
|
+
if (file === 'generated.rb') {
|
|
25
|
+
return YAML.dump(parseGeneratedDataFile(fileContent));
|
|
26
|
+
}
|
|
19
27
|
return fileContent;
|
|
20
28
|
}
|
|
21
29
|
async function loadLocalFile(file) {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { HeuristicsSchema, LanguagesScema
|
|
1
|
+
import { HeuristicsSchema, LanguagesScema } from '../../types/schema.js';
|
|
2
2
|
type LoadedData = {
|
|
3
3
|
langData: LanguagesScema;
|
|
4
|
-
vendorData: VendorSchema;
|
|
5
|
-
docData: VendorSchema;
|
|
6
4
|
heuristicsData: HeuristicsSchema;
|
|
7
|
-
generatedData: string[];
|
|
8
5
|
vendorPaths: string[];
|
|
9
6
|
};
|
|
10
7
|
/** Load data from github-linguist web repo or cached local file. */
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import YAML from 'js-yaml';
|
|
2
|
-
import { loadFile
|
|
2
|
+
import { loadFile } from './loadDataFiles.js';
|
|
3
3
|
let data = null;
|
|
4
4
|
async function initRetrieveData(offline) {
|
|
5
|
-
// Only load the data on
|
|
5
|
+
// Only load the data on mount
|
|
6
6
|
if (data)
|
|
7
7
|
return;
|
|
8
8
|
const langData = (await loadFile('languages.yml', offline).then(YAML.load));
|
|
9
9
|
const vendorData = (await loadFile('vendor.yml', offline).then(YAML.load));
|
|
10
10
|
const docData = (await loadFile('documentation.yml', offline).then(YAML.load));
|
|
11
11
|
const heuristicsData = (await loadFile('heuristics.yml', offline).then(YAML.load));
|
|
12
|
-
const generatedData = (await loadFile('generated.rb', offline).then(
|
|
12
|
+
const generatedData = (await loadFile('generated.rb', offline).then(YAML.load));
|
|
13
13
|
const vendorPaths = [...vendorData, ...docData, ...generatedData];
|
|
14
14
|
data = {
|
|
15
15
|
langData,
|
|
16
|
-
vendorData,
|
|
17
|
-
docData,
|
|
18
16
|
heuristicsData,
|
|
19
|
-
generatedData,
|
|
20
17
|
vendorPaths,
|
|
21
18
|
};
|
|
22
19
|
}
|
|
@@ -14,6 +14,7 @@ export default function pcre(regex) {
|
|
|
14
14
|
// Remove PCRE-only syntax
|
|
15
15
|
replace(/([*+]){2}/g, '$1');
|
|
16
16
|
replace(/\(\?>/g, '(?:');
|
|
17
|
+
replace(/\\g\<[^>]+\>/g, '.*?'); // referenced named groups not supported
|
|
17
18
|
// Remove start/end-of-file markers
|
|
18
19
|
if (/\\[AZ]/.test(finalRegex)) {
|
|
19
20
|
replace(/\\A/g, '^');
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import FS from 'node:fs';
|
|
2
|
+
import Path from 'node:path';
|
|
3
|
+
import { normPath } from '../../program/fs/normalisedPath.js';
|
|
4
|
+
import { colouredMsg, hexToRgb } from '../utils.js';
|
|
5
|
+
export default async function defaultOutput(args, data) {
|
|
6
|
+
const { files, languages, unknown, repository } = data;
|
|
7
|
+
if (args.minSize) {
|
|
8
|
+
// Ignore languages with a bytes/% size less than the declared min size
|
|
9
|
+
const totalSize = languages.bytes;
|
|
10
|
+
const minSizeAmt = parseFloat(args.minSize.replace(/[a-z]+$/i, '')); // '2KB' -> 2
|
|
11
|
+
const minSizeUnit = args.minSize.replace(/^\d+/, '').toLowerCase(); // '2KB' -> 'kb'
|
|
12
|
+
const checkBytes = minSizeUnit !== 'loc'; // whether to check bytes or loc
|
|
13
|
+
const conversionFactors = {
|
|
14
|
+
['b']: (n) => n,
|
|
15
|
+
['kb']: (n) => n * 1e3,
|
|
16
|
+
['mb']: (n) => n * 1e6,
|
|
17
|
+
['%']: (n) => (n * totalSize) / 100,
|
|
18
|
+
['loc']: (n) => n,
|
|
19
|
+
};
|
|
20
|
+
const minBytesSize = conversionFactors[minSizeUnit](+minSizeAmt);
|
|
21
|
+
const other = { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 } };
|
|
22
|
+
// Apply specified minimums: delete language results that do not reach the threshold
|
|
23
|
+
for (const [lang, data] of Object.entries(languages.results)) {
|
|
24
|
+
const checkUnit = checkBytes ? data.bytes : data.lines.content;
|
|
25
|
+
if (checkUnit < minBytesSize) {
|
|
26
|
+
// Add to 'other' count
|
|
27
|
+
other.count++;
|
|
28
|
+
other.bytes += data.bytes;
|
|
29
|
+
other.lines.total += data.lines.total;
|
|
30
|
+
other.lines.content += data.lines.content;
|
|
31
|
+
// Remove language result
|
|
32
|
+
delete languages.results[lang];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (other.bytes) {
|
|
36
|
+
languages.results['Other'] = other;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const sortedEntries = Object.entries(languages.results).sort((a, b) => (a[1].bytes < b[1].bytes ? +1 : -1));
|
|
40
|
+
const totalBytes = languages.bytes;
|
|
41
|
+
console.log(`\n Analysed ${files.bytes.toLocaleString()} B from ${files.count} files with linguist-js`);
|
|
42
|
+
console.log(`\n Language analysis results: \n`);
|
|
43
|
+
let count = 0;
|
|
44
|
+
if (sortedEntries.length === 0)
|
|
45
|
+
console.log(` None`);
|
|
46
|
+
// Collate files per language
|
|
47
|
+
const filesPerLanguage = {};
|
|
48
|
+
if (args.listFiles) {
|
|
49
|
+
for (const language of Object.keys(languages.results)) {
|
|
50
|
+
filesPerLanguage[language] = [];
|
|
51
|
+
}
|
|
52
|
+
for (const [file, lang] of Object.entries(files.results)) {
|
|
53
|
+
if (lang) {
|
|
54
|
+
filesPerLanguage[lang].push(file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// List parsed results
|
|
59
|
+
for (const [lang, { bytes, lines }] of sortedEntries) {
|
|
60
|
+
const colour = hexToRgb(repository[lang].color ?? '#ededed');
|
|
61
|
+
const percent = (bytes) => (bytes / (totalBytes || 1)) * 100;
|
|
62
|
+
const fmtd = {
|
|
63
|
+
index: (++count).toString().padStart(2, ' '),
|
|
64
|
+
lang: lang.padEnd(24, ' '),
|
|
65
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
66
|
+
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
67
|
+
loc: lines.content.toLocaleString().padStart(10, ' '),
|
|
68
|
+
icon: colouredMsg(colour, '\u2588'),
|
|
69
|
+
};
|
|
70
|
+
console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B ${fmtd.loc} LOC`);
|
|
71
|
+
// If using `listFiles` option, list all files tagged as this language
|
|
72
|
+
if (args.listFiles) {
|
|
73
|
+
console.log(); // padding
|
|
74
|
+
for (const file of filesPerLanguage[lang]) {
|
|
75
|
+
let relFile = normPath(Path.relative(Path.resolve('.'), file));
|
|
76
|
+
if (!relFile.startsWith('../')) {
|
|
77
|
+
relFile = './' + relFile;
|
|
78
|
+
}
|
|
79
|
+
const fileStat = await FS.promises.stat(file);
|
|
80
|
+
const bytes = fileStat.size;
|
|
81
|
+
const fmtd2 = {
|
|
82
|
+
file: relFile.padEnd(42, ' '),
|
|
83
|
+
percent: percent(bytes).toFixed(2).padStart(5, ' '),
|
|
84
|
+
bytes: bytes.toLocaleString().padStart(10, ' '),
|
|
85
|
+
};
|
|
86
|
+
console.log(` ${fmtd.icon} ${fmtd2.file} ${fmtd2.percent}% ${fmtd2.bytes} B`);
|
|
87
|
+
}
|
|
88
|
+
console.log(); // padding
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!args.listFiles) {
|
|
92
|
+
console.log(); // padding
|
|
93
|
+
}
|
|
94
|
+
console.log(` Total: ${totalBytes.toLocaleString()} B`);
|
|
95
|
+
// List unknown files/extensions
|
|
96
|
+
if (unknown.bytes > 0) {
|
|
97
|
+
console.log(`\n Unknown files and extensions:`);
|
|
98
|
+
for (const [name, bytes] of Object.entries(unknown.filenames)) {
|
|
99
|
+
console.log(` '${name}': ${bytes.toLocaleString()} B`);
|
|
100
|
+
}
|
|
101
|
+
for (const [ext, bytes] of Object.entries(unknown.extensions)) {
|
|
102
|
+
console.log(` '*${ext}': ${bytes.toLocaleString()} B`);
|
|
103
|
+
}
|
|
104
|
+
console.log(` Total: ${unknown.bytes.toLocaleString()} B`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function treeOutput(args, data) {
|
|
2
|
+
const treeParts = args.tree.split('.');
|
|
3
|
+
let nestedData = data;
|
|
4
|
+
for (const part of treeParts) {
|
|
5
|
+
if (!nestedData[part]) {
|
|
6
|
+
throw Error(`TraversalError: Key '${part}' cannot be found on output object.`);
|
|
7
|
+
}
|
|
8
|
+
nestedData = nestedData[part];
|
|
9
|
+
}
|
|
10
|
+
console.log(nestedData);
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import linguist from '../index.js';
|
|
2
|
+
import defaultOutput from './output/default.js';
|
|
3
|
+
import treeOutput from './output/tree.js';
|
|
4
|
+
const validCategories = ['data', 'programming', 'prose', 'markup'];
|
|
5
|
+
export default async function runCliAnalysis(args) {
|
|
6
|
+
// Check arguments
|
|
7
|
+
if (args.categories?.some((category) => !validCategories.includes(category))) {
|
|
8
|
+
console.log(`Error: '${args.categories.join(', ')}' contains an invalid category.`);
|
|
9
|
+
console.log(`\tValid options: ${validCategories.join(', ')}.`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Fetch language data
|
|
13
|
+
const root = args.analyze === true ? '.' : args.analyze;
|
|
14
|
+
const data = await linguist(root, args);
|
|
15
|
+
// Print output
|
|
16
|
+
if (!args.json) {
|
|
17
|
+
defaultOutput(args, data);
|
|
18
|
+
}
|
|
19
|
+
else if (args.tree) {
|
|
20
|
+
treeOutput(args, data);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.dir(data, { depth: null });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function colouredMsg([r, g, b], msg) {
|
|
2
|
+
return `\u001B[${38};2;${r};${g};${b}m${msg}\u001b[0m`;
|
|
3
|
+
}
|
|
4
|
+
export function hexToRgb(hex) {
|
|
5
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
6
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
7
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
8
|
+
return [r, g, b];
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import FS from 'node:fs';
|
|
3
|
+
import runCliAnalysis from './cli/runCliAnalysis.js';
|
|
4
|
+
const packageJson = JSON.parse(FS.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
5
|
+
const VERSION = packageJson.version;
|
|
6
|
+
program
|
|
7
|
+
.name('linguist')
|
|
8
|
+
.usage('--analyze [<folders...>] [<options...>]')
|
|
9
|
+
.option('-a|--analyze [folders...]', 'Analyse the languages of all files in a folder')
|
|
10
|
+
.option('-i|--ignoredFiles <files...>', `A list of file path globs to ignore`)
|
|
11
|
+
.option('-l|--ignoredLanguages <languages...>', `A list of languages to ignore`)
|
|
12
|
+
.option('-c|--categories <categories...>', 'Language categories to include in output')
|
|
13
|
+
.option('-C|--childLanguages [bool]', 'Display child languages instead of their parents', false)
|
|
14
|
+
.option('-j|--json [bool]', 'Display the output as JSON', false)
|
|
15
|
+
.option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
|
|
16
|
+
.option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
|
|
17
|
+
.option('-m|--minSize <size>', 'Minimum size of file to show language results for (must have a unit: b, kb, mb, %, or loc)')
|
|
18
|
+
.option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
|
|
19
|
+
.option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
|
|
20
|
+
.option('-L|--calculateLines [bool]', 'Calculate lines of code totals', true)
|
|
21
|
+
.option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
|
|
22
|
+
.option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
|
|
23
|
+
.option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
|
|
24
|
+
.option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
|
|
25
|
+
.option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
|
|
26
|
+
.option('-D|--checkDetected [bool]', 'Force files marked with linguist-detectable to always appear in output', true)
|
|
27
|
+
.option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
|
|
28
|
+
.option('-S|--checkShebang [bool]', 'Check shebang lines for explicit classification', true)
|
|
29
|
+
.option('-M|--checkModeline [bool]', 'Check modelines for explicit classification', true)
|
|
30
|
+
.helpOption(`-h|--help`, 'Display this help message')
|
|
31
|
+
.version(VERSION, '-v|--version', 'Display the installed version of linguist-js');
|
|
32
|
+
program.parse(process.argv);
|
|
33
|
+
const args = program.opts();
|
|
34
|
+
// Normalise arguments
|
|
35
|
+
for (const arg in args) {
|
|
36
|
+
const normalise = (val) => {
|
|
37
|
+
if (typeof val !== 'string')
|
|
38
|
+
return val;
|
|
39
|
+
val = val.replace(/^=/, '');
|
|
40
|
+
if (val.match(/true$|false$/))
|
|
41
|
+
val = val === 'true';
|
|
42
|
+
return val;
|
|
43
|
+
};
|
|
44
|
+
if (Array.isArray(args[arg]))
|
|
45
|
+
args[arg] = args[arg].map(normalise);
|
|
46
|
+
else
|
|
47
|
+
args[arg] = normalise(args[arg]);
|
|
48
|
+
}
|
|
49
|
+
// Run Linguist
|
|
50
|
+
if (args.analyze) {
|
|
51
|
+
void runCliAnalysis(args);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(`Welcome to linguist-js, a JavaScript port of GitHub's language analyzer.`);
|
|
55
|
+
console.log(`Type 'linguist --help' for a list of commands.`);
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
exports.parseGeneratedDataFile = parseGeneratedDataFile;
|
|
7
|
+
exports.default = loadFile;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const cross_fetch_1 = __importDefault(require("cross-fetch"));
|
|
11
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
12
|
+
const cache = new node_cache_1.default({});
|
|
13
|
+
async function loadWebFile(file) {
|
|
14
|
+
// Return cache if it exists
|
|
15
|
+
const cachedContent = cache.get(file);
|
|
16
|
+
if (cachedContent)
|
|
17
|
+
return cachedContent;
|
|
18
|
+
// Otherwise cache the request
|
|
19
|
+
const dataUrl = (file) => `https://raw.githubusercontent.com/github/linguist/HEAD/lib/linguist/${file}`;
|
|
20
|
+
// Load file content, falling back to the local file if the request fails
|
|
21
|
+
const fileContent = await (0, cross_fetch_1.default)(dataUrl(file)).then(data => data.text()).catch(async () => await loadLocalFile(file));
|
|
22
|
+
cache.set(file, fileContent);
|
|
23
|
+
return fileContent;
|
|
24
|
+
}
|
|
25
|
+
async function loadLocalFile(file) {
|
|
26
|
+
const filePath = path_1.default.resolve(__dirname, '../../ext', file);
|
|
27
|
+
return fs_1.default.promises.readFile(filePath).then(buffer => buffer.toString());
|
|
28
|
+
}
|
|
29
|
+
/** Nukes unused `generated.rb` file content. */
|
|
30
|
+
function parseGeneratedDataFile(fileContent) {
|
|
31
|
+
var _a;
|
|
32
|
+
return [...(_a = fileContent.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []];
|
|
33
|
+
}
|
|
34
|
+
/** Load a data file from github-linguist. */
|
|
35
|
+
function loadFile(file, offline = false) {
|
|
36
|
+
return offline ? loadLocalFile(file) : loadWebFile(file);
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
exports.normAbsPath = exports.normPath = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const normPath = function normalisedPath(...inputPaths) {
|
|
9
|
+
return path_1.default.join(...inputPaths).replace(/\\/g, '/');
|
|
10
|
+
};
|
|
11
|
+
exports.normPath = normPath;
|
|
12
|
+
const normAbsPath = function normalisedAbsolutePath(...inputPaths) {
|
|
13
|
+
return path_1.default.resolve(...inputPaths).replace(/\\/g, '/');
|
|
14
|
+
};
|
|
15
|
+
exports.normAbsPath = normAbsPath;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as T from '../types';
|
|
2
|
+
export type FlagAttributes = {
|
|
3
|
+
'vendored': boolean | null;
|
|
4
|
+
'generated': boolean | null;
|
|
5
|
+
'documentation': boolean | null;
|
|
6
|
+
'detectable': boolean | null;
|
|
7
|
+
'binary': boolean | null;
|
|
8
|
+
'language': T.LanguageResult;
|
|
9
|
+
};
|
|
10
|
+
export type ParsedGitattributes = Array<{
|
|
11
|
+
glob: T.FileGlob;
|
|
12
|
+
attrs: FlagAttributes;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Parses a gitattributes file.
|
|
16
|
+
*/
|
|
17
|
+
export default function parseAttributes(content: string, folderRoot?: string): ParsedGitattributes;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseAttributes;
|
|
4
|
+
const norm_path_1 = require("./norm-path");
|
|
5
|
+
/**
|
|
6
|
+
* Parses a gitattributes file.
|
|
7
|
+
*/
|
|
8
|
+
function parseAttributes(content, folderRoot = '.') {
|
|
9
|
+
var _a, _b;
|
|
10
|
+
const output = [];
|
|
11
|
+
for (const rawLine of content.split('\n')) {
|
|
12
|
+
const line = rawLine.replace(/#.*/, '').trim();
|
|
13
|
+
if (!line)
|
|
14
|
+
continue;
|
|
15
|
+
const parts = line.split(/\s+/g);
|
|
16
|
+
const fileGlob = parts[0];
|
|
17
|
+
const relFileGlob = (0, norm_path_1.normPath)(folderRoot, fileGlob);
|
|
18
|
+
const attrParts = parts.slice(1);
|
|
19
|
+
const isTrue = (str) => !str.startsWith('-') && !str.endsWith('=false');
|
|
20
|
+
const isFalse = (str) => str.startsWith('-') || str.endsWith('=false');
|
|
21
|
+
const trueParts = (str) => attrParts.filter(part => part.includes(str) && isTrue(part));
|
|
22
|
+
const falseParts = (str) => attrParts.filter(part => part.includes(str) && isFalse(part));
|
|
23
|
+
const hasTrueParts = (str) => trueParts(str).length > 0;
|
|
24
|
+
const hasFalseParts = (str) => falseParts(str).length > 0;
|
|
25
|
+
const boolOrNullVal = (str) => hasTrueParts(str) ? true : hasFalseParts(str) ? false : null;
|
|
26
|
+
const attrs = {
|
|
27
|
+
'generated': boolOrNullVal('linguist-generated'),
|
|
28
|
+
'vendored': boolOrNullVal('linguist-vendored'),
|
|
29
|
+
'documentation': boolOrNullVal('linguist-documentation'),
|
|
30
|
+
'detectable': boolOrNullVal('linguist-detectable'),
|
|
31
|
+
'binary': hasTrueParts('binary') || hasFalseParts('text') ? true : hasFalseParts('binary') || hasTrueParts('text') ? false : null,
|
|
32
|
+
'language': (_b = (_a = trueParts('linguist-language').at(-1)) === null || _a === void 0 ? void 0 : _a.split('=')[1]) !== null && _b !== void 0 ? _b : null,
|
|
33
|
+
};
|
|
34
|
+
output.push({ glob: relFileGlob, attrs });
|
|
35
|
+
}
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function parseGitignore(content: string): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseGitignore;
|
|
4
|
+
function parseGitignore(content) {
|
|
5
|
+
const readableData = content
|
|
6
|
+
// Remove comments unless escaped
|
|
7
|
+
.replace(/(?<!\\)#.+/g, '')
|
|
8
|
+
// Remove whitespace unless escaped
|
|
9
|
+
.replace(/(?:(?<!\\)\s)+$/g, '');
|
|
10
|
+
const arrayData = readableData.split(/\r?\n/).filter(data => data);
|
|
11
|
+
return arrayData;
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
exports.default = readFileChunk;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
/**
|
|
9
|
+
* Read part of a file on disc.
|
|
10
|
+
* @throws 'EPERM' if the file is not readable.
|
|
11
|
+
*/
|
|
12
|
+
async function readFileChunk(filename, onlyFirstLine = false) {
|
|
13
|
+
const chunkSize = 100;
|
|
14
|
+
const stream = fs_1.default.createReadStream(filename, { highWaterMark: chunkSize });
|
|
15
|
+
let content = '';
|
|
16
|
+
for await (const data of stream) { // may throw
|
|
17
|
+
content += data.toString();
|
|
18
|
+
if (onlyFirstLine && content.includes('\n')) {
|
|
19
|
+
return content.split(/\r?\n/)[0];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Ignore } from 'ignore';
|
|
2
|
+
interface WalkInput {
|
|
3
|
+
/** Whether this is walking the tree from the root */
|
|
4
|
+
init: boolean;
|
|
5
|
+
/** The common root absolute path of all folders being checked */
|
|
6
|
+
commonRoot: string;
|
|
7
|
+
/** The absolute path that each folder is relative to */
|
|
8
|
+
folderRoots: string[];
|
|
9
|
+
/** The absolute path of folders being checked */
|
|
10
|
+
folders: string[];
|
|
11
|
+
/** An instantiated Ignore object listing ignored files */
|
|
12
|
+
ignored: Ignore;
|
|
13
|
+
}
|
|
14
|
+
interface WalkOutput {
|
|
15
|
+
files: string[];
|
|
16
|
+
folders: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Generate list of files in a directory. */
|
|
19
|
+
export default function walk(data: WalkInput): WalkOutput;
|
|
20
|
+
export {};
|