diff --git a/README.md b/README.md index af29a52..22dafd8 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,18 @@ export default defineConfig([ -| **Rule Name** | **Description** | **Recommended** | -| :----------------------------------------------------------------------- | :------------------------------------- | :-------------: | -| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes | -| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes | -| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes | -| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes | -| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes | -| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no | -| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no | -| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes | -| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no | +| **Rule Name** | **Description** | **Recommended** | +| :--------------------------------------------------------------------------- | :------------------------------------------ | :-------------: | +| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes | +| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes | +| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes | +| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes | +| [`no-invalid-import-placement`](./docs/rules/no-invalid-import-placement.md) | Disallow invalid placement of @import rules | yes | +| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes | +| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no | +| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no | +| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes | +| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no | diff --git a/docs/rules/no-invalid-import-placement.md b/docs/rules/no-invalid-import-placement.md new file mode 100644 index 0000000..3d1dca0 --- /dev/null +++ b/docs/rules/no-invalid-import-placement.md @@ -0,0 +1,85 @@ +# no-invalid-import-placement + +Disallow invalid placement of `@import` rules. + +## Background + +The `@import` rule must be placed at the beginning of a stylesheet, before any other at-rules (except `@charset` and `@layer`) and style rules. If placed elsewhere, browsers will ignore the `@import` rule, resulting in the imported styles being missing from the page. + +## Rule Details + +This rule warns when it finds an `@import` rule that appears after any other at-rules or style rules in a stylesheet (ignoring `@charset` and `@layer` rules). + +Examples of **incorrect** code: + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* @import after style rules */ +a { + color: red; +} +@import "foo.css"; +``` + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* @import after at-rules */ +@media screen { +} +@import "bar.css"; +``` + +Examples of **correct** code: + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* @import at the beginning */ +@import "foo.css"; +a { + color: red; +} +``` + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* @import after @charset */ +@charset "utf-8"; +@import "bar.css"; +a { + color: red; +} +``` + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* @import after @layer */ +@layer base; +@import "baz.css"; +a { + color: red; +} +``` + +```css +/* eslint css/no-invalid-import-placement: "error" */ + +/* Multiple @import rules together */ +@import "foo.css"; +@import "bar.css"; +a { + color: red; +} +``` + +## When Not to Use It + +You can disable this rule if your stylesheets don't use `@import` or if you're not concerned about the impact of incorrect placement on style loading. + +## Prior Art + +- [`no-invalid-position-at-import-rule`](https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule/) diff --git a/src/index.js b/src/index.js index f6a30b1..45ebad6 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import noDuplicateImports from "./rules/no-duplicate-imports.js"; import noImportant from "./rules/no-important.js"; import noInvalidProperties from "./rules/no-invalid-properties.js"; import noInvalidAtRules from "./rules/no-invalid-at-rules.js"; +import noInvalidImportPlacement from "./rules/no-invalid-import-placement.js"; import preferLogicalProperties from "./rules/prefer-logical-properties.js"; import relativeFontUnits from "./rules/relative-font-units.js"; import useLayers from "./rules/use-layers.js"; @@ -37,6 +38,7 @@ const plugin = { "no-important": noImportant, "no-invalid-at-rules": noInvalidAtRules, "no-invalid-properties": noInvalidProperties, + "no-invalid-import-placement": noInvalidImportPlacement, "prefer-logical-properties": preferLogicalProperties, "relative-font-units": relativeFontUnits, "use-layers": useLayers, @@ -51,6 +53,7 @@ const plugin = { "css/no-important": "error", "css/no-invalid-at-rules": "error", "css/no-invalid-properties": "error", + "css/no-invalid-import-placement": "error", "css/use-baseline": "warn", }), }, diff --git a/src/rules/no-invalid-import-placement.js b/src/rules/no-invalid-import-placement.js new file mode 100644 index 0000000..f534030 --- /dev/null +++ b/src/rules/no-invalid-import-placement.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to enforce correct placement of `@import` rules in CSS. + * @author thecalamiity + */ + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** + * @import { CSSRuleDefinition } from "../types.js" + * @typedef {"invalidImportPlacement"} NoInvalidImportPlacementMessageIds + * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidImportPlacementMessageIds }>} NoInvalidImportPlacementRuleDefinition + */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const IGNORED_AT_RULES = new Set(["charset", "layer"]); + +//----------------------------------------------------------------------------- +// Rule Definition +//----------------------------------------------------------------------------- + +/** @type {NoInvalidImportPlacementRuleDefinition} */ +export default { + meta: { + type: "problem", + + docs: { + description: "Disallow invalid placement of @import rules", + recommended: true, + url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-import-placement.md", + }, + + messages: { + invalidImportPlacement: + "@import must be placed before all other rules, except @charset and @layer.", + }, + }, + + create(context) { + let hasSeenNonImportRule = false; + + return { + Atrule(node) { + const name = node.name.toLowerCase(); + + if (IGNORED_AT_RULES.has(name)) { + return; + } + + if (name === "import") { + if (hasSeenNonImportRule) { + context.report({ + node, + messageId: "invalidImportPlacement", + }); + } + } else { + hasSeenNonImportRule = true; + } + }, + + Rule() { + hasSeenNonImportRule = true; + }, + }; + }, +}; diff --git a/tests/rules/no-invalid-import-placement.test.js b/tests/rules/no-invalid-import-placement.test.js new file mode 100644 index 0000000..b5224bf --- /dev/null +++ b/tests/rules/no-invalid-import-placement.test.js @@ -0,0 +1,163 @@ +/** + * @fileoverview Tests for no-invalid-import-placement rule. + * @author thecalamiity + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import rule from "../../src/rules/no-invalid-import-placement.js"; +import css from "../../src/index.js"; +import { RuleTester } from "eslint"; +import dedent from "dedent"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + plugins: { + css, + }, + language: "css/css", +}); + +ruleTester.run("no-invalid-import-placement", rule, { + valid: [ + "@import 'foo.css';", + "@import url('foo.css');", + "@import 'foo.css' screen;", + "@import url('foo.css') supports(display: grid) screen and (max-width: 400px);", + "@import 'foo.css' layer;", + "@import 'foo.css' layer(base);", + dedent` + /* comment */ + @import 'foo.css';`, + dedent` + @charset 'utf-8'; + @import 'foo.css';`, + dedent` + @layer base; + @import 'foo.css';`, + dedent` + @import 'foo.css'; + @import 'bar.css';`, + dedent` + @CHARSET 'utf-8'; + @LAYER base; + @imPORT 'foo.css'; + @import 'bar.css';`, + dedent` + @import 'foo.css'; + a { color: red; }`, + dedent` + @charset 'UTF-8'; + @layer base; + @import 'foo.css'; + a { color: red; }`, + ], + invalid: [ + { + code: dedent` + a { color: red; } + @import 'foo.css';`, + errors: [ + { + messageId: "invalidImportPlacement", + line: 2, + column: 1, + endLine: 2, + endColumn: 19, + }, + ], + }, + { + code: dedent` + @media screen { } + @import url('foo.css');`, + errors: [ + { + messageId: "invalidImportPlacement", + line: 2, + column: 1, + endLine: 2, + endColumn: 24, + }, + ], + }, + { + code: dedent` + @media print { } + @imPort URl('foo.css');`, + errors: [ + { + messageId: "invalidImportPlacement", + line: 2, + column: 1, + endLine: 2, + endColumn: 24, + }, + ], + }, + { + code: dedent` + @import 'foo.css'; + a {} + @import 'bar.css';`, + errors: [ + { + messageId: "invalidImportPlacement", + line: 3, + column: 1, + endLine: 3, + endColumn: 19, + }, + ], + }, + { + code: dedent` + a {} + @import 'foo.css'; + @import 'bar.css';`, + errors: [ + { + messageId: "invalidImportPlacement", + line: 2, + column: 1, + endLine: 2, + endColumn: 19, + }, + { + messageId: "invalidImportPlacement", + line: 3, + column: 1, + endLine: 3, + endColumn: 19, + }, + ], + }, + { + code: dedent` + @custom-rule {} + @import + 'foo.css';`, + languageOptions: { + customSyntax: { + atrules: { + "custom-rule": {}, + }, + }, + }, + errors: [ + { + messageId: "invalidImportPlacement", + line: 2, + column: 1, + endLine: 3, + endColumn: 11, + }, + ], + }, + ], +});