Skip to content

Commit 2c345bd

Browse files
committed
refactor(emcn): use tailwind-merge v3 instead of the cn package
The cn swap did not earn its keep. Benchmarked on this repo's own 4,673 class strings, with both libraries' caches warm — which is what a re-rendering UI is almost always in — cn is 4.2x faster per call (4.7ns vs 19.5ns). On a heavy 5,000-call render that is a 0.074ms saving: below anything perceivable, and far below React's own reconciliation. The 13.7x cold-path win only applies to strings neither cache has seen. Against that it cost 2.1 KB gzip over tailwind-merge v3, paid on every cold load on the critical path to first paint. cn trades gzip-compressibility for parse speed: its tables are pre-encoded data that gzip cannot squeeze, where tailwind-merge's config is repetitive JS that it can. Reverting also drops a dependency published eight days ago onto a recycled npm name, and lets the 7-day supply-chain gate go back on for the whole tree. Keeps everything the migration was actually worth: Tailwind v4, and a single class-name path now that apps/sim's direct clsx imports are gone. The 23 golden cases pass unchanged against tailwind-merge, so the merge semantics are identical; the built stylesheet is byte-for-byte the same.
1 parent 18bcb2b commit 2c345bd

12 files changed

Lines changed: 30 additions & 253 deletions

File tree

apps/docs/app/global.css

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
@source "../../../packages/emcn/src";
66
@source "../../../packages/workflow-renderer/src";
77

8-
/* emcn's compiled `cn` lookup tables encode Tailwind's own class-group literals
9-
as string data. The scanner cannot tell those from real class usage, so
10-
leaving the file in scope emits utilities nothing renders (`float-left`,
11-
`clear-end`, every `mix-blend-*`, …). apps/sim/app/_styles/globals.css
12-
carries the same exclusion — both apps scan packages/emcn/src. */
13-
@source not "../../../packages/emcn/src/lib/cn-tables.ts";
14-
158
/* Every @sim/emcn component expresses hover through `hover-hover:` so touch
169
devices never latch a sticky hover state. The app registers it as a plugin
1710
variant in apps/sim/app/_styles/globals.css; docs is CSS-first Tailwind v4 with no

apps/sim/app/_styles/globals.css

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@
2323
@source not '../../**/*.test.{js,ts,jsx,tsx,mdx}';
2424
@source not '../../**/*.spec.{js,ts,jsx,tsx,mdx}';
2525

26-
/**
27-
* emcn's compiled `cn` lookup tables encode Tailwind's own class-group literals
28-
* as string data. The scanner cannot tell those from real class usage, so
29-
* leaving the file in scope emits utilities nothing renders (`float-left`,
30-
* `clear-end`, …).
31-
*/
32-
@source not '../../../../packages/emcn/src/lib/cn-tables.ts';
33-
3426
/**
3527
* Nesting-aware dark variant: `dark:` utilities apply inside a `.dark` subtree
3628
* UNLESS they sit inside a nested `.light` island. The landing page pins itself

biome.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"!**/public/worker-*.js",
2828
"!**/public/fallback-*.js",
2929
"!**/apps/sim/tools/generated",
30-
"!**/packages/emcn/src/lib/cn-tables.ts",
3130
"!**/apps/docs/.source",
3231
"!**/apps/desktop/release",
3332
"!**/venv",

bun.lock

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunfig.toml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@ minimumReleaseAge = 604800
77
# dev builds, so every version is structurally younger than any age gate.
88
minimumReleaseAgeExcludes = [
99
"@typescript/native-preview",
10-
# TEMPORARY — remove on or after 2026-09-09, when cn@0.2.4 passes the gate on
11-
# its own. Verified required, not optional: `bun install --frozen-lockfile`
12-
# still resolves this dependency and fails without the entry, so CI needs it
13-
# too. The npm name was recycled in 2026 (its only releases older than the
14-
# gate are an unrelated 2013 package), so no version of the package we want
15-
# can currently satisfy the age check.
16-
#
17-
# While this is here the gate is OFF for `cn` entirely, including any future
18-
# bump and any transitive copy. `cn` runs at build time and ships in the
19-
# client bundle, so do not raise the version until the entry is gone.
20-
"cn",
2110
]
2211

2312
[run]

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"check:canvas-sentences": "bun run apps/sim/scripts/check-canvas-sentences.ts --require-coverage",
5959
"check:bare-icons": "bun run scripts/check-bare-icons.ts",
6060
"check:byok-providers": "bun run scripts/check-byok-providers.ts",
61-
"check:cn-tables": "bun run scripts/check-cn-tables.ts",
6261
"check:icon-paths": "bun run scripts/check-icon-paths.ts",
6362
"check:icon-path-precision": "bun run scripts/check-icon-path-precision.ts",
6463
"check:migrations": "bun run scripts/check-migrations-safety.ts",

packages/emcn/cn.config.mjs

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/emcn/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
}
2828
},
2929
"scripts": {
30-
"cn:build": "cn build --full --config cn.config.mjs -o src/lib/cn-tables.ts",
3130
"type-check": "tsc --noEmit",
3231
"test": "vitest run",
3332
"lint": "biome check --write --unsafe .",
@@ -37,7 +36,8 @@
3736
},
3837
"dependencies": {
3938
"@sim/utils": "workspace:*",
40-
"cn": "0.2.4"
39+
"clsx": "^2.1.1",
40+
"tailwind-merge": "3.6.0"
4141
},
4242
"peerDependencies": {
4343
"@radix-ui/react-avatar": "^1.1.10",

0 commit comments

Comments
 (0)