Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/treeshake-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Tree-shake test

on:
pull_request:

jobs:
treeshake-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run tree-shake test
run: pnpm --filter treeshake-test test

- name: Upload results table as artifact
uses: actions/upload-artifact@v4
with:
name: treeshake-table
path: ./apps/treeshake-test/results.md
4 changes: 4 additions & 0 deletions apps/treeshake-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/

results.md

2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { f32, sizeOf } from 'typegpu/data';
console.log(sizeOf(f32));
2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as d from 'typegpu/data';
console.log(d.sizeOf(d.f32));
2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import tgpu from 'typegpu';
console.log(tgpu.resolve({ externals: {} }));
75 changes: 75 additions & 0 deletions apps/treeshake-test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import {
bundleWithEsbuild,
bundleWithTsdown,
bundleWithWebpack,
generateMarkdownReport,
getFileSize,
} from './utils.ts';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const examples = [
'example1.ts',
'example2.ts',
'example3.ts',
];

const outDir = path.resolve(__dirname, 'dist');

console.log('Output directory:', outDir);

async function main() {
console.log('Starting bundler efficiency measurement...');
await fs.mkdir(outDir, { recursive: true });

const results: { example: string; bundler: string; size: number }[] = [];

for (const example of examples) {
console.log('\n================================');
console.log(`Processing ${example}...`);
console.log('================================\n');
const examplePath = path.resolve(__dirname, 'examples', example);
const exampleOutDir = path.join(outDir, path.basename(example, '.ts'));
await fs.mkdir(exampleOutDir, { recursive: true });

try {
console.log('Bundling with tsdown...');
const tsdownOut = await bundleWithTsdown(examplePath, exampleOutDir);
const tsdownSize = await getFileSize(tsdownOut);
results.push({ example, bundler: 'tsdown', size: tsdownSize });
console.log(`tsdown bundle size: ${tsdownSize} bytes`);
} catch (error) {
console.error('tsdown failed:', error);
}

try {
console.log('Bundling with esbuild...');
const esbuildOut = await bundleWithEsbuild(examplePath, exampleOutDir);
const esbuildSize = await getFileSize(esbuildOut);
results.push({ example, bundler: 'esbuild', size: esbuildSize });
console.log(`esbuild bundle size: ${esbuildSize} bytes`);
} catch (error) {
console.error('esbuild failed:', error);
}

try {
console.log('Bundling with webpack...');
const webpackOut = await bundleWithWebpack(examplePath, exampleOutDir);
const webpackSize = await getFileSize(webpackOut);
results.push({ example, bundler: 'webpack', size: webpackSize });
console.log(`webpack bundle size: ${webpackSize} bytes`);
} catch (error) {
console.error('webpack failed:', error);
}
}

await generateMarkdownReport(results);

console.log('\nMeasurement complete. Results saved to results.md');
}

main().catch(console.error);
23 changes: 23 additions & 0 deletions apps/treeshake-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "treeshake-test",
"private": true,
"version": "0.0.0",
"description": "Tree-shake testing app for TypeGPU",
"type": "module",
"scripts": {
"test": "tsx index.ts"
},
"dependencies": {
"typegpu": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"esbuild": "^0.25.10",
"ts-loader": "^9.5.4",
"tsdown": "^0.15.6",
"tsx": "^4.19.2",
"typescript": "catalog:types",
"webpack": "^5.102.0",
"webpack-cli": "^6.0.1"
}
}
8 changes: 8 additions & 0 deletions apps/treeshake-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": ["examples/**/*.ts"]
}
11 changes: 11 additions & 0 deletions apps/treeshake-test/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'tsdown';

export default defineConfig({
format: 'esm',
clean: false,
minify: true,
treeshake: true,
platform: 'neutral',
external: [],
noExternal: ['typegpu', /^typegpu\/.*$/], // typegpu/*
});
172 changes: 172 additions & 0 deletions apps/treeshake-test/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { build as esbuild } from 'esbuild';
import webpack from 'webpack';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';

const execFileAsync = promisify(execFile);

export async function bundleWithEsbuild(
entryPath: string,
outDir: string,
): Promise<string> {
const entryFileName = path.basename(entryPath, '.ts');
const outPath = path.join(outDir, `${entryFileName}.esbuild.js`);
await esbuild({
entryPoints: [entryPath],
bundle: true,
outfile: outPath,
format: 'esm',
minify: true,
treeShaking: true,
});
return outPath;
}

export async function bundleWithWebpack(
entryPath: string,
outDir: string,
): Promise<string> {
const entryFileName = path.basename(entryPath, '.ts');
const outPath = path.join(outDir, `${entryFileName}.webpack.js`);

return new Promise((resolve, reject) => {
webpack(
{
entry: entryPath,
output: {
path: path.dirname(outPath),
filename: path.basename(outPath),
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
compilerOptions: {
module: 'es2015',
target: 'es2015',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
},
transpileOnly: true,
},
},
exclude: /node_modules/,
},
],
},
optimization: {
minimize: true,
},
},
(err, stats) => {
if (err || stats?.hasErrors()) {
console.error(stats?.toString());
reject(err || new Error('Webpack bundling failed'));
} else {
resolve(outPath);
}
},
);
});
}

export async function bundleWithTsdown(
entryPath: string,
outDir: string,
): Promise<string> {
const entryFileName = path.basename(entryPath, '.ts');
const outPath = path.join(outDir, `${entryFileName}.tsdown.js`);

try {
console.log('Running tsdown with options...');

const { stdout, stderr } = await execFileAsync('npx', [
'tsdown',
entryPath,
'--out-dir',
outDir,
'--config',
'tsdown.config.ts',
], {
cwd: process.cwd(),
});

console.log('tsdown stdout:', stdout);
if (stderr) console.log('tsdown stderr:', stderr);

const files = await fs.readdir(outDir);
const tsdownFile = files.find((file) =>
file.includes(entryFileName) && file.endsWith('.js')
);
if (tsdownFile && tsdownFile !== `${entryFileName}.tsdown.js`) {
const actualOutPath = path.join(outDir, tsdownFile);
await fs.rename(actualOutPath, outPath);
return outPath;
}
if (tsdownFile) {
return path.join(outDir, tsdownFile);
}

throw new Error('tsdown output file not found');
} catch (error) {
throw new Error(`tsdown bundling failed: ${error}`);
}
}

export async function getFileSize(filePath: string): Promise<number> {
const stats = await fs.stat(filePath);
return stats.size;
}

export async function generateMarkdownReport(
results: { example: string; bundler: string; size: number }[],
) {
const grouped: Record<string, { bundler: string; size: number }[]> = {};
for (const r of results) {
const arr = grouped[r.example] ?? [];
arr.push({ bundler: r.bundler, size: r.size });
grouped[r.example] = arr;
}

let report = '# Bundler Efficiency Report\n\n';

for (const example of Object.keys(grouped)) {
const rows = grouped[example] ?? [];
// Read snippet code
let snippet = '';
try {
const snippetPath = path.join(
'examples',
example + (example.endsWith('.ts') ? '' : '.ts'),
);
snippet = await fs.readFile(snippetPath, 'utf8');
} catch (e) {
snippet = '_Could not read example source._';
}
report += `## ${example}\n\n`;
report += `\`\`\`typescript\n${snippet.trim()}\n\`\`\`\n\n`;
report += '| Bundler | Bundle Size (bytes) |\n';
report += '|---------|---------------------|\n';
for (const row of rows) {
report += `| \`${row.bundler}\` | ${row.size} |\n`;
}
report += '\n';
}

// General table
report += '---\n\n';
report += '| Example File | Bundler | Bundle Size (bytes) |\n';
report += '|--------------|-----------|---------------------|\n';
for (const result of results) {
report +=
`| \`${result.example}\` | \`${result.bundler}\` | ${result.size} |\n`;
}

await fs.writeFile('results.md', report);
}
Loading