-
-
Notifications
You must be signed in to change notification settings - Fork 58
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
feat(compiler): static branching to handle conditional expressions #233
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 8522fde:
|
jsxAttributes.forEach((jsxAttribute) => { | ||
if (Node.isJsxAttribute(jsxAttribute)) { | ||
const propName = jsxAttribute.getNameNode().getFullText(); | ||
const propName = jsxAttribute.getNameNode().getText(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: @poteboy I guess the usage of getFullText
was probably a mistake. Due to this we have several trim()
in compiler package. I did not include the removals of trim()
in this PR but we can remove them as well in later PRs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, thanks this helps a lot :)
Note on code structure: things like classifyProps lives in existing file rather than |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing! I feel that the logic has become simpler and more readable compared to the previous pull request. It would be even better if we could use this logic to support pseud props.
LGTM 🎉
@naruaway please resolve conflict 🙏 |
4f1879e
to
f23b415
Compare
@kotarella1110 thanks! Conflict resolved now Note that as discussed, one of potential bugs of this PR could be "style override rule can change between static and dynamic" but we think this PR does not make the problem worse since if any of our style props have conflicting output style, this problem already exists |
@@ -0,0 +1,21 @@ | |||
export type AttributeValue = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since AttributeValue
has already been defined in compile.ts
, it seems redundant to me to redefine the same type.
@@ -50,6 +63,28 @@ const extractAttribute = (jsxAttribute: JsxAttribute) => { | |||
const expression = initializer.getExpression(); | |||
if (!expression) return; | |||
|
|||
// fontSize={... ? ... : ...} | |||
const conditionalExpression = match(expression) | |||
.when(Node.isConditionalExpression, (conditional) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually prefer this logic to be placed in expression.ts
because, as a reader, it's more intuitive to have all expressions handled in expression.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the initial reason why I put it here is to limit the scope of the change; but at this point probably better to clean and unify the logic there. Let me try a little bit refactoring although it will affect more such as pseduo props handling, which might be actually desired as long as.... it works without unexpected bug :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@poteboy, now I updated this PR to include some refactorings and pseudo props should be also handled properly for conditional expression (cc @kotarella1110) 👍
f23b415
to
5b55106
Compare
const inputCode = ` | ||
import { k } from '@kuma-ui/core' | ||
function App({ flag }) { | ||
return <k.div p={2} m={flag ? 100 : 10} _hover={flag ? { color: 'blue', bgColor: 'gray'} : {color: 'green'}} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: See the snapshot for how this is compiled. Note that conditional expression can appear at the top level of pseudo props or per prop level. If conditional expressions are nested, it will bail out
let propValue; | ||
// If the propName starts with underscore, use extractPseudoAttribute | ||
if (propName.trim().startsWith("_")) { | ||
propValue = extractPseudoAttribute(jsxAttribute); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: now extractPseudoAttribute
is gone and we just use the same function, extractAttribute
for both
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current changes, the logic for determining the pseudo prop has been removed. This is unsafe, so it is necessary to add similar determination logic in handleJsxExpression
by tracing back to the parent node from the expression.
|
||
export const handleJsxExpression = ( | ||
node: Node<ts.Node> | ||
): types.Value | undefined => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: Now the semantics of the return value of this function is clearer: undefined
means "failed to evaluate" and types.Value
can contain undefined
. Previously these are not separated and <Box color={undefined} />
was not statically compiled since undefined
means "it failed to extract in the logic"
/** | ||
* Normalize ts-morph Node to make further processing simpler | ||
*/ | ||
const normalizeNode = (node: Node): Node => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I copied this from decode.ts
, which is now removed. Now this is used internally by handleJsxExpression
consistently. Previous pattern was error prone since people might forget to call decode
where necessary. I think there is no reason to split the call between decode
and normalizeNode
. Note that in the future probably we can replace this kind of logic with some library such as ts-evaluate
.when(Node.isObjectLiteralExpression, (obj) => { | ||
// TODO | ||
return undefined; | ||
const entries: [string, types.Value][] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mostly copied the logic in pseudo.ts
, which is now removed.
Note that in the original logic, computed properties were silently erased, which is unexpected.
For example, _hover
prop was removed from <Box _hover={{ ['color']: 'red'}} />
without generating the red color CSS. In my PR, when it has computed property, it is just considered as "dynamic". I think introducing more powerful evaluator can make this pattern "static" as well in the future
@@ -56,4 +56,4 @@ jobs: | |||
run: pnpm install | |||
|
|||
- name: Run lint | |||
run: pnpm lint | |||
run: pnpm build && pnpm lint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I realized ESLint emits false positive errors without pnpm build
. Probably the correct way to fix it to somehow create a command using turborepo for lint
to depend on build
but I am not familiar. This should work for now at least for PR pipelines
Object.entries(props) | ||
.filter(([, value]) => value !== undefined) | ||
.sort((a, b) => a[0].localeCompare(b[0])) | ||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- FIXME |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ESLint error is an actual bug. This shows that proper TypeScript typing (basically, eliminating any
) with type-aware ESLint rules are really powerful to detect bugs 🎉
The error is the following:
error Invalid type "StaticValue" of template literal expression @typescript-eslint/restrict-template-expressions
This indicates that we are trying to serialize object into string, which is not expected. I tested main
(2648f5eb5ac8d8cb1a50dd8c84539e314eb4afaa
) and confirmed that generateKey returns something like _hover:[object Object]|bg:#576ddf|borderRadius:14px|color:white|cursor:pointer|fontWeight:600|p:16px 32px
, which is broken since nested object is stringified as [object Object]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I filed #246
whenFalse: StaticValue; | ||
}; | ||
|
||
export type Value = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: This is the most important type in this PR, which tries to encode the potential patterns (e.g. we see that conditional can only contain StaticValue
, which means that nested conditionals are not allowed)
return undefined; | ||
const entries: [string, types.Value][] = []; | ||
for (const prop of obj.getProperties()) { | ||
if (Node.isPropertyAssignment(prop)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer early returns over full-body conditional wrapping in function declaration :)
return types.staticValue( | ||
Object.fromEntries( | ||
entries.map((e) => | ||
e[1].type === "Static" ? [e[0], e[1].value] : ({} as never) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The seems redundant as we've already confirmed that all elements in entries
are static. We could directly pass entries
to Object.fromEntries
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, actually this is accidental since the argument of staticValue is StaticValue
but the type of Object.fromEntries(entries)
is { [k: string]: types.Value; }
. Ideally type check should fail and somehow I thought it fails but it actually passes 🤦
Anyway the runtime logic is correct so as long as we cover this part in test cases it should be acceptable, thanks for removing redundant checks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, this operation is actually necessary to handle pseudo props. I'm not sure why though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also missed this although I wrote it: it's because e[1].value
is needed to unwrap the value from Static
variant. e[1]
is like {type: "Static", value: /* actual value here */ }
handle isArrayLiteralExpression iff pseudo props
2f2371e
to
d66a83f
Compare
This is a continuation of #229
After discussing with core maintainers, we decided to separate class names per conditionals.
In this way we no longer have to limit the combination limit up to arbitrary number such as "8" as seen in the previous PR.
The strong assumption here is that each style prop is isolated with each other. It's like "atomic CSS" but this PR just generates minimum number of classnames rather than actually generating separate classnames per each style prop.
Due to this "atomic-ish" assumption, now the logic is actually simpler
Most of the things in the previous PR description still applies. For example, if we decide to ship this PR, we should update documentation to clarify "static vs. dynamic"
Example
becomes:
Note that
flag
is used twice in the origianl code but the compiled one only includes one conditional forflag
Verification using Next.js
I tested with the following:
Before this PR
Conditionals were treated as "dynamic" (indicated by 🦄 classNames) and there is a flash of unstyled content (FOUC) since I did not set up
![before](https://private-user-images.githubusercontent.com/2931577/254759416-74fe700b-1d4e-4f96-b8ee-c1ea97f640e9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTgyNzM4ODIsIm5iZiI6MTcxODI3MzU4MiwicGF0aCI6Ii8yOTMxNTc3LzI1NDc1OTQxNi03NGZlNzAwYi0xZDRlLTRmOTYtYjhlZS1jMWVhOTdmNjQwZTkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYxMyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MTNUMTAxMzAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NWZjNDUwYTNmYzYzODViMjZiZDVjMGVkODgzMzNmZDcxYmQwNmFiMGYzMWQ1NjdkMWU4ZGNjNGFkMTQzY2FhNiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.IwZy9Sy0ve-8Hrtk5BdJ__BV-n5LkuQlODiewnZjysE)
KumaRegistry
After this PR
Conditionals are treated as "static" (indicated by 🐻 classNames). There is no FOUC
![after](https://private-user-images.githubusercontent.com/2931577/254759442-156d88d6-d1a0-46d5-874b-75ba49195384.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTgyNzM4ODIsIm5iZiI6MTcxODI3MzU4MiwicGF0aCI6Ii8yOTMxNTc3LzI1NDc1OTQ0Mi0xNTZkODhkNi1kMWEwLTQ2ZDUtODc0Yi03NWJhNDkxOTUzODQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYxMyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MTNUMTAxMzAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MjI5ZDhkYTc5YTc2YTBkZDg4Y2U4NjUwZWIwOTkzNGQzNjExNTg4MTcwYzg5NmM4NmEwZDVlNTlkOGFiMGMxOCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.NisSV5h8k69LUH3fzUQbI4LusGZYUySjdHrQOZUme4A)
![prod-after](https://private-user-images.githubusercontent.com/2931577/254759457-fa8ee307-476b-4346-9b34-da8276c67bf7.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTgyNzM4ODIsIm5iZiI6MTcxODI3MzU4MiwicGF0aCI6Ii8yOTMxNTc3LzI1NDc1OTQ1Ny1mYThlZTMwNy00NzZiLTQzNDYtOWIzNC1kYTgyNzZjNjdiZjcucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYxMyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MTNUMTAxMzAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ODMxNWFlNTkzMjRlNWUzYWQwMDE4ZjU5ZThmYzZkYzhlMGVlYjRkMTY5M2NkNDhlYjA5OGY2MzA4MTQ2NTJlYiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.hdpMzUsYOdxLmfV9VDcNxHrkfEepID87gvYzhsXNWAg)
Note that I confirmed it works as well for production build