From 757c24175bcde0546dc8ca6cafa5e9d99da83e9b Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:39:02 +1100 Subject: [PATCH] Created some scripts to analyze Closes #289 --- build/analyze-dependencies.js | 129 ++++++++++++++++++++++++++++++++ build/analyze-imports.js | 136 ++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 build/analyze-dependencies.js create mode 100644 build/analyze-imports.js diff --git a/build/analyze-dependencies.js b/build/analyze-dependencies.js new file mode 100644 index 00000000..81acec7d --- /dev/null +++ b/build/analyze-dependencies.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const webpackConfig = require('../webpack.config.js'); + +class DependencyAnalyzer { + constructor() { + this.dependencies = { + background: new Set(), + ui: new Set(), + content: new Set(), + shared: new Set(), + }; + } + + analyzeDependencies(stats) { + const modules = new Map(); + + // Use compilation.modules instead of chunk.getModules() + stats.compilation.modules.forEach((module) => { + if (!module.resource || !module.resource.includes('node_modules')) { + return; + } + + const pkg = module.resource.split('node_modules/')[1].split('/')[0]; + const chunks = Array.from(module.chunksIterable || []); + + chunks.forEach((chunk) => { + const target = chunk.name?.includes('background') + ? 'background' + : chunk.name?.includes('ui') + ? 'ui' + : chunk.name?.includes('content') + ? 'content' + : null; + + if (target) { + if (!modules.has(pkg)) { + modules.set(pkg, new Set()); + } + modules.get(pkg).add(target); + } + }); + }); + + // Categorize dependencies + modules.forEach((targets, pkg) => { + if (targets.size > 1) { + this.dependencies.shared.add(pkg); + } else { + const [target] = targets; + this.dependencies[target].add(pkg); + } + }); + + return this; + } + + generateReport() { + let report = '# Extension Dependencies Analysis\n\n'; + report += `> Generated on ${new Date().toLocaleString()}\n\n`; + + // Add summary + report += '## Summary\n\n'; + Object.entries(this.dependencies).forEach(([target, deps]) => { + report += `- **${target}**: ${deps.size} packages\n`; + }); + report += '\n'; + + // Add detailed lists + Object.entries(this.dependencies).forEach(([target, deps]) => { + report += `## ${target.charAt(0).toUpperCase() + target.slice(1)} Dependencies\n\n`; + if (deps.size === 0) { + report += '_No dependencies_\n\n'; + } else { + Array.from(deps) + .sort() + .forEach((dep) => { + report += `- \`${dep}\`\n`; + }); + report += '\n'; + } + }); + + return report; + } +} + +// Prepare build environment +console.log('Preparing build environment...'); + +// Copy manifest +fs.copyFileSync( + path.join(__dirname, '../_raw/manifest/manifest.dev.json'), + path.join(__dirname, '../_raw/manifest.json') +); + +// Clean dist directory +const distPath = path.join(__dirname, '../dist'); +if (!fs.existsSync(distPath)) { + fs.mkdirSync(distPath); +} else { + fs.rmSync(distPath, { recursive: true }); + fs.mkdirSync(distPath); +} + +// Copy _raw contents to dist +fs.cpSync(path.join(__dirname, '../_raw'), distPath, { recursive: true }); + +// Get webpack config using the same configuration as build:dev +const config = webpackConfig({ config: 'dev' }); + +// Run the analysis +console.log('Starting webpack build and analysis...'); +const analyzer = new DependencyAnalyzer(); + +webpack(config, (err, stats) => { + if (err || stats.hasErrors()) { + console.error('Build failed:', err || stats.toString()); + process.exit(1); + } + + console.log('Build complete, analyzing dependencies...'); + const report = analyzer.analyzeDependencies(stats).generateReport(); + + const reportPath = path.join(__dirname, '../extension-dependencies.md'); + fs.writeFileSync(reportPath, report); + console.log(`Analysis complete! Check ${reportPath}`); +}); diff --git a/build/analyze-imports.js b/build/analyze-imports.js new file mode 100644 index 00000000..05810eef --- /dev/null +++ b/build/analyze-imports.js @@ -0,0 +1,136 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; + +class ImportAnalyzer { + constructor() { + this.imports = new Map(); // package -> Set of files using it + this.packageLocations = { + background: new Set(), + ui: new Set(), + content: new Set(), + }; + } + + analyzeFile(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const relativePath = path.relative(process.cwd(), filePath); + + try { + const ast = parser.parse(content, { + sourceType: 'module', + plugins: ['typescript', 'jsx'], + }); + + traverse(ast, { + ImportDeclaration: (path) => { + const importPath = path.node.source.value; + + // Only analyze external packages (not relative imports) + if (!importPath.startsWith('.') && !importPath.startsWith('@/')) { + const packageName = importPath.startsWith('@') + ? importPath.split('/').slice(0, 2).join('/') + : importPath.split('/')[0]; + + if (!this.imports.has(packageName)) { + this.imports.set(packageName, new Set()); + } + this.imports.get(packageName).add(relativePath); + + // Categorize by location + if (relativePath.includes('background/')) { + this.packageLocations.background.add(packageName); + } else if (relativePath.includes('ui/')) { + this.packageLocations.ui.add(packageName); + } else if (relativePath.includes('content/')) { + this.packageLocations.content.add(packageName); + } + } + }, + CallExpression(path) { + // Check for require() calls + if (path.node.callee.name === 'require') { + const arg = path.node.arguments[0]; + if (arg && arg.type === 'StringLiteral') { + const importPath = arg.value; + if (!importPath.startsWith('.') && !importPath.startsWith('@/')) { + const packageName = importPath.startsWith('@') + ? importPath.split('/').slice(0, 2).join('/') + : importPath.split('/')[0]; + + if (!this.imports.has(packageName)) { + this.imports.set(packageName, new Set()); + } + this.imports.get(packageName).add(relativePath); + + // Categorize by location + if (relativePath.includes('background/')) { + this.packageLocations.background.add(packageName); + } else if (relativePath.includes('ui/')) { + this.packageLocations.ui.add(packageName); + } else if (relativePath.includes('content/')) { + this.packageLocations.content.add(packageName); + } + } + } + } + }, + }); + } catch (error) { + console.warn(`Failed to parse ${relativePath}:`, error.message); + } + } + + generateReport() { + let report = '# Dependency Usage Analysis\n\n'; + report += `> Generated on ${new Date().toLocaleString()}\n\n`; + + // Summary by location + report += '## Package Usage by Location\n\n'; + Object.entries(this.packageLocations).forEach(([location, packages]) => { + report += `### ${location.charAt(0).toUpperCase() + location.slice(1)}\n\n`; + Array.from(packages) + .sort() + .forEach((pkg) => { + const usageCount = this.imports.get(pkg).size; + report += `- \`${pkg}\` (${usageCount} ${usageCount === 1 ? 'file' : 'files'})\n`; + }); + report += '\n'; + }); + + // Detailed usage + report += '## Detailed Package Usage\n\n'; + Array.from(this.imports.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([pkg, files]) => { + report += `### \`${pkg}\`\n\n`; + Array.from(files) + .sort() + .forEach((file) => { + report += `- ${file}\n`; + }); + report += '\n'; + }); + + return report; + } +} + +// Run the analysis +console.log('Starting import analysis...'); +const analyzer = new ImportAnalyzer(); + +// Find all TypeScript and JavaScript files +const files = glob.sync('src/**/*.{ts,tsx,js,jsx}', { + ignore: ['**/node_modules/**', '**/dist/**'], +}); + +files.forEach((file) => { + analyzer.analyzeFile(file); +}); + +const report = analyzer.generateReport(); +fs.writeFileSync('dependency-usage.md', report); +console.log('Analysis complete! Check dependency-usage.md');