Files
self/app/scripts/test-tree-shaking.cjs
Justin Hernandez 82d26669bc Enable tree-shakeable exports (#823)
* Add tree-shakeable exports

* Migrate imports for tree-shakeable paths

* Document ESM extension requirement

* udpates

* install new lock

* yarn nice

* build deps

* save working index export no wildcard approach

* save wip

* fix building

* add tree shaking doc and examples

* sort package json files

* update package.json

* fix analyzing web

* make sure that web is built

* wip tree shaking

* building works again. save wip logic

* use granular imports

* wip test

* save wip

* Remove hardcoded .d.ts files and setup automatic TypeScript declaration generation

- Remove redundant constants.d.ts, types.d.ts, utils.d.ts files
- Add build:types script to automatically generate TypeScript declarations
- Update tsup config to disable DTS generation (handled separately)
- Update .gitignore to prevent future commits of generated .d.ts files
- Fixes import resolution errors in app by ensuring declarations are always generated

* Add .gitignore rules for generated TypeScript declarations

* ignore dts files

* Remove redundant index.js re-export files

- Remove constants.js, types.js, utils.js as they're redundant with tsup build
- These were just re-exports pointing to dist files that tsup generates
- package.json exports already point directly to built files
- Update .gitignore to prevent future commits of these generated files
- tsup handles all the building, no manual re-export files needed

* save current wip fixes

* add tsup config for web building

* common prettier and fix imports

* prettier

* fix tests

* implement level 3 tree shaking

* improve splitting

* optimize vite web building and prettier

* remove comments

* sort export params

* feedback and fix pipelines

* fix circuit-names path

* fix test

* fix building

* sort

* fix building

* allow cursor to edit scripts

* fix loadDocumentCatalog undefined

* fix build settings

* fix build settings

* additional metro tree shaking

* improved discovery script for xcode building

* pr feedback and fix camelCasing

* simplify shim setup

* fix xcode building and add command to test building

* remove comment

* simplify
2025-08-02 16:55:05 -07:00

312 lines
9.3 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Tree shaking test configurations
const TEST_CONFIGS = [
{
name: 'full-import',
description: 'Import everything from @selfxyz/common (worst case)',
imports: `import * as common from '@selfxyz/common';
console.log('API_URL:', common.API_URL);
console.log('hash function exists:', typeof common.hash);`,
},
{
name: 'mixed-import',
description: 'Mixed import pattern (current typical usage)',
imports: `import { API_URL, hash, buildSMT, generateCommitment } from '@selfxyz/common';
console.log('API_URL:', API_URL);
console.log('hash result:', hash('test'));`,
},
{
name: 'granular-constants',
description: 'Only constants via granular import (best case)',
imports: `import { API_URL } from '@selfxyz/common/constants';
console.log('API_URL:', API_URL);`,
},
{
name: 'granular-utils',
description: 'Only hash utils via granular import',
imports: `import { hash, customHasher } from '@selfxyz/common/utils';
console.log('hash result:', hash('test'));`,
},
{
name: 'granular-mixed',
description: 'Mixed granular imports (recommended pattern)',
imports: `import { API_URL } from '@selfxyz/common/constants';
import { hash } from '@selfxyz/common/utils';
console.log('API_URL:', API_URL);
console.log('hash result:', hash('test'));`,
},
];
function formatBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
}
function createTestApp(config, testDir, commonPackagePath) {
const appDir = path.join(testDir, config.name);
fs.mkdirSync(appDir, { recursive: true });
// Create package.json
const packageJson = {
name: `tree-shaking-test-${config.name}`,
version: '1.0.0',
private: true,
type: 'module',
dependencies: {
'@selfxyz/common': `file:${commonPackagePath}`,
},
};
fs.writeFileSync(
path.join(appDir, 'package.json'),
JSON.stringify(packageJson, null, 2),
);
// Create test file
const testContent = `// ${config.description}
${config.imports}
`;
fs.writeFileSync(path.join(appDir, 'index.js'), testContent);
return appDir;
}
function createWebpackConfig(appDir) {
const webpackConfig = `const path = require('path');
module.exports = {
mode: 'production',
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
resolve: {
extensions: ['.js', '.ts'],
},
optimization: {
usedExports: true,
sideEffects: false,
minimize: true,
},
target: 'node',
externals: {
// Don't bundle node modules for more accurate size comparison
...require('webpack-node-externals')(),
},
stats: {
modules: true,
reasons: true,
usedExports: true,
providedExports: true,
},
};
`;
fs.writeFileSync(path.join(appDir, 'webpack.config.cjs'), webpackConfig);
}
function runTest(config, testDir, commonPackagePath) {
console.log(`\n🧪 Testing: ${config.name}`);
console.log(`📝 ${config.description}`);
const appDir = createTestApp(config, testDir, commonPackagePath);
try {
// Install dependencies
console.log(' 📦 Installing dependencies...');
execSync('yarn install', {
cwd: appDir,
stdio: 'pipe',
});
// Build with webpack for size analysis
createWebpackConfig(appDir);
// Install webpack locally for this test
execSync('yarn add -D webpack webpack-cli webpack-node-externals', {
cwd: appDir,
stdio: 'pipe',
env: { ...process.env, CI: 'true' }, // Set CI environment to prevent interactive prompts
});
console.log(' 🔨 Building bundle...');
execSync('yarn webpack --mode=production', {
cwd: appDir,
stdio: 'pipe',
env: { ...process.env, CI: 'true' }, // Set CI environment to prevent interactive prompts
});
// Measure bundle size
const bundlePath = path.join(appDir, 'dist', 'bundle.js');
if (fs.existsSync(bundlePath)) {
const bundleSize = fs.statSync(bundlePath).size;
console.log(` 📊 Bundle size: ${formatBytes(bundleSize)}`);
return { config: config.name, size: bundleSize };
} else {
console.log(' ❌ Bundle not found');
return { config: config.name, size: -1 };
}
} catch (error) {
console.log(` ❌ Test failed: ${error.message}`);
return { config: config.name, size: -1, error: error.message };
}
}
function generateReport(results) {
console.log('\n📊 TREE SHAKING EFFECTIVENESS REPORT');
console.log('=====================================');
const validResults = results.filter(r => r.size > 0);
if (validResults.length === 0) {
console.log('❌ No valid results to compare');
return;
}
// Sort by bundle size
validResults.sort((a, b) => a.size - b.size);
const baseline = validResults.find(r => r.config === 'full-import');
const smallest = validResults[0];
console.log('\nBundle Sizes (smallest to largest):');
validResults.forEach((result, index) => {
const icon =
index === 0 ? '🏆' : index === 1 ? '🥈' : index === 2 ? '🥉' : '📦';
let comparison = '';
if (baseline && result.config !== 'full-import') {
const rawDiff = baseline.size - result.size;
if (rawDiff > 0) {
const reduction = ((rawDiff / baseline.size) * 100).toFixed(1);
const savedBytes = formatBytes(rawDiff);
comparison = ` (${reduction}% smaller, saves ${savedBytes})`;
}
}
console.log(
`${icon} ${result.config.padEnd(20)} ${formatBytes(result.size)}${comparison}`,
);
});
if (baseline && smallest.config !== 'full-import') {
const rawMaxDiff = baseline.size - smallest.size;
if (rawMaxDiff > 0) {
const maxReduction = ((rawMaxDiff / baseline.size) * 100).toFixed(1);
const maxSaved = formatBytes(rawMaxDiff);
console.log(
`\n🎯 Maximum tree shaking benefit: ${maxReduction}% reduction (${maxSaved} saved)`,
);
}
}
// Recommendations
console.log('\n💡 RECOMMENDATIONS:');
if (validResults.some(r => r.config.startsWith('granular'))) {
console.log(
'✅ Use granular imports like "@selfxyz/common/constants" for better tree shaking',
);
}
console.log('✅ Avoid "import * as" patterns when possible');
console.log('✅ Import only what you need from each module');
// Check if tree shaking is working
const hasVariation =
Math.max(...validResults.map(r => r.size)) -
Math.min(...validResults.map(r => r.size)) >
1024;
if (!hasVariation) {
console.log(
'\n⚠ WARNING: Bundle sizes are very similar - tree shaking may not be working effectively',
);
console.log(' Check that "sideEffects": false is set in package.json');
console.log(' Ensure proper ESM exports are configured');
} else {
console.log(
'\n✅ Tree shaking appears to be working - different import patterns show different bundle sizes',
);
}
}
async function main() {
console.log('🌳 Tree Shaking Effectiveness Test');
console.log('==================================');
// Create temporary test directory
const testDir = path.join(
os.tmpdir(),
'tree-shaking-tests',
Date.now().toString(),
);
fs.mkdirSync(testDir, { recursive: true });
console.log(`📁 Test directory: ${testDir}`);
try {
// Ensure @selfxyz/common is built
console.log('\n🔨 Building @selfxyz/common...');
const commonDir = path.join(__dirname, '..', '..', 'common');
execSync('yarn workspace @selfxyz/common build', {
stdio: 'inherit',
cwd: path.join(__dirname, '..', '..'),
});
// Copy the built common package to test directory for file:// reference
const commonPackagePath = path.join(testDir, 'common-package');
console.log(`📦 Copying @selfxyz/common to test directory...`);
// Copy package.json, dist folder, and other necessary files
fs.mkdirSync(commonPackagePath, { recursive: true });
fs.copyFileSync(
path.join(commonDir, 'package.json'),
path.join(commonPackagePath, 'package.json'),
);
// Copy dist directory recursively
const copyDir = (src, dest) => {
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
};
copyDir(path.join(commonDir, 'dist'), path.join(commonPackagePath, 'dist'));
// Run all tests
const results = [];
for (const config of TEST_CONFIGS) {
const result = runTest(config, testDir, commonPackagePath);
results.push(result);
}
// Generate report
generateReport(results);
console.log(`\n📁 Test artifacts available at: ${testDir}`);
} catch (error) {
console.error('❌ Test suite failed:', error.message);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { TEST_CONFIGS, runTest, generateReport, createTestApp };