Repository: eslint/css
Pull request #103: feat: Type checking for all APIs and rules
Author: login: nzakas
Labels: name: feature
name: accepted
description: There is consensus among the team that this change meets the criteria for inclusion
Created: 2025-04-02T18:47:07Z | Merged: 2025-04-13T09:50:38Z

<!--
    Thank you for contributing!

    ESLint adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/latest/contribute/pull-requests)
    - Update or create tests
    - If performance-related, include a benchmark
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What is the purpose of this pull request?

Enable type checking for the plugin.

#### What changes did you make? (Give an overview)

- Added type definitions to match `@eslint/json`
- Added type definitions for each rule
- Updated `@eslint/css-tree`

#### Related Issues

<!-- include tags like "fixes #123" or "refs #123" -->

#### Is there anything you'd like reviewers to focus on?

Right now, Rollup is removing the type definitions for `no-invalid-at-rules` and `no-invalid-properties`. I can't quite figure out why that is. I could use some help with this.


<!-- markdownlint-disable-file MD004 -->


Linked issue #123: Bug: Parsing errors in nested selectors
### Environment

ESLint version: `9.26.0`
@eslint/css version: `0.7.0`
Node version: `23.6.1`
npm version: `10.9.0`
Operating System: `Debian 12`

### Which language are you using?

stylesheet

### What did you do?

<details>
<summary>Configuration</summary>

```js
import css from "@eslint/css";
import {defineConfig} from "eslint/config";

export default defineConfig([
  {files: ["**/*.css"], plugins: {css}, language: "css/css", extends: ["css/recommended"]},
]);

```
</details>

I get parsing errors in nested selectors - see comments in the following CSS example:

```css
.class1 {
  background-color: blue;

  span { /* <------------- Parsing error: Colon is expected */
    background-color: green;
  }
}

.class2 {
  background-color: yellow;

  div[class^="x"] { /* <------------- Parsing error: Colon is expected */
    background-color: red;
  }
}

.class3 {
  background-color: black;

  > thead { /* <------------- Parsing error: Identifier is expected */
    background-color: white;
  }
}
```


### What did you expect to happen?

No errors.

### What actually happened?

```bash
   4:8  error  Parsing error: Colon is expected
  12:6  error  Parsing error: Colon is expected
  20:3  error  Parsing error: Identifier is expected
```

### Link to Minimal Reproducible Example

-

### Participation

- [ ] I am willing to submit a pull request for this issue.

### Additional comments

-
Linked issue comments: user: login: cbovis
created_at: 2025-05-05T08:54:20Z
body: @KristjanESPERANTO tolerant parsing needs to be enabled for nested selectors.

https://github.com/eslint/css?tab=readme-ov-file#tolerant-mode
author_association: NONE
user: login: Tanujkanti4441
created_at: 2025-05-05T10:51:23Z
body: Yes, we just need to enable tolerant option here to avoid reporting parsing errors.

```js
languageOptions: {
     tolerant: true,
},
```

 [ASTExplorer](https://explorer.eslint.org/#eslint-explorer=IntcInN0YXRlXCI6e1widG9vbFwiOlwiYXN0XCIsXCJjb2RlXCI6e1wiamF2YXNjcmlwdFwiOlwiY2xhc3MgQSB7XFxuICAgICAgYSA9IDU7XFxuICAgICAgYiA9IHRoaXMuYTtcXG4gICAgICBjID0gdGhpcy5hO1xcbn1cIixcImpzb25cIjpcIi8qKlxcbiAqIFR5cGUgb3IgcGFzdGUgc29tZSBKU09OIGhlcmUgdG8gbGVhcm4gbW9yZSBhYm91dFxcbiAqIHRoZSBzdGF0aWMgYW5hbHlzaXMgdGhhdCBFU0xpbnQgY2FuIGRvIGZvciB5b3UuXFxuICpcXG4gKiBUaGUgdGFicyBhcmU6XFxuICpcXG4gKiAtIEFTVCAtIFRoZSBBYnN0cmFjdCBTeW50YXggVHJlZSBvZiB0aGUgY29kZSwgd2hpY2ggY2FuXFxuICogICBiZSB1c2VmdWwgdG8gdW5kZXJzdGFuZCB0aGUgc3RydWN0dXJlIG9mIHRoZSBjb2RlLiBZb3VcXG4gKiAgIGNhbiB2aWV3IHRoaXMgc3RydWN0dXJlIGFzIEpTT04gb3IgaW4gYSB0cmVlIGZvcm1hdC5cXG4gKlxcbiAqIFlvdSBjYW4gY2hhbmdlIHRoZSB3YXkgdGhhdCB0aGUgSlNPTiBjb2RlIGlzIGludGVycHJldGVkXFxuICogYnkgY2xpY2tpbmcgXFxcIkpTT05cXFwiIGluIHRoZSBoZWFkZXIgYW5kIHNlbGVjdGluZyBkaWZmZXJlbnRcXG4gKiBvcHRpb25zLlxcbiAqXFxuICogVGhpcyBleGFtcGxlIGlzIGluIEpTT05DIG1vZGUsIHdoaWNoIGFsbG93cyBjb21tZW50cy5cXG4gKi9cXG5cXG57XFxuICAgIFxcXCJrZXkxXFxcIjogW3RydWUsIGZhbHNlLCBudWxsXSxcXG4gICAgXFxcImtleTJcXFwiOiB7XFxuICAgICAgICBcXFwia2V5M1xcXCI6IFsxLCAyLCBcXFwiM1xcXCIsIDFlMTAsIDFlLTNdXFxuICAgIH1cXG59XCIsXCJtYXJrZG93blwiOlwiPCEtLVxcblR5cGUgb3IgcGFzdGUgc29tZSBNYXJrZG93biBoZXJlIHRvIGxlYXJuIG1vcmUgYWJvdXRcXG50aGUgc3RhdGljIGFuYWx5c2lzIHRoYXQgRVNMaW50IGNhbiBkbyBmb3IgeW91LlxcblxcblRoZSB0YWJzIGFyZTpcXG5cXG4tIEFTVCAtIFRoZSBBYnN0cmFjdCBTeW50YXggVHJlZSBvZiB0aGUgY29kZSwgd2hpY2ggY2FuXFxuYmUgdXNlZnVsIHRvIHVuZGVyc3RhbmQgdGhlIHN0cnVjdHVyZSBvZiB0aGUgY29kZS4gWW91XFxuY2FuIHZpZXcgdGhpcyBzdHJ1Y3R1cmUgYXMgSlNPTiBvciBpbiBhIHRyZWUgZm9ybWF0LlxcblxcbllvdSBjYW4gY2hhbmdlIHRoZSB3YXkgdGhhdCB0aGUgTWFya2Rvd24gY29kZSBpcyBpbnRlcnByZXRlZFxcbmJ5IGNsaWNraW5nIFxcXCJNYXJrZG93blxcXCIgaW4gdGhlIGhlYWRlciBhbmQgc2VsZWN0aW5nIGRpZmZlcmVudFxcbm9wdGlvbnMuXFxuXFxuVGhpcyBleGFtcGxlIGlzIGluIENvbW1vbk1hcmsgbW9kZS5cXG4tLT5cXG5cXG4jIEVTTGludCBNYXJrZG93biBFeGFtcGxlXFxuXFxuVGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgTWFya2Rvd24gZmlsZSB0aGF0IGNhbiBiZSBwYXJzZWRcXG5ieSBFU0xpbnQuIE1hcmtkb3duIGlzIGEgc2ltcGxlIG1hcmt1cCBsYW5ndWFnZSB0aGF0IGlzXFxub2Z0ZW4gdXNlZCBmb3IgZG9jdW1lbnRhdGlvbi5cXG5cXG4jIyBGZWF0dXJlc1xcblxcbi0gTWFrZSB0aGluZ3MgKml0YWxpYyosICoqYm9sZCoqLCBvciBgY29kZWBcXG4tIENyZWF0ZSBbbGlua3NdKGh0dHBzOi8vZXNsaW50Lm9yZylcXG4tIFN1cHBvcnRzIEhUTUwgPHNwYW4gc3R5bGU9XFxcImNvbG9yOiByZWQ7XFxcIj5lbGVtZW50czwvc3Bhbj5cXG4tIExpc3RzXFxuICAtIE5lc3RlZCBsaXN0c1wiLFwiY3NzXCI6XCIuY2xhc3MxIHtcXG4gIGJhY2tncm91bmQtY29sb3I6IGJsdWU7XFxuXFxuICBzcGFuIHsgLyogPC0tLS0tLS0tLS0tLS0gUGFyc2luZyBlcnJvcjogQ29sb24gaXMgZXhwZWN0ZWQgKi9cXG4gICAgYmFja2dyb3VuZC1jb2xvcjogZ3JlZW47XFxuICB9XFxufVxcblxcbi5jbGFzczIge1xcbiAgYmFja2dyb3VuZC1jb2xvcjogeWVsbG93O1xcblxcbiAgZGl2W2NsYXNzXj1cXFwieFxcXCJdIHsgLyogPC0tLS0tLS0tLS0tLS0gUGFyc2luZyBlcnJvcjogQ29sb24gaXMgZXhwZWN0ZWQgKi9cXG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmVkO1xcbiAgfVxcbn1cXG5cXG4uY2xhc3MzIHtcXG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xcblxcbiAgPiB0aGVhZCB7IC8qIDwtLS0tLS0tLS0tLS0tIFBhcnNpbmcgZXJyb3I6IElkZW50aWZpZXIgaXMgZXhwZWN0ZWQgKi9cXG4gICAgYmFja2dyb3VuZC1jb2xvcjogd2hpdGU7XFxuICB9XFxufVwifSxcImxhbmd1YWdlXCI6XCJjc3NcIixcImpzT3B0aW9uc1wiOntcInBhcnNlclwiOlwiZXNwcmVlXCIsXCJzb3VyY2VUeXBlXCI6XCJtb2R1bGVcIixcImVzVmVyc2lvblwiOlwibGF0ZXN0XCIsXCJpc0pTWFwiOnRydWV9LFwianNvbk9wdGlvbnNcIjp7XCJqc29uTW9kZVwiOlwianNvbmNcIn0sXCJjc3NPcHRpb25zXCI6e1wiY3NzTW9kZVwiOlwiY3NzXCIsXCJ0b2xlcmFudFwiOnRydWV9LFwibWFya2Rvd25PcHRpb25zXCI6e1wibWFya2Rvd25Nb2RlXCI6XCJjb21tb25tYXJrXCJ9LFwid3JhcFwiOnRydWUsXCJ2aWV3TW9kZXNcIjp7XCJhc3RWaWV3XCI6XCJ0cmVlXCIsXCJzY29wZVZpZXdcIjpcImZsYXRcIixcInBhdGhWaWV3XCI6XCJncmFwaFwifSxcInBhdGhJbmRleFwiOntcImluZGV4XCI6MSxcImluZGV4ZXNcIjozfSxcImVzcXVlcnlTZWxlY3RvclwiOntcInNlbGVjdG9yXCI6XCJcIn19LFwidmVyc2lvblwiOjB9Ig%3D%3D)
author_association: CONTRIBUTOR
user: login: KristjanESPERANTO
created_at: 2025-05-08T06:20:16Z
body: Thank you both, that helped me! 🙂

But isn't the following sentence [in the docs](https://github.com/eslint/css#tolerant-mode) wrong?

`Setting tolerant to true is necessary if you are using custom syntax ... that aren't part of the standard CSS syntax.`

Nesting is now standard CSS syntax.
author_association: NONE
user: login: nzakas
created_at: 2025-05-08T14:57:14Z
body: Yes, nesting is part of CSS now and we are working on getting it implemented in the parser. It's not all there yet, so you can use `tolerant: true` to avoid parsing errors in the meantime.
author_association: MEMBER
user: login: KristjanESPERANTO
created_at: 2025-05-08T18:59:45Z
body: Many thanks for the explanation and also for all the work on ESLint! I am thrilled about the steps you have taken recently with ESLint.
author_association: NONE
user: login: nzakas
created_at: 2025-05-08T19:23:13Z
body: Thanks @KristjanESPERANTO for the feedback. We appreciate it! 🙏 
author_association: MEMBER
user: login: antti5
created_at: 2025-09-01T22:49:49Z
body: I also ran into this issue with certain nested CSS selectors, and I was about to create a new issue. However, I decided double-check the closed issues, and it turned out that the tolerant mode indeed solved also my issue.

Maybe add a mention of this to the NPM / GitHub pages? Tolerant mode is described, but there's no mention that it could be necessary also for perfectly standard CSS syntax.

One of the selectors that caused parsing errors for me is what I think is really quite common in nested CSS: `a:focus &`
author_association: NONE
user: login: nzakas
created_at: 2025-09-02T14:56:27Z
body: @antti5 please open a separate issue with a code example that is still failing for you.
author_association: MEMBER

Diff:
diff --git a/jsr.json b/jsr.json
index 5dd5462b..9c8950b5 100644
--- a/jsr.json
+++ b/jsr.json
@@ -7,10 +7,7 @@
   },
   "publish": {
     "include": [
-      "dist/esm/index.js",
-      "dist/esm/index.d.ts",
-      "dist/esm/syntax/index.js",
-      "dist/esm/syntax/index.d.ts",
+      "dist/esm",
       "README.md",
       "jsr.json",
       "LICENSE"
diff --git a/package.json b/package.json
index 07edcaea..75766be1 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
   "scripts": {
     "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
     "build:cts": "node -e \"fs.copyFileSync('dist/esm/index.d.ts', 'dist/cjs/index.d.cts')\"",
-    "build:syntax-cts": "node -e \"fs.copyFileSync('dist/esm/syntax/index.d.ts', 'dist/cjs/syntax/index.d.cts')\"",
+    "build:syntax-cts": "node -e \"fs.copyFileSync('dist/esm/syntax/index.d.ts', 'dist/cjs/syntax/index.d.cts')\" && node tools/update-cts.js dist/cjs/types.cts dist/cjs/index.d.cts",
     "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts && tsc -p tsconfig.syntax.json && npm run build:syntax-cts",
     "build:readme": "node tools/update-readme.js",
     "build:update-rules-docs": "node tools/update-rules-docs.js",
@@ -85,8 +85,8 @@
   ],
   "license": "Apache-2.0",
   "dependencies": {
-    "@eslint/core": "^0.10.0",
-    "@eslint/css-tree": "^3.3.1",
+    "@eslint/core": "^0.13.0",
+    "@eslint/css-tree": "^3.3.3",
     "@eslint/plugin-kit": "^0.2.5"
   },
   "devDependencies": {
@@ -94,7 +94,7 @@
     "c8": "^9.1.0",
     "compute-baseline": "^0.3.0",
     "dedent": "^1.5.3",
-    "eslint": "^9.11.1",
+    "eslint": "^9.24.0",
     "eslint-config-eslint": "^11.0.0",
     "eslint-plugin-eslint-plugin": "^6.3.2",
     "got": "^14.4.2",
@@ -105,7 +105,8 @@
     "prettier": "^3.4.1",
     "rollup": "^4.16.2",
     "rollup-plugin-copy": "^3.5.0",
-    "typescript": "^5.4.5",
+    "rollup-plugin-delete": "^3.0.1",
+    "typescript": "^5.8.2",
     "web-features": "^2.23.0",
     "yorkie": "^2.0.0"
   },
diff --git a/rollup.config.js b/rollup.config.js
index 4f33b062..f8c6a48a 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,3 +1,6 @@
+import copy from "rollup-plugin-copy";
+import del from "rollup-plugin-delete";
+
 export default [
 	{
 		input: "src/index.js",
@@ -12,6 +15,19 @@ export default [
 				banner: '// @ts-self-types="./index.d.ts"',
 			},
 		],
+		plugins: [
+			del({ targets: ["dist/*"] }),
+			copy({
+				targets: [
+					{
+						src: "src/types.ts",
+						dest: "dist/cjs",
+						rename: "types.cts",
+					},
+					{ src: "src/types.ts", dest: "dist/esm" },
+				],
+			}),
+		],
 	},
 	{
 		input: "src/syntax/index.js",
diff --git a/src/index.js b/src/index.js
index cb6aeda2..c84d4f2d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -58,4 +58,5 @@ const plugin = {
 }
 
 export default plugin;
-export { CSSLanguage, CSSSourceCode };
+export { CSSSourceCode };
+export * from "./languages/css-language.js";
diff --git a/src/languages/css-language.js b/src/languages/css-language.js
index e14071b8..c88ed74f 100644
--- a/src/languages/css-language.js
+++ b/src/languages/css-language.js
@@ -21,18 +21,13 @@ import { visitorKeys } from "./css-visitor-keys.js";
 // Types
 //-----------------------------------------------------------------------------
 
-/** @typedef {import("@eslint/css-tree").CssNode} CssNode */
-/** @typedef {import("@eslint/css-tree").CssNodePlain} CssNodePlain */
-/** @typedef {import("@eslint/css-tree").StyleSheet} StyleSheet */
-/** @typedef {import("@eslint/css-tree").Comment} Comment */
-/** @typedef {import("@eslint/css-tree").Lexer} Lexer */
-/** @typedef {import("@eslint/css-tree").SyntaxConfig} SyntaxConfig */
-/** @typedef {import("@eslint/core").Language} Language */
-/** @typedef {import("@eslint/core").OkParseResult<CssNodePlain> & { comments: Comment[], lexer: Lexer }} OkParseResult */
-/** @typedef {import("@eslint/core").ParseResult<CssNodePlain>} ParseResult */
-/** @typedef {import("@eslint/core").File} File */
-/** @typedef {import("@eslint/core").FileError} FileError */
+/**
+ * @import { CssNodePlain, Comment, Lexer, StyleSheetPlain, SyntaxConfig } from "@eslint/css-tree"
+ * @import { Language, OkParseResult, ParseResult, File, FileError } from "@eslint/core";
+ */
 
+/** @typedef {OkParseResult<StyleSheetPlain> & { comments: Comment[], lexer: Lexer }} CSSOkParseResult */
+/** @typedef {ParseResult<StyleSheetPlain>} CSSParseResult */
 /**
  * @typedef {Object} CSSLanguageOptions
  * @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
@@ -62,7 +57,7 @@ const blockCloserTokenTypes = new Map([
 
 /**
  * CSS Language Object
- * @implements {Language}
+ * @implements {Language<{ LangOptions: CSSLanguageOptions; Code: CSSSourceCode; RootNode: StyleSheetPlain; Node: CssNodePlain}>}
  */
 export class CSSLanguage {
 	/**
@@ -135,7 +130,7 @@ export class CSSLanguage {
 	 * @param {File} file The virtual file to parse.
 	 * @param {Object} [context] The parsing context.
 	 * @param {CSSLanguageOptions} [context.languageOptions] The language options to use for parsing.
-	 * @returns {ParseResult} The result of parsing.
+	 * @returns {CSSParseResult} The result of parsing.
 	 */
 	parse(file, { languageOptions = {} } = {}) {
 		// Note: BOM already removed
@@ -242,7 +237,7 @@ export class CSSLanguage {
 
 			return {
 				ok: true,
-				ast: root,
+				ast: /** @type {StyleSheetPlain} */ (root),
 				comments,
 				lexer,
 			};
@@ -257,7 +252,7 @@ export class CSSLanguage {
 	/**
 	 * Creates a new `CSSSourceCode` object from the given information.
 	 * @param {File} file The virtual file to create a `CSSSourceCode` object from.
-	 * @param {OkParseResult} parseResult The result returned from `parse()`.
+	 * @param {CSSOkParseResult} parseResult The result returned from `parse()`.
 	 * @returns {CSSSourceCode} The new `CSSSourceCode` object.
 	 */
 	createSourceCode(file, parseResult) {
diff --git a/src/languages/css-source-code.js b/src/languages/css-source-code.js
index 41200fe9..da6a43e0 100644
--- a/src/languages/css-source-code.js
+++ b/src/languages/css-source-code.js
@@ -19,21 +19,12 @@ import { visitorKeys } from "./css-visitor-keys.js";
 // Types
 //-----------------------------------------------------------------------------
 
-/** @typedef {import("@eslint/css-tree").CssNode} CssNode */
-/** @typedef {import("@eslint/css-tree").CssNodePlain} CssNodePlain */
-/** @typedef {import("@eslint/css-tree").BlockPlain} BlockPlain */
-/** @typedef {import("@eslint/css-tree").Comment} Comment */
-/** @typedef {import("@eslint/css-tree").Lexer} Lexer */
-/** @typedef {import("@eslint/core").SourceRange} SourceRange */
-/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
-/** @typedef {import("@eslint/core").SourceLocationWithOffset} SourceLocationWithOffset */
-/** @typedef {import("@eslint/core").File} File */
-/** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
-/** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
-/** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
-/** @typedef {import("@eslint/core").FileProblem} FileProblem */
-/** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
-/** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
+/**
+ * @import { CssNode, CssNodePlain, Comment, Lexer, StyleSheetPlain } from "@eslint/css-tree"
+ * @import { SourceRange, SourceLocation, FileProblem, DirectiveType, RulesConfig, TextSourceCode } from "@eslint/core"
+ * @import { CSSSyntaxElement } from "../types.js"
+ * @import { CSSLanguageOptions } from "./css-language.js"
+ */
 
 //-----------------------------------------------------------------------------
 // Helpers
@@ -73,7 +64,8 @@ class CSSTraversalStep extends VisitNodeStep {
 //-----------------------------------------------------------------------------
 
 /**
- * CSS Source Code Object
+ * CSS Source Code Object.
+ * @implements {TextSourceCode<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
  */
 export class CSSSourceCode extends TextSourceCodeBase {
 	/**
@@ -96,7 +88,7 @@ export class CSSSourceCode extends TextSourceCodeBase {
 
 	/**
 	 * The AST of the source code.
-	 * @type {CssNodePlain}
+	 * @type {StyleSheetPlain}
 	 */
 	ast = undefined;
 
@@ -116,7 +108,7 @@ export class CSSSourceCode extends TextSourceCodeBase {
 	 * Creates a new instance.
 	 * @param {Object} options The options for the instance.
 	 * @param {string} options.text The source code text.
-	 * @param {CssNodePlain} options.ast The root AST node.
+	 * @param {StyleSheetPlain} options.ast The root AST node.
 	 * @param {Array<Comment>} options.comments The comment nodes in the source code.
 	 * @param {Lexer} options.lexer The lexer used to parse the source code.
 	 */
diff --git a/src/rules/no-duplicate-imports.js b/src/rules/no-duplicate-imports.js
index c1ff97b6..9613b56a 100644
--- a/src/rules/no-duplicate-imports.js
+++ b/src/rules/no-duplicate-imports.js
@@ -3,9 +3,26 @@
  * @author Nicholas C. Zakas
  */
 
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"duplicateImport"} NoDuplicateKeysMessageIds
+ * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
+ */
+
+//-----------------------------------------------------------------------------
+// Rule
+//-----------------------------------------------------------------------------
+
+/**
+ * @type {NoDuplicateImportsRuleDefinition}
+ */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Disallow duplicate @import rules",
diff --git a/src/rules/no-empty-blocks.js b/src/rules/no-empty-blocks.js
index d715524b..2836dd8c 100644
--- a/src/rules/no-empty-blocks.js
+++ b/src/rules/no-empty-blocks.js
@@ -3,9 +3,24 @@
  * @author Nicholas C. Zakas
  */
 
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"emptyBlock"} NoEmptyBlocksMessageIds
+ * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoEmptyBlocksMessageIds }>} NoEmptyBlocksRuleDefinition
+ */
+
+//-----------------------------------------------------------------------------
+// Rule Definition
+//-----------------------------------------------------------------------------
+
+/** @type {NoEmptyBlocksRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Disallow empty blocks",
diff --git a/src/rules/no-invalid-at-rules.js b/src/rules/no-invalid-at-rules.js
index 71a1e0f2..c2c0204d 100644
--- a/src/rules/no-invalid-at-rules.js
+++ b/src/rules/no-invalid-at-rules.js
@@ -9,6 +9,17 @@
 
 import { isSyntaxMatchError } from "../util.js";
 
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { AtrulePlain } from "@eslint/css-tree"
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude"} NoInvalidAtRulesMessageIds
+ * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulesMessageIds }>} NoInvalidAtRulesRuleDefinition
+ */
+
 //-----------------------------------------------------------------------------
 // Helpers
 //-----------------------------------------------------------------------------
@@ -41,9 +52,10 @@ function extractMetaDataFromError(error) {
 // Rule Definition
 //-----------------------------------------------------------------------------
 
+/** @type {NoInvalidAtRulesRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Disallow invalid at-rules",
@@ -110,7 +122,9 @@ export default {
 
 			"AtRule > Block > Declaration"(node) {
 				// get at rule node
-				const atRule = sourceCode.getParent(sourceCode.getParent(node));
+				const atRule = /** @type {AtrulePlain} */ (
+					sourceCode.getParent(sourceCode.getParent(node))
+				);
 
 				const { error } = lexer.matchAtruleDescriptor(
 					atRule.name,
diff --git a/src/rules/no-invalid-properties.js b/src/rules/no-invalid-properties.js
index 9cd20ee9..d88de047 100644
--- a/src/rules/no-invalid-properties.js
+++ b/src/rules/no-invalid-properties.js
@@ -9,13 +9,24 @@
 
 import { isSyntaxMatchError } from "../util.js";
 
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"invalidPropertyValue" | "unknownProperty"} NoInvalidPropertiesMessageIds
+ * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
+ */
+
 //-----------------------------------------------------------------------------
 // Rule Definition
 //-----------------------------------------------------------------------------
 
+/** @type {NoInvalidPropertiesRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Disallow invalid properties",
diff --git a/src/rules/prefer-logical-properties.js b/src/rules/prefer-logical-properties.js
index e8bce544..27e8ac3d 100644
--- a/src/rules/prefer-logical-properties.js
+++ b/src/rules/prefer-logical-properties.js
@@ -1,3 +1,17 @@
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"notLogicalProperty" | "notLogicalValue" | "notLogicalUnit"} PreferLogicalPropertiesMessageIds
+ * @typedef {[{
+ *     allowProperties?: string[],
+ *     allowUnits?: string[]
+ * }]} PreferLogicalPropertiesOptions
+ * @typedef {CSSRuleDefinition<{ RuleOptions: PreferLogicalPropertiesOptions, MessageIds: PreferLogicalPropertiesMessageIds }>} PreferLogicalPropertiesRuleDefinition
+ */
+
 //-----------------------------------------------------------------------------
 // Helpers
 //-----------------------------------------------------------------------------
@@ -118,11 +132,13 @@ const unitReplacements = new Map([
 //-----------------------------------------------------------------------------
 // Rule Definition
 //-----------------------------------------------------------------------------
+
+/** @type {PreferLogicalPropertiesRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
-		fixable: /** @type {const} */ ("code"),
+		fixable: "code",
 
 		docs: {
 			description: "Enforce the use of logical properties",
@@ -194,6 +210,7 @@ export default {
 
 				if (
 					propertyValuesReplacements.get(node.property) &&
+					node.value.type === "Value" &&
 					node.value.children[0].type === "Identifier"
 				) {
 					const nodeValue = node.value.children[0].name;
diff --git a/src/rules/use-baseline.js b/src/rules/use-baseline.js
index 03d9bde8..fbb7d00c 100644
--- a/src/rules/use-baseline.js
+++ b/src/rules/use-baseline.js
@@ -23,9 +23,15 @@ import { namedColors } from "../data/colors.js";
 // Type Definitions
 //-----------------------------------------------------------------------------
 
-/** @typedef {import("@eslint/css-tree").AtrulePlain} AtrulePlain */
-/** @typedef {import("@eslint/css-tree").Identifier} Identifier */
-/** @typedef {import("@eslint/css-tree").FunctionNodePlain} FunctionNodePlain */
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @import { Identifier, FunctionNodePlain } from "@eslint/css-tree"
+ * @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineType" | "notBaselineMediaCondition" | "notBaselineSelector"} UseBaselineMessageIds
+ * @typedef {[{
+ *     available?: "widely" | "newly" | number
+ * }]} UseBaselineOptions
+ * @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition
+ */
 
 //-----------------------------------------------------------------------------
 // Helpers
@@ -396,9 +402,10 @@ class BaselineAvailability {
 // Rule Definition
 //-----------------------------------------------------------------------------
 
+/** @type {UseBaselineRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Enforce the use of baseline features",
@@ -488,7 +495,7 @@ export default {
 					data: {
 						property,
 						value: child.name,
-						availability: baselineAvailability.availability,
+						availability: String(baselineAvailability.availability),
 					},
 				});
 			}
@@ -496,7 +503,7 @@ export default {
 
 		/**
 		 * Checks a property value function to see if it's a baseline feature.
-		 * @param {import("@eslint/css-tree").FunctionNodePlain} child The node to check.
+		 * @param {FunctionNodePlain} child The node to check.
 		 * @returns {void}
 		 **/
 		function checkPropertyValueFunction(child) {
@@ -513,7 +520,7 @@ export default {
 					messageId: "notBaselineType",
 					data: {
 						type: child.name,
-						availability: baselineAvailability.availability,
+						availability: String(baselineAvailability.availability),
 					},
 				});
 			}
@@ -603,7 +610,9 @@ export default {
 							messageId: "notBaselineProperty",
 							data: {
 								property,
-								availability: baselineAvailability.availability,
+								availability: String(
+									baselineAvailability.availability,
+								),
 							},
 						});
 
@@ -699,7 +708,9 @@ export default {
 							messageId: "notBaselineMediaCondition",
 							data: {
 								condition: child.name,
-								availability: baselineAvailability.availability,
+								availability: String(
+									baselineAvailability.availability,
+								),
 							},
 						});
 					}
@@ -730,7 +741,9 @@ export default {
 						messageId: "notBaselineAtRule",
 						data: {
 							atRule: node.name,
-							availability: baselineAvailability.availability,
+							availability: String(
+								baselineAvailability.availability,
+							),
 						},
 					});
 				}
@@ -775,7 +788,9 @@ export default {
 						messageId: "notBaselineSelector",
 						data: {
 							selector,
-							availability: baselineAvailability.availability,
+							availability: String(
+								baselineAvailability.availability,
+							),
 						},
 					});
 				}
@@ -794,7 +809,7 @@ export default {
 					messageId: "notBaselineSelector",
 					data: {
 						selector,
-						availability: baselineAvailability.availability,
+						availability: String(baselineAvailability.availability),
 					},
 				});
 			},
diff --git a/src/rules/use-layers.js b/src/rules/use-layers.js
index 19b94f10..636f2500 100644
--- a/src/rules/use-layers.js
+++ b/src/rules/use-layers.js
@@ -3,13 +3,29 @@
  * @author Nicholas C. Zakas
  */
 
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch"} UseLayersMessageIds
+ * @typedef {[{
+ *     allowUnnamedLayers?: boolean,
+ *     requireImportLayers?: boolean,
+ *     layerNamePattern?: string
+ * }]} UseLayersOptions
+ * @typedef {CSSRuleDefinition<{ RuleOptions: UseLayersOptions, MessageIds: UseLayersMessageIds }>} UseLayersRuleDefinition
+ */
+
 //-----------------------------------------------------------------------------
 // Rule Definition
 //-----------------------------------------------------------------------------
 
+/** @type {UseLayersRuleDefinition} */
 export default {
 	meta: {
-		type: /** @type {const} */ ("problem"),
+		type: "problem",
 
 		docs: {
 			description: "Require use of layers",
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 00000000..a3eeb11e
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,73 @@
+/**
+ * @fileoverview Additional types for this package.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import type { RuleVisitor, RuleDefinition } from "@eslint/core";
+
+import type { CssNodePlain, CssNodeNames } from "@eslint/css-tree";
+
+import type { CSSLanguageOptions, CSSSourceCode } from "./index.js";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/** Adds matching `:exit` selectors for all properties of a `RuleVisitor`. */
+type WithExit<RuleVisitorType extends RuleVisitor> = {
+	[Key in keyof RuleVisitorType as
+		| Key
+		| `${Key & string}:exit`]: RuleVisitorType[Key];
+};
+
+//------------------------------------------------------------------------------
+// Types
+//------------------------------------------------------------------------------
+
+/**
+ * A CSS syntax element, including nodes and comments.
+ */
+export type CSSSyntaxElement = CssNodePlain | Comment;
+
+type CSSNodeVisitor = {
+	[Type in CssNodeNames]: (
+		node: Extract<CssNodePlain, { type: Type }>,
+	) => void;
+};
+
+/**
+ * A visitor for CSS nodes.
+ */
+export interface CSSRuleVisitor
+	extends RuleVisitor,
+		Partial<WithExit<CSSNodeVisitor>> {}
+
+export type CSSRuleDefinitionTypeOptions = {
+	RuleOptions: unknown[];
+	MessageIds: string;
+	ExtRuleDocs: Record<string, unknown>;
+};
+
+/**
+ * A rule definition for CSS.
+ */
+export type CSSRuleDefinition<
+	Options extends Partial<CSSRuleDefinitionTypeOptions> = {},
+> = RuleDefinition<
+	// Language specific type options (non-configurable)
+	{
+		LangOptions: CSSLanguageOptions;
+		Code: CSSSourceCode;
+		Visitor: CSSRuleVisitor;
+		Node: CssNodePlain;
+	} & Required<
+		// Rule specific type options (custom)
+		Options &
+			// Rule specific type options (defaults)
+			Omit<CSSRuleDefinitionTypeOptions, keyof Options>
+	>
+>;
diff --git a/src/util.js b/src/util.js
index 994d97b3..7e97e61d 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,9 @@
 // Type Definitions
 //-----------------------------------------------------------------------------
 
-/** @typedef {import("@eslint/css-tree").SyntaxMatchError} SyntaxMatchError */
+/**
+ * @import { SyntaxMatchError } from "@eslint/css-tree"
+ */
 
 //-----------------------------------------------------------------------------
 // Helpers
diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js
index ce8b16d1..21dafff8 100644
--- a/tools/dedupe-types.js
+++ b/tools/dedupe-types.js
@@ -4,7 +4,7 @@
  * it encounters a duplicate typedef.
  *
  * Usage:
- *  node scripts/strip-typedefs.js filename1.js filename2.js ...
+ *  node tools/dedupe-types.js filename1.js filename2.js ...
  *
  * @author Nicholas C. Zakas
  */
@@ -19,25 +19,69 @@ import fs from "node:fs";
 // Main
 //-----------------------------------------------------------------------------
 
+const importRegExp =
+	/^\s*\*\s*@import\s*\{\s*(?<ids>[^,}]+(?:\s*,\s*[^,}]+)*)\s*\}\s*from\s*"(?<source>[^"]+)"/u;
+
 // read files from the command line
 const files = process.argv.slice(2);
 
 files.forEach(filePath => {
 	const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu);
-	const typedefs = new Set();
+	const imports = new Map();
 
+	// find all imports and remove them
 	const remainingLines = lines.filter(line => {
-		if (!line.startsWith("/** @typedef {import")) {
+		if (!line.startsWith(" * @import")) {
 			return true;
 		}
 
-		if (typedefs.has(line)) {
-			return false;
+		const match = importRegExp.exec(line);
+
+		if (!match) {
+			throw Error("@import statement must be on a single line.");
+		}
+
+		const source = match.groups.source;
+		const ids = match.groups.ids.split(/,/gu).map(id => id.trim());
+
+		// save the import data
+
+		if (!imports.has(source)) {
+			imports.set(source, new Set());
 		}
 
-		typedefs.add(line);
-		return true;
+		const existingIds = imports.get(source);
+		ids.forEach(id => existingIds.add(id));
+
+		return false;
 	});
 
-	fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8");
+	// create a new import statement for each unique import
+	const jsdocBlock = ["/**"];
+
+	imports.forEach((ids, source) => {
+		// if it's a local file, we don't need it
+		if (source.startsWith("./")) {
+			return;
+		}
+
+		const idList = Array.from(ids).join(", ");
+		jsdocBlock.push(` * @import { ${idList} } from "${source}"`);
+	});
+
+	// add the new import statements to the top of the file
+	jsdocBlock.push(" */");
+	remainingLines.unshift(...jsdocBlock);
+	remainingLines.unshift(""); // add a blank line before the block
+
+	// replace references to ../types.ts with ./types.ts
+	let text = remainingLines
+		.join("\n")
+		.replace(/\.\.\/types\.js/gu, "./types.js");
+
+	if (filePath.endsWith(".cjs")) {
+		text = text.replace("types.js", "types.cjs");
+	}
+
+	fs.writeFileSync(filePath, text, "utf8");
 });
diff --git a/tools/update-cts.js b/tools/update-cts.js
new file mode 100644
index 00000000..b394d091
--- /dev/null
+++ b/tools/update-cts.js
@@ -0,0 +1,29 @@
+/**
+ * @fileoverview Replaces all .js references with .cjs references.
+ *
+ * Usage:
+ *  node tools/update-cts.js filename1.js filename2.js ...
+ *
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import fs from "node:fs";
+
+//-----------------------------------------------------------------------------
+// Main
+//-----------------------------------------------------------------------------
+
+// read files from the command line
+const files = process.argv.slice(2);
+
+files.forEach(filePath => {
+	let text = fs.readFileSync(filePath, "utf8");
+
+	text = text.replace(/(\.\/(index|types))\.js/gu, "$1.cjs");
+
+	fs.writeFileSync(filePath, text, "utf8");
+});


Reviews:
user: login: fasttime
body: The CI build is now failing on the "Verify JSR Publish" job. It looks like `"dist/esm/types.ts"` is missing in the list of included files in `jsr.json`:

https://github.com/eslint/css/blob/2519f4e2d06c403977fe1292dd76761cab8986c0/jsr.json#L9-L17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T04:39:59Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 17 out of 19 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (2)</summary>

* **jsr.json**: Language not supported
* **package.json**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/rules/use-baseline.js:495**
* The repeated inline conversion of baselineAvailability.availability using String() suggests an underlying type mismatch. Consider revising the type of baselineAvailability.availability upstream so that it already has the expected string type.
```
availability: baselineAvailability.availability,
```
**rollup.config.js:25**
* [nitpick] Please verify that using the ".cts" extension for the CommonJS type definitions aligns with project conventions and tooling expectations.
```
rename: "types.cts",
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:40:11Z
user: login: fasttime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T13:19:58Z
user: login: fasttime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T13:25:15Z
user: login: fasttime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T13:38:06Z
user: login: fasttime
body: LGTM, thanks! I would like another review.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T04:57:42Z
user: login: mdjermanovic
body: LGTM, thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-13T09:50:30Z

Review comments:
path: tests/types/cjs-import.test.cts
user: login: fasttime
body: It looks like this file was changed by mistake.
created_at: 2025-04-10T12:58:08Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,8 @@
 import css from "@eslint/css";
-import { ESLint } from "eslint";
+import type { CSSLanguageOptions } from "@eslint/css";
+// import { ESLint } from "eslint";
 
-css satisfies ESLint.Plugin;
path: tests/types/types.test.ts
user: login: fasttime
body: If we update `"eslint"` in package.json to `^9.23.0`, this test will pass again.
created_at: 2025-04-10T13:08:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,73 @@
+/**
+ * @fileoverview Additional types for this package.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import type { RuleVisitor, RuleDefinition } from "@eslint/core";
+
+import type { CssNodePlain, CssNodeNames } from "@eslint/css-tree";
+
+import type { CSSLanguageOptions, CSSSourceCode } from "./index.js";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/** Adds matching `:exit` selectors for all properties of a `RuleVisitor`. */
+type WithExit<RuleVisitorType extends RuleVisitor> = {
+	[Key in keyof RuleVisitorType as
+		| Key
+		| `${Key & string}:exit`]: RuleVisitorType[Key];
+};
+
+//------------------------------------------------------------------------------
+// Types
+//------------------------------------------------------------------------------
+
+/**
+ * A JSON syntax element, including nodes and tokens.
path: src/types.ts
user: login: fasttime
body: ```suggestion
 * A CSS syntax element, including nodes and comments.
```

Maybe like this?
created_at: 2025-04-10T13:25:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,73 @@
+/**
+ * @fileoverview Additional types for this package.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import type { RuleVisitor, RuleDefinition } from "@eslint/core";
+
+import type { CssNodePlain, CssNodeNames } from "@eslint/css-tree";
+
+import type { CSSLanguageOptions, CSSSourceCode } from "./index.js";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/** Adds matching `:exit` selectors for all properties of a `RuleVisitor`. */
+type WithExit<RuleVisitorType extends RuleVisitor> = {
+	[Key in keyof RuleVisitorType as
+		| Key
+		| `${Key & string}:exit`]: RuleVisitorType[Key];
+};
+
+//------------------------------------------------------------------------------
+// Types
+//------------------------------------------------------------------------------
+
+/**
+ * A JSON syntax element, including nodes and tokens.
+ */
+export type CSSSyntaxElement = CssNodePlain | Comment;
+
+type CSSNodeVisitor = {
+	[Type in CssNodeNames]: (
+		node: Extract<CssNodePlain, { type: Type }>,
+	) => void;
+};
+
+/**
+ * A visitor for CSS nodes.
+ */
+export interface CSSRuleVisitor
+	extends RuleVisitor,
+		Partial<WithExit<CSSNodeVisitor>> {}
+
+export type CSSRuleDefinitionTypeOptions = {
+	RuleOptions: unknown[];
+	MessageIds: string;
+	ExtRuleDocs: Record<string, unknown>;
+};
+
+/**
+ * A rule definition for CSS.
+ */
+export type CSSRuleDefinition<
+	Options extends Partial<CSSRuleDefinitionTypeOptions> = {},
+> = RuleDefinition<
+	// Language specific type options (non-configurable)
+	{
+		LangOptions: {};
path: src/types.ts
user: login: fasttime
body: ```suggestion
		LangOptions: CSSLanguageOptions;
```
created_at: 2025-04-10T13:38:05Z
author_association: MEMBER

PR comments:
user: login: fasttime
created_at: 2025-04-03T09:55:16Z
body: > Right now, Rollup is removing the type definitions for `no-invalid-at-rules` and `no-invalid-properties`. I can't quite figure out why that is. I could use some help with this.

Probably in both cases Rollup thinks that the JSDoc comments under "Type Definitions" are part of the next statement, i.e. `import { isSyntaxMatchError } from "../util.js";`, and since those statements are removed from the bundle, so the comments are also removed. One possible workaround is moving the "Type Definitions" section just above "RuleDefinition":

```js
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 * @import { AtrulePlain } from "@eslint/css-tree"
 * @import { CSSRuleDefinition } from "../types.js"
 * @typedef {"unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude"} NoInvalidAtRulesMessageIds
 * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulesMessageIds }>} NoInvalidAtRulesRuleDefinition
 */

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
```

author_association: MEMBER
user: login: nzakas
created_at: 2025-04-03T20:57:47Z
body: Ah, that is brilliant! Thanks!
author_association: MEMBER
user: login: nzakas
created_at: 2025-04-03T21:19:23Z
body: Tests are failing due to this: https://github.com/eslint/csstree/issues/27
author_association: MEMBER
user: login: nzakas
created_at: 2025-04-09T15:59:20Z
body: Thanks!
author_association: MEMBER