From ce8d7831f2f550db92e939308b2f4f733d262af8 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Tue, 30 Jan 2024 21:58:43 +0000
Subject: [PATCH 01/12] Fixed Bugs & Support Tailwind's Universal Selector #6
[-] Removed `customTailwindDarkModeSelector` Option
[#] Updated Unit Tests
---
src/config.ts | 1 -
src/index.ts | 2 +-
src/type.ts | 2 -
src/utils.test.ts | 369 ++++++++++++++++++++++++++++++----------------
src/utils.ts | 141 +++++++++++-------
5 files changed, 328 insertions(+), 187 deletions(-)
diff --git a/src/config.ts b/src/config.ts
index 8940fa8..cf4f8dd 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -21,7 +21,6 @@ const defaultOptions: Options = {
enableMarkers: false, // Enable or disable the obfuscate marker classes.
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
- customTailwindDarkModeSelector: null, // [TailwindCSS ONLY] The custom new dark mode selector, e.g. "dark-mode".
logLevel: "info", // Log level
};
diff --git a/src/index.ts b/src/index.ts
index 1ec42d8..731aeb7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -40,7 +40,7 @@ function obfuscate(options: Options) {
classSuffix: options.classSuffix,
classIgnore: options.classIgnore,
- customTailwindDarkModeSelector: options.customTailwindDarkModeSelector,
+ enableObfuscateMarkers: options.enableMarkers,
});
log("success", "Obfuscation", "Class conversion JSON created/updated");
diff --git a/src/type.ts b/src/type.ts
index 4041304..29c2593 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -23,7 +23,6 @@ type Options = {
enableMarkers: boolean;
markers: string[];
removeMarkersAfterObfuscated: boolean;
- customTailwindDarkModeSelector: string | null;
logLevel: LogLevel;
}
@@ -48,7 +47,6 @@ type OptionalOptions = {
enableMarkers?: boolean;
markers?: string[];
removeMarkersAfterObfuscated?: boolean;
- customTailwindDarkModeSelector?: string | null;
logLevel?: LogLevel;
}
diff --git a/src/utils.test.ts b/src/utils.test.ts
index aa00ecc..e301f21 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -372,99 +372,159 @@ describe("getFilenameFromPath", () => {
describe("extractClassFromSelector", () => {
test("should extract single class from simple selector", () => {
+ const sample = ".example";
+
// Act
- const result = extractClassFromSelector(".example");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["example"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["example"]
+ });
});
test("should extract multiple classes from complex selector", () => {
+ const sample = ":is(.some-class .some-class\\:bg-dark::-moz-placeholder)[data-active=\'true\']";
+
// Act
- const result = extractClassFromSelector(":is(.some-class .some-class\\:bg-dark::-moz-placeholder)[data-active=\'true\']");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["some-class", "some-class\\:bg-dark"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["some-class", "some-class\\:bg-dark"]
+ });
});
test("should handle selector with no classes", () => {
+ const sample = "div";
+
// Act
- const result = extractClassFromSelector("div");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual([]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: []
+ });
});
test("should handle selector with action pseudo-classes and not extract them", () => {
+ const sample = ".btn:hover .btn-active::after";
+
// Act
- const result = extractClassFromSelector(".btn:hover .btn-active::after");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["btn", "btn-active"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["btn", "btn-active"]
+ });
});
-
+
test("should handle selector with vendor pseudo-classes and not extract them", () => {
+ const sample = ".btn-moz:-moz-focusring .btn-ms::-ms-placeholder .btn-webkit::-webkit-placeholder .btn-o::-o-placeholder";
+
// Act
- const result = extractClassFromSelector(".btn-moz:-moz-focusring .btn-ms::-ms-placeholder .btn-webkit::-webkit-placeholder .btn-o::-o-placeholder");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["btn-moz", "btn-ms", "btn-webkit", "btn-o"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["btn-moz", "btn-ms", "btn-webkit", "btn-o"]
+ });
});
test("should handle selector with escaped characters", () => {
+ const sample = ".escaped\\:class:action";
+
// Act
- const result = extractClassFromSelector(".escaped\\:class:action");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["escaped\\:class", "action"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["escaped\\:class", "action"]
+ });
});
test("should handle selector with multiple classes separated by spaces", () => {
+ const sample = ".class1 .class2 .class3";
+
// Act
- const result = extractClassFromSelector(".class1 .class2 .class3");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1", "class2", "class3"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2", "class3"]
+ });
});
test("should handle selector with multiple classes separated by commas", () => {
+ const sample = ".class1, .class2, .class3";
+
// Act
- const result = extractClassFromSelector(".class1, .class2, .class3");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1", "class2", "class3"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2", "class3"]
+ });
});
test("should handle selector with a combination of classes and ids", () => {
+ const sample = ".class1 #id .class2";
+
// Act
- const result = extractClassFromSelector(".class1 #id .class2");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1", "class2"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2"]
+ });
});
- test("should handle selector with attribute selectors", () => {
+ test("should handle [attribute] selector", () => {
+ const sample = ".class1[data-attr=\"value\"] .class2[data-attr='value']";
+
// Act
- const result = extractClassFromSelector(".class1[data-attr='value'] .class2");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1", "class2"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1[data-attr=\"value\"]", "class2[data-attr='value']"]
+ });
});
- test("should handle [attribute] selector", () => {
+ test("should handle action pseudo-class selector correctly", () => {
+ const sample = ".class1\\:hover\\:class2:after .class3\\:hover\\:class4:after:hover :is(.class5 .class6\\:hover\\:class7:hover:after) :is(.hover\\:class8\\:class9):after";
+
// Act
- const result = extractClassFromSelector(".class1[data-attr=\"value\"]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1[data-attr=\"value\"]"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1\\:hover\\:class2", "class3\\:hover\\:class4", "class5", "class6\\:hover\\:class7", "hover\\:class8\\:class9"]
+ });
});
-
+
test("should ignore [attribute] selector that not in the same scope as class", () => {
+ const sample = ":is(.class1 .class2\\:class3\\:\\!class4)[aria-selected=\"true\"]";
+
// Act
- const result = extractClassFromSelector(":is(.class1 .class2\\:class3\\:\\!class4)[aria-selected=\"true\"]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1", "class2\\:class3\\:\\!class4"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2\\:class3\\:\\!class4"]
+ });
});
test("should return null for invalid input types", () => {
@@ -481,67 +541,120 @@ describe("extractClassFromSelector", () => {
//? Tailwind CSS
//? *********************
test("should handle Tailwind CSS important selector '!'", () => {
+ const sample = ".\\!my-0 .some-class\\:\\!bg-white";
+
// Act
- const result = extractClassFromSelector(".\\!my-0 .some-class\\:\\!bg-white");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["\\!my-0", "some-class\\:\\!bg-white"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["\\!my-0", "some-class\\:\\!bg-white"]
+ })
});
test("should handle Tailwind CSS selector with start with '-'", () => {
+ const sample = ".-class-1";
+
// Act
- const result = extractClassFromSelector(".-class-1");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["-class-1"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["-class-1"]
+ })
});
-
+
test("should handle Tailwind CSS selector with '.' at the number", () => {
+ const sample = ".class-0\\.5 .class-1\\.125";
+
// Act
- const result = extractClassFromSelector(".class-0\\.5 .class-1\\.125");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class-0\\.5", "class-1\\.125"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class-0\\.5", "class-1\\.125"]
+ })
});
-
+
test("should handle Tailwind CSS selector with '/' at the number", () => {
+ const sample = ".class-1\\/2";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class-1\\/2"]
+ })
+ });
+
+ test("should handle Tailwind CSS universal selector", () => {
+ const sample = ".\\*\\:class1 .class2\\*\\:class3";
+
// Act
- const result = extractClassFromSelector(".class-1\\/2");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class-1\\/2"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["\\*\\:class1", "class2", "class3"]
+ })
});
test("should handle Tailwind CSS [custom parameter] selector", () => {
+ const sample = ".class1[100] .class2-[200]";
+
// Act
- const result = extractClassFromSelector(".class1[100] .class2-[200]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1[100]", "class2-[200]"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1[100]", "class2-[200]"]
+ })
});
-
+
test("should handle Tailwind CSS [custom parameter] selector with escaped characters", () => {
+ const sample = ".class1\\[1em\\] .class2-\\[2em\\] .class3\\[3\\%\\] .class4-\\[4\\%\\]";
+
// Act
- const result = extractClassFromSelector(".class1\\[1em\\] .class2-\\[2em\\] .class3\\[3\\%\\] .class4-\\[4\\%\\]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1\\[1em\\]", "class2-\\[2em\\]", "class3\\[3\\%\\]", "class4-\\[4\\%\\]"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1\\[1em\\]", "class2-\\[2em\\]", "class3\\[3\\%\\]", "class4-\\[4\\%\\]"]
+ })
});
-
+
test("should handle complex Tailwind CSS [custom parameter] selector", () => {
+ const sample = ".w-\\[calc\\(10\\%\\+5px\\)\\]";
+
// Act
- const result = extractClassFromSelector(".w-\\[calc\\(10\\%\\+5px\\)\\]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["w-\\[calc\\(10\\%\\+5px\\)\\]"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["w-\\[calc\\(10\\%\\+5px\\)\\]"]
+ })
});
test("should ignore Tailwind CSS [custom parameter] selector that not in the same scope as class", () => {
+ const sample = ":is(.class1)[100]";
+
// Act
- const result = extractClassFromSelector(":is(.class1)[100]");
+ const result = extractClassFromSelector(sample);
// Assert
- expect(result).toEqual(["class1"]);
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1"]
+ })
});
});
@@ -551,118 +664,118 @@ describe("extractClassFromSelector", () => {
describe("searchForwardComponent", () => {
- test("should return component name when jsx format is correct", () => {
- // Arrange
- const content = `const element = o.jsx(ComponentName, {data: dataValue, index: "date"});`;
+ test("should return component name when jsx format is correct", () => {
+ // Arrange
+ const content = `const element = o.jsx(ComponentName, {data: dataValue, index: "date"});`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["ComponentName"]);
- });
+ // Assert
+ expect(result).toEqual(["ComponentName"]);
+ });
- test("should return multiple component names for multiple matches", () => {
- // Arrange
- const content = `o.jsx(FirstComponent, props); o.jsx(SecondComponent, otherProps);`;
+ test("should return multiple component names for multiple matches", () => {
+ // Arrange
+ const content = `o.jsx(FirstComponent, props); o.jsx(SecondComponent, otherProps);`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["FirstComponent", "SecondComponent"]);
- });
+ // Assert
+ expect(result).toEqual(["FirstComponent", "SecondComponent"]);
+ });
- test("should return an empty array when no component name is found", () => {
- // Arrange
- const content = `o.jsx("h1", {data: dataValue, index: "date"});`;
+ test("should return an empty array when no component name is found", () => {
+ // Arrange
+ const content = `o.jsx("h1", {data: dataValue, index: "date"});`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual([]);
- });
+ // Assert
+ expect(result).toEqual([]);
+ });
- test("should return an empty array when content is empty", () => {
- // Arrange
- const content = "";
+ test("should return an empty array when content is empty", () => {
+ // Arrange
+ const content = "";
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual([]);
- });
+ // Assert
+ expect(result).toEqual([]);
+ });
- test("should return an empty array when jsx is not used", () => {
- // Arrange
- const content = `const element = React.createElement("div", null, "Hello World");`;
+ test("should return an empty array when jsx is not used", () => {
+ // Arrange
+ const content = `const element = React.createElement("div", null, "Hello World");`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual([]);
- });
+ // Assert
+ expect(result).toEqual([]);
+ });
- test("should handle special characters in component names", () => {
- // Arrange
- const content = `o.jsx($Comp_1, props); o.jsx(_Comp$2, otherProps);`;
+ test("should handle special characters in component names", () => {
+ // Arrange
+ const content = `o.jsx($Comp_1, props); o.jsx(_Comp$2, otherProps);`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["$Comp_1", "_Comp$2"]);
- });
+ // Assert
+ expect(result).toEqual(["$Comp_1", "_Comp$2"]);
+ });
- test("should not return component names when they are quoted", () => {
- // Arrange
- const content = `o.jsx("ComponentName", props); o.jsx('AnotherComponent', otherProps);`;
+ test("should not return component names when they are quoted", () => {
+ // Arrange
+ const content = `o.jsx("ComponentName", props); o.jsx('AnotherComponent', otherProps);`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual([]);
- });
+ // Assert
+ expect(result).toEqual([]);
+ });
- test("should return component names when they are followed by a brace", () => {
- // Arrange
- const content = `o.jsx(ComponentName, {props: true});`;
+ test("should return component names when they are followed by a brace", () => {
+ // Arrange
+ const content = `o.jsx(ComponentName, {props: true});`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["ComponentName"]);
- });
+ // Assert
+ expect(result).toEqual(["ComponentName"]);
+ });
- test("should handle content with line breaks and multiple jsx calls", () => {
- // Arrange
- const content = `
+ test("should handle content with line breaks and multiple jsx calls", () => {
+ // Arrange
+ const content = `
o.jsx(FirstComponent, {data: dataValue});
o.jsx(SecondComponent, {index: "date"});
o.jsx(ThirdComponent, {flag: true});
`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["FirstComponent", "SecondComponent", "ThirdComponent"]);
- });
+ // Assert
+ expect(result).toEqual(["FirstComponent", "SecondComponent", "ThirdComponent"]);
+ });
- test("should handle content with nested jsx calls", () => {
- // Arrange
- const content = `o.jsx(ParentComponent, {children: o.jsx(ChildComponent, {})})`;
+ test("should handle content with nested jsx calls", () => {
+ // Arrange
+ const content = `o.jsx(ParentComponent, {children: o.jsx(ChildComponent, {})})`;
- // Act
- const result = searchForwardComponent(content);
+ // Act
+ const result = searchForwardComponent(content);
- // Assert
- expect(result).toEqual(["ParentComponent", "ChildComponent"]);
- });
+ // Assert
+ expect(result).toEqual(["ParentComponent", "ChildComponent"]);
+ });
});
diff --git a/src/utils.ts b/src/utils.ts
index 6850535..1951544 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -678,6 +678,9 @@ function createNewClassName(mode: obfuscateMode, className: string, classPrefix:
* Extracts classes from a CSS selector.
*
* @param selector - The CSS selector to extract classes from.
+ * @param replacementClassNames - The replacement class names.
+ * The position of the class name in the array should match the
+ * position of the class in the selector that you want to replece.
* @returns An array of extracted classes.
*
* @example
@@ -688,13 +691,38 @@ function createNewClassName(mode: obfuscateMode, className: string, classPrefix:
* // Returns: []
* extractClassFromSelector("div");
*/
-function extractClassFromSelector(selector: string) {
+function extractClassFromSelector(selector: string, replacementClassNames?: (string | undefined)[]) {
+ function toBase64Key(str: string) {
+ return `${Buffer.from(str).toString("base64")}`;
+ }
+ function fromBase64Key(str: string) {
+ return `${Buffer.from(str, "base64").toString("ascii")}`;
+ }
+
+ function createKey(str: string) {
+ const b64 = toBase64Key(str).replace(/=/g, "");
+ return `{{{{{{${b64}}}}}}}`;
+ }
+
+ function decodeKey(str: string) {
+ const regex = /{{{{{{([\w\+\/]+)}}}}}}/g;
+ str = str.replace(regex, (match, p1) => {
+ // Calculate the number of '=' needed
+ const padding = p1.length % 4 === 0 ? 0 : 4 - (p1.length % 4);
+ // Add back the '='
+ const b64 = p1 + "=".repeat(padding);
+ return fromBase64Key(b64);
+ });
+ return str;
+ }
+
+ //? "(?:\\\*)?" for "*" selector, eg. ".\*\:pt-2"
//? "\\\:" for eg.".hover\:border-b-2:hover" the ".hover\:border-b-2" should be in the same group
//? "\\\.\d+" for number with ".", eg. ".ml-1\.5" the ".ml-1.5" should be in the same group, before that ".ml-1\.5" will split into ".ml-1" and ".5"
//? "\\\/\d+" for number with "/", eg. ".bg-emerald-400\/20" the ".bg-emerald-400\/20" should be in the same group, before that ".bg-emerald-400\/20" will split into ".bg-emerald-400" and "\/20"
//? "(?:\\?\[[\w\-="\\%\+\(\)]+\])?" for [attribute / Tailwind CSS custom parameter] selector
- const extractClassRegex = /(?<=[.:!*\s]|(? {
- selector = selector.replace(actionSelector, "");
+ const regex = new RegExp(`(? {
+ console.log(selector);
+ return createKey(match);
+ });
});
-
- // remove vendor pseudo class
- vendorPseudoClassRegexes.forEach((regex) => {
- selector = selector.replace(regex, "");
+
+ // replace vendor pseudo class
+ vendorPseudoClassRegexes.forEach((regex, i) => {
+ selector = selector.replace(regex, (match) => {
+ return createKey(match);
+ });
});
let classes = selector.match(extractClassRegex) as string[] | undefined;
- return classes || [];
-}
+ // replace classes with replacementClassNames
+ if (replacementClassNames !== undefined) {
+ selector = selector.replace(extractClassRegex, (originalClassName) => {
+ return replacementClassNames.shift() || originalClassName;
+ });
+ }
+ selector = decodeKey(selector);
-function getKeyByValue(object: { [key: string]: string }, value: string) {
- return Object.keys(object).find(key => object[key] === value);
+ return {
+ selector: selector,
+ extractedClasses: classes || []
+ };
}
function createClassConversionJson(
@@ -749,7 +790,7 @@ function createClassConversionJson(
classSuffix = "",
classIgnore = [],
- customTailwindDarkModeSelector = null
+ enableObfuscateMarkers = false,
}: {
classConversionJsonFolderPath: string,
buildFolderPath: string,
@@ -760,7 +801,7 @@ function createClassConversionJson(
classSuffix?: string,
classIgnore?: string[],
- customTailwindDarkModeSelector?: string | null
+ enableObfuscateMarkers?: boolean,
}) {
if (!fs.existsSync(classConversionJsonFolderPath)) {
fs.mkdirSync(classConversionJsonFolderPath);
@@ -768,9 +809,14 @@ function createClassConversionJson(
const selectorConversion: ClassConversion = loadAndMergeJsonFiles(classConversionJsonFolderPath);
+ // pre-defined ".dark", mainly for tailwindcss dark mode
+ if (enableObfuscateMarkers) {
+ selectorConversion[".dark"] = ".dark";
+ }
+
+ // get all css selectors
const cssPaths = findAllFilesWithExt(".css", buildFolderPath);
const selectors: string[] = [];
-
cssPaths.forEach((cssPath) => {
const cssContent = fs.readFileSync(cssPath, "utf-8");
const cssObj = css.parse(cssContent);
@@ -780,11 +826,6 @@ function createClassConversionJson(
// remove duplicated selectors
const uniqueSelectors = [...new Set(selectors)];
- // for tailwindcss dark mode
- if (customTailwindDarkModeSelector) {
- selectorConversion[".dark"] = `.${customTailwindDarkModeSelector}`;
- }
-
const allowClassStartWith = [".", ":is(", ":where(", ":not("
, ":matches(", ":nth-child(", ":nth-last-child("
, ":nth-of-type(", ":nth-last-of-type(", ":first-child("
@@ -803,9 +844,13 @@ function createClassConversionJson(
for (let i = 0; i < uniqueSelectors.length; i++) {
const originalSelector = uniqueSelectors[i];
- selectorClassPair[originalSelector] = extractClassFromSelector(originalSelector) || [];
+ const { extractedClasses } = extractClassFromSelector(originalSelector) || [];
+ selectorClassPair[originalSelector] = extractedClasses;
}
+ //? since a multi part selector normally grouped by multiple basic selectors
+ //? so we need to obfuscate the basic selector first
+ //? eg. ":is(.class1 .class2)" grouped by ".class1" and ".class2"
// sort the selectorClassPair by the number of classes in the selector (from least to most)
// and remove the selector with no class
const sortedSelectorClassPair = Object.entries(selectorClassPair)
@@ -818,47 +863,33 @@ function createClassConversionJson(
if (selectorClasses.length == 0) {
continue;
}
+
let selector = originalSelector;
let classes = selectorConversion[selector] ? [selectorConversion[selector].slice(1)] : selectorClasses;
+
if (classes && allowClassStartWith.some((start) => selector.startsWith(start))) {
- if (selectorClasses.length > 1) {
- const haveNotFoundClass = classes.some((className) => {
- return !selectorConversion[`.${className}`];
- });
- classes = haveNotFoundClass ? [originalSelector.slice(1)] : classes;
- }
- classes.forEach((className) => {
+ classes = classes.map((className) => {
if (classIgnore.includes(className)) {
- return;
+ return className;
}
- let newClassName = selectorConversion[`.${className}`];
-
- if (selectorConversion[originalSelector]) {
- selector = selectorConversion[originalSelector];
- } else {
- if (!newClassName) {
- newClassName = createNewClassName(mode, className, classPrefix, classSuffix, classNameLength);
- selectorConversion[`.${className}`] = `.${newClassName}`;
- } else {
- newClassName = newClassName.slice(1);
- }
- selector = selector.replace(className, newClassName);
+ let obfuscatedSelector = selectorConversion[`.${className}`];
+ if (!obfuscatedSelector) {
+ const obfuscatedClass = createNewClassName(mode, className, classPrefix, classSuffix, classNameLength);
+ obfuscatedSelector = `.${obfuscatedClass}`;
+ selectorConversion[`.${className}`] = obfuscatedSelector;
}
+ // if (selector.includes("dark\\:ring-dark-tremor-ring")) {
+ // console.log(selector);
+ // }
+ // if (obfuscatedSelector.length !== 6) {
+ // console.log(selector);
+ // }
+ return obfuscatedSelector.slice(1)
});
- selectorConversion[originalSelector] = selector;
-
- // for tailwindcss dark mode
- if (originalSelector.startsWith(`:is(.dark .dark\\:`)) {
- const obfuscatedDarkSelector = selectorConversion[".dark"];
- //eg. :is(.dark .dark\\:bg-emerald-400\\/20 .dark\\:bg-emerald-400\\/20) => .dark\\:bg-emerald-400\\/20
- const matchWholeDarkSelector = /(?<=\.dark\s)([\w\\\/\-:.]*)/;
- const match = originalSelector.match(matchWholeDarkSelector);
- const wholeDarkSelector = match ? match[0] : "";
- if (obfuscatedDarkSelector && classes.length > 2) {
- //? since during the obfuscation, the class name will remove the "." at the start, so we need to add it back to prevent the class name got sliced
- const obfuscatedWholeDarkSelector = wholeDarkSelector.replace(".dark", obfuscatedDarkSelector).replace(classes[2], selectorConversion[`.${classes[2]}`].slice(1));
- selectorConversion[wholeDarkSelector] = obfuscatedWholeDarkSelector;
- }
+ const { selector: obfuscatedSelector } = extractClassFromSelector(originalSelector, classes);
+ selectorConversion[originalSelector] = obfuscatedSelector;
+ if (originalSelector.includes(".dark")) {
+ console.log(selector);
}
}
}
From 50ce7bfa86d2b6046a22121a634f2068d2ac59fd Mon Sep 17 00:00:00 2001
From: Freeman
Date: Tue, 30 Jan 2024 23:17:10 +0000
Subject: [PATCH 02/12] Fixed `:hover` Action #6
---
src/utils.test.ts | 2 +-
src/utils.ts | 54 +++++++++++++++++------------------------------
2 files changed, 20 insertions(+), 36 deletions(-)
diff --git a/src/utils.test.ts b/src/utils.test.ts
index e301f21..dd8f6b0 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -445,7 +445,7 @@ describe("extractClassFromSelector", () => {
// Assert
expect(result).toEqual({
selector: sample,
- extractedClasses: ["escaped\\:class", "action"]
+ extractedClasses: ["escaped\\:class"]
});
});
diff --git a/src/utils.ts b/src/utils.ts
index 1951544..cf007ec 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -522,6 +522,10 @@ function copyCssData(targetSelector: string, newSelectorName: string, cssObj: an
// remove empty selectors
item.selectors = item.selectors.filter((selector: any) => selector !== "");
if (item.selectors.includes(targetSelector)) {
+ // if (targetSelector.endsWith(".hover\\:border-b-2:hover")) {
+ // if (targetSelector.endsWith("border-b-2:hover")) {
+ // console.log("targetSelector", targetSelector);
+ // }
const newRule = JSON.parse(JSON.stringify(item));
newRule.selectors = [newSelectorName];
@@ -552,6 +556,12 @@ function obfuscateCss(classConversion: ClassConversion, cssPath: string) {
}
});
+ // join all selectors with action selectors
+ const actionSelectors = getAllSelector(cssObj).filter((selector) => selector.match(findActionSelectorsRegex));
+ actionSelectors.forEach((actionSelector) => {
+ usedKeyRegistery.add(actionSelector);
+ });
+
// copy css rules
usedKeyRegistery.forEach((key) => {
const originalSelectorName = key;
@@ -564,7 +574,8 @@ function obfuscateCss(classConversion: ClassConversion, cssPath: string) {
log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
const cssOptions = {
- compress: true,
+ // compress: true,
+ compress: false,
};
const cssObfuscatedContent = css.stringify(cssObj, cssOptions);
@@ -674,6 +685,11 @@ function createNewClassName(mode: obfuscateMode, className: string, classPrefix:
return newClassName;
}
+//? CSS action selectors always at the end of the selector
+//? and they can be stacked, eg. "class:hover:active"
+//? action selectors can start with ":" or "::"
+const findActionSelectorsRegex = /(? {
- const regex = new RegExp(`(? {
- console.log(selector);
- return createKey(match);
- });
+ selector = selector.replace(findActionSelectorsRegex, (match) => {
+ return createKey(match);
});
// replace vendor pseudo class
@@ -859,7 +854,6 @@ function createClassConversionJson(
for (let i = 0; i < sortedSelectorClassPair.length; i++) {
const [originalSelector, selectorClasses] = sortedSelectorClassPair[i];
- // const selectorStartWith = originalSelector.slice(0, 1);
if (selectorClasses.length == 0) {
continue;
}
@@ -878,19 +872,10 @@ function createClassConversionJson(
obfuscatedSelector = `.${obfuscatedClass}`;
selectorConversion[`.${className}`] = obfuscatedSelector;
}
- // if (selector.includes("dark\\:ring-dark-tremor-ring")) {
- // console.log(selector);
- // }
- // if (obfuscatedSelector.length !== 6) {
- // console.log(selector);
- // }
return obfuscatedSelector.slice(1)
});
const { selector: obfuscatedSelector } = extractClassFromSelector(originalSelector, classes);
selectorConversion[originalSelector] = obfuscatedSelector;
- if (originalSelector.includes(".dark")) {
- console.log(selector);
- }
}
}
@@ -987,7 +972,6 @@ function obfuscateForwardComponentJs(searchContent: string, wholeContent: string
componentObfuscatedcomponentCodePairs.push(...childComponentObfuscatedcomponentCodePairs);
}
- console.log(componentObfuscatedcomponentCodePairs);
return componentObfuscatedcomponentCodePairs;
}
From d62f49be18c2aea0318a7c15db4bd76c26af8d90 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 00:48:59 +0000
Subject: [PATCH 03/12] Automatically Remove Original CSS after Full
Obfuscation #6
---
src/index.ts | 2 +-
src/utils.test.ts | 63 ++++++++++++++++-------------------------------
src/utils.ts | 60 ++++++++++++++++++++++++++++++--------------
3 files changed, 63 insertions(+), 62 deletions(-)
diff --git a/src/index.ts b/src/index.ts
index 731aeb7..1ee738a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -40,7 +40,7 @@ function obfuscate(options: Options) {
classSuffix: options.classSuffix,
classIgnore: options.classIgnore,
- enableObfuscateMarkers: options.enableMarkers,
+ enableObfuscateMarkerClasses: options.enableMarkers,
});
log("success", "Obfuscation", "Class conversion JSON created/updated");
diff --git a/src/utils.test.ts b/src/utils.test.ts
index dd8f6b0..7859e9e 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -3,7 +3,8 @@ import css from "css";
import {
copyCssData, findContentBetweenMarker,
findHtmlTagContentsByClass, getFilenameFromPath,
- extractClassFromSelector, searchForwardComponent
+ extractClassFromSelector, searchForwardComponent,
+ renameCssSelector
} from "./utils";
const testCss = `
@@ -72,27 +73,6 @@ const testCss = `
// }
// return recursive(cssObj.stylesheet.rules) || [];
// }
-// function renameCssSelector(oldSelector: string, newSelector: string, cssObj: any): any[] {
-// function recursive(rules: any[]): any[] {
-// return rules.map((item: any) => {
-// if (item.rules) {
-// return { ...item, rules: recursive(item.rules) };
-// } else if (item.selectors) {
-// // remove empty selectors
-// item.selectors = item.selectors.filter((selector: any) => selector !== "");
-
-// let updatedSelectors = item.selectors.map((selector: any) =>
-// selector === oldSelector ? newSelector : selector
-// );
-
-// return { ...item, selectors: updatedSelectors };
-// } else {
-// return item;
-// }
-// });
-// }
-// return recursive(cssObj.stylesheet.rules);
-// }
// describe("getCssRulesIncludedSelector", () => {
// it("should return the correct CSS rules (single selector, no nested rule)", () => {
@@ -136,30 +116,29 @@ const testCss = `
// });
// });
-// describe("renameCssSelector", () => {
-// it("should rename the CSS selector (single selector, no nested rule)", () => {
-// const cssObj = css.parse(testCss);
+describe("renameCssSelector", () => {
+ it("should rename the CSS selector (single selector, no nested rule)", () => {
+ const cssObj = css.parse(testCss);
-// const oldSelector = ".s1-1";
-// const newSelector = ".s1-1-new";
-// const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 33, "column": 9 }, "end": { "line": 33, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 34, "column": 9 }, "end": { "line": 34, "column": 23 } } }], "position": { "start": { "line": 31, "column": 5 }, "end": { "line": 35, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 36, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 39, "column": 5 }, "end": { "line": 39, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 40, "column": 5 }, "end": { "line": 40, "column": 19 } } }], "position": { "start": { "line": 38, "column": 1 }, "end": { "line": 41, "column": 2 } } }];
+ const oldSelector = ".s1-1";
+ const newSelector = ".s1-1-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-// const result = renameCssSelector(oldSelector, newSelector, cssObj);
-// expect(result).toEqual(expectedOutput);
-// });
+ const result = renameCssSelector(oldSelector, newSelector, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
-// // it("should rename the CSS selector (multiple nested media queries)", () => {
-// // const cssObj = css.parse(testCss);
+ it("should rename the CSS selector (multiple nested media queries)", () => {
+ const cssObj = css.parse(testCss);
-// // const oldSelector = ".s2-2";
-// // const newSelector = ".s2-2-new";
-// // const expectedOutput = [{ "type": "rule", "selectors": [".s0"], "declarations": [{ "type": "declaration", "property": "background", "value": "#eee", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 21 } } }, { "type": "declaration", "property": "color", "value": "#888", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 16 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#eee", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 25 } } }, { "type": "declaration", "property": "color", "value": "#888", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 20 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#eee", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 29 } } }, { "type": "declaration", "property": "color", "value": "#888", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 24 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#eee", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 29 } } }, { "type": "declaration", "property": "color", "value": "#888", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 24 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#eee", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 29 } } }, { "type": "declaration", "property": "color", "value": "#888", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 24 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 30, "column": 2 } } }];
+ const oldSelector = ".s2-2";
+ const newSelector = ".s2-2-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-new", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-// // const result = renameCssSelector(oldSelector, newSelector, cssObj);
-// // console.log(JSON.stringify(result));
-// // expect(result).toEqual(expectedOutput);
-// // });
-// });
+ const result = renameCssSelector(oldSelector, newSelector, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+});
//! ================================
//! copyCssData
@@ -591,7 +570,7 @@ describe("extractClassFromSelector", () => {
extractedClasses: ["class-1\\/2"]
})
});
-
+
test("should handle Tailwind CSS universal selector", () => {
const sample = ".\\*\\:class1 .class2\\*\\:class3";
diff --git a/src/utils.ts b/src/utils.ts
index cf007ec..4a974ba 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -215,7 +215,7 @@ function replaceJsonKeysInFiles(
// Obfuscate CSS files
cssPaths.forEach((cssPath) => {
- obfuscateCss(classConversion, cssPath);
+ obfuscateCss(classConversion, cssPath, enableObfuscateMarkerClasses);
});
}
@@ -492,7 +492,6 @@ function obfuscateJs(content: string, key: string, classCoversion: ClassConversi
});
}
-
const { obfuscatedContent, usedKeys } = obfuscateKeys(classCoversion, truncatedContent, contentIgnoreRegexes);
addKeysToRegistery(usedKeys);
if (truncatedContent !== obfuscatedContent) {
@@ -521,12 +520,9 @@ function copyCssData(targetSelector: string, newSelectorName: string, cssObj: an
} else if (item.selectors) {
// remove empty selectors
item.selectors = item.selectors.filter((selector: any) => selector !== "");
- if (item.selectors.includes(targetSelector)) {
- // if (targetSelector.endsWith(".hover\\:border-b-2:hover")) {
- // if (targetSelector.endsWith("border-b-2:hover")) {
- // console.log("targetSelector", targetSelector);
- // }
+ // check if the selector is the target selector
+ if (item.selectors.includes(targetSelector)) {
const newRule = JSON.parse(JSON.stringify(item));
newRule.selectors = [newSelectorName];
@@ -543,14 +539,38 @@ function copyCssData(targetSelector: string, newSelectorName: string, cssObj: an
return cssObj;
}
-function obfuscateCss(classConversion: ClassConversion, cssPath: string) {
+function renameCssSelector(oldSelector: string, newSelector: string, cssObj: any) {
+ function recursive(rules: any[]): any[] {
+ return rules.map((item: any) => {
+ if (item.rules) {
+ return { ...item, rules: recursive(item.rules) };
+ } else if (item.selectors) {
+ // remove empty selectors
+ item.selectors = item.selectors.filter((selector: any) => selector !== "");
+
+ let updatedSelectors = item.selectors.map((selector: any) =>
+ selector === oldSelector ? newSelector : selector
+ );
+
+ return { ...item, selectors: updatedSelectors };
+ } else {
+ return item;
+ }
+ });
+ }
+
+ cssObj.stylesheet.rules = recursive(cssObj.stylesheet.rules);
+ return cssObj;
+}
+
+function obfuscateCss(selectorConversion: ClassConversion, cssPath: string, replaceOriginalSelector: boolean = false) {
let cssContent = fs.readFileSync(cssPath, "utf-8");
let cssObj = css.parse(cssContent);
const cssRulesCount = cssObj.stylesheet.rules.length;
// join all selectors start with ":" (eg. ":is")
- Object.keys(classConversion).forEach((key) => {
+ Object.keys(selectorConversion).forEach((key) => {
if (key.startsWith(":")) {
usedKeyRegistery.add(key);
}
@@ -562,20 +582,22 @@ function obfuscateCss(classConversion: ClassConversion, cssPath: string) {
usedKeyRegistery.add(actionSelector);
});
- // copy css rules
+ // modify css rules
usedKeyRegistery.forEach((key) => {
const originalSelectorName = key;
- const obfuscatedSelectorName = classConversion[key];
+ const obfuscatedSelectorName = selectorConversion[key];
if (obfuscatedSelectorName) {
- // copy the original css rules and paste it with the obfuscated selector name
- cssObj = copyCssData(originalSelectorName, classConversion[key], cssObj);
+ if (replaceOriginalSelector) {
+ cssObj = renameCssSelector(originalSelectorName, selectorConversion[key], cssObj);
+ } else {
+ cssObj = copyCssData(originalSelectorName, selectorConversion[key], cssObj);
+ }
}
});
log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
const cssOptions = {
- // compress: true,
- compress: false,
+ compress: true,
};
const cssObfuscatedContent = css.stringify(cssObj, cssOptions);
@@ -785,7 +807,7 @@ function createClassConversionJson(
classSuffix = "",
classIgnore = [],
- enableObfuscateMarkers = false,
+ enableObfuscateMarkerClasses = false,
}: {
classConversionJsonFolderPath: string,
buildFolderPath: string,
@@ -796,7 +818,7 @@ function createClassConversionJson(
classSuffix?: string,
classIgnore?: string[],
- enableObfuscateMarkers?: boolean,
+ enableObfuscateMarkerClasses?: boolean,
}) {
if (!fs.existsSync(classConversionJsonFolderPath)) {
fs.mkdirSync(classConversionJsonFolderPath);
@@ -805,7 +827,7 @@ function createClassConversionJson(
const selectorConversion: ClassConversion = loadAndMergeJsonFiles(classConversionJsonFolderPath);
// pre-defined ".dark", mainly for tailwindcss dark mode
- if (enableObfuscateMarkers) {
+ if (enableObfuscateMarkerClasses) {
selectorConversion[".dark"] = ".dark";
}
@@ -980,5 +1002,5 @@ export {
, replaceJsonKeysInFiles, setLogLevel
, copyCssData, findContentBetweenMarker, findHtmlTagContentsByClass
, findAllFilesWithExt, createClassConversionJson, extractClassFromSelector
- , obfuscateKeys, searchForwardComponent, obfuscateForwardComponentJs
+ , obfuscateKeys, searchForwardComponent, obfuscateForwardComponentJs, renameCssSelector
};
From 6b8c1337021b6a0febcd40ab782af71ee5edfc0d Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 01:27:04 +0000
Subject: [PATCH 04/12] Restructured Folder Structure
---
.prettierignore | 5 +
.prettierrc | 7 +
src/config.ts | 2 +-
src/handlers/css.test.ts | 489 +++++++++++++++++++++++++++++
src/handlers/css.ts | 364 ++++++++++++++++++++++
src/handlers/html.test.ts | 29 ++
src/handlers/html.ts | 106 +++++++
src/handlers/js.test.ts | 125 ++++++++
src/handlers/js.ts | 128 ++++++++
src/index.ts | 12 +-
src/{type.ts => types.ts} | 4 +-
src/utils.test.ts | 630 +-------------------------------------
src/utils.ts | 579 +----------------------------------
13 files changed, 1278 insertions(+), 1202 deletions(-)
create mode 100644 .prettierignore
create mode 100644 .prettierrc
create mode 100644 src/handlers/css.test.ts
create mode 100644 src/handlers/css.ts
create mode 100644 src/handlers/html.test.ts
create mode 100644 src/handlers/html.ts
create mode 100644 src/handlers/js.test.ts
create mode 100644 src/handlers/js.ts
rename src/{type.ts => types.ts} (94%)
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..7605e7f
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,5 @@
+build
+coverage
+dist
+node_modules
+*.test.*
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..cf7c9eb
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": false,
+ "printWidth": 100,
+ "tabWidth": 2
+}
diff --git a/src/config.ts b/src/config.ts
index cf4f8dd..0905ca2 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,4 +1,4 @@
-import { type Options, type OptionalOptions } from "./type";
+import { type Options, type OptionalOptions } from "./types";
const defaultOptions: Options = {
enable: true, // Enable or disable the plugin.
diff --git a/src/handlers/css.test.ts b/src/handlers/css.test.ts
new file mode 100644
index 0000000..947affa
--- /dev/null
+++ b/src/handlers/css.test.ts
@@ -0,0 +1,489 @@
+// @ts-ignore
+import css from "css";
+
+import {
+ copyCssData,
+ renameCssSelector,
+ extractClassFromSelector,
+} from "./css";
+
+const testCss = `
+.s0-1 {
+ background: #181810;
+ color: #181811;
+}
+
+@media (min-width: 640px)
+{
+ .s1-1
+ {
+ background: #181812;
+ color: #181813;
+ }
+
+ @media (min-width: 768px)
+ {
+ .s2-1, .s2-1-1 {
+ background: #181814;
+ color: #181815;
+ },
+ .s2-1, .s2-1-1 {
+ background: #181814;
+ color: #181815;
+ },
+ .s2-2, .s2-2-2 {
+ background: #181816;
+ color: #181817;
+ },
+ .s2-3 {
+ background: #181818;
+ color: #181819;
+ }
+ }
+
+ .s1-2
+ {
+ background: #181820;
+ color: #181821;
+ }
+}
+
+.s0-2 {
+ background: #181822;
+ color: #181823;
+}
+`
+
+// function getCssRulesIncludedSelector(selector: string, cssObj: any): any[] {
+// function recursive(rules: any[]) {
+// for (const item of rules) {
+// if (item.rules) {
+// const result: any = recursive(item.rules);
+// if (result !== null) {
+// return [{ ...item, rules: result }];
+// }
+// } else if (item.selectors.includes(selector)) {
+// // remove empty selectors
+// item.selectors = item.selectors.filter((selector: any) => selector !== "");
+
+// return [{ ...item, selectors: [selector] }];
+// }
+// }
+// return null;
+// }
+// return recursive(cssObj.stylesheet.rules) || [];
+// }
+
+// describe("getCssRulesIncludedSelector", () => {
+// it("should return the correct CSS rules (single selector, no nested rule)", () => {
+// const cssObj = css.parse(testCss);
+
+// const selector = ".s0-1";
+// const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }];
+
+// const result = getCssRulesIncludedSelector(selector, cssObj);
+// expect(result).toEqual(expectedOutput);
+// });
+
+// it("should return the correct CSS rules (multiple nested rules)", () => {
+// const cssObj = css.parse(testCss);
+
+// const selector = ".s2-3";
+// const expectedOutput = [{ "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 36, "column": 2 } } }];
+
+// const result = getCssRulesIncludedSelector(selector, cssObj);
+// expect(result).toEqual(expectedOutput);
+// });
+
+// it("should return the correct CSS rules (multiple selector in same rule)", () => {
+// const cssObj = css.parse(testCss);
+
+// const selector = ".s2-2-2";
+// const expectedOutput = [{ "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 36, "column": 2 } } }];
+
+// const result = getCssRulesIncludedSelector(selector, cssObj);
+// expect(result).toEqual(expectedOutput);
+// });
+
+// it("should return the empty array", () => {
+// const cssObj = css.parse(testCss);
+
+// const selector = ".s2-2-3";
+// const expectedOutput: [] = [];
+
+// const result = getCssRulesIncludedSelector(selector, cssObj);
+// expect(result).toEqual(expectedOutput);
+// });
+// });
+
+
+//! ================================
+//! renameCssSelector
+//! ================================
+
+describe("renameCssSelector", () => {
+ it("should rename the CSS selector (single selector, no nested rule)", () => {
+ const cssObj = css.parse(testCss);
+
+ const oldSelector = ".s1-1";
+ const newSelector = ".s1-1-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = renameCssSelector(oldSelector, newSelector, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+
+ it("should rename the CSS selector (multiple nested media queries)", () => {
+ const cssObj = css.parse(testCss);
+
+ const oldSelector = ".s2-2";
+ const newSelector = ".s2-2-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-new", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = renameCssSelector(oldSelector, newSelector, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+});
+
+//! ================================
+//! copyCssData
+//! ================================
+
+describe("copyCssData", () => {
+ it("should copy the CSS data (single selector, no nested rule)", () => {
+ const cssObj = css.parse(testCss);
+
+ const targetSelector = ".s0-2";
+ const newSelectorName = ".s0-2-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = copyCssData(targetSelector, newSelectorName, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+
+ it("should copy the CSS data (multiple nested rules)", () => {
+ const cssObj = css.parse(testCss);
+
+ const targetSelector = ".s2-3";
+ const newSelectorName = ".s2-3-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = copyCssData(targetSelector, newSelectorName, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+
+ it("should copy the CSS data (multiple selector in same rule)", () => {
+ const cssObj = css.parse(testCss);
+
+ const targetSelector = ".s2-2-2";
+ const newSelectorName = ".s2-2-2-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-2-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = copyCssData(targetSelector, newSelectorName, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+
+ it("should copy the CSS data (same selector with different declarations)", () => {
+ const cssObj = css.parse(testCss);
+
+ const targetSelector = ".s2-1";
+ const newSelectorName = ".s2-1-new";
+ const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
+
+ const result = copyCssData(targetSelector, newSelectorName, cssObj);
+ expect(result.stylesheet.rules).toEqual(expectedOutput);
+ });
+});
+
+//! ================================
+//! extractClassFromSelector
+//! ================================
+
+describe("extractClassFromSelector", () => {
+
+ test("should extract single class from simple selector", () => {
+ const sample = ".example";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["example"]
+ });
+ });
+
+ test("should extract multiple classes from complex selector", () => {
+ const sample = ":is(.some-class .some-class\\:bg-dark::-moz-placeholder)[data-active=\'true\']";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["some-class", "some-class\\:bg-dark"]
+ });
+ });
+
+ test("should handle selector with no classes", () => {
+ const sample = "div";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: []
+ });
+ });
+
+ test("should handle selector with action pseudo-classes and not extract them", () => {
+ const sample = ".btn:hover .btn-active::after";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["btn", "btn-active"]
+ });
+ });
+
+ test("should handle selector with vendor pseudo-classes and not extract them", () => {
+ const sample = ".btn-moz:-moz-focusring .btn-ms::-ms-placeholder .btn-webkit::-webkit-placeholder .btn-o::-o-placeholder";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["btn-moz", "btn-ms", "btn-webkit", "btn-o"]
+ });
+ });
+
+ test("should handle selector with escaped characters", () => {
+ const sample = ".escaped\\:class:action";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["escaped\\:class"]
+ });
+ });
+
+ test("should handle selector with multiple classes separated by spaces", () => {
+ const sample = ".class1 .class2 .class3";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2", "class3"]
+ });
+ });
+
+ test("should handle selector with multiple classes separated by commas", () => {
+ const sample = ".class1, .class2, .class3";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2", "class3"]
+ });
+ });
+
+ test("should handle selector with a combination of classes and ids", () => {
+ const sample = ".class1 #id .class2";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2"]
+ });
+ });
+
+ test("should handle [attribute] selector", () => {
+ const sample = ".class1[data-attr=\"value\"] .class2[data-attr='value']";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1[data-attr=\"value\"]", "class2[data-attr='value']"]
+ });
+ });
+
+ test("should handle action pseudo-class selector correctly", () => {
+ const sample = ".class1\\:hover\\:class2:after .class3\\:hover\\:class4:after:hover :is(.class5 .class6\\:hover\\:class7:hover:after) :is(.hover\\:class8\\:class9):after";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1\\:hover\\:class2", "class3\\:hover\\:class4", "class5", "class6\\:hover\\:class7", "hover\\:class8\\:class9"]
+ });
+ });
+
+ test("should ignore [attribute] selector that not in the same scope as class", () => {
+ const sample = ":is(.class1 .class2\\:class3\\:\\!class4)[aria-selected=\"true\"]";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1", "class2\\:class3\\:\\!class4"]
+ });
+ });
+
+ test("should return null for invalid input types", () => {
+ // Act & Assert
+ // @ts-ignore
+ expect(() => extractClassFromSelector(null)).toThrow(TypeError);
+ // @ts-ignore
+ expect(() => extractClassFromSelector(undefined)).toThrow(TypeError);
+ expect(() => extractClassFromSelector(123 as any)).toThrow(TypeError);
+ });
+
+
+ //? *********************
+ //? Tailwind CSS
+ //? *********************
+ test("should handle Tailwind CSS important selector '!'", () => {
+ const sample = ".\\!my-0 .some-class\\:\\!bg-white";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["\\!my-0", "some-class\\:\\!bg-white"]
+ })
+ });
+
+ test("should handle Tailwind CSS selector with start with '-'", () => {
+ const sample = ".-class-1";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["-class-1"]
+ })
+ });
+
+ test("should handle Tailwind CSS selector with '.' at the number", () => {
+ const sample = ".class-0\\.5 .class-1\\.125";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class-0\\.5", "class-1\\.125"]
+ })
+ });
+
+ test("should handle Tailwind CSS selector with '/' at the number", () => {
+ const sample = ".class-1\\/2";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class-1\\/2"]
+ })
+ });
+
+ test("should handle Tailwind CSS universal selector", () => {
+ const sample = ".\\*\\:class1 .class2\\*\\:class3";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["\\*\\:class1", "class2", "class3"]
+ })
+ });
+
+ test("should handle Tailwind CSS [custom parameter] selector", () => {
+ const sample = ".class1[100] .class2-[200]";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1[100]", "class2-[200]"]
+ })
+ });
+
+ test("should handle Tailwind CSS [custom parameter] selector with escaped characters", () => {
+ const sample = ".class1\\[1em\\] .class2-\\[2em\\] .class3\\[3\\%\\] .class4-\\[4\\%\\]";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1\\[1em\\]", "class2-\\[2em\\]", "class3\\[3\\%\\]", "class4-\\[4\\%\\]"]
+ })
+ });
+
+ test("should handle complex Tailwind CSS [custom parameter] selector", () => {
+ const sample = ".w-\\[calc\\(10\\%\\+5px\\)\\]";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["w-\\[calc\\(10\\%\\+5px\\)\\]"]
+ })
+ });
+
+ test("should ignore Tailwind CSS [custom parameter] selector that not in the same scope as class", () => {
+ const sample = ":is(.class1)[100]";
+
+ // Act
+ const result = extractClassFromSelector(sample);
+
+ // Assert
+ expect(result).toEqual({
+ selector: sample,
+ extractedClasses: ["class1"]
+ })
+ });
+});
diff --git a/src/handlers/css.ts b/src/handlers/css.ts
new file mode 100644
index 0000000..f271864
--- /dev/null
+++ b/src/handlers/css.ts
@@ -0,0 +1,364 @@
+import path from "path";
+import fs from "fs";
+// @ts-ignore
+import css from 'css';
+
+import {
+ log,
+ getRandomString,
+ simplifyString,
+ loadAndMergeJsonFiles,
+ findAllFilesWithExt,
+ usedKeyRegistery,
+ getFilenameFromPath,
+} from "../utils";
+import { obfuscateMode, SelectorConversion } from "../types";
+
+function createNewClassName(mode: obfuscateMode, className: string, classPrefix: string = "", classSuffix: string = "", classNameLength: number = 5) {
+ let newClassName = className;
+
+ switch (mode) {
+ case "random":
+ newClassName = getRandomString(classNameLength);
+ break;
+ case "simplify":
+ newClassName = simplifyString(className);
+ break;
+ default:
+ break;
+ }
+
+ if (classPrefix) {
+ newClassName = `${classPrefix}${newClassName}`;
+ }
+ if (classSuffix) {
+ newClassName = `${newClassName}${classSuffix}`;
+ }
+
+ return newClassName;
+}
+
+//? CSS action selectors always at the end of the selector
+//? and they can be stacked, eg. "class:hover:active"
+//? action selectors can start with ":" or "::"
+const findActionSelectorsRegex = /(? {
+ // Calculate the number of '=' needed
+ const padding = p1.length % 4 === 0 ? 0 : 4 - (p1.length % 4);
+ // Add back the '='
+ const b64 = p1 + "=".repeat(padding);
+ return fromBase64Key(b64);
+ });
+ return str;
+ }
+
+ //? "(?:\\\*)?" for "*" selector, eg. ".\*\:pt-2"
+ //? "\\\:" for eg.".hover\:border-b-2:hover" the ".hover\:border-b-2" should be in the same group
+ //? "\\\.\d+" for number with ".", eg. ".ml-1\.5" the ".ml-1.5" should be in the same group, before that ".ml-1\.5" will split into ".ml-1" and ".5"
+ //? "\\\/\d+" for number with "/", eg. ".bg-emerald-400\/20" the ".bg-emerald-400\/20" should be in the same group, before that ".bg-emerald-400\/20" will split into ".bg-emerald-400" and "\/20"
+ //? "(?:\\?\[[\w\-="\\%\+\(\)]+\])?" for [attribute / Tailwind CSS custom parameter] selector
+ const extractClassRegex = /(?<=[.:!\s]|(? {
+ return createKey(match);
+ });
+
+ // replace vendor pseudo class
+ vendorPseudoClassRegexes.forEach((regex, i) => {
+ selector = selector.replace(regex, (match) => {
+ return createKey(match);
+ });
+ });
+
+ let classes = selector.match(extractClassRegex) as string[] | undefined;
+
+ // replace classes with replacementClassNames
+ if (replacementClassNames !== undefined) {
+ selector = selector.replace(extractClassRegex, (originalClassName) => {
+ return replacementClassNames.shift() || originalClassName;
+ });
+ }
+ selector = decodeKey(selector);
+
+ return {
+ selector: selector,
+ extractedClasses: classes || []
+ };
+}
+
+function getAllSelector(cssObj: any): any[] {
+ const selectors: string[] = [];
+ function recursive(rules: any[]) {
+ for (const item of rules) {
+ if (item.rules) {
+ recursive(item.rules);
+ } else if (item.selectors) {
+ // remove empty selectors
+ item.selectors = item.selectors.filter((selector: any) => selector !== "");
+
+ selectors.push(...item.selectors);
+ }
+ }
+ return null;
+ }
+ recursive(cssObj.stylesheet.rules);
+ return selectors;
+}
+
+function createSelectorConversionJson(
+ {
+ selectorConversionJsonFolderPath,
+ buildFolderPath,
+
+ mode = "random",
+ classNameLength = 5,
+ classPrefix = "",
+ classSuffix = "",
+ classIgnore = [],
+
+ enableObfuscateMarkerClasses = false,
+ }: {
+ selectorConversionJsonFolderPath: string,
+ buildFolderPath: string,
+
+ mode?: obfuscateMode,
+ classNameLength?: number,
+ classPrefix?: string,
+ classSuffix?: string,
+ classIgnore?: string[],
+
+ enableObfuscateMarkerClasses?: boolean,
+ }) {
+ if (!fs.existsSync(selectorConversionJsonFolderPath)) {
+ fs.mkdirSync(selectorConversionJsonFolderPath);
+ }
+
+ const selectorConversion: SelectorConversion = loadAndMergeJsonFiles(selectorConversionJsonFolderPath);
+
+ // pre-defined ".dark", mainly for tailwindcss dark mode
+ if (enableObfuscateMarkerClasses) {
+ selectorConversion[".dark"] = ".dark";
+ }
+
+ // get all css selectors
+ const cssPaths = findAllFilesWithExt(".css", buildFolderPath);
+ const selectors: string[] = [];
+ cssPaths.forEach((cssPath) => {
+ const cssContent = fs.readFileSync(cssPath, "utf-8");
+ const cssObj = css.parse(cssContent);
+ selectors.push(...getAllSelector(cssObj));
+ });
+
+ // remove duplicated selectors
+ const uniqueSelectors = [...new Set(selectors)];
+
+ const allowClassStartWith = [".", ":is(", ":where(", ":not("
+ , ":matches(", ":nth-child(", ":nth-last-child("
+ , ":nth-of-type(", ":nth-last-of-type(", ":first-child("
+ , ":last-child(", ":first-of-type(", ":last-of-type("
+ , ":only-child(", ":only-of-type(", ":empty(", ":link("
+ , ":visited(", ":active(", ":hover(", ":focus(", ":target("
+ , ":lang(", ":enabled(", ":disabled(", ":checked(", ":default("
+ , ":indeterminate(", ":root(", ":before("
+ , ":after(", ":first-letter(", ":first-line(", ":selection("
+ , ":read-only(", ":read-write(", ":fullscreen(", ":optional("
+ , ":required(", ":valid(", ":invalid(", ":in-range(", ":out-of-range("
+ , ":placeholder-shown("
+ ];
+
+ const selectorClassPair: { [key: string]: string[] } = {};
+
+ for (let i = 0; i < uniqueSelectors.length; i++) {
+ const originalSelector = uniqueSelectors[i];
+ const { extractedClasses } = extractClassFromSelector(originalSelector) || [];
+ selectorClassPair[originalSelector] = extractedClasses;
+ }
+
+ //? since a multi part selector normally grouped by multiple basic selectors
+ //? so we need to obfuscate the basic selector first
+ //? eg. ":is(.class1 .class2)" grouped by ".class1" and ".class2"
+ // sort the selectorClassPair by the number of classes in the selector (from least to most)
+ // and remove the selector with no class
+ const sortedSelectorClassPair = Object.entries(selectorClassPair)
+ .sort((a, b) => a[1].length - b[1].length)
+ .filter((pair) => pair[1].length > 0);
+
+ for (let i = 0; i < sortedSelectorClassPair.length; i++) {
+ const [originalSelector, selectorClasses] = sortedSelectorClassPair[i];
+ if (selectorClasses.length == 0) {
+ continue;
+ }
+
+ let selector = originalSelector;
+ let classes = selectorConversion[selector] ? [selectorConversion[selector].slice(1)] : selectorClasses;
+
+ if (classes && allowClassStartWith.some((start) => selector.startsWith(start))) {
+ classes = classes.map((className) => {
+ if (classIgnore.includes(className)) {
+ return className;
+ }
+ let obfuscatedSelector = selectorConversion[`.${className}`];
+ if (!obfuscatedSelector) {
+ const obfuscatedClass = createNewClassName(mode, className, classPrefix, classSuffix, classNameLength);
+ obfuscatedSelector = `.${obfuscatedClass}`;
+ selectorConversion[`.${className}`] = obfuscatedSelector;
+ }
+ return obfuscatedSelector.slice(1)
+ });
+ const { selector: obfuscatedSelector } = extractClassFromSelector(originalSelector, classes);
+ selectorConversion[originalSelector] = obfuscatedSelector;
+ }
+ }
+
+ const jsonPath = path.join(process.cwd(), selectorConversionJsonFolderPath, "conversion.json");
+ fs.writeFileSync(jsonPath, JSON.stringify(selectorConversion, null, 2));
+}
+
+function copyCssData(targetSelector: string, newSelectorName: string, cssObj: any) {
+ function recursive(rules: any[]): any[] {
+ return rules.map((item: any) => {
+ if (item.rules) {
+ let newRules = recursive(item.rules);
+ if (Array.isArray(newRules)) {
+ newRules = newRules.flat();
+ }
+ return { ...item, rules: newRules };
+ } else if (item.selectors) {
+ // remove empty selectors
+ item.selectors = item.selectors.filter((selector: any) => selector !== "");
+
+ // check if the selector is the target selector
+ if (item.selectors.includes(targetSelector)) {
+ const newRule = JSON.parse(JSON.stringify(item));
+ newRule.selectors = [newSelectorName];
+
+ return [item, newRule];
+ } else {
+ return item;
+ }
+ } else {
+ return item;
+ }
+ });
+ }
+ cssObj.stylesheet.rules = recursive(cssObj.stylesheet.rules).flat();
+ return cssObj;
+}
+
+function renameCssSelector(oldSelector: string, newSelector: string, cssObj: any) {
+ function recursive(rules: any[]): any[] {
+ return rules.map((item: any) => {
+ if (item.rules) {
+ return { ...item, rules: recursive(item.rules) };
+ } else if (item.selectors) {
+ // remove empty selectors
+ item.selectors = item.selectors.filter((selector: any) => selector !== "");
+
+ let updatedSelectors = item.selectors.map((selector: any) =>
+ selector === oldSelector ? newSelector : selector
+ );
+
+ return { ...item, selectors: updatedSelectors };
+ } else {
+ return item;
+ }
+ });
+ }
+
+ cssObj.stylesheet.rules = recursive(cssObj.stylesheet.rules);
+ return cssObj;
+}
+
+function obfuscateCss(selectorConversion: SelectorConversion, cssPath: string, replaceOriginalSelector: boolean = false) {
+ let cssContent = fs.readFileSync(cssPath, "utf-8");
+
+ let cssObj = css.parse(cssContent);
+ const cssRulesCount = cssObj.stylesheet.rules.length;
+
+ // join all selectors start with ":" (eg. ":is")
+ Object.keys(selectorConversion).forEach((key) => {
+ if (key.startsWith(":")) {
+ usedKeyRegistery.add(key);
+ }
+ });
+
+ // join all selectors with action selectors
+ const actionSelectors = getAllSelector(cssObj).filter((selector) => selector.match(findActionSelectorsRegex));
+ actionSelectors.forEach((actionSelector) => {
+ usedKeyRegistery.add(actionSelector);
+ });
+
+ // modify css rules
+ usedKeyRegistery.forEach((key) => {
+ const originalSelectorName = key;
+ const obfuscatedSelectorName = selectorConversion[key];
+ if (obfuscatedSelectorName) {
+ if (replaceOriginalSelector) {
+ cssObj = renameCssSelector(originalSelectorName, selectorConversion[key], cssObj);
+ } else {
+ cssObj = copyCssData(originalSelectorName, selectorConversion[key], cssObj);
+ }
+ }
+ });
+ log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
+
+ const cssOptions = {
+ compress: true,
+ };
+ const cssObfuscatedContent = css.stringify(cssObj, cssOptions);
+
+ const sizeBefore = Buffer.byteLength(cssContent, "utf8");
+ fs.writeFileSync(cssPath, cssObfuscatedContent);
+ const sizeAfter = Buffer.byteLength(cssObfuscatedContent, "utf8");
+ const percentChange = Math.round(((sizeAfter) / sizeBefore) * 100);
+ log("success", "CSS obfuscated:", `Size from ${sizeBefore} to ${sizeAfter} bytes (${percentChange}%) in ${getFilenameFromPath(cssPath)}`);
+}
+
+export {
+ copyCssData,
+ renameCssSelector,
+ createSelectorConversionJson,
+ obfuscateCss,
+ extractClassFromSelector,
+}
\ No newline at end of file
diff --git a/src/handlers/html.test.ts b/src/handlers/html.test.ts
new file mode 100644
index 0000000..2f74499
--- /dev/null
+++ b/src/handlers/html.test.ts
@@ -0,0 +1,29 @@
+import {
+ findHtmlTagContentsByClass,
+} from "./html";
+
+//! ================================
+//! findHtmlTagContentsByClass
+//! ================================
+
+describe("findHtmlTagContentsByClass", () => {
+ const content = `0123456
`;
+
+ it("should return the correct content within the tag that with a given class", () => {
+ const targetClass = "test1";
+
+ const expectedOutput = [''];
+
+ const result = findHtmlTagContentsByClass(content, targetClass);
+ expect(result).toEqual(expectedOutput);
+ });
+
+ it("should return empty array if no content found", () => {
+ const targetClass = "test5";
+
+ const expectedOutput: any[] = [];
+
+ const result = findHtmlTagContentsByClass(content, targetClass);
+ expect(result).toEqual(expectedOutput);
+ });
+});
diff --git a/src/handlers/html.ts b/src/handlers/html.ts
new file mode 100644
index 0000000..16f3131
--- /dev/null
+++ b/src/handlers/html.ts
@@ -0,0 +1,106 @@
+import { log } from "../utils";
+
+function findHtmlTagContentsRecursive(content: string, targetTag: string, targetClass: string | null = null, foundTagContents: string[] = [], deep: number = 0, maxDeep: number = -1) {
+ let contentAfterTag = content;
+ const startTagWithClassRegexStr = targetClass ?
+ // ref: https://stackoverflow.com/a/16559544
+ `(<\\w+?\\s+?class\\s*=\\s*['\"][^'\"]*?\\b${targetClass}\\b)`
+ : "";
+ const startTagRegexStr = `(<${targetTag}[\\s|>])`;
+ const endTagRegexStr = `(<\/${targetTag}>)`;
+
+ // clear content before the start tag
+ const clearContentBeforeStartTagRegex = new RegExp(`${startTagWithClassRegexStr ? startTagWithClassRegexStr + ".*|" + startTagRegexStr : startTagRegexStr + ".*"}`, "i");
+ const contentAfterStartTagMatch = contentAfterTag.match(clearContentBeforeStartTagRegex);
+ if (contentAfterStartTagMatch) {
+ contentAfterTag = contentAfterStartTagMatch[0];
+ }
+
+ let endTagCont = 0;
+
+ const endTagContRegex = new RegExp(endTagRegexStr, "gi");
+ const endTagContMatch = contentAfterTag.match(endTagContRegex);
+ if (endTagContMatch) {
+ endTagCont = endTagContMatch.length;
+ }
+
+ let closeTagPoition = 0;
+
+ const tagPatternRegex = new RegExp(`${startTagWithClassRegexStr ? startTagWithClassRegexStr + "|" + startTagRegexStr : startTagRegexStr}|${endTagRegexStr}`, "gi");
+ const tagPatternMatch = contentAfterTag.match(tagPatternRegex);
+ if (tagPatternMatch) {
+ let tagCount = 0;
+ let markedPosition = false;
+ for (let i = 0; i < tagPatternMatch.length; i++) {
+ if (tagPatternMatch[i].startsWith("")) {
+ if (!markedPosition) {
+ closeTagPoition = endTagCont - tagCount;
+ markedPosition = true;
+ }
+ tagCount--;
+ } else {
+ tagCount++;
+ }
+ if (tagCount == 0) {
+ break;
+ }
+ };
+ }
+
+ // match the last html end tag of all content and all content before it
+ const tagEndRegex = new RegExp(`(.*)${endTagRegexStr}`, "i");
+
+ for (let i = 0; i < closeTagPoition; i++) {
+ const tagCloseMatch = contentAfterTag.match(tagEndRegex);
+ if (tagCloseMatch) {
+ contentAfterTag = tagCloseMatch[1];
+ }
+ }
+
+ const clearContentAfterCloseTagRegex = new RegExp(`.*${endTagRegexStr}`, "i");
+ const clearContentAfterCloseTagMatch = contentAfterTag.match(clearContentAfterCloseTagRegex);
+ if (clearContentAfterCloseTagMatch) {
+ contentAfterTag = clearContentAfterCloseTagMatch[0];
+ foundTagContents.push(contentAfterTag);
+ }
+
+ // replace the contentAfterTag in content with ""
+ // only replace the first match
+ const remainingHtmlRegex = new RegExp(contentAfterTag.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + "(.*)", "i");
+ const remainingHtmlMatch = content.match(remainingHtmlRegex);
+ if (remainingHtmlMatch) {
+ const remainingHtml = remainingHtmlMatch[1];
+ // check if any html tag is left
+ const remainingHtmlTagRegex = new RegExp(`(<\\w+?>)`, "i");
+ const remainingHtmlTagMatch = remainingHtml.match(remainingHtmlTagRegex);
+ if (remainingHtmlTagMatch) {
+ if (maxDeep === -1 || deep < maxDeep) {
+ return findHtmlTagContentsRecursive(remainingHtml, targetTag, targetClass, foundTagContents, deep + 1, maxDeep);
+ } else {
+ log("warn", "HTML search:", "Max deep reached, recursive break");
+ return foundTagContents;
+ }
+ }
+ }
+
+ return foundTagContents;
+}
+function findHtmlTagContents(content: string, targetTag: string, targetClass: string | null = null) {
+ return findHtmlTagContentsRecursive(content, targetTag, targetClass);
+}
+
+function findHtmlTagContentsByClass(content: string, targetClass: string) {
+ const regex = new RegExp(`(<(\\w+)\\s+class\\s*=\\s*['\"][^'\"]*?\\b${targetClass}\\b)`, "i");
+ const match = content.match(regex);
+ if (match) {
+ const tag = match[2];
+ return findHtmlTagContents(content, tag, targetClass);
+ } else {
+ return [];
+ }
+}
+
+export {
+ findHtmlTagContents,
+ findHtmlTagContentsByClass,
+}
\ No newline at end of file
diff --git a/src/handlers/js.test.ts b/src/handlers/js.test.ts
new file mode 100644
index 0000000..f8e5a76
--- /dev/null
+++ b/src/handlers/js.test.ts
@@ -0,0 +1,125 @@
+import {
+ searchForwardComponent,
+} from "./js";
+
+//! ================================
+//! searchForwardComponent
+//! ================================
+
+describe("searchForwardComponent", () => {
+
+ test("should return component name when jsx format is correct", () => {
+ // Arrange
+ const content = `const element = o.jsx(ComponentName, {data: dataValue, index: "date"});`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["ComponentName"]);
+ });
+
+ test("should return multiple component names for multiple matches", () => {
+ // Arrange
+ const content = `o.jsx(FirstComponent, props); o.jsx(SecondComponent, otherProps);`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["FirstComponent", "SecondComponent"]);
+ });
+
+ test("should return an empty array when no component name is found", () => {
+ // Arrange
+ const content = `o.jsx("h1", {data: dataValue, index: "date"});`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual([]);
+ });
+
+ test("should return an empty array when content is empty", () => {
+ // Arrange
+ const content = "";
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual([]);
+ });
+
+ test("should return an empty array when jsx is not used", () => {
+ // Arrange
+ const content = `const element = React.createElement("div", null, "Hello World");`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual([]);
+ });
+
+ test("should handle special characters in component names", () => {
+ // Arrange
+ const content = `o.jsx($Comp_1, props); o.jsx(_Comp$2, otherProps);`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["$Comp_1", "_Comp$2"]);
+ });
+
+ test("should not return component names when they are quoted", () => {
+ // Arrange
+ const content = `o.jsx("ComponentName", props); o.jsx('AnotherComponent', otherProps);`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual([]);
+ });
+
+ test("should return component names when they are followed by a brace", () => {
+ // Arrange
+ const content = `o.jsx(ComponentName, {props: true});`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["ComponentName"]);
+ });
+
+ test("should handle content with line breaks and multiple jsx calls", () => {
+ // Arrange
+ const content = `
+ o.jsx(FirstComponent, {data: dataValue});
+ o.jsx(SecondComponent, {index: "date"});
+ o.jsx(ThirdComponent, {flag: true});
+ `;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["FirstComponent", "SecondComponent", "ThirdComponent"]);
+ });
+
+ test("should handle content with nested jsx calls", () => {
+ // Arrange
+ const content = `o.jsx(ParentComponent, {children: o.jsx(ChildComponent, {})})`;
+
+ // Act
+ const result = searchForwardComponent(content);
+
+ // Assert
+ expect(result).toEqual(["ParentComponent", "ChildComponent"]);
+ });
+
+});
diff --git a/src/handlers/js.ts b/src/handlers/js.ts
new file mode 100644
index 0000000..c4d97d3
--- /dev/null
+++ b/src/handlers/js.ts
@@ -0,0 +1,128 @@
+import {
+ log,
+ findContentBetweenMarker,
+ replaceFirstMatch,
+ normalizePath,
+ obfuscateKeys,
+ addKeysToRegistery,
+ findClosestSymbolPosition
+} from "../utils";
+
+import { SelectorConversion } from "../types";
+
+
+function searchForwardComponent(content: string) {
+ const componentSearchRegex = /(?<=\.jsx\()[^,|"|']+/g;
+ //eg. o.jsx(yt,{data:yc,index:"date
+ // then return yt
+ //eg. o.jsx("h1",{data:yc,index:"date
+ // then nothing should be returned
+
+ const match = content.match(componentSearchRegex);
+ if (match) {
+ return match;
+ }
+ return [];
+}
+
+function searchComponent(content: string, componentName: string) {
+ const componentSearchRegex = new RegExp(`\\b(?:const|let|var)\\s+(${componentName})\\s*=\\s*.*?(\\{)`, "g");
+ // eg, let yt=l().forwardRef((e,t)=>{let
+ const match = content.match(componentSearchRegex);
+ let openSymbolPos = -1;
+ if (match) {
+ openSymbolPos = content.indexOf(match[0]) + match[0].length;
+ }
+
+ const closeMarkerPos = findClosestSymbolPosition(content, "{", "}", openSymbolPos, "forward");
+ const componentContent = content.slice(openSymbolPos, closeMarkerPos);
+
+ return componentContent;
+}
+
+function obfuscateForwardComponentJs(searchContent: string, wholeContent: string, selectorConversion: SelectorConversion) {
+ const componentNames = searchForwardComponent(searchContent).filter((componentName) => {
+ return !componentName.includes(".");
+ });
+
+ const componentsCode = componentNames.map(componentName => {
+ const componentContent = searchComponent(wholeContent, componentName);
+ return {
+ name: componentName,
+ code: componentContent
+ }
+ });
+ const componentsObfuscatedCode = componentsCode.map((componentContent) => {
+ const classNameBlocks = findContentBetweenMarker(componentContent.code, "className:", "{", "}");
+ const obfuscatedClassNameBlocks = classNameBlocks.map(block => {
+ const { obfuscatedContent, usedKeys } = obfuscateKeys(selectorConversion, block);
+ addKeysToRegistery(usedKeys);
+ return obfuscatedContent;
+ });
+
+ if (classNameBlocks.length !== obfuscatedClassNameBlocks.length) {
+ log("error", `Component obfuscation:`, `classNameBlocks.length !== obfuscatedClassNameBlocks.length`);
+ return componentContent;
+ }
+ let obscuredCode = componentContent.code;
+ for (let i = 0; i < classNameBlocks.length; i++) {
+ obscuredCode = replaceFirstMatch(obscuredCode, classNameBlocks[i], obfuscatedClassNameBlocks[i]);
+ }
+ log("debug", `Obscured keys in component:`, componentContent.name);
+ return {
+ name: componentContent.name,
+ code: obscuredCode
+ }
+ });
+
+ const componentObfuscatedcomponentCodePairs: { name: string, componentCode: string, componentObfuscatedCode: string }[] = [];
+ for (let i = 0; i < componentsCode.length; i++) {
+ if (componentsCode[i] !== componentsObfuscatedCode[i]) {
+ componentObfuscatedcomponentCodePairs.push({
+ name: componentsCode[i].name,
+ componentCode: componentsCode[i].code,
+ componentObfuscatedCode: componentsObfuscatedCode[i].code
+ });
+ }
+ }
+
+ for (let i = 0; i < componentsCode.length; i++) {
+ const childComponentObfuscatedcomponentCodePairs = obfuscateForwardComponentJs(componentsCode[i].code, wholeContent, selectorConversion);
+ componentObfuscatedcomponentCodePairs.push(...childComponentObfuscatedcomponentCodePairs);
+ }
+
+ return componentObfuscatedcomponentCodePairs;
+}
+
+function obfuscateJs(content: string, key: string, classCoversion: SelectorConversion
+ , filePath: string, contentIgnoreRegexes: RegExp[] = [], enableForwardComponentObfuscation = false) {
+ const truncatedContents = findContentBetweenMarker(content, key, "{", "}");
+ truncatedContents.forEach((truncatedContent) => {
+
+ if (enableForwardComponentObfuscation) {
+ //! this is a experimental feature, it may not work properly
+ const componentObfuscatedcomponentCodePairs = obfuscateForwardComponentJs(truncatedContent, content, classCoversion);
+ componentObfuscatedcomponentCodePairs.map((pair) => {
+ const { componentCode, componentObfuscatedCode } = pair;
+ if (componentCode !== componentObfuscatedCode) {
+ content = replaceFirstMatch(content, componentCode, componentObfuscatedCode);
+ log("debug", `Obscured keys in component:`, `${normalizePath(filePath)}`);
+ }
+ });
+ }
+
+ const { obfuscatedContent, usedKeys } = obfuscateKeys(classCoversion, truncatedContent, contentIgnoreRegexes);
+ addKeysToRegistery(usedKeys);
+ if (truncatedContent !== obfuscatedContent) {
+ content = content.replace(truncatedContent, obfuscatedContent);
+ log("debug", `Obscured keys with marker "${key}":`, `${normalizePath(filePath)}`);
+ }
+ });
+ return content;
+}
+
+export {
+ obfuscateForwardComponentJs,
+ obfuscateJs,
+ searchForwardComponent,
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 1ee738a..f43fa97 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,12 +5,13 @@ import {
log,
replaceJsonKeysInFiles,
setLogLevel,
- createClassConversionJson,
findAllFilesWithExt,
} from "./utils";
+import { createSelectorConversionJson } from "./handlers/css";
+
import Config from "./config";
-import { Options, OptionalOptions } from "./type";
+import { Options, OptionalOptions } from "./types";
function obfuscate(options: Options) {
setLogLevel(options.logLevel);
@@ -30,8 +31,8 @@ function obfuscate(options: Options) {
}
log("info", "Obfuscation", "Creating/Updating class conversion JSON");
- createClassConversionJson({
- classConversionJsonFolderPath: options.classConversionJsonFolderPath,
+ createSelectorConversionJson({
+ selectorConversionJsonFolderPath: options.classConversionJsonFolderPath,
buildFolderPath: options.buildFolderPath,
mode: options.mode,
@@ -47,7 +48,7 @@ function obfuscate(options: Options) {
replaceJsonKeysInFiles({
targetFolder: options.buildFolderPath,
allowExtensions: options.allowExtensions,
- classConversionJsonFolderPath: options.classConversionJsonFolderPath,
+ selectorConversionJsonFolderPath: options.classConversionJsonFolderPath,
contentIgnoreRegexes: options.contentIgnoreRegexes,
@@ -94,6 +95,7 @@ function obfuscateCli() {
const config = new Config(configPath ? require(configPath) : undefined).get();
obfuscate(config);
log("success", "Obfuscation", "Obfuscation complete");
+ log("info", "Give me a ⭐️ on GitHub if you like this plugin", "https://github.com/soranoo/next-css-obfuscator");
}
export { obfuscateCli, type OptionalOptions as Options };
diff --git a/src/type.ts b/src/types.ts
similarity index 94%
rename from src/type.ts
rename to src/types.ts
index 29c2593..5ae169c 100644
--- a/src/type.ts
+++ b/src/types.ts
@@ -1,6 +1,6 @@
type LogLevel = "debug" | "info" | "warn" | "error" | "success";
type obfuscateMode = "random" | "simplify";
-type ClassConversion = { [key: string]: string };
+type SelectorConversion = { [key: string]: string };
type Options = {
enable: boolean;
@@ -54,7 +54,7 @@ type OptionalOptions = {
export {
type LogLevel,
type obfuscateMode,
- type ClassConversion,
+ type SelectorConversion,
type Options,
type OptionalOptions,
}
\ No newline at end of file
diff --git a/src/utils.test.ts b/src/utils.test.ts
index 7859e9e..96f1ed7 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -1,195 +1,8 @@
-// @ts-ignore
-import css from "css";
import {
- copyCssData, findContentBetweenMarker,
- findHtmlTagContentsByClass, getFilenameFromPath,
- extractClassFromSelector, searchForwardComponent,
- renameCssSelector
+ findContentBetweenMarker,
+ getFilenameFromPath,
} from "./utils";
-const testCss = `
-.s0-1 {
- background: #181810;
- color: #181811;
-}
-
-@media (min-width: 640px)
-{
- .s1-1
- {
- background: #181812;
- color: #181813;
- }
-
- @media (min-width: 768px)
- {
- .s2-1, .s2-1-1 {
- background: #181814;
- color: #181815;
- },
- .s2-1, .s2-1-1 {
- background: #181814;
- color: #181815;
- },
- .s2-2, .s2-2-2 {
- background: #181816;
- color: #181817;
- },
- .s2-3 {
- background: #181818;
- color: #181819;
- }
- }
-
- .s1-2
- {
- background: #181820;
- color: #181821;
- }
-}
-
-.s0-2 {
- background: #181822;
- color: #181823;
-}
-`
-
-// function getCssRulesIncludedSelector(selector: string, cssObj: any): any[] {
-// function recursive(rules: any[]) {
-// for (const item of rules) {
-// if (item.rules) {
-// const result: any = recursive(item.rules);
-// if (result !== null) {
-// return [{ ...item, rules: result }];
-// }
-// } else if (item.selectors.includes(selector)) {
-// // remove empty selectors
-// item.selectors = item.selectors.filter((selector: any) => selector !== "");
-
-// return [{ ...item, selectors: [selector] }];
-// }
-// }
-// return null;
-// }
-// return recursive(cssObj.stylesheet.rules) || [];
-// }
-
-// describe("getCssRulesIncludedSelector", () => {
-// it("should return the correct CSS rules (single selector, no nested rule)", () => {
-// const cssObj = css.parse(testCss);
-
-// const selector = ".s0-1";
-// const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }];
-
-// const result = getCssRulesIncludedSelector(selector, cssObj);
-// expect(result).toEqual(expectedOutput);
-// });
-
-// it("should return the correct CSS rules (multiple nested rules)", () => {
-// const cssObj = css.parse(testCss);
-
-// const selector = ".s2-3";
-// const expectedOutput = [{ "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 36, "column": 2 } } }];
-
-// const result = getCssRulesIncludedSelector(selector, cssObj);
-// expect(result).toEqual(expectedOutput);
-// });
-
-// it("should return the correct CSS rules (multiple selector in same rule)", () => {
-// const cssObj = css.parse(testCss);
-
-// const selector = ".s2-2-2";
-// const expectedOutput = [{ "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 29, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 36, "column": 2 } } }];
-
-// const result = getCssRulesIncludedSelector(selector, cssObj);
-// expect(result).toEqual(expectedOutput);
-// });
-
-// it("should return the empty array", () => {
-// const cssObj = css.parse(testCss);
-
-// const selector = ".s2-2-3";
-// const expectedOutput: [] = [];
-
-// const result = getCssRulesIncludedSelector(selector, cssObj);
-// expect(result).toEqual(expectedOutput);
-// });
-// });
-
-describe("renameCssSelector", () => {
- it("should rename the CSS selector (single selector, no nested rule)", () => {
- const cssObj = css.parse(testCss);
-
- const oldSelector = ".s1-1";
- const newSelector = ".s1-1-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = renameCssSelector(oldSelector, newSelector, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-
- it("should rename the CSS selector (multiple nested media queries)", () => {
- const cssObj = css.parse(testCss);
-
- const oldSelector = ".s2-2";
- const newSelector = ".s2-2-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-new", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = renameCssSelector(oldSelector, newSelector, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-});
-
-//! ================================
-//! copyCssData
-//! ================================
-
-describe("copyCssData", () => {
- it("should copy the CSS data (single selector, no nested rule)", () => {
- const cssObj = css.parse(testCss);
-
- const targetSelector = ".s0-2";
- const newSelectorName = ".s0-2-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = copyCssData(targetSelector, newSelectorName, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-
- it("should copy the CSS data (multiple nested rules)", () => {
- const cssObj = css.parse(testCss);
-
- const targetSelector = ".s2-3";
- const newSelectorName = ".s2-3-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = copyCssData(targetSelector, newSelectorName, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-
- it("should copy the CSS data (multiple selector in same rule)", () => {
- const cssObj = css.parse(testCss);
-
- const targetSelector = ".s2-2-2";
- const newSelectorName = ".s2-2-2-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2-2-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = copyCssData(targetSelector, newSelectorName, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-
- it("should copy the CSS data (same selector with different declarations)", () => {
- const cssObj = css.parse(testCss);
-
- const targetSelector = ".s2-1";
- const newSelectorName = ".s2-1-new";
- const expectedOutput = [{ "type": "rule", "selectors": [".s0-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181810", "position": { "start": { "line": 3, "column": 5 }, "end": { "line": 3, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181811", "position": { "start": { "line": 4, "column": 5 }, "end": { "line": 4, "column": 19 } } }], "position": { "start": { "line": 2, "column": 1 }, "end": { "line": 5, "column": 2 } } }, { "type": "media", "media": "(min-width: 640px)", "rules": [{ "type": "rule", "selectors": [".s1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181812", "position": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181813", "position": { "start": { "line": 12, "column": 9 }, "end": { "line": 12, "column": 23 } } }], "position": { "start": { "line": 9, "column": 5 }, "end": { "line": 13, "column": 6 } } }, { "type": "media", "media": "(min-width: 768px)", "rules": [{ "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 18, "column": 13 }, "end": { "line": 18, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 19, "column": 13 }, "end": { "line": 19, "column": 27 } } }], "position": { "start": { "line": 17, "column": 9 }, "end": { "line": 20, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1", ".s2-1-1"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-1-new"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181814", "position": { "start": { "line": 22, "column": 13 }, "end": { "line": 22, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181815", "position": { "start": { "line": 23, "column": 13 }, "end": { "line": 23, "column": 27 } } }], "position": { "start": { "line": 20, "column": 10 }, "end": { "line": 24, "column": 10 } } }, { "type": "rule", "selectors": [".s2-2", ".s2-2-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181816", "position": { "start": { "line": 26, "column": 13 }, "end": { "line": 26, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181817", "position": { "start": { "line": 27, "column": 13 }, "end": { "line": 27, "column": 27 } } }], "position": { "start": { "line": 24, "column": 10 }, "end": { "line": 28, "column": 10 } } }, { "type": "rule", "selectors": [".s2-3"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181818", "position": { "start": { "line": 30, "column": 13 }, "end": { "line": 30, "column": 32 } } }, { "type": "declaration", "property": "color", "value": "#181819", "position": { "start": { "line": 31, "column": 13 }, "end": { "line": 31, "column": 27 } } }], "position": { "start": { "line": 28, "column": 10 }, "end": { "line": 32, "column": 10 } } }], "position": { "start": { "line": 15, "column": 5 }, "end": { "line": 33, "column": 6 } } }, { "type": "rule", "selectors": [".s1-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181820", "position": { "start": { "line": 37, "column": 9 }, "end": { "line": 37, "column": 28 } } }, { "type": "declaration", "property": "color", "value": "#181821", "position": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 23 } } }], "position": { "start": { "line": 35, "column": 5 }, "end": { "line": 39, "column": 6 } } }], "position": { "start": { "line": 7, "column": 1 }, "end": { "line": 40, "column": 2 } } }, { "type": "rule", "selectors": [".s0-2"], "declarations": [{ "type": "declaration", "property": "background", "value": "#181822", "position": { "start": { "line": 43, "column": 5 }, "end": { "line": 43, "column": 24 } } }, { "type": "declaration", "property": "color", "value": "#181823", "position": { "start": { "line": 44, "column": 5 }, "end": { "line": 44, "column": 19 } } }], "position": { "start": { "line": 42, "column": 1 }, "end": { "line": 45, "column": 2 } } }];
-
- const result = copyCssData(targetSelector, newSelectorName, cssObj);
- expect(result.stylesheet.rules).toEqual(expectedOutput);
- });
-});
-
//! ================================
//! findContentBetweenMarker
@@ -222,31 +35,7 @@ describe("findContentBetweenMarker", () => {
// });
});
-//! ================================
-//! findHtmlTagContentsByClass
-//! ================================
-
-describe("findHtmlTagContentsByClass", () => {
- const content = `0123456
`;
- it("should return the correct content within the tag that with a given class", () => {
- const targetClass = "test1";
-
- const expectedOutput = [''];
-
- const result = findHtmlTagContentsByClass(content, targetClass);
- expect(result).toEqual(expectedOutput);
- });
-
- it("should return empty array if no content found", () => {
- const targetClass = "test5";
-
- const expectedOutput: any[] = [];
-
- const result = findHtmlTagContentsByClass(content, targetClass);
- expect(result).toEqual(expectedOutput);
- });
-});
//! ================================
//! getFilenameFromPath
@@ -343,418 +132,3 @@ describe("getFilenameFromPath", () => {
});
});
-
-//! ================================
-//! extractClassFromSelector
-//! ================================
-
-describe("extractClassFromSelector", () => {
-
- test("should extract single class from simple selector", () => {
- const sample = ".example";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["example"]
- });
- });
-
- test("should extract multiple classes from complex selector", () => {
- const sample = ":is(.some-class .some-class\\:bg-dark::-moz-placeholder)[data-active=\'true\']";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["some-class", "some-class\\:bg-dark"]
- });
- });
-
- test("should handle selector with no classes", () => {
- const sample = "div";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: []
- });
- });
-
- test("should handle selector with action pseudo-classes and not extract them", () => {
- const sample = ".btn:hover .btn-active::after";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["btn", "btn-active"]
- });
- });
-
- test("should handle selector with vendor pseudo-classes and not extract them", () => {
- const sample = ".btn-moz:-moz-focusring .btn-ms::-ms-placeholder .btn-webkit::-webkit-placeholder .btn-o::-o-placeholder";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["btn-moz", "btn-ms", "btn-webkit", "btn-o"]
- });
- });
-
- test("should handle selector with escaped characters", () => {
- const sample = ".escaped\\:class:action";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["escaped\\:class"]
- });
- });
-
- test("should handle selector with multiple classes separated by spaces", () => {
- const sample = ".class1 .class2 .class3";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1", "class2", "class3"]
- });
- });
-
- test("should handle selector with multiple classes separated by commas", () => {
- const sample = ".class1, .class2, .class3";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1", "class2", "class3"]
- });
- });
-
- test("should handle selector with a combination of classes and ids", () => {
- const sample = ".class1 #id .class2";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1", "class2"]
- });
- });
-
- test("should handle [attribute] selector", () => {
- const sample = ".class1[data-attr=\"value\"] .class2[data-attr='value']";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1[data-attr=\"value\"]", "class2[data-attr='value']"]
- });
- });
-
- test("should handle action pseudo-class selector correctly", () => {
- const sample = ".class1\\:hover\\:class2:after .class3\\:hover\\:class4:after:hover :is(.class5 .class6\\:hover\\:class7:hover:after) :is(.hover\\:class8\\:class9):after";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1\\:hover\\:class2", "class3\\:hover\\:class4", "class5", "class6\\:hover\\:class7", "hover\\:class8\\:class9"]
- });
- });
-
- test("should ignore [attribute] selector that not in the same scope as class", () => {
- const sample = ":is(.class1 .class2\\:class3\\:\\!class4)[aria-selected=\"true\"]";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1", "class2\\:class3\\:\\!class4"]
- });
- });
-
- test("should return null for invalid input types", () => {
- // Act & Assert
- // @ts-ignore
- expect(() => extractClassFromSelector(null)).toThrow(TypeError);
- // @ts-ignore
- expect(() => extractClassFromSelector(undefined)).toThrow(TypeError);
- expect(() => extractClassFromSelector(123 as any)).toThrow(TypeError);
- });
-
-
- //? *********************
- //? Tailwind CSS
- //? *********************
- test("should handle Tailwind CSS important selector '!'", () => {
- const sample = ".\\!my-0 .some-class\\:\\!bg-white";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["\\!my-0", "some-class\\:\\!bg-white"]
- })
- });
-
- test("should handle Tailwind CSS selector with start with '-'", () => {
- const sample = ".-class-1";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["-class-1"]
- })
- });
-
- test("should handle Tailwind CSS selector with '.' at the number", () => {
- const sample = ".class-0\\.5 .class-1\\.125";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class-0\\.5", "class-1\\.125"]
- })
- });
-
- test("should handle Tailwind CSS selector with '/' at the number", () => {
- const sample = ".class-1\\/2";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class-1\\/2"]
- })
- });
-
- test("should handle Tailwind CSS universal selector", () => {
- const sample = ".\\*\\:class1 .class2\\*\\:class3";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["\\*\\:class1", "class2", "class3"]
- })
- });
-
- test("should handle Tailwind CSS [custom parameter] selector", () => {
- const sample = ".class1[100] .class2-[200]";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1[100]", "class2-[200]"]
- })
- });
-
- test("should handle Tailwind CSS [custom parameter] selector with escaped characters", () => {
- const sample = ".class1\\[1em\\] .class2-\\[2em\\] .class3\\[3\\%\\] .class4-\\[4\\%\\]";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1\\[1em\\]", "class2-\\[2em\\]", "class3\\[3\\%\\]", "class4-\\[4\\%\\]"]
- })
- });
-
- test("should handle complex Tailwind CSS [custom parameter] selector", () => {
- const sample = ".w-\\[calc\\(10\\%\\+5px\\)\\]";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["w-\\[calc\\(10\\%\\+5px\\)\\]"]
- })
- });
-
- test("should ignore Tailwind CSS [custom parameter] selector that not in the same scope as class", () => {
- const sample = ":is(.class1)[100]";
-
- // Act
- const result = extractClassFromSelector(sample);
-
- // Assert
- expect(result).toEqual({
- selector: sample,
- extractedClasses: ["class1"]
- })
- });
-});
-
-//! ================================
-//! searchForwardComponent
-//! ================================
-
-describe("searchForwardComponent", () => {
-
- test("should return component name when jsx format is correct", () => {
- // Arrange
- const content = `const element = o.jsx(ComponentName, {data: dataValue, index: "date"});`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["ComponentName"]);
- });
-
- test("should return multiple component names for multiple matches", () => {
- // Arrange
- const content = `o.jsx(FirstComponent, props); o.jsx(SecondComponent, otherProps);`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["FirstComponent", "SecondComponent"]);
- });
-
- test("should return an empty array when no component name is found", () => {
- // Arrange
- const content = `o.jsx("h1", {data: dataValue, index: "date"});`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual([]);
- });
-
- test("should return an empty array when content is empty", () => {
- // Arrange
- const content = "";
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual([]);
- });
-
- test("should return an empty array when jsx is not used", () => {
- // Arrange
- const content = `const element = React.createElement("div", null, "Hello World");`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual([]);
- });
-
- test("should handle special characters in component names", () => {
- // Arrange
- const content = `o.jsx($Comp_1, props); o.jsx(_Comp$2, otherProps);`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["$Comp_1", "_Comp$2"]);
- });
-
- test("should not return component names when they are quoted", () => {
- // Arrange
- const content = `o.jsx("ComponentName", props); o.jsx('AnotherComponent', otherProps);`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual([]);
- });
-
- test("should return component names when they are followed by a brace", () => {
- // Arrange
- const content = `o.jsx(ComponentName, {props: true});`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["ComponentName"]);
- });
-
- test("should handle content with line breaks and multiple jsx calls", () => {
- // Arrange
- const content = `
- o.jsx(FirstComponent, {data: dataValue});
- o.jsx(SecondComponent, {index: "date"});
- o.jsx(ThirdComponent, {flag: true});
- `;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["FirstComponent", "SecondComponent", "ThirdComponent"]);
- });
-
- test("should handle content with nested jsx calls", () => {
- // Arrange
- const content = `o.jsx(ParentComponent, {children: o.jsx(ChildComponent, {})})`;
-
- // Act
- const result = searchForwardComponent(content);
-
- // Assert
- expect(result).toEqual(["ParentComponent", "ChildComponent"]);
- });
-
-});
diff --git a/src/utils.ts b/src/utils.ts
index 4a974ba..ed7eed6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,8 +1,10 @@
import fs from "fs";
import path from "path";
-// @ts-ignore
-import css from 'css';
-import { LogLevel, obfuscateMode, ClassConversion } from "./type";
+import { LogLevel, SelectorConversion } from "./types";
+
+import { obfuscateCss } from "./handlers/css";
+import { findHtmlTagContentsByClass, findHtmlTagContents } from "./handlers/html";
+import { obfuscateJs } from "./handlers/js";
//! ====================
//! Log
@@ -58,7 +60,7 @@ function replaceJsonKeysInFiles(
{
targetFolder,
allowExtensions,
- classConversionJsonFolderPath,
+ selectorConversionJsonFolderPath,
contentIgnoreRegexes,
@@ -72,7 +74,7 @@ function replaceJsonKeysInFiles(
}: {
targetFolder: string,
allowExtensions: string[],
- classConversionJsonFolderPath: string,
+ selectorConversionJsonFolderPath: string,
contentIgnoreRegexes: RegExp[],
@@ -86,7 +88,7 @@ function replaceJsonKeysInFiles(
}) {
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js
- const classConversion: ClassConversion = loadAndMergeJsonFiles(classConversionJsonFolderPath);
+ const classConversion: SelectorConversion = loadAndMergeJsonFiles(selectorConversionJsonFolderPath);
if (removeObfuscateMarkerClassesAfterObfuscated) {
obfuscateMarkerClasses.forEach(obfuscateMarkerClass => {
@@ -220,7 +222,7 @@ function replaceJsonKeysInFiles(
}
-function obfuscateKeys(jsonData: ClassConversion, fileContent: string, contentIgnoreRegexes: RegExp[] = []) {
+function obfuscateKeys(jsonData: SelectorConversion, fileContent: string, contentIgnoreRegexes: RegExp[] = []) {
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js
const usedKeys = new Set();
@@ -295,7 +297,6 @@ function normalizePath(filePath: string) {
return filePath.replace(/\\/g, "/");
}
-
function loadAndMergeJsonFiles(jsonFolderPath: string) {
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js
const jsonFiles: { [key: string]: any } = {};
@@ -309,106 +310,6 @@ function loadAndMergeJsonFiles(jsonFolderPath: string) {
return jsonFiles;
}
-function findHtmlTagContentsRecursive(content: string, targetTag: string, targetClass: string | null = null, foundTagContents: string[] = [], deep: number = 0, maxDeep: number = -1) {
- let contentAfterTag = content;
- const startTagWithClassRegexStr = targetClass ?
- // ref: https://stackoverflow.com/a/16559544
- `(<\\w+?\\s+?class\\s*=\\s*['\"][^'\"]*?\\b${targetClass}\\b)`
- : "";
- const startTagRegexStr = `(<${targetTag}[\\s|>])`;
- const endTagRegexStr = `(<\/${targetTag}>)`;
-
- // clear content before the start tag
- const clearContentBeforeStartTagRegex = new RegExp(`${startTagWithClassRegexStr ? startTagWithClassRegexStr + ".*|" + startTagRegexStr : startTagRegexStr + ".*"}`, "i");
- const contentAfterStartTagMatch = contentAfterTag.match(clearContentBeforeStartTagRegex);
- if (contentAfterStartTagMatch) {
- contentAfterTag = contentAfterStartTagMatch[0];
- }
-
- let endTagCont = 0;
-
- const endTagContRegex = new RegExp(endTagRegexStr, "gi");
- const endTagContMatch = contentAfterTag.match(endTagContRegex);
- if (endTagContMatch) {
- endTagCont = endTagContMatch.length;
- }
-
- let closeTagPoition = 0;
-
- const tagPatternRegex = new RegExp(`${startTagWithClassRegexStr ? startTagWithClassRegexStr + "|" + startTagRegexStr : startTagRegexStr}|${endTagRegexStr}`, "gi");
- const tagPatternMatch = contentAfterTag.match(tagPatternRegex);
- if (tagPatternMatch) {
- let tagCount = 0;
- let markedPosition = false;
- for (let i = 0; i < tagPatternMatch.length; i++) {
- if (tagPatternMatch[i].startsWith("")) {
- if (!markedPosition) {
- closeTagPoition = endTagCont - tagCount;
- markedPosition = true;
- }
- tagCount--;
- } else {
- tagCount++;
- }
- if (tagCount == 0) {
- break;
- }
- };
- }
-
- // match the last html end tag of all content and all content before it
- const tagEndRegex = new RegExp(`(.*)${endTagRegexStr}`, "i");
-
- for (let i = 0; i < closeTagPoition; i++) {
- const tagCloseMatch = contentAfterTag.match(tagEndRegex);
- if (tagCloseMatch) {
- contentAfterTag = tagCloseMatch[1];
- }
- }
-
- const clearContentAfterCloseTagRegex = new RegExp(`.*${endTagRegexStr}`, "i");
- const clearContentAfterCloseTagMatch = contentAfterTag.match(clearContentAfterCloseTagRegex);
- if (clearContentAfterCloseTagMatch) {
- contentAfterTag = clearContentAfterCloseTagMatch[0];
- foundTagContents.push(contentAfterTag);
- }
-
- // replace the contentAfterTag in content with ""
- // only replace the first match
- const remainingHtmlRegex = new RegExp(contentAfterTag.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + "(.*)", "i");
- const remainingHtmlMatch = content.match(remainingHtmlRegex);
- if (remainingHtmlMatch) {
- const remainingHtml = remainingHtmlMatch[1];
- // check if any html tag is left
- const remainingHtmlTagRegex = new RegExp(`(<\\w+?>)`, "i");
- const remainingHtmlTagMatch = remainingHtml.match(remainingHtmlTagRegex);
- if (remainingHtmlTagMatch) {
- if (maxDeep === -1 || deep < maxDeep) {
- return findHtmlTagContentsRecursive(remainingHtml, targetTag, targetClass, foundTagContents, deep + 1, maxDeep);
- } else {
- log("warn", "HTML search:", "Max deep reached, recursive break");
- return foundTagContents;
- }
- }
- }
-
- return foundTagContents;
-}
-function findHtmlTagContents(content: string, targetTag: string, targetClass: string | null = null) {
- return findHtmlTagContentsRecursive(content, targetTag, targetClass);
-}
-
-function findHtmlTagContentsByClass(content: string, targetClass: string) {
- const regex = new RegExp(`(<(\\w+)\\s+class\\s*=\\s*['\"][^'\"]*?\\b${targetClass}\\b)`, "i");
- const match = content.match(regex);
- if (match) {
- const tag = match[2];
- return findHtmlTagContents(content, tag, targetClass);
- } else {
- return [];
- }
-}
-
/**
*
* @param content
@@ -474,140 +375,12 @@ function findContentBetweenMarker(content: string, targetStr: string, openMarker
return truncatedContents;
}
-
-function obfuscateJs(content: string, key: string, classCoversion: ClassConversion
- , filePath: string, contentIgnoreRegexes: RegExp[] = [], enableForwardComponentObfuscation = false) {
- const truncatedContents = findContentBetweenMarker(content, key, "{", "}");
- truncatedContents.forEach((truncatedContent) => {
-
- if (enableForwardComponentObfuscation) {
- //! this is a experimental feature, it may not work properly
- const componentObfuscatedcomponentCodePairs = obfuscateForwardComponentJs(truncatedContent, content, classCoversion);
- componentObfuscatedcomponentCodePairs.map((pair) => {
- const { componentCode, componentObfuscatedCode } = pair;
- if (componentCode !== componentObfuscatedCode) {
- content = replaceFirstMatch(content, componentCode, componentObfuscatedCode);
- log("debug", `Obscured keys in component:`, `${normalizePath(filePath)}`);
- }
- });
- }
-
- const { obfuscatedContent, usedKeys } = obfuscateKeys(classCoversion, truncatedContent, contentIgnoreRegexes);
- addKeysToRegistery(usedKeys);
- if (truncatedContent !== obfuscatedContent) {
- content = content.replace(truncatedContent, obfuscatedContent);
- log("debug", `Obscured keys with marker "${key}":`, `${normalizePath(filePath)}`);
- }
- });
- return content;
-}
-
function addKeysToRegistery(usedKeys: Set) {
usedKeys.forEach((key) => {
usedKeyRegistery.add(key);
});
}
-function copyCssData(targetSelector: string, newSelectorName: string, cssObj: any) {
- function recursive(rules: any[]): any[] {
- return rules.map((item: any) => {
- if (item.rules) {
- let newRules = recursive(item.rules);
- if (Array.isArray(newRules)) {
- newRules = newRules.flat();
- }
- return { ...item, rules: newRules };
- } else if (item.selectors) {
- // remove empty selectors
- item.selectors = item.selectors.filter((selector: any) => selector !== "");
-
- // check if the selector is the target selector
- if (item.selectors.includes(targetSelector)) {
- const newRule = JSON.parse(JSON.stringify(item));
- newRule.selectors = [newSelectorName];
-
- return [item, newRule];
- } else {
- return item;
- }
- } else {
- return item;
- }
- });
- }
- cssObj.stylesheet.rules = recursive(cssObj.stylesheet.rules).flat();
- return cssObj;
-}
-
-function renameCssSelector(oldSelector: string, newSelector: string, cssObj: any) {
- function recursive(rules: any[]): any[] {
- return rules.map((item: any) => {
- if (item.rules) {
- return { ...item, rules: recursive(item.rules) };
- } else if (item.selectors) {
- // remove empty selectors
- item.selectors = item.selectors.filter((selector: any) => selector !== "");
-
- let updatedSelectors = item.selectors.map((selector: any) =>
- selector === oldSelector ? newSelector : selector
- );
-
- return { ...item, selectors: updatedSelectors };
- } else {
- return item;
- }
- });
- }
-
- cssObj.stylesheet.rules = recursive(cssObj.stylesheet.rules);
- return cssObj;
-}
-
-function obfuscateCss(selectorConversion: ClassConversion, cssPath: string, replaceOriginalSelector: boolean = false) {
- let cssContent = fs.readFileSync(cssPath, "utf-8");
-
- let cssObj = css.parse(cssContent);
- const cssRulesCount = cssObj.stylesheet.rules.length;
-
- // join all selectors start with ":" (eg. ":is")
- Object.keys(selectorConversion).forEach((key) => {
- if (key.startsWith(":")) {
- usedKeyRegistery.add(key);
- }
- });
-
- // join all selectors with action selectors
- const actionSelectors = getAllSelector(cssObj).filter((selector) => selector.match(findActionSelectorsRegex));
- actionSelectors.forEach((actionSelector) => {
- usedKeyRegistery.add(actionSelector);
- });
-
- // modify css rules
- usedKeyRegistery.forEach((key) => {
- const originalSelectorName = key;
- const obfuscatedSelectorName = selectorConversion[key];
- if (obfuscatedSelectorName) {
- if (replaceOriginalSelector) {
- cssObj = renameCssSelector(originalSelectorName, selectorConversion[key], cssObj);
- } else {
- cssObj = copyCssData(originalSelectorName, selectorConversion[key], cssObj);
- }
- }
- });
- log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
-
- const cssOptions = {
- compress: true,
- };
- const cssObfuscatedContent = css.stringify(cssObj, cssOptions);
-
- const sizeBefore = Buffer.byteLength(cssContent, "utf8");
- fs.writeFileSync(cssPath, cssObfuscatedContent);
- const sizeAfter = Buffer.byteLength(cssObfuscatedContent, "utf8");
- const percentChange = Math.round(((sizeAfter) / sizeBefore) * 100);
- log("success", "CSS obfuscated:", `Size from ${sizeBefore} to ${sizeAfter} bytes (${percentChange}%) in ${getFilenameFromPath(cssPath)}`);
-}
-
/**
* Find all files with the specified extension in the build folder
* @param ext - the extension of the files to find (e.g. .css) "." is required
@@ -645,25 +418,6 @@ function findAllFilesWithExt(ext: string, targetFolderPath: string): string[] {
return targetExtFiles;
}
-function getAllSelector(cssObj: any): any[] {
- const selectors: string[] = [];
- function recursive(rules: any[]) {
- for (const item of rules) {
- if (item.rules) {
- recursive(item.rules);
- } else if (item.selectors) {
- // remove empty selectors
- item.selectors = item.selectors.filter((selector: any) => selector !== "");
-
- selectors.push(...item.selectors);
- }
- }
- return null;
- }
- recursive(cssObj.stylesheet.rules);
- return selectors;
-}
-
function getRandomString(length: number) {
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js
// Generate a random string of characters with the specified length
@@ -683,257 +437,6 @@ function simplifyString(str: string) {
: tempStr;
}
-function createNewClassName(mode: obfuscateMode, className: string, classPrefix: string = "", classSuffix: string = "", classNameLength: number = 5) {
- let newClassName = className;
-
- switch (mode) {
- case "random":
- newClassName = getRandomString(classNameLength);
- break;
- case "simplify":
- newClassName = simplifyString(className);
- break;
- default:
- break;
- }
-
- if (classPrefix) {
- newClassName = `${classPrefix}${newClassName}`;
- }
- if (classSuffix) {
- newClassName = `${newClassName}${classSuffix}`;
- }
-
- return newClassName;
-}
-
-//? CSS action selectors always at the end of the selector
-//? and they can be stacked, eg. "class:hover:active"
-//? action selectors can start with ":" or "::"
-const findActionSelectorsRegex = /(? {
- // Calculate the number of '=' needed
- const padding = p1.length % 4 === 0 ? 0 : 4 - (p1.length % 4);
- // Add back the '='
- const b64 = p1 + "=".repeat(padding);
- return fromBase64Key(b64);
- });
- return str;
- }
-
- //? "(?:\\\*)?" for "*" selector, eg. ".\*\:pt-2"
- //? "\\\:" for eg.".hover\:border-b-2:hover" the ".hover\:border-b-2" should be in the same group
- //? "\\\.\d+" for number with ".", eg. ".ml-1\.5" the ".ml-1.5" should be in the same group, before that ".ml-1\.5" will split into ".ml-1" and ".5"
- //? "\\\/\d+" for number with "/", eg. ".bg-emerald-400\/20" the ".bg-emerald-400\/20" should be in the same group, before that ".bg-emerald-400\/20" will split into ".bg-emerald-400" and "\/20"
- //? "(?:\\?\[[\w\-="\\%\+\(\)]+\])?" for [attribute / Tailwind CSS custom parameter] selector
- const extractClassRegex = /(?<=[.:!\s]|(? {
- return createKey(match);
- });
-
- // replace vendor pseudo class
- vendorPseudoClassRegexes.forEach((regex, i) => {
- selector = selector.replace(regex, (match) => {
- return createKey(match);
- });
- });
-
- let classes = selector.match(extractClassRegex) as string[] | undefined;
-
- // replace classes with replacementClassNames
- if (replacementClassNames !== undefined) {
- selector = selector.replace(extractClassRegex, (originalClassName) => {
- return replacementClassNames.shift() || originalClassName;
- });
- }
- selector = decodeKey(selector);
-
- return {
- selector: selector,
- extractedClasses: classes || []
- };
-}
-
-function createClassConversionJson(
- {
- classConversionJsonFolderPath,
- buildFolderPath,
-
- mode = "random",
- classNameLength = 5,
- classPrefix = "",
- classSuffix = "",
- classIgnore = [],
-
- enableObfuscateMarkerClasses = false,
- }: {
- classConversionJsonFolderPath: string,
- buildFolderPath: string,
-
- mode?: obfuscateMode,
- classNameLength?: number,
- classPrefix?: string,
- classSuffix?: string,
- classIgnore?: string[],
-
- enableObfuscateMarkerClasses?: boolean,
- }) {
- if (!fs.existsSync(classConversionJsonFolderPath)) {
- fs.mkdirSync(classConversionJsonFolderPath);
- }
-
- const selectorConversion: ClassConversion = loadAndMergeJsonFiles(classConversionJsonFolderPath);
-
- // pre-defined ".dark", mainly for tailwindcss dark mode
- if (enableObfuscateMarkerClasses) {
- selectorConversion[".dark"] = ".dark";
- }
-
- // get all css selectors
- const cssPaths = findAllFilesWithExt(".css", buildFolderPath);
- const selectors: string[] = [];
- cssPaths.forEach((cssPath) => {
- const cssContent = fs.readFileSync(cssPath, "utf-8");
- const cssObj = css.parse(cssContent);
- selectors.push(...getAllSelector(cssObj));
- });
-
- // remove duplicated selectors
- const uniqueSelectors = [...new Set(selectors)];
-
- const allowClassStartWith = [".", ":is(", ":where(", ":not("
- , ":matches(", ":nth-child(", ":nth-last-child("
- , ":nth-of-type(", ":nth-last-of-type(", ":first-child("
- , ":last-child(", ":first-of-type(", ":last-of-type("
- , ":only-child(", ":only-of-type(", ":empty(", ":link("
- , ":visited(", ":active(", ":hover(", ":focus(", ":target("
- , ":lang(", ":enabled(", ":disabled(", ":checked(", ":default("
- , ":indeterminate(", ":root(", ":before("
- , ":after(", ":first-letter(", ":first-line(", ":selection("
- , ":read-only(", ":read-write(", ":fullscreen(", ":optional("
- , ":required(", ":valid(", ":invalid(", ":in-range(", ":out-of-range("
- , ":placeholder-shown("
- ];
-
- const selectorClassPair: { [key: string]: string[] } = {};
-
- for (let i = 0; i < uniqueSelectors.length; i++) {
- const originalSelector = uniqueSelectors[i];
- const { extractedClasses } = extractClassFromSelector(originalSelector) || [];
- selectorClassPair[originalSelector] = extractedClasses;
- }
-
- //? since a multi part selector normally grouped by multiple basic selectors
- //? so we need to obfuscate the basic selector first
- //? eg. ":is(.class1 .class2)" grouped by ".class1" and ".class2"
- // sort the selectorClassPair by the number of classes in the selector (from least to most)
- // and remove the selector with no class
- const sortedSelectorClassPair = Object.entries(selectorClassPair)
- .sort((a, b) => a[1].length - b[1].length)
- .filter((pair) => pair[1].length > 0);
-
- for (let i = 0; i < sortedSelectorClassPair.length; i++) {
- const [originalSelector, selectorClasses] = sortedSelectorClassPair[i];
- if (selectorClasses.length == 0) {
- continue;
- }
-
- let selector = originalSelector;
- let classes = selectorConversion[selector] ? [selectorConversion[selector].slice(1)] : selectorClasses;
-
- if (classes && allowClassStartWith.some((start) => selector.startsWith(start))) {
- classes = classes.map((className) => {
- if (classIgnore.includes(className)) {
- return className;
- }
- let obfuscatedSelector = selectorConversion[`.${className}`];
- if (!obfuscatedSelector) {
- const obfuscatedClass = createNewClassName(mode, className, classPrefix, classSuffix, classNameLength);
- obfuscatedSelector = `.${obfuscatedClass}`;
- selectorConversion[`.${className}`] = obfuscatedSelector;
- }
- return obfuscatedSelector.slice(1)
- });
- const { selector: obfuscatedSelector } = extractClassFromSelector(originalSelector, classes);
- selectorConversion[originalSelector] = obfuscatedSelector;
- }
- }
-
- const jsonPath = path.join(process.cwd(), classConversionJsonFolderPath, "conversion.json");
- fs.writeFileSync(jsonPath, JSON.stringify(selectorConversion, null, 2));
-}
-
-function searchForwardComponent(content: string) {
- const componentSearchRegex = /(?<=\.jsx\()[^,|"|']+/g;
- //eg. o.jsx(yt,{data:yc,index:"date
- // then return yt
- //eg. o.jsx("h1",{data:yc,index:"date
- // then nothing should be returned
-
- const match = content.match(componentSearchRegex);
- if (match) {
- return match;
- }
- return [];
-}
-
-function searchComponent(content: string, componentName: string) {
- const componentSearchRegex = new RegExp(`\\b(?:const|let|var)\\s+(${componentName})\\s*=\\s*.*?(\\{)`, "g");
- // eg, let yt=l().forwardRef((e,t)=>{let
- const match = content.match(componentSearchRegex);
- let openSymbolPos = -1;
- if (match) {
- openSymbolPos = content.indexOf(match[0]) + match[0].length;
- }
-
- const closeMarkerPos = findClosestSymbolPosition(content, "{", "}", openSymbolPos, "forward");
- const componentContent = content.slice(openSymbolPos, closeMarkerPos);
-
- return componentContent;
-}
-
function replaceFirstMatch(source: string, find: string, replace: string): string {
const index = source.indexOf(find);
if (index !== -1) {
@@ -942,65 +445,9 @@ function replaceFirstMatch(source: string, find: string, replace: string): strin
return source;
}
-
-function obfuscateForwardComponentJs(searchContent: string, wholeContent: string, classConversion: ClassConversion) {
- const componentNames = searchForwardComponent(searchContent).filter((componentName) => {
- return !componentName.includes(".");
- });
-
- const componentsCode = componentNames.map(componentName => {
- const componentContent = searchComponent(wholeContent, componentName);
- return {
- name: componentName,
- code: componentContent
- }
- });
- const componentsObfuscatedCode = componentsCode.map((componentContent) => {
- const classNameBlocks = findContentBetweenMarker(componentContent.code, "className:", "{", "}");
- const obfuscatedClassNameBlocks = classNameBlocks.map(block => {
- const { obfuscatedContent, usedKeys } = obfuscateKeys(classConversion, block);
- addKeysToRegistery(usedKeys);
- return obfuscatedContent;
- });
-
- if (classNameBlocks.length !== obfuscatedClassNameBlocks.length) {
- log("error", `Component obfuscation:`, `classNameBlocks.length !== obfuscatedClassNameBlocks.length`);
- return componentContent;
- }
- let obscuredCode = componentContent.code;
- for (let i = 0; i < classNameBlocks.length; i++) {
- obscuredCode = replaceFirstMatch(obscuredCode, classNameBlocks[i], obfuscatedClassNameBlocks[i]);
- }
- log("debug", `Obscured keys in component:`, componentContent.name);
- return {
- name: componentContent.name,
- code: obscuredCode
- }
- });
-
- const componentObfuscatedcomponentCodePairs: { name: string, componentCode: string, componentObfuscatedCode: string }[] = [];
- for (let i = 0; i < componentsCode.length; i++) {
- if (componentsCode[i] !== componentsObfuscatedCode[i]) {
- componentObfuscatedcomponentCodePairs.push({
- name: componentsCode[i].name,
- componentCode: componentsCode[i].code,
- componentObfuscatedCode: componentsObfuscatedCode[i].code
- });
- }
- }
-
- for (let i = 0; i < componentsCode.length; i++) {
- const childComponentObfuscatedcomponentCodePairs = obfuscateForwardComponentJs(componentsCode[i].code, wholeContent, classConversion);
- componentObfuscatedcomponentCodePairs.push(...childComponentObfuscatedcomponentCodePairs);
- }
-
- return componentObfuscatedcomponentCodePairs;
-}
-
export {
- getFilenameFromPath, log, normalizePath
- , replaceJsonKeysInFiles, setLogLevel
- , copyCssData, findContentBetweenMarker, findHtmlTagContentsByClass
- , findAllFilesWithExt, createClassConversionJson, extractClassFromSelector
- , obfuscateKeys, searchForwardComponent, obfuscateForwardComponentJs, renameCssSelector
+ getFilenameFromPath, log, normalizePath, loadAndMergeJsonFiles
+ , replaceJsonKeysInFiles, setLogLevel, findContentBetweenMarker, replaceFirstMatch
+ , findAllFilesWithExt, getRandomString, simplifyString, usedKeyRegistery
+ , obfuscateKeys, findClosestSymbolPosition, addKeysToRegistery
};
From 016b123ee733258daf8bd1b7e7c764eec5f54d1f Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 01:27:59 +0000
Subject: [PATCH 05/12] Updated Docs
---
README.md | 67 +++++++++++++++++++++++++++++++++++++------
docs/upgrade-to-v2.md | 1 -
package-lock.json | 20 +++++++++++--
package.json | 9 ++++--
4 files changed, 83 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 99bc9b7..1333b7f 100644
--- a/README.md
+++ b/README.md
@@ -7,11 +7,23 @@ Project start on 30-10-2023
[](https://www.npmjs.com/package/next-css-obfuscator) [](https://www.npmjs.com/package/next-css-obfuscator)
-### 🎉 Version 2 has NOW been released 🎉
+Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for better reading experience.
+
+### 🎉 Version 2.1.0 has NOW been released 🎉
+ Shout out to [hoangnhan2ka3](https://github.com/hoangnhan2ka3) for providing a wonderful [issue](https://github.com/soranoo/next-css-obfuscator/issues/6) report.
+
+ #### Changes:
+ - Much Much Much better CSS selector obfuscation
+ - Auto delete orginal CSS after obfuscation (only apply at full obfuscation)
+ - Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
+ - Support TailwindCSS Universal Selector (eg. `*:pt-4`)
+ - More tests
+
+### Version 2 (Major Update)
This version is deeply inspired by [PostCSS-Obfuscator](https://github.com/n4j1Br4ch1D/postcss-obfuscator). Shout out to [n4j1Br4ch1D](https://github.com/n4j1Br4ch1D) for creating such a great package and thank you [tremor](https://github.com/tremorlabs) for sponsoring this project.
#### Changes:
- - Support basic partially obfuscation
+ - Support basic partial obfuscation
- Support TailwindCSS Dark Mode
- New configuration file `next-css-obfuscator.config.cjs`
- More configuration options
@@ -47,6 +59,8 @@ Give me a ⭐ if you like it.
- [1. Not work at Vercel after updated](#1-not-work-at-vercel-after-updated)
- [2. Lazy Setup - Obfuscate all files](#2-lazy-setup---obfuscate-all-files)
- [3. It was working normally just now, but not now?](#3-it-was-working-normally-just-now-but-not-now)
+ - [4. Why are some original selectors still in the obfuscated CSS file after full obfuscation?](#4-why-are-some-original-selectors-still-in-the-obfuscated-css-file-after-full-obfuscation)
+ - [5. Why did I get a copy of the original CSS after partial obfuscation?](#5-why-did-i-get-a-copy-of-the-original-css-after-partial-obfuscation)
- [👀 Demos](#-demos)
- [⭐ TODO](#-todo)
- [🐛 Known Issues](#-known-issues)
@@ -101,9 +115,9 @@ Edit the build files directly. (It may not be the best solution but it works.)
(Theoretically it supports all CSS frameworks but I only tested it with TailwindCSS.)
-
+- ⌛ TIME 🕛
## 🚀 Getting Started
@@ -269,7 +283,6 @@ It may not be the best setting but it works for me. :)
|enableMarkers|boolean|false|Enable or disable the obfuscation markers.|
|markers|string[ ]|[ ]|Classes that indicate component(s) need to obfuscate.|
|removeMarkersAfterObfuscated|boolean|true|Remove the obfuscation markers from HTML elements after obfuscation.|
-|customTailwindDarkModeSelector|string \| null|null| [TailwindCSS ONLY] The custom new dark mode selector, e.g. "dark-mode".|
|logLevel|"debug" \| "info" \| "warn" \| "error" \| "success"| "info"|The log level.|
###### All options in one place
@@ -295,7 +308,6 @@ module.exports = {
enableMarkers: false, // Enable or disable the obfuscate marker classes.
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
- customTailwindDarkModeSelector: null, // [TailwindCSS ONLY] The custom new dark mode selector, e.g. "dark-mode".
logLevel: "info", // Log level
};
@@ -321,6 +333,43 @@ Enable `enableMarkers` and put the obfuscate marker class at every component inc
Your convertion table may be messed up. Try to delete the `classConversionJsonFolderPath`(default: `css-obfuscator`) folder to reset the convertion table.
+### 4. Why are some original selectors still in the obfuscated CSS file after full obfuscation?
+
+In a normal situation, the package will only remove the original CSS that is related to the obfuscation and you should not see any CSS sharing the same declaration block.
+
+You are not expected to see this:
+```css
+/* example */
+
+/* orginal form */
+.text-stone-300 {
+ --tw-text-opacity: 1;
+ color: rgb(214 211 209 / var(--tw-text-opacity));
+}
+
+/* obfuscated form */
+.d8964 {
+ --tw-text-opacity: 1;
+ color: rgb(214 211 209 / var(--tw-text-opacity));
+}
+```
+But this:
+```css
+/* example */
+
+/* obfuscated form */
+.d8964 {
+ --tw-text-opacity: 1;
+ color: rgb(214 211 209 / var(--tw-text-opacity));
+}
+```
+
+If you encounter the first situation, it means something is wrong with the obfuscation. You may need to raise an [issue](https://github.com/soranoo/next-css-obfuscator/issues) with your configuration and the related code.
+
+### 5. Why did I get a copy of the original CSS after partial obfuscation?
+
+Since the original CSS may referenced by other components not included in the obfuscation, the package will not remove the original CSS to prevent breaking the the site.
+
## 👀 Demos
1. [Next 14 App Router](https://github.com/soranoo/next-css-obfuscator/tree/main/demo/next14-app-router)
@@ -328,15 +377,17 @@ Your convertion table may be messed up. Try to delete the `classConversionJsonFo
## ⭐ TODO
-- [x] Partially obfuscation
+- [x] Partial obfuscation
- [x] To be a totally independent package (remove dependency on [PostCSS-Obfuscator](https://github.com/n4j1Br4ch1D/postcss-obfuscato))
- [ ] More tests
+- [ ] More domes ?
## 🐛 Known Issues
-- Partially obfuscation
+- Partial Obfuscation
- Not work with complex component. (eg. A component with children components)
- Reason: The obfuscation marker can't locate the correct code block to obfuscate.
+ - Potential Solution: track the function/variable call stack to locate the correct code block to obfuscate.
## 💖 Sponsors
diff --git a/docs/upgrade-to-v2.md b/docs/upgrade-to-v2.md
index e959262..c4cc31d 100644
--- a/docs/upgrade-to-v2.md
+++ b/docs/upgrade-to-v2.md
@@ -36,5 +36,4 @@ We have added a new individual configuration file `next-css-obfuscator.config.cj
| ➡️ | refreshClassConversionJson |
| ➡️ | enableMarkers |
| ➡️ | removeMarkersAfterObfuscated |
-| ➡️ | customTailwindDarkModeSelector |
| ➡️ | logLevel |
diff --git a/package-lock.json b/package-lock.json
index e0f19c6..4628390 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "next-css-obfuscator",
- "version": "2.0.0-beta.23",
+ "version": "2.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "next-css-obfuscator",
- "version": "2.0.0-beta.23",
+ "version": "2.0.6",
"license": "MIT",
"dependencies": {
"css-parse": "^2.0.0",
@@ -20,6 +20,7 @@
"@types/node": "^20.8.10",
"@types/react": "^18.2.31",
"jest": "^29.7.0",
+ "prettier": "^3.2.4",
"ts-jest": "^29.1.1",
"tslib": "^2.6.2",
"typescript": "^5.0.2"
@@ -3196,6 +3197,21 @@
"node": ">=8"
}
},
+ "node_modules/prettier": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
+ "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
diff --git a/package.json b/package.json
index 05825b7..8ca6417 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,16 @@
{
"name": "next-css-obfuscator",
- "version": "2.0.6",
+ "version": "2.1.0-beta1",
"description": "A temporary solution for using postcss-obfuscator in Next.js.",
"main": "dist/index.js",
"type": "commonjs",
"scripts": {
- "build": "tsc",
+ "build": "npm run test && tsc",
"dev": "tsc -w",
"pub": "npm run build && npm publish",
- "test": "jest"
+ "test": "jest",
+ "publish": "npm run build && npm publish",
+ "publish@beta": "npm run build && npm publish --tag beta"
},
"repository": {
"type": "git",
@@ -25,6 +27,7 @@
"@types/node": "^20.8.10",
"@types/react": "^18.2.31",
"jest": "^29.7.0",
+ "prettier": "^3.2.4",
"ts-jest": "^29.1.1",
"tslib": "^2.6.2",
"typescript": "^5.0.2"
From 9da42ac4a8355ebbebc77654d212724fe67bb3c4 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 01:56:39 +0000
Subject: [PATCH 06/12] Fixed `README.md` Typos
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 1333b7f..331b371 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
Shout out to [hoangnhan2ka3](https://github.com/hoangnhan2ka3) for providing a wonderful [issue](https://github.com/soranoo/next-css-obfuscator/issues/6) report.
#### Changes:
- - Much Much Much better CSS selector obfuscation
- - Auto delete orginal CSS after obfuscation (only apply at full obfuscation)
+ - Much Much Much better quality of CSS selector obfuscation
+ - Delete original CSS automatically after obfuscation (only apply at full obfuscation)
- Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
- Support TailwindCSS Universal Selector (eg. `*:pt-4`)
- More tests
@@ -341,7 +341,7 @@ You are not expected to see this:
```css
/* example */
-/* orginal form */
+/* original form */
.text-stone-300 {
--tw-text-opacity: 1;
color: rgb(214 211 209 / var(--tw-text-opacity));
From bb81bcfdff76307d5117a69feed23d945fb2e5e3 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 10:54:21 +0000
Subject: [PATCH 07/12] Fixed Incorrect Full Obfuscation CSS Deletion Behavior
#6
---
src/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils.ts b/src/utils.ts
index ed7eed6..16ffb35 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -217,7 +217,7 @@ function replaceJsonKeysInFiles(
// Obfuscate CSS files
cssPaths.forEach((cssPath) => {
- obfuscateCss(classConversion, cssPath, enableObfuscateMarkerClasses);
+ obfuscateCss(classConversion, cssPath, !enableObfuscateMarkerClasses);
});
}
From 7c6b06c6fff9fcc113976f085827181ef61b4333 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 11:41:01 +0000
Subject: [PATCH 08/12] Added `removeOriginalCss` option
---
src/config.ts | 2 +-
src/index.ts | 1 +
src/types.ts | 2 ++
src/utils.ts | 4 +++-
4 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/config.ts b/src/config.ts
index 0905ca2..aaa6736 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -21,7 +21,7 @@ const defaultOptions: Options = {
enableMarkers: false, // Enable or disable the obfuscate marker classes.
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
-
+ removeOriginalCss: false, // Delete original CSS from CSS files if it has a obfuscated version.
logLevel: "info", // Log level
};
diff --git a/src/index.ts b/src/index.ts
index f43fa97..265f7ca 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -59,6 +59,7 @@ function obfuscate(options: Options) {
enableObfuscateMarkerClasses: options.enableMarkers,
obfuscateMarkerClasses: options.markers,
removeObfuscateMarkerClassesAfterObfuscated: options.removeMarkersAfterObfuscated,
+ removeOriginalCss: options.removeOriginalCss,
});
}
diff --git a/src/types.ts b/src/types.ts
index 5ae169c..9fe351c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -23,6 +23,7 @@ type Options = {
enableMarkers: boolean;
markers: string[];
removeMarkersAfterObfuscated: boolean;
+ removeOriginalCss: boolean;
logLevel: LogLevel;
}
@@ -47,6 +48,7 @@ type OptionalOptions = {
enableMarkers?: boolean;
markers?: string[];
removeMarkersAfterObfuscated?: boolean;
+ removeOriginalCss?: boolean;
logLevel?: LogLevel;
}
diff --git a/src/utils.ts b/src/utils.ts
index 16ffb35..6503320 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -71,6 +71,7 @@ function replaceJsonKeysInFiles(
enableObfuscateMarkerClasses,
obfuscateMarkerClasses,
removeObfuscateMarkerClassesAfterObfuscated,
+ removeOriginalCss,
}: {
targetFolder: string,
allowExtensions: string[],
@@ -85,6 +86,7 @@ function replaceJsonKeysInFiles(
enableObfuscateMarkerClasses: boolean,
obfuscateMarkerClasses: string[],
removeObfuscateMarkerClassesAfterObfuscated: boolean,
+ removeOriginalCss: boolean,
}) {
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js
@@ -217,7 +219,7 @@ function replaceJsonKeysInFiles(
// Obfuscate CSS files
cssPaths.forEach((cssPath) => {
- obfuscateCss(classConversion, cssPath, !enableObfuscateMarkerClasses);
+ obfuscateCss(classConversion, cssPath, removeOriginalCss);
});
}
From 80aacb7eb5f9fc639cf4b4c286fc69c4ef4150e3 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 14:59:03 +0000
Subject: [PATCH 09/12] Fixed Incorrect Class Extraction with Normal Attribute
& Fixed Universal Selectors #6
---
src/handlers/css.test.ts | 6 +++---
src/handlers/css.ts | 31 ++++++++++++++++++++++++++++---
2 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/src/handlers/css.test.ts b/src/handlers/css.test.ts
index 947affa..7606ccf 100644
--- a/src/handlers/css.test.ts
+++ b/src/handlers/css.test.ts
@@ -327,7 +327,7 @@ describe("extractClassFromSelector", () => {
// Assert
expect(result).toEqual({
selector: sample,
- extractedClasses: ["class1[data-attr=\"value\"]", "class2[data-attr='value']"]
+ extractedClasses: ["class1", "class2"]
});
});
@@ -436,7 +436,7 @@ describe("extractClassFromSelector", () => {
});
test("should handle Tailwind CSS [custom parameter] selector", () => {
- const sample = ".class1[100] .class2-[200]";
+ const sample = ".class1\\[100\\] .class2-\\[200\\]";
// Act
const result = extractClassFromSelector(sample);
@@ -444,7 +444,7 @@ describe("extractClassFromSelector", () => {
// Assert
expect(result).toEqual({
selector: sample,
- extractedClasses: ["class1[100]", "class2-[200]"]
+ extractedClasses: ["class1\\[100\\]", "class2-\\[200\\]"]
})
});
diff --git a/src/handlers/css.ts b/src/handlers/css.ts
index f271864..ab3ff81 100644
--- a/src/handlers/css.ts
+++ b/src/handlers/css.ts
@@ -90,7 +90,7 @@ function extractClassFromSelector(selector: string, replacementClassNames?: (str
//? "\\\.\d+" for number with ".", eg. ".ml-1\.5" the ".ml-1.5" should be in the same group, before that ".ml-1\.5" will split into ".ml-1" and ".5"
//? "\\\/\d+" for number with "/", eg. ".bg-emerald-400\/20" the ".bg-emerald-400\/20" should be in the same group, before that ".bg-emerald-400\/20" will split into ".bg-emerald-400" and "\/20"
//? "(?:\\?\[[\w\-="\\%\+\(\)]+\])?" for [attribute / Tailwind CSS custom parameter] selector
- const extractClassRegex = /(?<=[.:!\s]|(? selector.startsWith(start))) {
classes = classes.map((className) => {
+
+ // apply ignore list
if (classIgnore.includes(className)) {
return className;
}
+
+ // try to get the obfuscated selector from the selectorConversion
+ // if not found, create a new one
let obfuscatedSelector = selectorConversion[`.${className}`];
if (!obfuscatedSelector) {
const obfuscatedClass = createNewClassName(mode, className, classPrefix, classSuffix, classNameLength);
obfuscatedSelector = `.${obfuscatedClass}`;
selectorConversion[`.${className}`] = obfuscatedSelector;
}
+
+ // return the obfuscated class
return obfuscatedSelector.slice(1)
});
+
+ // obfuscate the selector
const { selector: obfuscatedSelector } = extractClassFromSelector(originalSelector, classes);
+
selectorConversion[originalSelector] = obfuscatedSelector;
}
}
@@ -310,7 +320,11 @@ function renameCssSelector(oldSelector: string, newSelector: string, cssObj: any
return cssObj;
}
-function obfuscateCss(selectorConversion: SelectorConversion, cssPath: string, replaceOriginalSelector: boolean = false) {
+function obfuscateCss(
+ selectorConversion: SelectorConversion,
+ cssPath: string,
+ replaceOriginalSelector: boolean = false
+) {
let cssContent = fs.readFileSync(cssPath, "utf-8");
let cssObj = css.parse(cssContent);
@@ -329,6 +343,12 @@ function obfuscateCss(selectorConversion: SelectorConversion, cssPath: string, r
usedKeyRegistery.add(actionSelector);
});
+ // join all universal selectors (with ">*")
+ const universalSelectors = getAllSelector(cssObj).filter((selector) => selector.includes(">*"));
+ universalSelectors.forEach((universalSelector) => {
+ usedKeyRegistery.add(universalSelector);
+ });
+
// modify css rules
usedKeyRegistery.forEach((key) => {
const originalSelectorName = key;
@@ -341,7 +361,12 @@ function obfuscateCss(selectorConversion: SelectorConversion, cssPath: string, r
}
}
});
- log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
+
+ if (replaceOriginalSelector) {
+ log("info", "CSS rules:", `Modified ${usedKeyRegistery.size} CSS rules to ${getFilenameFromPath(cssPath)}`);
+ } else {
+ log("info", "CSS rules:", `Added ${cssObj.stylesheet.rules.length - cssRulesCount} new CSS rules to ${getFilenameFromPath(cssPath)}`);
+ }
const cssOptions = {
compress: true,
From 2ade691e149812ebb06ec049f8e575782e21f075 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 16:37:47 +0000
Subject: [PATCH 10/12] Added/Merged Options
[+] `removeOriginalCss` Option
[#] Merged `includeAnyMatchRegexes` and `excludeAnyMatchRegexes` options into `whiteListedFolderPaths` and `blackListedFolderPaths` options
---
README.md | 40 +++++++++++++-------------
docs/upgrade-to-v2.md | 66 +++++++++++++++++++++----------------------
package.json | 3 +-
src/config.ts | 2 --
src/handlers/css.ts | 4 +--
src/index.ts | 11 +++++---
src/types.ts | 18 ++++++------
src/utils.ts | 27 ++++--------------
8 files changed, 79 insertions(+), 92 deletions(-)
diff --git a/README.md b/README.md
index 331b371..a10bf38 100644
--- a/README.md
+++ b/README.md
@@ -15,11 +15,15 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
#### Changes:
- Much Much Much better quality of CSS selector obfuscation
- Delete original CSS automatically after obfuscation (only apply at full obfuscation)
- - Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
- Support TailwindCSS Universal Selector (eg. `*:pt-4`)
- More tests
-### Version 2 (Major Update)
+ ### Configuration Changes:
+ - Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
+ - Merged `includeAnyMatchRegexes` and `excludeAnyMatchRegexes` options into `whiteListedFolderPaths` and `blackListedFolderPaths` options. (Directly move the regexes to the `whiteListedFolderPaths` and `blackListedFolderPaths` options)
+ - Added `removeOriginalCss` option, default to `false`. Set to `true` to delete original CSS from CSS files if it has a obfuscated version.
+
+### 💥 Version 2 (Major Update)
This version is deeply inspired by [PostCSS-Obfuscator](https://github.com/n4j1Br4ch1D/postcss-obfuscator). Shout out to [n4j1Br4ch1D](https://github.com/n4j1Br4ch1D) for creating such a great package and thank you [tremor](https://github.com/tremorlabs) for sponsoring this project.
#### Changes:
@@ -31,8 +35,8 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
- More tests
- Better CSS parsing
- #### Migration Guide:
- - [Migrate from version 1.x to 2.x](docs/upgrade-to-v2.md)
+### Migration Guide:
+- [Migrate from version 1.x to 2.x](docs/upgrade-to-v2.md)
[version 1.x README](https://github.com/soranoo/next-css-obfuscator/tree/v.1.1.0)
@@ -48,6 +52,7 @@ Give me a ⭐ if you like it.
- [How does this package work?](#how-does-this-package-work)
- [🗝️ Features](#️-features)
- [🛠️ Development Environment](#️-development-environment)
+- [📦 Requirements](#-requirements)
- [🚀 Getting Started](#-getting-started)
- [Installation](#installation)
- [Setup](#setup)
@@ -59,7 +64,7 @@ Give me a ⭐ if you like it.
- [1. Not work at Vercel after updated](#1-not-work-at-vercel-after-updated)
- [2. Lazy Setup - Obfuscate all files](#2-lazy-setup---obfuscate-all-files)
- [3. It was working normally just now, but not now?](#3-it-was-working-normally-just-now-but-not-now)
- - [4. Why are some original selectors still in the obfuscated CSS file after full obfuscation?](#4-why-are-some-original-selectors-still-in-the-obfuscated-css-file-after-full-obfuscation)
+ - [4. Why are some original selectors still in the obfuscated CSS file even the `removeOriginalCss` option is set to `true`?](#4-why-are-some-original-selectors-still-in-the-obfuscated-css-file-even-the-removeoriginalcss-option-is-set-to-true)
- [5. Why did I get a copy of the original CSS after partial obfuscation?](#5-why-did-i-get-a-copy-of-the-original-css-after-partial-obfuscation)
- [👀 Demos](#-demos)
- [⭐ TODO](#-todo)
@@ -249,13 +254,12 @@ module.exports = {
refreshClassConversionJson: false, // recommended set to true if not in production
allowExtensions: [".jsx", ".tsx", ".js", ".ts", ".html", ".rsc"],
- blackListedFolderPaths: ["./.next/cache"],
- excludeAnyMatchRegexes: [
+ blackListedFolderPaths: [
+ "./.next/cache",
/\.next\/server\/pages\/api/,
/_document..*js/,
/_app-.*/,
- ],
- customTailwindDarkModeSelector: "dm",
+ ]
};
```
@@ -273,16 +277,15 @@ It may not be the best setting but it works for me. :)
|classLength|number|5|The length of the obfuscated class name if in random mode.|
|classPrefix|string|""|The prefix of the obfuscated class name.|
|classSuffix|string|""|The suffix of the obfuscated class name.|
-|classIgnore|string[ ]|[ ]|The class names to be ignored during obfuscation.|
+|classIgnore|(string | Regex)[ ]|[ ]|The class names to be ignored during obfuscation.|
|allowExtensions|string[ ]|[".jsx", ".tsx", ".js", ".ts", ".html", ".rsc"]|The file extensions to be processed.|
|contentIgnoreRegexes|RegExp[ ]|[ ]|The regexes to match the content to be ignored during obfuscation.|
-|whiteListedFolderPaths|string[ ]|[ ]|The folder paths to be processed. Empty array means all folders will be processed.|
-|blackListedFolderPaths|string[ ]|[ ]|The folder paths to be ignored.|
-|includeAnyMatchRegexes|RegExp[ ]|[ ]|The regexes to match the file/folder paths to be processed.|
-|excludeAnyMatchRegex|RegExp[ ]|[ ]|The regexes to match the file/folder paths to be ignored.|
+|whiteListedFolderPaths|(string | Regex)[ ]|[ ]|The folder paths/Regex to be processed. Empty array means all folders will be processed.|
+|blackListedFolderPaths|(string | Regex)[ ]|[ ]|The folder paths/Regex to be ignored.|
|enableMarkers|boolean|false|Enable or disable the obfuscation markers.|
|markers|string[ ]|[ ]|Classes that indicate component(s) need to obfuscate.|
|removeMarkersAfterObfuscated|boolean|true|Remove the obfuscation markers from HTML elements after obfuscation.|
+|removeOriginalCss|boolean|false|Delete original CSS from CSS files if it has a obfuscated version. (*NOT recommended* using in partial obfuscation)
|logLevel|"debug" \| "info" \| "warn" \| "error" \| "success"| "info"|The log level.|
###### All options in one place
@@ -303,11 +306,10 @@ module.exports = {
whiteListedFolderPaths: [], // Only obfuscate files in these folders
blackListedFolderPaths: ["./.next/cache"], // Don't obfuscate files in these folders
- includeAnyMatchRegexes: [], // The regexes to match the file/folder paths to be processed.
- excludeAnyMatchRegexes: [], // The regexes to match the file/folder paths to be ignored.
enableMarkers: false, // Enable or disable the obfuscate marker classes.
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
+ removeOriginalCss: false, // Delete original CSS from CSS files if it has a obfuscated version.
logLevel: "info", // Log level
};
@@ -333,13 +335,13 @@ Enable `enableMarkers` and put the obfuscate marker class at every component inc
Your convertion table may be messed up. Try to delete the `classConversionJsonFolderPath`(default: `css-obfuscator`) folder to reset the convertion table.
-### 4. Why are some original selectors still in the obfuscated CSS file after full obfuscation?
+### 4. Why are some original selectors still in the obfuscated CSS file even the `removeOriginalCss` option is set to `true`?
In a normal situation, the package will only remove the original CSS that is related to the obfuscation and you should not see any CSS sharing the same declaration block.
You are not expected to see this:
```css
-/* example */
+/* example.css */
/* original form */
.text-stone-300 {
@@ -355,7 +357,7 @@ You are not expected to see this:
```
But this:
```css
-/* example */
+/* example.css */
/* obfuscated form */
.d8964 {
diff --git a/docs/upgrade-to-v2.md b/docs/upgrade-to-v2.md
index c4cc31d..df4ea02 100644
--- a/docs/upgrade-to-v2.md
+++ b/docs/upgrade-to-v2.md
@@ -4,36 +4,36 @@
We have added a new individual configuration file `next-css-obfuscator.config.cjs`. The old configuration in `postcss.config.cjs` was deprecated. You can use the following Table to migrate your configuration.
-| Old configuration | New configuration |
-| -------------------- | ------------------------------ |
-| enable | enabled |
-| length | classLength |
-| classMethod | mode |
-| classPrefix | classPrefix |
-| classSuffix | classSuffix |
-| classIgnore | classIgnore |
-| ids | ⛔ |
-| idMethod | ⛔ |
-| idPrefix | ⛔ |
-| idSuffix | ⛔ |
-| idIgnore | ⛔ |
-| indicatorStart | ⛔ |
-| indicatorEnd | ⛔ |
-| jsonsPath | classConversionJsonFolderPath |
-| srcPath | buildFolderPath |
-| desPath | buildFolderPath |
-| extensions | allowExtensions |
-| ➡️ | contentIgnoreRegexes |
-| formatJson | ⛔ |
-| showConfig | ⛔ |
-| keepData | ⛔ |
-| preRun | ⛔ |
-| callBack | ⛔ |
-| whiteListedPaths | whiteListedFolderPaths |
-| blackListedPaths | blackListedFolderPaths |
-| ➡️ | includeAnyMatchRegexes |
-| excludeAnyMatchRegex | excludeAnyMatchRegex |
-| ➡️ | refreshClassConversionJson |
-| ➡️ | enableMarkers |
-| ➡️ | removeMarkersAfterObfuscated |
-| ➡️ | logLevel |
+| Old configuration | New configuration |
+| -------------------- | ----------------------------- |
+| enable | enabled |
+| length | classLength |
+| classMethod | mode |
+| classPrefix | classPrefix |
+| classSuffix | classSuffix |
+| classIgnore | classIgnore |
+| ids | ⛔ |
+| idMethod | ⛔ |
+| idPrefix | ⛔ |
+| idSuffix | ⛔ |
+| idIgnore | ⛔ |
+| indicatorStart | ⛔ |
+| indicatorEnd | ⛔ |
+| jsonsPath | classConversionJsonFolderPath |
+| srcPath | buildFolderPath |
+| desPath | buildFolderPath |
+| extensions | allowExtensions |
+| ➡️ | contentIgnoreRegexes |
+| formatJson | ⛔ |
+| showConfig | ⛔ |
+| keepData | ⛔ |
+| preRun | ⛔ |
+| callBack | ⛔ |
+| whiteListedPaths | whiteListedFolderPaths |
+| blackListedPaths | blackListedFolderPaths |
+| excludeAnyMatchRegex | blackListedFolderPaths |
+| ➡️ | refreshClassConversionJson |
+| ➡️ | enableMarkers |
+| ➡️ | removeMarkersAfterObfuscated |
+| ➡️ | removeOriginalCss |
+| ➡️ | logLevel |
diff --git a/package.json b/package.json
index 8ca6417..22ea561 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,12 @@
{
"name": "next-css-obfuscator",
- "version": "2.1.0-beta1",
+ "version": "2.1.0-beta2",
"description": "A temporary solution for using postcss-obfuscator in Next.js.",
"main": "dist/index.js",
"type": "commonjs",
"scripts": {
"build": "npm run test && tsc",
"dev": "tsc -w",
- "pub": "npm run build && npm publish",
"test": "jest",
"publish": "npm run build && npm publish",
"publish@beta": "npm run build && npm publish --tag beta"
diff --git a/src/config.ts b/src/config.ts
index aaa6736..07aabd3 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -16,8 +16,6 @@ const defaultOptions: Options = {
whiteListedFolderPaths: [], // Only obfuscate files in these folders
blackListedFolderPaths: ["./.next/cache"], // Don't obfuscate files in these folders
- includeAnyMatchRegexes: [], // The regexes to match the file/folder paths to be processed.
- excludeAnyMatchRegexes: [], // The regexes to match the file/folder paths to be ignored.
enableMarkers: false, // Enable or disable the obfuscate marker classes.
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
diff --git a/src/handlers/css.ts b/src/handlers/css.ts
index ab3ff81..f61467c 100644
--- a/src/handlers/css.ts
+++ b/src/handlers/css.ts
@@ -166,7 +166,7 @@ function createSelectorConversionJson(
classNameLength?: number,
classPrefix?: string,
classSuffix?: string,
- classIgnore?: string[],
+ classIgnore?: (string | RegExp)[],
enableObfuscateMarkerClasses?: boolean,
}) {
@@ -237,7 +237,7 @@ function createSelectorConversionJson(
classes = classes.map((className) => {
// apply ignore list
- if (classIgnore.includes(className)) {
+ if (classIgnore.some(regex => new RegExp(regex).test(className))) {
return className;
}
diff --git a/src/index.ts b/src/index.ts
index 265f7ca..8aeb84d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -45,6 +45,11 @@ function obfuscate(options: Options) {
});
log("success", "Obfuscation", "Class conversion JSON created/updated");
+ if ((options.includeAnyMatchRegexes && options.includeAnyMatchRegexes.length > 0)
+ || (options.excludeAnyMatchRegexes && options.excludeAnyMatchRegexes.length > 0)) {
+ log("warn", "Obfuscation", "'includeAnyMatchRegexes' and 'excludeAnyMatchRegexes' are deprecated, please use whiteListedFolderPaths and blackListedFolderPaths instead");
+ }
+
replaceJsonKeysInFiles({
targetFolder: options.buildFolderPath,
allowExtensions: options.allowExtensions,
@@ -52,10 +57,8 @@ function obfuscate(options: Options) {
contentIgnoreRegexes: options.contentIgnoreRegexes,
- whiteListedFolderPaths: options.whiteListedFolderPaths,
- blackListedFolderPaths: options.blackListedFolderPaths,
- includeAnyMatchRegexes: options.includeAnyMatchRegexes,
- excludeAnyMatchRegexes: options.excludeAnyMatchRegexes,
+ whiteListedFolderPaths: [...options.whiteListedFolderPaths, ...(options.includeAnyMatchRegexes || [])],
+ blackListedFolderPaths: [...options.blackListedFolderPaths, ...(options.excludeAnyMatchRegexes || [])],
enableObfuscateMarkerClasses: options.enableMarkers,
obfuscateMarkerClasses: options.markers,
removeObfuscateMarkerClassesAfterObfuscated: options.removeMarkersAfterObfuscated,
diff --git a/src/types.ts b/src/types.ts
index 9fe351c..c5feb49 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -12,14 +12,14 @@ type Options = {
classLength: number;
classPrefix: string;
classSuffix: string;
- classIgnore: string[];
+ classIgnore: (string | RegExp)[];
allowExtensions: string[];
contentIgnoreRegexes: RegExp[];
- whiteListedFolderPaths: string[];
- blackListedFolderPaths: string[];
- includeAnyMatchRegexes: RegExp[];
- excludeAnyMatchRegexes: RegExp[];
+ whiteListedFolderPaths: (string | RegExp)[];
+ blackListedFolderPaths: (string | RegExp)[];
+ includeAnyMatchRegexes?: RegExp[]; //! @deprecated
+ excludeAnyMatchRegexes?: RegExp[]; //! @deprecated
enableMarkers: boolean;
markers: string[];
removeMarkersAfterObfuscated: boolean;
@@ -41,10 +41,10 @@ type OptionalOptions = {
allowExtensions?: string[];
contentIgnoreRegexes: RegExp[];
- whiteListedFolderPaths?: string[];
- blackListedFolderPaths?: string[];
- includeAnyMatchRegexes?: RegExp[];
- excludeAnyMatchRegexes?: RegExp[];
+ whiteListedFolderPaths?: (string | RegExp)[];
+ blackListedFolderPaths?: (string | RegExp)[];
+ includeAnyMatchRegexes?: RegExp[]; //! @deprecated
+ excludeAnyMatchRegexes?: RegExp[]; //! @deprecated
enableMarkers?: boolean;
markers?: string[];
removeMarkersAfterObfuscated?: boolean;
diff --git a/src/utils.ts b/src/utils.ts
index 6503320..5abd0ab 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -66,8 +66,6 @@ function replaceJsonKeysInFiles(
whiteListedFolderPaths,
blackListedFolderPaths,
- includeAnyMatchRegexes,
- excludeAnyMatchRegexes,
enableObfuscateMarkerClasses,
obfuscateMarkerClasses,
removeObfuscateMarkerClassesAfterObfuscated,
@@ -79,10 +77,8 @@ function replaceJsonKeysInFiles(
contentIgnoreRegexes: RegExp[],
- whiteListedFolderPaths: string[],
- blackListedFolderPaths: string[],
- includeAnyMatchRegexes: RegExp[],
- excludeAnyMatchRegexes: RegExp[],
+ whiteListedFolderPaths: (string | RegExp)[],
+ blackListedFolderPaths: (string | RegExp)[],
enableObfuscateMarkerClasses: boolean,
obfuscateMarkerClasses: string[],
removeObfuscateMarkerClassesAfterObfuscated: boolean,
@@ -115,25 +111,14 @@ function replaceJsonKeysInFiles(
let isTargetFile = true;
if (whiteListedFolderPaths.length > 0) {
isTargetFile = whiteListedFolderPaths.some((incloudPath) => {
- return normalizePath(filePath).includes(normalizePath(incloudPath));
+ const regex = new RegExp(incloudPath);
+ return regex.test(normalizePath(filePath));
});
}
if (blackListedFolderPaths.length > 0) {
const res = !blackListedFolderPaths.some((incloudPath) => {
- return normalizePath(filePath).includes(normalizePath(incloudPath));
- });
- if (!res) {
- isTargetFile = false;
- }
- }
- if (includeAnyMatchRegexes.length > 0) {
- isTargetFile = includeAnyMatchRegexes.some((regex) => {
- return normalizePath(filePath).match(regex);
- });
- }
- if (excludeAnyMatchRegexes.length > 0) {
- const res = !excludeAnyMatchRegexes.some((regex) => {
- return normalizePath(filePath).match(regex);
+ const regex = new RegExp(incloudPath);
+ return regex.test(normalizePath(filePath));
});
if (!res) {
isTargetFile = false;
From cc64ff07602b7b36a62f53716ed3c2347a00bb46 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Wed, 31 Jan 2024 16:45:25 +0000
Subject: [PATCH 11/12] Removed Double `Obfuscation` in Log #6
---
README.md | 1 +
src/index.ts | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a10bf38..9caa6c1 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
- Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
- Merged `includeAnyMatchRegexes` and `excludeAnyMatchRegexes` options into `whiteListedFolderPaths` and `blackListedFolderPaths` options. (Directly move the regexes to the `whiteListedFolderPaths` and `blackListedFolderPaths` options)
- Added `removeOriginalCss` option, default to `false`. Set to `true` to delete original CSS from CSS files if it has a obfuscated version.
+ - `classIgnore` option now supports Regex.
### 💥 Version 2 (Major Update)
This version is deeply inspired by [PostCSS-Obfuscator](https://github.com/n4j1Br4ch1D/postcss-obfuscator). Shout out to [n4j1Br4ch1D](https://github.com/n4j1Br4ch1D) for creating such a great package and thank you [tremor](https://github.com/tremorlabs) for sponsoring this project.
diff --git a/src/index.ts b/src/index.ts
index 8aeb84d..9aeeaba 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -98,7 +98,7 @@ function obfuscateCli() {
const config = new Config(configPath ? require(configPath) : undefined).get();
obfuscate(config);
- log("success", "Obfuscation", "Obfuscation complete");
+ log("success", "Obfuscation", "Completed~");
log("info", "Give me a ⭐️ on GitHub if you like this plugin", "https://github.com/soranoo/next-css-obfuscator");
}
From 9b51f351c710c209ad880892c5744227ee87b6d6 Mon Sep 17 00:00:00 2001
From: Freeman
Date: Thu, 1 Feb 2024 00:02:24 +0000
Subject: [PATCH 12/12] Updated Docs and Default Config
---
README.md | 52 +++++++++++++++++------
demo/next14-app-router/package-lock.json | 8 ++--
demo/next14-app-router/package.json | 4 +-
docs/imgs/banner.pdn | Bin 0 -> 439145 bytes
docs/imgs/banner.png | Bin 0 -> 228099 bytes
package.json | 18 +++-----
src/config.ts | 4 +-
7 files changed, 53 insertions(+), 33 deletions(-)
create mode 100644 docs/imgs/banner.pdn
create mode 100644 docs/imgs/banner.png
diff --git a/README.md b/README.md
index 9caa6c1..c50a0ae 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,27 @@ Project start on 30-10-2023
 [](LICENSE) [](https://github.com/soranoo/Donation)
+[](https://github.com/soranoo/next-css-obfuscator)
[](https://www.npmjs.com/package/next-css-obfuscator) [](https://www.npmjs.com/package/next-css-obfuscator)
-Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for better reading experience.
+---
+
+Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for better reading experience and latest docs. 😎
+
+---
+
### 🎉 Version 2.1.0 has NOW been released 🎉
- Shout out to [hoangnhan2ka3](https://github.com/hoangnhan2ka3) for providing a wonderful [issue](https://github.com/soranoo/next-css-obfuscator/issues/6) report.
+ Shout out to [hoangnhan2ka3](https://github.com/hoangnhan2ka3) for providing a 💪wonderful [issue](https://github.com/soranoo/next-css-obfuscator/issues/6) report and a demo site.
- #### Changes:
+ #### 📌 Changes
- Much Much Much better quality of CSS selector obfuscation
- Delete original CSS automatically after obfuscation (only apply at full obfuscation)
- Support TailwindCSS Universal Selector (eg. `*:pt-4`)
- More tests
- ### Configuration Changes:
+ #### 📌 Configuration Changes
- Removed `customTailwindDarkModeSelector` option, the dark mode selector will be automatically obfuscated at full obfuscation.
- Merged `includeAnyMatchRegexes` and `excludeAnyMatchRegexes` options into `whiteListedFolderPaths` and `blackListedFolderPaths` options. (Directly move the regexes to the `whiteListedFolderPaths` and `blackListedFolderPaths` options)
- Added `removeOriginalCss` option, default to `false`. Set to `true` to delete original CSS from CSS files if it has a obfuscated version.
@@ -27,7 +33,7 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
### 💥 Version 2 (Major Update)
This version is deeply inspired by [PostCSS-Obfuscator](https://github.com/n4j1Br4ch1D/postcss-obfuscator). Shout out to [n4j1Br4ch1D](https://github.com/n4j1Br4ch1D) for creating such a great package and thank you [tremor](https://github.com/tremorlabs) for sponsoring this project.
- #### Changes:
+ #### 📌 Changes
- Support basic partial obfuscation
- Support TailwindCSS Dark Mode
- New configuration file `next-css-obfuscator.config.cjs`
@@ -36,7 +42,7 @@ Visit the [GitHub Page](https://github.com/soranoo/next-css-obfuscator/) for bet
- More tests
- Better CSS parsing
-### Migration Guide:
+### 📚 Migration Guides
- [Migrate from version 1.x to 2.x](docs/upgrade-to-v2.md)
@@ -71,6 +77,7 @@ Give me a ⭐ if you like it.
- [⭐ TODO](#-todo)
- [🐛 Known Issues](#-known-issues)
- [💖 Sponsors](#-sponsors)
+- [🦾 Special Thanks](#-special-thanks)
- [🤝 Contributing](#-contributing)
- [📝 License](#-license)
- [☕ Donation](#-donation)
@@ -215,6 +222,9 @@ For convenience, you may update your build script to:
to make sure the build is always obfuscated and no need to run `obfuscate-build` manually.
+> [!NOTE]\
+> It is a good idea to add the `/css-obfuscator` folder to `.gitignore` to prevent the convertion table from being uploaded to the repository.
+
#### Partially obfuscate
To partially obfuscate your project, you have to add the obfuscate marker class to the components you want to obfuscate.
@@ -260,9 +270,11 @@ module.exports = {
/\.next\/server\/pages\/api/,
/_document..*js/,
/_app-.*/,
- ]
+ /__.*/, // <= maybe helpful if you are using Next.js Lcal Fonts [1*]
+ ],
};
```
+[*1] See this [comment](https://github.com/soranoo/next-css-obfuscator/issues/6#issuecomment-1919495298)
It may not be the best setting but it works for me. :)
@@ -278,11 +290,11 @@ It may not be the best setting but it works for me. :)
|classLength|number|5|The length of the obfuscated class name if in random mode.|
|classPrefix|string|""|The prefix of the obfuscated class name.|
|classSuffix|string|""|The suffix of the obfuscated class name.|
-|classIgnore|(string | Regex)[ ]|[ ]|The class names to be ignored during obfuscation.|
+|classIgnore|(string \| Regex)[ ]|[ ]|The class names to be ignored during obfuscation.|
|allowExtensions|string[ ]|[".jsx", ".tsx", ".js", ".ts", ".html", ".rsc"]|The file extensions to be processed.|
-|contentIgnoreRegexes|RegExp[ ]|[ ]|The regexes to match the content to be ignored during obfuscation.|
-|whiteListedFolderPaths|(string | Regex)[ ]|[ ]|The folder paths/Regex to be processed. Empty array means all folders will be processed.|
-|blackListedFolderPaths|(string | Regex)[ ]|[ ]|The folder paths/Regex to be ignored.|
+|contentIgnoreRegexes|RegExp[ ]|[/\.jsxs\)\("\w+"/g]|The regexes to match the content to be ignored during obfuscation.|
+|whiteListedFolderPaths|(string \| Regex)[ ]|[ ]|The folder paths/Regex to be processed. Empty array means all folders will be processed.|
+|blackListedFolderPaths|(string \| Regex)[ ]|[ ]|The folder paths/Regex to be ignored.|
|enableMarkers|boolean|false|Enable or disable the obfuscation markers.|
|markers|string[ ]|[ ]|Classes that indicate component(s) need to obfuscate.|
|removeMarkersAfterObfuscated|boolean|true|Remove the obfuscation markers from HTML elements after obfuscation.|
@@ -303,7 +315,9 @@ module.exports = {
classSuffix: "", // Suffix of the obfuscated class name.
classIgnore: [], // The class names to be ignored during obfuscation.
allowExtensions: [".jsx", ".tsx", ".js", ".ts", ".html", ".rsc"], // The file extensions to be processed.
- contentIgnoreRegexes: [], // The regexes to match the file content to be ignored during obfuscation.
+ contentIgnoreRegexes: [
+ /\.jsxs\)\("\w+"/g, // avoid accidentally obfuscate the HTML tag
+ ], // The regexes to match the file content to be ignored during obfuscation.
whiteListedFolderPaths: [], // Only obfuscate files in these folders
blackListedFolderPaths: ["./.next/cache"], // Don't obfuscate files in these folders
@@ -311,7 +325,6 @@ module.exports = {
markers: ["next-css-obfuscation"], // Classes that indicate component(s) need to obfuscate.
removeMarkersAfterObfuscated: true, // Remove the obfuscation markers from HTML elements after obfuscation.
removeOriginalCss: false, // Delete original CSS from CSS files if it has a obfuscated version.
-
logLevel: "info", // Log level
};
```
@@ -377,6 +390,7 @@ Since the original CSS may referenced by other components not included in the ob
1. [Next 14 App Router](https://github.com/soranoo/next-css-obfuscator/tree/main/demo/next14-app-router)
2. [Next 14 App Router Partially Obfuscated](https://github.com/soranoo/next-css-obfuscator/tree/main/demo/next14-app-router-partially-obfuscated)
+3. [hoangnhan.co.uk](https://hoangnhan.co.uk/) (BY [hoangnhan2ka3](https://github.com/hoangnhan2ka3))
## ⭐ TODO
@@ -408,6 +422,18 @@ Since the original CSS may referenced by other components not included in the ob
#### Individuals (0)
+## 🦾 Special Thanks
+
+
## 🤝 Contributing
Contributions are welcome! If you find a bug or have a feature request, please open an issue. If you want to contribute code, please fork the repository and run `npm run test` before submit a pull request.
diff --git a/demo/next14-app-router/package-lock.json b/demo/next14-app-router/package-lock.json
index 22e695e..6b95f13 100644
--- a/demo/next14-app-router/package-lock.json
+++ b/demo/next14-app-router/package-lock.json
@@ -25,7 +25,7 @@
"cross-env": "^7.0.3",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
- "next-css-obfuscator": "^2.0.0",
+ "next-css-obfuscator": "^2.1.0-beta2",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"prettier": "^3.1.0",
@@ -3538,9 +3538,9 @@
}
},
"node_modules/next-css-obfuscator": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/next-css-obfuscator/-/next-css-obfuscator-2.0.0.tgz",
- "integrity": "sha512-74GgfbuB0wSpWynauAotQhZfJs2upI+HbPcjs5hBVN3kcZelolWjrdJsiGN+SdB/CBGy1gidjLSW87fCSVT7PA==",
+ "version": "2.1.0-beta2",
+ "resolved": "https://registry.npmjs.org/next-css-obfuscator/-/next-css-obfuscator-2.1.0-beta2.tgz",
+ "integrity": "sha512-w9+nNi/9dGCubqgCDOBOTNPZEKGpjl+xYnc3zjTm0ho91vTiIhFO3pvnPwAoJ0HhAsIJ+3N1nSCudctlaVo/kQ==",
"dev": true,
"dependencies": {
"css-parse": "^2.0.0",
diff --git a/demo/next14-app-router/package.json b/demo/next14-app-router/package.json
index 375d142..a1ccc7a 100644
--- a/demo/next14-app-router/package.json
+++ b/demo/next14-app-router/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "next build && npm run obfuscate-build",
+ "build": "next build ",
"dev": "next dev",
"lint": "next lint",
"start": "next start",
@@ -28,7 +28,7 @@
"cross-env": "^7.0.3",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
- "next-css-obfuscator": "^2.0.0",
+ "next-css-obfuscator": "^2.1.0-beta2",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"prettier": "^3.1.0",
diff --git a/docs/imgs/banner.pdn b/docs/imgs/banner.pdn
new file mode 100644
index 0000000000000000000000000000000000000000..d6f81a1ac6556c1a53f19847a6fc22bdf553c249
GIT binary patch
literal 439145
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