dispersa 0.4.3 → 1.0.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 (58) hide show
  1. package/README.md +65 -30
  2. package/dist/android-CRDfSB3_.d.cts +126 -0
  3. package/dist/android-DANJjjPO.d.ts +126 -0
  4. package/dist/builders.cjs +206 -62
  5. package/dist/builders.cjs.map +1 -1
  6. package/dist/builders.d.cts +12 -11
  7. package/dist/builders.d.ts +12 -11
  8. package/dist/builders.js +206 -62
  9. package/dist/builders.js.map +1 -1
  10. package/dist/cli/cli.js +120 -7
  11. package/dist/cli/cli.js.map +1 -1
  12. package/dist/cli/config.d.ts +321 -0
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/index.js +119 -7
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/dispersa-BC1kDF5u.d.ts +118 -0
  17. package/dist/dispersa-DL3J_Pmz.d.cts +118 -0
  18. package/dist/errors-qT4sJgSA.d.cts +104 -0
  19. package/dist/errors-qT4sJgSA.d.ts +104 -0
  20. package/dist/errors.cjs.map +1 -1
  21. package/dist/errors.d.cts +1 -83
  22. package/dist/errors.d.ts +1 -83
  23. package/dist/errors.js.map +1 -1
  24. package/dist/filters.cjs.map +1 -1
  25. package/dist/filters.d.cts +2 -2
  26. package/dist/filters.d.ts +2 -2
  27. package/dist/filters.js.map +1 -1
  28. package/dist/{index-CNT2Meyf.d.cts → index-Dajm5rvM.d.ts} +311 -132
  29. package/dist/{index-CqdaN3X0.d.ts → index-De6SjZYH.d.cts} +311 -132
  30. package/dist/index.cjs +799 -353
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +8 -329
  33. package/dist/index.d.ts +8 -329
  34. package/dist/index.js +793 -353
  35. package/dist/index.js.map +1 -1
  36. package/dist/lint.cjs +1017 -0
  37. package/dist/lint.cjs.map +1 -0
  38. package/dist/lint.d.cts +463 -0
  39. package/dist/lint.d.ts +463 -0
  40. package/dist/lint.js +997 -0
  41. package/dist/lint.js.map +1 -0
  42. package/dist/preprocessors.d.cts +2 -2
  43. package/dist/preprocessors.d.ts +2 -2
  44. package/dist/renderers.cjs.map +1 -1
  45. package/dist/renderers.d.cts +7 -6
  46. package/dist/renderers.d.ts +7 -6
  47. package/dist/renderers.js.map +1 -1
  48. package/dist/transforms.d.cts +2 -2
  49. package/dist/transforms.d.ts +2 -2
  50. package/dist/{types-CZb19kiq.d.ts → types-8MLtztK3.d.ts} +56 -1
  51. package/dist/{types-CussyWwe.d.cts → types-BHBHRm0a.d.cts} +56 -1
  52. package/dist/{types-BAv39mum.d.cts → types-BltzwVYK.d.cts} +1 -1
  53. package/dist/{types-DWKq-eJj.d.cts → types-CAdUV-fa.d.cts} +1 -1
  54. package/dist/{types-CzHa7YkW.d.ts → types-DztXKlka.d.ts} +1 -1
  55. package/dist/{types-Bc0kA7De.d.ts → types-TQHV1MrY.d.cts} +19 -1
  56. package/dist/{types-Bc0kA7De.d.cts → types-TQHV1MrY.d.ts} +19 -1
  57. package/dist/{types-BzNcG-rI.d.ts → types-ebxDimRz.d.ts} +1 -1
  58. package/package.json +11 -1
package/dist/lint.js ADDED
@@ -0,0 +1,997 @@
1
+ import { createRequire } from 'module';
2
+ import { isAbsolute, resolve } from 'path';
3
+ import process from 'process';
4
+ import { createJiti } from 'jiti';
5
+
6
+ // src/lint/create-rule.ts
7
+ function createRule(rule) {
8
+ return rule;
9
+ }
10
+
11
+ // src/shared/errors/index.ts
12
+ var DispersaError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "DispersaError";
16
+ if (typeof Error.captureStackTrace === "function") {
17
+ Error.captureStackTrace(this, this.constructor);
18
+ }
19
+ }
20
+ };
21
+ var ConfigurationError = class extends DispersaError {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "ConfigurationError";
25
+ }
26
+ };
27
+ var PluginLoader = class {
28
+ cwd;
29
+ jiti = null;
30
+ cache = /* @__PURE__ */ new Map();
31
+ constructor(options = {}) {
32
+ this.cwd = options.cwd ?? process.cwd();
33
+ }
34
+ /**
35
+ * Load a plugin from an inline object or module path
36
+ *
37
+ * @param source - Plugin object or module path string
38
+ * @returns Loaded plugin
39
+ * @throws {ConfigurationError} If plugin cannot be loaded or is invalid
40
+ */
41
+ async load(source) {
42
+ if (this.isPluginObject(source)) {
43
+ this.validatePlugin(source);
44
+ return source;
45
+ }
46
+ const modulePath = source;
47
+ const cached = this.cache.get(modulePath);
48
+ if (cached) {
49
+ return cached;
50
+ }
51
+ const plugin = await this.loadFromModule(modulePath);
52
+ this.validatePlugin(plugin);
53
+ this.cache.set(modulePath, plugin);
54
+ return plugin;
55
+ }
56
+ /**
57
+ * Load multiple plugins
58
+ *
59
+ * @param plugins - Record of namespace to plugin source
60
+ * @returns Record of namespace to loaded plugin
61
+ */
62
+ async loadAll(plugins) {
63
+ const entries = Object.entries(plugins);
64
+ const loaded = await Promise.all(
65
+ entries.map(async ([namespace, source]) => [namespace, await this.load(source)])
66
+ );
67
+ return Object.fromEntries(loaded);
68
+ }
69
+ /**
70
+ * Check if source is an inline plugin object
71
+ */
72
+ isPluginObject(source) {
73
+ return typeof source !== "string";
74
+ }
75
+ /**
76
+ * Load a plugin from a module path
77
+ */
78
+ async loadFromModule(modulePath) {
79
+ const resolvedPath = isAbsolute(modulePath) ? modulePath : resolve(this.cwd, modulePath);
80
+ const isFilePath = modulePath.startsWith("./") || modulePath.startsWith("../") || isAbsolute(modulePath);
81
+ try {
82
+ if (isFilePath) {
83
+ return await this.loadFromFile(resolvedPath);
84
+ }
85
+ return await this.loadFromPackage(modulePath);
86
+ } catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ throw new ConfigurationError(`Failed to load lint plugin '${modulePath}': ${message}`);
89
+ }
90
+ }
91
+ /**
92
+ * Load a plugin from a file path using jiti (supports TypeScript)
93
+ */
94
+ async loadFromFile(filePath) {
95
+ this.jiti ??= createJiti(this.cwd, {
96
+ interopDefault: true
97
+ });
98
+ const loaded = await this.jiti(filePath);
99
+ const plugin = this.extractPlugin(loaded);
100
+ if (!plugin) {
101
+ throw new ConfigurationError(`Plugin file '${filePath}' does not export a valid LintPlugin`);
102
+ }
103
+ return plugin;
104
+ }
105
+ /**
106
+ * Load a plugin from a package name
107
+ */
108
+ async loadFromPackage(packageName) {
109
+ const require2 = createRequire(this.cwd);
110
+ let resolvedPath;
111
+ try {
112
+ resolvedPath = require2.resolve(packageName, { paths: [this.cwd] });
113
+ } catch {
114
+ try {
115
+ resolvedPath = require2.resolve(packageName);
116
+ } catch {
117
+ throw new ConfigurationError(
118
+ `Cannot find package '${packageName}'. Make sure it is installed.`
119
+ );
120
+ }
121
+ }
122
+ this.jiti ??= createJiti(this.cwd, {
123
+ interopDefault: true
124
+ });
125
+ const loaded = await this.jiti(resolvedPath);
126
+ const plugin = this.extractPlugin(loaded);
127
+ if (!plugin) {
128
+ throw new ConfigurationError(`Package '${packageName}' does not export a valid LintPlugin`);
129
+ }
130
+ return plugin;
131
+ }
132
+ /**
133
+ * Extract plugin from loaded module
134
+ *
135
+ * Supports multiple export patterns:
136
+ * - export default plugin
137
+ * - export const plugin = {...}
138
+ * - module.exports = plugin (CJS)
139
+ */
140
+ extractPlugin(loaded) {
141
+ if (!loaded || typeof loaded !== "object") {
142
+ return null;
143
+ }
144
+ const module = loaded;
145
+ if (module.default && this.isValidPluginStructure(module.default)) {
146
+ return module.default;
147
+ }
148
+ if (module.plugin && this.isValidPluginStructure(module.plugin)) {
149
+ return module.plugin;
150
+ }
151
+ if (this.isValidPluginStructure(loaded)) {
152
+ return loaded;
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Check if object has required plugin structure
158
+ */
159
+ isValidPluginStructure(obj) {
160
+ if (!obj || typeof obj !== "object") {
161
+ return false;
162
+ }
163
+ const plugin = obj;
164
+ const rules = plugin.rules;
165
+ if (plugin.meta === void 0 || typeof plugin.meta !== "object" || !plugin.meta.name || rules === void 0 || Object.keys(rules).length === 0) {
166
+ return false;
167
+ }
168
+ return true;
169
+ }
170
+ /**
171
+ * Validate a loaded plugin
172
+ */
173
+ validatePlugin(plugin) {
174
+ if (!plugin.meta) {
175
+ throw new ConfigurationError("Lint plugin must have a meta property with name");
176
+ }
177
+ if (!plugin.meta.name) {
178
+ throw new ConfigurationError("Lint plugin meta.name is required");
179
+ }
180
+ if (!plugin.rules || typeof plugin.rules !== "object" || Object.keys(plugin.rules).length === 0) {
181
+ throw new ConfigurationError(
182
+ `Lint plugin '${plugin.meta.name}' must have a non-empty rules object`
183
+ );
184
+ }
185
+ for (const [ruleName, rule] of Object.entries(plugin.rules)) {
186
+ if (!rule.meta) {
187
+ throw new ConfigurationError(
188
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta property`
189
+ );
190
+ }
191
+ if (!rule.meta.messages || typeof rule.meta.messages !== "object") {
192
+ throw new ConfigurationError(
193
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta.messages`
194
+ );
195
+ }
196
+ if (typeof rule.create !== "function") {
197
+ throw new ConfigurationError(
198
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing create function`
199
+ );
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Clear the plugin cache
205
+ */
206
+ clearCache() {
207
+ this.cache.clear();
208
+ }
209
+ };
210
+
211
+ // src/lint/lint-runner.ts
212
+ var LintRunner = class {
213
+ config;
214
+ pluginLoader;
215
+ resolvedConfig = null;
216
+ warn;
217
+ constructor(config) {
218
+ this.config = config;
219
+ this.pluginLoader = new PluginLoader();
220
+ this.warn = config.onWarn ?? console.warn;
221
+ }
222
+ /**
223
+ * Run all configured rules against the provided tokens
224
+ *
225
+ * Rules are executed in parallel for performance. Issues are collected
226
+ * and returned with counts by severity.
227
+ *
228
+ * @param tokens - Resolved tokens to lint
229
+ * @returns Lint result with issues and counts
230
+ */
231
+ async run(tokens) {
232
+ this.resolvedConfig ??= await this.resolveConfig();
233
+ const { rules, plugins } = this.resolvedConfig;
234
+ const issues = [];
235
+ if (Object.keys(rules).length === 0) {
236
+ return { issues: [], errorCount: 0, warningCount: 0 };
237
+ }
238
+ const rulePromises = Object.entries(rules).map(async ([ruleId, ruleConfig]) => {
239
+ const { severity, options } = ruleConfig;
240
+ const rule = this.resolveRule(ruleId, plugins);
241
+ if (!rule) {
242
+ this.warn(`[lint] Unknown rule '${ruleId}' - no plugin provides this rule`);
243
+ return [];
244
+ }
245
+ const reports = [];
246
+ const applicableTokens = this.filterTokensByAppliesTo(tokens, rule.meta.appliesTo);
247
+ const mergedOptions = rule.defaultOptions ? { ...rule.defaultOptions, ...options } : options;
248
+ const context = {
249
+ id: ruleId,
250
+ options: mergedOptions,
251
+ tokens: applicableTokens,
252
+ report: (descriptor) => {
253
+ reports.push(descriptor);
254
+ }
255
+ };
256
+ try {
257
+ await rule.create(context);
258
+ } catch (error) {
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ return [
261
+ {
262
+ ruleId: "lint/rule-error",
263
+ severity: "error",
264
+ message: `Rule '${ruleId}' failed: ${message}`,
265
+ tokenName: "(rule execution)",
266
+ tokenPath: []
267
+ }
268
+ ];
269
+ }
270
+ return reports.map((report) => {
271
+ const messageTemplate = rule.meta.messages[report.messageId];
272
+ const message = messageTemplate ? this.interpolateMessage(messageTemplate, report.data) : report.messageId;
273
+ return {
274
+ ruleId,
275
+ severity,
276
+ message,
277
+ tokenName: report.token.name,
278
+ tokenPath: report.token.path
279
+ };
280
+ });
281
+ });
282
+ const allIssues = await Promise.all(rulePromises);
283
+ issues.push(...allIssues.flat());
284
+ const errorCount = issues.filter((i) => i.severity === "error").length;
285
+ const warningCount = issues.filter((i) => i.severity === "warn").length;
286
+ return { issues, errorCount, warningCount };
287
+ }
288
+ /**
289
+ * Resolve configuration: load plugins, parse rule configs
290
+ */
291
+ async resolveConfig() {
292
+ const { plugins: pluginSources = {}, rules: ruleConfigs = {} } = this.config;
293
+ const plugins = await this.pluginLoader.loadAll(pluginSources);
294
+ const rules = {};
295
+ for (const [ruleId, config] of Object.entries(ruleConfigs)) {
296
+ const resolved = this.resolveRuleConfig(config);
297
+ if (resolved) {
298
+ rules[ruleId] = resolved;
299
+ }
300
+ }
301
+ return {
302
+ enabled: true,
303
+ failOnError: this.config.failOnError ?? true,
304
+ plugins,
305
+ rules
306
+ };
307
+ }
308
+ filterTokensByAppliesTo(tokens, appliesTo) {
309
+ if (!appliesTo || appliesTo === "all") {
310
+ return tokens;
311
+ }
312
+ const filtered = {};
313
+ for (const [name, token] of Object.entries(tokens)) {
314
+ if (token.$type && appliesTo.includes(token.$type)) {
315
+ filtered[name] = token;
316
+ }
317
+ }
318
+ return filtered;
319
+ }
320
+ /**
321
+ * Parse rule configuration into resolved format
322
+ */
323
+ resolveRuleConfig(config) {
324
+ if (typeof config === "string") {
325
+ if (config === "off") {
326
+ return null;
327
+ }
328
+ return { severity: config, options: {} };
329
+ }
330
+ const [severity, options = {}] = config;
331
+ if (severity === "off") {
332
+ return null;
333
+ }
334
+ return { severity, options };
335
+ }
336
+ /**
337
+ * Resolve a rule from plugins by rule ID
338
+ *
339
+ * Rule IDs are formatted as 'namespace/rule-name'
340
+ */
341
+ resolveRule(ruleId, plugins) {
342
+ const separatorIndex = ruleId.indexOf("/");
343
+ if (separatorIndex === -1) {
344
+ return null;
345
+ }
346
+ const namespace = ruleId.slice(0, separatorIndex);
347
+ const ruleName = ruleId.slice(separatorIndex + 1);
348
+ const plugin = plugins[namespace];
349
+ if (!plugin) {
350
+ return null;
351
+ }
352
+ return plugin.rules[ruleName] ?? null;
353
+ }
354
+ /**
355
+ * Interpolate message template with data
356
+ *
357
+ * Replaces {{key}} placeholders with values from data
358
+ */
359
+ interpolateMessage(template, data) {
360
+ if (!data) {
361
+ return template;
362
+ }
363
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
364
+ const value = data[key];
365
+ return value !== void 0 ? String(value) : `{{${key}}}`;
366
+ });
367
+ }
368
+ /**
369
+ * Clear the plugin cache
370
+ */
371
+ clearCache() {
372
+ this.pluginLoader.clearCache();
373
+ this.resolvedConfig = null;
374
+ }
375
+ };
376
+
377
+ // src/lint/utils/glob-matcher.ts
378
+ var MAX_CACHE_SIZE = 1e3;
379
+ var cache = /* @__PURE__ */ new Map();
380
+ function globToRegex(pattern) {
381
+ const cached = cache.get(pattern);
382
+ if (cached) {
383
+ cache.delete(pattern);
384
+ cache.set(pattern, cached);
385
+ return cached;
386
+ }
387
+ const regex = new RegExp(
388
+ "^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
389
+ );
390
+ if (cache.size >= MAX_CACHE_SIZE) {
391
+ const oldest = cache.keys().next().value;
392
+ if (oldest !== void 0) {
393
+ cache.delete(oldest);
394
+ }
395
+ }
396
+ cache.set(pattern, regex);
397
+ return regex;
398
+ }
399
+ function matchesGlob(name, patterns) {
400
+ return patterns.some((pattern) => globToRegex(pattern).test(name));
401
+ }
402
+
403
+ // src/lint/utils/reference-extractor.ts
404
+ var ALIAS_PATTERN = /\{([^}]+)\}/g;
405
+ function extractReferences(value) {
406
+ const refs = [];
407
+ if (typeof value === "string") {
408
+ let match;
409
+ ALIAS_PATTERN.lastIndex = 0;
410
+ while ((match = ALIAS_PATTERN.exec(value)) !== null) {
411
+ if (match[1]) {
412
+ refs.push(match[1]);
413
+ }
414
+ }
415
+ } else if (Array.isArray(value)) {
416
+ for (const item of value) {
417
+ refs.push(...extractReferences(item));
418
+ }
419
+ } else if (typeof value === "object" && value !== null) {
420
+ for (const v of Object.values(value)) {
421
+ refs.push(...extractReferences(v));
422
+ }
423
+ }
424
+ return refs;
425
+ }
426
+
427
+ // src/lint/rules/naming-convention.ts
428
+ var PATTERNS = {
429
+ "kebab-case": /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/,
430
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
431
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
432
+ snake_case: /^([a-z][a-z0-9]*)(_[a-z0-9]+)*$/,
433
+ "screaming-snake": /^([A-Z][A-Z0-9]*)(_[A-Z0-9]+)*$/
434
+ };
435
+ var DEFAULT_PATTERN = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
436
+ var namingConvention = createRule({
437
+ meta: {
438
+ name: "naming-convention",
439
+ description: "Enforce consistent token naming conventions",
440
+ messages: {
441
+ INVALID_FORMAT: "Token '{{name}}' does not match '{{format}}' format",
442
+ INVALID_SEGMENT: "Segment '{{segment}}' in token '{{name}}' does not match '{{format}}' format"
443
+ }
444
+ },
445
+ defaultOptions: { format: "kebab-case", allowNumericSegments: true },
446
+ create({ tokens, options, report }) {
447
+ const format = options.format ?? "kebab-case";
448
+ const ignore = options.ignore ?? [];
449
+ const customPattern = options.pattern;
450
+ const allowNumericSegments = options.allowNumericSegments ?? true;
451
+ let segmentPattern;
452
+ if (customPattern) {
453
+ segmentPattern = new RegExp(customPattern);
454
+ } else {
455
+ segmentPattern = PATTERNS[format] ?? DEFAULT_PATTERN;
456
+ }
457
+ const numericPattern = /^\d+$/;
458
+ for (const token of Object.values(tokens)) {
459
+ if (ignore.length > 0 && matchesGlob(token.name, ignore)) {
460
+ continue;
461
+ }
462
+ const segments = token.path;
463
+ let hasError = false;
464
+ for (const segment of segments) {
465
+ if (allowNumericSegments && numericPattern.test(segment)) {
466
+ continue;
467
+ }
468
+ if (!segmentPattern.test(segment)) {
469
+ report({
470
+ token,
471
+ messageId: "INVALID_SEGMENT",
472
+ data: { name: token.name, segment, format: customPattern ?? format }
473
+ });
474
+ hasError = true;
475
+ break;
476
+ }
477
+ }
478
+ if (!hasError && customPattern && !segmentPattern.test(token.name)) {
479
+ report({
480
+ token,
481
+ messageId: "INVALID_FORMAT",
482
+ data: { name: token.name, format: customPattern }
483
+ });
484
+ }
485
+ }
486
+ }
487
+ });
488
+
489
+ // src/lint/rules/no-deprecated-usage.ts
490
+ var noDeprecatedUsage = createRule({
491
+ meta: {
492
+ name: "no-deprecated-usage",
493
+ description: "Disallow references to deprecated tokens",
494
+ messages: {
495
+ REFERENCES_DEPRECATED: "Token '{{name}}' references deprecated token '{{ref}}'. {{reason}}"
496
+ }
497
+ },
498
+ defaultOptions: {},
499
+ create({ tokens, options, report }) {
500
+ const ignore = options.ignore ?? [];
501
+ const deprecatedTokens = /* @__PURE__ */ new Map();
502
+ for (const token of Object.values(tokens)) {
503
+ if (token.$deprecated) {
504
+ const reason = typeof token.$deprecated === "string" ? token.$deprecated : "";
505
+ deprecatedTokens.set(token.name, reason || true);
506
+ }
507
+ }
508
+ if (deprecatedTokens.size === 0) {
509
+ return;
510
+ }
511
+ for (const token of Object.values(tokens)) {
512
+ if (ignore.length > 0 && matchesGlob(token.name, ignore)) {
513
+ continue;
514
+ }
515
+ if (deprecatedTokens.has(token.name)) {
516
+ continue;
517
+ }
518
+ const refs = extractReferences(token.originalValue);
519
+ for (const ref of refs) {
520
+ const deprecation = deprecatedTokens.get(ref);
521
+ if (deprecation) {
522
+ const reason = deprecation === true ? "" : `(${deprecation})`;
523
+ report({
524
+ token,
525
+ messageId: "REFERENCES_DEPRECATED",
526
+ data: { name: token.name, ref, reason }
527
+ });
528
+ }
529
+ }
530
+ }
531
+ }
532
+ });
533
+
534
+ // src/lint/rules/no-duplicate-values.ts
535
+ function valueKey(value) {
536
+ if (value === null) {
537
+ return "null";
538
+ }
539
+ if (value === void 0) {
540
+ return "undefined";
541
+ }
542
+ if (typeof value === "object") {
543
+ return JSON.stringify(sortKeys(value));
544
+ }
545
+ return String(value);
546
+ }
547
+ function sortKeys(obj) {
548
+ if (Array.isArray(obj)) {
549
+ return obj.map(sortKeys);
550
+ }
551
+ if (typeof obj === "object" && obj !== null) {
552
+ const sorted = {};
553
+ for (const key of Object.keys(obj).sort()) {
554
+ sorted[key] = sortKeys(obj[key]);
555
+ }
556
+ return sorted;
557
+ }
558
+ return obj;
559
+ }
560
+ var noDuplicateValues = createRule({
561
+ meta: {
562
+ name: "no-duplicate-values",
563
+ description: "Detect tokens with duplicate values (excluding aliases)",
564
+ messages: {
565
+ DUPLICATE_VALUE: "Token '{{name}}' has the same value as '{{duplicate}}'. Consider using an alias instead."
566
+ }
567
+ },
568
+ defaultOptions: {},
569
+ create({ tokens, options, report }) {
570
+ const ignore = options.ignore ?? [];
571
+ const types = options.types;
572
+ const valueMap = /* @__PURE__ */ new Map();
573
+ for (const token of Object.values(tokens)) {
574
+ if (ignore.length > 0 && matchesGlob(token.name, ignore)) {
575
+ continue;
576
+ }
577
+ if (token._isAlias) {
578
+ continue;
579
+ }
580
+ if (types && types.length > 0 && !types.includes(token.$type ?? "")) {
581
+ continue;
582
+ }
583
+ const key = valueKey(token.$value);
584
+ const existing = valueMap.get(key);
585
+ if (existing) {
586
+ existing.push(token);
587
+ } else {
588
+ valueMap.set(key, [token]);
589
+ }
590
+ }
591
+ for (const tokenList of valueMap.values()) {
592
+ if (tokenList.length > 1) {
593
+ const first = tokenList[0];
594
+ if (!first) {
595
+ continue;
596
+ }
597
+ for (let i = 1; i < tokenList.length; i++) {
598
+ const current = tokenList[i];
599
+ if (current) {
600
+ report({
601
+ token: current,
602
+ messageId: "DUPLICATE_VALUE",
603
+ data: { name: current.name, duplicate: first.name }
604
+ });
605
+ }
606
+ }
607
+ }
608
+ }
609
+ }
610
+ });
611
+
612
+ // src/lint/rules/path-schema/matcher.ts
613
+ var PathSchemaMatcher = class {
614
+ segments;
615
+ pathPatterns;
616
+ transitionRules;
617
+ constructor(config) {
618
+ this.segments = config.segments ?? {};
619
+ this.pathPatterns = this.compilePaths(config.paths ?? [], this.segments);
620
+ this.transitionRules = this.compileTransitions(config.transitions ?? []);
621
+ }
622
+ /**
623
+ * Validate a token against the schema
624
+ */
625
+ validate(token) {
626
+ const violations = [];
627
+ const pathSegments = token.path;
628
+ const hasPaths = this.pathPatterns.length > 0;
629
+ const hasTransitions = this.transitionRules.length > 0;
630
+ if (hasTransitions) {
631
+ const transitionViolations = this.validateTransitions(pathSegments, token.name);
632
+ violations.push(...transitionViolations);
633
+ }
634
+ if (hasPaths) {
635
+ const matchesAny = this.pathPatterns.some((p) => this.matchPattern(p, pathSegments));
636
+ if (!matchesAny) {
637
+ violations.push({
638
+ type: "INVALID_PATH",
639
+ data: { path: token.name }
640
+ });
641
+ }
642
+ }
643
+ return violations;
644
+ }
645
+ /**
646
+ * Validate transitions between segments.
647
+ *
648
+ * Deny rules are checked independently (any match = violation).
649
+ * Allow rules use OR semantics: at least one must match.
650
+ */
651
+ validateTransitions(segments, tokenName) {
652
+ const violations = [];
653
+ for (let i = 0; i < segments.length - 1; i++) {
654
+ const from = segments[i];
655
+ const to = segments[i + 1];
656
+ if (!from || !to) {
657
+ continue;
658
+ }
659
+ const applicableRules = this.transitionRules.filter((r) => this.matchesPattern(from, r.from));
660
+ if (applicableRules.length === 0) {
661
+ continue;
662
+ }
663
+ const denyRules = applicableRules.filter((r) => r.allow === false);
664
+ const allowRules = applicableRules.filter((r) => r.allow !== false);
665
+ for (const rule of denyRules) {
666
+ if (this.matchesPattern(to, rule.to)) {
667
+ violations.push({
668
+ type: "FORBIDDEN_TRANSITION",
669
+ data: { from, to, path: tokenName }
670
+ });
671
+ }
672
+ }
673
+ if (allowRules.length > 0) {
674
+ const anyAllowMatches = allowRules.some((r) => this.matchesPattern(to, r.to));
675
+ if (!anyAllowMatches) {
676
+ violations.push({
677
+ type: "FORBIDDEN_TRANSITION",
678
+ data: { from, to, path: tokenName }
679
+ });
680
+ }
681
+ }
682
+ }
683
+ return violations;
684
+ }
685
+ /**
686
+ * Check if a value matches a pattern
687
+ */
688
+ matchesPattern(value, pattern) {
689
+ if (typeof pattern === "string") {
690
+ return value === pattern;
691
+ }
692
+ if (Array.isArray(pattern)) {
693
+ return pattern.includes(value);
694
+ }
695
+ return pattern.test(value);
696
+ }
697
+ /**
698
+ * Compile path patterns into matcher structures
699
+ */
700
+ compilePaths(patterns, segments) {
701
+ return patterns.map((p) => this.parsePattern(p, segments));
702
+ }
703
+ /**
704
+ * Parse a path pattern string into compiled form
705
+ * - `{name}` is a segment placeholder
706
+ * - `*` is a wildcard that matches any single segment
707
+ * - `.` is the path separator (implicit between segments)
708
+ */
709
+ parsePattern(pattern, _segments) {
710
+ const parts = [];
711
+ const regex = /\{(\w+)\}|(\*)|([^{}*]+)/g;
712
+ let match;
713
+ while ((match = regex.exec(pattern)) !== null) {
714
+ if (match[1]) {
715
+ parts.push({ type: "segment", name: match[1] });
716
+ } else if (match[2]) {
717
+ parts.push({ type: "wildcard" });
718
+ } else if (match[3]) {
719
+ parts.push({ type: "literal", value: match[3] });
720
+ }
721
+ }
722
+ return parts;
723
+ }
724
+ /**
725
+ * Match path segments against a compiled pattern using dynamic programming.
726
+ * Supports optional segments via DP table.
727
+ *
728
+ * DP[i][j] = can we match path[0..i) with pattern[0..j)?
729
+ */
730
+ matchPattern(pattern, pathSegments) {
731
+ const patternParts = pattern.filter((p) => p.type === "segment" || p.type === "wildcard");
732
+ const pathLen = pathSegments.length;
733
+ const patternLen = patternParts.length;
734
+ const dp = [];
735
+ for (let i = 0; i <= pathLen; i++) {
736
+ dp[i] = [];
737
+ for (let j = 0; j <= patternLen; j++) {
738
+ dp[i][j] = false;
739
+ }
740
+ }
741
+ dp[0][0] = true;
742
+ for (let i = 0; i <= pathLen; i++) {
743
+ for (let j = 0; j <= patternLen; j++) {
744
+ const currentState = dp[i][j];
745
+ if (!currentState) {
746
+ continue;
747
+ }
748
+ if (i === pathLen) {
749
+ if (j < patternLen && this.isPartOptional(patternParts[j])) {
750
+ dp[i][j + 1] = true;
751
+ }
752
+ continue;
753
+ }
754
+ if (j === patternLen) {
755
+ if (i === pathLen) {
756
+ dp[i][j] = true;
757
+ }
758
+ continue;
759
+ }
760
+ const part = patternParts[j];
761
+ if (i < pathLen && this.matchPatternPart(part, pathSegments[i])) {
762
+ dp[i + 1][j + 1] = true;
763
+ }
764
+ if (this.isPartOptional(part)) {
765
+ dp[i][j + 1] = true;
766
+ }
767
+ }
768
+ }
769
+ return dp[pathLen][patternLen] ?? false;
770
+ }
771
+ /**
772
+ * Check if a pattern part is optional based on its segment definition
773
+ */
774
+ isPartOptional(part) {
775
+ if (part.type !== "segment" || !part.name) {
776
+ return false;
777
+ }
778
+ const segmentDef = this.segments[part.name];
779
+ return segmentDef?.optional ?? false;
780
+ }
781
+ /**
782
+ * Match a single pattern part against a path segment value
783
+ */
784
+ matchPatternPart(part, value) {
785
+ if (part.type === "wildcard") {
786
+ return true;
787
+ }
788
+ if (part.type === "segment" && part.name) {
789
+ const segment = this.segments[part.name];
790
+ if (!segment) {
791
+ return true;
792
+ }
793
+ return this.matchesSegmentDefinition(value, segment);
794
+ }
795
+ return false;
796
+ }
797
+ /**
798
+ * Check if a value matches a segment definition
799
+ */
800
+ matchesSegmentDefinition(value, definition) {
801
+ const { values } = definition;
802
+ if (Array.isArray(values)) {
803
+ return values.some((v) => typeof v === "string" ? v === value : v.test(value));
804
+ }
805
+ return values.test(value);
806
+ }
807
+ /**
808
+ * Compile transition rules
809
+ */
810
+ compileTransitions(transitions) {
811
+ return transitions.map((t) => ({
812
+ from: t.from,
813
+ to: t.to,
814
+ allow: t.allow ?? true
815
+ }));
816
+ }
817
+ };
818
+
819
+ // src/lint/rules/path-schema/index.ts
820
+ var pathSchema = createRule({
821
+ meta: {
822
+ name: "path-schema",
823
+ description: "Enforce token path segment structure",
824
+ messages: {
825
+ INVALID_PATH: "Token path '{{path}}' does not match any defined pattern",
826
+ UNKNOWN_SEGMENT: "Segment '{{segment}}' at position {{position}} in '{{path}}' is not valid",
827
+ FORBIDDEN_TRANSITION: "Segment '{{to}}' cannot follow '{{from}}' in path '{{path}}'"
828
+ }
829
+ },
830
+ defaultOptions: {
831
+ segments: {},
832
+ paths: [],
833
+ transitions: []
834
+ },
835
+ create({ tokens, options, report }) {
836
+ const ignore = options.ignore ?? [];
837
+ if ((!options.paths || options.paths.length === 0) && (!options.transitions || options.transitions.length === 0)) {
838
+ return;
839
+ }
840
+ const matcher = new PathSchemaMatcher(options);
841
+ for (const token of Object.values(tokens)) {
842
+ if (ignore.length > 0 && matchesGlob(token.name, ignore)) {
843
+ continue;
844
+ }
845
+ const violations = matcher.validate(token);
846
+ for (const violation of violations) {
847
+ report({
848
+ token,
849
+ messageId: violation.type,
850
+ data: violation.data
851
+ });
852
+ }
853
+ }
854
+ }
855
+ });
856
+
857
+ // src/lint/rules/require-description.ts
858
+ var requireDescription = createRule({
859
+ meta: {
860
+ name: "require-description",
861
+ description: "Require tokens to have descriptions",
862
+ messages: {
863
+ MISSING_DESCRIPTION: "Token '{{name}}' is missing a description",
864
+ TOO_SHORT: "Token '{{name}}' description is too short ({{length}} chars, min {{minLength}})"
865
+ }
866
+ },
867
+ defaultOptions: { minLength: 1 },
868
+ create({ tokens, options, report }) {
869
+ const minLength = options.minLength ?? 1;
870
+ const ignore = options.ignore ?? [];
871
+ for (const token of Object.values(tokens)) {
872
+ if (ignore.length > 0 && matchesGlob(token.name, ignore)) {
873
+ continue;
874
+ }
875
+ if (!token.$description) {
876
+ report({
877
+ token,
878
+ messageId: "MISSING_DESCRIPTION",
879
+ data: { name: token.name }
880
+ });
881
+ } else if (token.$description.length < minLength) {
882
+ report({
883
+ token,
884
+ messageId: "TOO_SHORT",
885
+ data: {
886
+ name: token.name,
887
+ length: token.$description.length,
888
+ minLength
889
+ }
890
+ });
891
+ }
892
+ }
893
+ }
894
+ });
895
+
896
+ // src/lint/rules/index.ts
897
+ function buildDispersaPlugin() {
898
+ const rules = {
899
+ "require-description": requireDescription,
900
+ "naming-convention": namingConvention,
901
+ "no-deprecated-usage": noDeprecatedUsage,
902
+ "no-duplicate-values": noDuplicateValues,
903
+ "path-schema": pathSchema
904
+ };
905
+ const plugin = { meta: { name: "dispersa" }, rules, configs: {} };
906
+ const recommended = {
907
+ plugins: { dispersa: plugin },
908
+ rules: {
909
+ "dispersa/require-description": "warn",
910
+ "dispersa/naming-convention": ["error", { format: "kebab-case" }],
911
+ "dispersa/no-deprecated-usage": "warn"
912
+ }
913
+ };
914
+ const strict = {
915
+ plugins: { dispersa: plugin },
916
+ rules: {
917
+ "dispersa/require-description": "error",
918
+ "dispersa/naming-convention": ["error", { format: "kebab-case" }],
919
+ "dispersa/no-deprecated-usage": "error",
920
+ "dispersa/no-duplicate-values": "error"
921
+ }
922
+ };
923
+ const minimal = {
924
+ plugins: { dispersa: plugin },
925
+ rules: {
926
+ "dispersa/no-deprecated-usage": "warn"
927
+ }
928
+ };
929
+ plugin.configs = { recommended, strict, minimal };
930
+ return { plugin, recommended, strict, minimal };
931
+ }
932
+ var {
933
+ plugin: dispersaPlugin,
934
+ recommended: recommendedConfig,
935
+ strict: strictConfig,
936
+ minimal: minimalConfig
937
+ } = buildDispersaPlugin();
938
+
939
+ // src/cli/formatters/lint-formatter.ts
940
+ var formatLintJson = (result) => {
941
+ return JSON.stringify(result, null, 2);
942
+ };
943
+ var formatLintStylish = (result) => {
944
+ const lines = [];
945
+ if (result.issues.length === 0) {
946
+ return "\u2713 No lint issues found";
947
+ }
948
+ const byToken = /* @__PURE__ */ new Map();
949
+ for (const issue of result.issues) {
950
+ const existing = byToken.get(issue.tokenName) ?? [];
951
+ existing.push(issue);
952
+ byToken.set(issue.tokenName, existing);
953
+ }
954
+ for (const [tokenName, issues] of byToken) {
955
+ lines.push(``);
956
+ lines.push(` ${tokenName}`);
957
+ for (const issue of issues) {
958
+ const severity = issue.severity === "error" ? "\u2716" : "\u26A0";
959
+ const label = issue.severity === "error" ? "error" : "warning";
960
+ lines.push(` ${severity} ${label}: ${issue.message} [${issue.ruleId}]`);
961
+ }
962
+ }
963
+ lines.push(``);
964
+ if (result.errorCount > 0 || result.warningCount > 0) {
965
+ const parts = [];
966
+ if (result.errorCount > 0) {
967
+ parts.push(`${result.errorCount} error${result.errorCount === 1 ? "" : "s"}`);
968
+ }
969
+ if (result.warningCount > 0) {
970
+ parts.push(`${result.warningCount} warning${result.warningCount === 1 ? "" : "s"}`);
971
+ }
972
+ lines.push(`\u2716 ${parts.join(", ")}`);
973
+ }
974
+ return lines.join("\n");
975
+ };
976
+ var formatLintCompact = (result) => {
977
+ const lines = [];
978
+ for (const issue of result.issues) {
979
+ const severity = issue.severity.toUpperCase();
980
+ lines.push(`${severity}: ${issue.ruleId} - ${issue.message} (token: ${issue.tokenName})`);
981
+ }
982
+ if (result.errorCount > 0 || result.warningCount > 0) {
983
+ lines.push(`SUMMARY: ${result.errorCount} errors, ${result.warningCount} warnings`);
984
+ }
985
+ return lines.join("\n");
986
+ };
987
+ /**
988
+ * @license MIT
989
+ * Copyright (c) 2025-present Dispersa
990
+ *
991
+ * This source code is licensed under the MIT license found in the
992
+ * LICENSE file in the root directory of this source tree.
993
+ */
994
+
995
+ export { LintRunner, PluginLoader, createRule, dispersaPlugin, formatLintCompact, formatLintJson, formatLintStylish, minimalConfig, namingConvention, noDeprecatedUsage, noDuplicateValues, pathSchema, recommendedConfig, requireDescription, strictConfig };
996
+ //# sourceMappingURL=lint.js.map
997
+ //# sourceMappingURL=lint.js.map