Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
- name: 📦 Install dependencies
run: pnpm install

- name: 🎨 Check for non-RTL CSS classes
run: pnpm rtl:check
- name: 🎨 Check for non-RTL/non-a11y CSS classes
run: pnpm lint:css

- name: 🌐 Compare translations
run: pnpm i18n:check
Expand Down
52 changes: 46 additions & 6 deletions app/components/Compare/PackageSelector.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { NO_DEPENDENCY_ID } from '~/composables/usePackageComparison'
import { checkPackageExists } from '~/utils/package-name'

const packages = defineModel<string[]>({ required: true })

Expand All @@ -13,6 +14,12 @@ const maxPackages = computed(() => props.max ?? 4)
// Input state
const inputValue = shallowRef('')
const isInputFocused = shallowRef(false)
const isCheckingPackage = shallowRef(false)
const packageError = shallowRef('')

watch(inputValue, () => {
packageError.value = ''
})

// Use the shared npm search composable
const { data: searchData, status } = useNpmSearch(inputValue, { size: 15 })
Expand Down Expand Up @@ -76,10 +83,36 @@ function removePackage(name: string) {
packages.value = packages.value.filter(p => p !== name)
}

function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && inputValue.value.trim()) {
e.preventDefault()
addPackage(inputValue.value.trim())
async function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Enter' || !inputValue.value.trim() || isCheckingPackage.value) return
e.preventDefault()

const name = inputValue.value.trim()
if (packages.value.length >= maxPackages.value) return
if (packages.value.includes(name)) return

// If it matches a dropdown result, add immediately (already confirmed to exist)
const exactMatch = filteredResults.value.find(r => r.name === name)
if (exactMatch) {
addPackage(exactMatch.name)
return
}

// Otherwise, verify it exists on npm
isCheckingPackage.value = true
packageError.value = ''
try {
const exists = await checkPackageExists(name)
if (name !== inputValue.value.trim()) return // stale guard
if (exists) {
addPackage(name)
} else {
packageError.value = `Package "${name}" was not found on npm.`
}
} catch {
packageError.value = 'Could not verify package. Please try again.'
} finally {
isCheckingPackage.value = false
}
Comment on lines +86 to 116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid misleading “not found” on network failures.
checkPackageExists returns false for any fetch error, so the “not found” message will also show on registry/network issues and the catch branch will effectively never run. Consider returning a richer status from the util, or make the message neutral.

💡 Possible message adjustment
-      packageError.value = `Package "${name}" was not found on npm.`
+      packageError.value = `Package "${name}" was not found or could not be verified.`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Enter' || !inputValue.value.trim() || isCheckingPackage.value) return
e.preventDefault()
const name = inputValue.value.trim()
if (packages.value.length >= maxPackages.value) return
if (packages.value.includes(name)) return
// If it matches a dropdown result, add immediately (already confirmed to exist)
const exactMatch = filteredResults.value.find(r => r.name === name)
if (exactMatch) {
addPackage(exactMatch.name)
return
}
// Otherwise, verify it exists on npm
isCheckingPackage.value = true
packageError.value = ''
try {
const exists = await checkPackageExists(name)
if (name !== inputValue.value.trim()) return // stale guard
if (exists) {
addPackage(name)
} else {
packageError.value = `Package "${name}" was not found on npm.`
}
} catch {
packageError.value = 'Could not verify package. Please try again.'
} finally {
isCheckingPackage.value = false
}
async function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Enter' || !inputValue.value.trim() || isCheckingPackage.value) return
e.preventDefault()
const name = inputValue.value.trim()
if (packages.value.length >= maxPackages.value) return
if (packages.value.includes(name)) return
// If it matches a dropdown result, add immediately (already confirmed to exist)
const exactMatch = filteredResults.value.find(r => r.name === name)
if (exactMatch) {
addPackage(exactMatch.name)
return
}
// Otherwise, verify it exists on npm
isCheckingPackage.value = true
packageError.value = ''
try {
const exists = await checkPackageExists(name)
if (name !== inputValue.value.trim()) return // stale guard
if (exists) {
addPackage(name)
} else {
packageError.value = `Package "${name}" was not found or could not be verified.`
}
} catch {
packageError.value = 'Could not verify package. Please try again.'
} finally {
isCheckingPackage.value = false
}
}

}

Expand Down Expand Up @@ -138,7 +171,8 @@ function handleBlur() {
class="absolute inset-y-0 start-3 flex items-center text-fg-subtle pointer-events-none group-focus-within:text-accent"
aria-hidden="true"
>
<span class="i-carbon:search w-4 h-4" />
<span v-if="isCheckingPackage" class="i-carbon:renew w-4 h-4 animate-spin" />
<span v-else class="i-carbon:search w-4 h-4" />
</span>
<input
id="package-search"
Expand All @@ -149,7 +183,8 @@ function handleBlur() {
? $t('compare.selector.search_first')
: $t('compare.selector.search_add')
"
class="w-full bg-bg-subtle border border-border rounded-lg ps-10 pe-4 py-2.5 font-mono text-sm text-fg placeholder:text-fg-subtle motion-reduce:transition-none duration-200 focus:border-accent focus-visible:(outline-2 outline-accent/70)"
:disabled="isCheckingPackage"
class="w-full bg-bg-subtle border border-border rounded-lg ps-10 pe-4 py-2.5 font-mono text-sm text-fg placeholder:text-fg-subtle motion-reduce:transition-none duration-200 focus:border-accent focus-visible:(outline-2 outline-accent/70) disabled:opacity-60 disabled:cursor-wait"
aria-autocomplete="list"
@focus="isInputFocused = true"
@blur="handleBlur"
Expand Down Expand Up @@ -205,6 +240,11 @@ function handleBlur() {
</button>
</div>
</Transition>

<!-- Package not found error -->
<p v-if="packageError" class="text-xs text-red-400 mt-1" role="alert">
{{ packageError }}
</p>
</div>

<!-- Hint -->
Expand Down
2 changes: 1 addition & 1 deletion app/components/LicenseDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const hasAnyValidLicense = computed(() => tokens.value.some(t => t.type === 'lic
{{ token.value }}
</a>
<span v-else-if="token.type === 'license'">{{ token.value }}</span>
<span v-else-if="token.type === 'operator'" class="text-[0.65em]">{{ token.value }}</span>
<span v-else-if="token.type === 'operator'" class="text-4xs">{{ token.value }}</span>
</template>
<span
v-if="hasAnyValidLicense"
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"i18n:check:fix": "node scripts/compare-translations.ts --fix",
"i18n:report": "node scripts/find-invalid-translations.ts",
"i18n:report:fix": "node scripts/remove-unused-translations.ts",
"rtl:check": "node scripts/rtl-checker.ts",
"knip": "knip",
"knip:fix": "knip --fix",
"lint": "oxlint && oxfmt --check",
"lint:fix": "oxlint --fix && oxfmt",
"lint:css": "node scripts/unocss-checker.ts",
"generate": "nuxt generate",
"npmx-connector": "pnpm --filter npmx-connector dev",
"generate-pwa-icons": "pwa-assets-generator",
Expand Down Expand Up @@ -151,6 +151,9 @@
"*.{js,ts,mjs,cjs,vue}": [
"pnpm oxlint --fix"
],
"*.vue": [
"pnpm lint:css"
],
"*.{js,ts,mjs,cjs,vue,json,yml,md,html,css}": [
"pnpm oxfmt"
]
Expand Down
20 changes: 18 additions & 2 deletions scripts/rtl-checker.ts → scripts/unocss-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import { createGenerator } from 'unocss'
import { presetRtl } from '../uno-preset-rtl.ts'
import { presetA11y } from '../uno-preset-a11y.ts'
import { COLORS } from './utils.ts'
import { presetWind4 } from 'unocss'

const argvFiles = process.argv.slice(2)
const APP_DIRECTORY = fileURLToPath(new URL('../app', import.meta.url))

async function checkFile(path: Dirent): Promise<string | undefined> {
Expand All @@ -33,6 +35,17 @@ async function checkFile(path: Dirent): Promise<string | undefined> {
`${COLORS.red} ❌ [RTL] ${filename}:${idx}${ruleIdx > -1 ? `:${ruleIdx + 1}` : ''} - ${warning}${COLORS.reset}`,
)
}),
presetA11y((warning, rule) => {
let entry = warnings.get(idx)
if (!entry) {
entry = []
warnings.set(idx, entry)
}
const ruleIdx = line.indexOf(rule)
entry.push(
`${COLORS.red} ❌ [A11y] ${filename}:${idx}${ruleIdx > -1 ? `:${ruleIdx + 1}` : ''} - ${warning}${COLORS.reset}`,
)
}),
],
})
const lines = file.split('\n')
Expand All @@ -46,7 +59,10 @@ async function checkFile(path: Dirent): Promise<string | undefined> {
}

async function check(): Promise<void> {
const dir = glob('**/*.vue', { withFileTypes: true, cwd: APP_DIRECTORY })
const dir = glob(argvFiles.length > 0 ? argvFiles : '**/*.vue', {
withFileTypes: true,
cwd: APP_DIRECTORY,
})
Comment on lines +62 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine the full unocss-checker.ts to understand argument handling
echo "=== Full unocss-checker.ts content ==="
cat scripts/unocss-checker.ts

echo -e "\n=== Checking for APP_DIRECTORY definition ==="
rg "APP_DIRECTORY" scripts/ --type ts -B2 -A2

Repository: npmx-dev/npmx.dev

Length of output: 3519


🏁 Script executed:

#!/bin/bash
# Check lint-staged configuration and how the script is invoked
echo "=== lint-staged config in package.json ==="
cat package.json | grep -A10 "lint-staged"

echo -e "\n=== Scripts section in package.json ==="
cat package.json | grep -A20 '"scripts"'

echo -e "\n=== Check how lint:css is defined ==="
grep -r "lint:css" package.json

Repository: npmx-dev/npmx.dev

Length of output: 1916


Fix the file path handling in unocss-checker.ts when invoked via lint-staged.

When lint-staged runs pnpm lint:css, it passes staged file paths as arguments relative to the repository root (e.g., app/components/Foo.vue). However, the glob is executed with cwd: APP_DIRECTORY (an absolute path pointing to the app directory), causing a mismatch. The script attempts to locate files at paths like <APP_DIRECTORY>/app/components/Foo.vue instead of <APP_DIRECTORY>/components/Foo.vue, which will fail.

Either strip the app/ prefix from paths passed by lint-staged, or adjust the cwd and path handling logic.

let hasErrors = false
for await (const file of dir) {
const result = await checkFile(file)
Expand All @@ -61,7 +77,7 @@ async function check(): Promise<void> {
process.exit(1)
} else {
// oxlint-disable-next-line no-console -- success logging
console.log(`${COLORS.green}✅ CSS RTL check passed!${COLORS.reset}`)
console.log(`${COLORS.green}✅ CSS check passed!${COLORS.reset}`)
}
}

Expand Down
60 changes: 56 additions & 4 deletions test/nuxt/components/compare/PackageSelector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { flushPromises } from '@vue/test-utils'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import PackageSelector from '~/components/Compare/PackageSelector.vue'

// Mock checkPackageExists
vi.mock('~/utils/package-name', () => ({
checkPackageExists: vi.fn(),
}))

import { checkPackageExists } from '~/utils/package-name'
const mockCheckPackageExists = vi.mocked(checkPackageExists)

// Mock $fetch for useNpmSearch
const mockFetch = vi.fn()
vi.stubGlobal('$fetch', mockFetch)

describe('PackageSelector', () => {
beforeEach(() => {
mockFetch.mockReset()
mockCheckPackageExists.mockReset()
mockFetch.mockResolvedValue({
objects: [
{ package: { name: 'lodash', description: 'Lodash modular utilities' } },
Expand All @@ -18,6 +28,7 @@ describe('PackageSelector', () => {
total: 2,
time: new Date().toISOString(),
})
mockCheckPackageExists.mockResolvedValue(true)
})

describe('selected packages display', () => {
Expand Down Expand Up @@ -132,37 +143,78 @@ describe('PackageSelector', () => {
})

describe('adding packages', () => {
it('adds package on Enter key', async () => {
it('adds package on Enter key when package exists', async () => {
mockCheckPackageExists.mockResolvedValue(true)

const component = await mountSuspended(PackageSelector, {
props: {
modelValue: [],
},
})

const input = component.find('input')
await input.setValue('lodash')
await input.trigger('keydown', { key: 'Enter' })
await flushPromises()

const emitted = component.emitted('update:modelValue')
expect(emitted).toBeTruthy()
expect(emitted![0]![0]).toEqual(['lodash'])
})

it('adds "no dep" entry on Enter key', async () => {
const component = await mountSuspended(PackageSelector, {
props: {
modelValue: [],
},
})

const input = component.find('input')
await input.setValue('my-package')
await input.setValue('no dep')
await input.trigger('keydown', { key: 'Enter' })

const emitted = component.emitted('update:modelValue')
expect(emitted).toBeTruthy()
expect(emitted![0]![0]).toEqual(['my-package'])
expect(emitted![0]![0]).toEqual(['__no_dependency__'])
})

it('clears input after adding package', async () => {
mockCheckPackageExists.mockResolvedValue(true)

const component = await mountSuspended(PackageSelector, {
props: {
modelValue: [],
},
})

const input = component.find('input')
await input.setValue('my-package')
await input.setValue('lodash')
await input.trigger('keydown', { key: 'Enter' })
await flushPromises()

// Input should be cleared
expect((input.element as HTMLInputElement).value).toBe('')
})

it('does not add non-existent packages', async () => {
mockCheckPackageExists.mockResolvedValue(false)

const component = await mountSuspended(PackageSelector, {
props: {
modelValue: [],
},
})

const input = component.find('input')
await input.setValue('nonexistent-pkg')
await input.trigger('keydown', { key: 'Enter' })
await flushPromises()

const emitted = component.emitted('update:modelValue')
expect(emitted).toBeFalsy()
expect(component.find('[role="alert"]').exists()).toBe(true)
})

it('does not add duplicate packages', async () => {
const component = await mountSuspended(PackageSelector, {
props: {
Expand Down
Loading
Loading