Files
upstream-tldraw/apps/dotcom/client/scripts/measure-bundle-size.test.ts
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

251 lines
7.1 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { gzipSync } from 'zlib'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
BundleAsset,
getAssetCategory,
getBundleSizeEvents,
getInitialAssetNames,
measureBundleSize,
stripContentHash,
summarizeAssets,
} from './measure-bundle-size'
describe('getAssetCategory', () => {
it('categorizes assets by extension', () => {
expect(getAssetCategory('index-BdIhNi0J.js')).toBe('js')
expect(getAssetCategory('index-BdIhNi0J.css')).toBe('css')
expect(getAssetCategory('IBMPlexSans-Medium-CxYz1234.woff2')).toBe('font')
expect(getAssetCategory('0_merged-abcd1234.svg')).toBe('image')
expect(getAssetCategory('en-abcd1234.json')).toBe('json')
expect(getAssetCategory('something-abcd1234.wasm')).toBe('other')
})
})
describe('stripContentHash', () => {
it('removes the content hash so a chunk is comparable across builds', () => {
expect(stripContentHash('index-BdIhNi0J.js')).toBe('index.js')
expect(stripContentHash('TlaEditor-_a1B2c3D.js')).toBe('TlaEditor.js')
expect(stripContentHash('IBMPlexSans-Medium-CxYz1234.woff2')).toBe('IBMPlexSans-Medium.woff2')
})
it('leaves names without a hash alone', () => {
expect(stripContentHash('index.js')).toBe('index.js')
expect(stripContentHash('IBMPlexSans-Medium.woff2')).toBe('IBMPlexSans-Medium.woff2')
})
})
describe('getInitialAssetNames', () => {
it('collects every asset index.html references', () => {
const html = `<!doctype html><html><head>
<script type="module" crossorigin src="/assets/index-BdIhNi0J.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-CxYz1234.js">
<link rel="stylesheet" crossorigin href="/assets/index-DeFg5678.css">
<link rel="preload" href="/assets/IBMPlexSans-Medium-HiJk9012.woff2" as="font">
<link rel="preload" href="/assets/0_merged-LmNo3456.svg" as="image">
<link rel="icon" href="/favicon.svg">
</head><body></body></html>`
expect(getInitialAssetNames(html)).toEqual(
new Set([
'index-BdIhNi0J.js',
'vendor-CxYz1234.js',
'index-DeFg5678.css',
'IBMPlexSans-Medium-HiJk9012.woff2',
'0_merged-LmNo3456.svg',
])
)
})
it('ignores query strings and assets outside /assets', () => {
const html = `<script src="/assets/index-BdIhNi0J.js?v=1"></script><img src="/logo.png">`
expect(getInitialAssetNames(html)).toEqual(new Set(['index-BdIhNi0J.js']))
})
})
describe('summarizeAssets', () => {
function asset(overrides: Partial<BundleAsset>): BundleAsset {
return {
fileName: 'index-BdIhNi0J.js',
name: 'index.js',
category: 'js',
bytes: 100,
gzipBytes: 40,
isInitial: false,
...overrides,
}
}
const noAssets = {
font: { count: 0, bytes: 0, gzipBytes: 0 },
image: { count: 0, bytes: 0, gzipBytes: 0 },
json: { count: 0, bytes: 0, gzipBytes: 0 },
other: { count: 0, bytes: 0, gzipBytes: 0 },
}
it('totals assets overall, by category, and across the initial payload', () => {
const report = summarizeAssets([
asset({ bytes: 100, gzipBytes: 40, isInitial: true }),
asset({ bytes: 200, gzipBytes: 60 }),
asset({ category: 'css', bytes: 50, gzipBytes: 20, isInitial: true }),
])
expect(report.total).toEqual({ count: 3, bytes: 350, gzipBytes: 120 })
expect(report.initial).toEqual({ count: 2, bytes: 150, gzipBytes: 60 })
expect(report.byCategory).toEqual({
js: { count: 2, bytes: 300, gzipBytes: 100 },
css: { count: 1, bytes: 50, gzipBytes: 20 },
...noAssets,
})
expect(report.initialByCategory).toEqual({
js: { count: 1, bytes: 100, gzipBytes: 40 },
css: { count: 1, bytes: 50, gzipBytes: 20 },
...noAssets,
})
})
})
describe('measureBundleSize', () => {
let staticDir: string
beforeEach(() => {
staticDir = mkdtempSync(join(tmpdir(), 'bundle-size-'))
mkdirSync(join(staticDir, 'assets'))
})
afterEach(() => {
rmSync(staticDir, { recursive: true, force: true })
})
function writeAsset(fileName: string, contents: string) {
writeFileSync(join(staticDir, 'assets', fileName), contents)
}
it('measures raw and gzipped sizes, and marks assets index.html loads as initial', () => {
const entry = 'a'.repeat(1000)
const lazy = 'b'.repeat(500)
writeAsset('index-BdIhNi0J.js', entry)
writeAsset('TlaEditor-CxYz1234.js', lazy)
writeFileSync(
join(staticDir, 'index.html'),
`<script type="module" src="/assets/index-BdIhNi0J.js"></script>`
)
const report = measureBundleSize(staticDir)
expect(report.assets).toEqual([
{
fileName: 'index-BdIhNi0J.js',
name: 'index.js',
category: 'js',
bytes: 1000,
gzipBytes: gzipSync(entry).byteLength,
isInitial: true,
},
{
fileName: 'TlaEditor-CxYz1234.js',
name: 'TlaEditor.js',
category: 'js',
bytes: 500,
gzipBytes: gzipSync(lazy).byteLength,
isInitial: false,
},
])
expect(report.initial.bytes).toBe(1000)
})
it('skips source maps, which are only downloaded with devtools open', () => {
writeAsset('index-BdIhNi0J.js', 'console.log(1)')
writeAsset('index-BdIhNi0J.js.map', 'x'.repeat(10000))
writeFileSync(join(staticDir, 'index.html'), '<html></html>')
const report = measureBundleSize(staticDir)
expect(report.assets.map((asset) => asset.fileName)).toEqual(['index-BdIhNi0J.js'])
})
})
describe('getBundleSizeEvents', () => {
it('reports totals once and the largest assets individually', () => {
const report = summarizeAssets([
{
fileName: 'index-BdIhNi0J.js',
name: 'index.js',
category: 'js',
bytes: 100,
gzipBytes: 40,
isInitial: true,
},
])
const [summary, ...assets] = getBundleSizeEvents(report, { git_commit: 'abc123' })
expect(summary).toEqual({
event: 'dotcom_bundle_size',
distinctId: 'dotcom-bundle-size',
properties: {
git_commit: 'abc123',
total_bytes: 100,
total_gzip_bytes: 40,
total_count: 1,
initial_bytes: 100,
initial_gzip_bytes: 40,
initial_count: 1,
js_bytes: 100,
js_gzip_bytes: 40,
js_count: 1,
initial_js_bytes: 100,
initial_js_gzip_bytes: 40,
initial_js_count: 1,
css_bytes: 0,
css_gzip_bytes: 0,
css_count: 0,
initial_css_bytes: 0,
initial_css_gzip_bytes: 0,
initial_css_count: 0,
font_bytes: 0,
font_gzip_bytes: 0,
font_count: 0,
initial_font_bytes: 0,
initial_font_gzip_bytes: 0,
initial_font_count: 0,
image_bytes: 0,
image_gzip_bytes: 0,
image_count: 0,
initial_image_bytes: 0,
initial_image_gzip_bytes: 0,
initial_image_count: 0,
json_bytes: 0,
json_gzip_bytes: 0,
json_count: 0,
initial_json_bytes: 0,
initial_json_gzip_bytes: 0,
initial_json_count: 0,
other_bytes: 0,
other_gzip_bytes: 0,
other_count: 0,
initial_other_bytes: 0,
initial_other_gzip_bytes: 0,
initial_other_count: 0,
},
})
expect(assets).toEqual([
{
event: 'dotcom_bundle_size_asset',
distinctId: 'dotcom-bundle-size',
properties: {
git_commit: 'abc123',
asset_name: 'index.js',
asset_file_name: 'index-BdIhNi0J.js',
asset_category: 'js',
asset_is_initial: true,
bytes: 100,
gzip_bytes: 40,
},
},
])
})
})