diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx
index 69ee5d65..bed48de0 100644
--- a/components/Icons/index.tsx
+++ b/components/Icons/index.tsx
@@ -1253,3 +1253,23 @@ export function SecurityIcon({ style }: IconProps) {
);
}
+
+export function LintToolsIcon({ style }: IconProps) {
+ return (
+
+ );
+}
diff --git a/components/Library/MetaData.tsx b/components/Library/MetaData.tsx
index 964f677c..87545f00 100644
--- a/components/Library/MetaData.tsx
+++ b/components/Library/MetaData.tsx
@@ -12,6 +12,7 @@ import {
ForkIcon,
IssueIcon,
LicenseIcon,
+ LintToolsIcon,
ModuleIcon,
NativeCodeIcon,
NightlyTestIcon,
@@ -25,7 +26,7 @@ import { ConfigPluginContent } from '~/components/Library/ConfigPlugin';
import { Tooltip } from '~/components/Tooltip';
import { type LibraryDataEntryType, type LibraryType, type MetadataEntryType } from '~/types';
import { formatBytes } from '~/util/formatBytes';
-import { formatPackageManager, pluralize } from '~/util/strings';
+import { formatLintTools, formatPackageManager, pluralize } from '~/util/strings';
import tw from '~/util/tailwind';
import { DirectoryScore } from './DirectoryScore';
@@ -266,6 +267,16 @@ function generateSecondaryData(library: LibraryType, skipExamples: boolean): Met
tooltip: 'Package manager',
}
: null,
+ skipExamples && (github?.lintTools?.length ?? 0) > 0
+ ? {
+ id: 'lintTools',
+ icon: ,
+ content: (
+
{github.lintTools?.map(formatLintTools).join(', ')}
+ ),
+ tooltip: 'Lint tools',
+ }
+ : null,
];
}
@@ -291,7 +302,7 @@ function MetaData({ library, secondary, skipExamples = false }: Props) {
);
return tooltip ? (
-
+
{tooltip}
) : (
diff --git a/scripts/fetch-github-data.ts b/scripts/fetch-github-data.ts
index 93337eae..10f94dfe 100644
--- a/scripts/fetch-github-data.ts
+++ b/scripts/fetch-github-data.ts
@@ -4,6 +4,7 @@ import { type LibraryLicenseType, type LibraryType } from '~/types';
import detectModuleType from '~/util/github/detectModuleType';
import hasConfigPlugin from '~/util/github/hasConfigPlugin';
import {
+ detectLintStack,
detectPackageManager,
hasCCFile,
hasChangelogFile,
@@ -252,5 +253,6 @@ function createRepoDataWithResponse(json: any, monorepo: boolean): LibraryType['
json.packageManager ??
detectPackageManager(json.files) ??
detectPackageManager(json.rootFiles),
+ lintTools: detectLintStack(json.files) ?? detectLintStack(json.rootFiles),
};
}
diff --git a/types/index.ts b/types/index.ts
index 08365383..f0338e87 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -101,6 +101,7 @@ export type LibraryType = LibraryDataEntryType & {
configPlugin?: boolean;
moduleType?: 'expo' | 'nitro' | 'turbo';
packageManager?: string;
+ lintTools?: string[];
urls: {
repo: string;
homepage?: string | null;
diff --git a/util/github/hasFiles.ts b/util/github/hasFiles.ts
index 28647cee..c4237446 100644
--- a/util/github/hasFiles.ts
+++ b/util/github/hasFiles.ts
@@ -2,41 +2,41 @@ import { type RepositoryTreeNode } from '~/types';
type RootFiles = { entries: RepositoryTreeNode[] } | null;
-function hasMatchingFiles(rootFiles: RootFiles, fileNames: string[]) {
+function hasMatchingFiles(rootFiles: RootFiles, fileNames: Set) {
if (!rootFiles?.entries.length) {
return false;
}
return rootFiles.entries.some(
- node => fileNames.includes(node.name.toLowerCase()) && node.type === 'blob'
+ node => fileNames.has(node.name.toLowerCase()) && node.type === 'blob'
);
}
-const README_FILE_NAMES = ['readme', 'readme.md', 'readme.mdx'];
+const README_FILE_NAMES = new Set(['readme', 'readme.md', 'readme.mdx']);
export function hasReadmeFile(rootFiles: RootFiles) {
return hasMatchingFiles(rootFiles, README_FILE_NAMES);
}
-const CONTRIBUTING_FILE_NAMES = ['contributing.md'];
+const CONTRIBUTING_FILE_NAMES = new Set(['contributing.md']);
export function hasContributingFile(rootFiles: RootFiles) {
return hasMatchingFiles(rootFiles, CONTRIBUTING_FILE_NAMES);
}
-const CHANGELOG_FILE_NAMES = ['changelog.md'];
+const CHANGELOG_FILE_NAMES = new Set(['changelog.md']);
export function hasChangelogFile(rootFiles: RootFiles) {
return hasMatchingFiles(rootFiles, CHANGELOG_FILE_NAMES);
}
-const CC_FILE_NAMES = ['code_of_conduct.md'];
+const CC_FILE_NAMES = new Set(['code_of_conduct.md']);
export function hasCCFile(rootFiles: RootFiles) {
return hasMatchingFiles(rootFiles, CC_FILE_NAMES);
}
-const SECURITY_FILE_NAMES = ['security.md'];
+const SECURITY_FILE_NAMES = new Set(['security.md']);
export function hasSecurityFile(rootFiles: RootFiles) {
return hasMatchingFiles(rootFiles, SECURITY_FILE_NAMES);
@@ -44,24 +44,89 @@ export function hasSecurityFile(rootFiles: RootFiles) {
export function detectPackageManager(rootFiles: RootFiles) {
// LOCKS
- if (hasMatchingFiles(rootFiles, ['bun.lock', 'bun.lockb'])) {
+ if (hasMatchingFiles(rootFiles, new Set(['bun.lock', 'bun.lockb']))) {
return 'bun';
- } else if (hasMatchingFiles(rootFiles, ['pnpm-lock.yaml', 'shrinkwrap.yaml'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['pnpm-lock.yaml', 'shrinkwrap.yaml']))) {
return 'pnpm';
- } else if (hasMatchingFiles(rootFiles, ['yarn.lock'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['yarn.lock']))) {
return 'yarn';
- } else if (hasMatchingFiles(rootFiles, ['package-lock.json', 'shrinkwrap.json'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['package-lock.json', 'shrinkwrap.json']))) {
return 'npm';
}
// CONFIGS
- if (hasMatchingFiles(rootFiles, ['bunfig.toml'])) {
+ if (hasMatchingFiles(rootFiles, new Set(['bunfig.toml']))) {
return 'bun';
- } else if (hasMatchingFiles(rootFiles, ['.pnpm-workspace.yaml', '.pnpmfile.cjs'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['.pnpm-workspace.yaml', '.pnpmfile.cjs']))) {
return 'pnpm';
- } else if (hasMatchingFiles(rootFiles, ['.yarnrc'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['.yarnrc']))) {
return 'yarn@1';
- } else if (hasMatchingFiles(rootFiles, ['.yarnrc.yml'])) {
+ } else if (hasMatchingFiles(rootFiles, new Set(['.yarnrc.yml']))) {
return 'yarn@2+';
}
return undefined;
}
+
+const OXLINT_CONFIG_FILE_NAMES = new Set([
+ '.oxlintrc.json',
+ '.oxlintrc.jsonc',
+ 'oxlint.config.mts',
+ 'oxlint.config.ts',
+ 'oxlint.config.mjs',
+ 'oxlint.config.js',
+]);
+const OXFMT_CONFIG_FILE_NAMES = new Set([
+ '.oxfmtrc.json',
+ '.oxfmtrc.jsonc',
+ 'oxfmt.config.mts',
+ 'oxfmt.config.ts',
+ 'oxfmt.config.mjs',
+ 'oxfmt.config.js',
+]);
+const ESLINT_CONFIG_FILE_NAMES = new Set([
+ 'eslint.config.js',
+ 'eslint.config.mjs',
+ 'eslint.config.cjs',
+ 'eslint.config.ts',
+ 'eslint.config.mjs',
+ 'eslint.config.cjs',
+ '.eslintrc.js',
+ '.eslintrc.cjs',
+ '.eslintrc.json',
+ '.eslintrc.yaml',
+ '.eslintrc.yml',
+]);
+const PRETTIER_CONFIG_FILE_NAMES = new Set([
+ '.prettierrc',
+ '.prettierrc.json',
+ '.prettierrc.json5',
+ '.prettierrc.yaml',
+ '.prettierrc.yml',
+ '.prettierrc.js',
+ '.prettierrc.cjs',
+ '.prettierrc.toml',
+ '.prettierrc.mjs',
+ 'prettier.config.js',
+ 'prettier.config.cjs',
+ 'prettier.config.mjs',
+]);
+const BIOME_CONFIG_FILE_NAMES = new Set(['biome.json', 'biome.jsonc']);
+
+export function detectLintStack(rootFiles: RootFiles) {
+ const lintTools = [];
+ if (hasMatchingFiles(rootFiles, OXLINT_CONFIG_FILE_NAMES)) {
+ lintTools.push('oxlint');
+ }
+ if (hasMatchingFiles(rootFiles, OXFMT_CONFIG_FILE_NAMES)) {
+ lintTools.push('oxfmt');
+ }
+ if (hasMatchingFiles(rootFiles, ESLINT_CONFIG_FILE_NAMES)) {
+ lintTools.push('eslint');
+ }
+ if (hasMatchingFiles(rootFiles, PRETTIER_CONFIG_FILE_NAMES)) {
+ lintTools.push('prettier');
+ }
+ if (hasMatchingFiles(rootFiles, BIOME_CONFIG_FILE_NAMES)) {
+ lintTools.push('biome');
+ }
+ return lintTools;
+}
diff --git a/util/strings.ts b/util/strings.ts
index 095b6d67..32573b2b 100644
--- a/util/strings.ts
+++ b/util/strings.ts
@@ -61,6 +61,22 @@ export function formatPackageManager(pmRaw?: string) {
}
}
+export function formatLintTools(lintTool: string) {
+ if (lintTool === 'oxlint') {
+ return 'Oxlint';
+ } else if (lintTool === 'oxfmt') {
+ return 'Oxfmt';
+ } else if (lintTool === 'eslint') {
+ return 'ESLint';
+ } else if (lintTool === 'prettier') {
+ return 'Prettier';
+ } else if (lintTool === 'biome') {
+ return 'Biome';
+ } else {
+ return lintTool;
+ }
+}
+
export function childrenToText(children: ReactNode): string {
return Children.toArray(children)
.map(child => {