Skip to content

Commit 0bbd1f6

Browse files
committed
Show estimated size of generated classes
1 parent 75d56a0 commit 0bbd1f6

File tree

4 files changed

+127
-0
lines changed

4 files changed

+127
-0
lines changed

packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,59 @@ defineTest({
3737
])
3838
},
3939
})
40+
41+
defineTest({
42+
name: 'The user is warned when @source inline(…) generates a lerge amount of CSS',
43+
fs: {
44+
'app.css': css`
45+
@import 'tailwindcss';
46+
`,
47+
},
48+
prepare: async ({ root }) => ({
49+
client: await createClient({ root }),
50+
}),
51+
handle: async ({ client }) => {
52+
let document = await client.open({
53+
lang: 'css',
54+
text: css`
55+
@import 'tailwindcss';
56+
@source inline("{,dark:}{,{sm,md,lg,xl,2xl}:}{,{hover,focus,active}:}{flex,underline,bg-red-{50,{100..900.100},950}{,/{0..100}}}");
57+
`,
58+
})
59+
60+
let lenses = await document.codeLenses()
61+
62+
expect(lenses).toEqual([
63+
{
64+
range: {
65+
start: { line: 1, character: 15 },
66+
end: { line: 1, character: 129 },
67+
},
68+
command: {
69+
title: 'Generates 14,784 classes',
70+
command: '',
71+
},
72+
},
73+
{
74+
range: {
75+
start: { line: 1, character: 15 },
76+
end: { line: 1, character: 129 },
77+
},
78+
command: {
79+
title: 'At least 3MB of CSS',
80+
command: '',
81+
},
82+
},
83+
{
84+
range: {
85+
start: { line: 1, character: 15 },
86+
end: { line: 1, character: 129 },
87+
},
88+
command: {
89+
title: 'This may slow down your bundler/browser',
90+
command: '',
91+
},
92+
},
93+
])
94+
},
95+
})

packages/tailwindcss-language-service/src/codeLensProvider.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { CodeLens } from 'vscode-languageserver'
44
import braces from 'braces'
55
import { findAll, indexToPosition } from './util/find'
66
import { absoluteRange } from './util/absoluteRange'
7+
import { formatBytes } from './util/format-bytes'
8+
import { estimatedClassSize } from './util/estimated-class-size'
79

810
export async function getCodeLens(state: State, doc: TextDocument): Promise<CodeLens[]> {
911
if (!state.enabled) return []
@@ -38,13 +40,36 @@ async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise<Co
3840
end: indexToPosition(text, match.indices.groups.glob[1]),
3941
})
4042

43+
let size = 0
44+
for (let className of expanded) {
45+
size += estimatedClassSize(className)
46+
}
47+
4148
lenses.push({
4249
range: slice,
4350
command: {
4451
title: `Generates ${countFormatter.format(expanded.size)} classes`,
4552
command: '',
4653
},
4754
})
55+
56+
if (size >= 1_000_000) {
57+
lenses.push({
58+
range: slice,
59+
command: {
60+
title: `At least ${formatBytes(size)} of CSS`,
61+
command: '',
62+
},
63+
})
64+
65+
lenses.push({
66+
range: slice,
67+
command: {
68+
title: `This may slow down your bundler/browser`,
69+
command: '',
70+
},
71+
})
72+
}
4873
}
4974

5075
return lenses
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { segment } from './segment'
2+
3+
/**
4+
* Calculates the approximate size of a generated class
5+
*
6+
* This is meant to be a lower bound, as the actual size of a class can vary
7+
* depending on the actual CSS properties and values, configured theme, etc…
8+
*/
9+
export function estimatedClassSize(className: string) {
10+
let size = 0
11+
12+
// We estimate the size using the following structure which gives a reasonable
13+
// lower bound on the size of the generated CSS:
14+
//
15+
// .class-name {
16+
// &:variant-1 {
17+
// &:variant-2 {
18+
// …
19+
// }
20+
// }
21+
// }
22+
23+
// Class name
24+
size += 1 + className.length + 3
25+
size += 2
26+
27+
// Variants + nesting
28+
for (let [depth, variantName] of segment(className, ':').entries()) {
29+
size += (depth + 1) * 2 + 2 + variantName.length + 3
30+
size += (depth + 1) * 2 + 2
31+
}
32+
33+
// ~1.95x is a rough growth factor due to the actual properties being present
34+
return size * 1.95
35+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']
2+
3+
export function formatBytes(n: number) {
4+
let i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1000))
5+
return new Intl.NumberFormat('en', {
6+
notation: 'compact',
7+
style: 'unit',
8+
unit: UNITS[i],
9+
unitDisplay: 'narrow',
10+
}).format(n / 1000 ** i)
11+
}

0 commit comments

Comments
 (0)