Skip to content

Commit 89edc2b

Browse files
Adebesin-Cellclaude
andcommitted
refactor: use extended useVisibleItems for progressive org loading
Extend useVisibleItems with an optional onExpand callback that supports async data loading. Wire it into the org page so expanding triggers loadAll() to fetch remaining packages from Algolia. - useVisibleItems gains onExpand + isExpanding (backwards-compatible) - useOrgPackages keeps progressive loading (250 on SSR, rest on demand) - Promise lock scoped inside composable (fixes cross-instance bug) - Org page uses useVisibleItems(packages, 250, { onExpand: loadAll }) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5686329 commit 89edc2b

File tree

6 files changed

+187
-131
lines changed

6 files changed

+187
-131
lines changed

.claude/skills/vite-plus

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../node_modules/vite-plus/skills/vite-plus

.trae/skills/vite-plus

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../node_modules/vite-plus/skills/vite-plus

CLAUDE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!--VITE PLUS START-->
2+
3+
# Using Vite+, the Unified Toolchain for the Web
4+
5+
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`.
6+
7+
## Vite+ Workflow
8+
9+
`vp` is a global binary that handles the full development lifecycle. Run `vp help` to print a list of commands and `vp <command> --help` for information about a specific command.
10+
11+
### Start
12+
13+
- create - Create a new project from a template
14+
- migrate - Migrate an existing project to Vite+
15+
- config - Configure hooks and agent integration
16+
- staged - Run linters on staged files
17+
- install (`i`) - Install dependencies
18+
- env - Manage Node.js versions
19+
20+
### Develop
21+
22+
- dev - Run the development server
23+
- check - Run format, lint, and TypeScript type checks
24+
- lint - Lint code
25+
- fmt - Format code
26+
- test - Run tests
27+
28+
### Execute
29+
30+
- run - Run monorepo tasks
31+
- exec - Execute a command from local `node_modules/.bin`
32+
- dlx - Execute a package binary without installing it as a dependency
33+
- cache - Manage the task cache
34+
35+
### Build
36+
37+
- build - Build for production
38+
- pack - Build libraries
39+
- preview - Preview production build
40+
41+
### Manage Dependencies
42+
43+
Vite+ automatically detects and wraps the underlying package manager such as pnpm, npm, or Yarn through the `packageManager` field in `package.json` or package manager-specific lockfiles.
44+
45+
- add - Add packages to dependencies
46+
- remove (`rm`, `un`, `uninstall`) - Remove packages from dependencies
47+
- update (`up`) - Update packages to latest versions
48+
- dedupe - Deduplicate dependencies
49+
- outdated - Check for outdated packages
50+
- list (`ls`) - List installed packages
51+
- why (`explain`) - Show why a package is installed
52+
- info (`view`, `show`) - View package information from the registry
53+
- link (`ln`) / unlink - Manage local package links
54+
- pm - Forward a command to the package manager
55+
56+
### Maintain
57+
58+
- upgrade - Update `vp` itself to the latest version
59+
60+
These commands map to their corresponding tools. For example, `vp dev --port 3000` runs Vite's dev server and works the same as Vite. `vp test` runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using `vp --version`. This is useful when researching documentation, features, and bugs.
61+
62+
## Common Pitfalls
63+
64+
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
65+
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. They do not exist. Use `vp test` and `vp lint` instead.
66+
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vp test` command, run it using `vp run test`.
67+
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
68+
- **Use Vite+ wrappers for one-off binaries:** Use `vp dlx` instead of package-manager-specific `dlx`/`npx` commands.
69+
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
70+
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.
71+
72+
## Review Checklist for Agents
73+
74+
- [ ] Run `vp install` after pulling remote changes and before getting started.
75+
- [ ] Run `vp check` and `vp test` to validate changes.
76+
<!--VITE PLUS END-->

app/composables/npm/useOrgPackages.ts

Lines changed: 60 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,22 @@ const INITIAL_BATCH_SIZE = 250
99
const ALGOLIA_BATCH_SIZE = 1000
1010

1111
export interface OrgPackagesResponse extends NpmSearchResponse {
12-
/** Total number of packages in the org (may exceed objects.length if not all loaded yet) */
12+
/** Total number of packages in the org (may exceed objects.length before loadAll) */
1313
totalPackages: number
14-
/** Whether there are more packages that haven't been loaded yet */
15-
isTruncated: boolean
1614
}
1715

1816
function emptyOrgResponse(): OrgPackagesResponse {
1917
return {
2018
...emptySearchResponse(),
2119
totalPackages: 0,
22-
isTruncated: false,
2320
}
2421
}
2522

2623
/**
2724
* Fetch packages for an npm organization with progressive loading.
2825
*
2926
* 1. Gets the authoritative package list from the npm registry (single request)
30-
* 2. Fetches metadata for the first batch immediately
27+
* 2. Fetches metadata for the first batch immediately (fast SSR)
3128
* 3. Remaining packages are loaded on-demand via `loadAll()`
3229
*/
3330
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
@@ -40,22 +37,11 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
4037
})
4138
const { getPackagesByNameSlice } = useAlgoliaSearch()
4239

43-
// --- Progressive loading state ---
44-
const cache = shallowRef<{
45-
org: string
46-
allNames: string[]
47-
objects: NpmSearchResult[]
48-
totalPackages: number
49-
} | null>(null)
40+
// Tracks all package names so loadAll() knows what to fetch
41+
const allNames = shallowRef<string[]>([])
42+
const loadedObjects = shallowRef<NpmSearchResult[]>([])
5043

51-
const isLoadingMore = shallowRef(false)
52-
53-
const hasMore = computed(() => {
54-
if (!cache.value) return false
55-
return cache.value.objects.length < cache.value.allNames.length
56-
})
57-
58-
// Promise lock to prevent duplicate loadAll calls
44+
// Promise lock — scoped inside the composable to avoid cross-instance sharing
5945
let loadAllPromise: Promise<void> | null = null
6046

6147
const asyncData = useLazyAsyncData(
@@ -90,11 +76,12 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
9076
}
9177

9278
if (packageNames.length === 0) {
93-
cache.value = { org, allNames: [], objects: [], totalPackages: 0 }
79+
allNames.value = []
80+
loadedObjects.value = []
9481
return emptyOrgResponse()
9582
}
9683

97-
const totalPackages = packageNames.length
84+
allNames.value = packageNames
9885
const initialNames = packageNames.slice(0, INITIAL_BATCH_SIZE)
9986

10087
// Fetch metadata for first batch
@@ -135,30 +122,25 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
135122
.map(metaToSearchResult)
136123
}
137124

138-
cache.value = {
139-
org,
140-
allNames: packageNames,
141-
objects: initialObjects,
142-
totalPackages,
143-
}
125+
loadedObjects.value = initialObjects
144126

145127
return {
146128
isStale: false,
147129
objects: initialObjects,
148130
total: initialObjects.length,
149-
totalPackages,
150-
isTruncated: packageNames.length > initialObjects.length,
131+
totalPackages: packageNames.length,
151132
time: new Date().toISOString(),
152133
} satisfies OrgPackagesResponse
153134
},
154135
{ default: emptyOrgResponse },
155136
)
156137

157-
/** Load all remaining packages that weren't fetched in the initial batch */
138+
/** Load all remaining packages that weren't fetched in the initial batch. */
158139
async function loadAll(): Promise<void> {
159-
if (!hasMore.value) return
140+
const names = allNames.value
141+
if (names.length <= loadedObjects.value.length) return
160142

161-
// Reuse existing promise if already running
143+
// Reuse in-flight promise to prevent duplicate fetches
162144
if (loadAllPromise) {
163145
await loadAllPromise
164146
return
@@ -173,109 +155,71 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
173155
}
174156

175157
async function _doLoadAll(): Promise<void> {
176-
const currentCache = cache.value
177-
if (!currentCache || currentCache.objects.length >= currentCache.allNames.length) return
158+
const names = allNames.value
159+
const current = loadedObjects.value
160+
if (names.length <= current.length) return
178161

179-
const org = currentCache.org
180-
isLoadingMore.value = true
162+
const org = toValue(orgName)
163+
const remainingNames = names.slice(current.length)
181164

182-
try {
183-
const remainingNames = currentCache.allNames.slice(currentCache.objects.length)
165+
let newObjects: NpmSearchResult[] = []
184166

185-
if (searchProviderValue.value === 'algolia') {
186-
// Split remaining into batches and fetch in parallel
187-
const batches: string[][] = []
188-
for (let i = 0; i < remainingNames.length; i += ALGOLIA_BATCH_SIZE) {
189-
batches.push(remainingNames.slice(i, i + ALGOLIA_BATCH_SIZE))
190-
}
167+
if (searchProviderValue.value === 'algolia') {
168+
const batches: string[][] = []
169+
for (let i = 0; i < remainingNames.length; i += ALGOLIA_BATCH_SIZE) {
170+
batches.push(remainingNames.slice(i, i + ALGOLIA_BATCH_SIZE))
171+
}
191172

192-
const results = await Promise.allSettled(
193-
batches.map(batch => getPackagesByNameSlice(batch)),
194-
)
173+
const results = await Promise.allSettled(batches.map(batch => getPackagesByNameSlice(batch)))
195174

196-
if (toValue(orgName) !== org) return
175+
if (toValue(orgName) !== org) return
197176

198-
const newObjects: NpmSearchResult[] = []
199-
for (const result of results) {
200-
if (result.status === 'fulfilled') {
201-
newObjects.push(...result.value)
202-
}
177+
for (const result of results) {
178+
if (result.status === 'fulfilled') {
179+
newObjects.push(...result.value)
203180
}
204-
205-
if (newObjects.length > 0) {
206-
const existingNames = new Set(currentCache.objects.map(o => o.package.name))
207-
const deduped = newObjects.filter(o => !existingNames.has(o.package.name))
208-
cache.value = {
209-
...currentCache,
210-
objects: [...currentCache.objects, ...deduped],
181+
}
182+
} else {
183+
const metaResults = await mapWithConcurrency(
184+
remainingNames,
185+
async name => {
186+
try {
187+
return await $fetch<PackageMetaResponse>(
188+
`/api/registry/package-meta/${encodePackageName(name)}`,
189+
)
190+
} catch {
191+
return null
211192
}
212-
}
213-
} else {
214-
// npm fallback: fetch with concurrency
215-
const metaResults = await mapWithConcurrency(
216-
remainingNames,
217-
async name => {
218-
try {
219-
return await $fetch<PackageMetaResponse>(
220-
`/api/registry/package-meta/${encodePackageName(name)}`,
221-
)
222-
} catch {
223-
return null
224-
}
225-
},
226-
10,
227-
)
193+
},
194+
10,
195+
)
228196

229-
if (toValue(orgName) !== org) return
197+
if (toValue(orgName) !== org) return
230198

231-
const newObjects = metaResults
232-
.filter((meta): meta is PackageMetaResponse => meta !== null)
233-
.map(metaToSearchResult)
234-
235-
if (newObjects.length > 0) {
236-
const existingNames = new Set(currentCache.objects.map(o => o.package.name))
237-
const deduped = newObjects.filter(o => !existingNames.has(o.package.name))
238-
cache.value = {
239-
...currentCache,
240-
objects: [...currentCache.objects, ...deduped],
241-
}
242-
}
243-
}
244-
} finally {
245-
isLoadingMore.value = false
199+
newObjects = metaResults
200+
.filter((meta): meta is PackageMetaResponse => meta !== null)
201+
.map(metaToSearchResult)
246202
}
247-
}
248203

249-
// Reset cache when provider changes
250-
watch(
251-
() => searchProviderValue.value,
252-
() => {
253-
cache.value = null
254-
loadAllPromise = null
255-
},
256-
)
204+
if (newObjects.length > 0) {
205+
const existingNames = new Set(current.map(o => o.package.name))
206+
const deduped = newObjects.filter(o => !existingNames.has(o.package.name))
207+
const all = [...current, ...deduped]
208+
loadedObjects.value = all
257209

258-
// Computed data that prefers cache
259-
const data = computed<OrgPackagesResponse | null>(() => {
260-
const org = toValue(orgName)
261-
if (cache.value && cache.value.org === org) {
262-
return {
210+
// Update asyncData so the page sees the new objects
211+
asyncData.data.value = {
263212
isStale: false,
264-
objects: cache.value.objects,
265-
total: cache.value.objects.length,
266-
totalPackages: cache.value.totalPackages,
267-
isTruncated: cache.value.objects.length < cache.value.allNames.length,
213+
objects: all,
214+
total: all.length,
215+
totalPackages: names.length,
268216
time: new Date().toISOString(),
269217
}
270218
}
271-
return asyncData.data.value
272-
})
219+
}
273220

274221
return {
275222
...asyncData,
276-
data,
277-
isLoadingMore,
278-
hasMore,
279223
loadAll,
280224
}
281225
}

0 commit comments

Comments
 (0)