tailwind-canonical 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +91 -18
  2. package/dist/cli/cli.d.ts +0 -1
  3. package/dist/cli/cli.js +111 -87
  4. package/dist/cli/index.d.ts +0 -1
  5. package/dist/cli/index.js +0 -1
  6. package/dist/core/analyzer.d.ts +0 -1
  7. package/dist/core/analyzer.js +0 -1
  8. package/dist/core/class-strings.d.ts +0 -1
  9. package/dist/core/class-strings.js +0 -1
  10. package/dist/core/config.d.ts +2 -3
  11. package/dist/core/config.js +9 -2
  12. package/dist/core/consistency.d.ts +0 -1
  13. package/dist/core/consistency.js +1 -6
  14. package/dist/core/deduplicator.d.ts +0 -1
  15. package/dist/core/deduplicator.js +10 -6
  16. package/dist/core/fixer.d.ts +0 -1
  17. package/dist/core/fixer.js +0 -1
  18. package/dist/core/lexicon.d.ts +1 -1
  19. package/dist/core/lexicon.js +1 -2
  20. package/dist/core/merger.d.ts +0 -1
  21. package/dist/core/merger.js +35 -29
  22. package/dist/core/rules.d.ts +2 -1
  23. package/dist/core/rules.js +56 -1
  24. package/dist/core/scanner.d.ts +0 -1
  25. package/dist/core/scanner.js +0 -1
  26. package/dist/core/sorter.d.ts +0 -1
  27. package/dist/core/sorter.js +5 -3
  28. package/dist/core/suppressions.d.ts +0 -1
  29. package/dist/core/suppressions.js +0 -1
  30. package/dist/core/typos.d.ts +0 -1
  31. package/dist/core/typos.js +0 -1
  32. package/dist/eslint/plugin.d.ts +0 -1
  33. package/dist/eslint/plugin.js +2 -2
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/io/analyzer.d.ts +0 -1
  37. package/dist/io/analyzer.js +0 -1
  38. package/dist/io/config.d.ts +9 -1
  39. package/dist/io/config.js +66 -5
  40. package/dist/io/consistency.d.ts +0 -1
  41. package/dist/io/consistency.js +0 -1
  42. package/dist/io/deduplicator.d.ts +0 -1
  43. package/dist/io/deduplicator.js +0 -1
  44. package/dist/io/fixer.d.ts +0 -1
  45. package/dist/io/fixer.js +0 -1
  46. package/dist/io/merger.d.ts +0 -1
  47. package/dist/io/merger.js +0 -1
  48. package/dist/io/scanner.d.ts +0 -1
  49. package/dist/io/scanner.js +0 -1
  50. package/dist/io/sorter.d.ts +0 -1
  51. package/dist/io/sorter.js +0 -1
  52. package/dist/io/typos.d.ts +0 -1
  53. package/dist/io/typos.js +0 -1
  54. package/package.json +10 -9
  55. package/dist/cli/cli.d.ts.map +0 -1
  56. package/dist/cli/cli.js.map +0 -1
  57. package/dist/cli/index.d.ts.map +0 -1
  58. package/dist/cli/index.js.map +0 -1
  59. package/dist/core/analyzer.d.ts.map +0 -1
  60. package/dist/core/analyzer.js.map +0 -1
  61. package/dist/core/class-strings.d.ts.map +0 -1
  62. package/dist/core/class-strings.js.map +0 -1
  63. package/dist/core/config.d.ts.map +0 -1
  64. package/dist/core/config.js.map +0 -1
  65. package/dist/core/consistency.d.ts.map +0 -1
  66. package/dist/core/consistency.js.map +0 -1
  67. package/dist/core/deduplicator.d.ts.map +0 -1
  68. package/dist/core/deduplicator.js.map +0 -1
  69. package/dist/core/fixer.d.ts.map +0 -1
  70. package/dist/core/fixer.js.map +0 -1
  71. package/dist/core/lexicon.d.ts.map +0 -1
  72. package/dist/core/lexicon.js.map +0 -1
  73. package/dist/core/merger.d.ts.map +0 -1
  74. package/dist/core/merger.js.map +0 -1
  75. package/dist/core/rules.d.ts.map +0 -1
  76. package/dist/core/rules.js.map +0 -1
  77. package/dist/core/scanner.d.ts.map +0 -1
  78. package/dist/core/scanner.js.map +0 -1
  79. package/dist/core/sorter.d.ts.map +0 -1
  80. package/dist/core/sorter.js.map +0 -1
  81. package/dist/core/suppressions.d.ts.map +0 -1
  82. package/dist/core/suppressions.js.map +0 -1
  83. package/dist/core/typos.d.ts.map +0 -1
  84. package/dist/core/typos.js.map +0 -1
  85. package/dist/eslint/plugin.d.ts.map +0 -1
  86. package/dist/eslint/plugin.js.map +0 -1
  87. package/dist/index.d.ts.map +0 -1
  88. package/dist/index.js.map +0 -1
  89. package/dist/io/analyzer.d.ts.map +0 -1
  90. package/dist/io/analyzer.js.map +0 -1
  91. package/dist/io/config.d.ts.map +0 -1
  92. package/dist/io/config.js.map +0 -1
  93. package/dist/io/consistency.d.ts.map +0 -1
  94. package/dist/io/consistency.js.map +0 -1
  95. package/dist/io/deduplicator.d.ts.map +0 -1
  96. package/dist/io/deduplicator.js.map +0 -1
  97. package/dist/io/fixer.d.ts.map +0 -1
  98. package/dist/io/fixer.js.map +0 -1
  99. package/dist/io/merger.d.ts.map +0 -1
  100. package/dist/io/merger.js.map +0 -1
  101. package/dist/io/scanner.d.ts.map +0 -1
  102. package/dist/io/scanner.js.map +0 -1
  103. package/dist/io/sorter.d.ts.map +0 -1
  104. package/dist/io/sorter.js.map +0 -1
  105. package/dist/io/typos.d.ts.map +0 -1
  106. package/dist/io/typos.js.map +0 -1
@@ -26,20 +26,32 @@ function parseClassToken(token) {
26
26
  base: parts.at(-1) ?? token,
27
27
  };
28
28
  }
29
- function isLeadingClass(base) {
30
- return /^!?leading-/.test(base);
31
- }
32
- function isArbitraryTextSize(base) {
33
- return /^!?text-\[-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh)\]$/.test(base);
34
- }
35
- function customTextClassSet(customTextTokens) {
36
- return new Set(Object.values(customTextTokens ?? {}).map((token) => `text-${token}`));
37
- }
29
+ const RE_LEADING = /^!?leading-/;
30
+ const RE_ARBITRARY_TEXT_SIZE = /^!?text-\[-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh)\]$/;
38
31
  function isUnsafeTextSize(base, customTextClasses) {
39
32
  const normalized = base.startsWith('!') ? base.slice(1) : base;
40
- return customTextClasses.has(normalized) || isArbitraryTextSize(normalized);
33
+ return (customTextClasses.has(normalized) || RE_ARBITRARY_TEXT_SIZE.test(normalized));
34
+ }
35
+ /**
36
+ * Last `leading-*` class declared before `cls` in the original string that
37
+ * twMerge dropped, matching `cls`'s variant modifiers. Scans backwards from
38
+ * `cls`'s own position — a leading declared after it never applied to it.
39
+ */
40
+ function lastLeadingBefore(originalClasses, cls, mergedTokens) {
41
+ // twMerge only filters tokens, never synthesises one, so `cls` is always
42
+ // present in the original — `findIndex` cannot return -1 here.
43
+ const at = originalClasses.findIndex(({ token }) => token === cls.token);
44
+ for (let i = at - 1; i >= 0; i -= 1) {
45
+ const candidate = originalClasses[i];
46
+ if (RE_LEADING.test(candidate.base) &&
47
+ candidate.modifiers === cls.modifiers &&
48
+ !mergedTokens.has(candidate.token)) {
49
+ return candidate;
50
+ }
51
+ }
52
+ return undefined;
41
53
  }
42
- function restoreLeadingForUnsafeTextSizes(original, merged, opts) {
54
+ function restoreLeadingForUnsafeTextSizes(original, merged, customTextClasses) {
43
55
  const originalClasses = original
44
56
  .split(/\s+/)
45
57
  .filter(Boolean)
@@ -49,34 +61,28 @@ function restoreLeadingForUnsafeTextSizes(original, merged, opts) {
49
61
  .filter(Boolean)
50
62
  .map(parseClassToken);
51
63
  const mergedTokens = new Set(mergedClasses.map(({ token }) => token));
52
- const customTextClasses = customTextClassSet(opts.customTextTokens);
64
+ const keptLeadingModifiers = new Set(mergedClasses
65
+ .filter(({ base }) => RE_LEADING.test(base))
66
+ .map(({ modifiers }) => modifiers));
53
67
  const restored = [];
54
68
  for (const cls of mergedClasses) {
55
- const leading = originalClasses
56
- .slice(0, originalClasses.findIndex(({ token }) => token === cls.token))
57
- .filter((candidate) => isLeadingClass(candidate.base) &&
58
- candidate.modifiers === cls.modifiers &&
59
- !mergedTokens.has(candidate.token))
60
- .at(-1);
61
- if (leading &&
62
- isUnsafeTextSize(cls.base, customTextClasses) &&
63
- !mergedClasses.some((candidate) => isLeadingClass(candidate.base) &&
64
- candidate.modifiers === cls.modifiers)) {
65
- restored.push(leading.token);
66
- mergedTokens.add(leading.token);
69
+ if (isUnsafeTextSize(cls.base, customTextClasses) &&
70
+ !keptLeadingModifiers.has(cls.modifiers)) {
71
+ const leading = lastLeadingBefore(originalClasses, cls, mergedTokens);
72
+ if (leading) {
73
+ restored.push(leading.token);
74
+ mergedTokens.add(leading.token);
75
+ }
67
76
  }
68
77
  restored.push(cls.token);
69
78
  }
70
79
  return restored.join(' ');
71
80
  }
72
81
  export function mergeContent(content, twMerge, opts = {}) {
73
- return replaceClassStrings(content, (classes) => {
74
- const merged = twMerge(classes);
75
- return restoreLeadingForUnsafeTextSizes(classes, merged, opts);
76
- }, {
82
+ const customTextClasses = new Set(Object.values(opts.customTextTokens ?? {}).map((token) => `text-${token}`));
83
+ return replaceClassStrings(content, (classes) => restoreLeadingForUnsafeTextSizes(classes, twMerge(classes), customTextClasses), {
77
84
  ...opts,
78
85
  isSuppressed: makeLineSuppressor(content),
79
86
  });
80
87
  }
81
88
  export { mergeFile } from '../io/merger.js';
82
- //# sourceMappingURL=merger.js.map
@@ -40,7 +40,8 @@ export type Config = {
40
40
  rareScaleMaxFiles?: number;
41
41
  rareScaleMaxCount?: number;
42
42
  defaultCommand?: DefaultCommandConfig;
43
+ /** Target Tailwind major. v3 restricts suggestions to its discrete scale. */
44
+ tailwindVersion?: 3 | 4;
43
45
  };
44
46
  export declare function suggestCanonical(cls: string, config?: Config): Suggestion | null;
45
47
  export {};
46
- //# sourceMappingURL=rules.d.ts.map
@@ -102,6 +102,47 @@ const FRACTION_RE = new RegExp(`^(${FRACTION_PREFIXES.join('|')})-\\[(\\d+(?:\\.
102
102
  const OPACITY_SCALE = new Set([
103
103
  0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100,
104
104
  ]);
105
+ /**
106
+ * Numeric spacing steps the ÷4 rule may emit that Tailwind v3 actually ships.
107
+ * v4 derives every step from `calc(var(--spacing) * n)`, so any integer works
108
+ * there; v3 ships a discrete scale and a class outside it generates no CSS.
109
+ * The ÷4 rule only ever yields integers, so v3's half steps are unreachable
110
+ * and absent here. Measured by compiling tailwindcss@3.3.7.
111
+ */
112
+ const V3_SPACING_STEPS = new Set([
113
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44,
114
+ 48, 52, 56, 60, 64, 72, 80, 96,
115
+ ]);
116
+ /** No numeric spacing scale at all in v3.3, the documented floor. */
117
+ const V3_UNSCALED_SPACING_PREFIXES = new Set(['size']);
118
+ /** Only the zero step exists in v3.3; the scale itself arrived after 3.3. */
119
+ const V3_ZERO_ONLY_SPACING_PREFIXES = new Set(['min-w', 'max-w', 'min-h']);
120
+ const V3_INSET_FRACTIONS = ['1/2', '1/3', '2/3', '1/4', '3/4'];
121
+ const V3_HEIGHT_FRACTIONS = [
122
+ ...V3_INSET_FRACTIONS,
123
+ '1/5',
124
+ '2/5',
125
+ '3/5',
126
+ '4/5',
127
+ '1/6',
128
+ '5/6',
129
+ ];
130
+ /**
131
+ * Fractions each utility ships in v3.3 — the scale differs per utility, not
132
+ * just per family, so this is keyed on both. A prefix absent from this table
133
+ * has no fraction scale at all (`min-w`, `max-w`, `min-h`, `max-h`).
134
+ */
135
+ const V3_FRACTIONS_BY_PREFIX = {
136
+ w: new Set([...V3_HEIGHT_FRACTIONS, '1/12', '11/12']),
137
+ h: new Set(V3_HEIGHT_FRACTIONS),
138
+ inset: new Set(V3_INSET_FRACTIONS),
139
+ top: new Set(V3_INSET_FRACTIONS),
140
+ left: new Set(V3_INSET_FRACTIONS),
141
+ right: new Set(V3_INSET_FRACTIONS),
142
+ bottom: new Set(V3_INSET_FRACTIONS),
143
+ 'translate-x': new Set(V3_INSET_FRACTIONS),
144
+ 'translate-y': new Set(V3_INSET_FRACTIONS),
145
+ };
105
146
  function remToPx(rem) {
106
147
  const px = rem * 16;
107
148
  return Number.isInteger(px) ? px : null;
@@ -114,11 +155,20 @@ function isIgnored(cls, patterns) {
114
155
  return re.test(cls);
115
156
  });
116
157
  }
158
+ /** True when the ÷4 result would be a class v3 never generates. */
159
+ function isMissingInV3(prefix, step) {
160
+ if (V3_UNSCALED_SPACING_PREFIXES.has(prefix))
161
+ return true;
162
+ if (V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))
163
+ return step !== 0;
164
+ return !V3_SPACING_STEPS.has(step);
165
+ }
117
166
  export function suggestCanonical(cls, config = {}) {
118
167
  if (isIgnored(cls, config.ignorePatterns))
119
168
  return null;
120
169
  const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
121
170
  const spacingTokens = config.customSpacingTokens ?? {};
171
+ const v3 = config.tailwindVersion === 3;
122
172
  // text-[Npx]
123
173
  const textPxMatch = cls.match(/^text-\[(\d+)px\]$/);
124
174
  if (textPxMatch) {
@@ -160,6 +210,8 @@ export function suggestCanonical(cls, config = {}) {
160
210
  };
161
211
  }
162
212
  if (px % 4 === 0) {
213
+ if (v3 && isMissingInV3(prefix, px / 4))
214
+ return null;
163
215
  return {
164
216
  original: cls,
165
217
  canonical: `${prefix}-${px / 4}`,
@@ -183,6 +235,8 @@ export function suggestCanonical(cls, config = {}) {
183
235
  };
184
236
  }
185
237
  if (px % 4 === 0) {
238
+ if (v3 && isMissingInV3(prefix, px / 4))
239
+ return null;
186
240
  return {
187
241
  original: cls,
188
242
  canonical: `${prefix}-${px / 4}`,
@@ -199,6 +253,8 @@ export function suggestCanonical(cls, config = {}) {
199
253
  const fraction = FRACTION_MAP[pct];
200
254
  if (!fraction)
201
255
  return null;
256
+ if (v3 && !V3_FRACTIONS_BY_PREFIX[prefix]?.has(fraction))
257
+ return null;
202
258
  return {
203
259
  original: cls,
204
260
  canonical: `${prefix}-${fraction}`,
@@ -238,4 +294,3 @@ export function suggestCanonical(cls, config = {}) {
238
294
  }
239
295
  return null;
240
296
  }
241
- //# sourceMappingURL=rules.js.map
@@ -7,4 +7,3 @@ export type ScanOptions = {
7
7
  export declare function isGlob(target: string): boolean;
8
8
  export declare function globToRegex(pattern: string): RegExp;
9
9
  export { resolveTargets, scanFiles } from '../io/scanner.js';
10
- //# sourceMappingURL=scanner.d.ts.map
@@ -64,4 +64,3 @@ export function globToRegex(pattern) {
64
64
  return new RegExp(`${re}$`);
65
65
  }
66
66
  export { resolveTargets, scanFiles } from '../io/scanner.js';
67
- //# sourceMappingURL=scanner.js.map
@@ -7,4 +7,3 @@ export declare function sortContent(content: string, opts?: ClassStringOpts, sor
7
7
  count: number;
8
8
  };
9
9
  export { sortFile } from '../io/sorter.js';
10
- //# sourceMappingURL=sorter.d.ts.map
@@ -37,6 +37,7 @@ const TYPOGRAPHY_KEYWORDS = new Set([
37
37
  'antialiased',
38
38
  'subpixel-antialiased',
39
39
  ]);
40
+ const RE_DARK_PRINT = /^(dark|print)$/;
40
41
  const TEXT_SIZES = /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|center|right|justify|start|end)$/;
41
42
  const RE_LAYOUT = /^(overflow|overscroll|object|float|clear|isolation|box|columns)[-]/;
42
43
  const RE_INSET = /^(inset|top|right|bottom|left|z)[-]/;
@@ -98,7 +99,9 @@ function getCategory(cls) {
98
99
  return 'colors';
99
100
  if (base === 'shadow' ||
100
101
  base === 'blur' ||
101
- ['grayscale', 'invert', 'sepia'].includes(base) ||
102
+ base === 'grayscale' ||
103
+ base === 'invert' ||
104
+ base === 'sepia' ||
102
105
  RE_EFFECTS.test(base))
103
106
  return 'effects';
104
107
  if (base === 'transition' || RE_TRANSITIONS.test(base))
@@ -128,7 +131,7 @@ function getVariantOrder(cls) {
128
131
  const segments = cls.slice(0, cls.lastIndexOf(':')).split(':');
129
132
  if (segments.some((s) => RE_BREAKPOINTS.test(s)))
130
133
  return 1;
131
- if (segments.some((s) => /^(dark|print)$/.test(s)))
134
+ if (segments.some((s) => RE_DARK_PRINT.test(s)))
132
135
  return 2;
133
136
  return 3;
134
137
  }
@@ -163,4 +166,3 @@ export function sortContent(content, opts = {}, sortOrder) {
163
166
  });
164
167
  }
165
168
  export { sortFile } from '../io/sorter.js';
166
- //# sourceMappingURL=sorter.js.map
@@ -5,4 +5,3 @@ export declare function indexToLineCol(content: string, index: number): {
5
5
  col: number;
6
6
  };
7
7
  export declare function lineAt(content: string, offset: number): number;
8
- //# sourceMappingURL=suppressions.d.ts.map
@@ -41,4 +41,3 @@ export function indexToLineCol(content, index) {
41
41
  export function lineAt(content, offset) {
42
42
  return indexToLineCol(content, offset).line;
43
43
  }
44
- //# sourceMappingURL=suppressions.js.map
@@ -11,4 +11,3 @@ export declare function detectTypo(cls: string, extraColors?: ReadonlySet<string
11
11
  } | null;
12
12
  export declare function analyzeTyposContent(filePath: string, content: string, config?: Config): TypoFinding[];
13
13
  export { analyzeTyposFile } from '../io/typos.js';
14
- //# sourceMappingURL=typos.d.ts.map
@@ -68,4 +68,3 @@ export function analyzeTyposContent(filePath, content, config = {}) {
68
68
  return findings;
69
69
  }
70
70
  export { analyzeTyposFile } from '../io/typos.js';
71
- //# sourceMappingURL=typos.js.map
@@ -15,4 +15,3 @@ interface TailwindCanonicalPlugin {
15
15
  }
16
16
  declare const plugin: TailwindCanonicalPlugin;
17
17
  export default plugin;
18
- //# sourceMappingURL=plugin.d.ts.map
@@ -48,7 +48,7 @@ const noArbitraryCanonical = {
48
48
  // Schema accepts the full tailwind-canonical Config so a single shared
49
49
  // config object can be passed as rule options without schema errors (#42).
50
50
  // Honored by this rule (via suggestCanonical): customTextTokens,
51
- // customSpacingTokens, ignorePatterns.
51
+ // customSpacingTokens, ignorePatterns, tailwindVersion.
52
52
  // Accepted but ignored here (CLI-only): functionNames, attributeNames,
53
53
  // sortOrder.
54
54
  schema: [
@@ -58,6 +58,7 @@ const noArbitraryCanonical = {
58
58
  customTextTokens: { type: 'object' },
59
59
  customSpacingTokens: { type: 'object' },
60
60
  ignorePatterns: { type: 'array' },
61
+ tailwindVersion: { enum: [3, 4] },
61
62
  functionNames: { type: 'array', items: { type: 'string' } },
62
63
  attributeNames: { type: 'array', items: { type: 'string' } },
63
64
  sortOrder: { type: 'array', items: { type: 'string' } },
@@ -151,4 +152,3 @@ plugin.configs.recommended = {
151
152
  },
152
153
  };
153
154
  export default plugin;
154
- //# sourceMappingURL=plugin.js.map
package/dist/index.d.ts CHANGED
@@ -14,4 +14,3 @@ export type { SortCategory } from './core/sorter.js';
14
14
  export { DEFAULT_SORT_ORDER, sortClasses, sortFile } from './core/sorter.js';
15
15
  export type { TypoFinding } from './core/typos.js';
16
16
  export { analyzeTyposFile, detectTypo } from './core/typos.js';
17
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -16,4 +16,3 @@ export { suggestCanonical } from './core/rules.js';
16
16
  export { resolveTargets, scanFiles } from './core/scanner.js';
17
17
  export { DEFAULT_SORT_ORDER, sortClasses, sortFile } from './core/sorter.js';
18
18
  export { analyzeTyposFile, detectTypo } from './core/typos.js';
19
- //# sourceMappingURL=index.js.map
@@ -1,4 +1,3 @@
1
1
  import { type Finding } from '../core/analyzer.js';
2
2
  import type { Config } from '../core/rules.js';
3
3
  export declare function analyzeFile(filePath: string, config?: Config): Finding[];
4
- //# sourceMappingURL=analyzer.d.ts.map
@@ -4,4 +4,3 @@ export function analyzeFile(filePath, config = {}) {
4
4
  const content = readFileSync(filePath, 'utf8');
5
5
  return analyzeContent(filePath, content, config);
6
6
  }
7
- //# sourceMappingURL=analyzer.js.map
@@ -1,3 +1,11 @@
1
1
  import type { Config } from '../core/rules.js';
2
+ /**
3
+ * The config extension that actually loads here. `.ts`/`.mts` need Node type
4
+ * stripping (>=22.18, or >=22.6 behind --experimental-strip-types), so short
5
+ * of that we scaffold plain ESM instead — `.mjs` under CommonJS, `.js` where
6
+ * the nearest package.json already declares `"type": "module"`. Both load on
7
+ * every supported Node, so preferring them when a flag is in play costs
8
+ * nothing.
9
+ */
10
+ export declare function resolveInitFilename(cwd: string): string;
2
11
  export declare function loadConfig(cwd: string): Promise<Config>;
3
- //# sourceMappingURL=config.d.ts.map
package/dist/io/config.js CHANGED
@@ -1,7 +1,56 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { basename, dirname, join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { CONFIG_FILENAMES, validateConfig } from '../core/config.js';
5
+ /** The `type` of the package.json that governs `dir`, per Node resolution. */
6
+ function nearestPackageType(dir) {
7
+ let current = dir;
8
+ while (true) {
9
+ const manifest = join(current, 'package.json');
10
+ if (existsSync(manifest)) {
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
13
+ return parsed.type === 'module' ? 'module' : 'commonjs';
14
+ }
15
+ catch {
16
+ return 'commonjs';
17
+ }
18
+ }
19
+ const parent = dirname(current);
20
+ if (parent === current)
21
+ return 'commonjs';
22
+ current = parent;
23
+ }
24
+ }
25
+ const STRIP_FLAG = /--experimental-(strip|transform)-types/;
26
+ /**
27
+ * Whether `.ts` loads on a bare `node`, as opposed to only under a flag.
28
+ * `process.features.typescript` is also `"strip"` when the flag supplied it,
29
+ * so a config scaffolded in a flagged process would fail on every later
30
+ * unflagged run — including plain `npx tailwind-canonical`.
31
+ */
32
+ function stripsTypesUnflagged() {
33
+ if (!process.features.typescript)
34
+ return false;
35
+ if (process.execArgv.some((arg) => STRIP_FLAG.test(arg)))
36
+ return false;
37
+ return !STRIP_FLAG.test(process.env.NODE_OPTIONS ?? '');
38
+ }
39
+ /**
40
+ * The config extension that actually loads here. `.ts`/`.mts` need Node type
41
+ * stripping (>=22.18, or >=22.6 behind --experimental-strip-types), so short
42
+ * of that we scaffold plain ESM instead — `.mjs` under CommonJS, `.js` where
43
+ * the nearest package.json already declares `"type": "module"`. Both load on
44
+ * every supported Node, so preferring them when a flag is in play costs
45
+ * nothing.
46
+ */
47
+ export function resolveInitFilename(cwd) {
48
+ if (stripsTypesUnflagged())
49
+ return 'tailwind-canonical.config.ts';
50
+ return nearestPackageType(cwd) === 'module'
51
+ ? 'tailwind-canonical.config.js'
52
+ : 'tailwind-canonical.config.mjs';
53
+ }
5
54
  function findConfigPath(cwd) {
6
55
  let dir = cwd;
7
56
  while (true) {
@@ -26,13 +75,25 @@ export async function loadConfig(cwd) {
26
75
  mod = await import(pathToFileURL(path).href);
27
76
  }
28
77
  catch (err) {
29
- if (filename.endsWith('.ts') &&
78
+ const msg = err instanceof Error ? err.message : String(err);
79
+ const type = nearestPackageType(dirname(path));
80
+ if (/\.m?ts$/.test(filename) &&
30
81
  err.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
31
- const msg = err instanceof Error ? err.message : String(err);
32
- throw new Error(`Loading ${filename} failed on this Node version (type stripping requires Node >=22.6 with --experimental-strip-types or >=23.6). Rename the config to tailwind-canonical.config.js or upgrade Node. Original error: ${msg}`);
82
+ const js = type === 'module'
83
+ ? 'tailwind-canonical.config.js'
84
+ : 'tailwind-canonical.config.mjs';
85
+ throw new Error(`Loading ${filename} failed on this Node version (type stripping requires Node >=22.18, or >=22.6 with --experimental-strip-types). Either upgrade Node, or rename the config to ${js} and drop its TypeScript-only syntax — the "import type" line and the trailing "satisfies Config" — since a bare rename leaves it unparseable. Original error: ${msg}`);
86
+ }
87
+ if (err instanceof SyntaxError &&
88
+ /\bexport\b|\bimport\b/.test(msg) &&
89
+ !/\.m[tj]s$/.test(filename) &&
90
+ type !== 'module') {
91
+ const esm = filename.endsWith('.ts')
92
+ ? 'tailwind-canonical.config.mts'
93
+ : 'tailwind-canonical.config.mjs';
94
+ throw new Error(`${filename} uses ESM syntax but is loaded as CommonJS, because the nearest package.json has no "type": "module". Rename it to ${esm}, or add "type": "module" to your package.json. Original error: ${msg}`);
33
95
  }
34
96
  throw err;
35
97
  }
36
98
  return validateConfig(mod.default, filename);
37
99
  }
38
- //# sourceMappingURL=config.js.map
@@ -1,4 +1,3 @@
1
1
  import { type ConsistencyOptions, type ConsistencyReport } from '../core/consistency.js';
2
2
  import type { Config } from '../core/rules.js';
3
3
  export declare function analyzeConsistencyFiles(filePaths: string[], config?: Config, options?: ConsistencyOptions, onError?: (file: string, err: unknown) => void): ConsistencyReport;
4
- //# sourceMappingURL=consistency.d.ts.map
@@ -18,4 +18,3 @@ export function analyzeConsistencyFiles(filePaths, config = {}, options = {}, on
18
18
  }
19
19
  return analyzeConsistency(input, options);
20
20
  }
21
- //# sourceMappingURL=consistency.js.map
@@ -1,3 +1,2 @@
1
1
  import type { ClassStringOpts } from '../core/class-strings.js';
2
2
  export declare function dedupeFile(filePath: string, opts?: ClassStringOpts): number;
3
- //# sourceMappingURL=deduplicator.d.ts.map
@@ -7,4 +7,3 @@ export function dedupeFile(filePath, opts = {}) {
7
7
  writeFileSync(filePath, result, 'utf8');
8
8
  return count;
9
9
  }
10
- //# sourceMappingURL=deduplicator.js.map
@@ -1,3 +1,2 @@
1
1
  import type { Config } from '../core/rules.js';
2
2
  export declare function fixFile(filePath: string, config?: Config): number;
3
- //# sourceMappingURL=fixer.d.ts.map
package/dist/io/fixer.js CHANGED
@@ -7,4 +7,3 @@ export function fixFile(filePath, config = {}) {
7
7
  writeFileSync(filePath, result, 'utf8');
8
8
  return count;
9
9
  }
10
- //# sourceMappingURL=fixer.js.map
@@ -1,3 +1,2 @@
1
1
  import type { ClassStringOpts } from '../core/class-strings.js';
2
2
  export declare function mergeFile(filePath: string, opts?: ClassStringOpts): Promise<number>;
3
- //# sourceMappingURL=merger.d.ts.map
package/dist/io/merger.js CHANGED
@@ -14,4 +14,3 @@ export async function mergeFile(filePath, opts = {}) {
14
14
  writeFileSync(filePath, result, 'utf8');
15
15
  return count;
16
16
  }
17
- //# sourceMappingURL=merger.js.map
@@ -1,4 +1,3 @@
1
1
  import { type ScanOptions } from '../core/scanner.js';
2
2
  export declare function scanFiles(target: string, options?: ScanOptions): string[];
3
3
  export declare function resolveTargets(patterns: string[], options?: ScanOptions): Promise<string[]>;
4
- //# sourceMappingURL=scanner.d.ts.map
@@ -81,4 +81,3 @@ export async function resolveTargets(patterns, options = {}) {
81
81
  }
82
82
  return [...files].sort();
83
83
  }
84
- //# sourceMappingURL=scanner.js.map
@@ -1,4 +1,3 @@
1
1
  import type { ClassStringOpts } from '../core/class-strings.js';
2
2
  import { type SortCategory } from '../core/sorter.js';
3
3
  export declare function sortFile(filePath: string, opts?: ClassStringOpts, sortOrder?: SortCategory[]): number;
4
- //# sourceMappingURL=sorter.d.ts.map
package/dist/io/sorter.js CHANGED
@@ -7,4 +7,3 @@ export function sortFile(filePath, opts = {}, sortOrder) {
7
7
  writeFileSync(filePath, result, 'utf8');
8
8
  return count;
9
9
  }
10
- //# sourceMappingURL=sorter.js.map
@@ -1,4 +1,3 @@
1
1
  import type { Config } from '../core/rules.js';
2
2
  import { type TypoFinding } from '../core/typos.js';
3
3
  export declare function analyzeTyposFile(filePath: string, config?: Config): TypoFinding[];
4
- //# sourceMappingURL=typos.d.ts.map
package/dist/io/typos.js CHANGED
@@ -4,4 +4,3 @@ export function analyzeTyposFile(filePath, config = {}) {
4
4
  const content = readFileSync(filePath, 'utf8');
5
5
  return analyzeTyposContent(filePath, content, config);
6
6
  }
7
- //# sourceMappingURL=typos.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwind-canonical",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Lint and auto-fix Tailwind CSS arbitrary values that have canonical equivalents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  "./eslint": {
14
14
  "types": "./dist/eslint/plugin.d.ts",
15
15
  "import": "./dist/eslint/plugin.js"
16
- }
16
+ },
17
+ "./package.json": "./package.json"
17
18
  },
18
19
  "bin": {
19
20
  "tailwind-canonical": "./dist/cli/index.js"
@@ -53,14 +54,14 @@
53
54
  ],
54
55
  "license": "MIT",
55
56
  "devDependencies": {
56
- "@biomejs/biome": "^2.4.16",
57
+ "@biomejs/biome": "^2.5.11",
57
58
  "@types/estree": "^1.0.9",
58
- "@types/node": "^22.0.0",
59
- "knip": "^6.14.2",
60
- "lefthook": "^2.1.8",
59
+ "@types/node": "^22.20.1",
60
+ "knip": "^6.34.0",
61
+ "lefthook": "^2.1.12",
61
62
  "tailwind-merge": "^3.6.0",
62
- "tsx": "^4.0.0",
63
- "typescript": "^6.0.0"
63
+ "tsx": "^4.23.13",
64
+ "typescript": "^6.0.3"
64
65
  },
65
66
  "peerDependencies": {
66
67
  "eslint": ">=8.0.0",
@@ -75,7 +76,7 @@
75
76
  }
76
77
  },
77
78
  "scripts": {
78
- "build": "tsc",
79
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
79
80
  "dev": "tsc --watch",
80
81
  "test": "node --import=tsx/esm --test \"src/**/*.test.ts\"",
81
82
  "typecheck": "tsc --noEmit",
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":"AAgEA,MAAM,MAAM,IAAI,GAAG;IACjB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzB,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B,CAAC;AAQF,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjE,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1C,KAAK,KAAK,GAAG;IACX,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAyJF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAqE/C;AAgDD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,CAyBnD;AAgfD,wBAAsB,GAAG,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,IAAkB,GACvB,OAAO,CAAC,SAAS,CAAC,CA0RpB"}