@maz-ui/stylelint-config 5.0.0-beta.0 → 5.0.0-beta.11

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,5 +1,14 @@
1
1
  import { Config } from 'stylelint';
2
2
 
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
+
3
12
  type StylelintConfig = Config;
4
13
  type StylelintRules = Config['rules'];
5
14
  type StylelintOverride = NonNullable<Config['overrides']>[number];
@@ -8,8 +17,9 @@ type StylelintOverride = NonNullable<Config['overrides']>[number];
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.
@@ -105,6 +125,35 @@ interface MazStylelintOptions {
105
125
  * Additional plugins merged on top of the resolved configuration.
106
126
  */
107
127
  plugins?: NonNullable<Config['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
  /**
@@ -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,5 +1,14 @@
1
1
  import { Config } from 'stylelint';
2
2
 
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
+
3
12
  type StylelintConfig = Config;
4
13
  type StylelintRules = Config['rules'];
5
14
  type StylelintOverride = NonNullable<Config['overrides']>[number];
@@ -8,8 +17,9 @@ type StylelintOverride = NonNullable<Config['overrides']>[number];
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.
@@ -105,6 +125,35 @@ interface MazStylelintOptions {
105
125
  * Additional plugins merged on top of the resolved configuration.
106
126
  */
107
127
  plugins?: NonNullable<Config['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
  /**
@@ -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,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import { join } from 'node:path';
4
+ import { createConsola } from 'consola';
5
+ import { noInvalidApplyPlugin, noInvalidThemeFunctionPlugin, noAtomicClassPlugin, noApplyPlugin, noArbitraryValuePlugin, noThemeFunctionPlugin, noScreenDirectivePlugin, noTailwindDirectivePlugin, noImportDirectivePlugin, noCssLayerPlugin } from 'stylelint-plugin-tailwindcss';
3
6
 
4
7
  const baseRules = {
5
8
  /**
@@ -76,6 +79,29 @@ const GLOBAL_IGNORES = [
76
79
  "**/*.min.scss"
77
80
  ];
78
81
 
82
+ const LEVELS = {
83
+ silent: Number.NEGATIVE_INFINITY,
84
+ error: 0,
85
+ warning: 1,
86
+ normal: 2,
87
+ default: 3,
88
+ debug: 4,
89
+ trace: 5,
90
+ verbose: Number.POSITIVE_INFINITY
91
+ };
92
+ function createLogger() {
93
+ const consola = createConsola();
94
+ return {
95
+ setLevel: (level) => {
96
+ consola.level = LEVELS[level];
97
+ },
98
+ info: (msg) => consola.info(msg),
99
+ debug: (msg) => consola.debug(msg),
100
+ verbose: (msg) => consola.verbose(msg),
101
+ box: (opts) => consola.box(opts)
102
+ };
103
+ }
104
+
79
105
  const LOGICAL_PLUGIN = ["stylelint-use-logical-spec"];
80
106
  const logicalRules = {
81
107
  "liberty/use-logical-spec": ["always"]
@@ -111,6 +137,43 @@ const tailwindRules = {
111
137
  const tailwindAtRuleNoUnknown = {
112
138
  "at-rule-no-unknown": [true, { ignoreAtRules: TAILWIND_AT_RULES }]
113
139
  };
140
+ const tailwindPluginMinimal = {};
141
+ const tailwindPluginMinimalInstances = [];
142
+ const tailwindPluginRecommended = {
143
+ "tailwindcss/no-invalid-apply": true,
144
+ "tailwindcss/no-invalid-theme-function": true,
145
+ "tailwindcss/no-atomic-class": true,
146
+ "tailwindcss/no-apply": true,
147
+ "tailwindcss/no-arbitrary-value": true
148
+ };
149
+ const tailwindPluginRecommendedInstances = [
150
+ noInvalidApplyPlugin,
151
+ noInvalidThemeFunctionPlugin,
152
+ noAtomicClassPlugin,
153
+ noApplyPlugin,
154
+ noArbitraryValuePlugin
155
+ ];
156
+ const tailwindPluginStrict = {
157
+ ...tailwindPluginRecommended,
158
+ "tailwindcss/no-theme-function": true,
159
+ "tailwindcss/no-screen-directive": true,
160
+ "tailwindcss/no-tailwind-directive": true,
161
+ "tailwindcss/no-import-directive": true,
162
+ "tailwindcss/no-css-layer": true
163
+ };
164
+ const tailwindPluginStrictInstances = [
165
+ ...tailwindPluginRecommendedInstances,
166
+ noThemeFunctionPlugin,
167
+ noScreenDirectivePlugin,
168
+ noTailwindDirectivePlugin,
169
+ noImportDirectivePlugin,
170
+ noCssLayerPlugin
171
+ ];
172
+ const TAILWIND_POLICY_PLUGINS = {
173
+ minimal: tailwindPluginMinimalInstances,
174
+ recommended: tailwindPluginRecommendedInstances,
175
+ strict: tailwindPluginStrictInstances
176
+ };
114
177
 
115
178
  const SCSS_AT_RULES = [
116
179
  "use",
@@ -148,6 +211,15 @@ function scssRules(includeTailwind) {
148
211
  };
149
212
  }
150
213
 
214
+ const TAG = "[@maz-ui/stylelint-config]";
215
+ const internalRequire = createRequire(import.meta.url);
216
+ function resolveInternal(name) {
217
+ try {
218
+ return internalRequire.resolve(name);
219
+ } catch {
220
+ return name;
221
+ }
222
+ }
151
223
  function readConsumerPackageJson() {
152
224
  try {
153
225
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"));
@@ -161,13 +233,57 @@ function hasDependency(pkg, ...names) {
161
233
  const buckets = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies];
162
234
  return names.some((name) => buckets.some((bucket) => bucket && name in bucket));
163
235
  }
236
+ function resolveTailwind(option, pkg) {
237
+ if (option === false)
238
+ return { value: false, source: "explicit" };
239
+ if (option === true)
240
+ return { value: "minimal", source: "explicit" };
241
+ if (typeof option === "string")
242
+ return { value: option, source: "explicit" };
243
+ const detected = hasDependency(pkg, "tailwindcss", "@tailwindcss/vite");
244
+ return { value: detected ? "minimal" : false, source: "auto-detected" };
245
+ }
246
+ function resolveBoolean(explicit, detected) {
247
+ if (explicit !== void 0)
248
+ return { value: explicit, source: "explicit" };
249
+ return { value: detected, source: "auto-detected" };
250
+ }
251
+ function formatSourceTag(source) {
252
+ if (source === "auto-detected")
253
+ return " (auto-detected)";
254
+ if (source === "default")
255
+ return " (default)";
256
+ return "";
257
+ }
258
+ function formatResolutionBox(resolved) {
259
+ const tailwindLabel = resolved.tailwind === false ? "off" : resolved.tailwind;
260
+ const orderLabel = resolved.order === false ? "off" : resolved.order ?? "recess";
261
+ const rows = [
262
+ ["vue", `${resolved.vue}${formatSourceTag(resolved.vueSource)}`],
263
+ ["html", `${resolved.html}${formatSourceTag(resolved.htmlSource)}`],
264
+ ["tailwind", `${tailwindLabel}${formatSourceTag(resolved.tailwindSource)}`],
265
+ ["scss", `${resolved.scss}${formatSourceTag(resolved.scssSource)}`],
266
+ ["logical", String(resolved.logical)],
267
+ ["order", String(orderLabel)]
268
+ ];
269
+ const labelWidth = Math.max(...rows.map(([k]) => k.length));
270
+ return rows.map(([k, v]) => `${k.padEnd(labelWidth)} ${v}`).join("\n");
271
+ }
164
272
  function resolveOptions(options) {
165
273
  const pkg = readConsumerPackageJson();
274
+ const vue = resolveBoolean(options.vue, hasDependency(pkg, "vue", "nuxt"));
275
+ const html = options.html === void 0 ? { value: false, source: "default" } : { value: options.html, source: "explicit" };
276
+ const tailwind = resolveTailwind(options.tailwind, pkg);
277
+ const scss = resolveBoolean(options.scss, hasDependency(pkg, "sass", "sass-embedded", "node-sass"));
166
278
  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"),
279
+ vue: vue.value,
280
+ vueSource: vue.source,
281
+ html: html.value,
282
+ htmlSource: html.source,
283
+ tailwind: tailwind.value,
284
+ tailwindSource: tailwind.source,
285
+ scss: scss.value,
286
+ scssSource: scss.source,
171
287
  logical: options.logical ?? true,
172
288
  order: options.order ?? "recess"
173
289
  };
@@ -175,62 +291,110 @@ function resolveOptions(options) {
175
291
  function applyScss(resolved, ctx) {
176
292
  if (!resolved.scss)
177
293
  return;
178
- ctx.extends.push("stylelint-config-recommended-scss");
294
+ ctx.extends.push(resolveInternal("stylelint-config-standard-scss"));
179
295
  Object.assign(ctx.rules, scssRules(resolved.tailwind));
180
296
  ctx.rules["at-rule-no-unknown"] = null;
181
297
  ctx.overrides.push({
182
298
  files: ["**/*.scss"],
183
- customSyntax: "postcss-scss"
299
+ customSyntax: resolveInternal("postcss-scss")
184
300
  });
301
+ ctx.log.debug(`${TAG} SCSS: extended stylelint-config-standard-scss + postcss-scss override`);
185
302
  }
186
303
  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");
304
+ if (resolved.vue) {
305
+ ctx.extends.push(resolveInternal("stylelint-config-recommended-vue"));
306
+ ctx.log.debug(`${TAG} Vue: extended stylelint-config-recommended-vue`);
307
+ }
308
+ if (resolved.html) {
309
+ ctx.extends.push(resolveInternal("stylelint-config-html"));
310
+ ctx.log.debug(`${TAG} HTML: extended stylelint-config-html`);
311
+ }
191
312
  if (!resolved.vue && !resolved.html)
192
313
  return;
193
314
  const vueGlobs = resolved.vue ? ["**/*.vue"] : [];
194
315
  const htmlGlobs = resolved.html ? ["**/*.html"] : [];
195
316
  ctx.overrides.push({
196
317
  files: [...vueGlobs, ...htmlGlobs],
197
- customSyntax: "postcss-html"
318
+ customSyntax: resolveInternal("postcss-html")
198
319
  });
199
320
  }
321
+ const TAILWIND_POLICY_RULES = {
322
+ minimal: tailwindPluginMinimal,
323
+ recommended: tailwindPluginRecommended,
324
+ strict: tailwindPluginStrict
325
+ };
326
+ function applyTailwind(resolved, ctx) {
327
+ if (!resolved.tailwind)
328
+ return;
329
+ Object.assign(ctx.rules, tailwindRules);
330
+ if (!resolved.scss)
331
+ Object.assign(ctx.rules, tailwindAtRuleNoUnknown);
332
+ const plugins = TAILWIND_POLICY_PLUGINS[resolved.tailwind];
333
+ if (plugins.length === 0) {
334
+ ctx.log.debug(`${TAG} Tailwind: at-rule whitelist only ("${resolved.tailwind}" policy \u2014 no plugin loaded)`);
335
+ return;
336
+ }
337
+ ctx.plugins.push(...plugins);
338
+ Object.assign(ctx.rules, TAILWIND_POLICY_RULES[resolved.tailwind]);
339
+ ctx.log.debug(`${TAG} Tailwind: loaded ${plugins.length} plugin(s) for "${resolved.tailwind}" policy`);
340
+ }
200
341
  function applyLogical(resolved, ctx) {
201
342
  if (!resolved.logical)
202
343
  return;
203
- ctx.plugins.push(...LOGICAL_PLUGIN);
344
+ ctx.plugins.push(...LOGICAL_PLUGIN.map((name) => resolveInternal(name)));
204
345
  Object.assign(ctx.rules, logicalRules);
346
+ ctx.log.debug(`${TAG} Logical: loaded stylelint-use-logical-spec`);
205
347
  }
206
348
  function applyUserOverrides(options, ctx) {
207
- if (Array.isArray(options.plugins) && options.plugins.length > 0)
349
+ if (Array.isArray(options.plugins) && options.plugins.length > 0) {
208
350
  ctx.plugins.push(...options.plugins);
209
- if (options.rules)
351
+ ctx.log.debug(`${TAG} User: appended ${options.plugins.length} plugin(s)`);
352
+ }
353
+ if (options.rules) {
210
354
  Object.assign(ctx.rules, options.rules);
211
- if (options.overrides?.length)
355
+ ctx.log.debug(`${TAG} User: merged ${Object.keys(options.rules).length} rule override(s)`);
356
+ }
357
+ if (options.overrides?.length) {
212
358
  ctx.overrides.push(...options.overrides);
359
+ ctx.log.debug(`${TAG} User: appended ${options.overrides.length} per-file override(s)`);
360
+ }
361
+ if (options.extends?.length) {
362
+ ctx.extends.push(...options.extends);
363
+ ctx.log.debug(`${TAG} User: extended ${options.extends.join(", ")}`);
364
+ }
213
365
  }
214
366
  function defineConfig(options = {}) {
367
+ const log = createLogger();
368
+ log.setLevel(options.logLevel ?? "default");
215
369
  const resolved = resolveOptions(options);
370
+ log.box({
371
+ title: "@maz-ui/stylelint-config",
372
+ message: formatResolutionBox(resolved),
373
+ style: { borderColor: "cyan", padding: 1 }
374
+ });
216
375
  const ctx = {
217
- extends: ["stylelint-config-standard"],
376
+ extends: [resolveInternal("stylelint-config-standard")],
218
377
  plugins: [],
219
378
  rules: { ...baseRules },
220
- overrides: []
379
+ overrides: [],
380
+ log
221
381
  };
222
- if (resolved.order === "recess")
223
- ctx.extends.push("stylelint-config-recess-order");
382
+ if (resolved.order === "recess") {
383
+ ctx.extends.push(resolveInternal("stylelint-config-recess-order"));
384
+ log.debug(`${TAG} Order: extended stylelint-config-recess-order`);
385
+ } else if (resolved.order === "alphabetical") {
386
+ ctx.plugins.push(resolveInternal("stylelint-order"));
387
+ ctx.rules["order/properties-alphabetical-order"] = true;
388
+ log.debug(`${TAG} Order: loaded stylelint-order with properties-alphabetical-order`);
389
+ }
224
390
  applyScss(resolved, ctx);
225
391
  applyVueAndHtml(resolved, ctx);
226
- if (resolved.tailwind) {
227
- Object.assign(ctx.rules, tailwindRules);
228
- if (!resolved.scss)
229
- Object.assign(ctx.rules, tailwindAtRuleNoUnknown);
230
- }
392
+ applyTailwind(resolved, ctx);
231
393
  applyLogical(resolved, ctx);
232
394
  applyUserOverrides(options, ctx);
233
395
  const ignoreFiles = options.ignoresOverride ?? [...GLOBAL_IGNORES, ...options.ignores ?? []];
396
+ log.verbose(`${TAG} Final extends (${ctx.extends.length}): ${ctx.extends.join(", ")}`);
397
+ 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
398
  return {
235
399
  extends: ctx.extends,
236
400
  plugins: ctx.plugins.length > 0 ? ctx.plugins : void 0,
@@ -240,4 +404,4 @@ function defineConfig(options = {}) {
240
404
  };
241
405
  }
242
406
 
243
- export { GLOBAL_IGNORES, TAILWIND_AT_RULES, baseRules, defineConfig, logicalRules, scssRules, tailwindAtRuleNoUnknown, tailwindRules };
407
+ 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-beta.0",
4
+ "version": "5.0.0-beta.11",
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",
@@ -44,14 +45,16 @@
44
45
  "stylelint": ">=16.0.0 <18.0.0"
45
46
  },
46
47
  "dependencies": {
48
+ "consola": "^3.4.2",
47
49
  "postcss-html": "^1.8.1",
48
50
  "postcss-scss": "^4.0.9",
49
51
  "stylelint-config-html": "^1.1.0",
50
52
  "stylelint-config-recess-order": "^7.7.0",
51
- "stylelint-config-recommended-scss": "^17.0.1",
52
53
  "stylelint-config-recommended-vue": "^1.6.1",
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.1",
57
+ "stylelint-plugin-tailwindcss": "^2.0.3",
55
58
  "stylelint-use-logical-spec": "^5.0.1"
56
59
  },
57
60
  "devDependencies": {
@@ -59,7 +62,7 @@
59
62
  "stylelint": "^17.11.0",
60
63
  "unbuild": "^3.6.1",
61
64
  "vitest": "^4.1.5",
62
- "@maz-ui/eslint-config": "5.0.0-beta.0"
65
+ "@maz-ui/eslint-config": "5.0.0-beta.11"
63
66
  },
64
67
  "lint-staged": {
65
68
  "*.{js,jsx,ts,tsx,mjs,mts,cjs,md,yml,json}": [
@@ -70,7 +73,7 @@
70
73
  "codegen": "nx build",
71
74
  "build": "unbuild",
72
75
  "dev": "unbuild --stub",
73
- "typecheck": "tsc --noEmit --skipLibCheck && tsc --noEmit --skipLibCheck --project tsconfig.test.json",
76
+ "typecheck": "tsc --build",
74
77
  "lint": "cross-env NODE_ENV=production eslint .",
75
78
  "lint:fix": "pnpm lint --fix",
76
79
  "test:unit": "vitest run",