Skip to content

Commit

Permalink
feat(compiler): static branching to handle conditional expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
naruaway committed Jul 18, 2023
1 parent 76188d4 commit 23fe484
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 56 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitest/coverage-c8": "^0.31.4",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
Expand Down
45 changes: 45 additions & 0 deletions packages/babel-plugin/src/__test__/__snapshots__/k.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ function App() {
"
`;
exports[`k api > Snapshot tests (runtime: automatic) > using conditionals should match snapshot 1`] = `
"
.🐻-4229161508 { font-size: 24px; } .🐻-220318275 { font-size: 16px; }
import { Box as __Box } from \\"@kuma-ui/core\\";
import React from \\"react\\";
import { k } from '@kuma-ui/core';
function App({
flag
}) {
return <__Box as=\\"div\\" IS_KUMA_DEFAULT={true} className={[\\"🐻-4229161508\\",\\"🐻-220318275\\"][1*!(flag)]}></__Box>;
}
"
`;
exports[`k api > Snapshot tests (runtime: automatic) > using pseudo elements should match snapshot 1`] = `
"
.🐻-2631981251 { padding: 2px; }.🐻-2631981251:after { color: blue; }
Expand Down Expand Up @@ -134,6 +149,21 @@ function App() {
"
`;
exports[`k api > Snapshot tests (runtime: classic) > using conditionals should match snapshot 1`] = `
"
.🐻-4229161508 { font-size: 24px; } .🐻-220318275 { font-size: 16px; }
import { Box as __Box } from \\"@kuma-ui/core\\";
import React from \\"react\\";
import { k } from '@kuma-ui/core';
function App({
flag
}) {
return <__Box as=\\"div\\" IS_KUMA_DEFAULT={true} className={[\\"🐻-4229161508\\",\\"🐻-220318275\\"][1*!(flag)]}></__Box>;
}
"
`;
exports[`k api > Snapshot tests (runtime: classic) > using pseudo elements should match snapshot 1`] = `
"
.🐻-2631981251 { padding: 2px; }.🐻-2631981251:after { color: blue; }
Expand Down Expand Up @@ -227,6 +257,21 @@ function App() {
"
`;
exports[`k api > Snapshot tests (runtime: undefined) > using conditionals should match snapshot 1`] = `
"
.🐻-4229161508 { font-size: 24px; } .🐻-220318275 { font-size: 16px; }
import { Box as __Box } from \\"@kuma-ui/core\\";
import React from \\"react\\";
import { k } from '@kuma-ui/core';
function App({
flag
}) {
return <__Box as=\\"div\\" IS_KUMA_DEFAULT={true} className={[\\"🐻-4229161508\\",\\"🐻-220318275\\"][1*!(flag)]}></__Box>;
}
"
`;
exports[`k api > Snapshot tests (runtime: undefined) > using pseudo elements should match snapshot 1`] = `
"
.🐻-2631981251 { padding: 2px; }.🐻-2631981251:after { color: blue; }
Expand Down
14 changes: 14 additions & 0 deletions packages/babel-plugin/src/__test__/k.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ describe("k api", () => {
expect(getExpectSnapshot(result)).toMatchSnapshot();
});

test("using conditionals should match snapshot", async () => {
// Arrange
const inputCode = `
import { k } from '@kuma-ui/core'
function App({flag}) {
return <k.div fontSize={flag ? 24 : 16}></k.div>
}
`;
// Act
const result = await babelTransform(inputCode);
// Assert
expect(getExpectSnapshot(result)).toMatchSnapshot();
});

test("using responsive props should match snapshot", async () => {
// Arrange
const inputCode = `
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"scripts": {
"build": "tsup --config ../../tsup.config.ts",
"typecheck": "tsc --noEmit --composite false",
"test": "vitest run",
"lint": "eslint './src/**/*.{js,ts,jsx,tsx}'",
"lint:fix": "eslint --fix './src/**/*.{js,ts,jsx,tsx}'"
},
Expand Down
67 changes: 41 additions & 26 deletions packages/compiler/src/collector/collect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {
Project,
Node,
SyntaxKind,
JsxOpeningElement,
JsxSelfClosingElement,
JsxAttribute,
Expand All @@ -10,12 +8,14 @@ import { match } from "ts-pattern";
import { decode } from "./decode";
import { handleJsxExpression } from "./expression";
import { extractPseudoAttribute } from "./pseudo";
import { UnevaluatedConditionalExpression, unevaluatedConditionalExpression } from "../static-branching";

export const collectPropsFromJsx = (
node: JsxOpeningElement | JsxSelfClosingElement
) => {
): Record<string, any> => {
const jsxAttributes = node.getAttributes();
const extracted: Record<string, any> = {};

jsxAttributes.forEach((jsxAttribute) => {
if (Node.isJsxAttribute(jsxAttribute)) {
const propName = jsxAttribute.getNameNode().getFullText();
Expand All @@ -31,32 +31,47 @@ export const collectPropsFromJsx = (
extracted[propName] = propValue;
}
});

return extracted;
};

const extractAttribute = (jsxAttribute: JsxAttribute) => {

type AttributeValue = string | number | boolean | (string | number | undefined)[]


const extractAttribute = (jsxAttribute: JsxAttribute): AttributeValue | UnevaluatedConditionalExpression | undefined => {
const initializer = jsxAttribute.getInitializer();

return (
match(initializer)
// fontSize='24px'
.when(Node.isStringLiteral, (initializer) => {
const value = initializer.getLiteralText();
return value;
})
// fontSize={...}
.when(Node.isJsxExpression, (initializer) => {
const expression = initializer.getExpression();
if (!expression) return;

const decodedNode = decode(expression);
return handleJsxExpression(decodedNode);
})
// If no initializer is present (e.g., <Spacer horizontal />), treat the prop as true
.when(
() => initializer === undefined,
() => true
)
.otherwise(() => undefined)
);
return match(initializer)
// fontSize='24px'
.when(Node.isStringLiteral, (initializer) => {
const value = initializer.getLiteralText();
return value;
})
// fontSize={...}
.when(Node.isJsxExpression, (initializer) => {
const expression = initializer.getExpression();
if (!expression) return;

// fontSize={... ? ... : ...}
const conditionalExpression = match(expression).when(Node.isConditionalExpression, (conditional) => {
const condition = conditional.getCondition();
const whenTrue = handleJsxExpression(decode(conditional.getWhenTrue()));
const whenFalse = handleJsxExpression(decode(conditional.getWhenFalse()));
if (whenTrue === undefined || whenFalse === undefined) {
return undefined
}
return unevaluatedConditionalExpression({ expression: condition.getText(), whenTrue, whenFalse });
}).otherwise(() => undefined)
if (conditionalExpression) return conditionalExpression;

const decodedNode = decode(expression);
return handleJsxExpression(decodedNode);
})
// If no initializer is present (e.g., <Spacer horizontal />), treat the prop as true
.when(
() => initializer === undefined,
() => true
)
.otherwise(() => undefined)
};
2 changes: 1 addition & 1 deletion packages/compiler/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const compile = (
componentName,
openingElement,
extractedPropsMap
);
)
if (result) css.push(result.css);
}
});
Expand Down
98 changes: 69 additions & 29 deletions packages/compiler/src/extractor/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
JsxOpeningElement,
JsxSelfClosingElement,
} from "ts-morph";
import * as ts from "ts-morph";
import {
isStyledProp,
isPseudoProps,
Expand All @@ -20,11 +21,12 @@ import {
componentHandler,
} from "@kuma-ui/core/components/componentList";
import { theme } from "@kuma-ui/sheet";
import { evaluateStaticBranching } from "../static-branching";

export const extractProps = (
const evaluateProps = (
componentName: (typeof componentList)[keyof typeof componentList],
jsx: JsxOpeningElement | JsxSelfClosingElement,
propsMap: Record<string, any>
propsMap: Record<string, any>,
) => {
const styledProps: { [key: string]: any } = {};
const pseudoProps: { [key: string]: any } = {};
Expand All @@ -51,7 +53,7 @@ export const extractProps = (
Object.assign(
componentVariantProps,
variant?.baseStyle,
variant?.variants?.[propValue as string]
variant?.variants?.[propValue as string],
);
jsx.getAttribute("variant")?.remove();
} else if (propName.trim() === "IS_KUMA_DEFAULT") {
Expand Down Expand Up @@ -101,31 +103,6 @@ export const extractProps = (
// If no generatedClassName is returned, the component should remain intact
if (!generatedClassName) return { css };

const classNameAttr = jsx.getAttribute("className");
let newClassName = generatedClassName;
let newClassNameInitializer = "";

// Check if a className attribute already exists
if (classNameAttr && Node.isJsxAttribute(classNameAttr)) {
const initializer = classNameAttr.getInitializer();
// If the initializer is a string literal, simply concatenate the new className (i.e., className="hoge")
if (Node.isStringLiteral(initializer)) {
const existingClassName = initializer.getLiteralText();
if (existingClassName) newClassName += " " + existingClassName;
newClassNameInitializer = `"${newClassName}"`;
}
// If the initializer is a JsxExpression, create a template literal to combine the classNames at runtime (i.e., className={hoge})
else if (Node.isJsxExpression(initializer)) {
const expression = initializer.getExpression();
if (expression) {
newClassNameInitializer = `\`${newClassName} \${${expression.getText()}}\``;
}
}
classNameAttr.remove();
} else {
newClassNameInitializer = `"${newClassName}"`;
}

for (const styledPropKey of Object.keys(styledProps)) {
jsx.getAttribute(styledPropKey)?.remove();
}
Expand All @@ -138,11 +115,74 @@ export const extractProps = (
jsx.getAttribute(componentPropsKey)?.remove();
}

return { css, generatedClassName };
};

const updateClassNameAttr = (
jsx: JsxOpeningElement | JsxSelfClosingElement,
className: string | { type: 'expression', expression: string } | undefined,
) => {
if (!className) {
return;
}
const classNameAttr = jsx.getAttribute("className");
let newClassNameInitializer = "";

// Check if a className attribute already exists
if (classNameAttr && Node.isJsxAttribute(classNameAttr)) {
const initializer = classNameAttr.getInitializer();
const existingClassName = Node.isStringLiteral(initializer)
? initializer.getText()
: Node.isJsxExpression(initializer)
? initializer.getExpression()?.getText()
: undefined;

if (existingClassName) {
newClassNameInitializer = typeof className !== 'string' ? `\`\${${className.expression}} \${${existingClassName}}\`` : `\`${className} \${${existingClassName}}\``;
}
classNameAttr.remove();
} else {
newClassNameInitializer = typeof className !== 'string' ? className.expression : JSON.stringify(className);
}

jsx.addAttribute({
name: "className",
initializer: `{${newClassNameInitializer}}`,
});
return { css };
};


export const extractProps = (
componentName: (typeof componentList)[keyof typeof componentList],
jsx: JsxOpeningElement | JsxSelfClosingElement,
propsMapWithUnevaluatedConditionals: Record<string, any>,
): { css: string } | undefined => {
const { propsMapList, indexExpression } = evaluateStaticBranching(
propsMapWithUnevaluatedConditionals,
);

const evaluationResults = propsMapList.map((propsMap) =>
evaluateProps(componentName, jsx, propsMap),
);

const kumaClassNames: string[] = evaluationResults.map(
(r) => r?.generatedClassName ?? "",
);

const switchingClassNameExpression = `${JSON.stringify(
kumaClassNames,
)}[${indexExpression}]`;

updateClassNameAttr(
jsx,
kumaClassNames.length === 1
? kumaClassNames[0] ? kumaClassNames[0] : undefined
: { type: 'expression', expression: switchingClassNameExpression },
);

return {
css: evaluationResults.map((r) => r?.css ?? "").join(" "),
};
};

/**
Expand Down

0 comments on commit 23fe484

Please sign in to comment.