Skip to content

Commit efd3119

Browse files
authored
feat: preload dependencies on server startup and improve UI (#72)
1 parent 9eeece0 commit efd3119

File tree

24 files changed

+1140
-640
lines changed

24 files changed

+1140
-640
lines changed

packages/kit/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ServerFunctions {
1313
getComponents: () => Promise<Component[]>;
1414
getRoutes: () => any;
1515
getQwikPackages: () => Promise<[string, string][]>;
16+
getAllDependencies: () => Promise<any[]>;
1617
installPackage: (
1718
packageName: string,
1819
isDev?: boolean,

packages/plugin/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { createServerRpc, setViteServerContext, VIRTUAL_QWIK_DEVTOOLS_KEY, INNER
44
import VueInspector from 'vite-plugin-inspect'
55
import useCollectHooksSource from './utils/useCollectHooks'
66
import { parseQwikCode } from './parse/parse';
7+
import { startPreloading } from './npm/index';
78

89

910
export function qwikDevtools(): Plugin[] {
1011
let _config: ResolvedConfig;
1112
const qwikData = new Map<string, any>();
13+
let preloadStarted = false;
1214
const qwikDevtoolsPlugin: Plugin = {
1315
name: 'vite-plugin-qwik-devtools',
1416
apply: 'serve',
@@ -39,6 +41,14 @@ export function qwikDevtools(): Plugin[] {
3941
},
4042
configResolved(viteConfig) {
4143
_config = viteConfig;
44+
45+
// Start preloading as early as possible, right after config is resolved
46+
if (!preloadStarted) {
47+
preloadStarted = true;
48+
startPreloading({ config: _config }).catch((err) => {
49+
console.error('[Qwik DevTools] Failed to start preloading:', err);
50+
});
51+
}
4252
},
4353
transform: {
4454
order: 'pre',
@@ -88,6 +98,8 @@ export function qwikDevtools(): Plugin[] {
8898
const rpcFunctions = getServerFunctions({ server, config: _config, qwikData });
8999

90100
createServerRpc(rpcFunctions);
101+
102+
// Preloading should have already started in configResolved
91103
},
92104
}
93105
return [

packages/plugin/src/npm/index.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ import { NpmInfo } from '@devtools/kit';
44
import { execSync } from 'child_process';
55
import path from 'path';
66

7+
// In-memory cache for npm package information
8+
interface CacheEntry {
9+
data: any;
10+
timestamp: number;
11+
}
12+
13+
const packageCache = new Map<string, CacheEntry>();
14+
const CACHE_TTL = 1000 * 60 * 30; // 30 minutes cache TTL
15+
16+
// Preloaded dependencies cache - loaded at server startup
17+
let preloadedDependencies: any[] | null = null;
18+
let isPreloading = false;
19+
let preloadPromise: Promise<any[]> | null = null;
20+
21+
function getCachedPackage(name: string): any | null {
22+
const cached = packageCache.get(name);
23+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
24+
return cached.data;
25+
}
26+
packageCache.delete(name);
27+
return null;
28+
}
29+
30+
function setCachedPackage(name: string, data: any): void {
31+
packageCache.set(name, {
32+
data,
33+
timestamp: Date.now(),
34+
});
35+
}
36+
737
export async function detectPackageManager(
838
projectRoot: string,
939
): Promise<'npm' | 'pnpm' | 'yarn'> {
@@ -38,6 +68,194 @@ export async function detectPackageManager(
3868
}
3969
}
4070

71+
// Preload dependencies function - moved to module scope
72+
const preloadDependencies = async (config: any): Promise<any[]> => {
73+
if (preloadedDependencies) {
74+
console.log('[Qwik DevTools] Dependencies already preloaded');
75+
return preloadedDependencies;
76+
}
77+
78+
if (isPreloading && preloadPromise) {
79+
console.log('[Qwik DevTools] Preloading already in progress...');
80+
return preloadPromise;
81+
}
82+
83+
isPreloading = true;
84+
console.log('[Qwik DevTools] Starting to preload dependencies...');
85+
86+
preloadPromise = (async () => {
87+
const pathToPackageJson = config.configFileDependencies.find(
88+
(file: string) => file.endsWith('package.json'),
89+
);
90+
91+
if (!pathToPackageJson) {
92+
preloadedDependencies = [];
93+
isPreloading = false;
94+
console.log('[Qwik DevTools] No package.json found');
95+
return [];
96+
}
97+
98+
try {
99+
const pkgJson = await fsp.readFile(pathToPackageJson, 'utf-8');
100+
const pkg = JSON.parse(pkgJson);
101+
102+
const allDeps = {
103+
...pkg.dependencies || {},
104+
...pkg.devDependencies || {},
105+
...pkg.peerDependencies || {},
106+
};
107+
108+
const dependencies = Object.entries<string>(allDeps);
109+
110+
// Check cache first
111+
const cachedPackages: any[] = [];
112+
const uncachedDependencies: [string, string][] = [];
113+
114+
for (const [name, version] of dependencies) {
115+
const cached = getCachedPackage(name);
116+
if (cached) {
117+
cachedPackages.push({ ...cached, version });
118+
} else {
119+
uncachedDependencies.push([name, version]);
120+
}
121+
}
122+
123+
if (uncachedDependencies.length === 0) {
124+
preloadedDependencies = cachedPackages;
125+
isPreloading = false;
126+
return cachedPackages;
127+
}
128+
129+
// Load all dependencies - use larger batch for initial preload
130+
const batchSize = 100;
131+
const batches = [];
132+
for (let i = 0; i < uncachedDependencies.length; i += batchSize) {
133+
batches.push(uncachedDependencies.slice(i, i + batchSize));
134+
}
135+
136+
const fetchedPackages: any[] = [];
137+
138+
console.log(`[Qwik DevTools] Fetching ${uncachedDependencies.length} packages in parallel...`);
139+
140+
const allBatchPromises = batches.map(async (batch) => {
141+
const batchPromises = batch.map(async ([name, version]) => {
142+
try {
143+
const controller = new AbortController();
144+
const timeoutId = setTimeout(() => controller.abort(), 5000); // Longer timeout for initial load
145+
146+
const response = await fetch(`https://registry.npmjs.org/${name}`, {
147+
headers: {
148+
'Accept': 'application/json',
149+
},
150+
signal: controller.signal,
151+
});
152+
153+
clearTimeout(timeoutId);
154+
155+
if (!response.ok) {
156+
throw new Error(`HTTP ${response.status}`);
157+
}
158+
159+
const packageData = await response.json();
160+
161+
const latestVersion = packageData['dist-tags']?.latest || version;
162+
const versionData = packageData.versions?.[latestVersion] || packageData.versions?.[version];
163+
164+
let repositoryUrl = versionData?.repository?.url || packageData.repository?.url;
165+
if (repositoryUrl) {
166+
repositoryUrl = repositoryUrl
167+
.replace(/^git\+/, '')
168+
.replace(/^ssh:\/\/git@/, 'https://')
169+
.replace(/\.git$/, '');
170+
}
171+
172+
let iconUrl = null;
173+
174+
if (packageData.logo) {
175+
iconUrl = packageData.logo;
176+
} else if (name.startsWith('@')) {
177+
const scope = name.split('/')[0].substring(1);
178+
iconUrl = `https://avatars.githubusercontent.com/${scope}?size=64`;
179+
} else if (repositoryUrl?.includes('github.com')) {
180+
const repoMatch = repositoryUrl.match(/github\.com\/([^\/]+)/);
181+
if (repoMatch) {
182+
iconUrl = `https://avatars.githubusercontent.com/${repoMatch[1]}?size=64`;
183+
}
184+
}
185+
186+
const packageInfo = {
187+
name,
188+
version,
189+
description: versionData?.description || packageData.description || 'No description available',
190+
author: versionData?.author || packageData.author,
191+
homepage: versionData?.homepage || packageData.homepage,
192+
repository: repositoryUrl,
193+
npmUrl: `https://www.npmjs.com/package/${name}`,
194+
iconUrl,
195+
};
196+
197+
setCachedPackage(name, packageInfo);
198+
return packageInfo;
199+
} catch (error) {
200+
const basicInfo = {
201+
name,
202+
version,
203+
description: 'No description available',
204+
npmUrl: `https://www.npmjs.com/package/${name}`,
205+
iconUrl: null,
206+
};
207+
208+
setCachedPackage(name, basicInfo);
209+
return basicInfo;
210+
}
211+
});
212+
213+
const batchResults = await Promise.allSettled(batchPromises);
214+
return batchResults
215+
.filter((result): result is PromiseFulfilledResult<any> => result.status === 'fulfilled')
216+
.map(result => result.value);
217+
});
218+
219+
const allBatchResults = await Promise.all(allBatchPromises);
220+
for (const batchResult of allBatchResults) {
221+
fetchedPackages.push(...batchResult);
222+
}
223+
224+
const allPackages = [...cachedPackages, ...fetchedPackages];
225+
preloadedDependencies = allPackages;
226+
isPreloading = false;
227+
228+
console.log(`[Qwik DevTools] ✓ Successfully preloaded ${allPackages.length} dependencies`);
229+
230+
return allPackages;
231+
} catch (error) {
232+
console.error('[Qwik DevTools] ✗ Failed to preload dependencies:', error);
233+
preloadedDependencies = [];
234+
isPreloading = false;
235+
return [];
236+
}
237+
})();
238+
239+
return preloadPromise;
240+
};
241+
242+
// Export function to start preloading from plugin initialization
243+
export async function startPreloading({ config }: { config: any }) {
244+
const startTime = Date.now();
245+
console.log('[Qwik DevTools] 🚀 Initiating dependency preload (background)...');
246+
247+
// Start preloading in background, don't wait for it
248+
preloadDependencies(config).then(() => {
249+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
250+
console.log(`[Qwik DevTools] ⚡ Preload completed in ${duration}s`);
251+
}).catch((err) => {
252+
console.error('[Qwik DevTools] ✗ Preload failed:', err);
253+
});
254+
255+
// Return immediately, don't block
256+
return Promise.resolve();
257+
}
258+
41259
export function getNpmFunctions({ config }: ServerContext) {
42260
return {
43261
async getQwikPackages(): Promise<NpmInfo> {
@@ -57,6 +275,24 @@ export function getNpmFunctions({ config }: ServerContext) {
57275
}
58276
},
59277

278+
async getAllDependencies(): Promise<any[]> {
279+
// Return preloaded data immediately if available
280+
if (preloadedDependencies) {
281+
console.log('[Qwik DevTools] Returning preloaded dependencies');
282+
return preloadedDependencies;
283+
}
284+
285+
// If preloading is in progress, wait for it
286+
if (isPreloading && preloadPromise) {
287+
console.log('[Qwik DevTools] Waiting for preload to complete...');
288+
return preloadPromise;
289+
}
290+
291+
// If preloading hasn't started (shouldn't happen), start it now
292+
console.log('[Qwik DevTools] Warning: Preload not started, starting now...');
293+
return preloadDependencies(config);
294+
},
295+
60296
async installPackage(
61297
packageName: string,
62298
isDev = true,

packages/ui/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"devDependencies": {
4040
"@devtools/kit": "workspace:*",
4141
"@qwikest/icons": "^0.0.13",
42+
"@tailwindcss/postcss": "^4.0.0",
43+
"@tailwindcss/vite": "^4.0.0",
4244
"@types/eslint": "8.56.10",
4345
"@types/node": "20.14.11",
4446
"@types/react": "^18.2.28",
@@ -61,7 +63,7 @@
6163
"react-dom": "18.2.0",
6264
"shiki": "^3.8.1",
6365
"superjson": "^2.2.2",
64-
"tailwindcss": "^3.4.6",
66+
"tailwindcss": "^4.0.0",
6567
"typescript": "5.4.5",
6668
"vite": "7.1.3",
6769
"vite-hot-client": "2.0.4",

packages/ui/postcss.config.cjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module.exports = {
22
plugins: {
3-
tailwindcss: {},
4-
autoprefixer: {},
3+
'@tailwindcss/postcss': { preflight: false },
54
},
65
};

packages/ui/src/components/Tab/Tab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const Tab = component$<TabProps>(({ state, id, title }) => {
1515
class={{
1616
'flex h-10 w-10 items-center justify-center rounded-lg p-2.5 transition-all duration-200':
1717
true,
18-
'bg-foreground/5 hover:bg-foreground/10 text-muted-foreground hover:bg-primary-hover hover:text-foreground':
18+
'bg-transparent hover:bg-foreground/5 text-muted-foreground hover:text-foreground':
1919
state.activeTab !== id,
2020
'shadow-accent/35 bg-accent text-white shadow-lg':
2121
state.activeTab === id,

packages/ui/src/components/TabContent/TabContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export const TabContent = component$(() => {
77
<Slot name="title" />
88
</div>
99

10-
<Slot name="content" />
10+
<div class="flex-1 overflow-y-auto pb-6">
11+
<Slot name="content" />
12+
</div>
1113
</div>
1214
);
1315
});

packages/ui/src/components/Tree/Tree.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,12 @@ const TreeNodeComponent = component$(
7575
style={{ paddingLeft: `${props.level * props.gap}px` }}
7676
class={`flex w-full cursor-pointer items-center p-1 ${
7777
isActive
78-
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-700/40 font-semibold'
78+
? 'border border-emerald-300 bg-emerald-100 font-semibold text-emerald-700 dark:border-emerald-700/40 dark:bg-emerald-900/30 dark:text-emerald-300'
7979
: ''
8080
}`}
8181
onClick$={handleNodeClick}
8282
>
83-
<div
84-
class={`inline-flex items-center rounded-md px-2 py-1`}
85-
>
83+
<div class={`inline-flex items-center rounded-md px-2 py-1`}>
8684
{hasChildren ? (
8785
<HiChevronUpMini
8886
class={`mr-2 h-4 w-4 flex-shrink-0 text-muted-foreground transition-transform duration-200 ${

0 commit comments

Comments
 (0)