linguist-js 2.9.2 → 3.0.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.
Files changed (124) hide show
  1. package/bin/index.js +1 -1
  2. package/dist/analyser/classifiers/byAttributes.d.ts +3 -0
  3. package/dist/analyser/classifiers/byAttributes.js +13 -0
  4. package/dist/analyser/classifiers/byExtension.d.ts +3 -0
  5. package/dist/analyser/classifiers/byExtension.js +31 -0
  6. package/dist/analyser/classifiers/byFilename.d.ts +3 -0
  7. package/dist/analyser/classifiers/byFilename.js +10 -0
  8. package/dist/analyser/classifiers/byHeuristics.d.ts +3 -0
  9. package/dist/analyser/classifiers/byHeuristics.js +67 -0
  10. package/dist/analyser/classifiers/byModeline.d.ts +3 -0
  11. package/dist/analyser/classifiers/byModeline.js +22 -0
  12. package/dist/analyser/classifiers/byShebang.d.ts +3 -0
  13. package/dist/analyser/classifiers/byShebang.js +27 -0
  14. package/dist/analyser/index.d.ts +3 -0
  15. package/dist/analyser/index.js +12 -0
  16. package/dist/analyser/pipeline/aggregate.d.ts +4 -0
  17. package/dist/analyser/pipeline/aggregate.js +98 -0
  18. package/dist/analyser/pipeline/classify.d.ts +3 -0
  19. package/dist/analyser/pipeline/classify.js +32 -0
  20. package/dist/analyser/pipeline/filter.d.ts +2 -0
  21. package/dist/analyser/pipeline/filter.js +14 -0
  22. package/dist/analyser/pipeline/heuristics.d.ts +3 -0
  23. package/dist/analyser/pipeline/heuristics.js +12 -0
  24. package/dist/analyser/pipeline/normalise.d.ts +2 -0
  25. package/dist/analyser/pipeline/normalise.js +9 -0
  26. package/dist/cli/output/default.d.ts +3 -0
  27. package/dist/cli/output/default.js +106 -0
  28. package/dist/cli/output/tree.d.ts +3 -0
  29. package/dist/cli/output/tree.js +11 -0
  30. package/dist/cli/runCliAnalysis.d.ts +2 -0
  31. package/dist/cli/runCliAnalysis.js +26 -0
  32. package/dist/cli/utils.d.ts +2 -0
  33. package/dist/cli/utils.js +9 -0
  34. package/dist/cli.js +14 -143
  35. package/dist/entry/analyseFs.d.ts +2 -0
  36. package/dist/entry/analyseFs.js +11 -0
  37. package/dist/entry/analyseRaw.d.ts +4 -0
  38. package/dist/entry/analyseRaw.js +11 -0
  39. package/dist/index.d.ts +19 -4
  40. package/dist/index.js +18 -483
  41. package/dist/input/fromFilesystem.d.ts +4 -0
  42. package/dist/input/fromFilesystem.js +115 -0
  43. package/dist/input/fromRawContent.d.ts +2 -0
  44. package/dist/input/fromRawContent.js +19 -0
  45. package/dist/input/normaliseOpts.d.ts +2 -0
  46. package/dist/input/normaliseOpts.js +13 -0
  47. package/dist/program/classes/attributes.d.ts +11 -0
  48. package/dist/program/classes/attributes.js +37 -0
  49. package/dist/program/data/loadDataFiles.d.ts +4 -0
  50. package/dist/program/data/loadDataFiles.js +32 -0
  51. package/dist/program/data/retrieveData.d.ts +12 -0
  52. package/dist/program/data/retrieveData.js +27 -0
  53. package/dist/program/fs/normalisedPath.d.ts +3 -0
  54. package/dist/program/fs/normalisedPath.js +17 -0
  55. package/dist/program/fs/readFile.js +17 -0
  56. package/dist/program/fs/walkTree.d.ts +20 -0
  57. package/dist/program/fs/walkTree.js +82 -0
  58. package/dist/program/parsing/parseGitattributes.d.ts +17 -0
  59. package/dist/program/parsing/parseGitattributes.js +33 -0
  60. package/dist/program/parsing/parseGitignore.js +9 -0
  61. package/dist/program/processFiles.d.ts +9 -0
  62. package/dist/program/processFiles.js +111 -0
  63. package/dist/program/utils/pcre.js +35 -0
  64. package/dist/src/cli/output/default.d.ts +3 -0
  65. package/dist/src/cli/output/default.js +106 -0
  66. package/dist/src/cli/output/tree.d.ts +3 -0
  67. package/dist/src/cli/output/tree.js +11 -0
  68. package/dist/src/cli/runCliAnalysis.d.ts +2 -0
  69. package/dist/src/cli/runCliAnalysis.js +25 -0
  70. package/dist/src/cli/utils.d.ts +2 -0
  71. package/dist/src/cli/utils.js +9 -0
  72. package/dist/src/cli.d.ts +1 -0
  73. package/dist/src/cli.js +56 -0
  74. package/dist/src/helpers/convert-pcre.d.ts +2 -0
  75. package/dist/src/helpers/parse-gitignore.d.ts +1 -0
  76. package/dist/src/helpers/read-file.d.ts +5 -0
  77. package/dist/{helpers → src/helpers}/read-file.js +1 -1
  78. package/dist/src/index.d.ts +5 -0
  79. package/dist/src/index.js +470 -0
  80. package/dist/src/program/data/loadData.d.ts +4 -0
  81. package/dist/src/program/data/loadData.js +32 -0
  82. package/dist/src/program/fs/normalisedPath.d.ts +2 -0
  83. package/dist/src/program/fs/normalisedPath.js +7 -0
  84. package/dist/src/program/fs/readFile.d.ts +5 -0
  85. package/dist/src/program/fs/readFile.js +17 -0
  86. package/dist/src/program/fs/walkTree.d.ts +20 -0
  87. package/dist/src/program/fs/walkTree.js +82 -0
  88. package/dist/src/program/parsing/parseGitattributes.d.ts +17 -0
  89. package/dist/src/program/parsing/parseGitattributes.js +33 -0
  90. package/dist/src/program/parsing/parseGitignore.d.ts +1 -0
  91. package/dist/src/program/parsing/parseGitignore.js +9 -0
  92. package/dist/src/program/utils/pcre.d.ts +2 -0
  93. package/dist/src/program/utils/pcre.js +35 -0
  94. package/dist/src/types/schema.d.ts +37 -0
  95. package/dist/src/types/schema.js +1 -0
  96. package/dist/src/types/types.d.ts +65 -0
  97. package/dist/src/types/types.js +1 -0
  98. package/dist/types/schema.d.ts +37 -0
  99. package/dist/types/schema.js +1 -0
  100. package/dist/types/types.d.ts +84 -0
  101. package/dist/types/types.js +1 -0
  102. package/ext/generated.rb +6 -0
  103. package/ext/heuristics.yml +102 -11
  104. package/ext/languages.yml +526 -88
  105. package/ext/vendor.yml +1 -0
  106. package/package.json +14 -13
  107. package/readme.md +99 -111
  108. /package/dist/{helpers/read-file.d.ts → program/fs/readFile.d.ts} +0 -0
  109. /package/dist/{helpers/parse-gitignore.d.ts → program/parsing/parseGitignore.d.ts} +0 -0
  110. /package/dist/{helpers/convert-pcre.d.ts → program/utils/pcre.d.ts} +0 -0
  111. /package/dist/{helpers → src/helpers}/convert-pcre.js +0 -0
  112. /package/dist/{helpers → src/helpers}/load-data.d.ts +0 -0
  113. /package/dist/{helpers → src/helpers}/load-data.js +0 -0
  114. /package/dist/{helpers → src/helpers}/norm-path.d.ts +0 -0
  115. /package/dist/{helpers → src/helpers}/norm-path.js +0 -0
  116. /package/dist/{helpers → src/helpers}/parse-gitattributes.d.ts +0 -0
  117. /package/dist/{helpers → src/helpers}/parse-gitattributes.js +0 -0
  118. /package/dist/{helpers → src/helpers}/parse-gitignore.js +0 -0
  119. /package/dist/{helpers → src/helpers}/walk-tree.d.ts +0 -0
  120. /package/dist/{helpers → src/helpers}/walk-tree.js +0 -0
  121. /package/dist/{schema.d.ts → src/schema.d.ts} +0 -0
  122. /package/dist/{schema.js → src/schema.js} +0 -0
  123. /package/dist/{types.d.ts → src/types.d.ts} +0 -0
  124. /package/dist/{types.js → src/types.js} +0 -0
@@ -0,0 +1,11 @@
1
+ import { FileGlob, RelFile } from '../../types/types.js';
2
+ import { FlagAttributes } from '../parsing/parseGitattributes.js';
3
+ /** Stores parsed attribute information per file glob */
4
+ export default class Attributes {
5
+ #private;
6
+ constructor();
7
+ get attributes(): Record<FileGlob, FlagAttributes>;
8
+ add(glob: FileGlob, attributes: FlagAttributes): void;
9
+ getFlaggedGlobs(attr: keyof FlagAttributes, val: boolean): string[];
10
+ findAttrsForPath(relFilePath: RelFile): FlagAttributes | null;
11
+ }
@@ -0,0 +1,37 @@
1
+ import ignore from 'ignore';
2
+ /** Stores parsed attribute information per file glob */
3
+ export default class Attributes {
4
+ #attributes;
5
+ constructor() {
6
+ this.#attributes = {};
7
+ }
8
+ get attributes() {
9
+ return this.#attributes;
10
+ }
11
+ add(glob, attributes) {
12
+ this.#attributes[glob] = attributes;
13
+ }
14
+ getFlaggedGlobs(attr, val) {
15
+ return Object.entries(this.#attributes)
16
+ .filter(([, attrs]) => attrs[attr] === val)
17
+ .map(([glob]) => glob);
18
+ }
19
+ findAttrsForPath(relFilePath) {
20
+ const resultAttrs = {};
21
+ for (const glob in this.#attributes) {
22
+ const matchingAttrs = this.#attributes[glob];
23
+ // Check if glob matches rel path
24
+ if (ignore().add(glob).ignores(relFilePath)) {
25
+ for (const [attr, val] of Object.entries(matchingAttrs)) {
26
+ if (val != null) {
27
+ resultAttrs[attr] = val;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ if (!JSON.stringify(resultAttrs).length) {
33
+ return null;
34
+ }
35
+ return resultAttrs;
36
+ }
37
+ }
@@ -0,0 +1,4 @@
1
+ /** Nukes unused `generated.rb` file content. */
2
+ export declare function parseGeneratedDataFile(fileContent: string): string[];
3
+ /** Load a data file from github-linguist. */
4
+ export declare function loadFile(file: string, offline?: boolean): Promise<string>;
@@ -0,0 +1,32 @@
1
+ import Cache from 'node-cache';
2
+ import FS from 'node:fs';
3
+ import Path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const cache = new Cache({});
6
+ const dirname = Path.dirname(fileURLToPath(import.meta.url));
7
+ async function loadWebFile(file) {
8
+ // Return cache if it exists
9
+ const cachedContent = cache.get(file);
10
+ if (cachedContent)
11
+ return cachedContent;
12
+ // Otherwise cache the request
13
+ const dataUrl = (file) => `https://raw.githubusercontent.com/github/linguist/HEAD/lib/linguist/${file}`;
14
+ // Load file content, falling back to the local file if the request fails
15
+ const fileContent = await fetch(dataUrl(file))
16
+ .then((data) => data.text())
17
+ .catch(async () => await loadLocalFile(file));
18
+ cache.set(file, fileContent);
19
+ return fileContent;
20
+ }
21
+ async function loadLocalFile(file) {
22
+ const filePath = Path.resolve(dirname, '../../../ext', file);
23
+ return FS.promises.readFile(filePath).then((buffer) => buffer.toString());
24
+ }
25
+ /** Nukes unused `generated.rb` file content. */
26
+ export function parseGeneratedDataFile(fileContent) {
27
+ return [...(fileContent.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm) ?? [])];
28
+ }
29
+ /** Load a data file from github-linguist. */
30
+ export function loadFile(file, offline = false) {
31
+ return offline ? loadLocalFile(file) : loadWebFile(file);
32
+ }
@@ -0,0 +1,12 @@
1
+ import { HeuristicsSchema, LanguagesScema, VendorSchema } from '../../types/schema.js';
2
+ type LoadedData = {
3
+ langData: LanguagesScema;
4
+ vendorData: VendorSchema;
5
+ docData: VendorSchema;
6
+ heuristicsData: HeuristicsSchema;
7
+ generatedData: string[];
8
+ vendorPaths: string[];
9
+ };
10
+ /** Load data from github-linguist web repo or cached local file. */
11
+ export default function retrieveData(offline: boolean): Promise<LoadedData>;
12
+ export {};
@@ -0,0 +1,27 @@
1
+ import YAML from 'js-yaml';
2
+ import { loadFile, parseGeneratedDataFile } from './loadDataFiles.js';
3
+ let data = null;
4
+ async function initRetrieveData(offline) {
5
+ // Only load the data on mont
6
+ if (data)
7
+ return;
8
+ const langData = (await loadFile('languages.yml', offline).then(YAML.load));
9
+ const vendorData = (await loadFile('vendor.yml', offline).then(YAML.load));
10
+ const docData = (await loadFile('documentation.yml', offline).then(YAML.load));
11
+ const heuristicsData = (await loadFile('heuristics.yml', offline).then(YAML.load));
12
+ const generatedData = (await loadFile('generated.rb', offline).then(parseGeneratedDataFile));
13
+ const vendorPaths = [...vendorData, ...docData, ...generatedData];
14
+ data = {
15
+ langData,
16
+ vendorData,
17
+ docData,
18
+ heuristicsData,
19
+ generatedData,
20
+ vendorPaths,
21
+ };
22
+ }
23
+ /** Load data from github-linguist web repo or cached local file. */
24
+ export default async function retrieveData(offline) {
25
+ await initRetrieveData(offline);
26
+ return data;
27
+ }
@@ -0,0 +1,3 @@
1
+ export declare const normPath: (...inputPaths: string[]) => string;
2
+ export declare const normAbsPath: (...inputPaths: string[]) => string;
3
+ export declare const getFileExtension: (filePath: string) => string;
@@ -0,0 +1,17 @@
1
+ import Path from 'node:path';
2
+ export const normPath = function normalisedPath(...inputPaths) {
3
+ return Path.join(...inputPaths).replace(/\\/g, '/');
4
+ };
5
+ export const normAbsPath = function normalisedAbsolutePath(...inputPaths) {
6
+ return Path.resolve(...inputPaths).replace(/\\/g, '/');
7
+ };
8
+ export const getFileExtension = function getFileExtension(filePath) {
9
+ const extension = Path.extname(filePath).toLowerCase();
10
+ if (extension)
11
+ return extension;
12
+ const basename = Path.basename(filePath);
13
+ if (basename.startsWith('.') && basename.length > 1 && basename.indexOf('.', 1) === -1) {
14
+ return basename.toLowerCase();
15
+ }
16
+ return '';
17
+ };
@@ -0,0 +1,17 @@
1
+ import FS from 'node:fs';
2
+ /**
3
+ * Read part of a file on disc.
4
+ * @throws 'EPERM' if the file is not readable.
5
+ */
6
+ export default async function readFileChunk(filename, onlyFirstLine = false) {
7
+ const chunkSize = 100;
8
+ const stream = FS.createReadStream(filename, { highWaterMark: chunkSize });
9
+ let content = '';
10
+ for await (const data of stream) { // may throw
11
+ content += data.toString();
12
+ if (onlyFirstLine) {
13
+ return content.split(/\r?\n/)[0];
14
+ }
15
+ }
16
+ return content;
17
+ }
@@ -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 walkTree(data: WalkInput): WalkOutput;
20
+ export {};
@@ -0,0 +1,82 @@
1
+ import FS from 'node:fs';
2
+ import Path from 'node:path';
3
+ import parseGitignore from '../parsing/parseGitignore.js';
4
+ import { normAbsPath, normPath } from './normalisedPath.js';
5
+ let allFiles;
6
+ let allFolders;
7
+ /** Generate list of files in a directory. */
8
+ export default function walkTree(data) {
9
+ const { init, commonRoot, folderRoots, folders, ignored } = data;
10
+ // Initialise files and folders lists
11
+ if (init) {
12
+ allFiles = new Set();
13
+ allFolders = new Set();
14
+ }
15
+ // Walk tree of a folder
16
+ if (folders.length === 1) {
17
+ const folder = folders[0];
18
+ const localRoot = folderRoots[0].replace(commonRoot, '').replace(/^\//, '');
19
+ // Get list of files and folders inside this folder
20
+ const files = FS.readdirSync(folder).map((file) => {
21
+ // Create path relative to root
22
+ const base = normAbsPath(folder, file).replace(commonRoot, '.');
23
+ // Add trailing slash to mark directories
24
+ const isDir = FS.lstatSync(Path.resolve(commonRoot, base)).isDirectory();
25
+ return isDir ? `${base}/` : base;
26
+ });
27
+ // Read and apply gitignores
28
+ const gitignoreFilename = normPath(folder, '.gitignore');
29
+ if (FS.existsSync(gitignoreFilename)) {
30
+ const gitignoreContents = FS.readFileSync(gitignoreFilename, 'utf-8');
31
+ const ignoredPaths = parseGitignore(gitignoreContents);
32
+ const rootRelIgnoredPaths = ignoredPaths.map((ignorePath) =>
33
+ // get absolute path of the ignore glob
34
+ normPath(folder, ignorePath)
35
+ // convert abs ignore glob to be relative to the root folder
36
+ .replace(commonRoot + '/', ''));
37
+ ignored.add(rootRelIgnoredPaths);
38
+ }
39
+ // Add gitattributes if present
40
+ const gitattributesPath = normPath(folder, '.gitattributes');
41
+ if (FS.existsSync(gitattributesPath)) {
42
+ allFiles.add(gitattributesPath);
43
+ }
44
+ // Loop through files and folders
45
+ for (const file of files) {
46
+ // Create absolute path for disc operations
47
+ const path = normAbsPath(commonRoot, file);
48
+ const localPath = localRoot ? file.replace(`./${localRoot}/`, '') : file.replace('./', '');
49
+ // Skip if nonexistant
50
+ const nonExistant = !FS.existsSync(path);
51
+ if (nonExistant)
52
+ continue;
53
+ // Skip if marked in gitignore
54
+ const isIgnored = ignored.test(localPath).ignored;
55
+ if (isIgnored)
56
+ continue;
57
+ // Add absolute folder path to list
58
+ allFolders.add(normAbsPath(folder));
59
+ // Check if this is a folder or file
60
+ if (file.endsWith('/')) {
61
+ // Recurse into subfolders
62
+ allFolders.add(path);
63
+ walkTree({ init: false, commonRoot, folderRoots, folders: [path], ignored });
64
+ }
65
+ else {
66
+ // Add file path to list
67
+ allFiles.add(path);
68
+ }
69
+ }
70
+ }
71
+ // Recurse into all folders
72
+ else {
73
+ for (const i in folders) {
74
+ walkTree({ init: false, commonRoot, folderRoots: [folderRoots[i]], folders: [folders[i]], ignored });
75
+ }
76
+ }
77
+ // Return absolute files and folders lists
78
+ return {
79
+ files: [...allFiles].map((file) => file.replace(/^\./, commonRoot)),
80
+ folders: [...allFolders],
81
+ };
82
+ }
@@ -0,0 +1,17 @@
1
+ import * as T from '../../types/types.js';
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 parseGitattributes(content: string, folderRoot?: string): ParsedGitattributes;
@@ -0,0 +1,33 @@
1
+ import { normPath } from '../fs/normalisedPath.js';
2
+ /**
3
+ * Parses a gitattributes file.
4
+ */
5
+ export default function parseGitattributes(content, folderRoot = '.') {
6
+ const output = [];
7
+ for (const rawLine of content.split('\n')) {
8
+ const line = rawLine.replace(/#.*/, '').trim();
9
+ if (!line)
10
+ continue;
11
+ const parts = line.split(/\s+/g);
12
+ const fileGlob = parts[0];
13
+ const relFileGlob = normPath(folderRoot, fileGlob);
14
+ const attrParts = parts.slice(1);
15
+ const isTrue = (str) => !str.startsWith('-') && !str.endsWith('=false');
16
+ const isFalse = (str) => str.startsWith('-') || str.endsWith('=false');
17
+ const trueParts = (str) => attrParts.filter(part => part.includes(str) && isTrue(part));
18
+ const falseParts = (str) => attrParts.filter(part => part.includes(str) && isFalse(part));
19
+ const hasTrueParts = (str) => trueParts(str).length > 0;
20
+ const hasFalseParts = (str) => falseParts(str).length > 0;
21
+ const boolOrNullVal = (str) => hasTrueParts(str) ? true : hasFalseParts(str) ? false : null;
22
+ const attrs = {
23
+ 'generated': boolOrNullVal('linguist-generated'),
24
+ 'vendored': boolOrNullVal('linguist-vendored'),
25
+ 'documentation': boolOrNullVal('linguist-documentation'),
26
+ 'detectable': boolOrNullVal('linguist-detectable'),
27
+ 'binary': hasTrueParts('binary') || hasFalseParts('text') ? true : hasFalseParts('binary') || hasTrueParts('text') ? false : null,
28
+ 'language': trueParts('linguist-language').at(-1)?.split('=')[1] ?? null,
29
+ };
30
+ output.push({ glob: relFileGlob, attrs });
31
+ }
32
+ return output;
33
+ }
@@ -0,0 +1,9 @@
1
+ export default function parseGitignore(content) {
2
+ const readableData = content
3
+ // Remove comments unless escaped
4
+ .replace(/(?<!\\)#.+/g, '')
5
+ // Remove whitespace unless escaped
6
+ .replace(/(?:(?<!\\)\s)+$/g, '');
7
+ const arrayData = readableData.split(/\r?\n/).filter(data => data);
8
+ return arrayData;
9
+ }
@@ -0,0 +1,9 @@
1
+ import * as T from '../types/types.js';
2
+ import Attributes from './classes/attributes.js';
3
+ type ProcessFilesResult = {
4
+ files: T.AbsFile[];
5
+ manualAttributes: Attributes;
6
+ relPath(file: T.AbsFile): T.RelFile;
7
+ };
8
+ export default function processFiles(input: string[], opts: T.Options, useRawContent: boolean, vendorPaths: string[]): Promise<ProcessFilesResult>;
9
+ export {};
@@ -0,0 +1,111 @@
1
+ import commonPrefix from 'common-path-prefix';
2
+ import ignore from 'ignore';
3
+ import FS from 'node:fs';
4
+ import Path from 'node:path';
5
+ import Attributes from './classes/attributes.js';
6
+ import { normPath } from './fs/normalisedPath.js';
7
+ import readFileChunk from './fs/readFile.js';
8
+ import walkTree from './fs/walkTree.js';
9
+ import parseGitattributes from './parsing/parseGitattributes.js';
10
+ const binaryData = JSON.parse(FS.readFileSync(new URL('../../node_modules/binary-extensions/binary-extensions.json', import.meta.url), 'utf-8'));
11
+ export default async function processFiles(input, opts, useRawContent, vendorPaths) {
12
+ // Set a common root path so that vendor paths do not incorrectly match parent folders
13
+ const resolvedInput = input.map((path) => normPath(Path.resolve(path)));
14
+ const commonRoot = (input.length > 1 ? commonPrefix(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
15
+ const relPath = (file) => (useRawContent ? file : normPath(Path.relative(commonRoot, file)));
16
+ const unRelPath = (file) => (useRawContent ? file : normPath(Path.resolve(commonRoot, file)));
17
+ // Other helper functions
18
+ const filterOutIgnored = (files, ignored) => ignored.filter(files.map((file) => relPath(file))).map((file) => unRelPath(file));
19
+ //*PREPARE FILES AND DATA*//
20
+ // Prepare list of ignored files
21
+ const ignored = ignore();
22
+ ignored.add('.git/');
23
+ ignored.add(opts.ignoredFiles ?? []);
24
+ const regexIgnores = opts.keepVendored ? [] : vendorPaths.map((path) => RegExp(path, 'i'));
25
+ // Load file paths and folders
26
+ let files;
27
+ if (useRawContent) {
28
+ // Uses raw file content
29
+ files = input;
30
+ }
31
+ else {
32
+ // Uses directory on disc
33
+ const data = walkTree({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, ignored });
34
+ files = data.files;
35
+ }
36
+ // Fetch and normalise gitattributes data of all subfolders and save to metadata
37
+ const manualAttributes = new Attributes();
38
+ if (!useRawContent && opts.checkAttributes) {
39
+ const nestedAttrFiles = files.filter((file) => file.endsWith('.gitattributes'));
40
+ for (const attrFile of nestedAttrFiles) {
41
+ const relAttrFile = relPath(attrFile);
42
+ const relAttrFolder = Path.dirname(relAttrFile);
43
+ const contents = await readFileChunk(attrFile);
44
+ const parsed = parseGitattributes(contents, relAttrFolder);
45
+ for (const { glob, attrs } of parsed) {
46
+ manualAttributes.add(glob, attrs);
47
+ }
48
+ }
49
+ }
50
+ // Remove files that are linguist-ignored via regex by default unless explicitly unignored in gitattributes
51
+ const filesToIgnore = [];
52
+ for (const file of files) {
53
+ const relFile = relPath(file);
54
+ const isRegexIgnored = regexIgnores.some((pattern) => pattern.test(relFile));
55
+ if (!isRegexIgnored) {
56
+ // Checking overrides is moot if file is not even marked as ignored by default
57
+ continue;
58
+ }
59
+ const fileAttrs = manualAttributes.findAttrsForPath(relPath(file));
60
+ if (fileAttrs?.generated === false || fileAttrs?.vendored === false) {
61
+ // File is explicitly marked as *not* to be ignored
62
+ // do nothing
63
+ }
64
+ else {
65
+ filesToIgnore.push(file);
66
+ }
67
+ }
68
+ files = files.filter((file) => !filesToIgnore.includes(file));
69
+ // Apply vendor file path matches and filter out vendored files
70
+ if (!opts.keepVendored) {
71
+ // Get data of files that have been manually marked with metadata
72
+ const vendorTrueGlobs = [
73
+ ...manualAttributes.getFlaggedGlobs('vendored', true),
74
+ ...manualAttributes.getFlaggedGlobs('generated', true),
75
+ ...manualAttributes.getFlaggedGlobs('documentation', true),
76
+ ];
77
+ const vendorFalseGlobs = [
78
+ ...manualAttributes.getFlaggedGlobs('vendored', false),
79
+ ...manualAttributes.getFlaggedGlobs('generated', false),
80
+ ...manualAttributes.getFlaggedGlobs('documentation', false),
81
+ ];
82
+ // Set up glob ignore object to use for expanding globs to match files
83
+ const vendorTrueIgnore = ignore().add(vendorTrueGlobs);
84
+ const vendorFalseIgnore = ignore().add(vendorFalseGlobs);
85
+ // Remove all files marked as vendored by default
86
+ const excludedFiles = files.filter((file) => vendorPaths.some((pathPtn) => RegExp(pathPtn, 'i').test(relPath(file))));
87
+ files = files.filter((file) => !excludedFiles.includes(file));
88
+ // Re-add removed files that are overridden manually in gitattributes
89
+ const overriddenExcludedFiles = excludedFiles.filter((file) => vendorFalseIgnore.ignores(relPath(file)));
90
+ files.push(...overriddenExcludedFiles);
91
+ // Remove files explicitly marked as vendored in gitattributes
92
+ files = files.filter((file) => !vendorTrueIgnore.ignores(relPath(file)));
93
+ }
94
+ // Filter out binary files
95
+ if (!opts.keepBinary) {
96
+ // Filter out files that are binary by default
97
+ files = files.filter((file) => !binaryData.some((ext) => file.endsWith('.' + ext)));
98
+ // Filter out manually specified binary files
99
+ const binaryIgnored = ignore().add(manualAttributes.getFlaggedGlobs('binary', true));
100
+ files = filterOutIgnored(files, binaryIgnored);
101
+ // Re-add files manually marked not as binary
102
+ const binaryUnignored = ignore().add(manualAttributes.getFlaggedGlobs('binary', false));
103
+ const unignoredList = filterOutIgnored(files, binaryUnignored);
104
+ files.push(...unignoredList);
105
+ }
106
+ return {
107
+ files,
108
+ manualAttributes,
109
+ relPath,
110
+ };
111
+ }
@@ -0,0 +1,35 @@
1
+ /** Convert a PCRE regex into JS. */
2
+ export default function pcre(regex) {
3
+ let finalRegex = regex;
4
+ const replace = (search, replace) => finalRegex = finalRegex.replace(search, replace);
5
+ const finalFlags = new Set();
6
+ // Convert inline flag declarations
7
+ const inlineMatches = regex.matchAll(/\?(-)?([a-z]):/g);
8
+ const startMatches = regex.matchAll(/\(\?(-)?([a-z]+)\)/g);
9
+ for (const [match, isNegative, flags] of [...inlineMatches, ...startMatches]) {
10
+ replace(match, '');
11
+ const func = (flag) => isNegative ? finalFlags.delete(flag) : finalFlags.add(flag);
12
+ [...flags].forEach(func);
13
+ }
14
+ // Remove PCRE-only syntax
15
+ replace(/([*+]){2}/g, '$1');
16
+ replace(/\(\?>/g, '(?:');
17
+ // Remove start/end-of-file markers
18
+ if (/\\[AZ]/.test(finalRegex)) {
19
+ replace(/\\A/g, '^');
20
+ replace(/\\Z/g, '$');
21
+ finalFlags.delete('m');
22
+ }
23
+ else {
24
+ finalFlags.add('m');
25
+ }
26
+ // Reformat free-spacing mode
27
+ if (finalFlags.has('x')) {
28
+ finalFlags.delete('x');
29
+ replace(/#.+/g, '');
30
+ replace(/^\s+|\s+$|\n/gm, '');
31
+ replace(/\s+/g, ' ');
32
+ }
33
+ // Return final regex
34
+ return RegExp(finalRegex, [...finalFlags].join(''));
35
+ }
@@ -0,0 +1,3 @@
1
+ import { OptionValues } from 'commander';
2
+ import { Results } from '../../types/types.js';
3
+ export default function defaultOutput(args: OptionValues, data: Results): Promise<void>;
@@ -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,3 @@
1
+ import { OptionValues } from 'commander';
2
+ import { Results } from '../../types/types.js';
3
+ export default function treeOutput(args: OptionValues, data: Results): void;
@@ -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,2 @@
1
+ import { OptionValues } from 'commander';
2
+ export default function runCliAnalysis(args: OptionValues): Promise<void>;
@@ -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
+ }