Repository: microsoft/griffel (ts)

Issue:
eslint-plugin: enforce ::before over :before
ESLint plugin should prevent usage of ":before":

```js
makeStyles({
  // ❌ should throw
  root: {
    ":before": {}
  },
  // ✅ should pass
  root: {
    "::before": {}
  }
});
```

- check our codebase for ":before"/":after"
- rule should implement autofix


Pull request:
eslint-plugin: enforce pseudo element naming with :: over :
This fixes #304.

It is basically the same rule as for `no-shorthands`, but instead of checking
whether the property is an `Identifier`, we check for `StringLiteral`.

Invalid Pseudo Element Names are:
`:before` and `:after`

Autofix works as intended:
```js
...
':before': {},
...
```
will fix to
```js
...
'::before': {},
...
```

Note: I am not the best programmer, so if you find something that should be changed or is not working as intended, let me know and I will try to fix it.


Patch:
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index a210587..8b5308b 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -30,7 +30,8 @@ This plugin exports recommended configuration that enforce good practices, but y
   "plugins": ["@griffel"],
   "rules": {
     "@griffel/hook-naming": "error",
-    "@griffel/no-shorthands": "warn"
+    "@griffel/no-shorthands": "error",
+    "@griffel/pseudo-element-naming": "error"
   }
 }
 ```
@@ -41,7 +42,9 @@ You can find more info about enabled rules in the [Supported Rules section](#sup
 
 **Key**: 🔧 = fixable
 
-| Name                                                     | Description                                                                | 🔧  |
-| -------------------------------------------------------- | -------------------------------------------------------------------------- | --- |
-| [`@griffel/hook-naming`](./src/rules/hook-naming.md)     | Ensure that hooks returned by the `makeStyles()` function start with "use" |     |
-| [`@griffel/no-shorthands`](./src/rules/no-shorthands.md) | Enforce usage of CSS longhands                                             |     |
+| Name                                                                     | Description                                                                                                     | 🔧  |
+| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | --- |
+| [`@griffel/hook-naming`](./src/rules/hook-naming.md)                     | Ensure that hooks returned by the `makeStyles()` function start with "use"                                      | ❌  |
+| [`@griffel/no-shorthands`](./src/rules/no-shorthands.md)                 | Enforce usage of CSS longhands                                                                                  | ❌  |
+| [`@griffel/styles-file`](./src/rules/styles-file.md)                     | Ensures that all `makeStyles()` and `makeResetStyles()` calls are placed in a `.styles.js` or `.styles.ts` file | ❌  |
+| [`@griffel/pseudo-element-naming`](./src/rules/pseudo-element-naming.md) | Ensures that all Pseudo Elements start with two colons                                                          | ✅  |
diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts
index 47e1dde..43ee8ed 100644
--- a/packages/eslint-plugin/src/configs/recommended.ts
+++ b/packages/eslint-plugin/src/configs/recommended.ts
@@ -3,5 +3,6 @@ export const recommendedConfig = {
   rules: {
     '@griffel/hook-naming': 'error',
     '@griffel/no-shorthands': 'error',
+    '@griffel/pseudo-element-naming': 'error',
   },
 };
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
index 3606248..6fb7dbd 100644
--- a/packages/eslint-plugin/src/index.ts
+++ b/packages/eslint-plugin/src/index.ts
@@ -1,6 +1,7 @@
 import { recommendedConfig } from './configs/recommended';
 import { hookNamingRule } from './rules/hook-naming';
 import { noShorthandsRule } from './rules/no-shorthands';
+import { pseudoElementNamingRule } from './rules/pseudo-element-naming';
 import { stylesFileRule } from './rules/styles-file';
 
 export = {
@@ -11,5 +12,6 @@ export = {
     'hook-naming': hookNamingRule,
     'no-shorthands': noShorthandsRule,
     'styles-file': stylesFileRule,
+    'pseudo-element-naming': pseudoElementNamingRule,
   },
 };
diff --git a/packages/eslint-plugin/src/rules/pseudo-element-naming.md b/packages/eslint-plugin/src/rules/pseudo-element-naming.md
new file mode 100644
index 0000000..eca57dc
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/pseudo-element-naming.md
@@ -0,0 +1,33 @@
+# Enforce that Pseudo elements start with two colons
+
+Ensures that all Pseudo Elements start with two colons (`::before`) instead of one colon (`:before`).
+
+## Rule Details
+
+Pseudo **Elements** should always start with two colons while Pseudeo **Selectors** should always start with one colon.
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useClasses = makeStyles({
+  root: {
+    ':before': {},
+    ':after': {},
+  },
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useClasses = makeStyles({
+  root: {
+    '::before': {},
+    '::after': {},
+  },
+});
+```
diff --git a/packages/eslint-plugin/src/rules/pseudo-element-naming.ts b/packages/eslint-plugin/src/rules/pseudo-element-naming.ts
new file mode 100644
index 0000000..a1db5e8
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/pseudo-element-naming.ts
@@ -0,0 +1,78 @@
+import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
+
+import { createRule } from '../utils/createRule';
+import { isStringLiteral, isMakeStylesIdentifier, isObjectExpression, isProperty } from '../utils/helpers';
+
+export const RULE_NAME = 'pseudo-element-naming';
+
+const PSEUDO_ELEMENTS = [':before', ':after'];
+
+function findInvalidPseudoElementProperties(
+  node: TSESTree.ObjectExpression,
+  isRoot = false,
+  result: TSESTree.StringLiteral[] = [],
+): TSESTree.StringLiteral[] {
+  for (const propertyNode of node.properties) {
+    if (isProperty(propertyNode)) {
+      if (isStringLiteral(propertyNode.key) && !isRoot) {
+        if (PSEUDO_ELEMENTS.includes(propertyNode.key.value)) {
+          result.push(propertyNode.key);
+        }
+      }
+
+      if (isObjectExpression(propertyNode.value)) {
+        findInvalidPseudoElementProperties(propertyNode.value, false, result);
+      }
+    }
+  }
+
+  return result;
+}
+
+export const pseudoElementNamingRule: ReturnType<ReturnType<typeof ESLintUtils.RuleCreator>> = createRule({
+  name: RULE_NAME,
+  meta: {
+    fixable: 'code',
+    type: 'problem',
+    docs: {
+      description: 'Enforce that Pseudo elements start with two colons (::) instead of one colon (:)',
+      recommended: 'error',
+    },
+    messages: {
+      invalidPseudoElementNameFound: 'Pseudo elements must start with two colons (::)',
+    },
+    schema: [
+      {
+        type: 'string',
+      },
+    ],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      CallExpression(node) {
+        if (isMakeStylesIdentifier(node.callee)) {
+          const argument = node.arguments[0];
+
+          if (isObjectExpression(argument)) {
+            const invalidPseudoElementProperties = findInvalidPseudoElementProperties(argument, true);
+
+            invalidPseudoElementProperties.forEach(invalidPseudoElementProperty => {
+              context.report({
+                node: invalidPseudoElementProperty,
+                messageId: 'invalidPseudoElementNameFound',
+                fix: function (fixer) {
+                  const start = invalidPseudoElementProperty.range[0] + 1;
+                  const end = invalidPseudoElementProperty.range[1];
+
+                  return fixer.insertTextBeforeRange([start, end], ':');
+                },
+              });
+            });
+          }
+        }
+      },
+    };
+  },
+});
diff --git a/packages/eslint-plugin/src/utils/helpers.ts b/packages/eslint-plugin/src/utils/helpers.ts
index 11d8608..d33c84b 100644
--- a/packages/eslint-plugin/src/utils/helpers.ts
+++ b/packages/eslint-plugin/src/utils/helpers.ts
@@ -4,6 +4,7 @@ type IsHelper<NodeType extends AST_NODE_TYPES> = (node: TSESTree.Node | null | u
   type: NodeType;
 };
 
+const isLiteral: IsHelper<AST_NODE_TYPES.Literal> = ASTUtils.isNodeOfType(AST_NODE_TYPES.Literal);
 export const isIdentifier: IsHelper<AST_NODE_TYPES.Identifier> = ASTUtils.isIdentifier;
 export const isObjectExpression: IsHelper<AST_NODE_TYPES.ObjectExpression> = ASTUtils.isNodeOfType(
   AST_NODE_TYPES.ObjectExpression,
@@ -13,3 +14,6 @@ export const isProperty: IsHelper<AST_NODE_TYPES.Property> = ASTUtils.isNodeOfTy
 export function isMakeStylesIdentifier(node: TSESTree.Node | null | undefined): node is TSESTree.Identifier {
   return isIdentifier(node) && node.name === 'makeStyles';
 }
+export function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral {
+  return isLiteral(node) && typeof node.value === 'string';
+}
diff --git a/packages/webpack-loader/CHANGELOG.json b/packages/webpack-loader/CHANGELOG.json
index e9a3cd5..c94df60 100644
--- a/packages/webpack-loader/CHANGELOG.json
+++ b/packages/webpack-loader/CHANGELOG.json
@@ -1,6 +1,21 @@
 {
   "name": "@griffel/webpack-loader",
   "entries": [
+    {
+      "date": "Thu, 10 Aug 2023 09:00:28 GMT",
+      "tag": "@griffel/webpack-loader_v2.1.21",
+      "version": "2.1.21",
+      "comments": {
+        "patch": [
+          {
+            "author": "miclo@microsoft.com",
+            "package": "@griffel/webpack-loader",
+            "commit": "74950294ac114b26108214ab8a020d04275e2577",
+            "comment": "feat: Add 'inheritResolveOptions' to configure resolve options in Webpack"
+          }
+        ]
+      }
+    },
     {
       "date": "Mon, 31 Jul 2023 12:18:47 GMT",
       "tag": "@griffel/webpack-loader_v2.1.20",
diff --git a/packages/webpack-loader/CHANGELOG.md b/packages/webpack-loader/CHANGELOG.md
index 4c84b34..6914dee 100644
--- a/packages/webpack-loader/CHANGELOG.md
+++ b/packages/webpack-loader/CHANGELOG.md
@@ -1,9 +1,17 @@
 # Change Log - @griffel/webpack-loader
 
-This log was last generated on Mon, 31 Jul 2023 12:18:47 GMT and should not be manually modified.
+This log was last generated on Thu, 10 Aug 2023 09:00:28 GMT and should not be manually modified.
 
 <!-- Start content -->
 
+## 2.1.21
+
+Thu, 10 Aug 2023 09:00:28 GMT
+
+### Patches
+
+- feat: Add 'inheritResolveOptions' to configure resolve options in Webpack (miclo@microsoft.com)
+
 ## 2.1.20
 
 Mon, 31 Jul 2023 12:18:47 GMT
diff --git a/packages/webpack-loader/README.md b/packages/webpack-loader/README.md
index a346662..d411de3 100644
--- a/packages/webpack-loader/README.md
+++ b/packages/webpack-loader/README.md
@@ -10,6 +10,7 @@ A loader for Webpack 5 that performs build time transforms for [`@griffel/react`
 - [Usage](#usage)
   - [Handling Griffel re-exports](#handling-griffel-re-exports)
   - [Configuring Babel settings](#configuring-babel-settings)
+  - [Configuring webpack resolve options](#configuring-webpack-resolve-options)
   - [Configuring module evaluation](#configuring-module-evaluation)
 - [Troubleshooting](#troubleshooting)
 
@@ -157,6 +158,29 @@ module.exports = {
 };
 ```
 
+### Configuring webpack resolve options
+If your `@griffel/react` modules import other files (eg., a set of common mixins or colors for your app), the loader resolves these using `enhanced-resolve`. By default, it inherits the settings `resolve.alias`, `resolve.modules`, and `resolve.modules` from your Webpack config, while using its own default values for `resolve.extensions` and `resolve.conditionNames`.
+
+If you want to change this behavior, you can choose which [`resolve` options](https://webpack.js.org/configuration/resolve/) are inherited from your Webpack config.
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.(js|jsx)$/,
+        exclude: /node_modules/,
+        use: {
+          loader: '@griffel/webpack-loader',
+          options: {
+            inheritResolveOptions: ['alias', 'modules', 'plugins', 'conditionNames'],
+          },
+        },
+      },
+    ],
+  },
+};
+```
+
 ### Configuring module evaluation
 
 ```js
diff --git a/packages/webpack-loader/__fixtures__/webpack-resolve-options/code.ts b/packages/webpack-loader/__fixtures__/webpack-resolve-options/code.ts
new file mode 100644
index 0000000..daf6f7f
--- /dev/null
+++ b/packages/webpack-loader/__fixtures__/webpack-resolve-options/code.ts
@@ -0,0 +1,8 @@
+import { makeStyles } from '@griffel/react';
+import color from './color';
+
+export const styles = makeStyles({
+  root: {
+    backgroundColor: color,
+  },
+});
diff --git a/packages/webpack-loader/__fixtures__/webpack-resolve-options/color.jsx b/packages/webpack-loader/__fixtures__/webpack-resolve-options/color.jsx
new file mode 100644
index 0000000..cd0cebe
--- /dev/null
+++ b/packages/webpack-loader/__fixtures__/webpack-resolve-options/color.jsx
@@ -0,0 +1,3 @@
+const color = 'blue';
+
+export default color;
\ No newline at end of file
diff --git a/packages/webpack-loader/__fixtures__/webpack-resolve-options/output.ts b/packages/webpack-loader/__fixtures__/webpack-resolve-options/output.ts
new file mode 100644
index 0000000..3e9985b
--- /dev/null
+++ b/packages/webpack-loader/__fixtures__/webpack-resolve-options/output.ts
@@ -0,0 +1,12 @@
+import { __styles } from '@griffel/react';
+import color from './color';
+export const styles = __styles(
+  {
+    root: {
+      De3pzq: 'f1bh81bl',
+    },
+  },
+  {
+    d: ['.f1bh81bl{background-color:blue;}'],
+  },
+);
diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json
index 7b3acc6..7d0efca 100644
--- a/packages/webpack-loader/package.json
+++ b/packages/webpack-loader/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@griffel/webpack-loader",
-  "version": "2.1.20",
+  "version": "2.1.21",
   "description": "Webpack loader that performs build time transforms for Griffel",
   "license": "MIT",
   "repository": {
diff --git a/packages/webpack-loader/src/schema.ts b/packages/webpack-loader/src/schema.ts
new file mode 100644
index 0000000..1764c17
--- /dev/null
+++ b/packages/webpack-loader/src/schema.ts
@@ -0,0 +1,13 @@
+import type { JSONSchema7 } from 'json-schema';
+import { configSchema } from '@griffel/babel-preset';
+
+export const optionsSchema: JSONSchema7 = {
+  ...configSchema,
+  properties: {
+    ...configSchema.properties,
+    inheritResolveOptions: {
+      type: 'array',
+      items: { type: 'string', enum: ['alias', 'modules', 'plugins', 'conditionNames', 'extensions'] },
+    },
+  },
+};
diff --git a/packages/webpack-loader/src/webpackLoader.ts b/packages/webpack-loader/src/webpackLoader.ts
index 798ff7b..5ef1cdb 100644
--- a/packages/webpack-loader/src/webpackLoader.ts
+++ b/packages/webpack-loader/src/webpackLoader.ts
@@ -1,11 +1,14 @@
-import { configSchema, BabelPluginOptions, EvalCache, Module } from '@griffel/babel-preset';
+import { BabelPluginOptions, EvalCache, Module } from '@griffel/babel-preset';
 import * as enhancedResolve from 'enhanced-resolve';
 import * as path from 'path';
 import * as webpack from 'webpack';
 
 import { transformSync, TransformResult, TransformOptions } from './transformSync';
+import { optionsSchema } from './schema';
 
-export type WebpackLoaderOptions = BabelPluginOptions;
+export type WebpackLoaderOptions = BabelPluginOptions & {
+  inheritResolveOptions?: ('alias' | 'modules' | 'plugins' | 'conditionNames' | 'extensions')[];
+};
 
 type WebpackLoaderParams = Parameters<webpack.LoaderDefinitionFunction<WebpackLoaderOptions>>;
 
@@ -46,10 +49,10 @@ export function webpackLoader(
   // https://github.com/webpack/webpack/issues/14946
   this.cacheable();
 
-  const options = this.getOptions(configSchema);
+  const { inheritResolveOptions = ['alias', 'modules', 'plugins'], ...babelConfig } = this.getOptions(optionsSchema);
 
   // Early return to handle cases when makeStyles() calls are not present, allows to avoid expensive invocation of Babel
-  if (!shouldTransformSourceCode(sourceCode, options.modules)) {
+  if (!shouldTransformSourceCode(sourceCode, babelConfig.modules)) {
     this.callback(null, sourceCode, inputSourceMap);
     return;
   }
@@ -68,9 +71,12 @@ export function webpackLoader(
 
   const resolveSync = enhancedResolve.create.sync({
     ...resolveOptionsDefaults,
-    alias: resolveOptionsFromWebpackConfig.alias,
-    modules: resolveOptionsFromWebpackConfig.modules,
-    plugins: resolveOptionsFromWebpackConfig.plugins,
+    ...Object.fromEntries(
+      inheritResolveOptions.map(resolveOptionKey => [
+        resolveOptionKey,
+        resolveOptionsFromWebpackConfig[resolveOptionKey],
+      ]),
+    ),
   });
 
   const originalResolveFilename = Module._resolveFilename;
@@ -101,7 +107,7 @@ export function webpackLoader(
       enableSourceMaps: this.sourceMap || false,
       inputSourceMap: parseSourceMap(inputSourceMap),
 
-      pluginOptions: options,
+      pluginOptions: babelConfig,
     });
   } catch (err) {
     error = err as Error;


Test patch:
diff --git a/change/@griffel-eslint-plugin-de59e2ef-e29f-4852-bc03-5bf3b563f991.json b/change/@griffel-eslint-plugin-de59e2ef-e29f-4852-bc03-5bf3b563f991.json
new file mode 100644
index 0000000..fbf72df
--- /dev/null
+++ b/change/@griffel-eslint-plugin-de59e2ef-e29f-4852-bc03-5bf3b563f991.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: add pseudo element naming rule",
+  "packageName": "@griffel/eslint-plugin",
+  "email": "levin@uncu.de",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/eslint-plugin/src/rules/pseudo-element-naming.test.ts b/packages/eslint-plugin/src/rules/pseudo-element-naming.test.ts
new file mode 100644
index 0000000..e6c01d5
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/pseudo-element-naming.test.ts
@@ -0,0 +1,86 @@
+import { TSESLint } from '@typescript-eslint/utils';
+import * as path from 'path';
+
+import { pseudoElementNamingRule, RULE_NAME } from './pseudo-element-naming';
+
+const ruleTester = new TSESLint.RuleTester({
+  parser: path.resolve('./node_modules/@typescript-eslint/parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run(RULE_NAME, pseudoElementNamingRule, {
+  valid: [
+    {
+      name: 'without Pseudo Element',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: { backgroundColor: 'red' },
+});
+`,
+    },
+    {
+      name: 'with Valid Pseudo Element',
+      code: `
+    import { makeStyles } from '@griffel/react';
+
+    export const useStyles = makeStyles({
+      root: { '::before': { backgroundColor: 'red' } },
+    });
+    `,
+    },
+    {
+      name: 'Invalid Pseudo Elements can be used as slot names',
+      code: `
+    import { makeStyles } from '@griffel/react';
+
+    export const useStyles = makeStyles({
+      ':before': { backgroundColor: 'red' },
+    });
+    `,
+    },
+  ],
+
+  invalid: [
+    {
+      name: 'Invalid Pseudo Element',
+      code: `
+    import { makeStyles } from '@griffel/react';
+    export const useStyles = makeStyles({
+      root: { ':before': { backgroundColor: 'red' } },
+    });
+    `,
+      output: `
+    import { makeStyles } from '@griffel/react';
+    export const useStyles = makeStyles({
+      root: { '::before': { backgroundColor: 'red' } },
+    });
+    `,
+      errors: [{ messageId: 'invalidPseudoElementNameFound' }],
+    },
+    {
+      name: 'Invalid Pseudo Element in a selector',
+      code: `
+    import { makeStyles } from '@griffel/react';
+    export const useStyles = makeStyles({
+      root: {
+        ':hover': { ':before': { backgroundColor: 'red' } }
+      },
+    });
+    `,
+      output: `
+    import { makeStyles } from '@griffel/react';
+    export const useStyles = makeStyles({
+      root: {
+        ':hover': { '::before': { backgroundColor: 'red' } }
+      },
+    });
+    `,
+      errors: [{ messageId: 'invalidPseudoElementNameFound' }],
+    },
+  ],
+});
diff --git a/packages/webpack-loader/src/webpackLoader.test.ts b/packages/webpack-loader/src/webpackLoader.test.ts
index 2acae75..7ca1135 100644
--- a/packages/webpack-loader/src/webpackLoader.test.ts
+++ b/packages/webpack-loader/src/webpackLoader.test.ts
@@ -257,6 +257,18 @@ describe('webpackLoader', () => {
     },
   });
 
+  // Asserts that "inheritResolveOptions" are handled properly
+  testFixture('webpack-resolve-options', {
+    loaderOptions: {
+      inheritResolveOptions: ['extensions'],
+    },
+    webpackConfig: {
+      resolve: {
+        extensions: ['.ts', '.jsx'],
+      },
+    },
+  });
+
   // Asserts that aliases are resolved properly in Babel plugin with resolve plugins
   testFixture('webpack-resolve-plugins', {
     webpackConfig: {