Skip to content

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

Merged
merged 14 commits into from
Apr 21, 2025
Merged

feat-support-tailwindcss-v4 #68

merged 14 commits into from
Apr 21, 2025

Conversation

soranoo
Copy link
Owner

@soranoo soranoo commented Apr 21, 2025

Summary by CodeRabbit

  • New Features

    • Introduced automated release workflow for publishing on main and beta branches.
    • Added support for TailwindCSS 4, nested CSS, and CSS idents obfuscation.
    • Added a migration guide and upgrade documentation for version 3.
    • Integrated new linting and formatting tools with unified configuration.
    • Added batch CSS obfuscation with improved external library integration.
  • Breaking Changes

    • Major update to version 3.0.0-b1 with breaking configuration changes and deprecated options.
    • Deprecated and replaced several configuration options; see README and migration guide for details.
    • Removed implicit preservation of TailwindCSS dark mode class; requires explicit configuration.
  • Bug Fixes

    • Improved CSS and HTML class obfuscation robustness and accuracy.
    • Enhanced JavaScript class name extraction with CSS selector unescaping.
  • Refactor

    • Simplified and modernized CSS obfuscation using external libraries css-seasoning and lightningcss-wasm.
    • Removed deprecated recursive HTML parsing utilities.
    • Converted key functions to arrow functions with inline exports for clarity.
    • Cleaned up and modularized utility and handler functions, including path filtering and JSON loading.
    • Updated type definitions to reuse external types and mark deprecated fields.
    • Unified option handling and removed deprecated configuration usage.
  • Documentation

    • Updated README with major changes, migration instructions, and new usage examples.
    • Added detailed upgrade guide for migrating to version 3.
  • Chores

    • Removed obsolete configuration and test files.
    • Enhanced automated release and linting processes.
    • Added .prettierignore and .prettierrc removals reflecting new formatting defaults.
    • Updated TypeScript config to exclude release configuration file.

soranoo added 12 commits April 20, 2025 14:44
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.
Copy link

vercel bot commented Apr 21, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
next-css-obfuscator ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 21, 2025 11:15pm

Copy link

coderabbitai bot commented Apr 21, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This 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 (prefix, suffix, ignorePatterns), deprecating and removing several old options. New documentation, upgrade guides, and semantic-release automation are added. The codebase adopts the Biome toolchain for linting and formatting, and test suites are updated to reflect the new API. Several deprecated functions and test files are removed, and type definitions are refactored for clarity and maintainability.

Changes

File(s) / Path(s) Change Summary
.github/workflows/release.yml, release.config.cjs Added GitHub Actions workflow for semantic-release and its configuration, enabling automated releases for main and beta branches.
.prettierignore, .prettierrc Removed Prettier configuration and ignore files.
biome.json Added Biome configuration for formatting and linting, replacing Prettier.
package.json Bumped version to 3.0.0-b1, updated scripts, dependencies, and devDependencies for new tooling and semantic-release.
tsconfig.json Excluded release.config.cjs from TypeScript compilation.
README.md Updated documentation for version 3, highlighting breaking changes, new features, and migration notes.
docs/upgrade-to-v3.md Added a detailed upgrade guide for migrating from v2 to v3, covering all breaking changes and configuration updates.
src/config.ts, src/types.ts Refactored configuration and type definitions: added new options, marked old ones as deprecated, updated types for external library integration, and improved type safety.
src/handlers/css.ts Rewrote CSS obfuscation logic to use css-seasoning and lightningcss-wasm, removed all custom parsing and deprecated modes, added batch processing and conversion table merging.
src/handlers/html.ts Removed deprecated HTML content extraction functions, improved class name handling with CSS unescaping, and streamlined exports.
src/handlers/js-ast.ts, src/handlers/js.ts Converted main functions to exported arrow functions, updated export style, and made minor formatting changes.
src/index.ts Refactored main entry: switched to async/await, updated to use new options, removed deprecated logic, and improved logging and output handling.
src/utils.ts Removed deprecated/unused utilities, added unified path filtering, refactored file search and conversion table loading, and modularized exports.
src/__tests__/html.test.ts, src/__tests__/js-ast.test.ts, src/__tests__/utils.test.ts Updated or removed tests to match new API, removed tests for deprecated or deleted functions.
src/handlers/css.test.ts, src/handlers/js.test.ts Deleted test files for removed or refactored functions.
src/__tests__/js.test.ts Added new test suite for searchForwardComponent covering various JSX parsing scenarios.

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
Loading

Poem

🐇✨
Version three is here, a leap so grand,
With new configs and docs at hand.
CSS now seasoned, lightning-fast,
Old code and tests are in the past.
Biome keeps our code in line,
Semantic-release—now it’s time!
A rabbit hops, with joy and cheer,
Obfuscation magic—v3 is here!


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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 issue

Remove 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 issue

Avoid 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 intact

This 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 to main and beta, builds the package, and invokes semantic-release with the appropriate tokens. To further optimize CI duration, consider adding a step to cache the node_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. - 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)

🪛 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 now undefined for random seed generation, and enableJsAst 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 &quot;. Because the result is already wrapped in
double‑quotes ("${...}"), downstream consumers will see &quot; inside
another pair of quotes, rendering the literal &quot; 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 debug console.log

Debug prints left in production code clutter stdout and can leak paths in CI
logs. The logging utility already provides a debug level.

-  console.log({ jsonPath });

58-61: Ensure the output directory exists before writing the JSON

If the user supplies a relative classConversionJsonFolderPath, the folder is
created inside obfuscateCssFiles, 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 shared conversionTables 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

📥 Commits

Reviewing files that changed from the base of the PR and between 76d2834 and 138a4af.

⛔ 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 '

' to deeply equal '
'.


[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 new release.config.cjs correctly defines the main and beta branches and configures the essential plugins for conventional commit analysis, release notes generation, npm publishing from the dist 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 excluding src/*.test.* along with the new release.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 of searchStringLiterals and obfuscateJsWithAst 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 deprecated findHtmlTagContentsByClass import with obfuscateHtmlClassNames 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 of searchStringLiterals 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 for searchForwardComponent.

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, and ignorePatterns 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.

Comment on lines +32 to +35
"css-seasoning": "^1.4.1",
"html-escaper": "^3.0.3",
"htmlparser2": "^10.0.0",
"recoverable-random": "^1.0.5",
"lightningcss-wasm": "^1.29.3",
Copy link

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].
  • 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:


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: ⚠️ No official reference in Tailwind v4 docs—please test that it supports modern CSS features (cascade layers, @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+).

Comment on lines +45 to +52
ignorePatterns: {
...options.ignorePatterns,
selectors: [
...options.ignorePatterns?.selectors || [],
...(options.classIgnore || []),
]
},

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
ignorePatterns: {
...options.ignorePatterns,
selectors: [
...options.ignorePatterns?.selectors || [],
...(options.classIgnore || []),
]
},
ignorePatterns: {
...(options.ignorePatterns ?? {}),
selectors: [
...(options.ignorePatterns?.selectors ?? []),
...(options.classIgnore || []),
]
},

src/types.ts Outdated
Comment on lines 52 to 73
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;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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;
};

Comment on lines +45 to +50
// 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));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 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 });
}

Comment on lines +161 to 188
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;
}
});
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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;
}
});
}));

Comment on lines +54 to +63
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}`);
}
Copy link

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.

Suggested change
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}`);
}

Comment on lines +98 to +103
// 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)}`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 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
Comment on lines 288 to 289
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "").slice(1);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Double .slice(1) removes two characters

replacement drops the first character twice (e.g. .abcbc). 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.

Suggested change
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "").slice(1);
const replacement = `$1` + selectorConversion[key].slice(1).replace(/\\/g, "");

Copy link

@coderabbitai coderabbitai bot left a 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 the recursive: 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 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

98-102: 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");

161-187: 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({
        …
      });
      …
-  });
+  }));
src/utils.ts (1)

288-288: Double .slice(1) removes two characters

replacement drops the first character twice (e.g. .abcbc). 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 replacing void with undefined in the union type

Using 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 with undefined 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 comment

The 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 function

Add 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 files

Also, there's a typo in "nessesary" which should be "necessary".

src/utils.ts (2)

285-285: Use const for variables that aren't reassigned

The variable exactMatchRegex is only assigned once, so it should use const instead of let.

-    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 traversal

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 138a4af and 3c9cd7a.

📒 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's type imports

Using 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 refactoring

Converting 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 formatting

The added blank line helps with code readability by creating a visual separation between related blocks of code.

Comment on lines 144 to 145
//ref: https://github.com/n4j1Br4ch1D/postcss-obfuscator/blob/main/utils.js

Copy link

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.

Comment on lines +531 to 541
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);
}
});
Copy link

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.

Suggested change
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}`);
}
});

@soranoo soranoo changed the base branch from main to beta April 21, 2025 23:15
@soranoo soranoo merged commit e33c10f into beta Apr 21, 2025
4 checks passed
@soranoo soranoo deleted the feat-support-tailwindcss-v4 branch April 21, 2025 23:16
Copy link

🎉 This PR is included in version 1.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Copy link

🎉 This PR is included in version 3.0.0-beta.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

@coderabbitai coderabbitai bot mentioned this pull request May 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant