Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rvetere committed Jan 15, 2024
1 parent a7613d4 commit e801116
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 57 deletions.
86 changes: 83 additions & 3 deletions packages/next-yak/loaders/__tests__/cssloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,17 @@ const headline = css\`
`
)
).toMatchInlineSnapshot(`
"._yak_0 {
"._yak_0 {
transition: color var(--🦬18fi82j0) var(--🦬18fi82j1);
display: block;
}
}
._yak_1 { color: orange }"
._yak_1 {
color: orangevar(--🦬18fi82j2)}
"
`);
});

Expand Down Expand Up @@ -446,3 +451,78 @@ const Wrapper = styled.div\`
}"
`);
});

it.only("should support nested expressions", async () => {
expect(
await cssloader.call(
loaderContext,
`
import { styled, keyframes, css } from "next-yak";
const Component = styled.div\`
background-color: red;
color: white;
\${({ active }) => active && css\`
background-color: blue;
\`}
border: 1px solid black;
&:focus {
background-color: green;
\${({ active }) => active && css\`
background-color: blue;
\${({ active }) => active && css\`
background-color: brown;
\`}
\`}
border: 2px solid pink;
}
\`;
const Component2 = styled.div\`
color: hotpink;
\`;
`
)
).toMatchInlineSnapshot(`
".Component_0 {
background-color: red;
color: white;
border: 1px solid black;
&:focus {
background-color: green;
border: 2px solid pink;
}
}
._yak_1 {
background-color: blue;
}
._yak_2 {
background-color: blue;
}
._yak_3 {
background-color: brown;
}
.Component2_4 {
color: hotpink;
}
"
`);
});
158 changes: 104 additions & 54 deletions packages/next-yak/loaders/cssloader.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { is } from "@babel/types";

/// @ts-check
const getYakImports = require("./lib/getYakImports.cjs");
const babel = require("@babel/core");
Expand Down Expand Up @@ -34,7 +36,7 @@ module.exports = async function cssLoader(source) {
imports.forEach(({ localName, importedName }) => {
replaces[localName] = constantValues[importedName];
});
}),
})
);

// parse source with babel
Expand Down Expand Up @@ -63,6 +65,14 @@ module.exports = async function cssLoader(source) {
keyframes: undefined,
};

/**
* Babel iterates over the full TaggedLiteralExpression before it iterates over their children
* To keep the order as written in the original code the code fragments are stored in an ordered map
* @typedef {{ selector: string,quasiCode: string[], cssPartExpressions: CssPartExpression[] }} CssPartExpression
* @type {Map<import("@babel/core").NodePath<import("@babel/types").TaggedTemplateExpression>, CssPartExpression>}
*/
const rootCssParts = new Map();

let index = 0;
let varIndex = 0;
/** @type {string | null} */
Expand All @@ -73,7 +83,7 @@ module.exports = async function cssLoader(source) {

/**
* find all css template literals in ast
* @type {{ code: string, loc: number }[]}
* @type {{ code: string }[]}
*/
const cssCode = [];
babel.traverse(ast, {
Expand Down Expand Up @@ -127,7 +137,7 @@ module.exports = async function cssLoader(source) {
const isStyledLiteral =
t.isMemberExpression(tag) &&
t.isIdentifier(
/** @type {babel.types.MemberExpression} */ (tag).object,
/** @type {babel.types.MemberExpression} */ (tag).object
) &&
/** @type {babel.types.Identifier} */ (
/** @type {babel.types.MemberExpression} */ (tag).object
Expand All @@ -136,7 +146,7 @@ module.exports = async function cssLoader(source) {
const isStyledCall =
t.isCallExpression(tag) &&
t.isIdentifier(
/** @type {babel.types.CallExpression} */ (tag).callee,
/** @type {babel.types.CallExpression} */ (tag).callee
) &&
/** @type {babel.types.Identifier} */ (
/** @type {babel.types.CallExpression} */ (tag).callee
Expand All @@ -157,13 +167,6 @@ module.exports = async function cssLoader(source) {
) {
return;
}
// Store class name for the created variable for later replacements
// e.g. const MyStyledDiv = styled.div`color: red;`
// "MyStyledDiv" -> "selector-0"
const variableName =
isStyledLiteral || isStyledCall || isAttrsCall || isKeyFrameLiteral
? getStyledComponentName(path)
: "_yak";

replaceQuasiExpressionTokens(
path.node.quasi,
Expand All @@ -176,81 +179,101 @@ module.exports = async function cssLoader(source) {
}
return false;
},
t,
t
);

const parent = getClosestTemplateLiteralExpressionParentPath(
path,
localVarNames
);

// Store class name for the created variable for later replacements
// e.g. const MyStyledDiv = styled.div`color: red;`
// "MyStyledDiv" -> "selector-0"
const variableName =
isStyledLiteral || isStyledCall || isAttrsCall || isKeyFrameLiteral
? getStyledComponentName(path)
: "_yak";

// Keep the same selector for all quasis belonging to the same css block
const literalSelectorIndex = index++;
const literalSelector = localIdent(
variableName,
literalSelectorIndex,
isKeyFrameLiteral ? "keyframes" : "selector",
isKeyFrameLiteral ? "keyframes" : "selector"
);

/** @type {CssPartExpression} */
const currentCssParts = {
quasiCode: [],
cssPartExpressions: [],
selector: !parent ? literalSelector : `&:where(${literalSelector})`
};
const parentCssParts = parent && rootCssParts.get(parent);
if (parentCssParts) {
parentCssParts.cssPartExpressions.push(currentCssParts);
} else {
rootCssParts.set(path, currentCssParts);
}

// Replace the tagged template expression with a call to the 'styled' function
const quasis = path.node.quasi.quasis;
const quasiTypes = quasis.map((quasi) => quasiClassifier(quasi.value.raw, []));
const quasiTypes = quasis.map((quasi) =>
quasiClassifier(quasi.value.raw, [])
);

let wasInsideCssValue = false;
for (let i = 0; i < quasis.length; i++) {
const quasi = quasis[i];
// skip empty quasis
if (quasiTypes[i].empty) {
continue;
}
let code = quasi.value.raw;
let isMerging = false;
// loop over all quasis belonging to the same css block
while (i < quasis.length - 1) {
const type = quasiTypes[i];
// expressions after a partial css are converted into css variables
if (
type.unknownSelector ||
type.insideCssValue ||
(isMerging && type.empty)
) {
isMerging = true;
if (!hashedFile) {
const relativePath = relative(rootContext, resourcePath);
hashedFile = murmurhash2_32_gc(relativePath);
}
// replace the expression with a css variable
code += `var(--🦬${hashedFile}${varIndex++})`;
// as we are after the css block, we need to increment i
// to get the very next quasi
i++;
code += quasis[i].value.raw;
} else if (type.empty) {
// empty quasis are also added to keep spacings
// e.g. `transition: color ${duration} ${easing};`
i++;
code += quasis[i].value.raw;
} else {
break;
const type = quasiTypes[i];
console.log({ type, wasInsideCssValue, rawValue: quasi.value.raw });
// expressions after a partial css are converted into css variables
if (
type.unknownSelector ||
type.insideCssValue ||
(type.empty && wasInsideCssValue)
) {
wasInsideCssValue = true;
if (!hashedFile) {
const relativePath = relative(rootContext, resourcePath);
hashedFile = murmurhash2_32_gc(relativePath);
}
// as we are after the css block, we need to increment i
// to get the very next quasi
cssCode.push({ code: unEscapeCssCode(quasi.value.raw) });
// replace the expression with a css variable
cssCode.push({ code: `var(--🦬${hashedFile}${varIndex++})` });
} else {
wasInsideCssValue = false;
// code is added
// empty quasis are also added to keep spacings
// e.g. `transition: color ${duration} ${easing};`
cssCode.push({ code: unEscapeCssCode(quasi.value.raw) });
}

cssCode.push({
code: `${literalSelector} { ${unEscapeCssCode(code)} }`,
loc: quasi.loc?.start.line || 0,
code: `:where(${literalSelector}) {\n`,
});
}
depthFirst(subPath);
cssCode.push({
code: "}\n\n",
});


// Store class name for the created variable for later replacements
// e.g. const MyStyledDiv = styled.div`color: red;`
// "MyStyledDiv" -> "selector-0"
if (isStyledLiteral || isStyledCall || isAttrsCall) {
variableNameToStyledClassName.set(
variableName,
localIdent(variableName, literalSelectorIndex, "selector"),
localIdent(variableName, literalSelectorIndex, "selector")
);
}
},
});

// sort by loc
cssCode.sort((a, b) => a.loc - b.loc);

return cssCode.map((code) => code.code).join("\n\n");
return cssCode.map((code) => code.code).join("");
};

/**
Expand All @@ -260,3 +283,30 @@ module.exports = async function cssLoader(source) {
* @param {string} code
*/
const unEscapeCssCode = (code) => code.replace(/\\\\/gi, "\\");

/**
* Searches the closest parent TaggedTemplateExpression using a name from localNames
* @param {import("@babel/core").NodePath<import("@babel/types").TaggedTemplateExpression>} path
* @param {{ css?: string , styled?: string }} localNames
* @returns {import("@babel/core").NodePath<import("@babel/types").TaggedTemplateExpression> | null}
*/
const getClosestTemplateLiteralExpressionParentPath = (
path,
{ css, styled }
) => {
let parent = path.parentPath;
while (parent) {
if (
babel.types.isTaggedTemplateExpression(parent.node) &&
babel.types.isIdentifier(parent.node.tag) &&
(parent.node.tag.name === css || parent.node.tag.name === styled)
) {
return /** @type {import("@babel/core").NodePath<import("@babel/types").TaggedTemplateExpression>} */ (parent);
}
if (!parent.parentPath) {
return null;
}
parent = parent.parentPath;
}
return null;
};

0 comments on commit e801116

Please sign in to comment.