@vijayhardaha/dev-config 2.4.0 → 2.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vijayhardaha/dev-config",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Reusable development configurations for Next.js + TypeScript projects",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -29,12 +29,27 @@ describe('eslint/next.js', () => {
29
29
  expect(configObject.files).toContain('custom/**/*.tsx');
30
30
  });
31
31
 
32
- // Test that Tailwind plugins are registered on the main config by default.
33
- it('should register Tailwind plugins by default', async () => {
32
+ // Test that Tailwind plugins and rules are omitted by default.
33
+ it('should omit Tailwind plugins and rules by default', async () => {
34
34
  const module = await import('../next.js');
35
35
  const result = module.createConfig();
36
36
  const configObject = result.at(-1);
37
37
 
38
+ // Verify that no Tailwind plugins are centrally registered.
39
+ expect(configObject.plugins['better-tailwindcss']).toBeUndefined();
40
+ expect(configObject.plugins.tailwindcss).toBeUndefined();
41
+
42
+ // Verify that no Tailwind rules are present in the config.
43
+ expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('better-tailwindcss/'))).toHaveLength(0);
44
+ expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('tailwindcss/'))).toHaveLength(0);
45
+ });
46
+
47
+ // Test that Tailwind plugins, rules, and settings register when opted in.
48
+ it('should register Tailwind plugins when tailwind option is true', async () => {
49
+ const module = await import('../next.js');
50
+ const result = module.createConfig({ tailwind: true });
51
+ const configObject = result.at(-1);
52
+
38
53
  // Verify that the better-tailwindcss plugin is centrally registered.
39
54
  expect(configObject.plugins['better-tailwindcss']).toBeDefined();
40
55
 
@@ -48,25 +63,6 @@ describe('eslint/next.js', () => {
48
63
  expect(configObject.rules['tailwindcss/no-unnecessary-arbitrary-value']).toBe('warn');
49
64
  });
50
65
 
51
- // Test that Tailwind plugins, rules, and settings are omitted when disabled.
52
- it('should omit Tailwind plugins and rules when tailwind option is false', async () => {
53
- const module = await import('../next.js');
54
- const result = module.createConfig({ tailwind: false });
55
- const configObject = result.at(-1);
56
-
57
- // Verify that no Tailwind plugins are centrally registered.
58
- expect(configObject.plugins['better-tailwindcss']).toBeUndefined();
59
- expect(configObject.plugins.tailwindcss).toBeUndefined();
60
-
61
- // Verify that no Tailwind rules are present in the config.
62
- expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('better-tailwindcss/'))).toHaveLength(0);
63
- expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('tailwindcss/'))).toHaveLength(0);
64
-
65
- // Verify that no Tailwind settings are present in the config.
66
- expect(configObject.settings.tailwindcss).toBeUndefined();
67
- expect(configObject.settings['better-tailwindcss']).toBeUndefined();
68
- });
69
-
70
66
  // Test that user settings override the generated Tailwind settings.
71
67
  it('should let user settings override Tailwind settings', async () => {
72
68
  const module = await import('../next.js');
@@ -29,12 +29,27 @@ describe('eslint/react.js', () => {
29
29
  expect(configObject.files).toContain('custom/**/*.tsx');
30
30
  });
31
31
 
32
- // Test that Tailwind plugins are registered on the main config by default.
33
- it('should register Tailwind plugins by default', async () => {
32
+ // Test that Tailwind plugins and rules are omitted by default.
33
+ it('should omit Tailwind plugins and rules by default', async () => {
34
34
  const module = await import('../react.js');
35
35
  const result = module.createConfig();
36
36
  const configObject = result.at(-1);
37
37
 
38
+ // Verify that no Tailwind plugins are centrally registered.
39
+ expect(configObject.plugins['better-tailwindcss']).toBeUndefined();
40
+ expect(configObject.plugins.tailwindcss).toBeUndefined();
41
+
42
+ // Verify that no Tailwind rules are present in the config.
43
+ expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('better-tailwindcss/'))).toHaveLength(0);
44
+ expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('tailwindcss/'))).toHaveLength(0);
45
+ });
46
+
47
+ // Test that Tailwind plugins, rules, and settings register when opted in.
48
+ it('should register Tailwind plugins when tailwind option is true', async () => {
49
+ const module = await import('../react.js');
50
+ const result = module.createConfig({ tailwind: true });
51
+ const configObject = result.at(-1);
52
+
38
53
  // Verify that the better-tailwindcss plugin is centrally registered.
39
54
  expect(configObject.plugins['better-tailwindcss']).toBeDefined();
40
55
 
@@ -47,23 +62,4 @@ describe('eslint/react.js', () => {
47
62
  // Verify that the arbitrary value replacement rule is enabled.
48
63
  expect(configObject.rules['tailwindcss/no-unnecessary-arbitrary-value']).toBe('warn');
49
64
  });
50
-
51
- // Test that Tailwind plugins, rules, and settings are omitted when disabled.
52
- it('should omit Tailwind plugins and rules when tailwind option is false', async () => {
53
- const module = await import('../react.js');
54
- const result = module.createConfig({ tailwind: false });
55
- const configObject = result.at(-1);
56
-
57
- // Verify that no Tailwind plugins are centrally registered.
58
- expect(configObject.plugins['better-tailwindcss']).toBeUndefined();
59
- expect(configObject.plugins.tailwindcss).toBeUndefined();
60
-
61
- // Verify that no Tailwind rules are present in the config.
62
- expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('better-tailwindcss/'))).toHaveLength(0);
63
- expect(Object.keys(configObject.rules).filter((rule) => rule.startsWith('tailwindcss/'))).toHaveLength(0);
64
-
65
- // Verify that no Tailwind settings are present in the config.
66
- expect(configObject.settings.tailwindcss).toBeUndefined();
67
- expect(configObject.settings['better-tailwindcss']).toBeUndefined();
68
- });
69
65
  });
@@ -63,7 +63,7 @@ describe('eslint tailwind autofix', () => {
63
63
  '',
64
64
  ].join('\n');
65
65
 
66
- const result = await lintAndFix(projectDir, nextModule, code);
66
+ const result = await lintAndFix(projectDir, nextModule, code, { tailwind: true });
67
67
 
68
68
  expect(result.output).toContain('aspect-3/4');
69
69
  expect(result.output).toContain('max-w-70');
@@ -81,7 +81,10 @@ describe('eslint tailwind autofix', () => {
81
81
  '',
82
82
  ].join('\n');
83
83
 
84
- const result = await lintAndFix(projectDir, nextModule, code, { rules: { 'import-x/order': 'off' } });
84
+ const result = await lintAndFix(projectDir, nextModule, code, {
85
+ tailwind: true,
86
+ rules: { 'import-x/order': 'off' },
87
+ });
85
88
 
86
89
  expect(result.output).toContain('data-[side=bottom]:translate-y-10');
87
90
  expect(result.output).toContain('dark:hover:-translate-x-10');
@@ -94,7 +97,7 @@ describe('eslint tailwind autofix', () => {
94
97
  + 'transition-all hover:-translate-y-1 hover:shadow-lg dark:border-slate-800 dark:bg-slate-900';
95
98
  const code = ['const Card = () => (', ` <div className="${longClasses}">Card</div>`, ');', ''].join('\n');
96
99
 
97
- const result = await lintAndFix(projectDir, nextModule, code);
100
+ const result = await lintAndFix(projectDir, nextModule, code, { tailwind: true });
98
101
 
99
102
  expect(result.output).toMatch(/className="\n\s+flex/);
100
103
  }, 60_000);
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // Shared handle to stub prettier.resolveConfig before modules evaluate.
4
+ const mockResolveConfig = vi.hoisted(() => vi.fn());
5
+
6
+ // Intercept prettier so entry-point detection can be simulated without
7
+ // touching real consumer configs on disk.
8
+ vi.mock('prettier', async (importOriginal) => {
9
+ const actual = await importOriginal();
10
+ return { ...actual, resolveConfig: mockResolveConfig };
11
+ });
12
+
13
+ describe('eslint/lib/rules.js with prettier-plugin-tailwindcss', () => {
14
+ it('drops ordering and wrapping rules when prettier-plugin-tailwindcss is active', async () => {
15
+ // Simulate a consumer Prettier config that enables the Tailwind plugin.
16
+ mockResolveConfig.mockResolvedValue({ plugins: ['prettier-plugin-tailwindcss'] });
17
+
18
+ const rules = await import('../rules.js');
19
+ const result = rules.tailwindRules();
20
+
21
+ // Verify formatter-owned concerns are left to Prettier.
22
+ expect(result['better-tailwindcss/enforce-consistent-class-order']).toBeUndefined();
23
+ expect(result['better-tailwindcss/enforce-consistent-line-wrapping']).toBeUndefined();
24
+
25
+ // Verify lint-only concerns stay enabled.
26
+ expect(result['better-tailwindcss/enforce-canonical-classes']).toBe('warn');
27
+ expect(result['better-tailwindcss/no-unnecessary-whitespace']).toBe('warn');
28
+ expect(result['tailwindcss/no-unnecessary-arbitrary-value']).toBe('warn');
29
+ });
30
+
31
+ it('keeps ordering and wrapping rules without the prettier plugin', async () => {
32
+ // Simulate a consumer Prettier config without the Tailwind plugin.
33
+ mockResolveConfig.mockResolvedValue({});
34
+
35
+ vi.resetModules();
36
+ const rules = await import('../rules.js');
37
+ const result = rules.tailwindRules();
38
+
39
+ // Verify all four better-tailwindcss rules remain enabled.
40
+ expect(result['better-tailwindcss/enforce-consistent-class-order']).toBe('warn');
41
+ expect(result['better-tailwindcss/enforce-consistent-line-wrapping']).toEqual(['warn', { printWidth: 120 }]);
42
+ expect(result['better-tailwindcss/enforce-canonical-classes']).toBe('warn');
43
+ expect(result['better-tailwindcss/no-unnecessary-whitespace']).toBe('warn');
44
+ });
45
+
46
+ it('falls back to full rules when prettier config resolution fails', async () => {
47
+ // Simulate prettier being unusable during config evaluation.
48
+ mockResolveConfig.mockRejectedValue(new Error('simulated resolution failure'));
49
+
50
+ vi.resetModules();
51
+ const rules = await import('../rules.js');
52
+ const result = rules.tailwindRules();
53
+
54
+ // Verify ordering and wrapping rules stay enabled on failure.
55
+ expect(result['better-tailwindcss/enforce-consistent-class-order']).toBe('warn');
56
+ expect(result['better-tailwindcss/enforce-consistent-line-wrapping']).toEqual(['warn', { printWidth: 120 }]);
57
+ });
58
+
59
+ it('logs an interop notice under DEBUG when the prettier plugin is active', async () => {
60
+ // Enable debug logging and simulate the active Tailwind Prettier plugin.
61
+ process.env.DEBUG = 'eslint';
62
+ mockResolveConfig.mockResolvedValue({ plugins: ['prettier-plugin-tailwindcss'] });
63
+ vi.resetModules();
64
+ const debugCalls = [];
65
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation((...args) => debugCalls.push(args.join(' ')));
66
+ debugSpy.mockClear();
67
+
68
+ // Re-import so top-level detection runs again under this env.
69
+ await import('../rules.js');
70
+
71
+ // Verify that the interop decision is logged.
72
+ expect(debugCalls.some((message) => message.includes('left to Prettier'))).toBe(true);
73
+
74
+ delete process.env.DEBUG;
75
+ });
76
+ });
@@ -127,7 +127,7 @@ const buildConfigObject = ({
127
127
  * @param {object} [config.parserOptions] - Parser options.
128
128
  * @param {object} [config.settings] - Settings object.
129
129
  * @param {object} [config.rules] - Additional rules.
130
- * @param {object} [config.options] - User-provided options. Tailwind rules are enabled by default and can be disabled with `tailwind: false`.
130
+ * @param {object} [config.options] - User-provided options. Tailwind rules are disabled by default and can be enabled with `tailwind: true`.
131
131
  * @param {boolean} [config.typescript] - Enable TypeScript support.
132
132
  *
133
133
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
@@ -144,7 +144,7 @@ export const buildConfig = ({
144
144
  options = {},
145
145
  typescript = false,
146
146
  }) => {
147
- const opts = { prettier: true, importOrder: true, jsdoc: true, tailwind: true, ...options };
147
+ const opts = { prettier: true, importOrder: true, jsdoc: true, tailwind: false, ...options };
148
148
 
149
149
  const conditionalPluginList = getEnabledPlugins(conditionalPlugins, opts);
150
150
 
@@ -40,6 +40,31 @@ try {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Indicates whether the consumer Prettier config enables the Tailwind
45
+ * Prettier plugin. When present, class ordering and line wrapping belong to
46
+ * the formatter, and enforcing them here too would cause circular fixes.
47
+ *
48
+ * @type {boolean}
49
+ */
50
+ let prettierTailwindPluginDetected = false;
51
+ try {
52
+ const prettierModule = await import('prettier');
53
+ const resolvedPrettierConfig = await prettierModule.resolveConfig(path.join(process.cwd(), 'package.json'));
54
+ prettierTailwindPluginDetected = Boolean(
55
+ Array.isArray(resolvedPrettierConfig?.plugins)
56
+ && resolvedPrettierConfig.plugins.includes('prettier-plugin-tailwindcss')
57
+ );
58
+ if (prettierTailwindPluginDetected && (process.env.DEBUG?.includes('eslint') || process.env.DEBUG === '*')) {
59
+ console.debug(
60
+ '[ESLint Config] Detected "prettier-plugin-tailwindcss" in the consumer Prettier config. '
61
+ + 'Class ordering and line wrapping rules are left to Prettier.'
62
+ );
63
+ }
64
+ } catch {
65
+ // Detection is best-effort; without a resolvable config, lint rules stay active.
66
+ }
67
+
43
68
  /**
44
69
  * Candidate paths probed (relative to the consumer project root) for the main
45
70
  * Tailwind CSS v4 entry stylesheet used for theme resolution.
@@ -96,12 +121,16 @@ export const tailwindSettings = () => {
96
121
  /**
97
122
  * Creates Tailwind class rules covering canonical class names, whitespace,
98
123
  * ordering, line wrapping, and arbitrary value scale replacement. Rules rely
99
- * on plugins registered separately via `getTailwindCentralPlugins`.
124
+ * on plugins registered separately via `getTailwindCentralPlugins`. When the
125
+ * consumer enables `prettier-plugin-tailwindcss`, ordering and wrapping are
126
+ * omitted so the formatter owns those concerns without circular fixes.
127
+ *
128
+ * @param {boolean} [tailwind] - Enable Tailwind CSS class rules.
100
129
  *
101
130
  * @returns {object} Tailwind-related ESLint rules (may be partial when plugins are missing).
102
131
  */
103
- export const tailwindRules = () => {
104
- if (!betterTailwindcssPlugin) {
132
+ export const tailwindRules = (tailwind = true) => {
133
+ if (!tailwind || !betterTailwindcssPlugin) {
105
134
  return {};
106
135
  }
107
136
 
@@ -112,11 +141,15 @@ export const tailwindRules = () => {
112
141
  // Collapse redundant whitespace inside class strings.
113
142
  'better-tailwindcss/no-unnecessary-whitespace': 'warn',
114
143
 
115
- // Enforce consistent class ordering inside class strings.
116
- 'better-tailwindcss/enforce-consistent-class-order': 'warn',
144
+ // Enforce consistent class ordering inside class strings. Skipped when
145
+ // prettier-plugin-tailwindcss already sorts classes during formatting.
146
+ ...(prettierTailwindPluginDetected ? {} : { 'better-tailwindcss/enforce-consistent-class-order': 'warn' }),
117
147
 
118
- // Wrap long class strings into readable multi-line groups.
119
- 'better-tailwindcss/enforce-consistent-line-wrapping': ['warn', { printWidth: PRETTIER.BASE.printWidth }],
148
+ // Wrap long class strings into readable multi-line groups. Skipped when
149
+ // prettier-plugin-tailwindcss already wraps classes during formatting.
150
+ ...(prettierTailwindPluginDetected
151
+ ? {}
152
+ : { 'better-tailwindcss/enforce-consistent-line-wrapping': ['warn', { printWidth: PRETTIER.BASE.printWidth }] }),
120
153
 
121
154
  // Replace arbitrary values with matching theme scale utilities (e.g. `p-[16px]` to `p-4`).
122
155
  ...(tailwindCssEslintPlugin && { 'tailwindcss/no-unnecessary-arbitrary-value': 'warn' }),
@@ -273,7 +306,7 @@ const importOrderRules = (importOrder = true) =>
273
306
  * @returns {object} ESLint rules object.
274
307
  */
275
308
  export const commonRules = (options = {}) => {
276
- const { typescript = true, importOrder = true, prettier = true, jsdoc = true, tailwind = true } = options;
309
+ const { typescript = true, importOrder = true, prettier = true, jsdoc = true, tailwind = false } = options;
277
310
 
278
311
  return {
279
312
  // ---- TypeScript Rules ----
@@ -289,6 +322,6 @@ export const commonRules = (options = {}) => {
289
322
  ...jsdocRules(jsdoc),
290
323
 
291
324
  // ---- Tailwind Integration ----
292
- ...(tailwind && tailwindRules()),
325
+ ...tailwindRules(tailwind),
293
326
  };
294
327
  };
@@ -39,7 +39,7 @@ const prepareNextConfig = (configs) => configs.filter((c) => c.name !== 'next/ty
39
39
  * @param {boolean} [options.a11y] - Enable accessibility rules.
40
40
  * @param {boolean} [options.importOrder] - Enable import order rules.
41
41
  * @param {boolean} [options.jsdoc] - Enable JSDoc rules for public/exported APIs.
42
- * @param {boolean} [options.tailwind] - Enable Tailwind CSS class rules (default: true).
42
+ * @param {boolean} [options.tailwind] - Enable Tailwind CSS class rules (default: false).
43
43
  * @param {string[]} [options.ignores] - Additional ignore patterns.
44
44
  * @param {object} [options.rules] - Additional or overridden rules.
45
45
  * @param {object} [options.settings] - Additional settings.
@@ -52,7 +52,7 @@ const prepareNextConfig = (configs) => configs.filter((c) => c.name !== 'next/ty
52
52
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
53
53
  */
54
54
  export const createConfig = (options = {}) => {
55
- const { prettier = true, react = true, a11y = true, importOrder = true, jsdoc = true, tailwind = true } = options;
55
+ const { prettier = true, react = true, a11y = true, importOrder = true, jsdoc = true, tailwind = false } = options;
56
56
 
57
57
  return buildConfig({
58
58
  files: [...files.withTs, ...(options.files || [])],
@@ -26,7 +26,7 @@ import { buildConfig, files, getTailwindCentralPlugins } from './lib/index.js';
26
26
  * @param {boolean} [options.a11y] - Enable accessibility rules.
27
27
  * @param {boolean} [options.importOrder] - Enable import order rules.
28
28
  * @param {boolean} [options.jsdoc] - Enable JSDoc rules for public/exported APIs.
29
- * @param {boolean} [options.tailwind] - Enable Tailwind CSS class rules (default: true).
29
+ * @param {boolean} [options.tailwind] - Enable Tailwind CSS class rules (default: false).
30
30
  * @param {string[]} [options.ignores] - Additional ignore patterns.
31
31
  * @param {object} [options.rules] - Additional or overridden rules.
32
32
  * @param {object} [options.settings] - Additional settings.
@@ -39,7 +39,7 @@ import { buildConfig, files, getTailwindCentralPlugins } from './lib/index.js';
39
39
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
40
40
  */
41
41
  export const createConfig = (options = {}) => {
42
- const { prettier = true, a11y = true, importOrder = true, jsdoc = true, tailwind = true } = options;
42
+ const { prettier = true, a11y = true, importOrder = true, jsdoc = true, tailwind = false } = options;
43
43
 
44
44
  return buildConfig({
45
45
  files: [...files.withTs, ...(options.files || [])],