Open 0xdevalias opened 1 year ago
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.
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
: moduleB
: exportsG
: requireThe 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;
},
});
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
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:
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:
IntrinsicElementsTemplateFunctionsMap
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.
TailwindInterface
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:Functions
m
andp
- 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.Creating Styled Components
b
andy
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.Component
b
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]"
.Component
y
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"
.Summary
- 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.
Supporting this would be quite challenging to achieve because of the form of it. PRs are welcome if anyone is interested in this.
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':
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
?
Looking at the Tailwind-Styled-Component
package.json
, we can see that Tailwind-Styled-Component
relies on 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:
A simple javascript utility for conditionally joining classNames together
A tiny (228B) utility for constructing
className
strings conditionally.
We can find the definition of twMerge
in the code here:
export const twMerge = createTailwindMerge(getDefaultConfig)
createTailwindMerge
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:
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.
@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 (https://github.com/pionxzh/wakaru/issues/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.
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..
Nice finding for twMerge and the x.y.[domElement]
, I will try to work on some POC next week.
Edit: This comment has been replicated/referenced in the following more specifically relevant issue here:
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 https://github.com/pionxzh/wakaru/issues/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"
`;
This relates to the 'module-detection' feature described in the following issue:
41
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: