soranoo / next-css-obfuscator

A package deeply inspired by PostCSS-Obfuscator but for Next.js.
https://next-css-obfuscator.vercel.app
MIT License
71 stars 3 forks source link

Not working properly with class-variance-authority (CVA) #36

Closed FadyAmir223 closed 3 months ago

FadyAmir223 commented 3 months ago

Type

Checklist

  1. [x] Updated the package to the latest version
  2. [x] Read all documentation
  3. [x] No related issue
  4. [x] Meaningful issue title

Environment

Describe the bug shadcn-ui uses CVA which the package doesn't recognize

const labelVariants = cva(
  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
)

the problem lies in the assumption that classes will be inlined after "className"

if (t.isIdentifier(path.node.key) && path.node.key.name === "className")

but in the bundle it puts the the string in a function then call it below

      const l = (0, i.j)(
        'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
      )
      let d = a.forwardRef((e, t) => {
        const { className: r, ...i } = e
        return (0, n.jsx)(s.f, { ref: t, className: (0, o.cn)(l(), r), ...i })
      })

more complex example

const c = (0, n.j)('inline-flex items-center justify-center', {
  variants: {
    variant: {
      default: 'bg-primary text-primary-foreground',
      destructive: 'bg-destructive text-destructive-foreground',
    },
    size: {
      default: 'h-9 px-4 py-2',
    },
  },
  defaultVariants: { variant: 'default', size: 'default' },
})
const l = i.forwardRef(
  ({ className: e, variant: t, size: r, asChild: n = !1, ...i }, l) => {
    const d = n ? a.g7 : 'button'
    return s.jsx(d, {
      className: (0, o.cn)(c({ variant: t, size: r, className: e })),
      ref: l,
      ...i,
    })
  },
)

it can be avoided by turning off removeOriginalCss option but I suggested the simplify variation like medium to reduce the size of css file, doing that defeats the purpose of the package

a suggested solution is to compare the conversion.json keys with the string boundary after ensuring that text inside is tailwind classes, but I'm sure you can come up with a better idea :)

Config

module.exports = {
  enable: true,
  mode: 'simplify',
  refreshClassConversionJson: process.env.NODE_ENV !== 'production',
  removeOriginalCss: true,
  blackListedFolderPaths: [
    './.next/cache',
    /\.next\/server\/pages\/api/,
    /_document..*js/,
    /_app-.*/,
    /__.*/,
  ],
}
soranoo commented 3 months ago

Sorry, I don't think it is possible to fix since there is no identifiable pattern. To prevent breaking the JavaScript accidentally, a strict className searching strategy has been implemented inside the non-AST based obfuscation.

Solutions

  1. Set enableJsAst to true inside the config. The className tracing strategy will be applied by enabling this option. This strategy enables the obfuscator to find out all className related variables.

For Future Reference

The line you mentioned (the following) is inside the js-ast.ts and is not relevant to the config you provided.

if (t.isIdentifier(path.node.key) && path.node.key.name === "className")

The functions inside js-ast.ts won't be activated when enableJsAst=false(default config)

FadyAmir223 commented 3 months ago

If there is no pattern we can create it, we may require developers to add marker like "next-css-obfuscator" in strings used by CVA then remove it in the bundle and obfuscate

soranoo commented 3 months ago

Then removeMarkersAfterObfuscated can be your friend~

FadyAmir223 commented 3 months ago

I tried to apply partial obfuscation to the whole project, but for some reason cn() (tailwindMerge) didn't work as expected (e.g. rounded-full didn't overwrite rounded-md) so I think the solution is to enable a marker alongside the full obfuscation to cover the blind spots like CVA

soranoo commented 3 months ago

You can achieve this using the following config,

module.exports = {
enableObfuscateMarkerClasses = true,
obfuscateMarkerClasses = [".jsx", "{your_custom_key}"]
// rest of your config...
}

The "class" .jsx(js pattern) is the default marker for searching class names in non-AST based full obfuscation and it won't be removed after obfuscation is completed.