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',