forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1export type DevDependencySuggestionReason = 'known-package' | 'readme-hint'
2
3export interface DevDependencySuggestion {
4 recommended: boolean
5 reason?: DevDependencySuggestionReason
6}
7
8const KNOWN_DEV_DEPENDENCY_PACKAGES = new Set<string>([
9 'biome',
10 'chai',
11 'eslint',
12 'esbuild',
13 'husky',
14 'jest',
15 'lint-staged',
16 'mocha',
17 'oxc',
18 'oxfmt',
19 'oxlint',
20 'playwright',
21 'prettier',
22 'rolldown',
23 'rollup',
24 'stylelint',
25 'ts-jest',
26 'ts-node',
27 'tsx',
28 'turbo',
29 'typescript',
30 'vite',
31 'vitest',
32 'webpack',
33])
34
35const KNOWN_DEV_DEPENDENCY_PACKAGE_PREFIXES = [
36 '@typescript-eslint/',
37 'eslint-',
38 'prettier-',
39 'vite-',
40 'webpack-',
41 'babel-',
42]
43
44function isKnownDevDependencyPackage(packageName: string): boolean {
45 const normalized = packageName.toLowerCase()
46 if (normalized.startsWith('@types/')) {
47 return true
48 }
49 // Match scoped packages by name segment, e.g. @scope/eslint-config
50 const namePart = normalized.includes('/') ? normalized.split('/').pop() : normalized
51 if (!namePart) return false
52
53 return (
54 KNOWN_DEV_DEPENDENCY_PACKAGES.has(normalized) ||
55 KNOWN_DEV_DEPENDENCY_PACKAGES.has(namePart) ||
56 KNOWN_DEV_DEPENDENCY_PACKAGE_PREFIXES.some(prefix =>
57 prefix.startsWith('@') ? normalized.startsWith(prefix) : namePart.startsWith(prefix),
58 )
59 )
60}
61
62function escapeRegExp(text: string): string {
63 return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
64}
65
66function hasReadmeDevInstallHint(packageName: string, readmeContent?: string | null): boolean {
67 if (!readmeContent) return false
68
69 const escapedName = escapeRegExp(packageName)
70 const escapedNpmName = escapeRegExp(`npm:${packageName}`)
71 const packageSpec = `(?:${escapedName}|${escapedNpmName})(?:@[\\w.-]+)?`
72
73 const patterns = [
74 // npm install -D pkg / pnpm add --save-dev pkg
75 new RegExp(
76 String.raw`(?:npm|pnpm|yarn|bun|vlt)\s+(?:install|add|i)\s+(?:--save-dev|--dev|-d)\s+${packageSpec}`,
77 'i',
78 ),
79 // npm install pkg --save-dev / pnpm add pkg -D
80 new RegExp(
81 String.raw`(?:npm|pnpm|yarn|bun|vlt)\s+(?:install|add|i)\s+${packageSpec}\s+(?:--save-dev|--dev|-d)`,
82 'i',
83 ),
84 // deno add -D npm:pkg
85 new RegExp(String.raw`deno\s+add\s+(?:--dev|-D)\s+${packageSpec}`, 'i'),
86 ]
87
88 return patterns.some(pattern => pattern.test(readmeContent))
89}
90
91export function getDevDependencySuggestion(
92 packageName: string,
93 readmeContent?: string | null,
94): DevDependencySuggestion {
95 if (isKnownDevDependencyPackage(packageName)) {
96 return {
97 recommended: true,
98 reason: 'known-package',
99 }
100 }
101
102 if (hasReadmeDevInstallHint(packageName, readmeContent)) {
103 return {
104 recommended: true,
105 reason: 'readme-hint',
106 }
107 }
108
109 return { recommended: false }
110}