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
-
[module-detection / smart-rename]
styled-components
/Tailwind-Styled-Component
libs
-
For the more general case of 'module detection' (and related concepts), see these issues:
- pionxzh/wakaru#41
-
Module detection
-
- pionxzh/wakaru#73
-
add a 'module graph'
-
- pionxzh/wakaru#74
-
explore 'AST fingerprinting' for module/function identification (eg. to assist smart / stable renames, etc)
-
And other similar deep dives / reverse engineering / module identification attempts:
- https://github.com/pionxzh/wakaru/issues?q=%22%5Bmodule-detection%5D%22
- pionxzh/wakaru#50
-
Support detect and replace
swc
'sruntime-helpers
-
- pionxzh/wakaru#55
-
Support detect and replace
microsoft/tslib
'sruntime-helpers
-
- pionxzh/wakaru#79
-
[module-detection]
zustand
- React state management
-
- pionxzh/wakaru#80
-
[module-detection]
js-xss
-
- pionxzh/wakaru#86
-
[module-detection]
statsig-io/js-client
-
- pionxzh/wakaru#87
-
[module-detection]
DataDog/browser-sdk
-
- pionxzh/wakaru#88
-
[module-detection]
TanStack/query
-
- pionxzh/wakaru#89
-
[module-detection]
radix-ui/primitives
-
- ?etc?
- pionxzh/wakaru#50
- https://github.com/0xdevalias/chatgpt-source-watch : Analyzing the evolution of ChatGPT's codebase through time with curated archives and scripts.
- Deobfuscating / Unminifying Obfuscated Web App Code (0xdevalias' gist)
- Reverse Engineering Webpack Apps (0xdevalias' gist)
- React Server Components, Next.js v13+, and Webpack: Notes on Streaming Wire Format (
__next_f
, etc) (0xdevalias' gist)) - Fingerprinting Minified JavaScript Libraries / AST Fingerprinting / Source Code Similarity / Etc (0xdevalias' gist)
- Bypassing Cloudflare, Akamai, etc (0xdevalias' gist)
- Debugging Electron Apps (and related memory issues) (0xdevalias' gist)
- devalias' Beeper CSS Hacks (0xdevalias' gist)
- Reverse Engineering Golang (0xdevalias' gist)
- Reverse Engineering on macOS (0xdevalias' gist)
(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.
(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:
- https://github.com/search?q=repo%3AMathiasGilson%2FTailwind-Styled-Component%20isTwElement%3F&type=code
- https://github.com/search?q=repo%3Aaplr%2Ftailwind-components%20isTwElement%3F&type=code
At first glance, both of these repos also appear to have the same domElements
as above:
- https://github.com/MathiasGilson/Tailwind-Styled-Component/blob/master/src/domElements.ts#L1-L6
- https://github.com/aplr/tailwind-components/blob/main/packages/tailwind-components/src/utils/domElements.ts#L1-L6
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.
(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:
- https://github.com/MathiasGilson/Tailwind-Styled-Component/blob/master/src/tailwind.tsx#L244
- https://github.com/aplr/tailwind-components/blob/main/packages/tailwind-components/src/utils/getComponentName.ts#L7
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
: moduleB
: exportsG
: 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;
},
});
(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. adiv
orspan
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 theJSX.IntrinsicElements
interface.
-
Explaining those types with ChatGPT:
Thanks for sharing the actual
TailwindInterface
. This interface extendsIntrinsicElementsTemplateFunctionsMap
and provides overloads for a generic function. Let's break down its structure:This type maps each key of
JSX.IntrinsicElements
(which represents every possible HTML tag as a string, like'a'
,'div'
,'span'
, etc.) to aTemplateFunction
specific to that tag's type. TheTemplateFunction
is a generic type that likely encapsulates the logic for creating styled components for each intrinsic element.This interface extends
IntrinsicElementsTemplateFunctionsMap
, meaning it includes all mappings from that type. Additionally, it provides three generic function signatures:
For Tailwind Components:
- This overload is for when the
component
is a Tailwind-specific component. TheTemplateFunction
returned is specific to the props of the Tailwind component.TailwindComponentInnerProps<C>
andTailwindComponentInnerOtherProps<C>
are likely types that extract or define the props relevant to a Tailwind component.For General React Components:
- This overload handles cases where the
component
is a general React component. TheTemplateFunction
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.For Intrinsic Elements:
- This is similar to the mappings in
IntrinsicElementsTemplateFunctionsMap
, but it's defined as a function overload. It allows calling theTailwindInterface
function with an intrinsic element (like'div'
,'a'
, etc.) as the argument, returning aTemplateFunction
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 inJSX.IntrinsicElements
. This design makes theTailwindInterface
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 exportedTailwindInterface
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:
- Both
m
andp
are memoization functions. They use(0, r._)(...)
to compute a class string (presumably using a library likeclsx
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.
b
andy
are styled components created usingd.Z.div
andd.Z.span
respectively.d.Z
refers toTailwindInterface
, anddiv
andspan
are specifying the type of HTML element to style.
b
is a styleddiv
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]"
.
y
is a styledspan
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"
.
- The code is utilizing the
TailwindInterface
to create two styled components,b
(adiv
) andy
(aspan
), 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
andp
ensures that the base styles are only computed once, improving performance.
(Ref)
Looking back at the main repo/usage docs for Tailwind-Styled-Component
:
- https://github.com/MathiasGilson/Tailwind-Styled-Component
- https://github.com/MathiasGilson/Tailwind-Styled-Component#usage
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
?
(Ref)
Looking at the Tailwind-Styled-Component
package.json
, we can see that Tailwind-Styled-Component
relies on tailwind-merge
:
- https://github.com/MathiasGilson/Tailwind-Styled-Component/blob/master/package.json#L64-L66
- https://github.com/dcastil/tailwind-merge
-
Utility function to efficiently merge Tailwind CSS classes in JS without style conflicts.
-
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:
- https://github.com/JedWatson/classnames
-
A simple javascript utility for conditionally joining classNames together
-
- https://github.com/lukeed/clsx
-
A tiny (228B) utility for constructing
className
strings conditionally.
-
We can find the definition of twMerge
in the code here:
- https://github.com/dcastil/tailwind-merge/blob/main/src/lib/tw-merge.ts#L3
export const twMerge = createTailwindMerge(getDefaultConfig)
- https://github.com/dcastil/tailwind-merge/blob/main/src/lib/create-tailwind-merge.ts
createTailwindMerge
- https://github.com/dcastil/tailwind-merge/blob/main/src/lib/default-config.ts
getDefaultConfig
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.
(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.
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.
(Ref)
@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..