diff --git a/.changeset/selfish-houses-film.md b/.changeset/selfish-houses-film.md new file mode 100644 index 0000000..c61cc0c --- /dev/null +++ b/.changeset/selfish-houses-film.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/devtools': patch +--- + +fix(plugin): preserve Qwik lazy render metadata when wrapping components diff --git a/packages/kit/src/types.ts b/packages/kit/src/types.ts index 2e2be80..bb27cd8 100644 --- a/packages/kit/src/types.ts +++ b/packages/kit/src/types.ts @@ -14,6 +14,15 @@ export interface ServerFunctions { getRoutes: () => any; getQwikPackages: () => Promise<[string, string][]>; getAllDependencies: () => Promise; + getDependenciesStatus: () => Promise<{ + phase: 'idle' | 'phase1' | 'phase2' | 'done' | 'error'; + loaded: number; + total: number; + startedAt: number | null; + finishedAt: number | null; + error?: string; + }>; + refreshDependencies: () => Promise; installPackage: ( packageName: string, isDev?: boolean, diff --git a/packages/playgrounds/src/root.tsx b/packages/playgrounds/src/root.tsx index 55dce1c..83c7f14 100644 --- a/packages/playgrounds/src/root.tsx +++ b/packages/playgrounds/src/root.tsx @@ -16,7 +16,7 @@ export default component$(() => { */ return ( - + {!isDev && ( diff --git a/packages/plugin/src/npm/index.ts b/packages/plugin/src/npm/index.ts index c348a01..27a9d6e 100644 --- a/packages/plugin/src/npm/index.ts +++ b/packages/plugin/src/npm/index.ts @@ -3,270 +3,294 @@ import fsp from 'node:fs/promises'; import { NpmInfo } from '@devtools/kit'; import { execSync } from 'child_process'; import path from 'path'; -import {debug} from 'debug' +import { debug } from 'debug'; const log = debug('qwik:devtools:npm'); -// In-memory cache for npm package information -interface CacheEntry { - data: any; - timestamp: number; + +/** + * This module intentionally favors readability and non-blocking behavior: + * - Phase1: fast local scan (node_modules//package.json) with limited concurrency. + * - Phase2: best-effort background enrich (optional small registry lookup). + * - RPC calls NEVER wait for heavy work (getAllDependencies returns immediately). + */ + +// ----------------------------- +// Types & state +// ----------------------------- + +type DependenciesPhase = 'idle' | 'phase1' | 'phase2' | 'done' | 'error'; + +export interface DependenciesStatus { + phase: DependenciesPhase; + loaded: number; + total: number; + startedAt: number | null; + finishedAt: number | null; + error?: string; } -const packageCache = new Map(); -const CACHE_TTL = 1000 * 60 * 30; // 30 minutes cache TTL +interface DependencyInfo { + name: string; + version: string; + description: string; + author?: any; + homepage?: string; + repository?: string; + npmUrl: string; + iconUrl: string | null; +} -// Preloaded dependencies cache - loaded at server startup -let preloadedDependencies: any[] | null = null; +let preloadedDependencies: DependencyInfo[] | null = null; let isPreloading = false; -let preloadPromise: Promise | null = null; +let preloadPromise: Promise | null = null; -function getCachedPackage(name: string): any | null { - const cached = packageCache.get(name); - if (cached && Date.now() - cached.timestamp < CACHE_TTL) { - return cached.data; - } - packageCache.delete(name); - return null; -} +let dependenciesStatus: DependenciesStatus = { + phase: 'idle', + loaded: 0, + total: 0, + startedAt: null, + finishedAt: null, +}; -function setCachedPackage(name: string, data: any): void { - packageCache.set(name, { - data, - timestamp: Date.now(), - }); +// ----------------------------- +// Small utilities (kept minimal) +// ----------------------------- + +async function fileExists(filePath: string): Promise { + try { + await fsp.access(filePath); + return true; + } catch { + return false; + } } -async function findNearestFileUp(startDir: string, fileName: string): Promise { +async function readJsonFile(filePath: string): Promise { try { - let currentDir = path.resolve(startDir); - // Guard against infinite loops by capping directory ascents - for (let i = 0; i < 100; i++) { - const candidate = path.join(currentDir, fileName); - const exists = await fsp - .access(candidate) - .then(() => true) - .catch(() => false); - if (exists) return candidate; - - const parent = path.dirname(currentDir); - if (parent === currentDir) break; - currentDir = parent; - } - return null; + const content = await fsp.readFile(filePath, 'utf-8'); + return JSON.parse(content); } catch { return null; } } +async function findNearestFileUp(startDir: string, fileName: string): Promise { + let currentDir = path.resolve(startDir); + for (let i = 0; i < 100; i++) { + const candidate = path.join(currentDir, fileName); + if (await fileExists(candidate)) return candidate; + const parent = path.dirname(currentDir); + if (parent === currentDir) break; + currentDir = parent; + } + return null; +} + function getProjectStartDirFromConfig(config: any): string { - // Prefer Vite's resolved root; fallback to the directory of the config file; finally cwd if (config?.root) return config.root; if (config?.configFile) return path.dirname(config.configFile); return process.cwd(); } +function nodeModulesPackageJsonPath(projectRoot: string, name: string): string { + return path.join(projectRoot, 'node_modules', ...name.split('/'), 'package.json'); +} + +function normalizeRepositoryUrl(repository: any): string | undefined { + const url = typeof repository === 'string' ? repository : repository?.url; + if (!url || typeof url !== 'string') return undefined; + return url + .replace(/^git\+/, '') + .replace(/^ssh:\/\/git@/, 'https://') + .replace(/\.git$/, ''); +} + +function guessIconUrl(name: string, repositoryUrl?: string): string | null { + if (name.startsWith('@')) { + const scope = name.split('/')[0].substring(1); + return `https://avatars.githubusercontent.com/${scope}?size=64`; + } + if (repositoryUrl?.includes('github.com')) { + const match = repositoryUrl.match(/github\.com\/([^\/]+)/); + if (match) return `https://avatars.githubusercontent.com/${match[1]}?size=64`; + } + return null; +} + +function deferToEventLoop(): Promise { + return new Promise((resolve) => setImmediate(resolve)); +} + +async function mapLimit( + items: T[], + concurrency: number, + fn: (item: T, index: number) => Promise, +): Promise { + if (items.length === 0) return []; + const results = new Array(items.length); + let cursor = 0; + const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => { + while (true) { + const i = cursor++; + if (i >= items.length) return; + results[i] = await fn(items[i], i); + } + }); + await Promise.all(workers); + return results; +} + export async function detectPackageManager( projectRoot: string, ): Promise<'npm' | 'pnpm' | 'yarn'> { - try { - if ( - await fsp - .access(path.join(projectRoot, 'pnpm-lock.yaml')) - .then(() => true) - .catch(() => false) - ) { - return 'pnpm'; - } - if ( - await fsp - .access(path.join(projectRoot, 'yarn.lock')) - .then(() => true) - .catch(() => false) - ) { - return 'yarn'; - } - if ( - await fsp - .access(path.join(projectRoot, 'package-lock.json')) - .then(() => true) - .catch(() => false) - ) { - return 'npm'; - } - return 'pnpm'; // default to pnpm if no lockfile found - } catch { - return 'pnpm'; - } + if (await fileExists(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm'; + if (await fileExists(path.join(projectRoot, 'yarn.lock'))) return 'yarn'; + if (await fileExists(path.join(projectRoot, 'package-lock.json'))) return 'npm'; + return 'pnpm'; } -// Preload dependencies function - moved to module scope -const preloadDependencies = async (config: any): Promise => { - if (preloadedDependencies) { - log('[Qwik DevTools] Dependencies already preloaded'); - return preloadedDependencies; - } +// ----------------------------- +// Dependencies preload (Phase1 + Phase2) +// ----------------------------- - if (isPreloading && preloadPromise) { - log('[Qwik DevTools] Preloading already in progress...'); - return preloadPromise; - } +async function phase1LocalIndex(projectRoot: string, deps: [string, string][]): Promise { + const localConcurrency = 16; + + return mapLimit(deps, localConcurrency, async ([name, requestedVersion]) => { + const pkgJsonPath = nodeModulesPackageJsonPath(projectRoot, name); + const installedPkg = await readJsonFile(pkgJsonPath); + + const version = installedPkg?.version || requestedVersion; + const repository = normalizeRepositoryUrl(installedPkg?.repository); + const iconUrl = guessIconUrl(name, repository); + + const info: DependencyInfo = { + name, + version, + description: installedPkg?.description || 'No description available', + author: installedPkg?.author, + homepage: installedPkg?.homepage, + repository, + npmUrl: `https://www.npmjs.com/package/${name}`, + iconUrl, + }; + + dependenciesStatus.loaded++; + if (dependenciesStatus.loaded % 50 === 0) await deferToEventLoop(); + + return info; + }); +} + +async function phase2BackgroundEnrich(deps: DependencyInfo[]): Promise { + const targets = deps.filter( + (p) => !p.repository || !p.iconUrl || p.description === 'No description available', + ); + + const enrichConcurrency = 6; + await mapLimit(targets, enrichConcurrency, async (p) => { + // local enrich first + const repo = normalizeRepositoryUrl(p.repository); + if (repo && repo !== p.repository) p.repository = repo; + if (!p.iconUrl) p.iconUrl = guessIconUrl(p.name, p.repository); + + // optional small registry lookup for missing description/repo + if (!p.repository || p.description === 'No description available') { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 1500); + const res = await fetch( + `https://registry.npmjs.org/${encodeURIComponent(p.name)}/${encodeURIComponent(p.version)}`, + { headers: { Accept: 'application/json' }, signal: controller.signal }, + ); + clearTimeout(timeoutId); + + if (res.ok) { + const data = await res.json(); + if (!p.repository) p.repository = normalizeRepositoryUrl(data?.repository); + if (p.description === 'No description available' && data?.description) { + p.description = data.description; + } + if (!p.iconUrl) p.iconUrl = guessIconUrl(p.name, p.repository); + } + } catch { + // ignore + } + } + + await deferToEventLoop(); + return p; + }); +} + +async function preloadDependencies(config: any): Promise { + if (preloadedDependencies) return preloadedDependencies; + if (isPreloading && preloadPromise) return preloadPromise; isPreloading = true; - log('[Qwik DevTools] Starting to preload dependencies...'); - + dependenciesStatus = { + phase: 'phase1', + loaded: 0, + total: 0, + startedAt: Date.now(), + finishedAt: null, + }; + preloadPromise = (async () => { const startDir = getProjectStartDirFromConfig(config); - const pathToPackageJson = await findNearestFileUp(startDir, 'package.json'); - - if (!pathToPackageJson) { + const packageJsonPath = await findNearestFileUp(startDir, 'package.json'); + if (!packageJsonPath) { preloadedDependencies = []; + dependenciesStatus.phase = 'done'; + dependenciesStatus.finishedAt = Date.now(); isPreloading = false; - log('[Qwik DevTools] No package.json found'); return []; } + const projectRoot = path.dirname(packageJsonPath); + const pkg = await readJsonFile(packageJsonPath); + + const allDeps = { + ...(pkg?.dependencies || {}), + ...(pkg?.devDependencies || {}), + ...(pkg?.peerDependencies || {}), + }; + const entries = Object.entries(allDeps); + dependenciesStatus.total = entries.length; + try { - const pkgJson = await fsp.readFile(pathToPackageJson, 'utf-8'); - const pkg = JSON.parse(pkgJson); - - const allDeps = { - ...pkg.dependencies || {}, - ...pkg.devDependencies || {}, - ...pkg.peerDependencies || {}, - }; + const list = await phase1LocalIndex(projectRoot, entries); + preloadedDependencies = list; + dependenciesStatus.phase = 'phase2'; - const dependencies = Object.entries(allDeps); - - // Check cache first - const cachedPackages: any[] = []; - const uncachedDependencies: [string, string][] = []; - - for (const [name, version] of dependencies) { - const cached = getCachedPackage(name); - if (cached) { - cachedPackages.push({ ...cached, version }); - } else { - uncachedDependencies.push([name, version]); - } - } - - if (uncachedDependencies.length === 0) { - preloadedDependencies = cachedPackages; + // Phase2 runs truly in background; never blocks Phase1 result. + void (async () => { + try { + await phase2BackgroundEnrich(list); + dependenciesStatus.phase = 'done'; + } catch (e) { + dependenciesStatus.phase = 'error'; + dependenciesStatus.error = e instanceof Error ? e.message : String(e); + } finally { + dependenciesStatus.finishedAt = Date.now(); isPreloading = false; - return cachedPackages; - } - - // Load all dependencies - use larger batch for initial preload - const batchSize = 100; - const batches = []; - for (let i = 0; i < uncachedDependencies.length; i += batchSize) { - batches.push(uncachedDependencies.slice(i, i + batchSize)); } + })(); - const fetchedPackages: any[] = []; - - log(`[Qwik DevTools] Fetching ${uncachedDependencies.length} packages in parallel...`); - - const allBatchPromises = batches.map(async (batch) => { - const batchPromises = batch.map(async ([name, version]) => { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); // Longer timeout for initial load - - const response = await fetch(`https://registry.npmjs.org/${name}`, { - headers: { - 'Accept': 'application/json', - }, - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const packageData = await response.json(); - - const latestVersion = packageData['dist-tags']?.latest || version; - const versionData = packageData.versions?.[latestVersion] || packageData.versions?.[version]; - - let repositoryUrl = versionData?.repository?.url || packageData.repository?.url; - if (repositoryUrl) { - repositoryUrl = repositoryUrl - .replace(/^git\+/, '') - .replace(/^ssh:\/\/git@/, 'https://') - .replace(/\.git$/, ''); - } - - let iconUrl = null; - - if (packageData.logo) { - iconUrl = packageData.logo; - } else if (name.startsWith('@')) { - const scope = name.split('/')[0].substring(1); - iconUrl = `https://avatars.githubusercontent.com/${scope}?size=64`; - } else if (repositoryUrl?.includes('github.com')) { - const repoMatch = repositoryUrl.match(/github\.com\/([^\/]+)/); - if (repoMatch) { - iconUrl = `https://avatars.githubusercontent.com/${repoMatch[1]}?size=64`; - } - } - - const packageInfo = { - name, - version, - description: versionData?.description || packageData.description || 'No description available', - author: versionData?.author || packageData.author, - homepage: versionData?.homepage || packageData.homepage, - repository: repositoryUrl, - npmUrl: `https://www.npmjs.com/package/${name}`, - iconUrl, - }; - - setCachedPackage(name, packageInfo); - return packageInfo; - } catch (error) { - const basicInfo = { - name, - version, - description: 'No description available', - npmUrl: `https://www.npmjs.com/package/${name}`, - iconUrl: null, - }; - - setCachedPackage(name, basicInfo); - return basicInfo; - } - }); - - const batchResults = await Promise.allSettled(batchPromises); - return batchResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map(result => result.value); - }); - - const allBatchResults = await Promise.all(allBatchPromises); - for (const batchResult of allBatchResults) { - fetchedPackages.push(...batchResult); - } - - const allPackages = [...cachedPackages, ...fetchedPackages]; - preloadedDependencies = allPackages; - isPreloading = false; - - log(`[Qwik DevTools] ✓ Successfully preloaded ${allPackages.length} dependencies`); - - return allPackages; - } catch (error) { - log('[Qwik DevTools] ✗ Failed to preload dependencies:', error); - preloadedDependencies = []; - isPreloading = false; - return []; - } - })(); + log(`[Qwik DevTools] ✓ Phase1 preloaded ${list.length} dependencies (local-first)`); + return list; + } catch (e) { + preloadedDependencies = []; + dependenciesStatus.phase = 'error'; + dependenciesStatus.error = e instanceof Error ? e.message : String(e); + dependenciesStatus.finishedAt = Date.now(); + isPreloading = false; + log('[Qwik DevTools] ✗ Failed to preload dependencies (local-first):', e); + return []; + } + })(); - return preloadPromise; + return preloadPromise; }; // Export function to start preloading from plugin initialization @@ -294,8 +318,7 @@ export function getNpmFunctions({ config }: ServerContext) { if (!pathToPackageJson) return []; try { - const pkgJson = await fsp.readFile(pathToPackageJson, 'utf-8'); - const pkg = JSON.parse(pkgJson); + const pkg = await readJsonFile(pathToPackageJson); return Object.entries(pkg.devDependencies).filter(([key]) => /@qwik/i.test(key), ); @@ -311,15 +334,31 @@ export function getNpmFunctions({ config }: ServerContext) { return preloadedDependencies; } - // If preloading is in progress, wait for it - if (isPreloading && preloadPromise) { - log('[Qwik DevTools] Waiting for preload to complete...'); - return preloadPromise; - } + // If preloading is in progress, NEVER wait (avoid blocking the whole dev server / UI). + if (isPreloading) return []; // If preloading hasn't started (shouldn't happen), start it now log('[Qwik DevTools] Warning: Preload not started, starting now...'); - return preloadDependencies(config); + void preloadDependencies(config); + return []; + }, + + async getDependenciesStatus() { + return dependenciesStatus; + }, + + async refreshDependencies(): Promise { + preloadedDependencies = null; + isPreloading = false; + preloadPromise = null; + dependenciesStatus = { + phase: 'idle', + loaded: 0, + total: 0, + startedAt: null, + finishedAt: null, + }; + void preloadDependencies(config); }, async installPackage( diff --git a/packages/plugin/src/virtualmodules/perfLazyWrapperPreamble.ts b/packages/plugin/src/virtualmodules/perfLazyWrapperPreamble.ts index 4134ee0..1698bc4 100644 --- a/packages/plugin/src/virtualmodules/perfLazyWrapperPreamble.ts +++ b/packages/plugin/src/virtualmodules/perfLazyWrapperPreamble.ts @@ -9,40 +9,50 @@ const perfLazyWrapperPreamble = `${perfRuntime} // [qwik-component-proxy] Render function wrapper const __qwik_wrap__ = (fn, name, viteId) => { + if (typeof fn !== 'function') return fn; let renderCount = 0; - return function (...args) { + + function wrapped(...args) { renderCount += 1; const phase = __qwik_perf_is_server__() ? 'ssr' : 'csr'; const start = performance.now(); - try { - const result = fn.apply(this, args); - const duration = performance.now() - start; - __qwik_perf_commit__({ - component: name, - phase, - duration, - start, - end: start + duration, - viteId, - renderCount, - }); - return result; - } catch (err) { - const duration = performance.now() - start; - __qwik_perf_commit__({ - component: name, - phase, - duration, - start, - end: start + duration, - error: __qwik_perf_to_error__(err), - viteId, - renderCount, - }); - throw err; - } + const result = fn.apply(this, args); + const duration = performance.now() - start; + __qwik_perf_commit__({ + component: name, + phase, + duration, + start, + end: start + duration, + viteId, + renderCount, + }); + return result; + } + + // Preserve Qwik-compiler metadata attached to the original function. + // (No try/catch per request; avoid touching special function props.) + const skip = { + length: true, + name: true, + arguments: true, + caller: true, + prototype: true, }; + + const descriptors = Object.getOwnPropertyDescriptors(fn); + for (const key of Object.keys(descriptors)) { + if (skip[key]) continue; + Object.defineProperty(wrapped, key, descriptors[key]); + } + + for (const sym of Object.getOwnPropertySymbols(fn)) { + const desc = Object.getOwnPropertyDescriptor(fn, sym); + if (desc) Object.defineProperty(wrapped, sym, desc); + } + + return wrapped; }; `; diff --git a/packages/plugin/src/virtualmodules/perfRuntime.ts b/packages/plugin/src/virtualmodules/perfRuntime.ts index 1d33067..fee189e 100644 --- a/packages/plugin/src/virtualmodules/perfRuntime.ts +++ b/packages/plugin/src/virtualmodules/perfRuntime.ts @@ -23,11 +23,6 @@ const __qwik_perf_init_csr__ = () => { const __qwik_perf_get_ssr_store__ = () => typeof process !== 'undefined' && process ? process : globalThis; -const __qwik_perf_to_error__ = (err) => ({ - name: (err && err.name) || 'Error', - message: err && err.message ? String(err.message) : String(err), -}); - const __qwik_perf_next_id__ = (perf) => { perf._id = (perf._id || 0) + 1; return perf._id; diff --git a/packages/plugin/src/virtualmodules/qwikComponentProxy.ts b/packages/plugin/src/virtualmodules/qwikComponentProxy.ts index ed19b82..eb43230 100644 --- a/packages/plugin/src/virtualmodules/qwikComponentProxy.ts +++ b/packages/plugin/src/virtualmodules/qwikComponentProxy.ts @@ -18,31 +18,17 @@ function componentQrl(qrl, options) { if(qrl.dev){ viteId = qrl.dev.file.replace(/[^/]*$/, qrl.dev.displayName); } - try { - const result = originalComponentQrl(qrl, options); - const duration = performance.now() - start; - __qwik_perf_commit_componentqrl__({ - component, - phase, - duration, - start, - viteId, - end: start + duration, - }); - return result; - } catch (err) { - const duration = performance.now() - start; - __qwik_perf_commit_componentqrl__({ - component, - phase, - duration, - start, - viteId, - end: start + duration, - error: __qwik_perf_to_error__(err), - }); - throw err; - } + const result = originalComponentQrl(qrl, options); + const duration = performance.now() - start; + __qwik_perf_commit_componentqrl__({ + component, + phase, + duration, + start, + viteId, + end: start + duration, + }); + return result; } export { componentQrl }; diff --git a/packages/ui/src/devtools.tsx b/packages/ui/src/devtools.tsx index f887059..2dd0a84 100644 --- a/packages/ui/src/devtools.tsx +++ b/packages/ui/src/devtools.tsx @@ -51,6 +51,25 @@ function getClientRpcFunctions() { }; } +function toDevtoolsRoutes(routes: any): RoutesInfo[] { + const children: RoutesInfo[] = routes?.children || []; + const directories: RoutesInfo[] = children.filter( + (child) => child.type === RouteType.DIRECTORY, + ); + + return [ + { + relativePath: '', + name: 'index', + type: RouteType.DIRECTORY, + path: '', + isSymbolicLink: false, + children: undefined, + }, + ...directories, + ]; +} + export const QwikDevtools = component$(() => { useStyles$(globalCss); const state = useStore({ @@ -64,8 +83,6 @@ export const QwikDevtools = component$(() => { isLoadingDependencies: false, }); - const clientReady = useSignal(false); - useVisibleTask$(async () => { const hot = await tryCreateHotContext(undefined, ['/']); if (!hot) { @@ -74,87 +91,58 @@ export const QwikDevtools = component$(() => { setViteClientContext(hot); createClientRpc(getClientRpcFunctions()); - clientReady.value = true; - }); - useVisibleTask$(async ({ track }) => { - track(() => clientReady.value); - if (!clientReady.value) return; const rpc = getViteClientRpc(); - try { - const assets = await rpc.getAssetsFromPublicDir(); - state.assets = assets; - } catch (error) { - log('Failed to load assets:', error); - } - }); - useVisibleTask$(async ({ track }) => { - track(() => clientReady.value); - if (!clientReady.value) return; - const rpc = getViteClientRpc(); - try { - const components = await rpc.getComponents(); - state.components = components; - } catch (error) { - log('Failed to load components:', error); - } - }); + // Group 1: load most data in parallel, each failure is isolated. + const group1 = Promise.allSettled([ + rpc.getAssetsFromPublicDir(), + rpc.getComponents(), + rpc.getRoutes(), + rpc.getQwikPackages(), + ]); - useVisibleTask$(async ({ track }) => { - track(() => clientReady.value); - if (!clientReady.value) return; - const rpc = getViteClientRpc(); - try { - const routes = await rpc.getRoutes(); - const children: RoutesInfo[] = routes?.children || []; - const directories: RoutesInfo[] = children.filter( - (child) => child.type === 'directory', - ); + // Group 2: load dependencies separately to keep a dedicated loading state. + state.isLoadingDependencies = true; + const depsPromise = rpc + .getAllDependencies() + .then((allDeps) => { + state.allDependencies = allDeps; + }) + .catch((error) => { + log('Failed to load all dependencies:', error); + }) + .finally(() => { + state.isLoadingDependencies = false; + }); - const values: RoutesInfo[] = [ - { - relativePath: '', - name: 'index', - type: RouteType.DIRECTORY, - path: '', - isSymbolicLink: false, - children: undefined, - }, - ...directories, - ]; + const [assetsRes, componentsRes, routesRes, packagesRes] = await group1; - state.routes = noSerialize(values); - } catch (error) { - log('Failed to load routes:', error); + if (assetsRes.status === 'fulfilled') { + state.assets = assetsRes.value; + } else { + log('Failed to load assets:', assetsRes.reason); } - }); - useVisibleTask$(async ({ track }) => { - track(() => clientReady.value); - if (!clientReady.value) return; - const rpc = getViteClientRpc(); - try { - const qwikPackages = await rpc.getQwikPackages(); - state.npmPackages = qwikPackages; - } catch (error) { - log('Failed to load Qwik packages:', error); + if (componentsRes.status === 'fulfilled') { + state.components = componentsRes.value; + } else { + log('Failed to load components:', componentsRes.reason); } - }); - useVisibleTask$(async ({ track }) => { - track(() => clientReady.value); - if (!clientReady.value) return; - const rpc = getViteClientRpc(); - state.isLoadingDependencies = true; - try { - const allDeps = await rpc.getAllDependencies(); - state.allDependencies = allDeps; - } catch (error) { - log('Failed to load all dependencies:', error); - } finally { - state.isLoadingDependencies = false; + if (routesRes.status === 'fulfilled') { + state.routes = noSerialize(toDevtoolsRoutes(routesRes.value)); + } else { + log('Failed to load routes:', routesRes.reason); } + + if (packagesRes.status === 'fulfilled') { + state.npmPackages = packagesRes.value; + } else { + log('Failed to load Qwik packages:', packagesRes.reason); + } + + await depsPromise; }); return (