tailwind-scramble 0.1.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,92 @@
1
+ # `tailwind-scramble`
2
+
3
+ [![npm version](https://img.shields.io/npm/v/tailwind-scramble.svg)](https://www.npmjs.com/package/tailwind-scramble)
4
+ [![license](https://img.shields.io/npm/l/tailwind-scramble.svg)](LICENSE)
5
+ [![typescript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/)
6
+
7
+ Scramble your Tailwind CSS class names in production builds. Replaces readable class names like `flex items-center` with random short identifiers like `akzmqt xbfplo`, making your markup harder to reverse-engineer.
8
+
9
+ Works with Next.js (Webpack and Turbopack), and supports class utility functions like `cn()`, `clsx()`, `cva()`, and others.
10
+
11
+ ## How it works
12
+
13
+ The package has two plugins that work together:
14
+
15
+ 1. **Webpack Plugin**: A bundler loader that parses your JSX/TSX, finds class name strings (in `className` attributes and utility function calls), and replaces each class with a unique random identifier. It writes the mapping to a JSON file.
16
+
17
+ 2. **PostCSS Plugin**: Reads the class mapping and rewrites the corresponding CSS selectors to match the scrambled names.
18
+
19
+ Both plugins share the mapping file, so the scrambled class names in your HTML and CSS always stay in sync.
20
+
21
+ ### Supported class utilities
22
+
23
+ The Webpack plugin detects and rewrites strings inside these functions:
24
+
25
+ - `cn()`
26
+ - `clsx()`
27
+ - `cx()`
28
+ - `cva()`
29
+ - `classnames()`
30
+ - `twMerge()`
31
+ - `twJoin()`
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install tailwind-scramble
37
+ ```
38
+
39
+ ## Setup (Next.js)
40
+
41
+ It is recommended to only use this plugin during production builds to make development easier.
42
+
43
+ ### Webpack
44
+
45
+ ```ts
46
+ // next.config.ts
47
+ import type { NextConfig } from "next";
48
+
49
+ const isProd = process.env.NODE_ENV === "production";
50
+
51
+ const nextConfig: NextConfig = {
52
+ webpack(config) {
53
+ if (isProd) {
54
+ config.module?.rules?.unshift({
55
+ test: /\.(tsx|ts|jsx|js|mjs)$/,
56
+ exclude: /node_modules/,
57
+ enforce: "pre",
58
+ use: [
59
+ {
60
+ loader: require.resolve("tailwind-scramble/webpack"),
61
+ options: {},
62
+ },
63
+ ],
64
+ });
65
+ }
66
+
67
+ return config;
68
+ },
69
+ };
70
+
71
+ export default nextConfig;
72
+ ```
73
+
74
+ ### PostCSS
75
+
76
+ ```js
77
+ // postcss.config.mjs
78
+ const isProd = process.env.NODE_ENV === "production";
79
+
80
+ const config = {
81
+ plugins: {
82
+ "@tailwindcss/postcss": {},
83
+ ...(isProd && { "tailwind-scramble/postcss": {} }),
84
+ },
85
+ };
86
+
87
+ export default config;
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,35 @@
1
+ const require_utils = require('./utils-BHmeg0xb.cjs');
2
+
3
+ //#region src/plugins/postcss-plugin.ts
4
+ function toTailwindSelector(className) {
5
+ return className.split("").map((ch) => {
6
+ if (/^[a-zA-Z0-9_-]$/.test(ch)) return ch;
7
+ return "\\" + ch;
8
+ }).join("");
9
+ }
10
+ const postcssPlugin = () => {
11
+ return {
12
+ postcssPlugin: "tailwind-scramble",
13
+ Once(root) {
14
+ console.log("[TailwindScramble] PostCSS Plugin Initialized");
15
+ const map = require_utils.readMap();
16
+ const keys = Object.keys(map);
17
+ if (keys.length === 0) return;
18
+ console.log("[TailwindScramble] Tailwind Keys:", keys.length);
19
+ root.walkRules((rule) => {
20
+ const original = rule.selector;
21
+ let rewritten = original;
22
+ for (const [orig, obf] of Object.entries(map)) {
23
+ const tw = "." + toTailwindSelector(orig);
24
+ const target = "." + obf;
25
+ if (rewritten === tw) rewritten = rewritten.split(tw).join(target);
26
+ }
27
+ if (rewritten !== original) rule.selector = rewritten;
28
+ });
29
+ }
30
+ };
31
+ };
32
+ postcssPlugin.postcss = true;
33
+
34
+ //#endregion
35
+ module.exports = postcssPlugin;
@@ -0,0 +1,5 @@
1
+ import { PluginCreator } from "postcss";
2
+
3
+ //#region src/plugins/postcss-plugin.d.ts
4
+ declare const postcssPlugin: PluginCreator<Record<string, never>>;
5
+ export = postcssPlugin;
@@ -0,0 +1,6 @@
1
+ import { PluginCreator } from "postcss";
2
+
3
+ //#region src/plugins/postcss-plugin.d.ts
4
+ declare const postcssPlugin: PluginCreator<Record<string, never>>;
5
+ //#endregion
6
+ export { postcssPlugin as default };
@@ -0,0 +1,35 @@
1
+ import { n as readMap } from "./utils-B3ZyiKiX.mjs";
2
+
3
+ //#region src/plugins/postcss-plugin.ts
4
+ function toTailwindSelector(className) {
5
+ return className.split("").map((ch) => {
6
+ if (/^[a-zA-Z0-9_-]$/.test(ch)) return ch;
7
+ return "\\" + ch;
8
+ }).join("");
9
+ }
10
+ const postcssPlugin = () => {
11
+ return {
12
+ postcssPlugin: "tailwind-scramble",
13
+ Once(root) {
14
+ console.log("[TailwindScramble] PostCSS Plugin Initialized");
15
+ const map = readMap();
16
+ const keys = Object.keys(map);
17
+ if (keys.length === 0) return;
18
+ console.log("[TailwindScramble] Tailwind Keys:", keys.length);
19
+ root.walkRules((rule) => {
20
+ const original = rule.selector;
21
+ let rewritten = original;
22
+ for (const [orig, obf] of Object.entries(map)) {
23
+ const tw = "." + toTailwindSelector(orig);
24
+ const target = "." + obf;
25
+ if (rewritten === tw) rewritten = rewritten.split(tw).join(target);
26
+ }
27
+ if (rewritten !== original) rule.selector = rewritten;
28
+ });
29
+ }
30
+ };
31
+ };
32
+ postcssPlugin.postcss = true;
33
+
34
+ //#endregion
35
+ export { postcssPlugin as default };
@@ -0,0 +1,19 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { customAlphabet } from "nanoid";
4
+
5
+ //#region src/common/utils.ts
6
+ const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz", 6);
7
+ const MAP_FILE = path.join(process.cwd(), ".next/tailwind-scramble-map.json");
8
+ function writeMap(classMap) {
9
+ const obj = Object.fromEntries(classMap);
10
+ fs.mkdirSync(path.dirname(MAP_FILE), { recursive: true });
11
+ fs.writeFileSync(MAP_FILE, JSON.stringify(obj, null, 2));
12
+ }
13
+ function readMap() {
14
+ if (!fs.existsSync(MAP_FILE)) return {};
15
+ return JSON.parse(fs.readFileSync(MAP_FILE, "utf8"));
16
+ }
17
+
18
+ //#endregion
19
+ export { readMap as n, writeMap as r, nanoid as t };
@@ -0,0 +1,71 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let fs = require("fs");
29
+ fs = __toESM(fs);
30
+ let path = require("path");
31
+ path = __toESM(path);
32
+ let nanoid = require("nanoid");
33
+
34
+ //#region src/common/utils.ts
35
+ const nanoid$1 = (0, nanoid.customAlphabet)("abcdefghijklmnopqrstuvwxyz", 6);
36
+ const MAP_FILE = path.default.join(process.cwd(), ".next/tailwind-scramble-map.json");
37
+ function writeMap(classMap) {
38
+ const obj = Object.fromEntries(classMap);
39
+ fs.default.mkdirSync(path.default.dirname(MAP_FILE), { recursive: true });
40
+ fs.default.writeFileSync(MAP_FILE, JSON.stringify(obj, null, 2));
41
+ }
42
+ function readMap() {
43
+ if (!fs.default.existsSync(MAP_FILE)) return {};
44
+ return JSON.parse(fs.default.readFileSync(MAP_FILE, "utf8"));
45
+ }
46
+
47
+ //#endregion
48
+ Object.defineProperty(exports, '__toESM', {
49
+ enumerable: true,
50
+ get: function () {
51
+ return __toESM;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, 'nanoid', {
55
+ enumerable: true,
56
+ get: function () {
57
+ return nanoid$1;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, 'readMap', {
61
+ enumerable: true,
62
+ get: function () {
63
+ return readMap;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, 'writeMap', {
67
+ enumerable: true,
68
+ get: function () {
69
+ return writeMap;
70
+ }
71
+ });
@@ -0,0 +1,92 @@
1
+ const require_utils = require('./utils-BHmeg0xb.cjs');
2
+ let _swc_core = require("@swc/core");
3
+
4
+ //#region src/plugins/webpack-plugin.ts
5
+ const classMap = /* @__PURE__ */ new Map();
6
+ const CLASS_FUNCTIONS = new Set([
7
+ "cn",
8
+ "cva",
9
+ "clsx",
10
+ "cx",
11
+ "twMerge",
12
+ "twJoin",
13
+ "classnames"
14
+ ]);
15
+ function get(cls) {
16
+ if (!classMap.has(cls)) classMap.set(cls, require_utils.nanoid());
17
+ return classMap.get(cls);
18
+ }
19
+ function rewriteString(value) {
20
+ return value.split(/\s+/).map(get).join(" ");
21
+ }
22
+ function rewriteStringLiteral(node) {
23
+ const newValue = rewriteString(node.value);
24
+ node.value = newValue;
25
+ node.raw = JSON.stringify(newValue);
26
+ }
27
+ function isClassFunction(node) {
28
+ if (node.type !== "CallExpression") return false;
29
+ const callee = node.callee;
30
+ if (callee?.type === "Identifier") return CLASS_FUNCTIONS.has(callee.value);
31
+ return false;
32
+ }
33
+ function rewriteStringsDeep(node) {
34
+ if (!node || typeof node !== "object") return;
35
+ if (node.type === "StringLiteral") {
36
+ rewriteStringLiteral(node);
37
+ return;
38
+ }
39
+ if (node.type === "TemplateLiteral") {
40
+ for (const quasi of node.quasis ?? []) if (quasi.raw) {
41
+ const rewritten = rewriteString(quasi.raw);
42
+ quasi.raw = rewritten;
43
+ quasi.cooked = rewritten;
44
+ }
45
+ for (const expr of node.expressions ?? []) rewriteStringsDeep(expr);
46
+ return;
47
+ }
48
+ if (node.type === "KeyValueProperty") {
49
+ rewriteStringsDeep(node.value);
50
+ return;
51
+ }
52
+ if (Array.isArray(node)) {
53
+ node.forEach(rewriteStringsDeep);
54
+ return;
55
+ }
56
+ for (const key in node) {
57
+ const child = node[key];
58
+ if (child && typeof child === "object") rewriteStringsDeep(child);
59
+ }
60
+ }
61
+ function bundlerPlugin(source) {
62
+ console.log("[TailwindScramble] Webpack Plugin Initialized");
63
+ const ast = (0, _swc_core.parseSync)(source, {
64
+ syntax: "typescript",
65
+ tsx: true,
66
+ decorators: false
67
+ });
68
+ function walk(node) {
69
+ if (!node || typeof node !== "object") return;
70
+ if (node.type === "JSXAttribute" && node.name?.type === "Identifier" && node.name.value === "className") {
71
+ const v = node.value;
72
+ if (v?.type === "StringLiteral") rewriteStringLiteral(v);
73
+ if (v?.type === "JSXExpressionContainer") walk(v.expression);
74
+ return;
75
+ }
76
+ if (isClassFunction(node)) {
77
+ rewriteStringsDeep(node.arguments);
78
+ return;
79
+ }
80
+ for (const key in node) {
81
+ const child = node[key];
82
+ if (Array.isArray(child)) child.forEach(walk);
83
+ else if (child && typeof child === "object") walk(child);
84
+ }
85
+ }
86
+ walk(ast);
87
+ require_utils.writeMap(classMap);
88
+ return (0, _swc_core.printSync)(ast).code;
89
+ }
90
+
91
+ //#endregion
92
+ module.exports = bundlerPlugin;
@@ -0,0 +1,3 @@
1
+ //#region src/plugins/webpack-plugin.d.ts
2
+ declare function bundlerPlugin(source: string): string;
3
+ export = bundlerPlugin;
@@ -0,0 +1,4 @@
1
+ //#region src/plugins/webpack-plugin.d.ts
2
+ declare function bundlerPlugin(source: string): string;
3
+ //#endregion
4
+ export { bundlerPlugin as default };
@@ -0,0 +1,92 @@
1
+ import { r as writeMap, t as nanoid } from "./utils-B3ZyiKiX.mjs";
2
+ import { parseSync, printSync } from "@swc/core";
3
+
4
+ //#region src/plugins/webpack-plugin.ts
5
+ const classMap = /* @__PURE__ */ new Map();
6
+ const CLASS_FUNCTIONS = new Set([
7
+ "cn",
8
+ "cva",
9
+ "clsx",
10
+ "cx",
11
+ "twMerge",
12
+ "twJoin",
13
+ "classnames"
14
+ ]);
15
+ function get(cls) {
16
+ if (!classMap.has(cls)) classMap.set(cls, nanoid());
17
+ return classMap.get(cls);
18
+ }
19
+ function rewriteString(value) {
20
+ return value.split(/\s+/).map(get).join(" ");
21
+ }
22
+ function rewriteStringLiteral(node) {
23
+ const newValue = rewriteString(node.value);
24
+ node.value = newValue;
25
+ node.raw = JSON.stringify(newValue);
26
+ }
27
+ function isClassFunction(node) {
28
+ if (node.type !== "CallExpression") return false;
29
+ const callee = node.callee;
30
+ if (callee?.type === "Identifier") return CLASS_FUNCTIONS.has(callee.value);
31
+ return false;
32
+ }
33
+ function rewriteStringsDeep(node) {
34
+ if (!node || typeof node !== "object") return;
35
+ if (node.type === "StringLiteral") {
36
+ rewriteStringLiteral(node);
37
+ return;
38
+ }
39
+ if (node.type === "TemplateLiteral") {
40
+ for (const quasi of node.quasis ?? []) if (quasi.raw) {
41
+ const rewritten = rewriteString(quasi.raw);
42
+ quasi.raw = rewritten;
43
+ quasi.cooked = rewritten;
44
+ }
45
+ for (const expr of node.expressions ?? []) rewriteStringsDeep(expr);
46
+ return;
47
+ }
48
+ if (node.type === "KeyValueProperty") {
49
+ rewriteStringsDeep(node.value);
50
+ return;
51
+ }
52
+ if (Array.isArray(node)) {
53
+ node.forEach(rewriteStringsDeep);
54
+ return;
55
+ }
56
+ for (const key in node) {
57
+ const child = node[key];
58
+ if (child && typeof child === "object") rewriteStringsDeep(child);
59
+ }
60
+ }
61
+ function bundlerPlugin(source) {
62
+ console.log("[TailwindScramble] Webpack Plugin Initialized");
63
+ const ast = parseSync(source, {
64
+ syntax: "typescript",
65
+ tsx: true,
66
+ decorators: false
67
+ });
68
+ function walk(node) {
69
+ if (!node || typeof node !== "object") return;
70
+ if (node.type === "JSXAttribute" && node.name?.type === "Identifier" && node.name.value === "className") {
71
+ const v = node.value;
72
+ if (v?.type === "StringLiteral") rewriteStringLiteral(v);
73
+ if (v?.type === "JSXExpressionContainer") walk(v.expression);
74
+ return;
75
+ }
76
+ if (isClassFunction(node)) {
77
+ rewriteStringsDeep(node.arguments);
78
+ return;
79
+ }
80
+ for (const key in node) {
81
+ const child = node[key];
82
+ if (Array.isArray(child)) child.forEach(walk);
83
+ else if (child && typeof child === "object") walk(child);
84
+ }
85
+ }
86
+ walk(ast);
87
+ writeMap(classMap);
88
+ return printSync(ast).code;
89
+ }
90
+
91
+ //#endregion
92
+ export { bundlerPlugin as default };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "tailwind-scramble",
3
+ "description": "Bundler plugin to scramble Tailwind Classes",
4
+ "version": "0.1.0",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist/**",
9
+ "README.md"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/akshatmittal/tailwind-scramble.git"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "import": "./dist/index.mjs",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "./webpack": {
21
+ "import": "./dist/webpack-plugin.mjs",
22
+ "require": "./dist/webpack-plugin.cjs"
23
+ },
24
+ "./postcss": {
25
+ "import": "./dist/postcss-plugin.mjs",
26
+ "require": "./dist/postcss-plugin.cjs"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "@changesets/changelog-github": "^0.5.2",
31
+ "@changesets/cli": "^2.29.8",
32
+ "@changesets/config": "^3.1.2",
33
+ "@swc/core": "^1.15.17",
34
+ "nanoid": "^5.1.6"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.10.15",
38
+ "eslint": "^9.39.3",
39
+ "tsdown": "^0.21.0-beta.2",
40
+ "typescript": "^5.9.2"
41
+ },
42
+ "peerDependencies": {
43
+ "postcss": "^8.5.6"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "keywords": [
49
+ "tailwindcss",
50
+ "scramble",
51
+ "tailwind",
52
+ "shadcn",
53
+ "bundler"
54
+ ],
55
+ "scripts": {
56
+ "lint": "eslint .",
57
+ "build": "tsdown",
58
+ "changeset": "changeset",
59
+ "version": "changeset version",
60
+ "release": "changeset publish"
61
+ }
62
+ }