build-intelligence-tools.js•15 kB
/**
* Build Intelligence Tools for WebSee MCP Server
*
* Provides advanced build artifact analysis capabilities including:
* - Webpack/Vite manifest parsing
* - Code chunk analysis
* - Module dependency tracking
* - Bundle size optimization
*
* @module build-intelligence-tools
*/
import { z } from 'zod';
import { BuildArtifactManager } from '../build-artifact-manager.js';
// ============================================================================
// Zod Schemas for Tool Validation
// ============================================================================
export const BuildGetManifestSchema = z.object({
url: z.string().url().describe('The application URL to analyze'),
});
export const BuildGetChunksSchema = z.object({
url: z.string().url().describe('The application URL to analyze'),
});
export const BuildFindModuleSchema = z.object({
url: z.string().url().describe('The application URL to analyze'),
moduleName: z.string().describe('Name or path of the module to find'),
});
export const BuildGetDependenciesSchema = z.object({
url: z.string().url().describe('The application URL to analyze'),
moduleName: z.string().optional().describe('Specific module to get dependencies for (optional)'),
});
export const BuildAnalyzeSizeSchema = z.object({
url: z.string().url().describe('The application URL to analyze'),
threshold: z
.number()
.optional()
.default(100)
.describe('Size threshold in KB to flag large modules'),
});
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Initialize page and load build artifacts
*/
async function initializeBuildAnalysis(page, url) {
// Navigate to the URL to trigger build artifact loading
await page.goto(url, { waitUntil: 'networkidle' });
// Extract project root from URL or use a default approach
// In production, this would be configured or detected
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
const buildManager = new BuildArtifactManager(projectRoot);
try {
await buildManager.loadBuildArtifacts();
}
catch (error) {
// If local artifacts aren't available, try to fetch from the page
await fetchBuildArtifactsFromPage(page, buildManager);
}
return buildManager;
}
/**
* Attempt to fetch build artifacts from the running page
*/
async function fetchBuildArtifactsFromPage(page, _buildManager) {
// Try to extract webpack stats from window.__webpack_require__ or similar
const webpackData = await page.evaluate(() => {
// Check for webpack
if (window.__webpack_require__) {
const webpack = window.__webpack_require__;
if (webpack.m) {
return {
type: 'webpack',
modules: Object.keys(webpack.m).map(id => ({
id,
name: webpack.m[id].toString().substring(0, 100),
size: webpack.m[id].toString().length,
})),
};
}
}
// Check for vite
if (window.__vite__) {
return {
type: 'vite',
modules: [],
};
}
return null;
});
if (!webpackData) {
throw new Error('Unable to load build artifacts from filesystem or page runtime');
}
}
/**
* Format bytes to KB string
*/
function formatKB(bytes) {
return (bytes / 1024).toFixed(2);
}
/**
* Format bytes to MB string
*/
function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(2);
}
/**
* Get file type from filename
*/
function getFileType(filename) {
const ext = filename.split('.').pop()?.toLowerCase();
if (ext === 'js' || ext === 'mjs' || ext === 'jsx' || ext === 'ts' || ext === 'tsx') {
return 'js';
}
if (ext === 'css' || ext === 'scss' || ext === 'sass' || ext === 'less') {
return 'css';
}
return 'other';
}
/**
* Extract module dependencies from source code
*/
function extractDependencies(source) {
if (!source)
return [];
const dependencies = [];
// Match ES6 imports
const importMatches = source.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g);
for (const match of importMatches) {
dependencies.push(match[1]);
}
// Match CommonJS requires
const requireMatches = source.matchAll(/require\(['"]([^'"]+)['"]\)/g);
for (const match of requireMatches) {
dependencies.push(match[1]);
}
return [...new Set(dependencies)];
}
// ============================================================================
// Tool Implementation Functions
// ============================================================================
/**
* Tool 1: Get build manifest
* Returns complete build manifest with chunks, assets, and modules
*/
export async function buildGetManifest(page, params) {
const buildManager = await initializeBuildAnalysis(page, params.url);
if (!buildManager.isLoaded()) {
throw new Error('Failed to load build manifest');
}
const summary = buildManager.getSummary();
// Get manifest data - this would need to be exposed by BuildArtifactManager
// For now, we'll return the summary data
return {
type: summary.type,
version: 'unknown', // BuildArtifactManager doesn't expose version in getSummary
chunks: [], // Would need to expose chunks from manifest
assets: [],
modules: summary.largestModules.map(m => ({
id: m.id,
name: m.name,
size: m.size,
chunks: m.chunks,
})),
};
}
/**
* Tool 2: Get all code chunks
* Returns detailed information about all chunks in the bundle
*/
export async function buildGetChunks(page, params) {
const buildManager = await initializeBuildAnalysis(page, params.url);
if (!buildManager.isLoaded()) {
throw new Error('Failed to load build manifest');
}
// Extract chunks from page scripts
const scriptChunks = await page.$$eval('script[src]', scripts => scripts.map((script) => ({
src: script.src,
async: script.async,
defer: script.defer,
})));
const chunks = scriptChunks.map((script, index) => {
const filename = script.src.split('/').pop() || `chunk-${index}`;
return {
id: index,
files: [filename],
modules: [],
size: 0, // Size would need to be fetched via network trace
sizeKB: '0.00',
entry: index === 0,
initial: script.defer === false && script.async === false,
};
});
return { chunks };
}
/**
* Tool 3: Find specific module in bundle
* Searches for a module by name and returns its details
*/
export async function buildFindModule(page, params) {
const buildManager = await initializeBuildAnalysis(page, params.url);
if (!buildManager.isLoaded()) {
throw new Error('Failed to load build manifest');
}
const module = buildManager.findModule(params.moduleName);
if (!module) {
return null;
}
const dependencies = extractDependencies(module.source || module.originalSource);
return {
name: module.name,
id: module.id,
size: module.size,
sizeKB: formatKB(module.size),
chunks: module.chunks,
dependencies,
source: module.source ? module.source.substring(0, 500) + '...' : undefined,
};
}
/**
* Tool 4: Get dependency graph
* Returns the dependency graph for all modules or a specific module
*/
export async function buildGetDependencies(page, params) {
const buildManager = await initializeBuildAnalysis(page, params.url);
if (!buildManager.isLoaded()) {
throw new Error('Failed to load build manifest');
}
const summary = buildManager.getSummary();
const dependencies = [];
// If specific module requested, find its dependencies
if (params.moduleName) {
const module = buildManager.findModule(params.moduleName);
if (module) {
const moduleDeps = extractDependencies(module.source || module.originalSource);
for (const dep of moduleDeps) {
const depModule = buildManager.findModule(dep);
dependencies.push({
name: dep,
size: depModule?.size || 0,
sizeKB: formatKB(depModule?.size || 0),
dependents: [params.moduleName],
chunks: depModule?.chunks || [],
});
}
}
}
else {
// Return all modules as dependency nodes
for (const module of summary.largestModules) {
dependencies.push({
name: module.name,
size: module.size,
sizeKB: formatKB(module.size),
dependents: [], // Would need to compute reverse dependencies
chunks: module.chunks,
});
}
}
return { dependencies };
}
/**
* Tool 5: Analyze bundle sizes
* Analyzes bundle sizes and provides optimization recommendations
*/
export async function buildAnalyzeSize(page, params) {
const buildManager = await initializeBuildAnalysis(page, params.url);
if (!buildManager.isLoaded()) {
throw new Error('Failed to load build manifest');
}
const summary = buildManager.getSummary();
// Analyze by type
const byType = {
js: { count: 0, size: 0, sizeKB: '0.00' },
css: { count: 0, size: 0, sizeKB: '0.00' },
other: { count: 0, size: 0, sizeKB: '0.00' },
};
let totalSize = 0;
const allAssets = [];
// Add module sizes
for (const module of summary.largestModules) {
const type = getFileType(module.name);
const typeKey = type;
byType[typeKey].count++;
byType[typeKey].size += module.size;
totalSize += module.size;
allAssets.push({
name: module.name,
size: module.size,
type,
});
}
// Format type sizes
for (const type of Object.keys(byType)) {
byType[type].sizeKB = formatKB(byType[type].size);
}
// Find large assets
const thresholdBytes = params.threshold * 1024;
const largeAssets = allAssets
.filter(asset => asset.size > thresholdBytes)
.sort((a, b) => b.size - a.size)
.slice(0, 10)
.map(asset => ({
name: asset.name,
size: asset.size,
sizeKB: formatKB(asset.size),
type: asset.type,
percentage: ((asset.size / totalSize) * 100).toFixed(2) + '%',
}));
// Generate recommendations
const recommendations = [];
if (largeAssets.length > 0) {
recommendations.push(`Found ${largeAssets.length} asset(s) exceeding ${params.threshold}KB threshold. Consider code splitting.`);
}
if (byType.js.size > 500 * 1024) {
recommendations.push(`Total JavaScript size is ${formatKB(byType.js.size)}KB. Consider lazy loading non-critical modules.`);
}
if (summary.totalChunks < 3 && totalSize > 200 * 1024) {
recommendations.push(`Only ${summary.totalChunks} chunk(s) detected. Consider implementing code splitting for better caching.`);
}
const duplicateModules = summary.largestModules.filter(m => m.chunks.length > 1);
if (duplicateModules.length > 5) {
recommendations.push(`${duplicateModules.length} modules appear in multiple chunks. Consider using commons chunks or tree shaking.`);
}
if (byType.css.size > 100 * 1024) {
recommendations.push(`CSS bundle size is ${formatKB(byType.css.size)}KB. Consider critical CSS extraction and lazy loading.`);
}
return {
total: totalSize,
totalKB: formatKB(totalSize),
totalMB: formatMB(totalSize),
byType,
large: largeAssets,
recommendations,
};
}
// ============================================================================
// MCP Tool Definitions Export
// ============================================================================
/**
* MCP-compatible tool definitions for registration
*/
export const BUILD_INTELLIGENCE_TOOLS = [
{
name: 'build_get_manifest',
description: 'Get webpack/vite build manifest including chunks, assets, and modules',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The application URL to analyze',
},
},
required: ['url'],
},
},
{
name: 'build_get_chunks',
description: 'Get all code chunks with their files, modules, and sizes',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The application URL to analyze',
},
},
required: ['url'],
},
},
{
name: 'build_find_module',
description: 'Find a specific module in the bundle by name or path',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The application URL to analyze',
},
moduleName: {
type: 'string',
description: 'Name or path of the module to find',
},
},
required: ['url', 'moduleName'],
},
},
{
name: 'build_get_dependencies',
description: 'Get dependency graph for all modules or a specific module',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The application URL to analyze',
},
moduleName: {
type: 'string',
description: 'Specific module to get dependencies for (optional)',
},
},
required: ['url'],
},
},
{
name: 'build_analyze_size',
description: 'Analyze bundle sizes by type and identify large modules with optimization recommendations',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The application URL to analyze',
},
threshold: {
type: 'number',
description: 'Size threshold in KB to flag large modules (default: 100)',
},
},
required: ['url'],
},
},
];
//# sourceMappingURL=build-intelligence-tools.js.map