Skip to content

Commit 6d92129

Browse files
committed
feat: suggest installing dependency as devDependency when appropriate
Closes #530
1 parent 50ed1cd commit 6d92129

File tree

9 files changed

+277
-2
lines changed

9 files changed

+277
-2
lines changed

app/components/Terminal/Install.vue

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const props = defineProps<{
66
packageName: string
77
requestedVersion?: string | null
88
jsrInfo?: JsrPackageInfo | null
9+
readmeHtml?: string | null
910
typesPackageName?: string | null
1011
executableInfo?: { hasExecutable: boolean; primaryCommand?: string } | null
1112
createPackageInfo?: { packageName: string } | null
@@ -28,6 +29,20 @@ function getInstallPartsForPM(pmId: PackageManagerId) {
2829
})
2930
}
3031
32+
const devDependencySuggestion = computed(() =>
33+
getDevDependencySuggestion(props.packageName, props.readmeHtml),
34+
)
35+
36+
function getDevInstallPartsForPM(pmId: PackageManagerId) {
37+
return getInstallCommandParts({
38+
packageName: props.packageName,
39+
packageManager: pmId,
40+
version: props.requestedVersion,
41+
jsrInfo: props.jsrInfo,
42+
dev: true,
43+
})
44+
}
45+
3146
// Generate run command parts for a specific package manager
3247
function getRunPartsForPM(pmId: PackageManagerId, command?: string) {
3348
return getRunCommandParts({
@@ -66,7 +81,7 @@ function getTypesInstallPartsForPM(pmId: PackageManagerId) {
6681
const pm = packageManagers.find(p => p.id === pmId)
6782
if (!pm) return []
6883
69-
const devFlag = pmId === 'bun' ? '-d' : '-D'
84+
const devFlag = getDevDependencyFlag(pmId)
7085
const pkgSpec = pmId === 'deno' ? `npm:${props.typesPackageName}` : props.typesPackageName
7186
7287
return [pm.label, pm.action, devFlag, pkgSpec]
@@ -93,6 +108,18 @@ const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command))
93108
94109
const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 })
95110
const copyCreateCommand = () => copyCreate(getFullCreateCommand())
111+
112+
const { copied: devInstallCopied, copy: copyDevInstall } = useClipboard({ copiedDuring: 2000 })
113+
const copyDevInstallCommand = () =>
114+
copyDevInstall(
115+
getInstallCommand({
116+
packageName: props.packageName,
117+
packageManager: selectedPM.value,
118+
version: props.requestedVersion,
119+
jsrInfo: props.jsrInfo,
120+
dev: true,
121+
}),
122+
)
96123
</script>
97124

98125
<template>
@@ -131,6 +158,41 @@ const copyCreateCommand = () => copyCreate(getFullCreateCommand())
131158
</button>
132159
</div>
133160

161+
<!-- Suggested dev dependency install command -->
162+
<template v-if="devDependencySuggestion.recommended">
163+
<div class="flex items-center gap-2 pt-1 select-none">
164+
<span class="text-fg-subtle font-mono text-sm"
165+
># {{ $t('package.get_started.dev_dependency_hint') }}</span
166+
>
167+
</div>
168+
<div
169+
v-for="pm in packageManagers"
170+
:key="`install-dev-${pm.id}`"
171+
:data-pm-cmd="pm.id"
172+
class="flex items-center gap-2 group/devinstallcmd min-w-0"
173+
>
174+
<span class="text-fg-subtle font-mono text-sm select-none shrink-0">$</span>
175+
<code class="font-mono text-sm min-w-0"
176+
><span
177+
v-for="(part, i) in getDevInstallPartsForPM(pm.id)"
178+
:key="i"
179+
:class="i === 0 ? 'text-fg' : 'text-fg-muted'"
180+
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
181+
></code
182+
>
183+
<button
184+
type="button"
185+
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/devinstallcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
186+
:aria-label="$t('package.get_started.copy_dev_command')"
187+
@click.stop="copyDevInstallCommand"
188+
>
189+
<span aria-live="polite">{{
190+
devInstallCopied ? $t('common.copied') : $t('common.copy')
191+
}}</span>
192+
</button>
193+
</div>
194+
</template>
195+
134196
<!-- @types package install - render all PM variants when types package exists -->
135197
<template v-if="typesPackageName && showTypesInInstall">
136198
<div

app/pages/package/[[org]]/[name].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ onKeyStroke(
10461046
:package-name="pkg.name"
10471047
:requested-version="requestedVersion"
10481048
:jsr-info="jsrInfo"
1049+
:readme-html="readmeData?.html"
10491050
:types-package-name="typesPackageName"
10501051
:executable-info="executableInfo"
10511052
:create-package-info="createPackageInfo"

app/utils/install-command.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,117 @@ export interface InstallCommandOptions {
6868
packageManager: PackageManagerId
6969
version?: string | null
7070
jsrInfo?: JsrPackageInfo | null
71+
dev?: boolean
72+
}
73+
74+
export type DevDependencySuggestionReason = 'known-package' | 'readme-hint'
75+
76+
export interface DevDependencySuggestion {
77+
recommended: boolean
78+
reason?: DevDependencySuggestionReason
79+
}
80+
81+
const KNOWN_DEV_DEPENDENCY_PACKAGES = new Set<string>([
82+
'@types',
83+
'biome',
84+
'chai',
85+
'eslint',
86+
'esbuild',
87+
'husky',
88+
'jest',
89+
'lint-staged',
90+
'mocha',
91+
'oxc',
92+
'oxfmt',
93+
'oxlint',
94+
'playwright',
95+
'prettier',
96+
'rolldown',
97+
'rollup',
98+
'stylelint',
99+
'ts-jest',
100+
'ts-node',
101+
'tsx',
102+
'turbo',
103+
'typescript',
104+
'vite',
105+
'vitest',
106+
'webpack',
107+
])
108+
109+
function isKnownDevDependencyPackage(packageName: string): boolean {
110+
const normalized = packageName.toLowerCase()
111+
if (normalized.startsWith('@types/')) {
112+
return true
113+
}
114+
// Match scoped packages by name segment, e.g. @scope/eslint-config
115+
const namePart = normalized.includes('/') ? normalized.split('/').pop() : normalized
116+
if (!namePart) return false
117+
118+
return (
119+
KNOWN_DEV_DEPENDENCY_PACKAGES.has(normalized) ||
120+
KNOWN_DEV_DEPENDENCY_PACKAGES.has(namePart) ||
121+
normalized.startsWith('@typescript-eslint/') ||
122+
namePart.startsWith('eslint-') ||
123+
namePart.startsWith('prettier-') ||
124+
namePart.startsWith('vite-') ||
125+
namePart.startsWith('webpack-') ||
126+
namePart.startsWith('babel-')
127+
)
128+
}
129+
130+
function escapeRegExp(text: string): string {
131+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
132+
}
133+
134+
function hasReadmeDevInstallHint(packageName: string, readmeHtml?: string | null): boolean {
135+
if (!readmeHtml) return false
136+
137+
const escapedName = escapeRegExp(packageName)
138+
const escapedNpmName = escapeRegExp(`npm:${packageName}`)
139+
const packageSpec = `(?:${escapedName}|${escapedNpmName})(?:@[\\w.-]+)?`
140+
141+
const patterns = [
142+
// npm install -D pkg / pnpm add --save-dev pkg
143+
new RegExp(
144+
String.raw`(?:npm|pnpm|yarn|bun|vlt)\s+(?:install|add|i)\s+(?:--save-dev|-d)\s+${packageSpec}`,
145+
'i',
146+
),
147+
// npm install pkg --save-dev / pnpm add pkg -D
148+
new RegExp(
149+
String.raw`(?:npm|pnpm|yarn|bun|vlt)\s+(?:install|add|i)\s+${packageSpec}\s+(?:--save-dev|-d)`,
150+
'i',
151+
),
152+
// deno add -D npm:pkg
153+
new RegExp(String.raw`deno\s+add\s+(?:--dev|-d)\s+${packageSpec}`, 'i'),
154+
]
155+
156+
return patterns.some(pattern => pattern.test(readmeHtml))
157+
}
158+
159+
export function getDevDependencySuggestion(
160+
packageName: string,
161+
readmeHtml?: string | null,
162+
): DevDependencySuggestion {
163+
if (isKnownDevDependencyPackage(packageName)) {
164+
return {
165+
recommended: true,
166+
reason: 'known-package',
167+
}
168+
}
169+
170+
if (hasReadmeDevInstallHint(packageName, readmeHtml)) {
171+
return {
172+
recommended: true,
173+
reason: 'readme-hint',
174+
}
175+
}
176+
177+
return { recommended: false }
178+
}
179+
180+
export function getDevDependencyFlag(packageManager: PackageManagerId): '-D' | '-d' {
181+
return packageManager === 'bun' ? '-d' : '-D'
71182
}
72183

73184
/**
@@ -108,8 +219,9 @@ export function getInstallCommandParts(options: InstallCommandOptions): string[]
108219

109220
const spec = getPackageSpecifier(options)
110221
const version = options.version ? `@${options.version}` : ''
222+
const devFlag = options.dev ? [getDevDependencyFlag(options.packageManager)] : []
111223

112-
return [pm.label, pm.action, `${spec}${version}`]
224+
return [pm.label, pm.action, ...devFlag, `${spec}${version}`]
113225
}
114226

115227
export interface ExecuteCommandOptions extends InstallCommandOptions {

i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
"title": "Get started",
191191
"pm_label": "Package manager",
192192
"copy_command": "Copy install command",
193+
"copy_dev_command": "Copy dev install command",
194+
"dev_dependency_hint": "Usually installed as a dev dependency",
193195
"view_types": "View {package}"
194196
},
195197
"create": {

i18n/locales/pl-PL.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@
180180
"title": "Zacznij",
181181
"pm_label": "Menedżer pakietów",
182182
"copy_command": "Kopiuj komendę instalacji",
183+
"copy_dev_command": "Kopiuj komendę instalacji dla dev",
184+
"dev_dependency_hint": "Zazwyczaj instalowane jako zależność deweloperska",
183185
"view_types": "Zobacz {package}"
184186
},
185187
"create": {

lunaria/files/en-GB.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
"title": "Get started",
191191
"pm_label": "Package manager",
192192
"copy_command": "Copy install command",
193+
"copy_dev_command": "Copy dev install command",
194+
"dev_dependency_hint": "Usually installed as a dev dependency",
193195
"view_types": "View {package}"
194196
},
195197
"create": {

lunaria/files/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
"title": "Get started",
191191
"pm_label": "Package manager",
192192
"copy_command": "Copy install command",
193+
"copy_dev_command": "Copy dev install command",
194+
"dev_dependency_hint": "Usually installed as a dev dependency",
193195
"view_types": "View {package}"
194196
},
195197
"create": {

lunaria/files/pl-PL.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@
180180
"title": "Zacznij",
181181
"pm_label": "Menedżer pakietów",
182182
"copy_command": "Kopiuj komendę instalacji",
183+
"copy_dev_command": "Kopiuj komendę instalacji dla dev",
184+
"dev_dependency_hint": "Zazwyczaj instalowane jako zależność deweloperska",
183185
"view_types": "Zobacz {package}"
184186
},
185187
"create": {

0 commit comments

Comments
 (0)