@maz-ui/stylelint-config 5.0.0-canary.ef64a02.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Reusable [Stylelint](https://stylelint.io/) configuration for Vue/Nuxt/JS/TS pro
5
5
  ## Features
6
6
 
7
7
  - 🚀 **Stylelint Standard base** — opinionated, maintained by the Stylelint team
8
- - 🎨 **Tailwind v4** — at-rule whitelist, `import-notation: string` enforced
8
+ - 🎨 **Tailwind v4** — at-rule whitelist, validation rules via `stylelint-plugin-tailwindcss` (3 strictness levels)
9
9
  - 🌍 **RTL-friendly** — logical properties via `stylelint-use-logical-spec`
10
10
  - 🔠 **Property ordering** — Recess-based by default, alphabetical or off
11
11
  - 💚 **Vue/HTML/SCSS** — auto-detected from your `package.json`
@@ -34,7 +34,7 @@ Vue, Tailwind and SCSS are auto-detected from your `package.json`.
34
34
  defineConfig({
35
35
  vue: true, // auto-detected from `vue`/`nuxt` deps
36
36
  html: false,
37
- tailwind: true, // auto-detected from `tailwindcss` dep
37
+ tailwind: true, // auto-detected from `tailwindcss` dep — see "Tailwind" below
38
38
  scss: false, // auto-detected from `sass`/`sass-embedded`/`node-sass`
39
39
  logical: true, // recommend logical CSS properties (RTL-friendly)
40
40
  order: 'recess', // 'recess' | 'alphabetical' | false
@@ -46,9 +46,81 @@ defineConfig({
46
46
  overrides: [
47
47
  { files: ['**/*.legacy.css'], rules: { 'color-no-invalid-hex': null } },
48
48
  ],
49
+ extends: ['stylelint-config-clean-order'], // layer extra shareable configs
50
+ logLevel: 'default', // 'silent' | 'default' | 'debug' | 'verbose' — see "Logging" below
49
51
  })
50
52
  ```
51
53
 
54
+ ### Logging
55
+
56
+ The preset can log what it resolves and applies, which is handy when you don't understand why a rule is on or off. Pass a `logLevel` (powered by `@maz-ui/node`'s consola-based logger):
57
+
58
+ | Value | What you see |
59
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `'silent'` | Nothing. |
61
+ | `'default'` _(default)_ | A titled box summarizing the resolved support (vue, tailwind, scss, html, logical, order) and whether each value was auto-detected or explicit. |
62
+ | `'debug'` | Above + each plugin / extends / overrides addition (`Loaded stylelint-use-logical-spec`, `Tailwind: loaded N plugins for "minimal" policy`, …). |
63
+ | `'verbose'` | Above + final shape (full `extends` list, plugin/rule/override/ignore counts). |
64
+
65
+ ```js
66
+ defineConfig({ logLevel: 'debug' })
67
+ ```
68
+
69
+ ### Tailwind support
70
+
71
+ | Value | Behavior |
72
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `true` _(or auto-detected)_ | Same as `'minimal'`. |
74
+ | `'minimal'` _(default when on)_ | At-rule whitelist + `import-notation: 'string'` + `at-rule-no-deprecated`. **No plugin loaded.** |
75
+ | `'recommended'` | `'minimal'` + `stylelint-plugin-tailwindcss`'s recommended preset (`no-invalid-apply`, `no-invalid-theme-function`, `no-atomic-class`, `no-apply`, `no-arbitrary-value`). |
76
+ | `'strict'` | `'recommended'` + `no-theme-function`, `no-screen-directive`, `no-tailwind-directive`, `no-import-directive`, `no-css-layer`. |
77
+ | `false` | Tailwind support disabled entirely. |
78
+
79
+ > [!IMPORTANT]
80
+ > The plugin rules in `'recommended'` and `'strict'` auto-resolve your project's Tailwind runtime from each linted file's path. Projects with a v4 prefix (`maz:`, `tw:`, …), a non-standard entry CSS, or a monorepo layout where Tailwind isn't where the plugin expects produce false positives. **`'minimal'` is the safe default.** Opt into `'recommended'` only if your project sits in the plugin's happy path.
81
+
82
+ Plugin rules come from [`stylelint-plugin-tailwindcss`](https://github.com/sonofmagic/dev-configs/tree/main/packages/stylelint-plugin-tailwindcss) and support both Tailwind v3 and v4.
83
+
84
+ ### Property ordering
85
+
86
+ | Value | Behavior |
87
+ | ---------------- | -------------------------------------------------------------------------- |
88
+ | `'recess'` | Extends `stylelint-config-recess-order` (default). |
89
+ | `'alphabetical'` | Loads `stylelint-order` and enables `order/properties-alphabetical-order`. |
90
+ | `false` | No ordering rules. |
91
+
92
+ To swap recess for another shareable order config (e.g. `stylelint-config-clean-order`), disable the built-in strategy and extend yours:
93
+
94
+ ```js
95
+ defineConfig({
96
+ order: false,
97
+ extends: ['stylelint-config-clean-order'],
98
+ })
99
+ ```
100
+
101
+ ### Layering shareable configs
102
+
103
+ The `extends` option appends entries **after** the built-ins, so user configs win Stylelint's cascade. Common cases:
104
+
105
+ ```js
106
+ defineConfig({
107
+ extends: [
108
+ 'stylelint-config-clean-order', // alternative property order
109
+ '@stylistic/stylelint-config', // restore stylistic rules retired from core
110
+ ],
111
+ })
112
+ ```
113
+
114
+ ### Auto-detection caveat (monorepos)
115
+
116
+ Auto-detection reads the `package.json` in `process.cwd()` only. In a monorepo where `tailwindcss`, `vue` or `sass` are hoisted to the root and missing from a sub-package's `package.json`, detection fails when Stylelint runs from that sub-package.
117
+
118
+ In monorepos, **set the relevant flags explicitly**:
119
+
120
+ ```js
121
+ defineConfig({ vue: true, tailwind: true, scss: false })
122
+ ```
123
+
52
124
  ## Documentation
53
125
 
54
126
  See the full documentation at [maz-ui.com/ecosystem/stylelint-config](https://maz-ui.com/ecosystem/stylelint-config).
package/dist/index.d.mts CHANGED
@@ -1,15 +1,25 @@
1
- import { Config } from 'stylelint';
1
+ import { Config as Config$1 } from 'stylelint';
2
2
 
3
- type StylelintConfig = Config;
4
- type StylelintRules = Config['rules'];
5
- type StylelintOverride = NonNullable<Config['overrides']>[number];
3
+ /**
4
+ * Mirror of `@maz-ui/node`'s LogLevel — kept inline here to avoid making this
5
+ * preset depend on `@maz-ui/node`, which would create a build cycle (the
6
+ * `eslint.config.ts` of `@maz-ui/node` imports `@maz-ui/eslint-config`, which
7
+ * itself depends on `@maz-ui/node` for the same logger). Direct usage of
8
+ * `consola` keeps the dependency graph acyclic.
9
+ */
10
+ type LogLevel = 'silent' | 'error' | 'warning' | 'normal' | 'default' | 'debug' | 'trace' | 'verbose';
11
+
12
+ type StylelintConfig = Config$1;
13
+ type StylelintRules = Config$1['rules'];
14
+ type StylelintOverride = NonNullable<Config$1['overrides']>[number];
6
15
  /**
7
16
  * Property order strategy.
8
17
  *
9
18
  * - `'recess'`: Recess-based ordering (`stylelint-config-recess-order`).
10
19
  * Pragmatic, widely adopted, groups related properties together.
11
- * - `'alphabetical'`: Strict A→Z ordering. Easy mental model, slightly worse
12
- * diffs than recess.
20
+ * - `'alphabetical'`: Strict A→Z ordering via `stylelint-order`'s
21
+ * `order/properties-alphabetical-order` rule. Easy mental model, slightly
22
+ * worse diffs than recess.
13
23
  * - `false`: Disable ordering rules entirely.
14
24
  */
15
25
  type StylelintOrderStrategy = 'recess' | 'alphabetical' | false;
@@ -32,13 +42,23 @@ interface MazStylelintOptions {
32
42
  /**
33
43
  * Enable Tailwind CSS support — whitelists the v4 at-rules
34
44
  * (`@apply`, `@reference`, `@variant`, `@theme`, `@layer`, `@utility`,
35
- * `@source`, `@custom-variant`, `@tailwind`, `@screen`, `@starting-style`)
36
- * and relaxes patterns that conflict with Tailwind's prefixed classes.
45
+ * `@source`, `@custom-variant`, `@tailwind`, `@screen`, `@starting-style`),
46
+ * relaxes patterns that conflict with Tailwind's prefixed classes, and
47
+ * loads `stylelint-plugin-tailwindcss` with the requested policy.
37
48
  *
38
- * Auto-detected from `tailwindcss` in the consuming `package.json` unless
39
- * explicitly set.
49
+ * Accepts:
50
+ * - `true` or auto-detected (`tailwindcss` in `package.json`): equivalent to
51
+ * `'minimal'`.
52
+ * - `'minimal'`: validation-only — flags typo'd utilities in `@apply`
53
+ * (`no-invalid-apply`) and invalid `theme(...)` paths
54
+ * (`no-invalid-theme-function`). No opinion on authoring style.
55
+ * - `'recommended'`: the plugin's recommended preset — also bans atomic
56
+ * classes in authored CSS, `@apply`, and arbitrary values.
57
+ * - `'strict'`: recommended plus architecture-level checks (no `theme()`,
58
+ * `@screen`, `@tailwind`, `@import`, `@layer`).
59
+ * - `false`: disable Tailwind support entirely.
40
60
  */
41
- tailwind?: boolean;
61
+ tailwind?: boolean | 'minimal' | 'recommended' | 'strict';
42
62
  /**
43
63
  * Enable SCSS support — extends `stylelint-config-recommended-scss` and
44
64
  * registers `postcss-scss` for `.scss` files.
@@ -104,7 +124,36 @@ interface MazStylelintOptions {
104
124
  /**
105
125
  * Additional plugins merged on top of the resolved configuration.
106
126
  */
107
- plugins?: NonNullable<Config['plugins']>;
127
+ plugins?: NonNullable<Config$1['plugins']>;
128
+ /**
129
+ * Additional shareable configs to extend, appended after the built-in
130
+ * extends so user configs win the cascade (e.g. `stylelint-config-clean-order`
131
+ * to replace recess ordering, or `stylelint-config-tailwindcss` for the
132
+ * official Tailwind plugin rules).
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * defineConfig({
137
+ * order: false,
138
+ * extends: ['stylelint-config-clean-order'],
139
+ * })
140
+ * ```
141
+ */
142
+ extends?: string[];
143
+ /**
144
+ * Verbosity of the logs emitted while resolving the config. Defaults to
145
+ * `'default'` so the resolved configuration box is shown when the preset
146
+ * loads. Set to `'silent'` to hide it, or `'debug'` / `'verbose'` to dig
147
+ * deeper.
148
+ *
149
+ * - `'silent'`: nothing is printed.
150
+ * - `'default'` *(default)*: a titled box summarizing the resolved support
151
+ * (auto-detection results, chosen Tailwind policy, …).
152
+ * - `'debug'`: also logs each plugin / extends / overrides addition.
153
+ * - `'verbose'`: also logs the final shape (extends list, rule count,
154
+ * ignore globs).
155
+ */
156
+ logLevel?: LogLevel;
108
157
  }
109
158
 
110
159
  /**
@@ -121,7 +170,7 @@ declare const baseRules: StylelintRules;
121
170
  * Glob patterns Stylelint should ignore by default. Mirrors the eslint-config
122
171
  * defaults plus a few CSS-specific build outputs.
123
172
  */
124
- declare const GLOBAL_IGNORES: string[];
173
+ declare const GLOBAL_IGNORES: Config;
125
174
 
126
175
  /**
127
176
  * Flag every physical property/value that has a logical equivalent.
@@ -139,7 +188,7 @@ declare const logicalRules: StylelintRules;
139
188
  * so projects that use SCSS *and* Tailwind don't get false positives from
140
189
  * `scss/at-rule-no-unknown`.
141
190
  */
142
- declare function scssRules(includeTailwind: boolean): StylelintRules;
191
+ declare function scssRules(includeTailwind: boolean | string): StylelintRules;
143
192
 
144
193
  /**
145
194
  * Tailwind v4 at-rules. New ones can be added without bumping a major
@@ -148,7 +197,7 @@ declare function scssRules(includeTailwind: boolean): StylelintRules;
148
197
  *
149
198
  * Source: https://tailwindcss.com/docs (v4 directives)
150
199
  */
151
- declare const TAILWIND_AT_RULES: string[];
200
+ declare const TAILWIND_AT_RULES: readonly ["theme", "utility", "source", "variant", "custom-variant", "reference", "apply", "layer", "screen", "starting-style", "tailwind", "config", "plugin", "responsive"];
152
201
  /**
153
202
  * Rules to whitelist Tailwind's at-rules and relax patterns that conflict
154
203
  * with Tailwind-prefixed classes (`maz:flex`, `dark:bg-…`).
@@ -167,6 +216,40 @@ declare const tailwindRules: StylelintRules;
167
216
  * recommended config.
168
217
  */
169
218
  declare const tailwindAtRuleNoUnknown: StylelintRules;
219
+ /**
220
+ * No plugin rules. `'minimal'` only ships the at-rule whitelist + a couple
221
+ * of CSS-level rules (`import-notation`, `at-rule-no-deprecated`) — no
222
+ * runtime Tailwind validation.
223
+ *
224
+ * Why empty: the plugin's validation rules (`no-invalid-apply`,
225
+ * `no-invalid-theme-function`) auto-resolve the project's Tailwind runtime
226
+ * from the linted file's path. Projects with a v4 prefix (`maz:`),
227
+ * non-standard entry CSS, or workspace layouts where Tailwind isn't where
228
+ * the plugin expects produce floods of false positives. Loading these
229
+ * rules without a way to scope them is hostile by default.
230
+ *
231
+ * Opt in to `'recommended'` if your project lives in the plugin's happy
232
+ * path.
233
+ */
234
+ declare const tailwindPluginMinimal: StylelintRules;
235
+ /**
236
+ * Plugin's recommended preset — `no-invalid-apply` and
237
+ * `no-invalid-theme-function` (validation), plus policies that discourage
238
+ * utility-first patterns inside authored CSS (`.flex { … }`, `@apply`,
239
+ * arbitrary values).
240
+ *
241
+ * Opt-in only: the validation rules need to resolve your Tailwind runtime
242
+ * from the linted file's path; if your project has a custom prefix, an
243
+ * unusual entry CSS or sits in a monorepo, expect false positives.
244
+ */
245
+ declare const tailwindPluginRecommended: StylelintRules;
246
+ /**
247
+ * Plugin's strict preset — adds architecture-level checks on top of
248
+ * recommended (no `theme()`, no `@screen`, no `@tailwind`, no `@import`,
249
+ * no `@layer`). Suited to projects that strictly separate utility tokens
250
+ * from authored CSS.
251
+ */
252
+ declare const tailwindPluginStrict: StylelintRules;
170
253
 
171
254
  /**
172
255
  * Build a Stylelint config tailored for Vue/Nuxt/JS/TS/Tailwind projects.
@@ -192,5 +275,5 @@ declare const tailwindAtRuleNoUnknown: StylelintRules;
192
275
  */
193
276
  declare function defineConfig(options?: MazStylelintOptions): StylelintConfig;
194
277
 
195
- export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindRules };
278
+ export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindPluginMinimal, tailwindPluginRecommended, tailwindPluginStrict, tailwindRules };
196
279
  export type { MazStylelintOptions, StylelintConfig, StylelintOrderStrategy, StylelintOverride, StylelintRules };
package/dist/index.d.ts CHANGED
@@ -1,15 +1,25 @@
1
- import { Config } from 'stylelint';
1
+ import { Config as Config$1 } from 'stylelint';
2
2
 
3
- type StylelintConfig = Config;
4
- type StylelintRules = Config['rules'];
5
- type StylelintOverride = NonNullable<Config['overrides']>[number];
3
+ /**
4
+ * Mirror of `@maz-ui/node`'s LogLevel — kept inline here to avoid making this
5
+ * preset depend on `@maz-ui/node`, which would create a build cycle (the
6
+ * `eslint.config.ts` of `@maz-ui/node` imports `@maz-ui/eslint-config`, which
7
+ * itself depends on `@maz-ui/node` for the same logger). Direct usage of
8
+ * `consola` keeps the dependency graph acyclic.
9
+ */
10
+ type LogLevel = 'silent' | 'error' | 'warning' | 'normal' | 'default' | 'debug' | 'trace' | 'verbose';
11
+
12
+ type StylelintConfig = Config$1;
13
+ type StylelintRules = Config$1['rules'];
14
+ type StylelintOverride = NonNullable<Config$1['overrides']>[number];
6
15
  /**
7
16
  * Property order strategy.
8
17
  *
9
18
  * - `'recess'`: Recess-based ordering (`stylelint-config-recess-order`).
10
19
  * Pragmatic, widely adopted, groups related properties together.
11
- * - `'alphabetical'`: Strict A→Z ordering. Easy mental model, slightly worse
12
- * diffs than recess.
20
+ * - `'alphabetical'`: Strict A→Z ordering via `stylelint-order`'s
21
+ * `order/properties-alphabetical-order` rule. Easy mental model, slightly
22
+ * worse diffs than recess.
13
23
  * - `false`: Disable ordering rules entirely.
14
24
  */
15
25
  type StylelintOrderStrategy = 'recess' | 'alphabetical' | false;
@@ -32,13 +42,23 @@ interface MazStylelintOptions {
32
42
  /**
33
43
  * Enable Tailwind CSS support — whitelists the v4 at-rules
34
44
  * (`@apply`, `@reference`, `@variant`, `@theme`, `@layer`, `@utility`,
35
- * `@source`, `@custom-variant`, `@tailwind`, `@screen`, `@starting-style`)
36
- * and relaxes patterns that conflict with Tailwind's prefixed classes.
45
+ * `@source`, `@custom-variant`, `@tailwind`, `@screen`, `@starting-style`),
46
+ * relaxes patterns that conflict with Tailwind's prefixed classes, and
47
+ * loads `stylelint-plugin-tailwindcss` with the requested policy.
37
48
  *
38
- * Auto-detected from `tailwindcss` in the consuming `package.json` unless
39
- * explicitly set.
49
+ * Accepts:
50
+ * - `true` or auto-detected (`tailwindcss` in `package.json`): equivalent to
51
+ * `'minimal'`.
52
+ * - `'minimal'`: validation-only — flags typo'd utilities in `@apply`
53
+ * (`no-invalid-apply`) and invalid `theme(...)` paths
54
+ * (`no-invalid-theme-function`). No opinion on authoring style.
55
+ * - `'recommended'`: the plugin's recommended preset — also bans atomic
56
+ * classes in authored CSS, `@apply`, and arbitrary values.
57
+ * - `'strict'`: recommended plus architecture-level checks (no `theme()`,
58
+ * `@screen`, `@tailwind`, `@import`, `@layer`).
59
+ * - `false`: disable Tailwind support entirely.
40
60
  */
41
- tailwind?: boolean;
61
+ tailwind?: boolean | 'minimal' | 'recommended' | 'strict';
42
62
  /**
43
63
  * Enable SCSS support — extends `stylelint-config-recommended-scss` and
44
64
  * registers `postcss-scss` for `.scss` files.
@@ -104,7 +124,36 @@ interface MazStylelintOptions {
104
124
  /**
105
125
  * Additional plugins merged on top of the resolved configuration.
106
126
  */
107
- plugins?: NonNullable<Config['plugins']>;
127
+ plugins?: NonNullable<Config$1['plugins']>;
128
+ /**
129
+ * Additional shareable configs to extend, appended after the built-in
130
+ * extends so user configs win the cascade (e.g. `stylelint-config-clean-order`
131
+ * to replace recess ordering, or `stylelint-config-tailwindcss` for the
132
+ * official Tailwind plugin rules).
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * defineConfig({
137
+ * order: false,
138
+ * extends: ['stylelint-config-clean-order'],
139
+ * })
140
+ * ```
141
+ */
142
+ extends?: string[];
143
+ /**
144
+ * Verbosity of the logs emitted while resolving the config. Defaults to
145
+ * `'default'` so the resolved configuration box is shown when the preset
146
+ * loads. Set to `'silent'` to hide it, or `'debug'` / `'verbose'` to dig
147
+ * deeper.
148
+ *
149
+ * - `'silent'`: nothing is printed.
150
+ * - `'default'` *(default)*: a titled box summarizing the resolved support
151
+ * (auto-detection results, chosen Tailwind policy, …).
152
+ * - `'debug'`: also logs each plugin / extends / overrides addition.
153
+ * - `'verbose'`: also logs the final shape (extends list, rule count,
154
+ * ignore globs).
155
+ */
156
+ logLevel?: LogLevel;
108
157
  }
109
158
 
110
159
  /**
@@ -121,7 +170,7 @@ declare const baseRules: StylelintRules;
121
170
  * Glob patterns Stylelint should ignore by default. Mirrors the eslint-config
122
171
  * defaults plus a few CSS-specific build outputs.
123
172
  */
124
- declare const GLOBAL_IGNORES: string[];
173
+ declare const GLOBAL_IGNORES: Config;
125
174
 
126
175
  /**
127
176
  * Flag every physical property/value that has a logical equivalent.
@@ -139,7 +188,7 @@ declare const logicalRules: StylelintRules;
139
188
  * so projects that use SCSS *and* Tailwind don't get false positives from
140
189
  * `scss/at-rule-no-unknown`.
141
190
  */
142
- declare function scssRules(includeTailwind: boolean): StylelintRules;
191
+ declare function scssRules(includeTailwind: boolean | string): StylelintRules;
143
192
 
144
193
  /**
145
194
  * Tailwind v4 at-rules. New ones can be added without bumping a major
@@ -148,7 +197,7 @@ declare function scssRules(includeTailwind: boolean): StylelintRules;
148
197
  *
149
198
  * Source: https://tailwindcss.com/docs (v4 directives)
150
199
  */
151
- declare const TAILWIND_AT_RULES: string[];
200
+ declare const TAILWIND_AT_RULES: readonly ["theme", "utility", "source", "variant", "custom-variant", "reference", "apply", "layer", "screen", "starting-style", "tailwind", "config", "plugin", "responsive"];
152
201
  /**
153
202
  * Rules to whitelist Tailwind's at-rules and relax patterns that conflict
154
203
  * with Tailwind-prefixed classes (`maz:flex`, `dark:bg-…`).
@@ -167,6 +216,40 @@ declare const tailwindRules: StylelintRules;
167
216
  * recommended config.
168
217
  */
169
218
  declare const tailwindAtRuleNoUnknown: StylelintRules;
219
+ /**
220
+ * No plugin rules. `'minimal'` only ships the at-rule whitelist + a couple
221
+ * of CSS-level rules (`import-notation`, `at-rule-no-deprecated`) — no
222
+ * runtime Tailwind validation.
223
+ *
224
+ * Why empty: the plugin's validation rules (`no-invalid-apply`,
225
+ * `no-invalid-theme-function`) auto-resolve the project's Tailwind runtime
226
+ * from the linted file's path. Projects with a v4 prefix (`maz:`),
227
+ * non-standard entry CSS, or workspace layouts where Tailwind isn't where
228
+ * the plugin expects produce floods of false positives. Loading these
229
+ * rules without a way to scope them is hostile by default.
230
+ *
231
+ * Opt in to `'recommended'` if your project lives in the plugin's happy
232
+ * path.
233
+ */
234
+ declare const tailwindPluginMinimal: StylelintRules;
235
+ /**
236
+ * Plugin's recommended preset — `no-invalid-apply` and
237
+ * `no-invalid-theme-function` (validation), plus policies that discourage
238
+ * utility-first patterns inside authored CSS (`.flex { … }`, `@apply`,
239
+ * arbitrary values).
240
+ *
241
+ * Opt-in only: the validation rules need to resolve your Tailwind runtime
242
+ * from the linted file's path; if your project has a custom prefix, an
243
+ * unusual entry CSS or sits in a monorepo, expect false positives.
244
+ */
245
+ declare const tailwindPluginRecommended: StylelintRules;
246
+ /**
247
+ * Plugin's strict preset — adds architecture-level checks on top of
248
+ * recommended (no `theme()`, no `@screen`, no `@tailwind`, no `@import`,
249
+ * no `@layer`). Suited to projects that strictly separate utility tokens
250
+ * from authored CSS.
251
+ */
252
+ declare const tailwindPluginStrict: StylelintRules;
170
253
 
171
254
  /**
172
255
  * Build a Stylelint config tailored for Vue/Nuxt/JS/TS/Tailwind projects.
@@ -192,5 +275,5 @@ declare const tailwindAtRuleNoUnknown: StylelintRules;
192
275
  */
193
276
  declare function defineConfig(options?: MazStylelintOptions): StylelintConfig;
194
277
 
195
- export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindRules };
278
+ export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindPluginMinimal, tailwindPluginRecommended, tailwindPluginStrict, tailwindRules };
196
279
  export type { MazStylelintOptions, StylelintConfig, StylelintOrderStrategy, StylelintOverride, StylelintRules };
package/dist/index.mjs CHANGED
@@ -1,5 +1,9 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createConsola } from 'consola';
6
+ import { noInvalidApplyPlugin, noInvalidThemeFunctionPlugin, noAtomicClassPlugin, noApplyPlugin, noArbitraryValuePlugin, noThemeFunctionPlugin, noScreenDirectivePlugin, noTailwindDirectivePlugin, noImportDirectivePlugin, noCssLayerPlugin } from 'stylelint-plugin-tailwindcss';
3
7
 
4
8
  const baseRules = {
5
9
  /**
@@ -76,6 +80,29 @@ const GLOBAL_IGNORES = [
76
80
  "**/*.min.scss"
77
81
  ];
78
82
 
83
+ const LEVELS = {
84
+ silent: Number.NEGATIVE_INFINITY,
85
+ error: 0,
86
+ warning: 1,
87
+ normal: 2,
88
+ default: 3,
89
+ debug: 4,
90
+ trace: 5,
91
+ verbose: Number.POSITIVE_INFINITY
92
+ };
93
+ function createLogger() {
94
+ const consola = createConsola();
95
+ return {
96
+ setLevel: (level) => {
97
+ consola.level = LEVELS[level];
98
+ },
99
+ info: (msg) => consola.info(msg),
100
+ debug: (msg) => consola.debug(msg),
101
+ verbose: (msg) => consola.verbose(msg),
102
+ box: (opts) => consola.box(opts)
103
+ };
104
+ }
105
+
79
106
  const LOGICAL_PLUGIN = ["stylelint-use-logical-spec"];
80
107
  const logicalRules = {
81
108
  "liberty/use-logical-spec": ["always"]
@@ -101,6 +128,14 @@ const TAILWIND_AT_RULES = [
101
128
  ];
102
129
  const tailwindRules = {
103
130
  "at-rule-no-deprecated": [true, { ignoreAtRules: ["apply"] }],
131
+ /**
132
+ * `stylelint-config-recommended@18` enables the core rule
133
+ * `at-rule-prelude-no-invalid`, which validates the prelude of `@apply`
134
+ * against css-tree. A list of Tailwind utilities (`@apply maz:flex …`) is
135
+ * not a valid prelude grammar, so every `@apply` triggers a false positive.
136
+ * Whitelist Tailwind's at-rules (keeping `media`, ignored by recommended).
137
+ */
138
+ "at-rule-prelude-no-invalid": [true, { ignoreAtRules: [...TAILWIND_AT_RULES, "media"] }],
104
139
  /**
105
140
  * Tailwind v4 only parses the `prefix(...)` modifier on the bare-string
106
141
  * `@import "..."` form; wrapping the URL in `url(...)` swallows the modifier
@@ -111,6 +146,43 @@ const tailwindRules = {
111
146
  const tailwindAtRuleNoUnknown = {
112
147
  "at-rule-no-unknown": [true, { ignoreAtRules: TAILWIND_AT_RULES }]
113
148
  };
149
+ const tailwindPluginMinimal = {};
150
+ const tailwindPluginMinimalInstances = [];
151
+ const tailwindPluginRecommended = {
152
+ "tailwindcss/no-invalid-apply": true,
153
+ "tailwindcss/no-invalid-theme-function": true,
154
+ "tailwindcss/no-atomic-class": true,
155
+ "tailwindcss/no-apply": true,
156
+ "tailwindcss/no-arbitrary-value": true
157
+ };
158
+ const tailwindPluginRecommendedInstances = [
159
+ noInvalidApplyPlugin,
160
+ noInvalidThemeFunctionPlugin,
161
+ noAtomicClassPlugin,
162
+ noApplyPlugin,
163
+ noArbitraryValuePlugin
164
+ ];
165
+ const tailwindPluginStrict = {
166
+ ...tailwindPluginRecommended,
167
+ "tailwindcss/no-theme-function": true,
168
+ "tailwindcss/no-screen-directive": true,
169
+ "tailwindcss/no-tailwind-directive": true,
170
+ "tailwindcss/no-import-directive": true,
171
+ "tailwindcss/no-css-layer": true
172
+ };
173
+ const tailwindPluginStrictInstances = [
174
+ ...tailwindPluginRecommendedInstances,
175
+ noThemeFunctionPlugin,
176
+ noScreenDirectivePlugin,
177
+ noTailwindDirectivePlugin,
178
+ noImportDirectivePlugin,
179
+ noCssLayerPlugin
180
+ ];
181
+ const TAILWIND_POLICY_PLUGINS = {
182
+ minimal: tailwindPluginMinimalInstances,
183
+ recommended: tailwindPluginRecommendedInstances,
184
+ strict: tailwindPluginStrictInstances
185
+ };
114
186
 
115
187
  const SCSS_AT_RULES = [
116
188
  "use",
@@ -148,6 +220,20 @@ function scssRules(includeTailwind) {
148
220
  };
149
221
  }
150
222
 
223
+ const TAG = "[@maz-ui/stylelint-config]";
224
+ const internalRequire = createRequire(import.meta.url);
225
+ function resolveInternal(name) {
226
+ try {
227
+ const resolved = import.meta.resolve(name);
228
+ return resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved;
229
+ } catch {
230
+ try {
231
+ return internalRequire.resolve(name);
232
+ } catch {
233
+ return name;
234
+ }
235
+ }
236
+ }
151
237
  function readConsumerPackageJson() {
152
238
  try {
153
239
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"));
@@ -161,13 +247,57 @@ function hasDependency(pkg, ...names) {
161
247
  const buckets = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];
162
248
  return names.some((name) => buckets.some((bucket) => bucket && name in bucket));
163
249
  }
250
+ function resolveTailwind(option, pkg) {
251
+ if (option === false)
252
+ return { value: false, source: "explicit" };
253
+ if (option === true)
254
+ return { value: "minimal", source: "explicit" };
255
+ if (typeof option === "string")
256
+ return { value: option, source: "explicit" };
257
+ const detected = hasDependency(pkg, "tailwindcss", "@tailwindcss/vite");
258
+ return { value: detected ? "minimal" : false, source: "auto-detected" };
259
+ }
260
+ function resolveBoolean(explicit, detected) {
261
+ if (explicit !== void 0)
262
+ return { value: explicit, source: "explicit" };
263
+ return { value: detected, source: "auto-detected" };
264
+ }
265
+ function formatSourceTag(source) {
266
+ if (source === "auto-detected")
267
+ return " (auto-detected)";
268
+ if (source === "default")
269
+ return " (default)";
270
+ return "";
271
+ }
272
+ function formatResolutionBox(resolved) {
273
+ const tailwindLabel = resolved.tailwind === false ? "off" : resolved.tailwind;
274
+ const orderLabel = resolved.order === false ? "off" : resolved.order ?? "recess";
275
+ const rows = [
276
+ ["vue", `${resolved.vue}${formatSourceTag(resolved.vueSource)}`],
277
+ ["html", `${resolved.html}${formatSourceTag(resolved.htmlSource)}`],
278
+ ["tailwind", `${tailwindLabel}${formatSourceTag(resolved.tailwindSource)}`],
279
+ ["scss", `${resolved.scss}${formatSourceTag(resolved.scssSource)}`],
280
+ ["logical", String(resolved.logical)],
281
+ ["order", String(orderLabel)]
282
+ ];
283
+ const labelWidth = Math.max(...rows.map(([k]) => k.length));
284
+ return rows.map(([k, v]) => `${k.padEnd(labelWidth)} ${v}`).join("\n");
285
+ }
164
286
  function resolveOptions(options) {
165
287
  const pkg = readConsumerPackageJson();
288
+ const vue = resolveBoolean(options.vue, hasDependency(pkg, "vue", "nuxt"));
289
+ const html = options.html === void 0 ? { value: false, source: "default" } : { value: options.html, source: "explicit" };
290
+ const tailwind = resolveTailwind(options.tailwind, pkg);
291
+ const scss = resolveBoolean(options.scss, hasDependency(pkg, "sass", "sass-embedded", "node-sass"));
166
292
  return {
167
- vue: options.vue ?? hasDependency(pkg, "vue", "nuxt"),
168
- html: options.html ?? false,
169
- tailwind: options.tailwind ?? hasDependency(pkg, "tailwindcss", "@tailwindcss/vite"),
170
- scss: options.scss ?? hasDependency(pkg, "sass", "sass-embedded", "node-sass"),
293
+ vue: vue.value,
294
+ vueSource: vue.source,
295
+ html: html.value,
296
+ htmlSource: html.source,
297
+ tailwind: tailwind.value,
298
+ tailwindSource: tailwind.source,
299
+ scss: scss.value,
300
+ scssSource: scss.source,
171
301
  logical: options.logical ?? true,
172
302
  order: options.order ?? "recess"
173
303
  };
@@ -175,62 +305,110 @@ function resolveOptions(options) {
175
305
  function applyScss(resolved, ctx) {
176
306
  if (!resolved.scss)
177
307
  return;
178
- ctx.extends.push("stylelint-config-recommended-scss");
308
+ ctx.extends.push(resolveInternal("stylelint-config-standard-scss"));
179
309
  Object.assign(ctx.rules, scssRules(resolved.tailwind));
180
310
  ctx.rules["at-rule-no-unknown"] = null;
181
311
  ctx.overrides.push({
182
312
  files: ["**/*.scss"],
183
- customSyntax: "postcss-scss"
313
+ customSyntax: resolveInternal("postcss-scss")
184
314
  });
315
+ ctx.log.debug(`${TAG} SCSS: extended stylelint-config-standard-scss + postcss-scss override`);
185
316
  }
186
317
  function applyVueAndHtml(resolved, ctx) {
187
- if (resolved.vue)
188
- ctx.extends.push("stylelint-config-recommended-vue");
189
- if (resolved.html)
190
- ctx.extends.push("stylelint-config-html");
318
+ if (resolved.vue) {
319
+ ctx.extends.push(resolveInternal("stylelint-config-recommended-vue"));
320
+ ctx.log.debug(`${TAG} Vue: extended stylelint-config-recommended-vue`);
321
+ }
322
+ if (resolved.html) {
323
+ ctx.extends.push(resolveInternal("stylelint-config-html"));
324
+ ctx.log.debug(`${TAG} HTML: extended stylelint-config-html`);
325
+ }
191
326
  if (!resolved.vue && !resolved.html)
192
327
  return;
193
328
  const vueGlobs = resolved.vue ? ["**/*.vue"] : [];
194
329
  const htmlGlobs = resolved.html ? ["**/*.html"] : [];
195
330
  ctx.overrides.push({
196
331
  files: [...vueGlobs, ...htmlGlobs],
197
- customSyntax: "postcss-html"
332
+ customSyntax: resolveInternal("postcss-html")
198
333
  });
199
334
  }
335
+ const TAILWIND_POLICY_RULES = {
336
+ minimal: tailwindPluginMinimal,
337
+ recommended: tailwindPluginRecommended,
338
+ strict: tailwindPluginStrict
339
+ };
340
+ function applyTailwind(resolved, ctx) {
341
+ if (!resolved.tailwind)
342
+ return;
343
+ Object.assign(ctx.rules, tailwindRules);
344
+ if (!resolved.scss)
345
+ Object.assign(ctx.rules, tailwindAtRuleNoUnknown);
346
+ const plugins = TAILWIND_POLICY_PLUGINS[resolved.tailwind];
347
+ if (plugins.length === 0) {
348
+ ctx.log.debug(`${TAG} Tailwind: at-rule whitelist only ("${resolved.tailwind}" policy \u2014 no plugin loaded)`);
349
+ return;
350
+ }
351
+ ctx.plugins.push(...plugins);
352
+ Object.assign(ctx.rules, TAILWIND_POLICY_RULES[resolved.tailwind]);
353
+ ctx.log.debug(`${TAG} Tailwind: loaded ${plugins.length} plugin(s) for "${resolved.tailwind}" policy`);
354
+ }
200
355
  function applyLogical(resolved, ctx) {
201
356
  if (!resolved.logical)
202
357
  return;
203
- ctx.plugins.push(...LOGICAL_PLUGIN);
358
+ ctx.plugins.push(...LOGICAL_PLUGIN.map((name) => resolveInternal(name)));
204
359
  Object.assign(ctx.rules, logicalRules);
360
+ ctx.log.debug(`${TAG} Logical: loaded stylelint-use-logical-spec`);
205
361
  }
206
362
  function applyUserOverrides(options, ctx) {
207
- if (Array.isArray(options.plugins) && options.plugins.length > 0)
363
+ if (Array.isArray(options.plugins) && options.plugins.length > 0) {
208
364
  ctx.plugins.push(...options.plugins);
209
- if (options.rules)
365
+ ctx.log.debug(`${TAG} User: appended ${options.plugins.length} plugin(s)`);
366
+ }
367
+ if (options.rules) {
210
368
  Object.assign(ctx.rules, options.rules);
211
- if (options.overrides?.length)
369
+ ctx.log.debug(`${TAG} User: merged ${Object.keys(options.rules).length} rule override(s)`);
370
+ }
371
+ if (options.overrides?.length) {
212
372
  ctx.overrides.push(...options.overrides);
373
+ ctx.log.debug(`${TAG} User: appended ${options.overrides.length} per-file override(s)`);
374
+ }
375
+ if (options.extends?.length) {
376
+ ctx.extends.push(...options.extends);
377
+ ctx.log.debug(`${TAG} User: extended ${options.extends.join(", ")}`);
378
+ }
213
379
  }
214
380
  function defineConfig(options = {}) {
381
+ const log = createLogger();
382
+ log.setLevel(options.logLevel ?? "default");
215
383
  const resolved = resolveOptions(options);
384
+ log.box({
385
+ title: "@maz-ui/stylelint-config",
386
+ message: formatResolutionBox(resolved),
387
+ style: { borderColor: "cyan", padding: 1 }
388
+ });
216
389
  const ctx = {
217
- extends: ["stylelint-config-standard"],
390
+ extends: [resolveInternal("stylelint-config-standard")],
218
391
  plugins: [],
219
392
  rules: { ...baseRules },
220
- overrides: []
393
+ overrides: [],
394
+ log
221
395
  };
222
- if (resolved.order === "recess")
223
- ctx.extends.push("stylelint-config-recess-order");
396
+ if (resolved.order === "recess") {
397
+ ctx.extends.push(resolveInternal("stylelint-config-recess-order"));
398
+ log.debug(`${TAG} Order: extended stylelint-config-recess-order`);
399
+ } else if (resolved.order === "alphabetical") {
400
+ ctx.plugins.push(resolveInternal("stylelint-order"));
401
+ ctx.rules["order/properties-alphabetical-order"] = true;
402
+ log.debug(`${TAG} Order: loaded stylelint-order with properties-alphabetical-order`);
403
+ }
224
404
  applyScss(resolved, ctx);
225
405
  applyVueAndHtml(resolved, ctx);
226
- if (resolved.tailwind) {
227
- Object.assign(ctx.rules, tailwindRules);
228
- if (!resolved.scss)
229
- Object.assign(ctx.rules, tailwindAtRuleNoUnknown);
230
- }
406
+ applyTailwind(resolved, ctx);
231
407
  applyLogical(resolved, ctx);
232
408
  applyUserOverrides(options, ctx);
233
409
  const ignoreFiles = options.ignoresOverride ?? [...GLOBAL_IGNORES, ...options.ignores ?? []];
410
+ log.verbose(`${TAG} Final extends (${ctx.extends.length}): ${ctx.extends.join(", ")}`);
411
+ log.verbose(`${TAG} Final plugin count: ${ctx.plugins.length}, rule count: ${Object.keys(ctx.rules).length}, override count: ${ctx.overrides.length}, ignore globs: ${ignoreFiles.length}`);
234
412
  return {
235
413
  extends: ctx.extends,
236
414
  plugins: ctx.plugins.length > 0 ? ctx.plugins : void 0,
@@ -240,4 +418,4 @@ function defineConfig(options = {}) {
240
418
  };
241
419
  }
242
420
 
243
- export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindRules };
421
+ export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindPluginMinimal, tailwindPluginRecommended, tailwindPluginStrict, tailwindRules };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maz-ui/stylelint-config",
3
3
  "type": "module",
4
- "version": "5.0.0-canary.ef64a02.0",
4
+ "version": "5.0.0",
5
5
  "description": "Stylelint configuration for CSS, SCSS, Vue and Tailwind CSS projects",
6
6
  "author": "Louis Mazel <me@loicmazuel.com>",
7
7
  "license": "MIT",
@@ -27,7 +27,8 @@
27
27
  "exports": {
28
28
  ".": {
29
29
  "types": "./dist/index.d.ts",
30
- "import": "./dist/index.mjs"
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
31
32
  }
32
33
  },
33
34
  "main": "./dist/index.mjs",
@@ -38,28 +39,29 @@
38
39
  "dist"
39
40
  ],
40
41
  "engines": {
41
- "node": ">=20.19.0"
42
+ "node": ">=22.12.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "stylelint": ">=16.0.0 <18.0.0"
45
46
  },
46
47
  "dependencies": {
47
- "postcss-html": "^1.8.1",
48
+ "consola": "^3.4.2",
49
+ "postcss-html": "^2.0.0",
48
50
  "postcss-scss": "^4.0.9",
49
- "stylelint-config-html": "^1.1.0",
50
- "stylelint-config-recess-order": "^7.7.0",
51
- "stylelint-config-recommended-scss": "^17.0.1",
52
- "stylelint-config-recommended-vue": "^1.6.1",
51
+ "stylelint-config-html": "^2.0.0",
52
+ "stylelint-config-recess-order": "^7.8.2",
53
+ "stylelint-config-recommended-vue": "^2.0.0",
53
54
  "stylelint-config-standard": "^40.0.0",
54
- "stylelint-scss": "^7.0.0",
55
+ "stylelint-config-standard-scss": "^17.0.0",
56
+ "stylelint-order": "^8.1.2",
57
+ "stylelint-plugin-tailwindcss": "^3.1.3",
55
58
  "stylelint-use-logical-spec": "^5.0.1"
56
59
  },
57
60
  "devDependencies": {
58
- "@vitest/coverage-v8": "^4.1.5",
59
- "stylelint": "^17.11.0",
60
- "unbuild": "^3.6.1",
61
- "vitest": "^4.1.5",
62
- "@maz-ui/eslint-config": "5.0.0-canary.ef64a02.0"
61
+ "@maz-ui/eslint-config": "5.0.0",
62
+ "@vitest/coverage-v8": "^5.0.5",
63
+ "stylelint": "^17.16.0",
64
+ "unbuild": "^3.6.1"
63
65
  },
64
66
  "lint-staged": {
65
67
  "*.{js,jsx,ts,tsx,mjs,mts,cjs,md,yml,json}": [
@@ -67,10 +69,9 @@
67
69
  ]
68
70
  },
69
71
  "scripts": {
70
- "codegen": "nx build",
71
72
  "build": "unbuild",
72
73
  "dev": "unbuild --stub",
73
- "typecheck": "tsc --noEmit --skipLibCheck && tsc --noEmit --skipLibCheck --project tsconfig.test.json",
74
+ "typecheck": "tsc --build",
74
75
  "lint": "cross-env NODE_ENV=production eslint .",
75
76
  "lint:fix": "pnpm lint --fix",
76
77
  "test:unit": "vitest run",