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