Skip to content
Open
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
11 changes: 5 additions & 6 deletions app/components/CollapsibleSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
headingLevel: 'h2',
})
const appSettings = useSettings()
const { localSettings } = useUserLocalSettings()
const buttonId = `${props.id}-collapsible-button`
const contentId = `${props.id}-collapsible-content`
Expand Down Expand Up @@ -47,17 +47,16 @@ onMounted(() => {
function toggle() {
isOpen.value = !isOpen.value
const removed = appSettings.settings.value.sidebar.collapsed.filter(c => c !== props.id)
const removed = localSettings.value.sidebar.collapsed.filter(c => c !== props.id)
if (isOpen.value) {
appSettings.settings.value.sidebar.collapsed = removed
localSettings.value.sidebar.collapsed = removed
} else {
removed.push(props.id)
appSettings.settings.value.sidebar.collapsed = removed
localSettings.value.sidebar.collapsed = removed
}
document.documentElement.dataset.collapsed =
appSettings.settings.value.sidebar.collapsed.join(' ')
document.documentElement.dataset.collapsed = localSettings.value.sidebar.collapsed.join(' ')
}
const ariaLabel = computed(() => {
Expand Down
6 changes: 3 additions & 3 deletions app/components/Header/ConnectorModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disconnect } =
useConnector()

const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()

const tokenInput = shallowRef('')
const portInput = shallowRef('31415')
Expand Down Expand Up @@ -67,7 +67,7 @@ function handleDisconnect() {
<div class="flex flex-col gap-2">
<SettingsToggle
:label="$t('connector.modal.auto_open_url')"
v-model="settings.connector.autoOpenURL"
v-model="localSettings.connector.autoOpenURL"
/>
</div>

Expand Down Expand Up @@ -201,7 +201,7 @@ function handleDisconnect() {
<div class="flex flex-col gap-2">
<SettingsToggle
:label="$t('connector.modal.auto_open_url')"
v-model="settings.connector.autoOpenURL"
v-model="localSettings.connector.autoOpenURL"
/>
</div>
</div>
Expand Down
6 changes: 2 additions & 4 deletions app/components/Settings/AccentColorPicker.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script setup lang="ts">
import { useAccentColor } from '~/composables/useSettings'

const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()

onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const id = settings.accentColorId
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
const id = preferences.accentColorId
Comment on lines 4 to +6
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

Guard JSON.parse in onPrehydrate to prevent crashes.

If localStorage.getItem('npmx-user-preferences') contains malformed JSON, the parse will throw and break the prehydrate logic. Wrap in try/catch for resilience.

🛠️ Suggested fix
 onPrehydrate(el => {
-  const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
-  const id = preferences.accentColorId
+  let id = null
+  try {
+    const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
+    id = preferences?.accentColorId
+  } catch {
+    // Ignore malformed JSON
+  }
   if (id) {

As per coding guidelines, "Use error handling patterns consistently".

📝 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
onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const id = settings.accentColorId
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
const id = preferences.accentColorId
onPrehydrate(el => {
let id = null
try {
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
id = preferences?.accentColorId
} catch {
// Ignore malformed JSON
}

if (id) {
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
if (input) {
Expand Down
4 changes: 2 additions & 2 deletions app/components/Settings/BgThemePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
const { backgroundThemes, selectedBackgroundTheme, setBackgroundTheme } = useBackgroundTheme()

onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const id = settings.preferredBackgroundTheme
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
Copy link
Contributor

Choose a reason for hiding this comment

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

here too we should try/catch the parse call and probably just fall back to {} like we do if it isn't set

const id = preferences.preferredBackgroundTheme
if (id) {
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
if (input) {
Expand Down
2 changes: 1 addition & 1 deletion app/composables/npm/useSearch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
import type { SearchProvider } from '~/composables/useSettings'
import type { SearchProvider } from '#shared/schemas/userPreferences'
import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch'
import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils'
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
Expand Down
4 changes: 2 additions & 2 deletions app/composables/useConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const STORAGE_KEY = 'npmx-connector'
const DEFAULT_PORT = 31415

export const useConnector = createSharedComposable(function useConnector() {
const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()

// Persisted connection config
const config = useState<{ token: string; port: number } | null>('connector-config', () => null)
Expand Down Expand Up @@ -308,7 +308,7 @@ export const useConnector = createSharedComposable(function useConnector() {
body: {
otp,
interactive: !otp,
openUrls: settings.value.connector.autoOpenURL,
openUrls: localSettings.value.connector.autoOpenURL,
},
})
if (response?.success) {
Expand Down
4 changes: 2 additions & 2 deletions app/composables/useInstallCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export function useInstallCommand(
installVersionOverride?: MaybeRefOrGetter<string | null>,
) {
const selectedPM = useSelectedPackageManager()
const { settings } = useSettings()
const { preferences } = useUserPreferencesState()

// Check if we should show @types in install command
const showTypesInInstall = computed(() => {
return settings.value.includeTypesInInstall && !!toValue(typesPackageName)
return preferences.value.includeTypesInInstall && !!toValue(typesPackageName)
})

const installCommandParts = computed(() => {
Expand Down
42 changes: 42 additions & 0 deletions app/composables/useLocalStorageHashProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createDefu } from 'defu'
import { createLocalStorageProvider } from '~/utils/storage'

const defu = createDefu((object, key, value) => {
if (Array.isArray(object[key]) && Array.isArray(value)) {
object[key] = value
return true
}
})

export function useLocalStorageHashProvider<T extends object>(key: string, defaultValue: T) {
const provider = createLocalStorageProvider<T>(key)
const data = ref<T>(defaultValue)

onMounted(() => {
const stored = provider.get()
if (stored) {
data.value = defu(stored, defaultValue)
}
})

function save() {
provider.set(data.value)
}

function reset() {
data.value = { ...defaultValue }
provider.remove()
}

function update<K extends keyof T>(key: K, value: T[K]) {
data.value[key] = value
save()
Comment on lines +11 to +33
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

Prevent shared default mutations in the storage provider

data is initialised with defaultValue by reference and reset() only shallow‑clones, so nested arrays/objects can mutate the defaults and make resets or new instances pick up modified values. Deep‑clone defaults on init/reset and when merging stored values.

🛠️ Suggested fix
 export function useLocalStorageHashProvider<T extends object>(key: string, defaultValue: T) {
   const provider = createLocalStorageProvider<T>(key)
-  const data = ref<T>(defaultValue)
+  const data = ref<T>(structuredClone(defaultValue))

   onMounted(() => {
     const stored = provider.get()
     if (stored) {
-      data.value = defu(stored, defaultValue)
+      data.value = defu(stored, structuredClone(defaultValue))
     }
   })

   function reset() {
-    data.value = { ...defaultValue }
+    data.value = structuredClone(defaultValue)
     provider.remove()
   }

}

return {
data,
save,
reset,
update,
}
}
7 changes: 3 additions & 4 deletions app/composables/usePackageListPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ import type {
} from '#shared/types/preferences'
import { DEFAULT_COLUMNS, DEFAULT_PREFERENCES } from '#shared/types/preferences'

const STORAGE_KEY = 'npmx-list-prefs'

/**
* Composable for managing package list display preferences
* Persists to localStorage and provides reactive state
*
*/
export function usePackageListPreferences() {
const {
data: preferences,
isHydrated,
save,
reset,
} = usePreferencesProvider<PackageListPreferences>(DEFAULT_PREFERENCES)
} = useLocalStorageHashProvider<PackageListPreferences>(STORAGE_KEY, DEFAULT_PREFERENCES)

// Computed accessors for common properties
const viewMode = computed({
Expand Down Expand Up @@ -90,7 +90,6 @@ export function usePackageListPreferences() {
return {
// Raw preferences
preferences,
isHydrated,

// Individual properties with setters
viewMode,
Expand Down
109 changes: 0 additions & 109 deletions app/composables/usePreferencesProvider.ts

This file was deleted.

Loading
Loading