cls-extended 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @cls-extended/core
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+
6
+ Zero-runtime Tailwind CSS responsive class transformer. Write cleaner responsive syntax that compiles to standard Tailwind classes at build time.
7
+
8
+ ## Features
9
+
10
+ - ⚡ **Zero Runtime Overhead** - All transformations happen at build time
11
+ - 🎨 **Better DX** - Cleaner, more maintainable responsive class syntax
12
+ - 🔧 **Universal** - Works with Vite, Webpack, Rollup, esbuild, Rspack, Rolldown, and Farm
13
+ - 📦 **Tiny Bundle** - ~8KB total, 0KB runtime impact
14
+ - 🔒 **Type Safe** - Full TypeScript support
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm i -D @cls-extended/core
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Setup Plugin
25
+
26
+ <details>
27
+ <summary>Vite</summary><br>
28
+
29
+ ```ts
30
+ // vite.config.ts
31
+ import clsExtended from '@cls-extended/core/adapters/vite'
32
+
33
+ export default defineConfig({
34
+ plugins: [clsExtended()],
35
+ })
36
+ ```
37
+
38
+ </details>
39
+
40
+ <details>
41
+ <summary>Webpack</summary><br>
42
+
43
+ ```js
44
+ // webpack.config.js
45
+ import clsExtended from '@cls-extended/core/adapters/webpack'
46
+
47
+ export default {
48
+ plugins: [clsExtended()],
49
+ }
50
+ ```
51
+
52
+ </details>
53
+
54
+ <details>
55
+ <summary>Rollup</summary><br>
56
+
57
+ ```ts
58
+ // rollup.config.js
59
+ import clsExtended from '@cls-extended/core/adapters/rollup'
60
+
61
+ export default {
62
+ plugins: [clsExtended()],
63
+ }
64
+ ```
65
+
66
+ </details>
67
+
68
+ <details>
69
+ <summary>esbuild</summary><br>
70
+
71
+ ```ts
72
+ import { build } from 'esbuild'
73
+ import clsExtended from '@cls-extended/core/adapters/esbuild'
74
+
75
+ build({
76
+ plugins: [clsExtended()],
77
+ })
78
+ ```
79
+
80
+ </details>
81
+
82
+ <details>
83
+ <summary>Rspack</summary><br>
84
+
85
+ ```ts
86
+ // rspack.config.js
87
+ import clsExtended from '@cls-extended/core/adapters/rspack'
88
+
89
+ export default {
90
+ plugins: [clsExtended()],
91
+ }
92
+ ```
93
+
94
+ </details>
95
+
96
+ ### In Your Components
97
+
98
+ ```tsx
99
+ import { cls } from '@cls-extended/core/api'
100
+
101
+ function Component() {
102
+ return (
103
+ <div className={cls('text-xl font-bold', {
104
+ md: 'text-2xl',
105
+ lg: 'text-3xl'
106
+ })}>
107
+ Responsive Text
108
+ </div>
109
+ )
110
+ }
111
+ ```
112
+
113
+ **Compiles to:**
114
+
115
+ ```tsx
116
+ <div className="text-xl font-bold md:text-2xl lg:text-3xl">
117
+ Responsive Text
118
+ </div>
119
+ ```
120
+
121
+ ## API
122
+
123
+ ### `cls(baseClasses, responsiveClasses?)`
124
+
125
+ Transform responsive Tailwind classes at build time.
126
+
127
+ **Parameters:**
128
+ - `baseClasses` (string) - Base Tailwind classes
129
+ - `responsiveClasses` (object, optional) - Responsive breakpoint classes
130
+
131
+ **Supported Breakpoints:**
132
+ - `sm` - 640px
133
+ - `md` - 768px
134
+ - `lg` - 1024px
135
+ - `xl` - 1280px
136
+ - `2xl` - 1536px
137
+
138
+ **Example:**
139
+
140
+ ```ts
141
+ cls('p-4 bg-white', {
142
+ md: 'p-6',
143
+ lg: 'p-8 shadow-lg'
144
+ })
145
+ // → "p-4 bg-white md:p-6 lg:p-8 lg:shadow-lg"
146
+ ```
147
+
148
+ ## How It Works
149
+
150
+ 1. **Build Time**: Plugin scans your code for `cls()` calls
151
+ 2. **AST Transform**: Parses and transforms the syntax using Babel
152
+ 3. **Output**: Generates standard Tailwind classes with zero runtime code
153
+
154
+ ## License
155
+
156
+ [MIT](./LICENSE) License © 2025 [Yeasin](https://github.com/yeasin2002)
157
+
158
+ <!-- Badges -->
159
+
160
+ [npm-version-src]: https://img.shields.io/npm/v/@cls-extended/core.svg
161
+ [npm-version-href]: https://npmjs.com/package/@cls-extended/core
162
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@cls-extended/core
163
+ [npm-downloads-href]: https://www.npmcharts.com/compare/@cls-extended/core?interval=30
@@ -0,0 +1,7 @@
1
+ import { Options } from "../core/options.js";
2
+ import { UnpluginInstance } from "unplugin";
3
+
4
+ //#region src/adapters/vite.d.ts
5
+ declare const vitePlugin: UnpluginInstance<Options | undefined, false>["vite"];
6
+ //#endregion
7
+ export { vitePlugin as default };
@@ -0,0 +1,7 @@
1
+ import unplugin from "../index.js";
2
+
3
+ //#region src/adapters/vite.ts
4
+ const vitePlugin = unplugin.vite;
5
+
6
+ //#endregion
7
+ export { vitePlugin as default };
@@ -0,0 +1,6 @@
1
+ import unplugin from "../index.js";
2
+
3
+ //#region src/adapters/webpack.d.ts
4
+ declare const _default: typeof unplugin.webpack;
5
+ //#endregion
6
+ export { _default as default };
@@ -0,0 +1,7 @@
1
+ import unplugin from "../index.js";
2
+
3
+ //#region src/adapters/webpack.ts
4
+ var webpack_default = unplugin.webpack;
5
+
6
+ //#endregion
7
+ export { webpack_default as default };
package/dist/api.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/api.d.ts
2
+ type ResponsiveBreakpoint = "sm" | "md" | "lg" | "xl" | "2xl";
3
+ type ResponsiveClasses = Partial<Record<ResponsiveBreakpoint, string>>;
4
+ /**
5
+ * Transform responsive Tailwind classes at build time
6
+ *
7
+ * @example
8
+ * tw('text-xl font-bold', { md: 'text-2xl', lg: 'text-3xl' })
9
+ * // Compiles to: "text-xl font-bold md:text-2xl lg:text-3xl"
10
+ */
11
+ declare function cls(baseClasses: string, responsiveClasses?: ResponsiveClasses): string;
12
+ //#endregion
13
+ export { ResponsiveBreakpoint, ResponsiveClasses, cls };
package/dist/api.js ADDED
@@ -0,0 +1,20 @@
1
+ //#region src/api.ts
2
+ /**
3
+ * Transform responsive Tailwind classes at build time
4
+ *
5
+ * @example
6
+ * tw('text-xl font-bold', { md: 'text-2xl', lg: 'text-3xl' })
7
+ * // Compiles to: "text-xl font-bold md:text-2xl lg:text-3xl"
8
+ */
9
+ function cls(baseClasses, responsiveClasses) {
10
+ if (!responsiveClasses) return baseClasses;
11
+ const parts = [baseClasses];
12
+ for (const [breakpoint, classes] of Object.entries(responsiveClasses)) {
13
+ const prefixed = classes.split(/\s+/).filter(Boolean).map((cls) => `${breakpoint}:${cls}`).join(" ");
14
+ parts.push(prefixed);
15
+ }
16
+ return parts.join(" ");
17
+ }
18
+
19
+ //#endregion
20
+ export { cls };
@@ -0,0 +1,34 @@
1
+ import { FilterPattern } from "unplugin";
2
+
3
+ //#region src/core/options.d.ts
4
+ interface Options {
5
+ /**
6
+ * Files to include in processing
7
+ * @default [/\.[jt]sx?$/]
8
+ */
9
+ include?: FilterPattern;
10
+ /**
11
+ * Files to exclude from processing
12
+ * @default [/node_modules/]
13
+ */
14
+ exclude?: FilterPattern;
15
+ /**
16
+ * Enable source map generation
17
+ * @default true
18
+ */
19
+ sourcemap?: boolean;
20
+ /**
21
+ * Custom Tailwind breakpoints
22
+ * @default { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px' }
23
+ */
24
+ breakpoints?: Record<string, string>;
25
+ /**
26
+ * Enable additional variant support (hover, focus, dark, etc.)
27
+ * @default false
28
+ */
29
+ enableVariants?: boolean;
30
+ }
31
+ type OptionsResolved = Required<Options>;
32
+ declare function resolveOptions(options?: Options): OptionsResolved;
33
+ //#endregion
34
+ export { Options, OptionsResolved, resolveOptions };
@@ -0,0 +1,19 @@
1
+ //#region src/core/options.ts
2
+ function resolveOptions(options = {}) {
3
+ return {
4
+ include: options.include ?? [/\.[jt]sx?$/],
5
+ exclude: options.exclude ?? [/node_modules/],
6
+ sourcemap: options.sourcemap ?? true,
7
+ breakpoints: options.breakpoints ?? {
8
+ sm: "640px",
9
+ md: "768px",
10
+ lg: "1024px",
11
+ xl: "1280px",
12
+ "2xl": "1536px"
13
+ },
14
+ enableVariants: options.enableVariants ?? false
15
+ };
16
+ }
17
+
18
+ //#endregion
19
+ export { resolveOptions };
@@ -0,0 +1,10 @@
1
+ //#region src/core/parser.d.ts
2
+ interface ClsCallExpression {
3
+ baseClasses: string;
4
+ responsiveClasses: Record<string, string>;
5
+ start: number;
6
+ end: number;
7
+ }
8
+ declare function findClsCalls(code: string): ClsCallExpression[];
9
+ //#endregion
10
+ export { ClsCallExpression, findClsCalls };
@@ -0,0 +1,57 @@
1
+ import { parse } from "@babel/parser";
2
+
3
+ //#region src/core/parser.ts
4
+ function findClsCalls(code) {
5
+ let ast;
6
+ try {
7
+ ast = parse(code, {
8
+ sourceType: "module",
9
+ plugins: ["jsx", "typescript"]
10
+ });
11
+ } catch {
12
+ return [];
13
+ }
14
+ const clsCalls = [];
15
+ function traverse(node) {
16
+ if (!node || typeof node !== "object") return;
17
+ if (node.type === "CallExpression") {
18
+ const callee = node.callee;
19
+ if (callee.type === "Identifier" && callee.name === "tw" || callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "tw") {
20
+ const args = node.arguments;
21
+ if (args.length === 0) return;
22
+ const baseArg = args[0];
23
+ if (baseArg.type !== "StringLiteral") return;
24
+ const baseClasses = baseArg.value;
25
+ const responsiveClasses = {};
26
+ if (args.length > 1) {
27
+ const responsiveArg = args[1];
28
+ if (responsiveArg.type === "ObjectExpression") {
29
+ for (const prop of responsiveArg.properties) if (prop.type === "ObjectProperty" && prop.value.type === "StringLiteral") {
30
+ let key;
31
+ if (prop.key.type === "Identifier") key = prop.key.name;
32
+ else if (prop.key.type === "StringLiteral") key = prop.key.value;
33
+ if (key) responsiveClasses[key] = prop.value.value;
34
+ }
35
+ }
36
+ }
37
+ clsCalls.push({
38
+ baseClasses,
39
+ responsiveClasses,
40
+ start: node.start,
41
+ end: node.end
42
+ });
43
+ }
44
+ }
45
+ for (const key in node) {
46
+ if (key === "loc" || key === "range" || key === "tokens") continue;
47
+ const value = node[key];
48
+ if (Array.isArray(value)) for (const item of value) traverse(item);
49
+ else if (value && typeof value === "object") traverse(value);
50
+ }
51
+ }
52
+ traverse(ast);
53
+ return clsCalls;
54
+ }
55
+
56
+ //#endregion
57
+ export { findClsCalls };
@@ -0,0 +1,10 @@
1
+ import { OptionsResolved } from "./options.js";
2
+
3
+ //#region src/core/transform.d.ts
4
+ interface TransformResult {
5
+ code: string;
6
+ map: any;
7
+ }
8
+ declare function transformClsCalls(code: string, _id: string, options: OptionsResolved): TransformResult | null;
9
+ //#endregion
10
+ export { TransformResult, transformClsCalls };
@@ -0,0 +1,33 @@
1
+ import { findClsCalls } from "./parser.js";
2
+ import MagicString from "magic-string";
3
+
4
+ //#region src/core/transform.ts
5
+ function transformClsCalls(code, _id, options) {
6
+ const twCalls = findClsCalls(code);
7
+ if (twCalls.length === 0) return null;
8
+ const s = new MagicString(code);
9
+ for (const call of twCalls) {
10
+ const transformedString = generateClassString(call, options);
11
+ s.overwrite(call.start, call.end, `"${transformedString}"`);
12
+ }
13
+ return {
14
+ code: s.toString(),
15
+ map: options.sourcemap ? s.generateMap({ hires: true }) : null
16
+ };
17
+ }
18
+ function generateClassString(call, options) {
19
+ const { baseClasses, responsiveClasses } = call;
20
+ const parts = [baseClasses];
21
+ for (const [breakpoint, classes] of Object.entries(responsiveClasses)) {
22
+ if (!options.breakpoints[breakpoint] && !options.enableVariants) {
23
+ console.warn(`Unknown breakpoint: ${breakpoint}`);
24
+ continue;
25
+ }
26
+ const prefixedClasses = classes.split(/\s+/).filter(Boolean).map((cls) => `${breakpoint}:${cls}`).join(" ");
27
+ parts.push(prefixedClasses);
28
+ }
29
+ return parts.filter(Boolean).join(" ");
30
+ }
31
+
32
+ //#endregion
33
+ export { transformClsCalls };
@@ -0,0 +1,7 @@
1
+ import { Options } from "./core/options.js";
2
+ import { UnpluginInstance } from "unplugin";
3
+
4
+ //#region src/index.d.ts
5
+ declare const unplugin: UnpluginInstance<Options | undefined, false>;
6
+ //#endregion
7
+ export { type Options, unplugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import { resolveOptions } from "./core/options.js";
2
+ import { transformClsCalls } from "./core/transform.js";
3
+ import { createUnplugin } from "unplugin";
4
+
5
+ //#region src/index.ts
6
+ const unplugin = createUnplugin((rawOptions = {}) => {
7
+ const options = resolveOptions(rawOptions);
8
+ return {
9
+ name: "cls-extended",
10
+ transform: {
11
+ filter: { id: {
12
+ include: options.include,
13
+ exclude: options.exclude
14
+ } },
15
+ handler(code, id) {
16
+ if (!code.includes("tw(")) return null;
17
+ return transformClsCalls(code, id, options);
18
+ }
19
+ }
20
+ };
21
+ });
22
+
23
+ //#endregion
24
+ export { unplugin as default };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "cls-extended",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "exports": {
6
+ ".": "./dist/index.js",
7
+ "./adapters/vite": "./dist/adapters/vite.js",
8
+ "./adapters/webpack": "./dist/adapters/webpack.js",
9
+ "./api": "./dist/api.js",
10
+ "./core/options": "./dist/core/options.js",
11
+ "./core/parser": "./dist/core/parser.js",
12
+ "./core/transform": "./dist/core/transform.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "typesVersions": {
16
+ "*": {
17
+ "*": [
18
+ "./dist/*.d.ts",
19
+ "./*"
20
+ ]
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=20.19.0"
31
+ },
32
+ "scripts": {
33
+ "lint": "eslint --cache .",
34
+ "lint:fix": "pnpm run lint --fix",
35
+ "build": "tsdown",
36
+ "dev": "tsdown --watch",
37
+ "test": "vitest",
38
+ "typecheck": "tsc --noEmit",
39
+ "prepublishOnly": "pnpm run build"
40
+ },
41
+ "dependencies": {
42
+ "@babel/parser": "^7.29.0",
43
+ "magic-string": "^0.30.21",
44
+ "unplugin": "^3.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@babel/types": "^7.29.0",
48
+ "@eslint/js": "^9.39.2",
49
+ "@sxzz/eslint-config": "^7.6.0",
50
+ "@sxzz/prettier-config": "^2.3.1",
51
+ "@sxzz/test-utils": "^0.5.15",
52
+ "@types/node": "^25.2.0",
53
+ "bumpp": "^10.4.0",
54
+ "eslint": "^9.39.2",
55
+ "globals": "^16.5.0",
56
+ "jiti": "^2.6.1",
57
+ "prettier": "^3.8.1",
58
+ "tsdown": "^0.20.1",
59
+ "tsdown-preset-sxzz": "^0.3.1",
60
+ "typescript": "^5.9.3",
61
+ "typescript-eslint": "^8.54.0",
62
+ "vitest": "^4.0.18"
63
+ },
64
+ "keywords": [
65
+ "unplugin",
66
+ "vite",
67
+ "webpack",
68
+ "rspack",
69
+ "rollup",
70
+ "rolldown",
71
+ "esbuild",
72
+ "farm"
73
+ ]
74
+ }