Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 0xdevalias/916e4ababd3cb5e3470b07a024cf3125 to your computer and use it in GitHub Desktop.
Save 0xdevalias/916e4ababd3cb5e3470b07a024cf3125 to your computer and use it in GitHub Desktop.
Deep Dive into reverse engineering how some webpacked code relates to the styled-components / Tailwind-Styled-Component libs

Reverse Engineered Webpack Tailwind-Styled-Component

Deep Dive into reverse engineering how some webpacked code relates to the styled-components / Tailwind-Styled-Component libs.

This was originally posted on the following issue, and the heading structure here will relate to the individual comments that were made there:

  • pionxzh/wakaru#40
    • smart-rename/similar for styled-components / Tailwind-Styled-Component libs

Table of Contents

See Also

My Other Related Deepdive Gist's and Projects

Comment 1

(Ref)

While looking through some decompiled code in a rather complex webpack bundled app, I've identified what seems to be the styled-components library (or something very similar to it):

It would be cool to be able to handle styled-components when reversing code.

Comment 2

(Ref)

The webpacked code that includes this (in this app) is in the following chunk:

And within this code, it specifically seems to be in 34303: function (U, B, G) { (which unpacks in this tool to module-34303.js)

Within the original code for that module, I identified a section of code that looks like this:

tU = [
  "a",
  abbr",
  "address",
  "area",
  "article",
// ..snip..

Which at first I manually correlated with the following from styled-components:

const elements = [
  'a',
  'abbr',
  'address',
  'area',
  'article',

But then later found this code:

tB = Symbol("isTwElement?"),

Which I then searched for on GitHub code search:

That seemed to lead me to these 2 repos:

At first glance, both of these repos also appear to have the same domElements as above:

But after accounting for differences in spacing, quotes, etc; and diffing them, it looks like the Tailwind-Styled-Components / tailwind-components libs have extra entries for head / title that styled-components doesn't have, and styled-components has a use entry that the other two don't have.

Based on this, we can compare against the code in our webpack bundled code, and see that it also has head / title, and is missing use; implying that it is one of the Tailwind Styled Components libs.

Comment 3

(Ref)

Right at the top of our webpacked code we see this Z wrapper that returns tq:

34303: function (U, B, G) {
      "use strict";
      G.d(B, {
        Z: function () {
          return tq;
        },
      });
// ..snip..

We find tq right at the bottom of this module:

// ..snip..
              return (
                (J[tB] = !0),
                "string" != typeof U
                  ? (J.displayName = U.displayName || U.name || "tw.Component")
                  : (J.displayName = "tw." + U),
                (J.withStyle = (U) => V(Z.concat(U))),
                J
              );
            };
            return V();
          },
        t$ = tU.reduce((U, B) => ({ ...U, [B]: tz(B) }), {}),
        tq = Object.assign(tz, t$);
    },

We can see some code here that sets displayName to tw.Component as a fallback. Searching those 2 tailwind repo's for tw.Component leads us to the following files:

Contrasting the function code that contains the tw.Component string with our webpacked code, it looks like it the webpacked code is using Tailwind-Styled-Component.

Looking at the end of the code in that file, we can see how it correlates with t$ / tq above:

const intrinsicElementsMap: IntrinsicElementsTemplateFunctionsMap = domElements.reduce(
    <K extends IntrinsicElementsKeys>(acc: IntrinsicElementsTemplateFunctionsMap, DomElement: K) => ({
        ...acc,
        [DomElement]: templateFunctionFactory(DomElement)
    }),
    {} as IntrinsicElementsTemplateFunctionsMap
)

const tw: TailwindInterface = Object.assign(templateFunctionFactory, intrinsicElementsMap)

export default tw

A typical webpack module when unminimised has the following basic structure:

function(module, exports, require) {
  // Module code goes here
}

We can see how that maps to our webpacked code:

34303: function (U, B, G) {

This means that:

  • U: module
  • B: exports
  • G: require

The Tailwind-Styled-Component code above ends in export default tw, and in our webpacked code we can see that it essentially exports the TailWindInterface as Z:

G.d(B, {
  Z: function () {
    return tq;
  },
});

Comment 4

(Ref)

Based on this knowledge, we can now find references to Tailwind-Styled-Component across the webpacked code by looking for an import of the module containing it (in this case: 34303); and then looking for the name it was exported with (in this case: Z)

Looking at a different chunk file that imports 34303:

We can find a module that uses 34303 like the following:

46110: function (e, t, n) {
  // ..snip..
  var r = n(4337),
  // ..snip..
    d = n(34303),
  // ..snip..
  var b = d.Z.div(m(), function (e) {
      return e.$isMessageRedesign
        ? "rounded-full h-7 w-7"
        : "rounded-sm h-[30px] w-[30px]";
    }),
    y = d.Z.span(
      p(),
      function (e) {
        return "warning" === e.$type && "bg-orange-500 text-white";
      },
      function (e) {
        return "danger" === e.$type && "bg-red-500 text-white";
      }
    ),
  // ..snip..

We can see that the 34303 module is imported as d, and then the Tailwind-Styled-Component TailWindInterface is accessed as:

  • d.Z.div
  • d.Z.span
  • etc

Looking back at how TailWindInterface is defined (Ref), we can see that it first reduces domElements (Ref) to intrinsicElementsMap; then Object.assign's that to templateFunctionFactory:

const intrinsicElementsMap: IntrinsicElementsTemplateFunctionsMap = domElements.reduce(
    <K extends IntrinsicElementsKeys>(acc: IntrinsicElementsTemplateFunctionsMap, DomElement: K) => ({
        ...acc,
        [DomElement]: templateFunctionFactory(DomElement)
    }),
    {} as IntrinsicElementsTemplateFunctionsMap
)

const tw: TailwindInterface = Object.assign(templateFunctionFactory, intrinsicElementsMap)

export default tw

We can also see the type definition for TailwindInterface:

export type IntrinsicElementsTemplateFunctionsMap = {
    [RTag in keyof JSX.IntrinsicElements]: TemplateFunction<JSX.IntrinsicElements[RTag]>
}

export interface TailwindInterface extends IntrinsicElementsTemplateFunctionsMap {
    <C extends TailwindComponent<any, any>>(component: C): TemplateFunction<
        TailwindComponentInnerProps<C>,
        TailwindComponentInnerOtherProps<C>
    >
    <C extends React.ComponentType<any>>(component: C): TemplateFunction<
        // Prevent functional components without props infering props as `unknown`
        C extends (P?: never) => any ? {} : React.ComponentPropsWithoutRef<C>
    >

    <C extends keyof JSX.IntrinsicElements>(component: C): TemplateFunction<JSX.IntrinsicElements[C]>
}

We can read about JSX.IntrinsicElements in TypeScript here:

  • https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements
    • In order to understand type checking with JSX, you must first understand the difference between intrinsic elements and value-based elements. Given a JSX expression <expr />, expr may either refer to something intrinsic to the environment (e.g. a div or span in a DOM environment) or to a custom component that you’ve created.

    • TypeScript uses the same convention that React does for distinguishing between these. An intrinsic element always begins with a lowercase letter, and a value-based element always begins with an uppercase letter.

    • Intrinsic elements are looked up on the special interface JSX.IntrinsicElements. By default, if this interface is not specified, then anything goes and intrinsic elements will not be type checked. However, if this interface is present, then the name of the intrinsic element is looked up as a property on the JSX.IntrinsicElements interface.

Explaining those types with ChatGPT:

Thanks for sharing the actual TailwindInterface. This interface extends IntrinsicElementsTemplateFunctionsMap and provides overloads for a generic function. Let's break down its structure:

IntrinsicElementsTemplateFunctionsMap

This type maps each key of JSX.IntrinsicElements (which represents every possible HTML tag as a string, like 'a', 'div', 'span', etc.) to a TemplateFunction specific to that tag's type. The TemplateFunction is a generic type that likely encapsulates the logic for creating styled components for each intrinsic element.

TailwindInterface

This interface extends IntrinsicElementsTemplateFunctionsMap, meaning it includes all mappings from that type. Additionally, it provides three generic function signatures:

  1. For Tailwind Components:

    • This overload is for when the component is a Tailwind-specific component. The TemplateFunction returned is specific to the props of the Tailwind component.
    • TailwindComponentInnerProps<C> and TailwindComponentInnerOtherProps<C> are likely types that extract or define the props relevant to a Tailwind component.
  2. For General React Components:

    • This overload handles cases where the component is a general React component. The TemplateFunction generated is based on the component's props.
    • The comment indicates special handling to prevent functional components without props from having their props inferred as unknown. Instead, it defaults to an empty object ({}) if the component does not accept any props.
  3. For Intrinsic Elements:

    • This is similar to the mappings in IntrinsicElementsTemplateFunctionsMap, but it's defined as a function overload. It allows calling the TailwindInterface function with an intrinsic element (like 'div', 'a', etc.) as the argument, returning a TemplateFunction for that specific element.

These overloads provide flexibility in how the TailwindInterface can be used. It supports creating styled components for both specific Tailwind components and general React components, as well as for any standard HTML element represented in JSX.IntrinsicElements. This design makes the TailwindInterface highly adaptable and usable across different types of components in a React application using Tailwind CSS for styling.

Based on that interface, we can now better understand what the d.Z.div / d.Z.span code from above is doing:

// ..snip..

      function m() {
        var e = (0, r._)([
          "relative p-1 ",
          " text-white flex items-center justify-center",
        ]);
        return (
          (m = function () {
            return e;
          }),
          e
        );
      }

      function p() {
        var e = (0, r._)([
          "\n  absolute w-4 h-4 rounded-full text-[10px] text-white flex  justify-center items-center right-0 top-[20px] -mr-2 border border-white\n  ",
          "\n  ",
          "\n",
        ]);
        return (
          (p = function () {
            return e;
          }),
          e
        );
      }

// ..snip..

  var b = d.Z.div(m(), function (e) {
      return e.$isMessageRedesign
        ? "rounded-full h-7 w-7"
        : "rounded-sm h-[30px] w-[30px]";
    }),

    y = d.Z.span(
      p(),
      function (e) {
        return "warning" === e.$type && "bg-orange-500 text-white";
      },
      function (e) {
        return "danger" === e.$type && "bg-red-500 text-white";
      }
    ),

Explained by ChatGPT:

In the provided code, d.Z corresponds to the exported TailwindInterface from your earlier message. The code is creating styled components using this interface, with specific styles and conditional styling based on props. Let's break down each part:

Functions m and p

  • Both m and p are memoization functions. They use (0, r._)(...) to compute a class string (presumably using a library like clsx for conditional and combined class names) and then redefine themselves to always return this computed class string in subsequent calls. This is a form of memoization to ensure the class string is only computed once.

Creating Styled Components b and y

  • b and y are styled components created using d.Z.div and d.Z.span respectively. d.Z refers to TailwindInterface, and div and span are specifying the type of HTML element to style.

Component b

  • b is a styled div element.
  • It starts with a base style generated by the m() function.
  • It also includes a function that adds conditional styling based on the $isMessageRedesign prop. If $isMessageRedesign is true, it adds "rounded-full h-7 w-7"; otherwise, it adds "rounded-sm h-[30px] w-[30px]".

Component y

  • y is a styled span element.
  • It starts with a base style generated by the p() function.
  • It has two additional functions for conditional styling based on the $type prop:
    • If $type is "warning", it adds "bg-orange-500 text-white".
    • If $type is "danger", it adds "bg-red-500 text-white".

Summary

  • The code is utilizing the TailwindInterface to create two styled components, b (a div) and y (a span), with base styles and additional conditional styles based on props.
  • The styling is dynamic, responding to props like $isMessageRedesign and $type, allowing these components to adapt their styles based on the context in which they are used.
  • The memoization in m and p ensures that the base styles are only computed once, improving performance.

Comment 5

(Ref)

Looking back at the main repo/usage docs for Tailwind-Styled-Component:

We can see that there are multiple ways of writing a styled component, including:

// Basic
const Container = tw.div`
    flex
    items-center
    // ..snip..
`

// Conditional class names
const Button = tw.button`
    flex
    ${(p) => (p.$primary ? "bg-indigo-600" : "bg-indigo-300")}
`

// etc

Along with some other potentially relevant notes:

Tailwind Styled Components supports Transient Props

Prefix the props name with a dollar sign ($) to prevent forwarding them to the DOM element

These usage examples are making use of JavaScript's Template Literals 'Tagged templates':

  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates
    • Tags allow you to parse template literals with a function. The first argument of a tag function contains an array of string values. The remaining arguments are related to the expressions.

    • The tag function can then perform whatever operations on these arguments you wish, and return the manipulated string. (Alternatively, it can return something completely different, as described in one of the following examples.)

    • Tag functions don't even need to return a string!

This will essentially end up routing through the TailwindInterface to the templateFunctionFactory (Ref)

const templateFunctionFactory: TailwindInterface = (<C extends React.ElementType>(Element: C): any => {
    return (template: TemplateStringsArray, ...templateElements: ((props: any) => string | undefined | null)[]) => {
// ..snip..

We can see that this function is a template literal 'tagged template' function that receives the static strings in the template param, and then all of the dynamic strings in the templateElements param.

I couldn't find much specifically about TemplateStringsArray, but here is 1 issue related to it, showing that it's a TypeScript thing:


Using the above examples from the README in the Babel REPL gives transformed code like this:

var _templateObject, _templateObject2;

function _taggedTemplateLiteral(strings, raw) {
  if (!raw) {
    raw = strings.slice(0);
  }
  return Object.freeze(
    Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })
  );
}

// Basic
var Container = tw.div(
  _templateObject ||
    (_templateObject = _taggedTemplateLiteral([
      "\n    flex\n    items-center\n    // ..snip..\n",
    ]))
);

// Conditional class names
var Button = tw.button(
  _templateObject2 ||
    (_templateObject2 = _taggedTemplateLiteral(["\n    flex\n    ", "\n"])),
  function (p) {
    return p.$primary ? "bg-indigo-600" : "bg-indigo-300";
  }
);

// etc

We can see how this code looks a lot like the earlier code from our webpacked app, though the babel code implicitly concatenates the template literal strings as part of it's transform, whereas our webpacked code receives them as an array (as per the JS standard), and then passes them to a helper function that seems to concatenate them (potentially something like classnames / clsx / similar; see notes above+later on for more on this):

// ..snip..

      function p() {
        var e = (0, r._)([
          "\n  absolute w-4 h-4 rounded-full text-[10px] text-white flex  justify-center items-center right-0 top-[20px] -mr-2 border border-white\n  ",
          "\n  ",
          "\n",
        ]);
        return (
          (p = function () {
            return e;
          }),
          e
        );
      }

// ..snip..

    y = d.Z.span(
      p(),
      function (e) {
        return "warning" === e.$type && "bg-orange-500 text-white";
      },
      function (e) {
        return "danger" === e.$type && "bg-red-500 text-white";
      }
    ),

If we were to manually re-write this back to how it would have looked in it's template literal form (ignoring the memoisation it does), it would have been something like this:

y = d.Z.span`
  absolute
  w-4
  h-4
  rounded-full
  text-[10px]
  text-white
  flex
  justify-center
  items-center
  right-0
  top-[20px]
  -mr-2
  border
  border-white

  ${(e) => (e.$type === "warning" && "bg-orange-500 text-white")}
  ${(e) => (e.$type === "danger" && "bg-red-500 text-white")}
`

Looking at where template and templateElements are processed within templateFunctionFactory; they're nested deeper within the TwComponentConstructor -> TwComponent -> in the JSX that returns FinalElement, specifically in the className prop:

// ..snip..

return (
    <FinalElement
        // ..snip..

        // set class names
        className={cleanTemplate(
            mergeArrays(
                template,
                templateElements.map((t) => t({ ...props, $as }))
            ),
            props.className
        )}

        // ..snip..
    />
)

// ..snip..

We can see that mergeArrays is called with template and templateElements.map((t) => t({ ...props, $as })); which essentially merges the 2 arrays (while handling falsy values):

export const mergeArrays = (template: TemplateStringsArray, templateElements: (string | undefined | null)[]) => {
    return template.reduce(
        (acc, c, i) => acc.concat(c || [], templateElements[i] || []), //  x || [] to remove false values e.g '', null, undefined. as Array.concat() ignores empty arrays i.e []
        [] as string[]
    )
}

We can then see that the result of that is passed to cleanTemplate; which does some further cleanup of the result returned from mergeArrays (template) and inheritedClasses, then passes them to twMerge (from tailwind-merge):

export const cleanTemplate = (template: Array<Interpolation<any>>, inheritedClasses: string = "") => {
    const newClasses: string[] = template
        .join(" ")
        .trim()
        .replace(/\n/g, " ") // replace newline with space
        .replace(/\s{2,}/g, " ") // replace line return by space
        .split(" ")
        .filter((c) => c !== ",") // remove comma introduced by template to string

    const inheritedClassesArray: string[] = inheritedClasses ? inheritedClasses.split(" ") : []

    return twMerge(
        ...newClasses
            .concat(inheritedClassesArray) // add new classes to inherited classes
            .filter((c: string) => c !== " ") // remove empty classes
    )
}

Neither mergeArrays nor cleanTemplate appear to do any memoisation on the template string data, so presumably that pattern is happening somewhere later on still.. perhaps within twMerge?

Comment 6

(Ref)

Looking at the Tailwind-Styled-Component package.json, we can see that Tailwind-Styled-Component relies on tailwind-merge:

Looking at the tailwind-merge API reference:

We can see that the 2 main functions appear to be:

function twMerge(
    ...classLists: Array<string | undefined | null | false | 0 | typeof classLists>
): string

Default function to use if you're using the default Tailwind config or are close enough to the default config.

If twMerge doesn't work for you, you can create your own custom merge function with extendTailwindMerge.

function twJoin(
    ...classLists: Array<string | undefined | null | false | 0 | typeof classLists>
): string

Function to join className strings conditionally without resolving conflicts.

It is used internally within twMerge and a direct subset of clsx. If you use clsx or classnames to apply Tailwind classes conditionally and don't need support for object arguments, you can use twJoin instead, it is a little faster and will save you a few hundred bytes in bundle size.

From these function signatures, and the description text of twJoin, we can see that this lib is quite similar (at least in API) to classnames / clsx / etc:

We can find the definition of twMerge in the code here:

Looking at createTailwindMerge, we can see that it returns a function, that wraps calling the functionToCall function. The first time that is accessed, it will map to initTailwindMerge, then the next time it's called it will map to tailwindMerge:

export function createTailwindMerge(
    createConfigFirst: CreateConfigFirst,
    ...createConfigRest: CreateConfigSubsequent[]
): TailwindMerge {
    let configUtils: ConfigUtils
    let cacheGet: ConfigUtils['cache']['get']
    let cacheSet: ConfigUtils['cache']['set']
    let functionToCall = initTailwindMerge

    function initTailwindMerge(classList: string) {
        const config = createConfigRest.reduce(
            (previousConfig, createConfigCurrent) => createConfigCurrent(previousConfig),
            createConfigFirst() as GenericConfig,
        )

        configUtils = createConfigUtils(config)
        cacheGet = configUtils.cache.get
        cacheSet = configUtils.cache.set
        functionToCall = tailwindMerge

        return tailwindMerge(classList)
    }

    function tailwindMerge(classList: string) {
        const cachedResult = cacheGet(classList)

        if (cachedResult) {
            return cachedResult
        }

        const result = mergeClassList(classList, configUtils)
        cacheSet(classList, result)

        return result
    }

    return function callTailwindMerge() {
        return functionToCall(twJoin.apply(null, arguments as any))
    }
}

This looks quite similar to the memoisation pattern in sections of our webpacked code, for example:

function p() {
  var e = (0, r._)([
    "\n  absolute w-4 h-4 rounded-full text-[10px] text-white flex  justify-center items-center right-0 top-[20px] -mr-2 border border-white\n  ",
    "\n  ",
    "\n",
  ]);
  return (
    (p = function () {
      return e;
    }),
    e
  );
}

Though while it shares a similar sort of memoisation pattern; it doesn't seem to actually be the same code.

Here are some references for tailwind-merge's memoisation/caching:

  • https://github.com/dcastil/tailwind-merge/blob/main/docs/features.md#performance
    • Results get cached by default, so you don't need to worry about wasteful re-renders. The library uses a computationally lightweight LRU cache which stores up to 500 different results by default. The cache is applied after all arguments are joined together to a single string. This means that if you call twMerge repeatedly with different arguments that result in the same string when joined, the cache will be hit.


Thinking more about the structure of the webpacked code from Tailwind-Styled-Component.. and how it calls the memoised code above..

y = d.Z.span(
  p(),
  function (e) {
    return "warning" === e.$type && "bg-orange-500 text-white";
  },
  function (e) {
    return "danger" === e.$type && "bg-red-500 text-white";
  }
),

..it kind of feels like the memoisation could be happening at a higher layer than tailwind-merge, and possibly even higher than Tailwind-Styled-Component..

I wonder if something in the webpack minimisation process is applying a memo to the text passed to the template tags; or perhaps this might even be something that is being done manually in the webpacked app itself.

Comment 7

(Ref)

@pionxzh Obviously all of the above deep dive research is a LOT, and I wouldn't expect you to read it all in depth right now, but based on what I discovered above, I think it might be possible to make some simple'ish inferences (though without being as robust as perfectly matching the module first (pionxzh/wakaru#41)).

Here's the first one, and i'll add the other one in a new comment after this.

Smart-Rename for 'function replaces self' memoisation pattern

We could potentially detect memoisation patterns like the following, and rename the function something more useful:

function p() {
  var e = (0, r._)([
    "\n  absolute w-4 h-4 rounded-full text-[10px] text-white flex  justify-center items-center right-0 top-[20px] -mr-2 border border-white\n  ",
    "\n  ",
    "\n",
  ]);
  return (
    (p = function () {
      return e;
    }),
    e
  );
}

Here's some basic code that ChatGPT generated for this:

const jscodeshift = require('jscodeshift').withParser('babylon');

const sourceCode = `TODO` // TODO: include the source code to be processed here

const ast = jscodeshift(sourceCode);

ast.find(jscodeshift.FunctionDeclaration)
  .forEach(path => {
    // Check if this function reassigns itself
    const hasSelfReassignment = jscodeshift(path)
      .find(jscodeshift.AssignmentExpression)
      .some(assignmentPath => {
        const left = assignmentPath.value.left;
        return left.type === 'Identifier' && left.name === path.value.id.name;
      });

    if (hasSelfReassignment) {
      const oldName = path.value.id.name
      const newName = `${path.value.id.name}Memo`

      // Rename the function
      path.value.id.name = newName;
      
      console.log(`Function ${oldName} is using a memoization pattern, renamed to ${newName}.`);
    } else {
      console.log(`Function ${path.value.id.name} is NOT using a memoization pattern.`);
    }
  });

// Further transformation code and printing the modified source code

You can see it in a REPL here:

The current output is something like this:

$ node jscodeshift-detect-self-memoize-function.js 

Function p is using a memoization pattern, renamed to pMemo.
Function q is NOT using a memoization pattern.

This could use the standard 'rename function' code that wakaru already uses to assign it a better name.

Comment 8

(Ref)

Smart rename for styled-components'ish patterns

@pionxzh As per my last comment, here is the other smart-rename'ish pattern that might be useful here:

While it wouldn't be fully robust unless we could guarantee the imported library (see #41), it seems that both styled-components and Tailwind-Styled-Component use a similar pattern of mapping over a set of standard DOM element names (Ref) to create their basic components.

In my example webpack code, this resulted in code that looked like the following:

var b = d.Z.div(m(), function (e) {
  return e.$isMessageRedesign
    ? "rounded-full h-7 w-7"
    : "rounded-sm h-[30px] w-[30px]";
}),

y = d.Z.span(
  p(),
  function (e) {
    return "warning" === e.$type && "bg-orange-500 text-white";
  },
  function (e) {
    return "danger" === e.$type && "bg-red-500 text-white";
  }
),

My assumption is that this code will always end up being accessed by x.y.[domElement], where x and y could be any arbitrary identifier; and domElement is a name from the following list (or similar, depending on which lib it is):

Based on those assumptions, we should be able to use some AST code like the following to detect usages of styled-components'ish patterns:

const jscodeshift = require('jscodeshift').withParser('babylon');

const sourceCode = `
function m() {
  var e = (0, r._)(["foo", "bar"]);
  return (
    (m = function () {
      return e;
    }),
    e
  );
}

function p() {
  var e = (0, r._)(["foo", "bar", "baz"]);
  return (
    (p = function () {
      return e;
    }),
    e
  );
}

var b = x.y.div(m(), function (e) {
    return e.$isMessageRedesign
      ? "rounded-full h-7 w-7"
      : "rounded-sm h-[30px] w-[30px]";
  }),
  y = x.y.span(
    p(),
    function (e) {
      return "warning" === e.$type && "bg-orange-500 text-white";
    },
    function (e) {
      return "danger" === e.$type && "bg-red-500 text-white";
    }
  );

const x0 = div("foo", (e) => "bar")
const x1 = a1.div("foo", (e) => "bar")
const x2 = a1.b1.div("foo", (e) => "bar")
const x3 = a1.b1.c1.div("foo", (e) => "bar")

const y0 = notAnElement("foo", (e) => "bar")
const y1 = a1.notAnElement("foo", (e) => "bar")
const y2 = a1.b1.notAnElement("foo", (e) => "bar")
const y3 = a1.b1.c1.notAnElement("foo", (e) => "bar")
`;

const domElements = [
  'a',
  'abbr',
  // ..snip..
  'div',
  // ..snip..
  'span',
  // ..snip..
];

const ast = jscodeshift(sourceCode);

ast.find(jscodeshift.CallExpression)
.forEach(path => {
  // Check if the callee is a MemberExpression
  if (path.value.callee.type === 'MemberExpression') {
    const memberExp = path.value.callee;

    // Check if the object of the MemberExpression is also a MemberExpression
    if (memberExp.object.type === 'MemberExpression') {
      const innerMemberExp = memberExp.object;

      // Ensure that the object of the inner MemberExpression is not another MemberExpression
      if (innerMemberExp.object.type !== 'MemberExpression' &&
          domElements.includes(memberExp.property.name)) {
        console.log(`Found styled-components'ish pattern ${innerMemberExp.object.name}.${innerMemberExp.property.name}.${memberExp.property.name}()`);

        // Transform CallExpression to TaggedTemplateExpression
        const args = path.value.arguments;

        // The first item in quasis is the static text before the first expression, the first item in expressions is the first dynamic expression, the second item in quasis is the static text after the first expression and before the second expression, and so on.
        const expressions = [];
        const quasis = [];
        
        args.forEach((arg, index) => {
          let value;

          const isFirst = index === 0;
          const isLast = index === args.length - 1;

          const prefix = isFirst ? '\n  ' : '\n  '
          const suffix = isLast ? '\n' : '\n  '
          
          if (arg.type === 'StringLiteral') {
            // Directly include string literals in the template
            value = { raw: `${prefix}${arg.value}${suffix}`, cooked: `${prefix}${arg.value}${suffix}` };
            quasis.push(jscodeshift.templateElement(value, false));
          } else {
            if (isFirst) {
              value = { raw: prefix, cooked: prefix };
              quasis.push(jscodeshift.templateElement(value, isLast));
            }

            value = { raw: suffix, cooked: suffix };
            quasis.push(jscodeshift.templateElement(value, isLast));
            
            // For non-string expressions, place them in ${}
            expressions.push(arg);
          }
        });
        
        const taggedTemplateExp = jscodeshift.taggedTemplateExpression(
          memberExp,
          jscodeshift.templateLiteral(quasis, expressions)
        );

        // Replace the original CallExpression with the new TaggedTemplateExpression
        jscodeshift(path).replaceWith(taggedTemplateExp);
      }
    }
  }
});

const newSourceCode = ast.toSource();
console.log("---");
console.log("Rewritten code:");
console.log(newSourceCode);

You can see it in a REPL here:

The current output is something like this:

$ node jscodeshift-detect-styled-components.js 

Found styled-components'ish pattern x.y.div()
Found styled-components'ish pattern x.y.span()
Found styled-components'ish pattern a1.b1.div()
---
Rewritten code:

// ..snip..

var b = x.y.div`
  ${m()}
  ${function (e) {
  return e.$isMessageRedesign
    ? "rounded-full h-7 w-7"
    : "rounded-sm h-[30px] w-[30px]";
}}
`;
  
var y = x.y.span`
  ${p()}
  ${function (e) { return "warning" === e.$type && "bg-orange-500 text-white"; }}
  ${function (e) { return "danger" === e.$type && "bg-red-500 text-white"; }}
`;

// ..snip..

const x2 = a1.b1.div`
  foo
  ${(e) => "bar"}
`

// ..snip..
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment