stablekit.ts 0.6.0 → 0.6.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.
@@ -26,6 +26,8 @@ module.exports = __toCommonJS(stylelint_exports);
26
26
  var pluginRuleName = "stablekit/no-functional-in-utility";
27
27
  var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
28
28
  var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
29
+ var nearDuplicateRuleName = "stablekit/no-near-duplicate-ruleset";
30
+ var undefinedTokenRuleName = "stablekit/no-undefined-token";
29
31
  function createFunctionalTokenPlugin(prefixes) {
30
32
  const rule = (enabled) => {
31
33
  return (root, result) => {
@@ -118,13 +120,112 @@ Declarations: ${fingerprint}`,
118
120
  rule
119
121
  };
120
122
  }
123
+ function createNearDuplicatePlugin() {
124
+ function collectDirectDecls(ruleNode) {
125
+ const map = /* @__PURE__ */ new Map();
126
+ for (const child of ruleNode.nodes ?? []) {
127
+ if (child.type === "decl") {
128
+ map.set(child.prop, child.value);
129
+ } else if (child.type === "atrule" && child.name === "apply") {
130
+ map.set(`@apply`, child.params);
131
+ }
132
+ }
133
+ return map;
134
+ }
135
+ const rule = (enabled) => {
136
+ return (root, result) => {
137
+ if (!enabled) return;
138
+ const groups = /* @__PURE__ */ new Map();
139
+ root.walkRules((ruleNode) => {
140
+ if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
141
+ const propMap = collectDirectDecls(ruleNode);
142
+ if (propMap.size < 2) return;
143
+ const propKey = [...propMap.keys()].sort().join(", ");
144
+ const entry = { selector: ruleNode.selector, propKey, propMap, node: ruleNode };
145
+ const group = groups.get(propKey);
146
+ if (group) {
147
+ for (const existing of group) {
148
+ let diffCount = 0;
149
+ let diffProp = "";
150
+ let diffOld = "";
151
+ let diffNew = "";
152
+ for (const [prop, value] of entry.propMap) {
153
+ const existingValue = existing.propMap.get(prop);
154
+ if (existingValue !== value) {
155
+ diffCount++;
156
+ diffProp = prop;
157
+ diffOld = existingValue ?? "(missing)";
158
+ diffNew = value;
159
+ }
160
+ }
161
+ if (diffCount === 0) continue;
162
+ if (diffCount === 1 && diffProp !== "@apply") {
163
+ result.warn(
164
+ `Near-duplicate ruleset \u2014 "${ruleNode.selector}" differs from "${existing.selector}" only in ${diffProp} (${diffOld} \u2192 ${diffNew}). Consider consolidating to a single class.`,
165
+ { node: ruleNode }
166
+ );
167
+ break;
168
+ }
169
+ }
170
+ group.push(entry);
171
+ } else {
172
+ groups.set(propKey, [entry]);
173
+ }
174
+ });
175
+ };
176
+ };
177
+ return {
178
+ ruleName: nearDuplicateRuleName,
179
+ rule
180
+ };
181
+ }
182
+ function createUndefinedTokenPlugin(runtimePrefixes) {
183
+ const varPattern = /var\(--([a-zA-Z0-9_-]+)/g;
184
+ const rule = (enabled) => {
185
+ return (root, result) => {
186
+ if (!enabled) return;
187
+ const defined = /* @__PURE__ */ new Set();
188
+ root.walkDecls((decl) => {
189
+ if (decl.prop.startsWith("--")) {
190
+ defined.add(decl.prop);
191
+ }
192
+ });
193
+ root.walkAtRules("theme", (atRule) => {
194
+ atRule.walkDecls((decl) => {
195
+ if (decl.prop.startsWith("--")) {
196
+ defined.add(decl.prop);
197
+ }
198
+ });
199
+ });
200
+ root.walkDecls((decl) => {
201
+ if (decl.prop.startsWith("--")) return;
202
+ let match;
203
+ varPattern.lastIndex = 0;
204
+ while ((match = varPattern.exec(decl.value)) !== null) {
205
+ const name = `--${match[1]}`;
206
+ if (defined.has(name)) continue;
207
+ if (runtimePrefixes.some((p) => name.startsWith(p))) continue;
208
+ result.warn(
209
+ `Reference to undefined custom property "${name}". This token is not defined anywhere in this file.`,
210
+ { node: decl }
211
+ );
212
+ }
213
+ });
214
+ };
215
+ };
216
+ return {
217
+ ruleName: undefinedTokenRuleName,
218
+ rule
219
+ };
220
+ }
121
221
  function createStyleLint(options = {}) {
122
222
  const {
123
223
  ignoreTypes = ["html", "body"],
124
224
  functionalTokens = [],
225
+ runtimeTokens = [],
125
226
  files = ["src/**/*.css"]
126
227
  } = options;
127
- const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
228
+ const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin(), createNearDuplicatePlugin(), createUndefinedTokenPlugin(runtimeTokens)];
128
229
  if (functionalTokens.length > 0) {
129
230
  plugins.push(createFunctionalTokenPlugin(functionalTokens));
130
231
  }
@@ -142,6 +243,10 @@ function createStyleLint(options = {}) {
142
243
  [descendantColorRuleName]: true,
143
244
  // Flag selectors with identical declarations for consolidation.
144
245
  [duplicateRulesetRuleName]: true,
246
+ // Flag near-duplicate rulesets (same props, differ by 1 value).
247
+ [nearDuplicateRuleName]: true,
248
+ // Flag var() references to custom properties not defined in this file.
249
+ [undefinedTokenRuleName]: true,
145
250
  // Ban animating layout properties — causes reflow on every frame.
146
251
  // Use transform (scaleY, translateY) or opacity instead.
147
252
  "declaration-property-value-disallowed-list": [
@@ -40,6 +40,11 @@ interface StyleLintOptions {
40
40
  * e.g. ["--color-status-", "--color-danger"]
41
41
  * Any var() referencing these inside @utility is a lint error. */
42
42
  functionalTokens?: string[];
43
+ /** Custom property prefixes set at runtime (JS, Radix, inline styles)
44
+ * that should not be flagged as undefined.
45
+ * e.g. ["--radix-", "--bar-"]
46
+ * @default [] */
47
+ runtimeTokens?: string[];
43
48
  /** Glob patterns for files to lint.
44
49
  * @default ["src/**\/*.css"] */
45
50
  files?: string[];
@@ -40,6 +40,11 @@ interface StyleLintOptions {
40
40
  * e.g. ["--color-status-", "--color-danger"]
41
41
  * Any var() referencing these inside @utility is a lint error. */
42
42
  functionalTokens?: string[];
43
+ /** Custom property prefixes set at runtime (JS, Radix, inline styles)
44
+ * that should not be flagged as undefined.
45
+ * e.g. ["--radix-", "--bar-"]
46
+ * @default [] */
47
+ runtimeTokens?: string[];
43
48
  /** Glob patterns for files to lint.
44
49
  * @default ["src/**\/*.css"] */
45
50
  files?: string[];
package/dist/stylelint.js CHANGED
@@ -2,6 +2,8 @@
2
2
  var pluginRuleName = "stablekit/no-functional-in-utility";
3
3
  var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
4
4
  var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
5
+ var nearDuplicateRuleName = "stablekit/no-near-duplicate-ruleset";
6
+ var undefinedTokenRuleName = "stablekit/no-undefined-token";
5
7
  function createFunctionalTokenPlugin(prefixes) {
6
8
  const rule = (enabled) => {
7
9
  return (root, result) => {
@@ -94,13 +96,112 @@ Declarations: ${fingerprint}`,
94
96
  rule
95
97
  };
96
98
  }
99
+ function createNearDuplicatePlugin() {
100
+ function collectDirectDecls(ruleNode) {
101
+ const map = /* @__PURE__ */ new Map();
102
+ for (const child of ruleNode.nodes ?? []) {
103
+ if (child.type === "decl") {
104
+ map.set(child.prop, child.value);
105
+ } else if (child.type === "atrule" && child.name === "apply") {
106
+ map.set(`@apply`, child.params);
107
+ }
108
+ }
109
+ return map;
110
+ }
111
+ const rule = (enabled) => {
112
+ return (root, result) => {
113
+ if (!enabled) return;
114
+ const groups = /* @__PURE__ */ new Map();
115
+ root.walkRules((ruleNode) => {
116
+ if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
117
+ const propMap = collectDirectDecls(ruleNode);
118
+ if (propMap.size < 2) return;
119
+ const propKey = [...propMap.keys()].sort().join(", ");
120
+ const entry = { selector: ruleNode.selector, propKey, propMap, node: ruleNode };
121
+ const group = groups.get(propKey);
122
+ if (group) {
123
+ for (const existing of group) {
124
+ let diffCount = 0;
125
+ let diffProp = "";
126
+ let diffOld = "";
127
+ let diffNew = "";
128
+ for (const [prop, value] of entry.propMap) {
129
+ const existingValue = existing.propMap.get(prop);
130
+ if (existingValue !== value) {
131
+ diffCount++;
132
+ diffProp = prop;
133
+ diffOld = existingValue ?? "(missing)";
134
+ diffNew = value;
135
+ }
136
+ }
137
+ if (diffCount === 0) continue;
138
+ if (diffCount === 1 && diffProp !== "@apply") {
139
+ result.warn(
140
+ `Near-duplicate ruleset \u2014 "${ruleNode.selector}" differs from "${existing.selector}" only in ${diffProp} (${diffOld} \u2192 ${diffNew}). Consider consolidating to a single class.`,
141
+ { node: ruleNode }
142
+ );
143
+ break;
144
+ }
145
+ }
146
+ group.push(entry);
147
+ } else {
148
+ groups.set(propKey, [entry]);
149
+ }
150
+ });
151
+ };
152
+ };
153
+ return {
154
+ ruleName: nearDuplicateRuleName,
155
+ rule
156
+ };
157
+ }
158
+ function createUndefinedTokenPlugin(runtimePrefixes) {
159
+ const varPattern = /var\(--([a-zA-Z0-9_-]+)/g;
160
+ const rule = (enabled) => {
161
+ return (root, result) => {
162
+ if (!enabled) return;
163
+ const defined = /* @__PURE__ */ new Set();
164
+ root.walkDecls((decl) => {
165
+ if (decl.prop.startsWith("--")) {
166
+ defined.add(decl.prop);
167
+ }
168
+ });
169
+ root.walkAtRules("theme", (atRule) => {
170
+ atRule.walkDecls((decl) => {
171
+ if (decl.prop.startsWith("--")) {
172
+ defined.add(decl.prop);
173
+ }
174
+ });
175
+ });
176
+ root.walkDecls((decl) => {
177
+ if (decl.prop.startsWith("--")) return;
178
+ let match;
179
+ varPattern.lastIndex = 0;
180
+ while ((match = varPattern.exec(decl.value)) !== null) {
181
+ const name = `--${match[1]}`;
182
+ if (defined.has(name)) continue;
183
+ if (runtimePrefixes.some((p) => name.startsWith(p))) continue;
184
+ result.warn(
185
+ `Reference to undefined custom property "${name}". This token is not defined anywhere in this file.`,
186
+ { node: decl }
187
+ );
188
+ }
189
+ });
190
+ };
191
+ };
192
+ return {
193
+ ruleName: undefinedTokenRuleName,
194
+ rule
195
+ };
196
+ }
97
197
  function createStyleLint(options = {}) {
98
198
  const {
99
199
  ignoreTypes = ["html", "body"],
100
200
  functionalTokens = [],
201
+ runtimeTokens = [],
101
202
  files = ["src/**/*.css"]
102
203
  } = options;
103
- const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
204
+ const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin(), createNearDuplicatePlugin(), createUndefinedTokenPlugin(runtimeTokens)];
104
205
  if (functionalTokens.length > 0) {
105
206
  plugins.push(createFunctionalTokenPlugin(functionalTokens));
106
207
  }
@@ -118,6 +219,10 @@ function createStyleLint(options = {}) {
118
219
  [descendantColorRuleName]: true,
119
220
  // Flag selectors with identical declarations for consolidation.
120
221
  [duplicateRulesetRuleName]: true,
222
+ // Flag near-duplicate rulesets (same props, differ by 1 value).
223
+ [nearDuplicateRuleName]: true,
224
+ // Flag var() references to custom properties not defined in this file.
225
+ [undefinedTokenRuleName]: true,
121
226
  // Ban animating layout properties — causes reflow on every frame.
122
227
  // Use transform (scaleY, translateY) or opacity instead.
123
228
  "declaration-property-value-disallowed-list": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stablekit.ts",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "React toolkit for layout stability — zero-shift components for loading states, content swaps, and spatial containers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",