webcrack icon indicating copy to clipboard operation
webcrack copied to clipboard

[plugin] plugin to support unminifying `goober` CSS-in-JS library patterns + related JSX decompilation

Open 0xdevalias opened this issue 9 months ago • 0 comments

Mostly creating this based on the exploration I did in https://github.com/j4k0xb/webcrack/issues/10#issuecomment-2693645060 in case there is no generic way to solve that in core, and it needs to be a more library specific plugin solution as per https://github.com/j4k0xb/webcrack/issues/143#issuecomment-2692345330 / https://github.com/j4k0xb/webcrack/issues/143#issuecomment-2692517232

This is also aligned to wakaru's proposed module-detection feature:

  • https://github.com/pionxzh/wakaru/issues/41

@j4k0xb I also don't expect this to be something you create; but figured since I already did the deeper exploration in this repo, I may as well create a standalone reference point for it, even if this issue ends up getting closed.


From my prior exploration:

Edit 3: Looking at the code from https://github.com/j4k0xb/webcrack/issues/10#issuecomment-2692599211 again, I think there is another case where JSX-like things may not be currently getting decompiled properly, which is syntax like this:

/* ..snip.. */
/* 541      */  var Z = h("div")`
/* 542      */    display: flex;
/* 543      */    justify-content: center;
/* 544      */    margin: 4px 10px;
/* 545      */    color: inherit;
/* 546      */    flex: 1 1 auto;
/* 547      */    white-space: pre-line;
/* 548      */  `;
/* ..snip.. */
/* 567      */  let c = t.createElement(Z, {
/* 568      */    ...e.ariaProps
/* 569      */  }, g(e.message, e));
/* ..snip.. */

Looking higher up in the file, we see the definition for h:

/* ..snip.. */
/* 106      */  function h(e, t) {
/* 107      */    let l = this || {};
/* 108      */    return function () {
/* 109      */      let i = arguments;
/* 110      */      function n(a, o) {
/* 111      */        let c = Object.assign({}, a);
/* 112      */        let s = c.className || n.className;
/* 113      */        l.p = Object.assign({
/* 114      */          theme: p && p()
/* 115      */        }, c);
/* 116      */        l.o = / *go\d+/.test(s);
/* 117      */        c.className = m.apply(l, i) + (s ? " " + s : "");
/* 118      */        if (t) {
/* 119      */          c.ref = o;
/* 120      */        }
/* 121      */        let r = e;
/* 122      */        if (e[0]) {
/* 123      */          r = c.as || e;
/* 124      */          delete c.as;
/* 125      */        }
/* 126      */        if (w && r[0]) {
/* 127      */          w(c);
/* 128      */        }
/* 129      */        return y(r, c);
/* 131      */      }
/* 132      */      if (t) {
/* 133      */        return t(n);
/* 134      */      } else {
/* 135      */        return n;
/* 136      */      }
/* 137      */    };
/* 138      */  }
/* ..snip.. */

And searching GitHub code for / *go\d+/.test leads us to the

  • https://github.com/search?type=code&q=%22%2F+*go%5Cd%2B%2F.test%22
    • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L20-L71
      • https://github.com/cristianbote/goober
        • goober, a less than 1KB css-in-js solution

      • https://goober.rocks/

Which we can then also see additional confirmation for in earlier code as well:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/core/get-sheet.js#L11-L25
/* ..snip.. */
/* 6        */  let i = e => typeof window == "object" ? ((e ? e.querySelector("#_goober") : window._goober) || Object.assign((e || document.head).appendChild(document.createElement("style")), {
/* 7        */    innerHTML: " ",
/* 8        */    id: "_goober"
/* 9        */  })).firstChild : e || l;
/* ..snip.. */

Which seems to be used across a number of libs/projects:

  • https://github.com/search?type=code&q=%22%23_goober%22+OR+%22window._goober%22

Sometimes inlined directly:

  • https://github.com/KevinVandy/tanstack-query/blob/69476f0ce5778afad4520ed42485b4110993afed/packages/query-devtools/src/utils.tsx#L305-L323

This may end up being another case where, similar to the comment made in https://github.com/j4k0xb/webcrack/issues/143#issuecomment-2692345330, the deeper specifics of this may belong in a separate plugin instead of webcrack core; but it makes me wonder if there is some kind of generic way we can identify a pattern of these sort of React component generator libraries so that the JSX decompilation can work effectively with them?

Similar'ish prior art from wakaru:

  • https://github.com/pionxzh/wakaru/issues/40
    • https://github.com/pionxzh/wakaru/issues/40#issuecomment-1809704264
    • https://github.com/pionxzh/wakaru/issues/40#issuecomment-1809962543

Looking back at the main format of the styled function (which was Z in the above code):

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L15-L20
    • `styled(tag, forwardRef)

This returns an inner wrapper function, which seems to use tagged template literal syntax to provide the CSS, and then it reads that from the arguments into _args:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L23-L24
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates

It then uses the _args to create the CSS class name:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L41-L43

And then processes the tag (eg. "div") passed to the original function:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L50-L59

Eventually 'rendering' that through the 'pragma' h:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L66

Which was assigned during setup earlier:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L4-L13

Tracing through the code in our bundle to find that 'pragma' function binding, we find t.createElement ends up being assigned to h (or y as it's called in our minified code):

/* ..snip.. */
/* 582      */  (function (e, t, l, i) {
/* 583      */    c.p = undefined;
/* 584      */    y = e;
/* 585      */    p = undefined;
/* 586      */    w = undefined;
/* 587      */  })(t.createElement);
/* ..snip.. */

And of course, we know that t relates to our React global:

/* ..snip.. */
/* 2        */  var t = window.React;
/* ..snip.. */

This obviously ends up going through a few extra steps of more library specific indirection that probably doesn't make sense to be in webcrack core.. but I wonder if we're able to trace/follow the React global / createElement 'pragma' / h through so that JSX decompilation can work correctly?

In the case of this library it also inserts the additional wrapping component Styled in the middle.. but I think if the createElement 'pragma' flowed through properly.. that might end up being properly figured out as nested JSX anyway; as the Styled just ends up wrapping our provided tag component:

  • https://github.com/cristianbote/goober/blob/5f0b43976fac214262c2c8921b1691fc4729ec98/src/styled.js#L69

Originally posted by @0xdevalias in https://github.com/j4k0xb/webcrack/issues/10#issuecomment-2693645060

See Also

  • https://github.com/j4k0xb/webcrack/issues/151

0xdevalias avatar Mar 03 '25 11:03 0xdevalias