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 +163 -0
- package/dist/adapters/vite.d.ts +7 -0
- package/dist/adapters/vite.js +7 -0
- package/dist/adapters/webpack.d.ts +6 -0
- package/dist/adapters/webpack.js +7 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.js +20 -0
- package/dist/core/options.d.ts +34 -0
- package/dist/core/options.js +19 -0
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.js +57 -0
- package/dist/core/transform.d.ts +10 -0
- package/dist/core/transform.js +33 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +24 -0
- package/package.json +74 -0
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
|
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 };
|
package/dist/index.d.ts
ADDED
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
|
+
}
|