diff --git a/app/components/Package/ManagerSelect.vue b/app/components/Package/ManagerSelect.vue
index 11b1bfb93a..d554ea439f 100644
--- a/app/components/Package/ManagerSelect.vue
+++ b/app/components/Package/ManagerSelect.vue
@@ -152,7 +152,7 @@ function handleKeydown(event: KeyboardEvent) {
"
:aria-label="$t('package.get_started.pm_label')"
:style="getDropdownStyle()"
- class="fixed bg-bg-subtle border border-border rounded-md shadow-lg z-50"
+ class="fixed overflow-hidden bg-bg-subtle border border-border rounded-md shadow-lg z-50"
>
p.id === pmId)
- if (!pm) return []
-
- const devFlag = getDevDependencyFlag(pmId)
- const pkgSpec = pmId === 'deno' ? `npm:${props.typesPackageName}` : props.typesPackageName
-
- return [pm.label, pm.action, devFlag, pkgSpec]
+ return getInstallCommandParts({
+ packageName: props.typesPackageName,
+ packageManager: pmId,
+ jsrInfo: null,
+ dev: true,
+ })
}
// Full run command for copying (uses current selected PM)
diff --git a/app/composables/useInstallCommand.ts b/app/composables/useInstallCommand.ts
index 4984434101..25e99d9b35 100644
--- a/app/composables/useInstallCommand.ts
+++ b/app/composables/useInstallCommand.ts
@@ -1,5 +1,4 @@
import type { JsrPackageInfo } from '#shared/types/jsr'
-import { getPackageManagerConfig } from '~/utils/install-command'
/**
* Composable for generating install commands with support for
@@ -44,21 +43,16 @@ export function useInstallCommand(
})
})
- // Get the dev dependency flag for the selected package manager
- const devFlag = computed(() => {
- // bun uses lowercase -d, all others use -D
- return selectedPM.value === 'bun' ? '-d' : '-D'
- })
-
// @types install command parts (for display)
const typesInstallCommandParts = computed(() => {
const types = toValue(typesPackageName)
if (!types) return []
- const packageManagerConfig = getPackageManagerConfig(selectedPM.value)
-
- const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types
-
- return [packageManagerConfig.label, packageManagerConfig.action, devFlag.value, pkgSpec]
+ return getInstallCommandParts({
+ packageName: types,
+ packageManager: selectedPM.value,
+ jsrInfo: null,
+ dev: true,
+ })
})
// Full install command including @types (for copying)
@@ -69,12 +63,13 @@ export function useInstallCommand(
return installCommand.value
}
- const packageManagerConfig = getPackageManagerConfig(selectedPM.value)
-
- const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types
-
// Use semicolon to separate commands
- return `${installCommand.value}; ${packageManagerConfig.label} ${packageManagerConfig.action} ${devFlag.value} ${pkgSpec}`
+ return `${installCommand.value}; ${getInstallCommand({
+ packageName: types,
+ packageManager: selectedPM.value,
+ jsrInfo: null,
+ dev: true,
+ })}`
})
// Copy state
diff --git a/app/utils/install-command.ts b/app/utils/install-command.ts
index 90d537c713..3b34116385 100644
--- a/app/utils/install-command.ts
+++ b/app/utils/install-command.ts
@@ -77,6 +77,15 @@ export const packageManagers = [
create: 'nub create',
icon: 'i-custom-nub',
},
+ {
+ id: 'ni',
+ label: 'ni',
+ action: '',
+ executeLocal: 'nlx --local',
+ executeRemote: 'nlx',
+ create: 'na create',
+ icon: 'i-custom-ni',
+ },
] as const
export type PackageManagerId = (typeof packageManagers)[number]['id']
@@ -147,6 +156,7 @@ export function getInstallCommandParts(options: InstallCommandOptions): string[]
const version = options.version ? `@${options.version}` : ''
const devFlag = options.dev ? [getDevDependencyFlag(options.packageManager)] : []
+ if (pm.action.length === 0) return [pm.label, ...devFlag, `${spec}${version}`]
return [pm.label, pm.action, ...devFlag, `${spec}${version}`]
}
diff --git a/app/utils/prehydrate.ts b/app/utils/prehydrate.ts
index e18fa54980..119b712835 100644
--- a/app/utils/prehydrate.ts
+++ b/app/utils/prehydrate.ts
@@ -21,7 +21,7 @@ export function initPreferencesOnPrehydrate() {
] satisfies typeof ACCENT_COLOR_IDS)
// Valid package manager IDs
- const validPMs = new Set(['npm', 'pnpm', 'yarn', 'bun', 'deno', 'vlt', 'vp', 'nub'])
+ const validPMs = new Set(['npm', 'pnpm', 'yarn', 'bun', 'deno', 'vlt', 'vp', 'nub', 'ni'])
const validBackgroundThemes = new Set(['neutral', 'stone', 'zinc', 'slate', 'black'])
const validForegroundThemes = new Set(['muted', 'standard', 'contrast'])
diff --git a/assets/media/custom-icons.json b/assets/media/custom-icons.json
index fe7c7571e0..f6407c5b4b 100644
--- a/assets/media/custom-icons.json
+++ b/assets/media/custom-icons.json
@@ -24,6 +24,11 @@
"width": 32,
"height": 32
},
+ "ni": {
+ "body": "",
+ "width": 12,
+ "height": 12
+ },
"license-minified": {
"body": "",
"width": 32,
diff --git a/assets/media/ni.svg b/assets/media/ni.svg
new file mode 100644
index 0000000000..7cdb8ac841
--- /dev/null
+++ b/assets/media/ni.svg
@@ -0,0 +1,5 @@
+
diff --git a/test/e2e/hydration.spec.ts b/test/e2e/hydration.spec.ts
index 047489fe46..bdf32f4714 100644
--- a/test/e2e/hydration.spec.ts
+++ b/test/e2e/hydration.spec.ts
@@ -101,6 +101,24 @@ test.describe('Hydration', () => {
}
})
+ test('package manager: persisted ni', async ({ page, goto, hydrationErrors }) => {
+ await injectLocalStorage(page, {
+ 'npmx-pm': 'ni',
+ })
+
+ await goto('/about', { waitUntil: 'hydration' })
+
+ expect(hydrationErrors).toEqual([])
+ await expect(page.locator('html')).toHaveAttribute('data-pm', 'ni')
+ })
+
+ test('package manager: ni query parameter', async ({ page, goto, hydrationErrors }) => {
+ await goto('/about?pm=ni', { waitUntil: 'hydration' })
+
+ expect(hydrationErrors).toEqual([])
+ await expect(page.locator('html')).toHaveAttribute('data-pm', 'ni')
+ })
+
// Default: "en-US" (LTR) → test "ar-EG" (RTL)
test.describe('locale: ar-EG (RTL)', () => {
for (const page of PAGES) {
diff --git a/test/unit/app/utils/install-command.spec.ts b/test/unit/app/utils/install-command.spec.ts
index 7b281257f1..a3c07039fa 100644
--- a/test/unit/app/utils/install-command.spec.ts
+++ b/test/unit/app/utils/install-command.spec.ts
@@ -289,6 +289,25 @@ describe('install command generation', () => {
expect(parts).toEqual(['vlt', 'install', 'lodash'])
})
+ it('omits an empty action for ni', () => {
+ expect(
+ getInstallCommandParts({
+ packageName: 'lodash',
+ packageManager: 'ni',
+ jsrInfo: jsrNotAvailable,
+ }),
+ ).toEqual(['ni', 'lodash'])
+
+ expect(
+ getInstallCommandParts({
+ packageName: 'eslint',
+ packageManager: 'ni',
+ jsrInfo: jsrNotAvailable,
+ dev: true,
+ }),
+ ).toEqual(['ni', '-D', 'eslint'])
+ })
+
it('joined parts match getInstallCommand output', () => {
const options = {
packageName: '@trpc/server',