-
-
Notifications
You must be signed in to change notification settings - Fork 3
feat-support-tailwindcss-v4 #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Moves test files from the `src/handlers` directory to the `src/__tests__` directory for better organization and separation of concerns. This change improves the project structure and makes it easier to locate and manage test files. Also, updates import paths in test files.
Moves the test file from the `src` directory to the `__tests__` directory for better organization and separation of concerns. This change improves the project structure and makes it easier to locate test files.
Migrates the project's code formatting and linting from Prettier to Biome. This change removes the existing Prettier configuration and implements Biome. The Biome configuration enforces consistent code style and catches potential issues, improving code quality and maintainability.
Upgrades the core CSS transformation engine to `css-seasoning` and `lightningcss-wasm`. - Replaces PostCSS with `css-seasoning` for CSS parsing and transformation. - Integrates `lightningcss-wasm` for CSS minification and optimization. - Introduces a new `obfuscateCssFiles` function for processing CSS files. - Removes the deprecated `classLength` option. - Improves the way of css obfuscation with new logic and features. This change significantly enhances the performance and flexibility of the CSS obfuscation process.
Refactors the CSS obfuscation process to leverage the `css-seasoning` library, replacing custom CSS parsing and manipulation logic. This change improves maintainability and leverages a dedicated library for CSS transformations. Removes the old CSS test file as the functionality is now covered by `css-seasoning`.
Refactors utility functions for better organization and reusability. - Removes unused random string generation and string simplification functions. - Exports previously internal functions to enhance module accessibility and facilitate testing. - Streamlines the codebase by removing dead code and improving overall structure.
Introduces breaking changes to support TailwindCSS 4, nested CSS, and CSS ident obfuscation. - Updates configuration options, including renaming, merging, and removing deprecated options. - Migrates to css-seasoning v1.4.1. - Enables JS AST parsing by default. - Updates documentation with v3 migration guide.
Sets up semantic release to automate the release process. This includes: - Adding a release workflow file. - Installing semantic-release and related plugins. - Creating a release configuration file. This automates versioning, changelog generation, and package publishing.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis update introduces a major new release (version 3) with significant changes across configuration, code structure, and tooling. The CSS obfuscation logic is refactored to use external libraries, replacing custom implementations and removing deprecated features. The configuration system is updated with new options ( Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI/API
participant Config
participant CSSHandler
participant Utils
participant Output
User->>CLI/API: Run obfuscation (with options)
CLI/API->>Config: Merge options with defaults
CLI/API->>CSSHandler: obfuscateCssFiles({options})
CSSHandler->>Utils: findAllFilesWithExt('.css', buildFolder, filters)
Utils-->>CSSHandler: List of CSS files
CSSHandler->>CSSHandler: For each CSS file: obfuscateCss({file, options})
CSSHandler->>Output: Write obfuscated CSS, update conversion tables
CSSHandler-->>CLI/API: Return merged conversion tables
CLI/API->>Utils: replaceJsonKeysInFiles({conversionTables, ...})
Utils->>Output: Write updated files (HTML/JS/JSON)
CLI/API->>Output: Save conversion.json
Poem
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🔭 Outside diff range comments (2)
README.md (1)
11-12
:⚠️ Potential issueRemove outdated Tailwind CSS 4 caution message.
The caution message stating that the package doesn't support TailwindCSS 4 should be removed since v3 now adds this support, as mentioned in the release notes below.
->[!CAUTION]\ -> This package has no support for TailwindCSS 4 yet, but is under development, and hopefully will be able to release within 1-2 months.🧰 Tools
🪛 LanguageTool
[typographical] ~12-~12: If specifying a range, consider using an en dash instead of a hyphen.
Context: ...opefully will be able to release within 1-2 months. --- Visit the [GitHub Page](h...(HYPHEN_TO_EN)
src/handlers/html.ts (1)
118-125
:⚠️ Potential issueAvoid blindly stripping the first character after
cssUnescape
cssUnescape(key).slice(1)
assumes every selector key starts with"."
.
If a consumer ever pushes an id selector (#foo
) or a non‑prefixed key into the
conversion table, the first character will be dropped, corrupting the lookup
string and silently skipping obfuscation.-const className = cssUnescape(key).slice(1); +const unescaped = cssUnescape(key); +const className = unescaped.startsWith(".") + ? unescaped.slice(1) // `.foo` → `foo` + : unescaped; // `#bar` / `foo` stay intactThis keeps today’s behaviour for real class‑selectors while remaining robust
for future selector types.
🧹 Nitpick comments (10)
.github/workflows/release.yml (1)
1-33
: Add GitHub Actions release workflow
This workflow runs on pushes tomain
andbeta
, builds the package, and invokessemantic-release
with the appropriate tokens. To further optimize CI duration, consider adding a step to cache thenode_modules
directory between runs.biome.json (1)
1-69
: Well-structured Biome configuration for consistent code formatting and linting.The Biome configuration is comprehensive and covers formatting, linting, and JavaScript-specific rules. The setup includes appropriate ignore patterns for node_modules and build directories, and enforces good practices like explicit typing and proper error handling.
A few suggestions to consider:
Consider moving the
complexity.noForEach
rule to a separate line without a trailing comma to maintain consistent formatting with other rule sections:"complexity": { "noUselessTypeConstraint": "error", "useLiteralKeys": "error", "useOptionalChain": "error", - "noForEach": "off", + "noForEach": "off" },README.md (1)
29-45
: Grammar issues in feature and configuration changes lists.There are several grammatical issues in the feature and configuration changes sections.
Fix the grammatical issues with "not longer" which should be "no longer":
- - Default `generatorSeed` not longer fixed to `-1`, but a random string. - - `simplify-seedable` mode is not longer supported. Use `random` mode instead. - - Deprecated `classLength` option, not longer supported. - - Not longer preserve TailwindCSS dark mode class names (ie `.dark`). Add the dark mode class name to the `ignorePatterns.selectors` option to preserve it. + - Default `generatorSeed` is no longer fixed to `-1`, but a random string. + - `simplify-seedable` mode is no longer supported. Use `random` mode instead. + - Deprecated `classLength` option, no longer supported. + - No longer preserves TailwindCSS dark mode class names (ie `.dark`). Add the dark mode class name to the `ignorePatterns.selectors` option to preserve it.🧰 Tools
🪛 LanguageTool
[style] ~33-~33: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...lwindCSS 4. - Support nested CSS. - Support CSS idents obfuscation. #### 📌 Conf...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[grammar] ~38-~38: Did you mean the adverb “no longer”?
Context: ...by default. - DefaultgeneratorSeed
not longer fixed to-1
, but a random string. -...(NOT_LONGER)
[grammar] ~39-~39: Did you mean the adverb “no longer”?
Context: ...string. -simplify-seedable
mode is not longer supported. Userandom
mode instead. ...(NOT_LONGER)
[grammar] ~41-~41: Did you mean the adverb “no longer”?
Context: ...d. - DeprecatedclassLength
option, not longer supported. - AddedignorePatterns
o...(NOT_LONGER)
[grammar] ~43-~43: Did you mean the adverb “no longer”?
Context: ... that match the regexes or strings. - Not longer preserve TailwindCSS dark mode class na...(NOT_LONGER)
🪛 markdownlint-cli2 (0.17.2)
29-29: Headings must start at the beginning of the line
null(MD023, heading-start-left)
31-31: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
32-32: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
33-33: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
35-35: Headings must start at the beginning of the line
null(MD023, heading-start-left)
37-37: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
38-38: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
39-39: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
40-40: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
41-41: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
42-42: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
43-43: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
44-44: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
45-45: Unordered list indentation
Expected: 0; Actual: 2(MD007, ul-indent)
src/handlers/js.ts (1)
97-128
: Refactored to arrow function with inline export and improved formatting.The
obfuscateJs
function has been refactored to an arrow function with inline export. There's a minor formatting issue with a line break in the parameter list that could be improved.Consider keeping the entire function signature on the same line or aligning parameters more consistently:
-export const obfuscateJs = (content: string, key: string, selectorCoversion: SelectorConversion - , filePath: string, contentIgnoreRegexes: RegExp[] = [], useAst: boolean = false) => { +export const obfuscateJs = ( + content: string, + key: string, + selectorCoversion: SelectorConversion, + filePath: string, + contentIgnoreRegexes: RegExp[] = [], + useAst: boolean = false +) => {src/config.ts (1)
3-56
: Updated configuration defaults with new options and deprecation markers.The configuration has been updated to include new options (
prefix
,suffix
,ignorePatterns
) and properly mark deprecated options with JSDoc comments. The default generator seed is nowundefined
for random seed generation, andenableJsAst
is enabled by default.There are a few grammatical issues in the deprecation comments:
Fix the grammar in the deprecation comments:
- /** - * @deprecated Not longer used from v3.0.0 and will be removed in the next major version. - */ + /** + * @deprecated No longer used from v3.0.0 and will be removed in the next major version. + */This correction should be applied to all similar deprecation comments.
src/handlers/html.ts (1)
90-93
: Guard against double‑escaping attribute values
htmlEscape
converts"
to"
. Because the result is already wrapped in
double‑quotes ("${...}"
), downstream consumers will see"
inside
another pair of quotes, rendering the literal"
in HTML.
If the original attribute value actually contains quotes we want that escaping,
otherwise it is redundant.Consider escaping only when needed:
- modifiedHtml += ` ${key}="${htmlEscape(attribs[key])}"`; + const value = attribs[key]; + modifiedHtml += ` ${key}="${/[<>"&]/.test(value) ? htmlEscape(value) : value}"`;This prevents inadvertent visual artefacts in the rendered markup.
src/index.ts (2)
58-61
: Remove stray debugconsole.log
Debug prints left in production code clutter stdout and can leak paths in CI
logs. The logging utility already provides adebug
level.- console.log({ jsonPath });
58-61
: Ensure the output directory exists before writing the JSONIf the user supplies a relative
classConversionJsonFolderPath
, the folder is
created insideobfuscateCssFiles
, but when an absolute path is given,
path.join(process.cwd(), …)
collapses to that absolute path and the directory
may not exist locally.+ fs.mkdirSync(path.dirname(jsonPath), { recursive: true }); fs.writeFileSync(jsonPath, JSON.stringify(conversionTables, null, 2));
Adds a one‑liner guard that prevents
ENOENT: no such file or directory
.src/utils.ts (2)
263-265
: Dead code & unused variable
cssPaths
is populated but the invocation that used it is commented out. Either restore CSS handling here or delete the variable and the commented block to keep the codebase clean.
148-149
: Mutation of sharedconversionTables
may have side‑effects
conversionTables.selectors[cssEscape(...)] = ""
directly mutates the object passed in from callers. Consider cloning or documenting this side‑effect to avoid unexpected leaks between processing steps.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (23)
.github/workflows/release.yml
(1 hunks).prettierignore
(0 hunks).prettierrc
(0 hunks)README.md
(11 hunks)biome.json
(1 hunks)docs/upgrade-to-v3.md
(1 hunks)package.json
(4 hunks)release.config.cjs
(1 hunks)src/__tests__/html.test.ts
(1 hunks)src/__tests__/js-ast.test.ts
(1 hunks)src/__tests__/js.test.ts
(1 hunks)src/__tests__/utils.test.ts
(1 hunks)src/config.ts
(1 hunks)src/handlers/css.test.ts
(0 hunks)src/handlers/css.ts
(1 hunks)src/handlers/html.ts
(4 hunks)src/handlers/js-ast.ts
(5 hunks)src/handlers/js.test.ts
(0 hunks)src/handlers/js.ts
(5 hunks)src/index.ts
(3 hunks)src/types.ts
(1 hunks)src/utils.ts
(18 hunks)tsconfig.json
(2 hunks)
💤 Files with no reviewable changes (4)
- .prettierrc
- .prettierignore
- src/handlers/js.test.ts
- src/handlers/css.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/__tests__/js.test.ts (1)
src/handlers/js.ts (1)
searchForwardComponent
(14-26)
src/handlers/js-ast.ts (1)
src/types.ts (1)
SelectorConversion
(5-5)
src/config.ts (2)
src/types.ts (2)
Options
(7-50)OptionalOptions
(52-77)src/index.ts (1)
OptionalOptions
(116-116)
src/handlers/js.ts (1)
src/types.ts (1)
SelectorConversion
(5-5)
src/index.ts (3)
src/types.ts (1)
Options
(7-50)src/handlers/css.ts (1)
obfuscateCssFiles
(113-192)src/utils.ts (3)
log
(58-86)getFilenameFromPath
(329-332)replaceJsonKeysInFiles
(114-266)
src/utils.ts (1)
src/types.ts (2)
LogLevel
(3-3)SelectorConversion
(5-5)
🪛 GitHub Actions: Tests
src/__tests__/html.test.ts
[error] 24-24: Test failure in 'obfuscateHtmlClassNames > should obfuscate class names correctly': expected '
' to deeply equal ''.[error] 38-38: Test failure in 'obfuscateHtmlClassNames > should handle nested tags with obfuscate class': expected '
[error] 65-65: Test failure in 'obfuscateHtmlClassNames > should handle script tags': expected obfuscated script content to include class names 'a b' but received empty class names.
[error] 78-78: Test failure in 'obfuscateHtmlClassNames > should handle void tags': expected '' but received '
'.
[error] 92-92: Test failure in 'obfuscateHtmlClassNames > should handle comments': expected '
' but received ''.[error] 131-131: Test failure in 'obfuscateHtmlClassNames > should handle HTML with multiple classes in one element': expected '
' but received ''.[error] 144-144: Test failure in 'obfuscateHtmlClassNames > should handle HTML with nested structures and multiple classes': expected nested elements with classes 'a', 'b', 'c' but received empty class attributes.
[error] 158-158: Test failure in 'obfuscateHtmlClassNames > should handle HTML with obfuscate marker class': expected nested spans with class 'a' but received empty class attributes.
[error] 172-172: Test failure in 'obfuscateHtmlClassNames > should handle HTML with multiple classes and obfuscate marker class': expected '
' but received ''.[error] 185-185: Test failure in 'obfuscateHtmlClassNames > should handle HTML instruction': expected '
' but received ''.[error] 201-201: Test failure in 'obfuscateHtmlClassNames > should handle double quot inside double quot': expected div with data-opts and class 'a' but received empty class attribute.
🪛 LanguageTool
README.md
[style] ~33-~33: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...lwindCSS 4. - Support nested CSS. - Support CSS idents obfuscation. #### 📌 Conf...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[grammar] ~38-~38: Did you mean the adverb “no longer”?
Context: ...by default. - Default generatorSeed
not longer fixed to -1
, but a random string. -...
(NOT_LONGER)
[grammar] ~39-~39: Did you mean the adverb “no longer”?
Context: ...string. - simplify-seedable
mode is not longer supported. Use random
mode instead. ...
(NOT_LONGER)
[grammar] ~41-~41: Did you mean the adverb “no longer”?
Context: ...d. - Deprecated classLength
option, not longer supported. - Added ignorePatterns
o...
(NOT_LONGER)
[grammar] ~43-~43: Did you mean the adverb “no longer”?
Context: ... that match the regexes or strings. - Not longer preserve TailwindCSS dark mode class na...
(NOT_LONGER)
[style] ~207-~207: Consider using a less frequent alternative to set your writing apart from others and make it sound more professional.
Context: ... } satisfies Options; ``` Feel free to checkout [📖 Config Options Reference](...
(FEEL_FREE_TO_STYLE_ME)
[grammar] ~207-~207: The word “to” is a noun. The verb is spelled with a space.
Context: ...es Options; ``` Feel free to checkout [📖 Config Options Reference](#-config-...
(NOUN_VERB_CONFUSION)
🪛 markdownlint-cli2 (0.17.2)
README.md
21-21: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
25-25: Blank line inside blockquote
null
(MD028, no-blanks-blockquote)
29-29: Headings must start at the beginning of the line
null
(MD023, heading-start-left)
31-31: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
32-32: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
33-33: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
35-35: Headings must start at the beginning of the line
null
(MD023, heading-start-left)
37-37: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
38-38: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
39-39: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
40-40: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
41-41: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
42-42: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
43-43: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
44-44: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
45-45: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
179-179: Headings must start at the beginning of the line
null
(MD023, heading-start-left)
330-330: Tables should be surrounded by blank lines
null
(MD058, blanks-around-tables)
🔇 Additional comments (20)
release.config.cjs (1)
1-23
: Introduce semantic-release configuration
The newrelease.config.cjs
correctly defines themain
andbeta
branches and configures the essential plugins for conventional commit analysis, release notes generation, npm publishing from thedist
directory, and Git integration with CI skipping. This setup aligns well with your automated release goals.tsconfig.json (1)
16-27
: Exclude config and test files from TypeScript compilation
Adding"newLine": "LF"
and excludingsrc/*.test.*
along with the newrelease.config.cjs
ensures the TS compiler ignores non‑source files and maintains consistent line endings.src/__tests__/js-ast.test.ts (1)
6-6
: Update import paths for AST handlers
The import ofsearchStringLiterals
andobfuscateJsWithAst
from../handlers/js-ast
reflects the refactor to arrow‑exported functions and new module layout.src/__tests__/html.test.ts (1)
6-7
: Switch to new HTML obfuscation handler
Replacing the deprecatedfindHtmlTagContentsByClass
import withobfuscateHtmlClassNames
from../handlers/html
aligns the tests with your refactored HTML handler API. Test cases cover a broad set of scenarios.src/__tests__/utils.test.ts (1)
2-6
: Imports updated to match simplified utils module.The imports have been correctly updated to match the simplified utils module, where the random string generation and string simplification functions have been removed. This aligns with the broader refactoring to use external libraries instead of custom implementations.
package.json (3)
3-3
: Version bump appropriately indicates major breaking changes.The version bump from 2.x to 3.0.0-b1 correctly follows semantic versioning for a major release with breaking changes. The beta tag indicates this is a pre-release version.
14-15
: Good addition of modern tooling scripts.Adding scripts for linting with Biome and semantic-release automation improves code quality and release management capabilities.
39-50
: Good modernization of dev tooling.The addition of Biome for linting and semantic-release for automated versioning and publishing aligns with modern JavaScript development practices and will improve code quality and release workflows.
src/handlers/js-ast.ts (2)
17-67
: Clean refactoring to arrow function expression.The
obfuscateJsWithAst
function has been refactored from a function declaration to an exported arrow function expression. This is a good stylistic change that aligns with modern JavaScript/TypeScript practices while preserving the exact same functionality.
77-369
: Clean refactoring ofsearchStringLiterals
to arrow function expression.Similar to the
obfuscateJsWithAst
function,searchStringLiterals
has been refactored to use an arrow function expression with inline export. The change is purely syntactic and doesn't alter the function's behavior, maintaining the robust AST traversal logic.src/__tests__/js.test.ts (1)
1-126
: Well-structured comprehensive test suite forsearchForwardComponent
.This new test suite thoroughly tests the
searchForwardComponent
function with a variety of scenarios:
- Standard component detection
- Multiple component handling
- Empty input cases
- Special character handling
- String literal exclusion
- Nested JSX calls
Each test follows a clear Arrange-Act-Assert pattern with descriptive test names and good coverage of edge cases.
docs/upgrade-to-v3.md (1)
1-21
: Clear and informative migration guide for v3.The migration guide effectively communicates breaking changes and provides a detailed comparison table of old vs. new configuration options. The explanations are clear and provide actionable guidance for users.
README.md (4)
21-28
: Clear version 3 announcement with proper migration instructions.The announcement of version 3 with the important notice and migration tips is well structured and informative.
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)
21-21: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3(MD001, heading-increment)
25-25: Blank line inside blockquote
null(MD028, no-blanks-blockquote)
359-371
: Clear deprecation notices for renamed configuration options.The deprecation notices and comments for the renamed configuration options are clear and consistent with the migration guide.
372-382
: New configuration options with clear documentation.The new
prefix
,suffix
, andignorePatterns
options are well documented with appropriate descriptions.
443-457
: Updated CSS example showing the new obfuscation style.The CSS example has been updated to show the new obfuscation style which now includes CSS ident obfuscation (using variable names like
--d89645
).src/handlers/js.ts (3)
14-26
: Refactored to arrow function with inline export.The
searchForwardComponent
function has been refactored from a function declaration to an arrow function with inline export. This modernization improves code style consistency.
28-41
: Refactored to non-exported arrow function.The
searchComponent
function has been refactored to an arrow function while correctly remaining non-exported.
43-95
: Refactored to arrow function with inline export.The
obfuscateForwardComponentJs
function has been refactored to an arrow function with inline export, maintaining the same functionality but with modern syntax.src/config.ts (1)
58-75
: Refactored Config class with improved formatting.The
Config
class has been refactored with improved formatting and consistent style.
"css-seasoning": "^1.4.1", | ||
"html-escaper": "^3.0.3", | ||
"htmlparser2": "^10.0.0", | ||
"recoverable-random": "^1.0.5", | ||
"lightningcss-wasm": "^1.29.3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify Tailwind v4 compatibility with new CSS dependencies.
The PR title mentions Tailwind v4 support, but there's no explicit reference to it in the package.json. The new CSS libraries (css-seasoning
and lightningcss-wasm
) may be needed for Tailwind v4 compatibility.
🌐 Web query:
Is css-seasoning and lightningcss-wasm compatible with Tailwind CSS v4? What are the requirements for supporting Tailwind CSS v4?
💡 Result:
Compatibility of css-seasoning and lightningcss-wasm with Tailwind CSS v4
There is no direct mention of "css-seasoning" or "lightningcss-wasm" in the official documentation or community discussions for Tailwind CSS v4 as of April 2025. However, based on the major changes and requirements of Tailwind CSS v4, we can provide an informed assessment.
lightningcss-wasm
- Tailwind CSS v4 uses Lightning CSS as its core engine. This is publicly stated in the v4 release announcements and documentation: "We've integrated Lightning CSS directly into the engine"[2][3]. This means that the processing and transformation of CSS in Tailwind v4 are powered by Lightning CSS, which itself is written in Rust and often distributed as a WASM (WebAssembly) package for JavaScript environments.
- Compatibility: If your workflow or tool uses
lightningcss-wasm
for processing CSS, it should, in theory, be compatible with Tailwind CSS v4, since Lightning CSS is now the processing backend for Tailwind. There may be some caveats depending on how tightly you need to couple your tools, but out-of-the-box, Tailwind v4 leverages Lightning CSS’s features, so there should be no fundamental incompatibility[2][3]. - Note: If you’re aiming for low-level manipulation or integration, ensure you’re using compatible versions of Lightning CSS, as Tailwind v4 may bundle or depend on a specific version.
css-seasoning
- No Official Reference: There is no reference to a tool or package named “css-seasoning” in the official Tailwind CSS v4 documentation, release notes, or upgrade guides[1][3][6].
- Inference: If "css-seasoning" is a custom tool or third-party library that processes or transforms CSS independently, its compatibility with Tailwind CSS v4 will depend on:
- Its support for modern CSS features (e.g.,
@property
,color-mix()
, cascade layers). - Its ability to work with Lightning CSS-processed output, which may include newer CSS syntax and constructs[3][6].
- Its support for modern CSS features (e.g.,
- Recommendation: Test your “css-seasoning” integration in a development environment with Tailwind v4 output. If it does not support the latest CSS features, updates or patches may be necessary.
Requirements for Supporting Tailwind CSS v4
Environment Requirements
- Node.js 20+ is required to install and upgrade projects to Tailwind CSS v4[6].
- Modern Browser Support: Tailwind v4 targets only modern browsers: Safari 16.4+, Chrome 111+, and Firefox 128+ because it relies on cutting-edge CSS features[3][6][10].
- Build Tooling: Tailwind v4 can be used with modern build tools like Vite and integrates with frameworks such as React, Next.js, and Vue[4][7].
Key Technical Requirements
Requirement | Details |
---|---|
CSS Engine | Uses Lightning CSS for all processing |
Modern CSS Features | Relies on cascade layers, @property , color-mix() , CSS variables |
Configuration | CSS-first configuration; customize via CSS, not JS files |
File Importation | Uses @import "tailwindcss"; instead of old @tailwind directives |
Browser Compatibility | Only works in modern browsers (see above); limited support for older |
Node.js Version | Node.js 20 or higher |
Deprecated Utilities | Some legacy utilities removed—check upgrade guides |
Migration & Upgrade
- Use the official upgrade tool (
npx @tailwindcss/upgrade
), which handles most migration steps automatically[6][7]. - Review any third-party tools or custom workflow steps for compatibility with modern CSS features and Lightning CSS output.
Summary Table: Compatibility Overview
Tool/Library | Compatible with Tailwind CSS v4? | Notes |
---|---|---|
lightningcss-wasm | Yes (core engine) | Directly integrated in v4; version compatibility may matter |
css-seasoning | Unclear/Unknown | Test with v4 output; must support modern CSS and Lightning CSS |
Conclusion
- lightningcss-wasm is compatible and in fact fundamental to Tailwind CSS v4, as the framework uses Lightning CSS as its processing engine[2][3].
- css-seasoning compatibility is uncertain—ensure it supports modern CSS features and can operate with the output of Lightning CSS.
- Supporting Tailwind CSS v4 requires modern JavaScript (Node.js 20+), modern browsers, and migration to CSS-first configuration using new import syntax and features[3][6][7][10].
Tailwind CSS v4 is a significant step forward in terms of tooling and modern web standards—ensure your other CSS tools are similarly up to date.
Citations:
- 1: https://tailwindcss.com/docs/compatibility
- 2: https://tailwindcss.com/blog/tailwindcss-v4-alpha
- 3: https://tailwindcss.com/blog/tailwindcss-v4
- 4: https://tailkits.com/blog/tailwind-css-v4-updates-features/
- 5: https://laravel-news.com/tailwind-css-v4-is-now-released
- 6: https://tailwindcss.com/docs/upgrade-guide
- 7: https://dev.to/utkarshthedev/tailwind-css-v40-quick-guide-2bh5
- 8: The Web is for everyone, not just people with the latest iPhone. tailwindlabs/tailwindcss#17243
- 9: https://labs.thisdot.co/blog/the-css-utility-hybrid-approach-with-tailwind-v4
- 10: https://tailwindcss.com/blog/tailwindcss-v4-1
Double‑check Tailwind CSS v4 compatibility
- lightningcss‑wasm: ✅ Tailwind v4 uses Lightning CSS as its core engine, so your existing lightningcss‑wasm
dependency should be compatible.
- css‑seasoning: @property
, color‑mix()
) and can process Lightning CSS output.
- Tailwind CSS itself: there’s no tailwindcss@^4.x
entry in package.json—add "tailwindcss": "^4.0.0"
under dependencies if you haven’t already.
- Environment requirements: ensure your build uses Node.js 20+ and a CSS‑first config (@import "tailwindcss";
) targeting modern browsers (Safari 16.4+, Chrome 111+, Firefox 128+).
ignorePatterns: { | ||
...options.ignorePatterns, | ||
selectors: [ | ||
...options.ignorePatterns?.selectors || [], | ||
...(options.classIgnore || []), | ||
] | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle options.ignorePatterns
being undefined
to avoid a runtime crash
Spreading undefined
({ ...options.ignorePatterns }
) throws
TypeError: Cannot convert undefined or null to object
.
- ignorePatterns: {
- ...options.ignorePatterns,
+ ignorePatterns: {
+ ...(options.ignorePatterns ?? {}),
selectors: [
- ...options.ignorePatterns?.selectors || [],
+ ...(options.ignorePatterns?.selectors ?? []),
...(options.classIgnore || []),
]
},
Using the nullish‑coalescing operator guarantees we always spread an object.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
ignorePatterns: { | |
...options.ignorePatterns, | |
selectors: [ | |
...options.ignorePatterns?.selectors || [], | |
...(options.classIgnore || []), | |
] | |
}, | |
ignorePatterns: { | |
...(options.ignorePatterns ?? {}), | |
selectors: [ | |
...(options.ignorePatterns?.selectors ?? []), | |
...(options.classIgnore || []), | |
] | |
}, |
src/types.ts
Outdated
export type OptionalOptions = { | ||
enable?: boolean; | ||
mode?: obfuscateMode; | ||
buildFolderPath?: string; | ||
classConversionJsonFolderPath?: string; | ||
refreshClassConversionJson?: boolean; | ||
|
||
classLength?: number; | ||
classPrefix?: string; | ||
classSuffix?: string; | ||
classIgnore?: string[]; | ||
allowExtensions?: string[]; | ||
contentIgnoreRegexes: RegExp[]; | ||
|
||
whiteListedFolderPaths?: (string | RegExp)[]; | ||
blackListedFolderPaths?: (string | RegExp)[]; | ||
enableMarkers?: boolean; | ||
markers?: string[]; | ||
removeMarkersAfterObfuscated?: boolean; | ||
removeOriginalCss?: boolean; | ||
generatorSeed?: string; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OptionalOptions
should mark every field as optional & include new ones
contentIgnoreRegexes
is currently required, contradicting the type’s intent
and breaking existing configs. ignorePatterns
, prefix
, and suffix
(available in Options
) are also missing.
export type OptionalOptions = {
@@
- classIgnore?: string[];
- allowExtensions?: string[];
- contentIgnoreRegexes: RegExp[];
+ prefix?: string;
+ suffix?: string;
+
+ classIgnore?: (string | RegExp)[];
+ ignorePatterns?: TransformProps["ignorePatterns"];
+
+ allowExtensions?: string[];
+ contentIgnoreRegexes?: RegExp[];
This realigns OptionalOptions
with its canonical purpose and prevents TS
compilation errors for users upgrading to v4.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export type OptionalOptions = { | |
enable?: boolean; | |
mode?: obfuscateMode; | |
buildFolderPath?: string; | |
classConversionJsonFolderPath?: string; | |
refreshClassConversionJson?: boolean; | |
classLength?: number; | |
classPrefix?: string; | |
classSuffix?: string; | |
classIgnore?: string[]; | |
allowExtensions?: string[]; | |
contentIgnoreRegexes: RegExp[]; | |
whiteListedFolderPaths?: (string | RegExp)[]; | |
blackListedFolderPaths?: (string | RegExp)[]; | |
enableMarkers?: boolean; | |
markers?: string[]; | |
removeMarkersAfterObfuscated?: boolean; | |
removeOriginalCss?: boolean; | |
generatorSeed?: string; | |
export type OptionalOptions = { | |
enable?: boolean; | |
mode?: obfuscateMode; | |
buildFolderPath?: string; | |
classConversionJsonFolderPath?: string; | |
refreshClassConversionJson?: boolean; | |
classLength?: number; | |
classPrefix?: string; | |
classSuffix?: string; | |
prefix?: string; | |
suffix?: string; | |
classIgnore?: (string | RegExp)[]; | |
ignorePatterns?: TransformProps["ignorePatterns"]; | |
allowExtensions?: string[]; | |
contentIgnoreRegexes?: RegExp[]; | |
whiteListedFolderPaths?: (string | RegExp)[]; | |
blackListedFolderPaths?: (string | RegExp)[]; | |
enableMarkers?: boolean; | |
markers?: string[]; | |
removeMarkersAfterObfuscated?: boolean; | |
removeOriginalCss?: boolean; | |
generatorSeed?: string; | |
}; |
// If no output path is provided, use the input path | ||
outCssPath = cssPath; | ||
} else if (!fs.existsSync(path.dirname(outCssPath))) { | ||
// Create the output directory if it doesn't exist | ||
fs.mkdirSync(path.dirname(outCssPath)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create output directory recursively
fs.mkdirSync(path.dirname(outCssPath));
will throw if the path contains multiple non‑existent nested folders. Use the recursive: true
flag to make the call idempotent and resilient.
- fs.mkdirSync(path.dirname(outCssPath));
+ fs.mkdirSync(path.dirname(outCssPath), { recursive: true });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// If no output path is provided, use the input path | |
outCssPath = cssPath; | |
} else if (!fs.existsSync(path.dirname(outCssPath))) { | |
// Create the output directory if it doesn't exist | |
fs.mkdirSync(path.dirname(outCssPath)); | |
} | |
// If no output path is provided, use the input path | |
outCssPath = cssPath; | |
} else if (!fs.existsSync(path.dirname(outCssPath))) { | |
// Create the output directory if it doesn't exist | |
fs.mkdirSync(path.dirname(outCssPath), { recursive: true }); | |
} |
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, | ||
isFullObfuscation: boolean = false, | ||
outCssPath?: string, | ||
) { | ||
if (!outCssPath) { | ||
outCssPath = cssPath; | ||
} else if (!fs.existsSync(path.dirname(outCssPath))) { | ||
fs.mkdirSync(path.dirname(outCssPath)); | ||
} | ||
|
||
let cssContent = fs.readFileSync(cssPath, "utf-8"); | ||
|
||
let cssObj = css.parse(cssContent); | ||
const cssRulesCount = cssObj.stylesheet.rules.length; | ||
|
||
if (isFullObfuscation) { | ||
Object.keys(selectorConversion).forEach((key) => { | ||
usedKeyRegistery.add(key); | ||
}); | ||
} else { | ||
// 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); | ||
}); | ||
|
||
// join all Tailwind CSS [child] selectors (eg. ".\[\&_\.side-box\]\:absolute .side-box") | ||
const tailwindCssChildSelectors = getAllSelector(cssObj).filter((selector) => selector.startsWith(".\\[")); | ||
tailwindCssChildSelectors.forEach((tailwindCssChildSelector) => { | ||
usedKeyRegistery.add(tailwindCssChildSelector); | ||
}); | ||
|
||
// join all child selectors (eg. ">*") | ||
const universalSelectors = getAllSelector(cssObj).filter((selector) => selector.includes(">")); | ||
universalSelectors.forEach((universalSelector) => { | ||
usedKeyRegistery.add(universalSelector); | ||
}); | ||
} | ||
|
||
// 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); | ||
} | ||
} | ||
Object.entries(newConversionTables.idents).forEach(([key, value]) => { | ||
if (!tables.idents[key]) { | ||
// If it doesn't exist, create a new entry | ||
tables.idents[key] = value; | ||
} | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forEach(async …)
does not await – results may be incomplete
Array.forEach
ignores returned promises, so obfuscateCssFiles
may resolve before any file has finished processing, returning partial conversion tables.
- cssPaths.forEach(async (cssPath) => {
+ await Promise.all(cssPaths.map(async (cssPath) => {
const { conversionTables: newConversionTables } = await obfuscateCss({
…
});
…
- });
+ }));
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
cssPaths.forEach(async (cssPath) => { | |
const { conversionTables: newConversionTables } = await obfuscateCss({ | |
cssPath: cssPath, | |
conversionTables: conversionTables, | |
prefix, | |
suffix, | |
mode, | |
ignorePatterns, | |
generatorSeed, | |
removeOriginalCss, | |
}); | |
// temporary remove vendor pseudo classes | |
vendorPseudoClassRegexes.forEach((regex, i) => { | |
selector = selector.replace(regex, (match) => { | |
return createKey(match); | |
}); | |
}); | |
// extract classes | |
let classes = selector.match(extractClassRegex) as string[] | undefined; | |
// replace classes with replacementClassNames | |
if (replacementClassNames !== undefined) { | |
selector = selector.replace(extractClassRegex, (originalClassName) => { | |
return replacementClassNames.shift() || originalClassName; | |
}); | |
} | |
// place back the pseudo classes | |
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, | |
generatorSeed = Math.random().toString().slice(2, 10), // take 8 digits from the random number | |
}: { | |
selectorConversionJsonFolderPath: string, | |
buildFolderPath: string, | |
mode?: obfuscateMode, | |
classNameLength?: number, | |
classPrefix?: string, | |
classSuffix?: string, | |
classIgnore?: (string | RegExp)[], | |
enableObfuscateMarkerClasses?: boolean, | |
generatorSeed?: string, | |
}) { | |
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)); | |
// Merge the conversion tables | |
Object.entries(newConversionTables.selectors).forEach(([key, value]) => { | |
if (!tables.selectors[key]) { | |
// If it doesn't exist, create a new entry | |
tables.selectors[key] = value; | |
} | |
}); | |
// 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 = selectorClasses; | |
if (classes && allowClassStartWith.some((start) => selector.startsWith(start))) { | |
classes = classes.map((className) => { | |
// apply ignore list | |
if (classIgnore.some(regex => { | |
if (typeof regex === "string") { | |
return className === regex; | |
} | |
return new RegExp(regex).test(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, generatorSeed); | |
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; | |
} | |
} | |
const jsonPath = path.join(process.cwd(), selectorConversionJsonFolderPath, "conversion.json"); | |
fs.writeFileSync(jsonPath, JSON.stringify(selectorConversion, null, 2)); | |
if (duplicationCheck(Object.keys(selectorConversion))) { | |
if (mode == "random") { | |
log("error", "Obfuscation", "Duplicated class names found in the conversion JSON, try to increase the class name length / open an issue on GitHub https://github.com/soranoo/next-css-obfuscator/issues"); | |
} else { | |
log("error", "Obfuscation", "Duplicated class names found in the conversion JSON, please open an issue on GitHub https://github.com/soranoo/next-css-obfuscator/issues"); | |
} | |
} | |
} | |
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, | |
isFullObfuscation: boolean = false, | |
outCssPath?: string, | |
) { | |
if (!outCssPath) { | |
outCssPath = cssPath; | |
} else if (!fs.existsSync(path.dirname(outCssPath))) { | |
fs.mkdirSync(path.dirname(outCssPath)); | |
} | |
let cssContent = fs.readFileSync(cssPath, "utf-8"); | |
let cssObj = css.parse(cssContent); | |
const cssRulesCount = cssObj.stylesheet.rules.length; | |
if (isFullObfuscation) { | |
Object.keys(selectorConversion).forEach((key) => { | |
usedKeyRegistery.add(key); | |
}); | |
} else { | |
// 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); | |
}); | |
// join all Tailwind CSS [child] selectors (eg. ".\[\&_\.side-box\]\:absolute .side-box") | |
const tailwindCssChildSelectors = getAllSelector(cssObj).filter((selector) => selector.startsWith(".\\[")); | |
tailwindCssChildSelectors.forEach((tailwindCssChildSelector) => { | |
usedKeyRegistery.add(tailwindCssChildSelector); | |
}); | |
// join all child selectors (eg. ">*") | |
const universalSelectors = getAllSelector(cssObj).filter((selector) => selector.includes(">")); | |
universalSelectors.forEach((universalSelector) => { | |
usedKeyRegistery.add(universalSelector); | |
}); | |
} | |
// 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); | |
} | |
} | |
Object.entries(newConversionTables.idents).forEach(([key, value]) => { | |
if (!tables.idents[key]) { | |
// If it doesn't exist, create a new entry | |
tables.idents[key] = value; | |
} | |
}); | |
}); | |
await Promise.all(cssPaths.map(async (cssPath) => { | |
const { conversionTables: newConversionTables } = await obfuscateCss({ | |
cssPath: cssPath, | |
conversionTables: conversionTables, | |
prefix, | |
suffix, | |
mode, | |
ignorePatterns, | |
generatorSeed, | |
removeOriginalCss, | |
}); | |
// Merge the conversion tables | |
Object.entries(newConversionTables.selectors).forEach(([key, value]) => { | |
if (!tables.selectors[key]) { | |
// If it doesn't exist, create a new entry | |
tables.selectors[key] = value; | |
} | |
}); | |
Object.entries(newConversionTables.idents).forEach(([key, value]) => { | |
if (!tables.idents[key]) { | |
// If it doesn't exist, create a new entry | |
tables.idents[key] = value; | |
} | |
}); | |
})); |
let transformerMode: TransformProps["mode"] = mode === "simplify" ? "minimal" : "hash"; | ||
if (!transformerMode) { | ||
// @ts-expect-error - "simplify-seedable" is deprecated but for backward compatibility | ||
if (mode === "simplify-seedable") { | ||
log("warn", "CSS obfuscation", "The 'simplify-seedable' mode is deprecated, please use 'random' or 'simplify' instead. Now will fall back to 'random' mode."); | ||
transformerMode = "hash"; | ||
} | ||
|
||
return newClassName; | ||
log("error", "CSS obfuscation", `Invalid mode: ${mode}`); | ||
throw new Error(`Invalid mode: ${mode}`); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
transformerMode
check is unreachable
transformerMode
is always set to either "minimal"
or "hash"
, so the subsequent if (!transformerMode)
branch (including the deprecated‑mode fallback) will never execute.
Move validation logic before assignment or compare mode
directly.
- let transformerMode: TransformProps["mode"] = mode === "simplify" ? "minimal" : "hash";
- if (!transformerMode) {
+ let transformerMode: TransformProps["mode"];
+
+ if (mode === "simplify") {
+ transformerMode = "minimal";
+ } else if (mode === "random") {
+ transformerMode = "hash";
+ } else {
// @ts-expect-error - "simplify-seedable" is deprecated but for backward compatibility
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
let transformerMode: TransformProps["mode"] = mode === "simplify" ? "minimal" : "hash"; | |
if (!transformerMode) { | |
// @ts-expect-error - "simplify-seedable" is deprecated but for backward compatibility | |
if (mode === "simplify-seedable") { | |
log("warn", "CSS obfuscation", "The 'simplify-seedable' mode is deprecated, please use 'random' or 'simplify' instead. Now will fall back to 'random' mode."); | |
transformerMode = "hash"; | |
} | |
return newClassName; | |
log("error", "CSS obfuscation", `Invalid mode: ${mode}`); | |
throw new Error(`Invalid mode: ${mode}`); | |
} | |
let transformerMode: TransformProps["mode"]; | |
if (mode === "simplify") { | |
transformerMode = "minimal"; | |
} else if (mode === "random") { | |
transformerMode = "hash"; | |
} else { | |
// @ts-expect-error - "simplify-seedable" is deprecated but for backward compatibility | |
if (mode === "simplify-seedable") { | |
log( | |
"warn", | |
"CSS obfuscation", | |
"The 'simplify-seedable' mode is deprecated, please use 'random' or 'simplify' instead. Now will fall back to 'random' mode." | |
); | |
transformerMode = "hash"; | |
} | |
log("error", "CSS obfuscation", `Invalid mode: ${mode}`); | |
throw new Error(`Invalid mode: ${mode}`); | |
} |
// Save the obfuscated CSS to the output path | ||
const sizeBefore = Buffer.byteLength(cssContent, "utf8"); | ||
fs.writeFileSync(outCssPath, obfuscatedCss); | ||
const sizeAfter = Buffer.byteLength(obfuscatedCss, "utf8"); | ||
const percentChange = Math.round(((sizeAfter) / sizeBefore) * 100); | ||
log("success", "CSS obfuscated:", `Size from ${sizeBefore} to ${sizeAfter} bytes (${percentChange}%) in ${getFilenameFromPath(cssPath)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writes wrong variable – breaks “keep original CSS” path
fs.writeFileSync(outCssPath, obfuscatedCss);
disregards the finCss
value that may contain the merged & minified CSS when removeOriginalCss === false
.
Size metrics are likewise mis‑reported.
- fs.writeFileSync(outCssPath, obfuscatedCss);
- const sizeAfter = Buffer.byteLength(obfuscatedCss, "utf8");
+ fs.writeFileSync(outCssPath, finCss);
+ const sizeAfter = Buffer.byteLength(finCss, "utf8");
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Save the obfuscated CSS to the output path | |
const sizeBefore = Buffer.byteLength(cssContent, "utf8"); | |
fs.writeFileSync(outCssPath, obfuscatedCss); | |
const sizeAfter = Buffer.byteLength(obfuscatedCss, "utf8"); | |
const percentChange = Math.round(((sizeAfter) / sizeBefore) * 100); | |
log("success", "CSS obfuscated:", `Size from ${sizeBefore} to ${sizeAfter} bytes (${percentChange}%) in ${getFilenameFromPath(cssPath)}`); | |
// Save the obfuscated CSS to the output path | |
const sizeBefore = Buffer.byteLength(cssContent, "utf8"); | |
fs.writeFileSync(outCssPath, finCss); | |
const sizeAfter = Buffer.byteLength(finCss, "utf8"); | |
const percentChange = Math.round((sizeAfter / sizeBefore) * 100); | |
log( | |
"success", | |
"CSS obfuscated:", | |
`Size from ${sizeBefore} to ${sizeAfter} bytes (${percentChange}%) in ${getFilenameFromPath(cssPath)}` | |
); |
src/utils.ts
Outdated
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "").slice(1); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double .slice(1)
removes two characters
replacement
drops the first character twice (e.g. .abc
➜ bc
). Remove the second .slice(1)
.
- const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "").slice(1);
+ const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "");
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "").slice(1); | |
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, ""); |
…tency in tests and handlers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (5)
src/handlers/css.ts (4)
48-49
: Create output directory recursively
fs.mkdirSync(path.dirname(outCssPath));
will throw if the path contains multiple non-existent nested folders. Use therecursive: true
flag to make the call idempotent and resilient.- fs.mkdirSync(path.dirname(outCssPath)); + fs.mkdirSync(path.dirname(outCssPath), { recursive: true });
54-63
:transformerMode
check is unreachable
transformerMode
is always set to either"minimal"
or"hash"
, so the subsequentif (!transformerMode)
branch (including the deprecated-mode fallback) will never execute.
Move validation logic before assignment or comparemode
directly.- let transformerMode: TransformProps["mode"] = mode === "simplify" ? "minimal" : "hash"; - if (!transformerMode) { + let transformerMode: TransformProps["mode"]; + + if (mode === "simplify") { + transformerMode = "minimal"; + } else if (mode === "random") { + transformerMode = "hash"; + } else { // @ts-expect-error - "simplify-seedable" is deprecated but for backward compatibility
98-102
: Writes wrong variable – breaks "keep original CSS" path
fs.writeFileSync(outCssPath, obfuscatedCss);
disregards thefinCss
value that may contain the merged & minified CSS whenremoveOriginalCss === false
.
Size metrics are likewise mis-reported.- fs.writeFileSync(outCssPath, obfuscatedCss); - const sizeAfter = Buffer.byteLength(obfuscatedCss, "utf8"); + fs.writeFileSync(outCssPath, finCss); + const sizeAfter = Buffer.byteLength(finCss, "utf8");
161-187
:forEach(async …)
does not await – results may be incomplete
Array.forEach
ignores returned promises, soobfuscateCssFiles
may resolve before any file has finished processing, returning partial conversion tables.- cssPaths.forEach(async (cssPath) => { + await Promise.all(cssPaths.map(async (cssPath) => { const { conversionTables: newConversionTables } = await obfuscateCss({ … }); … - }); + }));src/utils.ts (1)
288-288
: Double.slice(1)
removes two characters
replacement
drops the first character twice (e.g..abc
➜bc
). Remove the second.slice(1)
.- const replacement = `$1${selectorConversion[key].slice(1).replace(/\\/g, "").slice(1)}`; + const replacement = `$1${selectorConversion[key].slice(1).replace(/\\/g, "")}`;
🧹 Nitpick comments (5)
src/handlers/js-ast.ts (1)
77-82
: Consider replacingvoid
withundefined
in the union typeUsing
void
in a union type can be confusing as it represents the absence of a return value rather than a value itself. Consider replacing it withundefined
for better type clarity.-export const searchStringLiterals = (path: NodePath<t.Node>, - callback: (str: string) => void | string, +export const searchStringLiterals = (path: NodePath<t.Node>, + callback: (str: string) => undefined | string, //? keep track of scanned nodes to avoid infinite loop scannedNodes: Set<t.Node> = new Set() ) => {Also applies to: 369-369
🧰 Tools
🪛 Biome (1.9.4)
[error] 78-78: void is confusing inside a union type.
Unsafe fix: Use undefined instead.
(lint/suspicious/noConfusingVoidType)
src/handlers/css.ts (2)
8-8
: Remove or complete the TODO commentThe TODO comment
// TODO: html failed with .
appears to be incomplete. Either complete the comment with specific details or remove it if it's no longer relevant.-// TODO: html failed with . +// TODO: Fix HTML processing when selector ends with period
136-140
: Missing JSDoc for initialize functionAdd a JSDoc comment to explain what's happening during initialization, especially since this is an asynchronous operation waiting for multiple modules to load.
- // Initialize nessesary modules + // Initialize necessary modules for CSS transformation and minification + // Both modules need to be loaded before we can process any CSS filesAlso, there's a typo in "nessesary" which should be "necessary".
src/utils.ts (2)
285-285
: Use const for variables that aren't reassignedThe variable
exactMatchRegex
is only assigned once, so it should useconst
instead oflet
.- let exactMatchRegex = new RegExp(`([\\s"'\\\`]|^)(${keyUse})(?=$|[\\s"'\\\`]|\\\\n|\\\\",|\\\\"})`, 'g'); + const exactMatchRegex = new RegExp(`([\\s"'\\\`]|^)(${keyUse})(?=$|[\\s"'\\\`]|\\\\n|\\\\",|\\\\"})`, 'g');🧰 Tools
🪛 Biome (1.9.4)
[error] 285-285: This let declares a variable that is only assigned once.
'exactMatchRegex' is never reassigned.
Safe fix: Use const instead.
(lint/style/useConst)
460-462
: Optimize path filtering in directory traversalThe current implementation checks paths after reading each directory entry, which is inefficient. Consider filtering directories earlier to avoid unnecessary traversal.
- if (!shouldIncludePath(filePath, whiteList, blackList)) { - return; // Skip this file/directory - } + // Skip file/directory early if in blacklist + if (blackList.some(pattern => + typeof pattern === "string" ? normalizePath(filePath).includes(pattern) : pattern.test(normalizePath(filePath)))) { + return; + } + + // Skip if whitelist exists and path is not in it + if (whiteList.length > 0 && !whiteList.some(pattern => + typeof pattern === "string" ? normalizePath(filePath).includes(pattern) : pattern.test(normalizePath(filePath)))) { + return; + }This implementation is optional since it would replicate logic from
shouldIncludePath
, but it could be more efficient in deeply nested directories.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
bin/cli.mjs
(1 hunks)biome.json
(1 hunks)src/__tests__/html.test.ts
(6 hunks)src/config.ts
(1 hunks)src/handlers/css.ts
(1 hunks)src/handlers/js-ast.ts
(6 hunks)src/types.ts
(1 hunks)src/utils.ts
(17 hunks)
✅ Files skipped from review due to trivial changes (1)
- bin/cli.mjs
🚧 Files skipped from review as they are similar to previous changes (4)
- src/tests/html.test.ts
- biome.json
- src/config.ts
- src/types.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/utils.ts (2)
src/types.ts (2)
LogLevel
(3-3)SelectorConversion
(5-5)src/handlers/html.ts (1)
obfuscateHtmlClassNames
(9-155)
src/handlers/js-ast.ts (1)
src/types.ts (1)
SelectorConversion
(5-5)
🪛 Biome (1.9.4)
src/utils.ts
[error] 285-285: This let declares a variable that is only assigned once.
'exactMatchRegex' is never reassigned.
Safe fix: Use const instead.
(lint/style/useConst)
src/handlers/js-ast.ts
[error] 78-78: void is confusing inside a union type.
Unsafe fix: Use undefined instead.
(lint/suspicious/noConfusingVoidType)
🔇 Additional comments (3)
src/handlers/js-ast.ts (3)
1-1
: Good use of TypeScript'stype
importsUsing the
type
keyword for imports that are only used as types is a good practice. This makes it explicit that these imports are for type checking only and will be removed during transpilation, resulting in better tree-shaking and smaller bundle sizes.Also applies to: 3-3
17-22
: LGTM: Consistent function style refactoringConverting from function declarations to exported arrow functions makes the code more consistent with modern TypeScript practices and aligns with the same pattern being used elsewhere in the codebase.
Also applies to: 67-67
45-45
: Clean formattingThe added blank line helps with code readability by creating a visual separation between related blocks of code.
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Recursive directory creation for consistency
For consistency with the fix in css.ts, use the recursive: true
flag when creating directories.
- fs.mkdirSync(selectorConversionJsonFolderPath);
+ fs.mkdirSync(selectorConversionJsonFolderPath, { recursive: true });
Committable suggestion skipped: line range outside the PR's diff.
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8")); | ||
|
||
if (Object.keys(fileData).includes("ident") && Object.keys(fileData).includes("selector")) { | ||
Object.assign(tables.idents, fileData.ident); | ||
Object.assign(tables.selectors, fileData.selector); | ||
} else { | ||
// if the file doesn't have ident, it should be selector | ||
//? For backward compatibility | ||
Object.assign(tables.selectors, fileData); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for JSON parsing
The function doesn't handle potential errors when parsing JSON files. Add a try-catch block to handle invalid JSON files gracefully.
- const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
-
- if (Object.keys(fileData).includes("ident") && Object.keys(fileData).includes("selector")) {
- Object.assign(tables.idents, fileData.ident);
- Object.assign(tables.selectors, fileData.selector);
- } else {
- // if the file doesn't have ident, it should be selector
- //? For backward compatibility
- Object.assign(tables.selectors, fileData);
- }
+ try {
+ const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
+
+ if (Object.keys(fileData).includes("ident") && Object.keys(fileData).includes("selector")) {
+ Object.assign(tables.idents, fileData.ident);
+ Object.assign(tables.selectors, fileData.selector);
+ } else {
+ // if the file doesn't have ident, it should be selector
+ //? For backward compatibility
+ Object.assign(tables.selectors, fileData);
+ }
+ } catch (error) {
+ log("error", "Failed to parse JSON file", `${filePath}: ${error}`);
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8")); | |
if (Object.keys(fileData).includes("ident") && Object.keys(fileData).includes("selector")) { | |
Object.assign(tables.idents, fileData.ident); | |
Object.assign(tables.selectors, fileData.selector); | |
} else { | |
// if the file doesn't have ident, it should be selector | |
//? For backward compatibility | |
Object.assign(tables.selectors, fileData); | |
} | |
}); | |
try { | |
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8")); | |
if (Object.keys(fileData).includes("ident") && Object.keys(fileData).includes("selector")) { | |
Object.assign(tables.idents, fileData.ident); | |
Object.assign(tables.selectors, fileData.selector); | |
} else { | |
// if the file doesn't have ident, it should be selector | |
//? For backward compatibility | |
Object.assign(tables.selectors, fileData); | |
} | |
} catch (error) { | |
log("error", "Failed to parse JSON file", `${filePath}: ${error}`); | |
} | |
}); |
🎉 This PR is included in version 1.0.0-beta.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
🎉 This PR is included in version 3.0.0-beta.3 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary by CodeRabbit
New Features
Breaking Changes
Bug Fixes
Refactor
css-seasoning
andlightningcss-wasm
.Documentation
Chores
.prettierignore
and.prettierrc
removals reflecting new formatting defaults.