@vijayhardaha/dev-config 2.3.1 → 2.4.1

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.3.1",
3
+ "version": "2.4.1",
4
4
  "description": "Reusable development configurations for Next.js + TypeScript projects",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -66,31 +66,34 @@
66
66
  "typescript"
67
67
  ],
68
68
  "devDependencies": {
69
- "@eslint/js": "^10.0.1",
70
- "@commitlint/cli": "^21.2.1",
71
- "@commitlint/config-conventional": "^21.2.0",
69
+ "@commitlint/cli": "^21.2.2",
70
+ "@commitlint/config-conventional": "^21.2.2",
72
71
  "@commitlint/types": "^21.2.0",
73
- "@typescript-eslint/parser": "^8.66.0",
74
- "@typescript-eslint/eslint-plugin": "^8.66.0",
75
- "@vitest/coverage-v8": "^4.1.10",
76
- "eslint": "^10.8.0",
77
- "eslint-config-next": "^16.3.0",
72
+ "@eslint/js": "^10.0.1",
73
+ "@prettier/plugin-xml": "^3.4.2",
74
+ "@typescript-eslint/eslint-plugin": "^8.67.0",
75
+ "@typescript-eslint/parser": "^8.67.0",
76
+ "@vitest/coverage-v8": "^4.1.11",
77
+ "eslint": "^10.9.0",
78
+ "eslint-config-next": "^16.3.2",
78
79
  "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-better-tailwindcss": "^4.7.0",
79
81
  "eslint-plugin-import-x": "^4.17.1",
80
82
  "eslint-plugin-jsdoc": "^63.3.3",
81
83
  "eslint-plugin-jsx-a11y": "^6.10.2",
82
84
  "eslint-plugin-prettier": "^5.5.6",
83
85
  "eslint-plugin-react": "^7.37.5",
84
86
  "eslint-plugin-react-hooks": "^7.1.1",
85
- "globals": "^17.9.0",
87
+ "eslint-plugin-tailwindcss": "^4.4.0",
88
+ "globals": "^17.11.0",
86
89
  "husky": "^9.1.7",
87
- "next": "^16.3.0",
90
+ "next": "^16.3.2",
88
91
  "prettier": "^3.9.6",
89
- "@prettier/plugin-xml": "^3.4.2",
90
- "release-it": "^21.0.1",
92
+ "release-it": "^21.0.2",
93
+ "tailwindcss": "^4.3.3",
91
94
  "typescript": "^6.0.3",
92
- "typescript-eslint": "^8.66.0",
93
- "vitest": "^4.1.10"
95
+ "typescript-eslint": "^8.67.0",
96
+ "vitest": "^4.1.11"
94
97
  },
95
98
  "peerDependencies": {
96
99
  "@commitlint/cli": ">=21",
@@ -106,22 +109,24 @@
106
109
  "eslint-config-next": ">=16",
107
110
  "eslint-config-prettier": ">=10",
108
111
  "eslint-import-resolver-typescript": ">=4",
112
+ "eslint-plugin-better-tailwindcss": ">=4",
109
113
  "eslint-plugin-import-x": ">=4",
110
- "eslint-plugin-jsdoc": ">=62",
114
+ "eslint-plugin-jsdoc": ">=63",
111
115
  "eslint-plugin-jsx-a11y": ">=6",
112
116
  "eslint-plugin-prettier": ">=5",
113
117
  "eslint-plugin-react": ">=7",
114
- "eslint-plugin-react-hooks": ">=5",
118
+ "eslint-plugin-react-hooks": ">=7",
119
+ "eslint-plugin-tailwindcss": ">=4",
115
120
  "globals": ">=17",
116
121
  "husky": ">=9",
117
- "next-sitemap": ">=4",
118
122
  "next": ">=16",
123
+ "next-sitemap": ">=4",
119
124
  "prettier": ">=3",
120
125
  "stylelint": ">=17",
121
126
  "stylelint-config-property-sort-order-smacss": ">=11",
122
127
  "stylelint-config-standard-scss": ">=17",
123
128
  "stylelint-order": ">=8",
124
- "typescript": ">=5",
129
+ "typescript": ">=6",
125
130
  "typescript-eslint": ">=8"
126
131
  },
127
132
  "peerDependenciesMeta": {
@@ -149,6 +154,9 @@
149
154
  "eslint-import-resolver-typescript": {
150
155
  "optional": true
151
156
  },
157
+ "eslint-plugin-better-tailwindcss": {
158
+ "optional": true
159
+ },
152
160
  "eslint-plugin-jsx-a11y": {
153
161
  "optional": true
154
162
  },
@@ -158,6 +166,9 @@
158
166
  "eslint-plugin-react-hooks": {
159
167
  "optional": true
160
168
  },
169
+ "eslint-plugin-tailwindcss": {
170
+ "optional": true
171
+ },
161
172
  "husky": {
162
173
  "optional": true
163
174
  },
@@ -28,4 +28,58 @@ describe('eslint/next.js', () => {
28
28
 
29
29
  expect(configObject.files).toContain('custom/**/*.tsx');
30
30
  });
31
+
32
+ // Test that Tailwind plugins are registered on the main config by default.
33
+ it('should register Tailwind plugins by default', async () => {
34
+ const module = await import('../next.js');
35
+ const result = module.createConfig();
36
+ const configObject = result.at(-1);
37
+
38
+ // Verify that the better-tailwindcss plugin is centrally registered.
39
+ expect(configObject.plugins['better-tailwindcss']).toBeDefined();
40
+
41
+ // Verify that the tailwindcss plugin is centrally registered.
42
+ expect(configObject.plugins.tailwindcss).toBeDefined();
43
+
44
+ // Verify that the Tailwind canonicalization rule is enabled.
45
+ expect(configObject.rules['better-tailwindcss/enforce-canonical-classes']).toBe('warn');
46
+
47
+ // Verify that the arbitrary value replacement rule is enabled.
48
+ expect(configObject.rules['tailwindcss/no-unnecessary-arbitrary-value']).toBe('warn');
49
+ });
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('../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
+ // Test that user settings override the generated Tailwind settings.
71
+ it('should let user settings override Tailwind settings', async () => {
72
+ const module = await import('../next.js');
73
+ const result = module.createConfig({
74
+ settings: {
75
+ 'better-tailwindcss': { entryPoint: 'styles/site.css' },
76
+ tailwindcss: { cssConfigPath: 'styles/site.css' },
77
+ },
78
+ });
79
+ const configObject = result.at(-1);
80
+
81
+ // Verify that the user entry point wins over the auto-detected one.
82
+ expect(configObject.settings['better-tailwindcss'].entryPoint).toBe('styles/site.css');
83
+ expect(configObject.settings.tailwindcss.cssConfigPath).toBe('styles/site.css');
84
+ });
31
85
  });
@@ -28,4 +28,42 @@ describe('eslint/react.js', () => {
28
28
 
29
29
  expect(configObject.files).toContain('custom/**/*.tsx');
30
30
  });
31
+
32
+ // Test that Tailwind plugins are registered on the main config by default.
33
+ it('should register Tailwind plugins by default', async () => {
34
+ const module = await import('../react.js');
35
+ const result = module.createConfig();
36
+ const configObject = result.at(-1);
37
+
38
+ // Verify that the better-tailwindcss plugin is centrally registered.
39
+ expect(configObject.plugins['better-tailwindcss']).toBeDefined();
40
+
41
+ // Verify that the tailwindcss plugin is centrally registered.
42
+ expect(configObject.plugins.tailwindcss).toBeDefined();
43
+
44
+ // Verify that the Tailwind canonicalization rule is enabled.
45
+ expect(configObject.rules['better-tailwindcss/enforce-canonical-classes']).toBe('warn');
46
+
47
+ // Verify that the arbitrary value replacement rule is enabled.
48
+ expect(configObject.rules['tailwindcss/no-unnecessary-arbitrary-value']).toBe('warn');
49
+ });
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
+ });
31
69
  });
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { ESLint } from 'eslint';
6
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
7
+
8
+ /**
9
+ * Lints and fixes a TSX snippet against the Next.js config inside the temp
10
+ * fixture project.
11
+ *
12
+ * @param {object} projectDir - Fixture project root.
13
+ * @param {object} module - Imported next.js module.
14
+ * @param {string} code - TSX source code to lint.
15
+ * @param {object} [options] - createConfig options.
16
+ *
17
+ * @returns {Promise<object>} The single lint result for the snippet.
18
+ */
19
+ const lintAndFix = async (projectDir, module, code, options = {}) => {
20
+ const config = module.createConfig({
21
+ settings: {
22
+ 'better-tailwindcss': { entryPoint: 'src/app/globals.css' },
23
+ tailwindcss: { cssConfigPath: 'src/app/globals.css' },
24
+ },
25
+ ...options,
26
+ });
27
+ const eslint = new ESLint({ cwd: projectDir, overrideConfigFile: true, overrideConfig: config, fix: true });
28
+
29
+ const [result] = await eslint.lintText(code, { filePath: path.join(projectDir, 'src/Button.tsx') });
30
+
31
+ return result;
32
+ };
33
+
34
+ // Test suite for Tailwind autofix behavior through the shared config pipeline.
35
+ describe('eslint tailwind autofix', () => {
36
+ let projectDir;
37
+ let nextModule;
38
+
39
+ beforeAll(async () => {
40
+ nextModule = await import('../next.js');
41
+
42
+ // Create an isolated fixture project that mimics a Next.js app layout.
43
+ projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dev-config-tailwind-'));
44
+ fs.writeFileSync(
45
+ path.join(projectDir, 'package.json'),
46
+ JSON.stringify({ name: 'tailwind-fixture', private: true, type: 'module' })
47
+ );
48
+ fs.mkdirSync(path.join(projectDir, 'src/app'), { recursive: true });
49
+ fs.writeFileSync(path.join(projectDir, 'src/app/globals.css'), '@import "tailwindcss";\n');
50
+ fs.symlinkSync(path.resolve(process.cwd(), 'node_modules'), path.join(projectDir, 'node_modules'), 'dir');
51
+ });
52
+
53
+ afterAll(() => {
54
+ fs.rmSync(projectDir, { recursive: true, force: true });
55
+ });
56
+
57
+ // Test that arbitrary values with theme scale equivalents are replaced.
58
+ it('should replace arbitrary values with theme scale utilities', async () => {
59
+ const code = [
60
+ 'const Button = () => (',
61
+ ' <div className="aspect-[3/4] max-w-[280px] p-[16px]">Button</div>',
62
+ ');',
63
+ '',
64
+ ].join('\n');
65
+
66
+ const result = await lintAndFix(projectDir, nextModule, code);
67
+
68
+ expect(result.output).toContain('aspect-3/4');
69
+ expect(result.output).toContain('max-w-70');
70
+ expect(result.output).toContain('p-4');
71
+ }, 60_000);
72
+
73
+ // Test that cn() classes keep their variant prefixes after conversion.
74
+ it('should convert cn() classes while preserving variant prefixes', async () => {
75
+ const code = [
76
+ 'const Button = ({ side }) => (',
77
+ ` <div className={cn("data-[side=bottom]:translate-y-[2.5rem]", "dark:hover:translate-x-[-2.5rem]")}>`,
78
+ ' Button',
79
+ ' </div>',
80
+ ');',
81
+ '',
82
+ ].join('\n');
83
+
84
+ const result = await lintAndFix(projectDir, nextModule, code, { rules: { 'import-x/order': 'off' } });
85
+
86
+ expect(result.output).toContain('data-[side=bottom]:translate-y-10');
87
+ expect(result.output).toContain('dark:hover:-translate-x-10');
88
+ }, 60_000);
89
+
90
+ // Test that long class strings are wrapped into readable multi-line groups.
91
+ it('should wrap long class strings into multi-line groups', async () => {
92
+ const longClasses =
93
+ 'flex flex-col items-start justify-between gap-6 rounded-xl border border-slate-200 bg-white p-8 shadow-sm '
94
+ + 'transition-all hover:-translate-y-1 hover:shadow-lg dark:border-slate-800 dark:bg-slate-900';
95
+ const code = ['const Card = () => (', ` <div className="${longClasses}">Card</div>`, ');', ''].join('\n');
96
+
97
+ const result = await lintAndFix(projectDir, nextModule, code);
98
+
99
+ expect(result.output).toMatch(/className="\n\s+flex/);
100
+ }, 60_000);
101
+
102
+ // Test that disabling the tailwind option leaves class strings untouched.
103
+ it('should not touch class strings when tailwind option is false', async () => {
104
+ const code = [
105
+ 'const Button = () => (',
106
+ ' <div className="aspect-[3/4] max-w-[280px] p-[16px]">Button</div>',
107
+ ');',
108
+ '',
109
+ ].join('\n');
110
+
111
+ const result = await lintAndFix(projectDir, nextModule, code, { tailwind: false });
112
+
113
+ // Verify that no fixes were applied and the arbitrary classes remain.
114
+ expect(result.output).toBeUndefined();
115
+ expect(result.source).toContain('aspect-[3/4]');
116
+
117
+ // Verify that no Tailwind rule contributed messages.
118
+ const tailwindMessages = result.messages.filter(
119
+ (message) => message.ruleId?.startsWith('better-tailwindcss/') || message.ruleId?.startsWith('tailwindcss/')
120
+ );
121
+ expect(tailwindMessages).toHaveLength(0);
122
+ }, 60_000);
123
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // Hide the project .gitignore from the builder so the absent-gitignore
4
+ // fallback branches execute during config assembly.
5
+ vi.mock('node:fs', async (importOriginal) => {
6
+ const actual = await importOriginal();
7
+ return { ...actual, existsSync: (path) => (String(path).endsWith('.gitignore') ? false : actual.existsSync(path)) };
8
+ });
9
+
10
+ describe('eslint/lib/build-config.js branch paths', () => {
11
+ it('omits gitignore config when .gitignore is absent', async () => {
12
+ // Import after the mock is in place.
13
+ const buildConfigModule = await import('../build-config.js');
14
+
15
+ // Build a config without extra inputs.
16
+ const result = buildConfigModule.buildConfig({ files: ['**/*.js'] });
17
+
18
+ // Verify that the config array still ends with the main object.
19
+ expect(Array.isArray(result)).toBe(true);
20
+ expect(result.at(-1).files).toContain('**/*.js');
21
+ });
22
+
23
+ it('merges user globalIgnores with defaults', async () => {
24
+ // Import after the mock is in place.
25
+ const buildConfigModule = await import('../build-config.js');
26
+
27
+ // Build a config with user-provided global ignore patterns.
28
+ const result = buildConfigModule.buildConfig({
29
+ files: ['**/*.js'],
30
+ options: { globalIgnores: ['**/.cache-custom/'] },
31
+ });
32
+
33
+ // Verify that the user pattern lands alongside the defaults.
34
+ const ignoreConfig = result.find((cfg) => Array.isArray(cfg.ignores));
35
+ expect(ignoreConfig.ignores).toContain('**/.cache-custom/');
36
+ expect(ignoreConfig.ignores).toContain('**/node_modules/');
37
+ });
38
+
39
+ it('leaves ignores off the main object when none provided', async () => {
40
+ // Import after the mock is in place.
41
+ const buildConfigModule = await import('../build-config.js');
42
+
43
+ // Build a config without ignore overrides.
44
+ const result = buildConfigModule.buildConfig({ files: ['**/*.js'] });
45
+
46
+ // Verify that the main object carries no ignores property.
47
+ expect(result.at(-1).ignores).toBeUndefined();
48
+ });
49
+
50
+ it('spreads provided ignores onto the main object', async () => {
51
+ // Import after the mock is in place.
52
+ const buildConfigModule = await import('../build-config.js');
53
+
54
+ // Build a config with user-provided ignore patterns on the main object.
55
+ const result = buildConfigModule.buildConfig({ files: ['**/*.js'], options: { ignores: ['**/generated/'] } });
56
+
57
+ // Verify that only the provided patterns land on the main object.
58
+ expect(result.at(-1).ignores).toEqual(['**/generated/']);
59
+ });
60
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // Simulate the optional TypeScript import resolver being absent so the
4
+ // top-level fallback branch and its debug logging execute at load time.
5
+ vi.mock('eslint-import-resolver-typescript', () => {
6
+ throw new Error('simulated missing dependency');
7
+ });
8
+
9
+ describe('eslint/lib/build-config.js with missing optional resolver', () => {
10
+ it('falls back gracefully when eslint-import-resolver-typescript is missing', async () => {
11
+ // Enable debug logging before the module is evaluated so the fallback
12
+ // branch emits its install hint.
13
+ process.env.DEBUG = '*';
14
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
15
+
16
+ // Import after the mock and DEBUG are in place.
17
+ const buildConfigModule = await import('../build-config.js');
18
+
19
+ // Verify that config building still works without the optional resolver.
20
+ expect(typeof buildConfigModule.buildConfig).toBe('function');
21
+ expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('eslint-import-resolver-typescript'));
22
+
23
+ // Verify that import resolution falls back to the node resolver alone.
24
+ const result = buildConfigModule.buildConfig({ files: ['**/*.js'] });
25
+ expect(result.at(-1).settings['import-x/resolver-next']).toHaveLength(1);
26
+
27
+ delete process.env.DEBUG;
28
+ });
29
+
30
+ it('stays silent without DEBUG when the resolver is missing', async () => {
31
+ // Drop DEBUG so the fallback logging condition evaluates to false.
32
+ delete process.env.DEBUG;
33
+ vi.resetModules();
34
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
35
+ debugSpy.mockClear();
36
+
37
+ // Re-import so top-level fallback branches run again under this env.
38
+ const buildConfigModule = await import('../build-config.js');
39
+
40
+ // Verify that no install hint is logged without DEBUG.
41
+ expect(debugSpy).not.toHaveBeenCalled();
42
+ expect(typeof buildConfigModule.buildConfig).toBe('function');
43
+ });
44
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // Simulate both Tailwind plugins being absent so the lazy import catch
4
+ // branches and their debug logging execute during module evaluation.
5
+ vi.mock('eslint-plugin-better-tailwindcss', () => {
6
+ throw new Error('simulated missing dependency');
7
+ });
8
+
9
+ vi.mock('eslint-plugin-tailwindcss', () => {
10
+ throw new Error('simulated missing dependency');
11
+ });
12
+
13
+ // Report every probed entry point as existing so tailwindSettings takes the
14
+ // found-entry-point branch without touching the filesystem.
15
+ vi.mock('node:fs', async (importOriginal) => {
16
+ const actual = await importOriginal();
17
+ return { ...actual, existsSync: () => true };
18
+ });
19
+
20
+ describe('eslint/lib/rules.js with missing optional plugins', () => {
21
+ const debugCalls = [];
22
+ let rules;
23
+
24
+ it('registers no central plugins, emits no rules, and logs install hints', async () => {
25
+ // Enable debug logging before the module is evaluated so each missing
26
+ // plugin emits its install hint.
27
+ process.env.DEBUG = 'eslint';
28
+ vi.spyOn(console, 'debug').mockImplementation((...args) => debugCalls.push(args.join(' ')));
29
+
30
+ // Import after mocks and DEBUG are in place so top-level awaits hit the
31
+ // catch branches.
32
+ rules = await import('../rules.js');
33
+
34
+ // Verify that no Tailwind plugins are registered when both are missing.
35
+ expect(rules.getTailwindCentralPlugins()).toEqual({});
36
+
37
+ // Verify that no Tailwind rules are emitted without better-tailwindcss.
38
+ expect(rules.tailwindRules()).toEqual({});
39
+
40
+ // Verify that install hints are logged for both missing plugins.
41
+ expect(debugCalls.some((message) => message.includes('eslint-plugin-better-tailwindcss'))).toBe(true);
42
+ expect(debugCalls.some((message) => message.includes('eslint-plugin-tailwindcss'))).toBe(true);
43
+
44
+ delete process.env.DEBUG;
45
+ });
46
+
47
+ it('resolves settings from the first candidate entry point', async () => {
48
+ // Reuse the module loaded with plugins absent; entry point probing is
49
+ // unaffected by their absence.
50
+ rules ??= await import('../rules.js');
51
+
52
+ // Verify that settings point at the first probed candidate path.
53
+ expect(rules.tailwindSettings()).toEqual({
54
+ tailwindcss: { cssConfigPath: 'src/app/globals.css' },
55
+ 'better-tailwindcss': { entryPoint: 'src/app/globals.css' },
56
+ });
57
+ });
58
+
59
+ it('skips debug logging when DEBUG is unset', async () => {
60
+ // Drop DEBUG so both logging conditions evaluate to false.
61
+ delete process.env.DEBUG;
62
+ vi.resetModules();
63
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
64
+ debugSpy.mockClear();
65
+
66
+ // Re-import so top-level catch branches run again under this env.
67
+ await import('../rules.js');
68
+
69
+ // Verify that no install hints are logged without DEBUG.
70
+ expect(debugSpy).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('logs install hints under wildcard DEBUG', async () => {
74
+ // Wildcard DEBUG skips the substring check and enables logging.
75
+ process.env.DEBUG = '*';
76
+ vi.resetModules();
77
+ const debugCalls = [];
78
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation((...args) => debugCalls.push(args.join(' ')));
79
+ debugSpy.mockClear();
80
+
81
+ // Re-import so top-level catch branches run again under this env.
82
+ await import('../rules.js');
83
+
84
+ // Verify that install hints are logged for both missing plugins.
85
+ expect(debugCalls.some((message) => message.includes('eslint-plugin-better-tailwindcss'))).toBe(true);
86
+ expect(debugCalls.some((message) => message.includes('eslint-plugin-tailwindcss'))).toBe(true);
87
+ });
88
+ });
@@ -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
+ });
@@ -60,4 +60,43 @@ describe('eslint/lib/rules.js', () => {
60
60
  // Verify that the jsdoc/require-jsdoc rule is present in the config.
61
61
  expect(result['jsdoc/require-jsdoc']).toBeDefined();
62
62
  });
63
+
64
+ // Test that Tailwind rules are included when tailwind option is true.
65
+ it('should include Tailwind rules when tailwind is true', () => {
66
+ // Call commonRules with tailwind option enabled.
67
+ const result = commonRules({ tailwind: true });
68
+
69
+ // Verify that the Tailwind canonicalization rule is present in the config.
70
+ expect(result['better-tailwindcss/enforce-canonical-classes']).toBeDefined();
71
+
72
+ // Verify that the Tailwind whitespace cleanup rule is present in the config.
73
+ expect(result['better-tailwindcss/no-unnecessary-whitespace']).toBeDefined();
74
+
75
+ // Verify that the Tailwind ordering rule is present in the config.
76
+ expect(result['better-tailwindcss/enforce-consistent-class-order']).toBeDefined();
77
+
78
+ // Verify that the Tailwind arbitrary value replacement rule is present in the config.
79
+ expect(result['tailwindcss/no-unnecessary-arbitrary-value']).toBeDefined();
80
+ });
81
+
82
+ // Test that line wrapping aligns with the shared Prettier print width.
83
+ it('should configure line wrapping with the shared Prettier print width', () => {
84
+ // Call commonRules with tailwind option enabled.
85
+ const result = commonRules({ tailwind: true });
86
+
87
+ // Verify that the wrapping rule uses the shared print width constant.
88
+ expect(result['better-tailwindcss/enforce-consistent-line-wrapping']).toEqual(['warn', { printWidth: 120 }]);
89
+ });
90
+
91
+ // Test that Tailwind rules are excluded when tailwind option is false.
92
+ it('should exclude Tailwind rules when tailwind is false', () => {
93
+ // Call commonRules with tailwind option disabled.
94
+ const result = commonRules({ tailwind: false });
95
+
96
+ // Verify that no better-tailwindcss rules are present in the config.
97
+ expect(Object.keys(result).filter((rule) => rule.startsWith('better-tailwindcss/'))).toHaveLength(0);
98
+
99
+ // Verify that no tailwindcss rules are present in the config.
100
+ expect(Object.keys(result).filter((rule) => rule.startsWith('tailwindcss/'))).toHaveLength(0);
101
+ });
63
102
  });
@@ -10,7 +10,7 @@ import prettierRecommended from 'eslint-plugin-prettier/recommended';
10
10
  import { globalIgnores } from './ignores.js';
11
11
  import { commonLanguageOptions } from './language-options.js';
12
12
  import { getEnabledPlugins, flattenPlugins, fixupPlugins, stripPlugins, stripParser } from './plugin-helpers/index.js';
13
- import { commonRules } from './rules.js';
13
+ import { commonRules, tailwindSettings } from './rules.js';
14
14
  import { commonParser } from './setup.js';
15
15
 
16
16
  let createTypeScriptImportResolver;
@@ -66,6 +66,16 @@ const buildConfigObject = ({
66
66
  }) => {
67
67
  const { ignores, rules, settings, extend } = opts;
68
68
 
69
+ // Resolve feature flags up front so downstream wiring stays readable.
70
+ const importOrderEnabled = Boolean(opts.importOrder);
71
+ const jsdocEnabled = Boolean(opts.jsdoc);
72
+ const prettierEnabled = Boolean(opts.prettier);
73
+ const typescriptEnabled = Boolean(typescript);
74
+
75
+ // Tailwind is opt-out (enabled by default) but only applies to configs that
76
+ // register the Tailwind plugins centrally, so JS/TS-only configs stay clean.
77
+ const tailwindEnabled = Boolean(opts.tailwind) && 'better-tailwindcss' in centralPlugins;
78
+
69
79
  return {
70
80
  files: [...filePatterns],
71
81
  ...(ignores && { ignores }),
@@ -74,23 +84,30 @@ const buildConfigObject = ({
74
84
  ),
75
85
  languageOptions: {
76
86
  ...commonLanguageOptions,
77
- ...(typescript && commonParser),
87
+ ...(typescriptEnabled && commonParser),
78
88
  ...extraLanguageOptions,
79
- ...(typescript && { parserOptions: { tsconfigRootDir: process.cwd(), ...parserOptions } }),
89
+ ...(typescriptEnabled && { parserOptions: { tsconfigRootDir: process.cwd(), ...parserOptions } }),
80
90
  },
81
91
  settings: {
82
- ...(opts.importOrder && {
92
+ ...(importOrderEnabled && {
83
93
  'import-x/resolver-next': [
84
94
  createNodeResolver(),
85
95
  ...(createTypeScriptImportResolver ? [createTypeScriptImportResolver()] : []),
86
96
  ],
87
97
  }),
88
- ...(opts.jsdoc && { jsdoc: { mode: 'typescript' } }),
98
+ ...(jsdocEnabled && { jsdoc: { mode: 'typescript' } }),
99
+ ...(tailwindEnabled && tailwindSettings()),
89
100
  ...extraSettings,
90
101
  ...settings,
91
102
  },
92
103
  rules: {
93
- ...commonRules({ prettier: opts.prettier, importOrder: opts.importOrder, typescript, jsdoc: opts.jsdoc }),
104
+ ...commonRules({
105
+ prettier: prettierEnabled,
106
+ importOrder: importOrderEnabled,
107
+ typescript: typescriptEnabled,
108
+ jsdoc: jsdocEnabled,
109
+ tailwind: tailwindEnabled,
110
+ }),
94
111
  ...extraRules,
95
112
  ...rules,
96
113
  },
@@ -110,7 +127,7 @@ const buildConfigObject = ({
110
127
  * @param {object} [config.parserOptions] - Parser options.
111
128
  * @param {object} [config.settings] - Settings object.
112
129
  * @param {object} [config.rules] - Additional rules.
113
- * @param {object} [config.options] - User-provided options.
130
+ * @param {object} [config.options] - User-provided options. Tailwind rules are enabled by default and can be disabled with `tailwind: false`.
114
131
  * @param {boolean} [config.typescript] - Enable TypeScript support.
115
132
  *
116
133
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
@@ -127,7 +144,7 @@ export const buildConfig = ({
127
144
  options = {},
128
145
  typescript = false,
129
146
  }) => {
130
- const opts = { prettier: true, importOrder: true, jsdoc: true, ...options };
147
+ const opts = { prettier: true, importOrder: true, jsdoc: true, tailwind: true, ...options };
131
148
 
132
149
  const conditionalPluginList = getEnabledPlugins(conditionalPlugins, opts);
133
150
 
@@ -1,2 +1,3 @@
1
1
  export { files } from './files.js';
2
2
  export { buildConfig } from './build-config.js';
3
+ export { getTailwindCentralPlugins } from './rules.js';
@@ -1,5 +1,159 @@
1
1
  // ---- JSDoc Rules: enforce documentation on public/exported APIs ----
2
2
 
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ import { PRETTIER } from '../../config-constants.js';
7
+
8
+ // ---- Optional Tailwind Plugins ----------------------------------------------------------
9
+ // Both plugins are optional peers. They are loaded lazily so projects without
10
+ // Tailwind tooling can still use this package. When a plugin is missing, its
11
+ // rules are omitted instead of failing config resolution.
12
+
13
+ let betterTailwindcssPlugin = null;
14
+ try {
15
+ betterTailwindcssPlugin = (await import('eslint-plugin-better-tailwindcss')).default;
16
+ } catch {
17
+ betterTailwindcssPlugin = null;
18
+ // Debug logging for optional dependency failure
19
+ if (process.env.DEBUG?.includes('eslint') || process.env.DEBUG === '*') {
20
+ console.debug(
21
+ '[ESLint Config] Optional dependency "eslint-plugin-better-tailwindcss" not found. '
22
+ + 'Tailwind class wrapping, ordering, and canonicalization rules will be disabled. '
23
+ + 'Install it with: npm install --save-dev eslint-plugin-better-tailwindcss'
24
+ );
25
+ }
26
+ }
27
+
28
+ let tailwindCssEslintPlugin = null;
29
+ try {
30
+ tailwindCssEslintPlugin = (await import('eslint-plugin-tailwindcss')).default;
31
+ } catch {
32
+ tailwindCssEslintPlugin = null;
33
+ // Debug logging for optional dependency failure
34
+ if (process.env.DEBUG?.includes('eslint') || process.env.DEBUG === '*') {
35
+ console.debug(
36
+ '[ESLint Config] Optional dependency "eslint-plugin-tailwindcss" not found. '
37
+ + 'Arbitrary value scale replacement will be disabled. '
38
+ + 'Install it with: npm install --save-dev eslint-plugin-tailwindcss'
39
+ );
40
+ }
41
+ }
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
+
68
+ /**
69
+ * Candidate paths probed (relative to the consumer project root) for the main
70
+ * Tailwind CSS v4 entry stylesheet used for theme resolution.
71
+ *
72
+ * @type {string[]}
73
+ */
74
+ const TAILWIND_ENTRY_POINT_CANDIDATES = [
75
+ 'src/app/globals.css',
76
+ 'app/globals.css',
77
+ 'src/styles/globals.css',
78
+ 'styles/globals.css',
79
+ 'src/input.css',
80
+ 'input.css',
81
+ ];
82
+
83
+ /**
84
+ * Returns centrally-registered Tailwind plugins that are installed in the
85
+ * consuming project. Missing plugins are omitted so ESLint does not fail on
86
+ * unresolved plugin definitions.
87
+ *
88
+ * @returns {object} Map of plugin names to plugin objects (may be empty).
89
+ */
90
+ export const getTailwindCentralPlugins = () => ({
91
+ ...(betterTailwindcssPlugin && { 'better-tailwindcss': betterTailwindcssPlugin }),
92
+ ...(tailwindCssEslintPlugin && { tailwindcss: tailwindCssEslintPlugin }),
93
+ });
94
+
95
+ /**
96
+ * Resolves the Tailwind CSS entry stylesheet used for theme resolution by
97
+ * probing known locations relative to the project root.
98
+ *
99
+ * @returns {string|null} Relative entry point path, or null when none exists.
100
+ */
101
+ export const resolveTailwindEntryPoint = () =>
102
+ TAILWIND_ENTRY_POINT_CANDIDATES.find((candidate) => existsSync(path.resolve(process.cwd(), candidate))) ?? null;
103
+
104
+ /**
105
+ * Creates shared settings consumed by the Tailwind ESLint plugins. Returns an
106
+ * empty object when no entry stylesheet is found so plugins fall back to their
107
+ * own discovery. Users can override any value through `options.settings`.
108
+ *
109
+ * @returns {object} Tailwind-related ESLint shared settings.
110
+ */
111
+ export const tailwindSettings = () => {
112
+ const entryPoint = resolveTailwindEntryPoint();
113
+
114
+ if (!entryPoint) {
115
+ return {};
116
+ }
117
+
118
+ return { tailwindcss: { cssConfigPath: entryPoint }, 'better-tailwindcss': { entryPoint } };
119
+ };
120
+
121
+ /**
122
+ * Creates Tailwind class rules covering canonical class names, whitespace,
123
+ * ordering, line wrapping, and arbitrary value scale replacement. Rules rely
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
+ * @returns {object} Tailwind-related ESLint rules (may be partial when plugins are missing).
129
+ */
130
+ export const tailwindRules = () => {
131
+ if (!betterTailwindcssPlugin) {
132
+ return {};
133
+ }
134
+
135
+ return {
136
+ // Convert classes to their canonical form (e.g. `aspect-[3/4]` to `aspect-3/4`).
137
+ 'better-tailwindcss/enforce-canonical-classes': 'warn',
138
+
139
+ // Collapse redundant whitespace inside class strings.
140
+ 'better-tailwindcss/no-unnecessary-whitespace': 'warn',
141
+
142
+ // Enforce consistent class ordering inside class strings. Skipped when
143
+ // prettier-plugin-tailwindcss already sorts classes during formatting.
144
+ ...(prettierTailwindPluginDetected ? {} : { 'better-tailwindcss/enforce-consistent-class-order': 'warn' }),
145
+
146
+ // Wrap long class strings into readable multi-line groups. Skipped when
147
+ // prettier-plugin-tailwindcss already wraps classes during formatting.
148
+ ...(prettierTailwindPluginDetected
149
+ ? {}
150
+ : { 'better-tailwindcss/enforce-consistent-line-wrapping': ['warn', { printWidth: PRETTIER.BASE.printWidth }] }),
151
+
152
+ // Replace arbitrary values with matching theme scale utilities (e.g. `p-[16px]` to `p-4`).
153
+ ...(tailwindCssEslintPlugin && { 'tailwindcss/no-unnecessary-arbitrary-value': 'warn' }),
154
+ };
155
+ };
156
+
3
157
  /**
4
158
  * JSDoc rules that enforce documentation presence on public/exported APIs.
5
159
  * Covers `@param`, `@returns`, `@throws`, `@description`, and `@property-description`.
@@ -145,11 +299,12 @@ const importOrderRules = (importOrder = true) =>
145
299
  * @param {boolean} [options.importOrder] - Enable import order rules.
146
300
  * @param {boolean} [options.prettier] - Enable Prettier integration.
147
301
  * @param {boolean} [options.jsdoc] - Enable JSDoc rules for public/exported APIs.
302
+ * @param {boolean} [options.tailwind] - Enable Tailwind CSS class rules.
148
303
  *
149
304
  * @returns {object} ESLint rules object.
150
305
  */
151
306
  export const commonRules = (options = {}) => {
152
- const { typescript = true, importOrder = true, prettier = true, jsdoc = true } = options;
307
+ const { typescript = true, importOrder = true, prettier = true, jsdoc = true, tailwind = true } = options;
153
308
 
154
309
  return {
155
310
  // ---- TypeScript Rules ----
@@ -163,5 +318,8 @@ export const commonRules = (options = {}) => {
163
318
 
164
319
  // ---- JSDoc Integration ----
165
320
  ...jsdocRules(jsdoc),
321
+
322
+ // ---- Tailwind Integration ----
323
+ ...(tailwind && tailwindRules()),
166
324
  };
167
325
  };
@@ -14,7 +14,7 @@ import reactPlugin from 'eslint-plugin-react';
14
14
  import reactHooks from 'eslint-plugin-react-hooks';
15
15
  import tsEslint from 'typescript-eslint';
16
16
 
17
- import { buildConfig, files } from './lib/index.js';
17
+ import { buildConfig, files, getTailwindCentralPlugins } from './lib/index.js';
18
18
 
19
19
  /**
20
20
  * Removes the next/typescript item from the core-web-vitals config array.
@@ -39,6 +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
43
  * @param {string[]} [options.ignores] - Additional ignore patterns.
43
44
  * @param {object} [options.rules] - Additional or overridden rules.
44
45
  * @param {object} [options.settings] - Additional settings.
@@ -51,12 +52,17 @@ const prepareNextConfig = (configs) => configs.filter((c) => c.name !== 'next/ty
51
52
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
52
53
  */
53
54
  export const createConfig = (options = {}) => {
54
- const { prettier = true, react = true, a11y = true, importOrder = true, jsdoc = true } = options;
55
+ const { prettier = true, react = true, a11y = true, importOrder = true, jsdoc = true, tailwind = true } = options;
55
56
 
56
57
  return buildConfig({
57
58
  files: [...files.withTs, ...(options.files || [])],
58
59
  builtinPlugins: [...prepareNextConfig(nextCoreWebVitals)],
59
- centralPlugins: { react: reactPlugin, 'react-hooks': reactHooks, '@typescript-eslint': tsEslint.plugin },
60
+ centralPlugins: {
61
+ react: reactPlugin,
62
+ 'react-hooks': reactHooks,
63
+ '@typescript-eslint': tsEslint.plugin,
64
+ ...(tailwind && getTailwindCentralPlugins()),
65
+ },
60
66
  parserOptions: { ecmaFeatures: { jsx: true } },
61
67
  settings: { react: { version: 'detect' } },
62
68
  rules: {
@@ -66,7 +72,7 @@ export const createConfig = (options = {}) => {
66
72
  }),
67
73
  },
68
74
  typescript: true,
69
- options: { ...options, prettier, react, a11y, importOrder, jsdoc },
75
+ options: { ...options, prettier, react, a11y, importOrder, jsdoc, tailwind },
70
76
  });
71
77
  };
72
78
 
@@ -15,7 +15,7 @@ import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
15
15
  import reactHooks from 'eslint-plugin-react-hooks';
16
16
  import tsEslint from 'typescript-eslint';
17
17
 
18
- import { buildConfig, files } from './lib/index.js';
18
+ import { buildConfig, files, getTailwindCentralPlugins } from './lib/index.js';
19
19
 
20
20
  /**
21
21
  * Creates an ESLint configuration object for React projects with TypeScript
@@ -26,6 +26,7 @@ import { buildConfig, files } 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
30
  * @param {string[]} [options.ignores] - Additional ignore patterns.
30
31
  * @param {object} [options.rules] - Additional or overridden rules.
31
32
  * @param {object} [options.settings] - Additional settings.
@@ -38,7 +39,7 @@ import { buildConfig, files } from './lib/index.js';
38
39
  * @returns {import('eslint').Linter.Config[]} ESLint configuration array.
39
40
  */
40
41
  export const createConfig = (options = {}) => {
41
- const { prettier = true, a11y = true, importOrder = true, jsdoc = true } = options;
42
+ const { prettier = true, a11y = true, importOrder = true, jsdoc = true, tailwind = true } = options;
42
43
 
43
44
  return buildConfig({
44
45
  files: [...files.withTs, ...(options.files || [])],
@@ -47,7 +48,12 @@ export const createConfig = (options = {}) => {
47
48
  { ...reactHooks.configs.flat.recommended, files: [...files.withTs, ...(options.files || [])] },
48
49
  ...tsEslint.configs.recommended,
49
50
  ],
50
- centralPlugins: { react: reactPlugin, 'react-hooks': reactHooks, '@typescript-eslint': tsEslint.plugin },
51
+ centralPlugins: {
52
+ react: reactPlugin,
53
+ 'react-hooks': reactHooks,
54
+ '@typescript-eslint': tsEslint.plugin,
55
+ ...(tailwind && getTailwindCentralPlugins()),
56
+ },
51
57
  conditionalPlugins: {
52
58
  a11y: { ...jsxA11y.flatConfigs.recommended, files: [...files.withTs, ...(options.files || [])] },
53
59
  },
@@ -55,7 +61,7 @@ export const createConfig = (options = {}) => {
55
61
  settings: { react: { version: 'detect' } },
56
62
  rules: { 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': ['error', { ignore: ['jsx', 'global'] }] },
57
63
  typescript: true,
58
- options: { ...options, prettier, a11y, importOrder, jsdoc },
64
+ options: { ...options, prettier, a11y, importOrder, jsdoc, tailwind },
59
65
  });
60
66
  };
61
67
 
@@ -32,6 +32,11 @@ describe('Config Utils', () => {
32
32
  const result = mergeDeep({ arr: [1, 2] }, { arr: [3] });
33
33
  expect(result).toEqual({ arr: [3] });
34
34
  });
35
+
36
+ it('should skip non-object sources and initialize missing nested keys', () => {
37
+ const result = mergeDeep({ a: 1 }, null, { b: { c: 2 } });
38
+ expect(result).toEqual({ a: 1, b: { c: 2 } });
39
+ });
35
40
  });
36
41
 
37
42
  describe('filterObjectEntries', () => {
@@ -82,4 +82,24 @@ describe('next-sitemap/index.js', () => {
82
82
  expect(transformed).toContain('User-agent: *');
83
83
  expect(transformed).toContain('Allow: /');
84
84
  });
85
+
86
+ // Test that transform stamps each sitemap entry with changefreq, priority, and lastmod.
87
+ it('should return loc with metadata in transform', async () => {
88
+ // Dynamically import the module to test its function.
89
+ const module = await import('../index.js');
90
+
91
+ // Call createSitemapConfig to get default config.
92
+ const config = module.createSitemapConfig({ siteUrl: 'https://example.com' });
93
+
94
+ // Call the transform function with mock data.
95
+ const transformed = await config.transform(config, '/about/');
96
+
97
+ // Verify that the entry location is preserved and defaults are stamped.
98
+ expect(transformed.loc).toBe('/about/');
99
+ expect(transformed.changefreq).toBe(config.changefreq);
100
+ expect(transformed.priority).toBe(config.priority);
101
+
102
+ // Verify that lastmod uses ISO format without milliseconds.
103
+ expect(transformed.lastmod).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
104
+ });
85
105
  });