Skip to content
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

[module-detection / smart-rename] styled-components / Tailwind-Styled-Component libs #40

Open
Tracked by #41
0xdevalias opened this issue Nov 14, 2023 · 10 comments
Labels
enhancement New feature or request

Comments

@0xdevalias
Copy link

0xdevalias commented Nov 14, 2023

This relates to the 'module-detection' feature described in the following issue:


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.


This was originally based on the code I mentioned in the following comment:

A more general high level version of 'module detection' that this feature relates to is described in:


Edit: I've also captured all of my below notes on the following gist for easier future reference:

@0xdevalias
Copy link
Author

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.

@0xdevalias
Copy link
Author

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;
  },
});

@0xdevalias
Copy link
Author

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.

@pionxzh pionxzh mentioned this issue Nov 14, 2023
8 tasks
@pionxzh
Copy link
Owner

pionxzh commented Nov 14, 2023

Supporting this would be quite challenging to achieve because of the form of it. PRs are welcome if anyone is interested in this.

@0xdevalias
Copy link
Author

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?

@0xdevalias
Copy link
Author

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.

@0xdevalias
Copy link
Author

@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 (#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.

@0xdevalias
Copy link
Author

0xdevalias commented Nov 14, 2023

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

@pionxzh
Copy link
Owner

pionxzh commented Nov 14, 2023

Nice finding for twMerge and the x.y.[domElement], I will try to work on some POC next week.

@0xdevalias
Copy link
Author

0xdevalias commented Nov 20, 2023

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

@pionxzh I just had another idea about this, based on some of my deep diving into @swc/helpers tonight (see #50).. and I think this is actually another swc related transpilation; related to template literals.

Using the swc playground:

If I pass in some code like this:

const foo = bar`
  staticOne
  staticTwo
  ${dynamicOne}
  ${dynamicTwo}
  staticThree
  ${dynamicThree}
`

It transpiles to this:

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

function _templateObject() {
    var data = _tagged_template_literal([
        "\n  staticOne\n  staticTwo\n  ",
        "\n  ",
        "\n  staticThree\n  ",
        "\n"
    ]);
    _templateObject = function _templateObject() {
        return data;
    };
    return data;
}

var foo = bar(_templateObject(), dynamicOne, dynamicTwo, dynamicThree);

The _tagged_template_literal function comes from @swc/helpers:

Whereas the _templateObject is generated from our input data; and seems to follow the same 'self memoising function' pattern that I identified earlier in the webpacked code.

Looking at the signature for tagged template functions:

We can see that they take a strings param (represented by the memoised _templateObject), followed by a param for each dynamic expression; which we can see is what happens on the final line:

var foo = bar(_templateObject(), dynamicOne, dynamicTwo, dynamicThree);

Based on that, we could 're-symbolise' that webpacked code as:

function _templateObjectP() {
  var data = _tagged_template_literal([
    "\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",
  ]);

  _templateObjectP = function _templateObject() {
    return data;
  };

  return data;
}

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

Which we could then 'normalise' back to the original tagged template literal syntax as:

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
  function (e) {
    return "warning" === e.$type && "bg-orange-500 text-white";
  }
  function (e) {
    return "danger" === e.$type && "bg-red-500 text-white";
  }
`;

Or even simplify it further to just:

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) => "warning" === e.$type && "bg-orange-500 text-white"
  (e) => "danger" === e.$type && "bg-red-500 text-white"
`;

ChatGPT Explanation

The provided code snippet demonstrates how a tagged template literal is transpiled by SWC, a JavaScript/TypeScript compiler. Let's break down how it works:

Tagged Template Literals in JavaScript

Before diving into the transpiled code, it's important to understand what tagged template literals are in JavaScript. They allow you to parse template literals with a function. The syntax looks like this:

const result = tagFunction`string text ${expression} string text`;

In this syntax, tagFunction is a function that gets called with the parts of the template literal.

Transpiled Code Breakdown

The _tagged_template_literal Function

function _tagged_template_literal(strings, raw) {
    if (!raw) {
        raw = strings.slice(0);
    }
    return Object.freeze(Object.defineProperties(strings, {
        raw: {
            value: Object.freeze(raw)
        }
    }));
}
  • Purpose: This function is used to process the template strings and their raw representations.
  • Parameters:
    • strings: An array of string values from the template literal.
    • raw: The raw string representations (as if backslashes in the strings were not processed as escape characters).
  • Process:
    • If raw is not provided, it creates a copy of strings.
    • It then freezes the strings array and adds a raw property to it, containing the frozen raw array.
  • Freezing: Object.freeze is used to make sure that the arrays cannot be altered, which is important for template literals to maintain their integrity.

The _templateObject Function

function _templateObject() {
    var data = _tagged_template_literal([
        "\n  staticOne\n  staticTwo\n  ",
        "\n  ",
        "\n  staticThree\n  ",
        "\n"
    ]);
    _templateObject = function _templateObject() {
        return data;
    };
    return data;
}
  • Purpose: This function initializes the template strings and caches them.
  • Process:
    • It calls _tagged_template_literal with the static parts of the template literal.
    • The function is then overridden to return the cached data on subsequent calls. This ensures that the template strings are only processed once and then reused, which improves performance.

Usage in the Template Tag

var foo = bar(_templateObject(), dynamicOne, dynamicTwo, dynamicThree);
  • bar is the tag function.
  • _templateObject() returns the processed and frozen template strings.
  • dynamicOne, dynamicTwo, and dynamicThree are the dynamic expressions passed to the tag function.

Summary

The transpilation process converts a tagged template literal into a form where the template strings are processed and frozen for immutability and performance. The tag function then receives these processed strings and any dynamic values as arguments. This approach maintains the integrity of template literals while optimizing them for repeated use.

@pionxzh pionxzh added the enhancement New feature or request label Nov 21, 2023
@0xdevalias 0xdevalias changed the title smart-rename/similar for styled-components / Tailwind-Styled-Component libs [module-detection / smart-rename] styled-components / Tailwind-Styled-Component libs Dec 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants