diff --git a/app/components/Code/DirectoryListing.vue b/app/components/Code/DirectoryListing.vue index cae0f3635e..ea47dd1adb 100644 --- a/app/components/Code/DirectoryListing.vue +++ b/app/components/Code/DirectoryListing.vue @@ -2,6 +2,7 @@ import type { RouteLocationRaw } from 'vue-router' import type { RouteNamedMap } from 'vue-router/auto-routes' import { ADDITIONAL_ICONS, getFileIcon } from '~/utils/file-icons' +import { isPossiblyUnnecessaryContent } from '~/utils/package-content-hints' const props = defineProps<{ tree: PackageFileTree[] @@ -10,6 +11,8 @@ const props = defineProps<{ baseRoute: Pick }>() +const { t } = useI18n() + // Get the current directory's contents const currentContents = computed(() => { if (!props.currentPath) { @@ -103,6 +106,11 @@ const bytesFormatter = useBytesFormatter() @@ -117,7 +125,21 @@ const bytesFormatter = useBytesFormatter() /> - {{ node.name }} + {{ node.name }} + diff --git a/app/utils/package-content-hints.ts b/app/utils/package-content-hints.ts new file mode 100644 index 0000000000..8df8287950 --- /dev/null +++ b/app/utils/package-content-hints.ts @@ -0,0 +1,70 @@ +/** + * Heuristics for identifying files and directories that are commonly shipped + * to npm by accident: editor configs, lint/format settings, test trees, + * local-only env files, etc. Used by the package code browser to surface + * "this is probably bloat" hints next to affected nodes. + * + * Source list: https://github.com/npmx-dev/npmx.dev/issues/2582 + */ + +const POSSIBLY_UNNECESSARY_FILES: ReadonlySet = new Set([ + '.editorconfig', + '.prettierignore', + '.eslintignore', + '.gitignore', + '.gitattributes', + 'tsconfig.json', + '.node-version', + '.nvmrc', + 'mise.toml', + '.tool-versions', + '.env', + '.env.local', + '.env.development', + '.env.development.local', + '.env.test', + '.env.test.local', + '.env.production.local', + '.nycrc', + 'nyc.json', +]) + +const POSSIBLY_UNNECESSARY_DIRECTORIES: ReadonlySet = new Set([ + '.vscode', + '.claude', + '.github', + '.idea', + '.zed', + 'test', + 'tests', + '__tests__', + 'spec', + 'specs', +]) + +const POSSIBLY_UNNECESSARY_DIRECTORY_PATTERNS: readonly RegExp[] = [/^__.+__$/] + +const POSSIBLY_UNNECESSARY_PATTERNS: readonly RegExp[] = [ + /^eslint\.config\.(?:js|cjs|mjs|ts|mts|cts)$/, + /^\.eslintrc(?:\.(?:json|js|cjs|yml|yaml))?$/, + /^\.prettierrc(?:\.(?:json|js|cjs|yml|yaml|toml))?$/, + /^prettier\.config\.(?:js|cjs|mjs|ts|mts|cts)$/, + /^oxlint\.config\.(?:js|cjs|mjs|ts|mts|cts)$/, + /^\.oxlintrc(?:\.(?:json|js|cjs|yml|yaml))?$/, + /^oxfmt\.config\.(?:js|cjs|mjs|ts|mts|cts)$/, + /^\.oxfmtrc(?:\.(?:json|js|cjs|yml|yaml))?$/, + // Match common dot-prefixed config files without flagging all dotfiles; + // files like .npmrc, .npmignore, and .gitkeep can be intentional artifacts. + /^\.(?!npmrc$)[a-z][a-z0-9_-]*rc$/, + /^\.(?!npmrc\.)[a-z][a-z0-9_-]*rc\.(?:json|js|cjs|mjs|yml|yaml|toml)$/, + /^\.[a-z][a-z0-9_-]*\.config\.(?:js|cjs|mjs|ts|mts|cts)$/, +] + +export function isPossiblyUnnecessaryContent(name: string, type: 'file' | 'directory'): boolean { + if (type === 'directory') { + if (POSSIBLY_UNNECESSARY_DIRECTORIES.has(name)) return true + return POSSIBLY_UNNECESSARY_DIRECTORY_PATTERNS.some(pattern => pattern.test(name)) + } + if (POSSIBLY_UNNECESSARY_FILES.has(name)) return true + return POSSIBLY_UNNECESSARY_PATTERNS.some(pattern => pattern.test(name)) +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index f93a6d7472..8c3dd40e87 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -1025,7 +1025,8 @@ }, "file_path": "File path", "binary_file": "Binary file", - "binary_rendering_warning": "File type \"{contentType}\" is not supported for preview." + "binary_rendering_warning": "File type \"{contentType}\" is not supported for preview.", + "possibly_unnecessary": "May be unnecessary in a published package" }, "badges": { "provenance": { diff --git a/i18n/schema.json b/i18n/schema.json index 0338a9a4d3..f72074675a 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -3081,6 +3081,9 @@ }, "binary_rendering_warning": { "type": "string" + }, + "possibly_unnecessary": { + "type": "string" } }, "additionalProperties": false diff --git a/test/unit/app/utils/package-content-hints.spec.ts b/test/unit/app/utils/package-content-hints.spec.ts new file mode 100644 index 0000000000..05ab4637f3 --- /dev/null +++ b/test/unit/app/utils/package-content-hints.spec.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest' +import { isPossiblyUnnecessaryContent } from '~/utils/package-content-hints' + +describe('isPossiblyUnnecessaryContent', () => { + it('flags well-known editor and config filenames', () => { + expect(isPossiblyUnnecessaryContent('.editorconfig', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.gitignore', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.gitattributes', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.prettierignore', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.eslintignore', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('tsconfig.json', 'file')).toBe(true) + }) + + it('flags local environment files', () => { + expect(isPossiblyUnnecessaryContent('.env', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.env.local', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.env.development', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.env.test.local', 'file')).toBe(true) + }) + + it('flags node version files and coverage configs', () => { + expect(isPossiblyUnnecessaryContent('.node-version', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.nvmrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('mise.toml', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.tool-versions', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.nycrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('nyc.json', 'file')).toBe(true) + }) + + it('matches ESLint configuration patterns', () => { + expect(isPossiblyUnnecessaryContent('eslint.config.js', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('eslint.config.mjs', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('eslint.config.ts', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.eslintrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.eslintrc.json', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.eslintrc.yml', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.eslintrc.cjs', 'file')).toBe(true) + }) + + it('matches Prettier configuration patterns', () => { + expect(isPossiblyUnnecessaryContent('.prettierrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.prettierrc.json', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.prettierrc.yml', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('prettier.config.js', 'file')).toBe(true) + }) + + it('matches oxlint and oxfmt configuration patterns', () => { + expect(isPossiblyUnnecessaryContent('oxlint.config.ts', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.oxlintrc.json', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('oxfmt.config.ts', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.oxfmtrc.json', 'file')).toBe(true) + }) + + it('matches common dot-prefixed configuration patterns without over-flagging', () => { + expect(isPossiblyUnnecessaryContent('.babelrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.stylelintrc.json', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.browserslistrc', 'file')).toBe(true) + expect(isPossiblyUnnecessaryContent('.tailwind.config.js', 'file')).toBe(true) + // .npmrc is sometimes an intentional shipped artifact; do not flag it. + expect(isPossiblyUnnecessaryContent('.npmrc', 'file')).toBe(false) + }) + + it('flags editor and CI directories', () => { + expect(isPossiblyUnnecessaryContent('.vscode', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('.claude', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('.github', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('.idea', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('.zed', 'directory')).toBe(true) + }) + + it('flags test directories', () => { + expect(isPossiblyUnnecessaryContent('test', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('tests', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('__tests__', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('__mocks__', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('__snapshots__', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('spec', 'directory')).toBe(true) + expect(isPossiblyUnnecessaryContent('specs', 'directory')).toBe(true) + }) + + it('does not flag ordinary source files or directories', () => { + expect(isPossiblyUnnecessaryContent('index.js', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('package.json', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('README.md', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('LICENSE', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('main.ts', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('src', 'directory')).toBe(false) + expect(isPossiblyUnnecessaryContent('lib', 'directory')).toBe(false) + expect(isPossiblyUnnecessaryContent('dist', 'directory')).toBe(false) + }) + + it('does not confuse a directory name passed as a file with the directory match', () => { + expect(isPossiblyUnnecessaryContent('.vscode', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('test', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('tsconfig.json', 'directory')).toBe(false) + }) + + it('treats matching as case-sensitive (npm packages live in a case-sensitive world)', () => { + expect(isPossiblyUnnecessaryContent('TSCONFIG.JSON', 'file')).toBe(false) + expect(isPossiblyUnnecessaryContent('.VSCode', 'directory')).toBe(false) + }) +})