From 73146403aec60e8da1c902793a9d7df542592e34 Mon Sep 17 00:00:00 2001 From: Luca Schneider Date: Mon, 18 Dec 2023 12:37:05 +0100 Subject: [PATCH] Added error messages for dynamic selectors --- .../loaders/__tests__/tsloader.test.ts | 43 ++++++++++ .../next-yak/loaders/babel-yak-plugin.cjs | 83 ++++++++++++------- .../next-yak/loaders/lib/quasiClassifier.cjs | 9 +- packages/next-yak/package.json | 2 +- 4 files changed, 105 insertions(+), 32 deletions(-) diff --git a/packages/next-yak/loaders/__tests__/tsloader.test.ts b/packages/next-yak/loaders/__tests__/tsloader.test.ts index 0a9adb61..91a385e9 100644 --- a/packages/next-yak/loaders/__tests__/tsloader.test.ts +++ b/packages/next-yak/loaders/__tests__/tsloader.test.ts @@ -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}" + `); + }); }); diff --git a/packages/next-yak/loaders/babel-yak-plugin.cjs b/packages/next-yak/loaders/babel-yak-plugin.cjs index 70464cef..6b8f8073 100644 --- a/packages/next-yak/loaders/babel-yak-plugin.cjs +++ b/packages/next-yak/loaders/babel-yak-plugin.cjs @@ -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 @@ -115,7 +115,7 @@ 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 @@ -123,7 +123,7 @@ module.exports = function (babel, options) { 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 @@ -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; @@ -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 @@ -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; }); @@ -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); @@ -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}} @@ -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 @@ -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]); @@ -290,9 +317,9 @@ module.exports = function (babel, options) { t.objectExpression([ t.objectProperty( t.stringLiteral(`style`), - cssVariablesInlineStyle, + cssVariablesInlineStyle ), - ]), + ]) ); } @@ -307,7 +334,7 @@ module.exports = function (babel, options) { className: localIdent( variableName, literalSelectorIndex, - "className", + "className" ), astNode: styledCall, }); @@ -326,4 +353,4 @@ class InvalidPositionError extends Error { } } -module.exports.InvalidPositionError = InvalidPositionError; \ No newline at end of file +module.exports.InvalidPositionError = InvalidPositionError; diff --git a/packages/next-yak/loaders/lib/quasiClassifier.cjs b/packages/next-yak/loaders/lib/quasiClassifier.cjs index 6462666f..bc9567c6 100644 --- a/packages/next-yak/loaders/lib/quasiClassifier.cjs +++ b/packages/next-yak/loaders/lib/quasiClassifier.cjs @@ -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 @@ -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, }; }; diff --git a/packages/next-yak/package.json b/packages/next-yak/package.json index 0803df6a..e7c95e21 100644 --- a/packages/next-yak/package.json +++ b/packages/next-yak/package.json @@ -1,6 +1,6 @@ { "name": "next-yak", - "version": "0.0.19", + "version": "0.0.20", "type": "module", "types": "./dist/", "exports": {