Skip to content
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

Added error messages for dynamic selectors #43

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/next-yak/loaders/__tests__/tsloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,47 @@ const Icon = styled.div\`
line 11: found Expression inside \\"@media (min-width: 640px) { .bar {\\""
`);
});
it("should show error when a dynamic selector is used", async () => {
await expect(() =>
tsloader.call(
loaderContext,
`
import { styled, css } from "next-yak";

const test = "bar";

const Icon = styled.div\`
\${test} {
font-weight: bold;
}
\`
`
)
).rejects.toThrowErrorMatchingInlineSnapshot(`
"/some/special/path/page.tsx: Expressions are not allowed as selectors:
line 7: found \${test}"
`);
});

it("should show error when a dynamic selector is used after a comma", async () => {
await expect(() =>
tsloader.call(
loaderContext,
`
import { styled, css } from "next-yak";

const test = "bar";

const Icon = styled.div\`
\${test}, baz {
font-weight: bold;
}
\`
`
)
).rejects.toThrowErrorMatchingInlineSnapshot(`
"/some/special/path/page.tsx: Expressions are not allowed as selectors:
line 7: found \${test}"
`);
});
});
83 changes: 55 additions & 28 deletions packages/next-yak/loaders/babel-yak-plugin.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ module.exports = function (babel, options) {
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier("__styleYak"))],
t.stringLiteral(
`./${fileName}.yak.module.css!=!./${fileName}?./${fileName}.yak.module.css`,
),
),
`./${fileName}.yak.module.css!=!./${fileName}?./${fileName}.yak.module.css`
)
)
);

// Process import specifiers
Expand Down Expand Up @@ -115,15 +115,15 @@ module.exports = function (babel, options) {
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
).name === this.localVarNames.styled;
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 Down Expand Up @@ -169,15 +169,15 @@ module.exports = function (babel, options) {
astNode.arguments.push(
t.memberExpression(
t.identifier("__styleYak"),
t.identifier(className),
),
t.identifier(className)
)
);
}
return className;
}
return false;
},
t,
t
);

let literalSelectorWasUsed = false;
Expand All @@ -189,9 +189,9 @@ module.exports = function (babel, options) {
localIdent(
variableName,
literalSelectorIndex,
isKeyframesLiteral ? "animation" : "className",
),
),
isKeyframesLiteral ? "animation" : "className"
)
)
);

// Replace the tagged template expression with a call to the 'styled' function
Expand All @@ -200,7 +200,10 @@ module.exports = function (babel, options) {
/** @type {string[]} */
let currentNestingScopes = [];
const quasiTypes = quasis.map((quasi) => {
const classification = quasiClassifier(quasi.value.raw, currentNestingScopes);
const classification = quasiClassifier(
quasi.value.raw,
currentNestingScopes
);
currentNestingScopes = classification.currentNestingScopes;
return classification;
});
Expand All @@ -209,7 +212,26 @@ module.exports = function (babel, options) {
let cssVariablesInlineStyle;

for (let i = 0; i < quasis.length; i++) {
if (quasiTypes[i].empty) {
const type = quasiTypes[i];
if (type.unknownSelector) {
const expression = expressions[i - 1];
if (!expression) {
throw new Error(`Invalid css "${quasis[i].value.raw}"`);
}
let errorText = "Expressions are not allowed as selectors";
const line = expression.loc?.start.line || -1;
if (expression.start && expression.end) {
errorText += `:\n${
line !== -1 ? `line ${line}:` : ""
} found \${${this.file.code.slice(
expression.start,
expression.end
)}}`;
}
throw new InvalidPositionError(errorText);
}

if (type.empty) {
const expression = expressions[i];
if (expression) {
newArguments.add(expression);
Expand All @@ -229,11 +251,7 @@ module.exports = function (babel, options) {
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)
) {
if (type.insideCssValue || (isMerging && type.empty)) {
isMerging = true;
// expression: `x`
// { style: { --v0: x}}
Expand All @@ -253,16 +271,18 @@ module.exports = function (babel, options) {
}
const relativePath = relative(
rootContext,
resolve(rootContext, resourcePath),
resolve(rootContext, resourcePath)
);
hashedFile = murmurhash2_32_gc(relativePath);
}

// expression: `x`
// { style: { --v0: x}}
cssVariablesInlineStyle.properties.push(
t.objectProperty(
t.stringLiteral(`--🦬${hashedFile}${this.varIndex++}`),
/** @type {babel.types.Expression} */ (expression),
),
/** @type {babel.types.Expression} */ (expression)
)
);
} else if (type.empty) {
// empty quasis can be ignored in typescript
Expand All @@ -272,10 +292,17 @@ module.exports = function (babel, options) {
if (expressions[i]) {
if (quasiTypes[i].currentNestingScopes.length > 0) {
const errorExpression = expressions[i];
const name = errorExpression.type === "Identifier" ? `"${errorExpression.name}"` : "Expression";
const line = errorExpression.loc?.start.line || -1
const name =
errorExpression.type === "Identifier"
? `"${errorExpression.name}"`
: "Expression";
const line = errorExpression.loc?.start.line || -1;
throw new InvalidPositionError(
`Expressions are not allowed inside nested selectors:\n${line !== -1 ? `line ${line}: ` : ""}found ${name} inside "${quasiTypes[i].currentNestingScopes.join(" { ")} {"`,
`Expressions are not allowed inside nested selectors:\n${
line !== -1 ? `line ${line}: ` : ""
}found ${name} inside "${quasiTypes[
i
].currentNestingScopes.join(" { ")} {"`
);
}
newArguments.add(expressions[i]);
Expand All @@ -290,9 +317,9 @@ module.exports = function (babel, options) {
t.objectExpression([
t.objectProperty(
t.stringLiteral(`style`),
cssVariablesInlineStyle,
cssVariablesInlineStyle
),
]),
])
);
}

Expand All @@ -307,7 +334,7 @@ module.exports = function (babel, options) {
className: localIdent(
variableName,
literalSelectorIndex,
"className",
"className"
),
astNode: styledCall,
});
Expand All @@ -326,4 +353,4 @@ class InvalidPositionError extends Error {
}
}

module.exports.InvalidPositionError = InvalidPositionError;
module.exports.InvalidPositionError = InvalidPositionError;
9 changes: 6 additions & 3 deletions packages/next-yak/loaders/lib/quasiClassifier.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const stripCssComments = require("./stripCssComments.cjs");
* Checks a quasiValue and returns its type
*
* - empty: no expressions, no text
* - unknownSelector: starts with a `{` e.g. `{ opacity: 0.5; }`
* - unknownSelector: starts with a `{` e.g. `{ opacity: 0.5; }` or `,` e.g. `, bar { ... }`
* - insideCssValue: does not end with a `{` or `}` or `;` e.g. `color: `
*
* @param {string} quasiValue
Expand Down Expand Up @@ -80,8 +80,11 @@ module.exports = function quasiClassifier(quasiValue, currentNestingScopes) {

return {
empty: false,
unknownSelector: trimmedCssString[0] === "{",
insideCssValue: currentCharacter !== "{" && currentCharacter !== "}" && currentCharacter !== ";",
unknownSelector: trimmedCssString[0] === "{" || trimmedCssString[0] === ",",
insideCssValue:
currentCharacter !== "{" &&
currentCharacter !== "}" &&
currentCharacter !== ";",
currentNestingScopes: newNestingLevel,
};
};
2 changes: 1 addition & 1 deletion packages/next-yak/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-yak",
"version": "0.0.19",
"version": "0.0.20",
"type": "module",
"types": "./dist/",
"exports": {
Expand Down
Loading